[go: nahoru, domu]

blob: 84cc104f74796047da3cda9cd60f311d46f15d79 [file] [log] [blame]
// 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 {assertNotReached} from 'chrome://resources/ash/common/assert.js';
import {NativeEventTarget as EventTarget} from 'chrome://resources/ash/common/event_target.js';
import {isOneDrive} from '../../common/js/entry_utils.js';
import {EntryList, VolumeEntry} from '../../common/js/files_app_entry_types.js';
import {isArcVmEnabled, isGuestOsEnabled, isSinglePartitionFormatEnabled} from '../../common/js/flags.js';
import {str} from '../../common/js/translations.js';
import {RootType, VolumeType} from '../../common/js/volume_manager_types.js';
import {FilesAppEntry} from '../../externs/files_app_entry_interfaces.js';
import {DialogType} from '../../externs/ts/state.js';
import {getStore} from '../../state/store.js';
import {AndroidAppListModel} from './android_app_list_model.js';
import {DirectoryModel} from './directory_model.js';
import {FolderShortcutsDataModel} from './folder_shortcuts_data_model.js';
/**
* @enum {string}
*/
export const NavigationModelItemType = {
SHORTCUT: 'shortcut',
VOLUME: 'volume',
RECENT: 'recent',
CROSTINI: 'crostini',
GUEST_OS: 'guest-os',
ENTRY_LIST: 'entry-list',
DRIVE: 'drive',
ANDROID_APP: 'android-app',
TRASH: 'trash',
};
/**
* Navigation List sections. A section is just a visual grouping of some items.
*
* Sections:
* - TOP: Recents, Shortcuts.
* - MY_FILES: My Files (which includes Downloads, Crostini and Arc++ as
* its children).
* - REMOVABLE: Archives, MTPs, Media Views and Removables.
* - GOOGLE_DRIVE: Just Google Drive.
* - ODFS: Just ODFS.
* - CLOUD: All other cloud: SMBs, FSPs and Documents Providers.
* - ANDROID_APPS: ANDROID picker apps.
* @enum {string}
*/
export const NavigationSection = {
TOP: 'top',
MY_FILES: 'my_files',
REMOVABLE: 'removable',
GOOGLE_DRIVE: 'google_drive',
ODFS: 'odfs',
CLOUD: 'cloud',
ANDROID_APPS: 'android_apps',
};
/**
* Base item of NavigationListModel. Should not be created directly.
*/
export class NavigationModelItem {
/**
* @param {string} label
* @param {NavigationModelItemType} type
*/
constructor(label, type) {
this.label_ = label;
this.type_ = type;
/**
* @type {boolean} whether this item is disabled for selection.
*/
this.disabled_ = false;
/**
* @type {NavigationSection} section which this item belongs to.
*/
this.section_ = NavigationSection.TOP;
/** @type {number} original order when returned from VolumeManager. */
this.originalOrder_ = -1;
}
get label() {
return this.label_;
}
get type() {
return this.type_;
}
/** @return {boolean} */
get disabled() {
return this.disabled_;
}
/** @param {boolean} disabled */
set disabled(disabled) {
this.disabled_ = disabled;
}
/** @return {NavigationSection} */
get section() {
return this.section_;
}
/** @param {NavigationSection} section */
set section(section) {
this.section_ = section;
}
/** @return {number} */
get originalOrder() {
return this.originalOrder_;
}
set originalOrder(order) {
this.originalOrder_ = order;
}
}
/**
* Item of NavigationListModel for shortcuts.
*/
export class NavigationModelShortcutItem extends NavigationModelItem {
/**
* @param {string} label Label.
* @param {!DirectoryEntry} entry Entry. Cannot be null.
*/
constructor(label, entry) {
super(label, NavigationModelItemType.SHORTCUT);
this.entry_ = entry;
}
get entry() {
return this.entry_;
}
}
/**
* Item of NavigationListModel for Android apps.
*/
export class NavigationModelAndroidAppItem extends NavigationModelItem {
/**
* @param {!chrome.fileManagerPrivate.AndroidApp} androidApp Android app.
* Cannot be null.
*/
constructor(androidApp) {
super(androidApp.name, NavigationModelItemType.ANDROID_APP);
/** @private @type {!chrome.fileManagerPrivate.AndroidApp} */
this.androidApp_ = androidApp;
}
/** @return {!chrome.fileManagerPrivate.AndroidApp} */
get androidApp() {
return this.androidApp_;
}
}
/**
* Item of NavigationListModel for volumes.
*/
export class NavigationModelVolumeItem extends NavigationModelItem {
/**
* @param {string} label Label.
* @param {!import('../../externs/volume_info.js').VolumeInfo} volumeInfo
* Volume info for the volume. Cannot be null.
*/
constructor(label, volumeInfo) {
super(label, NavigationModelItemType.VOLUME);
this.volumeInfo_ = volumeInfo;
// Start resolving the display root because it is used
// for determining executability of commands.
this.volumeInfo_.resolveDisplayRoot(() => {}, () => {});
}
get volumeInfo() {
return this.volumeInfo_;
}
}
/**
* Item of NavigationListModel for a fake item such as Recent or Linux files.
*/
export class NavigationModelFakeItem extends NavigationModelItem {
/**
* @param {string} label Label on the menu button.
* @param {NavigationModelItemType} type
* @param {!FilesAppEntry} entry Fake entry for the root folder.
*/
constructor(label, type, entry) {
super(label, type);
this.entry_ = entry;
}
get entry() {
return this.entry_;
}
}
/**
* A navigation list model. This model combines multiple models.
*/
export class NavigationListModel extends EventTarget {
/**
* @param {!import('../../externs/volume_manager.js').VolumeManager}
* volumeManager VolumeManager instance.
* @param {!FolderShortcutsDataModel} shortcutListModel The list of folder
* shortcut.
* @param {?NavigationModelFakeItem} recentModelItem Recent folder.
* @param {!DirectoryModel} directoryModel
* @param {!AndroidAppListModel} androidAppListModel
* @param {!DialogType} dialogType
*/
constructor(
volumeManager, shortcutListModel, recentModelItem, directoryModel,
androidAppListModel, dialogType) {
super();
/**
* @private @type {!import('../../externs/volume_manager.js').VolumeManager}
* @const
*/
this.volumeManager_ = volumeManager;
/**
* @private @type {!FolderShortcutsDataModel}
* @const
*/
this.shortcutListModel_ = shortcutListModel;
/**
* @private @type {NavigationModelFakeItem}
* @const
*/
// @ts-ignore: error TS2322: Type 'NavigationModelFakeItem | null' is not
// assignable to type 'NavigationModelFakeItem'.
this.recentModelItem_ = recentModelItem;
/**
* @private @type {!DirectoryModel}
* @const
*/
this.directoryModel_ = directoryModel;
/**
* @private @type {!AndroidAppListModel}
*/
this.androidAppListModel_ = androidAppListModel;
/**
* @private @type {!DialogType}
*/
this.dialogType_ = dialogType;
/**
* Root folder for crostini Linux files.
* This field will be modified when crostini is enabled/disabled.
* @private @type {NavigationModelFakeItem}
*/
// @ts-ignore: error TS2322: Type 'null' is not assignable to type
// 'NavigationModelFakeItem'.
this.linuxFilesItem_ = null;
/**
* Root folders for Guest OS files.
* This field will be modified when new guests are added/removed.
* @private @type {!Array<!NavigationModelFakeItem>}
*/
// @ts-ignore: error TS7008: Member 'guestOsPlaceholders_' implicitly has an
// 'any[]' type.
this.guestOsPlaceholders_ = [];
/**
* Root folder for trash.
* @private @type {?NavigationModelFakeItem}
*/
this.trashItem_ = null;
/**
* NavigationModel for MyFiles, since DirectoryTree expect it to be always
* the same reference we keep the initial reference for reuse.
* @private @type {NavigationModelFakeItem}
*/
// @ts-ignore: error TS2322: Type 'null' is not assignable to type
// 'NavigationModelFakeItem'.
this.myFilesModel_ = null;
/**
* A collection of NavigationModel objects for Removable partition groups.
* Store the reference to each model here since DirectoryTree expects it to
* always have the same reference.
* @private @type {!Map<string, !NavigationModelFakeItem>}
*/
this.removableModels_ = new Map();
/**
* All root navigation items in display order.
* @private @type {!Array<!NavigationModelItem>}
*/
// @ts-ignore: error TS7008: Member 'navigationItems_' implicitly has an
// 'any[]' type.
this.navigationItems_ = [];
/** @private @type {?NavigationModelFakeItem} */
this.fakeDriveItem_;
// @ts-ignore: error TS7006: Parameter 'volumeInfo' implicitly has an 'any'
// type.
const volumeInfoToModelItem = volumeInfo => {
return new NavigationModelVolumeItem(volumeInfo.label, volumeInfo);
};
// @ts-ignore: error TS7006: Parameter 'entry' implicitly has an 'any' type.
const entryToModelItem = entry => {
const item = new NavigationModelShortcutItem(entry.name, entry);
return item;
};
/**
* Type of updated list.
* @enum {number}
* @const
*/
const ListType = {VOLUME_LIST: 1, SHORTCUT_LIST: 2, ANDROID_APP_LIST: 3};
Object.freeze(ListType);
// Generates this.volumeList_ and this.shortcutList_ from the models.
this.volumeList_ = [];
for (let i = 0; i < this.volumeManager_.volumeInfoList.length; i++) {
this.volumeList_.push(
volumeInfoToModelItem(this.volumeManager_.volumeInfoList.item(i)));
}
this.shortcutList_ = [];
for (let i = 0; i < this.shortcutListModel_.length; i++) {
const shortcutEntry =
/** @type {!Entry} */ (this.shortcutListModel_.item(i));
// @ts-ignore: error TS6133: 'volumeInfo' is declared but its value is
// never read.
const volumeInfo = this.volumeManager_.getVolumeInfo(shortcutEntry);
this.shortcutList_.push(entryToModelItem(shortcutEntry));
}
this.androidAppList_ = [];
for (let i = 0; i < this.androidAppListModel_.length(); i++) {
this.androidAppList_.push(
/** @type {chrome.fileManagerPrivate.AndroidApp} */ (
this.androidAppListModel_.item(i)));
}
// Reorder volumes, shortcuts, and optional items for initial display.
this.refreshNavigationItems();
// Generates a combined 'permuted' event from an event of either volumeList
// or shortcutList.
// @ts-ignore: error TS7006: Parameter 'event' implicitly has an 'any' type.
const permutedHandler = function(listType, event) {
let permutation;
// Build the volumeList.
if (listType == ListType.VOLUME_LIST) {
// The volume is mounted or unmounted.
const newList = [];
// Use the old instances if they just move.
for (let i = 0; i < event.detail.permutation.length; i++) {
if (event.detail.permutation[i] >= 0) {
// @ts-ignore: error TS2532: Object is possibly 'undefined'.
newList[event.detail.permutation[i]] = this.volumeList_[i];
}
}
// Create missing instances.
for (let i = 0; i < event.detail.newLength; i++) {
if (!newList[i]) {
newList[i] = volumeInfoToModelItem(
// @ts-ignore: error TS2339: Property 'volumeManager_' does not
// exist on type 'permutedHandler'.
this.volumeManager_.volumeInfoList.item(i));
}
}
this.volumeList_ = newList;
permutation = event.detail.permutation.slice();
// shortcutList part has not been changed, so the permutation should be
// just identity mapping with a shift.
// @ts-ignore: error TS2532: Object is possibly 'undefined'.
for (let i = 0; i < this.shortcutList_.length; i++) {
permutation.push(i + this.volumeList_.length);
}
} else if (listType == ListType.SHORTCUT_LIST) {
// Build the shortcutList.
// volumeList part has not been changed, so the permutation should be
// identity mapping.
permutation = [];
// @ts-ignore: error TS2532: Object is possibly 'undefined'.
for (let i = 0; i < this.volumeList_.length; i++) {
permutation[i] = i;
}
let modelIndex = 0;
let oldListIndex = 0;
const newList = [];
// @ts-ignore: error TS2551: Property 'shortcutListModel_' does not
// exist on type 'permutedHandler'. Did you mean 'shortcutList_'?
while (modelIndex < this.shortcutListModel_.length &&
// @ts-ignore: error TS2532: Object is possibly 'undefined'.
oldListIndex < this.shortcutList_.length) {
// @ts-ignore: error TS2551: Property 'shortcutListModel_' does not
// exist on type 'permutedHandler'. Did you mean 'shortcutList_'?
const shortcutEntry = this.shortcutListModel_.item(modelIndex);
// @ts-ignore: error TS2551: Property 'shortcutListModel_' does not
// exist on type 'permutedHandler'. Did you mean 'shortcutList_'?
const cmp = this.shortcutListModel_.compare(
/** @type {Entry} */ (shortcutEntry),
// @ts-ignore: error TS2532: Object is possibly 'undefined'.
this.shortcutList_[oldListIndex].entry);
if (cmp > 0) {
// The shortcut at shortcutList_[oldListIndex] is removed.
permutation.push(-1);
oldListIndex++;
continue;
}
if (cmp === 0) {
// Reuse the old instance.
// @ts-ignore: error TS2532: Object is possibly 'undefined'.
permutation.push(newList.length + this.volumeList_.length);
// @ts-ignore: error TS2532: Object is possibly 'undefined'.
newList.push(this.shortcutList_[oldListIndex]);
oldListIndex++;
} else {
// We needs to create a new instance for the shortcut entry.
newList.push(entryToModelItem(shortcutEntry));
}
modelIndex++;
}
// Add remaining (new) shortcuts if necessary.
// @ts-ignore: error TS2551: Property 'shortcutListModel_' does not
// exist on type 'permutedHandler'. Did you mean 'shortcutList_'?
for (; modelIndex < this.shortcutListModel_.length; modelIndex++) {
// @ts-ignore: error TS2551: Property 'shortcutListModel_' does not
// exist on type 'permutedHandler'. Did you mean 'shortcutList_'?
const shortcutEntry = this.shortcutListModel_.item(modelIndex);
newList.push(entryToModelItem(shortcutEntry));
}
// Fill remaining permutation if necessary.
// @ts-ignore: error TS2532: Object is possibly 'undefined'.
for (; oldListIndex < this.shortcutList_.length; oldListIndex++) {
permutation.push(-1);
}
this.shortcutList_ = newList;
} else if (listType == ListType.ANDROID_APP_LIST) {
this.androidAppList_ = [];
// @ts-ignore: error TS2551: Property 'androidAppListModel_' does not
// exist on type 'permutedHandler'. Did you mean 'androidAppList_'?
for (let i = 0; i < this.androidAppListModel_.length(); i++) {
// @ts-ignore: error TS2551: Property 'androidAppListModel_' does not
// exist on type 'permutedHandler'. Did you mean 'androidAppList_'?
this.androidAppList_.push(this.androidAppListModel_.item(i));
}
}
// Reorder items after permutation.
// @ts-ignore: error TS2339: Property 'refreshNavigationItems' does not
// exist on type 'permutedHandler'.
this.refreshNavigationItems();
// Dispatch permuted event.
const permutedEvent = new CustomEvent('permuted', {
detail: {
// @ts-ignore: error TS2532: Object is possibly 'undefined'
newLength: this.volumeList_.length + this.shortcutList_.length +
// @ts-ignore: error TS2532: Object is possibly 'undefined'
this.androidAppList_.length,
permutation,
},
});
// @ts-ignore: error TS2339: Property 'dispatchEvent' does not exist on
// type 'permutedHandler'.
this.dispatchEvent(permutedEvent);
};
this.volumeManager_.volumeInfoList.addEventListener(
'permuted', permutedHandler.bind(this, ListType.VOLUME_LIST));
this.shortcutListModel_.addEventListener(
'permuted', permutedHandler.bind(this, ListType.SHORTCUT_LIST));
this.androidAppListModel_.addEventListener(
'permuted', permutedHandler.bind(this, ListType.ANDROID_APP_LIST));
// 'change' event is just ignored, because it is not fired neither in
// the folder shortcut list nor in the volume info list.
// 'splice' and 'sorted' events are not implemented, since they are not used
// in list.js.
}
get length() {
return this.length_();
}
get folderShortcutList() {
return this.shortcutList_;
}
/**
* Set the crostini Linux files root and reorder items.
* @param {NavigationModelFakeItem} item Linux files root.
*/
set linuxFilesItem(item) {
this.linuxFilesItem_ = item;
this.refreshNavigationItems();
}
/**
* Set the Guest OS Files placeholder roots and reorder items.
* @param {!Array<!NavigationModelFakeItem>} items Guest OS Files roots.
*/
set guestOsPlaceholders(items) {
this.guestOsPlaceholders_ = items;
this.refreshNavigationItems();
}
/**
* Set the fake Drive root and reorder items.
* @param {?NavigationModelFakeItem} item Fake Drive root.
*/
set fakeDriveItem(item) {
this.fakeDriveItem_ = item;
this.refreshNavigationItems();
}
/**
* Set the fake Trash root and reorder items.
* @param {?NavigationModelFakeItem} item Fake Trash root.
*/
set fakeTrashItem(item) {
this.trashItem_ = item;
this.refreshNavigationItems();
}
/**
* Refresh list of navigation items.
*/
refreshNavigationItems() {
return this.orderAndNestItems_();
}
/**
* Reorder navigation items and nest some within "Downloads"
* which will be displayed as "My-Files". Desired order:
* 1. Recents.
* 2. Media Views (Images, Videos and Audio).
* 3. Shortcuts.
* 4. "My-Files" (grouping), actually Downloads volume.
* 4.1. Downloads
* 4.2. Play files (android volume) (if enabled).
* 4.3. Linux files (crostini volume or fake item) (if enabled).
* 5. Google Drive.
* 6. ODFS.
* 7. SMBs.
* 8. Other FSP (File System Provider) (when mounted).
* 9. Other volumes (MTP, ARCHIVE, REMOVABLE).
* 10. Add new services if (it exists).
* @private
*/
orderAndNestItems_() {
const volumeIndexes = {};
const volumeList = this.volumeList_;
// Find the index of each volumeType from the array volumeList_,
// for volumes that can have multiple entries it saves as list
// of indexes, otherwise saves the index as int directly.
for (let i = 0; i < volumeList.length; i++) {
// @ts-ignore: error TS2532: Object is possibly 'undefined'.
const volumeType = volumeList[i].volumeInfo.volumeType;
// @ts-ignore: error TS2532: Object is possibly 'undefined'.
volumeList[i].originalOrder = i;
// @ts-ignore: error TS6133: 'providedType' is declared but its value is
// never read.
let providedType;
// @ts-ignore: error TS6133: 'volumeId' is declared but its value is never
// read.
let volumeId;
switch (volumeType) {
case VolumeType.CROSTINI:
case VolumeType.DOWNLOADS:
case VolumeType.ANDROID_FILES:
// @ts-ignore: error TS7053: Element implicitly has an 'any' type
// because expression of type 'string' can't be used to index type
// '{}'.
volumeIndexes[volumeType] = i;
break;
case VolumeType.PROVIDED:
case VolumeType.REMOVABLE:
case VolumeType.ARCHIVE:
case VolumeType.MTP:
case VolumeType.DRIVE:
case VolumeType.MEDIA_VIEW:
case VolumeType.DOCUMENTS_PROVIDER:
case VolumeType.SMB:
case VolumeType.GUEST_OS:
// @ts-ignore: error TS7053: Element implicitly has an 'any' type
// because expression of type 'string' can't be used to index type
// '{}'.
if (!volumeIndexes[volumeType]) {
// @ts-ignore: error TS7053: Element implicitly has an 'any' type
// because expression of type 'string' can't be used to index type
// '{}'.
volumeIndexes[volumeType] = [i];
} else {
// @ts-ignore: error TS7053: Element implicitly has an 'any' type
// because expression of type 'string' can't be used to index type
// '{}'.
volumeIndexes[volumeType].push(i);
}
break;
default:
assertNotReached(`No explict order for VolumeType: "${volumeType}"`);
break;
}
}
/**
* @param {!VolumeType} volumeType the desired volume
* type to be filtered from volumeList.
* @return {NavigationModelVolumeItem}
*/
const getSingleVolume = volumeType => {
// @ts-ignore: error TS7053: Element implicitly has an 'any' type because
// expression of type 'string' can't be used to index type '{}'.
return volumeList[volumeIndexes[volumeType]];
};
/**
* @param {!VolumeType} volumeType the desired volume
* type to be filtered from volumeList.
* @return Array<!NavigationModelVolumeItem>
*/
const getVolumes = volumeType => {
// @ts-ignore: error TS7053: Element implicitly has an 'any' type because
// expression of type 'string' can't be used to index type '{}'.
const indexes = volumeIndexes[volumeType] || [];
// @ts-ignore: error TS7006: Parameter 'idx' implicitly has an 'any' type.
return indexes.map(idx => volumeList[idx]);
};
/**
* Removable volumes which share the same device path (i.e. partitions) are
* grouped.
* @return !Map<string, !Array<!NavigationModelVolumeItem>>
*/
const groupRemovables = () => {
const removableGroups = new Map();
const removableVolumes = getVolumes(VolumeType.REMOVABLE);
for (const removable of removableVolumes) {
// Partitions on the same physical device share device path and drive
// label. Create keys using these two identifiers.
const key = removable.volumeInfo.devicePath + '/' +
removable.volumeInfo.driveLabel;
if (!removableGroups.has(key)) {
// New key, so create a new array to hold partitions.
removableGroups.set(key, []);
}
// Add volume to array of volumes matching device path and drive label.
removableGroups.get(key).push(removable);
}
return removableGroups;
};
// Items as per required order.
this.navigationItems_ = [];
if (this.recentModelItem_) {
this.navigationItems_.push(this.recentModelItem_);
}
// Shortcuts.
for (const shortcut of this.shortcutList_) {
this.navigationItems_.push(shortcut);
}
let myFilesEntry;
let myFilesModel;
if (!this.myFilesModel_) {
// When MyFilesVolume is enabled we use the Downloads volume to be the
// MyFiles volume.
const myFilesVolumeModel = getSingleVolume(VolumeType.DOWNLOADS);
if (myFilesVolumeModel) {
myFilesEntry = new VolumeEntry(myFilesVolumeModel.volumeInfo);
myFilesModel = new NavigationModelFakeItem(
str('MY_FILES_ROOT_LABEL'), NavigationModelItemType.ENTRY_LIST,
myFilesEntry);
myFilesModel.section = NavigationSection.MY_FILES;
this.myFilesModel_ = myFilesModel;
} else {
// When MyFiles volume isn't available we create a empty EntryList to
// be MyFiles to be able to display Linux or Play volumes. However we
// don't save it back to this.MyFilesModel_ so it's always re-created.
myFilesEntry =
new EntryList(str('MY_FILES_ROOT_LABEL'), RootType.MY_FILES);
myFilesModel = new NavigationModelFakeItem(
myFilesEntry.label, NavigationModelItemType.ENTRY_LIST,
myFilesEntry);
myFilesModel.section = NavigationSection.MY_FILES;
}
} else {
myFilesEntry = this.myFilesModel_.entry;
myFilesModel = this.myFilesModel_;
}
this.navigationItems_.push(myFilesModel);
// Add Android to My Files.
const androidVolume = getSingleVolume(VolumeType.ANDROID_FILES);
if (androidVolume) {
// Only add volume if MyFiles doesn't have it yet.
// @ts-ignore: error TS2339: Property 'findIndexByVolumeInfo' does not
// exist on type 'FilesAppEntry | EntryList | VolumeEntry'.
if (myFilesEntry.findIndexByVolumeInfo(androidVolume.volumeInfo) === -1) {
const volumeEntry = new VolumeEntry(androidVolume.volumeInfo);
volumeEntry.disabled =
this.volumeManager_.isDisabled(VolumeType.ANDROID_FILES);
// @ts-ignore: error TS2339: Property 'addEntry' does not exist on type
// 'FilesAppEntry | EntryList | VolumeEntry'.
myFilesEntry.addEntry(volumeEntry);
}
} else {
// @ts-ignore: error TS2339: Property 'removeByVolumeType' does not exist
// on type 'FilesAppEntry | EntryList | VolumeEntry'.
myFilesEntry.removeByVolumeType(VolumeType.ANDROID_FILES);
}
// Add Linux to My Files.
const crostiniVolume = getSingleVolume(VolumeType.CROSTINI);
// Remove Crostini FakeEntry, it's re-added below if needed.
// @ts-ignore: error TS2339: Property 'removeAllByRootType' does not exist
// on type 'FilesAppEntry | EntryList | VolumeEntry'.
myFilesEntry.removeAllByRootType(RootType.CROSTINI);
if (crostiniVolume) {
// Crostini is mounted so add it if MyFiles doesn't have it yet.
// @ts-ignore: error TS2339: Property 'findIndexByVolumeInfo' does not
// exist on type 'FilesAppEntry | EntryList | VolumeEntry'.
if (myFilesEntry.findIndexByVolumeInfo(crostiniVolume.volumeInfo) ===
-1) {
const volumeEntry = new VolumeEntry(crostiniVolume.volumeInfo);
volumeEntry.disabled =
this.volumeManager_.isDisabled(VolumeType.CROSTINI);
// @ts-ignore: error TS2339: Property 'addEntry' does not exist on type
// 'FilesAppEntry | EntryList | VolumeEntry'.
myFilesEntry.addEntry(volumeEntry);
}
} else {
// @ts-ignore: error TS2339: Property 'removeByVolumeType' does not exist
// on type 'FilesAppEntry | EntryList | VolumeEntry'.
myFilesEntry.removeByVolumeType(VolumeType.CROSTINI);
if (this.linuxFilesItem_) {
// Here it's just a fake item, we link the navigation model so
// DirectoryTree can choose the correct DirectoryItem for it.
// @ts-ignore: error TS2339: Property 'navigationModel' does not exist
// on type 'FilesAppEntry'.
this.linuxFilesItem_.entry.navigationModel = this.linuxFilesItem_;
// @ts-ignore: error TS2339: Property 'addEntry' does not exist on type
// 'FilesAppEntry | EntryList | VolumeEntry'.
myFilesEntry.addEntry(this.linuxFilesItem_.entry);
}
}
// TODO(crbug/1293229): To start with, we only support listing and not
// mounting, which means we're just dealing with fake entries here. This
// gets updated to handle real volumes once we support mounting them.
if (isGuestOsEnabled()) {
// Remove all GuestOs placeholders, we readd any if they're needed.
// @ts-ignore: error TS2339: Property 'removeAllByRootType' does not exist
// on type 'FilesAppEntry | EntryList | VolumeEntry'.
myFilesEntry.removeAllByRootType(RootType.GUEST_OS);
// For each volume, add any which aren't already in the list.
let guestOsVolumes = getVolumes(VolumeType.GUEST_OS);
if (isArcVmEnabled()) {
// Remove GuestOs Android placeholder, similar to what we did for
// GuestOs placeholders. This should be readded if needed.
// @ts-ignore: error TS2339: Property 'removeAllByRootType' does not
// exist on type 'FilesAppEntry | EntryList | VolumeEntry'.
myFilesEntry.removeAllByRootType(RootType.ANDROID_FILES);
const androidVolume = getSingleVolume(VolumeType.ANDROID_FILES);
if (androidVolume) {
androidVolume.disabled =
this.volumeManager_.isDisabled(VolumeType.ANDROID_FILES);
guestOsVolumes = guestOsVolumes.concat(androidVolume);
}
}
for (const volume of guestOsVolumes) {
// @ts-ignore: error TS2339: Property 'findIndexByVolumeInfo' does not
// exist on type 'FilesAppEntry | EntryList | VolumeEntry'.
if (myFilesEntry.findIndexByVolumeInfo(volume.volumeInfo) === -1) {
const volumeEntry = new VolumeEntry(volume.volumeInfo);
volumeEntry.disabled =
this.volumeManager_.isDisabled(VolumeType.GUEST_OS);
// @ts-ignore: error TS2339: Property 'addEntry' does not exist on
// type 'FilesAppEntry | EntryList | VolumeEntry'.
myFilesEntry.addEntry(volumeEntry);
}
}
// For each entry in the list, remove any for volumes that no longer
// exist.
// @ts-ignore: error TS2339: Property 'getUiChildren' does not exist on
// type 'FilesAppEntry | EntryList | VolumeEntry'.
for (const volume of myFilesEntry.getUiChildren()) {
if (!volume.volumeInfo ||
volume.volumeInfo.volumeType != VolumeType.GUEST_OS) {
continue;
}
// @ts-ignore: error TS7006: Parameter 'v' implicitly has an 'any' type.
if (!guestOsVolumes.find(v => v.label === volume.name)) {
// @ts-ignore: error TS2339: Property 'removeChildEntry' does not
// exist on type 'FilesAppEntry | EntryList | VolumeEntry'.
myFilesEntry.removeChildEntry(volume);
}
}
// Now we add any guests we know about which don't already have a
// matching volume.
for (const item of this.guestOsPlaceholders_) {
// @ts-ignore: error TS7006: Parameter 'v' implicitly has an 'any' type.
if (!guestOsVolumes.find(v => v.label === item.label)) {
// Since it's a fake item, link the navigation model so
// DirectoryTree can choose the correct DirectoryItem for it.
// @ts-ignore: error TS2339: Property 'navigationModel' does not exist
// on type 'FilesAppEntry'.
item.entry.navigationModel = item;
// @ts-ignore: error TS2339: Property 'addEntry' does not exist on
// type 'FilesAppEntry | EntryList | VolumeEntry'.
myFilesEntry.addEntry(item.entry);
}
}
}
// Add Google Drive - the only Drive.
let hasDrive = false;
for (const driveItem of getVolumes(VolumeType.DRIVE)) {
driveItem.disabled = this.volumeManager_.isDisabled(VolumeType.DRIVE);
this.navigationItems_.push(driveItem);
driveItem.section = NavigationSection.GOOGLE_DRIVE;
hasDrive = true;
}
if (!hasDrive && this.fakeDriveItem_) {
this.navigationItems_.push(this.fakeDriveItem_);
this.fakeDriveItem_.section = NavigationSection.GOOGLE_DRIVE;
}
// Add ODFS.
for (const provided of getVolumes(VolumeType.PROVIDED)) {
if (isOneDrive(provided.volumeInfo)) {
provided.section = NavigationSection.ODFS;
const {volumes} = getStore().getState();
const volume = volumes[provided.volumeInfo.volumeId];
// The state might not have been initialized yet. The navigation items
// will be refreshed once the store gets populated.
if (volume) {
provided.disabled = volume.isDisabled;
}
this.navigationItems_.push(provided);
}
}
// Add SMB.
for (const provided of getVolumes(VolumeType.SMB)) {
this.navigationItems_.push(provided);
provided.section = NavigationSection.CLOUD;
}
// Add other FSPs.
for (const provided of getVolumes(VolumeType.PROVIDED)) {
// ODFS added already.
if (isOneDrive(provided.volumeInfo)) {
continue;
}
this.navigationItems_.push(provided);
provided.section = NavigationSection.CLOUD;
}
// Add DocumentsProviders to the same section of FSP.
for (const provider of getVolumes(VolumeType.DOCUMENTS_PROVIDER)) {
this.navigationItems_.push(provider);
provider.section = NavigationSection.CLOUD;
}
// Add REMOVABLE volumes and partitions.
const removableModels = new Map();
const disableRemovables =
this.volumeManager_.isDisabled(VolumeType.REMOVABLE);
for (const [devicePath, removableGroup] of groupRemovables().entries()) {
if (removableGroup.length == 1 && !isSinglePartitionFormatEnabled()) {
// Add unpartitioned removable device as a regular volume.
this.navigationItems_.push(removableGroup[0]);
removableGroup[0].section = NavigationSection.REMOVABLE;
removableGroup[0].disabled = disableRemovables;
continue;
}
// Multiple partitions found.
let removableModel;
let removableEntry;
if (this.removableModels_.has(devicePath)) {
// Removable model has been seen before. Use the same reference.
removableModel = this.removableModels_.get(devicePath);
// @ts-ignore: error TS18048: 'removableModel' is possibly 'undefined'.
removableModel.disabled = disableRemovables;
// @ts-ignore: error TS18048: 'removableModel' is possibly 'undefined'.
removableEntry = removableModel.entry;
} else {
// Create an EntryList for new removable group.
const rootLabel = removableGroup[0].volumeInfo.driveLabel ?
removableGroup[0].volumeInfo.driveLabel :
/*default*/ 'External Drive';
removableEntry = new EntryList(
rootLabel, RootType.REMOVABLE,
removableGroup[0].volumeInfo.devicePath);
removableModel = new NavigationModelFakeItem(
removableEntry.label, NavigationModelItemType.ENTRY_LIST,
removableEntry);
removableModel.disabled = disableRemovables;
removableModel.section = NavigationSection.REMOVABLE;
}
// Remove partitions that aren't available anymore.
const existingVolumeInfos =
// @ts-ignore: error TS7006: Parameter 'p' implicitly has an 'any'
// type.
new Set(removableGroup.map(p => p.volumeInfo));
// @ts-ignore: error TS2339: Property 'getUiChildren' does not exist on
// type 'FilesAppEntry | EntryList'.
for (const partition of removableEntry.getUiChildren()) {
if (!existingVolumeInfos.has(partition.volumeInfo)) {
// @ts-ignore: error TS2339: Property 'removeChildEntry' does not
// exist on type 'FilesAppEntry | EntryList'.
removableEntry.removeChildEntry(partition);
}
}
// Add partitions as entries.
for (const partition of removableGroup) {
// Only add partition if it doesn't exist as a child already.
// @ts-ignore: error TS2339: Property 'findIndexByVolumeInfo' does not
// exist on type 'FilesAppEntry | EntryList'.
if (removableEntry.findIndexByVolumeInfo(partition.volumeInfo) === -1) {
// @ts-ignore: error TS2339: Property 'addEntry' does not exist on
// type 'FilesAppEntry | EntryList'.
removableEntry.addEntry(new VolumeEntry(partition.volumeInfo));
}
}
removableModels.set(devicePath, removableModel);
// @ts-ignore: error TS2345: Argument of type 'NavigationModelFakeItem |
// undefined' is not assignable to parameter of type
// 'NavigationModelItem'.
this.navigationItems_.push(removableModel);
}
this.removableModels_ = removableModels;
// Join MTP, ARCHIVE. These types belong to same section.
// @ts-ignore: error TS7005: Variable 'otherVolumes' implicitly has an
// 'any[]' type.
const otherVolumes =
[].concat(getVolumes(VolumeType.ARCHIVE), getVolumes(VolumeType.MTP))
.sort((volume1, volume2) => {
// @ts-ignore: error TS2339: Property 'originalOrder' does not
// exist on type 'never'.
return volume1.originalOrder - volume2.originalOrder;
});
for (const volume of otherVolumes) {
this.navigationItems_.push(volume);
volume.section = NavigationSection.REMOVABLE;
}
for (const androidApp of this.androidAppList_) {
const androidAppItem = new NavigationModelAndroidAppItem(androidApp);
androidAppItem.section = NavigationSection.ANDROID_APPS;
this.navigationItems_.push(androidAppItem);
}
// Add Trash.
// This should only show when Files app is open as a standalone app. The ARC
// file selector, however, opens Files app as a standalone app but passes a
// query parameter to indicate the mode. As Trash is a fake volume, it is
// not filtered out in the filtered volume manager so perform it here
// instead.
if (this.dialogType_ === DialogType.FULL_PAGE &&
!this.volumeManager_.getMediaStoreFilesOnlyFilterEnabled() &&
this.trashItem_) {
this.navigationItems_.push(this.trashItem_);
}
}
/**
* Returns the item at the given index.
* @param {number} index The index of the entry to get.
* @return {NavigationModelItem|undefined} The item at the given index.
*/
item(index) {
return this.navigationItems_[index];
}
/**
* Returns the number of items in the model.
* @return {number} The length of the model.
* @private
*/
length_() {
return this.navigationItems_.length;
}
/**
* Returns the first matching item.
* @param {NavigationModelItem} modelItem The entry to find.
* @param {number=} opt_fromIndex If provided, then the searching start at
* the {@code opt_fromIndex}.
* @return {number} The index of the first found element or -1 if not found.
*/
indexOf(modelItem, opt_fromIndex) {
for (let i = opt_fromIndex || 0; i < this.length; i++) {
if (modelItem === this.item(i)) {
return i;
}
}
return -1;
}
/**
* Called externally when one of the items is not found on the filesystem.
* @param {!NavigationModelItem} modelItem The entry which is not found.
*/
onItemNotFoundError(modelItem) {
if (modelItem.type === NavigationModelItemType.SHORTCUT) {
this.shortcutListModel_.onItemNotFoundError(
/** @type {!NavigationModelShortcutItem} */ (modelItem).entry);
}
}
/**
* Get the index of Downloads volume in the volume list. Returns -1 if there
* is not the Downloads volume in the list.
* @returns {number} Index of the Downloads volume.
*/
findDownloadsVolumeIndex_() {
for (let i = 0; i < this.volumeList_.length; i++) {
// @ts-ignore: error TS2532: Object is possibly 'undefined'.
if (this.volumeList_[i].volumeInfo.volumeType == VolumeType.DOWNLOADS) {
return i;
}
}
return -1;
}
}