From be58b3438bc6a06ab92281cdb05d59bbb2837a3d Mon Sep 17 00:00:00 2001 From: Mati Date: Fri, 5 Jan 2024 00:12:22 -0300 Subject: [PATCH 1/5] feat(drive details panel): adds a button to hide/unhide items; improves error handling; various generic improvements PE-5309 --- .../drive_create/drive_create_cubit.dart | 1 + .../drive_detail/drive_detail_cubit.dart | 40 ++++++---- .../drive_detail/drive_detail_state.dart | 3 +- .../folder_create/folder_create_cubit.dart | 1 + lib/blocs/hide/hide_bloc.dart | 18 ++++- lib/blocs/upload/upload_cubit.dart | 1 + .../folder_data_item_upload_handle.dart | 2 + lib/components/details_panel.dart | 80 +++++++++++++++++-- lib/components/hide_dialog.dart | 41 +++++++++- lib/pages/app_router_delegate.dart | 16 ++-- .../components/drive_detail_data_list.dart | 80 +++++++------------ .../components/drive_explorer_item_tile.dart | 22 ++++- lib/pages/drive_detail/drive_detail_page.dart | 32 ++++---- lib/pages/shared_file/shared_file_page.dart | 2 +- 14 files changed, 227 insertions(+), 112 deletions(-) diff --git a/lib/blocs/drive_create/drive_create_cubit.dart b/lib/blocs/drive_create/drive_create_cubit.dart index e6e6960717..66bb94e1e7 100644 --- a/lib/blocs/drive_create/drive_create_cubit.dart +++ b/lib/blocs/drive_create/drive_create_cubit.dart @@ -98,6 +98,7 @@ class DriveCreateCubit extends Cubit { id: drive.rootFolderId, driveId: drive.id, name: driveName, + isHidden: false, ); final rootFolderDataItem = await _arweave.prepareEntityDataItem( diff --git a/lib/blocs/drive_detail/drive_detail_cubit.dart b/lib/blocs/drive_detail/drive_detail_cubit.dart index 31bff1bf94..3ff3290b94 100644 --- a/lib/blocs/drive_detail/drive_detail_cubit.dart +++ b/lib/blocs/drive_detail/drive_detail_cubit.dart @@ -38,7 +38,7 @@ class DriveDetailCubit extends Cubit { bool _forceDisableMultiselect = false; - bool _refreshSelectedItem = false; + final bool _refreshSelectedItem = true; bool _showHiddenFiles = false; @@ -152,6 +152,10 @@ class DriveDetailCubit extends Cubit { final rootFolderNode = await _driveDao.getFolderTree(driveId, drive.rootFolderId); + if (_refreshSelectedItem) { + logger.d('Refreshing selected item: $_selectedItem'); + } + if (_selectedItem != null && _refreshSelectedItem) { if (_selectedItem is FileDataTableItem) { final index = folderContents.files.indexWhere( @@ -159,22 +163,30 @@ class DriveDetailCubit extends Cubit { ); if (index >= 0) { + final item = folderContents.files[index]; + _selectedItem = DriveDataTableItemMapper.toFileDataTableItem( - folderContents.files[index], + item, _selectedItem!.index, _selectedItem!.isOwner, ); + + logger.d('Selected file: $_selectedItem'); } } else if (_selectedItem is FolderDataTableItem) { final index = folderContents.subfolders.indexWhere( (element) => element.id == _selectedItem!.id, ); if (index >= 0) { + final item = folderContents.subfolders[index]; + _selectedItem = DriveDataTableItemMapper.fromFolderEntry( - folderContents.subfolders[index], + item, _selectedItem!.index, _selectedItem!.isOwner, ); + + logger.d('Selected folder: $_selectedItem'); } } else { _selectedItem = DriveDataTableItemMapper.fromDrive( @@ -185,7 +197,7 @@ class DriveDetailCubit extends Cubit { ); } - _refreshSelectedItem = false; + // _refreshSelectedItem = false; } final currentFolderContents = parseEntitiesToDatatableItem( @@ -193,6 +205,8 @@ class DriveDetailCubit extends Cubit { isOwner: isDriveOwner(_auth, drive.ownerAddress), ); + logger.d('Drive detail state with item: $_selectedItem'); + if (state != null) { emit( state.copyWith( @@ -408,22 +422,14 @@ class DriveDetailCubit extends Cubit { ); } - void refreshDriveDataTable({ - bool reComputeFolderContents = false, - }) { - _refreshSelectedItem = true; + void refreshDriveDataTable() { + logger.d('Asked to refresh drive data table'); + + // _refreshSelectedItem = true; if (state is DriveDetailLoadSuccess) { final state = this.state as DriveDetailLoadSuccess; - if (reComputeFolderContents) { - openFolder( - path: state.folderInView.folder.path, - contentOrderBy: state.contentOrderBy, - contentOrderingMode: state.contentOrderingMode, - ); - } else { - emit(state.copyWith()); - } + emit(state.copyWith()); } } diff --git a/lib/blocs/drive_detail/drive_detail_state.dart b/lib/blocs/drive_detail/drive_detail_state.dart index 5e5828c668..efae0f3f69 100644 --- a/lib/blocs/drive_detail/drive_detail_state.dart +++ b/lib/blocs/drive_detail/drive_detail_state.dart @@ -51,7 +51,7 @@ class DriveDetailLoadSuccess extends DriveDetailState { this.hasFoldersSelected = false, this.selectedFilePreviewUrl, required this.driveIsEmpty, - this.selectedItem, + required this.selectedItem, required this.currentFolderContents, required this.isShowingHiddenFiles, }); @@ -113,6 +113,7 @@ class DriveDetailLoadSuccess extends DriveDetailState { _equatableBust, driveIsEmpty, multiselect, + selectedItem, ]; SelectedItem? maybeSelectedItem() => selectedItems.isNotEmpty ? selectedItems.first : null; diff --git a/lib/blocs/folder_create/folder_create_cubit.dart b/lib/blocs/folder_create/folder_create_cubit.dart index f8ea3f950b..1eeb8de79e 100644 --- a/lib/blocs/folder_create/folder_create_cubit.dart +++ b/lib/blocs/folder_create/folder_create_cubit.dart @@ -74,6 +74,7 @@ class FolderCreateCubit extends Cubit { driveId: targetFolder.driveId, parentFolderId: targetFolder.id, name: folderName, + isHidden: false, ); if (_turboUploadService.useTurboUpload) { final folderDataItem = await _arweave.prepareEntityDataItem( diff --git a/lib/blocs/hide/hide_bloc.dart b/lib/blocs/hide/hide_bloc.dart index 05e8a3a3cf..abef7fe0ff 100644 --- a/lib/blocs/hide/hide_bloc.dart +++ b/lib/blocs/hide/hide_bloc.dart @@ -199,10 +199,18 @@ class HideBloc extends Bloc { )); } - await _computeCostEstimate(); - await _computeBalanceEstimate(); - _computeIsFreeThanksToTurbo(); - _computeIsSufficientBalance(); + try { + await _computeCostEstimate(); + await _computeBalanceEstimate(); + _computeIsFreeThanksToTurbo(); + _computeIsSufficientBalance(); + } catch (e) { + emit(const FailureHideState( + hideAction: HideAction.hideFolder, + message: 'Error while computing cost estimate', + )); + return; + } emit( ConfirmingHideState( @@ -318,6 +326,8 @@ class HideBloc extends Bloc { ); final folderEntity = newFolder.asEntity(); + logger.d('Unhiding folder with JSON: ${folderEntity.toJson()}'); + final driveKey = await _driveDao.getDriveKey( event.driveId, profile.cipherKey, diff --git a/lib/blocs/upload/upload_cubit.dart b/lib/blocs/upload/upload_cubit.dart index 7695ef8ace..ab8d70ed71 100644 --- a/lib/blocs/upload/upload_cubit.dart +++ b/lib/blocs/upload/upload_cubit.dart @@ -776,6 +776,7 @@ class UploadCubit extends Cubit { id: metadata.id, name: metadata.name, parentFolderId: metadata.parentFolderId, + isHidden: false, ); if (metadata.metadataTxId == null) { diff --git a/lib/blocs/upload/upload_handles/folder_data_item_upload_handle.dart b/lib/blocs/upload/upload_handles/folder_data_item_upload_handle.dart index dd646f274d..e5ada1875b 100644 --- a/lib/blocs/upload/upload_handles/folder_data_item_upload_handle.dart +++ b/lib/blocs/upload/upload_handles/folder_data_item_upload_handle.dart @@ -25,6 +25,7 @@ class FolderDataItemUploadHandle implements UploadHandle, DataItemHandle { driveId: targetDriveId, parentFolderId: folder.parentFolderId, name: folder.name, + isHidden: false, ).toJson(), ).codeUnits.length; @@ -58,6 +59,7 @@ class FolderDataItemUploadHandle implements UploadHandle, DataItemHandle { driveId: targetDriveId, parentFolderId: folder.parentFolderId, name: folder.name, + isHidden: false, ); folderEntityTx = await arweave.prepareEntityDataItem( diff --git a/lib/components/details_panel.dart b/lib/components/details_panel.dart index a2ac462539..fddd89a844 100644 --- a/lib/components/details_panel.dart +++ b/lib/components/details_panel.dart @@ -1,9 +1,12 @@ import 'package:ardrive/authentication/ardrive_auth.dart'; import 'package:ardrive/blocs/fs_entry_preview/fs_entry_preview_cubit.dart'; +import 'package:ardrive/blocs/hide/hide_bloc.dart'; +import 'package:ardrive/blocs/hide/hide_event.dart'; import 'package:ardrive/components/app_version_widget.dart'; import 'package:ardrive/components/components.dart'; import 'package:ardrive/components/dotted_line.dart'; import 'package:ardrive/components/drive_rename_form.dart'; +import 'package:ardrive/components/hide_dialog.dart'; import 'package:ardrive/components/pin_indicator.dart'; import 'package:ardrive/components/sizes.dart'; import 'package:ardrive/components/truncated_address.dart'; @@ -38,7 +41,7 @@ class DetailsPanel extends StatefulWidget { const DetailsPanel({ super.key, required this.item, - required this.maybeSelectedItem, + // required this.maybeSelectedItem, required this.drivePrivacy, this.revisions, this.fileKey, @@ -50,7 +53,7 @@ class DetailsPanel extends StatefulWidget { }); final ArDriveDataTableItem item; - final SelectedItem? maybeSelectedItem; + // final SelectedItem? maybeSelectedItem; final Privacy drivePrivacy; final List? revisions; final SecretKey? fileKey; @@ -68,7 +71,8 @@ class _DetailsPanelState extends State { @override Widget build(BuildContext context) { return MultiBlocProvider( - // Specify a key to ensure a new cubit is provided when the folder/file id changes. + // Specify a key to ensure a new cubit is provided when the folder/file id + // changes. key: ValueKey( '${widget.item.driveId}${widget.item.id}${widget.item.name}', ), @@ -225,8 +229,15 @@ class _DetailsPanelState extends State { ScreenTypeLayout.builder( desktop: (context) => Column( children: [ - DetailsPanelToolbar( - item: widget.item, + BlocBuilder( + builder: (context, driveDetailState) { + final driveDetailLoadSuccess = + driveDetailState as DriveDetailLoadSuccess; + return DetailsPanelToolbar( + item: widget.item, + driveDetailLoadSuccess: driveDetailLoadSuccess, + ); + }, ), const SizedBox( height: 24, @@ -1151,15 +1162,15 @@ class DetailsPanelToolbar extends StatelessWidget { const DetailsPanelToolbar({ super.key, required this.item, + required this.driveDetailLoadSuccess, }); final ArDriveDataTableItem item; + final DriveDetailLoadSuccess driveDetailLoadSuccess; @override Widget build(BuildContext context) { - final drive = - (context.read().state as DriveDetailLoadSuccess) - .currentDrive; + final drive = driveDetailLoadSuccess.currentDrive; return Container( padding: const EdgeInsets.symmetric(vertical: 12), @@ -1259,6 +1270,59 @@ class DetailsPanelToolbar extends StatelessWidget { promptToMove(context, driveId: drive.id, selectedItems: [item]); }, ), + if (item.isOwner) + _buildActionIcon( + tooltip: item.isHidden ? 'Unhide' : 'Hide', + icon: item.isHidden + ? ArDriveIcons.eyeClosed(size: defaultIconSize) + : ArDriveIcons.eyeOpen(size: defaultIconSize), + onTap: () { + final hideBloc = context.read(); + final driveDetailCubit = context.read(); + + if (item is FileDataTableItem) { + if (item.isHidden) { + hideBloc.add(UnhideFileEvent( + driveId: item.driveId, + fileId: item.id, + )); + promptToHide( + context, + driveDetailCubit: driveDetailCubit, + ); + } else { + hideBloc.add(HideFileEvent( + driveId: item.driveId, + fileId: item.id, + )); + promptToHide( + context, + driveDetailCubit: driveDetailCubit, + ); + } + } else if (item is FolderDataTableItem) { + if (item.isHidden) { + hideBloc.add(UnhideFolderEvent( + driveId: item.driveId, + folderId: item.id, + )); + promptToHide( + context, + driveDetailCubit: driveDetailCubit, + ); + } else { + hideBloc.add(HideFolderEvent( + driveId: item.driveId, + folderId: item.id, + )); + promptToHide( + context, + driveDetailCubit: driveDetailCubit, + ); + } + } + }, + ), const Spacer(), _buildActionIcon( tooltip: appLocalizationsOf(context).close, diff --git a/lib/components/hide_dialog.dart b/lib/components/hide_dialog.dart index d665038065..bb0af3aa27 100644 --- a/lib/components/hide_dialog.dart +++ b/lib/components/hide_dialog.dart @@ -1,22 +1,31 @@ +import 'package:ardrive/blocs/drive_detail/drive_detail_cubit.dart'; import 'package:ardrive/blocs/hide/hide_bloc.dart'; import 'package:ardrive/blocs/hide/hide_event.dart'; import 'package:ardrive/blocs/hide/hide_state.dart'; +import 'package:ardrive/utils/app_localizations_wrapper.dart'; +import 'package:ardrive/utils/logger/logger.dart'; import 'package:ardrive_ui/ardrive_ui.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; Future promptToHide( - BuildContext context, -) async { + BuildContext context, { + required DriveDetailCubit driveDetailCubit, +}) async { return showAnimatedDialog( context, barrierDismissible: false, - content: const HideDialog(), + content: HideDialog(driveDetailCubit: driveDetailCubit), ); } class HideDialog extends StatelessWidget { - const HideDialog({super.key}); + final DriveDetailCubit _driveDetailCubit; + + const HideDialog({ + super.key, + required DriveDetailCubit driveDetailCubit, + }) : _driveDetailCubit = driveDetailCubit; @override Widget build(BuildContext context) { @@ -24,7 +33,12 @@ class HideDialog extends StatelessWidget { listener: (context, state) { if (state is SuccessHideState) { Navigator.of(context).pop(); + + // _driveDetailCubit.refreshDriveDataTable(); + + logger.d('Successfully hid entity'); } else if (state is ConfirmingHideState) { + _driveDetailCubit.refreshDriveDataTable(); context.read().add(const ConfirmUploadEvent()); } }, @@ -32,6 +46,7 @@ class HideDialog extends StatelessWidget { return ArDriveStandardModal( title: _buildTitle(context, state), content: _buildContent(context, state), + actions: _buildActions(context, state), ); }, ); @@ -66,4 +81,22 @@ class HideDialog extends StatelessWidget { ], ); } + + List? _buildActions( + BuildContext context, + HideState state, + ) { + if (state is FailureHideState) { + return [ + ModalAction( + action: () { + Navigator.of(context).pop(); + }, + title: appLocalizationsOf(context).cancel, + ), + ]; + } else { + return null; + } + } } diff --git a/lib/pages/app_router_delegate.dart b/lib/pages/app_router_delegate.dart index 8a7e4fef9b..141d06f7ed 100644 --- a/lib/pages/app_router_delegate.dart +++ b/lib/pages/app_router_delegate.dart @@ -192,14 +192,20 @@ class AppRouterDelegate extends RouterDelegate child: MultiBlocListener( listeners: [ BlocListener( - listener: (context, state) { - if (state is DriveDetailLoadSuccess) { - driveId = state.currentDrive.id; - driveFolderId = state.folderInView.folder.id; + listener: (context, driveDetailCubitState) { + logger.d( + 'DriveDetailCubit state at app_router: $driveDetailCubitState', + ); + + if (driveDetailCubitState is DriveDetailLoadSuccess) { + driveId = driveDetailCubitState.currentDrive.id; + driveFolderId = + driveDetailCubitState.folderInView.folder.id; //Can be null at the root folder of the drive notifyListeners(); - } else if (state is DriveDetailLoadNotFound) { + } else if (driveDetailCubitState + is DriveDetailLoadNotFound) { // Do not prompt the user to attach an unfound drive if they are logging out. final profileCubit = context.read(); diff --git a/lib/pages/drive_detail/components/drive_detail_data_list.dart b/lib/pages/drive_detail/components/drive_detail_data_list.dart index b73a46c66b..e66637b978 100644 --- a/lib/pages/drive_detail/components/drive_detail_data_list.dart +++ b/lib/pages/drive_detail/components/drive_detail_data_list.dart @@ -4,6 +4,8 @@ Widget _buildDataList( BuildContext context, DriveDetailLoadSuccess state, ) { + // logger.d('Building data list for state: $state'); + return _buildDataListContent( context, state.currentFolderContents, @@ -11,6 +13,7 @@ Widget _buildDataList( state.currentDrive, isMultiselecting: state.multiselect, isShowingHiddenFiles: state.isShowingHiddenFiles, + selectedItem: state.selectedItem, ); } @@ -66,32 +69,20 @@ class FolderDataTableItem extends ArDriveDataTableItem { final bool isGhostFolder; FolderDataTableItem({ - required String driveId, + required super.driveId, required String folderId, - required String name, - required DateTime lastUpdated, - required DateTime dateCreated, - required String contentType, - required String path, - String? fileStatusFromTransactions, + required super.name, + required super.lastUpdated, + required super.dateCreated, + required super.contentType, + required super.path, + super.fileStatusFromTransactions, + required super.isHidden, + required super.index, + required super.isOwner, this.parentFolderId, this.isGhostFolder = false, - required super.isHidden, - required int index, - required bool isOwner, - }) : super( - driveId: driveId, - path: path, - id: folderId, - name: name, - size: null, - lastUpdated: lastUpdated, - dateCreated: dateCreated, - contentType: contentType, - fileStatusFromTransactions: fileStatusFromTransactions, - index: index, - isOwner: isOwner, - ); + }) : super(id: folderId); @override List get props => [id, name, isHidden]; @@ -108,38 +99,26 @@ class FileDataTableItem extends ArDriveDataTableItem { final String? pinnedDataOwnerAddress; FileDataTableItem({ + required super.driveId, + required super.lastUpdated, + required super.name, + required super.size, + required super.dateCreated, + required super.contentType, + required super.path, + required super.isHidden, + super.fileStatusFromTransactions, + required super.index, + required super.isOwner, required this.fileId, - required String driveId, required this.parentFolderId, required this.dataTxId, - required DateTime lastUpdated, required this.lastModifiedDate, required this.metadataTx, required this.dataTx, required this.pinnedDataOwnerAddress, - required String name, - required int size, - required DateTime dateCreated, - required String contentType, - required String path, - required super.isHidden, - String? fileStatusFromTransactions, this.bundledIn, - required int index, - required bool isOwner, - }) : super( - path: path, - driveId: driveId, - id: fileId, - name: name, - size: size, - lastUpdated: lastUpdated, - dateCreated: dateCreated, - contentType: contentType, - fileStatusFromTransactions: fileStatusFromTransactions, - index: index, - isOwner: isOwner, - ); + }) : super(id: fileId); @override List get props => [fileId, name, isHidden]; @@ -152,6 +131,7 @@ Widget _buildDataListContent( Drive drive, { required bool isMultiselecting, required bool isShowingHiddenFiles, + required ArDriveDataTableItem? selectedItem, }) { final List filteredItems; if (isShowingHiddenFiles) { @@ -263,15 +243,15 @@ Widget _buildDataListContent( } else if (row is FileDataTableItem) { if (row.id == cubit.selectedItem?.id) { cubit.toggleSelectedItemDetails(); - return; + } else { + cubit.selectDataItem(row); } - cubit.selectDataItem(row); } }, ); }, rows: filteredItems, - selectedRow: context.watch().selectedItem, + selectedRow: 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 4e7ed7459d..f3e0474a9d 100644 --- a/lib/pages/drive_detail/components/drive_explorer_item_tile.dart +++ b/lib/pages/drive_detail/components/drive_explorer_item_tile.dart @@ -319,19 +319,26 @@ class _DriveExplorerItemTileTrailingState ArDriveDropdownItem( onClick: () { final hideBloc = context.read(); + final driveDetailCubit = context.read(); if (item.isHidden) { hideBloc.add(UnhideFolderEvent( driveId: widget.drive.id, folderId: item.id, )); - promptToHide(context); + promptToHide( + context, + driveDetailCubit: driveDetailCubit, + ); } else { hideBloc.add(HideFolderEvent( driveId: widget.drive.id, folderId: item.id, )); - promptToHide(context); + promptToHide( + context, + driveDetailCubit: driveDetailCubit, + ); } }, content: _buildItem( @@ -436,19 +443,26 @@ class _DriveExplorerItemTileTrailingState ArDriveDropdownItem( onClick: () { final hideBloc = context.read(); + final driveDetailCubit = context.read(); if (item.isHidden) { hideBloc.add(UnhideFileEvent( driveId: widget.drive.id, fileId: item.id, )); - promptToHide(context); + promptToHide( + context, + driveDetailCubit: driveDetailCubit, + ); } else { hideBloc.add(HideFileEvent( driveId: widget.drive.id, fileId: item.id, )); - promptToHide(context); + promptToHide( + context, + driveDetailCubit: driveDetailCubit, + ); } }, content: _buildItem( diff --git a/lib/pages/drive_detail/drive_detail_page.dart b/lib/pages/drive_detail/drive_detail_page.dart index 1fad9390ad..7aaa4183c5 100644 --- a/lib/pages/drive_detail/drive_detail_page.dart +++ b/lib/pages/drive_detail/drive_detail_page.dart @@ -448,6 +448,7 @@ class _DriveDetailPageState extends State { child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ + // Hey, mati Expanded( child: _buildDataList( context, @@ -484,20 +485,15 @@ class _DriveDetailPageState extends State { driveDetailState, context), ), child: driveDetailState.showSelectedItemDetails && - context - .read() - .selectedItem != - null + driveDetailState.selectedItem != null ? DetailsPanel( currentDrive: driveDetailState.currentDrive, isSharePage: false, drivePrivacy: driveDetailState.currentDrive.privacy, - maybeSelectedItem: - driveDetailState.maybeSelectedItem(), - item: context - .read() - .selectedItem!, + // maybeSelectedItem: + // driveDetailState.maybeSelectedItem(), + item: driveDetailState.selectedItem!, onNextImageNavigation: () { context .read() @@ -549,13 +545,13 @@ class _DriveDetailPageState extends State { } Widget _mobileView( - DriveDetailLoadSuccess state, + DriveDetailLoadSuccess driveDetailLoadSuccessState, bool hasSubfolders, bool hasFiles, ) { - final items = state.currentFolderContents; + final items = driveDetailLoadSuccessState.currentFolderContents; - if (state.showSelectedItemDetails && + if (driveDetailLoadSuccessState.showSelectedItemDetails && context.read().selectedItem != null) { return Material( child: WillPopScope( @@ -564,11 +560,11 @@ class _DriveDetailPageState extends State { return false; }, child: DetailsPanel( - currentDrive: state.currentDrive, + currentDrive: driveDetailLoadSuccessState.currentDrive, isSharePage: false, - drivePrivacy: state.currentDrive.privacy, - maybeSelectedItem: state.maybeSelectedItem(), - item: context.read().selectedItem!, + drivePrivacy: driveDetailLoadSuccessState.currentDrive.privacy, + // maybeSelectedItem: driveDetailLoadSuccessState.maybeSelectedItem(), + item: driveDetailLoadSuccessState.selectedItem!, onNextImageNavigation: () { context.read().selectNextImage(); }, @@ -590,7 +586,7 @@ class _DriveDetailPageState extends State { .withOpacity(0.5), drawer: const AppSideBar(), appBar: MobileAppBar( - leading: (state.showSelectedItemDetails && + leading: (driveDetailLoadSuccessState.showSelectedItemDetails && context.read().selectedItem != null) ? ArDriveIconButton( icon: ArDriveIcons.arrowLeft(), @@ -616,7 +612,7 @@ class _DriveDetailPageState extends State { }, ), body: _mobileViewContent( - state, + driveDetailLoadSuccessState, hasSubfolders, hasFiles, items, diff --git a/lib/pages/shared_file/shared_file_page.dart b/lib/pages/shared_file/shared_file_page.dart index 94bd8b86e3..0d417a3c06 100644 --- a/lib/pages/shared_file/shared_file_page.dart +++ b/lib/pages/shared_file/shared_file_page.dart @@ -52,7 +52,7 @@ class SharedFilePage extends StatelessWidget { false, ), isSharePage: true, - maybeSelectedItem: null, + // maybeSelectedItem: null, fileKey: state.fileKey, revisions: state.fileRevisions, drivePrivacy: state.fileKey != null ? 'private' : 'public', From c03dab874baca16a928b8df57565a44f63287531 Mon Sep 17 00:00:00 2001 From: Mati Date: Fri, 5 Jan 2024 00:14:38 -0300 Subject: [PATCH 2/5] test(entity version tag): updates failing tests PE-5309 --- test/models/entity_version_tag_test.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/test/models/entity_version_tag_test.dart b/test/models/entity_version_tag_test.dart index ad42ba9934..e1817aea4c 100644 --- a/test/models/entity_version_tag_test.dart +++ b/test/models/entity_version_tag_test.dart @@ -164,6 +164,7 @@ void main() { driveId: driveId, parentFolderId: rootFolderId, name: testEntityName, + isHidden: false, ); AppPlatform.setMockPlatform(platform: SystemPlatform.unknown); From 340bca182f93230b7efe35abf5d4bf5daece71cf Mon Sep 17 00:00:00 2001 From: Mati Date: Fri, 5 Jan 2024 11:40:31 -0300 Subject: [PATCH 3/5] chore(hygiene): cleans up the code PE-5309 --- lib/blocs/drive_detail/drive_detail_cubit.dart | 14 -------------- lib/components/details_panel.dart | 2 -- lib/components/hide_dialog.dart | 5 +---- lib/pages/app_router_delegate.dart | 4 ---- .../components/drive_detail_data_list.dart | 2 -- lib/pages/drive_detail/drive_detail_page.dart | 4 ---- lib/pages/shared_file/shared_file_page.dart | 1 - 7 files changed, 1 insertion(+), 31 deletions(-) diff --git a/lib/blocs/drive_detail/drive_detail_cubit.dart b/lib/blocs/drive_detail/drive_detail_cubit.dart index 3ff3290b94..81ae57ebef 100644 --- a/lib/blocs/drive_detail/drive_detail_cubit.dart +++ b/lib/blocs/drive_detail/drive_detail_cubit.dart @@ -152,10 +152,6 @@ class DriveDetailCubit extends Cubit { final rootFolderNode = await _driveDao.getFolderTree(driveId, drive.rootFolderId); - if (_refreshSelectedItem) { - logger.d('Refreshing selected item: $_selectedItem'); - } - if (_selectedItem != null && _refreshSelectedItem) { if (_selectedItem is FileDataTableItem) { final index = folderContents.files.indexWhere( @@ -170,8 +166,6 @@ class DriveDetailCubit extends Cubit { _selectedItem!.index, _selectedItem!.isOwner, ); - - logger.d('Selected file: $_selectedItem'); } } else if (_selectedItem is FolderDataTableItem) { final index = folderContents.subfolders.indexWhere( @@ -185,8 +179,6 @@ class DriveDetailCubit extends Cubit { _selectedItem!.index, _selectedItem!.isOwner, ); - - logger.d('Selected folder: $_selectedItem'); } } else { _selectedItem = DriveDataTableItemMapper.fromDrive( @@ -196,8 +188,6 @@ class DriveDetailCubit extends Cubit { _selectedItem!.isOwner, ); } - - // _refreshSelectedItem = false; } final currentFolderContents = parseEntitiesToDatatableItem( @@ -205,8 +195,6 @@ class DriveDetailCubit extends Cubit { isOwner: isDriveOwner(_auth, drive.ownerAddress), ); - logger.d('Drive detail state with item: $_selectedItem'); - if (state != null) { emit( state.copyWith( @@ -423,8 +411,6 @@ class DriveDetailCubit extends Cubit { } void refreshDriveDataTable() { - logger.d('Asked to refresh drive data table'); - // _refreshSelectedItem = true; if (state is DriveDetailLoadSuccess) { diff --git a/lib/components/details_panel.dart b/lib/components/details_panel.dart index 3de1eb7edb..8d15ef9b46 100644 --- a/lib/components/details_panel.dart +++ b/lib/components/details_panel.dart @@ -41,7 +41,6 @@ class DetailsPanel extends StatefulWidget { const DetailsPanel({ super.key, required this.item, - // required this.maybeSelectedItem, required this.drivePrivacy, this.revisions, this.fileKey, @@ -53,7 +52,6 @@ class DetailsPanel extends StatefulWidget { }); final ArDriveDataTableItem item; - // final SelectedItem? maybeSelectedItem; final Privacy drivePrivacy; final List? revisions; final SecretKey? fileKey; diff --git a/lib/components/hide_dialog.dart b/lib/components/hide_dialog.dart index edce46407d..8a45650058 100644 --- a/lib/components/hide_dialog.dart +++ b/lib/components/hide_dialog.dart @@ -33,10 +33,7 @@ class HideDialog extends StatelessWidget { listener: (context, state) { if (state is SuccessHideState) { Navigator.of(context).pop(); - - // _driveDetailCubit.refreshDriveDataTable(); - - logger.d('Successfully hid entity'); + logger.d('Successfully hid/unhid entity'); } else if (state is ConfirmingHideState) { _driveDetailCubit.refreshDriveDataTable(); context.read().add(const ConfirmUploadEvent()); diff --git a/lib/pages/app_router_delegate.dart b/lib/pages/app_router_delegate.dart index 141d06f7ed..9406dcf639 100644 --- a/lib/pages/app_router_delegate.dart +++ b/lib/pages/app_router_delegate.dart @@ -193,10 +193,6 @@ class AppRouterDelegate extends RouterDelegate listeners: [ BlocListener( listener: (context, driveDetailCubitState) { - logger.d( - 'DriveDetailCubit state at app_router: $driveDetailCubitState', - ); - if (driveDetailCubitState is DriveDetailLoadSuccess) { driveId = driveDetailCubitState.currentDrive.id; driveFolderId = 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 7f755e4729..4df7be28d9 100644 --- a/lib/pages/drive_detail/components/drive_detail_data_list.dart +++ b/lib/pages/drive_detail/components/drive_detail_data_list.dart @@ -4,8 +4,6 @@ Widget _buildDataList( BuildContext context, DriveDetailLoadSuccess state, ) { - // logger.d('Building data list for state: $state'); - return _buildDataListContent( context, state.currentFolderContents, diff --git a/lib/pages/drive_detail/drive_detail_page.dart b/lib/pages/drive_detail/drive_detail_page.dart index ceeb41538e..99c66d3043 100644 --- a/lib/pages/drive_detail/drive_detail_page.dart +++ b/lib/pages/drive_detail/drive_detail_page.dart @@ -490,7 +490,6 @@ class _DriveDetailPageState extends State { child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Hey, mati Expanded( child: _buildDataList( context, @@ -533,8 +532,6 @@ class _DriveDetailPageState extends State { isSharePage: false, drivePrivacy: driveDetailState.currentDrive.privacy, - // maybeSelectedItem: - // driveDetailState.maybeSelectedItem(), item: driveDetailState.selectedItem!, onNextImageNavigation: () { context @@ -605,7 +602,6 @@ class _DriveDetailPageState extends State { currentDrive: driveDetailLoadSuccessState.currentDrive, isSharePage: false, drivePrivacy: driveDetailLoadSuccessState.currentDrive.privacy, - // maybeSelectedItem: driveDetailLoadSuccessState.maybeSelectedItem(), item: driveDetailLoadSuccessState.selectedItem!, onNextImageNavigation: () { context.read().selectNextImage(); diff --git a/lib/pages/shared_file/shared_file_page.dart b/lib/pages/shared_file/shared_file_page.dart index 93c45d843d..15abfe2085 100644 --- a/lib/pages/shared_file/shared_file_page.dart +++ b/lib/pages/shared_file/shared_file_page.dart @@ -52,7 +52,6 @@ class SharedFilePage extends StatelessWidget { false, ), isSharePage: true, - // maybeSelectedItem: null, fileKey: state.fileKey, revisions: state.fileRevisions, drivePrivacy: state.fileKey != null ? 'private' : 'public', From 2fe2208e7c84a9ac373e94bf7e21deddbf85c9a0 Mon Sep 17 00:00:00 2001 From: Mati Date: Fri, 5 Jan 2024 12:35:41 -0300 Subject: [PATCH 4/5] feat(hide feature): sets up error handling PE-5309 --- lib/blocs/hide/hide_bloc.dart | 34 +++++++++++++++--------- lib/blocs/hide/hide_event.dart | 19 +++++++++++++ lib/blocs/hide/hide_state.dart | 11 +++----- lib/components/hide_dialog.dart | 47 +++++++++++++++++++++++++++++---- 4 files changed, 87 insertions(+), 24 deletions(-) diff --git a/lib/blocs/hide/hide_bloc.dart b/lib/blocs/hide/hide_bloc.dart index abef7fe0ff..443a86ed22 100644 --- a/lib/blocs/hide/hide_bloc.dart +++ b/lib/blocs/hide/hide_bloc.dart @@ -72,6 +72,7 @@ class HideBloc extends Bloc { on(_onConfirmUploadEvent); on(_onSelectUploadMethodEvent); on(_refreshTurboBalance); + on(_onErrorEvent); } bool get _useTurboUpload => @@ -199,18 +200,10 @@ class HideBloc extends Bloc { )); } - try { - await _computeCostEstimate(); - await _computeBalanceEstimate(); - _computeIsFreeThanksToTurbo(); - _computeIsSufficientBalance(); - } catch (e) { - emit(const FailureHideState( - hideAction: HideAction.hideFolder, - message: 'Error while computing cost estimate', - )); - return; - } + await _computeCostEstimate(); + await _computeBalanceEstimate(); + _computeIsFreeThanksToTurbo(); + _computeIsSufficientBalance(); emit( ConfirmingHideState( @@ -428,6 +421,13 @@ class HideBloc extends Bloc { ); } + void _onErrorEvent( + ErrorEvent event, + Emitter emit, + ) { + emit(FailureHideState(hideAction: event.hideAction)); + } + void _computeIsFreeThanksToTurbo() { final allowedDataItemSizeForTurbo = _appConfig.allowedDataItemSizeForTurbo; final forceNoFreeThanksToTurbo = _appConfig.forceNoFreeThanksToTurbo; @@ -563,4 +563,14 @@ class HideBloc extends Bloc { totalSize: _totalSize, ); } + + @override + void onError(Object error, StackTrace stackTrace) { + add(ErrorEvent( + error: error, + stackTrace: stackTrace, + hideAction: state.hideAction, + )); + super.onError(error, stackTrace); + } } diff --git a/lib/blocs/hide/hide_event.dart b/lib/blocs/hide/hide_event.dart index ef81ea9633..7e5049a3fe 100644 --- a/lib/blocs/hide/hide_event.dart +++ b/lib/blocs/hide/hide_event.dart @@ -1,3 +1,4 @@ +import 'package:ardrive/blocs/hide/hide_state.dart'; import 'package:ardrive/blocs/upload/upload_cubit.dart'; import 'package:ardrive_utils/ardrive_utils.dart'; import 'package:equatable/equatable.dart'; @@ -82,3 +83,21 @@ class RefreshTurboBalanceEvent extends HideEvent { @override List get props => []; } + +class ErrorEvent extends HideEvent { + final Object error; + final StackTrace stackTrace; + final HideAction hideAction; + + const ErrorEvent({ + required this.error, + required this.stackTrace, + required this.hideAction, + }); + + @override + List get props => [ + error, + stackTrace, + ]; +} diff --git a/lib/blocs/hide/hide_state.dart b/lib/blocs/hide/hide_state.dart index 4cc6e4e476..588a9d9fb4 100644 --- a/lib/blocs/hide/hide_state.dart +++ b/lib/blocs/hide/hide_state.dart @@ -24,7 +24,7 @@ class UploadingHideState extends HideState { }); @override - List get props => []; + List get props => []; } class PreparingAndSigningHideState extends HideState { @@ -33,7 +33,7 @@ class PreparingAndSigningHideState extends HideState { }); @override - List get props => []; + List get props => []; } class ConfirmingHideState extends HideState { @@ -127,19 +127,16 @@ class SuccessHideState extends HideState { }); @override - List get props => []; + List get props => []; } class FailureHideState extends HideState { - final String message; - const FailureHideState({ - required this.message, required super.hideAction, }); @override - List get props => [message]; + List get props => []; } enum HideAction { diff --git a/lib/components/hide_dialog.dart b/lib/components/hide_dialog.dart index 8a45650058..f35d80876a 100644 --- a/lib/components/hide_dialog.dart +++ b/lib/components/hide_dialog.dart @@ -41,15 +41,29 @@ class HideDialog extends StatelessWidget { }, builder: (context, state) { return ArDriveStandardModal( - title: _buildTitle(state.hideAction), - content: _buildContent(), + title: _buildTitle(state), + content: _buildContent(state), actions: _buildActions(context, state), ); }, ); } - String _buildTitle(HideAction hideAction) { + String _buildTitle(HideState state) { + final hideAction = state.hideAction; + if (state is FailureHideState) { + switch (hideAction) { + case HideAction.hideFile: + return 'Failed to hide file'; // TODO: localize + case HideAction.hideFolder: + return 'Failed to hide folder'; // TODO: localize + case HideAction.unhideFile: + return 'Failed to unhide file'; // TODO: localize + case HideAction.unhideFolder: + return 'Failed to unhide folder'; // TODO: localize + } + } + switch (hideAction) { case HideAction.hideFile: return 'Hiding file'; // TODO: localize @@ -62,7 +76,30 @@ class HideDialog extends StatelessWidget { } } - Widget _buildContent() { + Widget _buildContent(HideState state) { + if (state is FailureHideState) { + final hideAction = state.hideAction; + + switch (hideAction) { + case HideAction.hideFile: + return const Text( + 'Failed to hide file, please try again', // TODO: localize + ); + case HideAction.hideFolder: + return const Text( + 'Failed to hide folder, please try again', // TODO: localize + ); + case HideAction.unhideFile: + return const Text( + 'Failed to unhide file, please try again', // TODO: localize + ); + case HideAction.unhideFolder: + return const Text( + 'Failed to unhide folder, please try again', // TODO: localize + ); + } + } + return const Column( children: [ Center( @@ -82,7 +119,7 @@ class HideDialog extends StatelessWidget { action: () { Navigator.of(context).pop(); }, - title: appLocalizationsOf(context).cancel, + title: appLocalizationsOf(context).close, ), ]; } else { From 0e4f36c054b950c805495922c1219655e211464f Mon Sep 17 00:00:00 2001 From: Mati Date: Fri, 5 Jan 2024 14:21:33 -0300 Subject: [PATCH 5/5] feat(hide dialog): adds localizations PE-5309 --- lib/components/details_panel.dart | 16 ++++--- lib/components/hide_dialog.dart | 40 +++++++--------- lib/l10n/app_en.arb | 80 +++++++++++++++++++++++++++++++ 3 files changed, 105 insertions(+), 31 deletions(-) diff --git a/lib/components/details_panel.dart b/lib/components/details_panel.dart index 8d15ef9b46..4986dd6967 100644 --- a/lib/components/details_panel.dart +++ b/lib/components/details_panel.dart @@ -807,10 +807,10 @@ class _DetailsPanelState extends State { title = appLocalizationsOf(context).folderWasMoved; break; case RevisionAction.hide: - title = 'This folder was hidden'; + title = appLocalizationsOf(context).folderWasHidden; break; case RevisionAction.unhide: - title = 'This folder was unhidden'; + title = appLocalizationsOf(context).folderWasUnhidden; break; default: title = appLocalizationsOf(context).folderWasModified; @@ -837,10 +837,10 @@ class _DetailsPanelState extends State { .driveWasRenamed(revision.name); break; case RevisionAction.hide: - title = 'This drive was hidden'; + title = appLocalizationsOf(context).driveWasHidden; break; case RevisionAction.unhide: - title = 'This drive was unhidden'; + title = appLocalizationsOf(context).driveWasUnhidden; break; default: title = appLocalizationsOf(context).driveWasModified; @@ -901,10 +901,10 @@ class _DetailsPanelState extends State { ); break; case RevisionAction.hide: - title = 'This file was hidden'; + title = appLocalizationsOf(context).fileWasHidden; break; case RevisionAction.unhide: - title = 'This file was unhidden'; + title = appLocalizationsOf(context).fileWasUnhidden; break; default: title = appLocalizationsOf(context).fileWasModified; @@ -1277,7 +1277,9 @@ class DetailsPanelToolbar extends StatelessWidget { ), if (item.isOwner) _buildActionIcon( - tooltip: item.isHidden ? 'Unhide' : 'Hide', + tooltip: item.isHidden + ? appLocalizationsOf(context).unhide + : appLocalizationsOf(context).hide, icon: item.isHidden ? ArDriveIcons.eyeClosed(size: defaultIconSize) : ArDriveIcons.eyeOpen(size: defaultIconSize), diff --git a/lib/components/hide_dialog.dart b/lib/components/hide_dialog.dart index f35d80876a..69d25a9f80 100644 --- a/lib/components/hide_dialog.dart +++ b/lib/components/hide_dialog.dart @@ -41,62 +41,54 @@ class HideDialog extends StatelessWidget { }, builder: (context, state) { return ArDriveStandardModal( - title: _buildTitle(state), - content: _buildContent(state), + title: _buildTitle(context, state), + content: _buildContent(context, state), actions: _buildActions(context, state), ); }, ); } - String _buildTitle(HideState state) { + String _buildTitle(BuildContext context, HideState state) { final hideAction = state.hideAction; if (state is FailureHideState) { switch (hideAction) { case HideAction.hideFile: - return 'Failed to hide file'; // TODO: localize + return appLocalizationsOf(context).failedToHideFile; case HideAction.hideFolder: - return 'Failed to hide folder'; // TODO: localize + return appLocalizationsOf(context).failedToHideFolder; case HideAction.unhideFile: - return 'Failed to unhide file'; // TODO: localize + return appLocalizationsOf(context).failedToUnhideFile; case HideAction.unhideFolder: - return 'Failed to unhide folder'; // TODO: localize + return appLocalizationsOf(context).failedToUnhideFolder; } } switch (hideAction) { case HideAction.hideFile: - return 'Hiding file'; // TODO: localize + return appLocalizationsOf(context).hidingFile; case HideAction.hideFolder: - return 'Hiding folder'; // TODO: localize + return appLocalizationsOf(context).hidingFolder; case HideAction.unhideFile: - return 'Unhiding file'; // TODO: localize + return appLocalizationsOf(context).unhidingFile; case HideAction.unhideFolder: - return 'Unhiding folder'; // TODO: localize + return appLocalizationsOf(context).unhidingFolder; } } - Widget _buildContent(HideState state) { + Widget _buildContent(BuildContext context, HideState state) { if (state is FailureHideState) { final hideAction = state.hideAction; switch (hideAction) { case HideAction.hideFile: - return const Text( - 'Failed to hide file, please try again', // TODO: localize - ); + return Text(appLocalizationsOf(context).failedToHideFile); case HideAction.hideFolder: - return const Text( - 'Failed to hide folder, please try again', // TODO: localize - ); + return Text(appLocalizationsOf(context).failedToHideFolder); case HideAction.unhideFile: - return const Text( - 'Failed to unhide file, please try again', // TODO: localize - ); + return Text(appLocalizationsOf(context).failedToUnhideFile); case HideAction.unhideFolder: - return const Text( - 'Failed to unhide folder, please try again', // TODO: localize - ); + return Text(appLocalizationsOf(context).failedToUnhideFolder); } } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index da99ab2113..49745ad176 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -585,6 +585,10 @@ } } }, + "driveWasHidden": "This drive was hidden", + "@driveWasHidden": { + "description": "This drive was hidden." + }, "driveWasModified": "This drive was modified.", "@driveWasModified": { "description": "Drive activity (journal) entry: modified" @@ -599,6 +603,10 @@ } } }, + "driveWasUnhidden": "This drive was unhidden", + "@driveWasUnhidden": { + "description": "This drive was unhidden." + }, "duplicateFiles": "{numberOfDuplicateFiles, plural, zero{} one{A duplicate file found} other{Duplicate files found}}", "@duplicateFiles": { "description": "Count of conflicting items", @@ -733,6 +741,22 @@ "@failedToCreatePin": { "description": "The pin could not be created" }, + "failedToHideFile": "Failed to hide file", + "@failedToHideFile": { + "description": "Failed to hide file" + }, + "failedToHideFileTryAgain": "Failed to hide file, please try again", + "@failedToHideFileTryAgain": { + "description": "Failed to hide file, try again." + }, + "failedToHideFolder": "Failed to hide folder", + "@failedToHideFolder": { + "description": "Failed to hide folder" + }, + "failedToHideFolderTryAgain": "Failed to hide folder, please try again", + "@failedToHideFolderTryAgain": { + "description": "Failed to hide folder, try again." + }, "failedToRetrieveFileInfromation": "Failed to retrieve file information", "@failedToRetrieveFileInfromation": { "description": "Explains that there was an error while retrieving the file infromation" @@ -741,6 +765,22 @@ "@failedToSyncDrive": { "description": "The app wasn't able to sync the drive" }, + "failedToUnhideFile": "Failed to unhide file", + "@failedToUnhideFile": { + "description": "Failed to unhide file" + }, + "failedToUnhideFileTryAgain": "Failed to unhide file, please try again", + "@failedToUnhideFileTryAgain": { + "description": "Failed to unhide file, try again." + }, + "failedToUnhideFolder": "Failed to unhide folder", + "@failedToUnhideFolder": { + "description": "Failed to unhide folder" + }, + "failedToUnhideFolderTryAgain": "Failed to unhide folder, please try again", + "@failedToUnhideFolderTryAgain": { + "description": "Failed to unhide folder, try again." + }, "feedbackContent": "Would you be willing to leave feedback on your experience using the app?", "@feedbackContent": { "description": "Asks for feedback" @@ -873,6 +913,10 @@ } } }, + "fileWasHidden": "This file was hidden", + "@fileWasHidden": { + "description": "This file was hidden." + }, "fileWasModified": "This file was modified.", "@fileWasModified": { "description": "File activity (journal): modified" @@ -895,6 +939,10 @@ } } }, + "fileWasUnhidden": "This file was unhidden", + "@fileWasUnhidden": { + "description": "This file was unhidden." + }, "finishingThingsUp": "Finishing things up!", "@finishingThingsUp": { "description": "Finishing things up!" @@ -961,6 +1009,10 @@ } } }, + "folderWasHidden": "This folder was hidden", + "@folderWasHidden": { + "description": "This folder was hidden." + }, "folderWasModified": "This folder was modified.", "@folderWasModified": { "description": "Folder activity (journal): modified" @@ -979,6 +1031,10 @@ } } }, + "folderWasUnhidden": "This folder was unhidden", + "@folderWasUnhidden": { + "description": "This folder was unhidden." + }, "forgetWallet": "Forget wallet and change profile", "@forgetWallet": { "description": "Use a different wallet by forgetting the current one" @@ -1035,6 +1091,18 @@ }, "helpCenter": "Help Center", "@helpCenter": {}, + "hide": "Hide", + "@hide": { + "description": "Hide" + }, + "hidingFile": "Hiding file", + "@hidingFile": { + "description": "Hiding file" + }, + "hidingFolder": "Hiding folder", + "@hidingFolder": { + "description": "Hiding folder" + }, "howAreConversionsDetermined": "How are conversions determined?", "@howAreConversionsDetermined": {}, "howDoesKeyfileLoginWork": "How do keyfile and seed phrase login work?", @@ -1926,6 +1994,18 @@ "@unableToFetchEstimateAtThisTime": {}, "unableToUpdateQuote": "Unable to update quote. Please try again.", "@unableToUpdateQuote": {}, + "unhide": "Unhide", + "@unhide": { + "description": "Unhide" + }, + "unhidingFile": "Unhiding file", + "@unhidingFile": { + "description": "Unhiding file" + }, + "unhidingFolder": "Unhiding folder", + "@unhidingFolder": { + "description": "Unhiding folder" + }, "unit": "Unit", "@unit": {}, "units": "Units",