| OLD | NEW |
| (Empty) |
| 1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | |
| 2 // Use of this source code is governed by a BSD-style license that can be | |
| 3 // found in the LICENSE file. | |
| 4 | |
| 5 'use strict'; | |
| 6 | |
| 7 /** | |
| 8 * This object encapsulates everything related to tasks execution. | |
| 9 * | |
| 10 * TODO(hirono): Pass each component instead of the entire FileManager. | |
| 11 * @param {FileManager} fileManager FileManager instance. | |
| 12 * @param {Object=} opt_params File manager load parameters. | |
| 13 * @constructor | |
| 14 */ | |
| 15 function FileTasks(fileManager, opt_params) { | |
| 16 this.fileManager_ = fileManager; | |
| 17 this.params_ = opt_params; | |
| 18 this.tasks_ = null; | |
| 19 this.defaultTask_ = null; | |
| 20 this.entries_ = null; | |
| 21 | |
| 22 /** | |
| 23 * List of invocations to be called once tasks are available. | |
| 24 * | |
| 25 * @private | |
| 26 * @type {Array.<Object>} | |
| 27 */ | |
| 28 this.pendingInvocations_ = []; | |
| 29 } | |
| 30 | |
| 31 /** | |
| 32 * Location of the Chrome Web Store. | |
| 33 * | |
| 34 * @const | |
| 35 * @type {string} | |
| 36 */ | |
| 37 FileTasks.CHROME_WEB_STORE_URL = 'https://chrome.google.com/webstore'; | |
| 38 | |
| 39 /** | |
| 40 * Base URL of apps list in the Chrome Web Store. This constant is used in | |
| 41 * FileTasks.createWebStoreLink(). | |
| 42 * | |
| 43 * @const | |
| 44 * @type {string} | |
| 45 */ | |
| 46 FileTasks.WEB_STORE_HANDLER_BASE_URL = | |
| 47 'https://chrome.google.com/webstore/category/collection/file_handlers'; | |
| 48 | |
| 49 | |
| 50 /** | |
| 51 * The app ID of the video player app. | |
| 52 * @const | |
| 53 * @type {string} | |
| 54 */ | |
| 55 FileTasks.VIDEO_PLAYER_ID = 'jcgeabjmjgoblfofpppfkcoakmfobdko'; | |
| 56 | |
| 57 /** | |
| 58 * Returns URL of the Chrome Web Store which show apps supporting the given | |
| 59 * file-extension and mime-type. | |
| 60 * | |
| 61 * @param {string} extension Extension of the file (with the first dot). | |
| 62 * @param {string} mimeType Mime type of the file. | |
| 63 * @return {string} URL | |
| 64 */ | |
| 65 FileTasks.createWebStoreLink = function(extension, mimeType) { | |
| 66 if (!extension) | |
| 67 return FileTasks.CHROME_WEB_STORE_URL; | |
| 68 | |
| 69 if (extension[0] === '.') | |
| 70 extension = extension.substr(1); | |
| 71 else | |
| 72 console.warn('Please pass an extension with a dot to createWebStoreLink.'); | |
| 73 | |
| 74 var url = FileTasks.WEB_STORE_HANDLER_BASE_URL; | |
| 75 url += '?_fe=' + extension.toLowerCase().replace(/[^\w]/g, ''); | |
| 76 | |
| 77 // If a mime is given, add it into the URL. | |
| 78 if (mimeType) | |
| 79 url += '&_fmt=' + mimeType.replace(/[^-\w\/]/g, ''); | |
| 80 return url; | |
| 81 }; | |
| 82 | |
| 83 /** | |
| 84 * Complete the initialization. | |
| 85 * | |
| 86 * @param {Array.<Entry>} entries List of file entries. | |
| 87 * @param {Array.<string>=} opt_mimeTypes List of MIME types for each | |
| 88 * of the files. | |
| 89 */ | |
| 90 FileTasks.prototype.init = function(entries, opt_mimeTypes) { | |
| 91 this.entries_ = entries; | |
| 92 this.mimeTypes_ = opt_mimeTypes || []; | |
| 93 | |
| 94 // TODO(mtomasz): Move conversion from entry to url to custom bindings. | |
| 95 var urls = util.entriesToURLs(entries); | |
| 96 if (urls.length > 0) { | |
| 97 chrome.fileBrowserPrivate.getFileTasks(urls, this.mimeTypes_, | |
| 98 this.onTasks_.bind(this)); | |
| 99 } | |
| 100 }; | |
| 101 | |
| 102 /** | |
| 103 * Returns amount of tasks. | |
| 104 * | |
| 105 * @return {number} amount of tasks. | |
| 106 */ | |
| 107 FileTasks.prototype.size = function() { | |
| 108 return (this.tasks_ && this.tasks_.length) || 0; | |
| 109 }; | |
| 110 | |
| 111 /** | |
| 112 * Callback when tasks found. | |
| 113 * | |
| 114 * @param {Array.<Object>} tasks The tasks. | |
| 115 * @private | |
| 116 */ | |
| 117 FileTasks.prototype.onTasks_ = function(tasks) { | |
| 118 this.processTasks_(tasks); | |
| 119 for (var index = 0; index < this.pendingInvocations_.length; index++) { | |
| 120 var name = this.pendingInvocations_[index][0]; | |
| 121 var args = this.pendingInvocations_[index][1]; | |
| 122 this[name].apply(this, args); | |
| 123 } | |
| 124 this.pendingInvocations_ = []; | |
| 125 }; | |
| 126 | |
| 127 /** | |
| 128 * The list of known extensions to record UMA. | |
| 129 * Note: Because the data is recorded by the index, so new item shouldn't be | |
| 130 * inserted. | |
| 131 * | |
| 132 * @const | |
| 133 * @type {Array.<string>} | |
| 134 * @private | |
| 135 */ | |
| 136 FileTasks.UMA_INDEX_KNOWN_EXTENSIONS_ = Object.freeze([ | |
| 137 'other', '.3ga', '.3gp', '.aac', '.alac', '.asf', '.avi', '.bmp', '.csv', | |
| 138 '.doc', '.docx', '.flac', '.gif', '.jpeg', '.jpg', '.log', '.m3u', '.m3u8', | |
| 139 '.m4a', '.m4v', '.mid', '.mkv', '.mov', '.mp3', '.mp4', '.mpg', '.odf', | |
| 140 '.odp', '.ods', '.odt', '.oga', '.ogg', '.ogv', '.pdf', '.png', '.ppt', | |
| 141 '.pptx', '.ra', '.ram', '.rar', '.rm', '.rtf', '.wav', '.webm', '.webp', | |
| 142 '.wma', '.wmv', '.xls', '.xlsx', | |
| 143 ]); | |
| 144 | |
| 145 /** | |
| 146 * The list of executable file extensions. | |
| 147 * | |
| 148 * @const | |
| 149 * @type {Array.<string>} | |
| 150 */ | |
| 151 FileTasks.EXECUTABLE_EXTENSIONS = Object.freeze([ | |
| 152 '.exe', '.lnk', '.deb', '.dmg', '.jar', '.msi', | |
| 153 ]); | |
| 154 | |
| 155 /** | |
| 156 * The list of extensions to skip the suggest app dialog. | |
| 157 * @const | |
| 158 * @type {Array.<string>} | |
| 159 * @private | |
| 160 */ | |
| 161 FileTasks.EXTENSIONS_TO_SKIP_SUGGEST_APPS_ = Object.freeze([ | |
| 162 '.crdownload', '.dsc', '.inf', '.crx', | |
| 163 ]); | |
| 164 | |
| 165 /** | |
| 166 * Records trial of opening file grouped by extensions. | |
| 167 * | |
| 168 * @param {Array.<Entry>} entries The entries to be opened. | |
| 169 * @private | |
| 170 */ | |
| 171 FileTasks.recordViewingFileTypeUMA_ = function(entries) { | |
| 172 for (var i = 0; i < entries.length; i++) { | |
| 173 var entry = entries[i]; | |
| 174 var extension = FileType.getExtension(entry).toLowerCase(); | |
| 175 if (FileTasks.UMA_INDEX_KNOWN_EXTENSIONS_.indexOf(extension) < 0) { | |
| 176 extension = 'other'; | |
| 177 } | |
| 178 metrics.recordEnum( | |
| 179 'ViewingFileType', extension, FileTasks.UMA_INDEX_KNOWN_EXTENSIONS_); | |
| 180 } | |
| 181 }; | |
| 182 | |
| 183 /** | |
| 184 * Returns true if the taskId is for an internal task. | |
| 185 * | |
| 186 * @param {string} taskId Task identifier. | |
| 187 * @return {boolean} True if the task ID is for an internal task. | |
| 188 * @private | |
| 189 */ | |
| 190 FileTasks.isInternalTask_ = function(taskId) { | |
| 191 var taskParts = taskId.split('|'); | |
| 192 var appId = taskParts[0]; | |
| 193 var taskType = taskParts[1]; | |
| 194 var actionId = taskParts[2]; | |
| 195 // The action IDs here should match ones used in executeInternalTask_(). | |
| 196 return (appId === chrome.runtime.id && | |
| 197 taskType === 'file' && | |
| 198 (actionId === 'play' || | |
| 199 actionId === 'mount-archive' || | |
| 200 actionId === 'gallery' || | |
| 201 actionId === 'gallery-video')); | |
| 202 }; | |
| 203 | |
| 204 /** | |
| 205 * Processes internal tasks. | |
| 206 * | |
| 207 * @param {Array.<Object>} tasks The tasks. | |
| 208 * @private | |
| 209 */ | |
| 210 FileTasks.prototype.processTasks_ = function(tasks) { | |
| 211 this.tasks_ = []; | |
| 212 var id = chrome.runtime.id; | |
| 213 var isOnDrive = false; | |
| 214 var fm = this.fileManager_; | |
| 215 for (var index = 0; index < this.entries_.length; ++index) { | |
| 216 var locationInfo = fm.volumeManager.getLocationInfo(this.entries_[index]); | |
| 217 if (locationInfo && locationInfo.isDriveBased) { | |
| 218 isOnDrive = true; | |
| 219 break; | |
| 220 } | |
| 221 } | |
| 222 | |
| 223 for (var i = 0; i < tasks.length; i++) { | |
| 224 var task = tasks[i]; | |
| 225 var taskParts = task.taskId.split('|'); | |
| 226 | |
| 227 // Skip internal Files.app's handlers. | |
| 228 if (taskParts[0] === id && (taskParts[2] === 'auto-open' || | |
| 229 taskParts[2] === 'select' || taskParts[2] === 'open')) { | |
| 230 continue; | |
| 231 } | |
| 232 | |
| 233 // Tweak images, titles of internal tasks. | |
| 234 if (taskParts[0] === id && taskParts[1] === 'file') { | |
| 235 if (taskParts[2] === 'play') { | |
| 236 // TODO(serya): This hack needed until task.iconUrl is working | |
| 237 // (see GetFileTasksFileBrowserFunction::RunImpl). | |
| 238 task.iconType = 'audio'; | |
| 239 task.title = loadTimeData.getString('ACTION_LISTEN'); | |
| 240 } else if (taskParts[2] === 'mount-archive') { | |
| 241 task.iconType = 'archive'; | |
| 242 task.title = loadTimeData.getString('MOUNT_ARCHIVE'); | |
| 243 } else if (taskParts[2] === 'gallery' || | |
| 244 taskParts[2] === 'gallery-video') { | |
| 245 task.iconType = 'image'; | |
| 246 task.title = loadTimeData.getString('ACTION_OPEN'); | |
| 247 } else if (taskParts[2] === 'open-hosted-generic') { | |
| 248 if (this.entries_.length > 1) | |
| 249 task.iconType = 'generic'; | |
| 250 else // Use specific icon. | |
| 251 task.iconType = FileType.getIcon(this.entries_[0]); | |
| 252 task.title = loadTimeData.getString('ACTION_OPEN'); | |
| 253 } else if (taskParts[2] === 'open-hosted-gdoc') { | |
| 254 task.iconType = 'gdoc'; | |
| 255 task.title = loadTimeData.getString('ACTION_OPEN_GDOC'); | |
| 256 } else if (taskParts[2] === 'open-hosted-gsheet') { | |
| 257 task.iconType = 'gsheet'; | |
| 258 task.title = loadTimeData.getString('ACTION_OPEN_GSHEET'); | |
| 259 } else if (taskParts[2] === 'open-hosted-gslides') { | |
| 260 task.iconType = 'gslides'; | |
| 261 task.title = loadTimeData.getString('ACTION_OPEN_GSLIDES'); | |
| 262 } else if (taskParts[2] === 'view-swf') { | |
| 263 // Do not render this task if disabled. | |
| 264 if (!loadTimeData.getBoolean('SWF_VIEW_ENABLED')) | |
| 265 continue; | |
| 266 task.iconType = 'generic'; | |
| 267 task.title = loadTimeData.getString('ACTION_VIEW'); | |
| 268 } else if (taskParts[2] === 'view-pdf') { | |
| 269 // Do not render this task if disabled. | |
| 270 if (!loadTimeData.getBoolean('PDF_VIEW_ENABLED')) | |
| 271 continue; | |
| 272 task.iconType = 'pdf'; | |
| 273 task.title = loadTimeData.getString('ACTION_VIEW'); | |
| 274 } else if (taskParts[2] === 'view-in-browser') { | |
| 275 task.iconType = 'generic'; | |
| 276 task.title = loadTimeData.getString('ACTION_VIEW'); | |
| 277 } | |
| 278 } | |
| 279 | |
| 280 if (!task.iconType && taskParts[1] === 'web-intent') { | |
| 281 task.iconType = 'generic'; | |
| 282 } | |
| 283 | |
| 284 this.tasks_.push(task); | |
| 285 if (this.defaultTask_ === null && task.isDefault) { | |
| 286 this.defaultTask_ = task; | |
| 287 } | |
| 288 } | |
| 289 if (!this.defaultTask_ && this.tasks_.length > 0) { | |
| 290 // If we haven't picked a default task yet, then just pick the first one. | |
| 291 // This is not the preferred way we want to pick this, but better this than | |
| 292 // no default at all if the C++ code didn't set one. | |
| 293 this.defaultTask_ = this.tasks_[0]; | |
| 294 } | |
| 295 }; | |
| 296 | |
| 297 /** | |
| 298 * Executes default task. | |
| 299 * | |
| 300 * @param {function(boolean, Array.<string>)=} opt_callback Called when the | |
| 301 * default task is executed, or the error is occurred. | |
| 302 * @private | |
| 303 */ | |
| 304 FileTasks.prototype.executeDefault_ = function(opt_callback) { | |
| 305 FileTasks.recordViewingFileTypeUMA_(this.entries_); | |
| 306 this.executeDefaultInternal_(this.entries_, opt_callback); | |
| 307 }; | |
| 308 | |
| 309 /** | |
| 310 * Executes default task. | |
| 311 * | |
| 312 * @param {Array.<Entry>} entries Entries to execute. | |
| 313 * @param {function(boolean, Array.<Entry>)=} opt_callback Called when the | |
| 314 * default task is executed, or the error is occurred. | |
| 315 * @private | |
| 316 */ | |
| 317 FileTasks.prototype.executeDefaultInternal_ = function(entries, opt_callback) { | |
| 318 var callback = opt_callback || function(arg1, arg2) {}; | |
| 319 | |
| 320 if (this.defaultTask_ !== null) { | |
| 321 this.executeInternal_(this.defaultTask_.taskId, entries); | |
| 322 callback(true, entries); | |
| 323 return; | |
| 324 } | |
| 325 | |
| 326 // We don't have tasks, so try to show a file in a browser tab. | |
| 327 // We only do that for single selection to avoid confusion. | |
| 328 if (entries.length !== 1 || !entries[0]) | |
| 329 return; | |
| 330 | |
| 331 var filename = entries[0].name; | |
| 332 var extension = PathUtil.splitExtension(filename)[1]; | |
| 333 var mimeType = this.mimeTypes_[0]; | |
| 334 | |
| 335 var showAlert = function() { | |
| 336 var textMessageId; | |
| 337 var titleMessageId; | |
| 338 switch (extension) { | |
| 339 case '.exe': | |
| 340 textMessageId = 'NO_ACTION_FOR_EXECUTABLE'; | |
| 341 break; | |
| 342 case '.crx': | |
| 343 textMessageId = 'NO_ACTION_FOR_CRX'; | |
| 344 titleMessageId = 'NO_ACTION_FOR_CRX_TITLE'; | |
| 345 break; | |
| 346 default: | |
| 347 textMessageId = 'NO_ACTION_FOR_FILE'; | |
| 348 } | |
| 349 | |
| 350 var webStoreUrl = FileTasks.createWebStoreLink(extension, mimeType); | |
| 351 var text = strf(textMessageId, webStoreUrl, str('NO_ACTION_FOR_FILE_URL')); | |
| 352 var title = titleMessageId ? str(titleMessageId) : filename; | |
| 353 this.fileManager_.alert.showHtml(title, text, function() {}); | |
| 354 callback(false, urls); | |
| 355 }.bind(this); | |
| 356 | |
| 357 var onViewFilesFailure = function() { | |
| 358 var fm = this.fileManager_; | |
| 359 if (!fm.isOnDrive() || | |
| 360 !entries[0] || | |
| 361 FileTasks.EXTENSIONS_TO_SKIP_SUGGEST_APPS_.indexOf(extension) !== -1) { | |
| 362 showAlert(); | |
| 363 return; | |
| 364 } | |
| 365 | |
| 366 fm.openSuggestAppsDialog( | |
| 367 entries[0], | |
| 368 function() { | |
| 369 var newTasks = new FileTasks(fm); | |
| 370 newTasks.init(entries, this.mimeTypes_); | |
| 371 newTasks.executeDefault(); | |
| 372 callback(true, entries); | |
| 373 }.bind(this), | |
| 374 // Cancelled callback. | |
| 375 function() { | |
| 376 callback(false, entries); | |
| 377 }, | |
| 378 showAlert); | |
| 379 }.bind(this); | |
| 380 | |
| 381 var onViewFiles = function(result) { | |
| 382 switch (result) { | |
| 383 case 'opened': | |
| 384 callback(success, entries); | |
| 385 break; | |
| 386 case 'message_sent': | |
| 387 util.isTeleported(window).then(function(teleported) { | |
| 388 if (teleported) { | |
| 389 util.showOpenInOtherDesktopAlert( | |
| 390 this.fileManager_.ui.alertDialog, entries); | |
| 391 } | |
| 392 }.bind(this)); | |
| 393 callback(success, entries); | |
| 394 break; | |
| 395 case 'empty': | |
| 396 callback(success, entries); | |
| 397 break; | |
| 398 case 'failed': | |
| 399 onViewFilesFailure(); | |
| 400 break; | |
| 401 } | |
| 402 }.bind(this); | |
| 403 | |
| 404 this.checkAvailability_(function() { | |
| 405 // TODO(mtomasz): Pass entries instead. | |
| 406 var urls = util.entriesToURLs(entries); | |
| 407 var taskId = chrome.runtime.id + '|file|view-in-browser'; | |
| 408 chrome.fileBrowserPrivate.executeTask(taskId, urls, onViewFiles); | |
| 409 }.bind(this)); | |
| 410 }; | |
| 411 | |
| 412 /** | |
| 413 * Executes a single task. | |
| 414 * | |
| 415 * @param {string} taskId Task identifier. | |
| 416 * @param {Array.<Entry>=} opt_entries Entries to xecute on instead of | |
| 417 * this.entries_|. | |
| 418 * @private | |
| 419 */ | |
| 420 FileTasks.prototype.execute_ = function(taskId, opt_entries) { | |
| 421 var entries = opt_entries || this.entries_; | |
| 422 FileTasks.recordViewingFileTypeUMA_(entries); | |
| 423 this.executeInternal_(taskId, entries); | |
| 424 }; | |
| 425 | |
| 426 /** | |
| 427 * The core implementation to execute a single task. | |
| 428 * | |
| 429 * @param {string} taskId Task identifier. | |
| 430 * @param {Array.<Entry>} entries Entries to execute. | |
| 431 * @private | |
| 432 */ | |
| 433 FileTasks.prototype.executeInternal_ = function(taskId, entries) { | |
| 434 this.checkAvailability_(function() { | |
| 435 if (FileTasks.isInternalTask_(taskId)) { | |
| 436 var taskParts = taskId.split('|'); | |
| 437 this.executeInternalTask_(taskParts[2], entries); | |
| 438 } else { | |
| 439 // TODO(mtomasz): Pass entries instead. | |
| 440 var urls = util.entriesToURLs(entries); | |
| 441 chrome.fileBrowserPrivate.executeTask(taskId, urls, function(result) { | |
| 442 if (result !== 'message_sent') | |
| 443 return; | |
| 444 util.isTeleported(window).then(function(teleported) { | |
| 445 if (teleported) { | |
| 446 util.showOpenInOtherDesktopAlert( | |
| 447 this.fileManager_.ui.alertDialog, entries); | |
| 448 } | |
| 449 }.bind(this)); | |
| 450 }.bind(this)); | |
| 451 } | |
| 452 }.bind(this)); | |
| 453 }; | |
| 454 | |
| 455 /** | |
| 456 * Checks whether the remote files are available right now. | |
| 457 * | |
| 458 * @param {function} callback The callback. | |
| 459 * @private | |
| 460 */ | |
| 461 FileTasks.prototype.checkAvailability_ = function(callback) { | |
| 462 var areAll = function(props, name) { | |
| 463 var isOne = function(e) { | |
| 464 // If got no properties, we safely assume that item is unavailable. | |
| 465 return e && e[name]; | |
| 466 }; | |
| 467 return props.filter(isOne).length === props.length; | |
| 468 }; | |
| 469 | |
| 470 var fm = this.fileManager_; | |
| 471 var entries = this.entries_; | |
| 472 | |
| 473 var isDriveOffline = fm.volumeManager.getDriveConnectionState().type === | |
| 474 util.DriveConnectionType.OFFLINE; | |
| 475 | |
| 476 if (fm.isOnDrive() && isDriveOffline) { | |
| 477 fm.metadataCache_.get(entries, 'drive', function(props) { | |
| 478 if (areAll(props, 'availableOffline')) { | |
| 479 callback(); | |
| 480 return; | |
| 481 } | |
| 482 | |
| 483 fm.alert.showHtml( | |
| 484 loadTimeData.getString('OFFLINE_HEADER'), | |
| 485 props[0].hosted ? | |
| 486 loadTimeData.getStringF( | |
| 487 entries.length === 1 ? | |
| 488 'HOSTED_OFFLINE_MESSAGE' : | |
| 489 'HOSTED_OFFLINE_MESSAGE_PLURAL') : | |
| 490 loadTimeData.getStringF( | |
| 491 entries.length === 1 ? | |
| 492 'OFFLINE_MESSAGE' : | |
| 493 'OFFLINE_MESSAGE_PLURAL', | |
| 494 loadTimeData.getString('OFFLINE_COLUMN_LABEL'))); | |
| 495 }); | |
| 496 return; | |
| 497 } | |
| 498 | |
| 499 var isOnMetered = fm.volumeManager.getDriveConnectionState().type === | |
| 500 util.DriveConnectionType.METERED; | |
| 501 | |
| 502 if (fm.isOnDrive() && isOnMetered) { | |
| 503 fm.metadataCache_.get(entries, 'drive', function(driveProps) { | |
| 504 if (areAll(driveProps, 'availableWhenMetered')) { | |
| 505 callback(); | |
| 506 return; | |
| 507 } | |
| 508 | |
| 509 fm.metadataCache_.get(entries, 'filesystem', function(fileProps) { | |
| 510 var sizeToDownload = 0; | |
| 511 for (var i = 0; i !== entries.length; i++) { | |
| 512 if (!driveProps[i].availableWhenMetered) | |
| 513 sizeToDownload += fileProps[i].size; | |
| 514 } | |
| 515 fm.confirm.show( | |
| 516 loadTimeData.getStringF( | |
| 517 entries.length === 1 ? | |
| 518 'CONFIRM_MOBILE_DATA_USE' : | |
| 519 'CONFIRM_MOBILE_DATA_USE_PLURAL', | |
| 520 util.bytesToString(sizeToDownload)), | |
| 521 callback); | |
| 522 }); | |
| 523 }); | |
| 524 return; | |
| 525 } | |
| 526 | |
| 527 callback(); | |
| 528 }; | |
| 529 | |
| 530 /** | |
| 531 * Executes an internal task. | |
| 532 * | |
| 533 * @param {string} id The short task id. | |
| 534 * @param {Array.<Entry>} entries The entries to execute on. | |
| 535 * @private | |
| 536 */ | |
| 537 FileTasks.prototype.executeInternalTask_ = function(id, entries) { | |
| 538 var fm = this.fileManager_; | |
| 539 | |
| 540 if (id === 'play') { | |
| 541 var position = 0; | |
| 542 if (entries.length === 1) { | |
| 543 // If just a single audio file is selected pass along every audio file | |
| 544 // in the directory. | |
| 545 var selectedEntries = entries[0]; | |
| 546 entries = fm.getAllEntriesInCurrentDirectory().filter(FileType.isAudio); | |
| 547 position = entries.indexOf(selectedEntries); | |
| 548 } | |
| 549 // TODO(mtomasz): Pass entries instead. | |
| 550 var urls = util.entriesToURLs(entries); | |
| 551 chrome.fileBrowserPrivate.getProfiles(function(profiles, | |
| 552 currentId, | |
| 553 displayedId) { | |
| 554 fm.backgroundPage.launchAudioPlayer({items: urls, position: position}, | |
| 555 displayedId); | |
| 556 }); | |
| 557 return; | |
| 558 } | |
| 559 | |
| 560 if (id === 'mount-archive') { | |
| 561 this.mountArchivesInternal_(entries); | |
| 562 return; | |
| 563 } | |
| 564 | |
| 565 if (id === 'gallery' || id === 'gallery-video') { | |
| 566 this.openGalleryInternal_(entries); | |
| 567 return; | |
| 568 } | |
| 569 | |
| 570 console.error('Unexpected action ID: ' + id); | |
| 571 }; | |
| 572 | |
| 573 /** | |
| 574 * Mounts archives. | |
| 575 * | |
| 576 * @param {Array.<Entry>} entries Mount file entries list. | |
| 577 */ | |
| 578 FileTasks.prototype.mountArchives = function(entries) { | |
| 579 FileTasks.recordViewingFileTypeUMA_(entries); | |
| 580 this.mountArchivesInternal_(entries); | |
| 581 }; | |
| 582 | |
| 583 /** | |
| 584 * The core implementation of mounts archives. | |
| 585 * | |
| 586 * @param {Array.<Entry>} entries Mount file entries list. | |
| 587 * @private | |
| 588 */ | |
| 589 FileTasks.prototype.mountArchivesInternal_ = function(entries) { | |
| 590 var fm = this.fileManager_; | |
| 591 | |
| 592 var tracker = fm.directoryModel.createDirectoryChangeTracker(); | |
| 593 tracker.start(); | |
| 594 | |
| 595 // TODO(mtomasz): Pass Entries instead of URLs. | |
| 596 var urls = util.entriesToURLs(entries); | |
| 597 fm.resolveSelectResults_(urls, function(resolvedURLs) { | |
| 598 for (var index = 0; index < resolvedURLs.length; ++index) { | |
| 599 // TODO(mtomasz): Pass Entry instead of URL. | |
| 600 fm.volumeManager.mountArchive(resolvedURLs[index], | |
| 601 function(volumeInfo) { | |
| 602 if (tracker.hasChanged) { | |
| 603 tracker.stop(); | |
| 604 return; | |
| 605 } | |
| 606 volumeInfo.resolveDisplayRoot(function(displayRoot) { | |
| 607 if (tracker.hasChanged) { | |
| 608 tracker.stop(); | |
| 609 return; | |
| 610 } | |
| 611 fm.directoryModel.changeDirectoryEntry(displayRoot); | |
| 612 }, function() { | |
| 613 console.warn('Failed to resolve the display root after mounting.'); | |
| 614 tracker.stop(); | |
| 615 }); | |
| 616 }, function(url, error) { | |
| 617 tracker.stop(); | |
| 618 var path = util.extractFilePath(url); | |
| 619 var namePos = path.lastIndexOf('/'); | |
| 620 fm.alert.show(strf('ARCHIVE_MOUNT_FAILED', | |
| 621 path.substr(namePos + 1), error)); | |
| 622 }.bind(null, resolvedURLs[index])); | |
| 623 } | |
| 624 }); | |
| 625 }; | |
| 626 | |
| 627 /** | |
| 628 * Open the Gallery. | |
| 629 * | |
| 630 * @param {Array.<Entry>} entries List of selected entries. | |
| 631 */ | |
| 632 FileTasks.prototype.openGallery = function(entries) { | |
| 633 FileTasks.recordViewingFileTypeUMA_(entries); | |
| 634 this.openGalleryInternal_(entries); | |
| 635 }; | |
| 636 | |
| 637 /** | |
| 638 * The core implementation to open the Gallery. | |
| 639 * | |
| 640 * @param {Array.<Entry>} entries List of selected entries. | |
| 641 * @private | |
| 642 */ | |
| 643 FileTasks.prototype.openGalleryInternal_ = function(entries) { | |
| 644 var fm = this.fileManager_; | |
| 645 | |
| 646 var allEntries = | |
| 647 fm.getAllEntriesInCurrentDirectory().filter(FileType.isImageOrVideo); | |
| 648 | |
| 649 var galleryFrame = fm.document_.createElement('iframe'); | |
| 650 galleryFrame.className = 'overlay-pane'; | |
| 651 galleryFrame.scrolling = 'no'; | |
| 652 galleryFrame.setAttribute('webkitallowfullscreen', true); | |
| 653 | |
| 654 if (this.params_ && this.params_.gallery) { | |
| 655 // Remove the Gallery state from the location, we do not need it any more. | |
| 656 // TODO(mtomasz): Consider keeping the selection path. | |
| 657 util.updateAppState( | |
| 658 null, /* keep current directory */ | |
| 659 '', /* remove current selection */ | |
| 660 '' /* remove search. */); | |
| 661 } | |
| 662 | |
| 663 var savedAppState = JSON.parse(JSON.stringify(window.appState)); | |
| 664 var savedTitle = document.title; | |
| 665 | |
| 666 // Push a temporary state which will be replaced every time the selection | |
| 667 // changes in the Gallery and popped when the Gallery is closed. | |
| 668 util.updateAppState(); | |
| 669 | |
| 670 var onBack = function(selectedEntries) { | |
| 671 fm.directoryModel.selectEntries(selectedEntries); | |
| 672 fm.closeFilePopup(); // Will call Gallery.unload. | |
| 673 window.appState = savedAppState; | |
| 674 util.saveAppState(); | |
| 675 document.title = savedTitle; | |
| 676 }; | |
| 677 | |
| 678 var onAppRegionChanged = function(visible) { | |
| 679 fm.onFilePopupAppRegionChanged(visible); | |
| 680 }; | |
| 681 | |
| 682 galleryFrame.onload = function() { | |
| 683 galleryFrame.contentWindow.ImageUtil.metrics = metrics; | |
| 684 | |
| 685 // TODO(haruki): isOnReadonlyDirectory() only checks the permission for the | |
| 686 // root. We should check more granular permission to know whether the file | |
| 687 // is writable or not. | |
| 688 var readonly = fm.isOnReadonlyDirectory(); | |
| 689 var currentDir = fm.getCurrentDirectoryEntry(); | |
| 690 var downloadsVolume = | |
| 691 fm.volumeManager.getCurrentProfileVolumeInfo(RootType.DOWNLOADS); | |
| 692 var downloadsDir = downloadsVolume && downloadsVolume.fileSystem.root; | |
| 693 | |
| 694 // TODO(mtomasz): Pass Entry instead of localized name. Conversion to a | |
| 695 // display string should be done in gallery.js. | |
| 696 var readonlyDirName = null; | |
| 697 if (readonly && currentDir) | |
| 698 readonlyDirName = util.getEntryLabel(fm.volumeManager, currentDir); | |
| 699 | |
| 700 var context = { | |
| 701 // We show the root label in readonly warning (e.g. archive name). | |
| 702 readonlyDirName: readonlyDirName, | |
| 703 curDirEntry: currentDir, | |
| 704 saveDirEntry: readonly ? downloadsDir : null, | |
| 705 searchResults: fm.directoryModel.isSearching(), | |
| 706 metadataCache: fm.metadataCache_, | |
| 707 pageState: this.params_, | |
| 708 appWindow: chrome.app.window.current(), | |
| 709 onBack: onBack, | |
| 710 onClose: fm.onClose.bind(fm), | |
| 711 onMaximize: fm.onMaximize.bind(fm), | |
| 712 onMinimize: fm.onMinimize.bind(fm), | |
| 713 onAppRegionChanged: onAppRegionChanged, | |
| 714 loadTimeData: fm.backgroundPage.background.stringData | |
| 715 }; | |
| 716 galleryFrame.contentWindow.Gallery.open( | |
| 717 context, fm.volumeManager, allEntries, entries); | |
| 718 }.bind(this); | |
| 719 | |
| 720 galleryFrame.src = 'gallery.html'; | |
| 721 fm.openFilePopup(galleryFrame, fm.updateTitle_.bind(fm)); | |
| 722 }; | |
| 723 | |
| 724 /** | |
| 725 * Displays the list of tasks in a task picker combobutton. | |
| 726 * | |
| 727 * @param {cr.ui.ComboButton} combobutton The task picker element. | |
| 728 * @private | |
| 729 */ | |
| 730 FileTasks.prototype.display_ = function(combobutton) { | |
| 731 if (this.tasks_.length === 0) { | |
| 732 combobutton.hidden = true; | |
| 733 return; | |
| 734 } | |
| 735 | |
| 736 combobutton.clear(); | |
| 737 combobutton.hidden = false; | |
| 738 combobutton.defaultItem = this.createCombobuttonItem_(this.defaultTask_); | |
| 739 | |
| 740 var items = this.createItems_(); | |
| 741 | |
| 742 if (items.length > 1) { | |
| 743 var defaultIdx = 0; | |
| 744 | |
| 745 for (var j = 0; j < items.length; j++) { | |
| 746 combobutton.addDropDownItem(items[j]); | |
| 747 if (items[j].task.taskId === this.defaultTask_.taskId) | |
| 748 defaultIdx = j; | |
| 749 } | |
| 750 | |
| 751 combobutton.addSeparator(); | |
| 752 var changeDefaultMenuItem = combobutton.addDropDownItem({ | |
| 753 label: loadTimeData.getString('CHANGE_DEFAULT_MENU_ITEM') | |
| 754 }); | |
| 755 changeDefaultMenuItem.classList.add('change-default'); | |
| 756 } | |
| 757 }; | |
| 758 | |
| 759 /** | |
| 760 * Creates sorted array of available task descriptions such as title and icon. | |
| 761 * | |
| 762 * @return {Array} created array can be used to feed combobox, menus and so on. | |
| 763 * @private | |
| 764 */ | |
| 765 FileTasks.prototype.createItems_ = function() { | |
| 766 var items = []; | |
| 767 var title = this.defaultTask_.title + ' ' + | |
| 768 loadTimeData.getString('DEFAULT_ACTION_LABEL'); | |
| 769 items.push(this.createCombobuttonItem_(this.defaultTask_, title, true)); | |
| 770 | |
| 771 for (var index = 0; index < this.tasks_.length; index++) { | |
| 772 var task = this.tasks_[index]; | |
| 773 if (task !== this.defaultTask_) | |
| 774 items.push(this.createCombobuttonItem_(task)); | |
| 775 } | |
| 776 | |
| 777 items.sort(function(a, b) { | |
| 778 return a.label.localeCompare(b.label); | |
| 779 }); | |
| 780 | |
| 781 return items; | |
| 782 }; | |
| 783 | |
| 784 /** | |
| 785 * Updates context menu with default item. | |
| 786 * @private | |
| 787 */ | |
| 788 | |
| 789 FileTasks.prototype.updateMenuItem_ = function() { | |
| 790 this.fileManager_.updateContextMenuActionItems(this.defaultTask_, | |
| 791 this.tasks_.length > 1); | |
| 792 }; | |
| 793 | |
| 794 /** | |
| 795 * Creates combobutton item based on task. | |
| 796 * | |
| 797 * @param {Object} task Task to convert. | |
| 798 * @param {string=} opt_title Title. | |
| 799 * @param {boolean=} opt_bold Make a menu item bold. | |
| 800 * @return {Object} Item appendable to combobutton drop-down list. | |
| 801 * @private | |
| 802 */ | |
| 803 FileTasks.prototype.createCombobuttonItem_ = function(task, opt_title, | |
| 804 opt_bold) { | |
| 805 return { | |
| 806 label: opt_title || task.title, | |
| 807 iconUrl: task.iconUrl, | |
| 808 iconType: task.iconType, | |
| 809 task: task, | |
| 810 bold: opt_bold || false | |
| 811 }; | |
| 812 }; | |
| 813 | |
| 814 /** | |
| 815 * Shows modal action picker dialog with currently available list of tasks. | |
| 816 * | |
| 817 * @param {DefaultActionDialog} actionDialog Action dialog to show and update. | |
| 818 * @param {string} title Title to use. | |
| 819 * @param {string} message Message to use. | |
| 820 * @param {function(Object)} onSuccess Callback to pass selected task. | |
| 821 */ | |
| 822 FileTasks.prototype.showTaskPicker = function(actionDialog, title, message, | |
| 823 onSuccess) { | |
| 824 var items = this.createItems_(); | |
| 825 | |
| 826 var defaultIdx = 0; | |
| 827 for (var j = 0; j < items.length; j++) { | |
| 828 if (items[j].task.taskId === this.defaultTask_.taskId) | |
| 829 defaultIdx = j; | |
| 830 } | |
| 831 | |
| 832 actionDialog.show( | |
| 833 title, | |
| 834 message, | |
| 835 items, defaultIdx, | |
| 836 function(item) { | |
| 837 onSuccess(item.task); | |
| 838 }); | |
| 839 }; | |
| 840 | |
| 841 /** | |
| 842 * Decorates a FileTasks method, so it will be actually executed after the tasks | |
| 843 * are available. | |
| 844 * This decorator expects an implementation called |method + '_'|. | |
| 845 * | |
| 846 * @param {string} method The method name. | |
| 847 */ | |
| 848 FileTasks.decorate = function(method) { | |
| 849 var privateMethod = method + '_'; | |
| 850 FileTasks.prototype[method] = function() { | |
| 851 if (this.tasks_) { | |
| 852 this[privateMethod].apply(this, arguments); | |
| 853 } else { | |
| 854 this.pendingInvocations_.push([privateMethod, arguments]); | |
| 855 } | |
| 856 return this; | |
| 857 }; | |
| 858 }; | |
| 859 | |
| 860 FileTasks.decorate('display'); | |
| 861 FileTasks.decorate('updateMenuItem'); | |
| 862 FileTasks.decorate('execute'); | |
| 863 FileTasks.decorate('executeDefault'); | |
| OLD | NEW |