// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'chrome://resources/ash/common/cr_elements/cr_input/cr_input.js';
import type {CrButtonElement} from 'chrome://resources/ash/common/cr_elements/cr_button/cr_button.js';
import {loadTimeData} from 'chrome://resources/ash/common/load_time_data.m.js';
import {assert} from 'chrome://resources/js/assert.js';
import type {VolumeInfo} from '../../background/js/volume_info.js';
import type {VolumeManager} from '../../background/js/volume_manager.js';
import {getDlpRestrictionDetails, getHoldingSpaceState, startIOTask} from '../../common/js/api.js';
import {isModal} from '../../common/js/dialog_type.js';
import {getFocusedTreeItem} from '../../common/js/dom_utils.js';
import {entriesToURLs, getTreeItemEntry, isDirectoryEntry, isFakeEntry, isGrandRootEntryInDrive, isNonModifiable, isRecentRootType, isTeamDriveRoot, isTeamDrivesGrandRoot, isTrashEntry, isTrashRoot, unwrapEntry} from '../../common/js/entry_utils.js';
import {getExtension, getType, isEncrypted} from '../../common/js/file_type.js';
import type {FakeEntry, FilesAppDirEntry, FilesAppEntry} from '../../common/js/files_app_entry_types.js';
import {EntryList} from '../../common/js/files_app_entry_types.js';
import {isDlpEnabled, isDriveFsBulkPinningEnabled, isMirrorSyncEnabled, isSinglePartitionFormatEnabled} from '../../common/js/flags.js';
import {recordEnum, recordUserAction} from '../../common/js/metrics.js';
import {getFileErrorString, str, strf} from '../../common/js/translations.js';
import type {TrashEntry} from '../../common/js/trash.js';
import {deleteIsForever, RestoreFailedType, RestoreFailedTypesUMA, RestoreFailedUMA, shouldMoveToTrash} from '../../common/js/trash.js';
import {isNullOrUndefined, visitURL} from '../../common/js/util.js';
import {FileSystemType, isRecentArcEntry, RootType, VolumeError, VolumeType} from '../../common/js/volume_manager_types.js';
import {readSubDirectories, updateFileData} from '../../state/ducks/all_entries.js';
import {changeDirectory} from '../../state/ducks/current_directory.js';
import {DialogType} from '../../state/state.js';
import {getStore} from '../../state/store.js';
import {isTreeItem, isXfTree} from '../../widgets/xf_tree_util.js';
import type {FilesTooltip} from '../elements/files_tooltip.js';
import {type ActionsModel, CommonActionId, InternalActionId} from './actions_model.js';
import {type CommandHandlerDeps, MenuCommandsForUma, recordMenuItemSelected} from './command_handler.js';
import {canExecuteVisibleOnDriveInNormalAppModeOnly, containsNonInteractiveEntry, currentVolumeIsInteractive, getCommandEntries, getCommandEntry, getElementVolumeInfo, getEventEntry, getOnlyOneSelectedDirectory, getParentEntry, getSharesheetLaunchSource, hasCapability, isDriveEntries, isFromSelectionMenu, isOnlyMyDriveEntries, isOnTrashRoot, isRootEntry, shouldIgnoreEvents, shouldShowMenuItemsForEntry} from './file_manager_commands_util.js';
import type {PasteWithDestDirectoryEvent} from './file_transfer_controller.js';
import {getAllowedVolumeTypes, maybeStoreTimeOfFirstPin} from './holding_space_util.js';
import {PathComponent} from './path_component.js';
import type {Command} from './ui/command.js';
import {type CanExecuteEvent, type CommandEvent} from './ui/command.js';
import type {FilesConfirmDialog} from './ui/files_confirm_dialog.js';
/**
* Used to filter out `VolumeInfo` that don't exist and maintain the return
* array is of type `VolumeInfo[]` without null or undefined.
*/
function isVolumeInfo(volumeInfo: VolumeInfo|null|
undefined): volumeInfo is VolumeInfo {
return !isNullOrUndefined(volumeInfo);
}
/**
* A command.
*/
abstract class FilesCommand {
/**
* Handles the execute event.
* @param event Command event.
* @param fileManager CommandHandlerDeps.
*/
abstract execute(event: CommandEvent, fileManager: CommandHandlerDeps): void;
/**
* Handles the can execute event.
* By default, sets the command as always enabled.
* @param event Can execute event.
* @param fileManager CommandHandlerDeps.
*/
canExecute(event: CanExecuteEvent, _fileManager: CommandHandlerDeps) {
event.canExecute = true;
}
}
/**
* Unmounts external drive.
*/
export class UnmountCommand extends FilesCommand {
/**
* @param event Command event.
* @param fileManager CommandHandlerDeps.
*/
private async executeImpl_(event: Event, fileManager: CommandHandlerDeps) {
const errorCallback = (volumeType?: VolumeType) => {
if (volumeType === VolumeType.REMOVABLE) {
fileManager.ui.alertDialog.showHtml('', str('UNMOUNT_FAILED'));
} else {
fileManager.ui.alertDialog.showHtml('', str('UNMOUNT_PROVIDED_FAILED'));
}
};
// Find volumes to unmount.
let volumes: VolumeInfo[] = [];
let label = '';
const entry = getCommandEntry(fileManager, event.target);
if (entry instanceof EntryList) {
// The element is a group of removable partitions.
if (!entry) {
errorCallback();
return;
}
// Add child partitions to the list of volumes to be unmounted.
volumes = entry.getUiChildren()
.map(
child => ('volumeInfo' in child) ?
child.volumeInfo as VolumeInfo :
null)
.filter(isVolumeInfo);
label = entry.label || '';
} else {
// The element is a removable volume with no partitions.
const volumeInfo = getElementVolumeInfo(event.target, fileManager);
if (!volumeInfo) {
errorCallback();
return;
}
volumes.push(volumeInfo);
label = volumeInfo.label || '';
}
// Eject volumes of which there may be multiple.
const promises = volumes.map(async (volume) => {
try {
await fileManager.volumeManager.unmount(volume);
} catch (error) {
console.warn('Cannot unmount (redacted):', error);
console.debug(`Cannot unmount '${volume.volumeId}':`, error);
if (error !== VolumeError.PATH_NOT_MOUNTED) {
errorCallback(volume.volumeType);
}
}
});
await Promise.all(promises);
fileManager.ui.speakA11yMessage(strf('A11Y_VOLUME_EJECT', label));
}
execute(event: CommandEvent, fileManager: CommandHandlerDeps) {
this.executeImpl_(event, fileManager);
}
override canExecute(event: CanExecuteEvent, fileManager: CommandHandlerDeps) {
const volumeInfo = getElementVolumeInfo(event.target, fileManager);
const entry = getCommandEntry(fileManager, event.target);
let volumeType;
if (entry && entry instanceof EntryList) {
volumeType = entry.rootType;
} else if (volumeInfo) {
volumeType = volumeInfo.volumeType;
} else {
event.canExecute = false;
event.command.setHidden(true);
return;
}
event.canExecute =
(volumeType === VolumeType.ARCHIVE ||
volumeType === VolumeType.REMOVABLE ||
volumeType === VolumeType.PROVIDED || volumeType === VolumeType.SMB);
event.command.setHidden(!event.canExecute);
switch (volumeType) {
case VolumeType.ARCHIVE:
case VolumeType.PROVIDED:
case VolumeType.SMB:
event.command.label = str('CLOSE_VOLUME_BUTTON_LABEL');
break;
case VolumeType.REMOVABLE:
event.command.label = str('UNMOUNT_DEVICE_BUTTON_LABEL');
break;
}
}
}
/**
* Formats external drive.
*/
export class FormatCommand extends FilesCommand {
execute(event: CommandEvent, fileManager: CommandHandlerDeps) {
const directoryModel = fileManager.directoryModel;
let root: Entry|FilesAppEntry|undefined;
if (fileManager.ui.directoryTree?.contains(event.target as Node)) {
// The command is executed from the directory tree context menu.
root = getCommandEntry(fileManager, event.target);
} else {
// The command is executed from the gear menu.
root = directoryModel.getCurrentDirEntry();
}
// If an entry is not found from the event target, use the current
// directory. This can happen for the format button for unsupported and
// unrecognized volumes.
if (!root) {
root = directoryModel.getCurrentDirEntry();
}
assert(root);
const volumeInfo = fileManager.volumeManager.getVolumeInfo(root);
if (volumeInfo) {
fileManager.ui.formatDialog.showModal(volumeInfo);
}
}
override canExecute(event: CanExecuteEvent, fileManager: CommandHandlerDeps) {
const directoryModel = fileManager.directoryModel;
let root;
if (fileManager.ui.directoryTree?.contains(event.target as Node)) {
// The command is executed from the directory tree context menu.
root = getCommandEntry(fileManager, event.target);
} else {
// The command is executed from the gear menu.
root = directoryModel.getCurrentDirEntry();
}
// |root| is null for unrecognized volumes. Enable format command for such
// volumes.
const isUnrecognizedVolume = (root === null);
// See the comment in execute() for why doing this.
if (!root) {
root = directoryModel.getCurrentDirEntry();
}
const location = root && fileManager.volumeManager.getLocationInfo(root);
const writable = !!location && !location.isReadOnly;
const isRoot = location && location.isRootEntry;
// Enable the command if this is a removable device (e.g. a USB drive).
const removableRoot =
location && isRoot && location.rootType === RootType.REMOVABLE;
event.canExecute = !!removableRoot && (isUnrecognizedVolume || writable);
if (isSinglePartitionFormatEnabled()) {
let isDevice = false;
if (root && root instanceof EntryList) {
// root entry is device node if it has child (partition).
isDevice = !!removableRoot && root.getUiChildren().length > 0;
}
// Disable format command on device when SinglePartitionFormat on,
// erase command will be available.
event.command.setHidden(!removableRoot || isDevice);
} else {
event.command.setHidden(!removableRoot);
}
}
}
/**
* Deletes removable device partition, creates single partition and formats it.
*/
export class EraseDeviceCommand extends FilesCommand {
execute(event: CommandEvent, fileManager: CommandHandlerDeps) {
const root = getEventEntry(event, fileManager);
if (root && root instanceof EntryList) {
fileManager.ui.formatDialog.showEraseModal(root);
}
}
override canExecute(event: CanExecuteEvent, fileManager: CommandHandlerDeps) {
if (!isSinglePartitionFormatEnabled()) {
event.canExecute = false;
event.command.setHidden(true);
return;
}
const root = getEventEntry(event, fileManager);
const location = root && fileManager.volumeManager.getLocationInfo(root);
const writable = location && !location.isReadOnly;
const isRoot = location && location.isRootEntry;
const removableRoot =
location && isRoot && location.rootType === RootType.REMOVABLE;
let isDevice = false;
if (root && root instanceof EntryList) {
// root entry is device node if it has child (partition).
isDevice = !!removableRoot && root.getUiChildren().length > 0;
}
event.canExecute = !!removableRoot && !writable;
// Enable the command if this is a removable and device node.
event.command.setHidden(!removableRoot || !isDevice);
}
}
/**
* Initiates new folder creation.
*/
export class NewFolderCommand extends FilesCommand {
/**
* Whether a new-folder is in progress.
*/
private busy_ = false;
execute(event: CommandEvent, fileManager: CommandHandlerDeps) {
if (isOnTrashRoot(fileManager)) {
return;
}
let targetDirectory: DirectoryEntry|FilesAppDirEntry|null|undefined;
let executedFromDirectoryTree: boolean;
if (isXfTree(event.target)) {
const focusedTreeItem = getFocusedTreeItem(event.target);
targetDirectory = getTreeItemEntry(focusedTreeItem) as DirectoryEntry |
FilesAppDirEntry | null;
executedFromDirectoryTree = true;
} else if (isTreeItem(event.target)) {
targetDirectory = getTreeItemEntry(event.target) as DirectoryEntry |
FilesAppDirEntry | null;
executedFromDirectoryTree = true;
} else {
targetDirectory = fileManager.directoryModel.getCurrentDirEntry();
executedFromDirectoryTree = false;
}
const directoryModel = fileManager.directoryModel;
const listContainer = fileManager.ui.listContainer;
this.busy_ = true;
assert(targetDirectory);
const directoryEntry = unwrapEntry(targetDirectory) as DirectoryEntry;
this.generateNewDirectoryName_(directoryEntry).then((newName) => {
if (!executedFromDirectoryTree) {
listContainer.startBatchUpdates();
}
return new Promise(
directoryEntry.getDirectory.bind(
directoryEntry, newName, {create: true, exclusive: true}))
.then(
(newDirectory) => {
recordUserAction('CreateNewFolder');
// Select new directory and start rename operation.
if (executedFromDirectoryTree) {
const parentFileKey = directoryEntry.toURL();
// After new directory is created on parent directory, we
// need to expand it otherwise the new child item won't
// show, and also trigger a re-scan for the parent
// directory.
getStore().dispatch(updateFileData({
key: parentFileKey,
partialFileData: {expanded: true},
}));
getStore().dispatch(readSubDirectories(parentFileKey));
fileManager.ui.directoryTreeContainer
?.renameItemWithKeyWhenRendered(newDirectory.toURL());
this.busy_ = false;
} else {
directoryModel.updateAndSelectNewDirectory(newDirectory)
.then(() => {
listContainer.endBatchUpdates();
fileManager.namingController.initiateRename();
this.busy_ = false;
})
.catch(error => {
listContainer.endBatchUpdates();
this.busy_ = false;
console.warn(error);
});
}
},
(error) => {
if (!executedFromDirectoryTree) {
listContainer.endBatchUpdates();
}
this.busy_ = false;
fileManager.ui.alertDialog.show(strf(
'ERROR_CREATING_FOLDER', newName,
getFileErrorString(error.name)));
});
});
}
/**
* Generates new directory name.
*/
private generateNewDirectoryName_(
parentDirectory: DirectoryEntry, index: number = 0): Promise<string> {
const defaultName = str('DEFAULT_NEW_FOLDER_NAME');
const newName =
index === 0 ? defaultName : defaultName + ' (' + index + ')';
return new Promise(parentDirectory.getDirectory.bind(
parentDirectory, newName, {create: false}))
.then(_newEntry => {
return this.generateNewDirectoryName_(parentDirectory, index + 1);
})
.catch(() => {
return newName;
});
}
override canExecute(event: CanExecuteEvent, fileManager: CommandHandlerDeps) {
if (isOnTrashRoot(fileManager)) {
event.canExecute = false;
event.command.setHidden(true);
return;
}
const entries = getCommandEntries(fileManager, event.target);
// If there is a selected entry on a non-interactive volume, remove
// new-folder command.
if (entries.length > 0 &&
!containsNonInteractiveEntry(entries, fileManager)) {
event.canExecute = false;
event.command.setHidden(true);
return;
}
if (isXfTree(event.target) || isTreeItem(event.target)) {
const entry = entries[0];
if (!entry || isFakeEntry(entry) || isTeamDrivesGrandRoot(entry)) {
event.canExecute = false;
event.command.setHidden(true);
return;
}
const locationInfo = fileManager.volumeManager.getLocationInfo(entry);
event.canExecute = !!locationInfo && !locationInfo.isReadOnly &&
hasCapability(fileManager, [entry], 'canAddChildren');
event.command.setHidden(false);
} else {
// If blank space was clicked and current volume is non-interactive,
// remove new-folder command.
if (entries.length === 0 && !currentVolumeIsInteractive(fileManager)) {
event.canExecute = false;
event.command.setHidden(true);
return;
}
const directoryModel = fileManager.directoryModel;
const directoryEntry = fileManager.getCurrentDirectoryEntry()!;
event.canExecute = !fileManager.directoryModel.isReadOnly() &&
!fileManager.namingController.isRenamingInProgress() &&
!directoryModel.isSearching() &&
hasCapability(fileManager, [directoryEntry], 'canAddChildren');
event.command.setHidden(false);
}
if (this.busy_) {
event.canExecute = false;
}
}
}
/**
* Initiates new window creation.
*/
export class NewWindowCommand extends FilesCommand {
execute(_event: CommandEvent, fileManager: CommandHandlerDeps) {
fileManager.launchFileManager({
currentDirectoryURL: fileManager.getCurrentDirectoryEntry() &&
fileManager.getCurrentDirectoryEntry()!.toURL(),
});
}
override canExecute(event: CanExecuteEvent, fileManager: CommandHandlerDeps) {
event.canExecute = !!fileManager.getCurrentDirectoryEntry() &&
(fileManager.dialogType === DialogType.FULL_PAGE);
}
}
export class SelectAllCommand extends FilesCommand {
execute(_event: CommandEvent, fileManager: CommandHandlerDeps) {
fileManager.directoryModel.getFileListSelection().setCheckSelectMode(true);
fileManager.directoryModel.getFileListSelection().selectAll();
}
override canExecute(event: CanExecuteEvent, fileManager: CommandHandlerDeps) {
// Check we can select multiple items.
const multipleSelect =
fileManager.directoryModel.getFileListSelection().multiple;
// Check we are not inside an input element (e.g. the search box).
const inputElementActive =
document.activeElement instanceof HTMLInputElement ||
document.activeElement instanceof HTMLTextAreaElement ||
document.activeElement?.tagName.toLowerCase() === 'cr-input';
event.canExecute = multipleSelect && !inputElementActive &&
fileManager.directoryModel.getFileList().length > 0;
}
}
export class ToggleHiddenFilesCommand extends FilesCommand {
execute(event: CommandEvent, fileManager: CommandHandlerDeps) {
const visible = !fileManager.fileFilter.isHiddenFilesVisible();
fileManager.fileFilter.setHiddenFilesVisible(visible);
event.detail.command.checked =
visible; // Check-mark for "Show hidden files".
recordMenuItemSelected(
visible ? MenuCommandsForUma.HIDDEN_FILES_SHOW :
MenuCommandsForUma.HIDDEN_FILES_HIDE);
}
}
/**
* Toggles visibility of top-level Android folders which are not visible by
* default.
*/
export class ToggleHiddenAndroidFoldersCommand extends FilesCommand {
execute(event: CommandEvent, fileManager: CommandHandlerDeps) {
const visible = !fileManager.fileFilter.isAllAndroidFoldersVisible();
fileManager.fileFilter.setAllAndroidFoldersVisible(visible);
event.detail.command.checked = visible;
recordMenuItemSelected(
visible ? MenuCommandsForUma.HIDDEN_ANDROID_FOLDERS_SHOW :
MenuCommandsForUma.HIDDEN_ANDROID_FOLDERS_HIDE);
}
override canExecute(event: CanExecuteEvent, fileManager: CommandHandlerDeps) {
const hasAndroidFilesVolumeInfo =
!!fileManager.volumeManager.getCurrentProfileVolumeInfo(
VolumeType.ANDROID_FILES);
const currentRootType = fileManager.directoryModel.getCurrentRootType();
const isInMyFiles = currentRootType === RootType.MY_FILES ||
currentRootType === RootType.DOWNLOADS ||
currentRootType === RootType.CROSTINI ||
currentRootType === RootType.ANDROID_FILES;
event.canExecute = hasAndroidFilesVolumeInfo && isInMyFiles;
event.command.setHidden(!event.canExecute);
event.command.checked = fileManager.fileFilter.isAllAndroidFoldersVisible();
}
}
/**
* Toggles drive sync settings.
*/
export class DriveSyncSettingsCommand extends FilesCommand {
execute(_event: CommandEvent, fileManager: CommandHandlerDeps) {
const nowDriveSyncEnabledOnMeteredNetwork =
fileManager.ui.gearMenu.syncButton.hasAttribute('checked');
const changeInfo = {
driveSyncEnabledOnMeteredNetwork: !nowDriveSyncEnabledOnMeteredNetwork,
};
chrome.fileManagerPrivate.setPreferences(changeInfo);
recordMenuItemSelected(
nowDriveSyncEnabledOnMeteredNetwork ?
MenuCommandsForUma.MOBILE_DATA_ON :
MenuCommandsForUma.MOBILE_DATA_OFF);
}
override canExecute(event: CanExecuteEvent, fileManager: CommandHandlerDeps) {
event.canExecute = fileManager.directoryModel.isOnDrive();
event.command.setHidden(!event.canExecute);
}
}
/**
* Delete / Move to Trash command.
*/
export class DeleteCommand extends FilesCommand {
/**
*/
execute(event: CommandEvent, fileManager: CommandHandlerDeps) {
const entries = getCommandEntries(fileManager, event.target);
const permanentlyDelete = event.detail.command.id === 'delete';
// Execute might be called without a call of canExecute method, e.g.,
// called directly from code, crbug.com/509483. See toolbar controller
// delete button handling, for an example.
this.deleteEntries(entries, fileManager, permanentlyDelete);
}
override canExecute(event: CanExecuteEvent, fileManager: CommandHandlerDeps) {
const entries = getCommandEntries(fileManager, event.target);
// If entries contain fake, non-interactive or root entry, remove delete
// option.
if (!entries.every(shouldShowMenuItemsForEntry.bind(
null, fileManager.volumeManager))) {
event.canExecute = false;
event.command.setHidden(true);
return;
}
// Block fusebox volumes in SelectFileAsh (Lacros) file picker mode.
if (fileManager.volumeManager.getFuseBoxOnlyFilterEnabled()) {
// TODO(crbug/1292825) Make it work with fusebox volumes: MTP, etc.
if (fileManager.directoryModel.isOnFuseBox()) {
event.canExecute = false;
event.command.setHidden(true);
return;
}
}
event.canExecute = this.canDeleteEntries_(entries, fileManager);
// Remove if nothing is selected, e.g. user clicked in an empty
// space in the file list.
const noEntries = entries.length === 0;
event.command.setHidden(noEntries);
const isTrashDisabled =
!shouldMoveToTrash(entries, fileManager.volumeManager) ||
!fileManager.trashEnabled;
if (event.command.id === 'move-to-trash' && isTrashDisabled) {
event.canExecute = false;
event.command.setHidden(true);
}
// If the "move-to-trash" command is enabled, don't show the Delete command
// but still leave it executable.
if (event.command.id === 'delete' && !isTrashDisabled) {
event.command.setHidden(true);
}
}
/**
* Delete the entries (if the entries can be deleted).
* @param entries
* @param fileManager
* @param permanentlyDelete if true, entries are permanently deleted
* rather than moved to trash.
* @param dialog An optional delete confirm dialog.
* The default delete confirm dialog will be used if |dialog| is null.
* @public
*/
deleteEntries(
entries: Array<Entry|FilesAppEntry|FakeEntry>,
fileManager: CommandHandlerDeps, permanentlyDelete: boolean,
dialog: null|FilesConfirmDialog = null) {
// Verify that the entries are not fake, non-interactive or root entries,
// and that they can be deleted.
if (!entries.every(shouldShowMenuItemsForEntry.bind(
null, fileManager.volumeManager)) ||
!this.canDeleteEntries_(entries, fileManager)) {
return;
}
// Trashing an item shows an "Undo" visual signal instead of a confirmation
// dialog.
if (!permanentlyDelete &&
shouldMoveToTrash(entries, fileManager.volumeManager) &&
fileManager.trashEnabled) {
startIOTask(
chrome.fileManagerPrivate.IoTaskType.TRASH, entries,
/*params=*/ {});
return;
}
if (!dialog) {
dialog = fileManager.ui.deleteConfirmDialog;
} else if (dialog.showModalElement) {
dialog.showModalElement();
}
const dialogDoneCallback = () => {
dialog?.doneCallback?.();
document.querySelector<FilesTooltip>('files-tooltip')?.hideTooltip();
};
const deleteAction = () => {
dialogDoneCallback();
// Start the permanent delete.
startIOTask(
chrome.fileManagerPrivate.IoTaskType.DELETE, entries, /*params=*/ {});
};
const cancelAction = () => {
dialogDoneCallback();
};
// Files that are deleted from locations that are trash enabled (except
// Drive) should instead show copy indicating the files will be permanently
// deleted. For all other filesystem the permanent deletion can't
// necessarily be verified (e.g. a copy may be moved to the underlying
// filesystems version of trash).
if (deleteIsForever(entries, fileManager.volumeManager)) {
const title = entries.length === 1 ?
str('CONFIRM_PERMANENTLY_DELETE_ONE_TITLE') :
str('CONFIRM_PERMANENTLY_DELETE_SOME_TITLE');
const message = entries.length === 1 ?
strf('CONFIRM_PERMANENTLY_DELETE_ONE_DESC', entries[0]!.name) :
strf('CONFIRM_PERMANENTLY_DELETE_SOME_DESC', entries.length);
dialog.setOkLabel(str('PERMANENTLY_DELETE_FOREVER'));
dialog.showWithTitle(title, message, deleteAction, cancelAction);
return;
}
const deleteMessage = entries.length === 1 ?
strf('CONFIRM_DELETE_ONE', entries[0]!.name) :
strf('CONFIRM_DELETE_SOME', entries.length);
dialog.setOkLabel(str('DELETE_BUTTON_LABEL'));
dialog.show(deleteMessage, deleteAction, cancelAction);
}
/**
* Returns true if all entries can be deleted. Note: This does not check for
* root or fake entries.
*/
private canDeleteEntries_(
entries: Array<Entry|FilesAppEntry>,
fileManager: CommandHandlerDeps): boolean {
return entries.length > 0 &&
!this.containsReadOnlyEntry_(entries, fileManager) &&
fileManager.directoryModel.canDeleteEntries() &&
hasCapability(fileManager, entries, 'canDelete');
}
/**
* Returns True if entries can be deleted.
*/
canDeleteEntries(
entries: Array<Entry|FilesAppEntry>,
fileManager: CommandHandlerDeps): boolean {
// Verify that the entries are not fake, non-interactive or root entries,
// and that they can be deleted.
if (!entries.every(shouldShowMenuItemsForEntry.bind(
null, fileManager.volumeManager)) ||
!this.canDeleteEntries_(entries, fileManager)) {
return false;
}
return true;
}
/**
* Returns true if any entry belongs to a read-only volume or is
* forced to be read-only like MyFiles>Downloads.
*/
private containsReadOnlyEntry_(
entries: Array<Entry|FilesAppEntry>,
fileManager: CommandHandlerDeps): boolean {
return entries.some(entry => {
const locationInfo = fileManager.volumeManager.getLocationInfo(entry);
return (locationInfo && locationInfo.isReadOnly) ||
isNonModifiable(fileManager.volumeManager, entry);
});
}
}
/**
* Restores selected files from trash.
*/
export class RestoreFromTrashCommand extends FilesCommand {
private async execute_(event: CommandEvent, fileManager: CommandHandlerDeps) {
const entries =
getCommandEntries(fileManager, event.target) as TrashEntry[];
const infoEntries = [];
const failedParents: Array<{fileName: string, parentName: string}> = [];
for (const entry of entries) {
try {
const {exists, parentName} = await this.getParentName(
entry.restoreEntry, fileManager.volumeManager);
if (!exists) {
failedParents.push({fileName: entry.restoreEntry.name, parentName});
} else {
infoEntries.push(entry.infoEntry);
}
} catch (err) {
console.warn('Failed getting parent metadata for:', err);
}
}
if (failedParents && failedParents.length > 0) {
// Only a single item is being trashed and the parent doesn't exist.
if (failedParents.length === 1 && infoEntries.length === 0) {
recordEnum(
RestoreFailedUMA, RestoreFailedType.SINGLE_ITEM,
RestoreFailedTypesUMA);
fileManager.ui.alertDialog.show(
strf('CANT_RESTORE_SINGLE_ITEM', failedParents[0]!.parentName));
return;
}
// More than one item has been trashed but all the items have their
// parent removed.
if (failedParents.length > 1 && infoEntries.length === 0) {
const isParentFolderSame = failedParents.every(
p => p.parentName === failedParents[0]!.parentName);
// All the items were from the same parent folder.
if (isParentFolderSame) {
recordEnum(
RestoreFailedUMA, RestoreFailedType.MULTIPLE_ITEMS_SAME_PARENTS,
RestoreFailedTypesUMA);
fileManager.ui.alertDialog.show(strf(
'CANT_RESTORE_MULTIPLE_ITEMS_SAME_PARENTS',
failedParents[0]!.parentName));
return;
}
// All the items are from different parent folders.
recordEnum(
RestoreFailedUMA,
RestoreFailedType.MULTIPLE_ITEMS_DIFFERENT_PARENTS,
RestoreFailedTypesUMA);
fileManager.ui.alertDialog.show(
str('CANT_RESTORE_MULTIPLE_ITEMS_DIFFERENT_PARENTS'));
return;
}
// A mix of items with parents and without parents are attempting to be
// restored.
recordEnum(
RestoreFailedUMA, RestoreFailedType.MULTIPLE_ITEMS_MIXED,
RestoreFailedTypesUMA);
fileManager.ui.alertDialog.show(str('CANT_RESTORE_SOME_ITEMS'));
return;
}
startIOTask(
chrome.fileManagerPrivate.IoTaskType.RESTORE, infoEntries,
/*params=*/ {});
}
execute(event: CommandEvent, fileManager: CommandHandlerDeps) {
this.execute_(event, fileManager);
}
override canExecute(event: CanExecuteEvent, fileManager: CommandHandlerDeps) {
const entries = getCommandEntries(fileManager, event.target);
const enabled = entries.length > 0 && entries.every(e => isTrashEntry(e)) &&
fileManager.trashEnabled;
event.canExecute = enabled;
event.command.setHidden(!enabled);
}
/**
* Check whether the parent exists from a supplied entry and return the folder
* name (if it exists or doesn't).
* @param entry The entry to identify the parent from.
* volumeManager
*/
async getParentName(entry: Entry, volumeManager: VolumeManager) {
return new Promise<{exists: boolean, parentName: string}>(
(resolve, reject) => {
entry.getParent(
parent => resolve({exists: true, parentName: parent.name}),
err => {
// If this failed, it may be because the parent doesn't exist.
// Extract the parent from the path components in that case.
if (err.name === 'NotFoundError') {
const components = PathComponent.computeComponentsFromEntry(
entry, volumeManager);
resolve({
exists: false,
parentName: components[components.length - 2]?.name ?? '',
});
return;
}
reject(err);
});
});
}
}
/**
* Empties (permanently deletes all) files from trash.
*/
export class EmptyTrashCommand extends FilesCommand {
execute(_event: CommandEvent, fileManager: CommandHandlerDeps) {
fileManager.ui.emptyTrashConfirmDialog.showWithTitle(
str('CONFIRM_EMPTY_TRASH_TITLE'), str('CONFIRM_EMPTY_TRASH_DESC'),
() => {
startIOTask(
chrome.fileManagerPrivate.IoTaskType.EMPTY_TRASH, /*entries=*/[],
/*params=*/ {});
});
}
override canExecute(event: CanExecuteEvent, fileManager: CommandHandlerDeps) {
const entries = getCommandEntries(fileManager, event.target);
const trashRoot = entries.length === 1 && isTrashRoot(entries[0]!) &&
fileManager.trashEnabled;
event.canExecute = trashRoot || isOnTrashRoot(fileManager);
event.command.setHidden(!trashRoot);
}
}
/**
* Pastes files from clipboard.
*/
export class PasteCommand extends FilesCommand {
execute(event: CommandEvent, fileManager: CommandHandlerDeps) {
if (isOnTrashRoot(fileManager)) {
return;
}
fileManager.document.execCommand(event.detail.command.id);
}
override canExecute(event: CanExecuteEvent, fileManager: CommandHandlerDeps) {
if (isOnTrashRoot(fileManager)) {
event.canExecute = false;
event.command.setHidden(true);
return;
}
const fileTransferController = fileManager.fileTransferController;
event.canExecute = !!fileTransferController &&
!!fileTransferController.queryPasteCommandEnabled(
fileManager.directoryModel.getCurrentDirEntry());
// Hide this command if only one folder is selected.
event.command.setHidden(
!!getOnlyOneSelectedDirectory(fileManager.getSelection()));
const entries = getCommandEntries(fileManager, event.target);
// If there is a selected entry on a non-interactive volume, remove paste
// command.
if (entries.length > 0 &&
!containsNonInteractiveEntry(entries, fileManager)) {
event.canExecute = false;
event.command.setHidden(true);
return;
} else if (
entries.length === 0 && !currentVolumeIsInteractive(fileManager)) {
// If blank space was clicked and current volume is non-interactive,
// remove paste command.
event.canExecute = false;
event.command.setHidden(true);
return;
}
}
}
/**
* Pastes files from clipboard. This is basically same as 'paste'.
* This command is used for always showing the Paste command to gear menu.
*/
export class PasteIntoCurrentFolderCommand extends FilesCommand {
execute(_event: CommandEvent, fileManager: CommandHandlerDeps) {
fileManager.document.execCommand('paste');
}
override canExecute(event: CanExecuteEvent, fileManager: CommandHandlerDeps) {
const fileTransferController = fileManager.fileTransferController;
event.canExecute = !!fileTransferController &&
!!fileTransferController.queryPasteCommandEnabled(
fileManager.directoryModel.getCurrentDirEntry());
}
}
/**
* Pastes files from clipboard into the selected folder.
*/
export class PasteIntoFolderCommand extends FilesCommand {
execute(event: CommandEvent, fileManager: CommandHandlerDeps) {
if (isOnTrashRoot(fileManager)) {
return;
}
const entries = getCommandEntries(fileManager, event.target);
if (entries.length !== 1 || !entries[0]!.isDirectory ||
!shouldShowMenuItemsForEntry(fileManager.volumeManager, entries[0]!)) {
return;
}
// This handler tweaks the Event object for 'paste' event so that
// the FileTransferController can distinguish this 'paste-into-folder'
// command and know the destination directory.
const handler = (inEvent: PasteWithDestDirectoryEvent) => {
inEvent.destDirectory = entries[0]!;
};
fileManager.document.addEventListener(
'paste', handler as EventListener, true);
fileManager.document.execCommand('paste');
fileManager.document.removeEventListener(
'paste', handler as EventListener, true);
}
override canExecute(event: CanExecuteEvent, fileManager: CommandHandlerDeps) {
if (isOnTrashRoot(fileManager)) {
event.canExecute = false;
event.command.setHidden(true);
return;
}
const entries = getCommandEntries(fileManager, event.target);
// Show this item only when one directory is selected.
if (entries.length !== 1 || !entries[0]!.isDirectory ||
!shouldShowMenuItemsForEntry(fileManager.volumeManager, entries[0]!)) {
event.canExecute = false;
event.command.setHidden(true);
return;
}
const fileTransferController = fileManager.fileTransferController;
event.canExecute = !!fileTransferController &&
!!fileTransferController.queryPasteCommandEnabled(
entries[0] as DirectoryEntry | FakeEntry);
event.command.setHidden(false);
}
}
/**
* Cut/Copy command.
*/
export class CutCopyCommand extends FilesCommand {
execute(event: CommandEvent, fileManager: CommandHandlerDeps) {
if (isOnTrashRoot(fileManager)) {
return;
}
// Cancel check-select-mode on cut/copy. Any further selection of a dir
// should start a new selection rather than add to the existing selection.
fileManager.directoryModel.getFileListSelection().setCheckSelectMode(false);
fileManager.document.execCommand(event.detail.command.id);
}
override canExecute(event: CanExecuteEvent, fileManager: CommandHandlerDeps) {
const fileTransferController = fileManager.fileTransferController;
if (!fileTransferController) {
// File Open and SaveAs dialogs do not have a fileTransferController.
event.command.setHidden(true);
event.canExecute = false;
return;
}
const command = event.command;
const isMove = command.id === 'cut';
// Disable Copy command in Trash.
if (!isMove && isOnTrashRoot(fileManager)) {
event.command.setHidden(true);
event.canExecute = false;
return;
}
const entries = getCommandEntries(fileManager, event.target);
const target = event.target;
const volumeManager = fileManager.volumeManager;
command.setHidden(false);
/** If the operation is allowed in the Directory Tree. */
function canDoDirectoryTree(): boolean {
let entry: Entry|FilesAppEntry|null;
if (target && 'entry' in target) {
entry = target.entry as Entry | FilesAppEntry;
} else if (
getFocusedTreeItem(target) &&
getTreeItemEntry(getFocusedTreeItem(target))) {
entry = getTreeItemEntry(getFocusedTreeItem(target));
} else {
return false;
}
assert(entry);
// If entry is fake, non-interactive or root, remove cut/copy option.
if (!shouldShowMenuItemsForEntry(volumeManager, entry)) {
command.setHidden(true);
return false;
}
// For MyFiles/Downloads and MyFiles/PluginVm we only allow copy.
if (isMove && isNonModifiable(volumeManager, entry)) {
return false;
}
// Cut is unavailable on Shared Drive roots.
if (isTeamDriveRoot(entry)) {
return false;
}
const metadata =
fileManager.metadataModel.getCache([entry], ['canCopy', 'canDelete']);
assert(metadata.length === 1);
if (!isMove) {
return metadata[0]!.canCopy !== false;
}
// We need to check source volume is writable for move operation.
const volumeInfo = volumeManager.getVolumeInfo(entry);
return !volumeInfo?.isReadOnly && metadata[0]!.canCopy !== false &&
metadata[0]!.canDelete !== false;
}
/** @returns If the operation is allowed in the File List. */
function canDoFileList() {
assert(fileManager.document);
if (shouldIgnoreEvents(fileManager.document)) {
return false;
}
// If entries contain fake, non-interactive or root entry, remove cut/copy
// option.
if (!fileManager.getSelection().entries.every(
shouldShowMenuItemsForEntry.bind(null, volumeManager))) {
command.setHidden(true);
return false;
}
// If blank space was clicked and current volume is non-interactive,
// remove cut/copy command.
if (entries.length === 0 && !currentVolumeIsInteractive(fileManager)) {
command.setHidden(true);
return false;
}
// For MyFiles/Downloads we only allow copy.
if (isMove &&
fileManager.getSelection().entries.some(
isNonModifiable.bind(null, volumeManager))) {
return false;
}
return isMove ? fileTransferController?.canCutOrDrag() :
fileTransferController?.canCopyOrDrag();
}
const canDo = fileManager.ui.directoryTree?.contains(target as Node) ?
canDoDirectoryTree() :
canDoFileList();
event.canExecute = !!canDo;
command.disabled = !canDo;
}
}
/**
* Initiates file renaming.
*/
export class RenameCommand extends FilesCommand {
execute(event: CommandEvent, fileManager: CommandHandlerDeps) {
const entry = getCommandEntry(fileManager, event.target);
if (isNonModifiable(fileManager.volumeManager, entry)) {
return;
}
if (isOnTrashRoot(fileManager)) {
return;
}
let isRemovableRoot = false;
let volumeInfo = null;
if (entry) {
volumeInfo = fileManager.volumeManager.getVolumeInfo(entry);
// Checks whether the target is an external drive.
if (volumeInfo && isRootEntry(fileManager.volumeManager, entry)) {
isRemovableRoot = true;
}
}
if (isXfTree(event.target) || isTreeItem(event.target)) {
assert(fileManager.directoryTreeNamingController);
assert(volumeInfo);
if (isXfTree(event.target)) {
const treeItem = getFocusedTreeItem(event.target);
assert(treeItem);
fileManager.directoryTreeNamingController.attachAndStart(
treeItem, isRemovableRoot, volumeInfo);
} else if (isTreeItem(event.target)) {
fileManager.directoryTreeNamingController.attachAndStart(
event.target, isRemovableRoot, volumeInfo);
}
} else {
fileManager.namingController.initiateRename(isRemovableRoot, volumeInfo);
}
}
override canExecute(event: CanExecuteEvent, fileManager: CommandHandlerDeps) {
// Block fusebox volumes in SelectFileAsh (Lacros) file picker mode.
if (fileManager.volumeManager.getFuseBoxOnlyFilterEnabled()) {
// TODO(crbug/1292825) Make it work with fusebox volumes: MTP, etc.
if (fileManager.directoryModel.isOnFuseBox()) {
event.canExecute = false;
event.command.setHidden(true);
return;
}
}
if (isOnTrashRoot(fileManager)) {
event.canExecute = false;
event.command.setHidden(true);
return;
}
// Check if it is removable drive
if ((() => {
const root = getCommandEntry(fileManager, event.target);
// |root| is null for unrecognized volumes. Do not enable rename
// command for such volumes because they need to be formatted prior to
// rename.
if (!root || !isRootEntry(fileManager.volumeManager, root)) {
return false;
}
const volumeInfo = fileManager.volumeManager.getVolumeInfo(root);
const location = fileManager.volumeManager.getLocationInfo(root);
if (!volumeInfo || !location) {
event.command.setHidden(true);
event.canExecute = false;
return true;
}
const writable = !location.isReadOnly;
const removable = location.rootType === RootType.REMOVABLE;
event.canExecute =
removable && writable && !!volumeInfo.diskFileSystemType && [
FileSystemType.EXFAT,
FileSystemType.VFAT,
FileSystemType.NTFS,
].indexOf(volumeInfo.diskFileSystemType) > -1;
event.command.setHidden(!removable);
return removable;
})()) {
return;
}
// Check if it is file or folder
const renameTarget = isFromSelectionMenu(event) ?
fileManager.ui.listContainer.currentList :
event.target;
const entries = getCommandEntries(fileManager, renameTarget);
if (entries.length === 0 ||
!shouldShowMenuItemsForEntry(fileManager.volumeManager, entries[0]!) ||
entries.some(isNonModifiable.bind(null, fileManager.volumeManager))) {
event.canExecute = false;
event.command.setHidden(true);
return;
}
assert(renameTarget);
const parentEntry =
getParentEntry(renameTarget, fileManager.directoryModel);
const locationInfo = parentEntry ?
fileManager.volumeManager.getLocationInfo(parentEntry) :
null;
const volumeIsNotReadOnly = !!locationInfo && !locationInfo.isReadOnly;
// ARC doesn't support rename for now. http://b/232152680
const recentArcEntry = isRecentArcEntry(unwrapEntry(entries[0]!) as Entry);
// Drive grand roots do not support rename.
const isDriveGrandRoot = isGrandRootEntryInDrive(entries[0]!);
event.canExecute = entries.length === 1 && volumeIsNotReadOnly &&
!recentArcEntry && !isDriveGrandRoot &&
hasCapability(fileManager, entries, 'canRename');
event.command.setHidden(false);
}
}
/**
* Opens settings/files sub page.
*/
export class FilesSettingsCommand extends FilesCommand {
execute(_event: CommandEvent, _fileManager: CommandHandlerDeps) {
chrome.fileManagerPrivate.openSettingsSubpage('files');
}
override canExecute(
event: CanExecuteEvent, _fileManager: CommandHandlerDeps) {
event.canExecute = true;
}
}
/**
* Opens drive help.
*/
export class VolumeHelpCommand extends FilesCommand {
execute(_event: CommandEvent, fileManager: CommandHandlerDeps) {
if (fileManager.directoryModel.isOnDrive()) {
visitURL(str('GOOGLE_DRIVE_HELP_URL'));
recordMenuItemSelected(MenuCommandsForUma.DRIVE_HELP);
} else {
visitURL(str('FILES_APP_HELP_URL'));
recordMenuItemSelected(MenuCommandsForUma.HELP);
}
}
override canExecute(event: CanExecuteEvent, fileManager: CommandHandlerDeps) {
// Hides the help menu in modal dialog mode. It does not make much sense
// because after all, users cannot view the help without closing, and
// besides that the help page is about the Files app as an app, not about
// the dialog mode itself. It can also lead to hard-to-fix bug
// crbug.com/339089.
const hideHelp = isModal(fileManager.dialogType);
event.canExecute = !hideHelp;
event.command.setHidden(hideHelp);
}
}
/**
* Opens the send feedback window.
*/
export class SendFeedbackCommand extends FilesCommand {
execute(_event: CommandEvent, _fileManager: CommandHandlerDeps) {
chrome.fileManagerPrivate.sendFeedback();
}
}
/**
* Opens drive buy-more-space url.
*/
export class DriveBuyMoreSpaceCommand extends FilesCommand {
execute(_event: CommandEvent, _fileManager: CommandHandlerDeps) {
visitURL(str('GOOGLE_DRIVE_BUY_STORAGE_URL'));
recordMenuItemSelected(MenuCommandsForUma.DRIVE_BUY_MORE_SPACE);
}
override canExecute(event: CanExecuteEvent, fileManager: CommandHandlerDeps) {
canExecuteVisibleOnDriveInNormalAppModeOnly(event, fileManager);
}
}
/**
* Opens drive.google.com.
*/
export class DriveGoToDriveCommand extends FilesCommand {
execute(_event: CommandEvent, _fileManager: CommandHandlerDeps) {
visitURL(str('GOOGLE_DRIVE_ROOT_URL'));
recordMenuItemSelected(MenuCommandsForUma.DRIVE_GO_TO_DRIVE);
}
override canExecute(event: CanExecuteEvent, fileManager: CommandHandlerDeps) {
canExecuteVisibleOnDriveInNormalAppModeOnly(event, fileManager);
}
}
/**
* Opens a file with default task.
*/
export class DefaultTaskCommand extends FilesCommand {
execute(_event: CommandEvent, fileManager: CommandHandlerDeps) {
fileManager.taskController.executeDefaultTask();
}
override canExecute(event: CanExecuteEvent, fileManager: CommandHandlerDeps) {
event.canExecute = fileManager.taskController.canExecuteDefaultTask();
event.command.setHidden(fileManager.taskController.shouldHideDefaultTask());
}
}
/**
* Displays "open with" dialog for current selection.
*/
export class OpenWithCommand extends FilesCommand {
execute(_event: CommandEvent, _fileManager: CommandHandlerDeps) {
console.error(
`open-with command doesn't execute, ` +
`instead it only opens the sub-menu`);
}
override canExecute(event: CanExecuteEvent, fileManager: CommandHandlerDeps) {
const canExecute = fileManager.taskController.canExecuteOpenActions();
event.canExecute = canExecute;
event.command.setHidden(!canExecute);
}
}
/**
* Invoke Sharesheet.
*/
export class InvokeSharesheetCommand extends FilesCommand {
execute(event: CommandEvent, fileManager: CommandHandlerDeps) {
if (isOnTrashRoot(fileManager)) {
return;
}
const entries = fileManager.selectionHandler.selection.entries;
const launchSource = getSharesheetLaunchSource(event);
const dlpSourceUrls =
fileManager.metadataModel.getCache(entries, ['sourceUrl'])
.map(m => m.sourceUrl || '');
chrome.fileManagerPrivate
.invokeSharesheet(entriesToURLs(entries), launchSource, dlpSourceUrls)
.catch(console.warn);
}
override canExecute(event: CanExecuteEvent, fileManager: CommandHandlerDeps) {
if (isOnTrashRoot(fileManager)) {
event.canExecute = false;
event.command.setHidden(true);
return;
}
const entries = fileManager.selectionHandler.selection.entries;
if (!entries || entries.length === 0 ||
(entries.some(entry => entry.isDirectory) &&
(!isDriveEntries(entries, fileManager.volumeManager) ||
entries.length > 1))) {
event.canExecute = false;
event.command.setHidden(true);
event.command.disabled = true;
return;
}
event.canExecute = true;
// In the case where changing focus to action bar elements, it is safe
// to keep the command enabled if it was visible before, because there
// should be no change to the selected entries.
event.command.disabled =
!fileManager.ui.actionbar.contains(event.target as Node);
chrome.fileManagerPrivate.sharesheetHasTargets(entriesToURLs(entries))
.then((hasTargets: boolean) => {
event.command.setHidden(!hasTargets);
event.canExecute = hasTargets;
event.command.disabled = !hasTargets;
})
.catch(console.warn);
}
}
export class ToggleHoldingSpaceCommand extends FilesCommand {
/**
* Whether the command adds or removed items from holding space. The
* value is set in <code>canExecute()</code>. It will be true unless all
* selected items are already in the holding space.
*/
private addsItems_?: boolean;
execute(_event: CommandEvent, fileManager: CommandHandlerDeps) {
if (this.addsItems_ === undefined) {
return;
}
// Filter out entries from unsupported volumes.
const allowedVolumeTypes = getAllowedVolumeTypes();
const entries =
fileManager.selectionHandler.selection.entries.filter(entry => {
const volumeInfo = fileManager.volumeManager.getVolumeInfo(entry);
return volumeInfo &&
allowedVolumeTypes.includes(volumeInfo.volumeType);
});
chrome.fileManagerPrivate.toggleAddedToHoldingSpace(
entries.map(unwrapEntry) as Entry[], this.addsItems_, () => {});
if (this.addsItems_) {
maybeStoreTimeOfFirstPin();
}
recordMenuItemSelected(
this.addsItems_ ? MenuCommandsForUma.PIN_TO_HOLDING_SPACE :
MenuCommandsForUma.UNPIN_FROM_HOLDING_SPACE);
}
override canExecute(event: CanExecuteEvent, fileManager: CommandHandlerDeps) {
const command = event.command;
const allowedVolumeTypes = getAllowedVolumeTypes();
const currentRootType = fileManager.directoryModel.getCurrentRootType();
if (!isRecentRootType(currentRootType)) {
const volumeInfo = fileManager.directoryModel.getCurrentVolumeInfo();
if (!volumeInfo || !allowedVolumeTypes.includes(volumeInfo.volumeType)) {
event.canExecute = false;
command.setHidden(true);
return;
}
}
// Filter out entries from unsupported volumes.
const entries =
fileManager.selectionHandler.selection.entries.filter(entry => {
const volumeInfo = fileManager.volumeManager.getVolumeInfo(entry);
return volumeInfo &&
allowedVolumeTypes.includes(volumeInfo.volumeType);
});
if (entries.length === 0) {
event.canExecute = false;
command.setHidden(true);
return;
}
event.canExecute = true;
command.setHidden(false);
this.checkHoldingSpaceState(entries, command);
}
async checkHoldingSpaceState(
entries: Array<Entry|FilesAppEntry>, command: Command) {
// Update the command to add or remove holding space items depending on
// the current holding space state - the command will remove items only
// if all currently selected items are already in the holding space.
let state;
try {
state = await getHoldingSpaceState();
} catch (e) {
console.warn('Error getting holding space state', e);
}
if (!state) {
command.setHidden(true);
return;
}
const itemsSet: Record<string, boolean> = {};
state.itemUrls.forEach((item: string) => itemsSet[item] = true);
const selectedUrls = entriesToURLs(entries);
this.addsItems_ = selectedUrls.some(url => !itemsSet[url]);
command.label = this.addsItems_ ? str('HOLDING_SPACE_PIN_COMMAND_LABEL') :
str('HOLDING_SPACE_UNPIN_COMMAND_LABEL');
}
}
/**
* Opens containing folder of the focused file.
*/
export class GoToFileLocationCommand extends FilesCommand {
execute(event: CommandEvent, fileManager: CommandHandlerDeps) {
const entries = getCommandEntries(fileManager, event.target);
if (entries.length !== 1) {
return;
}
const components = PathComponent.computeComponentsFromEntry(
entries[0]!, fileManager.volumeManager);
// Entries in file list table should always have its containing folder.
// (i.e. Its path have at least two components: its parent and itself.)
assert(components.length >= 2);
const parentComponent = components[components.length - 2];
parentComponent?.resolveEntry().then(entry => {
if (entry && isDirectoryEntry(entry)) {
fileManager.directoryModel.changeDirectoryEntry(entry);
}
});
}
override canExecute(event: CanExecuteEvent, fileManager: CommandHandlerDeps) {
// Available in Recents, Audio, Images, and Videos.
if (!isRecentRootType(fileManager.directoryModel.getCurrentRootType())) {
event.canExecute = false;
event.command.setHidden(true);
return;
}
// Available for a single entry.
const entries = getCommandEntries(fileManager, event.target);
event.canExecute = entries.length === 1;
event.command.setHidden(!event.canExecute);
}
}
/**
* Displays QuickView for current selection.
*/
export class GetInfoCommand extends FilesCommand {
execute(_event: CommandEvent, _fileManager: CommandHandlerDeps) {
// 'get-info' command is executed by 'command' event handler in
// QuickViewController.
}
override canExecute(event: CanExecuteEvent, fileManager: CommandHandlerDeps) {
// QuickViewModel refers the file selection instead of event target.
const entries = fileManager.getSelection().entries;
if (entries.length === 0) {
event.canExecute = false;
event.command.setHidden(true);
return;
}
event.canExecute = entries.length >= 1;
event.command.setHidden(false);
}
}
/**
* Displays the Data Leak Prevention (DLP) Restriction details.
*/
export class DlpRestrictionDetailsCommand extends FilesCommand {
private async executeImpl_(
_event: CommandEvent, fileManager: CommandHandlerDeps) {
const entries = fileManager.getSelection().entries;
const metadata = fileManager.metadataModel.getCache(entries, ['sourceUrl']);
if (!metadata || metadata.length !== 1 || !metadata[0]!.sourceUrl) {
return;
}
const sourceUrl = metadata[0]!.sourceUrl;
try {
const details = await getDlpRestrictionDetails(sourceUrl);
fileManager.ui.dlpRestrictionDetailsDialog
?.showDlpRestrictionDetailsDialog(details);
} catch (e) {
console.warn(`Error showing DLP restriction details `, e);
}
}
execute(event: CommandEvent, fileManager: CommandHandlerDeps) {
this.executeImpl_(event, fileManager);
}
override canExecute(event: CanExecuteEvent, fileManager: CommandHandlerDeps) {
if (!isDlpEnabled()) {
event.canExecute = false;
event.command.setHidden(true);
return;
}
const entries = fileManager.getSelection().entries;
// Show this item only when one file is selected.
if (entries.length !== 1) {
event.canExecute = false;
event.command.setHidden(true);
return;
}
const metadata =
fileManager.metadataModel.getCache(entries, ['isDlpRestricted']);
if (!metadata || metadata.length !== 1) {
event.canExecute = false;
event.command.setHidden(true);
}
const isDlpRestricted = metadata[0]?.isDlpRestricted;
event.canExecute = !!isDlpRestricted;
event.command.setHidden(!isDlpRestricted);
}
}
/**
* Focuses search input box.
*/
export class SearchCommand extends FilesCommand {
execute(_event: CommandEvent, fileManager: CommandHandlerDeps) {
// If the current root is Trash we do nothing on search command. Preventing
// it from execution (in canExecute) does not work correctly, as then chrome
// start native search for an app window. Thus we always allow it and do
// nothing in trash.
const currentRootType = fileManager.directoryModel.getCurrentRootType();
if (currentRootType !== RootType.TRASH) {
// Cancel item selection.
fileManager.directoryModel.clearSelection();
// Open the query input via the search container.
fileManager.ui.searchContainer?.openSearch();
}
}
override canExecute(event: CanExecuteEvent, fileManager: CommandHandlerDeps) {
event.canExecute = !fileManager.namingController.isRenamingInProgress();
}
}
export class VolumeSwitchCommand extends FilesCommand {
constructor(private index_: number) {
super();
}
execute(_event: CommandEvent, fileManager: CommandHandlerDeps) {
const directoryTree = fileManager.ui.directoryTree;
const items = directoryTree?.items;
const treeItemEntry = getTreeItemEntry(items && items[this.index_ - 1]);
if (treeItemEntry) {
getStore().dispatch(changeDirectory({toKey: treeItemEntry.toURL()}));
}
}
override canExecute(event: CanExecuteEvent, fileManager: CommandHandlerDeps) {
event.canExecute = this.index_ > 0 &&
this.index_ <= (fileManager.ui.directoryTree?.items.length ?? 0);
}
}
/**
* Flips 'available offline' flag on the file.
*/
export class TogglePinnedCommand extends FilesCommand {
execute(_event: CommandEvent, fileManager: CommandHandlerDeps) {
const entries = fileManager.getSelection().entries;
const actionsController = fileManager.actionsController;
actionsController.getActionsForEntries(entries).then(
(actionsModel: ActionsModel|void) => {
if (!actionsModel) {
return;
}
const saveForOfflineAction =
actionsModel.getAction(CommonActionId.SAVE_FOR_OFFLINE);
const offlineNotNeededAction =
actionsModel.getAction(CommonActionId.OFFLINE_NOT_NECESSARY);
// Saving for offline has a priority if both actions are available.
let action = offlineNotNeededAction;
if (saveForOfflineAction && saveForOfflineAction.canExecute()) {
action = saveForOfflineAction;
}
if (action) {
actionsController.executeAction(action);
}
});
}
override canExecute(event: CanExecuteEvent, fileManager: CommandHandlerDeps) {
const entries = fileManager.getSelection().entries;
const command = event.command;
const actionsController = fileManager.actionsController;
// Avoid flickering menu height: synchronously define command visibility.
if (!isDriveEntries(entries, fileManager.volumeManager)) {
command.setHidden(true);
return;
}
// When the bulk pinning panel is enabled, the "Available offline" toggle
// should not be visible as the underlying functionality is handled
// automatically.
if (isDriveFsBulkPinningEnabled()) {
const state = getStore().getState();
const bulkPinningPref = !!state.preferences?.driveFsBulkPinningEnabled;
if (bulkPinningPref && isOnlyMyDriveEntries(entries, state)) {
command.setHidden(true);
event.canExecute = false;
return;
}
}
command.setHidden(false);
function canExecutePinned(actionsModel: ActionsModel|void) {
if (!actionsModel) {
return;
}
const saveForOfflineAction =
actionsModel.getAction(CommonActionId.SAVE_FOR_OFFLINE);
const offlineNotNeededAction =
actionsModel.getAction(CommonActionId.OFFLINE_NOT_NECESSARY);
let action = offlineNotNeededAction;
command.checked = !!offlineNotNeededAction;
if (saveForOfflineAction && saveForOfflineAction.canExecute()) {
action = saveForOfflineAction;
command.checked = false;
}
event.canExecute = !!action && action.canExecute();
command.disabled = !event.canExecute;
}
// Run synchrounously if possible.
const actionsModel =
actionsController.getInitializedActionsForEntries(entries);
if (actionsModel) {
canExecutePinned(actionsModel);
return;
}
event.canExecute = true;
// Run async, otherwise.
actionsController.getActionsForEntries(entries).then(canExecutePinned);
}
}
/**
* Extracts content of ZIP files in the current selection.
*/
export class ExtractAllCommand extends FilesCommand {
execute(_event: CommandEvent, fileManager: CommandHandlerDeps) {
if (isOnTrashRoot(fileManager)) {
return;
}
let dirEntry = fileManager.getCurrentDirectoryEntry();
if (!dirEntry ||
!fileManager.getSelection().entries.every(
shouldShowMenuItemsForEntry.bind(
null, fileManager.volumeManager))) {
return;
}
const selectionEntries = fileManager.getSelection().entries;
if (fileManager.directoryModel.isReadOnly()) {
dirEntry = fileManager.directoryModel.getMyFiles();
}
fileManager.taskController.startExtractIoTask(
selectionEntries, dirEntry as DirectoryEntry);
}
override canExecute(event: CanExecuteEvent, fileManager: CommandHandlerDeps) {
const dirEntry = fileManager.getCurrentDirectoryEntry();
const selection = fileManager.getSelection();
if (isOnTrashRoot(fileManager) || !dirEntry || !selection ||
selection.totalCount === 0) {
event.command.setHidden(true);
event.canExecute = false;
} else {
// Check the selected entries for a ZIP archive in the selected set.
for (const entry of selection.entries) {
if (getExtension(entry) === '.zip') {
event.command.setHidden(false);
event.canExecute = true;
return;
}
}
// Didn't find any ZIP files, disable extract-all.
event.command.setHidden(true);
event.canExecute = false;
}
}
}
/**
* Creates ZIP file for current selection.
*/
export class ZipSelectionCommand extends FilesCommand {
execute(_event: CommandEvent, fileManager: CommandHandlerDeps) {
if (isOnTrashRoot(fileManager)) {
return;
}
const dirEntry = fileManager.getCurrentDirectoryEntry();
if (!dirEntry ||
!fileManager.getSelection().entries.every(
shouldShowMenuItemsForEntry.bind(
null, fileManager.volumeManager))) {
return;
}
const selectionEntries = fileManager.getSelection().entries;
startIOTask(
chrome.fileManagerPrivate.IoTaskType.ZIP, selectionEntries,
{destinationFolder: dirEntry as DirectoryEntry});
}
override canExecute(event: CanExecuteEvent, fileManager: CommandHandlerDeps) {
if (isOnTrashRoot(fileManager)) {
event.canExecute = false;
event.command.setHidden(true);
return;
}
const dirEntry = fileManager.getCurrentDirectoryEntry();
const selection = fileManager.getSelection();
// Hide ZIP selection for single ZIP file selected.
if (selection.entries.length === 1 &&
getExtension(selection.entries[0]!) === '.zip') {
event.command.setHidden(true);
event.canExecute = false;
return;
}
if (!selection.entries.every(shouldShowMenuItemsForEntry.bind(
null, fileManager.volumeManager))) {
event.canExecute = false;
event.command.setHidden(true);
return;
}
// Hide if there isn't anything selected, meaning user clicked in an empty
// space in the file list.
const noEntries = selection.entries.length === 0;
event.command.setHidden(noEntries);
// TODO(crbug/1226915) Make it work with MTP.
const isOnEligibleLocation = fileManager.directoryModel.isOnNative();
// Hide if any encrypted files are selected, as we can't read them.
const hasEncryptedFile =
fileManager.metadataModel
.getCache(selection.entries, ['contentMimeType'])
.some(
(metadata, i) => isEncrypted(
selection.entries[i]!, metadata.contentMimeType));
event.canExecute = !!dirEntry && !fileManager.directoryModel.isReadOnly() &&
isOnEligibleLocation && selection && selection.totalCount > 0 &&
!hasEncryptedFile;
}
}
/**
* Opens the file in Drive for the user to manage sharing permissions etc.
*/
export class ManageInDriveCommand extends FilesCommand {
execute(event: CommandEvent, fileManager: CommandHandlerDeps) {
const entries = getCommandEntries(fileManager, event.target);
const actionsController = fileManager.actionsController;
fileManager.actionsController.getActionsForEntries(entries).then(
(actionsModel: ActionsModel|void) => {
if (!actionsModel) {
return;
}
const action =
actionsModel.getAction(InternalActionId.MANAGE_IN_DRIVE);
if (action) {
actionsController.executeAction(action);
}
});
}
override canExecute(event: CanExecuteEvent, fileManager: CommandHandlerDeps) {
const entries = getCommandEntries(fileManager, event.target);
const command = event.command;
const actionsController = fileManager.actionsController;
// Avoid flickering menu height: synchronously define command visibility.
if (!isDriveEntries(entries, fileManager.volumeManager)) {
command.setHidden(true);
return;
}
command.setHidden(false);
function canExecuteManageInDrive(actionsModel: ActionsModel|void) {
if (!actionsModel) {
return;
}
const action = actionsModel.getAction(InternalActionId.MANAGE_IN_DRIVE);
if (action) {
command.setHidden(!action);
event.canExecute = !!action && action.canExecute();
command.disabled = !event.canExecute;
}
}
// Run synchronously if possible.
const actionsModel =
actionsController.getInitializedActionsForEntries(entries);
if (actionsModel) {
canExecuteManageInDrive(actionsModel);
return;
}
event.canExecute = true;
// Run async, otherwise.
actionsController.getActionsForEntries(entries).then(
canExecuteManageInDrive);
}
}
/**
* Opens the Manage MirrorSync dialog if the flag is enabled.
*/
export class ManageMirrorsyncCommand extends FilesCommand {
execute(_event: CommandEvent, _fileManager: CommandHandlerDeps) {
chrome.fileManagerPrivate.openManageSyncSettings();
}
override canExecute(event: CanExecuteEvent, fileManager: CommandHandlerDeps) {
// MirrorSync is only available to sync local directories, only show the
// folder when navigated to a local directory.
const currentRootType = fileManager.directoryModel.getCurrentRootType();
event.canExecute = (currentRootType === RootType.MY_FILES ||
currentRootType === RootType.DOWNLOADS) &&
isMirrorSyncEnabled();
event.command.setHidden(!event.canExecute);
}
}
/**
* A command to share the target folder with the specified Guest OS.
*/
export class GuestOsShareCommand extends FilesCommand {
/**
* @param vmName Name of the vm to share into.
* @param typeForStrings VM type to identify the strings used for this VM e.g.
* LINUX or PLUGIN_VM.
* @param settingsPath Path to the page in settings to manage sharing.
* @param manageUma MenuCommandsForUma entry this command should emit metrics
* under when the toast to manage sharing is clicked on.
* @param shareUma MenuCommandsForUma entry this command should emit metrics
* under.
*/
constructor(
private vmName_: string, private typeForStrings_: string,
private settingsPath_: string, private manageUma_: MenuCommandsForUma,
private shareUma_: MenuCommandsForUma) {
super();
this.validateTranslationStrings_();
}
/**
* Asserts that the necessary strings have been loaded into loadTimeData.
*/
private validateTranslationStrings_() {
if (!loadTimeData.isInitialized()) {
// Tests might not set loadTimeData.
return;
}
const translations = [
`FOLDER_SHARED_WITH_${this.typeForStrings_}`,
`SHARE_ROOT_FOLDER_WITH_${this.typeForStrings_}_TITLE`,
`SHARE_ROOT_FOLDER_WITH_${this.typeForStrings_}`,
`SHARE_ROOT_FOLDER_WITH_${this.typeForStrings_}_DRIVE`,
];
for (const translation of translations) {
console.assert(
loadTimeData.valueExists(translation),
`VM ${this.vmName_} doesn't have the translation string ${
translation}`);
}
}
execute(event: CommandEvent, fileManager: CommandHandlerDeps) {
const entry = getCommandEntry(fileManager, event.target);
if (!entry || !entry.isDirectory) {
return;
}
const info = fileManager.volumeManager.getLocationInfo(entry);
if (!info) {
return;
}
const share = () => {
// Always persist shares via right-click > Share with Linux.
chrome.fileManagerPrivate.sharePathsWithCrostini(
this.vmName_, [unwrapEntry(entry) as Entry], true /* persist */,
() => {
if (chrome.runtime.lastError) {
console.warn(
'Error sharing with guest: ' +
chrome.runtime.lastError.message);
}
});
// Show the 'Manage $typeForStrings sharing' toast immediately, since
// the guest may take a while to start.
fileManager.ui.toast.show(
str(`FOLDER_SHARED_WITH_${this.typeForStrings_}`), {
text: str('MANAGE_TOAST_BUTTON_LABEL'),
callback: () => {
chrome.fileManagerPrivate.openSettingsSubpage(this.settingsPath_);
recordMenuItemSelected(this.manageUma_);
},
});
};
// Show a confirmation dialog if we are sharing the root of a volume.
// Non-Drive volume roots are always '/'.
if (entry.fullPath === '/') {
fileManager.ui.confirmDialog.showHtml(
str(`SHARE_ROOT_FOLDER_WITH_${this.typeForStrings_}_TITLE`),
strf(
`SHARE_ROOT_FOLDER_WITH_${this.typeForStrings_}`,
info.volumeInfo?.label),
share, () => {});
} else if (
info.isRootEntry &&
(info.rootType === RootType.DRIVE ||
info.rootType === RootType.COMPUTERS_GRAND_ROOT ||
info.rootType === RootType.SHARED_DRIVES_GRAND_ROOT)) {
// Only show the dialog for My Drive, Shared Drives Grand Root and
// Computers Grand Root. Do not show for roots of a single Shared
// Drive or Computer.
fileManager.ui.confirmDialog.showHtml(
str(`SHARE_ROOT_FOLDER_WITH_${this.typeForStrings_}_TITLE`),
str(`SHARE_ROOT_FOLDER_WITH_${this.typeForStrings_}_DRIVE`), share,
() => {});
} else {
// This is not a root, share it without confirmation dialog.
share();
}
recordMenuItemSelected(this.shareUma_);
}
override canExecute(event: CanExecuteEvent, fileManager: CommandHandlerDeps) {
// Must be single directory not already shared.
const entries = getCommandEntries(fileManager, event.target);
event.canExecute = entries.length === 1 && entries[0]!.isDirectory &&
!isFakeEntry(entries[0]!) &&
!fileManager.crostini.isPathShared(this.vmName_, entries[0]!) &&
fileManager.crostini.canSharePath(
this.vmName_, entries[0]!, true /* persist */);
event.command.setHidden(!event.canExecute);
}
}
/**
* Creates a command for the gear icon to manage sharing.
*/
export class GuestOsManagingSharingGearCommand extends FilesCommand {
/**
* @param vmName Name of the vm to share into.
* @param settingsPath Path to the page in settings to manage sharing.
* @param manageUma MenuCommandsForUma entry this command should emit metrics
* under when the toast to manage sharing is clicked on.
*/
constructor(
private vmName_: string, private settingsPath_: string,
private manageUma_: MenuCommandsForUma) {
super();
}
execute(_event: CommandEvent, _fileManager: CommandHandlerDeps) {
chrome.fileManagerPrivate.openSettingsSubpage(this.settingsPath_);
recordMenuItemSelected(this.manageUma_);
}
override canExecute(event: CanExecuteEvent, fileManager: CommandHandlerDeps) {
event.canExecute = fileManager.crostini.isEnabled(this.vmName_);
event.command.setHidden(!event.canExecute);
}
}
/**
* Creates a command for managing sharing.
*/
export class GuestOsManagingSharingCommand extends FilesCommand {
/**
* @param vmName Name of the vm to share into.
* @param settingsPath Path to the page in settings to manage sharing.
* @param manageUma MenuCommandsForUma entry this command should emit metrics
* under when the toast to manage sharing is clicked on.
*/
constructor(
private vmName_: string, private settingsPath_: string,
private manageUma_: MenuCommandsForUma) {
super();
}
execute(_event: CommandEvent, _fileManager: CommandHandlerDeps) {
chrome.fileManagerPrivate.openSettingsSubpage(this.settingsPath_);
recordMenuItemSelected(this.manageUma_);
}
override canExecute(event: CanExecuteEvent, fileManager: CommandHandlerDeps) {
const entries = getCommandEntries(fileManager, event.target);
event.canExecute = entries.length === 1 && entries[0]!.isDirectory &&
fileManager.crostini.isPathShared(this.vmName_, entries[0]!);
event.command.setHidden(!event.canExecute);
}
}
/**
* Creates a shortcut of the selected folder (single only).
*/
export class PinFolderCommand extends FilesCommand {
execute(event: CommandEvent, fileManager: CommandHandlerDeps) {
const entries = getCommandEntries(fileManager, event.target);
const actionsController = fileManager.actionsController;
fileManager.actionsController.getActionsForEntries(entries).then(
(actionsModel: ActionsModel|void) => {
if (!actionsModel) {
return;
}
const action =
actionsModel.getAction(InternalActionId.CREATE_FOLDER_SHORTCUT);
if (action) {
actionsController.executeAction(action);
}
});
}
override canExecute(event: CanExecuteEvent, fileManager: CommandHandlerDeps) {
const entries = getCommandEntries(fileManager, event.target);
const command = event.command;
const actionsController = fileManager.actionsController;
// Avoid flickering menu height: synchronously define command visibility.
if (!isDriveEntries(entries, fileManager.volumeManager)) {
command.setHidden(true);
return;
}
command.setHidden(false);
function canExecuteCreateShortcut(actionsModel: ActionsModel|void) {
if (!actionsModel) {
return;
}
const action =
actionsModel.getAction(InternalActionId.CREATE_FOLDER_SHORTCUT);
event.canExecute = !!action && action.canExecute();
command.disabled = !event.canExecute;
command.setHidden(!action);
}
// Run synchrounously if possible.
const actionsModel =
actionsController.getInitializedActionsForEntries(entries);
if (actionsModel) {
canExecuteCreateShortcut(actionsModel);
return;
}
event.canExecute = true;
command.setHidden(false);
// Run async, otherwise.
actionsController.getActionsForEntries(entries).then(
canExecuteCreateShortcut);
}
}
/**
* Removes the folder shortcut.
*/
export class UnpinFolderCommand extends FilesCommand {
execute(event: CommandEvent, fileManager: CommandHandlerDeps) {
const entries = getCommandEntries(fileManager, event.target);
const actionsController = fileManager.actionsController;
fileManager.actionsController.getActionsForEntries(entries).then(
(actionsModel: ActionsModel|void) => {
if (!actionsModel) {
return;
}
const action =
actionsModel.getAction(InternalActionId.REMOVE_FOLDER_SHORTCUT);
if (action) {
actionsController.executeAction(action);
}
});
}
override canExecute(event: CanExecuteEvent, fileManager: CommandHandlerDeps) {
const entries = getCommandEntries(fileManager, event.target);
const command = event.command;
const actionsController = fileManager.actionsController;
// Avoid flickering menu height: synchronously define command visibility.
if (!isDriveEntries(entries, fileManager.volumeManager)) {
command.setHidden(true);
return;
}
command.setHidden(false);
function canExecuteRemoveShortcut(actionsModel: ActionsModel|void) {
if (!actionsModel) {
return;
}
const action =
actionsModel.getAction(InternalActionId.REMOVE_FOLDER_SHORTCUT);
command.setHidden(!action);
event.canExecute = !!action && action.canExecute();
command.disabled = !event.canExecute;
}
// Run synchrounously if possible.
const actionsModel =
actionsController.getInitializedActionsForEntries(entries);
if (actionsModel) {
canExecuteRemoveShortcut(actionsModel);
return;
}
event.canExecute = true;
command.setHidden(false);
// Run async, otherwise.
actionsController.getActionsForEntries(entries).then(
canExecuteRemoveShortcut);
}
}
/**
* Zoom in to the Files app.
*/
export class ZoomInCommand extends FilesCommand {
execute(_event: CommandEvent, _fileManager: CommandHandlerDeps) {
chrome.fileManagerPrivate.zoom(
chrome.fileManagerPrivate.ZoomOperationType.IN);
}
}
/**
* Zoom out from the Files app.
*/
export class ZoomOutCommand extends FilesCommand {
execute(_event: CommandEvent, _fileManager: CommandHandlerDeps) {
chrome.fileManagerPrivate.zoom(
chrome.fileManagerPrivate.ZoomOperationType.OUT);
}
}
/**
* Reset the zoom factor.
*/
export class ZoomResetCommand extends FilesCommand {
execute(_event: CommandEvent, _fileManager: CommandHandlerDeps) {
chrome.fileManagerPrivate.zoom(
chrome.fileManagerPrivate.ZoomOperationType.RESET);
}
}
/**
* Sort the file list by name (in ascending order).
*/
export class SortByNameCommand extends FilesCommand {
execute(_event: CommandEvent, fileManager: CommandHandlerDeps) {
if (fileManager.directoryModel.getFileList()) {
fileManager.directoryModel.getFileList().sort('name', 'asc');
const msg = strf('COLUMN_SORTED_ASC', str('NAME_COLUMN_LABEL'));
fileManager.ui.speakA11yMessage(msg);
}
}
}
/**
* Sort the file list by size (in descending order).
*/
export class SortBySizeCommand extends FilesCommand {
execute(_event: CommandEvent, fileManager: CommandHandlerDeps) {
if (fileManager.directoryModel.getFileList()) {
fileManager.directoryModel.getFileList().sort('size', 'desc');
const msg = strf('COLUMN_SORTED_DESC', str('SIZE_COLUMN_LABEL'));
fileManager.ui.speakA11yMessage(msg);
}
}
}
/**
* Sort the file list by type (in ascending order).
*/
export class SortByTypeCommand extends FilesCommand {
execute(_event: CommandEvent, fileManager: CommandHandlerDeps) {
if (fileManager.directoryModel.getFileList()) {
fileManager.directoryModel.getFileList().sort('type', 'asc');
const msg = strf('COLUMN_SORTED_ASC', str('TYPE_COLUMN_LABEL'));
fileManager.ui.speakA11yMessage(msg);
}
}
}
/**
* Sort the file list by date-modified (in descending order).
*/
export class SortByDateCommand extends FilesCommand {
execute(_event: CommandEvent, fileManager: CommandHandlerDeps) {
if (fileManager.directoryModel.getFileList()) {
fileManager.directoryModel.getFileList().sort('modificationTime', 'desc');
const msg = strf('COLUMN_SORTED_DESC', str('DATE_COLUMN_LABEL'));
fileManager.ui.speakA11yMessage(msg);
}
}
}
/**
* Open inspector for foreground page.
*/
export class InspectNormalCommand extends FilesCommand {
execute(_event: CommandEvent, _fileManager: CommandHandlerDeps) {
chrome.fileManagerPrivate.openInspector(
chrome.fileManagerPrivate.InspectionType.NORMAL);
}
}
/**
* Open inspector for foreground page and bring focus to the console.
*/
export class InspectConsoleCommand extends FilesCommand {
execute(_event: CommandEvent, _fileManager: CommandHandlerDeps) {
chrome.fileManagerPrivate.openInspector(
chrome.fileManagerPrivate.InspectionType.CONSOLE);
}
}
/**
* Open inspector for foreground page in inspect element mode.
*/
export class InspectElementCommand extends FilesCommand {
execute(_event: CommandEvent, _fileManager: CommandHandlerDeps) {
chrome.fileManagerPrivate.openInspector(
chrome.fileManagerPrivate.InspectionType.ELEMENT);
}
}
/**
* Opens the gear menu.
*/
export class OpenGearMenuCommand extends FilesCommand {
execute(_event: CommandEvent, fileManager: CommandHandlerDeps) {
fileManager.ui.gearButton.showMenu(true);
}
}
/**
* Focus the first button visible on action bar (at the top).
*/
export class FocusActionBarCommand extends FilesCommand {
execute(_event: CommandEvent, fileManager: CommandHandlerDeps) {
fileManager.ui.actionbar
.querySelector<HTMLButtonElement|CrButtonElement>(
'button:not([hidden]), cr-button:not([hidden])')
?.focus();
}
}
/**
* Handle back button.
*/
export class BrowserBackCommand extends FilesCommand {
execute(_event: CommandEvent, _fileManager: CommandHandlerDeps) {
// TODO(fukino): It should be better to minimize Files app only when there
// is no back stack, and otherwise use BrowserBack for history navigation.
// https://crbug.com/624100.
// TODO(crbug.com/40701086): Implement minimize for files SWA, then
// call its minimize() function here.
}
}
/**
* Configures the currently selected volume.
*/
export class ConfigureCommand extends FilesCommand {
execute(event: CommandEvent, fileManager: CommandHandlerDeps) {
const volumeInfo = getElementVolumeInfo(event.target, fileManager);
if (volumeInfo && volumeInfo.configurable) {
fileManager.volumeManager.configure(volumeInfo);
}
}
override canExecute(event: CanExecuteEvent, fileManager: CommandHandlerDeps) {
const volumeInfo = getElementVolumeInfo(event.target, fileManager);
event.canExecute = !!volumeInfo && volumeInfo.configurable;
event.command.setHidden(!event.canExecute);
}
}
/**
* Refreshes the currently selected directory.
*/
export class RefreshCommand extends FilesCommand {
execute(_event: CommandEvent, fileManager: CommandHandlerDeps) {
fileManager.directoryModel.rescan(true /* refresh */);
fileManager.spinnerController.blink();
}
override canExecute(event: CanExecuteEvent, fileManager: CommandHandlerDeps) {
const currentDirEntry = fileManager.directoryModel.getCurrentDirEntry();
const volumeInfo = currentDirEntry &&
fileManager.volumeManager.getVolumeInfo(currentDirEntry);
event.canExecute = !!volumeInfo && !volumeInfo.watchable;
event.command.setHidden(
!event.canExecute ||
fileManager.directoryModel.getFileListSelection().getCheckSelectMode());
}
}
/**
* Sets the system wallpaper to the selected file.
*/
export class SetWallpaperCommand extends FilesCommand {
execute(_event: CommandEvent, fileManager: CommandHandlerDeps) {
if (isOnTrashRoot(fileManager)) {
return;
}
const entry = fileManager.getSelection().entries[0] as FileEntry;
new Promise<File>((resolve, reject) => {
entry.file(resolve, reject);
})
.then((blob: File) => {
const fileReader = new FileReader();
return new Promise<ArrayBuffer|null|undefined>((resolve, reject) => {
fileReader.onload = () => {
resolve(fileReader.result as ArrayBuffer);
};
fileReader.onerror = () => {
reject(fileReader.error);
};
fileReader.readAsArrayBuffer(blob);
});
})
.then((arrayBuffer: ArrayBuffer|null|undefined) => {
assert(arrayBuffer);
return chrome.wallpaper.setWallpaper({
data: arrayBuffer,
layout: chrome.wallpaper.WallpaperLayout.CENTER_CROPPED,
filename: 'wallpaper',
});
})
.catch(() => {
fileManager.ui.alertDialog.showHtml(
'', str('ERROR_INVALID_WALLPAPER'));
});
}
override canExecute(event: CanExecuteEvent, fileManager: CommandHandlerDeps) {
if (isOnTrashRoot(fileManager)) {
event.canExecute = false;
event.command.setHidden(true);
return;
}
const entries = fileManager.getSelection().entries;
if (entries.length === 0) {
event.canExecute = false;
event.command.setHidden(true);
return;
}
const type = getType(entries[0]!);
if (entries.length !== 1 || type.type !== 'image') {
event.canExecute = false;
event.command.setHidden(true);
return;
}
event.canExecute = type.subtype === 'JPEG' || type.subtype === 'PNG';
event.command.setHidden(false);
}
}
/**
* Opens settings/storage sub page.
*/
export class VolumeStorageCommand extends FilesCommand {
execute(_event: CommandEvent, _fileManager: CommandHandlerDeps) {
chrome.fileManagerPrivate.openSettingsSubpage('storage');
}
override canExecute(event: CanExecuteEvent, fileManager: CommandHandlerDeps) {
event.canExecute = false;
const currentVolumeInfo = fileManager.directoryModel.getCurrentVolumeInfo();
if (!currentVolumeInfo) {
return;
}
// Can execute only for local file systems.
if (currentVolumeInfo.volumeType === VolumeType.MY_FILES ||
currentVolumeInfo.volumeType === VolumeType.DOWNLOADS ||
currentVolumeInfo.volumeType === VolumeType.CROSTINI ||
currentVolumeInfo.volumeType === VolumeType.GUEST_OS ||
currentVolumeInfo.volumeType === VolumeType.ANDROID_FILES ||
currentVolumeInfo.volumeType === VolumeType.DOCUMENTS_PROVIDER) {
event.canExecute = true;
}
}
}
/**
* Opens "providers menu" to allow users to use providers/FSPs.
*/
export class ShowProvidersSubmenuCommand extends FilesCommand {
execute(_event: CommandEvent, fileManager: CommandHandlerDeps) {
fileManager.ui.gearButton.showSubMenu();
}
override canExecute(event: CanExecuteEvent, fileManager: CommandHandlerDeps) {
if (fileManager.dialogType !== DialogType.FULL_PAGE) {
event.canExecute = false;
} else {
event.canExecute = !fileManager.guestMode;
}
}
}