From 0698659646c124473e4a590e891d8a588115ca89 Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Mon, 7 Oct 2024 11:00:21 -0300 Subject: [PATCH 01/48] update share drive style --- lib/components/details_panel.dart | 7 +- lib/components/drive_share_dialog.dart | 139 ++++++++++++------------- 2 files changed, 70 insertions(+), 76 deletions(-) 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..972bee30d2 100644 --- a/lib/components/drive_share_dialog.dart +++ b/lib/components/drive_share_dialog.dart @@ -46,83 +46,76 @@ 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, + 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: [ + Expanded( + child: ArDriveTextFieldNew( + 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( - color: ArDriveTheme.of(context) - .themeData - .colors - .themeFgDefault, - ) - .copyWith( - decoration: TextDecoration.underline, - ), + 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: typography.paragraphLarge().copyWith( + decoration: TextDecoration.underline, + ), + ), ), - ), - ], - ), - 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, + ], + ), + 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) - ], + } 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, + ) + ], + ); + }, ); } From 71ac33620ef6e453e2be77df2d49b5be791ac51a Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Thu, 10 Oct 2024 14:04:21 -0300 Subject: [PATCH 02/48] hide drives + important fixes --- lib/app_shell.dart | 16 +- .../drive_detail/drive_detail_cubit.dart | 39 +- .../drive_detail/drive_detail_state.dart | 2 + lib/blocs/drives/drives_cubit.dart | 29 +- lib/blocs/hide/global_hide_bloc.dart | 40 ++ lib/blocs/hide/global_hide_event.dart | 26 ++ lib/blocs/hide/global_hide_state.dart | 61 +++ lib/blocs/hide/hide_bloc.dart | 168 ++++++-- lib/blocs/hide/hide_event.dart | 22 ++ lib/blocs/hide/hide_state.dart | 2 + lib/components/app_top_bar.dart | 41 ++ lib/components/hide_dialog.dart | 29 +- lib/components/side_bar.dart | 262 +++++++----- lib/dev_tools/shortcut_handler.dart | 18 +- lib/entities/drive_entity.dart | 3 + lib/main.dart | 10 + lib/models/database/database.dart | 8 +- lib/models/drive.dart | 1 + lib/models/drive_revision.dart | 3 +- lib/models/queries/drive_queries.drift | 13 + lib/models/tables/drive_revisions.drift | 2 + lib/models/tables/drives.drift | 2 + lib/pages/app_router_delegate.dart | 184 ++++----- .../components/drive_detail_data_list.dart | 232 +++++------ .../components/drive_explorer_item_tile.dart | 16 + .../drive_detail/components/hover_widget.dart | 2 +- lib/pages/drive_detail/drive_detail_page.dart | 373 +++++++++++------- .../drive_detail/models/data_table_item.dart | 11 +- lib/search/search_modal.dart | 34 +- .../domain/repositories/sync_repository.dart | 8 + .../user_preferences_repository.dart | 91 ++++- lib/user/user_preferences.dart | 27 +- .../lib/src/components/accordion.dart | 9 + .../src/components/data_table/data_table.dart | 9 + test/blocs/upload_cubit_test.dart | 3 +- test/core/upload/uploader_test.dart | 13 +- 36 files changed, 1256 insertions(+), 553 deletions(-) create mode 100644 lib/blocs/hide/global_hide_bloc.dart create mode 100644 lib/blocs/hide/global_hide_event.dart create mode 100644 lib/blocs/hide/global_hide_state.dart diff --git a/lib/app_shell.dart b/lib/app_shell.dart index 593e250891..a832137243 100644 --- a/lib/app_shell.dart +++ b/lib/app_shell.dart @@ -82,14 +82,15 @@ class AppShellState extends State { driveId: drivesState.selectedDriveId, )); } + if (syncState is SyncInProgress) {} } }, - 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 +182,9 @@ class AppShellState extends State { ), ), ], - ) - : scaffold, + ), + ]); + }, ), ); return ScreenTypeLayout.builder( diff --git a/lib/blocs/drive_detail/drive_detail_cubit.dart b/lib/blocs/drive_detail/drive_detail_cubit.dart index dcd6d765c8..c7416765ad 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; @@ -52,7 +52,7 @@ class DriveDetailCubit extends Cubit { bool _showHiddenFiles = false; DriveDetailCubit({ - required this.driveId, + required String driveId, String? initialFolderId, required ProfileCubit profileCubit, required DriveDao driveDao, @@ -70,6 +70,7 @@ class DriveDetailCubit extends Cubit { _breadcrumbBuilder = breadcrumbBuilder, _syncCubit = syncCubit, _driveRepository = driveRepository, + _driveId = driveId, super(DriveDetailLoadInProgress()) { if (driveId.isEmpty) { return; @@ -95,15 +96,34 @@ class DriveDetailCubit extends Cubit { } } + void showEmptyDriveDetail() async { + await _syncCubit.waitCurrentSync(); + + emit(DriveDetailLoadEmpty()); + } + + Future changeDrive(String driveId) async { + final drive = await _driveDao.driveById(driveId: driveId).getSingleOrNull(); + + if (drive == null) { + return; + } + + _driveId = driveId; + + openFolder(folderId: drive.rootFolderId); + } + void toggleHiddenFiles() { _showHiddenFiles = !_showHiddenFiles; refreshDriveDataTable(); } - void openFolder({ + Future openFolder({ String? folderId, String? otherDriveId, + String? selectedItemId, DriveOrder contentOrderBy = DriveOrder.name, OrderingMode contentOrderingMode = OrderingMode.asc, }) async { @@ -111,10 +131,9 @@ class DriveDetailCubit extends Cubit { await _syncCubit.waitCurrentSync(); try { - _selectedItem = null; _allImagesOfCurrentFolder = null; - String driveId = otherDriveId ?? this.driveId; + String driveId = otherDriveId ?? _driveId; emit(DriveDetailLoadInProgress()); @@ -195,6 +214,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, @@ -219,6 +244,7 @@ class DriveDetailCubit extends Cubit { pathSegments: pathSegments, driveIsEmpty: folderContents.files.isEmpty && folderContents.subfolders.isEmpty, + showSelectedItemDetails: _selectedItem != null, ), ); } else { @@ -242,6 +268,7 @@ class DriveDetailCubit extends Cubit { currentFolderContents: currentFolderContents, columnVisibility: columnsVisibility, isShowingHiddenFiles: _showHiddenFiles, + showSelectedItemDetails: _selectedItem != null, ), ); } @@ -319,6 +346,8 @@ class DriveDetailCubit extends Cubit { _selectedItem = item; + debugPrint('selectedItem: ${_selectedItem?.id}'); + int? selectedPage; if (openSelectedPage) { diff --git a/lib/blocs/drive_detail/drive_detail_state.dart b/lib/blocs/drive_detail/drive_detail_state.dart index d6f5178517..c9c56d9b19 100644 --- a/lib/blocs/drive_detail/drive_detail_state.dart +++ b/lib/blocs/drive_detail/drive_detail_state.dart @@ -144,4 +144,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..e72ec397b1 100644 --- a/lib/blocs/drives/drives_cubit.dart +++ b/lib/blocs/drives/drives_cubit.dart @@ -6,8 +6,11 @@ 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/logger.dart'; import 'package:ardrive/utils/user_utils.dart'; import 'package:ardrive_utils/ardrive_utils.dart'; +import 'package:collection/collection.dart'; import 'package:drift/drift.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -22,6 +25,7 @@ class DrivesCubit extends Cubit { final PromptToSnapshotBloc _promptToSnapshotBloc; final DriveDao _driveDao; final ArDriveAuth _auth; + final UserPreferencesRepository _userPreferencesRepository; late StreamSubscription _drivesSubscription; String? initialSelectedDriveId; @@ -32,10 +36,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) { @@ -69,8 +75,25 @@ class DrivesCubit extends Cubit { if (state is DrivesLoadSuccess && state.selectedDriveId != null) { selectedDriveId = state.selectedDriveId; } else { - selectedDriveId = initialSelectedDriveId ?? - (drives.isNotEmpty ? drives.first.id : null); + final userPreferences = await _userPreferencesRepository.load(); + + final userHasHiddenDrive = drives.any((d) => d.isHidden); + logger.d('User has hidden drive: $userHasHiddenDrive'); + + await _userPreferencesRepository + .saveUserHasHiddenItem(userHasHiddenDrive); + + if (userPreferences.lastSelectedDriveId != null) { + final lastSelectedDriveId = userPreferences.lastSelectedDriveId; + + if (drives.firstWhereOrNull((d) => d.id == lastSelectedDriveId) != + null) { + selectedDriveId = lastSelectedDriveId; + } + } else { + selectedDriveId = initialSelectedDriveId ?? + (drives.isNotEmpty ? drives.first.id : null); + } } final walletAddress = profileState is ProfileLoggedIn @@ -118,6 +141,8 @@ class DrivesCubit extends Cubit { ); _promptToSnapshotBloc.add(const SelectedDrive(driveId: null)); } + + _userPreferencesRepository.saveLastSelectedDriveId(driveId); emit(state); } diff --git a/lib/blocs/hide/global_hide_bloc.dart b/lib/blocs/hide/global_hide_bloc.dart new file mode 100644 index 0000000000..c805b2944f --- /dev/null +++ b/lib/blocs/hide/global_hide_bloc.dart @@ -0,0 +1,40 @@ +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)); + } + }); + + on((event, emit) async { + if (event is ShowItems) { + emit(ShowingHiddenItems(userHasHiddenDrive: event.userHasHiddenItems)); + _userPreferencesRepository.saveShowHiddenFiles(true); + } else if (event is HideItems) { + emit(HiddingItems(userHasHiddenDrive: event.userHasHiddenItems)); + _userPreferencesRepository.saveShowHiddenFiles(false); + } else if (event is RefreshOptions) { + final hasHiddenItems = await _driveDao.hasHiddenItems().getSingle(); + 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..0c656f4d5c 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,56 +146,93 @@ 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', ); - final entity = entryIsFile - ? currentEntry.asEntity() - : (currentEntry as FolderEntry).asEntity(); + late EntityWithCustomMetadata newEntryEntity; + late DataItem dataItem; + Drive? newDriveEntry; + FileEntry? newFileEntry; + FolderEntry? newFolderEntry; - final driveId = entryIsFile - ? currentEntry.driveId - : (currentEntry as FolderEntry).driveId; + if (currentEntry is Drive) { + newDriveEntry = currentEntry.copyWith( + isHidden: isHidden, + lastUpdated: DateTime.now(), + ); - final profile = _profileCubit.state as ProfileLoggedIn; - final driveKey = - await _driveDao.getDriveKey(driveId, profile.user.cipherKey); - final SecretKey? entityKey; + newEntryEntity = newDriveEntry.asEntity(); + final profile = _profileCubit.state as ProfileLoggedIn; - if (driveKey != null) { - if (entryIsFile) { - entityKey = await _crypto.deriveFileKey( - driveKey, - (entity as FileEntity).id!, - ); - } else { + 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; } + + dataItem = await _arweave.prepareEntityDataItem( + newEntryEntity, + profile.user.wallet, + key: entityKey, + ); } else { - entityKey = null; - } + final entity = entryIsFile + ? (currentEntry).asEntity() + : (currentEntry as FolderEntry).asEntity(); + + final driveId = entryIsFile + ? currentEntry.driveId + : (currentEntry as FolderEntry).driveId; + + final profile = _profileCubit.state as ProfileLoggedIn; + final driveKey = + await _driveDao.getDriveKey(driveId, profile.user.cipherKey); + final SecretKey? entityKey; - final newEntry = entryIsFile - ? currentEntry.copyWith( - isHidden: isHidden, - lastUpdated: DateTime.now(), - ) - : (currentEntry as FolderEntry).copyWith( - isHidden: isHidden, - lastUpdated: DateTime.now(), + if (driveKey != null) { + if (entryIsFile) { + entityKey = await _crypto.deriveFileKey( + driveKey, + (entity as FileEntity).id!, ); - final newEntryEntity = entryIsFile - ? (newEntry as FileEntry).asEntity() - : (newEntry as FolderEntry).asEntity(); - - final dataItem = await _arweave.prepareEntityDataItem( - newEntryEntity, - profile.user.wallet, - key: entityKey, - ); + } else { + entityKey = driveKey; + } + } else { + entityKey = null; + } + + if (entryIsFile) { + newFileEntry = currentEntry.copyWith( + isHidden: isHidden, + lastUpdated: DateTime.now(), + ); + } else if (entryIsFolder) { + newFolderEntry = currentEntry.copyWith( + isHidden: isHidden, + lastUpdated: DateTime.now(), + ); + } + + newEntryEntity = entryIsFile + ? (newFileEntry as FileEntry).asEntity() + : (newFolderEntry as FolderEntry).asEntity(); + + dataItem = await _arweave.prepareEntityDataItem( + newEntryEntity, + profile.user.wallet, + key: entityKey, + ); + } final dataItems = [dataItem]; @@ -203,9 +244,11 @@ class HideBloc extends Bloc { Future saveEntitiesToDb() async { await _driveDao.transaction(() async { if (entryIsFile) { - await _driveDao.writeToFile(newEntry as FileEntry); + await _driveDao.writeToFile(newFileEntry!); + } else if (entryIsDrive) { + await _driveDao.writeToDrive(newDriveEntry!); } else { - await _driveDao.writeToFolder(newEntry as FolderEntry); + await _driveDao.writeToFolder(newFolderEntry!); } newEntryEntity.txId = dataItem.id; @@ -216,6 +259,14 @@ class HideBloc extends Bloc { performedAction: isHidden ? RevisionAction.hide : RevisionAction.unhide, )); + } else if (entryIsDrive) { + final driveCompanion = + (newEntryEntity as DriveEntity).toRevisionCompanion( + performedAction: + isHidden ? RevisionAction.hide : RevisionAction.unhide, + ); + + await _driveDao.updateDrive(driveCompanion.toEntryCompanion()); } else { await _driveDao.insertFolderRevision( (newEntryEntity as FolderEntity).toRevisionCompanion( @@ -228,7 +279,9 @@ class HideBloc extends Bloc { final hideAction = entryIsFile ? (isHidden ? HideAction.hideFile : HideAction.unhideFile) - : (isHidden ? HideAction.hideFolder : HideAction.unhideFolder); + : entryIsDrive + ? (isHidden ? HideAction.hideDrive : HideAction.unhideDrive) + : (isHidden ? HideAction.hideFolder : HideAction.unhideFolder); emit( ConfirmingHideState( @@ -283,6 +336,37 @@ class HideBloc extends Bloc { } } + 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, 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..7e57f92faf 100644 --- a/lib/blocs/hide/hide_state.dart +++ b/lib/blocs/hide/hide_state.dart @@ -71,6 +71,8 @@ class FailureHideState extends HideState { enum HideAction { hideFile, hideFolder, + hideDrive, unhideFile, unhideFolder, + unhideDrive, } diff --git a/lib/components/app_top_bar.dart b/lib/components/app_top_bar.dart index a4d974b000..c0f66a758e 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,42 @@ 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; + return ArDriveIconButton( + tooltip: hideState is ShowingHiddenItems + ? 'Hide hidden items' + : 'Show hidden items', + icon: hideState is ShowingHiddenItems + ? ArDriveIcons.eyeOpen( + color: colorTokens.textMid, + ) + : ArDriveIcons.eyeClosed( + color: colorTokens.textMid, + ), + 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/hide_dialog.dart b/lib/components/hide_dialog.dart index b8fb4671b6..2bb2b1430f 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,8 +77,11 @@ 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()); @@ -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..f00438d977 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,93 +272,7 @@ 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 _buildAccordion(DrivesLoadSuccess state, bool isMobile) {} Widget _buildSideBarBottom() { return _isExpanded @@ -522,6 +437,7 @@ class DriveListTile extends StatelessWidget { final Drive drive; final bool hasAlert; final bool isSelected; + final bool isHidden; final VoidCallback onTap; const DriveListTile({ @@ -529,12 +445,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 +469,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 +633,121 @@ Future shareLogs({ ), ); } + +class _Accordion extends StatelessWidget { + const _Accordion({super.key, 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, + ), + ), + state.sharedDrives + .where((element) { + final isHidden = hideState is HiddingItems; + + return (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(), + ), + ], + ); + }, + ); + } +} +// 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 e3c31b42a3..378a497fce 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'; @@ -261,6 +262,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: @@ -416,6 +424,7 @@ class AppState extends State { RepositoryProvider( create: (_) => UserPreferencesRepository( themeDetector: ThemeDetector(), + auth: _.read(), ), ), RepositoryProvider( @@ -454,6 +463,7 @@ class AppState extends State { configService: configService, ), arnsRepository: _.read(), + userPreferencesRepository: _.read(), ), ), 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..2a1b177e65 100644 --- a/lib/models/drive_revision.dart +++ b/lib/models/drive_revision.dart @@ -36,7 +36,7 @@ extension DriveEntityExtensions on DriveEntity { /// This requires a `performedAction` to be specified. DriveRevisionsCompanion toRevisionCompanion( {required String performedAction}) => - DriveRevisionsCompanion.insert( + DriveRevisionsCompanion.insert( driveId: id!, ownerAddress: ownerAddress, rootFolderId: rootFolderId!, @@ -48,6 +48,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. diff --git a/lib/models/queries/drive_queries.drift b/lib/models/queries/drive_queries.drift index a871c824dd..b7f3e7adae 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..6933ee3ad7 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'; @@ -167,7 +168,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 +183,91 @@ 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( + // key: ValueKey(driveId), + 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, + ); + } + }, + ), + 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 +315,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 c566476806..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..eb02fdb08d 100644 --- a/lib/pages/drive_detail/components/drive_explorer_item_tile.dart +++ b/lib/pages/drive_detail/components/drive_explorer_item_tile.dart @@ -756,6 +756,22 @@ class EntityActionsMenu extends StatelessWidget { ), ), ), + 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..4129f8e52a 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,190 @@ 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(); - - 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 (state is DrivesLoadSuccess) { + if (state.userDrives.isNotEmpty) { + 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,11 +325,12 @@ 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; + final isShowingHiddenFiles = hideState is HiddingItems; return Column( children: [ @@ -474,6 +512,36 @@ class _DriveDetailPageState extends State { ), ), ), + 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( @@ -708,6 +776,7 @@ class _DriveDetailPageState extends State { DriveDetailLoadSuccess driveDetailLoadSuccessState, bool hasSubfolders, bool hasFiles, + GlobalHideState hideState, ) { final items = driveDetailLoadSuccessState.currentFolderContents; @@ -775,6 +844,7 @@ class _DriveDetailPageState extends State { hasSubfolders, hasFiles, items, + hideState, ), ); } @@ -784,8 +854,9 @@ class _DriveDetailPageState extends State { bool hasSubfolders, bool hasFiles, List items, + GlobalHideState globalHideState, ) { - final isShowingHiddenFiles = state.isShowingHiddenFiles; + final isShowingHiddenFiles = globalHideState is HiddingItems; final List filteredItems; @@ -828,6 +899,7 @@ class _DriveDetailPageState extends State { initialQuery: query, driveDetailCubit: context.read(), controller: controller, + drivesCubit: context.read(), ), ), ), @@ -1135,6 +1207,31 @@ class MobileFolderNavigation extends StatelessWidget { ), ), ), + 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( 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..30cbfb7dbe 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,6 +270,8 @@ class _SyncRepository implements SyncRepository { _arnsRepository .waitForARNSRecordsToUpdate() .then((value) => _arnsRepository.saveAllFilesWithAssignedNames()); + final hasHiddenItems = await _driveDao.hasHiddenItems().getSingle(); + await _userPreferencesRepository.saveUserHasHiddenItem(hasHiddenItems); await Future.wait( [ diff --git a/lib/user/repositories/user_preferences_repository.dart b/lib/user/repositories/user_preferences_repository.dart index 3d99adc0bd..8211e03636 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 clearLastSelectedDriveId(); + Future saveUserHasHiddenItem(bool userHasHiddenDrive); factory UserPreferencesRepository({ LocalKeyValueStore? store, required ThemeDetector themeDetector, + required ArDriveAuth auth, }) { return _UserPreferencesRepository( store: store, themeDetector: themeDetector, + auth: auth, ); } } @@ -21,28 +31,51 @@ 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) { + clearLastSelectedDriveId(); + } + }); + } + + 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 @@ -53,12 +86,47 @@ class _UserPreferencesRepository implements UserPreferencesRepository { ); } + @override + Future saveLastSelectedDriveId(String driveId) async { + (await _getStore()).putString( + 'lastSelectedDriveId', + driveId, + ); + } + + @override + Future saveShowHiddenFiles(bool showHiddenFiles) async { + (await _getStore()).putBool( + 'showHiddenFiles', + showHiddenFiles, + ); + } + + @override + Future saveUserHasHiddenItem(bool userHasHiddenDrive) async { + _currentUserPreferences = _currentUserPreferences!.copyWith( + userHasHiddenDrive: userHasHiddenDrive, + ); + + _userPreferencesController.sink.add(_currentUserPreferences!); + + (await _getStore()).putBool( + 'userHasHiddenDrive', + userHasHiddenDrive, + ); + } + Future _getStore() async { _store ??= await LocalKeyValueStore.getInstance(); return _store!; } + @override + Future clearLastSelectedDriveId() async { + (await _getStore()).remove('lastSelectedDriveId'); + } + // parse theme from string to ArDriveThemes ArDriveThemes _parseThemeFromLocalStorage(String theme) { switch (theme) { @@ -70,4 +138,7 @@ class _UserPreferencesRepository implements UserPreferencesRepository { return ArDriveThemes.light; } } + + @override + UserPreferences get currentUserPreferences => _currentUserPreferences!; } 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/data_table/data_table.dart b/packages/ardrive_ui/lib/src/components/data_table/data_table.dart index 606a58d50a..f1533e3f28 100644 --- a/packages/ardrive_ui/lib/src/components/data_table/data_table.dart +++ b/packages/ardrive_ui/lib/src/components/data_table/data_table.dart @@ -273,6 +273,9 @@ class _ArDriveDataTableState _sortRows(_sortedColumn!); } } + + debugPrint( + 'selectedItem on didUpdateWidget: ${_selectedItem.toString()}'); } } @@ -825,6 +828,12 @@ class _ArDriveDataTableState ) { final multiselect = getMultiSelectBox(); + final isSelected = _selectedItem == row; + + debugPrint('isSelected: $isSelected'); + debugPrint('row: ${row.toString()}'); + debugPrint('selectedItem: ${_selectedItem.toString()}'); + return GestureDetector( onTap: () { if (_isMultiSelecting) { diff --git a/test/blocs/upload_cubit_test.dart b/test/blocs/upload_cubit_test.dart index 2d806c01d1..2da5c6af57 100644 --- a/test/blocs/upload_cubit_test.dart +++ b/test/blocs/upload_cubit_test.dart @@ -132,7 +132,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, ); From 3a902ac4fad9d24afc4984e6cf3b7dfaf9f9afa8 Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Mon, 14 Oct 2024 10:26:35 -0300 Subject: [PATCH 03/48] fix tests f --- .../user_preferences_repository.dart | 3 - test/blocs/drives_cubit_test.dart | 8 ++- test/search/domain/bloc/search_bloc_test.dart | 1 + test/theme/theme_switcher_bloc_test.dart | 16 +++-- .../user_preferences_repository_test.dart | 58 ++++++++++++++++++- test/utils/link_generators_test.dart | 2 + 6 files changed, 77 insertions(+), 11 deletions(-) diff --git a/lib/user/repositories/user_preferences_repository.dart b/lib/user/repositories/user_preferences_repository.dart index 8211e03636..7398a3818e 100644 --- a/lib/user/repositories/user_preferences_repository.dart +++ b/lib/user/repositories/user_preferences_repository.dart @@ -138,7 +138,4 @@ class _UserPreferencesRepository implements UserPreferencesRepository { return ArDriveThemes.light; } } - - @override - UserPreferences get currentUserPreferences => _currentUserPreferences!; } 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/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..9e2d756178 100644 --- a/test/user/repositories/user_preferences_repository_test.dart +++ b/test/user/repositories/user_preferences_repository_test.dart @@ -6,6 +6,8 @@ import 'package:ardrive_ui/ardrive_ui.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 +17,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, ); }); @@ -33,15 +40,23 @@ void main() { final result = await repository.load(); - expect(result, const UserPreferences(currentTheme: ArDriveThemes.light)); + expect( + result, + const UserPreferences( + currentTheme: ArDriveThemes.light, lastSelectedDriveId: null)); }); test('should return saved theme from storage', () async { when(() => mockStore.getString('currentTheme')).thenReturn('dark'); + 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)); }); test('should save theme to storage', () async { @@ -54,5 +69,42 @@ 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'); + }); + + test('should return last selected drive id from storage', () async { + when(() => mockStore.getString('lastSelectedDriveId')) + .thenReturn('drive_id'); + final result = await repository.load(); + + expect( + result, + const UserPreferences( + currentTheme: ArDriveThemes.dark, + lastSelectedDriveId: 'drive_id')); + }); + + // test('should clean last selected drive id if user is not authenticated', + // () async { + // final repository = UserPreferencesRepository( + // store: mockStore, + // themeDetector: mockThemeDetector, + // auth: mockAuth, + // ); + + // when(() => mockStore.getString('lastSelectedDriveId')) + // .thenReturn('drive_id'); + // when(() => mockAuth.onAuthStateChanged()) + // .thenAnswer((_) => Stream.value(null)); + + // await repository.load(); + + // verify(() => mockStore.remove('lastSelectedDriveId')).called(1); + // }); }); } 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'; From e868e518ce57dbe82809a9f00d8426a525400b41 Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Tue, 15 Oct 2024 11:21:22 -0300 Subject: [PATCH 04/48] Update side_bar.dart --- lib/components/side_bar.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/components/side_bar.dart b/lib/components/side_bar.dart index f00438d977..204f9960d6 100644 --- a/lib/components/side_bar.dart +++ b/lib/components/side_bar.dart @@ -635,7 +635,7 @@ Future shareLogs({ } class _Accordion extends StatelessWidget { - const _Accordion({super.key, required this.state, required this.isMobile}); + const _Accordion({required this.state, required this.isMobile}); final DrivesLoadSuccess state; final bool isMobile; From 66a507b3da6baf7c74358e52d365094c218cca58 Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Wed, 16 Oct 2024 09:11:30 -0300 Subject: [PATCH 05/48] Update drive_share_dialog.dart --- lib/components/drive_share_dialog.dart | 57 +++++++++++++++++--------- 1 file changed, 37 insertions(+), 20 deletions(-) diff --git a/lib/components/drive_share_dialog.dart b/lib/components/drive_share_dialog.dart index 972bee30d2..0472f155f0 100644 --- a/lib/components/drive_share_dialog.dart +++ b/lib/components/drive_share_dialog.dart @@ -66,27 +66,44 @@ class DriveShareDialogState extends State { Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - Expanded( - child: ArDriveTextFieldNew( - 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: typography.paragraphLarge().copyWith( - decoration: TextDecoration.underline, + 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 + .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(), ), + ], + ), ), ), ], From 424b5c83b6506c3b124628fd2744053b0b01aa19 Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Thu, 17 Oct 2024 10:44:04 -0300 Subject: [PATCH 06/48] start work --- lib/blocs/upload/upload_cubit.dart | 128 ++++- lib/blocs/upload/upload_state.dart | 29 +- lib/components/upload_form.dart | 459 +++++++++++++----- .../ardrive_ui/lib/src/components/button.dart | 5 +- 4 files changed, 497 insertions(+), 124 deletions(-) diff --git a/lib/blocs/upload/upload_cubit.dart b/lib/blocs/upload/upload_cubit.dart index f995653243..c13880053d 100644 --- a/lib/blocs/upload/upload_cubit.dart +++ b/lib/blocs/upload/upload_cubit.dart @@ -105,6 +105,9 @@ class UploadCubit extends Cubit { List _manifestFiles = []; final List _selectedManifestFiles = []; + Map _arnsUndernamesLinkedToManifest = {}; + Map _arnsAntRecordsLinkedToManifest = {}; + UploadMethod? _manifestUploadMethod; bool _isManifestsUploadCancelled = false; @@ -112,19 +115,68 @@ class UploadCubit extends Cubit { void selectManifestFile(FileEntry file) { final readyState = state as UploadReady; + final manifestModel = UploadManifestModel( + name: file.name, + isCompleted: false, + freeThanksToTurbo: + file.size <= configService.config.allowedDataItemSizeForTurbo, + isUploading: false, + existingManifestFileId: file.id, + undername: _arnsUndernamesLinkedToManifest[file.id], + antRecord: _arnsAntRecordsLinkedToManifest[file.id], + ); + final newReadyState = readyState.copyWith( - selectedManifests: List.of(_selectedManifestFiles)..add(file)); + selectedManifests: List.of( + _selectedManifestFiles + .map( + (f) => UploadManifestModel( + name: f.name, + isCompleted: false, + freeThanksToTurbo: + f.size <= configService.config.allowedDataItemSizeForTurbo, + isUploading: false, + existingManifestFileId: f.id, + undername: _arnsUndernamesLinkedToManifest[f.id], + antRecord: _arnsAntRecordsLinkedToManifest[f.id], + ), + ) + .toList(), + )..add(manifestModel)); _selectedManifestFiles.add(file); emit(newReadyState); } + void linkManifestToUndername( + FileEntry file, ANTRecord antRecord, ARNSUndername undername) { + _arnsUndernamesLinkedToManifest[file.id] = undername; + _arnsAntRecordsLinkedToManifest[file.id] = antRecord; + } + + void unlinkManifestToUndername(FileEntry file) { + _arnsUndernamesLinkedToManifest.remove(file.id); + } + void unselectManifestFile(FileEntry file) { _selectedManifestFiles.remove(file); - emit((state as UploadReady) - .copyWith(selectedManifests: _selectedManifestFiles)); + emit((state as UploadReady).copyWith( + selectedManifests: _selectedManifestFiles + .map( + (f) => UploadManifestModel( + name: f.name, + isCompleted: false, + freeThanksToTurbo: + f.size <= configService.config.allowedDataItemSizeForTurbo, + isUploading: false, + existingManifestFileId: f.id, + undername: _arnsUndernamesLinkedToManifest[f.id], + antRecord: _arnsAntRecordsLinkedToManifest[f.id], + ), + ) + .toList())); } void setManifestUploadMethod( @@ -142,6 +194,8 @@ class UploadCubit extends Cubit { f.size <= configService.config.allowedDataItemSizeForTurbo, isUploading: false, existingManifestFileId: f.id, + undername: _arnsUndernamesLinkedToManifest[f.id], + antRecord: _arnsAntRecordsLinkedToManifest[f.id], ), ) .toList(); @@ -222,12 +276,28 @@ class UploadCubit extends Cubit { method: _manifestUploadMethod, ); - manifestModels[i] = - manifestModels[i].copyWith(isCompleted: true, isUploading: false); + if (manifestModels[i].undername != null) { + manifestModels[i] = manifestModels[i].copyWith( + isCompleted: false, isUploading: false, isAssigningUndername: true); + emit(UploadingManifests( + manifestFiles: manifestModels, + completedCount: ++completedCount, + )); + + await _arnsRepository.setUndernamesToFile( + undername: manifestModels[i].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, + completedCount: completedCount, )); } @@ -272,6 +342,7 @@ class UploadCubit extends Cubit { /// ArNS ANTRecord? _selectedAntRecord; + List _ants = []; ARNSUndername? _selectedUndername; /// Thumbnail upload @@ -403,10 +474,24 @@ class UploadCubit extends Cubit { loadingArNSNames: true, arnsCheckboxChecked: _showArnsNameSelectionCheckBoxValue, totalSize: await _getTotalSize(), - selectedManifests: _selectedManifestFiles, + selectedManifests: _selectedManifestFiles + .map( + (f) => UploadManifestModel( + name: f.name, + isCompleted: false, + freeThanksToTurbo: f.size <= + configService.config.allowedDataItemSizeForTurbo, + isUploading: false, + existingManifestFileId: f.id, + undername: _arnsUndernamesLinkedToManifest[f.id], + antRecord: _arnsAntRecordsLinkedToManifest[f.id], + ), + ) + .toList(), showSettings: showSettings, canShowSettings: showSettings, manifestFiles: _manifestFiles, + arnsRecords: _ants, ), ); @@ -445,9 +530,23 @@ class UploadCubit extends Cubit { showArnsNameSelection: false, arnsCheckboxChecked: _showArnsNameSelectionCheckBoxValue, totalSize: await _getTotalSize(), - selectedManifests: _selectedManifestFiles, + selectedManifests: _selectedManifestFiles + .map( + (f) => UploadManifestModel( + name: f.name, + isCompleted: false, + freeThanksToTurbo: f.size <= + configService.config.allowedDataItemSizeForTurbo, + isUploading: false, + existingManifestFileId: f.id, + undername: _arnsUndernamesLinkedToManifest[f.id], + antRecord: _arnsAntRecordsLinkedToManifest[f.id], + ), + ) + .toList(), showSettings: showSettings, manifestFiles: _manifestFiles, + arnsRecords: _ants, canShowSettings: showSettings, ), ); @@ -815,11 +914,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)); diff --git a/lib/blocs/upload/upload_state.dart b/lib/blocs/upload/upload_state.dart index 340c9c7a94..cc8e096bd9 100644 --- a/lib/blocs/upload/upload_state.dart +++ b/lib/blocs/upload/upload_state.dart @@ -107,10 +107,11 @@ class UploadReady extends UploadState { final bool loadingArNSNamesError; final bool arnsCheckboxChecked; final int totalSize; - final List selectedManifests; + final List selectedManifests; final List manifestFiles; final bool showSettings; final bool canShowSettings; + final List arnsRecords; final bool isArConnect; @@ -132,6 +133,7 @@ class UploadReady extends UploadState { required this.showSettings, required this.canShowSettings, required this.manifestFiles, + required this.arnsRecords, }); // copyWith @@ -151,9 +153,10 @@ class UploadReady extends UploadState { bool? loadingArNSNamesError, bool? arnsCheckboxChecked, int? totalSize, - List? selectedManifests, + List? selectedManifests, List? manifestFiles, bool? canShowSettings, + List? arnsRecords, }) { return UploadReady( loadingArNSNames: loadingArNSNames ?? this.loadingArNSNames, @@ -175,6 +178,7 @@ class UploadReady extends UploadState { showSettings: showSettings ?? this.showSettings, manifestFiles: manifestFiles ?? this.manifestFiles, canShowSettings: canShowSettings ?? this.canShowSettings, + arnsRecords: arnsRecords ?? this.arnsRecords, ); } @@ -304,7 +308,7 @@ class UploadingManifests extends UploadState { }); @override - List get props => [manifestFiles, completedCount]; + List get props => [UniqueKey()]; } class UploadWalletMismatch extends UploadState {} @@ -347,11 +351,13 @@ class UploadManifestSelectPaymentMethod extends UploadState { class UploadManifestModel extends Equatable { final String name; final bool isCompleted; + final bool isAssigningUndername; final bool freeThanksToTurbo; final bool isUploading; final String? existingManifestFileId; final IOFile? file; - + final ARNSUndername? undername; + final ANTRecord? antRecord; const UploadManifestModel({ required this.name, this.isCompleted = false, @@ -359,6 +365,9 @@ class UploadManifestModel extends Equatable { this.isUploading = false, this.existingManifestFileId, this.file, + this.undername, + this.antRecord, + this.isAssigningUndername = false, }); UploadManifestModel copyWith({ @@ -367,6 +376,9 @@ class UploadManifestModel extends Equatable { String? existingManifestFileId, bool? freeThanksToTurbo, IOFile? file, + ARNSUndername? undername, + ANTRecord? antRecord, + bool? isAssigningUndername, }) { return UploadManifestModel( name: name, @@ -376,6 +388,9 @@ class UploadManifestModel extends Equatable { existingManifestFileId ?? this.existingManifestFileId, freeThanksToTurbo: freeThanksToTurbo ?? this.freeThanksToTurbo, file: file ?? this.file, + undername: undername ?? this.undername, + antRecord: antRecord ?? this.antRecord, + isAssigningUndername: isAssigningUndername ?? this.isAssigningUndername, ); } @@ -385,7 +400,11 @@ class UploadManifestModel extends Equatable { isCompleted, isUploading, existingManifestFileId, - freeThanksToTurbo + freeThanksToTurbo, + isAssigningUndername, + antRecord, + undername, + file, ]; } diff --git a/lib/components/upload_form.dart b/lib/components/upload_form.dart index d5783dc195..15e8f48020 100644 --- a/lib/components/upload_form.dart +++ b/lib/components/upload_form.dart @@ -330,31 +330,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].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), - ], ], ); }, @@ -1012,7 +1024,8 @@ class _UploadReadyModalBaseState extends State { {bool scrollable = true}) { return Padding( padding: const EdgeInsets.only(bottom: 42.0), - child: ListView.builder( + child: ListView.separated( + separatorBuilder: (context, index) => const SizedBox(height: 4), physics: scrollable ? const ScrollPhysics() : const NeverScrollableScrollPhysics(), @@ -1020,61 +1033,138 @@ class _UploadReadyModalBaseState extends State { 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, + return _ManifestOptionTile( + file: file, + state: state, + ); + }, + ), + ); + } +} + +class _ManifestOptionTile extends StatefulWidget { + final UploadReady state; + final FileEntry file; + + const _ManifestOptionTile({ + required this.state, + required this.file, + }); + + @override + State<_ManifestOptionTile> createState() => __ManifestOptionTileState(); +} + +class __ManifestOptionTileState extends State<_ManifestOptionTile> { + bool _isExpanded = false; + + @override + Widget build(BuildContext context) { + final hiddenColor = + ArDriveTheme.of(context).themeData.colors.themeFgDisabled; + final typography = ArDriveTypographyNew.of(context); + final colorTokens = ArDriveTheme.of(context).themeData.colorTokens; + + return AnimatedContainer( + duration: const Duration(milliseconds: 300), + decoration: BoxDecoration( + color: colorTokens.containerL2, + borderRadius: BorderRadius.circular(5), + ), + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), + height: _isExpanded ? 130 : 50, + child: GestureDetector( + onTap: () { + setState(() { + _isExpanded = !_isExpanded; + }); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + mainAxisSize: MainAxisSize.max, + children: [ + Expanded( + child: Row( + children: [ + Flexible( + flex: 2, + child: Row( + children: [ + ArDriveIcons.manifest( + size: 16, + color: + widget.file.isHidden ? hiddenColor : null), + const SizedBox(width: 8), + Text( + widget.file.name, + style: typography.paragraphNormal( + color: + widget.file.isHidden ? hiddenColor : null, + ), + ), + if (widget.file.isHidden) ...[ + const SizedBox(width: 8), + Text('(hidden)', + style: typography.paragraphNormal( + color: hiddenColor, + )) + ] + ], + ), ), - ), - if (file.isHidden) ...[ - const SizedBox(width: 8), - Text('(hidden)', - style: typography.paragraphNormal( - color: hiddenColor, - )) - ] - ], + Flexible( + flex: 1, + child: ArDriveCheckBox( + checked: widget.state.selectedManifests + .contains(widget.file), + onChange: (value) { + if (value) { + context + .read() + .selectManifestFile(widget.file); + } else { + context + .read() + .unselectManifestFile(widget.file); + } + }, + ), + ), + ], + ), ), - ), - 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]); - } + ArDriveButtonNew( + text: 'Add ArNS', + typography: typography, + isDisabled: + !widget.state.selectedManifests.contains(widget.file), + fontStyle: typography.paragraphSmall(), + variant: ButtonVariant.primary, + maxWidth: 80, + maxHeight: 30, + onPressed: () { + setState(() { + _isExpanded = !_isExpanded; + }); }, - ), + ) + // TODO: Add back when we have the right UI for it + ], + ), + if (_isExpanded) ...[ + const SizedBox(height: 8), + Expanded( + flex: 1, + child: AntSelector(fileEntry: widget.file), ), - // TODO: Add back when we have the right UI for it - // const Expanded( - // flex: 1, - // child: AntSelector(), - // ), ], - ); - }, + ], + ), ), ); } @@ -2111,16 +2201,29 @@ class _UploadReviewWithLicenseWidget extends StatelessWidget { shrinkWrap: true, children: [ ...state.readyState.selectedManifests.map( - (e) => Row( + (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), + Text( + e.name, + style: typography.paragraphNormal( + fontWeight: ArFontWeight.semiBold, + ), + ), + ], ), + if (e.undername != null) ...[ + const SizedBox(height: 4), + Text( + 'ArNS Name: ${getLiteralARNSRecordName(e.undername!)}', + style: typography.paragraphNormal( + fontWeight: ArFontWeight.semiBold, + ), + ), + ], ], ), ), @@ -2913,35 +3016,119 @@ List getModalActions( } class AntSelector extends StatefulWidget { - const AntSelector({super.key}); + const AntSelector({super.key, required this.fileEntry}); + + final FileEntry fileEntry; @override State createState() => _AntSelectorState(); } class _AntSelectorState extends State { - final List ants = ['Ant 1', 'Ant 2', 'Ant 3']; - String _selectedAnt = 'Ant 1'; + ANTRecord? _selectedAnt; + ARNSUndername? _selectedUndername; + + List _arnsUndernames = []; + bool _loadingUndernames = false; + + loadARNSUndernames( + ANTRecord ant, + ) async { + setState(() { + _loadingUndernames = true; + }); + + _arnsUndernames = await context.read().getARNSUndernames(ant); + + setState(() { + _loadingUndernames = false; + }); + } @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(), - ], - ), - ), + final colorTokens = ArDriveTheme.of(context).themeData.colorTokens; + return BlocBuilder( + builder: (context, state) { + if (state is UploadReady) { + 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, + items: state.arnsRecords + .map((ant) => _buildDropdownItem(context, ant)) + .toList(), + child: ArDriveClickArea( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Row( + children: [ + _buildSelectedItem(context), + ], + ), + ArDriveIcons.chevronDown(), + ], + ), + ), + ), + ), + if (_loadingUndernames) const CircularProgressIndicator(), + if (!_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( + 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(), + ], + ), + ), + ), + ), + ), + ], + ); + } + return const SizedBox(); + }, ); } @@ -2949,32 +3136,45 @@ class _AntSelectorState extends State { final typography = ArDriveTypographyNew.of(context); return Text( - _selectedAnt, - style: typography.paragraphLarge( + _selectedAnt?.domain ?? 'Choose ArNS name', + style: typography.paragraphSmall( fontWeight: ArFontWeight.semiBold, ), ); } - ArDriveDropdownItem _buildDropdownItem(BuildContext context, String ant) { + 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: 164, + width: 235, 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, + Flexible( + child: Text( + ant.domain, + style: typography.paragraphSmall( + fontWeight: ArFontWeight.semiBold, + ), ), ), - if (ant == 'Ant 1') + if (ant.domain == _selectedAnt?.domain) ArDriveIcons.checkmark( size: 16, ) @@ -2985,6 +3185,49 @@ class _AntSelectorState extends State { onClick: () { setState(() { _selectedAnt = ant; + + _arnsUndernames = []; + 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; + + context.read().linkManifestToUndername( + widget.fileEntry, _selectedAnt!, _selectedUndername!); }); }, ); 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; From 04ea46d85de288a3c706914157e94a20ef7087cf Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Mon, 21 Oct 2024 16:06:38 -0300 Subject: [PATCH 07/48] always show shared drives --- lib/app_shell.dart | 1 - lib/blocs/drive_detail/drive_detail_cubit.dart | 2 -- lib/components/side_bar.dart | 11 +++-------- 3 files changed, 3 insertions(+), 11 deletions(-) diff --git a/lib/app_shell.dart b/lib/app_shell.dart index a832137243..8ef7c90ca7 100644 --- a/lib/app_shell.dart +++ b/lib/app_shell.dart @@ -82,7 +82,6 @@ class AppShellState extends State { driveId: drivesState.selectedDriveId, )); } - if (syncState is SyncInProgress) {} } }, builder: (context, syncState) { diff --git a/lib/blocs/drive_detail/drive_detail_cubit.dart b/lib/blocs/drive_detail/drive_detail_cubit.dart index c7416765ad..f7db166aaa 100644 --- a/lib/blocs/drive_detail/drive_detail_cubit.dart +++ b/lib/blocs/drive_detail/drive_detail_cubit.dart @@ -346,8 +346,6 @@ class DriveDetailCubit extends Cubit { _selectedItem = item; - debugPrint('selectedItem: ${_selectedItem?.id}'); - int? selectedPage; if (openSelectedPage) { diff --git a/lib/components/side_bar.dart b/lib/components/side_bar.dart index 204f9960d6..55908179b1 100644 --- a/lib/components/side_bar.dart +++ b/lib/components/side_bar.dart @@ -272,8 +272,6 @@ class _AppSideBarState extends State { ); } - // Widget _buildAccordion(DrivesLoadSuccess state, bool isMobile) {} - Widget _buildSideBarBottom() { return _isExpanded ? Padding( @@ -725,12 +723,9 @@ class _Accordion extends StatelessWidget { fontWeight: ArFontWeight.semiBold, ), ), - state.sharedDrives - .where((element) { - final isHidden = hideState is HiddingItems; - return (isHidden ? !element.isHidden : true); - }) + /// Shared drives are always visible + state.sharedDrives .map( (d) => DriveListTile( hasAlert: state.drivesWithAlerts.contains(d.id), @@ -739,7 +734,7 @@ class _Accordion extends StatelessWidget { context.read().selectDrive(d.id); }, isSelected: state.selectedDriveId == d.id, - isHidden: d.isHidden, + isHidden: false, ), ) .toList(), From 4d44634334362367d793f875ff51814ee1d3713e Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Mon, 21 Oct 2024 16:15:03 -0300 Subject: [PATCH 08/48] chore: remove commented code and prints --- lib/pages/app_router_delegate.dart | 1 - .../src/components/data_table/data_table.dart | 9 --------- .../user_preferences_repository_test.dart | 18 ------------------ 3 files changed, 28 deletions(-) diff --git a/lib/pages/app_router_delegate.dart b/lib/pages/app_router_delegate.dart index 6933ee3ad7..e621163a3a 100644 --- a/lib/pages/app_router_delegate.dart +++ b/lib/pages/app_router_delegate.dart @@ -184,7 +184,6 @@ class AppRouterDelegate extends RouterDelegate } }, child: BlocProvider( - // key: ValueKey(driveId), create: (context) => DriveDetailCubit( driveRepository: DriveRepository( driveDao: context.read(), diff --git a/packages/ardrive_ui/lib/src/components/data_table/data_table.dart b/packages/ardrive_ui/lib/src/components/data_table/data_table.dart index f1533e3f28..606a58d50a 100644 --- a/packages/ardrive_ui/lib/src/components/data_table/data_table.dart +++ b/packages/ardrive_ui/lib/src/components/data_table/data_table.dart @@ -273,9 +273,6 @@ class _ArDriveDataTableState _sortRows(_sortedColumn!); } } - - debugPrint( - 'selectedItem on didUpdateWidget: ${_selectedItem.toString()}'); } } @@ -828,12 +825,6 @@ class _ArDriveDataTableState ) { final multiselect = getMultiSelectBox(); - final isSelected = _selectedItem == row; - - debugPrint('isSelected: $isSelected'); - debugPrint('row: ${row.toString()}'); - debugPrint('selectedItem: ${_selectedItem.toString()}'); - return GestureDetector( onTap: () { if (_isMultiSelecting) { diff --git a/test/user/repositories/user_preferences_repository_test.dart b/test/user/repositories/user_preferences_repository_test.dart index 9e2d756178..f4cef92718 100644 --- a/test/user/repositories/user_preferences_repository_test.dart +++ b/test/user/repositories/user_preferences_repository_test.dart @@ -88,23 +88,5 @@ void main() { currentTheme: ArDriveThemes.dark, lastSelectedDriveId: 'drive_id')); }); - - // test('should clean last selected drive id if user is not authenticated', - // () async { - // final repository = UserPreferencesRepository( - // store: mockStore, - // themeDetector: mockThemeDetector, - // auth: mockAuth, - // ); - - // when(() => mockStore.getString('lastSelectedDriveId')) - // .thenReturn('drive_id'); - // when(() => mockAuth.onAuthStateChanged()) - // .thenAnswer((_) => Stream.value(null)); - - // await repository.load(); - - // verify(() => mockStore.remove('lastSelectedDriveId')).called(1); - // }); }); } From 307d9c1d49775e2a32608fd466da38f756de62aa Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Tue, 22 Oct 2024 08:43:42 -0300 Subject: [PATCH 09/48] test(user prefs/hide) - implement unit tests for the global_hide_bloc and user_prefs repository. --- lib/blocs/hide/global_hide_bloc.dart | 6 +- lib/blocs/hide/hide_bloc.dart | 1 + lib/models/daos/drive_dao/drive_dao.dart | 4 + .../user_preferences_repository.dart | 62 ++++--- test/blocs/hide/global_hide_bloc_test.dart | 148 +++++++++++++++++ .../user_preferences_repository_test.dart | 153 +++++++++++++++++- 6 files changed, 349 insertions(+), 25 deletions(-) create mode 100644 test/blocs/hide/global_hide_bloc_test.dart diff --git a/lib/blocs/hide/global_hide_bloc.dart b/lib/blocs/hide/global_hide_bloc.dart index c805b2944f..43fc70955e 100644 --- a/lib/blocs/hide/global_hide_bloc.dart +++ b/lib/blocs/hide/global_hide_bloc.dart @@ -27,12 +27,12 @@ class GlobalHideBloc extends Bloc { on((event, emit) async { if (event is ShowItems) { emit(ShowingHiddenItems(userHasHiddenDrive: event.userHasHiddenItems)); - _userPreferencesRepository.saveShowHiddenFiles(true); + await _userPreferencesRepository.saveShowHiddenFiles(true); } else if (event is HideItems) { emit(HiddingItems(userHasHiddenDrive: event.userHasHiddenItems)); - _userPreferencesRepository.saveShowHiddenFiles(false); + await _userPreferencesRepository.saveShowHiddenFiles(false); } else if (event is RefreshOptions) { - final hasHiddenItems = await _driveDao.hasHiddenItems().getSingle(); + final hasHiddenItems = await _driveDao.userHasHiddenItems(); emit(state.copyWith(userHasHiddenDrive: hasHiddenItems)); } }); diff --git a/lib/blocs/hide/hide_bloc.dart b/lib/blocs/hide/hide_bloc.dart index 0c656f4d5c..b37b171fff 100644 --- a/lib/blocs/hide/hide_bloc.dart +++ b/lib/blocs/hide/hide_bloc.dart @@ -274,6 +274,7 @@ class HideBloc extends Bloc { isHidden ? RevisionAction.hide : RevisionAction.unhide, )); } + }); } diff --git a/lib/models/daos/drive_dao/drive_dao.dart b/lib/models/daos/drive_dao/drive_dao.dart index 8c3a5e0b14..d7f9229b95 100644 --- a/lib/models/daos/drive_dao/drive_dao.dart +++ b/lib/models/daos/drive_dao/drive_dao.dart @@ -736,6 +736,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/user/repositories/user_preferences_repository.dart b/lib/user/repositories/user_preferences_repository.dart index 7398a3818e..8a186c8570 100644 --- a/lib/user/repositories/user_preferences_repository.dart +++ b/lib/user/repositories/user_preferences_repository.dart @@ -80,39 +80,41 @@ class _UserPreferencesRepository implements UserPreferencesRepository { @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 _getStore()).putString( - 'lastSelectedDriveId', - driveId, + await _updatePreference( + key: 'lastSelectedDriveId', + value: driveId, + updateFunction: (value) => + _currentUserPreferences!.copyWith(lastSelectedDriveId: value), ); } @override Future saveShowHiddenFiles(bool showHiddenFiles) async { - (await _getStore()).putBool( - 'showHiddenFiles', - showHiddenFiles, + await _updatePreference( + key: 'showHiddenFiles', + value: showHiddenFiles, + updateFunction: (value) => + _currentUserPreferences!.copyWith(showHiddenFiles: value), ); } @override Future saveUserHasHiddenItem(bool userHasHiddenDrive) async { - _currentUserPreferences = _currentUserPreferences!.copyWith( - userHasHiddenDrive: userHasHiddenDrive, - ); - - _userPreferencesController.sink.add(_currentUserPreferences!); - - (await _getStore()).putBool( - 'userHasHiddenDrive', - userHasHiddenDrive, + await _updatePreference( + key: 'userHasHiddenDrive', + value: userHasHiddenDrive, + updateFunction: (value) => + _currentUserPreferences!.copyWith(userHasHiddenDrive: value), ); } @@ -125,6 +127,12 @@ class _UserPreferencesRepository implements UserPreferencesRepository { @override Future clearLastSelectedDriveId() async { (await _getStore()).remove('lastSelectedDriveId'); + + _currentUserPreferences = _currentUserPreferences!.copyWith( + lastSelectedDriveId: null, + ); + + _userPreferencesController.sink.add(_currentUserPreferences!); } // parse theme from string to ArDriveThemes @@ -138,4 +146,22 @@ class _UserPreferencesRepository implements UserPreferencesRepository { return ArDriveThemes.light; } } + + Future _updatePreference({ + required String key, + required T value, + required UserPreferences Function(T) updateFunction, + }) async { + _currentUserPreferences = updateFunction(value); + _userPreferencesController.sink.add(_currentUserPreferences!); + + 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/test/blocs/hide/global_hide_bloc_test.dart b/test/blocs/hide/global_hide_bloc_test.dart new file mode 100644 index 0000000000..d108f1a0c7 --- /dev/null +++ b/test/blocs/hide/global_hide_bloc_test.dart @@ -0,0 +1,148 @@ +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.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/user/repositories/user_preferences_repository_test.dart b/test/user/repositories/user_preferences_repository_test.dart index f4cef92718..657031b88d 100644 --- a/test/user/repositories/user_preferences_repository_test.dart +++ b/test/user/repositories/user_preferences_repository_test.dart @@ -3,6 +3,7 @@ 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'; @@ -37,17 +38,25 @@ 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, lastSelectedDriveId: null)); + 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())); @@ -56,7 +65,11 @@ void main() { expect( result, const UserPreferences( - currentTheme: ArDriveThemes.dark, lastSelectedDriveId: null)); + currentTheme: ArDriveThemes.dark, + lastSelectedDriveId: null, + showHiddenFiles: false, + userHasHiddenDrive: false, + )); }); test('should save theme to storage', () async { @@ -75,18 +88,150 @@ void main() { .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')); + 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); + + await repository.clearLastSelectedDriveId(); + + 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); + var next = await queue.next; + var expected = initialPreferences.copyWith( + currentTheme: ArDriveThemes.dark, + ); + + expect( + next, + equals(expected), + ); + await repository.saveLastSelectedDriveId('new_drive_id'); + next = await queue.next; + expected = expected.copyWith(lastSelectedDriveId: 'new_drive_id'); + + expect( + next, + equals(expected), + ); + await repository.saveShowHiddenFiles(true); + next = await queue.next; + expected = expected.copyWith(showHiddenFiles: true); + + expect( + next, + equals(expected), + ); + await repository.saveUserHasHiddenItem(true); + next = await queue.next; + expected = expected.copyWith(userHasHiddenDrive: true); + + expect( + next, + equals(expected), + ); + + 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(); + }, + ); }); } From 630ca0c3646e593d44464d60ab5f388d739d9a45 Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Tue, 22 Oct 2024 08:54:20 -0300 Subject: [PATCH 10/48] fix(user prefs) - fix infinite updates reacting to changes on user prefs repo --- .../user_preferences_repository.dart | 1 - .../user_preferences_repository_test.dart | 30 ------------------- 2 files changed, 31 deletions(-) diff --git a/lib/user/repositories/user_preferences_repository.dart b/lib/user/repositories/user_preferences_repository.dart index 8a186c8570..c956c4e3f9 100644 --- a/lib/user/repositories/user_preferences_repository.dart +++ b/lib/user/repositories/user_preferences_repository.dart @@ -153,7 +153,6 @@ class _UserPreferencesRepository implements UserPreferencesRepository { required UserPreferences Function(T) updateFunction, }) async { _currentUserPreferences = updateFunction(value); - _userPreferencesController.sink.add(_currentUserPreferences!); final store = await _getStore(); if (value is String) { diff --git a/test/user/repositories/user_preferences_repository_test.dart b/test/user/repositories/user_preferences_repository_test.dart index 657031b88d..793a370b05 100644 --- a/test/user/repositories/user_preferences_repository_test.dart +++ b/test/user/repositories/user_preferences_repository_test.dart @@ -183,39 +183,9 @@ void main() { // Trigger preference changes await repository.saveTheme(ArDriveThemes.dark); - var next = await queue.next; - var expected = initialPreferences.copyWith( - currentTheme: ArDriveThemes.dark, - ); - - expect( - next, - equals(expected), - ); await repository.saveLastSelectedDriveId('new_drive_id'); - next = await queue.next; - expected = expected.copyWith(lastSelectedDriveId: 'new_drive_id'); - - expect( - next, - equals(expected), - ); await repository.saveShowHiddenFiles(true); - next = await queue.next; - expected = expected.copyWith(showHiddenFiles: true); - - expect( - next, - equals(expected), - ); await repository.saveUserHasHiddenItem(true); - next = await queue.next; - expected = expected.copyWith(userHasHiddenDrive: true); - - expect( - next, - equals(expected), - ); await repository.load(); From 7dae599594acad6c39ed6e8510fac84e4c667e0a Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Tue, 22 Oct 2024 09:29:09 -0300 Subject: [PATCH 11/48] fix hide icon and attach drives --- lib/blocs/drives/drives_cubit.dart | 32 +++++++++++++--------------- lib/blocs/hide/global_hide_bloc.dart | 2 ++ lib/pages/app_router_delegate.dart | 2 +- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/lib/blocs/drives/drives_cubit.dart b/lib/blocs/drives/drives_cubit.dart index e72ec397b1..dad88a7a95 100644 --- a/lib/blocs/drives/drives_cubit.dart +++ b/lib/blocs/drives/drives_cubit.dart @@ -7,10 +7,8 @@ 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/logger.dart'; import 'package:ardrive/utils/user_utils.dart'; import 'package:ardrive_utils/ardrive_utils.dart'; -import 'package:collection/collection.dart'; import 'package:drift/drift.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -72,27 +70,27 @@ class DrivesCubit extends Cubit { String? selectedDriveId; - if (state is DrivesLoadSuccess && state.selectedDriveId != null) { + if (state is DrivesLoadSuccess) { selectedDriveId = state.selectedDriveId; - } else { - final userPreferences = await _userPreferencesRepository.load(); + } - final userHasHiddenDrive = drives.any((d) => d.isHidden); - logger.d('User has hidden drive: $userHasHiddenDrive'); + if (selectedDriveId == null) { + if (initialSelectedDriveId != null && + initialSelectedDriveId!.isNotEmpty) { + selectedDriveId = initialSelectedDriveId; + } else { + final userPreferences = await _userPreferencesRepository.load(); - await _userPreferencesRepository - .saveUserHasHiddenItem(userHasHiddenDrive); + final userHasHiddenDrive = drives.any((d) => d.isHidden); + await _userPreferencesRepository + .saveUserHasHiddenItem(userHasHiddenDrive); - if (userPreferences.lastSelectedDriveId != null) { - final lastSelectedDriveId = userPreferences.lastSelectedDriveId; + selectedDriveId = userPreferences.lastSelectedDriveId; - if (drives.firstWhereOrNull((d) => d.id == lastSelectedDriveId) != - null) { - selectedDriveId = lastSelectedDriveId; + if (selectedDriveId == null || + !drives.any((d) => d.id == selectedDriveId)) { + selectedDriveId = drives.isNotEmpty ? drives.first.id : null; } - } else { - selectedDriveId = initialSelectedDriveId ?? - (drives.isNotEmpty ? drives.first.id : null); } } diff --git a/lib/blocs/hide/global_hide_bloc.dart b/lib/blocs/hide/global_hide_bloc.dart index 43fc70955e..fc820e0dff 100644 --- a/lib/blocs/hide/global_hide_bloc.dart +++ b/lib/blocs/hide/global_hide_bloc.dart @@ -24,6 +24,8 @@ class GlobalHideBloc extends Bloc { } }); + _userPreferencesRepository.load(); + on((event, emit) async { if (event is ShowItems) { emit(ShowingHiddenItems(userHasHiddenDrive: event.userHasHiddenItems)); diff --git a/lib/pages/app_router_delegate.dart b/lib/pages/app_router_delegate.dart index e621163a3a..ad9c922127 100644 --- a/lib/pages/app_router_delegate.dart +++ b/lib/pages/app_router_delegate.dart @@ -168,7 +168,7 @@ class AppRouterDelegate extends RouterDelegate shell = const LoginPage(gettingStarted: true); } else if (state is ProfileLoggedIn || anonymouslyShowDriveDetail) { - driveId = driveId ?? rootPath; + driveId = driveId ?? rootPath; shell = BlocListener( listener: (context, state) { From fdcd613df11b2164c52a27ca53b1211dd1d60a6c Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Tue, 22 Oct 2024 09:34:43 -0300 Subject: [PATCH 12/48] Update hide_dialog.dart --- lib/components/hide_dialog.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/components/hide_dialog.dart b/lib/components/hide_dialog.dart index 2bb2b1430f..13177abfa4 100644 --- a/lib/components/hide_dialog.dart +++ b/lib/components/hide_dialog.dart @@ -88,7 +88,7 @@ class HideDialog extends StatelessWidget { } }, builder: (context, state) { - return ArDriveStandardModal( + return ArDriveStandardModalNew( title: _buildTitle(context, state), content: _buildContent(context, state), actions: _buildActions(context, state), From d3f0cb6ef90f781b2df22673db6573585f9b6d0e Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Tue, 22 Oct 2024 09:35:21 -0300 Subject: [PATCH 13/48] Update drive_explorer_item_tile.dart --- lib/pages/drive_detail/components/drive_explorer_item_tile.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/drive_detail/components/drive_explorer_item_tile.dart b/lib/pages/drive_detail/components/drive_explorer_item_tile.dart index eb02fdb08d..8e9a4c50e8 100644 --- a/lib/pages/drive_detail/components/drive_explorer_item_tile.dart +++ b/lib/pages/drive_detail/components/drive_explorer_item_tile.dart @@ -533,7 +533,7 @@ class _DriveExplorerItemTileTrailingState }, content: _buildItem( appLocalizationsOf(context).preview, - ArDriveIcons.eyeOpen( + ArDriveIcons.newWindow( size: defaultIconSize, ), ), From e5105a010cc10be41744852ba494473864b88297 Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Tue, 22 Oct 2024 09:38:29 -0300 Subject: [PATCH 14/48] Update sync_repository.dart --- lib/sync/domain/repositories/sync_repository.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/sync/domain/repositories/sync_repository.dart b/lib/sync/domain/repositories/sync_repository.dart index 30cbfb7dbe..5a8e2c22b8 100644 --- a/lib/sync/domain/repositories/sync_repository.dart +++ b/lib/sync/domain/repositories/sync_repository.dart @@ -272,7 +272,7 @@ class _SyncRepository implements SyncRepository { .then((value) => _arnsRepository.saveAllFilesWithAssignedNames()); final hasHiddenItems = await _driveDao.hasHiddenItems().getSingle(); await _userPreferencesRepository.saveUserHasHiddenItem(hasHiddenItems); - + await _userPreferencesRepository.load(); await Future.wait( [ _updateTransactionStatuses( From 9f8b2f09837d3ce6e5a91418fb3585df5c4d4ace Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Tue, 22 Oct 2024 09:42:51 -0300 Subject: [PATCH 15/48] remove feedback survey modal --- lib/components/create_manifest_form.dart | 3 --- lib/components/upload_form.dart | 2 -- 2 files changed, 5 deletions(-) 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/upload_form.dart b/lib/components/upload_form.dart index d5783dc195..1f1016db4c 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'; @@ -218,7 +217,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(); } From 87ebc4ed8b0ab21c141b74f2d6925be2f5fc020b Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Tue, 22 Oct 2024 11:11:27 -0300 Subject: [PATCH 16/48] fix sync drives with hidden property --- lib/models/daos/drive_dao/drive_dao.dart | 1 + lib/models/drive_revision.dart | 5 ++++- lib/sync/domain/repositories/sync_repository.dart | 3 +++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/lib/models/daos/drive_dao/drive_dao.dart b/lib/models/daos/drive_dao/drive_dao.dart index d7f9229b95..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) { diff --git a/lib/models/drive_revision.dart b/lib/models/drive_revision.dart index 2a1b177e65..8f6e23d931 100644 --- a/lib/models/drive_revision.dart +++ b/lib/models/drive_revision.dart @@ -36,7 +36,7 @@ extension DriveEntityExtensions on DriveEntity { /// This requires a `performedAction` to be specified. DriveRevisionsCompanion toRevisionCompanion( {required String performedAction}) => - DriveRevisionsCompanion.insert( + DriveRevisionsCompanion.insert( driveId: id!, ownerAddress: ownerAddress, rootFolderId: rootFolderId!, @@ -58,6 +58,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/sync/domain/repositories/sync_repository.dart b/lib/sync/domain/repositories/sync_repository.dart index 5a8e2c22b8..4874e3445e 100644 --- a/lib/sync/domain/repositories/sync_repository.dart +++ b/lib/sync/domain/repositories/sync_repository.dart @@ -872,6 +872,7 @@ class _SyncRepository implements SyncRepository { id: Value(drive.id), lastBlockHeight: Value(currentBlockHeight), syncCursor: const Value(null), + isHidden: Value(drive.isHidden), )); } @@ -966,9 +967,11 @@ class _SyncRepository implements SyncRepository { final revisionPerformedAction = entity.getPerformedRevisionAction(latestRevision); + if (revisionPerformedAction == null) { continue; } + final revision = entity.toRevisionCompanion(performedAction: revisionPerformedAction); From 3b522088a3f24fbb029790c147ce9212cce11ba2 Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Tue, 22 Oct 2024 11:15:30 -0300 Subject: [PATCH 17/48] clear all configs when logging out --- lib/user/repositories/user_preferences_repository.dart | 10 +++++++--- .../repositories/user_preferences_repository_test.dart | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/user/repositories/user_preferences_repository.dart b/lib/user/repositories/user_preferences_repository.dart index c956c4e3f9..00a89e8429 100644 --- a/lib/user/repositories/user_preferences_repository.dart +++ b/lib/user/repositories/user_preferences_repository.dart @@ -12,7 +12,7 @@ abstract class UserPreferencesRepository { Future saveTheme(ArDriveThemes theme); Future saveLastSelectedDriveId(String driveId); Future saveShowHiddenFiles(bool showHiddenFiles); - Future clearLastSelectedDriveId(); + Future clear(); Future saveUserHasHiddenItem(bool userHasHiddenDrive); factory UserPreferencesRepository({ @@ -43,7 +43,7 @@ class _UserPreferencesRepository implements UserPreferencesRepository { super() { _auth.onAuthStateChanged().listen((user) { if (user == null) { - clearLastSelectedDriveId(); + clear(); } }); } @@ -125,11 +125,15 @@ class _UserPreferencesRepository implements UserPreferencesRepository { } @override - Future clearLastSelectedDriveId() async { + 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!); diff --git a/test/user/repositories/user_preferences_repository_test.dart b/test/user/repositories/user_preferences_repository_test.dart index 793a370b05..53d9e9ea83 100644 --- a/test/user/repositories/user_preferences_repository_test.dart +++ b/test/user/repositories/user_preferences_repository_test.dart @@ -134,7 +134,7 @@ void main() { when(() => mockStore.remove('lastSelectedDriveId')) .thenAnswer((_) async => true); - await repository.clearLastSelectedDriveId(); + await repository.clear(); verify(() => mockStore.remove('lastSelectedDriveId')).called(1); }); From abb527521da4baba17bbdc0c7e01c97394ce0f79 Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Tue, 22 Oct 2024 11:18:23 -0300 Subject: [PATCH 18/48] Update global_hide_bloc_test.dart fix tests --- test/blocs/hide/global_hide_bloc_test.dart | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/blocs/hide/global_hide_bloc_test.dart b/test/blocs/hide/global_hide_bloc_test.dart index d108f1a0c7..5231568d4a 100644 --- a/test/blocs/hide/global_hide_bloc_test.dart +++ b/test/blocs/hide/global_hide_bloc_test.dart @@ -19,6 +19,15 @@ void main() { 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(), From d84d6d9fe25ef95b7277f150c3a0484646a13595 Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Tue, 22 Oct 2024 11:44:38 -0300 Subject: [PATCH 19/48] Update user_preferences_repository_test.dart --- test/user/repositories/user_preferences_repository_test.dart | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/user/repositories/user_preferences_repository_test.dart b/test/user/repositories/user_preferences_repository_test.dart index 53d9e9ea83..55ae960d6c 100644 --- a/test/user/repositories/user_preferences_repository_test.dart +++ b/test/user/repositories/user_preferences_repository_test.dart @@ -133,7 +133,10 @@ void main() { 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); From 48b69469d1909e7cec068852607b57ad36cf3e31 Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Tue, 22 Oct 2024 14:33:29 -0300 Subject: [PATCH 20/48] refactors(hide drive) - refactor timestamp and action variables - remove the option to toggle hide state on the drive explorer (we don't use it anymore) - refactor where uses the drive detail state to know if we're showing hidden images --- .../drive_detail/drive_detail_cubit.dart | 47 ++++++------- .../drive_detail/drive_detail_state.dart | 6 -- .../fs_entry_move/fs_entry_move_bloc.dart | 13 ++-- .../fs_entry_move/fs_entry_move_event.dart | 8 ++- lib/blocs/hide/hide_bloc.dart | 24 ++++--- lib/components/app_top_bar.dart | 38 +++++++---- lib/components/fs_entry_move_form.dart | 13 ++-- lib/pages/drive_detail/drive_detail_page.dart | 68 +++++-------------- test/blocs/fs_entry_move_bloc_test.dart | 3 +- 9 files changed, 96 insertions(+), 124 deletions(-) diff --git a/lib/blocs/drive_detail/drive_detail_cubit.dart b/lib/blocs/drive_detail/drive_detail_cubit.dart index f7db166aaa..d0003bef0f 100644 --- a/lib/blocs/drive_detail/drive_detail_cubit.dart +++ b/lib/blocs/drive_detail/drive_detail_cubit.dart @@ -49,8 +49,6 @@ class DriveDetailCubit extends Cubit { bool _refreshSelectedItem = false; - bool _showHiddenFiles = false; - DriveDetailCubit({ required String driveId, String? initialFolderId, @@ -114,12 +112,6 @@ class DriveDetailCubit extends Cubit { openFolder(folderId: drive.rootFolderId); } - void toggleHiddenFiles() { - _showHiddenFiles = !_showHiddenFiles; - - refreshDriveDataTable(); - } - Future openFolder({ String? folderId, String? otherDriveId, @@ -240,7 +232,6 @@ class DriveDetailCubit extends Cubit { rowsPerPage: availableRowsPerPage.first, availableRowsPerPage: availableRowsPerPage, currentFolderContents: currentFolderContents, - isShowingHiddenFiles: _showHiddenFiles, pathSegments: pathSegments, driveIsEmpty: folderContents.files.isEmpty && folderContents.subfolders.isEmpty, @@ -267,7 +258,6 @@ class DriveDetailCubit extends Cubit { multiselect: false, currentFolderContents: currentFolderContents, columnVisibility: columnsVisibility, - isShowingHiddenFiles: _showHiddenFiles, showSelectedItemDetails: _selectedItem != null, ), ); @@ -474,37 +464,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, ); @@ -512,15 +509,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) { @@ -529,7 +524,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 c9c56d9b19..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, 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/hide_bloc.dart b/lib/blocs/hide/hide_bloc.dart index b37b171fff..966f946693 100644 --- a/lib/blocs/hide/hide_bloc.dart +++ b/lib/blocs/hide/hide_bloc.dart @@ -157,11 +157,12 @@ class HideBloc extends Bloc { Drive? newDriveEntry; FileEntry? newFileEntry; FolderEntry? newFolderEntry; + final timestamp = DateTime.now(); if (currentEntry is Drive) { newDriveEntry = currentEntry.copyWith( isHidden: isHidden, - lastUpdated: DateTime.now(), + lastUpdated: timestamp, ); newEntryEntity = newDriveEntry.asEntity(); @@ -214,12 +215,12 @@ class HideBloc extends Bloc { if (entryIsFile) { newFileEntry = currentEntry.copyWith( isHidden: isHidden, - lastUpdated: DateTime.now(), + lastUpdated: timestamp, ); } else if (entryIsFolder) { newFolderEntry = currentEntry.copyWith( isHidden: isHidden, - lastUpdated: DateTime.now(), + lastUpdated: timestamp, ); } @@ -274,20 +275,23 @@ class HideBloc extends Bloc { isHidden ? RevisionAction.hide : RevisionAction.unhide, )); } - }); } - final hideAction = entryIsFile - ? (isHidden ? HideAction.hideFile : HideAction.unhideFile) - : entryIsDrive - ? (isHidden ? HideAction.hideDrive : HideAction.unhideDrive) - : (isHidden ? HideAction.hideFolder : HideAction.unhideFolder); + 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: hideAction, + hideAction: action, dataItems: dataItems, saveEntitiesToDb: saveEntitiesToDb, ), diff --git a/lib/components/app_top_bar.dart b/lib/components/app_top_bar.dart index c0f66a758e..69605871b4 100644 --- a/lib/components/app_top_bar.dart +++ b/lib/components/app_top_bar.dart @@ -75,23 +75,31 @@ class GlobalHideToggleButton extends StatelessWidget { } 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: hideState is ShowingHiddenItems - ? 'Hide hidden items' - : 'Show hidden items', - icon: hideState is ShowingHiddenItems - ? ArDriveIcons.eyeOpen( - color: colorTokens.textMid, - ) - : ArDriveIcons.eyeClosed( - color: colorTokens.textMid, - ), + tooltip: tooltip, + icon: icon, onPressed: () { - context.read().add(hideState is ShowingHiddenItems - ? HideItems(userHasHiddenItems: hideState.userHasHiddenDrive) - : ShowItems( - userHasHiddenItems: hideState.userHasHiddenDrive, - )); + context.read().add( + hideState is ShowingHiddenItems + ? HideItems( + userHasHiddenItems: hideState.userHasHiddenDrive) + : ShowItems( + userHasHiddenItems: hideState.userHasHiddenDrive, + ), + ); }, ); }, 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/pages/drive_detail/drive_detail_page.dart b/lib/pages/drive_detail/drive_detail_page.dart index 4129f8e52a..543da301e2 100644 --- a/lib/pages/drive_detail/drive_detail_page.dart +++ b/lib/pages/drive_detail/drive_detail_page.dart @@ -327,11 +327,6 @@ class _DriveDetailPageState extends State { required bool canDownloadMultipleFiles, required GlobalHideState hideState, }) { - final driveDetailCubit = context.read(); - ArDriveTypographyNew.of(context); - - final isShowingHiddenFiles = hideState is HiddingItems; - return Column( children: [ const AppTopBar(), @@ -597,26 +592,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 && @@ -725,16 +700,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(), ), @@ -794,13 +775,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), ), ), ); @@ -1139,7 +1125,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); @@ -1266,23 +1251,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/test/blocs/fs_entry_move_bloc_test.dart b/test/blocs/fs_entry_move_bloc_test.dart index 75d2658a12..c16cfc0db0 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,7 +302,6 @@ void main() { build: () => FsEntryMoveBloc( crypto: ArDriveCrypto(), arweave: arweave, - driveDetailCubit: MockDriveDetailCubit(), turboUploadService: turboUploadService, syncCubit: syncBloc, driveId: driveId, @@ -320,6 +318,7 @@ void main() { bloc.add(FsEntryMoveSubmit( folderInView: (bloc.state as FsEntryMoveLoadSuccess).viewingFolder.folder, + showHiddenItems: false, )); } }, From b0c7bf42118072f426b4574410aaa9214f810849 Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Tue, 22 Oct 2024 15:44:58 -0300 Subject: [PATCH 21/48] refactor(hide) - refactor setHideStatus method --- lib/blocs/hide/hide_bloc.dart | 304 +++++++++++++++--------- lib/blocs/hide/hide_state.dart | 9 +- test/blocs/fs_entry_move_bloc_test.dart | 2 +- 3 files changed, 194 insertions(+), 121 deletions(-) diff --git a/lib/blocs/hide/hide_bloc.dart b/lib/blocs/hide/hide_bloc.dart index 966f946693..8762e0434d 100644 --- a/lib/blocs/hide/hide_bloc.dart +++ b/lib/blocs/hide/hide_bloc.dart @@ -152,132 +152,33 @@ class HideBloc extends Bloc { 'Entity to hide must be either a File, Folder or Drive', ); - late EntityWithCustomMetadata newEntryEntity; - late DataItem dataItem; - Drive? newDriveEntry; - FileEntry? newFileEntry; - FolderEntry? newFolderEntry; - final timestamp = DateTime.now(); + HideEntitySettings hideEntitySettings; if (currentEntry is Drive) { - newDriveEntry = currentEntry.copyWith( - isHidden: isHidden, - lastUpdated: timestamp, + hideEntitySettings = await _getDriveHideEntitySettings( + isHidden, + currentEntry, ); - - 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; - } - - dataItem = await _arweave.prepareEntityDataItem( - newEntryEntity, - profile.user.wallet, - key: entityKey, + } else if (currentEntry is FileEntry) { + hideEntitySettings = await _getFileHideEntitySettings( + isHidden, + currentEntry, ); } else { - final entity = entryIsFile - ? (currentEntry).asEntity() - : (currentEntry as FolderEntry).asEntity(); - - final driveId = entryIsFile - ? currentEntry.driveId - : (currentEntry as FolderEntry).driveId; - - final profile = _profileCubit.state as ProfileLoggedIn; - final driveKey = - await _driveDao.getDriveKey(driveId, profile.user.cipherKey); - final SecretKey? entityKey; - - if (driveKey != null) { - if (entryIsFile) { - entityKey = await _crypto.deriveFileKey( - driveKey, - (entity as FileEntity).id!, - ); - } else { - entityKey = driveKey; - } - } else { - entityKey = null; - } - - if (entryIsFile) { - newFileEntry = currentEntry.copyWith( - isHidden: isHidden, - lastUpdated: timestamp, - ); - } else if (entryIsFolder) { - newFolderEntry = currentEntry.copyWith( - isHidden: isHidden, - lastUpdated: timestamp, - ); - } - - newEntryEntity = entryIsFile - ? (newFileEntry as FileEntry).asEntity() - : (newFolderEntry as FolderEntry).asEntity(); - - dataItem = await _arweave.prepareEntityDataItem( - newEntryEntity, - profile.user.wallet, - key: entityKey, + hideEntitySettings = await _getFolderHideEntitySettings( + isHidden, + currentEntry as FolderEntry, ); } - final dataItems = [dataItem]; + final dataItems = [hideEntitySettings.dataItem]; - final paymentInfo = await _uploadPreparationManager - .getUploadPaymentInfoForEntityUpload(dataItem: dataItem); + final paymentInfo = + await _uploadPreparationManager.getUploadPaymentInfoForEntityUpload( + dataItem: hideEntitySettings.dataItem); _useTurboUpload = paymentInfo.isFreeUploadPossibleUsingTurbo; - Future saveEntitiesToDb() async { - await _driveDao.transaction(() async { - if (entryIsFile) { - await _driveDao.writeToFile(newFileEntry!); - } else if (entryIsDrive) { - await _driveDao.writeToDrive(newDriveEntry!); - } else { - await _driveDao.writeToFolder(newFolderEntry!); - } - - newEntryEntity.txId = dataItem.id; - - if (entryIsFile) { - await _driveDao.insertFileRevision( - (newEntryEntity as FileEntity).toRevisionCompanion( - performedAction: - isHidden ? RevisionAction.hide : RevisionAction.unhide, - )); - } else if (entryIsDrive) { - final driveCompanion = - (newEntryEntity as DriveEntity).toRevisionCompanion( - performedAction: - isHidden ? RevisionAction.hide : RevisionAction.unhide, - ); - - await _driveDao.updateDrive(driveCompanion.toEntryCompanion()); - } else { - await _driveDao.insertFolderRevision( - (newEntryEntity as FolderEntity).toRevisionCompanion( - performedAction: - isHidden ? RevisionAction.hide : RevisionAction.unhide, - )); - } - }); - } - HideAction action; if (entryIsFile) { @@ -293,11 +194,134 @@ class HideBloc extends Bloc { uploadMethod: UploadMethod.turbo, hideAction: action, dataItems: dataItems, - saveEntitiesToDb: saveEntitiesToDb, + hideEntitySettings: hideEntitySettings, ), ); } + Future> _getFolderHideEntitySettings( + bool isHidden, + FolderEntry currentEntry, + ) async { + final timestamp = DateTime.now(); + + final newFolderEntry = 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 = driveKey; + } + + 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( + newFileEntry.asEntity(), + profile.user.wallet, + key: entityKey, + ); + + final newEntryEntity = newFileEntry.asEntity(); + + newEntryEntity.txId = dataItem.id; + + return HideEntitySettings( + isHidden: isHidden, + entry: newFileEntry, + entity: newEntryEntity, + dataItem: dataItem, + ); + } + + Future> _getDriveHideEntitySettings( + bool isHidden, + Drive currentEntry, + ) async { + final timestamp = DateTime.now(); + + final newDriveEntry = currentEntry.copyWith( + isHidden: isHidden, + lastUpdated: timestamp, + ); + + 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 dataItem = await _arweave.prepareEntityDataItem( + newEntryEntity, + profile.user.wallet, + key: entityKey, + ); + + newEntryEntity.txId = dataItem.id; + + return HideEntitySettings( + isHidden: isHidden, + entry: newDriveEntry, + entity: newEntryEntity, + dataItem: dataItem, + ); + } + Future _onConfirmUploadEvent( ConfirmUploadEvent event, Emitter emit, @@ -331,7 +355,7 @@ class HideBloc extends Bloc { await _arweave.postTx(hideTx); } - await state.saveEntitiesToDb(); + await _saveNewRevision(state.hideEntitySettings); emit(SuccessHideState(hideAction: state.hideAction)); }); @@ -341,6 +365,39 @@ class HideBloc extends Bloc { } } + Future _saveNewRevision( + HideEntitySettings settings, + ) async { + await _driveDao.transaction(() async { + if (T 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 (T 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 (T 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.updateDrive(driveCompanion.toEntryCompanion()); + } + }); + } + Future _onHideDriveEvent( HideDriveEvent event, Emitter emit, @@ -389,3 +446,18 @@ 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_state.dart b/lib/blocs/hide/hide_state.dart index 7e57f92faf..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, ); } } diff --git a/test/blocs/fs_entry_move_bloc_test.dart b/test/blocs/fs_entry_move_bloc_test.dart index c16cfc0db0..fe51d39215 100644 --- a/test/blocs/fs_entry_move_bloc_test.dart +++ b/test/blocs/fs_entry_move_bloc_test.dart @@ -305,7 +305,7 @@ void main() { 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: [], From 836bc7aec2c93d82ca408af1094c690ad6eafd9f Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Tue, 22 Oct 2024 15:56:21 -0300 Subject: [PATCH 22/48] Update hide_bloc.dart --- lib/blocs/hide/hide_bloc.dart | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/lib/blocs/hide/hide_bloc.dart b/lib/blocs/hide/hide_bloc.dart index 8762e0434d..8be31a042d 100644 --- a/lib/blocs/hide/hide_bloc.dart +++ b/lib/blocs/hide/hide_bloc.dart @@ -365,11 +365,14 @@ class HideBloc extends Bloc { } } - Future _saveNewRevision( - HideEntitySettings settings, + Future _saveNewRevision( + HideEntitySettings settings, ) async { await _driveDao.transaction(() async { - if (T is FileEntry) { + logger.d('Entry is ${settings.entry.runtimeType}'); + logger.d('Entry is File: ${settings.entry is FileEntry}'); + + if (settings.entry is FileEntry) { await _driveDao.writeToFile(settings.entry as FileEntry); await _driveDao.insertFileRevision( @@ -377,7 +380,7 @@ class HideBloc extends Bloc { performedAction: settings.isHidden ? RevisionAction.hide : RevisionAction.unhide, )); - } else if (T is FolderEntry) { + } else if (settings.entry is FolderEntry) { await _driveDao.writeToFolder(settings.entry as FolderEntry); await _driveDao.insertFolderRevision( @@ -385,7 +388,7 @@ class HideBloc extends Bloc { performedAction: settings.isHidden ? RevisionAction.hide : RevisionAction.unhide, )); - } else if (T is Drive) { + } else if (settings.entry is Drive) { await _driveDao.writeToDrive(settings.entry as Drive); final driveCompanion = @@ -460,4 +463,3 @@ class HideEntitySettings { required this.dataItem, }); } -// From 77ce82e2faff2b338098533d0fe531baab9201fd Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Tue, 22 Oct 2024 15:56:31 -0300 Subject: [PATCH 23/48] Update hide_bloc.dart --- lib/blocs/hide/hide_bloc.dart | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/blocs/hide/hide_bloc.dart b/lib/blocs/hide/hide_bloc.dart index 8be31a042d..e9b4db8d83 100644 --- a/lib/blocs/hide/hide_bloc.dart +++ b/lib/blocs/hide/hide_bloc.dart @@ -369,9 +369,6 @@ class HideBloc extends Bloc { HideEntitySettings settings, ) async { await _driveDao.transaction(() async { - logger.d('Entry is ${settings.entry.runtimeType}'); - logger.d('Entry is File: ${settings.entry is FileEntry}'); - if (settings.entry is FileEntry) { await _driveDao.writeToFile(settings.entry as FileEntry); From 45a6f848d8021edbe1723f428dc57747c576d430 Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Tue, 22 Oct 2024 17:21:05 -0300 Subject: [PATCH 24/48] Update app_router_delegate.dart --- lib/pages/app_router_delegate.dart | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/lib/pages/app_router_delegate.dart b/lib/pages/app_router_delegate.dart index ad9c922127..bd9016cb58 100644 --- a/lib/pages/app_router_delegate.dart +++ b/lib/pages/app_router_delegate.dart @@ -135,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; @@ -168,7 +158,7 @@ class AppRouterDelegate extends RouterDelegate shell = const LoginPage(gettingStarted: true); } else if (state is ProfileLoggedIn || anonymouslyShowDriveDetail) { - driveId = driveId ?? rootPath; + driveId = driveId ?? rootPath; shell = BlocListener( listener: (context, state) { @@ -232,7 +222,13 @@ class AppRouterDelegate extends RouterDelegate driveId: driveId, driveName: driveName, driveKey: sharedDriveKey, - ); + ).then((_) { + sharedDriveKey = null; + sharedRawDriveKey = null; + driveId = null; + driveName = null; + notifyListeners(); + }); } }, ), From 55eb76948bbecd15fdcc5a4fc114d5dce97ba681 Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Thu, 24 Oct 2024 11:29:45 -0300 Subject: [PATCH 25/48] fix bugs --- lib/blocs/upload/upload_cubit.dart | 258 +++++++++++++++-------------- lib/blocs/upload/upload_state.dart | 21 +-- lib/components/upload_form.dart | 75 +++++++-- 3 files changed, 206 insertions(+), 148 deletions(-) diff --git a/lib/blocs/upload/upload_cubit.dart b/lib/blocs/upload/upload_cubit.dart index c13880053d..0ae64fe7aa 100644 --- a/lib/blocs/upload/upload_cubit.dart +++ b/lib/blocs/upload/upload_cubit.dart @@ -102,81 +102,103 @@ class UploadCubit extends Cubit { late final bool _isUploadFolders; /// Manifest - List _manifestFiles = []; - final List _selectedManifestFiles = []; + List _manifestFiles = []; + final List _selectedManifestFileIds = []; - Map _arnsUndernamesLinkedToManifest = {}; - Map _arnsAntRecordsLinkedToManifest = {}; + /// key is domain + /// value is undername name + final Map> _arnsUndernamesLinkedToManifest = {}; UploadMethod? _manifestUploadMethod; bool _isManifestsUploadCancelled = false; - void selectManifestFile(FileEntry file) { + void selectManifestFile(UploadManifestModel file) { final readyState = state as UploadReady; - final manifestModel = UploadManifestModel( - name: file.name, - isCompleted: false, - freeThanksToTurbo: - file.size <= configService.config.allowedDataItemSizeForTurbo, - isUploading: false, - existingManifestFileId: file.id, - undername: _arnsUndernamesLinkedToManifest[file.id], - antRecord: _arnsAntRecordsLinkedToManifest[file.id], - ); + // final manifestModel = UploadManifestModel( + // name: file.name, + // isCompleted: false, + // freeThanksToTurbo: + // file.size <= configService.config.allowedDataItemSizeForTurbo, + // isUploading: false, + // existingManifestFileId: file.id, + // undername: _arnsUndernamesLinkedToManifest[file.id], + // antRecord: _arnsAntRecordsLinkedToManifest[file.id], + // ); + + _selectedManifestFileIds.add(file.entry.id); final newReadyState = readyState.copyWith( - selectedManifests: List.of( - _selectedManifestFiles - .map( - (f) => UploadManifestModel( - name: f.name, - isCompleted: false, - freeThanksToTurbo: - f.size <= configService.config.allowedDataItemSizeForTurbo, - isUploading: false, - existingManifestFileId: f.id, - undername: _arnsUndernamesLinkedToManifest[f.id], - antRecord: _arnsAntRecordsLinkedToManifest[f.id], - ), - ) + selectedManifests: _selectedManifestFileIds + .map((id) => _manifestFiles.firstWhere((e) => e.entry.id == id)) .toList(), - )..add(manifestModel)); - - _selectedManifestFiles.add(file); + ); emit(newReadyState); } + Future isArNSNameAlreadyLinked( + {required ANTRecord record, ARNSUndername? undername}) async { + if (_arnsUndernamesLinkedToManifest[record.domain] == null) { + return false; + } + + final undernames = await getARNSUndernames(record); + final linkedUnderNames = _arnsUndernamesLinkedToManifest[record.domain]; + + if (linkedUnderNames == null) { + return undernames.length == 1; + } + + return linkedUnderNames.contains(undername?.name); + } + void linkManifestToUndername( - FileEntry file, ANTRecord antRecord, ARNSUndername undername) { - _arnsUndernamesLinkedToManifest[file.id] = undername; - _arnsAntRecordsLinkedToManifest[file.id] = antRecord; + UploadManifestModel file, ANTRecord antRecord, ARNSUndername? undername) { + final index = _manifestFiles + .indexWhere((element) => element.entry.id == file.entry.id); + _manifestFiles[index] = + file.copyWith(undername: undername, antRecord: antRecord); + if (_arnsUndernamesLinkedToManifest[antRecord.domain] == null) { + _arnsUndernamesLinkedToManifest[antRecord.domain] = []; + } + + if (undername != null) { + _arnsUndernamesLinkedToManifest[antRecord.domain]!.add(undername.name); + } } - void unlinkManifestToUndername(FileEntry file) { - _arnsUndernamesLinkedToManifest.remove(file.id); + void unlinkManifestToUndername(UploadManifestModel file) { + if (file.undername == null) { + return; + } + + final undername = file.undername!; + final antRecord = file.antRecord!; + + final index = _manifestFiles + .indexWhere((element) => element.entry.id == file.entry.id); + _manifestFiles[index] = UploadManifestModel( + entry: file.entry, + freeThanksToTurbo: file.freeThanksToTurbo, + existingManifestFileId: file.existingManifestFileId, + file: file.file, + undername: null, + antRecord: null, + ); + + _arnsUndernamesLinkedToManifest[antRecord.domain]?.remove(undername.name); } - void unselectManifestFile(FileEntry file) { - _selectedManifestFiles.remove(file); + void unselectManifestFile(UploadManifestModel file) { + _selectedManifestFileIds.remove(file.entry.id); emit((state as UploadReady).copyWith( - selectedManifests: _selectedManifestFiles - .map( - (f) => UploadManifestModel( - name: f.name, - isCompleted: false, - freeThanksToTurbo: - f.size <= configService.config.allowedDataItemSizeForTurbo, - isUploading: false, - existingManifestFileId: f.id, - undername: _arnsUndernamesLinkedToManifest[f.id], - antRecord: _arnsAntRecordsLinkedToManifest[f.id], - ), - ) - .toList())); + selectedManifests: _selectedManifestFileIds + .map((id) => _manifestFiles.firstWhere((e) => e.entry.id == id)) + .toList(), + )); } void setManifestUploadMethod( @@ -185,20 +207,10 @@ 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, - undername: _arnsUndernamesLinkedToManifest[f.id], - antRecord: _arnsAntRecordsLinkedToManifest[f.id], - ), - ) + final manifestModels = _selectedManifestFileIds + .map((id) => _manifestFiles.firstWhere((e) => e.entry.id == id)) .toList(); + for (int i = 0; i < manifestModels.length; i++) { if (_isManifestsUploadCancelled) { break; @@ -207,7 +219,7 @@ class UploadCubit extends Cubit { manifestModels[i] = manifestModels[i].copyWith(isUploading: true); await _createManifestCubit.prepareManifestTx( - manifestName: manifestModels[i].name, + manifestName: manifestModels[i].entry.name, folderId: _targetFolder.id, existingManifestFileId: manifestModels[i].existingManifestFileId, ); @@ -262,7 +274,7 @@ class UploadCubit extends Cubit { )); await _createManifestCubit.prepareManifestTx( - manifestName: manifestModels[i].name, + manifestName: manifestModels[i].entry.name, folderId: _targetFolder.id, existingManifestFileId: manifestModels[i].existingManifestFileId, ); @@ -276,22 +288,46 @@ class UploadCubit extends Cubit { method: _manifestUploadMethod, ); - if (manifestModels[i].undername != null) { - manifestModels[i] = manifestModels[i].copyWith( - isCompleted: false, isUploading: false, isAssigningUndername: true); - emit(UploadingManifests( - manifestFiles: manifestModels, - completedCount: ++completedCount, - )); + final manifestFile = await _driveDao + .fileById( + driveId: _driveId, + fileId: manifestModels[i].existingManifestFileId, + ) + .getSingleOrNull(); - await _arnsRepository.setUndernamesToFile( - undername: manifestModels[i].undername!, - driveId: _driveId, - fileId: manifestModels[i].existingManifestFileId!, - processId: manifestModels[i].antRecord!.processId, + if (manifestFile == null) { + throw StateError('Manifest file not found'); + } + + ARNSUndername undername; + + if (manifestModels[i].undername == null) { + undername = ARNSUndername( + name: '@', + domain: manifestModels[i].antRecord!.domain, + record: ARNSRecord( + transactionId: manifestFile.dataTxId, + ttlSeconds: 3600, + ), ); + } else { + undername = manifestModels[i].undername!; } + 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); @@ -301,16 +337,12 @@ class UploadCubit extends Cubit { )); } - emit(UploadComplete( - manifestFiles: _selectedManifestFiles, - )); + emit(UploadComplete()); } void cancelManifestsUpload() { _isManifestsUploadCancelled = true; - emit(UploadComplete( - manifestFiles: _selectedManifestFiles, - )); + emit(UploadComplete()); } /// License forms @@ -474,19 +506,8 @@ class UploadCubit extends Cubit { loadingArNSNames: true, arnsCheckboxChecked: _showArnsNameSelectionCheckBoxValue, totalSize: await _getTotalSize(), - selectedManifests: _selectedManifestFiles - .map( - (f) => UploadManifestModel( - name: f.name, - isCompleted: false, - freeThanksToTurbo: f.size <= - configService.config.allowedDataItemSizeForTurbo, - isUploading: false, - existingManifestFileId: f.id, - undername: _arnsUndernamesLinkedToManifest[f.id], - antRecord: _arnsAntRecordsLinkedToManifest[f.id], - ), - ) + selectedManifests: _selectedManifestFileIds + .map((id) => _manifestFiles.firstWhere((e) => e.entry.id == id)) .toList(), showSettings: showSettings, canShowSettings: showSettings, @@ -530,19 +551,8 @@ class UploadCubit extends Cubit { showArnsNameSelection: false, arnsCheckboxChecked: _showArnsNameSelectionCheckBoxValue, totalSize: await _getTotalSize(), - selectedManifests: _selectedManifestFiles - .map( - (f) => UploadManifestModel( - name: f.name, - isCompleted: false, - freeThanksToTurbo: f.size <= - configService.config.allowedDataItemSizeForTurbo, - isUploading: false, - existingManifestFileId: f.id, - undername: _arnsUndernamesLinkedToManifest[f.id], - antRecord: _arnsAntRecordsLinkedToManifest[f.id], - ), - ) + selectedManifests: _selectedManifestFileIds + .map((id) => _manifestFiles.firstWhere((e) => e.entry.id == id)) .toList(), showSettings: showSettings, manifestFiles: _manifestFiles, @@ -558,7 +568,7 @@ class UploadCubit extends Cubit { if (state is UploadReady) { if (_showArnsNameSelectionCheckBoxValue) { showArnsNameSelection(state as UploadReady); - } else if (_selectedManifestFiles.isNotEmpty) { + } else if (_selectedManifestFileIds.isNotEmpty) { emit(UploadReview(readyState: state as UploadReady)); } else { final readyState = state as UploadReady; @@ -1052,11 +1062,21 @@ class UploadCubit extends Cubit { ), ); - _manifestFiles = await _manifestRepository.getManifestFilesInFolder( + final manifestFileEntries = + await _manifestRepository.getManifestFilesInFolder( folderId: _targetFolder.id, driveId: _targetDrive.id, ); + _manifestFiles = manifestFileEntries + .map((e) => UploadManifestModel( + entry: e, + existingManifestFileId: e.id, + freeThanksToTurbo: + e.size <= configService.config.allowedDataItemSizeForTurbo, + )) + .toList(); + // if there are no files that can be used to generate a thumbnail, we disable the option if (!containsSupportedImageTypeForThumbnailGeneration) { _uploadThumbnail = false; @@ -1281,13 +1301,11 @@ class UploadCubit extends Cubit { ); } - if (_selectedManifestFiles.isNotEmpty) { + if (_selectedManifestFileIds.isNotEmpty) { await prepareManifestUpload(); } - emit(UploadComplete( - manifestFiles: _selectedManifestFiles, - )); + emit(UploadComplete()); unawaited(_profileCubit.refreshBalance()); }, @@ -1361,11 +1379,11 @@ class UploadCubit extends Cubit { 'Upload finished with success. Number of tasks: ${tasks.length}', ); - if (_selectedManifestFiles.isNotEmpty) { + if (_selectedManifestFileIds.isNotEmpty) { await prepareManifestUpload(); } - emit(UploadComplete(manifestFiles: _selectedManifestFiles)); + emit(UploadComplete()); PlausibleEventTracker.trackUploadSuccess(); }, diff --git a/lib/blocs/upload/upload_state.dart b/lib/blocs/upload/upload_state.dart index cc8e096bd9..8cea3bb68a 100644 --- a/lib/blocs/upload/upload_state.dart +++ b/lib/blocs/upload/upload_state.dart @@ -108,7 +108,7 @@ class UploadReady extends UploadState { final bool arnsCheckboxChecked; final int totalSize; final List selectedManifests; - final List manifestFiles; + final List manifestFiles; final bool showSettings; final bool canShowSettings; final List arnsRecords; @@ -154,7 +154,7 @@ class UploadReady extends UploadState { bool? arnsCheckboxChecked, int? totalSize, List? selectedManifests, - List? manifestFiles, + List? manifestFiles, bool? canShowSettings, List? arnsRecords, }) { @@ -292,10 +292,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 { @@ -349,21 +348,22 @@ 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; + 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, @@ -379,9 +379,10 @@ class UploadManifestModel extends Equatable { ARNSUndername? undername, ANTRecord? antRecord, bool? isAssigningUndername, + FileEntry? entry, }) { return UploadManifestModel( - name: name, + entry: entry ?? this.entry, isCompleted: isCompleted ?? this.isCompleted, isUploading: isUploading ?? this.isUploading, existingManifestFileId: @@ -396,7 +397,7 @@ class UploadManifestModel extends Equatable { @override List get props => [ - name, + entry, isCompleted, isUploading, existingManifestFileId, diff --git a/lib/components/upload_form.dart b/lib/components/upload_form.dart index 15e8f48020..ed170a4e2e 100644 --- a/lib/components/upload_form.dart +++ b/lib/components/upload_form.dart @@ -338,7 +338,7 @@ class _UploadingManifestsWidget extends StatelessWidget { ArDriveIcons.manifest(size: 16), const SizedBox(width: 8), Text( - '${state.manifestFiles[index].name}...', + '${state.manifestFiles[index].entry.name}...', style: typography.paragraphNormal( fontWeight: ArFontWeight.semiBold, ), @@ -1046,7 +1046,7 @@ class _UploadReadyModalBaseState extends State { class _ManifestOptionTile extends StatefulWidget { final UploadReady state; - final FileEntry file; + final UploadManifestModel file; const _ManifestOptionTile({ required this.state, @@ -1077,9 +1077,9 @@ class __ManifestOptionTileState extends State<_ManifestOptionTile> { height: _isExpanded ? 130 : 50, child: GestureDetector( onTap: () { - setState(() { - _isExpanded = !_isExpanded; - }); + // setState(() { + // _isExpanded = !_isExpanded; + // }); }, child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -1097,17 +1097,19 @@ class __ManifestOptionTileState extends State<_ManifestOptionTile> { children: [ ArDriveIcons.manifest( size: 16, - color: - widget.file.isHidden ? hiddenColor : null), + color: widget.file.entry.isHidden + ? hiddenColor + : null), const SizedBox(width: 8), Text( - widget.file.name, + widget.file.entry.name, style: typography.paragraphNormal( - color: - widget.file.isHidden ? hiddenColor : null, + color: widget.file.entry.isHidden + ? hiddenColor + : null, ), ), - if (widget.file.isHidden) ...[ + if (widget.file.entry.isHidden) ...[ const SizedBox(width: 8), Text('(hidden)', style: typography.paragraphNormal( @@ -1149,7 +1151,7 @@ class __ManifestOptionTileState extends State<_ManifestOptionTile> { maxHeight: 30, onPressed: () { setState(() { - _isExpanded = !_isExpanded; + _isExpanded = true; }); }, ) @@ -1160,7 +1162,7 @@ class __ManifestOptionTileState extends State<_ManifestOptionTile> { const SizedBox(height: 8), Expanded( flex: 1, - child: AntSelector(fileEntry: widget.file), + child: AntSelector(file: widget.file), ), ], ], @@ -2208,7 +2210,7 @@ class _UploadReviewWithLicenseWidget extends StatelessWidget { ArDriveIcons.manifest(size: 16), const SizedBox(width: 8), Text( - e.name, + e.entry.name, style: typography.paragraphNormal( fontWeight: ArFontWeight.semiBold, ), @@ -2331,7 +2333,7 @@ class _UploadReviewWithArnsNameWidget extends StatelessWidget { ArDriveIcons.manifest(size: 16), const SizedBox(width: 8), Text( - e.name, + e.entry.name, style: typography.paragraphNormal( fontWeight: ArFontWeight.semiBold, ), @@ -3016,9 +3018,9 @@ List getModalActions( } class AntSelector extends StatefulWidget { - const AntSelector({super.key, required this.fileEntry}); + const AntSelector({super.key, required this.file}); - final FileEntry fileEntry; + final UploadManifestModel file; @override State createState() => _AntSelectorState(); @@ -3030,6 +3032,7 @@ class _AntSelectorState extends State { List _arnsUndernames = []; bool _loadingUndernames = false; + bool _loadingIsAlreadyLinked = false; loadARNSUndernames( ANTRecord ant, @@ -3045,6 +3048,18 @@ class _AntSelectorState extends State { }); } + loadIsAlreadyLinked() async { + + setState(() { + _loadingIsAlreadyLinked = true; + }); + + _isAlreadyLinked = await context.read().isArNSNameAlreadyLinked( + record: widget.file.antRecord!, + undername: _selectedUndername, + ); + } + @override Widget build(BuildContext context) { final colorTokens = ArDriveTheme.of(context).themeData.colorTokens; @@ -3157,6 +3172,8 @@ class _AntSelectorState extends State { ArDriveDropdownItem _buildDropdownItem(BuildContext context, ANTRecord ant) { final typography = ArDriveTypographyNew.of(context); + final colorTokens = ArDriveTheme.of(context).themeData.colorTokens; + return ArDriveDropdownItem( content: SizedBox( width: 235, @@ -3171,6 +3188,7 @@ class _AntSelectorState extends State { ant.domain, style: typography.paragraphSmall( fontWeight: ArFontWeight.semiBold, + color: isAlreadyLinked ? colorTokens.textLow : null, ), ), ), @@ -3183,11 +3201,21 @@ class _AntSelectorState extends State { ), ), onClick: () { + if (isAlreadyLinked) { + return; + } + setState(() { _selectedAnt = ant; _arnsUndernames = []; loadARNSUndernames(ant); + + context.read().linkManifestToUndername( + widget.file, + _selectedAnt!, + null, + ); }); }, ); @@ -3197,6 +3225,12 @@ class _AntSelectorState extends State { BuildContext context, ARNSUndername undername) { final typography = ArDriveTypographyNew.of(context); + final isAlreadyLinked = context.read().isArNSNameAlreadyLinked( + record: _selectedAnt!, + undername: undername, + ); + final colorTokens = ArDriveTheme.of(context).themeData.colorTokens; + return ArDriveDropdownItem( content: SizedBox( width: 235, @@ -3211,6 +3245,7 @@ class _AntSelectorState extends State { undername.name, style: typography.paragraphSmall( fontWeight: ArFontWeight.semiBold, + color: isAlreadyLinked ? colorTokens.textLow : null, ), ), ), @@ -3223,11 +3258,15 @@ class _AntSelectorState extends State { ), ), onClick: () { + if (isAlreadyLinked) { + return; + } + setState(() { _selectedUndername = undername; context.read().linkManifestToUndername( - widget.fileEntry, _selectedAnt!, _selectedUndername!); + widget.file, _selectedAnt!, _selectedUndername!); }); }, ); From 14373a0c611165d4e88a34b370d49c3e536f7530 Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Mon, 28 Oct 2024 09:47:01 -0300 Subject: [PATCH 26/48] fix: missing hide button --- lib/app_shell.dart | 2 ++ lib/blocs/hide/hide_bloc.dart | 3 ++- lib/models/drive_revision.dart | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/lib/app_shell.dart b/lib/app_shell.dart index 8ef7c90ca7..b759fdc55d 100644 --- a/lib/app_shell.dart +++ b/lib/app_shell.dart @@ -368,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/blocs/hide/hide_bloc.dart b/lib/blocs/hide/hide_bloc.dart index e9b4db8d83..ea22602e4d 100644 --- a/lib/blocs/hide/hide_bloc.dart +++ b/lib/blocs/hide/hide_bloc.dart @@ -393,7 +393,8 @@ class HideBloc extends Bloc { performedAction: settings.isHidden ? RevisionAction.hide : RevisionAction.unhide, ); - await _driveDao.updateDrive(driveCompanion.toEntryCompanion()); + + await _driveDao.insertDriveRevision(driveCompanion); } }); } diff --git a/lib/models/drive_revision.dart b/lib/models/drive_revision.dart index 8f6e23d931..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 From 8acbf551a8d93e5bf66a209b36c6cc8fb6a2642f Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Mon, 28 Oct 2024 11:34:37 -0300 Subject: [PATCH 27/48] fix(attach drives): dont ask for attach a drive logging the user out --- lib/authentication/ardrive_auth.dart | 4 ++-- lib/blocs/drive_detail/drive_detail_cubit.dart | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) 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/drive_detail/drive_detail_cubit.dart b/lib/blocs/drive_detail/drive_detail_cubit.dart index d0003bef0f..7fd36f7425 100644 --- a/lib/blocs/drive_detail/drive_detail_cubit.dart +++ b/lib/blocs/drive_detail/drive_detail_cubit.dart @@ -142,6 +142,10 @@ class DriveDetailCubit extends Cubit { ), _profileCubit.stream.startWith(ProfileCheckingAvailability()), (drive, folderContents, _) async { + if (isClosed) { + return; + } + await _syncCubit.waitCurrentSync(); if (drive == null) { From 26361f65d3615e71ab870c43699f9a3e9d50a17c Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Mon, 28 Oct 2024 19:02:50 -0300 Subject: [PATCH 28/48] Update drive_detail_page.dart fix hide state on mobile --- lib/pages/drive_detail/drive_detail_page.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/pages/drive_detail/drive_detail_page.dart b/lib/pages/drive_detail/drive_detail_page.dart index 543da301e2..691b06a7af 100644 --- a/lib/pages/drive_detail/drive_detail_page.dart +++ b/lib/pages/drive_detail/drive_detail_page.dart @@ -842,7 +842,7 @@ class _DriveDetailPageState extends State { List items, GlobalHideState globalHideState, ) { - final isShowingHiddenFiles = globalHideState is HiddingItems; + final isShowingHiddenFiles = globalHideState is ShowingHiddenItems; final List filteredItems; From ed8d519a34e11031a263a70addb46e78688fb446 Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Tue, 29 Oct 2024 04:26:55 -0300 Subject: [PATCH 29/48] feat(assign arns name) - handle case where the same name could be assigned - minor improvements on the ui - add icons, load states, etc --- lib/blocs/upload/upload_cubit.dart | 219 +++---- lib/blocs/upload/upload_state.dart | 17 +- lib/components/upload_form.dart | 575 +++++------------- .../blocs/upload_manifest_options_bloc.dart | 164 +++++ .../blocs/upload_manifest_options_event.dart | 49 ++ .../blocs/upload_manifest_options_state.dart | 37 ++ .../manifest_options/manifest_options.dart | 516 ++++++++++++++++ .../ario_sdk/lib/src/models/ant_record.dart | 9 +- .../ario_sdk/lib/src/models/arns_record.dart | 8 +- .../utils/get_literal_arns_record_name.dart | 1 + 10 files changed, 1013 insertions(+), 582 deletions(-) create mode 100644 lib/core/upload/view/blocs/upload_manifest_options_bloc.dart create mode 100644 lib/core/upload/view/blocs/upload_manifest_options_event.dart create mode 100644 lib/core/upload/view/blocs/upload_manifest_options_state.dart create mode 100644 lib/core/upload/view/manifest_options/manifest_options.dart diff --git a/lib/blocs/upload/upload_cubit.dart b/lib/blocs/upload/upload_cubit.dart index 0ae64fe7aa..f85c218b3e 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,102 +103,21 @@ class UploadCubit extends Cubit { late final bool _isUploadFolders; /// Manifest - List _manifestFiles = []; - final List _selectedManifestFileIds = []; - /// key is domain - /// value is undername name - final Map> _arnsUndernamesLinkedToManifest = {}; + Map _manifestFiles = {}; + final List _selectedManifestModels = []; UploadMethod? _manifestUploadMethod; bool _isManifestsUploadCancelled = false; - void selectManifestFile(UploadManifestModel file) { - final readyState = state as UploadReady; - - // final manifestModel = UploadManifestModel( - // name: file.name, - // isCompleted: false, - // freeThanksToTurbo: - // file.size <= configService.config.allowedDataItemSizeForTurbo, - // isUploading: false, - // existingManifestFileId: file.id, - // undername: _arnsUndernamesLinkedToManifest[file.id], - // antRecord: _arnsAntRecordsLinkedToManifest[file.id], - // ); - - _selectedManifestFileIds.add(file.entry.id); - - final newReadyState = readyState.copyWith( - selectedManifests: _selectedManifestFileIds - .map((id) => _manifestFiles.firstWhere((e) => e.entry.id == id)) - .toList(), - ); - - emit(newReadyState); - } - - Future isArNSNameAlreadyLinked( - {required ANTRecord record, ARNSUndername? undername}) async { - if (_arnsUndernamesLinkedToManifest[record.domain] == null) { - return false; - } - - final undernames = await getARNSUndernames(record); - final linkedUnderNames = _arnsUndernamesLinkedToManifest[record.domain]; - - if (linkedUnderNames == null) { - return undernames.length == 1; - } - - return linkedUnderNames.contains(undername?.name); - } - - void linkManifestToUndername( - UploadManifestModel file, ANTRecord antRecord, ARNSUndername? undername) { - final index = _manifestFiles - .indexWhere((element) => element.entry.id == file.entry.id); - _manifestFiles[index] = - file.copyWith(undername: undername, antRecord: antRecord); - if (_arnsUndernamesLinkedToManifest[antRecord.domain] == null) { - _arnsUndernamesLinkedToManifest[antRecord.domain] = []; - } - - if (undername != null) { - _arnsUndernamesLinkedToManifest[antRecord.domain]!.add(undername.name); - } - } + void updateManifestSelection(List selections) { + _selectedManifestModels.clear(); - void unlinkManifestToUndername(UploadManifestModel file) { - if (file.undername == null) { - return; - } - - final undername = file.undername!; - final antRecord = file.antRecord!; - - final index = _manifestFiles - .indexWhere((element) => element.entry.id == file.entry.id); - _manifestFiles[index] = UploadManifestModel( - entry: file.entry, - freeThanksToTurbo: file.freeThanksToTurbo, - existingManifestFileId: file.existingManifestFileId, - file: file.file, - undername: null, - antRecord: null, - ); - - _arnsUndernamesLinkedToManifest[antRecord.domain]?.remove(undername.name); - } - - void unselectManifestFile(UploadManifestModel file) { - _selectedManifestFileIds.remove(file.entry.id); + _selectedManifestModels.addAll(selections); emit((state as UploadReady).copyWith( - selectedManifests: _selectedManifestFileIds - .map((id) => _manifestFiles.firstWhere((e) => e.entry.id == id)) - .toList(), + selectedManifestSelections: selections, )); } @@ -207,8 +127,14 @@ class UploadCubit extends Cubit { } Future prepareManifestUpload() async { - final manifestModels = _selectedManifestFileIds - .map((id) => _manifestFiles.firstWhere((e) => e.entry.id == 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++) { @@ -299,42 +225,51 @@ class UploadCubit extends Cubit { throw StateError('Manifest file not found'); } - ARNSUndername undername; + 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 = manifestModels[i].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, - )); + 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, - ); + 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); + manifestModels[i] = manifestModels[i].copyWith( + isCompleted: true, isUploading: false, isAssigningUndername: false); - emit(UploadingManifests( - manifestFiles: manifestModels, - completedCount: completedCount, - )); + emit(UploadingManifests( + manifestFiles: manifestModels, + completedCount: completedCount, + )); + } } emit(UploadComplete()); @@ -506,13 +441,12 @@ class UploadCubit extends Cubit { loadingArNSNames: true, arnsCheckboxChecked: _showArnsNameSelectionCheckBoxValue, totalSize: await _getTotalSize(), - selectedManifests: _selectedManifestFileIds - .map((id) => _manifestFiles.firstWhere((e) => e.entry.id == id)) - .toList(), showSettings: showSettings, canShowSettings: showSettings, - manifestFiles: _manifestFiles, + manifestFiles: _manifestFiles.values.toList(), arnsRecords: _ants, + showReviewButtonText: false, + selectedManifestSelections: _selectedManifestModels, ), ); @@ -551,13 +485,12 @@ class UploadCubit extends Cubit { showArnsNameSelection: false, arnsCheckboxChecked: _showArnsNameSelectionCheckBoxValue, totalSize: await _getTotalSize(), - selectedManifests: _selectedManifestFileIds - .map((id) => _manifestFiles.firstWhere((e) => e.entry.id == id)) - .toList(), showSettings: showSettings, - manifestFiles: _manifestFiles, + manifestFiles: _manifestFiles.values.toList(), arnsRecords: _ants, canShowSettings: showSettings, + showReviewButtonText: false, + selectedManifestSelections: _selectedManifestModels, ), ); } @@ -568,7 +501,7 @@ class UploadCubit extends Cubit { if (state is UploadReady) { if (_showArnsNameSelectionCheckBoxValue) { showArnsNameSelection(state as UploadReady); - } else if (_selectedManifestFileIds.isNotEmpty) { + } else if (_selectedManifestModels.isNotEmpty) { emit(UploadReview(readyState: state as UploadReady)); } else { final readyState = state as UploadReady; @@ -1068,20 +1001,28 @@ class UploadCubit extends Cubit { driveId: _targetDrive.id, ); - _manifestFiles = manifestFileEntries - .map((e) => UploadManifestModel( - entry: e, - existingManifestFileId: e.id, - freeThanksToTurbo: - e.size <= configService.config.allowedDataItemSizeForTurbo, - )) - .toList(); + _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( @@ -1301,7 +1242,7 @@ class UploadCubit extends Cubit { ); } - if (_selectedManifestFileIds.isNotEmpty) { + if (_selectedManifestModels.isNotEmpty) { await prepareManifestUpload(); } @@ -1379,7 +1320,7 @@ class UploadCubit extends Cubit { 'Upload finished with success. Number of tasks: ${tasks.length}', ); - if (_selectedManifestFileIds.isNotEmpty) { + if (_selectedManifestModels.isNotEmpty) { await prepareManifestUpload(); } diff --git a/lib/blocs/upload/upload_state.dart b/lib/blocs/upload/upload_state.dart index 8cea3bb68a..c71c3f7db9 100644 --- a/lib/blocs/upload/upload_state.dart +++ b/lib/blocs/upload/upload_state.dart @@ -107,13 +107,14 @@ class UploadReady extends UploadState { final bool loadingArNSNamesError; final bool arnsCheckboxChecked; final int totalSize; - final List selectedManifests; 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, @@ -129,11 +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 @@ -153,10 +155,11 @@ class UploadReady extends UploadState { bool? loadingArNSNamesError, bool? arnsCheckboxChecked, int? totalSize, - List? selectedManifests, List? manifestFiles, bool? canShowSettings, List? arnsRecords, + bool? showReviewButtonText, + List? selectedManifestSelections, }) { return UploadReady( loadingArNSNames: loadingArNSNames ?? this.loadingArNSNames, @@ -174,11 +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, ); } @@ -357,6 +362,7 @@ class UploadManifestModel extends Equatable { final IOFile? file; final ARNSUndername? undername; final ANTRecord? antRecord; + final bool selectionExpanded; const UploadManifestModel({ required this.entry, @@ -368,6 +374,7 @@ class UploadManifestModel extends Equatable { this.undername, this.antRecord, this.isAssigningUndername = false, + this.selectionExpanded = false, }); UploadManifestModel copyWith({ @@ -380,6 +387,7 @@ class UploadManifestModel extends Equatable { ANTRecord? antRecord, bool? isAssigningUndername, FileEntry? entry, + bool? selectionExpanded, }) { return UploadManifestModel( entry: entry ?? this.entry, @@ -392,6 +400,7 @@ class UploadManifestModel extends Equatable { undername: undername ?? this.undername, antRecord: antRecord ?? this.antRecord, isAssigningUndername: isAssigningUndername ?? this.isAssigningUndername, + selectionExpanded: selectionExpanded ?? this.selectionExpanded, ); } diff --git a/lib/components/upload_form.dart b/lib/components/upload_form.dart index 0657bcef80..2978dacc04 100644 --- a/lib/components/upload_form.dart +++ b/lib/components/upload_form.dart @@ -27,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'; @@ -50,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'; @@ -251,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) { @@ -1020,152 +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.separated( - separatorBuilder: (context, index) => const SizedBox(height: 4), - physics: scrollable - ? const ScrollPhysics() - : const NeverScrollableScrollPhysics(), - shrinkWrap: true, - itemCount: state.manifestFiles.length, - itemBuilder: (context, index) { - final file = state.manifestFiles[index]; - - return _ManifestOptionTile( - file: file, - state: state, - ); - }, - ), - ); - } -} - -class _ManifestOptionTile extends StatefulWidget { - final UploadReady state; - final UploadManifestModel file; - - const _ManifestOptionTile({ - required this.state, - required this.file, - }); - - @override - State<_ManifestOptionTile> createState() => __ManifestOptionTileState(); -} - -class __ManifestOptionTileState extends State<_ManifestOptionTile> { - bool _isExpanded = false; - - @override - Widget build(BuildContext context) { - final hiddenColor = - ArDriveTheme.of(context).themeData.colors.themeFgDisabled; - final typography = ArDriveTypographyNew.of(context); - final colorTokens = ArDriveTheme.of(context).themeData.colorTokens; - - return AnimatedContainer( - duration: const Duration(milliseconds: 300), - decoration: BoxDecoration( - color: colorTokens.containerL2, - borderRadius: BorderRadius.circular(5), - ), - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - height: _isExpanded ? 130 : 50, - child: GestureDetector( - onTap: () { - // setState(() { - // _isExpanded = !_isExpanded; - // }); - }, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Row( - mainAxisSize: MainAxisSize.max, - children: [ - Expanded( - child: Row( - children: [ - Flexible( - flex: 2, - child: Row( - children: [ - ArDriveIcons.manifest( - size: 16, - color: widget.file.entry.isHidden - ? hiddenColor - : null), - const SizedBox(width: 8), - Text( - widget.file.entry.name, - style: typography.paragraphNormal( - color: widget.file.entry.isHidden - ? hiddenColor - : null, - ), - ), - if (widget.file.entry.isHidden) ...[ - const SizedBox(width: 8), - Text('(hidden)', - style: typography.paragraphNormal( - color: hiddenColor, - )) - ] - ], - ), - ), - Flexible( - flex: 1, - child: ArDriveCheckBox( - checked: widget.state.selectedManifests - .contains(widget.file), - onChange: (value) { - if (value) { - context - .read() - .selectManifestFile(widget.file); - } else { - context - .read() - .unselectManifestFile(widget.file); - } - }, - ), - ), - ], - ), - ), - ArDriveButtonNew( - text: 'Add ArNS', - typography: typography, - isDisabled: - !widget.state.selectedManifests.contains(widget.file), - fontStyle: typography.paragraphSmall(), - variant: ButtonVariant.primary, - maxWidth: 80, - maxHeight: 30, - onPressed: () { - setState(() { - _isExpanded = true; - }); - }, - ) - // TODO: Add back when we have the right UI for it - ], - ), - if (_isExpanded) ...[ - const SizedBox(height: 8), - Expanded( - flex: 1, - child: AntSelector(file: widget.file), - ), - ], - ], - ), - ), + return ManifestOptions( + scrollable: scrollable, ); } } @@ -2173,7 +2081,7 @@ class _UploadReviewWithLicenseWidget extends StatelessWidget { ), ), ], - if (state.readyState.selectedManifests.isNotEmpty) ...[ + if (state.readyState.selectedManifestSelections.isNotEmpty) ...[ Padding( padding: const EdgeInsets.only(bottom: 16.0), child: ConstrainedBox( @@ -2200,28 +2108,44 @@ class _UploadReviewWithLicenseWidget extends StatelessWidget { child: ListView( shrinkWrap: true, children: [ - ...state.readyState.selectedManifests.map( + ...state.readyState.selectedManifestSelections.map( (e) => Column( children: [ Row( children: [ ArDriveIcons.manifest(size: 16), const SizedBox(width: 8), - Text( - e.entry.name, - style: typography.paragraphNormal( - fontWeight: ArFontWeight.semiBold, + Flexible( + child: Text( + e.manifest.name, + style: typography.paragraphNormal( + fontWeight: ArFontWeight.semiBold, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), ), ], ), - if (e.undername != null) ...[ - const SizedBox(height: 4), - Text( - 'ArNS Name: ${getLiteralARNSRecordName(e.undername!)}', - style: typography.paragraphNormal( - fontWeight: ArFontWeight.semiBold, - ), + 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, + ), + ), + ], ), ], ], @@ -2298,12 +2222,12 @@ class _UploadReviewWithArnsNameWidget extends StatelessWidget { ), ), ], - if (state.readyState.selectedManifests.isNotEmpty) ...[ + if (state.readyState.selectedManifestSelections.isNotEmpty) ...[ Padding( padding: const EdgeInsets.only(bottom: 16.0), child: ConstrainedBox( constraints: const BoxConstraints( - // maxHeight: 125, + maxHeight: 125, minWidth: kLargeDialogWidth, ), child: Column( @@ -2321,27 +2245,57 @@ class _UploadReviewWithArnsNameWidget extends StatelessWidget { ), const SizedBox(height: 4), Flexible( - child: ListView( - shrinkWrap: true, - children: [ - ...state.readyState.selectedManifests.map( - (e) => Row( - mainAxisSize: MainAxisSize.min, - children: [ - ArDriveIcons.manifest(size: 16), - const SizedBox(width: 8), - Text( - e.entry.name, - style: typography.paragraphNormal( - fontWeight: ArFontWeight.semiBold, + child: 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, + ), + ), + ], + ), + ], + ], + ), ), - ), - ], + ], + ), ), - ) + ), ], ), ), @@ -2984,7 +2938,7 @@ List getModalActions( variant: ButtonVariant.primary, ), ]; - } else if (state.selectedManifests.isNotEmpty) { + } else if (state.selectedManifestSelections.isNotEmpty) { return [ ArDriveButtonNew( isDisabled: !state.isNextButtonEnabled, @@ -3015,260 +2969,11 @@ List getModalActions( ]; } -class AntSelector extends StatefulWidget { - const AntSelector({super.key, required this.file}); - - final UploadManifestModel file; - - @override - State createState() => _AntSelectorState(); -} - -class _AntSelectorState extends State { - ANTRecord? _selectedAnt; - ARNSUndername? _selectedUndername; - - List _arnsUndernames = []; - bool _loadingUndernames = false; - bool _loadingIsAlreadyLinked = false; - - loadARNSUndernames( - ANTRecord ant, - ) async { - setState(() { - _loadingUndernames = true; - }); - - _arnsUndernames = await context.read().getARNSUndernames(ant); - - setState(() { - _loadingUndernames = false; - }); - } - - loadIsAlreadyLinked() async { - - setState(() { - _loadingIsAlreadyLinked = true; - }); - - _isAlreadyLinked = await context.read().isArNSNameAlreadyLinked( - record: widget.file.antRecord!, - undername: _selectedUndername, - ); - } - - @override - Widget build(BuildContext context) { - final colorTokens = ArDriveTheme.of(context).themeData.colorTokens; - return BlocBuilder( - builder: (context, state) { - if (state is UploadReady) { - 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, - items: state.arnsRecords - .map((ant) => _buildDropdownItem(context, ant)) - .toList(), - child: ArDriveClickArea( - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Row( - children: [ - _buildSelectedItem(context), - ], - ), - ArDriveIcons.chevronDown(), - ], - ), - ), - ), - ), - if (_loadingUndernames) const CircularProgressIndicator(), - if (!_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( - 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(), - ], - ), - ), - ), - ), - ), - ], - ); - } - 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, - ), - ); - } - - Widget _buildSelectedItemUndername(BuildContext context) { - final typography = ArDriveTypographyNew.of(context); - - return Text( - _selectedUndername?.name ?? 'under_name (optional)', - style: typography.paragraphSmall( - fontWeight: ArFontWeight.semiBold, - ), - ); +String getLiteralArNSName(ANTRecord record, ARNSUndername? undername) { + if (undername != null) { + return getLiteralARNSRecordName(undername); } - ArDriveDropdownItem _buildDropdownItem(BuildContext context, ANTRecord ant) { - final typography = ArDriveTypographyNew.of(context); - - final colorTokens = ArDriveTheme.of(context).themeData.colorTokens; - - 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, - color: isAlreadyLinked ? colorTokens.textLow : null, - ), - ), - ), - if (ant.domain == _selectedAnt?.domain) - ArDriveIcons.checkmark( - size: 16, - ) - ], - ), - ), - ), - onClick: () { - if (isAlreadyLinked) { - return; - } - - setState(() { - _selectedAnt = ant; - - _arnsUndernames = []; - loadARNSUndernames(ant); - - context.read().linkManifestToUndername( - widget.file, - _selectedAnt!, - null, - ); - }); - }, - ); - } - - ArDriveDropdownItem _buildDropdownItemUndername( - BuildContext context, ARNSUndername undername) { - final typography = ArDriveTypographyNew.of(context); - - final isAlreadyLinked = context.read().isArNSNameAlreadyLinked( - record: _selectedAnt!, - undername: undername, - ); - final colorTokens = ArDriveTheme.of(context).themeData.colorTokens; - - 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, - color: isAlreadyLinked ? colorTokens.textLow : null, - ), - ), - ), - if (undername.name == _selectedUndername?.name) - ArDriveIcons.checkmark( - size: 16, - ) - ], - ), - ), - ), - onClick: () { - if (isAlreadyLinked) { - return; - } - - setState(() { - _selectedUndername = undername; - - context.read().linkManifestToUndername( - widget.file, _selectedAnt!, _selectedUndername!); - }); - }, - ); - } + 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..35747df3c0 --- /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:bloc/bloc.dart'; +import 'package:equatable/equatable.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..b74064a7be --- /dev/null +++ b/lib/core/upload/view/manifest_options/manifest_options.dart @@ -0,0 +1,516 @@ +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 + ? 204 + : showingName + ? 70 + : 50, + child: GestureDetector( + onTap: () {}, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + 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: !widget.isSelected || + (state.arnsNamesLoaded && state.ants!.isEmpty), + fontStyle: typography.paragraphSmall(), + variant: ButtonVariant.primary, + maxWidth: 100, + 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: [ + Row( + children: [ + _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(), + ], + ), + ), + ), + ), + ), + if (isNameAlreadyInUse && + widget.manifestSelection.antRecord?.domain != + _selectedAnt?.domain) + Padding( + padding: const EdgeInsets.only(top: 8, left: 8), + child: Text( + 'Name already in use, please choose another name or select a undername', + style: typography.paragraphSmall( + fontWeight: ArFontWeight.semiBold, + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 8, left: 8), + child: 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 Column( + children: [ + Text( + _selectedAnt?.domain ?? 'Choose ArNS name', + style: typography.paragraphSmall( + fontWeight: ArFontWeight.semiBold, + ), + ), + ], + ); + } + + 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); + + final colorTokens = ArDriveTheme.of(context).themeData.colorTokens; + + 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/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}'; } + From 55c101e8cedfe5d4b1bc5fd96949303410a12961 Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Tue, 29 Oct 2024 04:28:11 -0300 Subject: [PATCH 30/48] Update manifest_options.dart --- lib/core/upload/view/manifest_options/manifest_options.dart | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/core/upload/view/manifest_options/manifest_options.dart b/lib/core/upload/view/manifest_options/manifest_options.dart index b74064a7be..065019a7c6 100644 --- a/lib/core/upload/view/manifest_options/manifest_options.dart +++ b/lib/core/upload/view/manifest_options/manifest_options.dart @@ -438,8 +438,6 @@ class _AntSelectorState extends State { ArDriveDropdownItem _buildDropdownItem(BuildContext context, ANTRecord ant) { final typography = ArDriveTypographyNew.of(context); - final colorTokens = ArDriveTheme.of(context).themeData.colorTokens; - return ArDriveDropdownItem( content: SizedBox( width: 235, From a0d48a99d5bf3ba3d6ef77f4f0a89d05b9f61202 Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Tue, 29 Oct 2024 04:30:17 -0300 Subject: [PATCH 31/48] chore: fix lint issues --- .../create_manifest_cubit.dart | 2 +- lib/blocs/upload/upload_cubit.dart | 2 +- .../blocs/upload_manifest_options_bloc.dart | 4 +- .../get_literal_arns_record_name_test.dart | 4 +- .../assign_name_bloc_test.dart | 54 +++++++++---------- .../domain/manifest_repository_test.dart | 6 +-- 6 files changed, 36 insertions(+), 36 deletions(-) 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/upload/upload_cubit.dart b/lib/blocs/upload/upload_cubit.dart index f85c218b3e..1ee7243204 100644 --- a/lib/blocs/upload/upload_cubit.dart +++ b/lib/blocs/upload/upload_cubit.dart @@ -1403,7 +1403,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/core/upload/view/blocs/upload_manifest_options_bloc.dart b/lib/core/upload/view/blocs/upload_manifest_options_bloc.dart index 35747df3c0..784a3eca9b 100644 --- a/lib/core/upload/view/blocs/upload_manifest_options_bloc.dart +++ b/lib/core/upload/view/blocs/upload_manifest_options_bloc.dart @@ -2,8 +2,8 @@ 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:bloc/bloc.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'; @@ -95,7 +95,7 @@ class UploadManifestOptionsBloc emit(_createReadyState()); }); - on((event, emit) { + on((event, emit) { if (reservedNames[event.antRecord.domain] == null) { reservedNames[event.antRecord.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/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/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( From bede9f2fcc436f8180bfd758b8cb7ecca26c80c6 Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Tue, 29 Oct 2024 07:08:19 -0300 Subject: [PATCH 32/48] feat(assign name arns) - improve ui --- lib/components/upload_form.dart | 20 ++++- .../manifest_options/manifest_options.dart | 81 ++++++++++--------- 2 files changed, 60 insertions(+), 41 deletions(-) diff --git a/lib/components/upload_form.dart b/lib/components/upload_form.dart index 2978dacc04..15b422ed57 100644 --- a/lib/components/upload_form.dart +++ b/lib/components/upload_form.dart @@ -2031,6 +2031,12 @@ class _UploadReviewWithLicenseWidget extends StatelessWidget { final typography = ArDriveTypographyNew.of(context); final colorTokens = ArDriveTheme.of(context).themeData.colorTokens; + double heightForManifestSelections = 125; + + if (readyState.params.arnsUnderName == null) { + heightForManifestSelections += 45; + } + return StatsScreen( readyState: readyState, modalActions: [ @@ -2085,8 +2091,8 @@ class _UploadReviewWithLicenseWidget extends StatelessWidget { Padding( padding: const EdgeInsets.only(bottom: 16.0), child: ConstrainedBox( - constraints: const BoxConstraints( - maxHeight: 125, + constraints: BoxConstraints( + maxHeight: heightForManifestSelections, minWidth: kLargeDialogWidth, ), child: Column( @@ -2177,6 +2183,12 @@ class _UploadReviewWithArnsNameWidget extends StatelessWidget { final typography = ArDriveTypographyNew.of(context); final colorTokens = ArDriveTheme.of(context).themeData.colorTokens; + double heightForManifestSelections = 125; + + if (state.readyState.params.arnsUnderName == null) { + heightForManifestSelections += 45; + } + return StatsScreen( readyState: state.readyState, modalActions: [ @@ -2226,8 +2238,8 @@ class _UploadReviewWithArnsNameWidget extends StatelessWidget { Padding( padding: const EdgeInsets.only(bottom: 16.0), child: ConstrainedBox( - constraints: const BoxConstraints( - maxHeight: 125, + constraints: BoxConstraints( + maxHeight: heightForManifestSelections, minWidth: kLargeDialogWidth, ), child: Column( diff --git a/lib/core/upload/view/manifest_options/manifest_options.dart b/lib/core/upload/view/manifest_options/manifest_options.dart index 065019a7c6..be48f3e01f 100644 --- a/lib/core/upload/view/manifest_options/manifest_options.dart +++ b/lib/core/upload/view/manifest_options/manifest_options.dart @@ -100,7 +100,7 @@ class __ManifestOptionTileState extends State<_ManifestOptionTile> { curve: Curves.easeInOut, padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), height: isExpanded - ? 204 + ? 168 : showingName ? 70 : 50, @@ -108,7 +108,7 @@ class __ManifestOptionTileState extends State<_ManifestOptionTile> { onTap: () {}, child: Column( crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ Row( mainAxisSize: MainAxisSize.max, @@ -160,7 +160,6 @@ class __ManifestOptionTileState extends State<_ManifestOptionTile> { ], ), ), - ArDriveTooltip( message: (state.arnsNamesLoaded && state.ants!.isEmpty) ? 'No ARNS names found for your wallet' @@ -172,7 +171,8 @@ class __ManifestOptionTileState extends State<_ManifestOptionTile> { ? 'Change ArNS' : 'Add ArNS', typography: typography, - isDisabled: !widget.isSelected || + isDisabled: isExpanded || + !widget.isSelected || (state.arnsNamesLoaded && state.ants!.isEmpty), fontStyle: typography.paragraphSmall(), variant: ButtonVariant.primary, @@ -365,41 +365,48 @@ class _AntSelectorState extends State { ), ), ), - if (isNameAlreadyInUse && - widget.manifestSelection.antRecord?.domain != - _selectedAnt?.domain) - Padding( - padding: const EdgeInsets.only(top: 8, left: 8), - child: Text( - 'Name already in use, please choose another name or select a undername', - style: typography.paragraphSmall( - fontWeight: ArFontWeight.semiBold, + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (isNameAlreadyInUse && + widget.manifestSelection.antRecord?.domain != + _selectedAnt?.domain) + Expanded( + child: Padding( + padding: const EdgeInsets.only(top: 8, left: 8), + child: Text( + 'Name already in use, please choose another name or select a undername', + style: typography.paragraphSmall( + fontWeight: ArFontWeight.semiBold, + ), + ), + ), + ), + Padding( + padding: const EdgeInsets.only(top: 8, left: 8), + child: 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, + )); + }, + ), ), ), - ), - Padding( - padding: const EdgeInsets.only(top: 8, left: 8), - child: 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, - )); - }, - ), - ), + ], ), ], ); From d4d4628d7b65cbd48dc06bb78c512962a802d5ea Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Tue, 29 Oct 2024 08:46:05 -0300 Subject: [PATCH 33/48] feat(assign arns name): fix spacing issues --- lib/components/upload_form.dart | 90 ++++++++++--------- .../manifest_options/manifest_options.dart | 56 ++++++------ 2 files changed, 74 insertions(+), 72 deletions(-) diff --git a/lib/components/upload_form.dart b/lib/components/upload_form.dart index 15b422ed57..d51241c330 100644 --- a/lib/components/upload_form.dart +++ b/lib/components/upload_form.dart @@ -2031,10 +2031,15 @@ class _UploadReviewWithLicenseWidget extends StatelessWidget { final typography = ArDriveTypographyNew.of(context); final colorTokens = ArDriveTheme.of(context).themeData.colorTokens; - double heightForManifestSelections = 125; + double heightForManifestSelections = + (readyState.selectedManifestSelections.length * 30) + 16; + + if (heightForManifestSelections > 200) { + heightForManifestSelections = 175; + } if (readyState.params.arnsUnderName == null) { - heightForManifestSelections += 45; + heightForManifestSelections += 50; } return StatsScreen( @@ -2087,9 +2092,10 @@ class _UploadReviewWithLicenseWidget extends StatelessWidget { ), ), ], + 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: BoxConstraints( maxHeight: heightForManifestSelections, @@ -2100,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( @@ -2109,21 +2114,40 @@ class _UploadReviewWithLicenseWidget extends StatelessWidget { ), ), const SizedBox(height: 4), - Flexible( - child: Expanded( - child: ListView( - shrinkWrap: true, - children: [ - ...state.readyState.selectedManifestSelections.map( - (e) => Column( - children: [ + 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.manifest(size: 16), + ArDriveIcons.arnsName(size: 16), const SizedBox(width: 8), Flexible( child: Text( - e.manifest.name, + getLiteralArNSName( + e.antRecord!, e.undername), style: typography.paragraphNormal( fontWeight: ArFontWeight.semiBold, ), @@ -2133,41 +2157,18 @@ class _UploadReviewWithLicenseWidget extends StatelessWidget { ), ], ), - 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), ], ); } @@ -2183,10 +2184,15 @@ class _UploadReviewWithArnsNameWidget extends StatelessWidget { final typography = ArDriveTypographyNew.of(context); final colorTokens = ArDriveTheme.of(context).themeData.colorTokens; - double heightForManifestSelections = 125; + double heightForManifestSelections = + (state.readyState.selectedManifestSelections.length * 30) + 16; + + if (heightForManifestSelections > 200) { + heightForManifestSelections = 200; + } if (state.readyState.params.arnsUnderName == null) { - heightForManifestSelections += 45; + heightForManifestSelections += 50; } return StatsScreen( diff --git a/lib/core/upload/view/manifest_options/manifest_options.dart b/lib/core/upload/view/manifest_options/manifest_options.dart index be48f3e01f..ffe37f30cc 100644 --- a/lib/core/upload/view/manifest_options/manifest_options.dart +++ b/lib/core/upload/view/manifest_options/manifest_options.dart @@ -162,7 +162,7 @@ class __ManifestOptionTileState extends State<_ManifestOptionTile> { ), ArDriveTooltip( message: (state.arnsNamesLoaded && state.ants!.isEmpty) - ? 'No ARNS names found for your wallet' + ? 'No ArNS names found for your wallet' : '', child: ArDriveButtonNew( text: !state.arnsNamesLoaded @@ -313,11 +313,7 @@ class _AntSelectorState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ - Row( - children: [ - _buildSelectedItem(context), - ], - ), + Flexible(child: _buildSelectedItem(context)), ArDriveIcons.chevronDown(), ], ), @@ -365,15 +361,20 @@ class _AntSelectorState extends State { ), ), ), - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - if (isNameAlreadyInUse && - widget.manifestSelection.antRecord?.domain != - _selectedAnt?.domain) - Expanded( - child: Padding( - padding: const EdgeInsets.only(top: 8, left: 8), + 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( @@ -381,10 +382,7 @@ class _AntSelectorState extends State { ), ), ), - ), - Padding( - padding: const EdgeInsets.only(top: 8, left: 8), - child: Align( + Align( alignment: Alignment.centerRight, child: ArDriveButtonNew( text: 'Add', @@ -405,8 +403,8 @@ class _AntSelectorState extends State { }, ), ), - ), - ], + ], + ), ), ], ); @@ -419,15 +417,13 @@ class _AntSelectorState extends State { Widget _buildSelectedItem(BuildContext context) { final typography = ArDriveTypographyNew.of(context); - return Column( - children: [ - Text( - _selectedAnt?.domain ?? 'Choose ArNS name', - style: typography.paragraphSmall( - fontWeight: ArFontWeight.semiBold, - ), - ), - ], + return Text( + _selectedAnt?.domain ?? 'Choose ArNS name', + style: typography.paragraphSmall( + fontWeight: ArFontWeight.semiBold, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ); } From 393c452ffc6d7a6c935394917cf8075dfa7881cb Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Tue, 29 Oct 2024 09:12:37 -0300 Subject: [PATCH 34/48] Update manifest_options.dart --- lib/core/upload/view/manifest_options/manifest_options.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/core/upload/view/manifest_options/manifest_options.dart b/lib/core/upload/view/manifest_options/manifest_options.dart index ffe37f30cc..827c3927ac 100644 --- a/lib/core/upload/view/manifest_options/manifest_options.dart +++ b/lib/core/upload/view/manifest_options/manifest_options.dart @@ -173,6 +173,7 @@ class __ManifestOptionTileState extends State<_ManifestOptionTile> { typography: typography, isDisabled: isExpanded || !widget.isSelected || + !state.arnsNamesLoaded || (state.arnsNamesLoaded && state.ants!.isEmpty), fontStyle: typography.paragraphSmall(), variant: ButtonVariant.primary, From f02cacebfb96c8713dd0ca49f2856eede4a89f9b Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Tue, 29 Oct 2024 09:17:29 -0300 Subject: [PATCH 35/48] Update manifest_options.dart --- lib/core/upload/view/manifest_options/manifest_options.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/core/upload/view/manifest_options/manifest_options.dart b/lib/core/upload/view/manifest_options/manifest_options.dart index 827c3927ac..755957b05f 100644 --- a/lib/core/upload/view/manifest_options/manifest_options.dart +++ b/lib/core/upload/view/manifest_options/manifest_options.dart @@ -177,7 +177,7 @@ class __ManifestOptionTileState extends State<_ManifestOptionTile> { (state.arnsNamesLoaded && state.ants!.isEmpty), fontStyle: typography.paragraphSmall(), variant: ButtonVariant.primary, - maxWidth: 100, + maxWidth: state.arnsNamesLoaded ? 100 : 120, maxHeight: 30, onPressed: () { context From 11524a2e540b89796d3256c993e637b7e3c65589 Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Tue, 29 Oct 2024 09:32:56 -0300 Subject: [PATCH 36/48] Update upload_form.dart --- lib/components/upload_form.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/components/upload_form.dart b/lib/components/upload_form.dart index d51241c330..da7a9de389 100644 --- a/lib/components/upload_form.dart +++ b/lib/components/upload_form.dart @@ -2253,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( From 7bdfdba2697e80bc39ea28826b97611856069fe7 Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Tue, 29 Oct 2024 09:52:01 -0300 Subject: [PATCH 37/48] bump version and release notes --- .../fastlane/metadata/android/en-US/changelogs/159.txt | 8 ++++++++ pubspec.yaml | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 android/fastlane/metadata/android/en-US/changelogs/159.txt 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/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' From 51b7c73afc6a709712a0d9464856a4609a26f993 Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Tue, 29 Oct 2024 10:50:37 -0300 Subject: [PATCH 38/48] Update upload_cubit.dart --- lib/blocs/upload/upload_cubit.dart | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/lib/blocs/upload/upload_cubit.dart b/lib/blocs/upload/upload_cubit.dart index 1ee7243204..77c3fa50f0 100644 --- a/lib/blocs/upload/upload_cubit.dart +++ b/lib/blocs/upload/upload_cubit.dart @@ -144,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].entry.name, - folderId: _targetFolder.id, + manifestName: manifestFileEntry.name, + folderId: manifestFileEntry.parentFolderId, existingManifestFileId: manifestModels[i].existingManifestFileId, ); From 6bb07c75dc81d92f435d92e7ff37c3f4303b90c1 Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Tue, 29 Oct 2024 10:50:48 -0300 Subject: [PATCH 39/48] Update upload_cubit.dart --- lib/blocs/upload/upload_cubit.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/blocs/upload/upload_cubit.dart b/lib/blocs/upload/upload_cubit.dart index 77c3fa50f0..8a0d24eb16 100644 --- a/lib/blocs/upload/upload_cubit.dart +++ b/lib/blocs/upload/upload_cubit.dart @@ -147,7 +147,7 @@ class UploadCubit extends Cubit { final manifestFileEntry = await _driveDao .fileById( driveId: _driveId, - fileId: manifestModels[i].existingManifestFileId!, + fileId: manifestModels[i].existingManifestFileId, ) .getSingle(); From cb8a2f6407fc0dc3a4c121a10c1f5370bca52c8a Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Tue, 29 Oct 2024 10:56:05 -0300 Subject: [PATCH 40/48] Update upload_cubit.dart --- lib/blocs/upload/upload_cubit.dart | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/lib/blocs/upload/upload_cubit.dart b/lib/blocs/upload/upload_cubit.dart index 8a0d24eb16..61ce3c5054 100644 --- a/lib/blocs/upload/upload_cubit.dart +++ b/lib/blocs/upload/upload_cubit.dart @@ -150,7 +150,7 @@ class UploadCubit extends Cubit { fileId: manifestModels[i].existingManifestFileId, ) .getSingle(); - + await _createManifestCubit.prepareManifestTx( manifestName: manifestFileEntry.name, folderId: manifestFileEntry.parentFolderId, @@ -206,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].entry.name, - folderId: _targetFolder.id, + manifestName: manifestFileEntry.name, + folderId: manifestFileEntry.parentFolderId, existingManifestFileId: manifestModels[i].existingManifestFileId, ); From 17537570aa79a7f92931ac392820fb759259e29b Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Tue, 29 Oct 2024 11:13:13 -0300 Subject: [PATCH 41/48] Update drive_detail_page.dart dont open root of the drive if the current drive is the selected one --- lib/pages/drive_detail/drive_detail_page.dart | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/pages/drive_detail/drive_detail_page.dart b/lib/pages/drive_detail/drive_detail_page.dart index 691b06a7af..2f4f1df08f 100644 --- a/lib/pages/drive_detail/drive_detail_page.dart +++ b/lib/pages/drive_detail/drive_detail_page.dart @@ -133,6 +133,13 @@ class _DriveDetailPageState extends State { listener: (context, state) { if (state is DrivesLoadSuccess) { if (state.userDrives.isNotEmpty) { + final driveDetailState = context.read().state; + + if (driveDetailState is DriveDetailLoadSuccess && + driveDetailState.currentDrive.id == state.selectedDriveId) { + return; + } + context .read() .changeDrive(state.selectedDriveId!); From bd950d82d083f8bb9ac371890598d5b9a05badf6 Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Tue, 29 Oct 2024 11:47:16 -0300 Subject: [PATCH 42/48] Update drive_detail_cubit.dart --- lib/blocs/drive_detail/drive_detail_cubit.dart | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/lib/blocs/drive_detail/drive_detail_cubit.dart b/lib/blocs/drive_detail/drive_detail_cubit.dart index 7fd36f7425..9741d69764 100644 --- a/lib/blocs/drive_detail/drive_detail_cubit.dart +++ b/lib/blocs/drive_detail/drive_detail_cubit.dart @@ -146,6 +146,10 @@ class DriveDetailCubit extends Cubit { return; } + if (driveId != _driveId) { + return; + } + await _syncCubit.waitCurrentSync(); if (drive == null) { From 65bac9075d0b7f43cd0e32017d046a15453c1ce5 Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Tue, 29 Oct 2024 11:51:27 -0300 Subject: [PATCH 43/48] Update upload_form.dart --- lib/components/upload_form.dart | 72 ++++++++++++++++----------------- 1 file changed, 35 insertions(+), 37 deletions(-) diff --git a/lib/components/upload_form.dart b/lib/components/upload_form.dart index da7a9de389..d8878d0859 100644 --- a/lib/components/upload_form.dart +++ b/lib/components/upload_form.dart @@ -2032,7 +2032,7 @@ class _UploadReviewWithLicenseWidget extends StatelessWidget { final colorTokens = ArDriveTheme.of(context).themeData.colorTokens; double heightForManifestSelections = - (readyState.selectedManifestSelections.length * 30) + 16; + (readyState.selectedManifestSelections.length * 30) + 50; if (heightForManifestSelections > 200) { heightForManifestSelections = 175; @@ -2185,7 +2185,7 @@ class _UploadReviewWithArnsNameWidget extends StatelessWidget { final colorTokens = ArDriveTheme.of(context).themeData.colorTokens; double heightForManifestSelections = - (state.readyState.selectedManifestSelections.length * 30) + 16; + (state.readyState.selectedManifestSelections.length * 30) + 50; if (heightForManifestSelections > 200) { heightForManifestSelections = 200; @@ -2242,7 +2242,7 @@ class _UploadReviewWithArnsNameWidget extends StatelessWidget { ], if (state.readyState.selectedManifestSelections.isNotEmpty) ...[ Padding( - padding: const EdgeInsets.only(bottom: 16.0), + padding: const EdgeInsets.only(bottom: 8.0), child: ConstrainedBox( constraints: BoxConstraints( maxHeight: heightForManifestSelections, @@ -2261,21 +2261,40 @@ class _UploadReviewWithArnsNameWidget extends StatelessWidget { ), ), const SizedBox(height: 4), - Flexible( - child: Expanded( - child: ListView( - shrinkWrap: true, - children: [ - ...state.readyState.selectedManifestSelections.map( - (e) => Column( - children: [ + 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.manifest(size: 16), + ArDriveIcons.arnsName(size: 16), const SizedBox(width: 8), Flexible( child: Text( - e.manifest.name, + getLiteralArNSName( + e.antRecord!, e.undername), style: typography.paragraphNormal( fontWeight: ArFontWeight.semiBold, ), @@ -2285,32 +2304,11 @@ class _UploadReviewWithArnsNameWidget extends StatelessWidget { ), ], ), - 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, - ), - ), - ], - ), - ], ], - ), + ], ), - ], - ), + ), + ], ), ), ], From ce762d57aba0d8b1e0efe8e697d18fd610ef7a8b Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Tue, 29 Oct 2024 12:34:43 -0300 Subject: [PATCH 44/48] Update drive_detail_cubit.dart --- lib/blocs/drive_detail/drive_detail_cubit.dart | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/blocs/drive_detail/drive_detail_cubit.dart b/lib/blocs/drive_detail/drive_detail_cubit.dart index 9741d69764..7000807446 100644 --- a/lib/blocs/drive_detail/drive_detail_cubit.dart +++ b/lib/blocs/drive_detail/drive_detail_cubit.dart @@ -38,6 +38,7 @@ class DriveDetailCubit extends Cubit { final DriveRepository _driveRepository; StreamSubscription? _folderSubscription; + StreamController? _folderSubscriptionController; final _defaultAvailableRowsPerPage = [25, 50, 75, 100]; List _selectedItems = []; @@ -107,6 +108,8 @@ class DriveDetailCubit extends Cubit { return; } + await _folderSubscription?.cancel(); + _driveId = driveId; openFolder(folderId: drive.rootFolderId); @@ -147,6 +150,8 @@ class DriveDetailCubit extends Cubit { } if (driveId != _driveId) { + await _folderSubscription?.cancel(); + _folderSubscription = null; return; } From a8d4fade2ee84f0ec8b1a9c00a0f407b777432c4 Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Tue, 29 Oct 2024 13:36:01 -0300 Subject: [PATCH 45/48] fixes --- lib/blocs/drive_detail/drive_detail_cubit.dart | 8 ++------ lib/blocs/fs_entry_info/fs_entry_info_cubit.dart | 8 ++++++++ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/blocs/drive_detail/drive_detail_cubit.dart b/lib/blocs/drive_detail/drive_detail_cubit.dart index 7000807446..02b1e443f3 100644 --- a/lib/blocs/drive_detail/drive_detail_cubit.dart +++ b/lib/blocs/drive_detail/drive_detail_cubit.dart @@ -149,12 +149,6 @@ class DriveDetailCubit extends Cubit { return; } - if (driveId != _driveId) { - await _folderSubscription?.cancel(); - _folderSubscription = null; - return; - } - await _syncCubit.waitCurrentSync(); if (drive == null) { @@ -294,6 +288,8 @@ class DriveDetailCubit extends Cubit { logger.e('An error occured mouting the drive explorer', e); }); + + await _folderSubscription?.asFuture(); } List parseEntitiesToDatatableItem({ 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, From 3f65f247c2737d73f716fb0bf225e5a01c0899bb Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Tue, 29 Oct 2024 13:36:41 -0300 Subject: [PATCH 46/48] Update drive_detail_cubit.dart --- lib/blocs/drive_detail/drive_detail_cubit.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/blocs/drive_detail/drive_detail_cubit.dart b/lib/blocs/drive_detail/drive_detail_cubit.dart index 02b1e443f3..9427f29ee9 100644 --- a/lib/blocs/drive_detail/drive_detail_cubit.dart +++ b/lib/blocs/drive_detail/drive_detail_cubit.dart @@ -38,7 +38,6 @@ class DriveDetailCubit extends Cubit { final DriveRepository _driveRepository; StreamSubscription? _folderSubscription; - StreamController? _folderSubscriptionController; final _defaultAvailableRowsPerPage = [25, 50, 75, 100]; List _selectedItems = []; From 0de5189d0a909410cef219800ae7d6afcfc956ec Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Tue, 29 Oct 2024 13:58:13 -0300 Subject: [PATCH 47/48] Update drive_detail_cubit.dart --- lib/blocs/drive_detail/drive_detail_cubit.dart | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lib/blocs/drive_detail/drive_detail_cubit.dart b/lib/blocs/drive_detail/drive_detail_cubit.dart index 9427f29ee9..46b7948e07 100644 --- a/lib/blocs/drive_detail/drive_detail_cubit.dart +++ b/lib/blocs/drive_detail/drive_detail_cubit.dart @@ -104,6 +104,8 @@ class DriveDetailCubit extends Cubit { final drive = await _driveDao.driveById(driveId: driveId).getSingleOrNull(); if (drive == null) { + await _syncCubit.waitCurrentSync(); + emit(DriveDetailLoadNotFound()); return; } @@ -148,6 +150,10 @@ class DriveDetailCubit extends Cubit { return; } + if (driveId != _driveId) { + return; + } + await _syncCubit.waitCurrentSync(); if (drive == null) { From 9cbe520e5ada8a8c4eafa9bd0ae6ea7aee4de655 Mon Sep 17 00:00:00 2001 From: Thiago Carvalho <32248947+thiagocarvalhodev@users.noreply.github.com> Date: Tue, 29 Oct 2024 14:02:47 -0300 Subject: [PATCH 48/48] fix remove option to hide shared drives --- .../components/drive_explorer_item_tile.dart | 33 +++--- lib/pages/drive_detail/drive_detail_page.dart | 108 +++++++++--------- 2 files changed, 72 insertions(+), 69 deletions(-) diff --git a/lib/pages/drive_detail/components/drive_explorer_item_tile.dart b/lib/pages/drive_detail/components/drive_explorer_item_tile.dart index 8e9a4c50e8..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: () { @@ -756,22 +756,23 @@ class EntityActionsMenu extends StatelessWidget { ), ), ), - 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), + 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/drive_detail_page.dart b/lib/pages/drive_detail/drive_detail_page.dart index 2f4f1df08f..fdb05db351 100644 --- a/lib/pages/drive_detail/drive_detail_page.dart +++ b/lib/pages/drive_detail/drive_detail_page.dart @@ -514,36 +514,37 @@ class _DriveDetailPageState extends State { ), ), ), - 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, - ), + 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( @@ -1199,31 +1200,32 @@ class MobileFolderNavigation extends StatelessWidget { ), ), ), - 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, - ), + 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(