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 01/23] 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 02/23] 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 03/23] 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 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 04/23] 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 05/23] 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 06/23] 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 07/23] 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 08/23] 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 09/23] 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 10/23] 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 11/23] 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 12/23] 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 13/23] 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 14/23] 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 15/23] 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 16/23] 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 17/23] 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 18/23] 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 19/23] 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 20/23] 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 21/23] 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 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 22/23] 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 23/23] 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) {