| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| /** |
| * @enum {string} |
| */ |
| const NavigationModelItemType = { |
| SHORTCUT: 'shortcut', |
| VOLUME: 'volume', |
| RECENT: 'recent', |
| CROSTINI: 'crostini', |
| ENTRY_LIST: 'entry-list', |
| DRIVE: 'drive', |
| }; |
| |
| /** |
| * 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. |
| * - CLOUD: Drive and FSPs. |
| * @enum {string} |
| */ |
| const NavigationSection = { |
| TOP: 'top', |
| MY_FILES: 'my_files', |
| REMOVABLE: 'removable', |
| CLOUD: 'cloud', |
| }; |
| |
| /** |
| * Base item of NavigationListModel. Should not be created directly. |
| */ |
| class NavigationModelItem { |
| /** |
| * @param {string} label |
| * @param {NavigationModelItemType} type |
| */ |
| constructor(label, type) { |
| this.label_ = label; |
| this.type_ = type; |
| |
| /** |
| * @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 {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. |
| */ |
| 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 volumes. |
| */ |
| class NavigationModelVolumeItem extends NavigationModelItem { |
| /** |
| * @param {string} label Label. |
| * @param {!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. |
| */ |
| 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. |
| */ |
| class NavigationListModel extends cr.EventTarget { |
| /** |
| * @param {!VolumeManager} volumeManager VolumeManager instance. |
| * @param {(!cr.ui.ArrayDataModel|!FolderShortcutsDataModel)} |
| * shortcutListModel The list of folder shortcut. |
| * @param {NavigationModelFakeItem} recentModelItem Recent folder. |
| * @param {!DirectoryModel} directoryModel |
| */ |
| constructor( |
| volumeManager, shortcutListModel, recentModelItem, directoryModel) { |
| super(); |
| |
| /** |
| * @private {!VolumeManager} |
| * @const |
| */ |
| this.volumeManager_ = volumeManager; |
| |
| /** |
| * @private {(!cr.ui.ArrayDataModel|!FolderShortcutsDataModel)} |
| * @const |
| */ |
| this.shortcutListModel_ = shortcutListModel; |
| |
| /** |
| * @private {NavigationModelFakeItem} |
| * @const |
| */ |
| this.recentModelItem_ = recentModelItem; |
| |
| /** |
| * @private {!DirectoryModel} |
| * @const |
| */ |
| this.directoryModel_ = directoryModel; |
| |
| /** |
| * Root folder for crostini Linux files. |
| * This field will be set asynchronously after calling |
| * chrome.fileManagerPrivate.isCrostiniEnabled. |
| * @private {NavigationModelFakeItem} |
| */ |
| this.linuxFilesItem_ = null; |
| |
| /** |
| * NavigationModel for MyFiles, since DirectoryTree expect it to be always |
| * the same reference we keep the initial reference for reuse. |
| * @private {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 {!Map<string, !NavigationModelFakeItem>} |
| */ |
| this.removableModels_ = new Map(); |
| |
| /** |
| * True when MyFiles should be a volume and Downloads just a plain folder |
| * inside it. When false MyFiles is an EntryList, which means UI only type, |
| * which contains Downloads as a child volume. |
| * @private {boolean} |
| */ |
| this.myFilesVolumeEnabled_ = |
| loadTimeData.valueExists('MY_FILES_VOLUME_ENABLED') && |
| loadTimeData.getBoolean('MY_FILES_VOLUME_ENABLED'); |
| |
| /** |
| * All root navigation items in display order. |
| * @private {!Array<!NavigationModelItem>} |
| */ |
| this.navigationItems_ = []; |
| |
| /** @private {?NavigationModelFakeItem} */ |
| this.fakeDriveItem_; |
| |
| const volumeInfoToModelItem = volumeInfo => { |
| return new NavigationModelVolumeItem(volumeInfo.label, volumeInfo); |
| }; |
| |
| 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}; |
| 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)); |
| const volumeInfo = this.volumeManager_.getVolumeInfo(shortcutEntry); |
| this.shortcutList_.push(entryToModelItem(shortcutEntry)); |
| } |
| |
| // Reorder volumes, shortcuts, and optional items for initial display. |
| this.reorderNavigationItems_(); |
| |
| // Generates a combined 'permuted' event from an event of either volumeList |
| // or shortcutList. |
| 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.permutation.length; i++) { |
| if (event.permutation[i] >= 0) { |
| newList[event.permutation[i]] = this.volumeList_[i]; |
| } |
| } |
| |
| // Create missing instances. |
| for (let i = 0; i < event.newLength; i++) { |
| if (!newList[i]) { |
| newList[i] = volumeInfoToModelItem( |
| this.volumeManager_.volumeInfoList.item(i)); |
| } |
| } |
| this.volumeList_ = newList; |
| |
| permutation = event.permutation.slice(); |
| |
| // shortcutList part has not been changed, so the permutation should be |
| // just identity mapping with a shift. |
| for (let i = 0; i < this.shortcutList_.length; i++) { |
| permutation.push(i + this.volumeList_.length); |
| } |
| } else { |
| // Build the shortcutList. |
| |
| // volumeList part has not been changed, so the permutation should be |
| // identity mapping. |
| |
| permutation = []; |
| for (let i = 0; i < this.volumeList_.length; i++) { |
| permutation[i] = i; |
| } |
| |
| let modelIndex = 0; |
| let oldListIndex = 0; |
| const newList = []; |
| while (modelIndex < this.shortcutListModel_.length && |
| oldListIndex < this.shortcutList_.length) { |
| const shortcutEntry = this.shortcutListModel_.item(modelIndex); |
| const cmp = this.shortcutListModel_.compare( |
| /** @type {Entry} */ (shortcutEntry), |
| 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. |
| permutation.push(newList.length + this.volumeList_.length); |
| 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. |
| for (; modelIndex < this.shortcutListModel_.length; modelIndex++) { |
| const shortcutEntry = this.shortcutListModel_.item(modelIndex); |
| newList.push(entryToModelItem(shortcutEntry)); |
| } |
| |
| // Fill remaining permutation if necessary. |
| for (; oldListIndex < this.shortcutList_.length; oldListIndex++) { |
| permutation.push(-1); |
| } |
| |
| this.shortcutList_ = newList; |
| } |
| |
| // Reorder items after permutation. |
| this.reorderNavigationItems_(); |
| |
| // Dispatch permuted event. |
| const permutedEvent = new Event('permuted'); |
| permutedEvent.newLength = |
| this.volumeList_.length + this.shortcutList_.length; |
| permutedEvent.permutation = permutation; |
| this.dispatchEvent(permutedEvent); |
| }; |
| |
| this.volumeManager_.volumeInfoList.addEventListener( |
| 'permuted', permutedHandler.bind(this, ListType.VOLUME_LIST)); |
| this.shortcutListModel_.addEventListener( |
| 'permuted', permutedHandler.bind(this, ListType.SHORTCUT_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. |
| * This setter is provided separate to the constructor since |
| * this field is set async after calling fileManagerPrivate.isCrostiniEnabled. |
| * @param {NavigationModelFakeItem} item Linux files root. |
| */ |
| set linuxFilesItem(item) { |
| this.linuxFilesItem_ = item; |
| this.reorderNavigationItems_(); |
| } |
| |
| /** |
| * Set the fake Drive root and reorder items. |
| * @param {NavigationModelFakeItem} item Fake Drive root. |
| */ |
| set fakeDriveItem(item) { |
| this.fakeDriveItem_ = item; |
| this.reorderNavigationItems_(); |
| } |
| |
| /** |
| * Reorder navigation items when command line flag new-files-app-navigation is |
| * enabled it nests Downloads, Linux and Android files under "My Files"; when |
| * it's disabled it has a flat structure with Linux files after Recent menu. |
| */ |
| reorderNavigationItems_() { |
| 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. Drive volumes. |
| * 6. Other FSP (File System Provider) (when mounted). |
| * 7. Other volumes (MTP, ARCHIVE, REMOVABLE, Zip volumes). |
| * 8. 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++) { |
| const volumeType = volumeList[i].volumeInfo.volumeType; |
| volumeList[i].originalOrder = i; |
| let providedType; |
| let volumeId; |
| switch (volumeType) { |
| case VolumeManagerCommon.VolumeType.CROSTINI: |
| case VolumeManagerCommon.VolumeType.DOWNLOADS: |
| case VolumeManagerCommon.VolumeType.ANDROID_FILES: |
| volumeIndexes[volumeType] = i; |
| break; |
| case VolumeManagerCommon.VolumeType.PROVIDED: |
| // ZipArchiver mounts zip files as PROVIDED volume type, however we |
| // want to display mounted zip files the same way as archive, so |
| // splitting them apart from PROVIDED. |
| volumeId = volumeList[i].volumeInfo.volumeId; |
| providedType = VolumeManagerCommon.VolumeType.PROVIDED; |
| if (volumeId.includes(NavigationListModel.ZIP_EXTENSION_ID)) { |
| providedType = NavigationListModel.ZIP_VOLUME_TYPE; |
| } |
| if (!volumeIndexes[providedType]) { |
| volumeIndexes[providedType] = [i]; |
| } else { |
| volumeIndexes[providedType].push(i); |
| } |
| break; |
| case VolumeManagerCommon.VolumeType.REMOVABLE: |
| case VolumeManagerCommon.VolumeType.ARCHIVE: |
| case VolumeManagerCommon.VolumeType.MTP: |
| case VolumeManagerCommon.VolumeType.DRIVE: |
| case VolumeManagerCommon.VolumeType.MEDIA_VIEW: |
| case VolumeManagerCommon.VolumeType.DOCUMENTS_PROVIDER: |
| if (!volumeIndexes[volumeType]) { |
| volumeIndexes[volumeType] = [i]; |
| } else { |
| volumeIndexes[volumeType].push(i); |
| } |
| break; |
| default: |
| assertNotReached(`No explict order for VolumeType: "${volumeType}"`); |
| break; |
| } |
| } |
| |
| /** |
| * @param {!VolumeManagerCommon.VolumeType} volumeType the desired volume |
| * type to be filtered from volumeList. |
| * @return {NavigationModelVolumeItem} |
| */ |
| const getSingleVolume = volumeType => { |
| return volumeList[volumeIndexes[volumeType]]; |
| }; |
| |
| /** |
| * @param {!VolumeManagerCommon.VolumeType} volumeType the desired volume |
| * type to be filtered from volumeList. |
| * @return Array<!NavigationModelVolumeItem> |
| */ |
| const getVolumes = volumeType => { |
| const indexes = volumeIndexes[volumeType] || []; |
| 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(VolumeManagerCommon.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. |
| let 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_); |
| } |
| |
| // Media View (Images, Videos and Audio). |
| for (const mediaView of getVolumes( |
| VolumeManagerCommon.VolumeType.MEDIA_VIEW)) { |
| this.navigationItems_.push(mediaView); |
| mediaView.section = NavigationSection.TOP; |
| } |
| // Shortcuts. |
| for (const shortcut of this.shortcutList_) { |
| this.navigationItems_.push(shortcut); |
| } |
| |
| let myFilesEntry, myFilesModel; |
| if (!this.myFilesModel_) { |
| if (this.myFilesVolumeEnabled_) { |
| // When MyFilesVolume is enabled we use the Downloads volume to be the |
| // MyFiles volume. |
| const myFilesVolumeModel = |
| getSingleVolume(VolumeManagerCommon.VolumeType.DOWNLOADS); |
| if (myFilesVolumeModel) { |
| myFilesEntry = new VolumeEntry(myFilesVolumeModel.volumeInfo); |
| myFilesModel = new NavigationModelFakeItem( |
| str('MY_FILES_ROOT_LABEL'), NavigationModelItemType.ENTRY_LIST, |
| myFilesEntry); |
| this.myFilesModel_ = myFilesModel; |
| } else { |
| // When MyFilesVolume 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'), |
| VolumeManagerCommon.RootType.MY_FILES); |
| myFilesModel = new NavigationModelFakeItem( |
| myFilesEntry.label, NavigationModelItemType.ENTRY_LIST, |
| myFilesEntry); |
| } |
| } else { |
| // Here is the initial version for MyFiles, which is only an entry in JS |
| // to be displayed in the DirectoryTree, cotaining Downloads, Linux and |
| // Play files volumes. |
| myFilesEntry = new EntryList( |
| str('MY_FILES_ROOT_LABEL'), VolumeManagerCommon.RootType.MY_FILES); |
| myFilesModel = new NavigationModelFakeItem( |
| myFilesEntry.label, NavigationModelItemType.ENTRY_LIST, |
| myFilesEntry); |
| myFilesModel.section = NavigationSection.MY_FILES; |
| this.myFilesModel_ = myFilesModel; |
| } |
| } else { |
| myFilesEntry = this.myFilesModel_.entry; |
| myFilesModel = this.myFilesModel_; |
| } |
| this.directoryModel_.setMyFiles(myFilesEntry); |
| this.navigationItems_.push(myFilesModel); |
| |
| // Add Downloads to My Files. |
| if (!this.myFilesVolumeEnabled_) { |
| const downloadsVolume = |
| getSingleVolume(VolumeManagerCommon.VolumeType.DOWNLOADS); |
| if (downloadsVolume) { |
| // Only add volume if MyFiles doesn't have it yet. |
| if (myFilesEntry.findIndexByVolumeInfo(downloadsVolume.volumeInfo) === |
| -1) { |
| myFilesEntry.addEntry(new VolumeEntry(downloadsVolume.volumeInfo)); |
| } |
| } else { |
| myFilesEntry.removeByVolumeType( |
| VolumeManagerCommon.VolumeType.DOWNLOADS); |
| } |
| } |
| |
| // Add Android to My Files. |
| const androidVolume = |
| getSingleVolume(VolumeManagerCommon.VolumeType.ANDROID_FILES); |
| if (androidVolume) { |
| // Only add volume if MyFiles doesn't have it yet. |
| if (myFilesEntry.findIndexByVolumeInfo(androidVolume.volumeInfo) === -1) { |
| myFilesEntry.addEntry(new VolumeEntry(androidVolume.volumeInfo)); |
| } |
| } else { |
| myFilesEntry.removeByVolumeType( |
| VolumeManagerCommon.VolumeType.ANDROID_FILES); |
| } |
| |
| // Add Linux to My Files. |
| const crostiniVolume = |
| getSingleVolume(VolumeManagerCommon.VolumeType.CROSTINI); |
| |
| // Remove Crostini FakeEntry, it's re-added below if needed. |
| myFilesEntry.removeByRootType(VolumeManagerCommon.RootType.CROSTINI); |
| if (crostiniVolume) { |
| // Crostini is mounted so add it if MyFiles doesn't have it yet. |
| if (myFilesEntry.findIndexByVolumeInfo(crostiniVolume.volumeInfo) === |
| -1) { |
| myFilesEntry.addEntry(new VolumeEntry(crostiniVolume.volumeInfo)); |
| } |
| } else { |
| myFilesEntry.removeByVolumeType(VolumeManagerCommon.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. |
| this.linuxFilesItem_.entry.navigationModel = this.linuxFilesItem_; |
| myFilesEntry.addEntry(this.linuxFilesItem_.entry); |
| } |
| } |
| |
| // Add Drive. |
| let hasDrive = false; |
| for (const driveItem of getVolumes(VolumeManagerCommon.VolumeType.DRIVE)) { |
| this.navigationItems_.push(driveItem); |
| driveItem.section = NavigationSection.CLOUD; |
| hasDrive = true; |
| } |
| if (!hasDrive && this.fakeDriveItem_) { |
| this.navigationItems_.push(this.fakeDriveItem_); |
| this.fakeDriveItem_.section = NavigationSection.CLOUD; |
| } |
| |
| // Add FSP. |
| for (const provided of getVolumes( |
| VolumeManagerCommon.VolumeType.PROVIDED)) { |
| this.navigationItems_.push(provided); |
| provided.section = NavigationSection.CLOUD; |
| } |
| |
| // Add DocumentsProviders to the same section of FSP. |
| for (const provider of getVolumes( |
| VolumeManagerCommon.VolumeType.DOCUMENTS_PROVIDER)) { |
| this.navigationItems_.push(provider); |
| provider.section = NavigationSection.CLOUD; |
| } |
| |
| // Add REMOVABLE volumes and partitions. |
| const removableModels = new Map(); |
| for (const [devicePath, removableGroup] of groupRemovables().entries()) { |
| if (removableGroup.length == 1) { |
| // Add unpartitioned removable device as a regular volume. |
| this.navigationItems_.push(removableGroup[0]); |
| removableGroup[0].section = NavigationSection.REMOVABLE; |
| continue; |
| } |
| |
| // Multiple partitions found. |
| let removableModel; |
| if (this.removableModels_.has(devicePath)) { |
| // Removable model has been seen before. Use the same reference. |
| removableModel = this.removableModels_.get(devicePath); |
| } else { |
| // Create an EntryList for new removable group. |
| const rootLabel = removableGroup[0].volumeInfo.driveLabel ? |
| removableGroup[0].volumeInfo.driveLabel : |
| /*default*/ 'External Drive'; |
| const removableEntry = new EntryList( |
| rootLabel, VolumeManagerCommon.RootType.REMOVABLE, devicePath); |
| removableModel = new NavigationModelFakeItem( |
| removableEntry.label, NavigationModelItemType.ENTRY_LIST, |
| removableEntry); |
| removableModel.section = NavigationSection.REMOVABLE; |
| // Add partitions as entries. |
| for (const partition of removableGroup) { |
| // Only add partition if it doesn't exist as a child already. |
| if (removableEntry.findIndexByVolumeInfo(partition.volumeInfo) === |
| -1) { |
| removableEntry.addEntry(new VolumeEntry(partition.volumeInfo)); |
| } |
| } |
| } |
| removableModels.set(devicePath, removableModel); |
| this.navigationItems_.push(removableModel); |
| } |
| this.removableModels_ = removableModels; |
| |
| // Join MTP, ARCHIVE. These types belong to same section. |
| const zipIndexes = volumeIndexes[NavigationListModel.ZIP_VOLUME_TYPE] || []; |
| const otherVolumes = |
| [].concat( |
| getVolumes(VolumeManagerCommon.VolumeType.ARCHIVE), |
| getVolumes(VolumeManagerCommon.VolumeType.MTP), |
| zipIndexes.map(idx => volumeList[idx])) |
| .sort((volume1, volume2) => { |
| return volume1.originalOrder - volume2.originalOrder; |
| }); |
| |
| for (const volume of otherVolumes) { |
| this.navigationItems_.push(volume); |
| volume.section = NavigationSection.REMOVABLE; |
| } |
| } |
| |
| /** |
| * 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++) { |
| if (this.volumeList_[i].volumeInfo.volumeType == |
| VolumeManagerCommon.VolumeType.DOWNLOADS) { |
| return i; |
| } |
| } |
| return -1; |
| } |
| } |
| |
| /** |
| * ZipArchiver mounts zip files as PROVIDED volume type. |
| * This is a special case for zip volumes to be able to split them apart from |
| * PROVIDED. |
| * @const |
| */ |
| NavigationListModel.ZIP_VOLUME_TYPE = '_ZIP_VOLUME_'; |
| |
| /** |
| * Id of the Zip Archiver extension, that can mount zip files. |
| * @const |
| */ |
| NavigationListModel.ZIP_EXTENSION_ID = 'dmboannefpncccogfdikhmhpmdnddgoe'; |