diff --git a/.vscode/settings.json b/.vscode/settings.json index 8c4ccf2184..8b7019c23a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,13 +4,6 @@ }, "bloc.newBlocTemplate": "equatable", "bloc.newCubitTemplate": "equatable", - "dart.additionalAnalyzerFileExtensions": [ - "drift" - ], - "cSpell.words": [ - "Hkdf", - "hmac", - "sublist", - "Widgetbook" - ] + "dart.additionalAnalyzerFileExtensions": ["drift"], + "cSpell.words": ["Hkdf", "hmac", "sublist", "Widgetbook"] } diff --git a/lib/blocs/create_manifest/create_manifest_cubit.dart b/lib/blocs/create_manifest/create_manifest_cubit.dart index bdd0802eda..f0a4dd25db 100644 --- a/lib/blocs/create_manifest/create_manifest_cubit.dart +++ b/lib/blocs/create_manifest/create_manifest_cubit.dart @@ -125,15 +125,19 @@ class CreateManifestCubit extends Cubit { Future loadFolder(String folderId) async { await _selectedFolderSubscription?.cancel(); - _selectedFolderSubscription = - _driveDao.watchFolderContents(drive.id, folderId: folderId).listen( - (f) => emit( - CreateManifestFolderLoadSuccess( - viewingRootFolder: f.folder.parentFolderId == null, - viewingFolder: f, - ), - ), - ); + _selectedFolderSubscription = _driveDao + .watchFolderContents( + drive.id, + folderId: folderId, + ) + .listen( + (f) => emit( + CreateManifestFolderLoadSuccess( + viewingRootFolder: f.folder.parentFolderId == null, + viewingFolder: f, + ), + ), + ); } /// User selected a new name due to name conflict, confirm that form is valid and check for conflicts again diff --git a/lib/blocs/drive_detail/drive_detail_cubit.dart b/lib/blocs/drive_detail/drive_detail_cubit.dart index 313c388a4c..a87f247566 100644 --- a/lib/blocs/drive_detail/drive_detail_cubit.dart +++ b/lib/blocs/drive_detail/drive_detail_cubit.dart @@ -40,6 +40,8 @@ class DriveDetailCubit extends Cubit { bool _refreshSelectedItem = false; + bool _showHiddenFiles = false; + DriveDetailCubit({ required this.driveId, String? initialFolderId, @@ -74,6 +76,12 @@ class DriveDetailCubit extends Cubit { } } + void toggleHiddenFiles() { + _showHiddenFiles = !_showHiddenFiles; + + refreshDriveDataTable(); + } + void openFolder({ required String path, DriveOrder contentOrderBy = DriveOrder.name, @@ -148,8 +156,10 @@ class DriveDetailCubit extends Cubit { ); if (index >= 0) { + final item = folderContents.files[index]; + _selectedItem = DriveDataTableItemMapper.toFileDataTableItem( - folderContents.files[index], + item, _selectedItem!.index, _selectedItem!.isOwner, ); @@ -159,8 +169,10 @@ class DriveDetailCubit extends Cubit { (element) => element.id == _selectedItem!.id, ); if (index >= 0) { + final item = folderContents.subfolders[index]; + _selectedItem = DriveDataTableItemMapper.fromFolderEntry( - folderContents.subfolders[index], + item, _selectedItem!.index, _selectedItem!.isOwner, ); @@ -173,8 +185,6 @@ class DriveDetailCubit extends Cubit { _selectedItem!.isOwner, ); } - - _refreshSelectedItem = false; } final currentFolderContents = parseEntitiesToDatatableItem( @@ -195,6 +205,7 @@ class DriveDetailCubit extends Cubit { rowsPerPage: availableRowsPerPage.first, availableRowsPerPage: availableRowsPerPage, currentFolderContents: currentFolderContents, + isShowingHiddenFiles: _showHiddenFiles, ), ); } else { @@ -212,6 +223,7 @@ class DriveDetailCubit extends Cubit { driveIsEmpty: rootFolderNode.isEmpty(), multiselect: false, currentFolderContents: currentFolderContents, + isShowingHiddenFiles: _showHiddenFiles, ), ); } @@ -400,8 +412,11 @@ class DriveDetailCubit extends Cubit { if (state is DriveDetailLoadSuccess) { await Future.delayed(const Duration(milliseconds: 100)); - emit((state as DriveDetailLoadSuccess) - .copyWith(forceRebuildKey: UniqueKey())); + final state = this.state as DriveDetailLoadSuccess; + emit(state.copyWith( + forceRebuildKey: UniqueKey(), + isShowingHiddenFiles: _showHiddenFiles, + )); } } @@ -444,14 +459,20 @@ class DriveDetailCubit extends Cubit { } final state = this.state as DriveDetailLoadSuccess; - final allImagesForFolder = state.currentFolderContents - .whereType() - .where( - (element) => supportedImageTypesInFilePreview.contains( - element.contentType, - ), - ) - .toList(); + + final isShowingHiddenFiles = state.isShowingHiddenFiles; + + final List allImagesForFolder = + state.currentFolderContents.whereType().where( + (element) { + final supportedImageType = supportedImageTypesInFilePreview.contains( + element.contentType, + ); + + return supportedImageType && + (isShowingHiddenFiles ? true : !element.isHidden); + }, + ).toList(); _allImagesOfCurrentFolder = allImagesForFolder; diff --git a/lib/blocs/drive_detail/drive_detail_state.dart b/lib/blocs/drive_detail/drive_detail_state.dart index f06dee299c..3d4b81ed37 100644 --- a/lib/blocs/drive_detail/drive_detail_state.dart +++ b/lib/blocs/drive_detail/drive_detail_state.dart @@ -37,6 +37,8 @@ class DriveDetailLoadSuccess extends DriveDetailState { final Key? forceRebuildKey; + final bool isShowingHiddenFiles; + DriveDetailLoadSuccess({ required this.currentDrive, required this.hasWritePermissions, @@ -51,9 +53,10 @@ class DriveDetailLoadSuccess extends DriveDetailState { this.hasFoldersSelected = false, this.selectedFilePreviewUrl, required this.driveIsEmpty, - this.selectedItem, + required this.selectedItem, required this.currentFolderContents, this.forceRebuildKey, + required this.isShowingHiddenFiles, }); DriveDetailLoadSuccess copyWith({ @@ -73,6 +76,7 @@ class DriveDetailLoadSuccess extends DriveDetailState { ArDriveDataTableItem? selectedItem, List? currentFolderContents, Key? forceRebuildKey, + bool? isShowingHiddenFiles, }) => DriveDetailLoadSuccess( forceRebuildKey: forceRebuildKey ?? this.forceRebuildKey, @@ -94,12 +98,16 @@ class DriveDetailLoadSuccess extends DriveDetailState { driveIsEmpty: driveIsEmpty ?? this.driveIsEmpty, currentFolderContents: currentFolderContents ?? this.currentFolderContents, + isShowingHiddenFiles: isShowingHiddenFiles ?? this.isShowingHiddenFiles, ); @override List get props => [ currentDrive, hasWritePermissions, + folderInView, + currentFolderContents, + isShowingHiddenFiles, contentOrderBy, contentOrderingMode, showSelectedItemDetails, @@ -111,6 +119,7 @@ class DriveDetailLoadSuccess extends DriveDetailState { driveIsEmpty, multiselect, forceRebuildKey, + selectedItem, ]; SelectedItem? maybeSelectedItem() => selectedItems.isNotEmpty ? selectedItems.first : null; diff --git a/lib/blocs/fs_entry_move/fs_entry_move_bloc.dart b/lib/blocs/fs_entry_move/fs_entry_move_bloc.dart index cb7adc7eb6..340055cacf 100644 --- a/lib/blocs/fs_entry_move/fs_entry_move_bloc.dart +++ b/lib/blocs/fs_entry_move/fs_entry_move_bloc.dart @@ -28,6 +28,7 @@ class FsEntryMoveBloc extends Bloc { final ProfileCubit _profileCubit; final SyncCubit _syncCubit; final ArDriveCrypto _crypto; + final DriveDetailCubit _driveDetailCubit; FsEntryMoveBloc({ required this.driveId, @@ -38,11 +39,13 @@ class FsEntryMoveBloc extends Bloc { required ProfileCubit profileCubit, required SyncCubit syncCubit, required ArDriveCrypto crypto, + required DriveDetailCubit driveDetailCubit, Platform platform = const LocalPlatform(), }) : _arweave = arweave, _turboUploadService = turboUploadService, _driveDao = driveDao, _profileCubit = profileCubit, + _driveDetailCubit = driveDetailCubit, _syncCubit = syncCubit, _crypto = crypto, super(const FsEntryMoveLoadInProgress()) { @@ -133,8 +136,10 @@ class FsEntryMoveBloc extends Bloc { required String folderId, required Emitter emit, }) async { - final folderStream = - _driveDao.watchFolderContents(driveId, folderId: folderId); + final folderStream = _driveDao.watchFolderContents( + driveId, + folderId: folderId, + ); await emit.forEach( folderStream, onData: (FolderWithContents folderWithContents) => FsEntryMoveLoadSuccess( @@ -176,21 +181,38 @@ class FsEntryMoveBloc extends Bloc { }) async { final driveKey = await _driveDao.getDriveKey(driveId, profile.cipherKey); final moveTxDataItems = []; + final isShowingHiddenItems = + (_driveDetailCubit.state as DriveDetailLoadSuccess) + .isShowingHiddenFiles; + final files = selectedItems.whereType().toList(); + + if (!isShowingHiddenItems) { + files.removeWhere((element) => element.isHidden); + } - final filesToMove = selectedItems - .whereType() + final filesToMove = files .where((file) => conflictingItems .where((conflictingFile) => conflictingFile.id == file.id) .isEmpty) .toList(); - final foldersToMove = selectedItems + files.clear(); + + final folders = selectedItems.whereType().toList(); + + if (!isShowingHiddenItems) { + folders.removeWhere((element) => element.isHidden); + } + + final foldersToMove = folders .whereType() .where((folder) => conflictingItems .where((conflictingFolder) => conflictingFolder.id == folder.id) .isEmpty) .toList(); + folders.clear(); + final folderMap = {}; await _driveDao.transaction(() async { 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 8a0d2d7331..06b2741216 100644 --- a/lib/blocs/fs_entry_rename/fs_entry_rename_cubit.dart +++ b/lib/blocs/fs_entry_rename/fs_entry_rename_cubit.dart @@ -60,7 +60,7 @@ class FsEntryRenameCubit extends Cubit { bool updateExtension = false, }) async { try { - late bool hasEntityWithSameName; + final bool hasEntityWithSameName; if (_isRenamingFolder) { hasEntityWithSameName = await _folderWithSameNameExists(newName); diff --git a/lib/blocs/ghost_fixer/ghost_fixer_cubit.dart b/lib/blocs/ghost_fixer/ghost_fixer_cubit.dart index ac193e3eba..e45c7f3bdb 100644 --- a/lib/blocs/ghost_fixer/ghost_fixer_cubit.dart +++ b/lib/blocs/ghost_fixer/ghost_fixer_cubit.dart @@ -52,7 +52,10 @@ class GhostFixerCubit extends Cubit { await _selectedFolderSubscription?.cancel(); _selectedFolderSubscription = _driveDao - .watchFolderContents(ghostFolder.driveId, folderId: folderId) + .watchFolderContents( + ghostFolder.driveId, + folderId: folderId, + ) .listen( (f) => emit( GhostFixerFolderLoadSuccess( @@ -129,6 +132,7 @@ class GhostFixerCubit extends Cubit { isGhost: false, lastUpdated: ghostFolder.lastUpdated, dateCreated: ghostFolder.dateCreated, + isHidden: ghostFolder.isHidden, ); final folderEntity = folder.asEntity(); diff --git a/lib/blocs/hide/hide_bloc.dart b/lib/blocs/hide/hide_bloc.dart new file mode 100644 index 0000000000..da8f5f273b --- /dev/null +++ b/lib/blocs/hide/hide_bloc.dart @@ -0,0 +1,301 @@ +import 'package:ardrive/authentication/ardrive_auth.dart'; +import 'package:ardrive/blocs/hide/hide_event.dart'; +import 'package:ardrive/blocs/hide/hide_state.dart'; +import 'package:ardrive/blocs/profile/profile_cubit.dart'; +import 'package:ardrive/blocs/upload/upload_cubit.dart'; +import 'package:ardrive/core/crypto/crypto.dart'; +import 'package:ardrive/core/upload/uploader.dart'; +import 'package:ardrive/entities/file_entity.dart'; +import 'package:ardrive/entities/folder_entity.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:arweave/arweave.dart'; +import 'package:cryptography/cryptography.dart'; +import 'package:drift/drift.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class HideBloc extends Bloc { + final ArweaveService _arweave; + final ArDriveCrypto _crypto; + final TurboUploadService _turboUploadService; + final DriveDao _driveDao; + final ProfileCubit _profileCubit; + final ArDriveUploadPreparationManager _uploadPreparationManager; + + HideBloc({ + required ArweaveService arweaveService, + required ArDriveCrypto crypto, + required TurboUploadService turboUploadService, + required DriveDao driveDao, + required ProfileCubit profileCubit, + required ArDriveAuth auth, + required ArDriveUploadPreparationManager uploadPreparationManager, + }) : _arweave = arweaveService, + _crypto = crypto, + _turboUploadService = turboUploadService, + _driveDao = driveDao, + _profileCubit = profileCubit, + _uploadPreparationManager = uploadPreparationManager, + super(const InitialHideState()) { + on(_onHideFileEvent); + on(_onHideFolderEvent); + on(_onUnhideFileEvent); + on(_onUnhideFolderEvent); + on(_onConfirmUploadEvent); + on(_onErrorEvent); + } + + bool _useTurboUpload = false; + + Future _onHideFileEvent( + HideFileEvent event, + Emitter emit, + ) async { + emit(const PreparingAndSigningHideState(hideAction: HideAction.hideFile)); + + final FileEntry currentFile = await _driveDao + .fileById( + driveId: event.driveId, + fileId: event.fileId, + ) + .getSingle(); + + await _setHideStatus( + currentFile, + emit, + isHidden: true, + ); + } + + Future _onHideFolderEvent( + HideFolderEvent event, + Emitter emit, + ) async { + emit(const PreparingAndSigningHideState(hideAction: HideAction.hideFolder)); + + logger.d('Hiding folder ${event.folderId} in drive ${event.driveId}'); + + final FolderEntry currentFolder = await _driveDao + .folderById( + driveId: event.driveId, + folderId: event.folderId, + ) + .getSingle(); + + await _setHideStatus( + currentFolder, + emit, + isHidden: true, + ); + } + + Future _onUnhideFileEvent( + UnhideFileEvent event, + Emitter emit, + ) async { + emit(const PreparingAndSigningHideState(hideAction: HideAction.unhideFile)); + + final FileEntry currentFile = await _driveDao + .fileById( + driveId: event.driveId, + fileId: event.fileId, + ) + .getSingle(); + + await _setHideStatus( + currentFile, + emit, + isHidden: false, + ); + } + + Future _onUnhideFolderEvent( + UnhideFolderEvent event, + Emitter emit, + ) async { + emit(const PreparingAndSigningHideState( + hideAction: HideAction.unhideFolder, + )); + + logger.d('Unhiding folder ${event.folderId} in drive ${event.driveId}'); + + final FolderEntry currentFolder = await _driveDao + .folderById( + driveId: event.driveId, + folderId: event.folderId, + ) + .getSingle(); + + await _setHideStatus( + currentFolder, + emit, + isHidden: false, + ); + } + + Future _setHideStatus( + Insertable currentEntry, + Emitter emit, { + required bool isHidden, + }) async { + final entryIsFile = currentEntry is FileEntry; + final entryIsFolder = currentEntry is FolderEntry; + + assert( + entryIsFile || entryIsFolder, + 'Entity to hide must be either a File or a Folder', + ); + + final entity = entryIsFile + ? currentEntry.asEntity() + : (currentEntry as FolderEntry).asEntity(); + + final driveId = entryIsFile + ? currentEntry.driveId + : (currentEntry as FolderEntry).driveId; + + final profile = _profileCubit.state as ProfileLoggedIn; + final driveKey = await _driveDao.getDriveKey(driveId, profile.cipherKey); + final SecretKey? entityKey; + + if (driveKey != null) { + if (entryIsFile) { + entityKey = await _crypto.deriveFileKey( + driveKey, + (entity as FileEntity).id!, + ); + } else { + entityKey = driveKey; + } + } else { + entityKey = null; + } + + final newEntry = entryIsFile + ? currentEntry.copyWith( + isHidden: isHidden, + lastUpdated: DateTime.now(), + ) + : (currentEntry as FolderEntry).copyWith( + isHidden: isHidden, + lastUpdated: DateTime.now(), + ); + final newEntryEntity = entryIsFile + ? (newEntry as FileEntry).asEntity() + : (newEntry as FolderEntry).asEntity(); + + final dataItem = await _arweave.prepareEntityDataItem( + newEntryEntity, + profile.wallet, + key: entityKey, + ); + + final dataItems = [dataItem]; + + final paymentInfo = await _uploadPreparationManager + .getUploadPaymentInfoForEntityUpload(dataItem: dataItem); + + _useTurboUpload = paymentInfo.isFreeUploadPossibleUsingTurbo; + + Future saveEntitiesToDb() async { + await _driveDao.transaction(() async { + if (entryIsFile) { + await _driveDao.writeToFile(newEntry as FileEntry); + } else { + await _driveDao.writeToFolder(newEntry as FolderEntry); + } + + newEntryEntity.txId = dataItem.id; + + if (entryIsFile) { + await _driveDao.insertFileRevision( + (newEntryEntity as FileEntity).toRevisionCompanion( + performedAction: + isHidden ? RevisionAction.hide : RevisionAction.unhide, + )); + } else { + await _driveDao.insertFolderRevision( + (newEntryEntity as FolderEntity).toRevisionCompanion( + performedAction: + isHidden ? RevisionAction.hide : RevisionAction.unhide, + )); + } + }); + } + + final hideAction = entryIsFile + ? (isHidden ? HideAction.hideFile : HideAction.unhideFile) + : (isHidden ? HideAction.hideFolder : HideAction.unhideFolder); + + emit( + ConfirmingHideState( + uploadMethod: UploadMethod.turbo, + hideAction: hideAction, + dataItems: dataItems, + saveEntitiesToDb: saveEntitiesToDb, + ), + ); + } + + Future _onConfirmUploadEvent( + ConfirmUploadEvent event, + Emitter emit, + ) async { + try { + final state = this.state as ConfirmingHideState; + final profile = _profileCubit.state as ProfileLoggedIn; + final dataItems = state.dataItems; + + emit(UploadingHideState(hideAction: state.hideAction)); + + await _driveDao.transaction(() async { + final dataBundle = await DataBundle.fromDataItems( + items: dataItems, + ); + + if (_useTurboUpload) { + final hideTx = await _arweave.prepareBundledDataItem( + dataBundle, + profile.wallet, + ); + await _turboUploadService.postDataItem( + dataItem: hideTx, + wallet: profile.wallet, + ); + } else { + final hideTx = await _arweave.prepareDataBundleTx( + dataBundle, + profile.wallet, + ); + await _arweave.postTx(hideTx); + } + + await state.saveEntitiesToDb(); + + emit(SuccessHideState(hideAction: state.hideAction)); + }); + } catch (e) { + logger.e('Error while hiding', e); + emit(FailureHideState(hideAction: state.hideAction)); + } + } + + void _onErrorEvent( + ErrorEvent event, + Emitter emit, + ) { + emit(FailureHideState(hideAction: event.hideAction)); + } + + @override + void onError(Object error, StackTrace stackTrace) { + add(ErrorEvent( + error: error, + stackTrace: stackTrace, + hideAction: state.hideAction, + )); + super.onError(error, stackTrace); + } +} diff --git a/lib/blocs/hide/hide_event.dart b/lib/blocs/hide/hide_event.dart new file mode 100644 index 0000000000..e667f54a88 --- /dev/null +++ b/lib/blocs/hide/hide_event.dart @@ -0,0 +1,104 @@ +import 'package:ardrive/blocs/hide/hide_state.dart'; +import 'package:ardrive/blocs/upload/upload_cubit.dart'; +import 'package:ardrive_utils/ardrive_utils.dart'; +import 'package:equatable/equatable.dart'; + +abstract class HideEvent extends Equatable { + const HideEvent(); +} + +class HideFileEvent extends HideEvent { + final DriveID driveId; + final FileID fileId; + + const HideFileEvent({ + required this.driveId, + required this.fileId, + }); + + @override + List get props => [driveId, fileId]; +} + +class HideFolderEvent extends HideEvent { + final DriveID driveId; + final FolderID folderId; + + const HideFolderEvent({ + required this.driveId, + required this.folderId, + }); + + @override + List get props => [driveId, folderId]; +} + +class UnhideFileEvent extends HideEvent { + final DriveID driveId; + final FileID fileId; + + const UnhideFileEvent({ + required this.driveId, + required this.fileId, + }); + + @override + List get props => [driveId, fileId]; +} + +class UnhideFolderEvent extends HideEvent { + final DriveID driveId; + final FolderID folderId; + + const UnhideFolderEvent({ + required this.driveId, + required this.folderId, + }); + + @override + List get props => [driveId, folderId]; +} + +class ConfirmUploadEvent extends HideEvent { + const ConfirmUploadEvent(); + + @override + List get props => []; +} + +class SelectUploadMethodEvent extends HideEvent { + final UploadMethod uploadMethod; + + const SelectUploadMethodEvent({ + required this.uploadMethod, + }); + + @override + List get props => [uploadMethod]; +} + +class RefreshTurboBalanceEvent extends HideEvent { + const RefreshTurboBalanceEvent(); + + @override + List get props => []; +} + +class ErrorEvent extends HideEvent { + final Object error; + final StackTrace stackTrace; + final HideAction hideAction; + + const ErrorEvent({ + required this.error, + required this.stackTrace, + required this.hideAction, + }); + + @override + List get props => [ + error, + stackTrace, + hideAction, + ]; +} diff --git a/lib/blocs/hide/hide_state.dart b/lib/blocs/hide/hide_state.dart new file mode 100644 index 0000000000..b257ce299d --- /dev/null +++ b/lib/blocs/hide/hide_state.dart @@ -0,0 +1,76 @@ +import 'package:ardrive/blocs/upload/upload_cubit.dart'; +import 'package:ardrive/core/upload/cost_calculator.dart'; +import 'package:arweave/arweave.dart'; +import 'package:equatable/equatable.dart'; + +abstract class HideState extends Equatable { + final HideAction hideAction; + + const HideState({ + required this.hideAction, + }); + + @override + List get props => [hideAction]; +} + +class InitialHideState extends HideState { + const InitialHideState() : super(hideAction: HideAction.hideFile); +} + +class UploadingHideState extends HideState { + const UploadingHideState({required super.hideAction}); +} + +class PreparingAndSigningHideState extends HideState { + const PreparingAndSigningHideState({required super.hideAction}); +} + +class ConfirmingHideState extends HideState { + final UploadMethod uploadMethod; + + final List dataItems; + final Future Function() saveEntitiesToDb; + + const ConfirmingHideState({ + required this.uploadMethod, + required super.hideAction, + required this.dataItems, + required this.saveEntitiesToDb, + }); + + @override + List get props => [ + uploadMethod, + hideAction, + ]; + + ConfirmingHideState copyWith({ + UploadMethod? uploadMethod, + UploadCostEstimate? costEstimateTurbo, + UploadCostEstimate? costEstimateAr, + HideAction? hideAction, + }) { + return ConfirmingHideState( + uploadMethod: uploadMethod ?? this.uploadMethod, + hideAction: hideAction ?? this.hideAction, + dataItems: dataItems, + saveEntitiesToDb: saveEntitiesToDb, + ); + } +} + +class SuccessHideState extends HideState { + const SuccessHideState({required super.hideAction}); +} + +class FailureHideState extends HideState { + const FailureHideState({required super.hideAction}); +} + +enum HideAction { + hideFile, + hideFolder, + unhideFile, + unhideFolder, +} diff --git a/lib/blocs/sync/ghost_folder.dart b/lib/blocs/sync/ghost_folder.dart index a1a7eee2af..2b43d2cefa 100644 --- a/lib/blocs/sync/ghost_folder.dart +++ b/lib/blocs/sync/ghost_folder.dart @@ -1,8 +1,11 @@ class GhostFolder { String folderId; String driveId; + bool isHidden; + GhostFolder({ required this.folderId, required this.driveId, + this.isHidden = false, }); } diff --git a/lib/blocs/sync/utils/add_file_entity_revisions.dart b/lib/blocs/sync/utils/add_file_entity_revisions.dart index 39edc74ee4..f47f59f61c 100644 --- a/lib/blocs/sync/utils/add_file_entity_revisions.dart +++ b/lib/blocs/sync/utils/add_file_entity_revisions.dart @@ -89,9 +89,12 @@ Future> .oldestFileRevisionByFileId(driveId: driveId, fileId: fileId) .getSingleOrNull(); + final dateCreated = oldestRevision?.dateCreated ?? + updatedFilesById[fileId]!.dateCreated.value; + updatedFilesById[fileId] = updatedFilesById[fileId]!.copyWith( - dateCreated: Value(oldestRevision?.dateCreated ?? - updatedFilesById[fileId]!.dateCreated as DateTime)); + dateCreated: Value(dateCreated), + ); } return updatedFilesById; diff --git a/lib/blocs/sync/utils/add_folder_entity_revisions.dart b/lib/blocs/sync/utils/add_folder_entity_revisions.dart index 016b06459b..00062affd0 100644 --- a/lib/blocs/sync/utils/add_folder_entity_revisions.dart +++ b/lib/blocs/sync/utils/add_folder_entity_revisions.dart @@ -74,9 +74,12 @@ Future> .oldestFolderRevisionByFolderId(driveId: driveId, folderId: folderId) .getSingleOrNull(); + final dateCreated = oldestRevision?.dateCreated ?? + updatedFoldersById[folderId]!.dateCreated.value; + updatedFoldersById[folderId] = updatedFoldersById[folderId]!.copyWith( - dateCreated: Value(oldestRevision?.dateCreated ?? - updatedFoldersById[folderId]!.dateCreated as DateTime)); + dateCreated: Value(dateCreated), + ); } return updatedFoldersById; diff --git a/lib/blocs/sync/utils/create_ghosts.dart b/lib/blocs/sync/utils/create_ghosts.dart index 22c368cb9f..81cfa73c87 100644 --- a/lib/blocs/sync/utils/create_ghosts.dart +++ b/lib/blocs/sync/utils/create_ghosts.dart @@ -44,6 +44,7 @@ Future createGhosts({ lastUpdated: DateTime.now(), isGhost: true, dateCreated: DateTime.now(), + isHidden: ghostFolder.isHidden, ); await driveDao.into(driveDao.folderEntries).insert(folderEntry); ghostFoldersByDrive.putIfAbsent( diff --git a/lib/blocs/upload/models/payment_method_info.dart b/lib/blocs/upload/models/payment_method_info.dart new file mode 100644 index 0000000000..1cb216e2e9 --- /dev/null +++ b/lib/blocs/upload/models/payment_method_info.dart @@ -0,0 +1,85 @@ +import 'package:ardrive/blocs/upload/models/upload_plan.dart'; +import 'package:ardrive/blocs/upload/upload_cubit.dart'; +import 'package:ardrive/core/upload/cost_calculator.dart'; +import 'package:equatable/equatable.dart'; + +class UploadPaymentMethodInfo extends Equatable { + final UploadMethod uploadMethod; + final UploadCostEstimate? costEstimateTurbo; + final UploadCostEstimate costEstimateAr; + final bool hasNoTurboBalance; + final bool isTurboUploadPossible; + final String arBalance; + final bool sufficientArBalance; + final String turboCredits; + final bool sufficentCreditsBalance; + final bool isFreeThanksToTurbo; + final UploadPlan? uploadPlanForAR; + final UploadPlan? uploadPlanForTurbo; + final int totalSize; + + const UploadPaymentMethodInfo({ + required this.uploadMethod, + required this.costEstimateTurbo, + required this.costEstimateAr, + required this.hasNoTurboBalance, + required this.isTurboUploadPossible, + required this.arBalance, + required this.sufficientArBalance, + required this.turboCredits, + required this.sufficentCreditsBalance, + required this.isFreeThanksToTurbo, + this.uploadPlanForAR, + this.uploadPlanForTurbo, + required this.totalSize, + }); + + // copy with + UploadPaymentMethodInfo copyWith({ + UploadMethod? uploadMethod, + UploadCostEstimate? costEstimateTurbo, + UploadCostEstimate? costEstimateAr, + bool? hasNoTurboBalance, + bool? isTurboUploadPossible, + String? arBalance, + bool? sufficientArBalance, + String? turboCredits, + bool? sufficentCreditsBalance, + bool? isFreeThanksToTurbo, + UploadPlan? uploadPlanForAR, + UploadPlan? uploadPlanForTurbo, + int? totalSize, + }) { + return UploadPaymentMethodInfo( + totalSize: totalSize ?? this.totalSize, + uploadPlanForAR: uploadPlanForAR ?? this.uploadPlanForAR, + uploadPlanForTurbo: uploadPlanForTurbo ?? this.uploadPlanForTurbo, + uploadMethod: uploadMethod ?? this.uploadMethod, + costEstimateTurbo: costEstimateTurbo ?? this.costEstimateTurbo, + costEstimateAr: costEstimateAr ?? this.costEstimateAr, + hasNoTurboBalance: hasNoTurboBalance ?? this.hasNoTurboBalance, + isTurboUploadPossible: + isTurboUploadPossible ?? this.isTurboUploadPossible, + arBalance: arBalance ?? this.arBalance, + sufficientArBalance: sufficientArBalance ?? this.sufficientArBalance, + turboCredits: turboCredits ?? this.turboCredits, + sufficentCreditsBalance: + sufficentCreditsBalance ?? this.sufficentCreditsBalance, + isFreeThanksToTurbo: isFreeThanksToTurbo ?? this.isFreeThanksToTurbo, + ); + } + + @override + List get props => [ + uploadMethod, + costEstimateTurbo, + costEstimateAr, + hasNoTurboBalance, + isTurboUploadPossible, + arBalance, + sufficientArBalance, + turboCredits, + sufficentCreditsBalance, + isFreeThanksToTurbo, + ]; +} diff --git a/lib/blocs/upload/payment_method/bloc/upload_payment_method_bloc.dart b/lib/blocs/upload/payment_method/bloc/upload_payment_method_bloc.dart new file mode 100644 index 0000000000..68fe7a4225 --- /dev/null +++ b/lib/blocs/upload/payment_method/bloc/upload_payment_method_bloc.dart @@ -0,0 +1,134 @@ +import 'package:ardrive/authentication/ardrive_auth.dart'; +import 'package:ardrive/blocs/blocs.dart'; +import 'package:ardrive/blocs/upload/models/payment_method_info.dart'; +import 'package:ardrive/core/upload/uploader.dart'; +import 'package:ardrive/turbo/utils/utils.dart'; +import 'package:ardrive/utils/logger.dart'; +import 'package:arweave/arweave.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +part 'upload_payment_method_event.dart'; +part 'upload_payment_method_state.dart'; + +class UploadPaymentMethodBloc + extends Bloc { + final ArDriveUploadPreparationManager _arDriveUploadManager; + final ArDriveAuth _auth; + final ProfileCubit _profileCubit; + + late UploadPreparation uploadPreparation; + + UploadPaymentMethodBloc( + this._profileCubit, + this._arDriveUploadManager, + this._auth, + ) : super(UploadPaymentMethodInitial()) { + on(_onUploadPaymentMethodEvent); + } + + Future _onUploadPaymentMethodEvent(UploadPaymentMethodEvent event, + Emitter emit) async { + if (event is PrepareUploadPaymentMethod) { + await _handlePrepareUploadPaymentMethod(event, emit); + } else if (event is ChangeUploadPaymentMethod) { + _handleChangeUploadPaymentMethod(event, emit); + } + } + + Future _handlePrepareUploadPaymentMethod( + PrepareUploadPaymentMethod event, + Emitter emit) async { + emit(UploadPaymentMethodLoading( + isArConnect: await _profileCubit.isCurrentProfileArConnect())); + + if (await _profileCubit.checkIfWalletMismatch()) { + emit(UploadPaymentMethodWalletMismatch()); + return; + } + + try { + uploadPreparation = + await _arDriveUploadManager.prepareUpload(params: event.params); + final paymentInfo = uploadPreparation.uploadPaymentInfo; + + final literalTurboBalance = convertWinstonToLiteralString( + uploadPreparation.uploadPaymentInfo.turboBalance); + final literalARBalance = + convertWinstonToLiteralString(_auth.currentUser.walletBalance); + + bool isTurboZeroBalance = + uploadPreparation.uploadPaymentInfo.turboBalance == BigInt.zero; + + emit( + UploadPaymentMethodLoaded( + canUpload: _canUploadWithMethod(paymentInfo.defaultPaymentMethod), + params: event.params, + paymentMethodInfo: UploadPaymentMethodInfo( + totalSize: uploadPreparation.uploadPaymentInfo.totalSize, + uploadPlanForAR: + uploadPreparation.uploadPlansPreparation.uploadPlanForAr, + uploadPlanForTurbo: + uploadPreparation.uploadPlansPreparation.uploadPlanForTurbo, + arBalance: literalARBalance, + costEstimateAr: uploadPreparation.uploadPaymentInfo.arCostEstimate, + costEstimateTurbo: + uploadPreparation.uploadPaymentInfo.turboCostEstimate, + hasNoTurboBalance: isTurboZeroBalance, + isFreeThanksToTurbo: uploadPreparation + .uploadPaymentInfo.isFreeUploadPossibleUsingTurbo, + isTurboUploadPossible: paymentInfo.isUploadEligibleToTurbo, + sufficentCreditsBalance: _canUploadWithMethod(UploadMethod.turbo), + sufficientArBalance: _canUploadWithMethod(UploadMethod.ar), + turboCredits: literalTurboBalance, + uploadMethod: paymentInfo.defaultPaymentMethod, + ), + ), + ); + } catch (e) { + logger.e('Upload preparation failed.', e); + emit(UploadPaymentMethodError()); + } + } + + void _handleChangeUploadPaymentMethod( + ChangeUploadPaymentMethod event, Emitter emit) { + if (state is UploadPaymentMethodLoaded) { + final currentState = state as UploadPaymentMethodLoaded; + final canUpload = _canUploadWithMethod(event.paymentMethod); + emit(currentState.copyWith( + paymentMethodInfo: currentState.paymentMethodInfo + .copyWith(uploadMethod: event.paymentMethod), + canUpload: canUpload, + )); + } + } + + bool _canUploadWithMethod(UploadMethod method) { + final profile = _profileCubit.state as ProfileLoggedIn; + + final paymentInfo = uploadPreparation.uploadPaymentInfo; + + bool sufficientBalanceToPayWithAR = + profile.walletBalance >= paymentInfo.arCostEstimate.totalCost; + bool sufficientBalanceToPayWithTurbo = + paymentInfo.turboCostEstimate.totalCost <= + uploadPreparation.uploadPaymentInfo.turboBalance; + + if (method == UploadMethod.ar && sufficientBalanceToPayWithAR) { + logger.d('Enabling button for AR payment method'); + return true; + } else if (method == UploadMethod.turbo && + paymentInfo.isUploadEligibleToTurbo && + sufficientBalanceToPayWithTurbo) { + logger.d('Enabling button for Turbo payment method'); + return true; + } else if (paymentInfo.isFreeUploadPossibleUsingTurbo) { + logger.d('Enabling button for free upload using Turbo'); + return true; + } else { + logger.d('Disabling button'); + return false; + } + } +} diff --git a/lib/blocs/upload/payment_method/bloc/upload_payment_method_event.dart b/lib/blocs/upload/payment_method/bloc/upload_payment_method_event.dart new file mode 100644 index 0000000000..7452ca5e94 --- /dev/null +++ b/lib/blocs/upload/payment_method/bloc/upload_payment_method_event.dart @@ -0,0 +1,42 @@ +part of 'upload_payment_method_bloc.dart'; + +sealed class UploadPaymentMethodEvent extends Equatable { + const UploadPaymentMethodEvent(); + + @override + List get props => []; +} + +final class PrepareUploadPaymentMethod extends UploadPaymentMethodEvent { + final UploadParams params; + + const PrepareUploadPaymentMethod({ + required this.params, + }); + + @override + List get props => []; +} + +final class PrepareUploadPaymentMethodWithDataItem + extends UploadPaymentMethodEvent { + final DataItem dataItem; + + const PrepareUploadPaymentMethodWithDataItem({ + required this.dataItem, + }); + + @override + List get props => []; +} + +final class ChangeUploadPaymentMethod extends UploadPaymentMethodEvent { + final UploadMethod paymentMethod; + + const ChangeUploadPaymentMethod({ + required this.paymentMethod, + }); + + @override + List get props => []; +} diff --git a/lib/blocs/upload/payment_method/bloc/upload_payment_method_state.dart b/lib/blocs/upload/payment_method/bloc/upload_payment_method_state.dart new file mode 100644 index 0000000000..b1705ca2d9 --- /dev/null +++ b/lib/blocs/upload/payment_method/bloc/upload_payment_method_state.dart @@ -0,0 +1,55 @@ +part of 'upload_payment_method_bloc.dart'; + +sealed class UploadPaymentMethodState extends Equatable { + const UploadPaymentMethodState(); + + @override + List get props => []; +} + +final class UploadPaymentMethodInitial extends UploadPaymentMethodState {} + +final class UploadPaymentMethodLoading extends UploadPaymentMethodState { + final bool isArConnect; + + const UploadPaymentMethodLoading({ + this.isArConnect = false, + }); +} + +final class UploadPaymentMethodLoaded extends UploadPaymentMethodState { + final UploadParams params; + final UploadPaymentMethodInfo paymentMethodInfo; + final bool canUpload; + + const UploadPaymentMethodLoaded({ + required this.params, + required this.paymentMethodInfo, + required this.canUpload, + }); + + @override + List get props => [params, paymentMethodInfo]; + + UploadPaymentMethodLoaded copyWith({ + UploadParams? params, + UploadPaymentMethodInfo? paymentMethodInfo, + bool? canUpload, + }) { + return UploadPaymentMethodLoaded( + params: params ?? this.params, + paymentMethodInfo: paymentMethodInfo ?? this.paymentMethodInfo, + canUpload: canUpload ?? this.canUpload, + ); + } +} + +final class UploadPaymentMethodError extends UploadPaymentMethodState { + @override + List get props => []; +} + +final class UploadPaymentMethodWalletMismatch extends UploadPaymentMethodState { + @override + List get props => []; +} 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 new file mode 100644 index 0000000000..466591104b --- /dev/null +++ b/lib/blocs/upload/payment_method/view/upload_payment_method_view.dart @@ -0,0 +1,74 @@ +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'; + +class UploadPaymentMethodView extends StatelessWidget { + const UploadPaymentMethodView({ + super.key, + required this.params, + required this.onUploadMethodChanged, + required this.onError, + this.onTurboTopupSucess, + this.loadingIndicator, + }); + + final Function(UploadMethod, UploadPaymentMethodInfo, bool) + onUploadMethodChanged; + final Function() onError; + final Function()? onTurboTopupSucess; + final UploadParams params; + final Widget? loadingIndicator; + + @override + Widget build(BuildContext context) { + return BlocConsumer( + listener: (context, state) { + if (state is UploadPaymentMethodLoaded) { + logger.d( + 'UploadPaymentMethodLoaded: ${state.paymentMethodInfo.uploadMethod}'); + onUploadMethodChanged( + state.paymentMethodInfo.uploadMethod, + state.paymentMethodInfo, + state.canUpload, + ); + } else if (state is UploadPaymentMethodError) { + onError(); + } + }, + builder: (context, state) { + if (state is UploadPaymentMethodLoaded) { + return PaymentMethodSelector( + uploadMethodInfo: state.paymentMethodInfo, + onArSelect: () { + context + .read() + .add(const ChangeUploadPaymentMethod( + paymentMethod: UploadMethod.ar, + )); + }, + onTurboSelect: () { + context + .read() + .add(const ChangeUploadPaymentMethod( + paymentMethod: UploadMethod.turbo, + )); + }, + onTurboTopupSucess: () { + onTurboTopupSucess?.call(); + }, + ); + } + if (loadingIndicator != null) { + return loadingIndicator!; + } + + return const SizedBox.shrink(); + }, + ); + } +} diff --git a/lib/blocs/upload/upload_cubit.dart b/lib/blocs/upload/upload_cubit.dart index bac026af20..3cc5c787dc 100644 --- a/lib/blocs/upload/upload_cubit.dart +++ b/lib/blocs/upload/upload_cubit.dart @@ -4,16 +4,15 @@ import 'package:ardrive/authentication/ardrive_auth.dart'; import 'package:ardrive/blocs/blocs.dart'; import 'package:ardrive/blocs/upload/limits.dart'; import 'package:ardrive/blocs/upload/models/models.dart'; +import 'package:ardrive/blocs/upload/models/payment_method_info.dart'; import 'package:ardrive/blocs/upload/upload_file_checker.dart'; import 'package:ardrive/core/activity_tracker.dart'; -import 'package:ardrive/core/upload/cost_calculator.dart'; import 'package:ardrive/core/upload/uploader.dart'; import 'package:ardrive/entities/file_entity.dart'; import 'package:ardrive/entities/folder_entity.dart'; import 'package:ardrive/main.dart'; import 'package:ardrive/models/models.dart'; import 'package:ardrive/turbo/services/upload_service.dart'; -import 'package:ardrive/turbo/utils/utils.dart'; import 'package:ardrive/utils/logger.dart'; import 'package:ardrive/utils/plausible_event_tracker/plausible_custom_event_properties.dart'; import 'package:ardrive/utils/plausible_event_tracker/plausible_event_tracker.dart'; @@ -45,7 +44,6 @@ class UploadCubit extends Cubit { final PstService _pst; final UploadFileChecker _uploadFileChecker; final ArDriveAuth _auth; - final ArDriveUploadPreparationManager _arDriveUploadManager; final ActivityTracker _activityTracker; late bool uploadFolders; @@ -53,35 +51,31 @@ class UploadCubit extends Cubit { late FolderEntry _targetFolder; UploadMethod? _uploadMethod; - void setUploadMethod(UploadMethod? method) { + void setUploadMethod( + UploadMethod? method, + UploadPaymentMethodInfo paymentInfo, + bool canUpload, + ) { logger.d('Upload method set to $method'); _uploadMethod = method; - bool isButtonEnabled = false; - if (state is UploadReady) { final uploadReady = state as UploadReady; - logger.d( - 'Sufficient Balance To Pay With AR: ${uploadReady.sufficientArBalance}'); - - if (_uploadMethod == UploadMethod.ar && uploadReady.sufficientArBalance) { - logger.d('Enabling button for AR payment method'); - isButtonEnabled = true; - } else if (_uploadMethod == UploadMethod.turbo && - uploadReady.isTurboUploadPossible && - uploadReady.sufficentCreditsBalance) { - logger.d('Enabling button for Turbo payment method'); - isButtonEnabled = true; - } else if (uploadReady.isFreeThanksToTurbo) { - logger.d('Enabling button for free upload using Turbo'); - isButtonEnabled = true; - } else { - logger.d('Disabling button'); - } emit(uploadReady.copyWith( + paymentInfo: paymentInfo, uploadMethod: method, - isButtonToUploadEnabled: isButtonEnabled, + isButtonToUploadEnabled: canUpload, + )); + } else if (state is UploadReadyToPrepare) { + emit(UploadReady( + params: (state as UploadReadyToPrepare).params, + paymentInfo: paymentInfo, + numberOfFiles: files.length, + uploadIsPublic: !_targetDrive.isPrivate, + isDragNDrop: isDragNDrop, + isButtonToUploadEnabled: canUpload, + isArConnect: (state as UploadReadyToPrepare).isArConnect, )); } } @@ -113,7 +107,6 @@ class UploadCubit extends Cubit { _driveDao = driveDao, _pst = pst, _auth = auth, - _arDriveUploadManager = arDriveUploadManager, _activityTracker = activityTracker, super(UploadPreparationInProgress()); @@ -304,8 +297,6 @@ class UploadCubit extends Cubit { Future prepareUploadPlanAndCostEstimates({ UploadActions? uploadAction, }) async { - final profile = _profileCubit.state as ProfileLoggedIn; - if (await _profileCubit.checkIfWalletMismatch()) { emit(UploadWalletMismatch()); return; @@ -326,94 +317,22 @@ class UploadCubit extends Cubit { ); try { - final uploadPreparation = await _arDriveUploadManager.prepareUpload( - params: UploadParams( - user: _auth.currentUser, - files: files, - targetFolder: _targetFolder, - targetDrive: _targetDrive, - conflictingFiles: conflictingFiles, - foldersByPath: foldersByPath, - ), - ); - - final paymentInfo = uploadPreparation.uploadPaymentInfo; - final uploadPlansPreparation = uploadPreparation.uploadPlansPreparation; - - _uploadMethod = paymentInfo.defaultPaymentMethod; - logger.d('Upload method: $_uploadMethod'); - if (await _profileCubit.checkIfWalletMismatch()) { emit(UploadWalletMismatch()); return; } - bool isTurboZeroBalance = - uploadPreparation.uploadPaymentInfo.turboBalance == BigInt.zero; - - logger.d( - 'Upload preparation finished\n' - 'UploadMethod: $_uploadMethod\n' - 'UploadPlan For AR: ${uploadPreparation.uploadPaymentInfo.arCostEstimate.toString()}\n' - 'UploadPlan For Turbo: ${uploadPreparation.uploadPlansPreparation.uploadPlanForTurbo.toString()}\n' - 'Turbo Balance: ${uploadPreparation.uploadPaymentInfo.turboBalance}\n' - 'AR Balance: ${_auth.currentUser.walletBalance}\n' - 'Is Turbo Upload Possible: ${paymentInfo.isUploadEligibleToTurbo}\n' - 'Is Zero Balance: $isTurboZeroBalance\n', - ); - - final literalBalance = convertWinstonToLiteralString( - uploadPreparation.uploadPaymentInfo.turboBalance); - final literalARBalance = - convertWinstonToLiteralString(_auth.currentUser.walletBalance); - - bool isButtonEnabled = false; - bool sufficientBalanceToPayWithAR = - profile.walletBalance >= paymentInfo.arCostEstimate.totalCost; - bool sufficientBalanceToPayWithTurbo = - paymentInfo.turboCostEstimate.totalCost <= - uploadPreparation.uploadPaymentInfo.turboBalance; - - logger.d( - 'Sufficient Balance To Pay With AR: $sufficientBalanceToPayWithAR'); - - if (_uploadMethod == UploadMethod.ar && sufficientBalanceToPayWithAR) { - logger.d('Enabling button for AR payment method'); - isButtonEnabled = true; - } else if (_uploadMethod == UploadMethod.turbo && - paymentInfo.isUploadEligibleToTurbo && - paymentInfo.isTurboAvailable && - sufficientBalanceToPayWithTurbo) { - logger.d('Enabling button for Turbo payment method'); - isButtonEnabled = true; - } else if (paymentInfo.isFreeUploadPossibleUsingTurbo) { - logger.d('Enabling button for free upload using Turbo'); - isButtonEnabled = true; - } else { - logger.d('Disabling button'); - } - emit( - UploadReady( - isTurboUploadPossible: paymentInfo.isUploadEligibleToTurbo && - paymentInfo.isTurboAvailable, - isZeroBalance: isTurboZeroBalance, - turboCredits: literalBalance, - uploadSize: paymentInfo.totalSize, - costEstimateAr: paymentInfo.arCostEstimate, - costEstimateTurbo: paymentInfo.turboCostEstimate, - credits: literalBalance, - arBalance: literalARBalance, - uploadIsPublic: _targetDrive.isPublic, - sufficientArBalance: - profile.walletBalance >= paymentInfo.arCostEstimate.totalCost, - uploadPlanForAR: uploadPlansPreparation.uploadPlanForAr, - uploadPlanForTurbo: uploadPlansPreparation.uploadPlanForTurbo, - isFreeThanksToTurbo: (paymentInfo.isFreeUploadPossibleUsingTurbo), - sufficentCreditsBalance: sufficientBalanceToPayWithTurbo, - uploadMethod: _uploadMethod!, - isButtonToUploadEnabled: isButtonEnabled, - isDragNDrop: isDragNDrop, + UploadReadyToPrepare( + params: UploadParams( + user: _auth.currentUser, + files: files, + targetFolder: _targetFolder, + targetDrive: _targetDrive, + conflictingFiles: conflictingFiles, + foldersByPath: foldersByPath, + ), + isArConnect: await _profileCubit.isCurrentProfileArConnect(), ), ); } catch (error, stacktrace) { @@ -901,6 +820,10 @@ class UploadCubit extends Cubit { return isPrivateForTesting || _targetDrive.isPrivate; } + void emitErrorFromPreparation() { + emit(UploadFailure(error: UploadErrors.unknown)); + } + @override void onError(Object error, StackTrace stackTrace) { if (error is TurboUploadTimeoutException) { diff --git a/lib/blocs/upload/upload_state.dart b/lib/blocs/upload/upload_state.dart index 4e80b4282a..0157fab585 100644 --- a/lib/blocs/upload/upload_state.dart +++ b/lib/blocs/upload/upload_state.dart @@ -63,136 +63,72 @@ class UploadFileTooLarge extends UploadState { List get props => [tooLargeFileNames]; } -/// [UploadReady] means that the upload is ready to be performed and is awaiting confirmation from the user. -class UploadReady extends UploadState { - /// The cost to upload the data, in AR. - final UploadCostEstimate costEstimateAr; - final UploadCostEstimate? costEstimateTurbo; +class UploadReadyToPrepare extends UploadState { + final UploadParams params; + final bool isArConnect; - /// Whether or not the user has sufficient AR to cover the `totalCost`. - final bool sufficientArBalance; - final bool isZeroBalance; + UploadReadyToPrepare({ + required this.params, + this.isArConnect = false, + }); - final bool sufficentCreditsBalance; + @override + List get props => [params]; +} - /// Whether or not the upload will be made public ie. without encryption. +/// [UploadReady] means that the upload is ready to be performed and is awaiting confirmation from the user. +class UploadReady extends UploadState { + final UploadPaymentMethodInfo paymentInfo; + final bool isButtonToUploadEnabled; + final bool isDragNDrop; final bool uploadIsPublic; + final int numberOfFiles; - final UploadPlan uploadPlanForAR; - final UploadPlan? uploadPlanForTurbo; - final bool isTurboUploadPossible; - final bool isFreeThanksToTurbo; - - final int uploadSize; + final UploadParams params; - final String credits; - final String arBalance; - final String turboCredits; - final UploadMethod uploadMethod; - final bool isButtonToUploadEnabled; - final bool isDragNDrop; + final bool isArConnect; UploadReady({ - required this.costEstimateAr, - required this.sufficientArBalance, + required this.paymentInfo, required this.uploadIsPublic, - required this.uploadPlanForAR, - required this.isFreeThanksToTurbo, - required this.uploadSize, - required this.credits, - required this.arBalance, - required this.sufficentCreditsBalance, - required this.turboCredits, - this.costEstimateTurbo, - required this.isZeroBalance, - this.uploadPlanForTurbo, - required this.isTurboUploadPossible, - required this.uploadMethod, required this.isButtonToUploadEnabled, this.isDragNDrop = false, + required this.params, + required this.numberOfFiles, + required this.isArConnect, }); // copyWith UploadReady copyWith({ - UploadCostEstimate? costEstimateAr, - UploadCostEstimate? costEstimateTurbo, - bool? sufficientArBalance, - bool? isZeroBalance, - bool? sufficentCreditsBalance, - bool? uploadIsPublic, - UploadPlan? uploadPlanForAR, - UploadPlan? uploadPlanForTurbo, - bool? isTurboUploadPossible, - bool? isFreeThanksToTurbo, - int? uploadSize, - String? credits, - String? arBalance, - String? turboCredits, + UploadPaymentMethodInfo? paymentInfo, UploadMethod? uploadMethod, bool? isButtonToUploadEnabled, + bool? isDragNDrop, + bool? uploadIsPublic, + int? numberOfFiles, + UploadParams? params, + bool? isArConnect, }) { return UploadReady( - costEstimateAr: costEstimateAr ?? this.costEstimateAr, - costEstimateTurbo: costEstimateTurbo ?? this.costEstimateTurbo, - sufficientArBalance: sufficientArBalance ?? this.sufficientArBalance, - isZeroBalance: isZeroBalance ?? this.isZeroBalance, - sufficentCreditsBalance: - sufficentCreditsBalance ?? this.sufficentCreditsBalance, + isArConnect: isArConnect ?? this.isArConnect, uploadIsPublic: uploadIsPublic ?? this.uploadIsPublic, - uploadPlanForAR: uploadPlanForAR ?? this.uploadPlanForAR, - uploadPlanForTurbo: uploadPlanForTurbo ?? this.uploadPlanForTurbo, - isTurboUploadPossible: - isTurboUploadPossible ?? this.isTurboUploadPossible, - isFreeThanksToTurbo: isFreeThanksToTurbo ?? this.isFreeThanksToTurbo, - uploadSize: uploadSize ?? this.uploadSize, - credits: credits ?? this.credits, - arBalance: arBalance ?? this.arBalance, - turboCredits: turboCredits ?? this.turboCredits, - uploadMethod: uploadMethod ?? this.uploadMethod, + isDragNDrop: isDragNDrop ?? this.isDragNDrop, + paymentInfo: paymentInfo ?? this.paymentInfo, + params: params ?? this.params, isButtonToUploadEnabled: isButtonToUploadEnabled ?? this.isButtonToUploadEnabled, + numberOfFiles: numberOfFiles ?? this.numberOfFiles, ); } @override List get props => [ - costEstimateAr, - costEstimateTurbo, - sufficientArBalance, - isZeroBalance, - sufficentCreditsBalance, - uploadIsPublic, - uploadPlanForAR, - uploadPlanForTurbo, - isTurboUploadPossible, - isFreeThanksToTurbo, - uploadSize, - credits, - arBalance, - turboCredits, - uploadMethod, + paymentInfo, isButtonToUploadEnabled, ]; @override - toString() => 'UploadReady { ' - 'costEstimateAr: $costEstimateAr, ' - 'costEstimateTurbo: $costEstimateTurbo, ' - 'sufficientArBalance: $sufficientArBalance, ' - 'isZeroBalance: $isZeroBalance, ' - 'sufficentCreditsBalance: $sufficentCreditsBalance, ' - 'uploadIsPublic: $uploadIsPublic, ' - 'uploadPlanForAR: $uploadPlanForAR, ' - 'uploadPlanForTurbo: $uploadPlanForTurbo, ' - 'isTurboUploadPossible: $isTurboUploadPossible, ' - 'isFreeThanksToTurbo: $isFreeThanksToTurbo, ' - 'uploadSize: $uploadSize, ' - 'credits: $credits, ' - 'arBalance: $arBalance, ' - 'turboCredits: $turboCredits, ' - 'uploadMethod: $uploadMethod, ' - 'isButtonToUploadEnabled: $isButtonToUploadEnabled, ' - '}'; + toString() => 'UploadReady { paymentInfo: $paymentInfo }'; } class UploadInProgress extends UploadState { diff --git a/lib/components/app_top_bar.dart b/lib/components/app_top_bar.dart index fc44bcc19e..2f0c3b1b6a 100644 --- a/lib/components/app_top_bar.dart +++ b/lib/components/app_top_bar.dart @@ -1,4 +1,3 @@ -// implement a widget that has 145 of height and maximum widget, and has a row as child import 'package:ardrive/blocs/sync/sync_cubit.dart'; import 'package:ardrive/components/profile_card.dart'; import 'package:ardrive/gift/reedem_button.dart'; diff --git a/lib/components/create_manifest_form.dart b/lib/components/create_manifest_form.dart index a3a3a1d73d..61f329518e 100644 --- a/lib/components/create_manifest_form.dart +++ b/lib/components/create_manifest_form.dart @@ -211,51 +211,53 @@ class _CreateManifestFormState extends State { if (state is CreateManifestInitial) { return ArDriveStandardModal( - width: kLargeDialogWidth, - title: appLocalizationsOf(context).addnewManifestEmphasized, - actions: [ - ModalAction( - action: () => Navigator.pop(context), - title: appLocalizationsOf(context).cancelEmphasized, - ), - ModalAction( - 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, - ), - ), - ]), + width: kLargeDialogWidth, + title: appLocalizationsOf(context).addnewManifestEmphasized, + actions: [ + ModalAction( + action: () => Navigator.pop(context), + title: appLocalizationsOf(context).cancelEmphasized, + ), + ModalAction( + 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, ), - manifestNameForm() - ], - )), - )); + const TextSpan(text: ' '), + TextSpan( + text: appLocalizationsOf(context).learnMore, + style: textStyle.copyWith( + decoration: TextDecoration.underline, + ), + recognizer: TapGestureRecognizer() + ..onTap = () => openUrl( + url: Resources.manifestLearnMoreLink, + ), + ), + ]), + ), + manifestNameForm() + ], + ), + ), + ), + ); } if (state is CreateManifestTurboUploadConfirmation) { final hasPendingFiles = state.folderHasPendingFiles; @@ -430,7 +432,9 @@ class _CreateManifestFormState extends State { final cubit = context.read(); final items = [ - ...state.viewingFolder.subfolders.map( + ...state.viewingFolder.subfolders + .where((element) => !element.isHidden) + .map( (f) { final enabled = !_isFolderEmpty( f.id, @@ -474,6 +478,7 @@ class _CreateManifestFormState extends State { }, ), ...state.viewingFolder.files + .where((element) => !element.isHidden) .map( (f) => Padding( padding: const EdgeInsets.symmetric( @@ -518,7 +523,8 @@ class _CreateManifestFormState extends State { width: double.infinity, height: 77, alignment: Alignment.centerLeft, - color: ArDriveTheme.of(context).themeData.colors.themeBgCanvas, + color: + ArDriveTheme.of(context).themeData.tableTheme.backgroundColor, child: Row( children: [ ArDriveClickArea( @@ -571,6 +577,38 @@ class _CreateManifestFormState extends State { ), ), const Divider(), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.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, + ), + ), + ), + ], + )), + ), Container( decoration: BoxDecoration( color: ArDriveTheme.of(context).themeData.colors.themeBgSurface, @@ -588,7 +626,7 @@ class _CreateManifestFormState extends State { .themeData .colors .themeFgDefault, - fontStyle: ArDriveTypography.body.buttonNormalRegular( + fontStyle: ArDriveTypography.body.buttonNormalBold( color: ArDriveTheme.of(context) .themeData .colors diff --git a/lib/components/create_snapshot_dialog.dart b/lib/components/create_snapshot_dialog.dart index 4bc385ab0f..0f76c30167 100644 --- a/lib/components/create_snapshot_dialog.dart +++ b/lib/components/create_snapshot_dialog.dart @@ -3,6 +3,7 @@ import 'package:ardrive/blocs/blocs.dart'; import 'package:ardrive/blocs/create_snapshot/create_snapshot_cubit.dart'; import 'package:ardrive/blocs/prompt_to_snapshot/prompt_to_snapshot_bloc.dart'; import 'package:ardrive/blocs/prompt_to_snapshot/prompt_to_snapshot_event.dart'; +import 'package:ardrive/blocs/upload/models/payment_method_info.dart'; import 'package:ardrive/components/components.dart'; import 'package:ardrive/components/payment_method_selector_widget.dart'; import 'package:ardrive/models/models.dart'; @@ -424,17 +425,21 @@ Widget _confirmDialog( ), } else ...{ PaymentMethodSelector( - uploadMethod: state.uploadMethod, - costEstimateTurbo: state.costEstimateTurbo, - costEstimateAr: state.costEstimateAr, - hasNoTurboBalance: state.hasNoTurboBalance, - isTurboUploadPossible: true, - arBalance: state.arBalance, - sufficientArBalance: state.sufficientBalanceToPayWithAr, - turboCredits: state.turboCredits, - sufficentCreditsBalance: - state.sufficientBalanceToPayWithTurbo, - isFreeThanksToTurbo: false, + uploadMethodInfo: UploadPaymentMethodInfo( + uploadMethod: state.uploadMethod, + totalSize: state.snapshotSize, + costEstimateTurbo: state.costEstimateTurbo, + costEstimateAr: state.costEstimateAr, + hasNoTurboBalance: state.hasNoTurboBalance, + isTurboUploadPossible: true, + arBalance: state.arBalance, + sufficientArBalance: + state.sufficientBalanceToPayWithAr, + turboCredits: state.turboCredits, + sufficentCreditsBalance: + state.sufficientBalanceToPayWithTurbo, + isFreeThanksToTurbo: false, + ), onTurboTopupSucess: () { createSnapshotCubit.refreshTurboBalance(); }, diff --git a/lib/components/details_panel.dart b/lib/components/details_panel.dart index e1a51668c4..66bdf67081 100644 --- a/lib/components/details_panel.dart +++ b/lib/components/details_panel.dart @@ -4,6 +4,7 @@ import 'package:ardrive/components/app_version_widget.dart'; import 'package:ardrive/components/components.dart'; import 'package:ardrive/components/dotted_line.dart'; import 'package:ardrive/components/drive_rename_form.dart'; +import 'package:ardrive/components/hide_dialog.dart'; import 'package:ardrive/components/pin_indicator.dart'; import 'package:ardrive/components/sizes.dart'; import 'package:ardrive/components/truncated_address.dart'; @@ -38,7 +39,6 @@ class DetailsPanel extends StatefulWidget { const DetailsPanel({ super.key, required this.item, - required this.maybeSelectedItem, required this.drivePrivacy, this.revisions, this.fileKey, @@ -50,7 +50,6 @@ class DetailsPanel extends StatefulWidget { }); final ArDriveDataTableItem item; - final SelectedItem? maybeSelectedItem; final Privacy drivePrivacy; final List? revisions; final SecretKey? fileKey; @@ -68,7 +67,8 @@ class _DetailsPanelState extends State { @override Widget build(BuildContext context) { return MultiBlocProvider( - // Specify a key to ensure a new cubit is provided when the folder/file id changes. + // Specify a key to ensure a new cubit is provided when the folder/file id + // changes. key: ValueKey( '${widget.item.driveId}${widget.item.id}${widget.item.name}', ), @@ -225,8 +225,15 @@ class _DetailsPanelState extends State { ScreenTypeLayout.builder( desktop: (context) => Column( children: [ - DetailsPanelToolbar( - item: widget.item, + BlocBuilder( + builder: (context, driveDetailState) { + final driveDetailLoadSuccess = + driveDetailState as DriveDetailLoadSuccess; + return DetailsPanelToolbar( + item: widget.item, + driveDetailLoadSuccess: driveDetailLoadSuccess, + ); + }, ), const SizedBox( height: 24, @@ -797,6 +804,12 @@ class _DetailsPanelState extends State { case RevisionAction.move: title = appLocalizationsOf(context).folderWasMoved; break; + case RevisionAction.hide: + title = appLocalizationsOf(context).folderWasHidden; + break; + case RevisionAction.unhide: + title = appLocalizationsOf(context).folderWasUnhidden; + break; default: title = appLocalizationsOf(context).folderWasModified; } @@ -821,6 +834,12 @@ class _DetailsPanelState extends State { title = appLocalizationsOf(context) .driveWasRenamed(revision.name); break; + case RevisionAction.hide: + title = appLocalizationsOf(context).driveWasHidden; + break; + case RevisionAction.unhide: + title = appLocalizationsOf(context).driveWasUnhidden; + break; default: title = appLocalizationsOf(context).driveWasModified; } @@ -879,6 +898,12 @@ class _DetailsPanelState extends State { fileKey: key, ); break; + case RevisionAction.hide: + title = appLocalizationsOf(context).fileWasHidden; + break; + case RevisionAction.unhide: + title = appLocalizationsOf(context).fileWasUnhidden; + break; default: title = appLocalizationsOf(context).fileWasModified; } @@ -1140,15 +1165,15 @@ class DetailsPanelToolbar extends StatelessWidget { const DetailsPanelToolbar({ super.key, required this.item, + required this.driveDetailLoadSuccess, }); final ArDriveDataTableItem item; + final DriveDetailLoadSuccess driveDetailLoadSuccess; @override Widget build(BuildContext context) { - final drive = - (context.read().state as DriveDetailLoadSuccess) - .currentDrive; + final drive = driveDetailLoadSuccess.currentDrive; return Container( padding: const EdgeInsets.symmetric(vertical: 12), @@ -1248,6 +1273,21 @@ class DetailsPanelToolbar extends StatelessWidget { promptToMove(context, driveId: drive.id, selectedItems: [item]); }, ), + if (item.isOwner) + _buildActionIcon( + tooltip: item.isHidden + ? appLocalizationsOf(context).unhide + : appLocalizationsOf(context).hide, + icon: item.isHidden + ? ArDriveIcons.eyeClosed(size: defaultIconSize) + : ArDriveIcons.eyeOpen(size: defaultIconSize), + onTap: () { + promptToToggleHideState( + context, + item: item, + ); + }, + ), const Spacer(), _buildActionIcon( tooltip: appLocalizationsOf(context).close, diff --git a/lib/components/fs_entry_move_form.dart b/lib/components/fs_entry_move_form.dart index 138421480e..a1f6fbd084 100644 --- a/lib/components/fs_entry_move_form.dart +++ b/lib/components/fs_entry_move_form.dart @@ -14,12 +14,12 @@ import '../pages/drive_detail/drive_detail_page.dart'; import 'components.dart'; Future promptToMove( - BuildContext context, { + BuildContext parentContext, { required String driveId, required List selectedItems, }) { return showArDriveDialog( - context, + parentContext, content: MultiBlocProvider( providers: [ BlocProvider( @@ -32,10 +32,11 @@ Future promptToMove( driveDao: context.read(), profileCubit: context.read(), syncCubit: context.read(), + driveDetailCubit: parentContext.read(), )..add(const FsEntryMoveInitial()), ), BlocProvider.value( - value: context.read(), + value: parentContext.read(), ) ], child: const FsEntryMoveForm(), @@ -63,286 +64,309 @@ class FsEntryMoveForm extends StatelessWidget { } }, builder: (context, state) { - return Builder(builder: (context) { - if (state is FsEntryMoveNameConflict) { - return ArDriveStandardModal( - title: appLocalizationsOf(context).nameConflict, - content: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Text( - appLocalizationsOf(context) - .itemMoveNameConflict(state.folderInView.name), + return BlocBuilder( + builder: (context, driveDetailState) { + return Builder(builder: (context) { + if (state is FsEntryMoveNameConflict) { + return ArDriveStandardModal( + title: appLocalizationsOf(context).nameConflict, + content: Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + appLocalizationsOf(context) + .itemMoveNameConflict(state.folderInView.name), + ), + for (final itemName in state.conflictingFileNames() + + state.conflictingFolderNames()) + Text(itemName), + ], ), - for (final itemName in state.conflictingFileNames() + - state.conflictingFolderNames()) - Text(itemName), - ], - ), - actions: [ - ModalAction( - action: () { - Navigator.pop(context); - }, - title: appLocalizationsOf(context).cancelEmphasized, - ), - if (!state.areAllItemsConflicting()) - ModalAction( - action: () { - context.read().add( - FsEntryMoveSkipConflicts( - folderInView: state.folderInView, - conflictingItems: state.conflictingItems, - ), - ); + actions: [ + ModalAction( + action: () { + Navigator.pop(context); + }, + title: appLocalizationsOf(context).cancelEmphasized, + ), + if (!state.areAllItemsConflicting()) + ModalAction( + action: () { + context.read().add( + FsEntryMoveSkipConflicts( + folderInView: state.folderInView, + conflictingItems: state.conflictingItems, + ), + ); + }, + title: appLocalizationsOf(context).skipEmphasized, + ), + ], + ); + } + if (state is FsEntryMoveLoadSuccess) { + final isShowingHiddenFiles = + (driveDetailState as DriveDetailLoadSuccess) + .isShowingHiddenFiles; + + final List subFolders; + if (isShowingHiddenFiles) { + subFolders = state.viewingFolder.subfolders; + } else { + subFolders = state.viewingFolder.subfolders + .where((f) => !f.isHidden) + .toList(); + } + + final items = [ + ...subFolders.map( + (f) { + final enabled = state.itemsToMove + .where((item) => item.id == f.id) + .isEmpty; + return Padding( + padding: const EdgeInsets.symmetric( + vertical: 16.0, horizontal: 16), + child: GestureDetector( + onTap: enabled + ? () { + context.read().add( + FsEntryMoveUpdateTargetFolder( + folderId: f.id, + ), + ); + } + : null, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + ArDriveIcons.folderOutline( + size: 16, + color: enabled ? null : _colorDisabled(context), + ), + const SizedBox(width: 8), + Expanded( + child: Text( + f.name, + style: + ArDriveTypography.body.inputNormalRegular( + color: enabled + ? null + : _colorDisabled(context), + ), + ), + ), + ArDriveIcons.carretRight( + size: 18, + color: enabled ? null : _colorDisabled(context), + ), + ], + ), + ), + ); }, - title: appLocalizationsOf(context).skipEmphasized, ), - ], - ); - } - if (state is FsEntryMoveLoadSuccess) { - final items = [ - ...state.viewingFolder.subfolders.map( - (f) { - final enabled = state.itemsToMove - .where((item) => item.id == f.id) - .isEmpty; - return Padding( - padding: const EdgeInsets.symmetric( - vertical: 16.0, horizontal: 16), - child: GestureDetector( - onTap: enabled - ? () { - context.read().add( - FsEntryMoveUpdateTargetFolder( - folderId: f.id, - ), - ); - } - : null, + ...state.viewingFolder.files.map( + (f) => Padding( + padding: const EdgeInsets.symmetric( + vertical: 16.0, + horizontal: 16, + ), child: Row( - mainAxisSize: MainAxisSize.min, children: [ - ArDriveIcons.folderOutline( + ArDriveIcons.fileOutlined( size: 16, - color: enabled ? null : _colorDisabled(context), + color: _colorDisabled(context), ), const SizedBox(width: 8), Expanded( child: Text( f.name, style: ArDriveTypography.body.inputNormalRegular( - color: enabled ? null : _colorDisabled(context), + color: _colorDisabled(context), ), ), ), - ArDriveIcons.carretRight( - size: 18, - color: enabled ? null : _colorDisabled(context), - ), ], ), ), - ); - }, - ), - ...state.viewingFolder.files.map( - (f) => Padding( - padding: const EdgeInsets.symmetric( - vertical: 16.0, - horizontal: 16, - ), - child: Row( - children: [ - ArDriveIcons.fileOutlined( - size: 16, - color: _colorDisabled(context), - ), - const SizedBox(width: 8), - Expanded( - child: Text( - f.name, - style: ArDriveTypography.body.inputNormalRegular( - color: _colorDisabled(context), - ), - ), - ), - ], ), - ), - ), - ]; + ]; - return Padding( - padding: const EdgeInsets.all(8.0), - child: ArDriveCard( - height: 441, - width: kMediumDialogWidth, - contentPadding: EdgeInsets.zero, - content: SizedBox( - height: 325, - 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 - .colors - .themeBgCanvas, - child: Row( - children: [ - AnimatedContainer( - width: !state.viewingRootFolder ? 20 : 0, - duration: const Duration(milliseconds: 200), - child: GestureDetector( - onTap: () { - context.read().add( - FsEntryMoveGoBackToParent( - folderInView: - state.viewingFolder.folder, - ), - ); - }, - child: AnimatedScale( + return Padding( + padding: const EdgeInsets.all(8.0), + child: ArDriveCard( + height: 441, + width: kMediumDialogWidth, + contentPadding: EdgeInsets.zero, + content: SizedBox( + height: 325, + 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 + .colors + .themeBgCanvas, + child: Row( + children: [ + AnimatedContainer( + width: !state.viewingRootFolder ? 20 : 0, duration: const Duration(milliseconds: 200), - scale: !state.viewingRootFolder ? 1 : 0, - child: ArDriveIcons.arrowLeft( - size: 32, + child: GestureDetector( + onTap: () { + context.read().add( + FsEntryMoveGoBackToParent( + folderInView: + state.viewingFolder.folder, + ), + ); + }, + 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: 8) - : const EdgeInsets.only(left: 0), - child: Text( - appLocalizationsOf(context).moveItems, - style: - ArDriveTypography.headline.headline5Bold(), - ), + AnimatedPadding( + duration: const Duration(milliseconds: 200), + padding: !state.viewingRootFolder + ? const EdgeInsets.only(left: 8) + : const EdgeInsets.only(left: 0), + child: Text( + appLocalizationsOf(context).moveItems, + style: ArDriveTypography.headline + .headline5Bold(), + ), + ), + const Spacer(), + GestureDetector( + onTap: () { + Navigator.pop(context); + }, + child: ArDriveIcons.x( + size: 24, + ), + ), + ], ), - const Spacer(), - GestureDetector( - onTap: () { - Navigator.pop(context); + ), + Expanded( + child: ListView.builder( + padding: EdgeInsets.zero, + itemCount: items.length, + itemBuilder: (context, index) { + return items[index]; }, - child: ArDriveIcons.x( - size: 24, - ), ), - ], - ), - ), - Expanded( - child: ListView.builder( - padding: EdgeInsets.zero, - itemCount: items.length, - itemBuilder: (context, index) { - return items[index]; - }, - ), - ), - const Divider(), - Container( - decoration: BoxDecoration( - color: ArDriveTheme.of(context) - .themeData - .colors - .themeBgSurface, - ), - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 16, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - ArDriveButton( - maxHeight: 36, - style: ArDriveButtonStyle.secondary, - backgroundColor: ArDriveTheme.of(context) - .themeData - .colors - .themeFgDefault, - fontStyle: - ArDriveTypography.body.buttonNormalRegular( - color: ArDriveTheme.of(context) + ), + const Divider(), + Container( + decoration: BoxDecoration( + color: ArDriveTheme.of(context) + .themeData + .colors + .themeBgSurface, + ), + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 16, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + ArDriveButton( + maxHeight: 36, + style: ArDriveButtonStyle.secondary, + backgroundColor: ArDriveTheme.of(context) + .themeData + .colors + .themeFgDefault, + fontStyle: ArDriveTypography.body + .buttonNormalRegular( + color: ArDriveTheme.of(context) + .themeData + .colors + .themeFgDefault, + ), + icon: ArDriveIcons.iconNewFolder1(), + text: appLocalizationsOf(context) + .createFolderEmphasized, + onPressed: () { + showArDriveDialog( + context, + content: BlocProvider( + create: (context) => + FolderCreateCubit( + driveId: state + .viewingFolder.folder.driveId, + parentFolderId: + state.viewingFolder.folder.id, + profileCubit: + context.read(), + arweave: + context.read(), + turboUploadService: context + .read(), + driveDao: context.read(), + ), + child: const FolderCreateForm(), + ), + ); + }), + ArDriveButton( + maxHeight: 36, + backgroundColor: ArDriveTheme.of(context) .themeData .colors .themeFgDefault, + fontStyle: ArDriveTypography.body + .buttonNormalRegular( + color: ArDriveTheme.of(context) + .themeData + .colors + .themeAccentSubtle, + ), + text: appLocalizationsOf(context) + .moveHereEmphasized, + onPressed: () { + context.read().add( + FsEntryMoveSubmit( + folderInView: + state.viewingFolder.folder, + ), + ); + context + .read() + .forceDisableMultiselect = true; + }, ), - icon: ArDriveIcons.iconNewFolder1(), - text: appLocalizationsOf(context) - .createFolderEmphasized, - onPressed: () { - showArDriveDialog( - context, - content: BlocProvider( - create: (context) => FolderCreateCubit( - driveId: - state.viewingFolder.folder.driveId, - parentFolderId: - state.viewingFolder.folder.id, - profileCubit: - context.read(), - arweave: context.read(), - turboUploadService: - context.read(), - driveDao: context.read(), - ), - child: const FolderCreateForm(), - ), - ); - }), - ArDriveButton( - maxHeight: 36, - backgroundColor: ArDriveTheme.of(context) - .themeData - .colors - .themeFgDefault, - fontStyle: - ArDriveTypography.body.buttonNormalRegular( - color: ArDriveTheme.of(context) - .themeData - .colors - .themeAccentSubtle, - ), - text: appLocalizationsOf(context) - .moveHereEmphasized, - onPressed: () { - context.read().add( - FsEntryMoveSubmit( - folderInView: - state.viewingFolder.folder, - ), - ); - context - .read() - .forceDisableMultiselect = true; - }, + ], ), - ], - ), - ) - ], + ) + ], + ), + ), ), - ), - ), - ); - } else { - return const SizedBox(); - } - }); + ); + } else { + return const SizedBox(); + } + }); + }, + ); }, ); } diff --git a/lib/components/hide_dialog.dart b/lib/components/hide_dialog.dart new file mode 100644 index 0000000000..a0a70b57ae --- /dev/null +++ b/lib/components/hide_dialog.dart @@ -0,0 +1,156 @@ +import 'package:ardrive/blocs/drive_detail/drive_detail_cubit.dart'; +import 'package:ardrive/blocs/hide/hide_bloc.dart'; +import 'package:ardrive/blocs/hide/hide_event.dart'; +import 'package:ardrive/blocs/hide/hide_state.dart'; +import 'package:ardrive/pages/drive_detail/drive_detail_page.dart'; +import 'package:ardrive/utils/app_localizations_wrapper.dart'; +import 'package:ardrive/utils/logger.dart'; +import 'package:ardrive_ui/ardrive_ui.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +Future promptToToggleHideState( + BuildContext context, { + required ArDriveDataTableItem item, +}) async { + final hideBloc = context.read(); + final driveDetailCubit = context.read(); + + final isHidden = item.isHidden; + + if (item is FileDataTableItem) { + if (isHidden) { + hideBloc.add(UnhideFileEvent( + driveId: item.driveId, + fileId: item.id, + )); + } else { + hideBloc.add(HideFileEvent( + driveId: item.driveId, + fileId: item.id, + )); + } + } else if (item is FolderDataTableItem) { + if (isHidden) { + hideBloc.add(UnhideFolderEvent( + driveId: item.driveId, + folderId: item.id, + )); + } else { + hideBloc.add(HideFolderEvent( + driveId: item.driveId, + folderId: item.id, + )); + } + } else { + throw UnimplementedError('Unknown item type: ${item.runtimeType}'); + } + + return showAnimatedDialog( + context, + barrierDismissible: false, + content: HideDialog(driveDetailCubit: driveDetailCubit), + ); +} + +class HideDialog extends StatelessWidget { + final DriveDetailCubit _driveDetailCubit; + + const HideDialog({ + super.key, + required DriveDetailCubit driveDetailCubit, + }) : _driveDetailCubit = driveDetailCubit; + + @override + Widget build(BuildContext context) { + return BlocConsumer( + listener: (context, state) { + if (state is SuccessHideState) { + Navigator.of(context).pop(); + logger.d('Successfully hid/unhid entity'); + _driveDetailCubit.refreshDriveDataTable(); + } else if (state is ConfirmingHideState) { + _driveDetailCubit.refreshDriveDataTable(); + context.read().add(const ConfirmUploadEvent()); + } + }, + builder: (context, state) { + return ArDriveStandardModal( + title: _buildTitle(context, state), + content: _buildContent(context, state), + actions: _buildActions(context, state), + ); + }, + ); + } + + String _buildTitle(BuildContext context, HideState state) { + final hideAction = state.hideAction; + if (state is FailureHideState) { + switch (hideAction) { + case HideAction.hideFile: + return appLocalizationsOf(context).failedToHideFile; + case HideAction.hideFolder: + return appLocalizationsOf(context).failedToHideFolder; + case HideAction.unhideFile: + return appLocalizationsOf(context).failedToUnhideFile; + case HideAction.unhideFolder: + return appLocalizationsOf(context).failedToUnhideFolder; + } + } + + switch (hideAction) { + case HideAction.hideFile: + return appLocalizationsOf(context).hidingFile; + case HideAction.hideFolder: + return appLocalizationsOf(context).hidingFolder; + case HideAction.unhideFile: + return appLocalizationsOf(context).unhidingFile; + case HideAction.unhideFolder: + return appLocalizationsOf(context).unhidingFolder; + } + } + + Widget _buildContent(BuildContext context, HideState state) { + if (state is FailureHideState) { + final hideAction = state.hideAction; + + switch (hideAction) { + case HideAction.hideFile: + return Text(appLocalizationsOf(context).failedToHideFile); + case HideAction.hideFolder: + return Text(appLocalizationsOf(context).failedToHideFolder); + case HideAction.unhideFile: + return Text(appLocalizationsOf(context).failedToUnhideFile); + case HideAction.unhideFolder: + return Text(appLocalizationsOf(context).failedToUnhideFolder); + } + } + + return const Column( + children: [ + Center( + child: CircularProgressIndicator(), + ), + ], + ); + } + + List? _buildActions( + BuildContext context, + HideState state, + ) { + if (state is FailureHideState) { + return [ + ModalAction( + action: () { + Navigator.of(context).pop(); + }, + title: appLocalizationsOf(context).close, + ), + ]; + } else { + return null; + } + } +} diff --git a/lib/components/payment_method_selector_widget.dart b/lib/components/payment_method_selector_widget.dart index cc188de72d..97ba90fd9b 100644 --- a/lib/components/payment_method_selector_widget.dart +++ b/lib/components/payment_method_selector_widget.dart @@ -1,37 +1,19 @@ +import 'package:ardrive/blocs/upload/models/payment_method_info.dart'; import 'package:ardrive/blocs/upload/upload_cubit.dart'; -import 'package:ardrive/core/upload/cost_calculator.dart'; import 'package:ardrive/turbo/topup/views/topup_modal.dart'; import 'package:ardrive_ui/ardrive_ui.dart'; import 'package:arweave/utils.dart'; import 'package:flutter/material.dart'; class PaymentMethodSelector extends StatelessWidget { - final UploadMethod uploadMethod; - final UploadCostEstimate? costEstimateTurbo; - final UploadCostEstimate costEstimateAr; - final bool hasNoTurboBalance; - final bool isTurboUploadPossible; - final String arBalance; - final bool sufficientArBalance; - final String turboCredits; - final bool sufficentCreditsBalance; - final bool isFreeThanksToTurbo; + final UploadPaymentMethodInfo uploadMethodInfo; final void Function() onTurboTopupSucess; final void Function() onArSelect; final void Function() onTurboSelect; const PaymentMethodSelector({ super.key, - required this.uploadMethod, - required this.costEstimateTurbo, - required this.costEstimateAr, - required this.hasNoTurboBalance, - required this.isTurboUploadPossible, - required this.arBalance, - required this.sufficientArBalance, - required this.turboCredits, - required this.sufficentCreditsBalance, - required this.isFreeThanksToTurbo, + required this.uploadMethodInfo, required this.onTurboTopupSucess, required this.onArSelect, required this.onTurboSelect, @@ -41,7 +23,7 @@ class PaymentMethodSelector extends StatelessWidget { Widget build(context) { return Column( children: [ - if (!isFreeThanksToTurbo) ...[ + if (!uploadMethodInfo.isFreeThanksToTurbo) ...[ _buildContent(context), const SizedBox(height: 16), _getInsufficientBalanceMessage(context: context), @@ -80,20 +62,22 @@ class PaymentMethodSelector extends StatelessWidget { options: [ // FIXME: rename to RadioButtonOption RadioButtonOptions( - value: uploadMethod == UploadMethod.ar, + value: uploadMethodInfo.uploadMethod == UploadMethod.ar, // TODO: Localization - text: 'Cost: ${winstonToAr(costEstimateAr.totalCost)} AR', + text: + 'Cost: ${winstonToAr(uploadMethodInfo.costEstimateAr.totalCost)} AR', textStyle: ArDriveTypography.body.buttonLargeBold(), ), - if (costEstimateTurbo != null && isTurboUploadPossible) + if (uploadMethodInfo.costEstimateTurbo != null && + uploadMethodInfo.isTurboUploadPossible) RadioButtonOptions( - value: uploadMethod == UploadMethod.turbo, + value: uploadMethodInfo.uploadMethod == UploadMethod.turbo, // TODO: Localization - text: hasNoTurboBalance + text: uploadMethodInfo.hasNoTurboBalance ? '' - : 'Cost: ${winstonToAr(costEstimateTurbo!.totalCost)} Credits', + : 'Cost: ${winstonToAr(uploadMethodInfo.costEstimateTurbo!.totalCost)} Credits', textStyle: ArDriveTypography.body.buttonLargeBold(), - content: hasNoTurboBalance + content: uploadMethodInfo.hasNoTurboBalance ? GestureDetector( onTap: () { showTurboTopupModal(context, onSuccess: () { @@ -146,8 +130,8 @@ class PaymentMethodSelector extends StatelessWidget { child: Text( index == 0 // TODO: localize - ? 'Wallet Balance: $arBalance AR' - : 'Turbo Balance: $turboCredits Credits', + ? 'Wallet Balance: ${uploadMethodInfo.arBalance} AR' + : 'Turbo Balance: ${uploadMethodInfo.turboCredits} Credits', style: ArDriveTypography.body.buttonNormalBold( color: ArDriveTheme.of(context).themeData.colors.themeFgMuted, @@ -164,9 +148,9 @@ class PaymentMethodSelector extends StatelessWidget { Widget _getInsufficientBalanceMessage({ required BuildContext context, }) { - if (uploadMethod == UploadMethod.turbo && - !sufficentCreditsBalance && - sufficientArBalance) { + if (uploadMethodInfo.uploadMethod == UploadMethod.turbo && + !uploadMethodInfo.sufficentCreditsBalance && + uploadMethodInfo.sufficientArBalance) { return GestureDetector( onTap: () { showTurboTopupModal(context, onSuccess: () { @@ -207,14 +191,16 @@ class PaymentMethodSelector extends StatelessWidget { ), ), ); - } else if (uploadMethod == UploadMethod.ar && !sufficientArBalance) { + } else if (uploadMethodInfo.uploadMethod == UploadMethod.ar && + !uploadMethodInfo.sufficientArBalance) { return Text( 'Insufficient AR balance for purchase.', style: ArDriveTypography.body.captionBold( color: ArDriveTheme.of(context).themeData.colors.themeErrorDefault, ), ); - } else if (!sufficentCreditsBalance && !sufficientArBalance) { + } else if (!uploadMethodInfo.sufficentCreditsBalance && + !uploadMethodInfo.sufficientArBalance) { return GestureDetector( onTap: () { showTurboTopupModal(context, onSuccess: () { diff --git a/lib/components/upload_form.dart b/lib/components/upload_form.dart index 6819f80551..aba33b8b04 100644 --- a/lib/components/upload_form.dart +++ b/lib/components/upload_form.dart @@ -6,10 +6,12 @@ import 'package:ardrive/blocs/feedback_survey/feedback_survey_cubit.dart'; import 'package:ardrive/blocs/upload/enums/conflicting_files_actions.dart'; import 'package:ardrive/blocs/upload/limits.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/blocs/upload/upload_file_checker.dart'; import 'package:ardrive/blocs/upload/upload_handles/file_v2_upload_handle.dart'; +import 'package:ardrive/blocs/upload/upload_handles/upload_handle.dart'; import 'package:ardrive/components/file_picker_modal.dart'; -import 'package:ardrive/components/payment_method_selector_widget.dart'; import 'package:ardrive/core/activity_tracker.dart'; import 'package:ardrive/core/arfs/entities/arfs_entities.dart'; import 'package:ardrive/core/crypto/crypto.dart'; @@ -100,58 +102,73 @@ Future promptToUpload( context, () => showArDriveDialog( context, - content: BlocProvider( - create: (context) => UploadCubit( - activityTracker: context.read(), - folder: ioFolder, - arDriveUploadManager: 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( + content: 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(), ), - uploadCostEstimateCalculatorForAR: - UploadCostEstimateCalculatorForAR( - arweaveService: context.read(), - pstService: context.read(), - arCostToUsd: ConvertArToUSD( - arweave: context.read(), - ), + turboCostCalculator: TurboCostCalculator( + paymentService: context.read(), ), ), - uploadPreparer: UploadPreparer( - uploadPlanUtils: UploadPlanUtils( - crypto: ArDriveCrypto(), + uploadCostEstimateCalculatorForAR: + UploadCostEstimateCalculatorForAR( + arweaveService: context.read(), + pstService: context.read(), + arCostToUsd: ConvertArToUSD( arweave: context.read(), - turboUploadService: context.read(), - driveDao: context.read(), ), ), ), - uploadFileChecker: context.read(), - driveId: driveId, - parentFolderId: parentFolderId, - files: selectedFiles, - profileCubit: context.read(), - pst: context.read(), - driveDao: context.read(), - uploadFolders: isFolderUpload, - auth: context.read(), - )..startUploadPreparation(), - child: const UploadForm(), + uploadPreparer: UploadPreparer( + uploadPlanUtils: UploadPlanUtils( + crypto: ArDriveCrypto(), + arweave: context.read(), + turboUploadService: context.read(), + driveDao: context.read(), + ), + ), + ), + child: MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => UploadCubit( + activityTracker: context.read(), + folder: ioFolder, + arDriveUploadManager: + context.read(), + uploadFileChecker: context.read(), + driveId: driveId, + parentFolderId: parentFolderId, + files: selectedFiles, + profileCubit: context.read(), + pst: context.read(), + driveDao: context.read(), + uploadFolders: isFolderUpload, + auth: context.read(), + )..startUploadPreparation(), + ), + BlocProvider( + create: (context) => UploadPaymentMethodBloc( + context.read(), + context.read(), + context.read(), + ), + ), + ], + child: const UploadForm(), + ), ), barrierDismissible: false, ), @@ -175,75 +192,48 @@ class _UploadFormState extends State { } @override - Widget build(BuildContext context) => BlocConsumer( - listener: (context, state) async { - if (state is UploadComplete || state is UploadWalletMismatch) { - if (!_isShowingCancelDialog) { - Navigator.pop(context); - context.read().openRemindMe(); - context.read().setUploading(false); - context.read().startSync(); - } - } else if (state is UploadPreparationInitialized) { - context.read().verifyFilesAboveWarningLimit(); - } - if (state is UploadWalletMismatch) { - Navigator.pop(context); - context.read().logoutProfile(); + Widget build(BuildContext context) => + BlocListener( + listener: (context, state) { + if (state is UploadPaymentMethodLoaded) { + context.read().setUploadMethod( + state.paymentMethodInfo.uploadMethod, + state.paymentMethodInfo, + state.canUpload, + ); + } else if (state is UploadPaymentMethodError) { + context.read().emitErrorFromPreparation(); } }, - buildWhen: (previous, current) => current is! UploadComplete, - builder: (context, state) { - if (state is UploadFolderNameConflict) { - return ArDriveStandardModal( - title: appLocalizationsOf(context).duplicateFolders( - state.conflictingFileNames.length, - ), - content: SizedBox( - width: kMediumDialogWidth, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - appLocalizationsOf(context) - .foldersWithTheSameNameAlreadyExists( - state.conflictingFileNames.length, - ), - style: ArDriveTypography.body.buttonNormalRegular(), - ), - 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(', \n'), - style: ArDriveTypography.body.buttonNormalRegular(), - ), - ), - ), - ], - ), - ), - actions: [ - if (!state.areAllFilesConflicting) - ModalAction( - action: () => - context.read().checkConflictingFiles(), - title: appLocalizationsOf(context).skipEmphasized, - ), - ModalAction( - action: () => Navigator.of(context).pop(false), - title: appLocalizationsOf(context).cancelEmphasized, + child: BlocConsumer( + listener: (context, state) async { + if (state is UploadComplete || state is UploadWalletMismatch) { + if (!_isShowingCancelDialog) { + Navigator.pop(context); + context.read().openRemindMe(); + context.read().setUploading(false); + context.read().startSync(); + } + } else if (state is UploadPreparationInitialized) { + context.read().verifyFilesAboveWarningLimit(); + } + if (state is UploadWalletMismatch) { + Navigator.pop(context); + context.read().logoutProfile(); + } else if (state is UploadReadyToPrepare) { + context + .read() + .add(PrepareUploadPaymentMethod(params: state.params)); + } + }, + buildWhen: (previous, current) => + (current is! UploadComplete && current is! UploadReadyToPrepare), + builder: (context, state) { + if (state is UploadFolderNameConflict) { + return ArDriveStandardModal( + title: appLocalizationsOf(context).duplicateFolders( + state.conflictingFileNames.length, ), - ], - ); - } else if (state is UploadFileConflict) { - return ArDriveStandardModal( - title: appLocalizationsOf(context) - .duplicateFiles(state.conflictingFileNames.length), content: SizedBox( width: kMediumDialogWidth, child: Column( @@ -252,16 +242,13 @@ class _UploadFormState extends State { children: [ Text( appLocalizationsOf(context) - .filesWithTheSameNameAlreadyExists( + .foldersWithTheSameNameAlreadyExists( state.conflictingFileNames.length, ), style: ArDriveTypography.body.buttonNormalRegular(), ), const SizedBox(height: 16), - Text( - appLocalizationsOf(context).conflictingFiles, - style: ArDriveTypography.body.buttonNormalRegular(), - ), + Text(appLocalizationsOf(context).conflictingFiles), const SizedBox(height: 8), ConstrainedBox( constraints: const BoxConstraints(maxHeight: 320), @@ -278,483 +265,538 @@ class _UploadFormState extends State { actions: [ if (!state.areAllFilesConflicting) ModalAction( - action: () => context - .read() - .prepareUploadPlanAndCostEstimates( - uploadAction: UploadActions.skip), + action: () => + context.read().checkConflictingFiles(), title: appLocalizationsOf(context).skipEmphasized, ), ModalAction( action: () => Navigator.of(context).pop(false), title: appLocalizationsOf(context).cancelEmphasized, ), - ModalAction( - action: () => context - .read() - .prepareUploadPlanAndCostEstimates( - uploadAction: UploadActions.replace), - title: appLocalizationsOf(context).replaceEmphasized, - ), - ]); - } else if (state is UploadFileTooLarge) { - return ArDriveStandardModal( - title: appLocalizationsOf(context) - .filesTooLarge(state.tooLargeFileNames.length), - content: SizedBox( - width: kMediumDialogWidth, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - kIsWeb - ? (state.isPrivate - ? appLocalizationsOf(context) - .filesTooLargeExplanationPrivate - : appLocalizationsOf(context) - .filesTooLargeExplanationPublic) - : appLocalizationsOf(context) - .filesTooLargeExplanationMobile, - style: ArDriveTypography.body.buttonNormalRegular(), + ], + ); + } else if (state is UploadFileConflict) { + return ArDriveStandardModal( + title: appLocalizationsOf(context) + .duplicateFiles(state.conflictingFileNames.length), + content: SizedBox( + width: kMediumDialogWidth, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + appLocalizationsOf(context) + .filesWithTheSameNameAlreadyExists( + state.conflictingFileNames.length, + ), + style: ArDriveTypography.body.buttonNormalRegular(), + ), + const SizedBox(height: 16), + Text( + appLocalizationsOf(context).conflictingFiles, + style: ArDriveTypography.body.buttonNormalRegular(), + ), + const SizedBox(height: 8), + ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 320), + child: SingleChildScrollView( + child: Text( + state.conflictingFileNames.join(', \n'), + style: + ArDriveTypography.body.buttonNormalRegular(), + ), + ), + ), + ], ), - const SizedBox(height: 16), - Text( - appLocalizationsOf(context).tooLargeForUpload, - style: ArDriveTypography.body.buttonNormalRegular(), + ), + actions: [ + if (!state.areAllFilesConflicting) + ModalAction( + action: () => context + .read() + .prepareUploadPlanAndCostEstimates( + uploadAction: UploadActions.skip), + title: appLocalizationsOf(context).skipEmphasized, + ), + ModalAction( + action: () => Navigator.of(context).pop(false), + title: appLocalizationsOf(context).cancelEmphasized, ), - const SizedBox(height: 8), - Text( - state.tooLargeFileNames.join(', '), - style: ArDriveTypography.body.buttonNormalRegular(), + ModalAction( + action: () => context + .read() + .prepareUploadPlanAndCostEstimates( + uploadAction: UploadActions.replace), + title: appLocalizationsOf(context).replaceEmphasized, ), - ], - ), - ), - actions: [ - ModalAction( - action: () => Navigator.of(context).pop(false), - title: appLocalizationsOf(context).cancelEmphasized, + ]); + } else if (state is UploadFileTooLarge) { + return ArDriveStandardModal( + title: appLocalizationsOf(context) + .filesTooLarge(state.tooLargeFileNames.length), + content: SizedBox( + width: kMediumDialogWidth, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + kIsWeb + ? (state.isPrivate + ? appLocalizationsOf(context) + .filesTooLargeExplanationPrivate + : appLocalizationsOf(context) + .filesTooLargeExplanationPublic) + : appLocalizationsOf(context) + .filesTooLargeExplanationMobile, + style: ArDriveTypography.body.buttonNormalRegular(), + ), + const SizedBox(height: 16), + Text( + appLocalizationsOf(context).tooLargeForUpload, + style: ArDriveTypography.body.buttonNormalRegular(), + ), + const SizedBox(height: 8), + Text( + state.tooLargeFileNames.join(', '), + style: ArDriveTypography.body.buttonNormalRegular(), + ), + ], + ), ), - if (state.hasFilesToUpload) + actions: [ ModalAction( - action: () => context - .read() - .skipLargeFilesAndCheckForConflicts(), - title: appLocalizationsOf(context).skipEmphasized, + action: () => Navigator.of(context).pop(false), + title: appLocalizationsOf(context).cancelEmphasized, + ), + if (state.hasFilesToUpload) + ModalAction( + action: () => context + .read() + .skipLargeFilesAndCheckForConflicts(), + title: appLocalizationsOf(context).skipEmphasized, + ), + ], + ); + } else if (state is UploadPreparationInProgress || + state is UploadPreparationInitialized) { + return ArDriveStandardModal( + title: appLocalizationsOf(context).preparingUpload, + content: SizedBox( + width: kMediumDialogWidth, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 16), + if (state is UploadPreparationInProgress && + state.isArConnect) + Text( + appLocalizationsOf(context).arConnectRemainOnThisTab, + style: ArDriveTypography.body.buttonNormalBold(), + ) + else + Text( + appLocalizationsOf(context).thisMayTakeAWhile, + style: ArDriveTypography.body.buttonNormalBold(), + ) + ], ), - ], - ); - } else if (state is UploadPreparationInProgress || - state is UploadPreparationInitialized) { - return ArDriveStandardModal( - title: appLocalizationsOf(context).preparingUpload, - content: SizedBox( - width: kMediumDialogWidth, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const CircularProgressIndicator(), - const SizedBox(height: 16), - if (state is UploadPreparationInProgress && - state.isArConnect) - Text( - appLocalizationsOf(context).arConnectRemainOnThisTab, - style: ArDriveTypography.body.buttonNormalBold(), - ) - else - Text( - appLocalizationsOf(context).thisMayTakeAWhile, - style: ArDriveTypography.body.buttonNormalBold(), - ) - ], ), - ), - ); - } else if (state is UploadReady) { - final numberOfFilesInBundles = - state.uploadPlanForAR.bundleUploadHandles.isNotEmpty - ? state.uploadPlanForAR.bundleUploadHandles - .map((e) => e.numberOfFiles) - .reduce((value, element) => value += element) - : 0; - final numberOfV2Files = - state.uploadPlanForAR.fileV2UploadHandles.length; + ); + } else if (state is UploadReady) { + int numberOfFilesInBundles = state.numberOfFiles; - logger.d( - 'is button to upload enabled: ${state.isButtonToUploadEnabled}'); + logger.d( + ' is button to upload enabled: ${state.isButtonToUploadEnabled}', + ); - final v2Files = state.uploadPlanForAR.fileV2UploadHandles.values - .map((e) => e) - .toList(); + final v2Files = state + .paymentInfo.uploadPlanForAR?.fileV2UploadHandles.values + .map((e) => e) + .toList(); - final bundles = state.uploadPlanForAR.bundleUploadHandles.toList(); + final bundles = state + .paymentInfo.uploadPlanForAR?.bundleUploadHandles + .toList(); - final files = [...v2Files, ...bundles]; + List? files; - PlausibleEventTracker.trackUploadReview( - drivePrivacy: state.uploadIsPublic - ? DrivePrivacy.public - : DrivePrivacy.private, - dragNDrop: state.isDragNDrop, - ); + if (v2Files != null) { + files = []; - return ArDriveStandardModal( - width: 408, - title: appLocalizationsOf(context) - .uploadNFiles(numberOfFilesInBundles + numberOfV2Files), - content: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Align( - alignment: Alignment.topCenter, - child: ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 256), - child: ArDriveScrollBar( - controller: _scrollController, - alwaysVisible: true, - child: ListView.builder( - padding: const EdgeInsets.only(top: 0), - controller: _scrollController, - shrinkWrap: true, - itemCount: files.length, - itemBuilder: (BuildContext context, int index) { - final file = files[index]; - if (file is FileV2UploadHandle) { - return Row( - children: [ - Flexible( - child: Text( - '${file.entity.name!} ', - style: ArDriveTypography.body.smallBold( - color: ArDriveTheme.of(context) - .themeData - .colors - .themeFgSubtle, - ), - ), - ), - Text( - filesize(file.size), - style: - ArDriveTypography.body.smallRegular( - color: ArDriveTheme.of(context) - .themeData - .colors - .themeFgMuted, - ), - ), - ], - ); - } else { - final bundle = file as BundleUploadHandle; + files.addAll(v2Files); + } + + if (bundles != null) { + files ??= []; - return ListView( - padding: EdgeInsets.zero, + files.addAll(bundles); + } + + PlausibleEventTracker.trackUploadReview( + drivePrivacy: state.uploadIsPublic + ? DrivePrivacy.public + : DrivePrivacy.private, + dragNDrop: state.isDragNDrop, + ); + + return ArDriveStandardModal( + width: 408, + title: appLocalizationsOf(context) + .uploadNFiles(numberOfFilesInBundles), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + files == null + ? const Center(child: CircularProgressIndicator()) + : Align( + alignment: Alignment.topCenter, + child: ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 256), + child: ArDriveScrollBar( + controller: _scrollController, + alwaysVisible: true, + child: ListView.builder( + padding: const EdgeInsets.only(top: 0), + controller: _scrollController, shrinkWrap: true, - children: bundle.fileEntities.map((e) { - return Row( - children: [ - Flexible( - child: Text( - '${e.name!} ', + itemCount: files.length, + itemBuilder: + (BuildContext context, int index) { + final file = files![index]; + if (file is FileV2UploadHandle) { + return Row( + children: [ + Flexible( + child: Text( + '${file.entity.name!} ', + style: ArDriveTypography.body + .smallBold( + color: + ArDriveTheme.of(context) + .themeData + .colors + .themeFgSubtle, + ), + ), + ), + Text( + filesize(file.size), style: ArDriveTypography.body - .smallBold( + .smallRegular( color: ArDriveTheme.of(context) .themeData .colors - .themeFgSubtle, + .themeFgMuted, ), ), - ), - Text( - filesize(e.size), - style: ArDriveTypography.body - .smallRegular( - color: ArDriveTheme.of(context) - .themeData - .colors - .themeFgMuted, - ), - ), - ], - ); - }).toList()); - } - }, - )), - ), - ), - const SizedBox(height: 8), - RichText( - text: TextSpan( - children: [ - TextSpan( - text: 'Size: ', - style: ArDriveTypography.body.buttonNormalRegular( - color: ArDriveTheme.of(context) - .themeData - .colors - .themeFgOnDisabled, + ], + ); + } else { + final bundle = + file as BundleUploadHandle; + + return ListView( + padding: EdgeInsets.zero, + shrinkWrap: true, + children: + bundle.fileEntities.map((e) { + return Row( + children: [ + Flexible( + child: Text( + '${e.name!} ', + style: ArDriveTypography + .body + .smallBold( + color: ArDriveTheme.of( + context) + .themeData + .colors + .themeFgSubtle, + ), + ), + ), + Text( + filesize(e.size), + style: ArDriveTypography + .body + .smallRegular( + color: ArDriveTheme.of( + context) + .themeData + .colors + .themeFgMuted, + ), + ), + ], + ); + }).toList()); + } + }, + )), + ), ), - ), - TextSpan( - text: filesize( - state.uploadSize, + const SizedBox(height: 8), + RichText( + text: TextSpan( + children: [ + TextSpan( + text: 'Size: ', + style: ArDriveTypography.body.buttonNormalRegular( + color: ArDriveTheme.of(context) + .themeData + .colors + .themeFgOnDisabled, + ), ), - style: ArDriveTypography.body - .buttonNormalBold( - color: ArDriveTheme.of(context) - .themeData - .colors - .themeFgDefault) - .copyWith(fontWeight: FontWeight.bold), - ), - ], - ), - ), - Text.rich( - TextSpan( - children: [ - if (state.isFreeThanksToTurbo) ...[ TextSpan( - text: appLocalizationsOf(context) - .freeTurboTransaction, - style: ArDriveTypography.body.buttonNormalRegular(), + text: filesize( + state.paymentInfo.totalSize, + ), + style: ArDriveTypography.body + .buttonNormalBold( + color: ArDriveTheme.of(context) + .themeData + .colors + .themeFgDefault) + .copyWith(fontWeight: FontWeight.bold), ), - ] - ], - style: ArDriveTypography.body.buttonNormalRegular(), + ], + ), ), - ), - const Divider( - height: 20, - ), - if (state.uploadIsPublic) ...{ - Text( - appLocalizationsOf(context).filesWillBeUploadedPublicly( - numberOfFilesInBundles + numberOfV2Files, + Text.rich( + TextSpan( + children: [ + if (state.paymentInfo.isFreeThanksToTurbo) ...[ + TextSpan( + text: appLocalizationsOf(context) + .freeTurboTransaction, + style: + ArDriveTypography.body.buttonNormalRegular(), + ), + ] + ], + style: ArDriveTypography.body.buttonNormalRegular(), ), - style: ArDriveTypography.body.buttonNormalRegular(), ), - const SizedBox( - height: 8, + const Divider( + height: 20, ), - }, - PaymentMethodSelector( - uploadMethod: state.uploadMethod, - costEstimateTurbo: state.costEstimateTurbo, - costEstimateAr: state.costEstimateAr, - hasNoTurboBalance: state.isZeroBalance, - isTurboUploadPossible: state.isTurboUploadPossible, - arBalance: state.arBalance, - sufficientArBalance: state.sufficientArBalance, - turboCredits: state.turboCredits, - sufficentCreditsBalance: state.sufficentCreditsBalance, - isFreeThanksToTurbo: state.isFreeThanksToTurbo, - onArSelect: () { - context - .read() - .setUploadMethod(UploadMethod.ar); - }, - onTurboSelect: () { - context - .read() - .setUploadMethod(UploadMethod.turbo); + if (state.uploadIsPublic) ...{ + Text( + appLocalizationsOf(context).filesWillBeUploadedPublicly( + state.numberOfFiles, + ), + style: ArDriveTypography.body.buttonNormalRegular(), + ), + const SizedBox( + height: 8, + ), }, - onTurboTopupSucess: () { - context.read().startUploadPreparation( - isRetryingToPayWithTurbo: true, + RepositoryProvider.value( + value: context.read(), + child: UploadPaymentMethodView( + onError: () { + context + .read() + .emitErrorFromPreparation(); + }, + onTurboTopupSucess: () { + context.read().startUploadPreparation( + isRetryingToPayWithTurbo: true, + ); + }, + onUploadMethodChanged: (method, info, canUpload) { + context + .read() + .setUploadMethod(method, info, canUpload); + }, + params: state.params, + ), + ), + ], + ), + actions: [ + ModalAction( + action: () => Navigator.of(context).pop(false), + title: appLocalizationsOf(context).cancelEmphasized, + ), + ModalAction( + isEnable: state.isButtonToUploadEnabled, + action: () { + context.read().startUpload( + uploadPlanForAr: state.paymentInfo.uploadPlanForAR!, + uploadPlanForTurbo: + state.paymentInfo.uploadPlanForTurbo, ); }, + title: appLocalizationsOf(context).uploadEmphasized, ), ], - ), - actions: [ - ModalAction( - action: () => Navigator.of(context).pop(false), - title: appLocalizationsOf(context).cancelEmphasized, - ), - ModalAction( - isEnable: state.isButtonToUploadEnabled, - action: () { - context.read().startUpload( - uploadPlanForAr: state.uploadPlanForAR, - uploadPlanForTurbo: state.uploadPlanForTurbo, - ); - }, - title: appLocalizationsOf(context).uploadEmphasized, - ), - ], - ); - } else if (state is UploadSigningInProgress) { - return ArDriveStandardModal( - title: state.uploadPlan.bundleUploadHandles.isNotEmpty - ? appLocalizationsOf(context).bundlingAndSigningUpload - : appLocalizationsOf(context).signingUpload, - content: SizedBox( - width: kMediumDialogWidth, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const CircularProgressIndicator(), - const SizedBox(height: 16), - if (state.isArConnect) - Text( - appLocalizationsOf(context).arConnectRemainOnThisTab, - style: ArDriveTypography.body.buttonNormalRegular(), - ) - else - Text(appLocalizationsOf(context).thisMayTakeAWhile, - style: ArDriveTypography.body.buttonNormalRegular()), - ], + ); + } else if (state is UploadSigningInProgress) { + return ArDriveStandardModal( + title: state.uploadPlan.bundleUploadHandles.isNotEmpty + ? appLocalizationsOf(context).bundlingAndSigningUpload + : appLocalizationsOf(context).signingUpload, + content: SizedBox( + width: kMediumDialogWidth, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 16), + if (state.isArConnect) + Text( + appLocalizationsOf(context).arConnectRemainOnThisTab, + style: ArDriveTypography.body.buttonNormalRegular(), + ) + else + Text(appLocalizationsOf(context).thisMayTakeAWhile, + style: + ArDriveTypography.body.buttonNormalRegular()), + ], + ), ), - ), - ); - } else if (state is UploadInProgressUsingNewUploader) { - return _uploadUsingNewUploader(state: state); - } else if (state is UploadInProgress) { - final numberOfFilesInBundles = - state.uploadPlan.bundleUploadHandles.isNotEmpty - ? state.uploadPlan.bundleUploadHandles - .map((e) => e.numberOfFiles) - .reduce((value, element) => value += element) - : 0; - final numberOfV2Files = state.uploadPlan.fileV2UploadHandles.length; + ); + } else if (state is UploadInProgressUsingNewUploader) { + return _uploadUsingNewUploader(state: state); + } else if (state is UploadInProgress) { + final numberOfFilesInBundles = + state.uploadPlan.bundleUploadHandles.isNotEmpty + ? state.uploadPlan.bundleUploadHandles + .map((e) => e.numberOfFiles) + .reduce((value, element) => value += element) + : 0; + final numberOfV2Files = + state.uploadPlan.fileV2UploadHandles.length; - final v2Files = - state.uploadPlan.fileV2UploadHandles.values.toList(); - final bundles = state.uploadPlan.bundleUploadHandles.toList(); - final files = [...v2Files, ...bundles]; + final v2Files = + state.uploadPlan.fileV2UploadHandles.values.toList(); + final bundles = state.uploadPlan.bundleUploadHandles.toList(); + final files = [...v2Files, ...bundles]; - return ArDriveStandardModal( - title: - '${appLocalizationsOf(context).uploadingNFiles(numberOfFilesInBundles + numberOfV2Files)} ${(state.progress * 100).toStringAsFixed(2)}%', - content: SizedBox( - width: kMediumDialogWidth, - child: ConstrainedBox( - constraints: const BoxConstraints(maxHeight: 256), - child: Scrollbar( - child: ListView.builder( - shrinkWrap: true, - itemCount: files.length, - itemBuilder: (BuildContext context, int index) { - if (files[index] is FileV2UploadHandle) { - final file = files[index] as FileV2UploadHandle; - return Column( - children: [ - ListTile( - contentPadding: EdgeInsets.zero, - title: Row( - children: [ - Text( - file.entity.name!, - style: ArDriveTypography.body - .buttonNormalBold( - color: ArDriveTheme.of(context) - .themeData - .colors - .themeFgDefault, + return ArDriveStandardModal( + title: + '${appLocalizationsOf(context).uploadingNFiles(numberOfFilesInBundles + numberOfV2Files)} ${(state.progress * 100).toStringAsFixed(2)}%', + content: SizedBox( + width: kMediumDialogWidth, + child: ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 256), + child: Scrollbar( + child: ListView.builder( + shrinkWrap: true, + itemCount: files.length, + itemBuilder: (BuildContext context, int index) { + if (files[index] is FileV2UploadHandle) { + final file = files[index] as FileV2UploadHandle; + return Column( + children: [ + ListTile( + contentPadding: EdgeInsets.zero, + title: Row( + children: [ + Text( + file.entity.name!, + style: ArDriveTypography.body + .buttonNormalBold( + color: ArDriveTheme.of(context) + .themeData + .colors + .themeFgDefault, + ), ), - ), - Text( - filesize(file.entity.size), - style: ArDriveTypography.body - .buttonNormalBold( - color: ArDriveTheme.of(context) - .themeData - .colors - .themeFgDefault, + Text( + filesize(file.entity.size), + style: ArDriveTypography.body + .buttonNormalBold( + color: ArDriveTheme.of(context) + .themeData + .colors + .themeFgDefault, + ), ), + ], + ), + subtitle: Text( + '${filesize(file.uploadedSize)}/${filesize(file.size)}', + style: ArDriveTypography.body + .buttonNormalRegular( + color: ArDriveTheme.of(context) + .themeData + .colors + .themeFgOnDisabled, ), - ], - ), - subtitle: Text( - '${filesize(file.uploadedSize)}/${filesize(file.size)}', - style: ArDriveTypography.body - .buttonNormalRegular( - color: ArDriveTheme.of(context) - .themeData - .colors - .themeFgOnDisabled, ), ), - ), - ], - ); - } else { - final file = files[index] as BundleUploadHandle; - return Column( - children: [ - ListTile( - contentPadding: EdgeInsets.zero, - title: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - for (var fileEntity in file.fileEntities) - Row( - children: [ - Text( - fileEntity.name!, - style: ArDriveTypography.body - .buttonNormalBold( - color: ArDriveTheme.of(context) - .themeData - .colors - .themeFgDefault, + ], + ); + } else { + final file = files[index] as BundleUploadHandle; + return Column( + children: [ + ListTile( + contentPadding: EdgeInsets.zero, + title: Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + for (var fileEntity in file.fileEntities) + Row( + children: [ + Text( + fileEntity.name!, + style: ArDriveTypography.body + .buttonNormalBold( + color: ArDriveTheme.of(context) + .themeData + .colors + .themeFgDefault, + ), ), - ), - Text( - filesize(fileEntity.size), - style: ArDriveTypography.body - .buttonNormalBold( - color: ArDriveTheme.of(context) - .themeData - .colors - .themeFgDefault, + Text( + filesize(fileEntity.size), + style: ArDriveTypography.body + .buttonNormalBold( + color: ArDriveTheme.of(context) + .themeData + .colors + .themeFgDefault, + ), ), - ), - ], - ), - ], - ), - subtitle: Text( - '${filesize(file.uploadedSize)}/${filesize(file.size)}', - style: ArDriveTypography.body - .buttonNormalRegular( - color: ArDriveTheme.of(context) - .themeData - .colors - .themeFgOnDisabled, + ], + ), + ], + ), + subtitle: Text( + '${filesize(file.uploadedSize)}/${filesize(file.size)}', + style: ArDriveTypography.body + .buttonNormalRegular( + color: ArDriveTheme.of(context) + .themeData + .colors + .themeFgOnDisabled, + ), ), ), - ), - ], - ); - } - }, + ], + ); + } + }, + ), ), ), ), - ), - ); - } else if (state is UploadCanceled) { - return ArDriveStandardModal( - title: 'Upload canceled', - description: 'Your upload was canceled', - actions: [ - ModalAction( - action: () => Navigator.of(context).pop(false), - title: appLocalizationsOf(context).okEmphasized, - ), - ], - ); - } else if (state is UploadFailure) { - if (state.error == UploadErrors.turboTimeout) { + ); + } else if (state is UploadCanceled) { return ArDriveStandardModal( - title: appLocalizationsOf(context).uploadFailed, - description: - appLocalizationsOf(context).yourUploadFailedTurboTimeout, + title: 'Upload canceled', + description: 'Your upload was canceled', actions: [ ModalAction( action: () => Navigator.of(context).pop(false), @@ -762,79 +804,94 @@ class _UploadFormState extends State { ), ], ); - } + } else if (state is UploadFailure) { + logger.e('Upload failed: ${state.error}'); + if (state.error == UploadErrors.turboTimeout) { + return ArDriveStandardModal( + title: appLocalizationsOf(context).uploadFailed, + description: + appLocalizationsOf(context).yourUploadFailedTurboTimeout, + actions: [ + ModalAction( + action: () => Navigator.of(context).pop(false), + title: appLocalizationsOf(context).okEmphasized, + ), + ], + ); + } - return ArDriveStandardModal( - width: kLargeDialogWidth, - title: 'Problem with Upload', - description: appLocalizationsOf(context).yourUploadFailed, - content: state.failedTasks != null - ? _failedUploadList(state.failedTasks!) - : null, - actions: state.failedTasks == null - ? null - : [ - ModalAction( - action: () => Navigator.of(context).pop(false), - title: 'Do Not Fix', - ), - ModalAction( - action: () { - context.read().retryUploads(); - }, - title: 'Re-Upload', - ), - ], - ); - } else if (state is UploadShowingWarning) { - return ArDriveStandardModal( - title: appLocalizationsOf(context).warningEmphasized, - content: SizedBox( - width: kMediumDialogWidth, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - appLocalizationsOf(context) - .weDontRecommendUploadsAboveASafeLimit( - filesize( - state.reason == UploadWarningReason.fileTooLarge - ? publicFileSafeSizeLimit - : nonChromeBrowserUploadSafeLimitUsingTurbo, + return ArDriveStandardModal( + width: kLargeDialogWidth, + title: 'Problem with Upload', + description: appLocalizationsOf(context).yourUploadFailed, + content: state.failedTasks != null + ? _failedUploadList(state.failedTasks!) + : null, + actions: state.failedTasks == null + ? null + : [ + ModalAction( + action: () => Navigator.of(context).pop(false), + title: 'Do Not Fix', + ), + ModalAction( + action: () { + context.read().retryUploads(); + }, + title: 'Re-Upload', + ), + ], + ); + } else if (state is UploadShowingWarning) { + return ArDriveStandardModal( + title: appLocalizationsOf(context).warningEmphasized, + content: SizedBox( + width: kMediumDialogWidth, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + appLocalizationsOf(context) + .weDontRecommendUploadsAboveASafeLimit( + filesize( + state.reason == UploadWarningReason.fileTooLarge + ? publicFileSafeSizeLimit + : nonChromeBrowserUploadSafeLimitUsingTurbo, + ), ), + style: ArDriveTypography.body.buttonNormalRegular(), ), - style: ArDriveTypography.body.buttonNormalRegular(), - ), - ], - ), - ), - actions: [ - ModalAction( - action: () => Navigator.of(context).pop(false), - title: appLocalizationsOf(context).cancelEmphasized, + ], + ), ), - ModalAction( - action: () { - if (state.uploadPlanForAR != null && - state.reason == - UploadWarningReason - .fileTooLargeOnNonChromeBrowser) { - return context.read().startUpload( - uploadPlanForAr: state.uploadPlanForAR!, - uploadPlanForTurbo: state.uploadPlanForTurbo, - ); - } + actions: [ + ModalAction( + action: () => Navigator.of(context).pop(false), + title: appLocalizationsOf(context).cancelEmphasized, + ), + ModalAction( + action: () { + if (state.uploadPlanForAR != null && + state.reason == + UploadWarningReason + .fileTooLargeOnNonChromeBrowser) { + return context.read().startUpload( + uploadPlanForAr: state.uploadPlanForAR!, + uploadPlanForTurbo: state.uploadPlanForTurbo, + ); + } - return context.read().checkFilesAboveLimit(); - }, - title: appLocalizationsOf(context).proceed, - ), - ], - ); - } - return const SizedBox(); - }, + return context.read().checkFilesAboveLimit(); + }, + title: appLocalizationsOf(context).proceed, + ), + ], + ); + } + return const SizedBox(); + }, + ), ); Widget _uploadUsingNewUploader({ diff --git a/lib/core/upload/uploader.dart b/lib/core/upload/uploader.dart index b3567813b9..3353ab9f69 100644 --- a/lib/core/upload/uploader.dart +++ b/lib/core/upload/uploader.dart @@ -281,7 +281,7 @@ class UploadPreparer { required UploadPlanUtils uploadPlanUtils, }) : _uploadPlanUtils = uploadPlanUtils; - Future prepareUpload(UploadParams params) async { + Future prepareFileUpload(UploadParams params) async { final uploadPlanForAR = await _mountUploadPlan( params: params, method: UploadMethod.ar, @@ -338,7 +338,59 @@ class UploadPaymentEvaluator { _auth = auth, _turboUploadCostCalculator = turboUploadCostCalculator; - Future getUploadPaymentInfo({ + /// Even if this feature flag is off, it will be possible to upload using turbo + /// for free files + bool get _canUseTurbo => _appConfig.useTurboUpload; + bool _isTurboAvailableToUploadAllFiles = true; + + Future getUploadPaymentInfoForEntities({ + required DataItem dataItem, + }) async { + final dataItemSize = dataItem.getSize(); + + UploadMethod uploadMethod; + + int totalSize = 0; + + BigInt turboBalance; + + turboBalance = await _getTurboBalance(canUseTurbo: _canUseTurbo); + + final turboCostEstimate = await _turboUploadCostCalculator.calculateCost( + totalSize: dataItemSize, + ); + + /// Calculate the upload with AR is not optional + final arCostEstimate = + await _uploadCostEstimateCalculatorForAR.calculateCost( + totalSize: dataItemSize, + ); + + final allowedDataItemSizeForTurbo = _appConfig.allowedDataItemSizeForTurbo; + + bool isFreeUploadPossibleUsingTurbo = + dataItem.getSize() <= allowedDataItemSizeForTurbo; + + uploadMethod = await _determineUploadMethod( + turboBalance, + dataItemSize, + dataItemSize, + _isTurboAvailableToUploadAllFiles, + ); + + return UploadPaymentInfo( + isTurboAvailable: _isTurboAvailableToUploadAllFiles, + defaultPaymentMethod: uploadMethod, + isUploadEligibleToTurbo: true, + arCostEstimate: arCostEstimate, + turboCostEstimate: turboCostEstimate, + isFreeUploadPossibleUsingTurbo: isFreeUploadPossibleUsingTurbo, + totalSize: totalSize, + turboBalance: turboBalance, + ); + } + + Future getUploadPaymentInfoForUploadPlans({ required UploadPlan uploadPlanForAR, required UploadPlan uploadPlanForTurbo, }) async { @@ -348,30 +400,9 @@ class UploadPaymentEvaluator { BigInt turboBalance; - /// Even if this feature flag is off, it will be possible to upload using turbo - /// for free files - bool isTurboAvailableToUploadAllFiles = _appConfig.useTurboUpload; - - if (isTurboAvailableToUploadAllFiles) { - /// Check the balance of the user - /// If we can't get the balance, turbo won't be available - try { - turboBalance = - await _turboBalanceRetriever.getBalance(_auth.currentUser.wallet); - - logger.i('Turbo balance: $turboBalance'); - } catch (e, stacktrace) { - logger.e( - 'An error occured while getting the turbo balance', - e, - stacktrace, - ); - isTurboAvailableToUploadAllFiles = false; - turboBalance = BigInt.zero; - } - } else { - turboBalance = BigInt.zero; - } + /// Check the balance of the user + /// If we can't get the balance, turbo won't be available + turboBalance = await _getTurboBalance(canUseTurbo: _canUseTurbo); final arBundleSizes = await sizeUtils .getSizeOfAllBundles(uploadPlanForAR.bundleUploadHandles); @@ -396,7 +427,7 @@ class UploadPaymentEvaluator { totalSize: turboBundleSizes, ); } catch (e) { - isTurboAvailableToUploadAllFiles = false; + _isTurboAvailableToUploadAllFiles = false; } } @@ -420,19 +451,15 @@ class UploadPaymentEvaluator { ); } - if ((isTurboAvailableToUploadAllFiles && - isUploadEligibleToTurbo && - turboBalance >= turboCostEstimate.totalCost) || - isFreeUploadPossibleUsingTurbo) { - totalSize = turboBundleSizes; - uploadMethod = UploadMethod.turbo; - } else { - totalSize = arBundleSizes + arFileSizes; - uploadMethod = UploadMethod.ar; - } + uploadMethod = await _determineUploadMethod( + turboBalance, + turboBundleSizes, + _appConfig.allowedDataItemSizeForTurbo, + _isTurboAvailableToUploadAllFiles, + ); return UploadPaymentInfo( - isTurboAvailable: isTurboAvailableToUploadAllFiles, + isTurboAvailable: _isTurboAvailableToUploadAllFiles, defaultPaymentMethod: uploadMethod, isUploadEligibleToTurbo: isUploadEligibleToTurbo, arCostEstimate: arCostEstimate, @@ -442,6 +469,53 @@ class UploadPaymentEvaluator { turboBalance: turboBalance, ); } + + Future _getTurboBalance({ + required bool canUseTurbo, + }) async { + if (!canUseTurbo) { + _isTurboAvailableToUploadAllFiles = false; + return BigInt.zero; + } + + try { + return await _turboBalanceRetriever.getBalance(_auth.currentUser.wallet); + } catch (e, stacktrace) { + logger.e( + 'An error occurred while getting the turbo balance', + e, + stacktrace, + ); + _isTurboAvailableToUploadAllFiles = false; + return BigInt.zero; + } + } + + Future _determineUploadMethod( + BigInt turboBalance, + int turboBundleSizes, + int allowedSizeForTurbo, + bool isTurboAvailableToUploadAllFiles) async { + try { + final turboCostEstimate = await _turboUploadCostCalculator.calculateCost( + totalSize: turboBundleSizes, + ); + + bool isFreeUploadPossibleUsingTurbo = + turboBundleSizes <= allowedSizeForTurbo; + + if ((isTurboAvailableToUploadAllFiles && + turboBalance >= turboCostEstimate.totalCost) || + isFreeUploadPossibleUsingTurbo) { + return UploadMethod.turbo; + } else { + return UploadMethod.ar; + } + } catch (e) { + _isTurboAvailableToUploadAllFiles = false; + return UploadMethod.ar; + } + } } class UploadPreparation { @@ -499,10 +573,10 @@ class ArDriveUploadPreparationManager { Future prepareUpload({ required UploadParams params, }) async { - final uploadPreparation = await _uploadPreparer.prepareUpload(params); + final uploadPreparation = await _uploadPreparer.prepareFileUpload(params); final uploadPaymentInfo = - await _uploadPaymentEvaluator.getUploadPaymentInfo( + await _uploadPaymentEvaluator.getUploadPaymentInfoForUploadPlans( uploadPlanForAR: uploadPreparation.uploadPlanForAr, uploadPlanForTurbo: uploadPreparation.uploadPlanForTurbo, ); @@ -512,6 +586,17 @@ class ArDriveUploadPreparationManager { uploadPaymentInfo: uploadPaymentInfo, ); } + + Future getUploadPaymentInfoForEntityUpload({ + required DataItem dataItem, + }) async { + final uploadPaymentInfo = + await _uploadPaymentEvaluator.getUploadPaymentInfoForEntities( + dataItem: dataItem, + ); + + return uploadPaymentInfo; + } } class UploadParams { diff --git a/lib/entities/entity.dart b/lib/entities/entity.dart index 0d1bb07e51..b3d64b890e 100644 --- a/lib/entities/entity.dart +++ b/lib/entities/entity.dart @@ -230,7 +230,7 @@ extension TransactionUtils on TransactionBase { /// Tags this transaction with the ArFS version currently in use. void addArFsTag() { - addTag(EntityTag.arFs, '0.13'); + addTag(EntityTag.arFs, '0.14'); } void addBundleTags() { diff --git a/lib/entities/file_entity.dart b/lib/entities/file_entity.dart index e8ef684a43..1320d9b74e 100644 --- a/lib/entities/file_entity.dart +++ b/lib/entities/file_entity.dart @@ -30,12 +30,12 @@ class FileEntity extends EntityWithCustomMetadata { int? size; @JsonKey(fromJson: _msToDateTime, toJson: _dateTimeToMs) DateTime? lastModifiedDate; - String? dataTxId; String? dataContentType; - - @JsonKey(name: 'pinnedDataOwner') + @JsonKey(name: 'pinnedDataOwner', includeIfNull: false) String? pinnedDataOwnerAddress; + @JsonKey(includeIfNull: false) + bool? isHidden; @override @JsonKey(includeFromJson: false, includeToJson: false) @@ -53,6 +53,7 @@ class FileEntity extends EntityWithCustomMetadata { 'lastModifiedDate', 'dataTxId', 'dataContentType', + 'isHidden', ]; FileEntity({ @@ -65,6 +66,7 @@ class FileEntity extends EntityWithCustomMetadata { this.dataTxId, this.dataContentType, this.pinnedDataOwnerAddress, + this.isHidden, }) : super(ArDriveCrypto()); FileEntity.withUserProvidedDetails({ @@ -148,6 +150,7 @@ class FileEntity extends EntityWithCustomMetadata { ); return entity; } + Map toJson() { final thisJson = _$FileEntityToJson(this); final custom = customJsonMetadata ?? {}; diff --git a/lib/entities/folder_entity.dart b/lib/entities/folder_entity.dart index 06d81fc9b2..ae81d06070 100644 --- a/lib/entities/folder_entity.dart +++ b/lib/entities/folder_entity.dart @@ -22,6 +22,8 @@ class FolderEntity extends EntityWithCustomMetadata { String? parentFolderId; String? name; + @JsonKey(includeIfNull: false) + bool? isHidden; @override @JsonKey(includeFromJson: false, includeToJson: false) @@ -35,6 +37,7 @@ class FolderEntity extends EntityWithCustomMetadata { @JsonKey(includeFromJson: false, includeToJson: false) List reservedJsonMetadataKeys = [ ...EntityWithCustomMetadata.sharedReservedJsonMetadataKeys, + 'isHidden', ]; FolderEntity({ @@ -42,6 +45,7 @@ class FolderEntity extends EntityWithCustomMetadata { this.driveId, this.parentFolderId, this.name, + this.isHidden, }) : super(ArDriveCrypto()); static Future fromTransaction( diff --git a/lib/entities/manifest_data.dart b/lib/entities/manifest_data.dart index c9c74c90af..e9dbf0ef2f 100644 --- a/lib/entities/manifest_data.dart +++ b/lib/entities/manifest_data.dart @@ -79,7 +79,8 @@ class ManifestData { final fileList = folderNode .getRecursiveFiles() // We will not include any existing manifests in the new manifest - .where((f) => f.dataContentType != ContentType.manifest); + // We will not include any hidden files in the new manifest + .where((f) => f.dataContentType != ContentType.manifest && !f.isHidden); final indexFile = () { final indexHtml = folderNode.files.values.firstWhereOrNull( diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index afed70e078..44f1d46c03 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -214,6 +214,10 @@ "@computingSnapshotData": { "description": "Computing Snapshot Data" }, + "concealHiddenItems": "Conceal hidden items", + "@concealHiddenItems": { + "description": "The action of concealing hidden items." + }, "confirm": "Confirm", "@confirm": {}, "confirmed": "Confirmed", @@ -581,6 +585,10 @@ } } }, + "driveWasHidden": "This drive was hidden", + "@driveWasHidden": { + "description": "This drive was hidden." + }, "driveWasModified": "This drive was modified.", "@driveWasModified": { "description": "Drive activity (journal) entry: modified" @@ -595,6 +603,10 @@ } } }, + "driveWasUnhidden": "This drive was unhidden", + "@driveWasUnhidden": { + "description": "This drive was unhidden." + }, "duplicateFiles": "{numberOfDuplicateFiles, plural, zero{} one{A duplicate file found} other{Duplicate files found}}", "@duplicateFiles": { "description": "Count of conflicting items", @@ -729,6 +741,22 @@ "@failedToCreatePin": { "description": "The pin could not be created" }, + "failedToHideFile": "Failed to hide file", + "@failedToHideFile": { + "description": "Failed to hide file" + }, + "failedToHideFileTryAgain": "Failed to hide file, please try again", + "@failedToHideFileTryAgain": { + "description": "Failed to hide file, try again." + }, + "failedToHideFolder": "Failed to hide folder", + "@failedToHideFolder": { + "description": "Failed to hide folder" + }, + "failedToHideFolderTryAgain": "Failed to hide folder, please try again", + "@failedToHideFolderTryAgain": { + "description": "Failed to hide folder, try again." + }, "failedToRetrieveFileInfromation": "Failed to retrieve file information", "@failedToRetrieveFileInfromation": { "description": "Explains that there was an error while retrieving the file infromation" @@ -737,6 +765,22 @@ "@failedToSyncDrive": { "description": "The app wasn't able to sync the drive" }, + "failedToUnhideFile": "Failed to unhide file", + "@failedToUnhideFile": { + "description": "Failed to unhide file" + }, + "failedToUnhideFileTryAgain": "Failed to unhide file, please try again", + "@failedToUnhideFileTryAgain": { + "description": "Failed to unhide file, try again." + }, + "failedToUnhideFolder": "Failed to unhide folder", + "@failedToUnhideFolder": { + "description": "Failed to unhide folder" + }, + "failedToUnhideFolderTryAgain": "Failed to unhide folder, please try again", + "@failedToUnhideFolderTryAgain": { + "description": "Failed to unhide folder, try again." + }, "feedbackContent": "Would you be willing to leave feedback on your experience using the app?", "@feedbackContent": { "description": "Asks for feedback" @@ -877,6 +921,10 @@ } } }, + "fileWasHidden": "This file was hidden", + "@fileWasHidden": { + "description": "This file was hidden." + }, "fileWasModified": "This file was modified.", "@fileWasModified": { "description": "File activity (journal): modified" @@ -899,6 +947,10 @@ } } }, + "fileWasUnhidden": "This file was unhidden", + "@fileWasUnhidden": { + "description": "This file was unhidden." + }, "finishingThingsUp": "Finishing things up!", "@finishingThingsUp": { "description": "Finishing things up!" @@ -965,6 +1017,10 @@ } } }, + "folderWasHidden": "This folder was hidden", + "@folderWasHidden": { + "description": "This folder was hidden." + }, "folderWasModified": "This folder was modified.", "@folderWasModified": { "description": "Folder activity (journal): modified" @@ -983,6 +1039,10 @@ } } }, + "folderWasUnhidden": "This folder was unhidden", + "@folderWasUnhidden": { + "description": "This folder was unhidden." + }, "forgetWallet": "Forget wallet and change profile", "@forgetWallet": { "description": "Use a different wallet by forgetting the current one" @@ -1039,6 +1099,18 @@ }, "helpCenter": "Help Center", "@helpCenter": {}, + "hide": "Hide", + "@hide": { + "description": "Hide" + }, + "hidingFile": "Hiding file", + "@hidingFile": { + "description": "Hiding file" + }, + "hidingFolder": "Hiding folder", + "@hidingFolder": { + "description": "Hiding folder" + }, "howAreConversionsDetermined": "How are conversions determined?", "@howAreConversionsDetermined": {}, "howDoesKeyfileLoginWork": "How do keyfile and seed phrase login work?", @@ -1606,6 +1678,10 @@ "@resyncTooltip": { "description": "resync tooltip text" }, + "revealHiddenItems": "Reveal hidden items", + "@revealHiddenItems": { + "description": "The action of revealing hidden items." + }, "review": "Review", "@review": {}, "rootFolderTxID": "Root Folder Tx ID", @@ -1926,6 +2002,18 @@ "@unableToFetchEstimateAtThisTime": {}, "unableToUpdateQuote": "Unable to update quote. Please try again.", "@unableToUpdateQuote": {}, + "unhide": "Unhide", + "@unhide": { + "description": "Unhide" + }, + "unhidingFile": "Unhiding file", + "@unhidingFile": { + "description": "Unhiding file" + }, + "unhidingFolder": "Unhiding folder", + "@unhidingFolder": { + "description": "Unhiding folder" + }, "unit": "Unit", "@unit": {}, "units": "Units", @@ -2180,4 +2268,4 @@ "@zippingYourFiles": { "description": "Download failure message when a file is too big" } -} \ No newline at end of file +} diff --git a/lib/main.dart b/lib/main.dart index b4a77b3d60..75d484b523 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,12 +3,15 @@ import 'dart:async'; import 'package:ardrive/authentication/ardrive_auth.dart'; import 'package:ardrive/blocs/activity/activity_cubit.dart'; import 'package:ardrive/blocs/feedback_survey/feedback_survey_cubit.dart'; +import 'package:ardrive/blocs/hide/hide_bloc.dart'; import 'package:ardrive/blocs/prompt_to_snapshot/prompt_to_snapshot_bloc.dart'; import 'package:ardrive/blocs/upload/limits.dart'; import 'package:ardrive/blocs/upload/upload_file_checker.dart'; import 'package:ardrive/components/keyboard_handler.dart'; import 'package:ardrive/core/activity_tracker.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/models/database/database_helpers.dart'; import 'package:ardrive/services/authentication/biometric_authentication.dart'; import 'package:ardrive/services/config/config_fetcher.dart'; @@ -17,6 +20,7 @@ import 'package:ardrive/theme/theme_switcher_bloc.dart'; import 'package:ardrive/theme/theme_switcher_state.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/user/repositories/user_preferences_repository.dart'; import 'package:ardrive/user/repositories/user_repository.dart'; import 'package:ardrive/utils/app_flavors.dart'; @@ -26,6 +30,7 @@ import 'package:ardrive/utils/mobile_screen_orientation.dart'; import 'package:ardrive/utils/mobile_status_bar.dart'; import 'package:ardrive/utils/pre_cache_assets.dart'; import 'package:ardrive/utils/secure_key_value_store.dart'; +import 'package:ardrive/utils/upload_plan_utils.dart'; import 'package:ardrive_http/ardrive_http.dart'; import 'package:ardrive_io/ardrive_io.dart'; import 'package:ardrive_logger/ardrive_logger.dart'; @@ -132,8 +137,12 @@ Future _initializeServices() async { httpClient: ArDriveHTTP(), ); + void refreshHTMLPageAtInterval(Duration duration) { + Timer.periodic(duration, (timer) => triggerHTMLPageReload()); + } + if (kIsWeb) { - _refreshHTMLPageAtInterval(const Duration(hours: 12)); + refreshHTMLPageAtInterval(const Duration(hours: 12)); } } @@ -268,6 +277,53 @@ class AppState extends State { driveDao: context.read(), ), ), + BlocProvider( + create: (context) => HideBloc( + auth: context.read(), + uploadPreparationManager: 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(), + ), + ), + ), + arweaveService: context.read(), + crypto: ArDriveCrypto(), + turboUploadService: context.read(), + driveDao: context.read(), + profileCubit: context.read(), + ), + ), BlocProvider( create: (context) => SharingFileBloc( context.read(), @@ -350,7 +406,3 @@ class AppState extends State { ), ]; } - -void _refreshHTMLPageAtInterval(Duration duration) { - Timer.periodic(duration, (timer) => triggerHTMLPageReload()); -} diff --git a/lib/models/daos/drive_dao/drive_dao.dart b/lib/models/daos/drive_dao/drive_dao.dart index da4dfad60a..75254bcbfe 100644 --- a/lib/models/daos/drive_dao/drive_dao.dart +++ b/lib/models/daos/drive_dao/drive_dao.dart @@ -132,6 +132,7 @@ class DriveDao extends DatabaseAccessor with _$DriveDaoMixin { driveId: driveId, name: name, path: rootPath, + isHidden: false, ), ); }); @@ -366,6 +367,7 @@ class DriveDao extends DatabaseAccessor with _$DriveDaoMixin { files = files.reversed.toList(); } } + return FolderWithContents( folder: folder, subfolders: subfolders, @@ -390,6 +392,7 @@ class DriveDao extends DatabaseAccessor with _$DriveDaoMixin { parentFolderId: Value(parentFolderId), name: folderName, path: path, + isHidden: false, ); await into(folderEntries).insert(folderEntriesCompanion); @@ -456,6 +459,7 @@ class DriveDao extends DatabaseAccessor with _$DriveDaoMixin { lastModifiedDate: entity.lastModifiedDate ?? DateTime.now(), dataContentType: Value(entity.dataContentType), pinnedDataOwnerAddress: Value(entity.pinnedDataOwnerAddress), + isHidden: entity.isHidden ?? false, ); return into(fileEntries).insert( diff --git a/lib/models/daos/drive_dao/folder_with_contents.dart b/lib/models/daos/drive_dao/folder_with_contents.dart index 0a9904125c..a6cb2fbb8d 100644 --- a/lib/models/daos/drive_dao/folder_with_contents.dart +++ b/lib/models/daos/drive_dao/folder_with_contents.dart @@ -4,8 +4,6 @@ class FolderWithContents extends Equatable { final List subfolders; final List files; final FolderEntry folder; - // This is nullable as it can be a while between the drive being not found, then added, - // and then the folders being loaded. const FolderWithContents( {required this.folder, required this.subfolders, required this.files}); diff --git a/lib/models/database/database.dart b/lib/models/database/database.dart index 4fc314ff6c..4ca9d62dae 100644 --- a/lib/models/database/database.dart +++ b/lib/models/database/database.dart @@ -57,6 +57,15 @@ class Database extends _$Database { fileRevisions, fileRevisions.pinnedDataOwnerAddress, ); + } else if (from == 17 && to == 18) { + // Then we're adding the isHidden column + logger.i('Migrating schema from v17 to v18'); + + await m.addColumn(folderRevisions, folderRevisions.isHidden); + await m.addColumn(fileRevisions, fileRevisions.isHidden); + + await m.addColumn(folderEntries, folderEntries.isHidden); + await m.addColumn(fileEntries, fileEntries.isHidden); } else if (from >= 1 && from < schemaVersion) { logger.w( 'No strategy set for migration v$from to v$to' diff --git a/lib/models/enums.dart b/lib/models/enums.dart index 8727e69a38..733eee11b5 100644 --- a/lib/models/enums.dart +++ b/lib/models/enums.dart @@ -3,6 +3,8 @@ abstract class RevisionAction { static const uploadNewVersion = 'upload-new-version'; static const rename = 'rename'; static const move = 'move'; + static const hide = 'hide'; + static const unhide = 'unhide'; } abstract class TransactionStatus { diff --git a/lib/models/file_entry.dart b/lib/models/file_entry.dart index 171349b4d3..f89620551b 100644 --- a/lib/models/file_entry.dart +++ b/lib/models/file_entry.dart @@ -15,6 +15,7 @@ extension FileEntryExtensions on FileEntry { lastModifiedDate: lastModifiedDate, dataContentType: dataContentType, pinnedDataOwnerAddress: pinnedDataOwnerAddress, + isHidden: isHidden, ); file.customJsonMetadata = parseCustomJsonMetadata(customJsonMetadata); diff --git a/lib/models/file_revision.dart b/lib/models/file_revision.dart index 2a196dd24f..e8486e96f6 100644 --- a/lib/models/file_revision.dart +++ b/lib/models/file_revision.dart @@ -22,6 +22,7 @@ extension FileRevisionsCompanionExtensions on FileRevisionsCompanion { customGQLTags: customGQLTags, customJsonMetadata: customJsonMetadata, pinnedDataOwnerAddress: pinnedDataOwnerAddress, + isHidden: isHidden.value, ); /// Returns a list of [NetworkTransactionsCompanion] representing the metadata and data transactions @@ -57,6 +58,7 @@ extension FileEntityExtensions on FileEntity { customGQLTags: Value(customGqlTagsAsString), customJsonMetadata: Value(customJsonMetadataAsString), pinnedDataOwnerAddress: Value(pinnedDataOwnerAddress), + isHidden: isHidden ?? false, ); FileRevision toRevision({ @@ -78,6 +80,7 @@ extension FileEntityExtensions on FileEntity { customGQLTags: customGqlTagsAsString, customJsonMetadata: customJsonMetadataAsString, pinnedDataOwnerAddress: pinnedDataOwnerAddress, + isHidden: isHidden ?? false, ); /// Returns the action performed on the file that lead to the new revision. @@ -91,6 +94,10 @@ extension FileEntityExtensions on FileEntity { return RevisionAction.move; } else if (dataTxId != previousRevision.dataTxId.value) { return RevisionAction.uploadNewVersion; + } else if (isHidden == true && previousRevision.isHidden.value == false) { + return RevisionAction.hide; + } else if (isHidden == false && previousRevision.isHidden.value == true) { + return RevisionAction.unhide; } return null; diff --git a/lib/models/folder_entry.dart b/lib/models/folder_entry.dart index 95bf1ce7e6..65a961b14e 100644 --- a/lib/models/folder_entry.dart +++ b/lib/models/folder_entry.dart @@ -10,6 +10,7 @@ extension FolderEntryExtensions on FolderEntry { driveId: driveId, parentFolderId: parentFolderId, name: name, + isHidden: isHidden, ); folder.customJsonMetadata = parseCustomJsonMetadata(customJsonMetadata); diff --git a/lib/models/folder_revision.dart b/lib/models/folder_revision.dart index 74f5639a42..2233fa98bd 100644 --- a/lib/models/folder_revision.dart +++ b/lib/models/folder_revision.dart @@ -21,6 +21,7 @@ extension FolderRevisionCompanionExtensions on FolderRevisionsCompanion { lastUpdated: dateCreated, customGQLTags: customGQLTags, customJsonMetadata: customJsonMetadata, + isHidden: isHidden.value, ); /// Returns a [NetworkTransactionsCompanion] representing the metadata transaction @@ -46,6 +47,7 @@ extension FolderEntityExtensions on FolderEntity { action: performedAction, customGQLTags: Value(customGqlTagsAsString), customJsonMetadata: Value(customJsonMetadataAsString), + isHidden: isHidden ?? false, ); /// Returns the action performed on the folder that lead to the new revision. @@ -57,6 +59,10 @@ extension FolderEntityExtensions on FolderEntity { return RevisionAction.rename; } else if (parentFolderId != previousRevision.parentFolderId.value) { return RevisionAction.move; + } else if (isHidden == true && previousRevision.isHidden.value == false) { + return RevisionAction.hide; + } else if (isHidden == false && previousRevision.isHidden.value == true) { + return RevisionAction.unhide; } return null; diff --git a/lib/models/tables/file_entries.drift b/lib/models/tables/file_entries.drift index f823fd2a64..6b835ecc40 100644 --- a/lib/models/tables/file_entries.drift +++ b/lib/models/tables/file_entries.drift @@ -18,6 +18,8 @@ CREATE TABLE file_entries ( customJsonMetadata TEXT, customGQLTags TEXT, + isHidden BOOLEAN NOT NULL, + dateCreated DATETIME NOT NULL DEFAULT (strftime('%s','now')), lastUpdated DATETIME NOT NULL DEFAULT (strftime('%s','now')), diff --git a/lib/models/tables/file_revisions.drift b/lib/models/tables/file_revisions.drift index acaf8b43f1..290b68a2a8 100644 --- a/lib/models/tables/file_revisions.drift +++ b/lib/models/tables/file_revisions.drift @@ -25,6 +25,8 @@ CREATE TABLE file_revisions ( pinnedDataOwnerAddress TEXT, + isHidden BOOLEAN NOT NULL, + PRIMARY KEY (fileId, driveId, dateCreated), FOREIGN KEY (metadataTxId) REFERENCES network_transactions(id), FOREIGN KEY (dataTxId) REFERENCES network_transactions(id) diff --git a/lib/models/tables/folder_entries.drift b/lib/models/tables/folder_entries.drift index a0fafa9f9b..ea591d6a63 100644 --- a/lib/models/tables/folder_entries.drift +++ b/lib/models/tables/folder_entries.drift @@ -10,9 +10,10 @@ CREATE TABLE folder_entries ( lastUpdated DATETIME NOT NULL DEFAULT (strftime('%s','now')), isGhost BOOLEAN NOT NULL DEFAULT FALSE, - customJsonMetadata TEXT, customGQLTags TEXT, + isHidden BOOLEAN NOT NULL, + PRIMARY KEY (id, driveId) ) As FolderEntry; diff --git a/lib/models/tables/folder_revisions.drift b/lib/models/tables/folder_revisions.drift index 82a97cd893..78c7a63560 100644 --- a/lib/models/tables/folder_revisions.drift +++ b/lib/models/tables/folder_revisions.drift @@ -16,6 +16,8 @@ CREATE TABLE folder_revisions ( customJsonMetadata TEXT, customGQLTags TEXT, + isHidden BOOLEAN NOT NULL, + PRIMARY KEY (folderId, driveId, dateCreated), FOREIGN KEY (metadataTxId) REFERENCES network_transactions(id) ); diff --git a/lib/pages/app_router_delegate.dart b/lib/pages/app_router_delegate.dart index b4e643ecd4..bbd6fd647c 100644 --- a/lib/pages/app_router_delegate.dart +++ b/lib/pages/app_router_delegate.dart @@ -195,14 +195,16 @@ class AppRouterDelegate extends RouterDelegate child: MultiBlocListener( listeners: [ BlocListener( - listener: (context, state) { - if (state is DriveDetailLoadSuccess) { - driveId = state.currentDrive.id; - driveFolderId = state.folderInView.folder.id; + listener: (context, driveDetailCubitState) { + if (driveDetailCubitState is DriveDetailLoadSuccess) { + driveId = driveDetailCubitState.currentDrive.id; + driveFolderId = + driveDetailCubitState.folderInView.folder.id; //Can be null at the root folder of the drive notifyListeners(); - } else if (state is DriveDetailLoadNotFound) { + } else if (driveDetailCubitState + is DriveDetailLoadNotFound) { // Do not prompt the user to attach an unfound drive if they are logging out. final profileCubit = context.read(); diff --git a/lib/pages/drive_detail/components/drive_detail_data_list.dart b/lib/pages/drive_detail/components/drive_detail_data_list.dart index 62f8dd6fa8..3b86458953 100644 --- a/lib/pages/drive_detail/components/drive_detail_data_list.dart +++ b/lib/pages/drive_detail/components/drive_detail_data_list.dart @@ -3,13 +3,16 @@ part of '../drive_detail_page.dart'; Widget _buildDataList( BuildContext context, DriveDetailLoadSuccess state, + Widget emptyState, ) { return _buildDataListContent( context, state.currentFolderContents, state.folderInView.folder, state.currentDrive, - state.multiselect, + isMultiselecting: state.multiselect, + isShowingHiddenFiles: state.isShowingHiddenFiles, + emptyState: emptyState, ); } @@ -24,6 +27,7 @@ abstract class ArDriveDataTableItem extends IndexedItem { final String driveId; final String path; final bool isOwner; + final bool isHidden; ArDriveDataTableItem({ required this.id, @@ -37,6 +41,7 @@ abstract class ArDriveDataTableItem extends IndexedItem { required this.path, required int index, required this.isOwner, + this.isHidden = false, }) : super(index); } @@ -51,6 +56,7 @@ class DriveDataItem extends ArDriveDataTableItem { super.path = '', required super.index, required super.isOwner, + super.isHidden, }); @override @@ -62,34 +68,23 @@ class FolderDataTableItem extends ArDriveDataTableItem { final bool isGhostFolder; FolderDataTableItem({ - required String driveId, + required super.driveId, required String folderId, - required String name, - required DateTime lastUpdated, - required DateTime dateCreated, - required String contentType, - required String path, - String? fileStatusFromTransactions, + required super.name, + required super.lastUpdated, + required super.dateCreated, + required super.contentType, + required super.path, + super.fileStatusFromTransactions, + super.isHidden, + required super.index, + required super.isOwner, this.parentFolderId, this.isGhostFolder = false, - required int index, - required bool isOwner, - }) : super( - driveId: driveId, - path: path, - id: folderId, - name: name, - size: null, - lastUpdated: lastUpdated, - dateCreated: dateCreated, - contentType: contentType, - fileStatusFromTransactions: fileStatusFromTransactions, - index: index, - isOwner: isOwner, - ); + }) : super(id: folderId); @override - List get props => [id, name]; + List get props => [id, name, isHidden]; } class FileDataTableItem extends ArDriveDataTableItem { @@ -103,56 +98,59 @@ class FileDataTableItem extends ArDriveDataTableItem { final String? pinnedDataOwnerAddress; FileDataTableItem({ + required super.driveId, + required super.lastUpdated, + required super.name, + required super.size, + required super.dateCreated, + required super.contentType, + required super.path, + super.isHidden, + super.fileStatusFromTransactions, + required super.index, + required super.isOwner, required this.fileId, - required String driveId, required this.parentFolderId, required this.dataTxId, - required DateTime lastUpdated, required this.lastModifiedDate, required this.metadataTx, required this.dataTx, required this.pinnedDataOwnerAddress, - required String name, - required int size, - required DateTime dateCreated, - required String contentType, - required String path, - String? fileStatusFromTransactions, this.bundledIn, - required int index, - required bool isOwner, - }) : super( - path: path, - driveId: driveId, - id: fileId, - name: name, - size: size, - lastUpdated: lastUpdated, - dateCreated: dateCreated, - contentType: contentType, - fileStatusFromTransactions: fileStatusFromTransactions, - index: index, - isOwner: isOwner, - ); + }) : super(id: fileId); @override - List get props => [fileId, name]; + List get props => [fileId, name, isHidden]; } Widget _buildDataListContent( BuildContext context, List items, FolderEntry folder, - Drive drive, - bool isMultiselecting, -) { + Drive drive, { + required bool isMultiselecting, + required bool isShowingHiddenFiles, + required Widget emptyState, +}) { + final List filteredItems; + + if (isShowingHiddenFiles) { + filteredItems = items.toList(); + } else { + filteredItems = items.where((item) => item.isHidden == false).toList(); + } + + if (filteredItems.isEmpty) { + return emptyState; + } + return LayoutBuilder(builder: (context, constraints) { final driveDetailCubitState = context.read().state; final forceRebuildKey = driveDetailCubitState is DriveDetailLoadSuccess ? driveDetailCubitState.forceRebuildKey : null; return ArDriveDataTable( - key: ValueKey(folder.id + forceRebuildKey.toString()), + key: ValueKey('${folder.id}-${forceRebuildKey.toString()}'), lockMultiSelect: context.watch().state is SyncInProgress || !context.watch().isMultiSelectEnabled, rowsPerPageText: appLocalizationsOf(context).rowsPerPage, @@ -239,6 +237,7 @@ Widget _buildDataListContent( size: row.size == null ? '-' : filesize(row.size), lastUpdated: yMMdDateFormatter.format(row.lastUpdated), dateCreated: yMMdDateFormatter.format(row.dateCreated), + isHidden: row.isHidden, onPressed: () { final cubit = context.read(); if (row is FolderDataTableItem) { @@ -250,14 +249,14 @@ Widget _buildDataListContent( } else if (row is FileDataTableItem) { if (row.id == cubit.selectedItem?.id) { cubit.toggleSelectedItemDetails(); - return; + } else { + cubit.selectDataItem(row); } - cubit.selectDataItem(row); } }, ); }, - rows: items, + rows: filteredItems, selectedRow: context.watch().selectedItem, ); }); @@ -329,6 +328,7 @@ class DriveDataTableItemMapper { dataTx: file.dataTx, index: index, pinnedDataOwnerAddress: file.pinnedDataOwnerAddress, + isHidden: file.isHidden, ); } @@ -350,6 +350,7 @@ class DriveDataTableItemMapper { dateCreated: folderEntry.dateCreated, contentType: 'folder', fileStatusFromTransactions: null, + isHidden: folderEntry.isHidden, ); } @@ -368,6 +369,7 @@ class DriveDataTableItemMapper { dateCreated: drive.dateCreated, contentType: 'drive', id: drive.id, + isHidden: false, // TODO: update me when drives can be hidden ); } @@ -391,6 +393,7 @@ class DriveDataTableItemMapper { dataTx: null, index: 0, pinnedDataOwnerAddress: revision.pinnedDataOwnerAddress, + isHidden: revision.isHidden, ); } } diff --git a/lib/pages/drive_detail/components/drive_explorer_item_tile.dart b/lib/pages/drive_detail/components/drive_explorer_item_tile.dart index fd63c81e01..dbc3233f55 100644 --- a/lib/pages/drive_detail/components/drive_explorer_item_tile.dart +++ b/lib/pages/drive_detail/components/drive_explorer_item_tile.dart @@ -3,6 +3,7 @@ import 'package:ardrive/components/components.dart'; import 'package:ardrive/components/csv_export_dialog.dart'; import 'package:ardrive/components/drive_rename_form.dart'; import 'package:ardrive/components/ghost_fixer_form.dart'; +import 'package:ardrive/components/hide_dialog.dart'; import 'package:ardrive/components/pin_indicator.dart'; import 'package:ardrive/download/multiple_file_download_modal.dart'; import 'package:ardrive/models/models.dart'; @@ -24,30 +25,43 @@ class DriveExplorerItemTile extends TableRowWidget { required String lastUpdated, required String dateCreated, required Function() onPressed, + required bool isHidden, }) : super( [ Padding( padding: const EdgeInsets.only(right: 8), child: Text( name, - style: ArDriveTypography.body.buttonNormalBold(), + style: ArDriveTypography.body.buttonNormalBold().copyWith( + color: isHidden ? Colors.grey : null, + ), overflow: TextOverflow.fade, maxLines: 1, softWrap: false, ), ), - Text(size, style: ArDriveTypography.body.captionRegular()), - Text(lastUpdated, style: ArDriveTypography.body.captionRegular()), - Text(dateCreated, style: ArDriveTypography.body.captionRegular()), + Text(size, style: _driveExplorerItemTileTextStyle(isHidden)), + Text(lastUpdated, style: _driveExplorerItemTileTextStyle(isHidden)), + Text(dateCreated, style: _driveExplorerItemTileTextStyle(isHidden)), ], ); } +TextStyle _driveExplorerItemTileTextStyle(bool isHidden) => + ArDriveTypography.body + .captionRegular() + .copyWith(color: isHidden ? Colors.grey : null); + class DriveExplorerItemTileLeading extends StatelessWidget { - const DriveExplorerItemTileLeading({super.key, required this.item}); + const DriveExplorerItemTileLeading({ + super.key, + required this.item, + }); final ArDriveDataTableItem item; + bool get isHidden => item.isHidden; + @override Widget build(BuildContext context) { return Padding( @@ -68,6 +82,8 @@ class DriveExplorerItemTileLeading extends StatelessWidget { alignment: Alignment.center, child: getIconForContentType( item.contentType, + ).copyWith( + color: isHidden ? Colors.grey : null, ), ), if (item.fileStatusFromTransactions != null) @@ -178,11 +194,6 @@ class DriveExplorerItemTileTrailing extends StatefulWidget { class _DriveExplorerItemTileTrailingState extends State { - @override - void initState() { - super.initState(); - } - @override Widget build(BuildContext context) { final item = widget.item; @@ -232,7 +243,6 @@ class _DriveExplorerItemTileTrailingState target: Alignment.topLeft, ), items: _getItems(widget.item, context), - // ignore: sized_box_for_whitespace child: HoverWidget( tooltip: appLocalizationsOf(context).showMenu, child: ArDriveIcons.kebabMenu(), @@ -243,7 +253,9 @@ class _DriveExplorerItemTileTrailingState } List _getItems( - ArDriveDataTableItem item, BuildContext context) { + ArDriveDataTableItem item, + BuildContext context, + ) { final isOwner = item.isOwner; if (item is FolderDataTableItem) { @@ -304,6 +316,7 @@ class _DriveExplorerItemTileTrailingState ), ), ), + hideFileDropdownItem(context, item), ], ArDriveDropdownItem( onClick: () { @@ -396,6 +409,7 @@ class _DriveExplorerItemTileTrailingState ), ), ), + hideFileDropdownItem(context, item), ], ArDriveDropdownItem( onClick: () { @@ -418,6 +432,7 @@ class _DriveExplorerItemTileTrailingState } } +// TODO: @thiagocarvalhodev remove this and use the AppPlatform class or change the name of the method bool isMobile(BuildContext context) { final isPortrait = MediaQuery.of(context).orientation == Orientation.portrait; return isPortrait; @@ -446,7 +461,6 @@ class EntityActionsMenu extends StatelessWidget { height: isMobile(context) ? 44 : 60, anchor: alignment, items: _getItems(item, context, withInfo), - // ignore: sized_box_for_whitespace child: HoverWidget( tooltip: appLocalizationsOf(context).showMenu, child: ArDriveIcons.dots(), @@ -509,6 +523,7 @@ class EntityActionsMenu extends StatelessWidget { ), ), ), + hideFileDropdownItem(context, item), ], if (withInfo) _buildInfoOption(context), ]; @@ -665,6 +680,7 @@ class EntityActionsMenu extends StatelessWidget { ), ), ), + hideFileDropdownItem(context, item), ], if (withInfo) _buildInfoOption(context) ]; @@ -690,3 +706,25 @@ class EntityActionsMenu extends StatelessWidget { return ArDriveDropdownItemTile(name: name, icon: icon); } } + +ArDriveDropdownItem hideFileDropdownItem( + BuildContext context, + ArDriveDataTableItem item, +) { + return ArDriveDropdownItem( + onClick: () { + promptToToggleHideState( + context, + item: item, + ); + }, + content: ArDriveDropdownItemTile( + name: item.isHidden + ? appLocalizationsOf(context).unhide + : appLocalizationsOf(context).hide, + icon: item.isHidden + ? ArDriveIcons.eyeOpen(size: defaultIconSize) + : ArDriveIcons.eyeClosed(size: defaultIconSize), + ), + ); +} diff --git a/lib/pages/drive_detail/drive_detail_page.dart b/lib/pages/drive_detail/drive_detail_page.dart index 00cb805949..37d98e0659 100644 --- a/lib/pages/drive_detail/drive_detail_page.dart +++ b/lib/pages/drive_detail/drive_detail_page.dart @@ -177,16 +177,29 @@ class _DriveDetailPageState extends State { ), ); } else if (driveDetailState is DriveDetailLoadSuccess) { - final hasSubfolders = - driveDetailState.folderInView.subfolders.isNotEmpty; + final isShowingHiddenFiles = + driveDetailState.isShowingHiddenFiles; + final bool hasSubfolders; + final bool hasFiles; + + if (isShowingHiddenFiles) { + hasSubfolders = + driveDetailState.folderInView.subfolders.isNotEmpty; + hasFiles = driveDetailState.folderInView.files.isNotEmpty; + } else { + hasSubfolders = driveDetailState.folderInView.subfolders + .where((e) => !e.isHidden) + .isNotEmpty; + hasFiles = driveDetailState.folderInView.files + .where((e) => !e.isHidden) + .isNotEmpty; + } final isOwner = isDriveOwner( context.read(), driveDetailState.currentDrive.ownerAddress, ); - final hasFiles = driveDetailState.folderInView.files.isNotEmpty; - final canDownloadMultipleFiles = driveDetailState.multiselect && context.read().selectedItems.isNotEmpty; @@ -219,7 +232,6 @@ class _DriveDetailPageState extends State { driveDetailState, hasSubfolders, hasFiles, - driveDetailState.currentFolderContents, ), ), ); @@ -240,6 +252,10 @@ class _DriveDetailPageState extends State { required bool isDriveOwner, required bool canDownloadMultipleFiles, }) { + final driveDetailCubit = context.read(); + + final isShowingHiddenFiles = driveDetailState.isShowingHiddenFiles; + return Column( children: [ const AppTopBar(), @@ -446,6 +462,26 @@ class _DriveDetailPageState extends State { ), ), ), + ArDriveDropdownItem( + onClick: () { + driveDetailCubit + .toggleHiddenFiles(); + }, + content: _buildItem( + isShowingHiddenFiles + ? appLocalizationsOf(context) + .concealHiddenItems + : appLocalizationsOf(context) + .revealHiddenItems, + isShowingHiddenFiles + ? ArDriveIcons.eyeClosed( + size: defaultIconSize, + ) + : ArDriveIcons.eyeOpen( + size: defaultIconSize, + ), + ), + ), if (!driveDetailState .hasWritePermissions && !isDriveOwner && @@ -489,6 +525,20 @@ class _DriveDetailPageState extends State { child: _buildDataList( context, driveDetailState, + Column( + children: [ + Expanded( + child: DriveDetailFolderEmptyCard( + driveId: driveDetailState + .currentDrive.id, + parentFolderId: driveDetailState + .folderInView.folder.id, + promptToAddFiles: driveDetailState + .hasWritePermissions, + ), + ), + ], + ), ), ), ], @@ -521,20 +571,13 @@ class _DriveDetailPageState extends State { driveDetailState, context), ), child: driveDetailState.showSelectedItemDetails && - context - .read() - .selectedItem != - null + driveDetailState.selectedItem != null ? DetailsPanel( currentDrive: driveDetailState.currentDrive, isSharePage: false, drivePrivacy: driveDetailState.currentDrive.privacy, - maybeSelectedItem: - driveDetailState.maybeSelectedItem(), - item: context - .read() - .selectedItem!, + item: driveDetailState.selectedItem!, onNextImageNavigation: () { context .read() @@ -586,12 +629,13 @@ class _DriveDetailPageState extends State { } Widget _mobileView( - DriveDetailLoadSuccess state, + DriveDetailLoadSuccess driveDetailLoadSuccessState, bool hasSubfolders, bool hasFiles, - List items, ) { - if (state.showSelectedItemDetails && + final items = driveDetailLoadSuccessState.currentFolderContents; + + if (driveDetailLoadSuccessState.showSelectedItemDetails && context.read().selectedItem != null) { return Material( child: WillPopScope( @@ -600,11 +644,10 @@ class _DriveDetailPageState extends State { return false; }, child: DetailsPanel( - currentDrive: state.currentDrive, + currentDrive: driveDetailLoadSuccessState.currentDrive, isSharePage: false, - drivePrivacy: state.currentDrive.privacy, - maybeSelectedItem: state.maybeSelectedItem(), - item: context.read().selectedItem!, + drivePrivacy: driveDetailLoadSuccessState.currentDrive.privacy, + item: driveDetailLoadSuccessState.selectedItem!, onNextImageNavigation: () { context.read().selectNextImage(); }, @@ -626,7 +669,7 @@ class _DriveDetailPageState extends State { .withOpacity(0.5), drawer: const AppSideBar(), appBar: MobileAppBar( - leading: (state.showSelectedItemDetails && + leading: (driveDetailLoadSuccessState.showSelectedItemDetails && context.read().selectedItem != null) ? ArDriveIconButton( icon: ArDriveIcons.arrowLeft(), @@ -652,7 +695,7 @@ class _DriveDetailPageState extends State { }, ), body: _mobileViewContent( - state, + driveDetailLoadSuccessState, hasSubfolders, hasFiles, items, @@ -666,6 +709,16 @@ class _DriveDetailPageState extends State { bool hasFiles, List items, ) { + final isShowingHiddenFiles = state.isShowingHiddenFiles; + + final List filteredItems; + + if (isShowingHiddenFiles) { + filteredItems = items.toList(); + } else { + filteredItems = items.where((item) => item.isHidden == false).toList(); + } + return Column( children: [ Padding( @@ -676,6 +729,7 @@ class _DriveDetailPageState extends State { child: MobileFolderNavigation( driveName: state.currentDrive.name, path: state.folderInView.folder.path, + isShowingHiddenFiles: isShowingHiddenFiles, ), ), ], @@ -693,12 +747,12 @@ class _DriveDetailPageState extends State { separatorBuilder: (context, index) => const SizedBox( height: 5, ), - itemCount: items.length, + itemCount: filteredItems.length, itemBuilder: (context, index) { return ArDriveItemListTile( - key: ObjectKey([items[index]]), + key: ObjectKey([filteredItems[index]]), drive: state.currentDrive, - item: items[index], + item: filteredItems[index], ); }, ) @@ -761,9 +815,10 @@ class ArDriveItemListTile extends StatelessWidget { children: [ Text( item.name, - style: ArDriveTypography.body - .captionRegular() - .copyWith(fontWeight: FontWeight.w700), + style: ArDriveTypography.body.captionRegular().copyWith( + fontWeight: FontWeight.w700, + color: item.isHidden ? Colors.grey : null, + ), ), Row( crossAxisAlignment: CrossAxisAlignment.center, @@ -829,10 +884,13 @@ class ArDriveItemListTile extends StatelessWidget { class MobileFolderNavigation extends StatelessWidget { final String path; final String driveName; + final bool isShowingHiddenFiles; + const MobileFolderNavigation({ super.key, required this.path, required this.driveName, + required this.isShowingHiddenFiles, }); @override @@ -878,6 +936,7 @@ class MobileFolderNavigation extends StatelessWidget { ), BlocBuilder( builder: (context, state) { + final driveDetailCubit = context.read(); if (state is DriveDetailLoadSuccess) { final isOwner = isDriveOwner(context.read(), state.currentDrive.ownerAddress); @@ -967,6 +1026,23 @@ class MobileFolderNavigation extends StatelessWidget { ), ), ), + ArDriveDropdownItem( + onClick: () { + driveDetailCubit.toggleHiddenFiles(); + }, + content: _buildItem( + isShowingHiddenFiles + ? appLocalizationsOf(context).concealHiddenItems + : appLocalizationsOf(context).revealHiddenItems, + isShowingHiddenFiles + ? ArDriveIcons.eyeClosed( + size: defaultIconSize, + ) + : ArDriveIcons.eyeOpen( + size: defaultIconSize, + ), + ), + ), if (!state.hasWritePermissions && !isOwner && context.read().state is ProfileLoggedIn) diff --git a/lib/pages/shared_file/shared_file_page.dart b/lib/pages/shared_file/shared_file_page.dart index b9903cbed6..15abfe2085 100644 --- a/lib/pages/shared_file/shared_file_page.dart +++ b/lib/pages/shared_file/shared_file_page.dart @@ -52,7 +52,6 @@ class SharedFilePage extends StatelessWidget { false, ), isSharePage: true, - maybeSelectedItem: null, fileKey: state.fileKey, revisions: state.fileRevisions, drivePrivacy: state.fileKey != null ? 'private' : 'public', diff --git a/lib/services/arweave/arweave_service.dart b/lib/services/arweave/arweave_service.dart index 026fac935c..5c31db2e1d 100644 --- a/lib/services/arweave/arweave_service.dart +++ b/lib/services/arweave/arweave_service.dart @@ -253,7 +253,7 @@ class ArweaveService { // MAYBE FIX: set a narrow concurrency limit - final List responses = await Future.wait( + final List entityDatas = await Future.wait( entityTxs.map( (entity) async { final tags = entity.tags; @@ -300,8 +300,7 @@ class ArweaveService { try { final entityType = transaction.getTag(EntityTag.entityType); - final entityResponse = responses[i]; - final rawEntityData = entityResponse; + final rawEntityData = entityDatas[i]; await metadataCache.put(transaction.id, rawEntityData); diff --git a/lib/utils/arfs_txs_filter.dart b/lib/utils/arfs_txs_filter.dart index 434d44c8be..8904c0ac07 100644 --- a/lib/utils/arfs_txs_filter.dart +++ b/lib/utils/arfs_txs_filter.dart @@ -1,7 +1,7 @@ import 'package:ardrive_utils/ardrive_utils.dart'; import 'package:arweave/arweave.dart'; -final supportedArFSVersions = ['0.10', '0.11', '0.12', '0.13']; +final supportedArFSVersions = ['0.10', '0.11', '0.12', '0.13', '0.14']; bool doesTagsContainValidArFSVersion(List tags) { return tags.any( diff --git a/lib/utils/non_hidden_items_filter.dart b/lib/utils/non_hidden_items_filter.dart new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/ardrive_uploader/test/metadata_generator_test.dart b/packages/ardrive_uploader/test/metadata_generator_test.dart index 054e31803c..27a1f5a298 100644 --- a/packages/ardrive_uploader/test/metadata_generator_test.dart +++ b/packages/ardrive_uploader/test/metadata_generator_test.dart @@ -43,7 +43,7 @@ void main() { appInfo = AppInfo( version: '2.22.0', platform: 'FlutterTest', - arfsVersion: '0.13', + arfsVersion: '0.14', appName: 'ardrive', ); diff --git a/packages/ardrive_utils/lib/src/app_info_services.dart b/packages/ardrive_utils/lib/src/app_info_services.dart index 44067e5005..b01ea60755 100644 --- a/packages/ardrive_utils/lib/src/app_info_services.dart +++ b/packages/ardrive_utils/lib/src/app_info_services.dart @@ -48,4 +48,4 @@ class AppInfoServices { } const String appName = 'ArDrive-App'; -const String arfsVersion = '0.13'; +const String arfsVersion = '0.14'; diff --git a/test/blocs/fs_entry_move_bloc_test.dart b/test/blocs/fs_entry_move_bloc_test.dart index 6a7934b287..0c040c68e1 100644 --- a/test/blocs/fs_entry_move_bloc_test.dart +++ b/test/blocs/fs_entry_move_bloc_test.dart @@ -65,22 +65,28 @@ void main() { // Create fake root folder for drive and sub folders batch.insertAll(db.folderEntries, [ FolderEntriesCompanion.insert( - id: rootFolderId, - driveId: driveId, - name: 'fake-drive-name', - path: ''), + id: rootFolderId, + driveId: driveId, + name: 'fake-drive-name', + path: '', + isHidden: false, + ), FolderEntriesCompanion.insert( - id: nestedFolderId, - driveId: driveId, - parentFolderId: Value(rootFolderId), - name: nestedFolderId, - path: '/$nestedFolderId'), + id: nestedFolderId, + driveId: driveId, + parentFolderId: Value(rootFolderId), + name: nestedFolderId, + path: '/$nestedFolderId', + isHidden: false, + ), FolderEntriesCompanion.insert( - id: conflictTestFolderId, - driveId: driveId, - parentFolderId: Value(rootFolderId), - name: conflictTestFolderId, - path: '/$conflictTestFolderId'), + id: conflictTestFolderId, + driveId: driveId, + parentFolderId: Value(rootFolderId), + name: conflictTestFolderId, + path: '/$conflictTestFolderId', + isHidden: false, + ), ]); // Insert fake files batch.insertAll( @@ -101,6 +107,7 @@ void main() { dateCreated: Value(defaultDate), lastModifiedDate: defaultDate, dataContentType: const Value(''), + isHidden: false, ); }, ), @@ -119,6 +126,7 @@ void main() { dateCreated: Value(defaultDate), lastModifiedDate: defaultDate, dataContentType: const Value(''), + isHidden: false, ); }, ), @@ -144,6 +152,7 @@ void main() { dateCreated: Value(defaultDate), lastModifiedDate: defaultDate, dataContentType: const Value(''), + isHidden: false, ); }, ), @@ -163,6 +172,7 @@ void main() { dateCreated: Value(defaultDate), lastModifiedDate: defaultDate, dataContentType: const Value(''), + isHidden: false, ); }, ), @@ -271,6 +281,7 @@ void main() { blocTest( 'throws when selectedItems is empty', build: () => FsEntryMoveBloc( + driveDetailCubit: MockDriveDetailCubit(), arweave: arweave, turboUploadService: turboUploadService, syncCubit: syncBloc, @@ -287,6 +298,7 @@ void main() { build: () => FsEntryMoveBloc( crypto: ArDriveCrypto(), arweave: arweave, + driveDetailCubit: MockDriveDetailCubit(), turboUploadService: turboUploadService, syncCubit: syncBloc, driveId: driveId, diff --git a/test/blocs/upload_cubit_test.dart b/test/blocs/upload_cubit_test.dart index 765cc69842..07c9cf8654 100644 --- a/test/blocs/upload_cubit_test.dart +++ b/test/blocs/upload_cubit_test.dart @@ -101,14 +101,16 @@ void main() { registerFallbackValue(Wallet()); registerFallbackValue(getFakeUser()); registerFallbackValue(FolderEntry( - id: '', - dateCreated: tDefaultDate, - driveId: '', - isGhost: false, - lastUpdated: tDefaultDate, - name: '', - parentFolderId: '', - path: '')); + id: '', + dateCreated: tDefaultDate, + driveId: '', + isGhost: false, + lastUpdated: tDefaultDate, + name: '', + parentFolderId: '', + path: '', + isHidden: false, + )); registerFallbackValue(Drive( id: '', @@ -364,7 +366,7 @@ void main() { expect: () => [ const TypeMatcher(), const TypeMatcher(), - const TypeMatcher() + const TypeMatcher() ], ); }); @@ -450,7 +452,7 @@ void main() { const TypeMatcher(), const TypeMatcher(), const TypeMatcher(), - const TypeMatcher(), + const TypeMatcher(), ]); blocTest( 'should not show the warning when file checker not found files above safe limit and emit UploadReady without user confirmation', @@ -470,7 +472,7 @@ void main() { const TypeMatcher(), const TypeMatcher(), const TypeMatcher(), - const TypeMatcher(), + const TypeMatcher(), ]); }, ); @@ -529,7 +531,7 @@ void main() { expect: () => [ UploadPreparationInitialized(), UploadPreparationInProgress(isArConnect: true), - const TypeMatcher() + const TypeMatcher() ], ); blocTest( @@ -549,7 +551,7 @@ void main() { expect: () => [ UploadPreparationInitialized(), UploadPreparationInProgress(isArConnect: false), - const TypeMatcher() + const TypeMatcher() ], ); @@ -672,7 +674,7 @@ void main() { UploadPreparationInitialized(), const TypeMatcher(), const TypeMatcher(), - const TypeMatcher(), + const TypeMatcher(), ], ); @@ -718,7 +720,7 @@ void main() { UploadPreparationInitialized(), const TypeMatcher(), const TypeMatcher(), - const TypeMatcher(), + const TypeMatcher(), ], ); }, diff --git a/test/core/upload/uploader_test.dart b/test/core/upload/uploader_test.dart index 1a81986607..44547d3d11 100644 --- a/test/core/upload/uploader_test.dart +++ b/test/core/upload/uploader_test.dart @@ -313,7 +313,8 @@ void main() { when(() => uploadPlan.bundleUploadHandles).thenReturn([mockBundle]); - final result = await uploadPaymentEvaluator.getUploadPaymentInfo( + final result = + await uploadPaymentEvaluator.getUploadPaymentInfoForUploadPlans( uploadPlanForAR: uploadPlan, uploadPlanForTurbo: uploadPlan, ); @@ -333,7 +334,8 @@ void main() { when(() => uploadPlan.bundleUploadHandles).thenReturn([mockBundle]); - final result = await uploadPaymentEvaluator.getUploadPaymentInfo( + final result = + await uploadPaymentEvaluator.getUploadPaymentInfoForUploadPlans( uploadPlanForAR: uploadPlan, uploadPlanForTurbo: uploadPlan, ); @@ -354,7 +356,8 @@ void main() { when(() => uploadPlan.bundleUploadHandles).thenReturn([mockBundle]); - final result = await uploadPaymentEvaluator.getUploadPaymentInfo( + final result = + await uploadPaymentEvaluator.getUploadPaymentInfoForUploadPlans( uploadPlanForAR: uploadPlan, uploadPlanForTurbo: uploadPlan, ); @@ -383,7 +386,8 @@ void main() { }); when(() => mockV2Upload.size).thenReturn(501); - final result = await uploadPaymentEvaluator.getUploadPaymentInfo( + final result = + await uploadPaymentEvaluator.getUploadPaymentInfoForUploadPlans( uploadPlanForAR: uploadPlan, uploadPlanForTurbo: uploadPlan, ); @@ -410,7 +414,8 @@ void main() { when(() => uploadPlan.bundleUploadHandles).thenReturn([mockBundle]); - final result = await uploadPaymentEvaluator.getUploadPaymentInfo( + final result = + await uploadPaymentEvaluator.getUploadPaymentInfoForUploadPlans( uploadPlanForAR: uploadPlan, uploadPlanForTurbo: uploadPlan, ); @@ -429,7 +434,8 @@ void main() { when(() => uploadPlan.bundleUploadHandles).thenReturn([]); - final result = await uploadPaymentEvaluator.getUploadPaymentInfo( + final result = + await uploadPaymentEvaluator.getUploadPaymentInfoForUploadPlans( uploadPlanForAR: uploadPlan, uploadPlanForTurbo: uploadPlan, ); @@ -462,7 +468,8 @@ void main() { 'fileId': mockV2Upload, }); - final result = await uploadPaymentEvaluator.getUploadPaymentInfo( + final result = + await uploadPaymentEvaluator.getUploadPaymentInfoForUploadPlans( uploadPlanForAR: uploadPlan, uploadPlanForTurbo: uploadPlan, ); @@ -498,7 +505,8 @@ void main() { 'fileId': mockV2Upload, }); - final result = await uploadPaymentEvaluator.getUploadPaymentInfo( + final result = + await uploadPaymentEvaluator.getUploadPaymentInfoForUploadPlans( uploadPlanForAR: uploadPlan, uploadPlanForTurbo: uploadPlan, ); @@ -529,8 +537,8 @@ void main() { when(() => uploadPlan.bundleUploadHandles).thenReturn([mockBundle]); - final result = - await paymentEvaluatorWithFeatureFlagFalse.getUploadPaymentInfo( + final result = await paymentEvaluatorWithFeatureFlagFalse + .getUploadPaymentInfoForUploadPlans( uploadPlanForAR: uploadPlan, uploadPlanForTurbo: uploadPlan, ); @@ -550,7 +558,8 @@ void main() { when(() => turboBalanceRetriever.getBalance(any())) .thenThrow(Exception('error')); - final result = await uploadPaymentEvaluator.getUploadPaymentInfo( + final result = + await uploadPaymentEvaluator.getUploadPaymentInfoForUploadPlans( uploadPlanForAR: uploadPlan, uploadPlanForTurbo: uploadPlan, ); @@ -576,7 +585,8 @@ void main() { when(() => turboUploadCostCalculator.calculateCost( totalSize: any(named: 'totalSize'))).thenThrow(Exception()); - final result = await uploadPaymentEvaluator.getUploadPaymentInfo( + final result = + await uploadPaymentEvaluator.getUploadPaymentInfoForUploadPlans( uploadPlanForAR: uploadPlan, uploadPlanForTurbo: uploadPlan, ); @@ -632,7 +642,8 @@ void main() { when(() => turboBalanceRetriever.getBalance(any())) .thenAnswer((_) async => BigInt.from(500)); - final result = await uploadPaymentEvaluator.getUploadPaymentInfo( + final result = + await uploadPaymentEvaluator.getUploadPaymentInfoForUploadPlans( uploadPlanForAR: uploadPlan, uploadPlanForTurbo: uploadPlan, ); @@ -671,7 +682,13 @@ void main() { when(() => mockFile.computeBundleSize()) .thenAnswer((invocation) => Future.value(501)); - final result = await uploadPaymentEvaluator.getUploadPaymentInfo( + when(() => uploadPlan.bundleUploadHandles).thenReturn([mockBundle]); + + when(() => mockBundle.computeBundleSize()) + .thenAnswer((invocation) => Future.value(501)); + + final result = + await uploadPaymentEvaluator.getUploadPaymentInfoForUploadPlans( uploadPlanForAR: uploadPlan, uploadPlanForTurbo: uploadPlan, ); @@ -692,7 +709,8 @@ void main() { when(() => turboBalanceRetriever.getBalance(any())) .thenAnswer((_) async => BigInt.from(500)); - final result = await uploadPaymentEvaluator.getUploadPaymentInfo( + final result = + await uploadPaymentEvaluator.getUploadPaymentInfoForUploadPlans( uploadPlanForAR: uploadPlan, uploadPlanForTurbo: uploadPlan, ); @@ -728,8 +746,14 @@ void main() { when(() => turboUploadCostCalculator.calculateCost( totalSize: any(named: 'totalSize'))) .thenAnswer((_) async => mockUploadCostEstimateTurbo); + when(() => uploadPlan.bundleUploadHandles).thenReturn([mockBundle]); + when(() => uploadPlan.bundleUploadHandles).thenReturn([mockBundle]); + when(() => mockBundle.computeBundleSize()) + .thenAnswer((invocation) => Future.value(501)); + when(() => mockFile.size).thenReturn(501); - final result = await uploadPaymentEvaluator.getUploadPaymentInfo( + final result = + await uploadPaymentEvaluator.getUploadPaymentInfoForUploadPlans( uploadPlanForAR: uploadPlan, uploadPlanForTurbo: uploadPlan, ); @@ -793,7 +817,7 @@ void main() { )).thenAnswer((_) async => uploadPlan); // Act - final result = await uploadPreparer.prepareUpload(uploadParams); + final result = await uploadPreparer.prepareFileUpload(uploadParams); // Assert expect(result.uploadPlanForAr, uploadPlan); @@ -825,7 +849,7 @@ void main() { )).thenThrow(Exception()); // Assert - expectLater(() => uploadPreparer.prepareUpload(uploadParams), + expectLater(() => uploadPreparer.prepareFileUpload(uploadParams), throwsA(isA())); }); @@ -896,9 +920,9 @@ void main() { turboBalance: BigInt.from(100), ); - when(() => uploadPreparer.prepareUpload(uploadParams)) + when(() => uploadPreparer.prepareFileUpload(uploadParams)) .thenAnswer((_) async => uploadPlansPreparation); - when(() => uploadPaymentEvaluator.getUploadPaymentInfo( + when(() => uploadPaymentEvaluator.getUploadPaymentInfoForUploadPlans( uploadPlanForAR: uploadPlan, uploadPlanForTurbo: uploadPlan)) .thenAnswer((_) async => uploadPaymentInfo); @@ -909,15 +933,15 @@ void main() { // Assert expect(result.uploadPlansPreparation, uploadPlansPreparation); expect(result.uploadPaymentInfo, uploadPaymentInfo); - verify(() => uploadPreparer.prepareUpload(any())).called(1); - verify(() => uploadPaymentEvaluator.getUploadPaymentInfo( + verify(() => uploadPreparer.prepareFileUpload(any())).called(1); + verify(() => uploadPaymentEvaluator.getUploadPaymentInfoForUploadPlans( uploadPlanForAR: any(named: 'uploadPlanForAR'), uploadPlanForTurbo: any(named: 'uploadPlanForTurbo'))).called(1); }); test('Should throw if preparing upload plans fails', () async { // Arrange - when(() => uploadPreparer.prepareUpload(any())) + when(() => uploadPreparer.prepareFileUpload(any())) .thenThrow(Exception('Failed to prepare upload plans')); // Act @@ -929,7 +953,7 @@ void main() { }); test('Should throw if upload prepartion fails', () async { - when(() => uploadPreparer.prepareUpload(uploadParams)) + when(() => uploadPreparer.prepareFileUpload(uploadParams)) .thenThrow(Exception('Failed to prepare upload plans')); final call = @@ -946,9 +970,9 @@ void main() { uploadPlanForTurbo: uploadPlan, ); - when(() => uploadPreparer.prepareUpload(uploadParams)) + when(() => uploadPreparer.prepareFileUpload(uploadParams)) .thenAnswer((_) async => uploadPlansPreparation); - when(() => uploadPaymentEvaluator.getUploadPaymentInfo( + when(() => uploadPaymentEvaluator.getUploadPaymentInfoForUploadPlans( uploadPlanForAR: uploadPlan, uploadPlanForTurbo: uploadPlan)) .thenThrow(Exception('Failed to get upload payment info')); @@ -992,6 +1016,7 @@ FolderEntry getFakeFolder() => FolderEntry( dateCreated: DateTime.now(), lastUpdated: DateTime.now(), isGhost: false, + isHidden: false, ); Drive getFakeDrive() => Drive( diff --git a/test/entities/manifest_data_test.dart b/test/entities/manifest_data_test.dart index a423c47427..a4eb65d853 100644 --- a/test/entities/manifest_data_test.dart +++ b/test/entities/manifest_data_test.dart @@ -16,119 +16,139 @@ void main() { final stubCurrentDate = DateTime.now(); final stubRootFolderEntry = FolderEntry( - id: stubEntityId, - dateCreated: stubCurrentDate, - driveId: stubEntityId, - isGhost: false, - parentFolderId: stubEntityId, - path: '/root-folder', - name: 'root-folder', - lastUpdated: stubCurrentDate); + id: stubEntityId, + dateCreated: stubCurrentDate, + driveId: stubEntityId, + isGhost: false, + parentFolderId: stubEntityId, + path: '/root-folder', + name: 'root-folder', + lastUpdated: stubCurrentDate, + isHidden: false, + ); final stubParentFolderEntry = FolderEntry( - id: stubEntityId, - dateCreated: stubCurrentDate, - driveId: stubEntityId, - isGhost: false, - parentFolderId: stubEntityId, - path: '/root-folder/parent-folder', - name: 'parent-folder', - lastUpdated: stubCurrentDate); + id: stubEntityId, + dateCreated: stubCurrentDate, + driveId: stubEntityId, + isGhost: false, + parentFolderId: stubEntityId, + path: '/root-folder/parent-folder', + name: 'parent-folder', + lastUpdated: stubCurrentDate, + isHidden: false, + ); final stubChildFolderEntry = FolderEntry( - id: stubEntityId, - dateCreated: stubCurrentDate, - driveId: stubEntityId, - isGhost: false, - parentFolderId: stubEntityId, - path: '/root-folder/parent-folder/child-folder', - name: 'child-folder', - lastUpdated: stubCurrentDate); + id: stubEntityId, + dateCreated: stubCurrentDate, + driveId: stubEntityId, + isGhost: false, + parentFolderId: stubEntityId, + path: '/root-folder/parent-folder/child-folder', + name: 'child-folder', + lastUpdated: stubCurrentDate, + isHidden: false, + ); final stubFileInRoot1 = FileEntry( - dataTxId: stubTxId, - dateCreated: stubCurrentDate, - size: 10, - path: '/root-folder/file-in-root-1', - name: 'file-in-root-1', - parentFolderId: stubEntityId, - lastUpdated: stubCurrentDate, - lastModifiedDate: stubCurrentDate, - id: 'file-in-root-1-entity-id', - driveId: stubEntityId); + dataTxId: stubTxId, + dateCreated: stubCurrentDate, + size: 10, + path: '/root-folder/file-in-root-1', + name: 'file-in-root-1', + parentFolderId: stubEntityId, + lastUpdated: stubCurrentDate, + lastModifiedDate: stubCurrentDate, + id: 'file-in-root-1-entity-id', + driveId: stubEntityId, + isHidden: false, + ); final stubFileInRoot2 = FileEntry( - dataTxId: stubTxId, - dateCreated: stubCurrentDate, - size: 10, - path: '/root-folder/file-in-root-2', - name: 'file-in-root-2', - parentFolderId: stubEntityId, - lastUpdated: stubCurrentDate, - lastModifiedDate: stubCurrentDate, - id: 'file-in-root-2-entity-id', - driveId: stubEntityId); + dataTxId: stubTxId, + dateCreated: stubCurrentDate, + size: 10, + path: '/root-folder/file-in-root-2', + name: 'file-in-root-2', + parentFolderId: stubEntityId, + lastUpdated: stubCurrentDate, + lastModifiedDate: stubCurrentDate, + id: 'file-in-root-2-entity-id', + driveId: stubEntityId, + isHidden: false, + ); final stubFileInParent1 = FileEntry( - dataTxId: stubTxId, - dateCreated: stubCurrentDate, - size: 10, - path: '/root-folder/parent-folder/file-in-parent-1', - name: 'file-in-parent-1', - parentFolderId: stubEntityId, - lastUpdated: stubCurrentDate, - lastModifiedDate: stubCurrentDate, - id: 'file-in-parent-1-entity-id', - driveId: stubEntityId); + dataTxId: stubTxId, + dateCreated: stubCurrentDate, + size: 10, + path: '/root-folder/parent-folder/file-in-parent-1', + name: 'file-in-parent-1', + parentFolderId: stubEntityId, + lastUpdated: stubCurrentDate, + lastModifiedDate: stubCurrentDate, + id: 'file-in-parent-1-entity-id', + driveId: stubEntityId, + isHidden: false, + ); final stubFileInParent2 = FileEntry( - dataTxId: stubTxId, - dateCreated: stubCurrentDate, - size: 10, - path: '/root-folder/parent-folder/file-in-parent-2', - name: 'file-in-parent-2', - parentFolderId: stubEntityId, - lastUpdated: stubCurrentDate, - lastModifiedDate: stubCurrentDate, - id: 'file-in-parent-2-entity-id', - driveId: stubEntityId); + dataTxId: stubTxId, + dateCreated: stubCurrentDate, + size: 10, + path: '/root-folder/parent-folder/file-in-parent-2', + name: 'file-in-parent-2', + parentFolderId: stubEntityId, + lastUpdated: stubCurrentDate, + lastModifiedDate: stubCurrentDate, + id: 'file-in-parent-2-entity-id', + driveId: stubEntityId, + isHidden: false, + ); final stubFileInChild1 = FileEntry( - dataTxId: stubTxId, - dateCreated: stubCurrentDate, - size: 10, - path: '/root-folder/parent-folder/child-folder/file-in-child-1', - name: 'file-in-child-1', - parentFolderId: stubEntityId, - lastUpdated: stubCurrentDate, - lastModifiedDate: stubCurrentDate, - id: 'file-in-child-1-entity-id', - driveId: stubEntityId); + dataTxId: stubTxId, + dateCreated: stubCurrentDate, + size: 10, + path: '/root-folder/parent-folder/child-folder/file-in-child-1', + name: 'file-in-child-1', + parentFolderId: stubEntityId, + lastUpdated: stubCurrentDate, + lastModifiedDate: stubCurrentDate, + id: 'file-in-child-1-entity-id', + driveId: stubEntityId, + isHidden: false, + ); final stubFileInChild2 = FileEntry( - dataTxId: stubTxId, - dateCreated: stubCurrentDate, - size: 10, - path: '/root-folder/parent-folder/child-folder/file-in-child-2', - name: 'file-in-child-2', - parentFolderId: stubEntityId, - lastUpdated: stubCurrentDate, - lastModifiedDate: stubCurrentDate, - id: 'file-in-child-2-entity-id', - driveId: stubEntityId); + dataTxId: stubTxId, + dateCreated: stubCurrentDate, + size: 10, + path: '/root-folder/parent-folder/child-folder/file-in-child-2', + name: 'file-in-child-2', + parentFolderId: stubEntityId, + lastUpdated: stubCurrentDate, + lastModifiedDate: stubCurrentDate, + id: 'file-in-child-2-entity-id', + driveId: stubEntityId, + isHidden: false, + ); final stubManifestFileInChild = FileEntry( - dataTxId: stubTxId, - dateCreated: stubCurrentDate, - size: 10, - path: '/root-folder/parent-folder/child-folder/file-in-child-2', - name: 'manifest-file-in-child', - parentFolderId: stubEntityId, - lastUpdated: stubCurrentDate, - lastModifiedDate: stubCurrentDate, - id: 'manifest-file-in-child-entity-id', - driveId: stubEntityId, - dataContentType: ContentType.manifest); + dataTxId: stubTxId, + dateCreated: stubCurrentDate, + size: 10, + path: '/root-folder/parent-folder/child-folder/file-in-child-2', + name: 'manifest-file-in-child', + parentFolderId: stubEntityId, + lastUpdated: stubCurrentDate, + lastModifiedDate: stubCurrentDate, + id: 'manifest-file-in-child-entity-id', + driveId: stubEntityId, + dataContentType: ContentType.manifest, + isHidden: false, + ); final stubChildFolderNode = FolderNode(folder: stubChildFolderEntry, subfolders: [], files: { diff --git a/test/entities/snapshot_entity_test.dart b/test/entities/snapshot_entity_test.dart index fae722a127..e338e0fce3 100644 --- a/test/entities/snapshot_entity_test.dart +++ b/test/entities/snapshot_entity_test.dart @@ -89,7 +89,7 @@ void main() { expect(transaction.tags.length, 8); expect(decodeBase64ToString(transaction.tags[0].name), equals('ArFS')); - expect(decodeBase64ToString(transaction.tags[0].value), equals('0.13')); + expect(decodeBase64ToString(transaction.tags[0].value), equals('0.14')); expect(decodeBase64ToString(transaction.tags[1].name), equals('Entity-Type')); expect(decodeBase64ToString(transaction.tags[1].value), diff --git a/test/models/daos/drive_dao_test.dart b/test/models/daos/drive_dao_test.dart index 3736e3f2f2..fa5f6a2d8c 100644 --- a/test/models/daos/drive_dao_test.dart +++ b/test/models/daos/drive_dao_test.dart @@ -41,8 +41,10 @@ void main() { }); // Any empty string is a root path test("watchFolder() with root path ('') returns root folder", () async { - final folderStream = - driveDao.watchFolderContents(driveId, folderPath: rootPath); + final folderStream = driveDao.watchFolderContents( + driveId, + folderPath: rootPath, + ); await Future.wait([ expectLater(folderStream.map((f) => f.folder.id), emits(rootFolderId)), @@ -50,8 +52,10 @@ void main() { }); test('watchFolder() returns correct number of files in root folder', () async { - final folderStream = - driveDao.watchFolderContents(driveId, folderPath: rootPath); + final folderStream = driveDao.watchFolderContents( + driveId, + folderPath: rootPath, + ); await Future.wait([ expectLater( @@ -64,8 +68,10 @@ void main() { }); test('watchFolder() returns correct number of folders in root folder', () async { - final folderStream = - driveDao.watchFolderContents(driveId, folderPath: rootPath); + final folderStream = driveDao.watchFolderContents( + driveId, + folderPath: rootPath, + ); await Future.wait([ expectLater( @@ -78,8 +84,10 @@ void main() { test('watchFolder() with subfolder path returns correct subfolder', () async { - final folderStream = driveDao.watchFolderContents(driveId, - folderPath: '/$emptyNestedFolderIdPrefix' '0'); + final folderStream = driveDao.watchFolderContents( + driveId, + folderPath: '/$emptyNestedFolderIdPrefix' '0', + ); await Future.wait([ expectLater(folderStream.map((f) => f.folder.id), @@ -87,8 +95,10 @@ void main() { ]); }); test('watchFolder() returns correct folders inside empty folder', () async { - final folderStream = driveDao.watchFolderContents(driveId, - folderPath: '/$emptyNestedFolderIdPrefix' '0'); + final folderStream = driveDao.watchFolderContents( + driveId, + folderPath: '/$emptyNestedFolderIdPrefix' '0', + ); await Future.wait([ expectLater( @@ -98,8 +108,10 @@ void main() { ]); }); test('watchFolder() returns correct files inside empty folder', () async { - final folderStream = driveDao.watchFolderContents(driveId, - folderPath: '/$emptyNestedFolderIdPrefix' '0'); + final folderStream = driveDao.watchFolderContents( + driveId, + folderPath: '/$emptyNestedFolderIdPrefix' '0', + ); await Future.wait([ expectLater( diff --git a/test/test_utils/mocks.dart b/test/test_utils/mocks.dart index 5d18070bca..5043b94301 100644 --- a/test/test_utils/mocks.dart +++ b/test/test_utils/mocks.dart @@ -202,22 +202,23 @@ FileDataTableItem createMockFileDataTableItem( index = 0, isOwner = true}) { return FileDataTableItem( - fileId: fileId, - driveId: driveId, - parentFolderId: parentFolderId, - dataTxId: dataTxId, - lastUpdated: lastUpdated ?? DateTime.now(), - lastModifiedDate: lastModifiedDate ?? DateTime.now(), - metadataTx: metadataTx, - dataTx: dataTx, - name: name, - size: size, - dateCreated: dateCreated ?? DateTime.now(), - contentType: 'contentType', - path: path, - index: index, - pinnedDataOwnerAddress: pinnedDataOwnerAddress, - isOwner: isOwner); + fileId: fileId, + driveId: driveId, + parentFolderId: parentFolderId, + dataTxId: dataTxId, + lastUpdated: lastUpdated ?? DateTime.now(), + lastModifiedDate: lastModifiedDate ?? DateTime.now(), + metadataTx: metadataTx, + dataTx: dataTx, + name: name, + size: size, + dateCreated: dateCreated ?? DateTime.now(), + contentType: 'contentType', + path: path, + index: index, + pinnedDataOwnerAddress: pinnedDataOwnerAddress, + isOwner: isOwner, + ); } FolderDataTableItem createMockFolderDataTableItem( @@ -237,18 +238,19 @@ FolderDataTableItem createMockFolderDataTableItem( index = 0, isOwner = true}) { return FolderDataTableItem( - driveId: driveId, - folderId: folderId, - name: name, - lastUpdated: lastUpdated ?? DateTime.now(), - dateCreated: dateCreated ?? DateTime.now(), - contentType: contentType, - path: path, - fileStatusFromTransactions: fileStatusFromTransactions, - parentFolderId: parentFolderId, - isGhostFolder: isGhostFolder, - index: index, - isOwner: isOwner); + driveId: driveId, + folderId: folderId, + name: name, + lastUpdated: lastUpdated ?? DateTime.now(), + dateCreated: dateCreated ?? DateTime.now(), + contentType: contentType, + path: path, + fileStatusFromTransactions: fileStatusFromTransactions, + parentFolderId: parentFolderId, + isGhostFolder: isGhostFolder, + index: index, + isOwner: isOwner, + ); } DriveDataItem createMockDriveDataItem( @@ -260,13 +262,14 @@ DriveDataItem createMockDriveDataItem( index = 0, isOwner = true}) { return DriveDataItem( - id: id, - driveId: driveId, - name: name, - lastUpdated: lastUpdated ?? DateTime.now(), - dateCreated: dateCreated ?? DateTime.now(), - index: index, - isOwner: isOwner); + id: id, + driveId: driveId, + name: name, + lastUpdated: lastUpdated ?? DateTime.now(), + dateCreated: dateCreated ?? DateTime.now(), + index: index, + isOwner: isOwner, + ); } FolderEntry createMockFolderEntry( @@ -290,6 +293,7 @@ FolderEntry createMockFolderEntry( path: path, parentFolderId: parentFolderId, isGhost: isGhost, + isHidden: false, ); } @@ -323,5 +327,6 @@ FileEntry createMockFileEntry( path: path, parentFolderId: parentFolderId, bundledIn: bundledIn, + isHidden: false, ); } diff --git a/test/test_utils/utils.dart b/test/test_utils/utils.dart index 237463bc7b..4d871a338a 100644 --- a/test/test_utils/utils.dart +++ b/test/test_utils/utils.dart @@ -62,16 +62,20 @@ Future addTestFilesToDb( db.folderEntries, [ FolderEntriesCompanion.insert( - id: rootFolderId, - driveId: driveId, - name: 'fake-drive-name', - path: ''), + id: rootFolderId, + driveId: driveId, + name: 'fake-drive-name', + path: '', + isHidden: false, + ), FolderEntriesCompanion.insert( - id: nestedFolderId, - driveId: driveId, - parentFolderId: Value(rootFolderId), - name: nestedFolderId, - path: '/$nestedFolderId'), + id: nestedFolderId, + driveId: driveId, + parentFolderId: Value(rootFolderId), + name: nestedFolderId, + path: '/$nestedFolderId', + isHidden: false, + ), ...List.generate( emptyNestedFolderCount, (i) { @@ -82,6 +86,7 @@ Future addTestFilesToDb( parentFolderId: Value(rootFolderId), name: folderId, path: '/$folderId', + isHidden: false, ); }, )..shuffle(Random(0)), @@ -106,6 +111,7 @@ Future addTestFilesToDb( dateCreated: Value(defaultDate), lastModifiedDate: defaultDate, dataContentType: const Value(''), + isHidden: false, ); }, )..shuffle(Random(0)), @@ -124,6 +130,7 @@ Future addTestFilesToDb( dateCreated: Value(defaultDate), lastModifiedDate: defaultDate, dataContentType: const Value(''), + isHidden: false, ); }, )..shuffle(Random(0)), @@ -149,6 +156,7 @@ Future addTestFilesToDb( dateCreated: Value(defaultDate), lastModifiedDate: defaultDate, dataContentType: const Value(''), + isHidden: false, ); }, )..shuffle(Random(0)), @@ -168,6 +176,7 @@ Future addTestFilesToDb( dateCreated: Value(defaultDate), lastModifiedDate: defaultDate, dataContentType: const Value(''), + isHidden: false, ); }, )..shuffle(Random(0)), diff --git a/test/utils/arfs_txs_filter_test.dart b/test/utils/arfs_txs_filter_test.dart index d7eeeaa066..235a731b07 100644 --- a/test/utils/arfs_txs_filter_test.dart +++ b/test/utils/arfs_txs_filter_test.dart @@ -12,6 +12,7 @@ void main() { Tag(arFsTagName, '0.11'), Tag(arFsTagName, '0.12'), Tag(arFsTagName, '0.13'), + Tag(arFsTagName, '0.14'), ]; for (final tag in tags) { @@ -23,7 +24,6 @@ void main() { () { final tags = [ Tag(arFsTagName, '0.9'), - Tag(arFsTagName, '0.14'), Tag(arFsTagName, '0.15'), Tag(arFsTagName, '0.16'), Tag(arFsTagName, 'Supercalifragilisticoespialidoso'), diff --git a/test/utils/link_generators_test.dart b/test/utils/link_generators_test.dart index 581bd3321f..7d92c1652a 100644 --- a/test/utils/link_generators_test.dart +++ b/test/utils/link_generators_test.dart @@ -94,6 +94,7 @@ void main() { lastModifiedDate: DateTime.now(), lastUpdated: DateTime.now(), dataContentType: '', + isHidden: false, ); testFileKeyBase64 = 'X123YZAB-CD4e5fgHIjKlmN6O7pqrStuVwxYzaBcd8E'; testFileKey = SecretKey(decodeBase64ToBytes(testFileKeyBase64));