diff --git a/android/fastlane/metadata/android/en-US/changelogs/159.txt b/android/fastlane/metadata/android/en-US/changelogs/159.txt new file mode 100644 index 0000000000..a834a23d28 --- /dev/null +++ b/android/fastlane/metadata/android/en-US/changelogs/159.txt @@ -0,0 +1,8 @@ +- New feature: Added option to assign an ArNS name during manifest auto-updates in the upload flow. +- New feature: Introduced hide/unhide feature for drives in the sidebar for improved organization. +- Introduced global toggle to show or hide all hidden drives, folders, and files +- Enhanced sidebar persistence during navigation and syncing. +- Improved details panel to retain state when switching views. +- Implemented memory of the last opened drive for quicker access. +- Updated share drive modal with new styles, colors, and copy button with icon. +- Removed feedback survey modal after uploads for a smoother experience. diff --git a/lib/app_shell.dart b/lib/app_shell.dart index 593e250891..b759fdc55d 100644 --- a/lib/app_shell.dart +++ b/lib/app_shell.dart @@ -84,12 +84,12 @@ class AppShellState extends State { } } }, - builder: (context, syncState) => syncState is SyncInProgress - ? Stack( + builder: (context, syncState) { + return Stack(children: [ + scaffold, + if (syncState is SyncInProgress) + Stack( children: [ - AbsorbPointer( - child: scaffold, - ), SizedBox.expand( child: Container( color: Colors.black.withOpacity(0.5), @@ -181,8 +181,9 @@ class AppShellState extends State { ), ), ], - ) - : scaffold, + ), + ]); + }, ), ); return ScreenTypeLayout.builder( @@ -367,6 +368,8 @@ class MobileAppBar extends StatelessWidget implements PreferredSizeWidget { ), ), const Spacer(), + const GlobalHideToggleButton(), + const SizedBox(width: 8), const SyncButton(), const SizedBox( width: 24, diff --git a/lib/authentication/ardrive_auth.dart b/lib/authentication/ardrive_auth.dart index 4266983045..0918bab9bb 100644 --- a/lib/authentication/ardrive_auth.dart +++ b/lib/authentication/ardrive_auth.dart @@ -236,12 +236,12 @@ class ArDriveAuthImpl implements ArDriveAuth { await _secureKeyValueStore.remove('biometricEnabled'); currentUser = null; await _disconnectFromArConnect(); - _userStreamController.add(null); } await _userRepository.deleteUser(); await _databaseHelpers.deleteAllTables(); - await (await _metadataCache).clear(); + (await _metadataCache).clear(); + _userStreamController.add(null); } catch (e, stacktrace) { logger.e('Failed to logout user', e, stacktrace); throw AuthenticationFailedException('Failed to logout user'); diff --git a/lib/blocs/create_manifest/create_manifest_cubit.dart b/lib/blocs/create_manifest/create_manifest_cubit.dart index f4e2775162..02532bb9bd 100644 --- a/lib/blocs/create_manifest/create_manifest_cubit.dart +++ b/lib/blocs/create_manifest/create_manifest_cubit.dart @@ -347,7 +347,7 @@ class CreateManifestCubit extends Cubit { return ARNSUndername( name: '@', domain: _selectedAntRecord!.domain, - record: ARNSRecord( + record: const ARNSRecord( transactionId: 'to_assign', ttlSeconds: 3600, ), diff --git a/lib/blocs/drive_detail/drive_detail_cubit.dart b/lib/blocs/drive_detail/drive_detail_cubit.dart index dcd6d765c8..46b7948e07 100644 --- a/lib/blocs/drive_detail/drive_detail_cubit.dart +++ b/lib/blocs/drive_detail/drive_detail_cubit.dart @@ -26,7 +26,7 @@ import 'package:rxdart/rxdart.dart'; part 'drive_detail_state.dart'; class DriveDetailCubit extends Cubit { - final String driveId; + String _driveId; final ProfileCubit _profileCubit; final DriveDao _driveDao; final ConfigService _configService; @@ -49,10 +49,8 @@ class DriveDetailCubit extends Cubit { bool _refreshSelectedItem = false; - bool _showHiddenFiles = false; - DriveDetailCubit({ - required this.driveId, + required String driveId, String? initialFolderId, required ProfileCubit profileCubit, required DriveDao driveDao, @@ -70,6 +68,7 @@ class DriveDetailCubit extends Cubit { _breadcrumbBuilder = breadcrumbBuilder, _syncCubit = syncCubit, _driveRepository = driveRepository, + _driveId = driveId, super(DriveDetailLoadInProgress()) { if (driveId.isEmpty) { return; @@ -95,15 +94,32 @@ class DriveDetailCubit extends Cubit { } } - void toggleHiddenFiles() { - _showHiddenFiles = !_showHiddenFiles; + void showEmptyDriveDetail() async { + await _syncCubit.waitCurrentSync(); + + emit(DriveDetailLoadEmpty()); + } + + Future changeDrive(String driveId) async { + final drive = await _driveDao.driveById(driveId: driveId).getSingleOrNull(); + + if (drive == null) { + await _syncCubit.waitCurrentSync(); + emit(DriveDetailLoadNotFound()); + return; + } + + await _folderSubscription?.cancel(); - refreshDriveDataTable(); + _driveId = driveId; + + openFolder(folderId: drive.rootFolderId); } - void openFolder({ + Future openFolder({ String? folderId, String? otherDriveId, + String? selectedItemId, DriveOrder contentOrderBy = DriveOrder.name, OrderingMode contentOrderingMode = OrderingMode.asc, }) async { @@ -111,10 +127,9 @@ class DriveDetailCubit extends Cubit { await _syncCubit.waitCurrentSync(); try { - _selectedItem = null; _allImagesOfCurrentFolder = null; - String driveId = otherDriveId ?? this.driveId; + String driveId = otherDriveId ?? _driveId; emit(DriveDetailLoadInProgress()); @@ -131,6 +146,14 @@ class DriveDetailCubit extends Cubit { ), _profileCubit.stream.startWith(ProfileCheckingAvailability()), (drive, folderContents, _) async { + if (isClosed) { + return; + } + + if (driveId != _driveId) { + return; + } + await _syncCubit.waitCurrentSync(); if (drive == null) { @@ -195,6 +218,12 @@ class DriveDetailCubit extends Cubit { isOwner: isDriveOwner(_auth, drive.ownerAddress), ); + if (selectedItemId != null) { + _selectedItem = currentFolderContents.firstWhere( + (element) => element.id == selectedItemId, + ); + } + final List pathSegments = await _breadcrumbBuilder.buildForFolder( folderId: folderContents.folder.id, @@ -215,10 +244,10 @@ class DriveDetailCubit extends Cubit { rowsPerPage: availableRowsPerPage.first, availableRowsPerPage: availableRowsPerPage, currentFolderContents: currentFolderContents, - isShowingHiddenFiles: _showHiddenFiles, pathSegments: pathSegments, driveIsEmpty: folderContents.files.isEmpty && folderContents.subfolders.isEmpty, + showSelectedItemDetails: _selectedItem != null, ), ); } else { @@ -241,7 +270,7 @@ class DriveDetailCubit extends Cubit { multiselect: false, currentFolderContents: currentFolderContents, columnVisibility: columnsVisibility, - isShowingHiddenFiles: _showHiddenFiles, + showSelectedItemDetails: _selectedItem != null, ), ); } @@ -264,6 +293,8 @@ class DriveDetailCubit extends Cubit { logger.e('An error occured mouting the drive explorer', e); }); + + await _folderSubscription?.asFuture(); } List parseEntitiesToDatatableItem({ @@ -447,37 +478,44 @@ class DriveDetailCubit extends Cubit { final state = this.state as DriveDetailLoadSuccess; emit(state.copyWith( forceRebuildKey: UniqueKey(), - isShowingHiddenFiles: _showHiddenFiles, )); } } - bool canNavigateThroughImages() { - final numberOfImages = getAllImagesOfCurrentFolder().length; + bool canNavigateThroughImages(bool showHiddenImages) { + final numberOfImages = getAllImagesOfCurrentFolder(showHiddenImages).length; return numberOfImages > 1; } - Future selectNextImage() => _selectImageRelativeToCurrent(1); - Future selectPreviousImage() => _selectImageRelativeToCurrent(-1); + Future selectNextImage(bool showHiddenImages) => + _selectImageRelativeToCurrent(1, showHiddenImages); + Future selectPreviousImage(bool showHiddenImages) => + _selectImageRelativeToCurrent(-1, showHiddenImages); - Future _selectImageRelativeToCurrent(int offset) async { - final currentIndex = getIndexForImage(_selectedItem as FileDataTableItem); + Future _selectImageRelativeToCurrent( + int offset, bool showHiddenImages) async { + final currentIndex = getIndexForImage( + _selectedItem as FileDataTableItem, + showHiddenImages, + ); final nextIndex = currentIndex + offset; - final nextImage = getImageForIndex(nextIndex); + final nextImage = getImageForIndex(nextIndex, showHiddenImages); await selectDataItem(nextImage); } - FileDataTableItem getImageForIndex(int index) { - final allImagesOfCurrentFolder = getAllImagesOfCurrentFolder(); + FileDataTableItem getImageForIndex(int index, bool showHiddenImages) { + final allImagesOfCurrentFolder = + getAllImagesOfCurrentFolder(showHiddenImages); final cyclicIndex = index % allImagesOfCurrentFolder.length; final image = allImagesOfCurrentFolder[cyclicIndex]; return image; } - int getIndexForImage(FileDataTableItem image) { - final allImagesOfCurrentFolder = getAllImagesOfCurrentFolder(); + int getIndexForImage(FileDataTableItem image, bool showHiddenImages) { + final allImagesOfCurrentFolder = + getAllImagesOfCurrentFolder(showHiddenImages); final index = allImagesOfCurrentFolder.indexWhere( (element) => element.id == image.id, ); @@ -485,15 +523,13 @@ class DriveDetailCubit extends Cubit { return index; } - List getAllImagesOfCurrentFolder() { + List getAllImagesOfCurrentFolder(bool showHiddenImages) { if (_allImagesOfCurrentFolder != null) { return _allImagesOfCurrentFolder!; } final state = this.state as DriveDetailLoadSuccess; - final isShowingHiddenFiles = state.isShowingHiddenFiles; - final List allImagesForFolder = state.currentFolderContents.whereType().where( (element) { @@ -502,7 +538,7 @@ class DriveDetailCubit extends Cubit { ); return supportedImageType && - (isShowingHiddenFiles ? true : !element.isHidden); + (showHiddenImages ? true : !element.isHidden); }, ).toList(); diff --git a/lib/blocs/drive_detail/drive_detail_state.dart b/lib/blocs/drive_detail/drive_detail_state.dart index d6f5178517..ae1d57df2f 100644 --- a/lib/blocs/drive_detail/drive_detail_state.dart +++ b/lib/blocs/drive_detail/drive_detail_state.dart @@ -41,8 +41,6 @@ class DriveDetailLoadSuccess extends DriveDetailState { final Map columnVisibility; final Key? forceRebuildKey; - final bool isShowingHiddenFiles; - DriveDetailLoadSuccess({ required this.currentDrive, required this.hasWritePermissions, @@ -61,7 +59,6 @@ class DriveDetailLoadSuccess extends DriveDetailState { required this.currentFolderContents, required this.columnVisibility, this.forceRebuildKey, - required this.isShowingHiddenFiles, required this.pathSegments, this.selectedPage, }); @@ -83,7 +80,6 @@ class DriveDetailLoadSuccess extends DriveDetailState { ArDriveDataTableItem? selectedItem, List? currentFolderContents, Key? forceRebuildKey, - bool? isShowingHiddenFiles, List? pathSegments, int? selectedPage, }) => @@ -109,7 +105,6 @@ class DriveDetailLoadSuccess extends DriveDetailState { driveIsEmpty: driveIsEmpty ?? this.driveIsEmpty, currentFolderContents: currentFolderContents ?? this.currentFolderContents, - isShowingHiddenFiles: isShowingHiddenFiles ?? this.isShowingHiddenFiles, pathSegments: pathSegments ?? this.pathSegments, ); @@ -119,7 +114,6 @@ class DriveDetailLoadSuccess extends DriveDetailState { hasWritePermissions, folderInView, currentFolderContents, - isShowingHiddenFiles, contentOrderBy, contentOrderingMode, showSelectedItemDetails, @@ -144,4 +138,6 @@ class DriveDetailLoadSuccess extends DriveDetailState { /// the user's profile. class DriveDetailLoadNotFound extends DriveDetailState {} +class DriveDetailLoadEmpty extends DriveDetailState {} + class DriveInitialLoading extends DriveDetailState {} diff --git a/lib/blocs/drives/drives_cubit.dart b/lib/blocs/drives/drives_cubit.dart index 21e2405070..dad88a7a95 100644 --- a/lib/blocs/drives/drives_cubit.dart +++ b/lib/blocs/drives/drives_cubit.dart @@ -6,6 +6,7 @@ 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/core/activity_tracker.dart'; import 'package:ardrive/models/models.dart'; +import 'package:ardrive/user/repositories/user_preferences_repository.dart'; import 'package:ardrive/utils/user_utils.dart'; import 'package:ardrive_utils/ardrive_utils.dart'; import 'package:drift/drift.dart'; @@ -22,6 +23,7 @@ class DrivesCubit extends Cubit { final PromptToSnapshotBloc _promptToSnapshotBloc; final DriveDao _driveDao; final ArDriveAuth _auth; + final UserPreferencesRepository _userPreferencesRepository; late StreamSubscription _drivesSubscription; String? initialSelectedDriveId; @@ -32,10 +34,12 @@ class DrivesCubit extends Cubit { required PromptToSnapshotBloc promptToSnapshotBloc, required DriveDao driveDao, required ActivityTracker activityTracker, + required UserPreferencesRepository userPreferencesRepository, }) : _profileCubit = profileCubit, _promptToSnapshotBloc = promptToSnapshotBloc, _driveDao = driveDao, _auth = auth, + _userPreferencesRepository = userPreferencesRepository, super(DrivesLoadInProgress()) { _auth.onAuthStateChanged().listen((user) { if (user == null) { @@ -66,11 +70,28 @@ class DrivesCubit extends Cubit { String? selectedDriveId; - if (state is DrivesLoadSuccess && state.selectedDriveId != null) { + if (state is DrivesLoadSuccess) { selectedDriveId = state.selectedDriveId; - } else { - selectedDriveId = initialSelectedDriveId ?? - (drives.isNotEmpty ? drives.first.id : null); + } + + if (selectedDriveId == null) { + if (initialSelectedDriveId != null && + initialSelectedDriveId!.isNotEmpty) { + selectedDriveId = initialSelectedDriveId; + } else { + final userPreferences = await _userPreferencesRepository.load(); + + final userHasHiddenDrive = drives.any((d) => d.isHidden); + await _userPreferencesRepository + .saveUserHasHiddenItem(userHasHiddenDrive); + + selectedDriveId = userPreferences.lastSelectedDriveId; + + if (selectedDriveId == null || + !drives.any((d) => d.id == selectedDriveId)) { + selectedDriveId = drives.isNotEmpty ? drives.first.id : null; + } + } } final walletAddress = profileState is ProfileLoggedIn @@ -118,6 +139,8 @@ class DrivesCubit extends Cubit { ); _promptToSnapshotBloc.add(const SelectedDrive(driveId: null)); } + + _userPreferencesRepository.saveLastSelectedDriveId(driveId); emit(state); } diff --git a/lib/blocs/fs_entry_info/fs_entry_info_cubit.dart b/lib/blocs/fs_entry_info/fs_entry_info_cubit.dart index aeed22a476..8488e7f85e 100644 --- a/lib/blocs/fs_entry_info/fs_entry_info_cubit.dart +++ b/lib/blocs/fs_entry_info/fs_entry_info_cubit.dart @@ -39,6 +39,10 @@ class FsEntryInfoCubit extends Cubit { .asStream() .listen( (f) async { + if (isClosed) { + return; + } + final metadataTxId = await _driveDao .latestFolderRevisionByFolderId( driveId: driveId, folderId: selectedItem.id) @@ -132,6 +136,10 @@ class FsEntryInfoCubit extends Cubit { .watchSingle() .listen( (d) async { + if (isClosed) { + return; + } + final rootFolderRevision = await _driveDao .latestFolderRevisionByFolderId( folderId: d.rootFolderId, 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 b308b48a55..aa880c5954 100644 --- a/lib/blocs/fs_entry_move/fs_entry_move_bloc.dart +++ b/lib/blocs/fs_entry_move/fs_entry_move_bloc.dart @@ -28,7 +28,6 @@ class FsEntryMoveBloc extends Bloc { final DriveDao _driveDao; final ProfileCubit _profileCubit; final ArDriveCrypto _crypto; - final DriveDetailCubit _driveDetailCubit; FsEntryMoveBloc({ required this.driveId, @@ -39,14 +38,12 @@ class FsEntryMoveBloc extends Bloc { required ProfileCubit profileCubit, required SyncCubit syncCubit, required ArDriveCrypto crypto, - required DriveDetailCubit driveDetailCubit, Platform platform = const LocalPlatform(), }) : _selectedItems = List.from(selectedItems, growable: false), _arweave = arweave, _turboUploadService = turboUploadService, _driveDao = driveDao, _profileCubit = profileCubit, - _driveDetailCubit = driveDetailCubit, _crypto = crypto, super(const FsEntryMoveLoadInProgress()) { if (_selectedItems.isEmpty) { @@ -82,6 +79,7 @@ class FsEntryMoveBloc extends Bloc { conflictingItems: conflictingItems, profile: profile, parentFolder: folderInView, + showHiddenItems: event.showHiddenItems, ); } catch (err, stacktrace) { // TODO: we must handle this error better. Currently, if an error occurs, it will emit the success state anyway. @@ -106,6 +104,7 @@ class FsEntryMoveBloc extends Bloc { parentFolder: folderInView, conflictingItems: event.conflictingItems, profile: profile, + showHiddenItems: event.showHiddenItems, ); emit(const FsEntryMoveSuccess()); } @@ -178,16 +177,14 @@ class FsEntryMoveBloc extends Bloc { required FolderEntry parentFolder, List conflictingItems = const [], required ProfileLoggedIn profile, + required bool showHiddenItems, }) async { final driveKey = await _driveDao.getDriveKey(driveId, profile.user.cipherKey); final moveTxDataItems = []; - final isShowingHiddenItems = - (_driveDetailCubit.state as DriveDetailLoadSuccess) - .isShowingHiddenFiles; final files = _selectedItems.whereType().toList(); - if (!isShowingHiddenItems) { + if (!showHiddenItems) { files.removeWhere((element) => element.isHidden); } @@ -201,7 +198,7 @@ class FsEntryMoveBloc extends Bloc { final folders = _selectedItems.whereType().toList(); - if (!isShowingHiddenItems) { + if (!showHiddenItems) { folders.removeWhere((element) => element.isHidden); } diff --git a/lib/blocs/fs_entry_move/fs_entry_move_event.dart b/lib/blocs/fs_entry_move/fs_entry_move_event.dart index dd81d9e4cc..f31705891f 100644 --- a/lib/blocs/fs_entry_move/fs_entry_move_event.dart +++ b/lib/blocs/fs_entry_move/fs_entry_move_event.dart @@ -27,20 +27,24 @@ class FsEntryMoveGoBackToParent extends FsEntryMoveEvent { class FsEntryMoveSubmit extends FsEntryMoveEvent { final FolderEntry folderInView; + final bool showHiddenItems; const FsEntryMoveSubmit({ required this.folderInView, + required this.showHiddenItems, }) : super(); @override - List get props => [folderInView]; + List get props => [folderInView, showHiddenItems]; } class FsEntryMoveSkipConflicts extends FsEntryMoveEvent { final FolderEntry folderInView; final List conflictingItems; + final bool showHiddenItems; const FsEntryMoveSkipConflicts({ required this.folderInView, required this.conflictingItems, + required this.showHiddenItems, }) : super(); @override - List get props => [folderInView, conflictingItems]; + List get props => [folderInView, conflictingItems, showHiddenItems]; } diff --git a/lib/blocs/hide/global_hide_bloc.dart b/lib/blocs/hide/global_hide_bloc.dart new file mode 100644 index 0000000000..fc820e0dff --- /dev/null +++ b/lib/blocs/hide/global_hide_bloc.dart @@ -0,0 +1,42 @@ +import 'package:ardrive/models/daos/drive_dao/drive_dao.dart'; +import 'package:ardrive/user/repositories/user_preferences_repository.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +part 'global_hide_event.dart'; +part 'global_hide_state.dart'; + +class GlobalHideBloc extends Bloc { + final UserPreferencesRepository _userPreferencesRepository; + final DriveDao _driveDao; + + GlobalHideBloc({ + required UserPreferencesRepository userPreferencesRepository, + required DriveDao driveDao, + }) : _userPreferencesRepository = userPreferencesRepository, + _driveDao = driveDao, + super(const GlobalHideInitial(userHasHiddenDrive: false)) { + _userPreferencesRepository.watch().listen((userPreferences) async { + if (userPreferences.showHiddenFiles) { + add(ShowItems(userHasHiddenItems: userPreferences.userHasHiddenDrive)); + } else { + add(HideItems(userHasHiddenItems: userPreferences.userHasHiddenDrive)); + } + }); + + _userPreferencesRepository.load(); + + on((event, emit) async { + if (event is ShowItems) { + emit(ShowingHiddenItems(userHasHiddenDrive: event.userHasHiddenItems)); + await _userPreferencesRepository.saveShowHiddenFiles(true); + } else if (event is HideItems) { + emit(HiddingItems(userHasHiddenDrive: event.userHasHiddenItems)); + await _userPreferencesRepository.saveShowHiddenFiles(false); + } else if (event is RefreshOptions) { + final hasHiddenItems = await _driveDao.userHasHiddenItems(); + emit(state.copyWith(userHasHiddenDrive: hasHiddenItems)); + } + }); + } +} diff --git a/lib/blocs/hide/global_hide_event.dart b/lib/blocs/hide/global_hide_event.dart new file mode 100644 index 0000000000..b1e81d815d --- /dev/null +++ b/lib/blocs/hide/global_hide_event.dart @@ -0,0 +1,26 @@ +part of 'global_hide_bloc.dart'; + +sealed class GlobalHideEvent extends Equatable { + const GlobalHideEvent(); + + @override + List get props => []; +} + +class HideItems extends GlobalHideEvent { + final bool userHasHiddenItems; + + const HideItems({required this.userHasHiddenItems}); +} + +class ShowItems extends GlobalHideEvent { + final bool userHasHiddenItems; + + const ShowItems({required this.userHasHiddenItems}); +} + +class RefreshOptions extends GlobalHideEvent { + final bool userHasHiddenItems; + + const RefreshOptions({required this.userHasHiddenItems}); +} diff --git a/lib/blocs/hide/global_hide_state.dart b/lib/blocs/hide/global_hide_state.dart new file mode 100644 index 0000000000..22e11521f4 --- /dev/null +++ b/lib/blocs/hide/global_hide_state.dart @@ -0,0 +1,61 @@ +part of 'global_hide_bloc.dart'; + +sealed class GlobalHideState extends Equatable { + const GlobalHideState({ + required this.userHasHiddenDrive, + }); + + final bool userHasHiddenDrive; + + @override + List get props => [userHasHiddenDrive]; + + GlobalHideState copyWith({ + bool? userHasHiddenDrive, + }); +} + +final class GlobalHideInitial extends GlobalHideState { + const GlobalHideInitial({ + required super.userHasHiddenDrive, + }); + + @override + GlobalHideState copyWith({ + bool? userHasHiddenDrive, + }) { + return GlobalHideInitial( + userHasHiddenDrive: userHasHiddenDrive ?? this.userHasHiddenDrive, + ); + } +} + +final class ShowingHiddenItems extends GlobalHideState { + const ShowingHiddenItems({ + required super.userHasHiddenDrive, + }); + + @override + GlobalHideState copyWith({ + bool? userHasHiddenDrive, + }) { + return ShowingHiddenItems( + userHasHiddenDrive: userHasHiddenDrive ?? this.userHasHiddenDrive, + ); + } +} + +final class HiddingItems extends GlobalHideState { + const HiddingItems({ + required super.userHasHiddenDrive, + }); + + @override + GlobalHideState copyWith({ + bool? userHasHiddenDrive, + }) { + return HiddingItems( + userHasHiddenDrive: userHasHiddenDrive ?? this.userHasHiddenDrive, + ); + } +} diff --git a/lib/blocs/hide/hide_bloc.dart b/lib/blocs/hide/hide_bloc.dart index 14af84b2f7..ea22602e4d 100644 --- a/lib/blocs/hide/hide_bloc.dart +++ b/lib/blocs/hide/hide_bloc.dart @@ -5,6 +5,8 @@ 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/drive_entity.dart'; +import 'package:ardrive/entities/entity.dart'; import 'package:ardrive/entities/file_entity.dart'; import 'package:ardrive/entities/folder_entity.dart'; import 'package:ardrive/models/models.dart'; @@ -44,6 +46,8 @@ class HideBloc extends Bloc { on(_onUnhideFileEvent); on(_onUnhideFolderEvent); on(_onConfirmUploadEvent); + on(_onHideDriveEvent); + on(_onUnhideDriveEvent); on(_onErrorEvent); } @@ -142,101 +146,179 @@ class HideBloc extends Bloc { }) async { final entryIsFile = currentEntry is FileEntry; final entryIsFolder = currentEntry is FolderEntry; - + final entryIsDrive = currentEntry is Drive; assert( - entryIsFile || entryIsFolder, - 'Entity to hide must be either a File or a Folder', + entryIsFile || entryIsFolder || entryIsDrive, + 'Entity to hide must be either a File, Folder or Drive', + ); + + HideEntitySettings hideEntitySettings; + + if (currentEntry is Drive) { + hideEntitySettings = await _getDriveHideEntitySettings( + isHidden, + currentEntry, + ); + } else if (currentEntry is FileEntry) { + hideEntitySettings = await _getFileHideEntitySettings( + isHidden, + currentEntry, + ); + } else { + hideEntitySettings = await _getFolderHideEntitySettings( + isHidden, + currentEntry as FolderEntry, + ); + } + + final dataItems = [hideEntitySettings.dataItem]; + + final paymentInfo = + await _uploadPreparationManager.getUploadPaymentInfoForEntityUpload( + dataItem: hideEntitySettings.dataItem); + + _useTurboUpload = paymentInfo.isFreeUploadPossibleUsingTurbo; + + HideAction action; + + if (entryIsFile) { + action = isHidden ? HideAction.hideFile : HideAction.unhideFile; + } else if (entryIsDrive) { + action = isHidden ? HideAction.hideDrive : HideAction.unhideDrive; + } else { + action = isHidden ? HideAction.hideFolder : HideAction.unhideFolder; + } + + emit( + ConfirmingHideState( + uploadMethod: UploadMethod.turbo, + hideAction: action, + dataItems: dataItems, + hideEntitySettings: hideEntitySettings, + ), ); + } - final entity = entryIsFile - ? currentEntry.asEntity() - : (currentEntry as FolderEntry).asEntity(); + Future> _getFolderHideEntitySettings( + bool isHidden, + FolderEntry currentEntry, + ) async { + final timestamp = DateTime.now(); - final driveId = entryIsFile - ? currentEntry.driveId - : (currentEntry as FolderEntry).driveId; + final newFolderEntry = currentEntry.copyWith( + isHidden: isHidden, + lastUpdated: timestamp, + ); final profile = _profileCubit.state as ProfileLoggedIn; - final driveKey = - await _driveDao.getDriveKey(driveId, profile.user.cipherKey); - final SecretKey? entityKey; + + final driveKey = await _driveDao.getDriveKey( + currentEntry.driveId, profile.user.cipherKey); + SecretKey? entityKey; if (driveKey != null) { - if (entryIsFile) { - entityKey = await _crypto.deriveFileKey( - driveKey, - (entity as FileEntity).id!, - ); - } else { - entityKey = driveKey; - } - } else { - entityKey = null; + entityKey = driveKey; } - 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( + newFolderEntry.asEntity(), + profile.user.wallet, + key: entityKey, + ); + + final newEntryEntity = newFolderEntry.asEntity(); + + newEntryEntity.txId = dataItem.id; + + return HideEntitySettings( + isHidden: isHidden, + entry: newFolderEntry, + entity: newEntryEntity, + dataItem: dataItem, + ); + } + + Future> _getFileHideEntitySettings( + bool isHidden, + FileEntry currentEntry, + ) async { + final timestamp = DateTime.now(); + + final newFileEntry = currentEntry.copyWith( + isHidden: isHidden, + lastUpdated: timestamp, + ); + + final profile = _profileCubit.state as ProfileLoggedIn; + + final driveKey = await _driveDao.getDriveKey( + currentEntry.driveId, profile.user.cipherKey); + SecretKey? entityKey; + + if (driveKey != null) { + entityKey = await _crypto.deriveFileKey( + driveKey, + currentEntry.id, + ); + } final dataItem = await _arweave.prepareEntityDataItem( - newEntryEntity, + newFileEntry.asEntity(), profile.user.wallet, key: entityKey, ); - final dataItems = [dataItem]; + final newEntryEntity = newFileEntry.asEntity(); - final paymentInfo = await _uploadPreparationManager - .getUploadPaymentInfoForEntityUpload(dataItem: dataItem); + newEntryEntity.txId = dataItem.id; - _useTurboUpload = paymentInfo.isFreeUploadPossibleUsingTurbo; + return HideEntitySettings( + isHidden: isHidden, + entry: newFileEntry, + entity: newEntryEntity, + dataItem: dataItem, + ); + } - Future saveEntitiesToDb() async { - await _driveDao.transaction(() async { - if (entryIsFile) { - await _driveDao.writeToFile(newEntry as FileEntry); - } else { - await _driveDao.writeToFolder(newEntry as FolderEntry); - } + Future> _getDriveHideEntitySettings( + bool isHidden, + Drive currentEntry, + ) async { + final timestamp = DateTime.now(); - newEntryEntity.txId = dataItem.id; + final newDriveEntry = currentEntry.copyWith( + isHidden: isHidden, + lastUpdated: timestamp, + ); - 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 newEntryEntity = newDriveEntry.asEntity(); + final profile = _profileCubit.state as ProfileLoggedIn; + + newEntryEntity.ownerAddress = profile.user.walletAddress; + + final driveKey = + await _driveDao.getDriveKey(currentEntry.id, profile.user.cipherKey); + final SecretKey? entityKey; + + if (driveKey != null) { + entityKey = driveKey; + } else { + entityKey = null; } - final hideAction = entryIsFile - ? (isHidden ? HideAction.hideFile : HideAction.unhideFile) - : (isHidden ? HideAction.hideFolder : HideAction.unhideFolder); + final dataItem = await _arweave.prepareEntityDataItem( + newEntryEntity, + profile.user.wallet, + key: entityKey, + ); - emit( - ConfirmingHideState( - uploadMethod: UploadMethod.turbo, - hideAction: hideAction, - dataItems: dataItems, - saveEntitiesToDb: saveEntitiesToDb, - ), + newEntryEntity.txId = dataItem.id; + + return HideEntitySettings( + isHidden: isHidden, + entry: newDriveEntry, + entity: newEntryEntity, + dataItem: dataItem, ); } @@ -273,7 +355,7 @@ class HideBloc extends Bloc { await _arweave.postTx(hideTx); } - await state.saveEntitiesToDb(); + await _saveNewRevision(state.hideEntitySettings); emit(SuccessHideState(hideAction: state.hideAction)); }); @@ -283,6 +365,71 @@ class HideBloc extends Bloc { } } + Future _saveNewRevision( + HideEntitySettings settings, + ) async { + await _driveDao.transaction(() async { + if (settings.entry is FileEntry) { + await _driveDao.writeToFile(settings.entry as FileEntry); + + await _driveDao.insertFileRevision( + (settings.entity as FileEntity).toRevisionCompanion( + performedAction: + settings.isHidden ? RevisionAction.hide : RevisionAction.unhide, + )); + } else if (settings.entry is FolderEntry) { + await _driveDao.writeToFolder(settings.entry as FolderEntry); + + await _driveDao.insertFolderRevision( + (settings.entity as FolderEntity).toRevisionCompanion( + performedAction: + settings.isHidden ? RevisionAction.hide : RevisionAction.unhide, + )); + } else if (settings.entry is Drive) { + await _driveDao.writeToDrive(settings.entry as Drive); + + final driveCompanion = + (settings.entity as DriveEntity).toRevisionCompanion( + performedAction: + settings.isHidden ? RevisionAction.hide : RevisionAction.unhide, + ); + + await _driveDao.insertDriveRevision(driveCompanion); + } + }); + } + + Future _onHideDriveEvent( + HideDriveEvent event, + Emitter emit, + ) async { + emit(const PreparingAndSigningHideState(hideAction: HideAction.hideDrive)); + + final drive = await _driveDao.driveById(driveId: event.driveId).getSingle(); + + await _setHideStatus( + drive, + emit, + isHidden: true, + ); + } + + Future _onUnhideDriveEvent( + UnhideDriveEvent event, + Emitter emit, + ) async { + emit( + const PreparingAndSigningHideState(hideAction: HideAction.unhideDrive)); + + final drive = await _driveDao.driveById(driveId: event.driveId).getSingle(); + + await _setHideStatus( + drive, + emit, + isHidden: false, + ); + } + void _onErrorEvent( ErrorEvent event, Emitter emit, @@ -300,3 +447,17 @@ class HideBloc extends Bloc { super.onError(error, stackTrace); } } + +class HideEntitySettings { + final bool isHidden; + final T entry; + final EntityWithCustomMetadata entity; + final DataItem dataItem; + + HideEntitySettings({ + required this.isHidden, + required this.entry, + required this.entity, + required this.dataItem, + }); +} diff --git a/lib/blocs/hide/hide_event.dart b/lib/blocs/hide/hide_event.dart index e667f54a88..c67ebd57f7 100644 --- a/lib/blocs/hide/hide_event.dart +++ b/lib/blocs/hide/hide_event.dart @@ -20,6 +20,28 @@ class HideFileEvent extends HideEvent { List get props => [driveId, fileId]; } +class HideDriveEvent extends HideEvent { + final DriveID driveId; + + const HideDriveEvent({ + required this.driveId, + }); + + @override + List get props => [driveId]; +} + +class UnhideDriveEvent extends HideEvent { + final DriveID driveId; + + const UnhideDriveEvent({ + required this.driveId, + }); + + @override + List get props => [driveId]; +} + class HideFolderEvent extends HideEvent { final DriveID driveId; final FolderID folderId; diff --git a/lib/blocs/hide/hide_state.dart b/lib/blocs/hide/hide_state.dart index b257ce299d..76c2b96f64 100644 --- a/lib/blocs/hide/hide_state.dart +++ b/lib/blocs/hide/hide_state.dart @@ -1,3 +1,4 @@ +import 'package:ardrive/blocs/hide/hide_bloc.dart'; import 'package:ardrive/blocs/upload/upload_cubit.dart'; import 'package:ardrive/core/upload/cost_calculator.dart'; import 'package:arweave/arweave.dart'; @@ -28,15 +29,14 @@ class PreparingAndSigningHideState extends HideState { class ConfirmingHideState extends HideState { final UploadMethod uploadMethod; - + final HideEntitySettings hideEntitySettings; final List dataItems; - final Future Function() saveEntitiesToDb; const ConfirmingHideState({ required this.uploadMethod, required super.hideAction, required this.dataItems, - required this.saveEntitiesToDb, + required this.hideEntitySettings, }); @override @@ -50,12 +50,13 @@ class ConfirmingHideState extends HideState { UploadCostEstimate? costEstimateTurbo, UploadCostEstimate? costEstimateAr, HideAction? hideAction, + HideEntitySettings? hideEntitySettings, }) { return ConfirmingHideState( uploadMethod: uploadMethod ?? this.uploadMethod, hideAction: hideAction ?? this.hideAction, dataItems: dataItems, - saveEntitiesToDb: saveEntitiesToDb, + hideEntitySettings: hideEntitySettings ?? this.hideEntitySettings, ); } } @@ -71,6 +72,8 @@ class FailureHideState extends HideState { enum HideAction { hideFile, hideFolder, + hideDrive, unhideFile, unhideFolder, + unhideDrive, } diff --git a/lib/blocs/upload/upload_cubit.dart b/lib/blocs/upload/upload_cubit.dart index f995653243..61ce3c5054 100644 --- a/lib/blocs/upload/upload_cubit.dart +++ b/lib/blocs/upload/upload_cubit.dart @@ -10,6 +10,7 @@ import 'package:ardrive/blocs/upload/upload_file_checker.dart'; import 'package:ardrive/core/activity_tracker.dart'; import 'package:ardrive/core/upload/domain/repository/upload_repository.dart'; import 'package:ardrive/core/upload/uploader.dart'; +import 'package:ardrive/core/upload/view/blocs/upload_manifest_options_bloc.dart'; import 'package:ardrive/main.dart'; import 'package:ardrive/manifest/domain/manifest_repository.dart'; import 'package:ardrive/models/forms/cc.dart'; @@ -102,29 +103,22 @@ class UploadCubit extends Cubit { late final bool _isUploadFolders; /// Manifest - List _manifestFiles = []; - final List _selectedManifestFiles = []; + + Map _manifestFiles = {}; + final List _selectedManifestModels = []; UploadMethod? _manifestUploadMethod; bool _isManifestsUploadCancelled = false; - void selectManifestFile(FileEntry file) { - final readyState = state as UploadReady; - - final newReadyState = readyState.copyWith( - selectedManifests: List.of(_selectedManifestFiles)..add(file)); + void updateManifestSelection(List selections) { + _selectedManifestModels.clear(); - _selectedManifestFiles.add(file); - - emit(newReadyState); - } + _selectedManifestModels.addAll(selections); - void unselectManifestFile(FileEntry file) { - _selectedManifestFiles.remove(file); - - emit((state as UploadReady) - .copyWith(selectedManifests: _selectedManifestFiles)); + emit((state as UploadReady).copyWith( + selectedManifestSelections: selections, + )); } void setManifestUploadMethod( @@ -133,18 +127,16 @@ class UploadCubit extends Cubit { } Future prepareManifestUpload() async { - final manifestModels = _selectedManifestFiles - .map( - (f) => UploadManifestModel( - name: f.name, - isCompleted: false, - freeThanksToTurbo: - f.size <= configService.config.allowedDataItemSizeForTurbo, - isUploading: false, - existingManifestFileId: f.id, - ), - ) + final manifestModels = _selectedManifestModels + .map((e) => UploadManifestModel( + entry: e.manifest, + freeThanksToTurbo: false, + existingManifestFileId: e.manifest.id, + antRecord: e.antRecord, + undername: e.undername, + )) .toList(); + for (int i = 0; i < manifestModels.length; i++) { if (_isManifestsUploadCancelled) { break; @@ -152,9 +144,16 @@ class UploadCubit extends Cubit { manifestModels[i] = manifestModels[i].copyWith(isUploading: true); + final manifestFileEntry = await _driveDao + .fileById( + driveId: _driveId, + fileId: manifestModels[i].existingManifestFileId, + ) + .getSingle(); + await _createManifestCubit.prepareManifestTx( - manifestName: manifestModels[i].name, - folderId: _targetFolder.id, + manifestName: manifestFileEntry.name, + folderId: manifestFileEntry.parentFolderId, existingManifestFileId: manifestModels[i].existingManifestFileId, ); @@ -207,9 +206,16 @@ class UploadCubit extends Cubit { completedCount: completedCount, )); + final manifestFileEntry = await _driveDao + .fileById( + driveId: _driveId, + fileId: manifestModels[i].existingManifestFileId, + ) + .getSingle(); + await _createManifestCubit.prepareManifestTx( - manifestName: manifestModels[i].name, - folderId: _targetFolder.id, + manifestName: manifestFileEntry.name, + folderId: manifestFileEntry.parentFolderId, existingManifestFileId: manifestModels[i].existingManifestFileId, ); @@ -222,25 +228,70 @@ class UploadCubit extends Cubit { method: _manifestUploadMethod, ); - manifestModels[i] = - manifestModels[i].copyWith(isCompleted: true, isUploading: false); + final manifestFile = await _driveDao + .fileById( + driveId: _driveId, + fileId: manifestModels[i].existingManifestFileId, + ) + .getSingleOrNull(); - emit(UploadingManifests( - manifestFiles: manifestModels, - completedCount: ++completedCount, - )); + if (manifestFile == null) { + throw StateError('Manifest file not found'); + } + + if (manifestModels[i].antRecord != null) { + ARNSUndername undername; + + if (manifestModels[i].undername == null) { + undername = ARNSUndername( + name: '@', + domain: manifestModels[i].antRecord!.domain, + record: ARNSRecord( + transactionId: manifestFile.dataTxId, + ttlSeconds: 3600, + ), + ); + } else { + undername = ARNSUndername( + name: manifestModels[i].undername!.name, + domain: manifestModels[i].antRecord!.domain, + record: ARNSRecord( + transactionId: manifestFile.dataTxId, + ttlSeconds: 3600, + ), + ); + } + + manifestModels[i] = manifestModels[i].copyWith( + isCompleted: false, isUploading: false, isAssigningUndername: true); + emit(UploadingManifests( + manifestFiles: manifestModels, + completedCount: ++completedCount, + )); + + await _arnsRepository.setUndernamesToFile( + undername: undername, + driveId: _driveId, + fileId: manifestModels[i].existingManifestFileId, + processId: manifestModels[i].antRecord!.processId, + ); + + manifestModels[i] = manifestModels[i].copyWith( + isCompleted: true, isUploading: false, isAssigningUndername: false); + + emit(UploadingManifests( + manifestFiles: manifestModels, + completedCount: completedCount, + )); + } } - emit(UploadComplete( - manifestFiles: _selectedManifestFiles, - )); + emit(UploadComplete()); } void cancelManifestsUpload() { _isManifestsUploadCancelled = true; - emit(UploadComplete( - manifestFiles: _selectedManifestFiles, - )); + emit(UploadComplete()); } /// License forms @@ -272,6 +323,7 @@ class UploadCubit extends Cubit { /// ArNS ANTRecord? _selectedAntRecord; + List _ants = []; ARNSUndername? _selectedUndername; /// Thumbnail upload @@ -403,10 +455,12 @@ class UploadCubit extends Cubit { loadingArNSNames: true, arnsCheckboxChecked: _showArnsNameSelectionCheckBoxValue, totalSize: await _getTotalSize(), - selectedManifests: _selectedManifestFiles, showSettings: showSettings, canShowSettings: showSettings, - manifestFiles: _manifestFiles, + manifestFiles: _manifestFiles.values.toList(), + arnsRecords: _ants, + showReviewButtonText: false, + selectedManifestSelections: _selectedManifestModels, ), ); @@ -445,10 +499,12 @@ class UploadCubit extends Cubit { showArnsNameSelection: false, arnsCheckboxChecked: _showArnsNameSelectionCheckBoxValue, totalSize: await _getTotalSize(), - selectedManifests: _selectedManifestFiles, showSettings: showSettings, - manifestFiles: _manifestFiles, + manifestFiles: _manifestFiles.values.toList(), + arnsRecords: _ants, canShowSettings: showSettings, + showReviewButtonText: false, + selectedManifestSelections: _selectedManifestModels, ), ); } @@ -459,7 +515,7 @@ class UploadCubit extends Cubit { if (state is UploadReady) { if (_showArnsNameSelectionCheckBoxValue) { showArnsNameSelection(state as UploadReady); - } else if (_selectedManifestFiles.isNotEmpty) { + } else if (_selectedManifestModels.isNotEmpty) { emit(UploadReview(readyState: state as UploadReady)); } else { final readyState = state as UploadReady; @@ -815,11 +871,22 @@ class UploadCubit extends Cubit { emit(UploadLoadingFilesSuccess()); } + Future> getARNSUndernames( + ANTRecord antRecord, + ) async { + return _arnsRepository.getARNSUndernames(antRecord); + } + Future startUploadPreparation({ bool isRetryingToPayWithTurbo = false, }) async { final walletAddress = await _auth.getWalletAddress(); - _arnsRepository.getAntRecordsForWallet(walletAddress!); + _arnsRepository.getAntRecordsForWallet(walletAddress!).then((value) { + _ants = value; + if (state is UploadReady) { + emit((state as UploadReady).copyWith(arnsRecords: value)); + } + }); _files .removeWhere((file) => filesNamesToExclude.contains(file.ioFile.name)); @@ -942,16 +1009,34 @@ class UploadCubit extends Cubit { ), ); - _manifestFiles = await _manifestRepository.getManifestFilesInFolder( + final manifestFileEntries = + await _manifestRepository.getManifestFilesInFolder( folderId: _targetFolder.id, driveId: _targetDrive.id, ); + _manifestFiles = {}; + + for (var entry in manifestFileEntries) { + _manifestFiles[entry.id] = UploadManifestModel( + entry: entry, + existingManifestFileId: entry.id, + freeThanksToTurbo: + entry.size <= configService.config.allowedDataItemSizeForTurbo, + ); + } + // if there are no files that can be used to generate a thumbnail, we disable the option if (!containsSupportedImageTypeForThumbnailGeneration) { _uploadThumbnail = false; } + if (manifestFileEntries.isNotEmpty) { + // load arns names + await _arnsRepository + .getAntRecordsForWallet(_auth.currentUser.walletAddress); + } + emit( UploadReadyToPrepare( params: UploadParams( @@ -1171,13 +1256,11 @@ class UploadCubit extends Cubit { ); } - if (_selectedManifestFiles.isNotEmpty) { + if (_selectedManifestModels.isNotEmpty) { await prepareManifestUpload(); } - emit(UploadComplete( - manifestFiles: _selectedManifestFiles, - )); + emit(UploadComplete()); unawaited(_profileCubit.refreshBalance()); }, @@ -1251,11 +1334,11 @@ class UploadCubit extends Cubit { 'Upload finished with success. Number of tasks: ${tasks.length}', ); - if (_selectedManifestFiles.isNotEmpty) { + if (_selectedManifestModels.isNotEmpty) { await prepareManifestUpload(); } - emit(UploadComplete(manifestFiles: _selectedManifestFiles)); + emit(UploadComplete()); PlausibleEventTracker.trackUploadSuccess(); }, @@ -1334,7 +1417,7 @@ class UploadCubit extends Cubit { return ARNSUndername( name: '@', domain: _selectedAntRecord!.domain, - record: ARNSRecord( + record: const ARNSRecord( transactionId: 'to_assign', ttlSeconds: 3600, ), diff --git a/lib/blocs/upload/upload_state.dart b/lib/blocs/upload/upload_state.dart index 340c9c7a94..c71c3f7db9 100644 --- a/lib/blocs/upload/upload_state.dart +++ b/lib/blocs/upload/upload_state.dart @@ -107,12 +107,14 @@ class UploadReady extends UploadState { final bool loadingArNSNamesError; final bool arnsCheckboxChecked; final int totalSize; - final List selectedManifests; - final List manifestFiles; + final List manifestFiles; + final List selectedManifestSelections; final bool showSettings; final bool canShowSettings; + final List arnsRecords; final bool isArConnect; + final bool showReviewButtonText; UploadReady({ required this.paymentInfo, @@ -128,10 +130,12 @@ class UploadReady extends UploadState { this.loadingArNSNamesError = false, required this.arnsCheckboxChecked, required this.totalSize, - required this.selectedManifests, required this.showSettings, required this.canShowSettings, required this.manifestFiles, + required this.arnsRecords, + required this.showReviewButtonText, + required this.selectedManifestSelections, }); // copyWith @@ -151,9 +155,11 @@ class UploadReady extends UploadState { bool? loadingArNSNamesError, bool? arnsCheckboxChecked, int? totalSize, - List? selectedManifests, - List? manifestFiles, + List? manifestFiles, bool? canShowSettings, + List? arnsRecords, + bool? showReviewButtonText, + List? selectedManifestSelections, }) { return UploadReady( loadingArNSNames: loadingArNSNames ?? this.loadingArNSNames, @@ -171,10 +177,13 @@ class UploadReady extends UploadState { loadingArNSNamesError ?? this.loadingArNSNamesError, arnsCheckboxChecked: arnsCheckboxChecked ?? this.arnsCheckboxChecked, totalSize: totalSize ?? this.totalSize, - selectedManifests: selectedManifests ?? this.selectedManifests, showSettings: showSettings ?? this.showSettings, manifestFiles: manifestFiles ?? this.manifestFiles, canShowSettings: canShowSettings ?? this.canShowSettings, + arnsRecords: arnsRecords ?? this.arnsRecords, + showReviewButtonText: showReviewButtonText ?? this.showReviewButtonText, + selectedManifestSelections: + selectedManifestSelections ?? this.selectedManifestSelections, ); } @@ -288,10 +297,9 @@ class UploadFailure extends UploadState { } class UploadComplete extends UploadState { - final List manifestFiles; final ARNSRecord? arnsRecord; - UploadComplete({required this.manifestFiles, this.arnsRecord}); + UploadComplete({this.arnsRecord}); } class UploadingManifests extends UploadState { @@ -304,7 +312,7 @@ class UploadingManifests extends UploadState { }); @override - List get props => [manifestFiles, completedCount]; + List get props => [UniqueKey()]; } class UploadWalletMismatch extends UploadState {} @@ -345,20 +353,28 @@ class UploadManifestSelectPaymentMethod extends UploadState { } class UploadManifestModel extends Equatable { - final String name; + final FileEntry entry; final bool isCompleted; + final bool isAssigningUndername; final bool freeThanksToTurbo; final bool isUploading; - final String? existingManifestFileId; + final String existingManifestFileId; final IOFile? file; + final ARNSUndername? undername; + final ANTRecord? antRecord; + final bool selectionExpanded; const UploadManifestModel({ - required this.name, + required this.entry, this.isCompleted = false, required this.freeThanksToTurbo, this.isUploading = false, - this.existingManifestFileId, + required this.existingManifestFileId, this.file, + this.undername, + this.antRecord, + this.isAssigningUndername = false, + this.selectionExpanded = false, }); UploadManifestModel copyWith({ @@ -367,25 +383,38 @@ class UploadManifestModel extends Equatable { String? existingManifestFileId, bool? freeThanksToTurbo, IOFile? file, + ARNSUndername? undername, + ANTRecord? antRecord, + bool? isAssigningUndername, + FileEntry? entry, + bool? selectionExpanded, }) { return UploadManifestModel( - name: name, + entry: entry ?? this.entry, isCompleted: isCompleted ?? this.isCompleted, isUploading: isUploading ?? this.isUploading, existingManifestFileId: existingManifestFileId ?? this.existingManifestFileId, freeThanksToTurbo: freeThanksToTurbo ?? this.freeThanksToTurbo, file: file ?? this.file, + undername: undername ?? this.undername, + antRecord: antRecord ?? this.antRecord, + isAssigningUndername: isAssigningUndername ?? this.isAssigningUndername, + selectionExpanded: selectionExpanded ?? this.selectionExpanded, ); } @override List get props => [ - name, + entry, isCompleted, isUploading, existingManifestFileId, - freeThanksToTurbo + freeThanksToTurbo, + isAssigningUndername, + antRecord, + undername, + file, ]; } diff --git a/lib/components/app_top_bar.dart b/lib/components/app_top_bar.dart index a4d974b000..69605871b4 100644 --- a/lib/components/app_top_bar.dart +++ b/lib/components/app_top_bar.dart @@ -1,4 +1,6 @@ import 'package:ardrive/blocs/drive_detail/drive_detail_cubit.dart'; +import 'package:ardrive/blocs/drives/drives_cubit.dart'; +import 'package:ardrive/blocs/hide/global_hide_bloc.dart'; import 'package:ardrive/components/profile_card.dart'; import 'package:ardrive/components/topbar/help_button.dart'; import 'package:ardrive/pages/drive_detail/components/dropdown_item.dart'; @@ -39,6 +41,7 @@ class AppTopBar extends StatelessWidget { context: context, driveDetailCubit: context.read(), controller: controller, + drivesCubit: context.read(), ); }, ), @@ -46,6 +49,8 @@ class AppTopBar extends StatelessWidget { ), const SizedBox(width: 24), const Spacer(), + const GlobalHideToggleButton(), + const SizedBox(width: 8), const SyncButton(), const SizedBox(width: 8), const HelpButtonTopBar(), @@ -58,6 +63,50 @@ class AppTopBar extends StatelessWidget { } } +class GlobalHideToggleButton extends StatelessWidget { + const GlobalHideToggleButton({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, hideState) { + if (!hideState.userHasHiddenDrive) { + return const SizedBox.shrink(); + } + + final colorTokens = ArDriveTheme.of(context).themeData.colorTokens; + + final tooltip = hideState is ShowingHiddenItems + ? 'Hide hidden items' + : 'Show hidden items'; + + final icon = hideState is ShowingHiddenItems + ? ArDriveIcons.eyeOpen( + color: colorTokens.textMid, + ) + : ArDriveIcons.eyeClosed( + color: colorTokens.textMid, + ); + + return ArDriveIconButton( + tooltip: tooltip, + icon: icon, + onPressed: () { + context.read().add( + hideState is ShowingHiddenItems + ? HideItems( + userHasHiddenItems: hideState.userHasHiddenDrive) + : ShowItems( + userHasHiddenItems: hideState.userHasHiddenDrive, + ), + ); + }, + ); + }, + ); + } +} + class SyncButton extends StatelessWidget { const SyncButton({super.key}); diff --git a/lib/components/create_manifest_form.dart b/lib/components/create_manifest_form.dart index 827567808f..9e19d676d9 100644 --- a/lib/components/create_manifest_form.dart +++ b/lib/components/create_manifest_form.dart @@ -3,7 +3,6 @@ import 'package:ardrive/arns/presentation/assign_name_modal.dart'; import 'package:ardrive/authentication/ardrive_auth.dart'; import 'package:ardrive/blocs/blocs.dart'; import 'package:ardrive/blocs/create_manifest/create_manifest_cubit.dart'; -import 'package:ardrive/blocs/feedback_survey/feedback_survey_cubit.dart'; import 'package:ardrive/blocs/upload/models/upload_file.dart'; import 'package:ardrive/blocs/upload/payment_method/bloc/upload_payment_method_bloc.dart'; import 'package:ardrive/blocs/upload/payment_method/view/upload_payment_method_view.dart'; @@ -284,7 +283,6 @@ class _CreateManifestFormState extends State { ModalAction( action: () { context.read().refreshDriveDataTable(); - context.read().openRemindMe(); Navigator.pop(context); }, title: 'Close', @@ -679,7 +677,6 @@ class _CreateManifestFormState extends State { user: context.read().currentUser, // Theres no thumbnail generation for manifests containsSupportedImageTypeForThumbnailGeneration: false, - ), ), ), diff --git a/lib/components/details_panel.dart b/lib/components/details_panel.dart index 41feae5c6c..d591fd4690 100644 --- a/lib/components/details_panel.dart +++ b/lib/components/details_panel.dart @@ -1249,21 +1249,22 @@ class _CopyButtonState extends State { OverlayEntry _createOverlayEntry(BuildContext parentContext) { final RenderBox button = context.findRenderObject() as RenderBox; final Offset buttonPosition = button.localToGlobal(Offset.zero); + final typography = ArDriveTypographyNew.of(context); + final colorTokens = ArDriveTheme.of(context).themeData.colorTokens; return OverlayEntry( builder: (context) => Positioned( left: buttonPosition.dx - widget.positionX, top: buttonPosition.dy - widget.positionY, child: Material( - color: widget.copyMessageColor ?? - ArDriveTheme.of(context).themeData.backgroundColor, + color: widget.copyMessageColor ?? colorTokens.containerL1, borderRadius: BorderRadius.circular(20), child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), child: Center( child: Text( 'Copied!', - style: ArDriveTypography.body.smallRegular(), + style: typography.paragraphNormal(), ), ), ), diff --git a/lib/components/drive_share_dialog.dart b/lib/components/drive_share_dialog.dart index 2a096f759a..0472f155f0 100644 --- a/lib/components/drive_share_dialog.dart +++ b/lib/components/drive_share_dialog.dart @@ -46,83 +46,93 @@ class DriveShareDialogState extends State { @override Widget build(BuildContext context) => BlocBuilder( - builder: (context, state) => ArDriveStandardModal( - width: kLargeDialogWidth, - title: appLocalizationsOf(context).shareDriveWithOthers, - description: state is DriveShareLoadSuccess ? state.drive.name : null, - content: SizedBox( + builder: (context, state) { + final typography = ArDriveTypographyNew.of(context); + + return ArDriveStandardModalNew( width: kLargeDialogWidth, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (state is DriveShareLoadInProgress) - const Center(child: CircularProgressIndicator()) - else if (state is DriveShareLoadSuccess) ...{ - Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: ArDriveTextField( - initialValue: state.driveShareLink.toString(), - isEnabled: false, - ), - ), - const SizedBox(width: 16), - CopyButton( - positionX: 4, - positionY: 40, - copyMessageColor: ArDriveTheme.of(context) - .themeData - .tableTheme - .selectedItemColor, - showCopyText: true, - text: state.driveShareLink.toString(), - child: Text( - appLocalizationsOf(context).copyLink, - style: ArDriveTypography.body - .buttonLargeRegular( + title: appLocalizationsOf(context).shareDriveWithOthers, + description: + state is DriveShareLoadSuccess ? state.drive.name : null, + content: SizedBox( + width: kLargeDialogWidth, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (state is DriveShareLoadInProgress) + const Center(child: CircularProgressIndicator()) + else if (state is DriveShareLoadSuccess) ...{ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Flexible( + child: Container( + padding: const EdgeInsets.fromLTRB(20, 14, 20, 14), + decoration: BoxDecoration( + color: ArDriveTheme.of(context) + .themeData + .colorTokens + .inputDisabled, + borderRadius: BorderRadius.circular(6), + border: Border.all( color: ArDriveTheme.of(context) .themeData - .colors - .themeFgDefault, - ) - .copyWith( - decoration: TextDecoration.underline, + .colorTokens + .strokeMid, ), + ), + child: Row( + children: [ + Expanded( + child: Text( + state.driveShareLink.toString(), + style: typography.paragraphNormal( + color: ArDriveTheme.of(context) + .themeData + .colorTokens + .textXLow, + fontWeight: ArFontWeight.semiBold, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 8), + CopyButton( + text: state.driveShareLink.toString(), + ), + ], + ), + ), ), - ), - ], - ), - const SizedBox(height: 16), - Text( - state.drive.isPublic - ? appLocalizationsOf(context) - .anyoneCanAccessThisDrivePublic - : appLocalizationsOf(context) - .anyoneCanAccessThisDrivePrivate, - style: ArDriveTypography.body.buttonLargeRegular( - color: ArDriveTheme.of(context) - .themeData - .colors - .themeFgDefault, + ], ), - ), - } else if (state is DriveShareLoadFail) - Text(state.message) - ], + const SizedBox(height: 16), + Text( + state.drive.isPublic + ? appLocalizationsOf(context) + .anyoneCanAccessThisDrivePublic + : appLocalizationsOf(context) + .anyoneCanAccessThisDrivePrivate, + style: typography.paragraphLarge(), + ), + } else if (state is DriveShareLoadFail) + Text(state.message) + ], + ), ), - ), - actions: [ - if (state is DriveShareLoadSuccess) - ModalAction( - action: () { - Navigator.pop(context); - context.read().openRemindMe(); - }, - title: appLocalizationsOf(context).doneEmphasized, - ) - ], - ), + actions: [ + if (state is DriveShareLoadSuccess) + ModalAction( + action: () { + Navigator.pop(context); + context.read().openRemindMe(); + }, + title: appLocalizationsOf(context).doneEmphasized, + ) + ], + ); + }, ); } diff --git a/lib/components/fs_entry_move_form.dart b/lib/components/fs_entry_move_form.dart index 95b3a347d4..6034842334 100644 --- a/lib/components/fs_entry_move_form.dart +++ b/lib/components/fs_entry_move_form.dart @@ -1,4 +1,5 @@ import 'package:ardrive/blocs/blocs.dart'; +import 'package:ardrive/blocs/hide/global_hide_bloc.dart'; import 'package:ardrive/core/crypto/crypto.dart'; import 'package:ardrive/models/models.dart'; import 'package:ardrive/pages/drive_detail/models/data_table_item.dart'; @@ -33,7 +34,6 @@ Future promptToMove( driveDao: context.read(), profileCubit: context.read(), syncCubit: context.read(), - driveDetailCubit: parentContext.read(), )..add(const FsEntryMoveInitial()), ), BlocProvider.value( @@ -98,6 +98,9 @@ class FsEntryMoveForm extends StatelessWidget { FsEntryMoveSkipConflicts( folderInView: state.folderInView, conflictingItems: state.conflictingItems, + showHiddenItems: context + .read() + .state is ShowingHiddenItems, ), ); }, @@ -107,12 +110,10 @@ class FsEntryMoveForm extends StatelessWidget { ); } if (state is FsEntryMoveLoadSuccess) { - final isShowingHiddenFiles = - (driveDetailState as DriveDetailLoadSuccess) - .isShowingHiddenFiles; + final globalHideBloc = context.read(); final List subFolders; - if (isShowingHiddenFiles) { + if (globalHideBloc.state is ShowingHiddenItems) { subFolders = state.viewingFolder.subfolders; } else { subFolders = state.viewingFolder.subfolders @@ -347,6 +348,8 @@ class FsEntryMoveForm extends StatelessWidget { FsEntryMoveSubmit( folderInView: state.viewingFolder.folder, + showHiddenItems: globalHideBloc + .state is ShowingHiddenItems, ), ); context diff --git a/lib/components/hide_dialog.dart b/lib/components/hide_dialog.dart index b8fb4671b6..13177abfa4 100644 --- a/lib/components/hide_dialog.dart +++ b/lib/components/hide_dialog.dart @@ -1,10 +1,10 @@ import 'package:ardrive/blocs/drive_detail/drive_detail_cubit.dart'; +import 'package:ardrive/blocs/hide/global_hide_bloc.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/models/data_table_item.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'; @@ -42,6 +42,16 @@ Future promptToToggleHideState( folderId: item.id, )); } + } else if (item is DriveDataItem) { + if (isHidden) { + hideBloc.add(UnhideDriveEvent( + driveId: item.driveId, + )); + } else { + hideBloc.add(HideDriveEvent( + driveId: item.driveId, + )); + } } else { throw UnimplementedError('Unknown item type: ${item.runtimeType}'); } @@ -67,15 +77,18 @@ class HideDialog extends StatelessWidget { listener: (context, state) { if (state is SuccessHideState) { Navigator.of(context).pop(); - logger.d('Successfully hid/unhid entity'); _driveDetailCubit.refreshDriveDataTable(); + context.read().add(RefreshOptions( + userHasHiddenItems: + context.read().state.userHasHiddenDrive, + )); } else if (state is ConfirmingHideState) { _driveDetailCubit.refreshDriveDataTable(); context.read().add(const ConfirmUploadEvent()); } }, builder: (context, state) { - return ArDriveStandardModal( + return ArDriveStandardModalNew( title: _buildTitle(context, state), content: _buildContent(context, state), actions: _buildActions(context, state), @@ -96,6 +109,10 @@ class HideDialog extends StatelessWidget { return appLocalizationsOf(context).failedToUnhideFile; case HideAction.unhideFolder: return appLocalizationsOf(context).failedToUnhideFolder; + case HideAction.hideDrive: + return 'Failed to hide drive'; + case HideAction.unhideDrive: + return 'Failed to unhide drive'; } } @@ -108,6 +125,10 @@ class HideDialog extends StatelessWidget { return appLocalizationsOf(context).unhidingFile; case HideAction.unhideFolder: return appLocalizationsOf(context).unhidingFolder; + case HideAction.hideDrive: + return 'Hiding drive'; + case HideAction.unhideDrive: + return 'Unhiding drive'; } } @@ -124,6 +145,10 @@ class HideDialog extends StatelessWidget { return Text(appLocalizationsOf(context).failedToUnhideFile); case HideAction.unhideFolder: return Text(appLocalizationsOf(context).failedToUnhideFolder); + case HideAction.hideDrive: + return const Text('Failed to hide drive'); + case HideAction.unhideDrive: + return const Text('Failed to unhide drive'); } } diff --git a/lib/components/side_bar.dart b/lib/components/side_bar.dart index d58a9e9d05..55908179b1 100644 --- a/lib/components/side_bar.dart +++ b/lib/components/side_bar.dart @@ -2,6 +2,7 @@ import 'package:ardrive/blocs/drive_detail/drive_detail_cubit.dart'; import 'package:ardrive/blocs/drives/drives_cubit.dart'; +import 'package:ardrive/blocs/hide/global_hide_bloc.dart'; import 'package:ardrive/blocs/profile/profile_cubit.dart'; import 'package:ardrive/components/app_version_widget.dart'; import 'package:ardrive/components/new_button/new_button.dart'; @@ -104,9 +105,9 @@ class _AppSideBarState extends State { (state.userDrives.isNotEmpty || state.sharedDrives.isNotEmpty)) { return Flexible( - child: _buildAccordion( - state, - true, + child: _Accordion( + state: state, + isMobile: true, ), ); } @@ -209,9 +210,9 @@ class _AppSideBarState extends State { child: Padding( padding: const EdgeInsets.only( left: 43.0), - child: _buildAccordion( - state, - false, + child: _Accordion( + isMobile: false, + state: state, ), ), ); @@ -271,94 +272,6 @@ class _AppSideBarState extends State { ); } - Widget _buildAccordion(DrivesLoadSuccess state, bool isMobile) { - final typography = ArDriveTypographyNew.of(context); - final colorTokens = ArDriveTheme.of(context).themeData.colorTokens; - - return ArDriveAccordion( - contentPadding: isMobile ? const EdgeInsets.all(4) : null, - key: ValueKey(state.userDrives.map((e) => e.name)), - backgroundColor: ArDriveTheme.of(context).themeData.backgroundColor, - children: [ - if (state.userDrives.isNotEmpty) - ArDriveAccordionItem( - isExpanded: true, - Text( - appLocalizationsOf(context).publicDrives.toUpperCase(), - style: typography.paragraphNormal( - fontWeight: ArFontWeight.semiBold, - color: colorTokens.textHigh, - ), - ), - state.userDrives - .where((element) => element.isPublic) - .map( - (d) => DriveListTile( - hasAlert: state.drivesWithAlerts.contains(d.id), - drive: d, - onTap: () { - if (state.selectedDriveId == d.id) { - // opens the root folder - context.read().openFolder(); - return; - } - context.read().selectDrive(d.id); - }, - isSelected: state.selectedDriveId == d.id, - ), - ) - .toList(), - ), - if (state.userDrives.isNotEmpty) - ArDriveAccordionItem( - isExpanded: true, - Text( - appLocalizationsOf(context).privateDrives.toUpperCase(), - style: typography.paragraphNormal( - fontWeight: ArFontWeight.semiBold, - color: colorTokens.textHigh, - ), - ), - state.userDrives - .where((element) => element.isPrivate) - .map( - (d) => DriveListTile( - hasAlert: state.drivesWithAlerts.contains(d.id), - drive: d, - onTap: () { - context.read().selectDrive(d.id); - }, - isSelected: state.selectedDriveId == d.id, - ), - ) - .toList(), - ), - if (state.sharedDrives.isNotEmpty) - ArDriveAccordionItem( - isExpanded: true, - Text( - appLocalizationsOf(context).sharedDrives.toUpperCase(), - style: typography.paragraphNormal( - fontWeight: ArFontWeight.semiBold, - ), - ), - state.sharedDrives - .map( - (d) => DriveListTile( - hasAlert: state.drivesWithAlerts.contains(d.id), - drive: d, - onTap: () { - context.read().selectDrive(d.id); - }, - isSelected: state.selectedDriveId == d.id, - ), - ) - .toList(), - ), - ], - ); - } - Widget _buildSideBarBottom() { return _isExpanded ? Padding( @@ -522,6 +435,7 @@ class DriveListTile extends StatelessWidget { final Drive drive; final bool hasAlert; final bool isSelected; + final bool isHidden; final VoidCallback onTap; const DriveListTile({ @@ -529,12 +443,14 @@ class DriveListTile extends StatelessWidget { required this.drive, required this.isSelected, required this.onTap, + required this.isHidden, this.hasAlert = false, }); @override Widget build(BuildContext context) { final typography = ArDriveTypographyNew.of(context); + final colorTokens = ArDriveTheme.of(context).themeData.colorTokens; return GestureDetector( key: key, @@ -551,19 +467,33 @@ class DriveListTile extends StatelessWidget { Flexible( child: HoverWidget( hoverScale: 1, - child: Text( - drive.name, - style: isSelected - ? typography.paragraphNormal( - fontWeight: ArFontWeight.semiBold, - ) - : typography.paragraphNormal( - fontWeight: ArFontWeight.semiBold, - color: ArDriveTheme.of(context) - .themeData - .colorTokens - .textLow, - ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + child: Text( + drive.name, + style: isSelected + ? typography.paragraphNormal( + fontWeight: ArFontWeight.semiBold, + ) + : typography.paragraphNormal( + fontWeight: ArFontWeight.semiBold, + color: isHidden + ? colorTokens.textLow + : ArDriveTheme.of(context) + .themeData + .colorTokens + .textMid, + ), + ), + ), + if (isHidden) ...{ + const SizedBox(width: 8), + ArDriveIcons.eyeClosed( + size: 16, color: colorTokens.textLow), + }, + ], ), ), ), @@ -701,3 +631,118 @@ Future shareLogs({ ), ); } + +class _Accordion extends StatelessWidget { + const _Accordion({required this.state, required this.isMobile}); + + final DrivesLoadSuccess state; + final bool isMobile; + + @override + Widget build(BuildContext context) { + final typography = ArDriveTypographyNew.of(context); + final colorTokens = ArDriveTheme.of(context).themeData.colorTokens; + + return BlocBuilder( + builder: (context, hideState) { + return ArDriveAccordion( + contentPadding: isMobile ? const EdgeInsets.all(4) : null, + backgroundColor: ArDriveTheme.of(context).themeData.backgroundColor, + children: [ + if (state.userDrives.isNotEmpty) + ArDriveAccordionItem( + isExpanded: true, + Text( + appLocalizationsOf(context).publicDrives.toUpperCase(), + style: typography.paragraphNormal( + fontWeight: ArFontWeight.semiBold, + color: colorTokens.textHigh, + ), + ), + state.userDrives + .where((element) { + final isHidden = hideState is HiddingItems; + + return element.isPublic && + (isHidden ? !element.isHidden : true); + }) + .map( + (d) => DriveListTile( + hasAlert: state.drivesWithAlerts.contains(d.id), + drive: d, + onTap: () { + if (state.selectedDriveId == d.id) { + // opens the root folder + context.read().openFolder(); + return; + } + context.read().selectDrive(d.id); + }, + isSelected: state.selectedDriveId == d.id, + isHidden: d.isHidden, + ), + ) + .toList(), + ), + if (state.userDrives.isNotEmpty) + ArDriveAccordionItem( + isExpanded: true, + Text( + appLocalizationsOf(context).privateDrives.toUpperCase(), + style: typography.paragraphNormal( + fontWeight: ArFontWeight.semiBold, + color: colorTokens.textHigh, + ), + ), + state.userDrives + .where((element) { + final isHidden = hideState is HiddingItems; + + return element.isPrivate && + (isHidden ? !element.isHidden : true); + }) + .map( + (d) => DriveListTile( + hasAlert: state.drivesWithAlerts.contains(d.id), + drive: d, + onTap: () { + context.read().selectDrive(d.id); + }, + isSelected: state.selectedDriveId == d.id, + isHidden: d.isHidden, + ), + ) + .toList(), + ), + if (state.sharedDrives.isNotEmpty) + ArDriveAccordionItem( + isExpanded: true, + Text( + appLocalizationsOf(context).sharedDrives.toUpperCase(), + style: typography.paragraphNormal( + fontWeight: ArFontWeight.semiBold, + ), + ), + + /// Shared drives are always visible + state.sharedDrives + .map( + (d) => DriveListTile( + hasAlert: state.drivesWithAlerts.contains(d.id), + drive: d, + onTap: () { + context.read().selectDrive(d.id); + }, + isSelected: state.selectedDriveId == d.id, + isHidden: false, + ), + ) + .toList(), + ), + ], + ); + }, + ); + } +} +// diff --git a/lib/components/upload_form.dart b/lib/components/upload_form.dart index d5783dc195..d8878d0859 100644 --- a/lib/components/upload_form.dart +++ b/lib/components/upload_form.dart @@ -8,7 +8,6 @@ import 'package:ardrive/arns/presentation/assign_name_modal.dart'; import 'package:ardrive/authentication/ardrive_auth.dart'; import 'package:ardrive/blocs/blocs.dart'; import 'package:ardrive/blocs/create_manifest/create_manifest_cubit.dart'; -import 'package:ardrive/blocs/feedback_survey/feedback_survey_cubit.dart'; import 'package:ardrive/blocs/upload/enums/conflicting_files_actions.dart'; import 'package:ardrive/blocs/upload/limits.dart'; import 'package:ardrive/blocs/upload/payment_method/bloc/upload_payment_method_bloc.dart'; @@ -28,6 +27,8 @@ import 'package:ardrive/core/arfs/repository/folder_repository.dart'; import 'package:ardrive/core/arfs/utils/arfs_revision_status_utils.dart'; import 'package:ardrive/core/upload/domain/repository/upload_repository.dart'; import 'package:ardrive/core/upload/uploader.dart'; +import 'package:ardrive/core/upload/view/blocs/upload_manifest_options_bloc.dart'; +import 'package:ardrive/core/upload/view/manifest_options/manifest_options.dart'; import 'package:ardrive/entities/manifest_data.dart'; import 'package:ardrive/l11n/validation_messages.dart'; import 'package:ardrive/main.dart'; @@ -51,6 +52,7 @@ import 'package:ardrive_ui/ardrive_ui.dart'; import 'package:ardrive_uploader/ardrive_uploader.dart'; import 'package:ardrive_utils/ardrive_utils.dart'; import 'package:ario_sdk/ario_sdk.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; @@ -218,7 +220,6 @@ class _UploadFormState extends State { if (state is UploadComplete || state is UploadWalletMismatch) { if (!_isShowingCancelDialog) { Navigator.pop(context); - context.read().openRemindMe(); context.read().setUploading(false); context.read().startSync(); } @@ -253,8 +254,57 @@ class _UploadFormState extends State { state is UploadPreparationInitialized) { return _PreparingUploadWidget(state: state); } else if (state is UploadReady) { - return _UploadReadyWidget( - state: state, driveDetailCubit: widget.driveDetailCubit); + return BlocProvider( + create: (context) { + List manifestSelections = []; + List selectedManifestIds = []; + + manifestSelections = state.manifestFiles.map((e) { + final selectedManifest = state.selectedManifestSelections + .firstWhereOrNull((selectedManifest) => + selectedManifest.manifest.id == e.entry.id); + + ANTRecord? antRecord; + ARNSUndername? undername; + + if (selectedManifest != null) { + antRecord = selectedManifest.antRecord; + undername = selectedManifest.undername; + selectedManifestIds.add(e.entry.id); + } + + return ManifestSelection( + manifest: e.entry, + antRecord: antRecord, + undername: undername, + ); + }).toList(); + + return UploadManifestOptionsBloc( + manifestFiles: manifestSelections, + arnsRepository: context.read(), + arDriveAuth: context.read(), + selectedManifestIds: selectedManifestIds, + )..add(LoadAnts()); + }, + child: BlocListener( + listener: (context, state) { + if (state is UploadManifestOptionsReady) { + context.read().updateManifestSelection( + state.manifestFiles + .where((e) => state.selectedManifestIds + .contains(e.manifest.id)) + .toList(), + ); + } + }, + child: _UploadReadyWidget( + state: state, + driveDetailCubit: widget.driveDetailCubit, + ), + ), + ); } else if (state is UploadConfiguringLicense) { return _UploadConfiguringLicenseWidget(state: state); } else if (state is UploadReview) { @@ -330,31 +380,43 @@ class _UploadingManifestsWidget extends StatelessWidget { itemCount: state.manifestFiles.length, shrinkWrap: true, itemBuilder: (context, index) { - return Row( + return Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - ArDriveIcons.manifest(size: 16), - const SizedBox(width: 8), - Text( - '${state.manifestFiles[index].name}...', - style: typography.paragraphNormal( - fontWeight: ArFontWeight.semiBold, - ), - ), - const Spacer(), - if (state.manifestFiles[index].isUploading) ...[ - const SizedBox(width: 8), - const SizedBox( - height: 16, - width: 16, - child: CircularProgressIndicator( - strokeWidth: 2, + Row( + children: [ + ArDriveIcons.manifest(size: 16), + const SizedBox(width: 8), + Text( + '${state.manifestFiles[index].entry.name}...', + style: typography.paragraphNormal( + fontWeight: ArFontWeight.semiBold, + ), ), + const Spacer(), + if (state.manifestFiles[index].isUploading) ...[ + const SizedBox(width: 8), + const SizedBox( + height: 16, + width: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + ), + ), + ], + if (state.manifestFiles[index].isCompleted) ...[ + const SizedBox(width: 8), + ArDriveIcons.checkCirle(size: 16), + ], + ], + ), + if (state.manifestFiles[index].isAssigningUndername) ...[ + const SizedBox(height: 2), + Text( + 'Assigning ArNS Name...', + style: typography.paragraphNormal(), ), ], - if (state.manifestFiles[index].isCompleted) ...[ - const SizedBox(width: 8), - ArDriveIcons.checkCirle(size: 16), - ], ], ); }, @@ -1010,72 +1072,8 @@ class _UploadReadyModalBaseState extends State { Widget manifestOptionsView( UploadReady state, BuildContext context, ArdriveTypographyNew typography, {bool scrollable = true}) { - return Padding( - padding: const EdgeInsets.only(bottom: 42.0), - child: ListView.builder( - physics: scrollable - ? const ScrollPhysics() - : const NeverScrollableScrollPhysics(), - shrinkWrap: true, - itemCount: state.manifestFiles.length, - itemBuilder: (context, index) { - final file = state.manifestFiles[index]; - final hiddenColor = - ArDriveTheme.of(context).themeData.colors.themeFgDisabled; - - return Row( - mainAxisSize: MainAxisSize.max, - children: [ - Flexible( - flex: 2, - child: Row( - children: [ - ArDriveIcons.manifest( - size: 16, color: file.isHidden ? hiddenColor : null), - const SizedBox(width: 8), - Text( - file.name, - style: typography.paragraphNormal( - color: file.isHidden ? hiddenColor : null, - ), - ), - if (file.isHidden) ...[ - const SizedBox(width: 8), - Text('(hidden)', - style: typography.paragraphNormal( - color: hiddenColor, - )) - ] - ], - ), - ), - Flexible( - flex: 1, - child: ArDriveCheckBox( - checked: state.selectedManifests - .contains(state.manifestFiles[index]), - onChange: (value) { - if (value) { - context - .read() - .selectManifestFile(state.manifestFiles[index]); - } else { - context - .read() - .unselectManifestFile(state.manifestFiles[index]); - } - }, - ), - ), - // TODO: Add back when we have the right UI for it - // const Expanded( - // flex: 1, - // child: AntSelector(), - // ), - ], - ); - }, - ), + return ManifestOptions( + scrollable: scrollable, ); } } @@ -2033,6 +2031,17 @@ class _UploadReviewWithLicenseWidget extends StatelessWidget { final typography = ArDriveTypographyNew.of(context); final colorTokens = ArDriveTheme.of(context).themeData.colorTokens; + double heightForManifestSelections = + (readyState.selectedManifestSelections.length * 30) + 50; + + if (heightForManifestSelections > 200) { + heightForManifestSelections = 175; + } + + if (readyState.params.arnsUnderName == null) { + heightForManifestSelections += 50; + } + return StatsScreen( readyState: readyState, modalActions: [ @@ -2083,12 +2092,13 @@ class _UploadReviewWithLicenseWidget extends StatelessWidget { ), ), ], - if (state.readyState.selectedManifests.isNotEmpty) ...[ + LicenseReviewInfo(licenseState: state.licenseState), + if (state.readyState.selectedManifestSelections.isNotEmpty) ...[ Padding( - padding: const EdgeInsets.only(bottom: 16.0), + padding: const EdgeInsets.only(bottom: 8.0), child: ConstrainedBox( - constraints: const BoxConstraints( - maxHeight: 125, + constraints: BoxConstraints( + maxHeight: heightForManifestSelections, minWidth: kLargeDialogWidth, ), child: Column( @@ -2096,7 +2106,6 @@ class _UploadReviewWithLicenseWidget extends StatelessWidget { mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.start, children: [ - const SizedBox(height: 8), Text( 'Updated manifest(s):', style: typography.paragraphNormal( @@ -2105,36 +2114,61 @@ class _UploadReviewWithLicenseWidget extends StatelessWidget { ), ), const SizedBox(height: 4), - Flexible( - child: Expanded( - child: ListView( - shrinkWrap: true, - children: [ - ...state.readyState.selectedManifests.map( - (e) => Row( - children: [ - ArDriveIcons.manifest(size: 16), - const SizedBox(width: 8), - Text( - e.name, - style: typography.paragraphNormal( - fontWeight: ArFontWeight.semiBold, + Expanded( + child: ListView( + shrinkWrap: true, + children: [ + ...state.readyState.selectedManifestSelections.map( + (e) => Column( + children: [ + Row( + children: [ + ArDriveIcons.manifest(size: 16), + const SizedBox(width: 8), + Flexible( + child: Text( + e.manifest.name, + style: typography.paragraphNormal( + fontWeight: ArFontWeight.semiBold, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), ), + ], + ), + if (e.antRecord != null || + e.undername != null) ...[ + const SizedBox(height: 2), + Row( + children: [ + ArDriveIcons.arnsName(size: 16), + const SizedBox(width: 8), + Flexible( + child: Text( + getLiteralArNSName( + e.antRecord!, e.undername), + style: typography.paragraphNormal( + fontWeight: ArFontWeight.semiBold, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], ), ], - ), + ], ), - ], - ), + ), + ], ), ), - const SizedBox(height: 8), ], ), ), ), ], - LicenseReviewInfo(licenseState: state.licenseState), ], ); } @@ -2150,6 +2184,17 @@ class _UploadReviewWithArnsNameWidget extends StatelessWidget { final typography = ArDriveTypographyNew.of(context); final colorTokens = ArDriveTheme.of(context).themeData.colorTokens; + double heightForManifestSelections = + (state.readyState.selectedManifestSelections.length * 30) + 50; + + if (heightForManifestSelections > 200) { + heightForManifestSelections = 200; + } + + if (state.readyState.params.arnsUnderName == null) { + heightForManifestSelections += 50; + } + return StatsScreen( readyState: state.readyState, modalActions: [ @@ -2195,12 +2240,12 @@ class _UploadReviewWithArnsNameWidget extends StatelessWidget { ), ), ], - if (state.readyState.selectedManifests.isNotEmpty) ...[ + if (state.readyState.selectedManifestSelections.isNotEmpty) ...[ Padding( - padding: const EdgeInsets.only(bottom: 16.0), + padding: const EdgeInsets.only(bottom: 8.0), child: ConstrainedBox( - constraints: const BoxConstraints( - // maxHeight: 125, + constraints: BoxConstraints( + maxHeight: heightForManifestSelections, minWidth: kLargeDialogWidth, ), child: Column( @@ -2208,7 +2253,6 @@ class _UploadReviewWithArnsNameWidget extends StatelessWidget { mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.start, children: [ - const SizedBox(height: 8), Text( 'Updated manifest(s):', style: typography.paragraphNormal( @@ -2217,28 +2261,56 @@ class _UploadReviewWithArnsNameWidget extends StatelessWidget { ), ), const SizedBox(height: 4), - Flexible( + Expanded( child: ListView( shrinkWrap: true, children: [ - ...state.readyState.selectedManifests.map( - (e) => Row( - mainAxisSize: MainAxisSize.min, + ...state.readyState.selectedManifestSelections.map( + (e) => Column( children: [ - ArDriveIcons.manifest(size: 16), - const SizedBox(width: 8), - Text( - e.name, - style: typography.paragraphNormal( - fontWeight: ArFontWeight.semiBold, - ), + Row( + children: [ + ArDriveIcons.manifest(size: 16), + const SizedBox(width: 8), + Flexible( + child: Text( + e.manifest.name, + style: typography.paragraphNormal( + fontWeight: ArFontWeight.semiBold, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], ), + if (e.antRecord != null || + e.undername != null) ...[ + const SizedBox(height: 2), + Row( + children: [ + ArDriveIcons.arnsName(size: 16), + const SizedBox(width: 8), + Flexible( + child: Text( + getLiteralArNSName( + e.antRecord!, e.undername), + style: typography.paragraphNormal( + fontWeight: ArFontWeight.semiBold, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ], ], ), ), ], ), - ) + ), ], ), ), @@ -2881,7 +2953,7 @@ List getModalActions( variant: ButtonVariant.primary, ), ]; - } else if (state.selectedManifests.isNotEmpty) { + } else if (state.selectedManifestSelections.isNotEmpty) { return [ ArDriveButtonNew( isDisabled: !state.isNextButtonEnabled, @@ -2912,83 +2984,11 @@ List getModalActions( ]; } -class AntSelector extends StatefulWidget { - const AntSelector({super.key}); - - @override - State createState() => _AntSelectorState(); -} - -class _AntSelectorState extends State { - final List ants = ['Ant 1', 'Ant 2', 'Ant 3']; - String _selectedAnt = 'Ant 1'; - - @override - Widget build(BuildContext context) { - return ArDriveDropdown( - height: 45, - items: ants.map((ant) => _buildDropdownItem(context, ant)).toList(), - child: ArDriveClickArea( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Row( - children: [ - _buildSelectedItem(context), - ], - ), - ArDriveIcons.chevronDown(), - ], - ), - ), - ); +String getLiteralArNSName(ANTRecord record, ARNSUndername? undername) { + if (undername != null) { + return getLiteralARNSRecordName(undername); } - Widget _buildSelectedItem(BuildContext context) { - final typography = ArDriveTypographyNew.of(context); - - return Text( - _selectedAnt, - style: typography.paragraphLarge( - fontWeight: ArFontWeight.semiBold, - ), - ); - } - - ArDriveDropdownItem _buildDropdownItem(BuildContext context, String ant) { - final typography = ArDriveTypographyNew.of(context); - - return ArDriveDropdownItem( - content: SizedBox( - width: 164, - height: 45, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - ant, - style: typography.paragraphLarge( - fontWeight: ArFontWeight.semiBold, - ), - ), - if (ant == 'Ant 1') - ArDriveIcons.checkmark( - size: 16, - ) - ], - ), - ), - ), - onClick: () { - setState(() { - _selectedAnt = ant; - }); - }, - ); - } + return record.domain; } - // diff --git a/lib/core/upload/view/blocs/upload_manifest_options_bloc.dart b/lib/core/upload/view/blocs/upload_manifest_options_bloc.dart new file mode 100644 index 0000000000..784a3eca9b --- /dev/null +++ b/lib/core/upload/view/blocs/upload_manifest_options_bloc.dart @@ -0,0 +1,164 @@ +import 'package:ardrive/arns/domain/arns_repository.dart'; +import 'package:ardrive/authentication/ardrive_auth.dart'; +import 'package:ardrive/models/models.dart'; +import 'package:ario_sdk/ario_sdk.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +part 'upload_manifest_options_event.dart'; +part 'upload_manifest_options_state.dart'; + +/// Outputs the selected manifest +/// with the corresponding names if any +class UploadManifestOptionsBloc + extends Bloc { + final ARNSRepository arnsRepository; + final ArDriveAuth arDriveAuth; + + final List manifestFiles; + final Set _selectedManifestIds = {}; + final Set _showingArNSSelection = {}; + Map> reservedNames = {}; + + List? _ants; + + UploadManifestOptionsBloc({ + required this.manifestFiles, + required this.arnsRepository, + required this.arDriveAuth, + List? selectedManifestIds, + }) : super(UploadManifestOptionsReady( + manifestFiles: Set.from(manifestFiles), + selectedManifestIds: Set.from(selectedManifestIds ?? []), + showingArNSSelection: const {}, + ants: null, + reservedNames: const {}, + arnsNamesLoaded: false, + )) { + if (selectedManifestIds != null) { + _selectedManifestIds.addAll(selectedManifestIds); + + final selectedManifests = selectedManifestIds + .map((id) => manifestFiles.firstWhere((e) => e.manifest.id == id)) + .toList(); + + for (var manifest in selectedManifests) { + if (manifest.antRecord != null) { + if (reservedNames[manifest.antRecord!.domain] == null) { + reservedNames[manifest.antRecord!.domain] = []; + } + + reservedNames[manifest.antRecord!.domain]! + .add(manifest.undername?.name ?? '@'); + } + } + } + + on((event, emit) async { + final walletAddress = await arDriveAuth.getWalletAddress(); + _ants = await arnsRepository.getAntRecordsForWallet(walletAddress!); + emit(_createReadyState()); + }); + + on((event, emit) async { + _selectedManifestIds.add(event.manifest.id); + emit(_createReadyState()); + }); + + on((event, emit) { + _selectedManifestIds.remove(event.manifest.id); + _showingArNSSelection.remove(event.manifest.id); + + final indexOf = + manifestFiles.indexWhere((e) => e.manifest.id == event.manifest.id); + + if (manifestFiles[indexOf].antRecord != null) { + reservedNames[manifestFiles[indexOf].antRecord!.domain]! + .remove(manifestFiles[indexOf].undername?.name ?? '@'); + + manifestFiles[indexOf] = ManifestSelection( + manifest: manifestFiles[indexOf].manifest, + ); + } + + emit(_createReadyState()); + }); + + on((event, emit) async { + _showingArNSSelection.add(event.manifest.id); + + emit(_createReadyState()); + }); + + on((event, emit) { + _showingArNSSelection.remove(event.manifest.id); + emit(_createReadyState()); + }); + + on((event, emit) { + if (reservedNames[event.antRecord.domain] == null) { + reservedNames[event.antRecord.domain] = []; + } + + final indexOf = + manifestFiles.indexWhere((e) => e.manifest.id == event.manifest.id); + + final manifest = manifestFiles[indexOf]; + + if (manifest.antRecord != null) { + reservedNames[manifest.antRecord!.domain]! + .remove(manifest.undername?.name ?? '@'); + } + + manifestFiles[indexOf] = manifestFiles[indexOf].copyWith( + antRecord: event.antRecord, + undername: event.undername, + ); + + reservedNames[event.antRecord.domain]!.add(event.undername?.name ?? '@'); + _showingArNSSelection.remove(event.manifest.id); + emit(_createReadyState()); + }); + } + + Future> getARNSUndernames( + ANTRecord antRecord, + ) async { + return arnsRepository.getARNSUndernames(antRecord); + } + + UploadManifestOptionsReady _createReadyState() { + return UploadManifestOptionsReady( + manifestFiles: Set.from(manifestFiles), + selectedManifestIds: Set.from(_selectedManifestIds), + showingArNSSelection: Set.from(_showingArNSSelection), + ants: _ants, + reservedNames: reservedNames, + arnsNamesLoaded: _ants != null, + ); + } +} + +class ManifestSelection extends Equatable { + final FileEntry manifest; + final ANTRecord? antRecord; + final ARNSUndername? undername; + + const ManifestSelection({ + required this.manifest, + this.antRecord, + this.undername, + }); + + @override + List get props => [manifest, antRecord?.domain, undername?.name]; + + ManifestSelection copyWith({ + ANTRecord? antRecord, + ARNSUndername? undername, + }) { + return ManifestSelection( + manifest: manifest, antRecord: antRecord, undername: undername); + } +} +// diff --git a/lib/core/upload/view/blocs/upload_manifest_options_event.dart b/lib/core/upload/view/blocs/upload_manifest_options_event.dart new file mode 100644 index 0000000000..d28392cd8e --- /dev/null +++ b/lib/core/upload/view/blocs/upload_manifest_options_event.dart @@ -0,0 +1,49 @@ +part of 'upload_manifest_options_bloc.dart'; + +sealed class UploadManifestOptionsEvent extends Equatable { + const UploadManifestOptionsEvent(); + + @override + List get props => []; +} + +final class SelectManifest extends UploadManifestOptionsEvent { + final FileEntry manifest; + + const SelectManifest({required this.manifest}); + + @override + List get props => [manifest]; +} + +final class DeselectManifest extends UploadManifestOptionsEvent { + final FileEntry manifest; + + const DeselectManifest({required this.manifest}); +} + +final class ShowArNSSelection extends UploadManifestOptionsEvent { + final FileEntry manifest; + + const ShowArNSSelection({required this.manifest}); +} + +final class HideArNSSelection extends UploadManifestOptionsEvent { + final FileEntry manifest; + + const HideArNSSelection({required this.manifest}); +} + +final class LinkManifestToUndername extends UploadManifestOptionsEvent { + final FileEntry manifest; + final ANTRecord antRecord; + final ARNSUndername? undername; + + const LinkManifestToUndername({ + required this.manifest, + required this.antRecord, + required this.undername, + }); +} + +final class LoadAnts extends UploadManifestOptionsEvent {} diff --git a/lib/core/upload/view/blocs/upload_manifest_options_state.dart b/lib/core/upload/view/blocs/upload_manifest_options_state.dart new file mode 100644 index 0000000000..9b8a72164c --- /dev/null +++ b/lib/core/upload/view/blocs/upload_manifest_options_state.dart @@ -0,0 +1,37 @@ +part of 'upload_manifest_options_bloc.dart'; + +sealed class UploadManifestOptionsState extends Equatable { + const UploadManifestOptionsState(); + + @override + List get props => []; +} + +final class UploadManifestOptionsInitial extends UploadManifestOptionsState {} + +final class UploadManifestOptionsReady extends UploadManifestOptionsState { + final Set manifestFiles; + final Set selectedManifestIds; + final Set showingArNSSelection; + final List? ants; + final Map> reservedNames; + final bool arnsNamesLoaded; + + const UploadManifestOptionsReady({ + required this.manifestFiles, + required this.selectedManifestIds, + required this.showingArNSSelection, + required this.ants, + required this.reservedNames, + required this.arnsNamesLoaded, + }); + + @override + List get props => [ + manifestFiles, + selectedManifestIds, + showingArNSSelection, + ants, + reservedNames, + ]; +} diff --git a/lib/core/upload/view/manifest_options/manifest_options.dart b/lib/core/upload/view/manifest_options/manifest_options.dart new file mode 100644 index 0000000000..755957b05f --- /dev/null +++ b/lib/core/upload/view/manifest_options/manifest_options.dart @@ -0,0 +1,518 @@ +import 'package:ardrive/components/components.dart'; +import 'package:ardrive/core/upload/view/blocs/upload_manifest_options_bloc.dart'; +import 'package:ardrive_ui/ardrive_ui.dart'; +import 'package:ario_sdk/ario_sdk.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class ManifestOptions extends StatelessWidget { + const ManifestOptions({super.key, this.scrollable = true}); + + final bool scrollable; + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is UploadManifestOptionsReady) { + final selectedManifestIds = state.selectedManifestIds; + final manifestFiles = state.manifestFiles; + + return Padding( + padding: const EdgeInsets.only(bottom: 42.0), + child: ListView.separated( + separatorBuilder: (context, index) => const SizedBox(height: 4), + physics: scrollable + ? const ScrollPhysics() + : const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: manifestFiles.length, + itemBuilder: (context, index) { + final file = manifestFiles.elementAt(index).manifest; + final isSelected = selectedManifestIds.contains(file.id); + + return _ManifestOptionTile( + manifestSelection: manifestFiles.elementAt(index), + isSelected: isSelected, + onSelect: () { + if (isSelected) { + context + .read() + .add(DeselectManifest(manifest: file)); + } else { + context + .read() + .add(SelectManifest(manifest: file)); + } + }, + ); + }, + ), + ); + } + + return const SizedBox(); + }, + ); + } +} + +class _ManifestOptionTile extends StatefulWidget { + final ManifestSelection manifestSelection; + final bool isSelected; + final VoidCallback onSelect; + + const _ManifestOptionTile({ + required this.manifestSelection, + required this.isSelected, + required this.onSelect, + }); + + @override + State<_ManifestOptionTile> createState() => __ManifestOptionTileState(); +} + +class __ManifestOptionTileState extends State<_ManifestOptionTile> { + @override + Widget build(BuildContext context) { + final state = context.read().state; + + if (state is UploadManifestOptionsReady) { + final isExpanded = state.showingArNSSelection + .contains(widget.manifestSelection.manifest.id); + final file = widget.manifestSelection.manifest; + + final hiddenColor = + ArDriveTheme.of(context).themeData.colors.themeFgDisabled; + final typography = ArDriveTypographyNew.of(context); + final colorTokens = ArDriveTheme.of(context).themeData.colorTokens; + final showingName = !isExpanded && + (widget.manifestSelection.antRecord != null || + widget.manifestSelection.undername != null); + final hasSelectedAnt = widget.manifestSelection.antRecord != null; + + return AnimatedContainer( + duration: const Duration(milliseconds: 300), + decoration: BoxDecoration( + color: colorTokens.containerL2, + borderRadius: BorderRadius.circular(5), + ), + curve: Curves.easeInOut, + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + height: isExpanded + ? 168 + : showingName + ? 70 + : 50, + child: GestureDetector( + onTap: () {}, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + Row( + mainAxisSize: MainAxisSize.max, + children: [ + Expanded( + child: Row( + children: [ + Flexible( + flex: 2, + child: Row( + children: [ + ArDriveIcons.manifest( + size: 16, + color: file.isHidden ? hiddenColor : null), + const SizedBox(width: 8), + Text( + file.name, + style: typography.paragraphNormal( + color: file.isHidden ? hiddenColor : null, + ), + ), + if (file.isHidden) ...[ + const SizedBox(width: 8), + Text('(hidden)', + style: typography.paragraphNormal( + color: hiddenColor, + )) + ] + ], + ), + ), + Flexible( + flex: 1, + child: ArDriveCheckBox( + checked: widget.isSelected, + onChange: (value) { + if (value) { + context + .read() + .add(SelectManifest(manifest: file)); + } else { + context + .read() + .add(DeselectManifest(manifest: file)); + } + }, + ), + ), + ], + ), + ), + ArDriveTooltip( + message: (state.arnsNamesLoaded && state.ants!.isEmpty) + ? 'No ArNS names found for your wallet' + : '', + child: ArDriveButtonNew( + text: !state.arnsNamesLoaded + ? 'Loading Names...' + : hasSelectedAnt + ? 'Change ArNS' + : 'Add ArNS', + typography: typography, + isDisabled: isExpanded || + !widget.isSelected || + !state.arnsNamesLoaded || + (state.arnsNamesLoaded && state.ants!.isEmpty), + fontStyle: typography.paragraphSmall(), + variant: ButtonVariant.primary, + maxWidth: state.arnsNamesLoaded ? 100 : 120, + maxHeight: 30, + onPressed: () { + context + .read() + .add(ShowArNSSelection(manifest: file)); + }, + ), + ) + // TODO: Add back when we have the right UI for it + ], + ), + if (showingName) ...[ + const SizedBox(height: 8), + Row( + children: [ + ArDriveIcons.arnsName( + size: 16, + color: colorTokens.textHigh, + ), + const SizedBox(width: 8), + Flexible( + child: Text( + getLiteralArNSName(widget.manifestSelection.antRecord!, + widget.manifestSelection.undername), + style: typography.paragraphNormal( + fontWeight: ArFontWeight.semiBold, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ], + if (isExpanded) ...[ + const SizedBox(height: 8), + Expanded( + flex: 1, + child: AntSelector( + manifestSelection: widget.manifestSelection, + ), + ), + ], + ], + ), + ), + ); + } + return const SizedBox(); + } +} + +class AntSelector extends StatefulWidget { + final ManifestSelection manifestSelection; + + const AntSelector({super.key, required this.manifestSelection}); + + @override + State createState() => _AntSelectorState(); +} + +class _AntSelectorState extends State { + ANTRecord? _selectedAnt; + ARNSUndername? _selectedUndername; + + List _arnsUndernames = []; + bool _loadingUndernames = false; + + Future loadARNSUndernames( + ANTRecord ant, + ) async { + setState(() { + _loadingUndernames = true; + }); + + _arnsUndernames = + await context.read().getARNSUndernames(ant); + _arnsUndernames.removeWhere((e) => e.name == '@'); + + setState(() { + _loadingUndernames = false; + }); + } + + @override + void initState() { + super.initState(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + + _selectedAnt = widget.manifestSelection.antRecord; + _selectedUndername = widget.manifestSelection.undername; + } + + @override + Widget build(BuildContext context) { + final colorTokens = ArDriveTheme.of(context).themeData.colorTokens; + final typography = ArDriveTypographyNew.of(context); + + return BlocBuilder( + builder: (context, state) { + if (state is UploadManifestOptionsReady) { + final reservedNames = + context.read().reservedNames; + + bool isNameAlreadyInUse = reservedNames[_selectedAnt?.domain] + ?.contains(_selectedUndername?.name ?? '@') ?? + false; + + return Column( + children: [ + Container( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + color: colorTokens.inputDisabled, + border: Border.all( + color: colorTokens.textXLow, + width: 1, + ), + ), + child: ArDriveDropdown( + height: 45, + maxHeight: (state.ants!.length > 6) ? 45 * 6 : null, + items: state.ants! + .map((ant) => _buildDropdownItem(context, ant)) + .toList(), + child: ArDriveClickArea( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Flexible(child: _buildSelectedItem(context)), + ArDriveIcons.chevronDown(), + ], + ), + ), + ), + ), + if (_loadingUndernames) const CircularProgressIndicator(), + if (_selectedUndername != null || + (!_loadingUndernames && _arnsUndernames.isNotEmpty)) + Padding( + padding: const EdgeInsets.only(top: 8), + child: Container( + alignment: Alignment.centerLeft, + padding: + const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(5), + color: colorTokens.inputDisabled, + border: Border.all( + color: colorTokens.textXLow, + width: 1, + ), + ), + child: ArDriveDropdown( + maxHeight: (_arnsUndernames.length > 6) ? 45 * 6 : null, + height: 45, + items: _arnsUndernames + .map((undername) => + _buildDropdownItemUndername(context, undername)) + .toList(), + child: ArDriveClickArea( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Row( + children: [ + _buildSelectedItemUndername(context), + ], + ), + ArDriveIcons.chevronDown(), + ], + ), + ), + ), + ), + ), + const Spacer(), + Padding( + padding: const EdgeInsets.only( + top: 8, + left: 8, + bottom: 4, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (isNameAlreadyInUse && + widget.manifestSelection.antRecord?.domain != + _selectedAnt?.domain) + Expanded( + child: Text( + 'Name already in use, please choose another name or select a undername', + style: typography.paragraphSmall( + fontWeight: ArFontWeight.semiBold, + ), + ), + ), + Align( + alignment: Alignment.centerRight, + child: ArDriveButtonNew( + text: 'Add', + typography: typography, + fontStyle: typography.paragraphSmall(), + variant: ButtonVariant.primary, + maxWidth: 80, + maxHeight: 30, + isDisabled: isNameAlreadyInUse || _selectedAnt == null, + onPressed: () { + context + .read() + .add(LinkManifestToUndername( + manifest: widget.manifestSelection.manifest, + antRecord: _selectedAnt!, + undername: _selectedUndername, + )); + }, + ), + ), + ], + ), + ), + ], + ); + } + return const SizedBox(); + }, + ); + } + + Widget _buildSelectedItem(BuildContext context) { + final typography = ArDriveTypographyNew.of(context); + + return Text( + _selectedAnt?.domain ?? 'Choose ArNS name', + style: typography.paragraphSmall( + fontWeight: ArFontWeight.semiBold, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ); + } + + Widget _buildSelectedItemUndername(BuildContext context) { + final typography = ArDriveTypographyNew.of(context); + + return Text( + _selectedUndername?.name ?? 'under_name (optional)', + style: typography.paragraphSmall( + fontWeight: ArFontWeight.semiBold, + ), + ); + } + + ArDriveDropdownItem _buildDropdownItem(BuildContext context, ANTRecord ant) { + final typography = ArDriveTypographyNew.of(context); + + return ArDriveDropdownItem( + content: SizedBox( + width: 235, + height: 45, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text( + ant.domain, + style: typography.paragraphSmall( + fontWeight: ArFontWeight.semiBold, + ), + ), + ), + if (ant.domain == _selectedAnt?.domain) + ArDriveIcons.checkmark( + size: 16, + ) + ], + ), + ), + ), + onClick: () { + setState(() { + _selectedAnt = ant; + + _arnsUndernames = []; + _selectedUndername = null; + loadARNSUndernames(ant); + }); + }, + ); + } + + ArDriveDropdownItem _buildDropdownItemUndername( + BuildContext context, ARNSUndername undername) { + final typography = ArDriveTypographyNew.of(context); + + return ArDriveDropdownItem( + content: SizedBox( + width: 235, + height: 45, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text( + undername.name, + style: typography.paragraphSmall( + fontWeight: ArFontWeight.semiBold, + ), + ), + ), + if (undername.name == _selectedUndername?.name) + ArDriveIcons.checkmark( + size: 16, + ) + ], + ), + ), + ), + onClick: () { + setState(() { + _selectedUndername = undername; + }); + }, + ); + } +} diff --git a/lib/dev_tools/shortcut_handler.dart b/lib/dev_tools/shortcut_handler.dart index 1d4f648caa..1483847896 100644 --- a/lib/dev_tools/shortcut_handler.dart +++ b/lib/dev_tools/shortcut_handler.dart @@ -2,11 +2,11 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; class Shortcut { - final LogicalKeyboardKey modifier; + final LogicalKeyboardKey? modifier; final LogicalKeyboardKey key; final VoidCallback action; - Shortcut({required this.modifier, required this.key, required this.action}); + Shortcut({this.modifier, required this.key, required this.action}); @override bool operator ==(Object other) { @@ -42,10 +42,16 @@ class ShortcutHandlerState extends State { autofocus: true, onKeyEvent: (KeyEvent event) { for (var shortcut in widget.shortcuts) { - if (HardwareKeyboard.instance - .isLogicalKeyPressed(shortcut.modifier) && - HardwareKeyboard.instance.isLogicalKeyPressed(shortcut.key)) { - shortcut.action(); + if (shortcut.modifier == null) { + if (HardwareKeyboard.instance.isLogicalKeyPressed(shortcut.key)) { + shortcut.action(); + } + } else { + if (HardwareKeyboard.instance + .isLogicalKeyPressed(shortcut.modifier!) && + HardwareKeyboard.instance.isLogicalKeyPressed(shortcut.key)) { + shortcut.action(); + } } } }, diff --git a/lib/entities/drive_entity.dart b/lib/entities/drive_entity.dart index 4d227fba0f..42c8586948 100644 --- a/lib/entities/drive_entity.dart +++ b/lib/entities/drive_entity.dart @@ -24,6 +24,8 @@ class DriveEntity extends EntityWithCustomMetadata { String? name; String? rootFolderId; + bool? isHidden; + @override @JsonKey(includeFromJson: false, includeToJson: false) List reservedGqlTags = [ @@ -45,6 +47,7 @@ class DriveEntity extends EntityWithCustomMetadata { this.rootFolderId, this.privacy, this.authMode, + this.isHidden, }) : super(ArDriveCrypto()); static Future fromTransaction( diff --git a/lib/main.dart b/lib/main.dart index 8dcdcdc891..0fe72a818a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,6 +5,7 @@ import 'package:ardrive/arns/domain/arns_repository.dart'; 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/global_hide_bloc.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'; @@ -262,6 +263,13 @@ class AppState extends State { List get blocProviders => [ ChangeNotifierProvider( create: (_) => ActivityTracker()), + BlocProvider( + create: (context) => GlobalHideBloc( + userPreferencesRepository: + context.read(), + driveDao: context.read(), + ), + ), BlocProvider( create: (context) => ThemeSwitcherBloc( userPreferencesRepository: @@ -417,6 +425,7 @@ class AppState extends State { RepositoryProvider( create: (_) => UserPreferencesRepository( themeDetector: ThemeDetector(), + auth: _.read(), ), ), RepositoryProvider( @@ -455,6 +464,7 @@ class AppState extends State { configService: configService, ), arnsRepository: _.read(), + userPreferencesRepository: _.read(), ), ), diff --git a/lib/models/daos/drive_dao/drive_dao.dart b/lib/models/daos/drive_dao/drive_dao.dart index 8c3a5e0b14..c27dc8a1db 100644 --- a/lib/models/daos/drive_dao/drive_dao.dart +++ b/lib/models/daos/drive_dao/drive_dao.dart @@ -308,6 +308,7 @@ class DriveDao extends DatabaseAccessor with _$DriveDaoMixin { privacy: entity.privacy!, dateCreated: Value(entity.createdAt), lastUpdated: Value(entity.createdAt), + isHidden: Value(entity.isHidden ?? false), ); if (entity.privacy == DrivePrivacyTag.private) { @@ -736,6 +737,10 @@ class DriveDao extends DatabaseAccessor with _$DriveDaoMixin { Future numberOfFolders() { return (select(folderEntries).table.count()).getSingle(); } + + Future userHasHiddenItems() { + return hasHiddenItems().getSingle(); + } } class FolderNotFoundInDriveException implements Exception { diff --git a/lib/models/database/database.dart b/lib/models/database/database.dart index 3e152d366a..fbd711328b 100644 --- a/lib/models/database/database.dart +++ b/lib/models/database/database.dart @@ -30,7 +30,7 @@ class Database extends _$Database { Database([QueryExecutor? e]) : super(e ?? openConnection()); @override - int get schemaVersion => 22; + int get schemaVersion => 23; @override MigrationStrategy get migration => MigrationStrategy( onCreate: (Migrator m) { @@ -142,6 +142,12 @@ class Database extends _$Database { logger.d('snapshot_entries table dropped'); } + if (from < 23) { + logger.d('Migrating schema from v22 to v23'); + + await m.addColumn(drives, drives.isHidden); + await m.addColumn(driveRevisions, driveRevisions.isHidden); + } } catch (e, stacktrace) { logger.e( 'CRITICAL! Failed to migrate database from $from to $to', diff --git a/lib/models/drive.dart b/lib/models/drive.dart index 2d70a1b26e..b41369ac79 100644 --- a/lib/models/drive.dart +++ b/lib/models/drive.dart @@ -14,6 +14,7 @@ extension DriveExtensions on Drive { name: name, rootFolderId: rootFolderId, privacy: privacy, + isHidden: isHidden, authMode: privacy == DrivePrivacyTag.private ? DriveAuthModeTag.password : DriveAuthModeTag.none, diff --git a/lib/models/drive_revision.dart b/lib/models/drive_revision.dart index 6739013840..83f63351b8 100644 --- a/lib/models/drive_revision.dart +++ b/lib/models/drive_revision.dart @@ -21,6 +21,7 @@ extension DriveRevisionCompanionExtensions on DriveRevisionsCompanion { privacy: privacy.value, customGQLTags: customGQLTags, customJsonMetadata: customJsonMetadata, + isHidden: isHidden, ); /// Returns a [NetworkTransactionsCompanion] representing the metadata transaction @@ -48,6 +49,7 @@ extension DriveEntityExtensions on DriveEntity { bundledIn: Value(bundledIn), customGQLTags: Value(customGqlTagsAsString), customJsonMetadata: Value(customJsonMetadataAsString), + isHidden: Value(isHidden ?? false), ); /// Returns the action performed on the Drive that lead to the new revision. @@ -57,6 +59,9 @@ extension DriveEntityExtensions on DriveEntity { return RevisionAction.create; } else if (name != previousRevision.name.value) { return RevisionAction.rename; + } else if (isHidden != null && + previousRevision.isHidden.value != isHidden) { + return isHidden! ? RevisionAction.hide : RevisionAction.unhide; } return null; diff --git a/lib/models/queries/drive_queries.drift b/lib/models/queries/drive_queries.drift index 76c8f22a37..3f35b2f603 100644 --- a/lib/models/queries/drive_queries.drift +++ b/lib/models/queries/drive_queries.drift @@ -41,6 +41,19 @@ ghostFolders: SELECT * FROM folder_entries WHERE isGhost = TRUE; +hasHiddenItems: + SELECT EXISTS( + SELECT 1 FROM drives + WHERE isHidden = TRUE + UNION ALL + SELECT 1 FROM folder_entries + WHERE isHidden = TRUE + UNION ALL + SELECT 1 FROM file_entries + WHERE isHidden = TRUE + ) AS hasHidden; + + foldersInFolderWithName: SELECT * FROM folder_entries WHERE driveId = :driveId AND parentFolderId = :parentFolderId AND name = :name; diff --git a/lib/models/tables/drive_revisions.drift b/lib/models/tables/drive_revisions.drift index 6f8866f550..7f233ec26f 100644 --- a/lib/models/tables/drive_revisions.drift +++ b/lib/models/tables/drive_revisions.drift @@ -20,6 +20,8 @@ CREATE TABLE drive_revisions ( customJsonMetadata TEXT, customGQLTags TEXT, + isHidden BOOLEAN NOT NULL DEFAULT FALSE, + PRIMARY KEY (driveId, dateCreated), FOREIGN KEY (metadataTxId) REFERENCES network_transactions(id) ); diff --git a/lib/models/tables/drives.drift b/lib/models/tables/drives.drift index d0a5718d5d..3fbbc83100 100644 --- a/lib/models/tables/drives.drift +++ b/lib/models/tables/drives.drift @@ -19,6 +19,8 @@ CREATE TABLE drives ( customJsonMetadata TEXT, customGQLTags TEXT, + isHidden BOOLEAN NOT NULL DEFAULT FALSE, + dateCreated DATETIME NOT NULL DEFAULT (strftime('%s','now')), lastUpdated DATETIME NOT NULL DEFAULT (strftime('%s','now')) ) As Drive; diff --git a/lib/pages/app_router_delegate.dart b/lib/pages/app_router_delegate.dart index 3906fe1e23..bd9016cb58 100644 --- a/lib/pages/app_router_delegate.dart +++ b/lib/pages/app_router_delegate.dart @@ -22,6 +22,7 @@ import 'package:ardrive/sync/domain/cubit/sync_cubit.dart'; import 'package:ardrive/sync/domain/repositories/sync_repository.dart'; import 'package:ardrive/theme/theme_switcher_bloc.dart'; import 'package:ardrive/theme/theme_switcher_state.dart'; +import 'package:ardrive/user/repositories/user_preferences_repository.dart'; import 'package:ardrive/utils/app_localizations_wrapper.dart'; import 'package:ardrive/utils/logger.dart'; import 'package:ardrive_ui/ardrive_ui.dart'; @@ -134,16 +135,6 @@ class AppRouterDelegate extends RouterDelegate gettingStarted = false; notifyListeners(); } - - // Cleans up any shared drives from previous sessions - // TODO: Find a better place to do this - final lastLoggedInUser = - state is ProfileLoggedIn ? state.user.walletAddress : null; - if (lastLoggedInUser != null) { - context - .read() - .deleteSharedPrivateDrives(lastLoggedInUser); - } }, builder: (context, state) { Widget? shell; @@ -167,7 +158,9 @@ class AppRouterDelegate extends RouterDelegate shell = const LoginPage(gettingStarted: true); } else if (state is ProfileLoggedIn || anonymouslyShowDriveDetail) { - shell = BlocConsumer( + driveId = driveId ?? rootPath; + + shell = BlocListener( listener: (context, state) { if (state is DrivesLoadSuccess) { final selectedDriveChanged = @@ -180,110 +173,96 @@ class AppRouterDelegate extends RouterDelegate notifyListeners(); } }, - builder: (context, state) { - Widget? shellPage; - if (state is DrivesLoadSuccess) { - shellPage = !state.hasNoDrives - ? DriveDetailPage( - context: navigatorKey.currentContext!, - anonymouslyShowDriveDetail: - anonymouslyShowDriveDetail, - ) - : NoDrivesPage( - anonymouslyShowDriveDetail: - anonymouslyShowDriveDetail, - ); - - driveId = state.selectedDriveId; - } - - shellPage ??= const SizedBox(); - driveId = driveId ?? rootPath; - - return BlocProvider( - key: ValueKey(driveId), - create: (context) => DriveDetailCubit( - driveRepository: DriveRepository( - driveDao: context.read(), - auth: context.read(), - ), - activityTracker: context.read(), - driveId: driveId!, - initialFolderId: driveFolderId, - profileCubit: context.read(), + child: BlocProvider( + create: (context) => DriveDetailCubit( + driveRepository: DriveRepository( driveDao: context.read(), - configService: context.read(), auth: context.read(), - breadcrumbBuilder: BreadcrumbBuilder( - context.read(), - ), - syncCubit: context.read(), ), - child: MultiBlocListener( - listeners: [ - BlocListener( - 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 (driveDetailCubitState - is DriveDetailLoadNotFound) { - // Do not prompt the user to attach an unfound drive if they are logging out. - final profileCubit = - context.read(); - - if (profileCubit.state is ProfileLoggingOut) { - logger.d( - 'Drive not found, but user is logging out. Not prompting to attach drive.'); - - clearState(); - - return; - } - - attachDrive( - context: context, - driveId: driveId, - driveName: driveName, - driveKey: sharedDriveKey, - ); - } - }, - ), - BlocListener( - listener: (context, state) { - if (state is FeedbackSurveyRemindMe && - state.isOpen) { - openFeedbackSurveyModal(context); - } else if (state is FeedbackSurveyRemindMe && - !state.isOpen) { - Navigator.pop(context); - } else if (state is FeedbackSurveyDontRemindMe && - !state.isOpen) { - Navigator.pop(context); - } - }, - ), - BlocListener( - listener: ((context, state) { - if (state is ProfileLoggingOut) { - context.read().reset(); + activityTracker: context.read(), + driveId: driveId!, + initialFolderId: driveFolderId, + profileCubit: context.read(), + driveDao: context.read(), + configService: context.read(), + auth: context.read(), + breadcrumbBuilder: BreadcrumbBuilder( + context.read(), + ), + syncCubit: context.read(), + ), + child: MultiBlocListener( + listeners: [ + BlocListener( + 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 (driveDetailCubitState + is DriveDetailLoadNotFound) { + // Do not prompt the user to attach an unfound drive if they are logging out. + final profileCubit = context.read(); + + if (profileCubit.state is ProfileLoggingOut) { + logger.d( + 'Drive not found, but user is logging out. Not prompting to attach drive.'); + + clearState(); + + return; } - }), - ), - ], - child: AppShell( - page: shellPage, + + attachDrive( + context: context, + driveId: driveId, + driveName: driveName, + driveKey: sharedDriveKey, + ).then((_) { + sharedDriveKey = null; + sharedRawDriveKey = null; + driveId = null; + driveName = null; + notifyListeners(); + }); + } + }, + ), + BlocListener( + listener: (context, state) { + if (state is FeedbackSurveyRemindMe && + state.isOpen) { + openFeedbackSurveyModal(context); + } else if (state is FeedbackSurveyRemindMe && + !state.isOpen) { + Navigator.pop(context); + } else if (state is FeedbackSurveyDontRemindMe && + !state.isOpen) { + Navigator.pop(context); + } + }, + ), + BlocListener( + listener: ((context, state) { + if (state is ProfileLoggingOut) { + context.read().reset(); + } + }), + ), + ], + child: AppShell( + page: DriveDetailPage( + context: navigatorKey.currentContext!, + anonymouslyShowDriveDetail: + anonymouslyShowDriveDetail, ), ), - ); - }, + ), + ), ); } @@ -331,6 +310,8 @@ class AppRouterDelegate extends RouterDelegate driveDao: context.read(), promptToSnapshotBloc: context.read(), + userPreferencesRepository: + 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 5341111bce..d7dc392cbc 100644 --- a/lib/pages/drive_detail/components/drive_detail_data_list.dart +++ b/lib/pages/drive_detail/components/drive_detail_data_list.dart @@ -9,10 +9,10 @@ Widget _buildDataList( context, state.currentFolderContents, state.folderInView.folder, + state.selectedItem, state.currentDrive, isMultiselecting: state.multiselect, columnVisibility: state.columnVisibility, - isShowingHiddenFiles: state.isShowingHiddenFiles, emptyState: emptyState, selectedPage: state.selectedPage, ); @@ -22,25 +22,13 @@ Widget _buildDataListContent( BuildContext context, List items, FolderEntry folder, + ArDriveDataTableItem? selectedItem, Drive drive, { required bool isMultiselecting, required Map columnVisibility, - required bool isShowingHiddenFiles, required Widget emptyState, int? selectedPage, }) { - 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 typography = ArDriveTypographyNew.of(context); final colorTokens = ArDriveTheme.of(context).themeData.colorTokens; @@ -113,123 +101,141 @@ Widget _buildDataListContent( final forceRebuildKey = driveDetailCubitState is DriveDetailLoadSuccess ? driveDetailCubitState.forceRebuildKey : null; - return ArDriveDataTable( - key: ValueKey( - '${folder.id}-${forceRebuildKey.toString()}${columns.length}'), - initialPage: selectedPage, - lockMultiSelect: context.watch().state is SyncInProgress || - !context.watch().isMultiSelectEnabled, - rowsPerPageText: appLocalizationsOf(context).rowsPerPage, - maxItemsPerPage: 100, - pageItemsDivisorFactor: 25, - onSelectedRows: (boxes) { - final bloc = context.read(); + return BlocBuilder( + builder: (context, hideState) { + List filteredItems = []; - if (boxes.isEmpty) { - bloc.setMultiSelect(false); - return; + if (hideState is HiddingItems) { + filteredItems = items.where((item) => !item.isHidden).toList(); + } else { + filteredItems = items.toList(); } - final multiSelectedItems = boxes - .map((e) => e.selectedItems.map((e) => e)) - .expand((e) => e) - .toList(); - - bloc.selectItems(multiSelectedItems); - }, - onChangeMultiSelecting: (isMultiselecting) { - context.read().setMultiSelect(isMultiselecting); - }, - onChangeColumnVisibility: (column) { - context.read().updateTableColumnVisibility(column); - }, - forceDisableMultiSelect: - context.read().forceDisableMultiselect, - columns: columns, - trailing: (file) => isMultiselecting - ? const SizedBox.shrink() - : DriveExplorerItemTileTrailing( - drive: drive, - item: file, - ), - leading: (file) => DriveExplorerItemTileLeading( - item: file, - ), - onRowTap: (item) { - final cubit = context.read(); - if (item is FolderDataTableItem) { - if (item.id == cubit.selectedItem?.id) { - cubit.openFolder(folderId: item.id); - } else { - cubit.selectDataItem(item); - } - } else if (item is FileDataTableItem) { - if (item.id == cubit.selectedItem?.id) { - cubit.toggleSelectedItemDetails(); - return; - } - - cubit.selectDataItem(item); + if (filteredItems.isEmpty) { + return emptyState; } - }, - sortRows: (list, columnIndex, ascDescSort) { - // Separate folders and files - List folders = []; - List files = []; - final lenght = list.length; + return ArDriveDataTable( + key: ValueKey( + '${folder.id}-${forceRebuildKey.toString()}${columns.length}-${hideState.toString()}'), + initialPage: selectedPage, + lockMultiSelect: context.watch().state is SyncInProgress || + !context.watch().isMultiSelectEnabled, + rowsPerPageText: appLocalizationsOf(context).rowsPerPage, + maxItemsPerPage: 100, + pageItemsDivisorFactor: 25, + onSelectedRows: (boxes) { + final bloc = context.read(); - for (int i = 0; i < lenght; i++) { - if (list[i] is FolderDataTableItem) { - folders.add(list[i]); - } else { - files.add(list[i]); - } - } + if (boxes.isEmpty) { + bloc.setMultiSelect(false); + return; + } - // Sort folders and files - _sortFoldersAndFiles(folders, files, columnIndex, ascDescSort); + final multiSelectedItems = boxes + .map((e) => e.selectedItems.map((e) => e)) + .expand((e) => e) + .toList(); - return folders + files; - }, - buildRow: (row) { - final typography = ArDriveTypographyNew.of(context); - final colorTokens = ArDriveTheme.of(context).themeData.colorTokens; - return DriveExplorerItemTile( - colorTokens: colorTokens, - name: row.name, - typography: typography, - size: row.size == null ? '-' : filesize(row.size), - lastUpdated: row.lastUpdated, - dateCreated: row.dateCreated, - dataTableItem: row, - license: row.licenseType == null - ? '' - : context - .read() - .licenseMetaByType(row.licenseType!) - .shortName, - isHidden: row.isHidden, - onPressed: () { + bloc.selectItems(multiSelectedItems); + }, + onChangeMultiSelecting: (isMultiselecting) { + context.read().setMultiSelect(isMultiselecting); + }, + onChangeColumnVisibility: (column) { + context + .read() + .updateTableColumnVisibility(column); + }, + forceDisableMultiSelect: + context.read().forceDisableMultiselect, + columns: columns, + trailing: (file) => isMultiselecting + ? const SizedBox.shrink() + : DriveExplorerItemTileTrailing( + drive: drive, + item: file, + ), + leading: (file) => DriveExplorerItemTileLeading( + item: file, + ), + onRowTap: (item) { final cubit = context.read(); - if (row is FolderDataTableItem) { - if (row.id == cubit.selectedItem?.id) { - cubit.openFolder(folderId: row.id); + if (item is FolderDataTableItem) { + if (item.id == cubit.selectedItem?.id) { + cubit.openFolder(folderId: item.id); } else { - cubit.selectDataItem(row); + cubit.selectDataItem(item); } - } else if (row is FileDataTableItem) { - if (row.id == cubit.selectedItem?.id) { + } else if (item is FileDataTableItem) { + if (item.id == cubit.selectedItem?.id) { cubit.toggleSelectedItemDetails(); + return; + } + + cubit.selectDataItem(item); + } + }, + sortRows: (list, columnIndex, ascDescSort) { + // Separate folders and files + List folders = []; + List files = []; + + final lenght = list.length; + + for (int i = 0; i < lenght; i++) { + if (list[i] is FolderDataTableItem) { + folders.add(list[i]); } else { - cubit.selectDataItem(row); + files.add(list[i]); } } + + // Sort folders and files + _sortFoldersAndFiles(folders, files, columnIndex, ascDescSort); + + return folders + files; + }, + buildRow: (row) { + final typography = ArDriveTypographyNew.of(context); + final colorTokens = ArDriveTheme.of(context).themeData.colorTokens; + return DriveExplorerItemTile( + colorTokens: colorTokens, + name: row.name, + typography: typography, + size: row.size == null ? '-' : filesize(row.size), + lastUpdated: row.lastUpdated, + dateCreated: row.dateCreated, + dataTableItem: row, + license: row.licenseType == null + ? '' + : context + .read() + .licenseMetaByType(row.licenseType!) + .shortName, + isHidden: row.isHidden, + onPressed: () { + final cubit = context.read(); + if (row is FolderDataTableItem) { + if (row.id == cubit.selectedItem?.id) { + cubit.openFolder(folderId: row.id); + } else { + cubit.selectDataItem(row); + } + } else if (row is FileDataTableItem) { + if (row.id == cubit.selectedItem?.id) { + cubit.toggleSelectedItemDetails(); + } else { + cubit.selectDataItem(row); + } + } + }, + ); }, + rows: filteredItems, + selectedRow: selectedItem, ); }, - rows: filteredItems, - selectedRow: context.watch().selectedItem, ); }); } 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 dc8010551a..fd5306abc2 100644 --- a/lib/pages/drive_detail/components/drive_explorer_item_tile.dart +++ b/lib/pages/drive_detail/components/drive_explorer_item_tile.dart @@ -477,7 +477,7 @@ class _DriveExplorerItemTileTrailingState ), ), ), - hideFileDropdownItem(context, item), + if (isOwner) hideFileDropdownItem(context, item), ], ArDriveDropdownItem( onClick: () { @@ -533,7 +533,7 @@ class _DriveExplorerItemTileTrailingState }, content: _buildItem( appLocalizationsOf(context).preview, - ArDriveIcons.eyeOpen( + ArDriveIcons.newWindow( size: defaultIconSize, ), ), @@ -756,6 +756,23 @@ class EntityActionsMenu extends StatelessWidget { ), ), ), + if (isOwner) + 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), + ), + ), ArDriveDropdownItem( onClick: () { promptToShareDrive( diff --git a/lib/pages/drive_detail/components/hover_widget.dart b/lib/pages/drive_detail/components/hover_widget.dart index a3c19ca578..78c4b706c0 100644 --- a/lib/pages/drive_detail/components/hover_widget.dart +++ b/lib/pages/drive_detail/components/hover_widget.dart @@ -13,7 +13,7 @@ class HoverWidget extends StatefulWidget { const HoverWidget({ super.key, required this.child, - this.hoverScale = 1.1, + this.hoverScale = 1.0, this.hoverColor, this.tooltip, this.padding, diff --git a/lib/pages/drive_detail/drive_detail_page.dart b/lib/pages/drive_detail/drive_detail_page.dart index 1441c48b36..fdb05db351 100644 --- a/lib/pages/drive_detail/drive_detail_page.dart +++ b/lib/pages/drive_detail/drive_detail_page.dart @@ -6,6 +6,7 @@ import 'package:ardrive/authentication/ardrive_auth.dart'; import 'package:ardrive/authentication/components/breakpoint_layout_builder.dart'; import 'package:ardrive/blocs/blocs.dart'; import 'package:ardrive/blocs/fs_entry_preview/fs_entry_preview_cubit.dart'; +import 'package:ardrive/blocs/hide/global_hide_bloc.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/prompt_to_snapshot/prompt_to_snapshot_state.dart'; @@ -18,6 +19,7 @@ import 'package:ardrive/components/details_panel.dart'; import 'package:ardrive/components/drive_detach_dialog.dart'; import 'package:ardrive/components/drive_rename_form.dart'; import 'package:ardrive/components/fs_entry_license_form.dart'; +import 'package:ardrive/components/hide_dialog.dart'; import 'package:ardrive/components/keyboard_handler.dart'; import 'package:ardrive/components/new_button/new_button.dart'; import 'package:ardrive/components/pin_file_dialog.dart'; @@ -37,6 +39,7 @@ import 'package:ardrive/pages/drive_detail/components/file_icon.dart'; import 'package:ardrive/pages/drive_detail/components/hover_widget.dart'; import 'package:ardrive/pages/drive_detail/components/unpreviewable_content.dart'; import 'package:ardrive/pages/drive_detail/models/data_table_item.dart'; +import 'package:ardrive/pages/no_drives/no_drives_page.dart'; import 'package:ardrive/search/search_modal.dart'; import 'package:ardrive/search/search_text_field.dart'; import 'package:ardrive/services/services.dart'; @@ -126,156 +129,197 @@ class _DriveDetailPageState extends State { return SharingFileListener( context: widget.context, child: SizedBox.expand( - child: BlocListener( + child: BlocListener( listener: (context, state) { - if (state is PromptToSnapshotPrompting) { - final bloc = context.read(); + if (state is DrivesLoadSuccess) { + if (state.userDrives.isNotEmpty) { + final driveDetailState = context.read().state; - final driveDetailState = context.read().state; - if (driveDetailState is DriveDetailLoadSuccess) { - final drive = driveDetailState.currentDrive; - promptToSnapshot( - context, - promptToSnapshotBloc: bloc, - drive: drive, - ).then((_) { - bloc.add(const SelectedDrive(driveId: null)); - }); + if (driveDetailState is DriveDetailLoadSuccess && + driveDetailState.currentDrive.id == state.selectedDriveId) { + return; + } + + context + .read() + .changeDrive(state.selectedDriveId!); + } else { + context.read().showEmptyDriveDetail(); } } }, - child: BlocBuilder( - buildWhen: (previous, current) { - return widget.context.read().state is! SyncInProgress; + child: BlocListener( + listener: (context, state) { + if (state is PromptToSnapshotPrompting) { + final bloc = context.read(); + + final driveDetailState = context.read().state; + if (driveDetailState is DriveDetailLoadSuccess) { + final drive = driveDetailState.currentDrive; + promptToSnapshot( + context, + promptToSnapshotBloc: bloc, + drive: drive, + ).then((_) { + bloc.add(const SelectedDrive(driveId: null)); + }); + } + } }, - builder: (context, driveDetailState) { - if (driveDetailState is DriveDetailLoadInProgress) { - return const Center(child: CircularProgressIndicator()); - } else if (driveDetailState is DriveInitialLoading) { - return ArDriveDevToolsShortcuts( - customShortcuts: [ - Shortcut( - modifier: LogicalKeyboardKey.shiftLeft, - key: LogicalKeyboardKey.keyH, - action: () { - ArDriveDevTools.instance - .showDevTools(optionalContext: context); - }, - ), - ], - child: ScreenTypeLayout.builder( - mobile: (context) { - return Scaffold( - drawerScrimColor: Colors.transparent, - drawer: const AppSideBar(), - appBar: const MobileAppBar(), - body: Padding( - padding: const EdgeInsets.all(8.0), - child: Center( - child: Text( - appLocalizationsOf(context) - .driveDoingInitialSetupMessage, - style: ArDriveTypography.body.buttonLargeBold(), - ), - ), - ), + child: BlocBuilder( + builder: (context, hideState) { + return BlocBuilder( + buildWhen: (previous, current) { + return widget.context.read().state + is! SyncInProgress; + }, + builder: (context, driveDetailState) { + if (driveDetailState is DriveDetailLoadEmpty) { + return NoDrivesPage( + anonymouslyShowDriveDetail: + widget.anonymouslyShowDriveDetail, ); - }, - desktop: (context) => Scaffold( - drawerScrimColor: Colors.transparent, - body: Column( - children: [ - const AppTopBar(), - Expanded( - child: Center( - child: Text( - appLocalizationsOf(context) - .driveDoingInitialSetupMessage, - style: ArDriveTypography.body.buttonLargeBold(), + } else if (driveDetailState is DriveDetailLoadInProgress) { + return const Center(child: CircularProgressIndicator()); + } else if (driveDetailState is DriveInitialLoading) { + return ArDriveDevToolsShortcuts( + customShortcuts: [ + Shortcut( + modifier: LogicalKeyboardKey.shiftLeft, + key: LogicalKeyboardKey.keyH, + action: () { + ArDriveDevTools.instance + .showDevTools(optionalContext: context); + }, + ), + ], + child: ScreenTypeLayout.builder( + mobile: (context) { + return Scaffold( + drawerScrimColor: Colors.transparent, + drawer: const AppSideBar(), + appBar: const MobileAppBar(), + body: Padding( + padding: const EdgeInsets.all(8.0), + child: Center( + child: Text( + appLocalizationsOf(context) + .driveDoingInitialSetupMessage, + style: ArDriveTypography.body + .buttonLargeBold(), + ), + ), ), + ); + }, + desktop: (context) => Scaffold( + drawerScrimColor: Colors.transparent, + body: Column( + children: [ + const AppTopBar(), + Expanded( + child: Center( + child: Text( + appLocalizationsOf(context) + .driveDoingInitialSetupMessage, + style: ArDriveTypography.body + .buttonLargeBold(), + ), + ), + ), + ], ), ), - ], - ), - ), - ), - ); - } else if (driveDetailState is DriveDetailLoadSuccess) { - final isShowingHiddenFiles = - driveDetailState.isShowingHiddenFiles; - final bool hasSubfolders; - final bool hasFiles; + ), + ); + } else if (driveDetailState is DriveDetailLoadSuccess) { + final isShowingHiddenFiles = + hideState is ShowingHiddenItems; + 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; - } + 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 isOwner = isDriveOwner( + context.read(), + driveDetailState.currentDrive.ownerAddress, + ); - final canDownloadMultipleFiles = driveDetailState.multiselect && - context.read().selectedItems.isNotEmpty; + final canDownloadMultipleFiles = + driveDetailState.multiselect && + context + .read() + .selectedItems + .isNotEmpty; - return ArDriveDevToolsShortcuts( - customShortcuts: [ - Shortcut( - modifier: LogicalKeyboardKey.shiftLeft, - key: LogicalKeyboardKey.keyH, - action: () { - ArDriveDevTools.instance - .showDevTools(optionalContext: context); - }, - ), - ], - child: ScreenTypeLayout.builder( - desktop: (context) => _desktopView( - isDriveOwner: isOwner, - driveDetailState: driveDetailState, - hasSubfolders: hasSubfolders, - hasFiles: hasFiles, - canDownloadMultipleFiles: canDownloadMultipleFiles, - ), - mobile: (context) => Scaffold( - resizeToAvoidBottomInset: false, - drawerScrimColor: Colors.transparent, - drawer: const AppSideBar(), - appBar: (driveDetailState.showSelectedItemDetails && - context.read().selectedItem != - null) - ? MobileAppBar( - leading: ArDriveIconButton( - icon: ArDriveIcons.arrowLeft(), - onPressed: () { - context - .read() - .toggleSelectedItemDetails(); - }, - ), - ) - : null, - body: _mobileView( - driveDetailState, - hasSubfolders, - hasFiles, - ), - ), - ), + return ArDriveDevToolsShortcuts( + customShortcuts: [ + Shortcut( + modifier: LogicalKeyboardKey.shiftLeft, + key: LogicalKeyboardKey.keyH, + action: () { + ArDriveDevTools.instance + .showDevTools(optionalContext: context); + }, + ), + ], + child: ScreenTypeLayout.builder( + desktop: (context) => _desktopView( + isDriveOwner: isOwner, + driveDetailState: driveDetailState, + hasSubfolders: hasSubfolders, + hasFiles: hasFiles, + canDownloadMultipleFiles: canDownloadMultipleFiles, + hideState: hideState, + ), + mobile: (context) => Scaffold( + resizeToAvoidBottomInset: false, + drawerScrimColor: Colors.transparent, + drawer: const AppSideBar(), + appBar: (driveDetailState.showSelectedItemDetails && + context + .read() + .selectedItem != + null) + ? MobileAppBar( + leading: ArDriveIconButton( + icon: ArDriveIcons.arrowLeft(), + onPressed: () { + context + .read() + .toggleSelectedItemDetails(); + }, + ), + ) + : null, + body: _mobileView( + driveDetailState, + hasSubfolders, + hasFiles, + hideState, + ), + ), + ), + ); + } else { + return const SizedBox(); + } + }, ); - } else { - return const SizedBox(); - } - }, + }, + ), ), ), ), @@ -288,12 +332,8 @@ class _DriveDetailPageState extends State { required bool hasFiles, required bool isDriveOwner, required bool canDownloadMultipleFiles, + required GlobalHideState hideState, }) { - final driveDetailCubit = context.read(); - ArDriveTypographyNew.of(context); - - final isShowingHiddenFiles = driveDetailState.isShowingHiddenFiles; - return Column( children: [ const AppTopBar(), @@ -474,6 +514,37 @@ class _DriveDetailPageState extends State { ), ), ), + if (isDriveOwner) + ArDriveDropdownItem( + onClick: () { + promptToToggleHideState( + context, + item: DriveDataTableItemMapper + .fromDrive( + driveDetailState.currentDrive, + (_) => null, + 0, + isDriveOwner, + ), + ); + }, + content: ArDriveDropdownItemTile( + name: driveDetailState + .currentDrive.isHidden + ? appLocalizationsOf(context) + .unhide + : appLocalizationsOf(context) + .hide, + icon: driveDetailState + .currentDrive.isHidden + ? ArDriveIcons.eyeOpen( + size: defaultIconSize, + ) + : ArDriveIcons.eyeClosed( + size: defaultIconSize, + ), + ), + ), ArDriveDropdownItem( onClick: () { promptToShareDrive( @@ -529,26 +600,6 @@ 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 && @@ -657,16 +708,22 @@ class _DriveDetailPageState extends State { onNextImageNavigation: () { context .read() - .selectNextImage(); + .selectNextImage( + hideState is ShowingHiddenItems, + ); }, onPreviousImageNavigation: () { context .read() - .selectPreviousImage(); + .selectPreviousImage( + hideState is ShowingHiddenItems, + ); }, canNavigateThroughImages: context .read() - .canNavigateThroughImages(), + .canNavigateThroughImages( + hideState is ShowingHiddenItems, + ), ) : const SizedBox(), ), @@ -708,6 +765,7 @@ class _DriveDetailPageState extends State { DriveDetailLoadSuccess driveDetailLoadSuccessState, bool hasSubfolders, bool hasFiles, + GlobalHideState hideState, ) { final items = driveDetailLoadSuccessState.currentFolderContents; @@ -725,13 +783,18 @@ class _DriveDetailPageState extends State { drivePrivacy: driveDetailLoadSuccessState.currentDrive.privacy, item: driveDetailLoadSuccessState.selectedItem!, onNextImageNavigation: () { - context.read().selectNextImage(); + context + .read() + .selectNextImage(hideState is ShowingHiddenItems); }, onPreviousImageNavigation: () { - context.read().selectPreviousImage(); + context + .read() + .selectPreviousImage(hideState is ShowingHiddenItems); }, - canNavigateThroughImages: - context.read().canNavigateThroughImages(), + canNavigateThroughImages: context + .read() + .canNavigateThroughImages(hideState is ShowingHiddenItems), ), ), ); @@ -775,6 +838,7 @@ class _DriveDetailPageState extends State { hasSubfolders, hasFiles, items, + hideState, ), ); } @@ -784,8 +848,9 @@ class _DriveDetailPageState extends State { bool hasSubfolders, bool hasFiles, List items, + GlobalHideState globalHideState, ) { - final isShowingHiddenFiles = state.isShowingHiddenFiles; + final isShowingHiddenFiles = globalHideState is ShowingHiddenItems; final List filteredItems; @@ -828,6 +893,7 @@ class _DriveDetailPageState extends State { initialQuery: query, driveDetailCubit: context.read(), controller: controller, + drivesCubit: context.read(), ), ), ), @@ -1067,7 +1133,6 @@ class MobileFolderNavigation extends StatelessWidget { ), BlocBuilder( builder: (context, state) { - final driveDetailCubit = context.read(); if (state is DriveDetailLoadSuccess) { final isOwner = isDriveOwner(context.read(), state.currentDrive.ownerAddress); @@ -1135,6 +1200,32 @@ class MobileFolderNavigation extends StatelessWidget { ), ), ), + if (isOwner) + ArDriveDropdownItem( + onClick: () { + promptToToggleHideState( + context, + item: DriveDataTableItemMapper.fromDrive( + state.currentDrive, + (_) => null, + 0, + isOwner, + ), + ); + }, + content: ArDriveDropdownItemTile( + name: state.currentDrive.isHidden + ? appLocalizationsOf(context).unhide + : appLocalizationsOf(context).hide, + icon: state.currentDrive.isHidden + ? ArDriveIcons.eyeOpen( + size: defaultIconSize, + ) + : ArDriveIcons.eyeClosed( + size: defaultIconSize, + ), + ), + ), ArDriveDropdownItem( onClick: () { promptToExportCSVData( @@ -1169,23 +1260,6 @@ 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/drive_detail/models/data_table_item.dart b/lib/pages/drive_detail/models/data_table_item.dart index d1ca0351bd..2bf774e7a2 100644 --- a/lib/pages/drive_detail/models/data_table_item.dart +++ b/lib/pages/drive_detail/models/data_table_item.dart @@ -116,7 +116,12 @@ class FileDataTableItem extends ArDriveDataTableItem { : super(id: fileId); @override - List get props => [fileId, name, isHidden]; + List get props => [fileId]; + + @override + String toString() { + return 'FileDataTableItem(fileId: $fileId, name: $name, isHidden: $isHidden)'; + } } class DriveDataTableItemMapper { @@ -186,7 +191,7 @@ class DriveDataTableItemMapper { ); } - static FolderDataTableItem fromFolderEntry( +static FolderDataTableItem fromFolderEntry( FolderEntry folderEntry, int index, bool isOwner, @@ -222,7 +227,7 @@ class DriveDataTableItemMapper { dateCreated: drive.dateCreated, contentType: 'drive', id: drive.id, - isHidden: false, // TODO: update me when drives can be hidden + isHidden: drive.isHidden, ); } diff --git a/lib/search/search_modal.dart b/lib/search/search_modal.dart index 9f1ed0a37b..9ad2b1a229 100644 --- a/lib/search/search_modal.dart +++ b/lib/search/search_modal.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:ardrive/authentication/components/login_modal.dart'; import 'package:ardrive/blocs/drive_detail/drive_detail_cubit.dart'; import 'package:ardrive/blocs/drives/drives_cubit.dart'; @@ -24,6 +26,7 @@ import '../models/models.dart'; Future showSearchModalBottomSheet({ required BuildContext context, required DriveDetailCubit driveDetailCubit, + required DrivesCubit drivesCubit, required TextEditingController controller, String? query, }) { @@ -51,6 +54,7 @@ Future showSearchModalBottomSheet({ initialQuery: query, driveDetailCubit: context.read(), controller: controller, + drivesCubit: drivesCubit, ), ), ), @@ -61,6 +65,7 @@ Future showSearchModalBottomSheet({ Future showSearchModalDesktop({ required BuildContext context, required DriveDetailCubit driveDetailCubit, + required DrivesCubit drivesCubit, required TextEditingController controller, String? query, }) { @@ -74,6 +79,7 @@ Future showSearchModalDesktop({ initialQuery: query, driveDetailCubit: context.read(), controller: controller, + drivesCubit: drivesCubit, ), barrierColor: colorTokens.containerL1.withOpacity(0.8), ); @@ -83,11 +89,13 @@ class FileSearchModal extends StatelessWidget { const FileSearchModal({ super.key, required this.driveDetailCubit, + required this.drivesCubit, this.initialQuery, required this.controller, }); final DriveDetailCubit driveDetailCubit; + final DrivesCubit drivesCubit; final String? initialQuery; final TextEditingController controller; @@ -108,6 +116,7 @@ class FileSearchModal extends StatelessWidget { driveDetailCubit: driveDetailCubit, initialQuery: initialQuery, controller: controller, + drivesCubit: drivesCubit, ), ); } @@ -116,12 +125,14 @@ class FileSearchModal extends StatelessWidget { class _FileSearchModal extends StatefulWidget { const _FileSearchModal({ required this.driveDetailCubit, + required this.drivesCubit, this.initialQuery, required this.controller, }); final String? initialQuery; final DriveDetailCubit driveDetailCubit; + final DrivesCubit drivesCubit; final TextEditingController controller; @override @@ -443,21 +454,26 @@ class _FileSearchModalState extends State<_FileSearchModal> { result, ); - await Future.delayed(const Duration(milliseconds: 300)); + await Future.delayed(const Duration(milliseconds: 100)); + + widget.drivesCubit.selectDrive(file.driveId); + + await Future.delayed(const Duration(milliseconds: 100)); widget.driveDetailCubit.openFolder( otherDriveId: file.driveId, folderId: file.parentFolderId, + selectedItemId: file.id, ); - await Future.delayed(const Duration(milliseconds: 300)); + late StreamSubscription listener; - widget.driveDetailCubit.selectDataItem( - file, - openSelectedPage: true, - ); - - // ignore: use_build_context_synchronously - Navigator.of(context).pop(); + listener = widget.driveDetailCubit.stream.listen((state) { + if (state is DriveDetailLoadSuccess) { + listener.cancel(); + // ignore: use_build_context_synchronously + Navigator.of(context).pop(); + } + }); } } diff --git a/lib/sync/domain/repositories/sync_repository.dart b/lib/sync/domain/repositories/sync_repository.dart index f456551053..4874e3445e 100644 --- a/lib/sync/domain/repositories/sync_repository.dart +++ b/lib/sync/domain/repositories/sync_repository.dart @@ -28,6 +28,7 @@ import 'package:ardrive/sync/domain/models/drive_entity_history.dart'; import 'package:ardrive/sync/domain/sync_progress.dart'; import 'package:ardrive/sync/utils/batch_processor.dart'; import 'package:ardrive/sync/utils/network_transaction_utils.dart'; +import 'package:ardrive/user/repositories/user_preferences_repository.dart'; import 'package:ardrive/utils/logger.dart'; import 'package:ardrive/utils/snapshots/drive_history_composite.dart'; import 'package:ardrive/utils/snapshots/gql_drive_history.dart'; @@ -94,6 +95,7 @@ abstract class SyncRepository { required BatchProcessor batchProcessor, required SnapshotValidationService snapshotValidationService, required ARNSRepository arnsRepository, + required UserPreferencesRepository userPreferencesRepository, }) { return _SyncRepository( arweave: arweave, @@ -103,6 +105,7 @@ abstract class SyncRepository { batchProcessor: batchProcessor, snapshotValidationService: snapshotValidationService, arnsRepository: arnsRepository, + userPreferencesRepository: userPreferencesRepository, ); } } @@ -115,6 +118,7 @@ class _SyncRepository implements SyncRepository { final BatchProcessor _batchProcessor; final SnapshotValidationService _snapshotValidationService; final ARNSRepository _arnsRepository; + final UserPreferencesRepository _userPreferencesRepository; final Map _ghostFolders = {}; final Set _folderIds = {}; @@ -129,12 +133,14 @@ class _SyncRepository implements SyncRepository { required BatchProcessor batchProcessor, required SnapshotValidationService snapshotValidationService, required ARNSRepository arnsRepository, + required UserPreferencesRepository userPreferencesRepository, }) : _arweave = arweave, _driveDao = driveDao, _configService = configService, _licenseService = licenseService, _snapshotValidationService = snapshotValidationService, _batchProcessor = batchProcessor, + _userPreferencesRepository = userPreferencesRepository, _arnsRepository = arnsRepository; @override @@ -264,7 +270,9 @@ class _SyncRepository implements SyncRepository { _arnsRepository .waitForARNSRecordsToUpdate() .then((value) => _arnsRepository.saveAllFilesWithAssignedNames()); - + final hasHiddenItems = await _driveDao.hasHiddenItems().getSingle(); + await _userPreferencesRepository.saveUserHasHiddenItem(hasHiddenItems); + await _userPreferencesRepository.load(); await Future.wait( [ _updateTransactionStatuses( @@ -864,6 +872,7 @@ class _SyncRepository implements SyncRepository { id: Value(drive.id), lastBlockHeight: Value(currentBlockHeight), syncCursor: const Value(null), + isHidden: Value(drive.isHidden), )); } @@ -958,9 +967,11 @@ class _SyncRepository implements SyncRepository { final revisionPerformedAction = entity.getPerformedRevisionAction(latestRevision); + if (revisionPerformedAction == null) { continue; } + final revision = entity.toRevisionCompanion(performedAction: revisionPerformedAction); diff --git a/lib/user/repositories/user_preferences_repository.dart b/lib/user/repositories/user_preferences_repository.dart index 3d99adc0bd..00a89e8429 100644 --- a/lib/user/repositories/user_preferences_repository.dart +++ b/lib/user/repositories/user_preferences_repository.dart @@ -1,3 +1,6 @@ +import 'dart:async'; + +import 'package:ardrive/authentication/ardrive_auth.dart'; import 'package:ardrive/theme/theme.dart'; import 'package:ardrive/user/user_preferences.dart'; import 'package:ardrive/utils/local_key_value_store.dart'; @@ -5,15 +8,22 @@ import 'package:ardrive_ui/ardrive_ui.dart'; abstract class UserPreferencesRepository { Future load(); + Stream watch(); Future saveTheme(ArDriveThemes theme); + Future saveLastSelectedDriveId(String driveId); + Future saveShowHiddenFiles(bool showHiddenFiles); + Future clear(); + Future saveUserHasHiddenItem(bool userHasHiddenDrive); factory UserPreferencesRepository({ LocalKeyValueStore? store, required ThemeDetector themeDetector, + required ArDriveAuth auth, }) { return _UserPreferencesRepository( store: store, themeDetector: themeDetector, + auth: auth, ); } } @@ -21,35 +31,90 @@ abstract class UserPreferencesRepository { class _UserPreferencesRepository implements UserPreferencesRepository { LocalKeyValueStore? _store; final ThemeDetector _themeDetector; + final ArDriveAuth _auth; _UserPreferencesRepository({ LocalKeyValueStore? store, required ThemeDetector themeDetector, + required ArDriveAuth auth, }) : _store = store, - _themeDetector = themeDetector; + _themeDetector = themeDetector, + _auth = auth, + super() { + _auth.onAuthStateChanged().listen((user) { + if (user == null) { + clear(); + } + }); + } + + UserPreferences? _currentUserPreferences; + final StreamController _userPreferencesController = + StreamController.broadcast(); + + @override + Stream watch() { + return _userPreferencesController.stream; + } @override Future load() async { _store ??= await LocalKeyValueStore.getInstance(); - final currentTheme = _store!.getString('currentTheme'); - - if (currentTheme != null) { - return UserPreferences( - currentTheme: _parseThemeFromLocalStorage(currentTheme), - ); - } + final currentTheme = _store!.getString('currentTheme') ?? + _themeDetector.getOSDefaultTheme().name; + final lastSelectedDriveId = _store!.getString('lastSelectedDriveId'); + final showHiddenFiles = _store!.getBool('showHiddenFiles') ?? false; - return UserPreferences( - currentTheme: _themeDetector.getOSDefaultTheme(), + _currentUserPreferences = UserPreferences( + currentTheme: _parseThemeFromLocalStorage(currentTheme), + lastSelectedDriveId: lastSelectedDriveId, + showHiddenFiles: showHiddenFiles, + userHasHiddenDrive: _store!.getBool('userHasHiddenDrive') ?? false, ); + + _userPreferencesController.sink.add(_currentUserPreferences!); + + return _currentUserPreferences!; } @override Future saveTheme(ArDriveThemes theme) async { - (await _getStore()).putString( - 'currentTheme', - theme.name, + await _updatePreference( + key: 'currentTheme', + value: theme.name, + updateFunction: (value) => + _currentUserPreferences!.copyWith(currentTheme: theme), + ); + } + + @override + Future saveLastSelectedDriveId(String driveId) async { + await _updatePreference( + key: 'lastSelectedDriveId', + value: driveId, + updateFunction: (value) => + _currentUserPreferences!.copyWith(lastSelectedDriveId: value), + ); + } + + @override + Future saveShowHiddenFiles(bool showHiddenFiles) async { + await _updatePreference( + key: 'showHiddenFiles', + value: showHiddenFiles, + updateFunction: (value) => + _currentUserPreferences!.copyWith(showHiddenFiles: value), + ); + } + + @override + Future saveUserHasHiddenItem(bool userHasHiddenDrive) async { + await _updatePreference( + key: 'userHasHiddenDrive', + value: userHasHiddenDrive, + updateFunction: (value) => + _currentUserPreferences!.copyWith(userHasHiddenDrive: value), ); } @@ -59,6 +124,21 @@ class _UserPreferencesRepository implements UserPreferencesRepository { return _store!; } + @override + Future clear() async { + (await _getStore()).remove('lastSelectedDriveId'); + (await _getStore()).remove('showHiddenFiles'); + (await _getStore()).remove('userHasHiddenDrive'); + + _currentUserPreferences = _currentUserPreferences!.copyWith( + lastSelectedDriveId: null, + showHiddenFiles: false, + userHasHiddenDrive: false, + ); + + _userPreferencesController.sink.add(_currentUserPreferences!); + } + // parse theme from string to ArDriveThemes ArDriveThemes _parseThemeFromLocalStorage(String theme) { switch (theme) { @@ -70,4 +150,21 @@ class _UserPreferencesRepository implements UserPreferencesRepository { return ArDriveThemes.light; } } + + Future _updatePreference({ + required String key, + required T value, + required UserPreferences Function(T) updateFunction, + }) async { + _currentUserPreferences = updateFunction(value); + + final store = await _getStore(); + if (value is String) { + await store.putString(key, value as String); + } else if (value is bool) { + await store.putBool(key, value as bool); + } else { + throw ArgumentError('Unsupported type for preference value'); + } + } } diff --git a/lib/user/user_preferences.dart b/lib/user/user_preferences.dart index f7a97c0d01..44054cdfc9 100644 --- a/lib/user/user_preferences.dart +++ b/lib/user/user_preferences.dart @@ -3,11 +3,36 @@ import 'package:equatable/equatable.dart'; class UserPreferences extends Equatable { final ArDriveThemes currentTheme; + final String? lastSelectedDriveId; + final bool showHiddenFiles; + final bool userHasHiddenDrive; const UserPreferences({ required this.currentTheme, + required this.lastSelectedDriveId, + this.showHiddenFiles = false, + this.userHasHiddenDrive = false, }); @override - List get props => [currentTheme.name]; + List get props => [ + currentTheme.name, + lastSelectedDriveId, + showHiddenFiles, + userHasHiddenDrive, + ]; + + UserPreferences copyWith({ + ArDriveThemes? currentTheme, + String? lastSelectedDriveId, + bool? showHiddenFiles, + bool? userHasHiddenDrive, + }) { + return UserPreferences( + currentTheme: currentTheme ?? this.currentTheme, + lastSelectedDriveId: lastSelectedDriveId ?? this.lastSelectedDriveId, + showHiddenFiles: showHiddenFiles ?? this.showHiddenFiles, + userHasHiddenDrive: userHasHiddenDrive ?? this.userHasHiddenDrive, + ); + } } diff --git a/packages/ardrive_ui/lib/src/components/accordion.dart b/packages/ardrive_ui/lib/src/components/accordion.dart index d46354e3b1..d505a590b8 100644 --- a/packages/ardrive_ui/lib/src/components/accordion.dart +++ b/packages/ardrive_ui/lib/src/components/accordion.dart @@ -48,6 +48,15 @@ class _ArDriveAccordionState extends State { super.initState(); } + @override + void didUpdateWidget(ArDriveAccordion oldWidget) { + super.didUpdateWidget(oldWidget); + + tiles = [...widget.children]; + controller = + List.generate(tiles.length, (index) => ExpansionTileController()); + } + @override Widget build(BuildContext context) { return Theme( diff --git a/packages/ardrive_ui/lib/src/components/button.dart b/packages/ardrive_ui/lib/src/components/button.dart index 089fb77cb5..4cd2e75dc1 100644 --- a/packages/ardrive_ui/lib/src/components/button.dart +++ b/packages/ardrive_ui/lib/src/components/button.dart @@ -366,8 +366,9 @@ class _ArDriveButtonNewState extends State { final text = Text(widget.text, textAlign: TextAlign.center, - style: typography.paragraphLarge( - color: foregroundColor, fontWeight: ArFontWeight.semiBold)); + style: widget.fontStyle ?? + typography.paragraphLarge( + color: foregroundColor, fontWeight: ArFontWeight.semiBold)); final buttonH = widget.maxHeight ?? buttonDefaultHeight; diff --git a/packages/ario_sdk/lib/src/models/ant_record.dart b/packages/ario_sdk/lib/src/models/ant_record.dart index 6165e85f08..f154fb3bcd 100644 --- a/packages/ario_sdk/lib/src/models/ant_record.dart +++ b/packages/ario_sdk/lib/src/models/ant_record.dart @@ -1,9 +1,14 @@ -class ANTRecord { +import 'package:equatable/equatable.dart'; + +class ANTRecord extends Equatable { final String domain; final String processId; - ANTRecord({ + const ANTRecord({ required this.domain, required this.processId, }); + + @override + List get props => [domain, processId]; } diff --git a/packages/ario_sdk/lib/src/models/arns_record.dart b/packages/ario_sdk/lib/src/models/arns_record.dart index dc86687c5f..fd1394756a 100644 --- a/packages/ario_sdk/lib/src/models/arns_record.dart +++ b/packages/ario_sdk/lib/src/models/arns_record.dart @@ -1,16 +1,20 @@ +import 'package:equatable/equatable.dart'; import 'package:json_annotation/json_annotation.dart'; part 'arns_record.g.dart'; @JsonSerializable() -class ARNSRecord { +class ARNSRecord extends Equatable { final String transactionId; final int ttlSeconds; - ARNSRecord({required this.transactionId, required this.ttlSeconds}); + const ARNSRecord({required this.transactionId, required this.ttlSeconds}); factory ARNSRecord.fromJson(Map json) => _$ARNSRecordFromJson(json); Map toJson() => _$ARNSRecordToJson(this); + + @override + List get props => [transactionId, ttlSeconds]; } diff --git a/packages/ario_sdk/lib/src/utils/get_literal_arns_record_name.dart b/packages/ario_sdk/lib/src/utils/get_literal_arns_record_name.dart index dcd7018f26..bcd4c8dd66 100644 --- a/packages/ario_sdk/lib/src/utils/get_literal_arns_record_name.dart +++ b/packages/ario_sdk/lib/src/utils/get_literal_arns_record_name.dart @@ -6,3 +6,4 @@ String getLiteralARNSRecordName(ARNSUndername undername) { } return '${undername.name}_${undername.domain}'; } + diff --git a/packages/ario_sdk/test/src/utils/get_literal_arns_record_name_test.dart b/packages/ario_sdk/test/src/utils/get_literal_arns_record_name_test.dart index c69a721ea8..2bd02abc4b 100644 --- a/packages/ario_sdk/test/src/utils/get_literal_arns_record_name_test.dart +++ b/packages/ario_sdk/test/src/utils/get_literal_arns_record_name_test.dart @@ -5,7 +5,7 @@ void main() { group('getLiteralARNSRecordName', () { test('returns the correct record name for a given ARNSUndername', () { // Arrange - final undername = ARNSUndername( + const undername = ARNSUndername( name: 'test', domain: 'example.com', record: ARNSRecord( @@ -24,7 +24,7 @@ void main() { test('returns the correct record name for a given ARNSUndername with @', () { // Arrange - final undername = ARNSUndername( + const undername = ARNSUndername( name: '@', // @ is the default name for the root domain domain: 'example.com', record: ARNSRecord( diff --git a/pubspec.yaml b/pubspec.yaml index 02383c7340..73c39ad21a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: Secure, permanent storage publish_to: 'none' -version: 2.56.0 +version: 2.57.0 environment: sdk: '>=3.2.0 <4.0.0' diff --git a/test/arns/presentation/assign_name_bloc/assign_name_bloc_test.dart b/test/arns/presentation/assign_name_bloc/assign_name_bloc_test.dart index 874ec963e5..e3375298a2 100644 --- a/test/arns/presentation/assign_name_bloc/assign_name_bloc_test.dart +++ b/test/arns/presentation/assign_name_bloc/assign_name_bloc_test.dart @@ -14,13 +14,13 @@ class MockFileDataTableItem extends Mock implements FileDataTableItem {} void main() { setUpAll(() { - registerFallbackValue(ARNSUndername( + registerFallbackValue(const ARNSUndername( name: 'test_undername', domain: 'test.ar', record: ARNSRecord(transactionId: 'test_tx_id', ttlSeconds: 3600), )); registerFallbackValue( - ANTRecord(domain: 'test.ar', processId: 'test_process_id')); + const ANTRecord(domain: 'test.ar', processId: 'test_process_id')); }); group('AssignNameBloc', () { @@ -51,8 +51,8 @@ void main() { // Arrange const walletAddress = 'test_wallet_address'; final antRecords = [ - ANTRecord(domain: 'test1.ar', processId: 'process1'), - ANTRecord(domain: 'test2.ar', processId: 'process2'), + const ANTRecord(domain: 'test1.ar', processId: 'process1'), + const ANTRecord(domain: 'test2.ar', processId: 'process2'), ]; when(() => mockAuth.getWalletAddress()) @@ -138,8 +138,8 @@ void main() { () async { // Arrange final antRecords = [ - ANTRecord(domain: 'domain1.ar', processId: 'process1'), - ANTRecord(domain: 'domain2.ar', processId: 'process2'), + const ANTRecord(domain: 'domain1.ar', processId: 'process1'), + const ANTRecord(domain: 'domain2.ar', processId: 'process2'), ]; final selectedName = antRecords[0]; @@ -165,12 +165,12 @@ void main() { () async { // Arrange final antRecords = [ - ANTRecord(domain: 'domain1.ar', processId: 'process1'), - ANTRecord(domain: 'domain2.ar', processId: 'process2'), + const ANTRecord(domain: 'domain1.ar', processId: 'process1'), + const ANTRecord(domain: 'domain2.ar', processId: 'process2'), ]; final selectedName = antRecords[1]; final undernames = [ - ARNSUndername( + const ARNSUndername( name: 'undername1', domain: 'domain1.ar', record: ARNSRecord(transactionId: 'tx1', ttlSeconds: 3600)), @@ -205,17 +205,17 @@ void main() { () async { // Arrange final antRecords = [ - ANTRecord(domain: 'domain1.ar', processId: 'process1'), - ANTRecord(domain: 'domain2.ar', processId: 'process2'), + const ANTRecord(domain: 'domain1.ar', processId: 'process1'), + const ANTRecord(domain: 'domain2.ar', processId: 'process2'), ]; final selectedName = antRecords[0]; final undernames = [ - ARNSUndername( + const ARNSUndername( name: 'undername1', domain: 'domain1.ar', record: ARNSRecord(transactionId: 'tx1', ttlSeconds: 3600), ), - ARNSUndername( + const ARNSUndername( name: 'undername2', domain: 'domain1.ar', record: ARNSRecord(transactionId: 'tx2', ttlSeconds: 3600), @@ -258,17 +258,17 @@ void main() { () async { // Arrange final antRecords = [ - ANTRecord(domain: 'domain1.ar', processId: 'process1'), - ANTRecord(domain: 'domain2.ar', processId: 'process2'), + const ANTRecord(domain: 'domain1.ar', processId: 'process1'), + const ANTRecord(domain: 'domain2.ar', processId: 'process2'), ]; final selectedName = antRecords[0]; final undernames = [ - ARNSUndername( + const ARNSUndername( name: 'undername1', domain: 'domain1.ar', record: ARNSRecord(transactionId: 'tx1', ttlSeconds: 3600), ), - ARNSUndername( + const ARNSUndername( name: 'undername2', domain: 'domain1.ar', record: ARNSRecord(transactionId: 'tx2', ttlSeconds: 3600), @@ -311,8 +311,8 @@ void main() { when(() => mockFileDataTableItem.fileId).thenReturn('test_file_id'); when(() => mockFileDataTableItem.driveId).thenReturn('test_drive_id'); final antRecords = [ - ANTRecord(domain: 'test1.ar', processId: 'process1'), - ANTRecord(domain: 'test2.ar', processId: 'process2'), + const ANTRecord(domain: 'test1.ar', processId: 'process1'), + const ANTRecord(domain: 'test2.ar', processId: 'process2'), ]; const walletAddress = 'test_wallet_address'; @@ -329,7 +329,7 @@ void main() { )).thenAnswer((_) async {}); when(() => mockArnsRepository.getARNSUndernames(any())).thenAnswer( (_) async => [ - ARNSUndername( + const ARNSUndername( name: 'undername', domain: 'domain', record: @@ -342,9 +342,9 @@ void main() { act: (bloc) { bloc.add(const LoadNames()); bloc.add( - SelectName(ANTRecord(domain: 'domain', processId: 'process_id'))); + const SelectName(ANTRecord(domain: 'domain', processId: 'process_id'))); bloc.add(const LoadUndernames()); - bloc.add(SelectUndername( + bloc.add(const SelectUndername( undername: ARNSUndername( name: 'undername', domain: 'domain', @@ -385,8 +385,8 @@ void main() { when(() => mockFileDataTableItem.fileId).thenReturn('test_file_id'); when(() => mockFileDataTableItem.driveId).thenReturn('test_drive_id'); final antRecords = [ - ANTRecord(domain: 'test1.ar', processId: 'process1'), - ANTRecord(domain: 'test2.ar', processId: 'process2'), + const ANTRecord(domain: 'test1.ar', processId: 'process1'), + const ANTRecord(domain: 'test2.ar', processId: 'process2'), ]; const walletAddress = 'test_wallet_address'; @@ -406,7 +406,7 @@ void main() { )).thenThrow(StateError('Test error')); when(() => mockArnsRepository.getARNSUndernames(any())).thenAnswer( (_) async => [ - ARNSUndername( + const ARNSUndername( name: 'undername', domain: 'domain', record: ARNSRecord(transactionId: 'test_tx_id', ttlSeconds: 3600), @@ -418,9 +418,9 @@ void main() { act: (bloc) { bloc.add(const LoadNames()); bloc.add( - SelectName(ANTRecord(domain: 'domain', processId: 'process_id'))); + const SelectName(ANTRecord(domain: 'domain', processId: 'process_id'))); bloc.add(const LoadUndernames()); - bloc.add(SelectUndername( + bloc.add(const SelectUndername( undername: ARNSUndername( name: 'undername', domain: 'domain', diff --git a/test/blocs/drives_cubit_test.dart b/test/blocs/drives_cubit_test.dart index 831dd814dc..83ac51ff7b 100644 --- a/test/blocs/drives_cubit_test.dart +++ b/test/blocs/drives_cubit_test.dart @@ -4,6 +4,7 @@ import 'package:ardrive/blocs/blocs.dart'; import 'package:ardrive/blocs/prompt_to_snapshot/prompt_to_snapshot_bloc.dart'; import 'package:ardrive/core/activity_tracker.dart'; import 'package:ardrive/models/models.dart'; +import 'package:ardrive/user/repositories/user_preferences_repository.dart'; import 'package:bloc_test/bloc_test.dart'; import 'package:mocktail/mocktail.dart'; import 'package:test/test.dart'; @@ -13,6 +14,9 @@ import '../test_utils/utils.dart'; class MockActivityTracker extends Mock implements ActivityTracker {} +class MockUserPreferencesRepository extends Mock + implements UserPreferencesRepository {} + void main() { group('DrivesCubit', () { late Database db; @@ -21,6 +25,7 @@ void main() { late ProfileCubit profileCubit; late DrivesCubit drivesCubit; late PromptToSnapshotBloc promptToSnapshotBloc; + late UserPreferencesRepository userPreferencesRepository; setUp(() { registerFallbackValue(SyncStateFake()); @@ -30,13 +35,14 @@ void main() { profileCubit = MockProfileCubit(); promptToSnapshotBloc = MockPromptToSnapshotBloc(); - + userPreferencesRepository = MockUserPreferencesRepository(); drivesCubit = DrivesCubit( activityTracker: MockActivityTracker(), auth: MockArDriveAuth(), profileCubit: profileCubit, driveDao: driveDao, promptToSnapshotBloc: promptToSnapshotBloc, + userPreferencesRepository: userPreferencesRepository, ); }); diff --git a/test/blocs/fs_entry_move_bloc_test.dart b/test/blocs/fs_entry_move_bloc_test.dart index 75d2658a12..fe51d39215 100644 --- a/test/blocs/fs_entry_move_bloc_test.dart +++ b/test/blocs/fs_entry_move_bloc_test.dart @@ -286,7 +286,6 @@ void main() { blocTest( 'throws when selectedItems is empty', build: () => FsEntryMoveBloc( - driveDetailCubit: MockDriveDetailCubit(), arweave: arweave, turboUploadService: turboUploadService, syncCubit: syncBloc, @@ -303,11 +302,10 @@ void main() { build: () => FsEntryMoveBloc( crypto: ArDriveCrypto(), arweave: arweave, - driveDetailCubit: MockDriveDetailCubit(), turboUploadService: turboUploadService, syncCubit: syncBloc, driveId: driveId, - driveDao: driveDao, + driveDao: driveDao, profileCubit: profileCubit, // TODO: revisit this when we have a better way to mock the selected items selectedItems: [], @@ -320,6 +318,7 @@ void main() { bloc.add(FsEntryMoveSubmit( folderInView: (bloc.state as FsEntryMoveLoadSuccess).viewingFolder.folder, + showHiddenItems: false, )); } }, diff --git a/test/blocs/hide/global_hide_bloc_test.dart b/test/blocs/hide/global_hide_bloc_test.dart new file mode 100644 index 0000000000..5231568d4a --- /dev/null +++ b/test/blocs/hide/global_hide_bloc_test.dart @@ -0,0 +1,157 @@ +import 'package:ardrive/blocs/hide/global_hide_bloc.dart'; +import 'package:ardrive/models/daos/drive_dao/drive_dao.dart'; +import 'package:ardrive/user/repositories/user_preferences_repository.dart'; +import 'package:ardrive/user/user_preferences.dart'; +import 'package:ardrive_ui/ardrive_ui.dart'; +import 'package:bloc_test/bloc_test.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mocktail/mocktail.dart'; + +class MockUserPreferencesRepository extends Mock + implements UserPreferencesRepository {} + +class MockDriveDao extends Mock implements DriveDao {} + +void main() { + late MockUserPreferencesRepository mockUserPreferencesRepository; + late MockDriveDao mockDriveDao; + + setUp(() { + mockUserPreferencesRepository = MockUserPreferencesRepository(); + mockDriveDao = MockDriveDao(); + when(() => mockUserPreferencesRepository.clear()).thenAnswer((_) async {}); + when(() => mockUserPreferencesRepository.load()).thenAnswer((_) async { + return const UserPreferences( + showHiddenFiles: false, + userHasHiddenDrive: false, + currentTheme: ArDriveThemes.light, + lastSelectedDriveId: '', + ); + }); + + when(() => mockUserPreferencesRepository.watch()).thenAnswer( + (_) => const Stream.empty(), + ); + when(() => mockDriveDao.userHasHiddenItems()) + .thenAnswer((_) async => false); + }); + + blocTest( + 'initial state is correct', + setUp: () { + when(() => mockUserPreferencesRepository.saveShowHiddenFiles(false)) + .thenAnswer((_) async {}); + }, + build: () => GlobalHideBloc( + userPreferencesRepository: mockUserPreferencesRepository, + driveDao: mockDriveDao, + ), + verify: (bloc) { + expect(bloc.state, const GlobalHideInitial(userHasHiddenDrive: false)); + }, + ); + + blocTest( + 'ShowItems event emits ShowingHiddenItems state and saves preference', + build: () { + when(() => mockUserPreferencesRepository.saveShowHiddenFiles(true)) + .thenAnswer((_) async {}); + return GlobalHideBloc( + userPreferencesRepository: mockUserPreferencesRepository, + driveDao: mockDriveDao, + ); + }, + act: (bloc) => bloc.add(const ShowItems(userHasHiddenItems: true)), + expect: () => [const ShowingHiddenItems(userHasHiddenDrive: true)], + verify: (_) { + verify(() => mockUserPreferencesRepository.saveShowHiddenFiles(true)) + .called(1); + }, + ); + + blocTest( + 'HideItems event emits HiddingItems state and saves preference', + build: () { + when(() => mockUserPreferencesRepository.saveShowHiddenFiles(false)) + .thenAnswer((_) async {}); + return GlobalHideBloc( + userPreferencesRepository: mockUserPreferencesRepository, + driveDao: mockDriveDao, + ); + }, + act: (bloc) => bloc.add(const HideItems(userHasHiddenItems: false)), + expect: () => [const HiddingItems(userHasHiddenDrive: false)], + verify: (_) { + verify(() => mockUserPreferencesRepository.saveShowHiddenFiles(false)) + .called(1); + }, + ); + + blocTest( + 'RefreshOptions event emits updated state with userHasHiddenDrive', + build: () { + when(() => mockDriveDao.userHasHiddenItems()) + .thenAnswer((_) async => true); + return GlobalHideBloc( + userPreferencesRepository: mockUserPreferencesRepository, + driveDao: mockDriveDao, + ); + }, + act: (bloc) => bloc.add(const RefreshOptions(userHasHiddenItems: true)), + expect: () => [const GlobalHideInitial(userHasHiddenDrive: true)], + verify: (_) { + verify(() => mockDriveDao.userHasHiddenItems()).called(1); + }, + ); + + blocTest( + 'UserPreferencesRepository updates trigger events', + build: () { + // This test case verifies that the GlobalHideBloc correctly responds to + // changes in the UserPreferencesRepository. It simulates two scenarios: + // + // 1. When showHiddenFiles is set to true: + // - The bloc should emit a ShowingHiddenItems state + // - The userHasHiddenDrive property should be true + // + // 2. When showHiddenFiles is set to false: + // - The bloc should emit a HiddingItems state + // - The userHasHiddenDrive property should remain true + when(() => mockUserPreferencesRepository.watch()).thenAnswer( + (_) => Stream.fromIterable([ + /// Show hidden files + const UserPreferences( + showHiddenFiles: true, + userHasHiddenDrive: true, + currentTheme: ArDriveThemes.light, + lastSelectedDriveId: ''), + + /// Hide hidden files + const UserPreferences( + showHiddenFiles: false, + userHasHiddenDrive: true, + currentTheme: ArDriveThemes.light, + lastSelectedDriveId: ''), + ]), + ); + + when(() => mockDriveDao.userHasHiddenItems()) + .thenAnswer((_) async => true); + + when(() => mockUserPreferencesRepository.saveShowHiddenFiles(any())) + .thenAnswer((_) async {}); + + return GlobalHideBloc( + userPreferencesRepository: mockUserPreferencesRepository, + driveDao: mockDriveDao, + ); + }, + expect: () => [ + /// Show hidden files + const ShowingHiddenItems(userHasHiddenDrive: true), + + /// Hide hidden files + const HiddingItems(userHasHiddenDrive: true), + ], + ); +} diff --git a/test/blocs/upload_cubit_test.dart b/test/blocs/upload_cubit_test.dart index 86b0e5c444..86b87207e2 100644 --- a/test/blocs/upload_cubit_test.dart +++ b/test/blocs/upload_cubit_test.dart @@ -151,7 +151,8 @@ void main() { ownerAddress: '', dateCreated: tDefaultDate, lastUpdated: tDefaultDate, - privacy: '')); + privacy: '', + isHidden: false)); registerFallbackValue(UploadParams( user: getFakeUser(), diff --git a/test/core/upload/uploader_test.dart b/test/core/upload/uploader_test.dart index e06dfa5b94..cdb8596421 100644 --- a/test/core/upload/uploader_test.dart +++ b/test/core/upload/uploader_test.dart @@ -1011,12 +1011,12 @@ AppConfig getFakeConfigForDisabledTurbo() => AppConfig( ), ); User getFakeUser() => User( - password: 'password', - wallet: getTestWallet(), - walletAddress: 'walletAddress', - walletBalance: BigInt.one, - cipherKey: SecretKey([]), - profileType: ProfileType.arConnect, + password: 'password', + wallet: getTestWallet(), + walletAddress: 'walletAddress', + walletBalance: BigInt.one, + cipherKey: SecretKey([]), + profileType: ProfileType.arConnect, errorFetchingIOTokens: false, ); @@ -1039,4 +1039,5 @@ Drive getFakeDrive() => Drive( rootFolderId: 'rootFolderId', ownerAddress: 'ownerAddress', privacy: 'privacy', + isHidden: false, ); diff --git a/test/manifest/domain/manifest_repository_test.dart b/test/manifest/domain/manifest_repository_test.dart index 83d6fb82af..ee6306307a 100644 --- a/test/manifest/domain/manifest_repository_test.dart +++ b/test/manifest/domain/manifest_repository_test.dart @@ -65,7 +65,7 @@ void main() async { registerFallbackValue(FileEntity()); registerFallbackValue(const FileRevisionsCompanion()); registerFallbackValue( - ARNSUndername( + const ARNSUndername( name: 'undername', domain: 'domain', record: ARNSRecord( @@ -284,7 +284,7 @@ void main() async { await repository.uploadManifest( params: mockUploadParams, processId: 'process_id', - undername: ARNSUndername( + undername: const ARNSUndername( name: 'undername', domain: 'domain', record: ARNSRecord( @@ -347,7 +347,7 @@ void main() async { await repository.uploadManifest( params: mockUploadParams, processId: 'process_id', - undername: ARNSUndername( + undername: const ARNSUndername( name: 'undername', domain: 'domain', record: ARNSRecord( diff --git a/test/search/domain/bloc/search_bloc_test.dart b/test/search/domain/bloc/search_bloc_test.dart index ea8aa40e84..aee9669e22 100644 --- a/test/search/domain/bloc/search_bloc_test.dart +++ b/test/search/domain/bloc/search_bloc_test.dart @@ -51,6 +51,7 @@ void main() { name: '', privacy: '', lastUpdated: DateTime.now(), + isHidden: false, ); final SearchResult result1 = SearchResult( diff --git a/test/theme/theme_switcher_bloc_test.dart b/test/theme/theme_switcher_bloc_test.dart index 26dbe7228d..0f5e763dcc 100644 --- a/test/theme/theme_switcher_bloc_test.dart +++ b/test/theme/theme_switcher_bloc_test.dart @@ -30,7 +30,9 @@ void main() { 'emits ThemeSwitcherLightTheme when LoadTheme succeeds with light theme', build: () { when(() => userPreferencesRepository.load()).thenAnswer( - (_) async => const UserPreferences(currentTheme: ArDriveThemes.light), + (_) async => const UserPreferences( + currentTheme: ArDriveThemes.light, + lastSelectedDriveId: 'drive_id'), ); return themeSwitcherBloc; }, @@ -42,7 +44,9 @@ void main() { 'emits ThemeSwitcherDarkTheme when LoadTheme succeeds with dark theme', build: () { when(() => userPreferencesRepository.load()).thenAnswer( - (_) async => const UserPreferences(currentTheme: ArDriveThemes.dark), + (_) async => const UserPreferences( + currentTheme: ArDriveThemes.dark, + lastSelectedDriveId: 'drive_id'), ); return themeSwitcherBloc; }, @@ -54,7 +58,9 @@ void main() { 'emits ThemeSwitcherDarkTheme when ChangeTheme from ThemeSwitcherLightTheme', build: () { when(() => userPreferencesRepository.load()).thenAnswer( - (_) async => const UserPreferences(currentTheme: ArDriveThemes.light), + (_) async => const UserPreferences( + currentTheme: ArDriveThemes.light, + lastSelectedDriveId: 'drive_id'), ); when(() => userPreferencesRepository.saveTheme(ArDriveThemes.dark)) .thenAnswer((_) => Future.value()); @@ -76,7 +82,9 @@ void main() { 'emits ThemeSwitcherLightTheme when ChangeTheme from ThemeSwitcherDarkTheme', build: () { when(() => userPreferencesRepository.load()).thenAnswer( - (_) async => const UserPreferences(currentTheme: ArDriveThemes.dark), + (_) async => const UserPreferences( + currentTheme: ArDriveThemes.dark, + lastSelectedDriveId: 'drive_id'), ); when(() => userPreferencesRepository.saveTheme(ArDriveThemes.light)) .thenAnswer((_) => Future.value()); diff --git a/test/user/repositories/user_preferences_repository_test.dart b/test/user/repositories/user_preferences_repository_test.dart index 23be354d78..55ae960d6c 100644 --- a/test/user/repositories/user_preferences_repository_test.dart +++ b/test/user/repositories/user_preferences_repository_test.dart @@ -3,9 +3,12 @@ import 'package:ardrive/user/repositories/user_preferences_repository.dart'; import 'package:ardrive/user/user_preferences.dart'; import 'package:ardrive/utils/local_key_value_store.dart'; import 'package:ardrive_ui/ardrive_ui.dart'; +import 'package:async/async.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mocktail/mocktail.dart'; +import '../../core/upload/uploader_test.dart'; + class MockLocalKeyValueStore extends Mock implements LocalKeyValueStore {} class MockThemeDetector extends Mock implements ThemeDetector {} @@ -15,13 +18,18 @@ void main() { late UserPreferencesRepository repository; late MockLocalKeyValueStore mockStore; late MockThemeDetector mockThemeDetector; + late MockArDriveAuth mockAuth; - setUp(() { + setUpAll(() { mockStore = MockLocalKeyValueStore(); mockThemeDetector = MockThemeDetector(); + mockAuth = MockArDriveAuth(); + when(() => mockAuth.onAuthStateChanged()) + .thenAnswer((_) => Stream.value(getFakeUser())); repository = UserPreferencesRepository( store: mockStore, themeDetector: mockThemeDetector, + auth: mockAuth, ); }); @@ -30,18 +38,38 @@ void main() { when(() => mockStore.getString('currentTheme')).thenReturn(null); when(() => mockThemeDetector.getOSDefaultTheme()) .thenReturn(ArDriveThemes.light); + when(() => mockStore.getBool('showHiddenFiles')).thenReturn(false); + when(() => mockStore.getBool('userHasHiddenDrive')).thenReturn(false); final result = await repository.load(); - expect(result, const UserPreferences(currentTheme: ArDriveThemes.light)); + expect( + result, + const UserPreferences( + currentTheme: ArDriveThemes.light, + lastSelectedDriveId: null, + showHiddenFiles: false, + userHasHiddenDrive: false, + )); }); test('should return saved theme from storage', () async { when(() => mockStore.getString('currentTheme')).thenReturn('dark'); + when(() => mockStore.getBool('showHiddenFiles')).thenReturn(false); + when(() => mockStore.getBool('userHasHiddenDrive')).thenReturn(false); + when(() => mockAuth.onAuthStateChanged()) + .thenAnswer((_) => Stream.value(getFakeUser())); final result = await repository.load(); - expect(result, const UserPreferences(currentTheme: ArDriveThemes.dark)); + expect( + result, + const UserPreferences( + currentTheme: ArDriveThemes.dark, + lastSelectedDriveId: null, + showHiddenFiles: false, + userHasHiddenDrive: false, + )); }); test('should save theme to storage', () async { @@ -54,5 +82,129 @@ void main() { mockStore.putString('currentTheme', ArDriveThemes.light.name)) .called(1); }); + + test('should save last selected drive id to storage', () async { + when(() => mockStore.putString('lastSelectedDriveId', 'drive_id')) + .thenAnswer((_) async => true); + + await repository.saveLastSelectedDriveId('drive_id'); + + verify(() => mockStore.putString('lastSelectedDriveId', 'drive_id')) + .called(1); + }); + + test('should return last selected drive id from storage', () async { + when(() => mockStore.getString('lastSelectedDriveId')) + .thenReturn('drive_id'); + when(() => mockStore.getString('currentTheme')).thenReturn('dark'); + when(() => mockStore.getBool('showHiddenFiles')).thenReturn(false); + when(() => mockStore.getBool('userHasHiddenDrive')).thenReturn(false); + + final result = await repository.load(); + + expect( + result, + const UserPreferences( + currentTheme: ArDriveThemes.dark, + lastSelectedDriveId: 'drive_id', + showHiddenFiles: false, + userHasHiddenDrive: false, + )); + }); + + test('should save show hidden files preference to storage', () async { + when(() => mockStore.putBool('showHiddenFiles', true)) + .thenAnswer((_) async => true); + + await repository.saveShowHiddenFiles(true); + + verify(() => mockStore.putBool('showHiddenFiles', true)).called(1); + }); + + test('should save user has hidden item preference to storage', () async { + when(() => mockStore.putBool('userHasHiddenDrive', true)) + .thenAnswer((_) async => true); + + await repository.saveUserHasHiddenItem(true); + + verify(() => mockStore.putBool('userHasHiddenDrive', true)).called(1); + }); + + test('should clear last selected drive id from storage', () async { + when(() => mockStore.remove('lastSelectedDriveId')) + .thenAnswer((_) async => true); + when(() => mockStore.remove('showHiddenFiles')).thenAnswer((_) async => true); + when(() => mockStore.remove('userHasHiddenDrive')) + .thenAnswer((_) async => true); + + await repository.clear(); + + verify(() => mockStore.remove('lastSelectedDriveId')).called(1); + }); + + test( + 'should watch for changes in user preferences', + () async { + const initialPreferences = UserPreferences( + currentTheme: ArDriveThemes.light, + lastSelectedDriveId: null, + showHiddenFiles: false, + userHasHiddenDrive: false, + ); + + when(() => mockStore.getString('currentTheme')).thenReturn('light'); + when(() => mockStore.getString('lastSelectedDriveId')).thenReturn(null); + when(() => mockStore.getBool('showHiddenFiles')).thenReturn(false); + when(() => mockStore.getBool('userHasHiddenDrive')).thenReturn(false); + + final stream = repository.watch(); + // Use a StreamQueue to easily work with the stream in tests + final queue = StreamQueue(stream); + + await repository.load(); // Ensure initial preferences are loaded + + expect( + await queue.next, + equals(initialPreferences), + ); + + when(() => mockStore.putString('currentTheme', ArDriveThemes.dark.name)) + .thenAnswer((_) async => true); + when(() => mockStore.putString('lastSelectedDriveId', 'new_drive_id')) + .thenAnswer((_) async => true); + when(() => mockStore.putBool('showHiddenFiles', true)) + .thenAnswer((_) async => true); + when(() => mockStore.putBool('userHasHiddenDrive', true)) + .thenAnswer((_) async => true); + + // Simulate changes in preferences + when(() => mockStore.getString('currentTheme')).thenReturn('dark'); + when(() => mockStore.getString('lastSelectedDriveId')) + .thenReturn('new_drive_id'); + when(() => mockStore.getBool('showHiddenFiles')).thenReturn(true); + when(() => mockStore.getBool('userHasHiddenDrive')).thenReturn(true); + + // Trigger preference changes + await repository.saveTheme(ArDriveThemes.dark); + await repository.saveLastSelectedDriveId('new_drive_id'); + await repository.saveShowHiddenFiles(true); + await repository.saveUserHasHiddenItem(true); + + await repository.load(); + + expect( + await queue.next, + const UserPreferences( + currentTheme: ArDriveThemes.dark, + lastSelectedDriveId: 'new_drive_id', + showHiddenFiles: true, + userHasHiddenDrive: true, + ), + ); + + // Clean up + await queue.cancel(); + }, + ); }); } diff --git a/test/utils/link_generators_test.dart b/test/utils/link_generators_test.dart index 93b7bf2e05..ab9aa443a9 100644 --- a/test/utils/link_generators_test.dart +++ b/test/utils/link_generators_test.dart @@ -21,6 +21,7 @@ void main() { privacy: DrivePrivacyTag.public, dateCreated: DateTime.now(), lastUpdated: DateTime.now(), + isHidden: false, ); testPrivateDrive = Drive( id: 'privateDriveId', @@ -30,6 +31,7 @@ void main() { privacy: DrivePrivacyTag.private, dateCreated: DateTime.now(), lastUpdated: DateTime.now(), + isHidden: false, ); testPrivateDriveKeyBase64 = 'X123YZAB-CD4e5fgHIjKlmN6O7pqrStuVwxYzaBcd8E';