summaryrefslogtreecommitdiff
path: root/chromium/chrome/browser/resources/file_manager
diff options
context:
space:
mode:
authorAndras Becsi <andras.becsi@digia.com>2014-03-18 13:16:26 +0100
committerFrederik Gladhorn <frederik.gladhorn@digia.com>2014-03-20 15:55:39 +0100
commit3f0f86b0caed75241fa71c95a5d73bc0164348c5 (patch)
tree92b9fb00f2e9e90b0be2262093876d4f43b6cd13 /chromium/chrome/browser/resources/file_manager
parente90d7c4b152c56919d963987e2503f9909a666d2 (diff)
downloadqtwebengine-chromium-3f0f86b0caed75241fa71c95a5d73bc0164348c5.tar.gz
Update to new stable branch 1750
This also includes an updated ninja and chromium dependencies needed on Windows. Change-Id: Icd597d80ed3fa4425933c9f1334c3c2e31291c42 Reviewed-by: Zoltan Arvai <zarvai@inf.u-szeged.hu> Reviewed-by: Zeno Albisser <zeno.albisser@digia.com>
Diffstat (limited to 'chromium/chrome/browser/resources/file_manager')
-rw-r--r--chromium/chrome/browser/resources/file_manager/OWNERS3
-rw-r--r--chromium/chrome/browser/resources/file_manager/background/js/background.js787
-rw-r--r--chromium/chrome/browser/resources/file_manager/background/js/file_operation_handler.js315
-rw-r--r--chromium/chrome/browser/resources/file_manager/background/js/file_operation_manager.js1400
-rw-r--r--chromium/chrome/browser/resources/file_manager/background/js/progress_center.js452
-rw-r--r--chromium/chrome/browser/resources/file_manager/background/js/test_util.js868
-rw-r--r--chromium/chrome/browser/resources/file_manager/background/js/volume_manager.js727
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/100/archive.pngbin0 -> 119 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/100/archive_white.pngbin0 -> 114 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/100/audio.pngbin0 -> 150 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/100/audio_white.pngbin0 -> 122 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/100/chart.pngbin0 -> 106 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/100/chart_white.pngbin0 -> 105 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/100/excel.pngbin0 -> 392 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/100/excel_white.pngbin0 -> 237 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/100/folder.pngbin0 -> 115 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/100/folder_white.pngbin0 -> 101 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/100/form.pngbin0 -> 128 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/100/form_white.pngbin0 -> 115 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gdoc.pngbin0 -> 100 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gdoc_white.pngbin0 -> 99 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gdraw.pngbin0 -> 165 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gdraw_white.pngbin0 -> 137 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/100/generic.pngbin0 -> 104 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/100/generic_white.pngbin0 -> 101 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/100/glink.pngbin0 -> 104 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/100/glink_white.pngbin0 -> 101 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gsheet.pngbin0 -> 107 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gsheet_white.pngbin0 -> 104 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gslides.pngbin0 -> 99 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gslides_white.pngbin0 -> 99 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gtable.pngbin0 -> 146 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gtable_white.pngbin0 -> 123 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/100/image.pngbin0 -> 215 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/100/image_white.pngbin0 -> 155 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/100/pdf.pngbin0 -> 123 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/100/pdf_white.pngbin0 -> 113 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/100/ppt.pngbin0 -> 178 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/100/ppt_white.pngbin0 -> 144 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/100/script.pngbin0 -> 180 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/100/script_white.pngbin0 -> 151 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/100/sites.pngbin0 -> 105 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/100/sites_white.pngbin0 -> 105 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/100/video.pngbin0 -> 117 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/100/video_white.pngbin0 -> 107 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/100/word.pngbin0 -> 372 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/100/word_white.pngbin0 -> 241 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/200/archive.pngbin0 -> 164 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/200/archive_white.pngbin0 -> 164 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/200/audio.pngbin0 -> 221 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/200/audio_white.pngbin0 -> 166 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/200/chart.pngbin0 -> 146 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/200/chart_white.pngbin0 -> 126 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/200/excel.pngbin0 -> 731 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/200/excel_white.pngbin0 -> 462 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/200/folder.pngbin0 -> 139 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/200/folder_white.pngbin0 -> 132 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/200/form.pngbin0 -> 194 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/200/form_white.pngbin0 -> 152 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gdoc.pngbin0 -> 145 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gdoc_white.pngbin0 -> 124 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gdraw.pngbin0 -> 279 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gdraw_white.pngbin0 -> 209 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/200/generic.pngbin0 -> 130 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/200/generic_white.pngbin0 -> 122 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/200/glink.pngbin0 -> 129 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/200/glink_white.pngbin0 -> 122 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gsheet.pngbin0 -> 148 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gsheet_white.pngbin0 -> 128 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gslides.pngbin0 -> 137 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gslides_white.pngbin0 -> 119 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gtable.pngbin0 -> 580 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gtable_white.pngbin0 -> 371 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/200/image.pngbin0 -> 348 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/200/image_white.pngbin0 -> 245 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/200/pdf.pngbin0 -> 167 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/200/pdf_white.pngbin0 -> 142 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/200/ppt.pngbin0 -> 295 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/200/ppt_white.pngbin0 -> 224 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/200/script.pngbin0 -> 333 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/200/script_white.pngbin0 -> 269 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/200/sites.pngbin0 -> 149 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/200/sites_white.pngbin0 -> 127 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/200/video.pngbin0 -> 225 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/200/video_white.pngbin0 -> 169 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/200/word.pngbin0 -> 619 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/file_types/200/word_white.pngbin0 -> 406 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/icon128.pngbin0 -> 1414 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/icon16.pngbin0 -> 267 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/icon256.pngbin0 -> 3169 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/icon32.pngbin0 -> 400 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/icon48.pngbin0 -> 562 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/icon64.pngbin0 -> 711 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/images/icon96.pngbin0 -> 1018 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/js/async_util.js262
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/js/path_util.js471
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/js/progress_center_common.js149
-rw-r--r--chromium/chrome/browser/resources/file_manager/common/js/util.js1259
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/css/action_choice.css172
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/css/audio_player.css404
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/css/combobutton.css34
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/css/common.css501
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/css/drive_welcome.css188
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/css/file_manager.css2075
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/css/file_types.css477
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/css/gallery.css1374
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/css/list.css73
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/css/media_controls.css605
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/css/menu.css38
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/css/table.css64
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/css/tree.css83
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/css/video_player.css103
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/common/2x/bubble_point_white.pngbin0 -> 192 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/common/2x/check_no_box.pngbin0 -> 313 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/common/2x/checkbox_white_checked.pngbin0 -> 404 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/common/2x/checkbox_white_unchecked.pngbin0 -> 166 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/common/2x/close_x_gray.pngbin0 -> 217 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/common/2x/disclosure_arrow_dk_grey.pngbin0 -> 104 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/common/bubble_point_white.pngbin0 -> 138 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/common/check_no_box.pngbin0 -> 191 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/common/checkbox_white_checked.pngbin0 -> 195 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/common/checkbox_white_unchecked.pngbin0 -> 95 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/common/close_x_gray.pngbin0 -> 169 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/common/disclosure_arrow_dk_grey.pngbin0 -> 91 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/common/spinner.svg13
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/black_folder.pngbin0 -> 138 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/breadcrumb-separator.pngbin0 -> 197 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/button_list_view.pngbin0 -> 99 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/button_list_view_white.pngbin0 -> 94 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/button_mosaic_view.pngbin0 -> 99 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/button_mosaic_view_white.pngbin0 -> 97 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/close_bar.pngbin0 -> 197 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/drive_logo.pngbin0 -> 11287 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/eject.pngbin0 -> 155 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/hashed_bg.gifbin0 -> 47 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/icon_search.pngbin0 -> 442 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/offline.pngbin0 -> 475 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/onbutton_trash.pngbin0 -> 191 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/process_drawer_button_closed.pngbin0 -> 121 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/process_drawer_button_closed_hover.pngbin0 -> 121 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/process_drawer_button_closed_pressed.pngbin0 -> 121 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/process_drawer_button_opened.pngbin0 -> 121 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/process_drawer_button_opened_hover.pngbin0 -> 122 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/process_drawer_button_opened_pressed.pngbin0 -> 122 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/search_clear.pngbin0 -> 271 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/search_clear_hover.pngbin0 -> 272 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/search_clear_pressed.pngbin0 -> 272 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/search_icon_active.pngbin0 -> 540 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/search_icon_inactive.pngbin0 -> 506 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/select_checkbox.pngbin0 -> 138 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/select_checkbox_checked.pngbin0 -> 261 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/sort_asc.pngbin0 -> 103 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/sort_desc.pngbin0 -> 106 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/vertical_separator.pngbin0 -> 77 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/warning_icon_square.pngbin0 -> 1878 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/white_folder.pngbin0 -> 132 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/black_folder.pngbin0 -> 103 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/breadcrumb-separator.pngbin0 -> 134 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/button_list_view.pngbin0 -> 95 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/button_list_view_white.pngbin0 -> 83 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/button_mosaic_view.pngbin0 -> 92 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/button_mosaic_view_white.pngbin0 -> 88 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/close_bar.pngbin0 -> 136 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/drive_logo.pngbin0 -> 5711 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/eject.pngbin0 -> 129 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/hashed_bg.gifbin0 -> 1100 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/offline.pngbin0 -> 255 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/onbutton_trash.pngbin0 -> 195 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/process_drawer_button_closed.pngbin0 -> 102 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/process_drawer_button_closed_hover.pngbin0 -> 102 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/process_drawer_button_closed_pressed.pngbin0 -> 102 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/process_drawer_button_opened.pngbin0 -> 104 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/process_drawer_button_opened_hover.pngbin0 -> 104 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/process_drawer_button_opened_pressed.pngbin0 -> 104 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/search_clear.pngbin0 -> 161 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/search_clear_hover.pngbin0 -> 161 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/search_clear_pressed.pngbin0 -> 160 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/search_icon_active.pngbin0 -> 295 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/search_icon_inactive.pngbin0 -> 278 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/select_checkbox.pngbin0 -> 109 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/select_checkbox_checked.pngbin0 -> 174 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/sort_asc.pngbin0 -> 93 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/sort_desc.pngbin0 -> 92 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/vertical_separator.pngbin0 -> 70 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/view_thumbs_black.pngbin0 -> 91 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/warning_icon_square.pngbin0 -> 849 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/white_folder.pngbin0 -> 100 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/arrow_left.pngbin0 -> 3180 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/arrow_right.pngbin0 -> 3166 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/back_to_files.pngbin0 -> 1943 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/bubble_point.pngbin0 -> 135 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/butterbar_close_button.pngbin0 -> 146 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/cursor_crop.pngbin0 -> 831 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/cursor_leftright.pngbin0 -> 801 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/cursor_move.pngbin0 -> 1217 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/cursor_nwse.pngbin0 -> 914 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/cursor_swne.pngbin0 -> 903 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/cursor_updown.pngbin0 -> 786 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_1up.pngbin0 -> 253 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_1up_selected.pngbin0 -> 249 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_autofix.pngbin0 -> 563 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_autofix_selected.pngbin0 -> 579 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_brightness.pngbin0 -> 412 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_brightness_selected.pngbin0 -> 398 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_contrast.pngbin0 -> 524 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_crop.pngbin0 -> 122 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_crop_selected.pngbin0 -> 125 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_delete.pngbin0 -> 142 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_delete_selected.pngbin0 -> 148 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_edit.pngbin0 -> 230 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_edit_selected.pngbin0 -> 238 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_mosaic.pngbin0 -> 166 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_mosaic_selected.pngbin0 -> 168 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_print.pngbin0 -> 175 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_print_selected.pngbin0 -> 184 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_redo.pngbin0 -> 369 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_redo_selected.pngbin0 -> 381 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_rotate.pngbin0 -> 723 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_rotate_left.pngbin0 -> 721 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_rotate_left_selected.pngbin0 -> 738 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_rotate_selected.pngbin0 -> 743 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_share.pngbin0 -> 261 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_share_selected.pngbin0 -> 267 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_slideshow.pngbin0 -> 257 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_slideshow_selected.pngbin0 -> 256 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_undo.pngbin0 -> 371 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_undo_selected.pngbin0 -> 388 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/slider_thumb.pngbin0 -> 392 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/slideshow-end.pngbin0 -> 3098 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/slideshow-pause.pngbin0 -> 1099 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/slideshow-play.pngbin0 -> 1727 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/arrow_left.pngbin0 -> 1773 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/arrow_right.pngbin0 -> 1772 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/back_to_files.pngbin0 -> 688 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/bubble_point.pngbin0 -> 102 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/butterbar_close_button.pngbin0 -> 139 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/cursor_crop.pngbin0 -> 410 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/cursor_leftright.pngbin0 -> 373 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/cursor_move.pngbin0 -> 484 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/cursor_nwse.pngbin0 -> 459 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/cursor_swne.pngbin0 -> 454 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/cursor_updown.pngbin0 -> 366 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_1up.pngbin0 -> 162 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_1up_selected.pngbin0 -> 162 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_autofix.pngbin0 -> 292 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_autofix_selected.pngbin0 -> 292 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_brightness.pngbin0 -> 266 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_brightness_selected.pngbin0 -> 257 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_contrast.pngbin0 -> 305 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_crop.pngbin0 -> 102 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_crop_selected.pngbin0 -> 108 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_delete.pngbin0 -> 112 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_delete_selected.pngbin0 -> 119 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_edit.pngbin0 -> 161 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_edit_selected.pngbin0 -> 164 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_mosaic.pngbin0 -> 124 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_mosaic_selected.pngbin0 -> 126 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_print.pngbin0 -> 144 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_print_selected.pngbin0 -> 148 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_redo.pngbin0 -> 243 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_redo_selected.pngbin0 -> 245 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_rotate.pngbin0 -> 367 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_rotate_left.pngbin0 -> 368 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_rotate_left_selected.pngbin0 -> 382 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_rotate_selected.pngbin0 -> 382 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_share.pngbin0 -> 165 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_share_selected.pngbin0 -> 170 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_slideshow.pngbin0 -> 173 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_slideshow_selected.pngbin0 -> 172 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_undo.pngbin0 -> 240 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_undo_selected.pngbin0 -> 243 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/slider_thumb.pngbin0 -> 221 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/slideshow-end.pngbin0 -> 1717 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/slideshow-pause.pngbin0 -> 648 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/gallery/slideshow-play.pngbin0 -> 1080 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/audio_player.pngbin0 -> 6721 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/drive.pngbin0 -> 2756 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/error.pngbin0 -> 2694 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_close.pngbin0 -> 151 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_collapse.pngbin0 -> 115 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_expand.pngbin0 -> 117 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_fullscreen.pngbin0 -> 579 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_fullscreen_disabled.pngbin0 -> 387 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_fullscreen_down.pngbin0 -> 1790 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_fullscreen_hover.pngbin0 -> 1726 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_loop.pngbin0 -> 516 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_loop_down.pngbin0 -> 2321 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_loop_hover.pngbin0 -> 2259 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_next.pngbin0 -> 409 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_next_down.pngbin0 -> 1905 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_next_hover.pngbin0 -> 1880 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_pause.pngbin0 -> 387 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_pause_audio.pngbin0 -> 148 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_pause_audio_down.pngbin0 -> 1087 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_pause_audio_hover.pngbin0 -> 1019 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_pause_down.pngbin0 -> 1580 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_pause_hover.pngbin0 -> 1518 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_play.pngbin0 -> 471 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_play_audio.pngbin0 -> 364 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_play_audio_down.pngbin0 -> 1655 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_play_audio_hover.pngbin0 -> 1642 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_play_disabled.pngbin0 -> 480 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_play_down.pngbin0 -> 2071 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_play_hover.pngbin0 -> 2027 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_previous.pngbin0 -> 427 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_previous_down.pngbin0 -> 1917 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_previous_hover.pngbin0 -> 1864 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_slider_thumb.pngbin0 -> 1544 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_slider_thumb_down.pngbin0 -> 2011 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_slider_thumb_hover.pngbin0 -> 1959 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_disabled.pngbin0 -> 520 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_disabled_down.pngbin0 -> 2385 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_disabled_hover.pngbin0 -> 2357 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_full.pngbin0 -> 743 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_full_disabled.pngbin0 -> 739 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_full_down.pngbin0 -> 2802 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_full_hover.pngbin0 -> 2779 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_level1.pngbin0 -> 453 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_level1_down.pngbin0 -> 1932 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_level1_hover.pngbin0 -> 1885 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_level2.pngbin0 -> 575 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_level2_down.pngbin0 -> 2314 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_level2_hover.pngbin0 -> 2280 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_volume_slider_thumb.pngbin0 -> 1394 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_volume_slider_thumb_down.pngbin0 -> 1795 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_volume_slider_thumb_hover.pngbin0 -> 1782 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/watch.pngbin0 -> 370 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/audio_player.pngbin0 -> 3172 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/drive.pngbin0 -> 1197 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/error.pngbin0 -> 1026 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/media_close.pngbin0 -> 112 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/media_collapse.pngbin0 -> 95 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/media_expand.pngbin0 -> 97 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/media_fullscreen.pngbin0 -> 237 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/media_fullscreen_disabled.pngbin0 -> 244 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/media_fullscreen_down.pngbin0 -> 890 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/media_fullscreen_hover.pngbin0 -> 873 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/media_loop.pngbin0 -> 309 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/media_loop_down.pngbin0 -> 1147 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/media_loop_hover.pngbin0 -> 1178 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/media_next.pngbin0 -> 226 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/media_next_down.pngbin0 -> 874 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/media_next_hover.pngbin0 -> 866 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/media_pause.pngbin0 -> 199 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/media_pause_audio.pngbin0 -> 132 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/media_pause_audio_down.pngbin0 -> 567 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/media_pause_audio_hover.pngbin0 -> 555 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/media_pause_down.pngbin0 -> 743 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/media_pause_hover.pngbin0 -> 742 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/media_play.pngbin0 -> 269 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/media_play_audio.pngbin0 -> 203 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/media_play_audio_down.pngbin0 -> 756 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/media_play_audio_hover.pngbin0 -> 771 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/media_play_disabled.pngbin0 -> 276 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/media_play_down.pngbin0 -> 940 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/media_play_hover.pngbin0 -> 952 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/media_previous.pngbin0 -> 229 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/media_previous_down.pngbin0 -> 883 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/media_previous_hover.pngbin0 -> 880 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/media_slider_thumb.pngbin0 -> 678 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/media_slider_thumb_down.pngbin0 -> 861 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/media_slider_thumb_hover.pngbin0 -> 842 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_disabled.pngbin0 -> 285 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_disabled_down.pngbin0 -> 1074 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_disabled_hover.pngbin0 -> 1104 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_full.pngbin0 -> 380 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_full_disabled.pngbin0 -> 372 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_full_down.pngbin0 -> 1278 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_full_hover.pngbin0 -> 1273 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_level1.pngbin0 -> 257 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_level1_down.pngbin0 -> 955 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_level1_hover.pngbin0 -> 953 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_level2.pngbin0 -> 313 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_level2_down.pngbin0 -> 1105 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_level2_hover.pngbin0 -> 1113 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/media_volume_slider_thumb.pngbin0 -> 627 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/media_volume_slider_thumb_down.pngbin0 -> 815 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/media_volume_slider_thumb_hover.pngbin0 -> 806 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/media/watch.pngbin0 -> 266 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/archive.pngbin0 -> 156 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_archive.pngbin0 -> 156 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_downloads.pngbin0 -> 200 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_drive.pngbin0 -> 332 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_hdd.pngbin0 -> 203 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_offline.pngbin0 -> 474 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_optical.pngbin0 -> 539 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_phone.pngbin0 -> 141 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_recent.pngbin0 -> 499 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_sd.pngbin0 -> 154 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_shared.pngbin0 -> 535 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_trash.pngbin0 -> 111 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_usb.pngbin0 -> 169 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/device_hd.pngbin0 -> 203 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/device_optical.pngbin0 -> 539 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/device_sd.pngbin0 -> 154 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/device_sd_large.pngbin0 -> 2304 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/device_usb.pngbin0 -> 169 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/device_usb_large.pngbin0 -> 4103 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/downloads.pngbin0 -> 200 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/drive.pngbin0 -> 332 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/drive_offline.pngbin0 -> 474 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/drive_recent.pngbin0 -> 499 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/drive_shared.pngbin0 -> 537 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/folder.pngbin0 -> 138 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_archive.pngbin0 -> 151 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_downloads.pngbin0 -> 193 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_drive.pngbin0 -> 329 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_hdd.pngbin0 -> 174 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_offline.pngbin0 -> 457 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_optical.pngbin0 -> 562 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_phone.pngbin0 -> 139 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_recent.pngbin0 -> 478 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_sd.pngbin0 -> 146 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_shared.pngbin0 -> 498 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_trash.pngbin0 -> 103 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_usb.pngbin0 -> 137 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_archive.pngbin0 -> 108 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_downloads.pngbin0 -> 117 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_drive.pngbin0 -> 182 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_folder.pngbin0 -> 103 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_hdd.pngbin0 -> 116 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_offline.pngbin0 -> 258 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_optical.pngbin0 -> 281 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_phone.pngbin0 -> 112 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_recent.pngbin0 -> 287 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_sd.pngbin0 -> 110 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_shared.pngbin0 -> 289 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_trash.pngbin0 -> 102 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_usb.pngbin0 -> 120 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/device_sd_large.pngbin0 -> 1117 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/device_usb_large.pngbin0 -> 1642 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_archive.pngbin0 -> 106 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_downloads.pngbin0 -> 116 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_drive.pngbin0 -> 179 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_folder.pngbin0 -> 100 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_hdd.pngbin0 -> 108 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_offline.pngbin0 -> 248 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_optical.pngbin0 -> 263 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_phone.pngbin0 -> 114 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_recent.pngbin0 -> 268 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_sd.pngbin0 -> 110 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_shared.pngbin0 -> 262 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_trash.pngbin0 -> 101 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_usb.pngbin0 -> 112 bytes
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/app_installer.js79
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/combobutton.js154
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/commandbutton.js136
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/cws_container_client.js249
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/default_action_dialog.js157
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/directory_contents.js770
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/directory_model.js1186
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/directory_tree.js676
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/drag_selector.js244
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/drive_banners.js660
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/error_counter.js16
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/error_dialog.js29
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/file_grid.js302
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/file_manager.js3688
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/file_manager_commands.js828
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/file_operation_manager_wrapper.js56
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/file_selection.js360
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/file_table.js1036
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/file_tasks.js834
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/file_transfer_controller.js860
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/file_type.js294
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/file_watcher.js227
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/folder_shortcuts_data_model.js293
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/commands.js455
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/exif_encoder.js569
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/filter.js612
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/image_adjust.js248
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/image_buffer.js184
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/image_editor.js1177
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/image_encoder.js228
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/image_transform.js493
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/image_util.js701
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/image_view.js1065
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/viewport.js430
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/main.js41
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/main_scripts.js132
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/media/audio_player.js628
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/media/media_controls.js1245
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/media/media_util.js421
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/media/mediaplayer_scripts.js33
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/media/player_testapi.js193
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/media/util.js179
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/media/video_player.js289
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/media/video_player_scripts.js34
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/metadata/byte_reader.js535
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/metadata/exif_parser.js439
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/metadata/function_parallel.js82
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/metadata/function_sequence.js133
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/metadata/id3_parser.js708
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/metadata/image_parsers.js198
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/metadata/metadata_cache.js1042
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/metadata/metadata_dispatcher.js226
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/metadata/metadata_parser.js62
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/metadata/mpeg_parser.js317
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/metrics.js131
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/navigation_list_model.js350
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/photo/gallery.js867
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/photo/gallery_item.js227
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/photo/gallery_scripts.js70
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/photo/mosaic_mode.js2012
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/photo/ribbon.js366
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/photo/slide_mode.js1354
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/scrollbar.js294
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/share_client.js188
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/share_dialog.js314
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/suggest_apps_dialog.js554
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/text_measure.js50
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/tree.css.js59
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/ui/breadcrumbs_controller.js262
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/ui/conflict_dialog.js132
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/ui/file_manager_dialog_base.js122
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/ui/file_manager_ui.js190
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/ui/navigation_list.js386
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/ui/preview_panel.js518
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/ui/progress_center_panel.js329
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/ui/search_box.js202
-rw-r--r--chromium/chrome/browser/resources/file_manager/foreground/js/volume_manager_wrapper.js334
-rw-r--r--chromium/chrome/browser/resources/file_manager/gallery.html79
-rw-r--r--chromium/chrome/browser/resources/file_manager/main.html413
-rw-r--r--chromium/chrome/browser/resources/file_manager/manifest.json231
-rw-r--r--chromium/chrome/browser/resources/file_manager/mediaplayer.html47
-rw-r--r--chromium/chrome/browser/resources/file_manager/video_player.html57
526 files changed, 49268 insertions, 0 deletions
diff --git a/chromium/chrome/browser/resources/file_manager/OWNERS b/chromium/chrome/browser/resources/file_manager/OWNERS
new file mode 100644
index 00000000000..bca0cd957a2
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/OWNERS
@@ -0,0 +1,3 @@
+hirono@chromium.org
+mtomasz@chromium.org
+yoshiki@chromium.org
diff --git a/chromium/chrome/browser/resources/file_manager/background/js/background.js b/chromium/chrome/browser/resources/file_manager/background/js/background.js
new file mode 100644
index 00000000000..e5fb6b85fc7
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/background/js/background.js
@@ -0,0 +1,787 @@
+// 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.
+
+'use strict';
+
+/**
+ * Number of runtime errors catched in the background page.
+ * @type {number}
+ */
+var JSErrorCount = 0;
+
+/**
+ * Counts runtime JavaScript errors.
+ */
+window.onerror = function() { JSErrorCount++; };
+
+/**
+ * Type of a Files.app's instance launch.
+ * @enum {number}
+ */
+var LaunchType = Object.freeze({
+ ALWAYS_CREATE: 0,
+ FOCUS_ANY_OR_CREATE: 1,
+ FOCUS_SAME_OR_CREATE: 2
+});
+
+/**
+ * Root class of the background page.
+ * @constructor
+ */
+function Background() {
+ /**
+ * Map of all currently open app windows. The key is an app id.
+ * @type {Object.<string, AppWindow>}
+ */
+ this.appWindows = {};
+
+ /**
+ * Synchronous queue for asynchronous calls.
+ * @type {AsyncUtil.Queue}
+ */
+ this.queue = new AsyncUtil.Queue();
+
+ /**
+ * Progress center of the background page.
+ * @type {ProgressCenter}
+ */
+ this.progressCenter = new ProgressCenter();
+
+ /**
+ * File operation manager.
+ * @type {FileOperationManager}
+ */
+ this.fileOperationManager = FileOperationManager.getInstance();
+
+ /**
+ * Event handler for progress center.
+ * @type {FileOperationHandler}
+ * @private
+ */
+ this.fileOperationHandler_ = new FileOperationHandler(this);
+
+ /**
+ * String assets.
+ * @type {Object.<string, string>}
+ */
+ this.stringData = null;
+
+ /**
+ * Callback list to be invoked after initialization.
+ * It turns to null after initialization.
+ *
+ * @type {Array.<function()>}
+ * @private
+ */
+ this.initializeCallbacks_ = [];
+
+ /**
+ * Last time when the background page can close.
+ *
+ * @type {number}
+ * @private
+ */
+ this.lastTimeCanClose_ = null;
+
+ // Seal self.
+ Object.seal(this);
+
+ // Initialize handlers.
+ chrome.fileBrowserHandler.onExecute.addListener(this.onExecute_.bind(this));
+ chrome.app.runtime.onLaunched.addListener(this.onLaunched_.bind(this));
+ chrome.app.runtime.onRestarted.addListener(this.onRestarted_.bind(this));
+ chrome.contextMenus.onClicked.addListener(
+ this.onContextMenuClicked_.bind(this));
+
+ // Fetch strings and initialize the context menu.
+ this.queue.run(function(callNextStep) {
+ chrome.fileBrowserPrivate.getStrings(function(strings) {
+ // Initialize string assets.
+ this.stringData = strings;
+ loadTimeData.data = strings;
+ this.initContextMenu_();
+
+ // Invoke initialize callbacks.
+ for (var i = 0; i < this.initializeCallbacks_.length; i++) {
+ this.initializeCallbacks_[i]();
+ }
+ this.initializeCallbacks_ = null;
+
+ callNextStep();
+ }.bind(this));
+ }.bind(this));
+}
+
+/**
+ * A number of delay milliseconds from the first call of tryClose to the actual
+ * close action.
+ * @type {number}
+ * @const
+ * @private
+ */
+Background.CLOSE_DELAY_MS_ = 5000;
+
+/**
+ * Make a key of window geometry preferences for the given initial URL.
+ * @param {string} url Initialize URL that the window has.
+ * @return {string} Key of window geometry preferences.
+ */
+Background.makeGeometryKey = function(url) {
+ return 'windowGeometry' + ':' + url;
+};
+
+/**
+ * Register callback to be invoked after initialization.
+ * If the initialization is already done, the callback is invoked immediately.
+ *
+ * @param {function()} callback Initialize callback to be registered.
+ */
+Background.prototype.ready = function(callback) {
+ if (this.initializeCallbacks_ !== null)
+ this.initializeCallbacks_.push(callback);
+ else
+ callback();
+};
+
+/**
+ * Checks the current condition of background page and closes it if possible.
+ */
+Background.prototype.tryClose = function() {
+ // If the file operation is going, the background page cannot close.
+ if (this.fileOperationManager.hasQueuedTasks()) {
+ this.lastTimeCanClose_ = null;
+ return;
+ }
+
+ var views = chrome.extension.getViews();
+ var closing = false;
+ for (var i = 0; i < views.length; i++) {
+ // If the window that is not the background page itself and it is not
+ // closing, the background page cannot close.
+ if (views[i] !== window && !views[i].closing) {
+ this.lastTimeCanClose_ = null;
+ return;
+ }
+ closing = closing || views[i].closing;
+ }
+
+ // If some windows are closing, or the background page can close but could not
+ // 5 seconds ago, We need more time for sure.
+ if (closing ||
+ this.lastTimeCanClose_ === null ||
+ Date.now() - this.lastTimeCanClose_ < Background.CLOSE_DELAY_MS_) {
+ if (this.lastTimeCanClose_ === null)
+ this.lastTimeCanClose_ = Date.now();
+ setTimeout(this.tryClose.bind(this), Background.CLOSE_DELAY_MS_);
+ return;
+ }
+
+ // Otherwise we can close the background page.
+ close();
+};
+
+/**
+ * Gets similar windows, it means with the same initial url.
+ * @param {string} url URL that the obtained windows have.
+ * @return {Array.<AppWindow>} List of similar windows.
+ */
+Background.prototype.getSimilarWindows = function(url) {
+ var result = [];
+ for (var appID in this.appWindows) {
+ if (this.appWindows[appID].contentWindow.appInitialURL === url)
+ result.push(this.appWindows[appID]);
+ }
+ return result;
+};
+
+/**
+ * Wrapper for an app window.
+ *
+ * Expects the following from the app scripts:
+ * 1. The page load handler should initialize the app using |window.appState|
+ * and call |util.platform.saveAppState|.
+ * 2. Every time the app state changes the app should update |window.appState|
+ * and call |util.platform.saveAppState| .
+ * 3. The app may have |unload| function to persist the app state that does not
+ * fit into |window.appState|.
+ *
+ * @param {string} url App window content url.
+ * @param {string} id App window id.
+ * @param {Object} options Options object to create it.
+ * @constructor
+ */
+function AppWindowWrapper(url, id, options) {
+ this.url_ = url;
+ this.id_ = id;
+ // Do deep copy for the template of options to assign customized params later.
+ this.options_ = JSON.parse(JSON.stringify(options));
+ this.window_ = null;
+ this.appState_ = null;
+ this.openingOrOpened_ = false;
+ this.queue = new AsyncUtil.Queue();
+ Object.seal(this);
+}
+
+/**
+ * Shift distance to avoid overlapping windows.
+ * @type {number}
+ * @const
+ */
+AppWindowWrapper.SHIFT_DISTANCE = 40;
+
+
+/**
+ * Opens the window.
+ *
+ * @param {Object} appState App state.
+ * @param {function()=} opt_callback Completion callback.
+ */
+AppWindowWrapper.prototype.launch = function(appState, opt_callback) {
+ // Check if the window is opened or not.
+ if (this.openingOrOpened_) {
+ console.error('The window is already opened.');
+ if (opt_callback)
+ opt_callback();
+ return;
+ }
+ this.openingOrOpened_ = true;
+
+ // Save application state.
+ this.appState_ = appState;
+
+ // Get similar windows, it means with the same initial url, eg. different
+ // main windows of Files.app.
+ var similarWindows = background.getSimilarWindows(this.url_);
+
+ // Restore maximized windows, to avoid hiding them to tray, which can be
+ // confusing for users.
+ this.queue.run(function(callback) {
+ for (var index = 0; index < similarWindows.length; index++) {
+ if (similarWindows[index].isMaximized()) {
+ var createWindowAndRemoveListener = function() {
+ similarWindows[index].onRestored.removeListener(
+ createWindowAndRemoveListener);
+ callback();
+ };
+ similarWindows[index].onRestored.addListener(
+ createWindowAndRemoveListener);
+ similarWindows[index].restore();
+ return;
+ }
+ }
+ // If no maximized windows, then create the window immediately.
+ callback();
+ });
+
+ // Obtains the last geometry.
+ var lastBounds;
+ this.queue.run(function(callback) {
+ var key = Background.makeGeometryKey(this.url_);
+ chrome.storage.local.get(key, function(preferences) {
+ if (!chrome.runtime.lastError)
+ lastBounds = preferences[key];
+ callback();
+ });
+ }.bind(this));
+
+ // Closure creating the window, once all preprocessing tasks are finished.
+ this.queue.run(function(callback) {
+ // Apply the last bounds.
+ if (lastBounds)
+ this.options_.bounds = lastBounds;
+
+ // Create a window.
+ chrome.app.window.create(this.url_, this.options_, function(appWindow) {
+ this.window_ = appWindow;
+ callback();
+ }.bind(this));
+ }.bind(this));
+
+ // After creating.
+ this.queue.run(function(callback) {
+ // If there is another window in the same position, shift the window.
+ var makeBoundsKey = function(bounds) {
+ return bounds.left + '/' + bounds.top;
+ };
+ var notAvailablePositions = {};
+ for (var i = 0; i < similarWindows.length; i++) {
+ var key = makeBoundsKey(similarWindows[i].getBounds());
+ notAvailablePositions[key] = true;
+ }
+ var candidateBounds = this.window_.getBounds();
+ while (true) {
+ var key = makeBoundsKey(candidateBounds);
+ if (!notAvailablePositions[key])
+ break;
+ // Make the position available to avoid an infinite loop.
+ notAvailablePositions[key] = false;
+ var nextLeft = candidateBounds.left + AppWindowWrapper.SHIFT_DISTANCE;
+ var nextRight = nextLeft + candidateBounds.width;
+ candidateBounds.left = nextRight >= screen.availWidth ?
+ nextRight % screen.availWidth : nextLeft;
+ var nextTop = candidateBounds.top + AppWindowWrapper.SHIFT_DISTANCE;
+ var nextBottom = nextTop + candidateBounds.height;
+ candidateBounds.top = nextBottom >= screen.availHeight ?
+ nextBottom % screen.availHeight : nextTop;
+ }
+ this.window_.moveTo(candidateBounds.left, candidateBounds.top);
+
+ // Save the properties.
+ var appWindow = this.window_;
+ background.appWindows[this.id_] = appWindow;
+ var contentWindow = appWindow.contentWindow;
+ contentWindow.appID = this.id_;
+ contentWindow.appState = this.appState_;
+ contentWindow.appInitialURL = this.url_;
+ if (window.IN_TEST)
+ contentWindow.IN_TEST = true;
+
+ // Register event listners.
+ appWindow.onBoundsChanged.addListener(this.onBoundsChanged_.bind(this));
+ appWindow.onClosed.addListener(this.onClosed_.bind(this));
+
+ // Callback.
+ if (opt_callback)
+ opt_callback();
+ callback();
+ }.bind(this));
+};
+
+/**
+ * Handles the onClosed extension API event.
+ * @private
+ */
+AppWindowWrapper.prototype.onClosed_ = function() {
+ // Unload the window.
+ var appWindow = this.window_;
+ var contentWindow = this.window_.contentWindow;
+ if (contentWindow.unload)
+ contentWindow.unload();
+ this.window_ = null;
+ this.openingOrOpened_ = false;
+
+ // Updates preferences.
+ if (contentWindow.saveOnExit) {
+ contentWindow.saveOnExit.forEach(function(entry) {
+ util.AppCache.update(entry.key, entry.value);
+ });
+ }
+ chrome.storage.local.remove(this.id_); // Forget the persisted state.
+
+ // Remove the window from the set.
+ delete background.appWindows[this.id_];
+
+ // If there is no application window, reset window ID.
+ if (!Object.keys(background.appWindows).length)
+ nextFileManagerWindowID = 0;
+ background.tryClose();
+};
+
+/**
+ * Handles onBoundsChanged extension API event.
+ * @private
+ */
+AppWindowWrapper.prototype.onBoundsChanged_ = function() {
+ var preferences = {};
+ preferences[Background.makeGeometryKey(this.url_)] =
+ this.window_.getBounds();
+ chrome.storage.local.set(preferences);
+};
+
+/**
+ * Wrapper for a singleton app window.
+ *
+ * In addition to the AppWindowWrapper requirements the app scripts should
+ * have |reload| method that re-initializes the app based on a changed
+ * |window.appState|.
+ *
+ * @param {string} url App window content url.
+ * @param {Object|function()} options Options object or a function to return it.
+ * @constructor
+ */
+function SingletonAppWindowWrapper(url, options) {
+ AppWindowWrapper.call(this, url, url, options);
+}
+
+/**
+ * Inherits from AppWindowWrapper.
+ */
+SingletonAppWindowWrapper.prototype = {__proto__: AppWindowWrapper.prototype};
+
+/**
+ * Open the window.
+ *
+ * Activates an existing window or creates a new one.
+ *
+ * @param {Object} appState App state.
+ * @param {function()=} opt_callback Completion callback.
+ */
+SingletonAppWindowWrapper.prototype.launch = function(appState, opt_callback) {
+ // If the window is not opened yet, just call the parent method.
+ if (!this.openingOrOpened_) {
+ AppWindowWrapper.prototype.launch.call(this, appState, opt_callback);
+ return;
+ }
+
+ // If the window is already opened, reload the window.
+ // The queue is used to wait until the window is opened.
+ this.queue.run(function(nextStep) {
+ this.window_.contentWindow.appState = appState;
+ this.window_.contentWindow.reload();
+ this.window_.focus();
+ if (opt_callback)
+ opt_callback();
+ nextStep();
+ }.bind(this));
+};
+
+/**
+ * Reopen a window if its state is saved in the local storage.
+ */
+SingletonAppWindowWrapper.prototype.reopen = function() {
+ chrome.storage.local.get(this.id_, function(items) {
+ var value = items[this.id_];
+ if (!value)
+ return; // No app state persisted.
+
+ try {
+ var appState = JSON.parse(value);
+ } catch (e) {
+ console.error('Corrupt launch data for ' + this.id_, value);
+ return;
+ }
+ this.launch(appState);
+ }.bind(this));
+};
+
+/**
+ * Prefix for the file manager window ID.
+ */
+var FILES_ID_PREFIX = 'files#';
+
+/**
+ * Regexp matching a file manager window ID.
+ */
+var FILES_ID_PATTERN = new RegExp('^' + FILES_ID_PREFIX + '(\\d*)$');
+
+/**
+ * Value of the next file manager window ID.
+ */
+var nextFileManagerWindowID = 0;
+
+/**
+ * File manager window create options.
+ * @type {Object}
+ * @const
+ */
+var FILE_MANAGER_WINDOW_CREATE_OPTIONS = Object.freeze({
+ bounds: Object.freeze({
+ left: Math.round(window.screen.availWidth * 0.1),
+ top: Math.round(window.screen.availHeight * 0.1),
+ width: Math.round(window.screen.availWidth * 0.8),
+ height: Math.round(window.screen.availHeight * 0.8)
+ }),
+ minWidth: 320,
+ minHeight: 240,
+ frame: 'none',
+ hidden: true,
+ transparentBackground: true
+});
+
+/**
+ * @param {Object=} opt_appState App state.
+ * @param {number=} opt_id Window id.
+ * @param {LaunchType=} opt_type Launch type. Default: ALWAYS_CREATE.
+ * @param {function(string)=} opt_callback Completion callback with the App ID.
+ */
+function launchFileManager(opt_appState, opt_id, opt_type, opt_callback) {
+ var type = opt_type || LaunchType.ALWAYS_CREATE;
+
+ // Wait until all windows are created.
+ background.queue.run(function(onTaskCompleted) {
+ // Check if there is already a window with the same path. If so, then
+ // reuse it instead of opening a new one.
+ if (type == LaunchType.FOCUS_SAME_OR_CREATE ||
+ type == LaunchType.FOCUS_ANY_OR_CREATE) {
+ if (opt_appState && opt_appState.defaultPath) {
+ for (var key in background.appWindows) {
+ if (!key.match(FILES_ID_PATTERN))
+ continue;
+
+ var contentWindow = background.appWindows[key].contentWindow;
+ if (contentWindow.appState &&
+ opt_appState.defaultPath == contentWindow.appState.defaultPath) {
+ background.appWindows[key].focus();
+ if (opt_callback)
+ opt_callback(key);
+ onTaskCompleted();
+ return;
+ }
+ }
+ }
+ }
+
+ // Focus any window if none is focused. Try restored first.
+ if (type == LaunchType.FOCUS_ANY_OR_CREATE) {
+ // If there is already a focused window, then finish.
+ for (var key in background.appWindows) {
+ if (!key.match(FILES_ID_PATTERN))
+ continue;
+
+ // The isFocused() method should always be available, but in case
+ // Files.app's failed on some error, wrap it with try catch.
+ try {
+ if (background.appWindows[key].contentWindow.isFocused()) {
+ if (opt_callback)
+ opt_callback(key);
+ onTaskCompleted();
+ return;
+ }
+ } catch (e) {
+ console.error(e.message);
+ }
+ }
+ // Try to focus the first non-minimized window.
+ for (var key in background.appWindows) {
+ if (!key.match(FILES_ID_PATTERN))
+ continue;
+
+ if (!background.appWindows[key].isMinimized()) {
+ background.appWindows[key].focus();
+ if (opt_callback)
+ opt_callback(key);
+ onTaskCompleted();
+ return;
+ }
+ }
+ // Restore and focus any window.
+ for (var key in background.appWindows) {
+ if (!key.match(FILES_ID_PATTERN))
+ continue;
+
+ background.appWindows[key].focus();
+ if (opt_callback)
+ opt_callback(key);
+ onTaskCompleted();
+ return;
+ }
+ }
+
+ // Create a new instance in case of ALWAYS_CREATE type, or as a fallback
+ // for other types.
+
+ var id = opt_id || nextFileManagerWindowID;
+ nextFileManagerWindowID = Math.max(nextFileManagerWindowID, id + 1);
+ var appId = FILES_ID_PREFIX + id;
+
+ var appWindow = new AppWindowWrapper(
+ 'main.html',
+ appId,
+ FILE_MANAGER_WINDOW_CREATE_OPTIONS);
+ appWindow.launch(opt_appState || {}, function() {
+ if (opt_callback)
+ opt_callback(appId);
+ onTaskCompleted();
+ });
+ });
+}
+
+/**
+ * Executes a file browser task.
+ *
+ * @param {string} action Task id.
+ * @param {Object} details Details object.
+ * @private
+ */
+Background.prototype.onExecute_ = function(action, details) {
+ var urls = details.entries.map(function(e) { return e.toURL(); });
+
+ switch (action) {
+ case 'play':
+ launchAudioPlayer({items: urls, position: 0});
+ break;
+
+ case 'watch':
+ launchVideoPlayer(urls[0]);
+ break;
+
+ default:
+ var launchEnable = null;
+ var queue = new AsyncUtil.Queue();
+ queue.run(function(nextStep) {
+ // If it is not auto-open (triggered by mounting external devices), we
+ // always launch Files.app.
+ if (action != 'auto-open') {
+ launchEnable = true;
+ nextStep();
+ return;
+ }
+ // If the disable-default-apps flag is on, Files.app is not opened
+ // automatically on device mount because it obstculs the manual test.
+ chrome.commandLinePrivate.hasSwitch('disable-default-apps',
+ function(flag) {
+ launchEnable = !flag;
+ nextStep();
+ });
+ });
+ queue.run(function(nextStep) {
+ if (!launchEnable) {
+ nextStep();
+ return;
+ }
+
+ // Every other action opens a Files app window.
+ var appState = {
+ params: {
+ action: action
+ },
+ defaultPath: details.entries[0].fullPath
+ };
+ // For mounted devices just focus any Files.app window. The mounted
+ // volume will appear on the navigation list.
+ var type = action == 'auto-open' ? LaunchType.FOCUS_ANY_OR_CREATE :
+ LaunchType.FOCUS_SAME_OR_CREATE;
+ launchFileManager(appState, /* App ID */ undefined, type, nextStep);
+ });
+ break;
+ }
+};
+
+/**
+ * Audio player window create options.
+ * @type {Object}
+ * @const
+ */
+var AUDIO_PLAYER_CREATE_OPTIONS = Object.freeze({
+ type: 'panel',
+ hidden: true,
+ minHeight: 35 + 58,
+ minWidth: 280,
+ height: 35 + 58,
+ width: 280
+});
+
+var audioPlayer = new SingletonAppWindowWrapper('mediaplayer.html',
+ AUDIO_PLAYER_CREATE_OPTIONS);
+
+/**
+ * Launch the audio player.
+ * @param {Object} playlist Playlist.
+ */
+function launchAudioPlayer(playlist) {
+ audioPlayer.launch(playlist);
+}
+
+var videoPlayer = new SingletonAppWindowWrapper('video_player.html',
+ {hidden: true});
+
+/**
+ * Launch the video player.
+ * @param {string} url Video url.
+ */
+function launchVideoPlayer(url) {
+ videoPlayer.launch({url: url});
+}
+
+/**
+ * Launches the app.
+ * @private
+ */
+Background.prototype.onLaunched_ = function() {
+ if (nextFileManagerWindowID == 0) {
+ // The app just launched. Remove window state records that are not needed
+ // any more.
+ chrome.storage.local.get(function(items) {
+ for (var key in items) {
+ if (items.hasOwnProperty(key)) {
+ if (key.match(FILES_ID_PATTERN))
+ chrome.storage.local.remove(key);
+ }
+ }
+ });
+ }
+ launchFileManager(null, null, LaunchType.FOCUS_ANY_OR_CREATE);
+};
+
+/**
+ * Restarted the app, restore windows.
+ * @private
+ */
+Background.prototype.onRestarted_ = function() {
+ // Reopen file manager windows.
+ chrome.storage.local.get(function(items) {
+ for (var key in items) {
+ if (items.hasOwnProperty(key)) {
+ var match = key.match(FILES_ID_PATTERN);
+ if (match) {
+ var id = Number(match[1]);
+ try {
+ var appState = JSON.parse(items[key]);
+ launchFileManager(appState, id);
+ } catch (e) {
+ console.error('Corrupt launch data for ' + id);
+ }
+ }
+ }
+ }
+ });
+
+ // Reopen sub-applications.
+ audioPlayer.reopen();
+ videoPlayer.reopen();
+};
+
+/**
+ * Handles clicks on a custom item on the launcher context menu.
+ * @param {OnClickData} info Event details.
+ * @private
+ */
+Background.prototype.onContextMenuClicked_ = function(info) {
+ if (info.menuItemId == 'new-window') {
+ // Find the focused window (if any) and use it's current path for the
+ // new window. If not found, then launch with the default path.
+ for (var key in background.appWindows) {
+ try {
+ if (background.appWindows[key].contentWindow.isFocused()) {
+ var appState = {
+ defaultPath: background.appWindows[key].contentWindow.
+ appState.defaultPath
+ };
+ launchFileManager(appState);
+ return;
+ }
+ } catch (ignore) {
+ // The isFocused method may not be defined during initialization.
+ // Therefore, wrapped with a try-catch block.
+ }
+ }
+
+ // Launch with the default path.
+ launchFileManager();
+ }
+};
+
+/**
+ * Initializes the context menu. Recreates if already exists.
+ * @private
+ */
+Background.prototype.initContextMenu_ = function() {
+ try {
+ chrome.contextMenus.remove('new-window');
+ } catch (ignore) {
+ // There is no way to detect if the context menu is already added, therefore
+ // try to recreate it every time.
+ }
+ chrome.contextMenus.create({
+ id: 'new-window',
+ contexts: ['launcher'],
+ title: str('NEW_WINDOW_BUTTON_LABEL')
+ });
+};
+
+/**
+ * Singleton instance of Background.
+ * @type {Background}
+ */
+window.background = new Background();
diff --git a/chromium/chrome/browser/resources/file_manager/background/js/file_operation_handler.js b/chromium/chrome/browser/resources/file_manager/background/js/file_operation_handler.js
new file mode 100644
index 00000000000..c51ed03515c
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/background/js/file_operation_handler.js
@@ -0,0 +1,315 @@
+// Copyright 2013 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.
+
+'use strict';
+
+/**
+ * An event handler of the background page for file operaitons.
+ * @param {Background} background Background page.
+ * @constructor
+ */
+var FileOperationHandler = function(background) {
+ /**
+ * Background page.
+ * @type {Background}
+ * @private
+ */
+ this.background_ = background;
+
+ /**
+ * File operation manager.
+ * @type {FileOperationManager}
+ * @private
+ */
+ this.fileOperationManager_ = background.fileOperationManager;
+
+ /**
+ * Progress center.
+ * @type {progressCenter}
+ * @private
+ */
+ this.progressCenter_ = background.progressCenter;
+
+ /**
+ * Pending items of delete operation.
+ *
+ * Delete operations are usually complete quickly.
+ * So we would not like to show the progress bar at first.
+ * If the operation takes more than FileOperationHandler.PENDING_TIME_MS_,
+ * we adds the item to the progress center.
+ *
+ * @type {Object.<string, ProgressCenterItem>}}
+ * @private
+ */
+ this.pendingItems_ = {};
+
+ // Register event.
+ this.fileOperationManager_.addEventListener(
+ 'copy-progress',
+ this.onCopyProgress_.bind(this));
+ this.fileOperationManager_.addEventListener(
+ 'delete',
+ this.onDeleteProgress_.bind(this));
+
+ // Seal the object.
+ Object.seal(this);
+};
+
+/**
+ * Pending time before a delete item is added to the progress center.
+ *
+ * @type {number}
+ * @const
+ * @private
+ */
+FileOperationHandler.PENDING_TIME_MS_ = 500;
+
+/**
+ * Generate a progress message from the event.
+ * @param {Event} event Progress event.
+ * @return {string} message.
+ * @private
+ */
+FileOperationHandler.getMessage_ = function(event) {
+ if (event.reason === 'ERROR') {
+ switch (event.error.code) {
+ case util.FileOperationErrorType.TARGET_EXISTS:
+ var name = event.error.data.name;
+ if (event.error.data.isDirectory)
+ name += '/';
+ switch (event.status.operationType) {
+ case 'COPY': return strf('COPY_TARGET_EXISTS_ERROR', name);
+ case 'MOVE': return strf('MOVE_TARGET_EXISTS_ERROR', name);
+ case 'ZIP': return strf('ZIP_TARGET_EXISTS_ERROR', name);
+ default: return strf('TRANSFER_TARGET_EXISTS_ERROR', name);
+ }
+
+ case util.FileOperationErrorType.FILESYSTEM_ERROR:
+ var detail = util.getFileErrorString(event.error.data.code);
+ switch (event.status.operationType) {
+ case 'COPY': return strf('COPY_FILESYSTEM_ERROR', detail);
+ case 'MOVE': return strf('MOVE_FILESYSTEM_ERROR', detail);
+ case 'ZIP': return strf('ZIP_FILESYSTEM_ERROR', detail);
+ default: return strf('TRANSFER_FILESYSTEM_ERROR', detail);
+ }
+
+ default:
+ switch (event.status.operationType) {
+ case 'COPY': return strf('COPY_UNEXPECTED_ERROR', event.error.code);
+ case 'MOVE': return strf('MOVE_UNEXPECTED_ERROR', event.error.code);
+ case 'ZIP': return strf('ZIP_UNEXPECTED_ERROR', event.error.code);
+ default: return strf('TRANSFER_UNEXPECTED_ERROR', event.error.code);
+ }
+ }
+ } else if (event.status.numRemainingItems === 1) {
+ var name = event.status.processingEntry.name;
+ switch (event.status.operationType) {
+ case 'COPY': return strf('COPY_FILE_NAME', name);
+ case 'MOVE': return strf('MOVE_FILE_NAME', name);
+ case 'ZIP': return strf('ZIP_FILE_NAME', name);
+ default: return strf('TRANSFER_FILE_NAME', name);
+ }
+ } else {
+ var remainNumber = event.status.numRemainingItems;
+ switch (event.status.operationType) {
+ case 'COPY': return strf('COPY_ITEMS_REMAINING', remainNumber);
+ case 'MOVE': return strf('MOVE_ITEMS_REMAINING', remainNumber);
+ case 'ZIP': return strf('ZIP_ITEMS_REMAINING', remainNumber);
+ default: return strf('TRANSFER_ITEMS_REMAINING', remainNumber);
+ }
+ }
+};
+
+/**
+ * Generates a delete message from the event.
+ * @param {Event} event Progress event.
+ * @return {string} message.
+ * @private
+ */
+FileOperationHandler.getDeleteMessage_ = function(event) {
+ if (event.reason === 'ERROR') {
+ return str('DELETE_ERROR');
+ } else if (event.entries.length == 1) {
+ var fileName = event.entries[0].name;
+ return strf('DELETE_FILE_NAME', fileName);
+ } else if (event.entries.length > 1) {
+ return strf('DELETE_ITEMS_REMAINING', event.entries.length);
+ } else {
+ return '';
+ }
+};
+
+/**
+ * Obtains ProgressItemType from OperationType of FileTransferManager.
+ * @param {string} operationType OperationType of FileTransferManager.
+ * @return {ProgressItemType} ProgreeType corresponding to the specified
+ * operation type.
+ * @private
+ */
+FileOperationHandler.getType_ = function(operationType) {
+ switch (operationType) {
+ case 'COPY': return ProgressItemType.COPY;
+ case 'MOVE': return ProgressItemType.MOVE;
+ case 'ZIP': return ProgressItemType.ZIP;
+ default:
+ console.error('Unknown operation type.');
+ return ProgressItemType.TRANSFER;
+ }
+};
+
+/**
+ * Handles the copy-progress event.
+ * @param {Event} event The copy-progress event.
+ * @private
+ */
+FileOperationHandler.prototype.onCopyProgress_ = function(event) {
+ // If the copy is finished, may be we can close the background page.
+ if (event.reason !== 'BEGIN' && event.reason !== 'PROGRESS')
+ this.background_.tryClose();
+
+ // Update progress center.
+ var progressCenter = this.progressCenter_;
+ var item;
+ switch (event.reason) {
+ case 'BEGIN':
+ item = new ProgressCenterItem();
+ item.id = event.taskId;
+ item.type = FileOperationHandler.getType_(event.status.operationType);
+ item.message = FileOperationHandler.getMessage_(event);
+ item.progressMax = event.status.totalBytes;
+ item.progressValue = event.status.processedBytes;
+ item.cancelCallback = this.fileOperationManager_.requestTaskCancel.bind(
+ this.fileOperationManager_,
+ event.taskId);
+ progressCenter.updateItem(item);
+ break;
+
+ case 'PROGRESS':
+ item = progressCenter.getItemById(event.taskId);
+ if (!item) {
+ console.error('Cannot find copying item.');
+ return;
+ }
+ item.message = FileOperationHandler.getMessage_(event);
+ item.progressValue = event.status.processedBytes;
+ progressCenter.updateItem(item);
+ break;
+
+ case 'SUCCESS':
+ case 'CANCELED':
+ case 'ERROR':
+ item = progressCenter.getItemById(event.taskId);
+ if (!item) {
+ // ERROR events can be dispatched before BEGIN events.
+ item = new ProgressCenterItem();
+ item.type = FileOperationHandler.getType_(event.status.operationType);
+ item.id = event.taskId;
+ item.progressMax = 1;
+ }
+ if (event.reason === 'SUCCESS') {
+ item.message = '';
+ item.state = ProgressItemState.COMPLETED;
+ item.progressValue = item.progressMax;
+ } else if (event.reason === 'CANCELED') {
+ item.message = '';
+ item.state = ProgressItemState.CANCELED;
+ } else {
+ item.message = FileOperationHandler.getMessage_(event);
+ item.state = ProgressItemState.ERROR;
+ }
+ progressCenter.updateItem(item);
+ break;
+ }
+};
+
+/**
+ * Handles the delete event.
+ * @param {Event} event The delete event.
+ * @private
+ */
+FileOperationHandler.prototype.onDeleteProgress_ = function(event) {
+ // If the copy is finished, may be we can close the background page.
+ if (event.reason !== 'BEGIN' && event.reason !== 'PROGRESS')
+ this.background_.tryClose();
+
+ // Update progress center.
+ var progressCenter = this.progressCenter_;
+ var item;
+ var pending;
+ switch (event.reason) {
+ case 'BEGIN':
+ item = new ProgressCenterItem();
+ item.id = event.taskId;
+ item.type = ProgressItemType.DELETE;
+ item.message = FileOperationHandler.getDeleteMessage_(event);
+ item.progressMax = event.totalBytes;
+ item.progressValue = event.processedBytes;
+ item.cancelCallback = this.fileOperationManager_.requestTaskCancel.bind(
+ this.fileOperationManager_,
+ event.taskId);
+ this.pendingItems_[item.id] = item;
+ setTimeout(this.showPendingItem_.bind(this, item),
+ FileOperationHandler.PENDING_TIME_MS_);
+ break;
+
+ case 'PROGRESS':
+ pending = event.taskId in this.pendingItems_;
+ item = this.pendingItems_[event.taskId] ||
+ progressCenter.getItemById(event.taskId);
+ if (!item) {
+ console.error('Cannot find deleting item.');
+ return;
+ }
+ item.message = FileOperationHandler.getDeleteMessage_(event);
+ item.progressMax = event.totalBytes;
+ item.progressValue = event.processedBytes;
+ if (!pending)
+ progressCenter.updateItem(item);
+ break;
+
+ case 'SUCCESS':
+ case 'CANCELED':
+ case 'ERROR':
+ // Obtain working variable.
+ pending = event.taskId in this.pendingItems_;
+ item = this.pendingItems_[event.taskId] ||
+ progressCenter.getItemById(event.taskId);
+ if (!item) {
+ console.error('Cannot find deleting item.');
+ return;
+ }
+
+ // Update the item.
+ item.message = FileOperationHandler.getDeleteMessage_(event);
+ if (event.reason === 'SUCCESS') {
+ item.state = ProgressItemState.COMPLETED;
+ item.progressValue = item.progressMax;
+ } else if (event.reason === 'CANCELED') {
+ item.state = ProgressItemState.CANCELED;
+ } else {
+ item.state = ProgressItemState.ERROR;
+ }
+
+ // Apply the change.
+ if (!pending || event.reason === 'ERROR')
+ progressCenter.updateItem(item);
+ if (pending)
+ delete this.pendingItems_[event.taskId];
+ break;
+ }
+};
+
+/**
+ * Shows the pending item.
+ *
+ * @param {ProgressCenterItem} item Pending item.
+ * @private
+ */
+FileOperationHandler.prototype.showPendingItem_ = function(item) {
+ // The item is already gone.
+ if (!this.pendingItems_[item.id])
+ return;
+ delete this.pendingItems_[item.id];
+ this.progressCenter_.updateItem(item);
+};
diff --git a/chromium/chrome/browser/resources/file_manager/background/js/file_operation_manager.js b/chromium/chrome/browser/resources/file_manager/background/js/file_operation_manager.js
new file mode 100644
index 00000000000..79cbe1a1d59
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/background/js/file_operation_manager.js
@@ -0,0 +1,1400 @@
+// Copyright 2013 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.
+
+'use strict';
+
+/**
+ * Utilities for FileOperationManager.
+ */
+var fileOperationUtil = {};
+
+/**
+ * Simple wrapper for util.deduplicatePath. On error, this method translates
+ * the FileError to FileOperationManager.Error object.
+ *
+ * @param {DirectoryEntry} dirEntry The target directory entry.
+ * @param {string} relativePath The path to be deduplicated.
+ * @param {function(string)} successCallback Callback run with the deduplicated
+ * path on success.
+ * @param {function(FileOperationManager.Error)} errorCallback Callback run on
+ * error.
+ */
+fileOperationUtil.deduplicatePath = function(
+ dirEntry, relativePath, successCallback, errorCallback) {
+ util.deduplicatePath(
+ dirEntry, relativePath, successCallback,
+ function(err) {
+ var onFileSystemError = function(error) {
+ errorCallback(new FileOperationManager.Error(
+ util.FileOperationErrorType.FILESYSTEM_ERROR, error));
+ };
+
+ if (err.code == FileError.PATH_EXISTS_ERR) {
+ // Failed to uniquify the file path. There should be an existing
+ // entry, so return the error with it.
+ util.resolvePath(
+ dirEntry, relativePath,
+ function(entry) {
+ errorCallback(new FileOperationManager.Error(
+ util.FileOperationErrorType.TARGET_EXISTS, entry));
+ },
+ onFileSystemError);
+ return;
+ }
+ onFileSystemError(err);
+ });
+};
+
+/**
+ * Traverses files/subdirectories of the given entry, and returns them.
+ * In addition, this method annotate the size of each entry. The result will
+ * include the entry itself.
+ *
+ * @param {Entry} entry The root Entry for traversing.
+ * @param {function(Array.<Entry>)} successCallback Called when the traverse
+ * is successfully done with the array of the entries.
+ * @param {function(FileError)} errorCallback Called on error with the first
+ * occurred error (i.e. following errors will just be discarded).
+ */
+fileOperationUtil.resolveRecursively = function(
+ entry, successCallback, errorCallback) {
+ var result = [];
+ var error = null;
+ var numRunningTasks = 0;
+
+ var maybeInvokeCallback = function() {
+ // If there still remain some running tasks, wait their finishing.
+ if (numRunningTasks > 0)
+ return;
+
+ if (error)
+ errorCallback(error);
+ else
+ successCallback(result);
+ };
+
+ // The error handling can be shared.
+ var onError = function(fileError) {
+ // If this is the first error, remember it.
+ if (!error)
+ error = fileError;
+ --numRunningTasks;
+ maybeInvokeCallback();
+ };
+
+ var process = function(entry) {
+ numRunningTasks++;
+ result.push(entry);
+ if (entry.isDirectory) {
+ // The size of a directory is 1 bytes here, so that the progress bar
+ // will work smoother.
+ // TODO(hidehiko): Remove this hack.
+ entry.size = 1;
+
+ // Recursively traverse children.
+ var reader = entry.createReader();
+ reader.readEntries(
+ function processSubEntries(subEntries) {
+ if (error || subEntries.length == 0) {
+ // If an error is found already, or this is the completion
+ // callback, then finish the process.
+ --numRunningTasks;
+ maybeInvokeCallback();
+ return;
+ }
+
+ for (var i = 0; i < subEntries.length; i++)
+ process(subEntries[i]);
+
+ // Continue to read remaining children.
+ reader.readEntries(processSubEntries, onError);
+ },
+ onError);
+ } else {
+ // For a file, annotate the file size.
+ entry.getMetadata(function(metadata) {
+ entry.size = metadata.size;
+ --numRunningTasks;
+ maybeInvokeCallback();
+ }, onError);
+ }
+ };
+
+ process(entry);
+};
+
+/**
+ * Copies source to parent with the name newName recursively.
+ * This should work very similar to FileSystem API's copyTo. The difference is;
+ * - The progress callback is supported.
+ * - The cancellation is supported.
+ *
+ * @param {Entry} source The entry to be copied.
+ * @param {DirectoryEntry} parent The entry of the destination directory.
+ * @param {string} newName The name of copied file.
+ * @param {function(string, string)} entryChangedCallback
+ * Callback invoked when an entry is created with the source url and
+ * the destination url.
+ * @param {function(string, number)} progressCallback Callback invoked
+ * periodically during the copying. It takes the source url and the
+ * processed bytes of it.
+ * @param {function(string)} successCallback Callback invoked when the copy
+ * is successfully done with the url of the created entry.
+ * @param {function(FileError)} errorCallback Callback invoked when an error
+ * is found.
+ * @return {function()} Callback to cancel the current file copy operation.
+ * When the cancel is done, errorCallback will be called. The returned
+ * callback must not be called more than once.
+ */
+fileOperationUtil.copyTo = function(
+ source, parent, newName, entryChangedCallback, progressCallback,
+ successCallback, errorCallback) {
+ var copyId = null;
+ var pendingCallbacks = [];
+
+ var onCopyProgress = function(progressCopyId, status) {
+ if (copyId == null) {
+ // If the copyId is not yet available, wait for it.
+ pendingCallbacks.push(
+ onCopyProgress.bind(null, progressCopyId, status));
+ return;
+ }
+
+ // This is not what we're interested in.
+ if (progressCopyId != copyId)
+ return;
+
+ switch (status.type) {
+ case 'begin_copy_entry':
+ break;
+
+ case 'end_copy_entry':
+ entryChangedCallback(status.sourceUrl, status.destinationUrl);
+ break;
+
+ case 'progress':
+ progressCallback(status.sourceUrl, status.size);
+ break;
+
+ case 'success':
+ chrome.fileBrowserPrivate.onCopyProgress.removeListener(onCopyProgress);
+ successCallback(status.destinationUrl);
+ break;
+
+ case 'error':
+ chrome.fileBrowserPrivate.onCopyProgress.removeListener(onCopyProgress);
+ errorCallback(util.createFileError(status.error));
+ break;
+
+ default:
+ // Found unknown state. Cancel the task, and return an error.
+ console.error('Unknown progress type: ' + status.type);
+ chrome.fileBrowserPrivate.onCopyProgress.removeListener(onCopyProgress);
+ chrome.fileBrowserPrivate.cancelCopy(copyId);
+ errorCallback(util.createFileError(FileError.INVALID_STATE_ERR));
+ }
+ };
+
+ // Register the listener before calling startCopy. Otherwise some events
+ // would be lost.
+ chrome.fileBrowserPrivate.onCopyProgress.addListener(onCopyProgress);
+
+ // Then starts the copy.
+ chrome.fileBrowserPrivate.startCopy(
+ source.toURL(), parent.toURL(), newName, function(startCopyId) {
+ // last error contains the FileError code on error.
+ if (chrome.runtime.lastError) {
+ // Unsubscribe the progress listener.
+ chrome.fileBrowserPrivate.onCopyProgress.removeListener(
+ onCopyProgress);
+ errorCallback(util.createFileError(
+ Integer.parseInt(chrome.runtime.lastError, 10)));
+ return;
+ }
+
+ copyId = startCopyId;
+ for (var i = 0; i < pendingCallbacks.length; i++) {
+ pendingCallbacks[i]();
+ }
+ });
+
+ return function() {
+ // If copyId is not yet available, wait for it.
+ if (copyId == null) {
+ pendingCallbacks.push(function() {
+ chrome.fileBrowserPrivate.cancelCopy(copyId);
+ });
+ return;
+ }
+
+ chrome.fileBrowserPrivate.cancelCopy(copyId);
+ };
+};
+
+/**
+ * Thin wrapper of chrome.fileBrowserPrivate.zipSelection to adapt its
+ * interface similar to copyTo().
+ *
+ * @param {Array.<Entry>} sources The array of entries to be archived.
+ * @param {DirectoryEntry} parent The entry of the destination directory.
+ * @param {string} newName The name of the archive to be created.
+ * @param {function(FileEntry)} successCallback Callback invoked when the
+ * operation is successfully done with the entry of the created archive.
+ * @param {function(FileError)} errorCallback Callback invoked when an error
+ * is found.
+ */
+fileOperationUtil.zipSelection = function(
+ sources, parent, newName, successCallback, errorCallback) {
+ // TODO(mtomasz): Pass Entries instead of URLs. Entries can be converted to
+ // URLs in custom bindings.
+ chrome.fileBrowserPrivate.zipSelection(
+ parent.toURL(),
+ util.entriesToURLs(sources),
+ newName, function(success) {
+ if (!success) {
+ // Failed to create a zip archive.
+ errorCallback(
+ util.createFileError(FileError.INVALID_MODIFICATION_ERR));
+ return;
+ }
+
+ // Returns the created entry via callback.
+ parent.getFile(
+ newName, {create: false}, successCallback, errorCallback);
+ });
+};
+
+/**
+ * @constructor
+ */
+function FileOperationManager() {
+ this.copyTasks_ = [];
+ this.deleteTasks_ = [];
+ this.taskIdCounter_ = 0;
+
+ this.eventRouter_ = new FileOperationManager.EventRouter();
+
+ Object.seal(this);
+}
+
+/**
+ * Get FileOperationManager instance. In case is hasn't been initialized, a new
+ * instance is created.
+ *
+ * @return {FileOperationManager} A FileOperationManager instance.
+ */
+FileOperationManager.getInstance = function() {
+ if (!FileOperationManager.instance_)
+ FileOperationManager.instance_ = new FileOperationManager();
+
+ return FileOperationManager.instance_;
+};
+
+/**
+ * Manages Event dispatching.
+ * Currently this can send three types of events: "copy-progress",
+ * "copy-operation-completed" and "delete".
+ *
+ * TODO(hidehiko): Reorganize the event dispatching mechanism.
+ * @constructor
+ * @extends {cr.EventTarget}
+ */
+FileOperationManager.EventRouter = function() {
+};
+
+/**
+ * Extends cr.EventTarget.
+ */
+FileOperationManager.EventRouter.prototype.__proto__ = cr.EventTarget.prototype;
+
+/**
+ * Dispatches a simple "copy-progress" event with reason and current
+ * FileOperationManager status. If it is an ERROR event, error should be set.
+ *
+ * @param {string} reason Event type. One of "BEGIN", "PROGRESS", "SUCCESS",
+ * "ERROR" or "CANCELLED". TODO(hidehiko): Use enum.
+ * @param {Object} status Current FileOperationManager's status. See also
+ * FileOperationManager.getStatus().
+ * @param {string} taskId ID of task related with the event.
+ * @param {FileOperationManager.Error=} opt_error The info for the error. This
+ * should be set iff the reason is "ERROR".
+ */
+FileOperationManager.EventRouter.prototype.sendProgressEvent = function(
+ reason, status, taskId, opt_error) {
+ var event = new Event('copy-progress');
+ event.reason = reason;
+ event.status = status;
+ event.taskId = taskId;
+ if (opt_error)
+ event.error = opt_error;
+ this.dispatchEvent(event);
+};
+
+/**
+ * Dispatches an event to notify that an entry is changed (created or deleted).
+ * @param {util.EntryChangedKind} kind The enum to represent if the entry is
+ * created or deleted.
+ * @param {Entry} entry The changed entry.
+ */
+FileOperationManager.EventRouter.prototype.sendEntryChangedEvent = function(
+ kind, entry) {
+ var event = new Event('entry-changed');
+ event.kind = kind;
+ event.entry = entry;
+ this.dispatchEvent(event);
+};
+
+/**
+ * Dispatches an event to notify entries are changed for delete task.
+ *
+ * @param {string} reason Event type. One of "BEGIN", "PROGRESS", "SUCCESS",
+ * or "ERROR". TODO(hidehiko): Use enum.
+ * @param {DeleteTask} task Delete task related with the event.
+ */
+FileOperationManager.EventRouter.prototype.sendDeleteEvent = function(
+ reason, task) {
+ var event = new Event('delete');
+ event.reason = reason;
+ event.taskId = task.taskId;
+ event.entries = task.entries;
+ event.totalBytes = task.totalBytes;
+ event.processedBytes = task.processedBytes;
+ // TODO(hirono): Remove the urls property from the event.
+ event.urls = util.entriesToURLs(task.entries);
+ this.dispatchEvent(event);
+};
+
+/**
+ * A record of a queued copy operation.
+ *
+ * Multiple copy operations may be queued at any given time. Additional
+ * Tasks may be added while the queue is being serviced. Though a
+ * cancel operation cancels everything in the queue.
+ *
+ * @param {util.FileOperationType} operationType The type of this operation.
+ * @param {Array.<Entry>} sourceEntries Array of source entries.
+ * @param {DirectoryEntry} targetDirEntry Target directory.
+ * @constructor
+ */
+FileOperationManager.Task = function(
+ operationType, sourceEntries, targetDirEntry) {
+ this.operationType = operationType;
+ this.sourceEntries = sourceEntries;
+ this.targetDirEntry = targetDirEntry;
+
+ /**
+ * An array of map from url to Entry being processed.
+ * @type {Array.<Object<string, Entry>>}
+ */
+ this.processingEntries = null;
+
+ /**
+ * Total number of bytes to be processed. Filled in initialize().
+ * @type {number}
+ */
+ this.totalBytes = 0;
+
+ /**
+ * Total number of already processed bytes. Updated periodically.
+ * @type {number}
+ */
+ this.processedBytes = 0;
+
+ this.deleteAfterCopy = false;
+
+ /**
+ * Set to true when cancel is requested.
+ * @private {boolean}
+ */
+ this.cancelRequested_ = false;
+
+ /**
+ * Callback to cancel the running process.
+ * @private {function()}
+ */
+ this.cancelCallback_ = null;
+
+ // TODO(hidehiko): After we support recursive copy, we don't need this.
+ // If directory already exists, we try to make a copy named 'dir (X)',
+ // where X is a number. When we do this, all subsequent copies from
+ // inside the subtree should be mapped to the new directory name.
+ // For example, if 'dir' was copied as 'dir (1)', then 'dir\file.txt' should
+ // become 'dir (1)\file.txt'.
+ this.renamedDirectories_ = [];
+};
+
+/**
+ * @param {function()} callback When entries resolved.
+ */
+FileOperationManager.Task.prototype.initialize = function(callback) {
+};
+
+/**
+ * Updates copy progress status for the entry.
+ *
+ * @param {number} size Number of bytes that has been copied since last update.
+ */
+FileOperationManager.Task.prototype.updateFileCopyProgress = function(size) {
+ this.completedBytes += size;
+};
+
+/**
+ * Requests cancellation of this task.
+ * When the cancellation is done, it is notified via callbacks of run().
+ */
+FileOperationManager.Task.prototype.requestCancel = function() {
+ this.cancelRequested_ = true;
+ if (this.cancelCallback_) {
+ this.cancelCallback_();
+ this.cancelCallback_ = null;
+ }
+};
+
+/**
+ * Runs the task. Sub classes must implement this method.
+ *
+ * @param {function(util.EntryChangedKind, Entry)} entryChangedCallback
+ * Callback invoked when an entry is changed.
+ * @param {function()} progressCallback Callback invoked periodically during
+ * the operation.
+ * @param {function()} successCallback Callback run on success.
+ * @param {function(FileOperationManager.Error)} errorCallback Callback run on
+ * error.
+ */
+FileOperationManager.Task.prototype.run = function(
+ entryChangedCallback, progressCallback, successCallback, errorCallback) {
+};
+
+/**
+ * Get states of the task.
+ * TOOD(hirono): Removes this method and sets a task to progress events.
+ * @return {object} Status object.
+ */
+FileOperationManager.Task.prototype.getStatus = function() {
+ var numRemainingItems = this.countRemainingItems();
+ return {
+ operationType: this.operationType,
+ numRemainingItems: numRemainingItems,
+ totalBytes: this.totalBytes,
+ processedBytes: this.processedBytes,
+ processingEntry: this.getSingleEntry()
+ };
+};
+
+/**
+ * Counts the number of remaining items.
+ * @return {number} Number of remaining items.
+ */
+FileOperationManager.Task.prototype.countRemainingItems = function() {
+ var count = 0;
+ for (var i = 0; i < this.processingEntries.length; i++) {
+ for (var url in this.processingEntries[i]) {
+ count++;
+ }
+ }
+ return count;
+};
+
+/**
+ * Obtains the single processing entry. If there are multiple processing
+ * entries, it returns null.
+ * @return {Entry} First entry.
+ */
+FileOperationManager.Task.prototype.getSingleEntry = function() {
+ if (this.countRemainingItems() !== 1)
+ return null;
+ for (var i = 0; i < this.processingEntries.length; i++) {
+ var entryMap = this.processingEntries[i];
+ for (var name in entryMap)
+ return entryMap[name];
+ }
+ return null;
+};
+
+/**
+ * Task to copy entries.
+ *
+ * @param {Array.<Entry>} sourceEntries Array of source entries.
+ * @param {DirectoryEntry} targetDirEntry Target directory.
+ * @constructor
+ * @extends {FileOperationManager.Task}
+ */
+FileOperationManager.CopyTask = function(sourceEntries, targetDirEntry) {
+ FileOperationManager.Task.call(
+ this, util.FileOperationType.COPY, sourceEntries, targetDirEntry);
+};
+
+/**
+ * Extends FileOperationManager.Task.
+ */
+FileOperationManager.CopyTask.prototype.__proto__ =
+ FileOperationManager.Task.prototype;
+
+/**
+ * Initializes the CopyTask.
+ * @param {function()} callback Called when the initialize is completed.
+ */
+FileOperationManager.CopyTask.prototype.initialize = function(callback) {
+ var group = new AsyncUtil.Group();
+ // Correct all entries to be copied for status update.
+ this.processingEntries = [];
+ for (var i = 0; i < this.sourceEntries.length; i++) {
+ group.add(function(index, callback) {
+ fileOperationUtil.resolveRecursively(
+ this.sourceEntries[index],
+ function(resolvedEntries) {
+ var resolvedEntryMap = {};
+ for (var j = 0; j < resolvedEntries.length; ++j) {
+ var entry = resolvedEntries[j];
+ entry.processedBytes = 0;
+ resolvedEntryMap[entry.toURL()] = entry;
+ }
+ this.processingEntries[index] = resolvedEntryMap;
+ callback();
+ }.bind(this),
+ function(error) {
+ console.error(
+ 'Failed to resolve for copy: %s',
+ util.getFileErrorMnemonic(error.code));
+ });
+ }.bind(this, i));
+ }
+
+ group.run(function() {
+ // Fill totalBytes.
+ this.totalBytes = 0;
+ for (var i = 0; i < this.processingEntries.length; i++) {
+ for (var url in this.processingEntries[i])
+ this.totalBytes += this.processingEntries[i][url].size;
+ }
+
+ callback();
+ }.bind(this));
+};
+
+/**
+ * Copies all entries to the target directory.
+ * Note: this method contains also the operation of "Move" due to historical
+ * reason.
+ *
+ * @param {function(util.EntryChangedKind, Entry)} entryChangedCallback
+ * Callback invoked when an entry is changed.
+ * @param {function()} progressCallback Callback invoked periodically during
+ * the copying.
+ * @param {function()} successCallback On success.
+ * @param {function(FileOperationManager.Error)} errorCallback On error.
+ * @override
+ */
+FileOperationManager.CopyTask.prototype.run = function(
+ entryChangedCallback, progressCallback, successCallback, errorCallback) {
+ // TODO(hidehiko): We should be able to share the code to iterate on entries
+ // with serviceMoveTask_().
+ if (this.sourceEntries.length == 0) {
+ successCallback();
+ return;
+ }
+
+ // TODO(hidehiko): Delete after copy is the implementation of Move.
+ // Migrate the part into MoveTask.run().
+ var deleteOriginals = function() {
+ var count = this.sourceEntries.length;
+
+ var onEntryDeleted = function(entry) {
+ entryChangedCallback(util.EntryChangedKind.DELETED, entry);
+ count--;
+ if (!count)
+ successCallback();
+ };
+
+ var onFilesystemError = function(err) {
+ errorCallback(new FileOperationManager.Error(
+ util.FileOperationErrorType.FILESYSTEM_ERROR, err));
+ };
+
+ for (var i = 0; i < this.sourceEntries.length; i++) {
+ var entry = this.sourceEntries[i];
+ util.removeFileOrDirectory(
+ entry, onEntryDeleted.bind(null, entry), onFilesystemError);
+ }
+ }.bind(this);
+
+ AsyncUtil.forEach(
+ this.sourceEntries,
+ function(callback, entry, index) {
+ if (this.cancelRequested_) {
+ errorCallback(new FileOperationManager.Error(
+ util.FileOperationErrorType.FILESYSTEM_ERROR,
+ util.createFileError(FileError.ABORT_ERR)));
+ return;
+ }
+ progressCallback();
+ this.processEntry_(
+ entry, this.targetDirEntry,
+ function(sourceUrl, destinationUrl) {
+ // Finalize the entry's progress state.
+ var entry = this.processingEntries[index][sourceUrl];
+ if (entry) {
+ this.processedBytes += entry.size - entry.processedBytes;
+ progressCallback();
+ delete this.processingEntries[index][sourceUrl];
+ }
+
+ webkitResolveLocalFileSystemURL(
+ destinationUrl, function(destinationEntry) {
+ entryChangedCallback(
+ util.EntryChangedKind.CREATED, destinationEntry);
+ });
+ }.bind(this),
+ function(source_url, size) {
+ var entry = this.processingEntries[index][source_url];
+ if (entry) {
+ this.processedBytes += size - entry.processedBytes;
+ entry.processedBytes = size;
+ progressCallback();
+ }
+ }.bind(this),
+ callback,
+ errorCallback);
+ },
+ function() {
+ if (this.deleteAfterCopy) {
+ deleteOriginals();
+ } else {
+ successCallback();
+ }
+ }.bind(this),
+ this);
+};
+
+/**
+ * Copies the source entry to the target directory.
+ *
+ * @param {Entry} sourceEntry An entry to be copied.
+ * @param {DirectoryEntry} destinationEntry The entry which will contain the
+ * copied entry.
+ * @param {function(string, string)} entryChangedCallback
+ * Callback invoked when an entry is created with the source url and
+ * the destination url.
+ * @param {function(string, number)} progressCallback Callback invoked
+ * periodically during the copying.
+ * @param {function()} successCallback On success.
+ * @param {function(FileOperationManager.Error)} errorCallback On error.
+ * @private
+ */
+FileOperationManager.CopyTask.prototype.processEntry_ = function(
+ sourceEntry, destinationEntry, entryChangedCallback, progressCallback,
+ successCallback, errorCallback) {
+ fileOperationUtil.deduplicatePath(
+ destinationEntry, sourceEntry.name,
+ function(destinationName) {
+ if (this.cancelRequested_) {
+ errorCallback(new FileOperationManager.Error(
+ util.FileOperationErrorType.FILESYSTEM_ERROR,
+ util.createFileError(FileError.ABORT_ERR)));
+ return;
+ }
+ this.cancelCallback_ = fileOperationUtil.copyTo(
+ sourceEntry, destinationEntry, destinationName,
+ entryChangedCallback, progressCallback,
+ function(entry) {
+ this.cancelCallback_ = null;
+ successCallback();
+ }.bind(this),
+ function(error) {
+ this.cancelCallback_ = null;
+ errorCallback(new FileOperationManager.Error(
+ util.FileOperationErrorType.FILESYSTEM_ERROR, error));
+ }.bind(this));
+ }.bind(this),
+ errorCallback);
+};
+
+/**
+ * Task to move entries.
+ *
+ * @param {Array.<Entry>} sourceEntries Array of source entries.
+ * @param {DirectoryEntry} targetDirEntry Target directory.
+ * @constructor
+ * @extends {FileOperationManager.Task}
+ */
+FileOperationManager.MoveTask = function(sourceEntries, targetDirEntry) {
+ FileOperationManager.Task.call(
+ this, util.FileOperationType.MOVE, sourceEntries, targetDirEntry);
+};
+
+/**
+ * Extends FileOperationManager.Task.
+ */
+FileOperationManager.MoveTask.prototype.__proto__ =
+ FileOperationManager.Task.prototype;
+
+/**
+ * Initializes the MoveTask.
+ * @param {function()} callback Called when the initialize is completed.
+ */
+FileOperationManager.MoveTask.prototype.initialize = function(callback) {
+ // This may be moving from search results, where it fails if we
+ // move parent entries earlier than child entries. We should
+ // process the deepest entry first. Since move of each entry is
+ // done by a single moveTo() call, we don't need to care about the
+ // recursive traversal order.
+ this.sourceEntries.sort(function(entry1, entry2) {
+ return entry2.fullPath.length - entry1.fullPath.length;
+ });
+
+ this.processingEntries = [];
+ for (var i = 0; i < this.sourceEntries.length; i++) {
+ var processingEntryMap = {};
+ var entry = this.sourceEntries[i];
+
+ // The move should be done with updating the metadata. So here we assume
+ // all the file size is 1 byte. (Avoiding 0, so that progress bar can
+ // move smoothly).
+ // TODO(hidehiko): Remove this hack.
+ entry.size = 1;
+ processingEntryMap[entry.toURL()] = entry;
+ this.processingEntries[i] = processingEntryMap;
+ }
+
+ callback();
+};
+
+/**
+ * Moves all entries in the task.
+ *
+ * @param {function(util.EntryChangedKind, Entry)} entryChangedCallback
+ * Callback invoked when an entry is changed.
+ * @param {function()} progressCallback Callback invoked periodically during
+ * the moving.
+ * @param {function()} successCallback On success.
+ * @param {function(FileOperationManager.Error)} errorCallback On error.
+ * @override
+ */
+FileOperationManager.MoveTask.prototype.run = function(
+ entryChangedCallback, progressCallback, successCallback, errorCallback) {
+ if (this.sourceEntries.length == 0) {
+ successCallback();
+ return;
+ }
+
+ AsyncUtil.forEach(
+ this.sourceEntries,
+ function(callback, entry, index) {
+ if (this.cancelRequested_) {
+ errorCallback(new FileOperationManager.Error(
+ util.FileOperationErrorType.FILESYSTEM_ERROR,
+ util.createFileError(FileError.ABORT_ERR)));
+ return;
+ }
+ progressCallback();
+ FileOperationManager.MoveTask.processEntry_(
+ entry, this.targetDirEntry, entryChangedCallback,
+ function() {
+ // Erase the processing entry.
+ this.processingEntries[index] = {};
+ this.processedBytes++;
+ callback();
+ }.bind(this),
+ errorCallback);
+ },
+ function() {
+ successCallback();
+ }.bind(this),
+ this);
+};
+
+/**
+ * Moves the sourceEntry to the targetDirEntry in this task.
+ *
+ * @param {Entry} sourceEntry An entry to be moved.
+ * @param {DirectoryEntry} destinationEntry The entry of the destination
+ * directory.
+ * @param {function(util.EntryChangedKind, Entry)} entryChangedCallback
+ * Callback invoked when an entry is changed.
+ * @param {function()} successCallback On success.
+ * @param {function(FileOperationManager.Error)} errorCallback On error.
+ * @private
+ */
+FileOperationManager.MoveTask.processEntry_ = function(
+ sourceEntry, destinationEntry, entryChangedCallback, successCallback,
+ errorCallback) {
+ fileOperationUtil.deduplicatePath(
+ destinationEntry,
+ sourceEntry.name,
+ function(destinationName) {
+ sourceEntry.moveTo(
+ destinationEntry, destinationName,
+ function(movedEntry) {
+ entryChangedCallback(util.EntryChangedKind.CREATED, movedEntry);
+ entryChangedCallback(util.EntryChangedKind.DELETED, sourceEntry);
+ successCallback();
+ },
+ function(error) {
+ errorCallback(new FileOperationManager.Error(
+ util.FileOperationErrorType.FILESYSTEM_ERROR, error));
+ });
+ },
+ errorCallback);
+};
+
+/**
+ * Task to create a zip archive.
+ *
+ * @param {Array.<Entry>} sourceEntries Array of source entries.
+ * @param {DirectoryEntry} targetDirEntry Target directory.
+ * @param {DirectoryEntry} zipBaseDirEntry Base directory dealt as a root
+ * in ZIP archive.
+ * @constructor
+ * @extends {FileOperationManager.Task}
+ */
+FileOperationManager.ZipTask = function(
+ sourceEntries, targetDirEntry, zipBaseDirEntry) {
+ FileOperationManager.Task.call(
+ this, util.FileOperationType.ZIP, sourceEntries, targetDirEntry);
+ this.zipBaseDirEntry = zipBaseDirEntry;
+};
+
+/**
+ * Extends FileOperationManager.Task.
+ */
+FileOperationManager.ZipTask.prototype.__proto__ =
+ FileOperationManager.Task.prototype;
+
+
+/**
+ * Initializes the ZipTask.
+ * @param {function()} callback Called when the initialize is completed.
+ */
+FileOperationManager.ZipTask.prototype.initialize = function(callback) {
+ var resolvedEntryMap = {};
+ var group = new AsyncUtil.Group();
+ for (var i = 0; i < this.sourceEntries.length; i++) {
+ group.add(function(index, callback) {
+ fileOperationUtil.resolveRecursively(
+ this.sourceEntries[index],
+ function(entries) {
+ for (var j = 0; j < entries.length; j++)
+ resolvedEntryMap[entries[j].toURL()] = entries[j];
+ callback();
+ },
+ function(error) {});
+ }.bind(this, i));
+ }
+
+ group.run(function() {
+ // For zip archiving, all the entries are processed at once.
+ this.processingEntries = [resolvedEntryMap];
+
+ this.totalBytes = 0;
+ for (var url in resolvedEntryMap)
+ this.totalBytes += resolvedEntryMap[url].size;
+
+ callback();
+ }.bind(this));
+};
+
+/**
+ * Runs a zip file creation task.
+ *
+ * @param {function(util.EntryChangedKind, Entry)} entryChangedCallback
+ * Callback invoked when an entry is changed.
+ * @param {function()} progressCallback Callback invoked periodically during
+ * the moving.
+ * @param {function()} successCallback On complete.
+ * @param {function(FileOperationManager.Error)} errorCallback On error.
+ * @override
+ */
+FileOperationManager.ZipTask.prototype.run = function(
+ entryChangedCallback, progressCallback, successCallback, errorCallback) {
+ // TODO(hidehiko): we should localize the name.
+ var destName = 'Archive';
+ if (this.sourceEntries.length == 1) {
+ var entryPath = this.sourceEntries[0].fullPath;
+ var i = entryPath.lastIndexOf('/');
+ var basename = (i < 0) ? entryPath : entryPath.substr(i + 1);
+ i = basename.lastIndexOf('.');
+ destName = ((i < 0) ? basename : basename.substr(0, i));
+ }
+
+ fileOperationUtil.deduplicatePath(
+ this.targetDirEntry, destName + '.zip',
+ function(destPath) {
+ // TODO: per-entry zip progress update with accurate byte count.
+ // For now just set completedBytes to same value as totalBytes so
+ // that the progress bar is full.
+ this.processedBytes = this.totalBytes;
+ progressCallback();
+
+ // The number of elements in processingEntries is 1. See also
+ // initialize().
+ var entries = [];
+ for (var url in this.processingEntries[0])
+ entries.push(this.processingEntries[0][url]);
+
+ fileOperationUtil.zipSelection(
+ entries,
+ this.zipBaseDirEntry,
+ destPath,
+ function(entry) {
+ entryChangedCallback(util.EntryChangedKind.CREATE, entry);
+ successCallback();
+ },
+ function(error) {
+ errorCallback(new FileOperationManager.Error(
+ util.FileOperationErrorType.FILESYSTEM_ERROR, error));
+ });
+ }.bind(this),
+ errorCallback);
+};
+
+/**
+ * Error class used to report problems with a copy operation.
+ * If the code is UNEXPECTED_SOURCE_FILE, data should be a path of the file.
+ * If the code is TARGET_EXISTS, data should be the existing Entry.
+ * If the code is FILESYSTEM_ERROR, data should be the FileError.
+ *
+ * @param {util.FileOperationErrorType} code Error type.
+ * @param {string|Entry|FileError} data Additional data.
+ * @constructor
+ */
+FileOperationManager.Error = function(code, data) {
+ this.code = code;
+ this.data = data;
+};
+
+// FileOperationManager methods.
+
+/**
+ * @return {Object} Status object.
+ */
+FileOperationManager.prototype.getStatus = function() {
+ // TODO(hidehiko): Reorganize the structure when delete queue is merged
+ // into copy task queue.
+ var result = {
+ // Set to util.FileOperationType if all the running/pending tasks is
+ // the same kind of task.
+ operationType: null,
+
+ // The number of entries to be processed.
+ numRemainingItems: 0,
+
+ // The total number of bytes to be processed.
+ totalBytes: 0,
+
+ // The number of bytes.
+ processedBytes: 0,
+
+ // Available if numRemainingItems == 1. Pointing to an Entry which is
+ // begin processed.
+ processingEntry: task.getSingleEntry()
+ };
+
+ var operationType =
+ this.copyTasks_.length > 0 ? this.copyTasks_[0].operationType : null;
+ var task = null;
+ for (var i = 0; i < this.copyTasks_.length; i++) {
+ task = this.copyTasks_[i];
+ if (task.operationType != operationType)
+ operationType = null;
+
+ // Assuming the number of entries is small enough, count every time.
+ result.numRemainingItems += task.countRemainingItems();
+ result.totalBytes += task.totalBytes;
+ result.processedBytes += task.processedBytes;
+ }
+
+ result.operationType = operationType;
+ return result;
+};
+
+/**
+ * Adds an event listener for the tasks.
+ * @param {string} type The name of the event.
+ * @param {function(Event)} handler The handler for the event.
+ * This is called when the event is dispatched.
+ */
+FileOperationManager.prototype.addEventListener = function(type, handler) {
+ this.eventRouter_.addEventListener(type, handler);
+};
+
+/**
+ * Removes an event listener for the tasks.
+ * @param {string} type The name of the event.
+ * @param {function(Event)} handler The handler to be removed.
+ */
+FileOperationManager.prototype.removeEventListener = function(type, handler) {
+ this.eventRouter_.removeEventListener(type, handler);
+};
+
+/**
+ * Says if there are any tasks in the queue.
+ * @return {boolean} True, if there are any tasks.
+ */
+FileOperationManager.prototype.hasQueuedTasks = function() {
+ return this.copyTasks_.length > 0 || this.deleteTasks_.length > 0;
+};
+
+/**
+ * Completely clear out the copy queue, either because we encountered an error
+ * or completed successfully.
+ *
+ * @private
+ */
+FileOperationManager.prototype.resetQueue_ = function() {
+ this.copyTasks_ = [];
+};
+
+/**
+ * Requests the specified task to be canceled.
+ * @param {string} taskId ID of task to be canceled.
+ */
+FileOperationManager.prototype.requestTaskCancel = function(taskId) {
+ var task = null;
+ for (var i = 0; i < this.copyTasks_.length; i++) {
+ task = this.copyTasks_[i];
+ if (task.taskId !== taskId)
+ continue;
+ task.requestCancel();
+ // If the task is not on progress, remove it immediately.
+ if (i !== 0) {
+ this.eventRouter_.sendProgressEvent('CANCELED',
+ task.getStatus(),
+ task.taskId);
+ this.copyTasks_.splice(i, 1);
+ }
+ }
+ for (var i = 0; i < this.deleteTasks_.length; i++) {
+ task = this.deleteTasks_[i];
+ if (task.taskId !== taskId)
+ continue;
+ task.cancelRequested = true;
+ // If the task is not on progress, remove it immediately.
+ if (i !== 0) {
+ this.eventRouter_.sendDeleteEvent('CANCELED', task);
+ this.deleteTasks_.splice(i, 1);
+ }
+ }
+};
+
+/**
+ * Kick off pasting.
+ *
+ * @param {Array.<string>} sourcePaths Path of the source files.
+ * @param {string} targetPath The destination path of the target directory.
+ * @param {boolean} isMove True if the operation is "move", otherwise (i.e.
+ * if the operation is "copy") false.
+ */
+FileOperationManager.prototype.paste = function(
+ sourcePaths, targetPath, isMove) {
+ // Do nothing if sourcePaths is empty.
+ if (sourcePaths.length == 0)
+ return;
+
+ var errorCallback = function(error) {
+ this.eventRouter_.sendProgressEvent(
+ 'ERROR',
+ this.getStatus(),
+ this.generateTaskId_(null),
+ new FileOperationManager.Error(
+ util.FileOperationErrorType.FILESYSTEM_ERROR, error));
+ }.bind(this);
+
+ var targetEntry = null;
+ var entries = [];
+
+ // Resolve paths to entries.
+ var resolveGroup = new AsyncUtil.Group();
+ resolveGroup.add(function(callback) {
+ webkitResolveLocalFileSystemURL(
+ util.makeFilesystemUrl(targetPath),
+ function(entry) {
+ if (!entry.isDirectory) {
+ // Found a non directory entry.
+ errorCallback(util.createFileError(FileError.TYPE_MISMATCH_ERR));
+ return;
+ }
+
+ targetEntry = entry;
+ callback();
+ },
+ errorCallback);
+ });
+
+ for (var i = 0; i < sourcePaths.length; i++) {
+ resolveGroup.add(function(sourcePath, callback) {
+ webkitResolveLocalFileSystemURL(
+ util.makeFilesystemUrl(sourcePath),
+ function(entry) {
+ entries.push(entry);
+ callback();
+ },
+ errorCallback);
+ }.bind(this, sourcePaths[i]));
+ }
+
+ resolveGroup.run(function() {
+ if (isMove) {
+ // Moving to the same directory is a redundant operation.
+ entries = entries.filter(function(entry) {
+ return targetEntry.fullPath + '/' + entry.name != entry.fullPath;
+ });
+
+ // Do nothing, if we have no entries to be moved.
+ if (entries.length == 0)
+ return;
+ }
+
+ this.queueCopy_(targetEntry, entries, isMove);
+ }.bind(this));
+};
+
+/**
+ * Checks if the move operation is available between the given two locations.
+ *
+ * @param {DirectoryEntry} sourceEntry An entry from the source.
+ * @param {DirectoryEntry} targetDirEntry Directory entry for the target.
+ * @return {boolean} Whether we can move from the source to the target.
+ */
+FileOperationManager.prototype.isMovable = function(sourceEntry,
+ targetDirEntry) {
+ return (PathUtil.isDriveBasedPath(sourceEntry.fullPath) &&
+ PathUtil.isDriveBasedPath(targetDirEntry.fullPath)) ||
+ (PathUtil.getRootPath(sourceEntry.fullPath) ==
+ PathUtil.getRootPath(targetDirEntry.fullPath));
+};
+
+/**
+ * Initiate a file copy.
+ *
+ * @param {DirectoryEntry} targetDirEntry Target directory.
+ * @param {Array.<Entry>} entries Entries to copy.
+ * @param {boolean} isMove In case of move.
+ * @return {FileOperationManager.Task} Copy task.
+ * @private
+ */
+FileOperationManager.prototype.queueCopy_ = function(
+ targetDirEntry, entries, isMove) {
+ // When copying files, null can be specified as source directory.
+ var task;
+ if (isMove) {
+ if (this.isMovable(entries[0], targetDirEntry)) {
+ task = new FileOperationManager.MoveTask(entries, targetDirEntry);
+ } else {
+ task = new FileOperationManager.CopyTask(entries, targetDirEntry);
+ task.deleteAfterCopy = true;
+ }
+ } else {
+ task = new FileOperationManager.CopyTask(entries, targetDirEntry);
+ }
+
+ task.taskId = this.generateTaskId_();
+ task.initialize(function() {
+ this.copyTasks_.push(task);
+ this.eventRouter_.sendProgressEvent('BEGIN', task.getStatus(), task.taskId);
+ if (this.copyTasks_.length == 1)
+ this.serviceAllTasks_();
+ }.bind(this));
+
+ return task;
+};
+
+/**
+ * Service all pending tasks, as well as any that might appear during the
+ * copy.
+ *
+ * @private
+ */
+FileOperationManager.prototype.serviceAllTasks_ = function() {
+ if (!this.copyTasks_.length) {
+ // All tasks have been serviced, clean up and exit.
+ chrome.power.releaseKeepAwake();
+ this.resetQueue_();
+ return;
+ }
+
+ // Prevent the system from sleeping while copy is in progress.
+ chrome.power.requestKeepAwake('system');
+
+ var onTaskProgress = function() {
+ this.eventRouter_.sendProgressEvent('PROGRESS',
+ this.copyTasks_[0].getStatus(),
+ this.copyTasks_[0].taskId);
+ }.bind(this);
+
+ var onEntryChanged = function(kind, entry) {
+ this.eventRouter_.sendEntryChangedEvent(kind, entry);
+ }.bind(this);
+
+ var onTaskError = function(err) {
+ var task = this.copyTasks_.shift();
+ var reason = err.data.code === FileError.ABORT_ERR ? 'CANCELED' : 'ERROR';
+ this.eventRouter_.sendProgressEvent(reason,
+ task.getStatus(),
+ task.taskId,
+ err);
+ this.serviceAllTasks_();
+ }.bind(this);
+
+ var onTaskSuccess = function() {
+ // The task at the front of the queue is completed. Pop it from the queue.
+ var task = this.copyTasks_.shift();
+ this.eventRouter_.sendProgressEvent('SUCCESS',
+ task.getStatus(),
+ task.taskId);
+ this.serviceAllTasks_();
+ }.bind(this);
+
+ var nextTask = this.copyTasks_[0];
+ this.eventRouter_.sendProgressEvent('PROGRESS',
+ nextTask.getStatus(),
+ nextTask.taskId);
+ nextTask.run(onEntryChanged, onTaskProgress, onTaskSuccess, onTaskError);
+};
+
+/**
+ * Timeout before files are really deleted (to allow undo).
+ */
+FileOperationManager.DELETE_TIMEOUT = 30 * 1000;
+
+/**
+ * Schedules the files deletion.
+ *
+ * @param {Array.<Entry>} entries The entries.
+ */
+FileOperationManager.prototype.deleteEntries = function(entries) {
+ // TODO(hirono): Make FileOperationManager.DeleteTask.
+ var task = Object.seal({
+ entries: entries,
+ taskId: this.generateTaskId_(),
+ entrySize: {},
+ totalBytes: 0,
+ processedBytes: 0,
+ cancelRequested: false
+ });
+
+ // Obtains entry size and sum them up.
+ var group = new AsyncUtil.Group();
+ for (var i = 0; i < task.entries.length; i++) {
+ group.add(function(entry, callback) {
+ entry.getMetadata(function(metadata) {
+ var index = task.entries.indexOf(entries);
+ task.entrySize[entry.toURL()] = metadata.size;
+ task.totalBytes += metadata.size;
+ callback();
+ }, function() {
+ // Fail to obtain the metadata. Use fake value 1.
+ task.entrySize[entry.toURL()] = 1;
+ task.totalBytes += 1;
+ callback();
+ });
+ }.bind(this, task.entries[i]));
+ }
+
+ // Add a delete task.
+ group.run(function() {
+ this.deleteTasks_.push(task);
+ this.eventRouter_.sendDeleteEvent('BEGIN', task);
+ if (this.deleteTasks_.length === 1)
+ this.serviceAllDeleteTasks_();
+ }.bind(this));
+};
+
+/**
+ * Service all pending delete tasks, as well as any that might appear during the
+ * deletion.
+ *
+ * Must not be called if there is an in-flight delete task.
+ *
+ * @private
+ */
+FileOperationManager.prototype.serviceAllDeleteTasks_ = function() {
+ this.serviceDeleteTask_(
+ this.deleteTasks_[0],
+ function() {
+ this.deleteTasks_.shift();
+ if (this.deleteTasks_.length)
+ this.serviceAllDeleteTasks_();
+ }.bind(this));
+};
+
+/**
+ * Performs the deletion.
+ *
+ * @param {Object} task The delete task (see deleteEntries function).
+ * @param {function()} callback Callback run on task end.
+ * @private
+ */
+FileOperationManager.prototype.serviceDeleteTask_ = function(task, callback) {
+ var queue = new AsyncUtil.Queue();
+
+ // Delete each entry.
+ var error = null;
+ var deleteOneEntry = function(inCallback) {
+ if (!task.entries.length || task.cancelRequested || error) {
+ inCallback();
+ return;
+ }
+ this.eventRouter_.sendDeleteEvent('PROGRESS', task);
+ util.removeFileOrDirectory(
+ task.entries[0],
+ function() {
+ this.eventRouter_.sendEntryChangedEvent(
+ util.EntryChangedKind.DELETED, task.entries[0]);
+ task.processedBytes += task.entrySize[task.entries[0].toURL()];
+ task.entries.shift();
+ deleteOneEntry(inCallback);
+ }.bind(this),
+ function(inError) {
+ error = inError;
+ inCallback();
+ }.bind(this));
+ }.bind(this);
+ queue.run(deleteOneEntry);
+
+ // Send an event and finish the async steps.
+ queue.run(function(inCallback) {
+ var reason;
+ if (error)
+ reason = 'ERROR';
+ else if (task.cancelRequested)
+ reason = 'CANCELED';
+ else
+ reason = 'SUCCESS';
+ this.eventRouter_.sendDeleteEvent(reason, task);
+ inCallback();
+ callback();
+ }.bind(this));
+};
+
+/**
+ * Creates a zip file for the selection of files.
+ *
+ * @param {Entry} dirEntry The directory containing the selection.
+ * @param {Array.<Entry>} selectionEntries The selected entries.
+ */
+FileOperationManager.prototype.zipSelection = function(
+ dirEntry, selectionEntries) {
+ var zipTask = new FileOperationManager.ZipTask(
+ selectionEntries, dirEntry, dirEntry);
+ zipTask.taskId = this.generateTaskId_(this.copyTasks_);
+ zipTask.zip = true;
+ zipTask.initialize(function() {
+ this.copyTasks_.push(zipTask);
+ this.eventRouter_.sendProgressEvent('BEGIN',
+ zipTask.getStatus(),
+ zipTask.taskId);
+ if (this.copyTasks_.length == 1)
+ this.serviceAllTasks_();
+ }.bind(this));
+};
+
+/**
+ * Generates new task ID.
+ *
+ * @return {string} New task ID.
+ * @private
+ */
+FileOperationManager.prototype.generateTaskId_ = function() {
+ return 'file-operation-' + this.taskIdCounter_++;
+};
diff --git a/chromium/chrome/browser/resources/file_manager/background/js/progress_center.js b/chromium/chrome/browser/resources/file_manager/background/js/progress_center.js
new file mode 100644
index 00000000000..b7d20cec316
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/background/js/progress_center.js
@@ -0,0 +1,452 @@
+// Copyright 2013 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.
+
+'use strict';
+
+/**
+ * Progress center at the background page.
+ * @constructor
+ */
+var ProgressCenter = function() {
+ /**
+ * Current items managed by the progress center.
+ * @type {Array.<ProgressItem>}
+ * @private
+ */
+ this.items_ = [];
+
+ /**
+ * Map of progress ID and notification ID.
+ * @type {Object.<string, string>}
+ * @private
+ */
+ this.notifications_ = new ProgressCenter.Notifications_(
+ this.requestCancel.bind(this));
+
+ /**
+ * List of panel UI managed by the progress center.
+ * @type {Array.<ProgressCenterPanel>}
+ * @private
+ */
+ this.panels_ = [];
+
+ /**
+ * Timeout callback to remove items.
+ * @type {ProgressCenter.TimeoutManager_}
+ * @private
+ */
+ this.resetTimeout_ = new ProgressCenter.TimeoutManager_(
+ this.reset_.bind(this));
+
+ Object.seal(this);
+};
+
+/**
+ * The default amount of milliseconds time, before a progress item will reset
+ * after the last complete.
+ * @type {number}
+ * @private
+ * @const
+ */
+ProgressCenter.RESET_DELAY_TIME_MS_ = 5000;
+
+/**
+ * Notifications created by progress center.
+ * @param {function(string)} cancelCallback Callback to notify the progress
+ * center of cancel operation.
+ * @constructor
+ * @private
+ */
+ProgressCenter.Notifications_ = function(cancelCallback) {
+ /**
+ * ID set of notifications that is progressing now.
+ * @type {Object.<string, ProgressCenter.Notifications_.NotificationState_>}
+ * @private
+ */
+ this.ids_ = {};
+
+ /**
+ * Async queue.
+ * @type {AsyncUtil.Queue}
+ * @private
+ */
+ this.queue_ = new AsyncUtil.Queue();
+
+ /**
+ * Callback to notify the progress center of cancel operation.
+ * @type {function(string)}
+ * @private
+ */
+ this.cancelCallback_ = cancelCallback;
+
+ chrome.notifications.onButtonClicked.addListener(
+ this.onButtonClicked_.bind(this));
+ chrome.notifications.onClosed.addListener(this.onClosed_.bind(this));
+
+ Object.seal(this);
+};
+
+/**
+ * State of notification.
+ * @enum {string}
+ * @const
+ * @private
+ */
+ProgressCenter.Notifications_.NotificationState_ = Object.freeze({
+ VISIBLE: 'visible',
+ DISMISSED: 'dismissed'
+});
+
+/**
+ * Updates the notification according to the item.
+ * @param {ProgressCenterItem} item Item to contain new information.
+ * @param {boolean} newItemAcceptable Whether to accept new item or not.
+ */
+ProgressCenter.Notifications_.prototype.updateItem = function(
+ item, newItemAcceptable) {
+ var NotificationState = ProgressCenter.Notifications_.NotificationState_;
+ var newlyAdded = !(item.id in this.ids_);
+
+ // If new item is not acceptable, just return.
+ if (newlyAdded && !newItemAcceptable)
+ return;
+
+ // Update the ID map and return if we does not show a notification for the
+ // item.
+ if (item.state === ProgressItemState.PROGRESSING) {
+ if (newlyAdded)
+ this.ids_[item.id] = NotificationState.VISIBLE;
+ else if (this.ids_[item.id] === NotificationState.DISMISSED)
+ return;
+ } else {
+ // This notification is no longer tracked.
+ var previousState = this.ids_[item.id];
+ delete this.ids_[item.id];
+ // Clear notifications for complete or canceled items.
+ if (item.state === ProgressItemState.CANCELED ||
+ item.state === ProgressItemState.COMPLETED) {
+ if (previousState === NotificationState.VISIBLE) {
+ this.queue_.run(function(proceed) {
+ chrome.notifications.clear(item.id, proceed);
+ });
+ }
+ return;
+ }
+ }
+
+ // Create/update the notification with the item.
+ this.queue_.run(function(proceed) {
+ var params = {
+ title: chrome.runtime.getManifest().name,
+ iconUrl: chrome.runtime.getURL('/common/images/icon96.png'),
+ type: item.state === ProgressItemState.PROGRESSING ? 'progress' : 'basic',
+ message: item.message,
+ buttons: item.cancelable ? [{title: str('CANCEL_LABEL')}] : undefined,
+ progress: item.state === ProgressItemState.PROGRESSING ?
+ item.progressRateByPercent : undefined
+ };
+ if (newlyAdded)
+ chrome.notifications.create(item.id, params, proceed);
+ else
+ chrome.notifications.update(item.id, params, proceed);
+ }.bind(this));
+};
+
+/**
+ * Handles cancel button click.
+ * @param {string} id Item ID.
+ * @private
+ */
+ProgressCenter.Notifications_.prototype.onButtonClicked_ = function(id) {
+ if (id in this.ids_)
+ this.cancelCallback_(id);
+};
+
+/**
+ * Handles notification close.
+ * @param {string} id Item ID.
+ * @private
+ */
+ProgressCenter.Notifications_.prototype.onClosed_ = function(id) {
+ if (id in this.ids_)
+ this.ids_[id] = ProgressCenter.Notifications_.NotificationState_.DISMISSED;
+};
+
+/**
+ * Utility for timeout callback.
+ *
+ * @param {function(*):*} callback Callback function.
+ * @constructor
+ * @private
+ */
+ProgressCenter.TimeoutManager_ = function(callback) {
+ this.callback_ = callback;
+ this.id_ = null;
+ Object.seal(this);
+};
+
+/**
+ * Requests timeout. Previous request is canceled.
+ * @param {number} milliseconds Time to invoke the callback function.
+ */
+ProgressCenter.TimeoutManager_.prototype.request = function(milliseconds) {
+ if (this.id_)
+ clearTimeout(this.id_);
+ this.id_ = setTimeout(function() {
+ this.id_ = null;
+ this.callback_();
+ }.bind(this), milliseconds);
+};
+
+/**
+ * Cancels the timeout and invoke the callback function synchronously.
+ */
+ProgressCenter.TimeoutManager_.prototype.callImmediately = function() {
+ if (this.id_)
+ clearTimeout(this.id_);
+ this.id_ = null;
+ this.callback_();
+};
+
+/**
+ * Updates the item in the progress center.
+ * If the item has a new ID, the item is added to the item list.
+ *
+ * @param {ProgressCenterItem} item Updated item.
+ */
+ProgressCenter.prototype.updateItem = function(item) {
+ // Update item.
+ var index = this.getItemIndex_(item.id);
+ if (index === -1)
+ this.items_.push(item);
+ else
+ this.items_[index] = item;
+
+ // Update panels.
+ var summarizedItem = this.getSummarizedItem_();
+ for (var i = 0; i < this.panels_.length; i++) {
+ this.panels_[i].updateItem(item);
+ this.panels_[i].updateCloseView(summarizedItem);
+ }
+
+ // Update notifications.
+ this.notifications_.updateItem(item, !this.panels_.length);
+
+ // Reset if there is no item.
+ for (var i = 0; i < this.items_.length; i++) {
+ switch (this.items_[i].state) {
+ case ProgressItemState.PROGRESSING:
+ return;
+ case ProgressItemState.ERROR:
+ this.resetTimeout_.request(ProgressCenter.RESET_DELAY_TIME_MS_);
+ return;
+ }
+ }
+
+ // Cancel timeout and call reset function immediately.
+ this.resetTimeout_.callImmediately();
+};
+
+/**
+ * Requests to cancel the progress item.
+ * @param {string} id Progress ID to be requested to cancel.
+ */
+ProgressCenter.prototype.requestCancel = function(id) {
+ var item = this.getItemById(id);
+ if (item && item.cancelCallback)
+ item.cancelCallback();
+};
+
+/**
+ * Adds a panel UI to the notification center.
+ * @param {ProgressCenterPanel} panel Panel UI.
+ */
+ProgressCenter.prototype.addPanel = function(panel) {
+ if (this.panels_.indexOf(panel) !== -1)
+ return;
+
+ // Update the panel list.
+ this.panels_.push(panel);
+
+ // Set the current items.
+ for (var i = 0; i < this.items_.length; i++)
+ panel.updateItem(this.items_[i]);
+ var summarizedItem = this.getSummarizedItem_();
+ if (summarizedItem)
+ panel.updateCloseView(summarizedItem);
+
+ // Register the cancel callback.
+ panel.cancelCallback = this.requestCancel.bind(this);
+};
+
+/**
+ * Removes a panel UI from the notification center.
+ * @param {ProgressCenterPanel} panel Panel UI.
+ */
+ProgressCenter.prototype.removePanel = function(panel) {
+ var index = this.panels_.indexOf(panel);
+ if (index === -1)
+ return;
+
+ this.panels_.splice(index, 1);
+ panel.cancelCallback = null;
+
+ // If there is no panel, show the notifications.
+ if (this.panels_.length)
+ return;
+ for (var i = 0; i < this.items_.length; i++)
+ this.notifications_.updateItem(this.items_[i], true);
+};
+
+/**
+ * Obtains the summarized item to be displayed in the closed progress center
+ * panel.
+ * @return {ProgressCenterItem} Summarized item. Returns null if there is no
+ * item.
+ * @private
+ */
+ProgressCenter.prototype.getSummarizedItem_ = function() {
+ var summarizedItem = new ProgressCenterItem();
+ var progressingItems = [];
+ var completedItems = [];
+ var canceledItems = [];
+ var errorItems = [];
+
+ for (var i = 0; i < this.items_.length; i++) {
+ // Count states.
+ switch (this.items_[i].state) {
+ case ProgressItemState.PROGRESSING:
+ progressingItems.push(this.items_[i]);
+ break;
+ case ProgressItemState.COMPLETED:
+ completedItems.push(this.items_[i]);
+ break;
+ case ProgressItemState.CANCELED:
+ canceledItems.push(this.items_[i]);
+ break;
+ case ProgressItemState.ERROR:
+ errorItems.push(this.items_[i]);
+ break;
+ }
+
+ // If all of the progressing items have the same type, then use
+ // it. Otherwise use TRANSFER, since it is the most generic.
+ if (this.items_[i].state === ProgressItemState.PROGRESSING) {
+ if (summarizedItem.type === null)
+ summarizedItem.type = this.items_[i].type;
+ else if (summarizedItem.type !== this.items_[i].type)
+ summarizedItem.type = ProgressItemType.TRANSFER;
+ }
+
+ // Sum up the progress values.
+ if (this.items_[i].state === ProgressItemState.PROGRESSING ||
+ this.items_[i].state === ProgressItemState.COMPLETED) {
+ summarizedItem.progressMax += this.items_[i].progressMax;
+ summarizedItem.progressValue += this.items_[i].progressValue;
+ }
+ }
+
+ // If there are multiple visible (progressing and error) items, show the
+ // summarized message.
+ if (progressingItems.length + errorItems.length > 1) {
+ // Set item message.
+ var messages = [];
+ if (progressingItems.length > 0) {
+ switch (summarizedItem.type) {
+ case ProgressItemType.COPY:
+ messages.push(str('COPY_PROGRESS_SUMMARY'));
+ break;
+ case ProgressItemType.MOVE:
+ messages.push(str('MOVE_PROGRESS_SUMMARY'));
+ break;
+ case ProgressItemType.DELETE:
+ messages.push(str('DELETE_PROGRESS_SUMMARY'));
+ break;
+ case ProgressItemType.ZIP:
+ messages.push(str('ZIP_PROGRESS_SUMMARY'));
+ break;
+ case ProgressItemType.TRANSFER:
+ messages.push(str('TRANSFER_PROGRESS_SUMMARY'));
+ break;
+ }
+ }
+ if (errorItems.length === 1)
+ messages.push(str('ERROR_PROGRESS_SUMMARY'));
+ else if (errorItems.length > 1)
+ messages.push(strf('ERROR_PROGRESS_SUMMARY_PLURAL', errorItems.length));
+
+ summarizedItem.summarized = true;
+ summarizedItem.message = messages.join(' ');
+ summarizedItem.state = progressingItems.length > 0 ?
+ ProgressItemState.PROGRESSING : ProgressItemState.ERROR;
+ return summarizedItem;
+ }
+
+ // If there is 1 visible item, show the item message.
+ if (progressingItems.length + errorItems.length === 1) {
+ var visibleItem = progressingItems[0] || errorItems[0];
+ summarizedItem.id = visibleItem.id;
+ summarizedItem.cancelCallback = visibleItem.cancelCallback;
+ summarizedItem.type = visibleItem.type;
+ summarizedItem.message = visibleItem.message;
+ summarizedItem.state = visibleItem.state;
+ return summarizedItem;
+ }
+
+ // If there is no visible item, the message can be empty.
+ if (completedItems.length > 0) {
+ summarizedItem.state = ProgressItemState.COMPLETED;
+ return summarizedItem;
+ }
+ if (canceledItems.length > 0) {
+ summarizedItem.state = ProgressItemState.CANCELED;
+ return summarizedItem;
+ }
+
+ // If there is no item, return null.
+ return null;
+};
+
+/**
+ * Obtains item by ID.
+ * @param {string} id ID of progress item.
+ * @return {ProgressCenterItem} Progress center item having the specified
+ * ID. Null if the item is not found.
+ */
+ProgressCenter.prototype.getItemById = function(id) {
+ return this.items_[this.getItemIndex_(id)];
+};
+
+/**
+ * Obtains item index that have the specifying ID.
+ * @param {string} id Item ID.
+ * @return {number} Item index. Returns -1 If the item is not found.
+ * @private
+ */
+ProgressCenter.prototype.getItemIndex_ = function(id) {
+ for (var i = 0; i < this.items_.length; i++) {
+ if (this.items_[i].id === id)
+ return i;
+ }
+ return -1;
+};
+
+/**
+ * Hides the progress center if there is no progressing items.
+ * @private
+ */
+ProgressCenter.prototype.reset_ = function() {
+ // If we have a progressing item, stop reset.
+ for (var i = 0; i < this.items_.length; i++) {
+ if (this.items_[i].state == ProgressItemState.PROGRESSING)
+ return;
+ }
+
+ // Reset items.
+ this.items_.splice(0, this.items_.length);
+
+ // Dispatch a event.
+ for (var i = 0; i < this.panels_.length; i++)
+ this.panels_[i].reset();
+};
diff --git a/chromium/chrome/browser/resources/file_manager/background/js/test_util.js b/chromium/chrome/browser/resources/file_manager/background/js/test_util.js
new file mode 100644
index 00000000000..3a3bd78857e
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/background/js/test_util.js
@@ -0,0 +1,868 @@
+// Copyright (c) 2013 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.
+
+/**
+ * Namespace for test related things.
+ */
+var test = test || {};
+
+/**
+ * Namespace for test utility functions.
+ *
+ * Public functions in the test.util.sync and the test.util.async namespaces are
+ * published to test cases and can be called by using callRemoteTestUtil. The
+ * arguments are serialized as JSON internally. If application ID is passed to
+ * callRemoteTestUtil, the content window of the application is added as the
+ * first argument. The functions in the test.util.async namespace are passed the
+ * callback function as the last argument.
+ */
+test.util = {};
+
+/**
+ * Namespace for synchronous utility functions.
+ */
+test.util.sync = {};
+
+/**
+ * Namespace for asynchronous utility functions.
+ */
+test.util.async = {};
+
+/**
+ * Extension ID of the testing extension.
+ * @type {string}
+ * @const
+ */
+test.util.TESTING_EXTENSION_ID = 'oobinhbdbiehknkpbpejbbpdbkdjmoco';
+
+/**
+ * Interval of checking a condition in milliseconds.
+ * @type {number}
+ * @const
+ * @private
+ */
+test.util.WAITTING_INTERVAL_ = 50;
+
+/**
+ * Repeats the function until it returns true.
+ * @param {function()} closure Function expected to return true.
+ * @private
+ */
+test.util.repeatUntilTrue_ = function(closure) {
+ var step = function() {
+ if (closure())
+ return;
+ setTimeout(step, test.util.WAITTING_INTERVAL_);
+ };
+ step();
+};
+
+/**
+ * Opens the main Files.app's window and waits until it is ready.
+ *
+ * @param {Object} appState App state.
+ * @param {function(string)} callback Completion callback with the new window's
+ * App ID.
+ */
+test.util.async.openMainWindow = function(appState, callback) {
+ var steps = [
+ function() {
+ launchFileManager(appState,
+ undefined, // opt_type
+ undefined, // opt_id
+ steps.shift());
+ },
+ function(appId) {
+ test.util.repeatUntilTrue_(function() {
+ if (!background.appWindows[appId])
+ return false;
+ var contentWindow = background.appWindows[appId].contentWindow;
+ var table = contentWindow.document.querySelector('#detail-table');
+ if (!table)
+ return false;
+ callback(appId);
+ return true;
+ });
+ }
+ ];
+ steps.shift()();
+};
+
+/**
+ * Waits for a window with the specified App ID prefix. Eg. `files` will match
+ * windows such as files#0, files#1, etc.
+ *
+ * @param {string} appIdPrefix ID prefix of the requested window.
+ * @param {function(string)} callback Completion callback with the new window's
+ * App ID.
+ */
+test.util.async.waitForWindow = function(appIdPrefix, callback) {
+ test.util.repeatUntilTrue_(function() {
+ for (var appId in background.appWindows) {
+ if (appId.indexOf(appIdPrefix) == 0 &&
+ background.appWindows[appId].contentWindow) {
+ callback(appId);
+ return true;
+ }
+ }
+ return false;
+ });
+};
+
+/**
+ * Gets a document in the Files.app's window, including iframes.
+ *
+ * @param {Window} contentWindow Window to be used.
+ * @param {string=} opt_iframeQuery Query for the iframe.
+ * @return {Document=} Returns the found document or undefined if not found.
+ * @private
+ */
+test.util.sync.getDocument_ = function(contentWindow, opt_iframeQuery) {
+ if (opt_iframeQuery) {
+ var iframe = contentWindow.document.querySelector(opt_iframeQuery);
+ return iframe && iframe.contentWindow && iframe.contentWindow.document;
+ }
+
+ return contentWindow.document;
+};
+
+/**
+ * Gets total Javascript error count from each app window.
+ * @return {number} Error count.
+ */
+test.util.sync.getErrorCount = function() {
+ var totalCount = JSErrorCount;
+ for (var appId in background.appWindows) {
+ var contentWindow = background.appWindows[appId].contentWindow;
+ if (contentWindow.JSErrorCount)
+ totalCount += contentWindow.JSErrorCount;
+ }
+ return totalCount;
+};
+
+/**
+ * Resizes the window to the specified dimensions.
+ *
+ * @param {Window} contentWindow Window to be tested.
+ * @param {number} width Window width.
+ * @param {number} height Window height.
+ * @return {boolean} True for success.
+ */
+test.util.sync.resizeWindow = function(contentWindow, width, height) {
+ background.appWindows[contentWindow.appID].resizeTo(width, height);
+ return true;
+};
+
+/**
+ * Returns an array with the files currently selected in the file manager.
+ *
+ * @param {Window} contentWindow Window to be tested.
+ * @return {Array.<string>} Array of selected files.
+ */
+test.util.sync.getSelectedFiles = function(contentWindow) {
+ var table = contentWindow.document.querySelector('#detail-table');
+ var rows = table.querySelectorAll('li');
+ var selected = [];
+ for (var i = 0; i < rows.length; ++i) {
+ if (rows[i].hasAttribute('selected')) {
+ selected.push(
+ rows[i].querySelector('.filename-label').textContent);
+ }
+ }
+ return selected;
+};
+
+/**
+ * Returns an array with the files on the file manager's file list.
+ *
+ * @param {Window} contentWindow Window to be tested.
+ * @return {Array.<Array.<string>>} Array of rows.
+ */
+test.util.sync.getFileList = function(contentWindow) {
+ var table = contentWindow.document.querySelector('#detail-table');
+ var rows = table.querySelectorAll('li');
+ var fileList = [];
+ for (var j = 0; j < rows.length; ++j) {
+ var row = rows[j];
+ fileList.push([
+ row.querySelector('.filename-label').textContent,
+ row.querySelector('.size').textContent,
+ row.querySelector('.type').textContent,
+ row.querySelector('.date').textContent
+ ]);
+ }
+ return fileList;
+};
+
+/**
+ * Checkes if the given label and path of the volume are selected.
+ * @param {Window} contentWindow Window to be tested.
+ * @param {string} label Correct label the selected volume should have.
+ * @param {string} path Correct path the selected volume should have.
+ * @return {boolean} True for success.
+ */
+test.util.sync.checkSelectedVolume = function(contentWindow, label, path) {
+ var list = contentWindow.document.querySelector('#navigation-list');
+ var rows = list.querySelectorAll('li');
+ var selected = [];
+ for (var i = 0; i < rows.length; ++i) {
+ if (rows[i].hasAttribute('selected'))
+ selected.push(rows[i]);
+ }
+ // Selected item must be one.
+ if (selected.length !== 1)
+ return false;
+
+ if (selected[0].modelItem.path !== path ||
+ selected[0].querySelector('.root-label').textContent !== label) {
+ return false;
+ }
+
+ return true;
+};
+
+/**
+ * Waits until the window is set to the specified dimensions.
+ *
+ * @param {Window} contentWindow Window to be tested.
+ * @param {number} width Requested width.
+ * @param {number} height Requested height.
+ * @param {function(Object)} callback Success callback with the dimensions.
+ */
+test.util.async.waitForWindowGeometry = function(
+ contentWindow, width, height, callback) {
+ test.util.repeatUntilTrue_(function() {
+ if (contentWindow.innerWidth == width &&
+ contentWindow.innerHeight == height) {
+ callback({width: width, height: height});
+ return true;
+ }
+ return false;
+ });
+};
+
+/**
+ * Waits for an element and returns it as an array of it's attributes.
+ *
+ * @param {Window} contentWindow Window to be tested.
+ * @param {string} targetQuery Query to specify the element.
+ * @param {?string} iframeQuery Iframe selector or null if no iframe.
+ * @param {boolean=} opt_inverse True if the function should return if the
+ * element disappears, instead of appearing.
+ * @param {function(Object)} callback Callback with a hash array of attributes
+ * and contents as text.
+ */
+test.util.async.waitForElement = function(
+ contentWindow, targetQuery, iframeQuery, opt_inverse, callback) {
+ test.util.repeatUntilTrue_(function() {
+ var doc = test.util.sync.getDocument_(contentWindow, iframeQuery);
+ if (!doc)
+ return false;
+ var element = doc.querySelector(targetQuery);
+ if (!element)
+ return !!opt_inverse;
+ var attributes = {};
+ for (var i = 0; i < element.attributes.length; i++) {
+ attributes[element.attributes[i].nodeName] =
+ element.attributes[i].nodeValue;
+ }
+ var text = element.textContent;
+ callback({attributes: attributes, text: text});
+ return !opt_inverse;
+ });
+};
+
+/**
+ * Calls getFileList until the number of displayed files is different from
+ * lengthBefore.
+ *
+ * @param {Window} contentWindow Window to be tested.
+ * @param {number} lengthBefore Number of items visible before.
+ * @param {function(Array.<Array.<string>>)} callback Change callback.
+ */
+test.util.async.waitForFileListChange = function(
+ contentWindow, lengthBefore, callback) {
+ test.util.repeatUntilTrue_(function() {
+ var files = test.util.sync.getFileList(contentWindow);
+ files.sort();
+ var notReadyRows = files.filter(function(row) {
+ return row.filter(function(cell) { return cell == '...'; }).length;
+ });
+ if (notReadyRows.length === 0 &&
+ files.length !== lengthBefore &&
+ files.length !== 0) {
+ callback(files);
+ return true;
+ } else {
+ return false;
+ }
+ });
+};
+
+/**
+ * Returns an array of items on the file manager's autocomplete list.
+ *
+ * @param {Window} contentWindow Window to be tested.
+ * @return {Array.<string>} Array of items.
+ */
+test.util.sync.getAutocompleteList = function(contentWindow) {
+ var list = contentWindow.document.querySelector('#autocomplete-list');
+ var lines = list.querySelectorAll('li');
+ var items = [];
+ for (var j = 0; j < lines.length; ++j) {
+ var line = lines[j];
+ items.push(line.innerText);
+ }
+ return items;
+};
+
+/**
+ * Performs autocomplete with the given query and waits until at least
+ * |numExpectedItems| items are shown, including the first item which
+ * always looks like "'<query>' - search Drive".
+ *
+ * @param {Window} contentWindow Window to be tested.
+ * @param {string} query Query used for autocomplete.
+ * @param {number} numExpectedItems number of items to be shown.
+ * @param {function(Array.<string>)} callback Change callback.
+ */
+test.util.async.performAutocompleteAndWait = function(
+ contentWindow, query, numExpectedItems, callback) {
+ // Dispatch a 'focus' event to the search box so that the autocomplete list
+ // is attached to the search box. Note that calling searchBox.focus() won't
+ // dispatch a 'focus' event.
+ var searchBox = contentWindow.document.querySelector('#search-box input');
+ var focusEvent = contentWindow.document.createEvent('Event');
+ focusEvent.initEvent('focus', true /* bubbles */, true /* cancelable */);
+ searchBox.dispatchEvent(focusEvent);
+
+ // Change the value of the search box and dispatch an 'input' event so that
+ // the autocomplete query is processed.
+ searchBox.value = query;
+ var inputEvent = contentWindow.document.createEvent('Event');
+ inputEvent.initEvent('input', true /* bubbles */, true /* cancelable */);
+ searchBox.dispatchEvent(inputEvent);
+
+ test.util.repeatUntilTrue_(function() {
+ var items = test.util.sync.getAutocompleteList(contentWindow);
+ if (items.length >= numExpectedItems) {
+ callback(items);
+ return true;
+ } else {
+ return false;
+ }
+ });
+};
+
+/**
+ * Waits until a dialog with an OK button is shown and accepts it.
+ *
+ * @param {Window} contentWindow Window to be tested.
+ * @param {function()} callback Success callback.
+ */
+test.util.async.waitAndAcceptDialog = function(contentWindow, callback) {
+ test.util.repeatUntilTrue_(function() {
+ var button = contentWindow.document.querySelector('.cr-dialog-ok');
+ if (!button)
+ return false;
+ button.click();
+ // Wait until the dialog is removed from the DOM.
+ test.util.repeatUntilTrue_(function() {
+ if (contentWindow.document.querySelector('.cr-dialog-container'))
+ return false;
+ callback();
+ return true;
+ });
+ return true;
+ });
+};
+
+/**
+ * Fakes pressing the down arrow until the given |filename| is selected.
+ *
+ * @param {Window} contentWindow Window to be tested.
+ * @param {string} filename Name of the file to be selected.
+ * @return {boolean} True if file got selected, false otherwise.
+ */
+test.util.sync.selectFile = function(contentWindow, filename) {
+ var table = contentWindow.document.querySelector('#detail-table');
+ var rows = table.querySelectorAll('li');
+ for (var index = 0; index < rows.length; ++index) {
+ test.util.sync.fakeKeyDown(contentWindow, '#file-list', 'Down', false);
+ var selection = test.util.sync.getSelectedFiles(contentWindow);
+ if (selection.length === 1 && selection[0] === filename)
+ return true;
+ }
+ console.error('Failed to select file "' + filename + '"');
+ return false;
+};
+
+/**
+ * Open the file by selectFile and fakeMouseDoubleClick.
+ *
+ * @param {Window} contentWindow Window to be tested.
+ * @param {string} filename Name of the file to be opened.
+ * @return {boolean} True if file got selected and a double click message is
+ * sent, false otherwise.
+ */
+test.util.sync.openFile = function(contentWindow, filename) {
+ var query = '#file-list li.table-row[selected] .filename-label span';
+ return test.util.sync.selectFile(contentWindow, filename) &&
+ test.util.sync.fakeMouseDoubleClick(contentWindow, query);
+};
+
+/**
+ * Selects a volume specified by its icon name
+ *
+ * @param {Window} contentWindow Window to be tested.
+ * @param {string} iconName Name of the volume icon.
+ * @param {function(boolean)} callback Callback function to notify the caller
+ * whether the target is found and mousedown and click events are sent.
+ */
+test.util.async.selectVolume = function(contentWindow, iconName, callback) {
+ var query = '[volume-type-icon=' + iconName + ']';
+ var driveQuery = '[volume-type-icon=drive]';
+ var isDriveSubVolume = iconName == 'drive_recent' ||
+ iconName == 'drive_shared_with_me' ||
+ iconName == 'drive_offline';
+ var preSelection = false;
+ var steps = {
+ checkQuery: function() {
+ if (contentWindow.document.querySelector(query)) {
+ steps.sendEvents();
+ return;
+ }
+ // If the target volume is sub-volume of drive, we must click 'drive'
+ // before clicking the sub-item.
+ if (!preSelection) {
+ if (!isDriveSubVolume) {
+ callback(false);
+ return;
+ }
+ if (!(test.util.sync.fakeMouseDown(contentWindow, driveQuery) &&
+ test.util.sync.fakeMouseClick(contentWindow, driveQuery))) {
+ callback(false);
+ return;
+ }
+ preSelection = true;
+ }
+ setTimeout(steps.checkQuery, 50);
+ },
+ sendEvents: function() {
+ // To change the selected volume, we have to send both events 'mousedown'
+ // and 'click' to the navigation list.
+ callback(test.util.sync.fakeMouseDown(contentWindow, query) &&
+ test.util.sync.fakeMouseClick(contentWindow, query));
+ }
+ };
+ steps.checkQuery();
+};
+
+/**
+ * Waits the contents of file list becomes to equal to expected contents.
+ *
+ * @param {Window} contentWindow Window to be tested.
+ * @param {Array.<Array.<string>>} expected Expected contents of file list.
+ * @param {{orderCheck:boolean=, ignoreLastModifiedTime:boolean=}=} opt_options
+ * Options of the comparison. If orderCheck is true, it also compares the
+ * order of files. If ignoreLastModifiedTime is true, it compares the file
+ * without its last modified time.
+ * @param {function()} callback Callback function to notify the caller that
+ * expected files turned up.
+ */
+test.util.async.waitForFiles = function(
+ contentWindow, expected, opt_options, callback) {
+ var options = opt_options || {};
+ test.util.repeatUntilTrue_(function() {
+ var files = test.util.sync.getFileList(contentWindow);
+ if (!options.orderCheck) {
+ files.sort();
+ expected.sort();
+ }
+ if (options.ignoreLastModifiedTime) {
+ for (var i = 0; i < Math.min(files.length, expected.length); i++) {
+ files[i][3] = '';
+ expected[i][3] = '';
+ }
+ }
+ if (chrome.test.checkDeepEq(expected, files)) {
+ callback(true);
+ return true;
+ }
+ return false;
+ });
+};
+
+/**
+ * Executes Javascript code on a webview and returns the result.
+ *
+ * @param {Window} contentWindow Window to be tested.
+ * @param {string} webViewQuery Selector for the web view.
+ * @param {string} code Javascript code to be executed within the web view.
+ * @param {function(*)} callback Callback function with results returned by the
+ * script.
+ */
+test.util.async.executeScriptInWebView = function(
+ contentWindow, webViewQuery, code, callback) {
+ var webView = contentWindow.document.querySelector(webViewQuery);
+ webView.executeScript({code: code}, callback);
+};
+
+/**
+ * Sends an event to the element specified by |targetQuery|.
+ *
+ * @param {Window} contentWindow Window to be tested.
+ * @param {string} targetQuery Query to specify the element.
+ * @param {Event} event Event to be sent.
+ * @param {string=} opt_iframeQuery Optional iframe selector.
+ * @return {boolean} True if the event is sent to the target, false otherwise.
+ */
+test.util.sync.sendEvent = function(
+ contentWindow, targetQuery, event, opt_iframeQuery) {
+ var doc = test.util.sync.getDocument_(contentWindow, opt_iframeQuery);
+ if (doc) {
+ var target = doc.querySelector(targetQuery);
+ if (target) {
+ target.dispatchEvent(event);
+ return true;
+ }
+ }
+ console.error('Target element for ' + targetQuery + ' not found.');
+ return false;
+};
+
+/**
+ * Sends an fake event having the specified type to the target query.
+ *
+ * @param {Window} contentWindow Window to be tested.
+ * @param {string} targetQuery Query to specify the element.
+ * @param {string} event Type of event.
+ * @return {boolean} True if the event is sent to the target, false otherwise.
+ */
+test.util.sync.fakeEvent = function(contentWindow, targetQuery, event) {
+ return test.util.sync.sendEvent(
+ contentWindow, targetQuery, new Event(event));
+};
+
+/**
+ * Sends a fake key event to the element specified by |targetQuery| with the
+ * given |keyIdentifier| and optional |ctrl| modifier to the file manager.
+ *
+ * @param {Window} contentWindow Window to be tested.
+ * @param {string} targetQuery Query to specify the element.
+ * @param {string} keyIdentifier Identifier of the emulated key.
+ * @param {boolean} ctrl Whether CTRL should be pressed, or not.
+ * @param {string=} opt_iframeQuery Optional iframe selector.
+ * @return {boolean} True if the event is sent to the target, false otherwise.
+ */
+test.util.sync.fakeKeyDown = function(
+ contentWindow, targetQuery, keyIdentifier, ctrl, opt_iframeQuery) {
+ var event = new KeyboardEvent(
+ 'keydown',
+ { bubbles: true, keyIdentifier: keyIdentifier, ctrlKey: ctrl });
+ return test.util.sync.sendEvent(
+ contentWindow, targetQuery, event, opt_iframeQuery);
+};
+
+/**
+ * Simulates a fake mouse click (left button, single click) on the element
+ * specified by |targetQuery|. This sends 'mouseover', 'mousedown', 'mouseup'
+ * and 'click' events in turns.
+ *
+ * @param {Window} contentWindow Window to be tested.
+ * @param {string} targetQuery Query to specify the element.
+ * @param {string=} opt_iframeQuery Optional iframe selector.
+ * @return {boolean} True if the all events are sent to the target, false
+ * otherwise.
+ */
+test.util.sync.fakeMouseClick = function(
+ contentWindow, targetQuery, opt_iframeQuery) {
+ var mouseOverEvent = new MouseEvent('mouseover', {bubbles: true, detail: 1});
+ var resultMouseOver = test.util.sync.sendEvent(
+ contentWindow, targetQuery, mouseOverEvent, opt_iframeQuery);
+ var mouseDownEvent = new MouseEvent('mousedown', {bubbles: true, detail: 1});
+ var resultMouseDown = test.util.sync.sendEvent(
+ contentWindow, targetQuery, mouseDownEvent, opt_iframeQuery);
+ var mouseUpEvent = new MouseEvent('mouseup', {bubbles: true, detail: 1});
+ var resultMouseUp = test.util.sync.sendEvent(
+ contentWindow, targetQuery, mouseUpEvent, opt_iframeQuery);
+ var clickEvent = new MouseEvent('click', {bubbles: true, detail: 1});
+ var resultClick = test.util.sync.sendEvent(
+ contentWindow, targetQuery, clickEvent, opt_iframeQuery);
+ return resultMouseOver && resultMouseDown && resultMouseUp && resultClick;
+};
+
+/**
+ * Simulates a fake double click event (left button) to the element specified by
+ * |targetQuery|.
+ *
+ * @param {Window} contentWindow Window to be tested.
+ * @param {string} targetQuery Query to specify the element.
+ * @param {string=} opt_iframeQuery Optional iframe selector.
+ * @return {boolean} True if the event is sent to the target, false otherwise.
+ */
+test.util.sync.fakeMouseDoubleClick = function(
+ contentWindow, targetQuery, opt_iframeQuery) {
+ // Double click is always preceded with a single click.
+ if (!test.util.sync.fakeMouseClick(
+ contentWindow, targetQuery, opt_iframeQuery)) {
+ return false;
+ }
+
+ // Send the second click event, but with detail equal to 2 (number of clicks)
+ // in a row.
+ var event = new MouseEvent('click', { bubbles: true, detail: 2 });
+ if (!test.util.sync.sendEvent(
+ contentWindow, targetQuery, event, opt_iframeQuery)) {
+ return false;
+ }
+
+ // Send the double click event.
+ var event = new MouseEvent('dblclick', { bubbles: true });
+ if (!test.util.sync.sendEvent(
+ contentWindow, targetQuery, event, opt_iframeQuery)) {
+ return false;
+ }
+
+ return true;
+};
+
+/**
+ * Sends a fake mouse down event to the element specified by |targetQuery|.
+ *
+ * @param {Window} contentWindow Window to be tested.
+ * @param {string} targetQuery Query to specify the element.
+ * @param {string=} opt_iframeQuery Optional iframe selector.
+ * @return {boolean} True if the event is sent to the target, false otherwise.
+ */
+test.util.sync.fakeMouseDown = function(
+ contentWindow, targetQuery, opt_iframeQuery) {
+ var event = new MouseEvent('mousedown', { bubbles: true });
+ return test.util.sync.sendEvent(
+ contentWindow, targetQuery, event, opt_iframeQuery);
+};
+
+/**
+ * Sends a fake mouse up event to the element specified by |targetQuery|.
+ *
+ * @param {Window} contentWindow Window to be tested.
+ * @param {string} targetQuery Query to specify the element.
+ * @param {string=} opt_iframeQuery Optional iframe selector.
+ * @return {boolean} True if the event is sent to the target, false otherwise.
+ */
+test.util.sync.fakeMouseUp = function(
+ contentWindow, targetQuery, opt_iframeQuery) {
+ var event = new MouseEvent('mouseup', { bubbles: true });
+ return test.util.sync.sendEvent(
+ contentWindow, targetQuery, event, opt_iframeQuery);
+};
+
+/**
+ * Selects |filename| and fakes pressing Ctrl+C, Ctrl+V (copy, paste).
+ *
+ * @param {Window} contentWindow Window to be tested.
+ * @param {string} filename Name of the file to be copied.
+ * @return {boolean} True if copying got simulated successfully. It does not
+ * say if the file got copied, or not.
+ */
+test.util.sync.copyFile = function(contentWindow, filename) {
+ if (!test.util.sync.selectFile(contentWindow, filename))
+ return false;
+ // Ctrl+C and Ctrl+V
+ test.util.sync.fakeKeyDown(contentWindow, '#file-list', 'U+0043', true);
+ test.util.sync.fakeKeyDown(contentWindow, '#file-list', 'U+0056', true);
+ return true;
+};
+
+/**
+ * Selects |filename| and fakes pressing the Delete key.
+ *
+ * @param {Window} contentWindow Window to be tested.
+ * @param {string} filename Name of the file to be deleted.
+ * @return {boolean} True if deleting got simulated successfully. It does not
+ * say if the file got deleted, or not.
+ */
+test.util.sync.deleteFile = function(contentWindow, filename) {
+ if (!test.util.sync.selectFile(contentWindow, filename))
+ return false;
+ // Delete
+ test.util.sync.fakeKeyDown(contentWindow, '#file-list', 'U+007F', false);
+ return true;
+};
+
+/**
+ * Wait for the elements' style to be changed as the expected values. The
+ * queries argument is a list of object that have the query property and the
+ * styles property. The query property is a string query to specify the
+ * element. The styles property is a string map of the style name and its
+ * expected value.
+ *
+ * @param {Window} contentWindow Window to be tested.
+ * @param {Array.<object>} queries Queries that specifies the elements and
+ * expected styles.
+ * @param {function()} callback Callback function to be notified the change of
+ * the styles.
+ */
+test.util.async.waitForStyles = function(contentWindow, queries, callback) {
+ test.util.repeatUntilTrue_(function() {
+ for (var i = 0; i < queries.length; i++) {
+ var element = contentWindow.document.querySelector(queries[i].query);
+ var styles = queries[i].styles;
+ for (var name in styles) {
+ if (contentWindow.getComputedStyle(element)[name] != styles[name])
+ return false;
+ }
+ }
+ callback();
+ return true;
+ });
+};
+
+/**
+ * Execute a command on the document in the specified window.
+ *
+ * @param {Window} contentWindow Window to be tested.
+ * @param {string} command Command name.
+ * @return {boolean} True if the command is executed successfully.
+ */
+test.util.sync.execCommand = function(contentWindow, command) {
+ return contentWindow.document.execCommand(command);
+};
+
+/**
+ * Override the installWebstoreItem method in private api for test.
+ *
+ * @param {Window} contentWindow Window to be tested.
+ * @param {string} expectedItemId Item ID to be called this method with.
+ * @param {?string} intendedError Error message to be returned when the item id
+ * matches. 'null' represents no error.
+ * @return {boolean} Always return true.
+ */
+test.util.sync.overrideInstallWebstoreItemApi =
+ function(contentWindow, expectedItemId, intendedError) {
+ var setLastError = function(message) {
+ contentWindow.chrome.runtime.lastError =
+ message ? {message: message} : null;
+ };
+
+ var installWebstoreItem = function(itemId, callback) {
+ setTimeout(function() {
+ if (itemId !== expectedItemId) {
+ setLastError('Invalid Chrome Web Store item ID');
+ callback();
+ return;
+ }
+
+ setLastError(intendedError);
+ callback();
+ });
+ };
+
+ test.util.executedTasks_ = [];
+ contentWindow.chrome.fileBrowserPrivate.installWebstoreItem =
+ installWebstoreItem;
+ return true;
+};
+
+/**
+ * Override the task-related methods in private api for test.
+ *
+ * @param {Window} contentWindow Window to be tested.
+ * @param {Array.<Object>} taskList List of tasks to be returned in
+ * fileBrowserPrivate.getFileTasks().
+ * @return {boolean} Always return true.
+ */
+test.util.sync.overrideTasks = function(contentWindow, taskList) {
+ var getFileTasks = function(urls, mime, onTasks) {
+ // Call onTask asynchronously (same with original getFileTasks).
+ setTimeout(function() {
+ onTasks(taskList);
+ });
+ };
+
+ var executeTask = function(taskId, url) {
+ test.util.executedTasks_.push(taskId);
+ };
+
+ test.util.executedTasks_ = [];
+ contentWindow.chrome.fileBrowserPrivate.getFileTasks = getFileTasks;
+ contentWindow.chrome.fileBrowserPrivate.executeTask = executeTask;
+ return true;
+};
+
+/**
+ * Check if Files.app has ordered to execute the given task or not yet. This
+ * method must be used with test.util.sync.overrideTasks().
+ *
+ * @param {Window} contentWindow Window to be tested.
+ * @param {string} taskId Taskid of the task which should be executed.
+ * @param {function()} callback Callback function to be notified the order of
+ * the execution.
+ */
+test.util.async.waitUntilTaskExecutes =
+ function(contentWindow, taskId, callback) {
+ if (!test.util.executedTasks_) {
+ console.error('Please call overrideTasks() first.');
+ return;
+ }
+
+ test.util.repeatUntilTrue_(function() {
+ if (test.util.executedTasks_.indexOf(taskId) === -1)
+ return false;
+ callback();
+ return true;
+ });
+};
+
+/**
+ * Registers message listener, which runs test utility functions.
+ */
+test.util.registerRemoteTestUtils = function() {
+ // Register the message listener.
+ var onMessage = chrome.runtime ? chrome.runtime.onMessageExternal :
+ chrome.extension.onMessageExternal;
+ // Return true for asynchronous functions and false for synchronous.
+ onMessage.addListener(function(request, sender, sendResponse) {
+ // Check the sender.
+ if (sender.id != test.util.TESTING_EXTENSION_ID) {
+ console.error('The testing extension must be white-listed.');
+ return false;
+ }
+ // Set a global flag that we are in tests, so other components are aware
+ // of it.
+ window.IN_TEST = true;
+ // Check the function name.
+ if (!request.func || request.func[request.func.length - 1] == '_') {
+ request.func = '';
+ }
+ // Prepare arguments.
+ var args = request.args.slice(); // shallow copy
+ if (request.appId) {
+ if (!background.appWindows[request.appId]) {
+ console.error('Specified window not found.');
+ return false;
+ }
+ args.unshift(background.appWindows[request.appId].contentWindow);
+ }
+ // Call the test utility function and respond the result.
+ if (test.util.async[request.func]) {
+ args[test.util.async[request.func].length - 1] = function() {
+ console.debug('Received the result of ' + request.func);
+ sendResponse.apply(null, arguments);
+ };
+ console.debug('Waiting for the result of ' + request.func);
+ test.util.async[request.func].apply(null, args);
+ return true;
+ } else if (test.util.sync[request.func]) {
+ sendResponse(test.util.sync[request.func].apply(null, args));
+ return false;
+ } else {
+ console.error('Invalid function name.');
+ return false;
+ }
+ });
+};
+
+// Register the test utils.
+test.util.registerRemoteTestUtils();
diff --git a/chromium/chrome/browser/resources/file_manager/background/js/volume_manager.js b/chromium/chrome/browser/resources/file_manager/background/js/volume_manager.js
new file mode 100644
index 00000000000..35e94f2cd7e
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/background/js/volume_manager.js
@@ -0,0 +1,727 @@
+// 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.
+
+'use strict';
+
+/**
+ * Represents each volume, such as "drive", "download directory", each "USB
+ * flush storage", or "mounted zip archive" etc.
+ *
+ * @param {util.VolumeType} volumeType The type of the volume.
+ * @param {string} mountPath Where the volume is mounted.
+ * @param {string} volumeId ID of the volume.
+ * @param {DirectoryEntry} root The root directory entry of this volume.
+ * @param {string} error The error if an error is found.
+ * @param {string} deviceType The type of device ('usb'|'sd'|'optical'|'mobile'
+ * |'unknown') (as defined in chromeos/disks/disk_mount_manager.cc).
+ * Can be null.
+ * @param {boolean} isReadOnly True if the volume is read only.
+ * @param {!{displayName:string, isCurrentProfile:boolean}} profile Profile
+ * information.
+ * @constructor
+ */
+function VolumeInfo(
+ volumeType,
+ mountPath,
+ volumeId,
+ root,
+ error,
+ deviceType,
+ isReadOnly,
+ profile) {
+ this.volumeType = volumeType;
+ // TODO(hidehiko): This should include FileSystem instance.
+ this.mountPath = mountPath;
+ this.volumeId = volumeId;
+ this.root = root;
+
+ // Note: This represents if the mounting of the volume is successfully done
+ // or not. (If error is empty string, the mount is successfully done).
+ // TODO(hidehiko): Rename to make this more understandable.
+ this.error = error;
+ this.deviceType = deviceType;
+ this.isReadOnly = isReadOnly;
+ this.profile = Object.freeze(profile);
+
+ // VolumeInfo is immutable.
+ Object.freeze(this);
+}
+
+/**
+ * Obtains a URL of the display root directory that users can see as a root.
+ * @return {string} URL of root entry.
+ */
+VolumeInfo.prototype.getDisplayRootDirectoryURL = function() {
+ return this.root.toURL() +
+ (this.volumeType === util.VolumeType.DRIVE ? '/root' : '');
+};
+
+/**
+ * Obtains volume label.
+ * @return {string} Label for the volume.
+ */
+VolumeInfo.prototype.getLabel = function() {
+ if (this.volumeType === util.VolumeType.DRIVE)
+ return str('DRIVE_DIRECTORY_LABEL');
+ else
+ return PathUtil.getFolderLabel(this.mountPath);
+};
+
+/**
+ * Utilities for volume manager implementation.
+ */
+var volumeManagerUtil = {};
+
+/**
+ * Throws an Error when the given error is not in util.VolumeError.
+ * @param {util.VolumeError} error Status string usually received from APIs.
+ */
+volumeManagerUtil.validateError = function(error) {
+ for (var key in util.VolumeError) {
+ if (error === util.VolumeError[key])
+ return;
+ }
+
+ throw new Error('Invalid mount error: ' + error);
+};
+
+/**
+ * Returns the root entry of a volume mounted at mountPath.
+ *
+ * @param {string} mountPath The mounted path of the volume.
+ * @param {function(DirectoryEntry)} successCallback Called when the root entry
+ * is found.
+ * @param {function(FileError)} errorCallback Called when an error is found.
+ * @private
+ */
+volumeManagerUtil.getRootEntry_ = function(
+ mountPath, successCallback, errorCallback) {
+ // We always request FileSystem here, because requestFileSystem() grants
+ // permissions if necessary, especially for Drive File System at first mount
+ // time.
+ // Note that we actually need to request FileSystem after multi file system
+ // support, so this will be more natural code then.
+ chrome.fileBrowserPrivate.requestFileSystem(
+ 'compatible',
+ function(fileSystem) {
+ // TODO(hidehiko): chrome.runtime.lastError should have error reason.
+ if (!fileSystem) {
+ errorCallback(util.createFileError(FileError.NOT_FOUND_ERR));
+ return;
+ }
+
+ fileSystem.root.getDirectory(
+ mountPath.substring(1), // Strip leading '/'.
+ {create: false}, successCallback, errorCallback);
+ });
+};
+
+/**
+ * Builds the VolumeInfo data from VolumeMetadata.
+ * @param {VolumeMetadata} volumeMetadata Metadata instance for the volume.
+ * @param {function(VolumeInfo)} callback Called on completion.
+ */
+volumeManagerUtil.createVolumeInfo = function(volumeMetadata, callback) {
+ volumeManagerUtil.getRootEntry_(
+ volumeMetadata.mountPath,
+ function(entry) {
+ if (volumeMetadata.volumeType === util.VolumeType.DRIVE) {
+ // After file system is mounted, we "read" drive grand root
+ // entry at first. This triggers full feed fetch on background.
+ // Note: we don't need to handle errors here, because even if
+ // it fails, accessing to some path later will just become
+ // a fast-fetch and it re-triggers full-feed fetch.
+ entry.createReader().readEntries(
+ function() { /* do nothing */ },
+ function(error) {
+ console.error(
+ 'Triggering full feed fetch is failed: ' +
+ util.getFileErrorMnemonic(error.code));
+ });
+ }
+ callback(new VolumeInfo(
+ volumeMetadata.volumeType,
+ volumeMetadata.mountPath,
+ volumeMetadata.volumeId,
+ entry,
+ volumeMetadata.mountCondition,
+ volumeMetadata.deviceType,
+ volumeMetadata.isReadOnly,
+ volumeMetadata.profile));
+ },
+ function(fileError) {
+ console.error('Root entry is not found: ' +
+ volumeMetadata.mountPath + ', ' +
+ util.getFileErrorMnemonic(fileError.code));
+ callback(new VolumeInfo(
+ volumeMetadata.volumeType,
+ volumeMetadata.mountPath,
+ volumeMetadata.volumeId,
+ null, // Root entry is not found.
+ volumeMetadata.mountCondition,
+ volumeMetadata.deviceType,
+ volumeMetadata.isReadOnly,
+ volumeMetadata.profile));
+ });
+};
+
+/**
+ * The order of the volume list based on root type.
+ * @type {Array.<string>}
+ * @const
+ * @private
+ */
+volumeManagerUtil.volumeListOrder_ = [
+ RootType.DRIVE, RootType.DOWNLOADS, RootType.ARCHIVE, RootType.REMOVABLE
+];
+
+/**
+ * Compares mount paths to sort the volume list order.
+ * @param {string} mountPath1 The mount path for the first volume.
+ * @param {string} mountPath2 The mount path for the second volume.
+ * @return {number} 0 if mountPath1 and mountPath2 are same, -1 if VolumeInfo
+ * mounted at mountPath1 should be listed before the one mounted at
+ * mountPath2, otherwise 1.
+ */
+volumeManagerUtil.compareMountPath = function(mountPath1, mountPath2) {
+ var order1 = volumeManagerUtil.volumeListOrder_.indexOf(
+ PathUtil.getRootType(mountPath1));
+ var order2 = volumeManagerUtil.volumeListOrder_.indexOf(
+ PathUtil.getRootType(mountPath2));
+ if (order1 !== order2)
+ return order1 < order2 ? -1 : 1;
+
+ if (mountPath1 !== mountPath2)
+ return mountPath1 < mountPath2 ? -1 : 1;
+
+ // The path is same.
+ return 0;
+};
+
+/**
+ * The container of the VolumeInfo for each mounted volume.
+ * @constructor
+ */
+function VolumeInfoList() {
+ /**
+ * Holds VolumeInfo instances.
+ * @type {cr.ui.ArrayDataModel}
+ * @private
+ */
+ this.model_ = new cr.ui.ArrayDataModel([]);
+
+ Object.freeze(this);
+}
+
+VolumeInfoList.prototype = {
+ get length() { return this.model_.length; }
+};
+
+/**
+ * Adds the event listener to listen the change of volume info.
+ * @param {string} type The name of the event.
+ * @param {function(Event)} handler The handler for the event.
+ */
+VolumeInfoList.prototype.addEventListener = function(type, handler) {
+ this.model_.addEventListener(type, handler);
+};
+
+/**
+ * Removes the event listener.
+ * @param {string} type The name of the event.
+ * @param {function(Event)} handler The handler to be removed.
+ */
+VolumeInfoList.prototype.removeEventListener = function(type, handler) {
+ this.model_.removeEventListener(type, handler);
+};
+
+/**
+ * Adds the volumeInfo to the appropriate position. If there already exists,
+ * just replaces it.
+ * @param {VolumeInfo} volumeInfo The information of the new volume.
+ */
+VolumeInfoList.prototype.add = function(volumeInfo) {
+ var index = this.findLowerBoundIndex_(volumeInfo.mountPath);
+ if (index < this.length &&
+ this.item(index).mountPath === volumeInfo.mountPath) {
+ // Replace the VolumeInfo.
+ this.model_.splice(index, 1, volumeInfo);
+ } else {
+ // Insert the VolumeInfo.
+ this.model_.splice(index, 0, volumeInfo);
+ }
+};
+
+/**
+ * Removes the VolumeInfo of the volume mounted at mountPath.
+ * @param {string} mountPath The path to the location where the volume is
+ * mounted.
+ */
+VolumeInfoList.prototype.remove = function(mountPath) {
+ var index = this.findLowerBoundIndex_(mountPath);
+ if (index < this.length && this.item(index).mountPath === mountPath)
+ this.model_.splice(index, 1);
+};
+
+/**
+ * Searches the information of the volume mounted at mountPath.
+ * @param {string} mountPath The path to the location where the volume is
+ * mounted.
+ * @return {VolumeInfo} The volume's information, or null if not found.
+ */
+VolumeInfoList.prototype.find = function(mountPath) {
+ var index = this.findLowerBoundIndex_(mountPath);
+ if (index < this.length && this.item(index).mountPath === mountPath)
+ return this.item(index);
+
+ // Not found.
+ return null;
+};
+
+/**
+ * Searches the information of the volume that contains an item pointed by the
+ * path.
+ * @param {string} path Path pointing an entry on a volume.
+ * @return {VolumeInfo} The volume's information, or null if not found.
+ */
+VolumeInfoList.prototype.findByPath = function(path) {
+ for (var i = 0; i < this.length; i++) {
+ var mountPath = this.item(i).mountPath;
+ if (path === mountPath || path.indexOf(mountPath + '/') === 0)
+ return this.item(i);
+ }
+ return null;
+};
+
+/**
+ * @param {string} mountPath The mount path of searched volume.
+ * @return {number} The index of the volume if found, or the inserting
+ * position of the volume.
+ * @private
+ */
+VolumeInfoList.prototype.findLowerBoundIndex_ = function(mountPath) {
+ // Assuming the number of elements in the array data model is very small
+ // in most cases, use simple linear search, here.
+ for (var i = 0; i < this.length; i++) {
+ if (volumeManagerUtil.compareMountPath(
+ this.item(i).mountPath, mountPath) >= 0)
+ return i;
+ }
+ return this.length;
+};
+
+/**
+ * @param {number} index The index of the volume in the list.
+ * @return {VolumeInfo} The VolumeInfo instance.
+ */
+VolumeInfoList.prototype.item = function(index) {
+ return this.model_.item(index);
+};
+
+/**
+ * VolumeManager is responsible for tracking list of mounted volumes.
+ *
+ * @constructor
+ * @extends {cr.EventTarget}
+ */
+function VolumeManager() {
+ /**
+ * The list of archives requested to mount. We will show contents once
+ * archive is mounted, but only for mounts from within this filebrowser tab.
+ * @type {Object.<string, Object>}
+ * @private
+ */
+ this.requests_ = {};
+
+ /**
+ * The list of VolumeInfo instances for each mounted volume.
+ * @type {VolumeInfoList}
+ */
+ this.volumeInfoList = new VolumeInfoList();
+
+ // The status should be merged into VolumeManager.
+ // TODO(hidehiko): Remove them after the migration.
+ this.driveConnectionState_ = {
+ type: util.DriveConnectionType.OFFLINE,
+ reason: util.DriveConnectionReason.NO_SERVICE
+ };
+
+ chrome.fileBrowserPrivate.onDriveConnectionStatusChanged.addListener(
+ this.onDriveConnectionStatusChanged_.bind(this));
+ this.onDriveConnectionStatusChanged_();
+}
+
+/**
+ * Invoked when the drive connection status is changed.
+ * @private_
+ */
+VolumeManager.prototype.onDriveConnectionStatusChanged_ = function() {
+ chrome.fileBrowserPrivate.getDriveConnectionState(function(state) {
+ this.driveConnectionState_ = state;
+ cr.dispatchSimpleEvent(this, 'drive-connection-changed');
+ }.bind(this));
+};
+
+/**
+ * Returns the drive connection state.
+ * @return {util.DriveConnectionType} Connection type.
+ */
+VolumeManager.prototype.getDriveConnectionState = function() {
+ return this.driveConnectionState_;
+};
+
+/**
+ * VolumeManager extends cr.EventTarget.
+ */
+VolumeManager.prototype.__proto__ = cr.EventTarget.prototype;
+
+/**
+ * Time in milliseconds that we wait a response for. If no response on
+ * mount/unmount received the request supposed failed.
+ */
+VolumeManager.TIMEOUT = 15 * 60 * 1000;
+
+/**
+ * Queue to run getInstance sequentially.
+ * @type {AsyncUtil.Queue}
+ * @private
+ */
+VolumeManager.getInstanceQueue_ = new AsyncUtil.Queue();
+
+/**
+ * The singleton instance of VolumeManager. Initialized by the first invocation
+ * of getInstance().
+ * @type {VolumeManager}
+ * @private
+ */
+VolumeManager.instance_ = null;
+
+/**
+ * Returns the VolumeManager instance asynchronously. If it is not created or
+ * under initialization, it will waits for the finish of the initialization.
+ * @param {function(VolumeManager)} callback Called with the VolumeManager
+ * instance.
+ */
+VolumeManager.getInstance = function(callback) {
+ VolumeManager.getInstanceQueue_.run(function(continueCallback) {
+ if (VolumeManager.instance_) {
+ callback(VolumeManager.instance_);
+ continueCallback();
+ return;
+ }
+
+ VolumeManager.instance_ = new VolumeManager();
+ VolumeManager.instance_.initialize_(function() {
+ callback(VolumeManager.instance_);
+ continueCallback();
+ });
+ });
+};
+
+/**
+ * Initializes mount points.
+ * @param {function()} callback Called upon the completion of the
+ * initialization.
+ * @private
+ */
+VolumeManager.prototype.initialize_ = function(callback) {
+ chrome.fileBrowserPrivate.getVolumeMetadataList(function(volumeMetadataList) {
+ // Create VolumeInfo for each volume.
+ var group = new AsyncUtil.Group();
+ for (var i = 0; i < volumeMetadataList.length; i++) {
+ group.add(function(volumeMetadata, continueCallback) {
+ volumeManagerUtil.createVolumeInfo(
+ volumeMetadata,
+ function(volumeInfo) {
+ this.volumeInfoList.add(volumeInfo);
+ if (volumeMetadata.volumeType === util.VolumeType.DRIVE)
+ this.onDriveConnectionStatusChanged_();
+ continueCallback();
+ }.bind(this));
+ }.bind(this, volumeMetadataList[i]));
+ }
+
+ // Then, finalize the initialization.
+ group.run(function() {
+ // Subscribe to the mount completed event when mount points initialized.
+ chrome.fileBrowserPrivate.onMountCompleted.addListener(
+ this.onMountCompleted_.bind(this));
+ callback();
+ }.bind(this));
+ }.bind(this));
+};
+
+/**
+ * Event handler called when some volume was mounted or unmounted.
+ * @param {MountCompletedEvent} event Received event.
+ * @private
+ */
+VolumeManager.prototype.onMountCompleted_ = function(event) {
+ if (event.eventType === 'mount') {
+ if (event.volumeMetadata.mountPath) {
+ var requestKey = this.makeRequestKey_(
+ 'mount',
+ event.volumeMetadata.sourcePath);
+
+ var error = event.status === 'success' ? '' : event.status;
+
+ volumeManagerUtil.createVolumeInfo(
+ event.volumeMetadata,
+ function(volumeInfo) {
+ this.volumeInfoList.add(volumeInfo);
+ this.finishRequest_(requestKey, event.status, volumeInfo.mountPath);
+
+ if (volumeInfo.volumeType === util.VolumeType.DRIVE) {
+ // Update the network connection status, because until the
+ // drive is initialized, the status is set to not ready.
+ // TODO(hidehiko): The connection status should be migrated into
+ // VolumeMetadata.
+ this.onDriveConnectionStatusChanged_();
+ }
+ }.bind(this));
+ } else {
+ console.warn('No mount path.');
+ this.finishRequest_(requestKey, event.status);
+ }
+ } else if (event.eventType === 'unmount') {
+ var mountPath = event.volumeMetadata.mountPath;
+ var status = event.status;
+ if (status === util.VolumeError.PATH_UNMOUNTED) {
+ console.warn('Volume already unmounted: ', mountPath);
+ status = 'success';
+ }
+ var requestKey = this.makeRequestKey_('unmount', mountPath);
+ var requested = requestKey in this.requests_;
+ var volumeInfo = this.volumeInfoList.find(mountPath);
+ if (event.status === 'success' && !requested && volumeInfo) {
+ console.warn('Mounted volume without a request: ', mountPath);
+ var e = new Event('externally-unmounted');
+ // TODO(mtomasz): The mountPath field is deprecated. Remove it.
+ e.mountPath = mountPath;
+ e.volumeInfo = volumeInfo;
+ this.dispatchEvent(e);
+ }
+ this.finishRequest_(requestKey, status);
+
+ if (event.status === 'success')
+ this.volumeInfoList.remove(mountPath);
+ }
+};
+
+/**
+ * Creates string to match mount events with requests.
+ * @param {string} requestType 'mount' | 'unmount'. TODO(hidehiko): Replace by
+ * enum.
+ * @param {string} path Source path provided by API for mount request, or
+ * mount path for unmount request.
+ * @return {string} Key for |this.requests_|.
+ * @private
+ */
+VolumeManager.prototype.makeRequestKey_ = function(requestType, path) {
+ return requestType + ':' + path;
+};
+
+/**
+ * @param {string} fileUrl File url to the archive file.
+ * @param {function(string)} successCallback Success callback.
+ * @param {function(util.VolumeError)} errorCallback Error callback.
+ */
+VolumeManager.prototype.mountArchive = function(
+ fileUrl, successCallback, errorCallback) {
+ chrome.fileBrowserPrivate.addMount(fileUrl, function(sourcePath) {
+ console.info(
+ 'Mount request: url=' + fileUrl + '; sourceUrl=' + sourcePath);
+ var requestKey = this.makeRequestKey_('mount', sourcePath);
+ this.startRequest_(requestKey, successCallback, errorCallback);
+ }.bind(this));
+};
+
+/**
+ * Unmounts volume.
+ * @param {string} mountPath Volume mounted path.
+ * @param {function(string)} successCallback Success callback.
+ * @param {function(util.VolumeError)} errorCallback Error callback.
+ */
+VolumeManager.prototype.unmount = function(mountPath,
+ successCallback,
+ errorCallback) {
+ var volumeInfo = this.volumeInfoList.find(mountPath);
+ if (!volumeInfo) {
+ errorCallback(util.VolumeError.NOT_MOUNTED);
+ return;
+ }
+
+ chrome.fileBrowserPrivate.removeMount(util.makeFilesystemUrl(mountPath));
+ var requestKey = this.makeRequestKey_('unmount', volumeInfo.mountPath);
+ this.startRequest_(requestKey, successCallback, errorCallback);
+};
+
+/**
+ * Resolves the absolute path to its entry. Shouldn't be used outside of the
+ * Files app's initialization.
+ * @param {string} path The path to be resolved.
+ * @param {function(Entry)} successCallback Called with the resolved entry on
+ * success.
+ * @param {function(FileError)} errorCallback Called on error.
+ */
+VolumeManager.prototype.resolveAbsolutePath = function(
+ path, successCallback, errorCallback) {
+ // Make sure the path is in the mounted volume.
+ var volumeInfo = this.getVolumeInfo(path);
+ if (!volumeInfo || !volumeInfo.root) {
+ errorCallback(util.createFileError(FileError.NOT_FOUND_ERR));
+ return;
+ }
+
+ webkitResolveLocalFileSystemURL(
+ util.makeFilesystemUrl(path), successCallback, errorCallback);
+};
+
+/**
+ * Obtains the information of the volume that containing an entry pointed by the
+ * specified path.
+ * TODO(hirono): Stop to use path to get a volume info.
+ *
+ * @param {string|Entry} target Path or Entry pointing anywhere on a volume.
+ * @return {VolumeInfo} The data about the volume.
+ */
+VolumeManager.prototype.getVolumeInfo = function(target) {
+ if (typeof target === 'string')
+ return this.volumeInfoList.findByPath(target);
+ else if (util.isFakeEntry(target))
+ return this.getCurrentProfileVolumeInfo(util.VolumeType.DRIVE);
+ else
+ return this.volumeInfoList.findByPath(target.fullPath);
+};
+
+/**
+ * Obtains a volume information from a file entry URL.
+ * TODO(hirono): Check a file system to find a volume.
+ *
+ * @param {string} url URL of entry.
+ * @return {VolumeInfo} Volume info.
+ */
+VolumeManager.prototype.getVolumeInfoByURL = function(url) {
+ return this.getVolumeInfo(util.extractFilePath(url));
+};
+
+/**
+ * Obtains a volume infomration of the current profile.
+ *
+ * @param {util.VolumeType} volumeType Volume type.
+ * @return {VolumeInfo} Volume info.
+ */
+VolumeManager.prototype.getCurrentProfileVolumeInfo = function(volumeType) {
+ for (var i = 0; i < this.volumeInfoList.length; i++) {
+ var volumeInfo = this.volumeInfoList.item(i);
+ if (volumeInfo.profile.isCurrentProfile &&
+ volumeInfo.volumeType === volumeType)
+ return volumeInfo;
+ }
+ return null;
+};
+
+/**
+ * Obtains location information from an entry.
+ *
+ * @param {Entry|Object} entry File or directory entry. It can be a fake entry.
+ * @return {EntryLocation} Location information.
+ */
+VolumeManager.prototype.getLocationInfo = function(entry) {
+ if (util.isFakeEntry(entry)) {
+ return new EntryLocation(
+ // TODO(hirono): Specify currect volume.
+ this.getCurrentProfileVolumeInfo(RootType.DRIVE),
+ entry.rootType,
+ true /* the entry points a root directory. */);
+ } else {
+ return this.getLocationInfoByPath(entry.fullPath);
+ }
+};
+
+/**
+ * Obtains location information from a path.
+ * TODO(hirono): Remove the method before introducing separate file system.
+ *
+ * @param {string} path Path.
+ * @return {EntryLocation} Location information.
+ */
+VolumeManager.prototype.getLocationInfoByPath = function(path) {
+ var volumeInfo = this.volumeInfoList.findByPath(path);
+ return volumeInfo && PathUtil.getLocationInfo(volumeInfo, path);
+};
+
+/**
+ * @param {string} key Key produced by |makeRequestKey_|.
+ * @param {function(string)} successCallback To be called when request finishes
+ * successfully.
+ * @param {function(util.VolumeError)} errorCallback To be called when
+ * request fails.
+ * @private
+ */
+VolumeManager.prototype.startRequest_ = function(key,
+ successCallback, errorCallback) {
+ if (key in this.requests_) {
+ var request = this.requests_[key];
+ request.successCallbacks.push(successCallback);
+ request.errorCallbacks.push(errorCallback);
+ } else {
+ this.requests_[key] = {
+ successCallbacks: [successCallback],
+ errorCallbacks: [errorCallback],
+
+ timeout: setTimeout(this.onTimeout_.bind(this, key),
+ VolumeManager.TIMEOUT)
+ };
+ }
+};
+
+/**
+ * Called if no response received in |TIMEOUT|.
+ * @param {string} key Key produced by |makeRequestKey_|.
+ * @private
+ */
+VolumeManager.prototype.onTimeout_ = function(key) {
+ this.invokeRequestCallbacks_(this.requests_[key],
+ util.VolumeError.TIMEOUT);
+ delete this.requests_[key];
+};
+
+/**
+ * @param {string} key Key produced by |makeRequestKey_|.
+ * @param {util.VolumeError|'success'} status Status received from the API.
+ * @param {string=} opt_mountPath Mount path.
+ * @private
+ */
+VolumeManager.prototype.finishRequest_ = function(key, status, opt_mountPath) {
+ var request = this.requests_[key];
+ if (!request)
+ return;
+
+ clearTimeout(request.timeout);
+ this.invokeRequestCallbacks_(request, status, opt_mountPath);
+ delete this.requests_[key];
+};
+
+/**
+ * @param {Object} request Structure created in |startRequest_|.
+ * @param {util.VolumeError|string} status If status === 'success'
+ * success callbacks are called.
+ * @param {string=} opt_mountPath Mount path. Required if success.
+ * @private
+ */
+VolumeManager.prototype.invokeRequestCallbacks_ = function(request, status,
+ opt_mountPath) {
+ var callEach = function(callbacks, self, args) {
+ for (var i = 0; i < callbacks.length; i++) {
+ callbacks[i].apply(self, args);
+ }
+ };
+ if (status === 'success') {
+ callEach(request.successCallbacks, this, [opt_mountPath]);
+ } else {
+ volumeManagerUtil.validateError(status);
+ callEach(request.errorCallbacks, this, [status]);
+ }
+};
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/archive.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/archive.png
new file mode 100644
index 00000000000..2a054b38515
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/archive.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/archive_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/archive_white.png
new file mode 100644
index 00000000000..94ffaf0f310
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/archive_white.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/audio.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/audio.png
new file mode 100644
index 00000000000..a5b4ac9e774
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/audio.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/audio_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/audio_white.png
new file mode 100644
index 00000000000..819e4065021
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/audio_white.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/chart.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/chart.png
new file mode 100644
index 00000000000..4f7bfa145eb
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/chart.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/chart_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/chart_white.png
new file mode 100644
index 00000000000..7500f3f83f6
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/chart_white.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/excel.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/excel.png
new file mode 100644
index 00000000000..cb27f5c242c
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/excel.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/excel_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/excel_white.png
new file mode 100644
index 00000000000..0900b09a741
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/excel_white.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/folder.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/folder.png
new file mode 100644
index 00000000000..3f6e17f0892
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/folder.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/folder_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/folder_white.png
new file mode 100644
index 00000000000..9a56eb9b8d8
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/folder_white.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/form.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/form.png
new file mode 100644
index 00000000000..c3d2087db8a
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/form.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/form_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/form_white.png
new file mode 100644
index 00000000000..21c18adf387
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/form_white.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gdoc.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gdoc.png
new file mode 100644
index 00000000000..102f1c0f432
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gdoc.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gdoc_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gdoc_white.png
new file mode 100644
index 00000000000..a66a338421f
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gdoc_white.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gdraw.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gdraw.png
new file mode 100644
index 00000000000..b1120418420
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gdraw.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gdraw_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gdraw_white.png
new file mode 100644
index 00000000000..f92f18b5f1e
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gdraw_white.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/generic.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/generic.png
new file mode 100644
index 00000000000..bff3cdacba7
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/generic.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/generic_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/generic_white.png
new file mode 100644
index 00000000000..a369f66eabc
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/generic_white.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/glink.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/glink.png
new file mode 100644
index 00000000000..bff3cdacba7
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/glink.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/glink_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/glink_white.png
new file mode 100644
index 00000000000..a369f66eabc
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/glink_white.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gsheet.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gsheet.png
new file mode 100644
index 00000000000..6fbd46b3052
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gsheet.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gsheet_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gsheet_white.png
new file mode 100644
index 00000000000..39cf43df5ff
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gsheet_white.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gslides.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gslides.png
new file mode 100644
index 00000000000..76523cc9746
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gslides.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gslides_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gslides_white.png
new file mode 100644
index 00000000000..ea14b91430c
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gslides_white.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gtable.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gtable.png
new file mode 100644
index 00000000000..a78b26e54f7
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gtable.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gtable_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gtable_white.png
new file mode 100644
index 00000000000..9198c9e99ee
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/gtable_white.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/image.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/image.png
new file mode 100644
index 00000000000..9ec2dbd5cb7
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/image.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/image_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/image_white.png
new file mode 100644
index 00000000000..739614ffc0a
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/image_white.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/pdf.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/pdf.png
new file mode 100644
index 00000000000..ec6d679f5c9
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/pdf.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/pdf_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/pdf_white.png
new file mode 100644
index 00000000000..076820461c0
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/pdf_white.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/ppt.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/ppt.png
new file mode 100644
index 00000000000..0a834451830
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/ppt.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/ppt_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/ppt_white.png
new file mode 100644
index 00000000000..f18491debb3
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/ppt_white.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/script.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/script.png
new file mode 100644
index 00000000000..054f59a78fc
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/script.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/script_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/script_white.png
new file mode 100644
index 00000000000..2760eb805b9
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/script_white.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/sites.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/sites.png
new file mode 100644
index 00000000000..e4e0ffe720f
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/sites.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/sites_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/sites_white.png
new file mode 100644
index 00000000000..11646f82866
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/sites_white.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/video.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/video.png
new file mode 100644
index 00000000000..73b70f63b23
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/video.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/video_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/video_white.png
new file mode 100644
index 00000000000..ac1c0806c9f
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/video_white.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/word.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/word.png
new file mode 100644
index 00000000000..a2bf315f901
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/word.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/word_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/word_white.png
new file mode 100644
index 00000000000..9bc0f21f7d7
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/100/word_white.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/archive.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/archive.png
new file mode 100644
index 00000000000..f51418150f7
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/archive.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/archive_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/archive_white.png
new file mode 100644
index 00000000000..6b522cf0c7b
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/archive_white.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/audio.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/audio.png
new file mode 100644
index 00000000000..ad2f540c2c3
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/audio.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/audio_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/audio_white.png
new file mode 100644
index 00000000000..997d6106b00
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/audio_white.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/chart.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/chart.png
new file mode 100644
index 00000000000..5b03c369a1a
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/chart.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/chart_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/chart_white.png
new file mode 100644
index 00000000000..366fc85f3ac
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/chart_white.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/excel.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/excel.png
new file mode 100644
index 00000000000..22a4eb5d6fd
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/excel.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/excel_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/excel_white.png
new file mode 100644
index 00000000000..bf0a61c07b5
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/excel_white.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/folder.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/folder.png
new file mode 100644
index 00000000000..782227f9bde
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/folder.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/folder_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/folder_white.png
new file mode 100644
index 00000000000..9124621632c
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/folder_white.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/form.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/form.png
new file mode 100644
index 00000000000..7908b559402
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/form.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/form_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/form_white.png
new file mode 100644
index 00000000000..0252d4c94fb
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/form_white.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gdoc.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gdoc.png
new file mode 100644
index 00000000000..80ae026b49d
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gdoc.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gdoc_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gdoc_white.png
new file mode 100644
index 00000000000..366180cbea4
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gdoc_white.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gdraw.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gdraw.png
new file mode 100644
index 00000000000..c36fca643fe
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gdraw.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gdraw_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gdraw_white.png
new file mode 100644
index 00000000000..98401d94722
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gdraw_white.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/generic.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/generic.png
new file mode 100644
index 00000000000..8df85a372f2
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/generic.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/generic_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/generic_white.png
new file mode 100644
index 00000000000..9b9b295bbac
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/generic_white.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/glink.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/glink.png
new file mode 100644
index 00000000000..d60d809d64f
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/glink.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/glink_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/glink_white.png
new file mode 100644
index 00000000000..d9a2070240e
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/glink_white.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gsheet.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gsheet.png
new file mode 100644
index 00000000000..0545e69dea8
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gsheet.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gsheet_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gsheet_white.png
new file mode 100644
index 00000000000..801de01d657
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gsheet_white.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gslides.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gslides.png
new file mode 100644
index 00000000000..3f1cd75575c
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gslides.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gslides_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gslides_white.png
new file mode 100644
index 00000000000..b6c24ab20f8
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gslides_white.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gtable.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gtable.png
new file mode 100644
index 00000000000..fec2061ef44
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gtable.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gtable_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gtable_white.png
new file mode 100644
index 00000000000..8c695c58aaa
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/gtable_white.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/image.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/image.png
new file mode 100644
index 00000000000..df61101582d
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/image.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/image_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/image_white.png
new file mode 100644
index 00000000000..46017e6895c
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/image_white.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/pdf.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/pdf.png
new file mode 100644
index 00000000000..0f62d9e620d
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/pdf.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/pdf_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/pdf_white.png
new file mode 100644
index 00000000000..e53bda7a989
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/pdf_white.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/ppt.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/ppt.png
new file mode 100644
index 00000000000..5f06e4bffd3
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/ppt.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/ppt_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/ppt_white.png
new file mode 100644
index 00000000000..9824222e668
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/ppt_white.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/script.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/script.png
new file mode 100644
index 00000000000..01a2a1ad4b6
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/script.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/script_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/script_white.png
new file mode 100644
index 00000000000..b04a21002f1
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/script_white.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/sites.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/sites.png
new file mode 100644
index 00000000000..541c328f7fe
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/sites.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/sites_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/sites_white.png
new file mode 100644
index 00000000000..613e5178f1c
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/sites_white.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/video.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/video.png
new file mode 100644
index 00000000000..a8346a3f200
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/video.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/video_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/video_white.png
new file mode 100644
index 00000000000..3cbcdba263d
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/video_white.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/word.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/word.png
new file mode 100644
index 00000000000..23b78f6782d
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/word.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/word_white.png b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/word_white.png
new file mode 100644
index 00000000000..424ea80a12f
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/file_types/200/word_white.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/icon128.png b/chromium/chrome/browser/resources/file_manager/common/images/icon128.png
new file mode 100644
index 00000000000..d4c6c555040
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/icon128.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/icon16.png b/chromium/chrome/browser/resources/file_manager/common/images/icon16.png
new file mode 100644
index 00000000000..dc3b8d8b399
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/icon16.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/icon256.png b/chromium/chrome/browser/resources/file_manager/common/images/icon256.png
new file mode 100644
index 00000000000..31864fbe0bc
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/icon256.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/icon32.png b/chromium/chrome/browser/resources/file_manager/common/images/icon32.png
new file mode 100644
index 00000000000..47b8a55acf1
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/icon32.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/icon48.png b/chromium/chrome/browser/resources/file_manager/common/images/icon48.png
new file mode 100644
index 00000000000..b3c7dbfe691
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/icon48.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/icon64.png b/chromium/chrome/browser/resources/file_manager/common/images/icon64.png
new file mode 100644
index 00000000000..3130c1c4059
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/icon64.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/images/icon96.png b/chromium/chrome/browser/resources/file_manager/common/images/icon96.png
new file mode 100644
index 00000000000..fdc1ab1fec2
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/images/icon96.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/common/js/async_util.js b/chromium/chrome/browser/resources/file_manager/common/js/async_util.js
new file mode 100644
index 00000000000..f74a9d49381
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/js/async_util.js
@@ -0,0 +1,262 @@
+// Copyright (c) 2013 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.
+
+'use strict';
+
+/**
+ * Namespace for async utility functions.
+ */
+var AsyncUtil = {};
+
+/**
+ * Asynchronous version of Array.forEach.
+ * This executes a provided function callback once per array element, then
+ * run completionCallback to notify the completion.
+ * The callback can be an asynchronous function, but the execution is
+ * sequentially done.
+ *
+ * @param {Array.<T>} array The array to be iterated.
+ * @param {function(function(), T, number, Array.<T>} callback The iteration
+ * callback. The first argument is a callback to notify the completion of
+ * the iteration.
+ * @param {function()} completionCallback Called when all iterations are
+ * completed.
+ * @param {Object=} opt_thisObject Bound to callback if given.
+ * @template T
+ */
+AsyncUtil.forEach = function(
+ array, callback, completionCallback, opt_thisObject) {
+ if (opt_thisObject)
+ callback = callback.bind(opt_thisObject);
+
+ var queue = new AsyncUtil.Queue();
+ for (var i = 0; i < array.length; i++) {
+ queue.run(function(element, index, iterationCompletionCallback) {
+ callback(iterationCompletionCallback, element, index, array);
+ }.bind(null, array[i], i));
+ }
+ queue.run(function(iterationCompletionCallback) {
+ completionCallback(); // Don't pass iteration completion callback.
+ });
+};
+
+/**
+ * Creates a class for executing several asynchronous closures in a fifo queue.
+ * Added tasks will be executed sequentially in order they were added.
+ *
+ * @constructor
+ */
+AsyncUtil.Queue = function() {
+ this.running_ = false;
+ this.closures_ = [];
+};
+
+/**
+ * @return {boolean} True when a task is running, otherwise false.
+ */
+AsyncUtil.Queue.prototype.isRunning = function() {
+ return this.running_;
+};
+
+/**
+ * Enqueues a closure to be executed.
+ * @param {function(function())} closure Closure with a completion callback to
+ * be executed.
+ */
+AsyncUtil.Queue.prototype.run = function(closure) {
+ this.closures_.push(closure);
+ if (!this.running_)
+ this.continue_();
+};
+
+/**
+ * Serves the next closure from the queue.
+ * @private
+ */
+AsyncUtil.Queue.prototype.continue_ = function() {
+ if (!this.closures_.length) {
+ this.running_ = false;
+ return;
+ }
+
+ // Run the next closure.
+ this.running_ = true;
+ var closure = this.closures_.shift();
+ closure(this.continue_.bind(this));
+};
+
+/**
+ * Cancels all pending tasks. Note that this does NOT cancel the task running
+ * currently.
+ */
+AsyncUtil.Queue.prototype.cancel = function() {
+ this.closures_ = [];
+};
+
+/**
+ * Creates a class for executing several asynchronous closures in a group in
+ * a dependency order.
+ *
+ * @constructor
+ */
+AsyncUtil.Group = function() {
+ this.addedTasks_ = {};
+ this.pendingTasks_ = {};
+ this.finishedTasks_ = {};
+ this.completionCallbacks_ = [];
+};
+
+/**
+ * Enqueues a closure to be executed after dependencies are completed.
+ *
+ * @param {function(function())} closure Closure with a completion callback to
+ * be executed.
+ * @param {Array.<string>=} opt_dependencies Array of dependencies. If no
+ * dependencies, then the the closure will be executed immediately.
+ * @param {string=} opt_name Task identifier. Specify to use in dependencies.
+ */
+AsyncUtil.Group.prototype.add = function(closure, opt_dependencies, opt_name) {
+ var length = Object.keys(this.addedTasks_).length;
+ var name = opt_name || ('(unnamed#' + (length + 1) + ')');
+
+ var task = {
+ closure: closure,
+ dependencies: opt_dependencies || [],
+ name: name
+ };
+
+ this.addedTasks_[name] = task;
+ this.pendingTasks_[name] = task;
+};
+
+/**
+ * Runs the enqueued closured in order of dependencies.
+ *
+ * @param {function()=} opt_onCompletion Completion callback.
+ */
+AsyncUtil.Group.prototype.run = function(opt_onCompletion) {
+ if (opt_onCompletion)
+ this.completionCallbacks_.push(opt_onCompletion);
+ this.continue_();
+};
+
+/**
+ * Runs enqueued pending tasks whose dependencies are completed.
+ * @private
+ */
+AsyncUtil.Group.prototype.continue_ = function() {
+ // If all of the added tasks have finished, then call completion callbacks.
+ if (Object.keys(this.addedTasks_).length ==
+ Object.keys(this.finishedTasks_).length) {
+ for (var index = 0; index < this.completionCallbacks_.length; index++) {
+ var callback = this.completionCallbacks_[index];
+ callback();
+ }
+ this.completionCallbacks_ = [];
+ return;
+ }
+
+ for (var name in this.pendingTasks_) {
+ var task = this.pendingTasks_[name];
+ var dependencyMissing = false;
+ for (var index = 0; index < task.dependencies.length; index++) {
+ var dependency = task.dependencies[index];
+ // Check if the dependency has finished.
+ if (!this.finishedTasks_[dependency])
+ dependencyMissing = true;
+ }
+ // All dependences finished, therefore start the task.
+ if (!dependencyMissing) {
+ delete this.pendingTasks_[task.name];
+ task.closure(this.finish_.bind(this, task));
+ }
+ }
+};
+
+/**
+ * Finishes the passed task and continues executing enqueued closures.
+ *
+ * @param {Object} task Task object.
+ * @private
+ */
+AsyncUtil.Group.prototype.finish_ = function(task) {
+ this.finishedTasks_[task.name] = task;
+ this.continue_();
+};
+
+/**
+ * Aggregates consecutive calls and executes the closure only once instead of
+ * several times. The first call is always called immediately, and the next
+ * consecutive ones are aggregated and the closure is called only once once
+ * |delay| amount of time passes after the last call to run().
+ *
+ * @param {function()} closure Closure to be aggregated.
+ * @param {number=} opt_delay Minimum aggregation time in milliseconds. Default
+ * is 50 milliseconds.
+ * @constructor
+ */
+AsyncUtil.Aggregation = function(closure, opt_delay) {
+ /**
+ * @type {number}
+ * @private
+ */
+ this.delay_ = opt_delay || 50;
+
+ /**
+ * @type {function()}
+ * @private
+ */
+ this.closure_ = closure;
+
+ /**
+ * @type {number?}
+ * @private
+ */
+ this.scheduledRunsTimer_ = null;
+
+ /**
+ * @type {number}
+ * @private
+ */
+ this.lastRunTime_ = 0;
+};
+
+/**
+ * Runs a closure. Skips consecutive calls. The first call is called
+ * immediately.
+ */
+AsyncUtil.Aggregation.prototype.run = function() {
+ // If recently called, then schedule the consecutive call with a delay.
+ if (Date.now() - this.lastRunTime_ < this.delay_) {
+ this.cancelScheduledRuns_();
+ this.scheduledRunsTimer_ = setTimeout(this.runImmediately_.bind(this),
+ this.delay_ + 1);
+ this.lastRunTime_ = Date.now();
+ return;
+ }
+
+ // Otherwise, run immediately.
+ this.runImmediately_();
+};
+
+/**
+ * Calls the schedule immediately and cancels any scheduled calls.
+ * @private
+ */
+AsyncUtil.Aggregation.prototype.runImmediately_ = function() {
+ this.cancelScheduledRuns_();
+ this.closure_();
+ this.lastRunTime_ = Date.now();
+};
+
+/**
+ * Cancels all scheduled runs (if any).
+ * @private
+ */
+AsyncUtil.Aggregation.prototype.cancelScheduledRuns_ = function() {
+ if (this.scheduledRunsTimer_) {
+ clearTimeout(this.scheduledRunsTimer_);
+ this.scheduledRunsTimer_ = null;
+ }
+};
diff --git a/chromium/chrome/browser/resources/file_manager/common/js/path_util.js b/chromium/chrome/browser/resources/file_manager/common/js/path_util.js
new file mode 100644
index 00000000000..3b0d4663cab
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/js/path_util.js
@@ -0,0 +1,471 @@
+// 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.
+
+'use strict';
+
+/**
+ * Type of a root directory.
+ * @enum {string}
+ * @const
+ */
+var RootType = Object.freeze({
+ // Root of local directory.
+ DOWNLOADS: 'downloads',
+
+ // Root of mounted archive file.
+ ARCHIVE: 'archive',
+
+ // Root of removal volume.
+ REMOVABLE: 'removable',
+
+ // Root of drive directory.
+ DRIVE: 'drive',
+
+ // Root for entries that is not located under RootType.DRIVE. e.g. shared
+ // files.
+ DRIVE_OTHER: 'drive_other',
+
+ // Fake root for offline available files on the drive.
+ DRIVE_OFFLINE: 'drive_offline',
+
+ // Fake root for shared files on the drive.
+ DRIVE_SHARED_WITH_ME: 'drive_shared_with_me',
+
+ // Fake root for recent files on the drive.
+ DRIVE_RECENT: 'drive_recent'
+});
+
+/**
+ * Top directory for each root type.
+ * TODO(mtomasz): Deprecated. Remove this.
+ * @enum {string}
+ * @const
+ */
+var RootDirectory = Object.freeze({
+ DOWNLOADS: '/Downloads',
+ ARCHIVE: '/archive',
+ REMOVABLE: '/removable',
+ DRIVE: '/drive',
+ DRIVE_OFFLINE: '/drive_offline', // A fake root. Not the actual filesystem.
+ DRIVE_SHARED_WITH_ME: '/drive_shared_with_me', // A fake root.
+ DRIVE_RECENT: '/drive_recent' // A fake root.
+});
+
+/**
+ * Sub root directory for Drive. "root" and "other". This is not used now.
+ * TODO(haruki): Add namespaces support. http://crbug.com/174233.
+ * @enum {string}
+ * @const
+ */
+var DriveSubRootDirectory = Object.freeze({
+ ROOT: 'root',
+ OTHER: 'other',
+});
+
+var PathUtil = {};
+
+/**
+ * The default mount point.
+ * TODO(mtomasz): Deprecated. Use the volume manager instead.
+ * @type {string}
+ * @const
+ */
+PathUtil.DEFAULT_MOUNT_POINT = '/Downloads';
+
+/**
+ * Checks if the given path represents a special search. Fake entries in
+ * RootDirectory correspond to special searches.
+ * @param {string} path Path to check.
+ * @return {boolean} True if the given path represents a special search.
+ */
+PathUtil.isSpecialSearchRoot = function(path) {
+ var type = PathUtil.getRootType(path);
+ return type == RootType.DRIVE_OFFLINE ||
+ type == RootType.DRIVE_SHARED_WITH_ME ||
+ type == RootType.DRIVE_RECENT;
+};
+
+/**
+ * Checks |path| and return true if it is under Google Drive or a special
+ * search root which represents a special search from Google Drive.
+ * @param {string} path Path to check.
+ * @return {boolean} True if the given path represents a Drive based path.
+ */
+PathUtil.isDriveBasedPath = function(path) {
+ var rootType = PathUtil.getRootType(path);
+ return rootType === RootType.DRIVE ||
+ rootType === RootType.DRIVE_SHARED_WITH_ME ||
+ rootType === RootType.DRIVE_RECENT ||
+ rootType === RootType.DRIVE_OFFLINE;
+};
+
+/**
+ * @param {string} path Path starting with '/'.
+ * @return {string} Top directory (starting with '/').
+ */
+PathUtil.getTopDirectory = function(path) {
+ var i = path.indexOf('/', 1);
+ return i === -1 ? path : path.substring(0, i);
+};
+
+/**
+ * Obtains the parent path of the specified path.
+ * @param {string} path Path string.
+ * @return {string} Parent path.
+ */
+PathUtil.getParentDirectory = function(path) {
+ if (path[path.length - 1] == '/')
+ return PathUtil.getParentDirectory(path.substring(0, path.length - 1));
+ var index = path.lastIndexOf('/');
+ if (index == 0)
+ return '/';
+ else if (index == -1)
+ return '.';
+ return path.substring(0, index);
+};
+
+/**
+ * @param {string} path Any unix-style path (may start or not start from root).
+ * @return {Array.<string>} Path components.
+ */
+PathUtil.split = function(path) {
+ var fromRoot = false;
+ if (path[0] === '/') {
+ fromRoot = true;
+ path = path.substring(1);
+ }
+
+ var components = path.split('/');
+ if (fromRoot)
+ components[0] = '/' + components[0];
+ return components;
+};
+
+/**
+ * Returns a directory part of the given |path|. In other words, the path
+ * without its base name.
+ *
+ * Examples:
+ * PathUtil.dirname('abc') -> ''
+ * PathUtil.dirname('a/b') -> 'a'
+ * PathUtil.dirname('a/b/') -> 'a/b'
+ * PathUtil.dirname('a/b/c') -> 'a/b'
+ * PathUtil.dirname('/') -> '/'
+ * PathUtil.dirname('/abc') -> '/'
+ * PathUtil.dirname('/abc/def') -> '/abc'
+ * PathUtil.dirname('') -> ''
+ *
+ * @param {string} path The path to be parsed.
+ * @return {string} The directory path.
+ */
+PathUtil.dirname = function(path) {
+ var index = path.lastIndexOf('/');
+ if (index < 0)
+ return '';
+ if (index == 0)
+ return '/';
+ return path.substring(0, index);
+};
+
+/**
+ * Returns the base name (the last component) of the given |path|. If the
+ * |path| ends with '/', returns an empty component.
+ *
+ * Examples:
+ * PathUtil.basename('abc') -> 'abc'
+ * PathUtil.basename('a/b') -> 'b'
+ * PathUtil.basename('a/b/') -> ''
+ * PathUtil.basename('a/b/c') -> 'c'
+ * PathUtil.basename('/') -> ''
+ * PathUtil.basename('/abc') -> 'abc'
+ * PathUtil.basename('/abc/def') -> 'def'
+ * PathUtil.basename('') -> ''
+ *
+ * @param {string} path The path to be parsed.
+ * @return {string} The base name.
+ */
+PathUtil.basename = function(path) {
+ var index = path.lastIndexOf('/');
+ return index >= 0 ? path.substring(index + 1) : path;
+};
+
+/**
+ * Join path components into a single path. Can be called either with a list of
+ * components as arguments, or with an array of components as the only argument.
+ *
+ * Examples:
+ * Path.join('abc', 'def') -> 'abc/def'
+ * Path.join('/', 'abc', 'def/ghi') -> '/abc/def/ghi'
+ * Path.join(['/abc/def', 'ghi']) -> '/abc/def/ghi'
+ *
+ * @return {string} Resulting path.
+ */
+PathUtil.join = function() {
+ var components;
+
+ if (arguments.length === 1 && typeof(arguments[0]) === 'object') {
+ components = arguments[0];
+ } else {
+ components = arguments;
+ }
+
+ var path = '';
+ for (var i = 0; i < components.length; i++) {
+ if (components[i][0] === '/') {
+ path = components[i];
+ continue;
+ }
+ if (path.length === 0 || path[path.length - 1] !== '/')
+ path += '/';
+ path += components[i];
+ }
+ return path;
+};
+
+/**
+ * @param {string} path Path starting with '/'.
+ * @return {RootType} RootType.DOWNLOADS, RootType.DRIVE etc.
+ */
+PathUtil.getRootType = function(path) {
+ var rootDir = PathUtil.getTopDirectory(path);
+ for (var type in RootDirectory) {
+ if (rootDir === RootDirectory[type])
+ return RootType[type];
+ }
+};
+
+/**
+ * @param {string} path Any path.
+ * @return {string} The root path.
+ */
+PathUtil.getRootPath = function(path) {
+ var type = PathUtil.getRootType(path);
+
+ if (type == RootType.DOWNLOADS || type == RootType.DRIVE_OFFLINE ||
+ type == RootType.DRIVE_SHARED_WITH_ME || type == RootType.DRIVE_RECENT)
+ return PathUtil.getTopDirectory(path);
+
+ if (type == RootType.DRIVE || type == RootType.ARCHIVE ||
+ type == RootType.REMOVABLE) {
+ var components = PathUtil.split(path);
+ if (components.length > 1) {
+ return PathUtil.join(components[0], components[1]);
+ } else {
+ return components[0];
+ }
+ }
+
+ return '/';
+};
+
+/**
+ * @param {string} path A path.
+ * @return {boolean} True if it is a path to the root.
+ */
+PathUtil.isRootPath = function(path) {
+ return PathUtil.getRootPath(path) === path;
+};
+
+/**
+ * @param {string} path A root path.
+ * @return {boolean} True if the given path is root and user can unmount it.
+ */
+PathUtil.isUnmountableByUser = function(path) {
+ if (!PathUtil.isRootPath(path))
+ return false;
+
+ var type = PathUtil.getRootType(path);
+ return (type == RootType.ARCHIVE || type == RootType.REMOVABLE);
+};
+
+/**
+ * @param {string} parent_path The parent path.
+ * @param {string} child_path The child path.
+ * @return {boolean} True if |parent_path| is parent file path of |child_path|.
+ */
+PathUtil.isParentPath = function(parent_path, child_path) {
+ if (!parent_path || parent_path.length == 0 ||
+ !child_path || child_path.length == 0)
+ return false;
+
+ if (parent_path[parent_path.length - 1] != '/')
+ parent_path += '/';
+
+ if (child_path[child_path.length - 1] != '/')
+ child_path += '/';
+
+ return child_path.indexOf(parent_path) == 0;
+};
+
+/**
+ * Return the localized name for the root.
+ * TODO(hirono): Support all RootTypes and stop to use paths.
+ *
+ * @param {string|RootType} path The full path of the root (starting with slash)
+ * or root type.
+ * @return {string} The localized name.
+ */
+PathUtil.getRootLabel = function(path) {
+ var str = function(id) {
+ return loadTimeData.getString(id);
+ };
+
+ if (path === RootDirectory.DOWNLOADS)
+ return str('DOWNLOADS_DIRECTORY_LABEL');
+
+ if (path === RootDirectory.ARCHIVE)
+ return str('ARCHIVE_DIRECTORY_LABEL');
+ if (PathUtil.isParentPath(RootDirectory.ARCHIVE, path))
+ return path.substring(RootDirectory.ARCHIVE.length + 1);
+
+ if (path === RootDirectory.REMOVABLE)
+ return str('REMOVABLE_DIRECTORY_LABEL');
+ if (PathUtil.isParentPath(RootDirectory.REMOVABLE, path))
+ return path.substring(RootDirectory.REMOVABLE.length + 1);
+
+ // TODO(haruki): Add support for "drive/root" and "drive/other".
+ if (path === RootDirectory.DRIVE + '/' + DriveSubRootDirectory.ROOT)
+ return str('DRIVE_MY_DRIVE_LABEL');
+
+ if (path === RootDirectory.DRIVE_OFFLINE)
+ return str('DRIVE_OFFLINE_COLLECTION_LABEL');
+
+ if (path === RootDirectory.DRIVE_SHARED_WITH_ME ||
+ path === RootType.DRIVE_SHARED_WITH_ME)
+ return str('DRIVE_SHARED_WITH_ME_COLLECTION_LABEL');
+
+ if (path === RootDirectory.DRIVE_RECENT)
+ return str('DRIVE_RECENT_COLLECTION_LABEL');
+
+ return path;
+};
+
+/**
+ * Return the label of the folder to be shown. Eg.
+ * - '/foo/bar/baz' -> 'baz'
+ * - '/hoge/fuga/ -> 'fuga'
+ * If the directory is root, returns the root label, which is same as
+ * PathUtil.getRootLabel().
+ *
+ * @param {string} directoryPath The full path of the folder.
+ * @return {string} The label to be shown.
+ */
+PathUtil.getFolderLabel = function(directoryPath) {
+ var label = '';
+ if (PathUtil.isRootPath(directoryPath))
+ label = PathUtil.getRootLabel(directoryPath);
+
+ if (label && label != directoryPath)
+ return label;
+
+ var matches = directoryPath.match(/([^\/]*)[\/]?$/);
+ if (matches[1])
+ return matches[1];
+
+ return directoryPath;
+};
+
+/**
+ * Returns if the given path can be a target path of folder shortcut.
+ *
+ * @param {string} directoryPath Directory path to be checked.
+ * @return {boolean} True if the path can be a target path of the shortcut.
+ */
+PathUtil.isEligibleForFolderShortcut = function(directoryPath) {
+ return !PathUtil.isSpecialSearchRoot(directoryPath) &&
+ !PathUtil.isRootPath(directoryPath) &&
+ PathUtil.isDriveBasedPath(directoryPath);
+};
+
+/**
+ * Extracts the extension of the path.
+ *
+ * Examples:
+ * PathUtil.splitExtension('abc.ext') -> ['abc', '.ext']
+ * PathUtil.splitExtension('a/b/abc.ext') -> ['a/b/abc', '.ext']
+ * PathUtil.splitExtension('a/b') -> ['a/b', '']
+ * PathUtil.splitExtension('.cshrc') -> ['', '.cshrc']
+ * PathUtil.splitExtension('a/b.backup/hoge') -> ['a/b.backup/hoge', '']
+ *
+ * @param {string} path Path to be extracted.
+ * @return {Array.<string>} Filename and extension of the given path.
+ */
+PathUtil.splitExtension = function(path) {
+ var dotPosition = path.lastIndexOf('.');
+ if (dotPosition <= path.lastIndexOf('/'))
+ dotPosition = -1;
+
+ var filename = dotPosition != -1 ? path.substr(0, dotPosition) : path;
+ var extension = dotPosition != -1 ? path.substr(dotPosition) : '';
+ return [filename, extension];
+};
+
+/**
+ * Obtains location information from a path.
+ *
+ * @param {!VolumeInfo} volumeInfo Volume containing an entry pointed by path.
+ * @param {string} fullPath Full path.
+ * @return {EntryLocation} Location information.
+ */
+PathUtil.getLocationInfo = function(volumeInfo, fullPath) {
+ var rootPath;
+ var rootType;
+ if (volumeInfo.volumeType === util.VolumeType.DRIVE) {
+ // If the volume is drive, root path can be either mountPath + '/root' or
+ // mountPath + '/other'.
+ if ((fullPath + '/').indexOf(volumeInfo.mountPath + '/root/') === 0) {
+ rootPath = volumeInfo.mountPath + '/root';
+ rootType = RootType.DRIVE;
+ } else if ((fullPath + '/').indexOf(
+ volumeInfo.mountPath + '/other/') === 0) {
+ rootPath = volumeInfo.mountPath + '/other';
+ rootType = RootType.DRIVE_OTHER;
+ } else {
+ throw new Error(fullPath + ' is an invalid drive path.');
+ }
+ } else {
+ // Otherwise, root path is same with a mount path of the volume.
+ rootPath = volumeInfo.mountPath;
+ switch (volumeInfo.volumeType) {
+ case util.VolumeType.DOWNLOADS: rootType = RootType.DOWNLOADS; break;
+ case util.VolumeType.REMOVABLE: rootType = RootType.REMOVABLE; break;
+ case util.VolumeType.ARCHIVE: rootType = RootType.ARCHIVE; break;
+ default: throw new Error(
+ 'Invalid volume type: ' + volumeInfo.volumeType);
+ }
+ }
+ var isRootEntry = (fullPath.substr(0, rootPath.length) || '/') === fullPath;
+ return new EntryLocation(volumeInfo, rootType, isRootEntry);
+};
+
+/**
+ * Location information which shows where the path points in FileManager's
+ * file system.
+ *
+ * @param {!VolumeInfo} volumeInfo Volume information.
+ * @param {RootType} rootType Root type.
+ * @param {boolean} isRootEntry Whether the entry is root entry or not.
+ * @constructor
+ */
+function EntryLocation(volumeInfo, rootType, isRootEntry) {
+ /**
+ * Volume information.
+ * @type {!VolumeInfo}
+ */
+ this.volumeInfo = volumeInfo;
+
+ /**
+ * Root type.
+ * @type {RootType}
+ */
+ this.rootType = rootType;
+
+ /**
+ * Whether the entry is root entry or not.
+ * @type {boolean}
+ */
+ this.isRootEntry = isRootEntry;
+
+ Object.freeze(this);
+}
diff --git a/chromium/chrome/browser/resources/file_manager/common/js/progress_center_common.js b/chromium/chrome/browser/resources/file_manager/common/js/progress_center_common.js
new file mode 100644
index 00000000000..f19d6cbb90b
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/js/progress_center_common.js
@@ -0,0 +1,149 @@
+// Copyright 2013 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.
+
+'use strict';
+
+/**
+ * Event of the ProgressCenter class.
+ * @enum {string}
+ * @const
+ */
+var ProgressCenterEvent = Object.freeze({
+ /**
+ * Background page notifies item update to application windows.
+ */
+ ITEM_UPDATED: 'itemUpdated',
+
+ /**
+ * Background page notifies all the items are cleared.
+ */
+ RESET: 'reset'
+});
+
+/**
+ * State of progress items.
+ * @enum {string}
+ * @const
+ */
+var ProgressItemState = Object.freeze({
+ PROGRESSING: 'progressing',
+ COMPLETED: 'completed',
+ ERROR: 'error',
+ CANCELED: 'canceled'
+});
+
+/**
+ * Type of progress items.
+ * @enum {string}
+ * @const
+ */
+var ProgressItemType = Object.freeze({
+ // The item is file copy operation.
+ COPY: 'copy',
+ // The item is file move operation.
+ MOVE: 'move',
+ // The item is file delete opeartion.
+ DELETE: 'delete',
+ // The item is file zip operation.
+ ZIP: 'zip',
+ // The item is general file transfer operation.
+ // This is used for the mixed operation of summarized item.
+ TRANSFER: 'transfer'
+});
+
+/**
+ * Item of the progress center.
+ * @constructor
+ */
+var ProgressCenterItem = function() {
+ /**
+ * Item ID.
+ * @type {string}
+ * @private
+ */
+ this.id_ = null;
+
+ /**
+ * State of the progress item.
+ * @type {ProgressItemState}
+ */
+ this.state = ProgressItemState.PROGRESSING;
+
+ /**
+ * Message of the progress item.
+ * @type {string}
+ */
+ this.message = '';
+
+ /**
+ * Max value of the progress.
+ * @type {number}
+ */
+ this.progressMax = 0;
+
+ /**
+ * Current value of the progress.
+ * @type {number}
+ */
+ this.progressValue = 0;
+
+ /**
+ * Type of progress item.
+ * @type {ProgressItemType}
+ */
+ this.type = null;
+
+ /**
+ * Whether the item is summarized item or not.
+ * @type {boolean}
+ */
+ this.summarized = false;
+
+ /**
+ * Callback function to cancel the item.
+ * @type {function()}
+ */
+ this.cancelCallback = null;
+
+ Object.seal(this);
+};
+
+ProgressCenterItem.prototype = {
+ /**
+ * Setter of Item ID.
+ * @param {string} value New value of ID.
+ */
+ set id(value) {
+ if (!this.id_)
+ this.id_ = value;
+ else
+ console.error('The ID is already set. (current ID: ' + this.id_ + ')');
+ },
+
+ /**
+ * Getter of Item ID.
+ * @return {string} Item ID.
+ */
+ get id() {
+ return this.id_;
+ },
+
+ /**
+ * Gets progress rate in percent.
+ * @return {number} Progress rate in percent.
+ */
+ get progressRateInPercent() {
+ return ~~(100 * this.progressValue / this.progressMax);
+ },
+
+ /**
+ * Whether the item can be canceled or not.
+ * @return {boolean} True if the item can be canceled.
+ */
+ get cancelable() {
+ return !!(this.state == ProgressItemState.PROGRESSING &&
+ this.cancelCallback &&
+ !this.summarized);
+ }
+};
diff --git a/chromium/chrome/browser/resources/file_manager/common/js/util.js b/chromium/chrome/browser/resources/file_manager/common/js/util.js
new file mode 100644
index 00000000000..07df6b81d88
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/common/js/util.js
@@ -0,0 +1,1259 @@
+// 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.
+
+'use strict';
+
+/**
+ * Namespace for utility functions.
+ */
+var util = {};
+
+/**
+ * Returns a function that console.log's its arguments, prefixed by |msg|.
+ *
+ * @param {string} msg The message prefix to use in the log.
+ * @param {function(...string)=} opt_callback A function to invoke after
+ * logging.
+ * @return {function(...string)} Function that logs.
+ */
+util.flog = function(msg, opt_callback) {
+ return function() {
+ var ary = Array.apply(null, arguments);
+ console.log(msg + ': ' + ary.join(', '));
+ if (opt_callback)
+ opt_callback.apply(null, arguments);
+ };
+};
+
+/**
+ * Returns a function that throws an exception that includes its arguments
+ * prefixed by |msg|.
+ *
+ * @param {string} msg The message prefix to use in the exception.
+ * @return {function(...string)} Function that throws.
+ */
+util.ferr = function(msg) {
+ return function() {
+ var ary = Array.apply(null, arguments);
+ throw new Error(msg + ': ' + ary.join(', '));
+ };
+};
+
+/**
+ * Install a sensible toString() on the FileError object.
+ *
+ * FileError.prototype.code is a numeric code describing the cause of the
+ * error. The FileError constructor has a named property for each possible
+ * error code, but provides no way to map the code to the named property.
+ * This toString() implementation fixes that.
+ */
+util.installFileErrorToString = function() {
+ FileError.prototype.toString = function() {
+ return '[object FileError: ' + util.getFileErrorMnemonic(this.code) + ']';
+ };
+};
+
+/**
+ * @param {number} code The file error code.
+ * @return {string} The file error mnemonic.
+ */
+util.getFileErrorMnemonic = function(code) {
+ for (var key in FileError) {
+ if (key.search(/_ERR$/) != -1 && FileError[key] == code)
+ return key;
+ }
+
+ return code;
+};
+
+/**
+ * @param {number} code File error code (from FileError object).
+ * @return {string} Translated file error string.
+ */
+util.getFileErrorString = function(code) {
+ for (var key in FileError) {
+ var match = /(.*)_ERR$/.exec(key);
+ if (match && FileError[key] == code) {
+ // This would convert 1 to 'NOT_FOUND'.
+ code = match[1];
+ break;
+ }
+ }
+ console.warn('File error: ' + code);
+ return loadTimeData.getString('FILE_ERROR_' + code) ||
+ loadTimeData.getString('FILE_ERROR_GENERIC');
+};
+
+/**
+ * @param {string} str String to escape.
+ * @return {string} Escaped string.
+ */
+util.htmlEscape = function(str) {
+ return str.replace(/[<>&]/g, function(entity) {
+ switch (entity) {
+ case '<': return '&lt;';
+ case '>': return '&gt;';
+ case '&': return '&amp;';
+ }
+ });
+};
+
+/**
+ * @param {string} str String to unescape.
+ * @return {string} Unescaped string.
+ */
+util.htmlUnescape = function(str) {
+ return str.replace(/&(lt|gt|amp);/g, function(entity) {
+ switch (entity) {
+ case '&lt;': return '<';
+ case '&gt;': return '>';
+ case '&amp;': return '&';
+ }
+ });
+};
+
+/**
+ * Iterates the entries contained by dirEntry, and invokes callback once for
+ * each entry. On completion, successCallback will be invoked.
+ *
+ * @param {DirectoryEntry} dirEntry The entry of the directory.
+ * @param {function(Entry, function())} callback Invoked for each entry.
+ * @param {function()} successCallback Invoked on completion.
+ * @param {function(FileError)} errorCallback Invoked if an error is found on
+ * directory entry reading.
+ */
+util.forEachDirEntry = function(
+ dirEntry, callback, successCallback, errorCallback) {
+ var reader = dirEntry.createReader();
+ var iterate = function() {
+ reader.readEntries(function(entries) {
+ if (entries.length == 0) {
+ successCallback();
+ return;
+ }
+
+ AsyncUtil.forEach(
+ entries,
+ function(forEachCallback, entry) {
+ // Do not pass index nor entries.
+ callback(entry, forEachCallback);
+ },
+ iterate);
+ }, errorCallback);
+ };
+ iterate();
+};
+
+/**
+ * Reads contents of directory.
+ * @param {DirectoryEntry} root Root entry.
+ * @param {string} path Directory path.
+ * @param {function(Array.<Entry>)} callback List of entries passed to callback.
+ */
+util.readDirectory = function(root, path, callback) {
+ var onError = function(e) {
+ callback([], e);
+ };
+ root.getDirectory(path, {create: false}, function(entry) {
+ var reader = entry.createReader();
+ var r = [];
+ var readNext = function() {
+ reader.readEntries(function(results) {
+ if (results.length == 0) {
+ callback(r, null);
+ return;
+ }
+ r.push.apply(r, results);
+ readNext();
+ }, onError);
+ };
+ readNext();
+ }, onError);
+};
+
+/**
+ * Utility function to resolve multiple directories with a single call.
+ *
+ * The successCallback will be invoked once for each directory object
+ * found. The errorCallback will be invoked once for each
+ * path that could not be resolved.
+ *
+ * The successCallback is invoked with a null entry when all paths have
+ * been processed.
+ *
+ * @param {DirEntry} dirEntry The base directory.
+ * @param {Object} params The parameters to pass to the underlying
+ * getDirectory calls.
+ * @param {Array.<string>} paths The list of directories to resolve.
+ * @param {function(!DirEntry)} successCallback The function to invoke for
+ * each DirEntry found. Also invoked once with null at the end of the
+ * process.
+ * @param {function(FileError)} errorCallback The function to invoke
+ * for each path that cannot be resolved.
+ */
+util.getDirectories = function(dirEntry, params, paths, successCallback,
+ errorCallback) {
+
+ // Copy the params array, since we're going to destroy it.
+ params = [].slice.call(params);
+
+ var onComplete = function() {
+ successCallback(null);
+ };
+
+ var getNextDirectory = function() {
+ var path = paths.shift();
+ if (!path)
+ return onComplete();
+
+ dirEntry.getDirectory(
+ path, params,
+ function(entry) {
+ successCallback(entry);
+ getNextDirectory();
+ },
+ function(err) {
+ errorCallback(err);
+ getNextDirectory();
+ });
+ };
+
+ getNextDirectory();
+};
+
+/**
+ * Utility function to resolve multiple files with a single call.
+ *
+ * The successCallback will be invoked once for each directory object
+ * found. The errorCallback will be invoked once for each
+ * path that could not be resolved.
+ *
+ * The successCallback is invoked with a null entry when all paths have
+ * been processed.
+ *
+ * @param {DirEntry} dirEntry The base directory.
+ * @param {Object} params The parameters to pass to the underlying
+ * getFile calls.
+ * @param {Array.<string>} paths The list of files to resolve.
+ * @param {function(!FileEntry)} successCallback The function to invoke for
+ * each FileEntry found. Also invoked once with null at the end of the
+ * process.
+ * @param {function(FileError)} errorCallback The function to invoke
+ * for each path that cannot be resolved.
+ */
+util.getFiles = function(dirEntry, params, paths, successCallback,
+ errorCallback) {
+ // Copy the params array, since we're going to destroy it.
+ params = [].slice.call(params);
+
+ var onComplete = function() {
+ successCallback(null);
+ };
+
+ var getNextFile = function() {
+ var path = paths.shift();
+ if (!path)
+ return onComplete();
+
+ dirEntry.getFile(
+ path, params,
+ function(entry) {
+ successCallback(entry);
+ getNextFile();
+ },
+ function(err) {
+ errorCallback(err);
+ getNextFile();
+ });
+ };
+
+ getNextFile();
+};
+
+/**
+ * Resolve a path to either a DirectoryEntry or a FileEntry, regardless of
+ * whether the path is a directory or file.
+ *
+ * @param {DirectoryEntry} root The root of the filesystem to search.
+ * @param {string} path The path to be resolved.
+ * @param {function(Entry)} resultCallback Called back when a path is
+ * successfully resolved. Entry will be either a DirectoryEntry or
+ * a FileEntry.
+ * @param {function(FileError)} errorCallback Called back if an unexpected
+ * error occurs while resolving the path.
+ */
+util.resolvePath = function(root, path, resultCallback, errorCallback) {
+ if (path == '' || path == '/') {
+ resultCallback(root);
+ return;
+ }
+
+ root.getFile(
+ path, {create: false},
+ resultCallback,
+ function(err) {
+ if (err.code == FileError.TYPE_MISMATCH_ERR) {
+ // Bah. It's a directory, ask again.
+ root.getDirectory(
+ path, {create: false},
+ resultCallback,
+ errorCallback);
+ } else {
+ errorCallback(err);
+ }
+ });
+};
+
+/**
+ * Locate the file referred to by path, creating directories or the file
+ * itself if necessary.
+ * @param {DirEntry} root The root entry.
+ * @param {string} path The file path.
+ * @param {function(FileEntry)} successCallback The callback.
+ * @param {function(FileError)} errorCallback The callback.
+ */
+util.getOrCreateFile = function(root, path, successCallback, errorCallback) {
+ var dirname = null;
+ var basename = null;
+
+ var onDirFound = function(dirEntry) {
+ dirEntry.getFile(basename, { create: true },
+ successCallback, errorCallback);
+ };
+
+ var i = path.lastIndexOf('/');
+ if (i > -1) {
+ dirname = path.substr(0, i);
+ basename = path.substr(i + 1);
+ } else {
+ basename = path;
+ }
+
+ if (!dirname) {
+ onDirFound(root);
+ return;
+ }
+
+ util.getOrCreateDirectory(root, dirname, onDirFound, errorCallback);
+};
+
+/**
+ * Locate the directory referred to by path, creating directories along the
+ * way.
+ * @param {DirEntry} root The root entry.
+ * @param {string} path The directory path.
+ * @param {function(FileEntry)} successCallback The callback.
+ * @param {function(FileError)} errorCallback The callback.
+ */
+util.getOrCreateDirectory = function(root, path, successCallback,
+ errorCallback) {
+ var names = path.split('/');
+
+ var getOrCreateNextName = function(dir) {
+ if (!names.length)
+ return successCallback(dir);
+
+ var name;
+ do {
+ name = names.shift();
+ } while (!name || name == '.');
+
+ dir.getDirectory(name, { create: true }, getOrCreateNextName,
+ errorCallback);
+ };
+
+ getOrCreateNextName(root);
+};
+
+/**
+ * Renames the entry to newName.
+ * @param {Entry} entry The entry to be renamed.
+ * @param {string} newName The new name.
+ * @param {function(Entry)} successCallback Callback invoked when the rename
+ * is successfully done.
+ * @param {function(FileError)} errorCallback Callback invoked when an error
+ * is found.
+ */
+util.rename = function(entry, newName, successCallback, errorCallback) {
+ entry.getParent(function(parent) {
+ // Before moving, we need to check if there is an existing entry at
+ // parent/newName, since moveTo will overwrite it.
+ // Note that this way has some timing issue. After existing check,
+ // a new entry may be create on background. However, there is no way not to
+ // overwrite the existing file, unfortunately. The risk should be low,
+ // assuming the unsafe period is very short.
+ (entry.isFile ? parent.getFile : parent.getDirectory).call(
+ parent, newName, {create: false},
+ function(entry) {
+ // The entry with the name already exists.
+ errorCallback(util.createFileError(FileError.PATH_EXISTS_ERR));
+ },
+ function(error) {
+ if (error.code != FileError.NOT_FOUND_ERR) {
+ // Unexpected error is found.
+ errorCallback(error);
+ return;
+ }
+
+ // No existing entry is found.
+ entry.moveTo(parent, newName, successCallback, errorCallback);
+ });
+ }, errorCallback);
+};
+
+/**
+ * Remove a file or a directory.
+ * @param {Entry} entry The entry to remove.
+ * @param {function()} onSuccess The success callback.
+ * @param {function(FileError)} onError The error callback.
+ */
+util.removeFileOrDirectory = function(entry, onSuccess, onError) {
+ if (entry.isDirectory)
+ entry.removeRecursively(onSuccess, onError);
+ else
+ entry.remove(onSuccess, onError);
+};
+
+/**
+ * Checks if an entry exists at |relativePath| in |dirEntry|.
+ * If exists, tries to deduplicate the path by inserting parenthesized number,
+ * such as " (1)", before the extension. If it still exists, tries the
+ * deduplication again by increasing the number up to 10 times.
+ * For example, suppose "file.txt" is given, "file.txt", "file (1).txt",
+ * "file (2).txt", ..., "file (9).txt" will be tried.
+ *
+ * @param {DirectoryEntry} dirEntry The target directory entry.
+ * @param {string} relativePath The path to be deduplicated.
+ * @param {function(string)} onSuccess Called with the deduplicated path on
+ * success.
+ * @param {function(FileError)} onError Called on error.
+ */
+util.deduplicatePath = function(dirEntry, relativePath, onSuccess, onError) {
+ // The trial is up to 10.
+ var MAX_RETRY = 10;
+
+ // Crack the path into three part. The parenthesized number (if exists) will
+ // be replaced by incremented number for retry. For example, suppose
+ // |relativePath| is "file (10).txt", the second check path will be
+ // "file (11).txt".
+ var match = /^(.*?)(?: \((\d+)\))?(\.[^.]*?)?$/.exec(relativePath);
+ var prefix = match[1];
+ var copyNumber = match[2] ? parseInt(match[2], 10) : 0;
+ var ext = match[3] ? match[3] : '';
+
+ // The path currently checking the existence.
+ var trialPath = relativePath;
+
+ var onNotResolved = function(err) {
+ // We expect to be unable to resolve the target file, since we're going
+ // to create it during the copy. However, if the resolve fails with
+ // anything other than NOT_FOUND, that's trouble.
+ if (err.code != FileError.NOT_FOUND_ERR) {
+ onError(err);
+ return;
+ }
+
+ // Found a path that doesn't exist.
+ onSuccess(trialPath);
+ };
+
+ var numRetry = MAX_RETRY;
+ var onResolved = function(entry) {
+ if (--numRetry == 0) {
+ // Hit the limit of the number of retrial.
+ // Note that we cannot create FileError object directly, so here we use
+ // Object.create instead.
+ onError(util.createFileError(FileError.PATH_EXISTS_ERR));
+ return;
+ }
+
+ ++copyNumber;
+ trialPath = prefix + ' (' + copyNumber + ')' + ext;
+ util.resolvePath(dirEntry, trialPath, onResolved, onNotResolved);
+ };
+
+ // Check to see if the target exists.
+ util.resolvePath(dirEntry, trialPath, onResolved, onNotResolved);
+};
+
+/**
+ * Convert a number of bytes into a human friendly format, using the correct
+ * number separators.
+ *
+ * @param {number} bytes The number of bytes.
+ * @return {string} Localized string.
+ */
+util.bytesToString = function(bytes) {
+ // Translation identifiers for size units.
+ var UNITS = ['SIZE_BYTES',
+ 'SIZE_KB',
+ 'SIZE_MB',
+ 'SIZE_GB',
+ 'SIZE_TB',
+ 'SIZE_PB'];
+
+ // Minimum values for the units above.
+ var STEPS = [0,
+ Math.pow(2, 10),
+ Math.pow(2, 20),
+ Math.pow(2, 30),
+ Math.pow(2, 40),
+ Math.pow(2, 50)];
+
+ var str = function(n, u) {
+ // TODO(rginda): Switch to v8Locale's number formatter when it's
+ // available.
+ return strf(u, n.toLocaleString());
+ };
+
+ var fmt = function(s, u) {
+ var rounded = Math.round(bytes / s * 10) / 10;
+ return str(rounded, u);
+ };
+
+ // Less than 1KB is displayed like '80 bytes'.
+ if (bytes < STEPS[1]) {
+ return str(bytes, UNITS[0]);
+ }
+
+ // Up to 1MB is displayed as rounded up number of KBs.
+ if (bytes < STEPS[2]) {
+ var rounded = Math.ceil(bytes / STEPS[1]);
+ return str(rounded, UNITS[1]);
+ }
+
+ // This loop index is used outside the loop if it turns out |bytes|
+ // requires the largest unit.
+ var i;
+
+ for (i = 2 /* MB */; i < UNITS.length - 1; i++) {
+ if (bytes < STEPS[i + 1])
+ return fmt(STEPS[i], UNITS[i]);
+ }
+
+ return fmt(STEPS[i], UNITS[i]);
+};
+
+/**
+ * Utility function to read specified range of bytes from file
+ * @param {File} file The file to read.
+ * @param {number} begin Starting byte(included).
+ * @param {number} end Last byte(excluded).
+ * @param {function(File, Uint8Array)} callback Callback to invoke.
+ * @param {function(FileError)} onError Error handler.
+ */
+util.readFileBytes = function(file, begin, end, callback, onError) {
+ var fileReader = new FileReader();
+ fileReader.onerror = onError;
+ fileReader.onloadend = function() {
+ callback(file, new ByteReader(fileReader.result));
+ };
+ fileReader.readAsArrayBuffer(file.slice(begin, end));
+};
+
+/**
+ * Write a blob to a file.
+ * Truncates the file first, so the previous content is fully overwritten.
+ * @param {FileEntry} entry File entry.
+ * @param {Blob} blob The blob to write.
+ * @param {function(Event)} onSuccess Completion callback. The first argument is
+ * a 'writeend' event.
+ * @param {function(FileError)} onError Error handler.
+ */
+util.writeBlobToFile = function(entry, blob, onSuccess, onError) {
+ var truncate = function(writer) {
+ writer.onerror = onError;
+ writer.onwriteend = write.bind(null, writer);
+ writer.truncate(0);
+ };
+
+ var write = function(writer) {
+ writer.onwriteend = onSuccess;
+ writer.write(blob);
+ };
+
+ entry.createWriter(truncate, onError);
+};
+
+/**
+ * Returns a string '[Ctrl-][Alt-][Shift-][Meta-]' depending on the event
+ * modifiers. Convenient for writing out conditions in keyboard handlers.
+ *
+ * @param {Event} event The keyboard event.
+ * @return {string} Modifiers.
+ */
+util.getKeyModifiers = function(event) {
+ return (event.ctrlKey ? 'Ctrl-' : '') +
+ (event.altKey ? 'Alt-' : '') +
+ (event.shiftKey ? 'Shift-' : '') +
+ (event.metaKey ? 'Meta-' : '');
+};
+
+/**
+ * @param {HTMLElement} element Element to transform.
+ * @param {Object} transform Transform object,
+ * contains scaleX, scaleY and rotate90 properties.
+ */
+util.applyTransform = function(element, transform) {
+ element.style.webkitTransform =
+ transform ? 'scaleX(' + transform.scaleX + ') ' +
+ 'scaleY(' + transform.scaleY + ') ' +
+ 'rotate(' + transform.rotate90 * 90 + 'deg)' :
+ '';
+};
+
+/**
+ * Makes filesystem: URL from the path.
+ * @param {string} path File or directory path.
+ * @return {string} URL.
+ */
+util.makeFilesystemUrl = function(path) {
+ path = path.split('/').map(encodeURIComponent).join('/');
+ var prefix = 'external';
+ return 'filesystem:' + chrome.runtime.getURL(prefix + path);
+};
+
+/**
+ * Extracts path from filesystem: URL.
+ * @param {string} url Filesystem URL.
+ * @return {string} The path.
+ */
+util.extractFilePath = function(url) {
+ var match =
+ /^filesystem:[\w-]*:\/\/[\w]*\/(external|persistent|temporary)(\/.*)$/.
+ exec(url);
+ var path = match && match[2];
+ if (!path) return null;
+ return decodeURIComponent(path);
+};
+
+/**
+ * Traverses a directory tree whose root is the given entry, and invokes
+ * callback for each entry. Upon completion, successCallback will be called.
+ * On error, errorCallback will be called.
+ *
+ * @param {Entry} entry The root entry.
+ * @param {function(Entry):boolean} callback Callback invoked for each entry.
+ * If this returns false, entries under it won't be traversed. Note that
+ * its siblings (and their children) will be still traversed.
+ * @param {function()} successCallback Called upon successful completion.
+ * @param {function(error)} errorCallback Called upon error.
+ */
+util.traverseTree = function(entry, callback, successCallback, errorCallback) {
+ if (!callback(entry)) {
+ successCallback();
+ return;
+ }
+
+ util.forEachDirEntry(
+ entry,
+ function(child, iterationCallback) {
+ util.traverseTree(child, callback, iterationCallback, errorCallback);
+ },
+ successCallback,
+ errorCallback);
+};
+
+/**
+ * A shortcut function to create a child element with given tag and class.
+ *
+ * @param {HTMLElement} parent Parent element.
+ * @param {string=} opt_className Class name.
+ * @param {string=} opt_tag Element tag, DIV is omitted.
+ * @return {Element} Newly created element.
+ */
+util.createChild = function(parent, opt_className, opt_tag) {
+ var child = parent.ownerDocument.createElement(opt_tag || 'div');
+ if (opt_className)
+ child.className = opt_className;
+ parent.appendChild(child);
+ return child;
+};
+
+/**
+ * Update the app state.
+ *
+ * @param {string} path Path to be put in the address bar after the hash.
+ * If null the hash is left unchanged.
+ * @param {string|Object=} opt_param Search parameter. Used directly if string,
+ * stringified if object. If omitted the search query is left unchanged.
+ */
+util.updateAppState = function(path, opt_param) {
+ window.appState = window.appState || {};
+ if (typeof opt_param == 'string')
+ window.appState.params = {};
+ else if (typeof opt_param == 'object')
+ window.appState.params = opt_param;
+ if (path)
+ window.appState.defaultPath = path;
+ util.saveAppState();
+ return;
+};
+
+/**
+ * Return a translated string.
+ *
+ * Wrapper function to make dealing with translated strings more concise.
+ * Equivalent to loadTimeData.getString(id).
+ *
+ * @param {string} id The id of the string to return.
+ * @return {string} The translated string.
+ */
+function str(id) {
+ return loadTimeData.getString(id);
+}
+
+/**
+ * Return a translated string with arguments replaced.
+ *
+ * Wrapper function to make dealing with translated strings more concise.
+ * Equivalent to loadTimeData.getStringF(id, ...).
+ *
+ * @param {string} id The id of the string to return.
+ * @param {...string} var_args The values to replace into the string.
+ * @return {string} The translated string with replaced values.
+ */
+function strf(id, var_args) {
+ return loadTimeData.getStringF.apply(loadTimeData, arguments);
+}
+
+/**
+ * Adapter object that abstracts away the the difference between Chrome app APIs
+ * v1 and v2. Is only necessary while the migration to v2 APIs is in progress.
+ * TODO(mtomasz): Clean up this. crbug.com/240606.
+ */
+util.platform = {
+ /**
+ * @return {boolean} True if Files.app is running as an open files or a select
+ * folder dialog. False otherwise.
+ */
+ runningInBrowser: function() {
+ return !window.appID;
+ },
+
+ /**
+ * @param {function(Object)} callback Function accepting a preference map.
+ */
+ getPreferences: function(callback) {
+ chrome.storage.local.get(callback);
+ },
+
+ /**
+ * @param {string} key Preference name.
+ * @param {function(string)} callback Function accepting the preference value.
+ */
+ getPreference: function(key, callback) {
+ chrome.storage.local.get(key, function(items) {
+ callback(items[key]);
+ });
+ },
+
+ /**
+ * @param {string} key Preference name.
+ * @param {string|Object} value Preference value.
+ * @param {function()=} opt_callback Completion callback.
+ */
+ setPreference: function(key, value, opt_callback) {
+ if (typeof value != 'string')
+ value = JSON.stringify(value);
+
+ var items = {};
+ items[key] = value;
+ chrome.storage.local.set(items, opt_callback);
+ }
+};
+
+/**
+ * Attach page load handler.
+ * @param {function()} handler Application-specific load handler.
+ */
+util.addPageLoadHandler = function(handler) {
+ document.addEventListener('DOMContentLoaded', function() {
+ handler();
+ });
+};
+
+/**
+ * Save app launch data to the local storage.
+ */
+util.saveAppState = function() {
+ if (window.appState)
+ util.platform.setPreference(window.appID, window.appState);
+};
+
+/**
+ * AppCache is a persistent timestamped key-value storage backed by
+ * HTML5 local storage.
+ *
+ * It is not designed for frequent access. In order to avoid costly
+ * localStorage iteration all data is kept in a single localStorage item.
+ * There is no in-memory caching, so concurrent access is _almost_ safe.
+ *
+ * TODO(kaznacheev) Reimplement this based on Indexed DB.
+ */
+util.AppCache = function() {};
+
+/**
+ * Local storage key.
+ */
+util.AppCache.KEY = 'AppCache';
+
+/**
+ * Max number of items.
+ */
+util.AppCache.CAPACITY = 100;
+
+/**
+ * Default lifetime.
+ */
+util.AppCache.LIFETIME = 30 * 24 * 60 * 60 * 1000; // 30 days.
+
+/**
+ * @param {string} key Key.
+ * @param {function(number)} callback Callback accepting a value.
+ */
+util.AppCache.getValue = function(key, callback) {
+ util.AppCache.read_(function(map) {
+ var entry = map[key];
+ callback(entry && entry.value);
+ });
+};
+
+/**
+ * Update the cache.
+ *
+ * @param {string} key Key.
+ * @param {string} value Value. Remove the key if value is null.
+ * @param {number=} opt_lifetime Maximum time to keep an item (in milliseconds).
+ */
+util.AppCache.update = function(key, value, opt_lifetime) {
+ util.AppCache.read_(function(map) {
+ if (value != null) {
+ map[key] = {
+ value: value,
+ expire: Date.now() + (opt_lifetime || util.AppCache.LIFETIME)
+ };
+ } else if (key in map) {
+ delete map[key];
+ } else {
+ return; // Nothing to do.
+ }
+ util.AppCache.cleanup_(map);
+ util.AppCache.write_(map);
+ });
+};
+
+/**
+ * @param {function(Object)} callback Callback accepting a map of timestamped
+ * key-value pairs.
+ * @private
+ */
+util.AppCache.read_ = function(callback) {
+ util.platform.getPreference(util.AppCache.KEY, function(json) {
+ if (json) {
+ try {
+ callback(JSON.parse(json));
+ } catch (e) {
+ // The local storage item somehow got messed up, start fresh.
+ }
+ }
+ callback({});
+ });
+};
+
+/**
+ * @param {Object} map A map of timestamped key-value pairs.
+ * @private
+ */
+util.AppCache.write_ = function(map) {
+ util.platform.setPreference(util.AppCache.KEY, JSON.stringify(map));
+};
+
+/**
+ * Remove over-capacity and obsolete items.
+ *
+ * @param {Object} map A map of timestamped key-value pairs.
+ * @private
+ */
+util.AppCache.cleanup_ = function(map) {
+ // Sort keys by ascending timestamps.
+ var keys = [];
+ for (var key in map) {
+ if (map.hasOwnProperty(key))
+ keys.push(key);
+ }
+ keys.sort(function(a, b) { return map[a].expire > map[b].expire });
+
+ var cutoff = Date.now();
+
+ var obsolete = 0;
+ while (obsolete < keys.length &&
+ map[keys[obsolete]].expire < cutoff) {
+ obsolete++;
+ }
+
+ var overCapacity = Math.max(0, keys.length - util.AppCache.CAPACITY);
+
+ var itemsToDelete = Math.max(obsolete, overCapacity);
+ for (var i = 0; i != itemsToDelete; i++) {
+ delete map[keys[i]];
+ }
+};
+
+/**
+ * Load an image.
+ *
+ * @param {Image} image Image element.
+ * @param {string} url Source url.
+ * @param {Object=} opt_options Hash array of options, eg. width, height,
+ * maxWidth, maxHeight, scale, cache.
+ * @param {function()=} opt_isValid Function returning false iff the task
+ * is not valid and should be aborted.
+ * @return {?number} Task identifier or null if fetched immediately from
+ * cache.
+ */
+util.loadImage = function(image, url, opt_options, opt_isValid) {
+ return ImageLoaderClient.loadToImage(url,
+ image,
+ opt_options || {},
+ function() {},
+ function() { image.onerror(); },
+ opt_isValid);
+};
+
+/**
+ * Cancels loading an image.
+ * @param {number} taskId Task identifier returned by util.loadImage().
+ */
+util.cancelLoadImage = function(taskId) {
+ ImageLoaderClient.getInstance().cancel(taskId);
+};
+
+/**
+ * Finds proerty descriptor in the object prototype chain.
+ * @param {Object} object The object.
+ * @param {string} propertyName The property name.
+ * @return {Object} Property descriptor.
+ */
+util.findPropertyDescriptor = function(object, propertyName) {
+ for (var p = object; p; p = Object.getPrototypeOf(p)) {
+ var d = Object.getOwnPropertyDescriptor(p, propertyName);
+ if (d)
+ return d;
+ }
+ return null;
+};
+
+/**
+ * Calls inherited property setter (useful when property is
+ * overriden).
+ * @param {Object} object The object.
+ * @param {string} propertyName The property name.
+ * @param {*} value Value to set.
+ */
+util.callInheritedSetter = function(object, propertyName, value) {
+ var d = util.findPropertyDescriptor(Object.getPrototypeOf(object),
+ propertyName);
+ d.set.call(object, value);
+};
+
+/**
+ * Returns true if the board of the device matches the given prefix.
+ * @param {string} boardPrefix The board prefix to match against.
+ * (ex. "x86-mario". Prefix is used as the actual board name comes with
+ * suffix like "x86-mario-something".
+ * @return {boolean} True if the board of the device matches the given prefix.
+ */
+util.boardIs = function(boardPrefix) {
+ // The board name should be lower-cased, but making it case-insensitive for
+ // backward compatibility just in case.
+ var board = str('CHROMEOS_RELEASE_BOARD');
+ var pattern = new RegExp('^' + boardPrefix, 'i');
+ return board.match(pattern) != null;
+};
+
+/**
+ * Adds an isFocused method to the current window object.
+ */
+util.addIsFocusedMethod = function() {
+ var focused = true;
+
+ window.addEventListener('focus', function() {
+ focused = true;
+ });
+
+ window.addEventListener('blur', function() {
+ focused = false;
+ });
+
+ /**
+ * @return {boolean} True if focused.
+ */
+ window.isFocused = function() {
+ return focused;
+ };
+};
+
+/**
+ * Makes a redirect to the specified Files.app's window from another window.
+ * @param {number} id Window id.
+ * @param {string} url Target url.
+ * @return {boolean} True if the window has been found. False otherwise.
+ */
+util.redirectMainWindow = function(id, url) {
+ // TODO(mtomasz): Implement this for Apps V2, once the photo importer is
+ // restored.
+ return false;
+};
+
+/**
+ * Checks, if the Files.app's window is in a full screen mode.
+ *
+ * @param {AppWindow} appWindow App window to be maximized.
+ * @return {boolean} True if the full screen mode is enabled.
+ */
+util.isFullScreen = function(appWindow) {
+ if (appWindow) {
+ return appWindow.isFullscreen();
+ } else {
+ console.error('App window not passed. Unable to check status of ' +
+ 'the full screen mode.');
+ return false;
+ }
+};
+
+/**
+ * Toggles the full screen mode.
+ *
+ * @param {AppWindow} appWindow App window to be maximized.
+ * @param {boolean} enabled True for enabling, false for disabling.
+ */
+util.toggleFullScreen = function(appWindow, enabled) {
+ if (appWindow) {
+ if (enabled)
+ appWindow.fullscreen();
+ else
+ appWindow.restore();
+ return;
+ }
+
+ console.error(
+ 'App window not passed. Unable to toggle the full screen mode.');
+};
+
+/**
+ * The type of a file operation.
+ * @enum {string}
+ */
+util.FileOperationType = {
+ COPY: 'COPY',
+ MOVE: 'MOVE',
+ ZIP: 'ZIP',
+};
+
+/**
+ * The type of a file operation error.
+ * @enum {number}
+ */
+util.FileOperationErrorType = {
+ UNEXPECTED_SOURCE_FILE: 0,
+ TARGET_EXISTS: 1,
+ FILESYSTEM_ERROR: 2,
+};
+
+/**
+ * The kind of an entry changed event.
+ * @enum {number}
+ */
+util.EntryChangedKind = {
+ CREATED: 0,
+ DELETED: 1,
+};
+
+/**
+ * Obtains whether an entry is fake or not.
+ * @param {Entry|Object} entry Entry of fake entry.
+ * @return {boolean} True if the given entry is fake.
+ */
+util.isFakeEntry = function(entry) {
+ return !('getParent' in entry);
+};
+
+/**
+ * Creates a FileError instance with given code.
+ * Note that we cannot create FileError instance by "new FileError(code)",
+ * unfortunately, so here we use Object.create.
+ * @param {number} code Error code for the FileError.
+ * @return {FileError} FileError instance
+ */
+util.createFileError = function(code) {
+ return Object.create(FileError.prototype, {
+ code: { get: function() { return code; } }
+ });
+};
+
+/**
+ * Compares two entries.
+ * @param {Entry|Object} entry1 The entry to be compared. Can be a fake.
+ * @param {Entry|Object} entry2 The entry to be compared. Can be a fake.
+ * @return {boolean} True if the both entry represents a same file or
+ * directory. Returns true if both entries are null.
+ */
+util.isSameEntry = function(entry1, entry2) {
+ // Currently, we can assume there is only one root.
+ // When we support multi-file system, we need to look at filesystem, too.
+ return (entry1 && entry2 && entry1.fullPath === entry2.fullPath) ||
+ (!entry1 && !entry2);
+};
+
+/**
+ * @param {Entry|Object} parent The parent entry. Can be a fake.
+ * @param {Entry|Object} child The child entry. Can be a fake.
+ * @return {boolean} True if parent entry is actualy the parent of the child
+ * entry.
+ */
+util.isParentEntry = function(parent, child) {
+ // Currently, we can assume there is only one root.
+ // When we support multi-file system, we need to look at filesystem, too.
+ return PathUtil.isParentPath(parent.fullPath, child.fullPath);
+};
+
+/**
+ * Views files in the browser.
+ *
+ * @param {Array.<string>} urls URLs of files to view.
+ * @param {function(bool)} callback Callback notifying success or not.
+ */
+util.viewFilesInBrowser = function(urls, callback) {
+ var taskId = chrome.runtime.id + '|file|view-in-browser';
+ chrome.fileBrowserPrivate.executeTask(taskId, urls, callback);
+};
+
+/**
+ * Visit the URL.
+ *
+ * If the browser is opening, the url is opened in a new tag, otherwise the url
+ * is opened in a new window.
+ *
+ * @param {string} url URL to visit.
+ */
+util.visitURL = function(url) {
+ var params = {url: url};
+ chrome.tabs.create(params, function() {
+ if (chrome.runtime.lastError)
+ chrome.windows.create(params);
+ });
+};
+
+/**
+ * Returns normalized current locale, or default locale - 'en'.
+ * @return {string} Current locale
+ */
+util.getCurrentLocaleOrDefault = function() {
+ // chrome.i18n.getMessage('@@ui_locale') can't be used in packed app.
+ // Instead, we pass it from C++-side with strings.
+ return str('UI_LOCALE') || 'en';
+};
+
+/**
+ * Converts array of entries to an array of corresponding URLs.
+ * @param {Array.<Entry>} entries Input array of entries.
+ * @return {Array.<string>} Output array of URLs.
+ */
+util.entriesToURLs = function(entries) {
+ // TODO(mtomasz): Make all callers use entries instead of URLs, and then
+ // remove this utility function.
+ console.warn('Converting entries to URLs is deprecated.');
+ return entries.map(function(entry) {
+ return entry.toURL();
+ });
+};
+
+/**
+ * Converts array of URLs to an array of corresponding Entries.
+ *
+ * @param {Array.<string>} urls Input array of URLs.
+ * @param {function(Array.<Entry>)} callback Completion callback with array of
+ * Entries.
+ */
+util.URLsToEntries = function(urls, callback) {
+ var result = [];
+ AsyncUtil.forEach(
+ urls,
+ function(forEachCallback, url) {
+ webkitResolveLocalFileSystemURL(url, function(entry) {
+ result.push(entry);
+ forEachCallback();
+ }, function() {
+ // Not an error. Possibly, the file is not accessible anymore.
+ console.warn('Failed to resolve the file with url: ' + url + '.');
+ forEachCallback();
+ });
+ },
+ function() {
+ callback(result);
+ });
+};
+
+/**
+ * Error type of VolumeManager.
+ * @enum {string}
+ * @const
+ */
+util.VolumeError = Object.freeze({
+ /* Internal errors */
+ NOT_MOUNTED: 'not_mounted',
+ TIMEOUT: 'timeout',
+
+ /* System events */
+ UNKNOWN: 'error_unknown',
+ INTERNAL: 'error_internal',
+ UNKNOWN_FILESYSTEM: 'error_unknown_filesystem',
+ UNSUPPORTED_FILESYSTEM: 'error_unsupported_filesystem',
+ INVALID_ARCHIVE: 'error_invalid_archive',
+ AUTHENTICATION: 'error_authentication',
+ PATH_UNMOUNTED: 'error_path_unmounted'
+});
+
+/**
+ * List of connection types of drive.
+ *
+ * Keep this in sync with the kDriveConnectionType* constants in
+ * private_api_dirve.cc.
+ *
+ * @enum {string}
+ * @const
+ */
+util.DriveConnectionType = Object.freeze({
+ OFFLINE: 'offline', // Connection is offline or drive is unavailable.
+ METERED: 'metered', // Connection is metered. Should limit traffic.
+ ONLINE: 'online' // Connection is online.
+});
+
+/**
+ * List of reasons of DriveConnectionType.
+ *
+ * Keep this in sync with the kDriveConnectionReason constants in
+ * private_api_drive.cc.
+ *
+ * @enum {string}
+ * @const
+ */
+util.DriveConnectionReason = Object.freeze({
+ NOT_READY: 'not_ready', // Drive is not ready or authentication is failed.
+ NO_NETWORK: 'no_network', // Network connection is unavailable.
+ NO_SERVICE: 'no_service' // Drive service is unavailable.
+});
+
+/**
+ * The type of each volume.
+ * @enum {string}
+ * @const
+ */
+util.VolumeType = Object.freeze({
+ DRIVE: 'drive',
+ DOWNLOADS: 'downloads',
+ REMOVABLE: 'removable',
+ ARCHIVE: 'archive'
+});
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/css/action_choice.css b/chromium/chrome/browser/resources/file_manager/foreground/css/action_choice.css
new file mode 100644
index 00000000000..508809c520d
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/css/action_choice.css
@@ -0,0 +1,172 @@
+/* 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. */
+
+input:focus,
+button:focus {
+ outline-color: rgb(77, 144, 254);
+}
+
+.action-choice {
+ -webkit-box-align: center;
+ -webkit-box-pack: center;
+ -webkit-user-select: none;
+ background: white;
+ bottom: 0;
+ color: black;
+ display: -webkit-box;
+ font-family: Open Sans, Droid Sans Fallback, sans-serif;
+ font-size: 84%;
+ left: 0;
+ margin: 0;
+ overflow: hidden;
+ padding: 10px;
+ position: absolute;
+ right: 0;
+ top: 0;
+}
+
+.action-choice[loading] .content,
+.action-choice:not([loading]) .loading {
+ display: none;
+}
+
+.action-choice h1 {
+ font-size: 14px;
+ font-weight: normal;
+ line-height: 1.5;
+ margin: 5px 3px;
+}
+
+/* The loading preview */
+
+.loading {
+ -webkit-box-align: center;
+ -webkit-box-orient: vertical;
+ color: #333;
+ display: -webkit-box;
+ font-size: 12px;
+}
+
+.spinner {
+ background-image: url('../images/common/spinner.svg');
+ background-size: 100%;
+ height: 21px;
+ left: 44px;
+ margin-left: -10px;
+ margin-top: -10px;
+ opacity: 0.5;
+ position: absolute;
+ top: 29px;
+ width: 21px;
+}
+
+.device-type {
+ height: 64px;
+ position: relative;
+ width: 116px;
+}
+
+.device-type[device-type=usb] {
+ background-image: -webkit-image-set(
+ url('../images/volumes/device_usb_large.png') 1x,
+ url('../images/volumes/2x/device_usb_large.png') 2x);
+}
+
+.device-type[device-type=sd] {
+ -webkit-transform: rotate(-90deg);
+ background-image: -webkit-image-set(
+ url('../images/volumes/device_sd_large.png') 1x,
+ url('../images/volumes/2x/device_sd_large.png') 2x);
+ bottom: 10px; /* Adjust the rotated image to not overlap with element below */
+}
+
+/* The action choice content */
+
+.content {
+ -webkit-box-align: start;
+ -webkit-box-orient: vertical;
+ display: -webkit-box;
+ height: 100%;
+ overflow: hidden;
+ position: relative;
+ width: 100%;
+}
+
+.previews {
+ -webkit-box-orient: horizontal;
+ -webkit-mask-image: linear-gradient(to left, rgba(0, 0, 0, 0) 0,
+ rgba(0, 0, 0, 1) 80px);
+ display: -webkit-box;
+ position: relative;
+ width: 100%;
+}
+
+.img-container {
+ height: 120px;
+ margin: 0 2px;
+ overflow: hidden;
+ position: relative;
+ width: 120px;
+}
+
+.img-container > img {
+ -webkit-user-drag: none;
+ position: absolute;
+}
+
+.counter {
+ color: #808080;
+ margin: 5px 3px;
+ width: 100%;
+}
+
+.choices {
+ width: 100%;
+}
+
+/* Padding counterweights negative margins of items, thus eliminating scroll
+ bar when it's not needed. Max height is set to fit 8 items before showing
+ scroll bar. */
+#actions-list {
+ display: block;
+ max-height: 328px;
+ outline: none;
+ overflow: auto;
+ padding: 1px 0;
+ position: relative;
+}
+
+#actions-list > li {
+ cursor: default;
+ list-style-type: none;
+}
+
+#actions-list > li > div {
+ background-position: 5px center;
+ background-repeat: no-repeat;
+ line-height: 39px;
+ padding-left: 43px;
+}
+
+#actions-list > [selected] {
+ background-color: #dedede;
+}
+
+#actions-list:focus > [selected] {
+ background-color: rgb(203, 219, 241);
+}
+
+#actions-list > [selected]:hover {
+ background-color: rgb(193, 211, 236);
+ border-color: hsl(214, 91%, 85%);
+}
+
+#actions-list > :hover {
+ background-color: #f1f1f1;
+ border-color: hsl(214, 91%, 85%);
+}
+
+#actions-list > li > div.disabled {
+ opacity: 0.5;
+}
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/css/audio_player.css b/chromium/chrome/browser/resources/file_manager/foreground/css/audio_player.css
new file mode 100644
index 00000000000..1f6b7bbfb91
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/css/audio_player.css
@@ -0,0 +1,404 @@
+/* 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. */
+
+body {
+ -webkit-user-select: none;
+ overflow: hidden;
+ padding: 0;
+}
+
+.audio-player {
+ background-color: #1d1d1d;
+ bottom: 0;
+ color: white;
+ cursor: default;
+ font-family: Open Sans, Droid Sans Fallback, sans-serif;
+ font-size: 10pt;
+ left: 0;
+ position: absolute;
+ right: 0;
+ top: 0;
+}
+
+/* Title buttons.
+ * In the collapsed/single-track mode they overlap the first track. */
+
+.title-button {
+ background-position: center center;
+ background-repeat: no-repeat;
+ cursor: pointer;
+ height: 29px;
+ opacity: 0.4;
+ position: absolute;
+ top: 0;
+ width: 29px;
+ z-index: 2;
+}
+
+.title-button:hover {
+ background-color: rgb(60, 126, 255) !important;
+ opacity: 1;
+}
+
+.audio-player:not(.collapsed):not(.single-track) > .title-button {
+ background-color: #1f1f1f;
+}
+
+.title-button.close {
+ background-image: -webkit-image-set(
+ url('../images/media/media_close.png') 1x,
+ url('../images/media/2x/media_close.png') 2x);
+ right: 0;
+}
+
+.title-button.collapse {
+ background-image: -webkit-image-set(
+ url('../images/media/media_collapse.png') 1x,
+ url('../images/media/2x/media_collapse.png') 2x);
+ right: 0;
+}
+
+.audio-player:not(.frameless) .title-button.close {
+ display: none;
+}
+
+.audio-player.frameless .title-button.collapse {
+ right: 29px;
+}
+
+.collapsed .title-button.collapse {
+ background-image: -webkit-image-set(
+ url('../images/media/media_expand.png') 1x,
+ url('../images/media/2x/media_expand.png') 2x);
+}
+
+.single-track .title-button.collapse {
+ display: none;
+}
+
+/* Common properties for track containers. */
+.audio-player > .track-list,
+.audio-player > .track-stack {
+ bottom: 35px; /* Room for the controls bar. */
+ left: 0;
+ position: absolute;
+ right: 0;
+}
+
+/* Scrollable list of tracks.
+ * Displayed in expanded mode if there is more than one track. */
+.audio-player > .track-list {
+ -webkit-box-align: center;
+ -webkit-box-orient: vertical;
+ -webkit-box-pack: start;
+ display: -webkit-box;
+ overflow-x: hidden;
+ overflow-y: auto;
+ top: 0;
+}
+
+/* A single track container.
+ * Displayed in the compact mode or when there is only one track. */
+.audio-player > .track-stack {
+ height: 58px;
+}
+
+.audio-player.collapsed > .track-list,
+.audio-player.single_track > .track-list,
+.audio-player:not(.collapsed):not(.single-track) > .track-stack {
+ opacity: 0;
+ pointer-events: none;
+}
+
+/* Track item. */
+.track {
+ -webkit-box-align: center;
+ -webkit-box-orient: horizontal;
+ -webkit-box-pack: start;
+ display: -webkit-box;
+ height: 58px;
+ width: 100%;
+}
+
+/* In the expanded mode the selected track is highlighted. */
+.track-list .track.selected {
+ background-color: #2d2d2d;
+}
+
+.track-list .track:hover {
+ background-color: #272727 !important;
+}
+
+.track-list .track:not(.selected) .data {
+ opacity: 0.7;
+}
+
+/* In the compact mode all tracks are in the same position, only the selected
+ is visible.*/
+.track-stack > .track {
+ position: absolute;
+ top: 0;
+}
+
+.track-stack > .track.selected {
+ z-index: 1;
+}
+
+/* Opacity transition is controlled differently for the text and the artwork.
+ * Text transition looks better if fade-in and fade-out go in parallel.
+ * For the artwork we start fading out the old icon only after the new one
+ * is completely opaque (otherwise when transitioning between identical icons
+ * we see some fading because the background transpires). */
+.track-stack > .track:not(.selected) .data,
+.track-stack > .track:not(.visible) .art {
+ opacity: 0;
+ transition: opacity 220ms ease-out;
+}
+
+/* Track data. */
+
+.track .art {
+ box-sizing: border-box;
+ height: 48px;
+ margin: 4px 0 6px 4px;
+ position: relative;
+ width: 48px;
+}
+
+.track .art.blank {
+ background-color: #111;
+ border: 1px solid #333;
+}
+
+.track .art img {
+ height: 100%;
+ width: 100%;
+}
+
+.track .art.blank img {
+ display: none;
+}
+
+.track .art.error {
+ background-image: -webkit-image-set(
+ url('../images/media/error.png') 1x,
+ url('../images/media/2x/error.png') 2x);
+ background-position: center center;
+ background-repeat: no-repeat;
+}
+
+.noart .track .art {
+ display: none;
+}
+
+.track .data {
+ -webkit-box-flex: 1;
+ -webkit-box-orient: vertical;
+ -webkit-box-pack: center;
+ display: -webkit-box;
+ margin-left: 8px;
+ margin-right: 4px;
+}
+
+.track .data .data-title,
+.track .data .data-artist {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.track .data .data-title {
+ font-weight: bold;
+}
+
+/* TODO(kaznacheev): Set to 20px when the audio player is title-less. */
+.single-track .data-title {
+ padding-right: 0;
+}
+
+/* TODO(kaznacheev): Set to 50px when the audio player is title-less. */
+.collapsed:not(.single-track) .data-title {
+ padding-right: 20px;
+}
+
+/* Controls bar. */
+
+.audio-controls {
+ -webkit-box-align: center;
+ -webkit-box-orient: horizontal;
+ -webkit-box-pack: center;
+ background-color: #2D2D2D;
+ border-top: 1px solid rgba(255, 255, 255, 0.1);
+ bottom: 0;
+ display: -webkit-box;
+ height: 30px;
+ left: 0;
+ padding: 0 0 4px 13px;
+ position: absolute;
+ right: 0;
+}
+
+.audio-controls .media-button {
+ height: 29px;
+ margin-top: 1px;
+ width: 29px;
+}
+
+.audio-controls .media-button.play {
+ margin-left: -10px;
+ margin-right: -8px;
+}
+
+.audio-controls .media-button.play > .default.normal,
+.audio-controls .media-button.play > .ended.normal {
+ background-image: -webkit-image-set(
+ url('../images/media/media_play_audio.png') 1x,
+ url('../images/media/2x/media_play_audio.png') 2x);
+}
+
+.audio-controls .media-button.play > .default.hover,
+.audio-controls .media-button.play > .ended.hover {
+ background-image: -webkit-image-set(
+ url('../images/media/media_play_audio_hover.png') 1x,
+ url('../images/media/2x/media_play_audio_hover.png') 2x);
+}
+
+.audio-controls .media-button.play > .default.active,
+.audio-controls .media-button.play > .ended.active {
+ background-image: -webkit-image-set(
+ url('../images/media/media_play_audio_down.png') 1x,
+ url('../images/media/2x/media_play_audio_down.png') 2x);
+}
+
+.audio-controls .media-button.play > .playing.normal {
+ background-image: -webkit-image-set(
+ url('../images/media/media_pause_audio.png') 1x,
+ url('../images/media/2x/media_pause_audio.png') 2x);
+}
+
+.audio-controls .media-button.play > .playing.hover {
+ background-image: -webkit-image-set(
+ url('../images/media/media_pause_audio_hover.png') 1x,
+ url('../images/media/2x/media_pause_audio_hover.png') 2x);
+}
+
+.audio-controls .media-button.play > .playing.active {
+ background-image: -webkit-image-set(
+ url('../images/media/media_pause_audio_down.png') 1x,
+ url('../images/media/2x/media_pause_audio_down.png') 2x);
+}
+
+.audio-controls .time-controls {
+ margin-left: 10px;
+ margin-right: 9px;
+}
+
+.audio-controls .time-controls .time {
+ margin-left: 11px;
+}
+
+.media-button.previous {
+ margin-left: -2px;
+}
+
+.media-button.previous > .normal {
+ background-image: -webkit-image-set(
+ url('../images/media/media_previous.png') 1x,
+ url('../images/media/2x/media_previous.png') 2x);
+}
+
+.media-button.previous > .hover {
+ background-image: -webkit-image-set(
+ url('../images/media/media_previous_hover.png') 1x,
+ url('../images/media/2x/media_previous_hover.png') 2x);
+}
+
+.media-button.previous > .active {
+ background-image: -webkit-image-set(
+ url('../images/media/media_previous_down.png') 1x,
+ url('../images/media/2x/media_previous_down.png') 2x);
+}
+
+.media-button.next {
+ margin-right: -2px;
+}
+
+.media-button.next > .normal {
+ background-image: -webkit-image-set(
+ url('../images/media/media_next.png') 1x,
+ url('../images/media/2x/media_next.png') 2x);
+}
+
+.media-button.next > .hover {
+ background-image: -webkit-image-set(
+ url('../images/media/media_next_hover.png') 1x,
+ url('../images/media/2x/media_next_hover.png') 2x);
+}
+
+.media-button.next > .active {
+ background-image: -webkit-image-set(
+ url('../images/media/media_next_down.png') 1x,
+ url('../images/media/2x/media_next_down.png') 2x);
+}
+
+.single-track .media-button.next,
+.single-track .media-button.previous {
+ display: none;
+}
+
+/* Customized scrollbar for the playlist. */
+
+::-webkit-scrollbar {
+ height: 16px;
+ width: 16px;
+}
+
+::-webkit-scrollbar-button {
+ height: 0;
+ width: 0;
+}
+
+::-webkit-scrollbar-thumb {
+ background-clip: padding-box;
+ background-color: rgba(255, 255, 255, 0.15);
+ box-shadow: inset 1px 1px 0 rgba(0, 0, 0, 0.10),
+ inset 0 -1px 0 rgba(0, 0, 0, 0.07);
+ min-height: 28px;
+ padding-top: 100px;
+}
+
+::-webkit-scrollbar-thumb:hover {
+ background-color: rgba(255,255,255,0.20);
+ box-shadow: inset 1px 1px 1px rgba(0, 0, 0, 0.25);
+}
+
+::-webkit-scrollbar-thumb:active {
+ background-color: rgba(255, 255, 255, 0.25);
+ box-shadow: inset 1px 1px 3px rgba(0, 0, 0, 0.35);
+}
+
+::-webkit-scrollbar-thumb:vertical {
+ border-bottom: 0 solid transparent;
+ border-left: 5px solid transparent;
+ border-right: 0 solid transparent;
+ border-top: 0 solid transparent;
+}
+
+::-webkit-scrollbar-track:hover {
+ background-color: rgba(0, 0, 0, 0.05);
+ box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.10);
+}
+
+::-webkit-scrollbar-track:active {
+ background-color: rgba(0, 0, 0, 0.05);
+ box-shadow: inset 1px 0 0 rgba(0, 0, 0, 0.14),
+ inset -1px -1px 0 rgba(0, 0, 0, 0.07);
+}
+
+::-webkit-scrollbar-track:vertical {
+ background-clip: padding-box;
+ background-color: transparent;
+ border-left: 5px solid transparent;
+ border-right: 0 solid transparent;
+}
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/css/combobutton.css b/chromium/chrome/browser/resources/file_manager/foreground/css/combobutton.css
new file mode 100644
index 00000000000..2651aad6bbb
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/css/combobutton.css
@@ -0,0 +1,34 @@
+/* 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. */
+
+.buttonbar button.combobutton {
+ -webkit-box-align: stretch;
+ display: -webkit-box;
+}
+
+.buttonbar .combobutton > .action {
+ background-position: left center;
+ background-repeat: no-repeat;
+ background-size: 16px 16px;
+ padding-left: 21px;
+}
+
+.buttonbar .combobutton > .trigger {
+ border-left: solid 1px #dcdcdc;
+ margin-left: 8px;
+ margin-right: -8px;
+ width: 22px;
+}
+
+.buttonbar .combobutton:not([multiple]) > .trigger {
+ display: none;
+}
+
+.buttonbar .combobutton > div > span.disclosureindicator {
+ -webkit-transform: rotate(90deg);
+}
+
+.buttonbar .combobutton[hidden] {
+ display: none;
+}
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/css/common.css b/chromium/chrome/browser/resources/file_manager/foreground/css/common.css
new file mode 100644
index 00000000000..e9051864258
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/css/common.css
@@ -0,0 +1,501 @@
+/* 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. */
+
+/* Special attribute to hide elements. */
+[hidden] {
+ display: none !important;
+}
+
+/* This file contains "borrowed" copy of standard styles. To simplify merging,
+ * when altering, please preserve original property value by adding comments. */
+input.common[type='checkbox'],
+input.common[type='radio'] {
+ -webkit-appearance: none;
+ border: 1px solid #555;
+ border-radius: 1px;
+ box-sizing: border-box;
+ cursor: default;
+ height: 13px;
+ margin: 0;
+ opacity: 0.4;
+ width: 13px;
+}
+
+input.common[type='checkbox']:hover,
+input.common[type='checkbox']:checked,
+input.common[type='radio']:hover,
+input.common[type='radio']:checked {
+ opacity: 1;
+}
+
+input.common[type='checkbox'] {
+ position: relative;
+}
+
+input.common[type='checkbox']:checked::after {
+ background-image: -webkit-image-set(
+ url('../images/common/check_no_box.png') 1x,
+ url('../images/common/2x/check_no_box.png') 2x);
+ background-position: -3px -4px;
+ background-repeat: no-repeat;
+}
+
+input.common[type='checkbox'].white {
+ border: none;
+}
+
+input.common[type='checkbox'].white:not(:checked)::after {
+ background-image: -webkit-image-set(
+ url('../images/common/checkbox_white_unchecked.png') 1x,
+ url('../images/common/2x/checkbox_white_unchecked.png') 2x);
+ background-position: -1px 0;
+}
+
+input.common[type='checkbox'].white:checked::after {
+ background-image: -webkit-image-set(
+ url('../images/common/checkbox_white_checked.png') 1x,
+ url('../images/common/2x/checkbox_white_checked.png') 2x);
+ background-position: -1px 0;
+}
+
+input.common[type='checkbox']::after {
+ content: '';
+ display: -webkit-box;
+ height: 15px;
+ left: -2px;
+ position: absolute;
+ top: -2px;
+ width: 17px;
+}
+
+.bubble {
+ background: #FFF;
+ border-radius: 2px;
+ cursor: default;
+ outline: 1px solid rgba(0, 0, 0, 0.2);
+ padding: 16px;
+}
+
+.bubble .pointer {
+ background: -webkit-image-set(
+ url('../images/common/bubble_point_white.png') 1x,
+ url('../images/common/2x/bubble_point_white.png') 2x);
+ display: block;
+ height: 11px;
+ left: 24px;
+ margin: 0 0 0 -5px;
+ outline: none;
+ position: absolute;
+ width: 17px;
+}
+
+.bubble .pointer:not(.bottom) {
+ top: -11px;
+}
+
+.bubble .pointer.bottom {
+ -webkit-transform: rotate(180deg);
+ bottom: -11px;
+}
+
+.bubble .close-x {
+ background: -webkit-image-set(
+ url('../images/common/close_x_gray.png') 1x,
+ url('../images/common/2x/close_x_gray.png') 2x);
+ height: 21px;
+ opacity: 0.3;
+ position: absolute;
+ right: 3px;
+ top: 3px;
+ width: 21px;
+}
+
+.bubble .close-x:hover {
+ opacity: 0.7;
+}
+
+.buttonbar {
+ display: -webkit-box;
+ height: 31px;
+}
+
+.buttonbar button:active img {
+ opacity: 1.0;
+}
+
+.buttonbar button:hover img {
+ opacity: 0.72;
+}
+
+.buttonbar button[disabled] img {
+ opacity: 0.9;
+}
+
+.buttonbar button img {
+ display: inline-block;
+ margin: -3px 0 0;
+ opacity: 0.55;
+ vertical-align: middle;
+}
+
+.buttonbar button.menubutton span.disclosureindicator {
+ -webkit-transform: rotate(90deg);
+ float: right;
+ margin-left: 7px;
+ margin-top: 10px;
+ opacity: .8;
+ transition: none;
+}
+
+span.disclosureindicator {
+ background-image: -webkit-image-set(
+ url('../images/common/disclosure_arrow_dk_grey.png') 1x,
+ url('../images/common/2x/disclosure_arrow_dk_grey.png') 2x);
+ background-position: center;
+ background-repeat: no-repeat;
+ display: inline-block;
+ height: 7px;
+ width: 5px;
+}
+
+/* "chrome-menu" class overrides some standard menu.css styles, to make custom
+ menus in FileBrowser look like native ChromeOS menus. */
+
+menu.chrome-menu {
+ background-color: rgb(250, 250, 250);
+ border-radius: 3px;
+ box-shadow: 0 1px 4px 0 rgba(0, 0, 0, .5);
+ color: rgb(34, 34, 34);
+ outline: none;
+ overflow: hidden;
+ padding: 5px 0;
+ transition: opacity 200ms ease-in;
+ z-index: 600; /* Must be below the overlay pane (1000). */
+}
+
+menu.chrome-menu[hidden] {
+ display: block !important; /* Overrides default [hidden] for animation. */
+ opacity: 0;
+ pointer-events: none;
+ visibility: hidden;
+}
+
+menu.chrome-menu.hide-delayed[hidden] {
+ transition-delay: 120ms;
+ transition-property: opacity, visibility;
+}
+
+menu.chrome-menu > :not(hr) {
+ background-position: right 10px center;
+ background-repeat: no-repeat;
+ line-height: 30px;
+ padding-left: 20px;
+ padding-right: 20px;
+}
+
+menu.chrome-menu > .menuitem-button {
+ background-position: center;
+ background-repeat: no-repeat;
+ border: 1px solid rgb(235, 235, 235);
+ height: 42px;
+ margin: -36px -1px -1px 0;
+ min-width: 60px;
+ padding: 0;
+ position: absolute;
+ width: 60px;
+}
+
+menu.chrome-menu > .menuitem-button[checked] {
+ background-color: rgb(235, 235, 235);
+}
+
+menu.chrome-menu > .menuitem-button.left {
+ right: 59px;
+}
+
+menu.chrome-menu > .menuitem-button.right {
+ right: 0;
+}
+
+menu.chrome-menu > menuitem[disabled] {
+ color: rgb(153, 153, 153);
+}
+
+menu.chrome-menu > menuitem:not([disabled])[selected],
+menu.chrome-menu > menuitem:not([disabled])[selected]:active {
+ background-color: rgb(66, 129, 244);
+ color: white;
+}
+
+menu.chrome-menu > hr {
+ background: rgb(235, 235, 235);
+ height: 1px;
+ margin: 5px 0;
+}
+
+menu.chrome-menu > menuitem[checked] {
+ background-image: -webkit-image-set(
+ url('../images/common/check_no_box.png') 1x,
+ url('../images/common/2x/check_no_box.png') 2x);
+}
+
+menu.chrome-menu > [checked]::before {
+ display: none;
+}
+
+menu[showShortcuts] > menuitem[shortcutText][selected]:not([disabled])::after {
+ color: white;
+}
+
+/**
+ * Ok/Cancel style buttons
+ * Height: 31px (content:21px + border:5px * 2)
+ **/
+button,
+input[type='button'],
+input[type='submit'],
+select {
+ background-color: rgb(250, 250, 250);
+ background-image: none;
+ background-position: center;
+ background-repeat: no-repeat;
+ border: 5px solid transparent;
+ border-image: -webkit-image-set(
+ url('chrome://resources/images/apps/button.png') 1x,
+ url('chrome://resources/images/2x/apps/button.png')
+ 2x) 5 / 5px / 2px repeat;
+ box-sizing: content-box;
+ color: rgb(34, 34, 34);
+ cursor: default;
+ height: 21px;
+ line-height: 21px;
+ margin: 0;
+ min-height: 21px;
+ min-width: 55px;
+ padding: 0 10px;
+ position: relative;
+ text-align: center;
+ z-index: 1;
+}
+
+.buttonbar button {
+ -webkit-margin-start: 10px;
+}
+
+button:hover,
+input[type='button']:hover,
+input[type='submit']:hover,
+select:hover {
+ border-image: -webkit-image-set(
+ url('chrome://resources/images/apps/button_hover.png') 1x,
+ url('chrome://resources/images/2x/apps/button_hover.png')
+ 2x) 5 fill / 5px / 2px repeat;
+ color: #222;
+}
+
+button:active,
+input[type='button']:active,
+input[type='submit']:active {
+ border-image: -webkit-image-set(
+ url('chrome://resources/images/apps/button_pressed.png') 1x,
+ url('chrome://resources/images/2x/apps/button_pressed.png')
+ 2x) 5 fill / 5px / 2px repeat;
+ color: #333;
+}
+
+button[disabled],
+input[type='button'][disabled],
+input[type='submit'][disabled],
+button[disabled]:hover,
+input[type='button'][disabled]:hover,
+input[type='submit'][disabled]:hover {
+ background-color: rgb(250, 250, 250);
+ background-image: none;
+ border-image: -webkit-image-set(
+ url('chrome://resources/images/apps/button.png') 1x,
+ url('chrome://resources/images/2x/apps/button.png')
+ 2x) 5 fill / 5px / 2px repeat;
+ color: rgb(150, 150, 150);
+}
+
+/* Gray progress bar. */
+.progress-bar {
+ background-color: #e6e6e6;
+ border-radius: 3px;
+ height: 6px;
+}
+
+.progress-track {
+ background-color: #888;
+ border-radius: 3px;
+ height: 6px;
+ min-width: 6px;
+}
+
+.progress-track.smoothed {
+ transition: width 1s linear;
+}
+
+/* Icons for the action choice dialog and choosing the default app. */
+div.import-photos-to-drive-icon {
+ background-image: -webkit-image-set(
+ url('../images/media/drive.png') 1x,
+ url('../images/media/2x/drive.png') 2x);
+}
+
+div.view-files-icon {
+ background-image: -webkit-image-set(
+ url('../../common/images/icon32.png') 1x,
+ url('../../common/images/icon64.png') 2x);
+}
+
+div.watch-single-video-icon {
+ background-image: -webkit-image-set(
+ url('../images/media/watch.png') 1x,
+ url('../images/media/2x/watch.png') 2x);
+}
+
+/* Pop-up dialogs. */
+
+.cr-dialog-container {
+ -webkit-box-align: center;
+ -webkit-box-pack: center;
+ -webkit-user-select: none;
+ display: -webkit-box;
+ height: 100%;
+ left: 0;
+ overflow: hidden;
+ position: absolute;
+ top: 0;
+ transition: opacity 250ms linear;
+ width: 100%;
+ z-index: 9999;
+}
+
+.cr-dialog-frame {
+ -webkit-box-orient: vertical;
+ background-color: rgb(250, 250, 250);
+ border: 1px solid rgb(255, 255, 255);
+ border-radius: 2px;
+ box-shadow: 0 1px 4px 0 rgba(0, 0, 0, .5);
+ color: rgb(34, 34, 34);
+ cursor: default;
+ display: -webkit-box;
+ padding: 20px;
+ position: relative;
+ width: 460px;
+}
+
+.cr-dialog-frame:focus {
+ outline: none;
+}
+
+@-webkit-keyframes pulse {
+ 0% {
+ -webkit-transform: scale(1);
+ }
+ 40% {
+ -webkit-transform: scale(1.02);
+ }
+ 60% {
+ -webkit-transform: scale(1.02);
+ }
+ 100% {
+ -webkit-transform: scale(1);
+ }
+}
+
+.cr-dialog-frame.pulse {
+ -webkit-animation-duration: 180ms;
+ -webkit-animation-iteration-count: 1;
+ -webkit-animation-name: pulse;
+ -webkit-animation-timing-function: ease-in-out;
+}
+
+.shown > .cr-dialog-frame {
+ -webkit-transform: perspective(500px) scale(1)
+ translateY(0) rotateX(0);
+ opacity: 1;
+}
+
+.cr-dialog-frame {
+ -webkit-transform: perspective(500px) scale(0.99)
+ translateY(-20px) rotateX(5deg);
+ opacity: 0;
+ transition: all 180ms;
+ transition-duration: 250ms;
+}
+
+.cr-dialog-shield {
+ background-color: white;
+ bottom: 0;
+ display: block;
+ left: 0;
+ opacity: 0;
+ pointer-events: none;
+ position: absolute;
+ right: 0;
+ top: 0;
+ transition: opacity 500ms;
+}
+
+.shown > .cr-dialog-shield {
+ opacity: 0.5;
+ transition: opacity 500ms;
+}
+
+.cr-dialog-title {
+ -webkit-margin-after: 10px;
+ -webkit-margin-end: 20px;
+ display: block;
+ font-size: 125%;
+ white-space: nowrap;
+ word-wrap: normal;
+}
+
+.cr-dialog-text {
+ margin: 13px 0;
+}
+
+.cr-dialog-text,
+.cr-dialog-title {
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+.cr-dialog-frame input {
+ box-sizing: border-box;
+ width: 100%;
+}
+
+.cr-dialog-buttons {
+ -webkit-box-orient: horizontal;
+ -webkit-box-pack: end;
+ display: -webkit-box;
+ padding-top: 10px;
+}
+
+.cr-dialog-buttons button {
+ -webkit-margin-start: 8px;
+ line-height: 1.8;
+}
+
+.cr-dialog-close {
+ background: url('chrome://theme/IDR_CLOSE_DIALOG') center no-repeat;
+ display: inline-block;
+ height: 44px;
+ opacity: 0.7;
+ position: absolute;
+ right: 0;
+ top: 0;
+ width: 44px;
+}
+
+.cr-dialog-close:hover {
+ background-image: url('chrome://theme/IDR_CLOSE_DIALOG_H');
+}
+
+.cr-dialog-close:active {
+ background-image: url('chrome://theme/IDR_CLOSE_DIALOG_P');
+}
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/css/drive_welcome.css b/chromium/chrome/browser/resources/file_manager/foreground/css/drive_welcome.css
new file mode 100644
index 00000000000..a3c5ddb8999
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/css/drive_welcome.css
@@ -0,0 +1,188 @@
+/* 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. */
+
+/* Google Drive welcome banners.*/
+.drive-welcome-wrapper {
+ /* This image looks good in high DPI as is. */
+ background-image: url(chrome://resources/images/clouds.png);
+ background-repeat: repeat-x;
+ color: #333;
+}
+
+.drive-welcome-icon {
+ background-image: -webkit-image-set(
+ url('../images/files/ui/drive_logo.png') 1x,
+ url('../images/files/ui/2x/drive_logo.png') 2x);
+ background-repeat: no-repeat;
+}
+
+.drive-welcome-links {
+ -webkit-box-orient: horizontal;
+ display: -webkit-box;
+}
+
+.drive-welcome-button {
+ -webkit-user-select: none;
+ background-image: linear-gradient(to bottom, #f5f5f5, #f1f1f1);
+ border: 1px solid rgba(0,0,0,0.1);
+ border-radius: 2px;
+ color: #444;
+ cursor: default;
+ display: inline-block;
+ font-size: 13px;
+ font-weight: bold;
+ height: 27px;
+ line-height: 27px;
+ padding: 0 8px;
+ text-align: center;
+ transition: all 218ms;
+}
+
+.drive-welcome-button:hover {
+ background-image: linear-gradient(to bottom, #f8f8f8, #f1f1f1);
+ border-color: #C6C6C6;
+ box-shadow: 0 1px 1px rgba(0,0,0,0.1);
+ color: #222;
+ transition: all 0;
+}
+
+.drive-welcome-button:active {
+ background-image: linear-gradient(to bottom, #f6f6f6, #f1f1f1);
+ box-shadow: inset 0 1px 2px rgba(0,0,0,0.1);
+}
+
+
+.drive-welcome-button.drive-welcome-start {
+ background-image:
+ linear-gradient(to bottom, rgb(77, 144, 254), rgb(71, 135, 237));
+ border-color: rgb(48, 121, 237);
+ color: white;
+ text-decoration: none;
+}
+
+.drive-welcome-button.drive-welcome-start:hover {
+ background-image:
+ linear-gradient(to bottom, rgb(77, 144, 254), rgb(53, 122, 232));
+ border-color: rgb(47, 91, 183);
+ box-shadow: 0 1px 1px rgba(0,0,0,0.1);
+}
+
+/* Header welcome banner. */
+.drive-welcome.header {
+ -webkit-box-flex: 0;
+ height: 100px;
+ overflow: hidden;
+ position: relative;
+ transition: height 180ms ease, visibility 0 linear 180ms;
+}
+
+.dialog-container:not([drive-welcome='header']) .drive-welcome.header {
+ height: 0;
+ visibility: hidden;
+}
+
+.drive-welcome.header .drive-welcome-wrapper {
+ -webkit-box-orient: horizontal;
+ background-size: 308px 100px;
+ bottom: 0;
+ display: -webkit-box;
+ left: 0;
+ position: absolute;
+ right: 0;
+ top: 0;
+}
+
+.drive-welcome.header .drive-welcome-icon {
+ background-position: center 18px;
+ background-size: 51px 44px;
+ width: 120px;
+}
+
+.drive-welcome.header .drive-welcome-message {
+ -webkit-box-flex: 1;
+ -webkit-box-orient: vertical;
+ display: -webkit-box;
+}
+
+.drive-welcome.header .drive-welcome-title {
+ font-size: 140%;
+ margin-bottom: 4px;
+ margin-top: 14px;
+}
+
+.drive-welcome.header .drive-welcome-text {
+ margin-bottom: 6px;
+}
+
+.drive-welcome.header .drive-welcome-dismiss {
+ display: none;
+}
+
+/* Full page welcome banner. */
+.drive-welcome.page {
+ bottom: 0;
+ left: 0;
+ position: absolute;
+ right: 0;
+ top: 0;
+}
+
+.dialog-container:not([drive-welcome='page']) .drive-welcome.page {
+ display: none;
+}
+
+.drive-welcome.page .cr-dialog-close {
+ display: none;
+}
+
+.drive-welcome.page .drive-welcome-wrapper {
+ -webkit-box-align: center;
+ -webkit-box-orient: vertical;
+ background-size: 520px 173px;
+ bottom: 0;
+ display: -webkit-box;
+ font-size: 120%;
+ left: 0;
+ overflow: hidden;
+ position: absolute;
+ right: 0;
+ top: 0;
+}
+
+.drive-welcome.page .drive-welcome-icon {
+ background-position: center center;
+ height: 240px;
+ left: 0;
+ right: 0;
+ top: 0;
+ width: 100%;
+}
+
+.drive-welcome.page .drive-welcome-message {
+ margin-left: 10px;
+ margin-right: 10px;
+ max-width: 525px;
+}
+
+.drive-welcome.page .drive-welcome-title {
+ font-size: 133%;
+ margin-bottom: 30px;
+ text-align: center;
+}
+
+.drive-welcome.page .drive-welcome-text {
+ margin-bottom: 24px;
+}
+
+.drive-welcome.page .drive-welcome-dismiss {
+ margin-left: 20px;
+}
+
+body:not([type='full-page']) .drive-welcome.page .drive-welcome-wrapper {
+ background-position: 0 0;
+}
+
+body:not([type='full-page']) .drive-welcome.page .drive-welcome-icon {
+ height: 200px;
+}
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/css/file_manager.css b/chromium/chrome/browser/resources/file_manager/foreground/css/file_manager.css
new file mode 100644
index 00000000000..560cef7e9a5
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/css/file_manager.css
@@ -0,0 +1,2075 @@
+/* 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. */
+
+/* The order of z-index:
+ * - 2: drag-selection-bodrder
+ * - 3: preview-panel
+ * - 500: scrollbar
+ * - 500: splitter
+ * - 525: spinner
+ * - 550: autocomplete-suggestions
+ * - 600: menus
+ * - 600: tooltip
+ * - 1000: preview thumbnail popup
+ * - 1000: overlay panel (ie. image editor)
+ */
+
+/* Special attribute used in HTML to hide elements. */
+body[type='folder'] [invisibleif~='folder'],
+body[type='upload-folder'] [invisibleif~='upload-folder'],
+body[type='saveas-file'] [invisibleif~='saveas-file'],
+body[type='open-file'] [invisibleif~='open-file'],
+body[type='open-multi-file'] [invisibleif~='open-multi-file'],
+body[type='full-page'] [invisibleif~='full-page'],
+
+body[type='folder'] [visibleif]:not([visibleif~='folder']),
+body[type='upload-folder'] [visibleif]:not([visibleif~='upload-folder']),
+body[type='saveas-file'] [visibleif]:not([visibleif~='saveas-file']),
+body[type='open-file'] [visibleif]:not([visibleif~='open-file']),
+body[type='open-multi-file'] [visibleif]:not([visibleif~='open-multi-file']),
+body[type='full-page'] [visibleif]:not([visibleif~='full-page']) {
+ display: none !important;
+}
+
+html {
+ height: 100%;
+}
+
+html.col-resize * {
+ cursor: col-resize !important;
+}
+
+/* Outer frame of the dialog. */
+body {
+ -webkit-box-flex: 1;
+ -webkit-box-orient: vertical;
+ -webkit-tap-highlight-color: transparent;
+ -webkit-user-select: none;
+ cursor: default;
+ display: -webkit-box;
+ height: 100%;
+ margin: 0;
+ padding: 0;
+ width: 100%;
+}
+
+/* Drop opacity of selected rows to give a visual feedback on copy/cut
+ * operation. */
+.blink {
+ opacity: 0.8;
+}
+
+::-webkit-scrollbar {
+ height: 0;
+ width: 0;
+}
+
+/* TODO(mtomasz): Flip scrollbars to the opposite side for RTL languages. */
+.scrollbar-vertical {
+ bottom: 0;
+ position: absolute;
+ right: 0;
+ top: 0;
+ width: 10px;
+ z-index: 500; /* Must be below the contextmenu (600). */
+}
+
+.scrollbar-button {
+ background-color: black;
+ border: 1px solid #ccc;
+ border-radius: 3px;
+ box-sizing: border-box;
+ height: 50%;
+ margin-right: 2px;
+ opacity: 0;
+ position: absolute;
+ transition: opacity 100ms;
+ width: 8px;
+}
+
+:hover > .scrollbar-vertical > .scrollbar-button {
+ opacity: 0.3;
+}
+
+.scrollbar-vertical > .scrollbar-button:hover {
+ opacity: 0.4;
+}
+
+.scrollbar-vertical > .scrollbar-button.pressed {
+ opacity: 0.5;
+}
+
+/* Main part of the dialog between header and footer. */
+.dialog-container {
+ -webkit-box-align: stretch;
+ -webkit-box-flex: 1;
+ -webkit-box-orient: horizontal;
+ background-color: white; /* Makes #drag-container invisible. */
+ border-radius: 2px;
+ display: -webkit-box;
+ overflow: hidden;
+ position: relative;
+}
+
+/* The style applied when a modal dialog box overlap the dialog container. */
+.dialog-container.disable-header-drag .dialog-navigation-list-header,
+.dialog-container.disable-header-drag .dialog-header {
+ -webkit-app-region: no-drag;
+}
+
+/* List/grid and preview are inside this container. */
+.dialog-main {
+ -webkit-box-align: stretch;
+ -webkit-box-flex: 1;
+ -webkit-box-orient: vertical;
+ display: -webkit-box;
+}
+
+/* Directory tree at the left. */
+.dialog-navigation-list {
+ -webkit-border-end: 1px solid rgba(20, 20, 22, 0.1);
+ -webkit-box-flex: 0;
+ -webkit-box-orient: vertical;
+ background-color: #f1f1f1;
+ display: flex;
+ flex-direction: column;
+ max-width: 50%;
+ min-width: 100px;
+ overflow: hidden;
+ position: relative;
+ width: 150px;
+}
+
+.dialog-navigation-list-header {
+ -webkit-app-region: drag;
+ flex: none;
+ height: 48px; /* Keep in sync with #dialog-header. */
+ line-height: 45px;
+}
+
+.dialog-navigation-list-header #app-name {
+ -webkit-margin-start: 15px;
+ color: #303030;
+ font-size: 130%;
+}
+
+.dialog-navigation-list-contents {
+ display: -webkit-box;
+ flex: 1 1 auto;
+ position: relative;
+}
+
+.dialog-navigation-list-footer {
+ display: -webkit-flex;
+ flex: none;
+ flex-direction: column;
+}
+
+/* A vertical splitter between the roots list and the file list. It is actually
+ a transparent area centered on the roots list right border.*/
+div.splitter {
+ -webkit-box-flex: 0;
+ cursor: col-resize;
+ margin-left: -3px;
+ margin-right: -3px;
+ position: relative;
+ width: 6px;
+ z-index: 500; /* Must be below the contextmenu (600). */
+}
+
+#navigation-list {
+ -webkit-box-flex: 1;
+ -webkit-box-orient: vertical;
+ display: -webkit-box;
+}
+
+#navigation-list > * {
+ height: 40px;
+ padding: 0 5px;
+}
+
+#navigation-list > .accepts,
+#navigation-list > [lead][selected],
+#navigation-list > [lead],
+#navigation-list > [selected],
+#navigation-list > [anchor] {
+ background-color: rgb(225, 225, 225);
+}
+
+#navigation-list:focus > .accepts,
+#navigation-list:focus > [lead][selected],
+#navigation-list:focus > [lead],
+#navigation-list:focus > [selected],
+#navigation-list:focus > [anchor] {
+ background-color: rgb(66, 129, 244);
+ color: white;
+}
+
+#navigation-list li.root-item {
+ -webkit-box-align: center;
+ display: -webkit-box;
+ line-height: 22px; /* To accomodate for icons. */
+ padding-left: 11px;
+}
+
+#navigation-list li.root-item > .root-label {
+ -webkit-box-flex: 1;
+ margin: 0 2px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+#navigation-list .volume-icon {
+ background-position: center 2px;
+ background-repeat: no-repeat;
+ height: 24px;
+ width: 24px;
+}
+
+#middlebar-header {
+ -webkit-border-end: 1px solid rgba(20, 20, 22, 0.1);
+ -webkit-box-sizing: border-box;
+ -webkit-padding-start: 20px;
+ color: rgb(100, 100, 100);
+ height: 47px;
+ line-height: 40px;
+ overflow-x: hidden;
+ position: absolute;
+ text-overflow: ellipsis;
+ width: 100%;
+}
+
+#directory-tree {
+ -webkit-border-end: 1px solid rgba(20, 20, 22, 0.1);
+ bottom: 0;
+ left: 0;
+ overflow-x: hidden;
+ overflow-y: auto;
+ padding-bottom: 0; /* For the preview panel. Will be overridden by JS. */
+ position: absolute;
+ right: 0;
+ top: 47px;
+}
+
+#directory-tree .tree-row {
+ cursor: pointer;
+ display: -webkit-box;
+ line-height: 29px;
+ padding: 0 3px;
+}
+
+/* For rows of subitems (non-top items) */
+#directory-tree .tree-children .tree-row {
+ line-height: 29px;
+}
+
+#directory-tree .tree-row > .expand-icon {
+ height: 37px;
+ left: 3px;
+ margin: -13px;
+ right: 3px;
+ top: 0;
+ vertical-align: middle;
+ width: 37px;
+}
+
+#directory-tree:focus .tree-row[selected] > .expand-icon {
+ background-image: -webkit-canvas(tree-triangle-inverted);
+}
+
+#directory-tree .tree-row > .volume-icon {
+ background-position: center 2px;
+ background-repeat: no-repeat;
+ height: 24px;
+ vertical-align: middle;
+ width: 24px;
+}
+
+#directory-tree .tree-row > .label {
+ -webkit-box-flex: 1;
+ display: block;
+ margin: 0 3px;
+ overflow-x: hidden;
+ text-overflow: ellipsis;
+}
+
+#directory-tree .tree-item.accepts > .tree-row,
+#directory-tree .tree-row[lead][selected],
+#directory-tree .tree-row[lead],
+#directory-tree .tree-row[selected],
+#directory-tree .tree-row[anchor] {
+ background-color: rgb(204, 204, 204);
+}
+
+#directory-tree .tree-item.accepts > .tree-row,
+#directory-tree .tree-row[lead][selected],
+#directory-tree .tree-row[lead],
+#directory-tree .tree-row[selected],
+#directory-tree .tree-row[anchor] {
+ background-color: rgb(225, 225, 225);
+}
+
+#directory-tree:focus .tree-item.accepts > .tree-row,
+#directory-tree:focus .tree-row[lead][selected],
+#directory-tree:focus .tree-row[lead],
+#directory-tree:focus .tree-row[selected],
+#directory-tree:focus .tree-row[anchor] {
+ background-color: rgb(193, 209, 232);
+}
+
+#directory-tree:focus .tree-item.accepts > .tree-row,
+#directory-tree:focus .tree-row[lead][selected],
+#directory-tree:focus .tree-row[lead],
+#directory-tree:focus .tree-row[selected],
+#directory-tree:focus .tree-row[anchor] {
+ background-color: rgb(66, 129, 244);
+ color: white;
+}
+
+#navigation-list .root-item > div.root-eject {
+ background-image: -webkit-image-set(
+ url('../images/files/ui/eject.png') 1x,
+ url('../images/files/ui/2x/eject.png') 2x);
+ background-position: center center;
+ background-repeat: no-repeat;
+ cursor: pointer;
+ height: 20px;
+ margin-right: 6px;
+ opacity: 0.7;
+ transition: opacity 70ms linear;
+ vertical-align: middle;
+ width: 20px;
+}
+
+#navigation-list:focus .root-item[selected] > div.root-eject {
+ -webkit-filter: brightness(0) invert();
+ opacity: 1;
+}
+
+#directory-tree .root-item[disabled] {
+ opacity: 0.5;
+ pointer-events: none;
+}
+
+/* Breadcrumbs and things under the title but above the list view. */
+.dialog-header {
+ -webkit-app-region: drag;
+ -webkit-box-align: center;
+ -webkit-box-orient: horizontal;
+ display: flex;
+ height: 48px;
+ margin: 0;
+ transition: all 180ms ease;
+}
+
+/* Search box */
+
+#search-box {
+ display: flex;
+ flex: auto;
+}
+
+#search-box.too-short {
+ visibility: hidden;
+}
+
+#search-box .icon {
+ -webkit-app-region: no-drag;
+ -webkit-padding-end: 0;
+ -webkit-padding-start: 10px;
+ background: transparent -webkit-image-set(
+ url(../images/files/ui/search_icon_inactive.png) 1x,
+ url(../images/files/ui/2x/search_icon_inactive.png) 2x)
+ no-repeat center;
+ flex: none;
+ height: 32px;
+ padding-bottom: 8px;
+ padding-top: 8px;
+ width: 32px;
+}
+
+#search-box .icon:hover,
+#search-box.has-cursor .icon,
+#search-box.has-text .icon {
+ background-image: -webkit-image-set(
+ url(../images/files/ui/search_icon_active.png) 1x,
+ url(../images/files/ui/2x/search_icon_active.png) 2x);
+}
+
+#search-box .full-size {
+ flex: 1 0 0;
+}
+
+#search-box input {
+ -webkit-app-region: no-drag;
+ background-color: #fff;
+ border-style: none;
+ color: #333;
+ cursor: default;
+ display: block;
+ height: 48px;
+ line-height: 1em;
+ margin: 0;
+ max-width: 100%;
+ outline: none;
+ padding: 0;
+}
+
+#search-box input::-webkit-search-cancel-button {
+ -webkit-appearance: none;
+}
+
+#search-box.has-cursor input,
+#search-box.has-text input {
+ cursor: text;
+}
+
+#search-box .clear {
+ -webkit-app-region: no-drag;
+ -webkit-margin-end: 30px;
+ align-self: center;
+ background: -webkit-image-set(
+ url(../images/files/ui/search_clear.png) 1x,
+ url(../images/files/ui/2x/search_clear.png) 2x)
+ no-repeat center;
+ border: none;
+ display: none;
+ flex: none;
+ height: 12px;
+ min-height: 0;
+ min-width: 0;
+ outline: none;
+ padding: 0;
+ width: 12px;
+}
+
+#search-box.has-text .clear {
+ display: block;
+}
+
+#search-box .clear:hover {
+ background-image: -webkit-image-set(
+ url(../images/files/ui/search_clear_hover.png) 1x,
+ url(../images/files/ui/2x/search_clear_hover.png) 2x);
+}
+
+#search-box .clear:active {
+ background-image: -webkit-image-set(
+ url(../images/files/ui/search_clear_pressed.png) 1x,
+ url(../images/files/ui/2x/search_clear_pressed.png) 2x);
+}
+
+.topbutton-bar {
+ flex: none;
+}
+
+/* Container for the detail and thumbnail list views. */
+.dialog-body {
+ -webkit-box-flex: 1;
+ -webkit-transition: all 180ms ease;
+ border-top: 1px solid rgba(20, 20, 22, 0.1);
+ position: relative;
+}
+
+.main-panel {
+ bottom: 0;
+ display: -webkit-box;
+ left: 0;
+ position: absolute;
+ right: 0;
+ top: 0;
+}
+
+.dialog-middlebar-contents {
+ display: -webkit-box;
+ max-width: 50%;
+ min-width: 45px;
+ position: relative;
+ width: 180px;
+}
+
+/* Container for the ok/cancel buttons. */
+.dialog-footer {
+ -webkit-box-align: center;
+ -webkit-box-orient: horizontal;
+ border-top: 1px solid rgb(225, 225, 225);
+ display: -webkit-box;
+ outline: none;
+ padding: 10px;
+}
+
+.progressable:not([progress]) .progress-bar,
+.progressable:not([progress]) .preparing-label {
+ display: none;
+}
+
+.progressable[progress] .ok,
+.progressable[progress] #filename-input-box,
+.progressable[progress] #preview-lines,
+.progressable[progress] .file-type {
+ display: none;
+}
+
+.progressable .progress-bar {
+ -webkit-box-flex: 1;
+ -webkit-margin-end: 20px;
+ -webkit-margin-start: 20px;
+}
+
+select.file-type:hover {
+ /* Original value is '5 fill', which hides the dropdown triangle. */
+ border-image-slice: 5;
+}
+
+/* The container for breadcrumb elements. */
+.breadcrumbs {
+ -webkit-box-align: center;
+ -webkit-box-flex: 1;
+ -webkit-box-orient: horizontal;
+ display: -webkit-box;
+ line-height: 20px;
+ overflow: hidden;
+ padding-top: 1px;
+}
+
+#dir-breadcrumbs {
+ -webkit-margin-end: 5px;
+ -webkit-margin-start: 10px;
+}
+
+/* The icon for offline mode */
+.offline-icon {
+ -webkit-margin-end: 0;
+ -webkit-margin-start: 10px;
+ background-image: -webkit-image-set(
+ url('../images/files/ui/offline.png') 1x,
+ url('../images/files/ui/2x/offline.png') 2x);
+ height: 16px;
+ opacity: 0;
+ transition-duration: 200ms;
+ transition-property: opacity;
+ transition-timing-function: ease-out;
+ width: 16px;
+}
+
+/* Transition for '-webkit-margin-start' (or -end) property is not working.
+ * So I added .offline-icon-space to animate 'width' property. */
+.offline-icon-space {
+ -webkit-margin-end: 0;
+ -webkit-margin-start: -26px; /* Clear width of .offline-icon */
+ transition-duration: 200ms;
+ transition-property: width;
+ transition-timing-function: ease-out;
+ width: 0;
+}
+
+body[drive] .dialog-container[connection='offline'] .offline-icon,
+body[drive] .dialog-container[connection='metered'] .offline-icon {
+ opacity: 1;
+}
+
+body[drive] .dialog-container[connection='offline'] .offline-icon-space,
+body[drive] .dialog-container[connection='metered'] .offline-icon-space {
+ width: 26px;
+}
+
+.breadcrumbs > [collapsed]::before {
+ content: '...';
+}
+
+.breadcrumbs > [collapsed] {
+ width: 1em;
+}
+
+/* A single directory name in the list of path breadcrumbs. */
+.breadcrumb-path {
+ color: #969696;
+ cursor: pointer;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+/* The final breadcrumb, representing the current directory. */
+#search-breadcrumbs .breadcrumb-path.breadcrumb-last {
+ color: #141414;
+ cursor: default;
+}
+
+/* The > arrow between breadcrumbs. */
+
+.breadcrumbs .separator {
+ background-image: -webkit-image-set(
+ url('../images/files/ui/breadcrumb-separator.png') 1x,
+ url('../images/files/ui/2x/breadcrumb-separator.png') 2x);
+ background-position: center center;
+ background-repeat: no-repeat;
+ height: 10px;
+ overflow: hidden;
+ width: 25px;
+}
+
+#filename-input-box input {
+ border: 1px solid #c8c8c8;
+ border-radius: 1px;
+ box-sizing: border-box;
+ height: 31px; /* border-box */
+ margin-right: 30px;
+}
+
+.filelist-panel {
+ -webkit-box-flex: 1;
+ -webkit-box-orient: vertical;
+ display: -webkit-box;
+}
+
+#list-container {
+ -webkit-box-flex: 1;
+ -webkit-box-orient: vertical;
+ display: -webkit-box;
+ position: relative;
+}
+
+#detail-table {
+ -webkit-box-flex: 1;
+ -webkit-box-orient: vertical;
+ display: -webkit-box;
+}
+
+#detail-table > list,
+.thumbnail-grid {
+ -webkit-box-flex: 1;
+ padding-bottom: 0; /* For the preview panel. Will be overridden by JS. */
+}
+
+#file-list .drag-selection-border {
+ -webkit-box-sizing: border-box;
+ background-color: rgba(255, 255, 255, 0.3);
+ border: 2px solid rgba(255, 255, 255, 0.6);
+ outline: 1px solid rgba(0, 0, 0, 0.1);
+ position: absolute;
+ z-index: 2;
+}
+
+.spinner {
+ background: 100% url(../images/common/spinner.svg);
+ height: 16px;
+ left: 50%;
+ margin-left: -8px;
+ margin-top: -8px;
+ opacity: 0.5;
+ position: absolute;
+ top: 50%;
+ width: 16px;
+}
+
+.spinner-layer {
+ background: url(../images/common/spinner.svg) center / 16px no-repeat;
+ bottom: 0;
+ left: 0;
+ position: absolute;
+ right: 0;
+ top: 0;
+ z-index: 525;
+}
+
+.downloads-warning {
+ -webkit-box-align: center;
+ -webkit-box-orient: horizontal;
+ background-color: #f0f0f0;
+ background-image: -webkit-image-set(
+ url('../images/files/ui/warning_icon_square.png') 1x,
+ url('../images/files/ui/2x/warning_icon_square.png') 2x);
+ background-position: 15px center;
+ background-repeat: no-repeat;
+ color: #666;
+ display: -webkit-box;
+ font-size: 13px;
+ height: 57px;
+ overflow: hidden;
+ padding-left: 57px; /* Make space for the icon. */
+ transition: height 70ms linear;
+}
+
+.downloads-warning[hidden] {
+ display: -webkit-box !important; /* Overrides [hidden] for animation. */
+ height: 0;
+}
+
+@-webkit-keyframes heightAnimation {
+ 0% {
+ height: 0;
+ display: -webkit-box;
+ }
+}
+
+/* Drive space warning banner. */
+.volume-warning {
+ -webkit-animation: heightAnimation 70ms linear;
+ -webkit-box-align: center;
+ -webkit-box-orient: horizontal;
+ background-image: url(chrome://resources/images/clouds.png);
+ background-repeat: repeat-x;
+ background-size: 150px 44px;
+ color: #333;
+ display: -webkit-box;
+ font-size: 13px;
+ height: 44px;
+ overflow: hidden;
+ position: relative;
+}
+
+.volume-warning[hidden] {
+ border-top-width: 0;
+ height: 0;
+}
+
+.volume-warning .drive-icon {
+ background-image: -webkit-image-set(
+ url('../images/files/ui/drive_logo.png') 1x,
+ url('../images/files/ui/2x/drive_logo.png') 2x);
+ background-position: center;
+ background-repeat: no-repeat;
+ background-size: 25px 22px;
+ height: 44px;
+ width: 50px;
+}
+
+.volume-warning .drive-text {
+ margin-right: 11px;
+}
+
+/* The cr.ui.Grid representing the detailed file list. */
+.thumbnail-grid {
+ overflow-y: auto;
+ padding-bottom: 0; /* For the preview panel. Will be overridden by JS. */
+ width: 100%;
+}
+
+body[type='full-page'] .thumbnail-frame > .img-container {
+ position: relative;
+}
+
+body[type='full-page'] .thumbnail-frame > .img-container,
+body[type='full-page'] .detail-name .detail-icon {
+ cursor: pointer;
+}
+
+.img-container > img {
+ -webkit-user-drag: none;
+ position: absolute;
+}
+
+.img-container > img:not(.cached):not(.drag-thumbnail) {
+ -webkit-animation: fadeIn 250ms linear;
+}
+
+.thumbnail-bottom {
+ -webkit-box-align: center;
+ -webkit-box-orient: horizontal;
+ -webkit-box-pack: center;
+ bottom: 0;
+ cursor: auto;
+ display: -webkit-box;
+ left: 0;
+ padding: 0 10px;
+ position: absolute;
+ right: 0;
+}
+
+.thumbnail-bottom .filename-label {
+ -webkit-box-flex: 1;
+}
+
+/* Styles specific for the grid view. */
+
+.thumbnail-grid .thumbnail-item {
+ -webkit-margin-start: 21px;
+ border: 3px solid transparent; /* Selection will make the border visible. */
+ margin-top: 20px;
+ position: relative;
+}
+
+.thumbnail-grid .thumbnail-frame {
+ background-color: rgb(245, 245, 245);
+ height: 120px;
+ overflow: hidden;
+ position: relative;
+ width: 160px;
+}
+
+.thumbnail-grid .thumbnail-item[selected] .thumbnail-frame,
+.thumbnail-grid .thumbnail-item.accepts .thumbnail-frame {
+ border-color: white;
+}
+
+.thumbnail-grid .img-container {
+ height: 100%;
+ width: 100%;
+}
+
+.thumbnail-grid .thumbnail-bottom {
+ background: rgba(0, 0, 0, 0.55);
+ color: #fff;
+ height: 30px;
+}
+
+/* Padding counterweights negative margins of items, thus eliminating scroll
+ bar when it's not needed. Max height is set to fit 8 items before showing
+ scroll bar. */
+#default-actions-list {
+ max-height: 328px;
+ padding: 1px 0;
+}
+
+#default-actions-list > li > * {
+ background-position: 5px center;
+ background-repeat: no-repeat;
+ background-size: 16px 16px;
+ padding-left: 26px;
+}
+
+#list-container list > li[selected],
+#list-container grid > li[selected],
+#default-actions-list > li[selected] {
+ background-color: rgb(225, 225, 225);
+}
+
+#list-container list:focus > li[selected],
+#list-container grid:focus > li[selected],
+#default-actions-list:focus > li[selected] {
+ background-color: rgb(66, 129, 244);
+ color: white;
+}
+
+#list-container list > li.accepts[selected],
+#list-container grid > li.accepts[selected] {
+ background-color: rgb(215, 215, 215);
+}
+
+#list-container list:focus > li.accepts[selected],
+#list-container grid:focus > li.accepts[selected] {
+ background-color: rgb(48, 125, 254);
+}
+
+#list-container list > li.accepts,
+#list-container grid > li.accepts {
+ background-color: #f1f1f1;
+}
+
+#list-container.nohover grid > .accepts {
+ background-color: transparent;
+}
+
+#directory-tree .tree-item.accepts > .tree-row,
+#navigation-list > .accepts,
+#list-container list > li.accepts,
+#list-container grid > li.accepts {
+ -webkit-animation: acceptsBlink 200ms linear 1s 3;
+}
+
+@-webkit-keyframes acceptsBlink {
+ 0% {
+ background-color: transparent;
+ }
+}
+
+.table-row-cell .selection-label {
+ -webkit-margin-end: 10px;
+ height: 15px;
+}
+
+.table-row-cell .filename-label,
+.thumbnail-item .filename-label,
+/* Show ellipsis in cells. The name column has different structure and overrides
+ this rule. */
+.table-row-cell > div {
+ display: block;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+/* Text box used for renaming in the detail list. */
+.table-row-cell input.rename {
+ border-width: 0;
+ padding: 2px 0;
+}
+
+input.rename:focus {
+ outline-color: rgb(77, 144, 254);
+}
+
+input.rename {
+ font: inherit;
+ line-height: 1;
+ text-align: inherit;
+}
+
+.table-row-cell .filename-label,
+.table-row-cell input.rename {
+ -webkit-box-flex: 1;
+}
+
+[renaming] > .filename-label {
+ display: none;
+}
+
+/* Text box used for renaming in the thumbnail list. */
+.thumbnail-grid input.rename {
+ -webkit-margin-start: -1px;
+ box-sizing: border-box;
+ height: 20px;
+ width: 114px;
+}
+
+/* The cr.ui.Table representing the detailed file list. */
+.detail-table {
+ width: 100%;
+}
+
+/* Bottom pane describing current selection. */
+.preview-panel {
+ -webkit-box-align: center;
+ -webkit-box-orient: horizontal;
+ -webkit-transition: background-color 150ms ease;
+ background: linear-gradient(
+ to bottom, rgba(255, 255, 255, 0.8), rgba(255, 255, 255, 1));
+ border-top: 1px solid rgba(20, 20, 22, 0.1);
+ bottom: 0;
+ display: -webkit-box;
+ height: 51px;
+ left: 0;
+ opacity: 1;
+ padding: 0 10px 0 7px;
+ position: absolute;
+ right: 0;
+ z-index: 3;
+}
+
+.preview-panel[visibility=hiding] {
+ -webkit-transform: translate(0, 5px);
+ opacity: 0;
+ /* Using all seems to cause preview panel and checkbox flicking issue. */
+ transition: opacity 220ms ease,
+ -webkit-transform 220ms ease;
+}
+
+.preview-panel[visibility=hidden] {
+ display: none;
+ opacity: 0;
+}
+
+.preview-panel > .left,
+.dialog-footer > .left {
+ -webkit-box-align: center;
+ -webkit-box-flex: 1;
+ -webkit-box-orient: horizontal;
+ display: -webkit-box;
+}
+
+.preview-panel > .right,
+.dialog-footer > .right {
+ -webkit-box-pack: end;
+}
+
+.preview-panel .preparing-label {
+ -webkit-margin-start: 30px;
+}
+
+.preview-panel .progress-bar {
+ -webkit-box-flex: 1;
+}
+
+.preview-thumbnails {
+ -webkit-box-orient: horizontal;
+ display: -webkit-box;
+ padding-left: 25px;
+}
+
+.preview-thumbnails > .img-container {
+ background-color: white;
+ background-size: 35px 35px; /* For file icons. */
+ border: 2px solid white;
+ box-shadow: 0 1px 1px rgba(80, 80, 80, 0.5);
+ box-sizing: border-box;
+ cursor: pointer;
+ height: 35px;
+ margin: 0 0 0 -25px; /* Overlapped images. */
+ overflow: hidden;
+ position: relative;
+ width: 35px;
+}
+
+.preview-thumbnails > .popup {
+ -webkit-transform: translate(0, 3px) scale(0.95);
+ background-color: #f2f2f2;
+ border: 2px solid #fff;
+ bottom: 8px;
+ box-shadow: 0 0 0 1px #F0F0F0,
+ 0 0 0 2px #D0D0D0,
+ 2px 2px 6px rgba(0, 0, 0, 0.2);
+ display: -webkit-flex;
+ left: -8px;
+ opacity: 0;
+ pointer-events: none;
+ position: absolute;
+ transition: opacity 180ms ease-in 300ms,
+ -webkit-transform 180ms ease-in 300ms;
+ z-index: 1000;
+}
+
+.preview-thumbnails.has-zoom:hover > .popup {
+ -webkit-transform: translate(0, 0) scale(1.0);
+ opacity: 1;
+ pointer-events: auto;
+}
+
+@-webkit-keyframes fadeIn {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+}
+
+.preview-thumbnails img {
+ -webkit-animation: fadeIn 180ms ease-in-out;
+}
+
+.preview-thumbnails > .popup > img {
+ -webkit-flex: 1 1 0;
+ -webkit-user-drag: none;
+}
+
+/* Table splitter element */
+.table-header-splitter {
+ background-image: -webkit-image-set(
+ url('../images/files/ui/vertical_separator.png') 1x,
+ url('../images/files/ui/2x/vertical_separator.png') 2x);
+ background-position: center;
+ background-repeat: repeat-y;
+ height: 20px;
+ top: 10px;
+ width: 5px;
+}
+
+.table-header-splitter:last-child {
+ display: none;
+}
+
+/* Container for a table header. */
+.table-header {
+ box-sizing: border-box;
+ height: 47px;
+}
+
+.table-header-sort-image-desc::after {
+ -webkit-padding-start: 13px;
+ background-image: -webkit-image-set(
+ url('../images/files/ui/sort_desc.png') 1x,
+ url('../images/files/ui/2x/sort_desc.png') 2x);
+ background-position: center center;
+ background-repeat: no-repeat;
+ color: #888;
+ content: '\00a0';
+ position: relative;
+ top: 1px;
+}
+
+.table-header-sort-image-asc::after {
+ -webkit-padding-start: 13px;
+ background-image: -webkit-image-set(
+ url('../images/files/ui/sort_asc.png') 1x,
+ url('../images/files/ui/2x/sort_asc.png') 2x);
+ background-position: center center;
+ background-repeat: no-repeat;
+ color: #888;
+ content: '\00a0';
+ position: relative;
+ top: -1px;
+}
+
+.preview-container .table-header {
+ border-radius: 0 4px 0 0;
+}
+
+/* Text label in a table header. */
+.table-header-label {
+ color: rgb(100, 100, 100);
+ line-height: 40px;
+ margin: 0 7px;
+}
+
+.table-row-cell > * {
+ -webkit-box-align: center;
+ -webkit-box-flex: 1;
+ -webkit-box-orient: horizontal;
+ padding: 0 10px;
+}
+
+.table-row-cell {
+ color: rgb(100, 100, 100);
+}
+
+.table-row-cell > .detail-name {
+ display: -webkit-box;
+}
+
+.table-row-cell > .detail-name {
+ color: rgb(0, 0, 0);
+}
+
+
+#list-container list:focus > [selected] .table-row-cell,
+#list-container list:focus > [selected] .detail-name {
+ color: white;
+}
+
+.table-row-cell {
+ -webkit-box-align: center;
+}
+
+.file-checkbox {
+ -webkit-margin-end: 0;
+ -webkit-margin-start: 0;
+ position: relative;
+ z-index: 2;
+}
+
+#select-all-checkbox {
+ -webkit-margin-end: 13px;
+ -webkit-margin-start: 3px;
+ margin-bottom: 0;
+ margin-top: 0;
+ vertical-align: middle;
+}
+
+#list-container .table-header #select-all-checkbox,
+#list-container li.table-row .file-checkbox {
+ -webkit-appearance: none;
+ background-image: -webkit-image-set(
+ url('../images/files/ui/select_checkbox.png') 1x,
+ url('../images/files/ui/2x/select_checkbox.png') 2x);
+ background-position: center;
+ background-repeat: no-repeat;
+ border-style: none;
+ height: 15px;
+ width: 15px;
+}
+
+#list-container li.table-row .file-checkbox {
+ vertical-align: top;
+}
+
+#list-container .table-header #select-all-checkbox::after,
+#list-container li.table-row .file-checkbox::after {
+ content: none;
+}
+
+#list-container .table-header #select-all-checkbox:checked,
+#list-container li.table-row .file-checkbox:checked {
+ background-image: -webkit-image-set(
+ url('../images/files/ui/select_checkbox_checked.png') 1x,
+ url('../images/files/ui/2x/select_checkbox_checked.png') 2x);
+}
+
+#list-container .table-header #select-all-checkbox:checked,
+#list-container list li.table-row[selected] .file-checkbox {
+ -webkit-filter: brightness(0) opacity(40%);
+}
+
+#list-container list:focus li.table-row[selected] .file-checkbox {
+ -webkit-filter: brightness(0) invert();
+}
+
+#list-container li.table-row,
+#default-actions-list li {
+ height: 29px;
+ line-height: 29px;
+}
+
+/* The icon in the name column. See file_types.css for specific icons. */
+.detail-icon {
+ height: 24px;
+ width: 24px;
+}
+
+#detail-table .detail-icon {
+ /* To shift the icon position. */
+ margin-bottom: 2px;
+}
+
+.metadata-item {
+ -webkit-box-flex: 1;
+ -webkit-box-orient: horizontal;
+ -webkit-padding-start: 8px;
+ display: -webkit-box;
+}
+
+.metadata-label {
+ -webkit-margin-end: 6px;
+}
+
+.preview-panel .spacer {
+ -webkit-box-flex: 1;
+}
+
+#delete-button {
+ min-width: 21px; /* overrride */
+ padding: 0; /* overrride */
+ width: 21px;
+}
+
+#delete-button::before {
+ /* Background image should be specified in the before pseudo element because
+ * border image fill is specified to delete-button. */
+ background: -webkit-image-set(
+ url(../images/files/ui/onbutton_trash.png) 1x,
+ url(../images/files/ui/2x/onbutton_trash.png) 2x) no-repeat center;
+ content: '';
+ display: block;
+ height: 100%;
+ width: 100%;
+}
+
+#delete-button[disabled] {
+ display: none;
+}
+
+#tasks-menu menuitem:not(.change-default) {
+ background-position: left 10px center;
+ padding-left: 32px;
+}
+
+#share-button {
+ display: block;
+ min-width: 0; /* overrride */
+}
+
+#preview-lines {
+ -webkit-box-flex: 1;
+ -webkit-margin-end: 10px;
+ -webkit-margin-start: 10px;
+ vertical-align: middle;
+}
+
+/* The selection summary text at the bottom of the preview pane. */
+.preview-summary {
+ color: #666;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ vertical-align: middle;
+ white-space: nowrap;
+}
+
+.preview-summary .calculating-size {
+ margin-left: 5px;
+ opacity: 0.5;
+}
+
+.detail-name > * {
+ -webkit-box-align: center;
+ display: -webkit-box;
+}
+
+/* Overriding input.common[type='checkbox'] rule in common.css. */
+.detail-name > input.common[type='checkbox'] {
+ -webkit-margin-end: 4px;
+ -webkit-margin-start: -1px;
+ border-color: #444;
+}
+
+list .detail-name > .file-checkbox::before,
+.pin::before {
+ /* Invisible area that reacts on mouse events. */
+ content: '';
+ display: -webkit-box;
+ height: 38px;
+ left: -8px;
+ position: absolute;
+ right: -9px;
+ top: -14px;
+}
+
+#filename-input-box {
+ -webkit-box-align: center;
+ -webkit-box-flex: 1;
+ display: -webkit-box;
+}
+
+#filename-input-box input {
+ -webkit-box-flex: 1;
+ display: -webkit-box;
+ padding: 1px 2px;
+}
+
+#filename-input-box .filename-label {
+ -webkit-box-orient: horizontal;
+ background-color: white;
+ color: #333;
+ display: -webkit-box;
+ padding-right: 4px;
+}
+
+body:not([type='saveas-file']) #filename-input-box {
+ display: none;
+}
+
+/* A vertical spring. */
+.vertical-spacer {
+ -webkit-box-flex: 1;
+ -webkit-box-orient: vertical;
+ display: -webkit-box;
+}
+
+/* Dimmed items */
+
+body[type='folder'] .file,
+body[type='upload-folder'] .file,
+body[drive] .dialog-container[connection='offline'] .dim-offline {
+ opacity: 0.4;
+}
+
+/* Overlay pane covering the entire file manager window (e.g. image editor)*/
+.overlay-pane {
+ -webkit-app-region: no-drag;
+ border: none;
+ height: 100%;
+ left: 0;
+ position: absolute;
+ top: 0;
+ width: 100%;
+ z-index: 1000; /* Must be above all elements in file manager container. */
+}
+
+/* When the overlay pane is visible hide everything else so that the tab order
+ is not confused. */
+body[overlay-visible] > :not(.overlay-pane) {
+ display: none !important;
+}
+
+/* Invisible container for elements representing files while dragging. */
+#drag-container {
+ left: 0;
+ /* Hack for extra margins caused by setDragImage(). */
+ padding: 1000px 0 0 1000px;
+ position: fixed;
+ top: 0;
+ z-index: -1; /* below .dialog-container */
+}
+
+#drag-container .drag-contents {
+ -webkit-box-orient: horizontal;
+ background-color: #fafafa;
+ border: 1px solid #bbb;
+ border-radius: 3px;
+ box-shadow: 0 2px 2px 0 rgba(0, 0, 0, .2);
+ display: -webkit-box;
+ margin-bottom: 5px;
+ padding: 6px;
+ transition: opacity 200ms ease-in;
+}
+
+#drag-container .drag-contents.for-image {
+ padding: 2px;
+}
+
+#drag-container .thumbnail-item {
+ -webkit-box-orient: horizontal;
+ display: -webkit-box;
+}
+
+/* When changing these properties please preserve these conditions:
+ 1. width == height (so that the rotated thumbnail does not look off-center)
+ 2. width % 8 == 0 (to minimize rounding errors in the centering code) */
+#drag-container .img-container {
+ -webkit-box-flex: 0;
+ display: -webkit-box;
+ height: 64px;
+ overflow: hidden;
+ position: relative;
+ width: 64px;
+}
+
+#drag-container .label {
+ -webkit-box-flex: 1;
+ font-weight: bold;
+ line-height: 24px;
+ max-width: 320px;
+ overflow: hidden;
+ padding: 0 5px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+menu.file-context-menu {
+ z-index: 600; /* Must be below the overlay pane (1000). */
+}
+
+menu.chrome-menu hr {
+ color: transparent;
+ font-size: 0;
+}
+
+div.offline {
+ -webkit-box-pack: center;
+ display: -webkit-box;
+}
+
+div.offline > * {
+ -webkit-box-align: center;
+ display: -webkit-box;
+}
+
+div.shade {
+ /* transition: opacity 1000ms linear; */
+ background-color: rgba(255, 255, 255, 0.8);
+ bottom: 0;
+ left: 0;
+ opacity: 0;
+ position: absolute;
+ right: 0;
+ top: 0;
+}
+
+div.shade[fadein] {
+ opacity: 1;
+}
+
+/* Message panel for unmounted Drive */
+#unmounted-panel,
+#format-panel {
+ bottom: 0;
+ color: #333;
+ display: none;
+ left: 0;
+ padding-left: 50px;
+ padding-top: 20px;
+ position: absolute;
+ right: 0;
+ top: 0;
+}
+
+body[drive='mounting'] .dialog-container #unmounted-panel,
+body[drive='error'] .dialog-container #unmounted-panel,
+body[unformatted] .dialog-container #format-panel {
+ display: block;
+}
+
+body[drive='unmounted'] .dialog-container .filelist-panel,
+body[drive='mounting'] .dialog-container .filelist-panel,
+body[drive='error'] .dialog-container .filelist-panel,
+body[unformatted] .dialog-container .filelist-panel {
+ /* Hide file list when Drive is not mounted.
+ Use opacity to avoid manual resizing.*/
+ opacity: 0;
+}
+
+#unmounted-panel > *,
+#format-panel > * {
+ -webkit-box-align: center;
+ -webkit-box-orient: horizontal;
+ -webkit-box-pack: start;
+ display: none;
+ height: 22px;
+ margin-bottom: 10px;
+}
+
+#unmounted-panel > .loading {
+ position: relative;
+}
+
+#unmounted-panel > .loading > .spinner-box {
+ bottom: 0;
+ position: absolute;
+ right: 100%;
+ top: 0;
+ width: 40px;
+}
+
+body[unformatted] #format-panel > .error,
+body[drive='mounting'] #unmounted-panel > .loading,
+body[drive='error'] #unmounted-panel > .error,
+#format-panel > #format-button,
+#unmounted-panel.retry-enabled > .learn-more {
+ display: -webkit-box;
+}
+
+.plain-link {
+ color: rgb(17, 85, 204);
+ cursor: pointer;
+ text-decoration: none;
+}
+
+.buttonbar > * {
+ position: relative;
+}
+
+.buttonbar .tooltip,
+.topbutton-bar .tooltip {
+ right: -12px;
+ top: 35px;
+}
+
+/* Tooltips */
+.tooltip {
+ background: #2d2d2d;
+ border-radius: 0;
+ box-shadow: 1px 2px 4px #ccc;
+ box-sizing: border-box;
+ color: white;
+ display: block;
+ font-size: 11px;
+ font-weight: bold;
+ height: 29px;
+ line-height: 29px;
+ margin-left: -20px;
+ min-width: 50px;
+ opacity: 0;
+ outline: 1px solid rgba(255, 255, 255, 0.5);
+ padding: 0 10px;
+ pointer-events: none;
+ position: absolute;
+ text-align: center;
+ top: 5px;
+ white-space: nowrap;
+ z-index: 600; /* Must be below the overlay pane (1000). */
+}
+
+.tooltip::after,
+.tooltip::before {
+ border-left: 5px solid transparent;
+ border-right: 5px solid transparent;
+ border-top: transparent;
+ content: '';
+ display: block;
+ margin-left: -5px;
+ position: absolute;
+ right: 24px;
+ top: -5px;
+}
+
+.tooltip::after {
+ border-bottom: 5px solid #2d2d2d;
+}
+
+.tooltip::before {
+ border-bottom: 5px solid rgba(255, 255, 255, 0.5);
+}
+
+/* Show with delay, disappear instantly */
+@-webkit-keyframes tooltip-show {
+ 0% { opacity: 0; }
+ 90% { opacity: 0; }
+ 100% { opacity: 1; }
+}
+
+:hover > .tooltip {
+ -webkit-animation-duration: 800ms;
+ -webkit-animation-iteration-count: 1;
+ -webkit-animation-name: tooltip-show;
+ -webkit-animation-timing-function: linear;
+ opacity: 1;
+}
+
+#no-search-results {
+ bottom: 0;
+ display: none;
+ left: 0;
+ padding: 10px;
+ position: absolute;
+ right: 0;
+ top: 28px; /* Leave room for the file list header. */
+}
+
+.dialog-container:not([drive-welcome='page']) #no-search-results[show] {
+ display: block;
+}
+
+#volume-space-info-contents {
+ -webkit-box-align: center;
+ display: -webkit-box;
+}
+
+#volume-space-info-contents > div {
+ -webkit-box-flex: 1;
+ -webkit-margin-start: 15px;
+ display: -webkit-box;
+}
+
+#list-container .table-header-inner {
+ height: 100%;
+}
+
+#list-container .table-header-cell:hover {
+ background-color: inherit;
+}
+
+#list-container .table-header-cell:first-child {
+ -webkit-box-sizing: border-box;
+ -webkit-padding-start: 8px;
+}
+
+button:focus {
+ outline-color: rgb(77, 144, 254);
+}
+
+#new-folder-button {
+ margin-right: 30px;
+}
+
+#default-action-dialog {
+ min-width: 300px;
+ width: auto;
+}
+
+.drive-welcome-wrapper {
+ /* drive_welcome.css will override it once loaded. */
+ display: none;
+}
+
+list.autocomplete-suggestions {
+ -webkit-margin-before: -7px;
+ -webkit-margin-start: -38px;
+ background-color: rgb(250, 250, 250);
+ border-radius: 3px;
+ box-shadow: 0 1px 4px 0 rgba(0, 0, 0, .5);
+ box-sizing: border-box; /* To match the width with the search box's. */
+ color: rgb(34, 34, 34);
+ overflow: hidden;
+ padding: 5px 0;
+ position: fixed;
+ width: 300px !important; /* This overrides the value specified by script. */
+ z-index: 550;
+}
+
+list.autocomplete-suggestions > li {
+ -webkit-box-align: center;
+ display: -webkit-box;
+ padding: 3px 0;
+}
+
+list.autocomplete-suggestions > li > div.detail-icon {
+ -webkit-margin-end: 6px;
+ -webkit-margin-start: 6px;
+}
+
+list.autocomplete-suggestions > li > div.detail-text {
+ -webkit-box-flex: 1;
+ overflow-x: hidden;
+ text-overflow: ellipsis;
+}
+
+list.autocomplete-suggestions > li > div.detail-text em {
+ color: rgb(150, 150, 150);
+ font-style: normal;
+}
+
+list.autocomplete-suggestions > li > div[search-icon] {
+ background: -webkit-image-set(
+ url('../images/files/ui/search_icon_active.png') 1x,
+ url('../images/files/ui/2x/search_icon_active.png') 2x);
+ background-position: center;
+ background-repeat: no-repeat;
+}
+
+list.autocomplete-suggestions > li[selected] > div[search-icon],
+list.autocomplete-suggestions > li[lead] > div[search-icon] {
+ -webkit-filter: brightness(0) invert();
+}
+
+list.autocomplete-suggestions > [selected],
+list.autocomplete-suggestions > [lead] {
+ background-color: rgb(66, 129, 244);
+ color: white;
+}
+
+list.autocomplete-suggestions > [selected] > div.detail-text em,
+list.autocomplete-suggestions > [lead] > div.detail-text em {
+ color: white;
+}
+
+#gear-menu {
+ margin-top: 8px;
+}
+
+#gear-menu > menuitem:not(.menuitem-button) {
+ margin-right: 50px;
+}
+
+/* View buttons in the gear menu. */
+
+menuitem#detail-view {
+ background-image: -webkit-image-set(
+ url('../images/files/ui/button_list_view.png') 1x,
+ url('../images/files/ui/2x/button_list_view.png') 2x);
+}
+
+menuitem#detail-view[selected]:not([disabled]),
+menuitem#detail-view[lead]:not([disabled]) {
+ background-image: -webkit-image-set(
+ url('../images/files/ui/button_list_view_white.png') 1x,
+ url('../images/files/ui/2x/button_list_view_white.png') 2x);
+}
+
+menuitem#thumbnail-view {
+ background-image: -webkit-image-set(
+ url('../images/files/ui/button_mosaic_view.png') 1x,
+ url('../images/files/ui/2x/button_mosaic_view.png') 2x);
+}
+
+menuitem#thumbnail-view[selected]:not([disabled]),
+menuitem#thumbnail-view[lead]:not([disabled]) {
+ background-image: -webkit-image-set(
+ url('../images/files/ui/button_mosaic_view_white.png') 1x,
+ url('../images/files/ui/2x/button_mosaic_view_white.png') 2x);
+}
+
+#iframe-drag-area {
+ -webkit-app-region: drag;
+ height: 48px;
+ left: 64px;
+ position: absolute;
+ right: 92px;
+ top: 0;
+ width: auto;
+ z-index: 101;
+}
+
+#suggest-app-dialog {
+ background-color: #fff;
+ border: 0;
+ padding: 0;
+ width: auto;
+}
+
+#suggest-app-dialog .cr-dialog-title {
+ /* Entire height: 44px (content-box 22px + padding 11px * 2) */
+ font-size: 16px;
+ height: 22px;
+ margin: 0;
+ padding: 11px 18px;
+}
+
+#suggest-app-dialog #webview-container {
+ border-bottom: solid 1px #bbb;
+ border-top: solid 1px #bbb;
+ position: relative;
+}
+
+#suggest-app-dialog.show-spinner #webview-container webview {
+ pointer-events: none;
+}
+
+#suggest-app-dialog:not(.show-spinner) .spinner-layer {
+ display: none;
+}
+
+#suggest-app-dialog .spinner-layer {
+ background-color: rgba(255, 255, 255, 0.7);
+}
+
+#suggest-app-dialog .cr-dialog-buttons,
+#suggest-app-dialog .cr-dialog-ok,
+#suggest-app-dialog .cr-dialog-cancel {
+ display: none;
+}
+
+#suggest-app-dialog .cr-dialog-text {
+ -webkit-padding-after: 10px;
+ -webkit-padding-before: 0;
+ -webkit-padding-end: 20px;
+ -webkit-padding-start: 20px;
+ margin: 0;
+}
+
+#suggest-app-dialog #buttons {
+ background: #eee;
+ width: 100%;
+}
+
+#suggest-app-dialog #buttons > #webstore-button {
+ -webkit-padding-after: 10px;
+ -webkit-padding-before: 10px;
+ -webkit-padding-end: 10px;
+ -webkit-padding-start: 36px;
+ background-image: -webkit-image-set(
+ url('chrome://theme/IDR_WEBSTORE_ICON_16') 1x,
+ url('chrome://theme/IDR_WEBSTORE_ICON_16@2x') 2x);
+ background-position: 12px center;
+ background-repeat: no-repeat;
+ color: #00f;
+ cursor: pointer;
+ display: inline-block;
+ height: 16px;
+}
+
+.cr-dialog-frame.error-dialog-frame {
+ width: 300px;
+}
+
+.error-dialog-frame .error-dialog-img {
+ background-image: -webkit-image-set(
+ url('chrome://theme/IDR_ERROR_NETWORK_GENERIC') 1x,
+ url('chrome://theme/IDR_ERROR_NETWORK_GENERIC@2x') 2x);
+ background-position: center;
+ background-repeat: no-repeat;
+ height: 40px;
+}
+
+.error-dialog-frame .cr-dialog-cancel {
+ display: none;
+}
+
+.error-dialog-frame .cr-dialog-close,
+.error-dialog-frame .cr-dialog-title {
+ display: none;
+}
+
+.error-dialog-frame .cr-dialog-text {
+ text-align: center;
+}
+
+.cr-dialog-frame.share-dialog-frame {
+ background-color: white;
+ width: auto;
+}
+
+.share-dialog-webview-wrapper {
+ height: 100px;
+ margin-top: 10px;
+ min-width: 300px;
+ overflow: hidden;
+ transition: height 200ms ease;
+}
+
+.share-dialog-webview {
+ height: 100%;
+ width: 100%;
+}
+
+.share-dialog-webview-wrapper:not(.loaded) .share-dialog-webview {
+ visibility: hidden;
+}
+
+.share-dialog-frame .cr-dialog-text,
+.share-dialog-frame .cr-dialog-buttons {
+ display: none;
+}
+
+#conflict-confirm-dialog .cr-dialog-buttons {
+ align-items: baseline;
+ display: flex;
+}
+
+#conflict-confirm-dialog input[type=checkbox] {
+ -webkit-margin-start: -2px;
+ width: auto;
+}
+
+#conflict-confirm-dialog label {
+ flex: 1 0 auto;
+}
+
+/* Progress center */
+
+@-webkit-keyframes progress-center-toggle {
+ /* Height values of each frame are set by script. */
+ from {
+ }
+ to {
+ }
+}
+
+#progress-center {
+ background-color: transparent;
+ border-top: 1px solid transparent;
+ overflow: hidden;
+ position: relative;
+ transition: background-color 300ms linear,
+ border 300ms linear;
+}
+
+#progress-center.opened {
+ background-color: #ebebeb;
+ border-top: 1px solid #d8d8d8;
+}
+
+#progress-center.animated {
+ -webkit-animation: progress-center-toggle 300ms ease-out;
+}
+
+#progress-center-open-view {
+ opacity: 1;
+ padding-top: 42px;
+ transition: opacity 300ms linear;
+}
+
+#progress-center:not(.opened) #progress-center-open-view {
+ opacity: 0;
+ pointer-events: none;
+ position: absolute;
+}
+
+#progress-center-close-view {
+ opacity: 1;
+ transition: opacity 300ms linear;
+}
+
+#progress-center.opened #progress-center-close-view {
+ opacity: 0;
+ pointer-events: none;
+ position: absolute;
+}
+
+#progress-center.animated #progress-center-open-view,
+#progress-center.animated #progress-center-close-view {
+ left: 0;
+ pointer-events: none;
+ position: absolute;
+ right: 0;
+ top: 0;
+ z-index: 1;
+}
+
+#progress-center li {
+ display: block;
+ min-height: 29px; /* label 17px + frame 12px */
+ padding-bottom: 20px;
+}
+
+#progress-center label {
+ color: #777;
+ display: block;
+ font: 12px/17px normal;
+ min-height: 17px;
+ overflow: hidden;
+ padding: 0 20px;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+#progress-center li.error label {
+ white-space: normal;
+}
+
+#progress-center .progress-frame {
+ -webkit-padding-end: 10px;
+ -webkit-padding-start: 20px;
+ align-items: center;
+ display: flex;
+ margin-top: 4px;
+}
+
+#progress-center li.error .progress-frame {
+ height: 0;
+}
+
+#progress-center .progress-bar {
+ -webkit-margin-end: 24px;
+ background: #d8d8d8;
+ border-radius: 3px;
+ flex: 1 0 0;
+ height: 6px;
+ opacity: 1;
+ overflow: hidden;
+}
+
+#progress-center li.error .progress-bar,
+#progress-center li.canceled .progress-bar {
+ visibility: hidden;
+}
+
+#progress-center .progress-track {
+ background: #787878;
+ height: 100%;
+}
+
+#progress-center .progress-track.animated {
+ transition: width 300ms linear;
+}
+
+#progress-center button {
+ background: orange;
+ border: none;
+ cursor: pointer;
+ flex: 0 0 auto;
+ height: 12px;
+ min-height: 0;
+ min-width: 0;
+ outline: none;
+ padding: 0;
+ width: 12px;
+ z-index: 0;
+}
+
+#progress-center button.toggle {
+ background: -webkit-image-set(
+ url(../images/files/ui/process_drawer_button_opened.png) 1x,
+ url(../images/files/ui/2x/process_drawer_button_opened.png) 2x)
+ no-repeat;
+ position: absolute;
+ right: 10px;
+ top: 17px; /* label height */
+ transition: top 300ms ease-out;
+ z-index: 1;
+}
+
+#progress-center button.toggle:hover {
+ background: -webkit-image-set(
+ url(../images/files/ui/process_drawer_button_opened_hover.png) 1x,
+ url(../images/files/ui/2x/process_drawer_button_opened_hover.png) 2x)
+ no-repeat;
+}
+
+#progress-center button.toggle:active {
+ background: -webkit-image-set(
+ url(../images/files/ui/process_drawer_button_opened_pressed.png) 1x,
+ url(../images/files/ui/2x/process_drawer_button_opened_pressed.png) 2x)
+ no-repeat;
+}
+
+/*
+ * If the closed progress center has only one item,
+ * toggle button turned into cancel button the item.
+ */
+#progress-center:not(.opened) #progress-center-close-view.single ~
+ button.toggle,
+#progress-center button.cancel {
+ background: -webkit-image-set(
+ url(../images/files/ui/close_bar.png) 1x,
+ url(../images/files/ui/2x/close_bar.png) 2x)
+ no-repeat;
+}
+
+#progress-center:not(.opened)
+ #progress-center-close-view.single:not(.cancelable) button.toggle,
+#progress-center li:not(.cancelable) button.cancel {
+ visibility: hidden;
+}
+
+#progress-center.opened button.toggle {
+ background: -webkit-image-set(
+ url(../images/files/ui/process_drawer_button_closed.png) 1x,
+ url(../images/files/ui/2x/process_drawer_button_closed.png) 2x)
+ no-repeat;
+ top: 10px;
+}
+
+#progress-center.opened button.toggle:hover {
+ background: -webkit-image-set(
+ url(../images/files/ui/process_drawer_button_closed_hover.png) 1x,
+ url(../images/files/ui/2x/process_drawer_button_closed_hover.png) 2x)
+ no-repeat;
+}
+
+#progress-center.opened button.toggle:active {
+ background: -webkit-image-set(
+ url(../images/files/ui/process_drawer_button_closed_pressed.png) 1x,
+ url(../images/files/ui/2x/process_drawer_button_closed_pressed.png) 2x)
+ no-repeat;
+}
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/css/file_types.css b/chromium/chrome/browser/resources/file_manager/foreground/css/file_types.css
new file mode 100644
index 00000000000..75603497f07
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/css/file_types.css
@@ -0,0 +1,477 @@
+/* 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. */
+
+/* Small icons for file types, used in lists and menus. */
+[file-type-icon] {
+ background-image: -webkit-image-set(
+ url('../../common/images/file_types/100/generic.png') 1x,
+ url('../../common/images/file_types/200/generic.png') 2x);
+ background-position: center;
+ background-repeat: no-repeat;
+}
+
+tree:focus .tree-item[selected] > .tree-row > [file-type-icon],
+list:focus [selected] [file-type-icon],
+list.autocomplete-suggestions [selected] [file-type-icon] {
+ background-image: -webkit-image-set(
+ url('../../common/images/file_types/100/generic_white.png') 1x,
+ url('../../common/images/file_types/200/generic_white.png') 2x);
+}
+
+[file-type-icon='archive'] {
+ background-image: -webkit-image-set(
+ url('../../common/images/file_types/100/archive.png') 1x,
+ url('../../common/images/file_types/200/archive.png') 2x);
+}
+
+tree:focus .tree-item[selected] > .tree-row > [file-type-icon='archive'],
+list:focus [selected] [file-type-icon='archive'],
+list.autocomplete-suggestions [selected] [file-type-icon='archive'] {
+ background-image: -webkit-image-set(
+ url('../../common/images/file_types/100/archive_white.png') 1x,
+ url('../../common/images/file_types/200/archive_white.png') 2x);
+}
+
+[file-type-icon='audio'] {
+ background-image: -webkit-image-set(
+ url('../../common/images/file_types/100/audio.png') 1x,
+ url('../../common/images/file_types/200/audio.png') 2x);
+}
+
+tree:focus .tree-item[selected] > .tree-row > [file-type-icon='audio'],
+list:focus [selected] [file-type-icon='audio'],
+list.autocomplete-suggestions [selected] [file-type-icon='audio'] {
+ background-image: -webkit-image-set(
+ url('../../common/images/file_types/100/audio_white.png') 1x,
+ url('../../common/images/file_types/200/audio_white.png') 2x);
+}
+
+[file-type-icon='excel'] {
+ background-image: -webkit-image-set(
+ url('../../common/images/file_types/100/excel.png') 1x,
+ url('../../common/images/file_types/200/excel.png') 2x);
+}
+
+tree:focus .tree-item[selected] > .tree-row > [file-type-icon='excel'],
+list:focus [selected] [file-type-icon='excel'],
+list.autocomplete-suggestions [selected] [file-type-icon='excel'] {
+ background-image: -webkit-image-set(
+ url('../../common/images/file_types/100/excel_white.png') 1x,
+ url('../../common/images/file_types/200/excel_white.png') 2x);
+}
+
+[file-type-icon='folder'] {
+ background-image: -webkit-image-set(
+ url('../../common/images/file_types/100/folder.png') 1x,
+ url('../../common/images/file_types/200/folder.png') 2x);
+}
+
+tree:focus .tree-item[selected] > .tree-row > [file-type-icon='folder'],
+list:focus [selected] [file-type-icon='folder'],
+list.autocomplete-suggestions [selected] [file-type-icon='folder'] {
+ background-image: -webkit-image-set(
+ url('../../common/images/file_types/100/folder_white.png') 1x,
+ url('../../common/images/file_types/200/folder_white.png') 2x);
+}
+
+[file-type-icon='form'] {
+ background-image: -webkit-image-set(
+ url('../../common/images/file_types/100/form.png') 1x,
+ url('../../common/images/file_types/200/form.png') 2x);
+}
+
+tree:focus .tree-item[selected] > .tree-row > [file-type-icon='form'],
+list:focus [selected] [file-type-icon='form'],
+list.autocomplete-suggestions [selected] [file-type-icon='form'] {
+ background-image: -webkit-image-set(
+ url('../../common/images/file_types/100/form_white.png') 1x,
+ url('../../common/images/file_types/200/form_white.png') 2x);
+}
+
+[file-type-icon='gdoc'] {
+ background-image: -webkit-image-set(
+ url('../../common/images/file_types/100/gdoc.png') 1x,
+ url('../../common/images/file_types/200/gdoc.png') 2x);
+}
+
+tree:focus .tree-item[selected] > .tree-row > [file-type-icon='gdoc'],
+list:focus [selected] [file-type-icon='gdoc'],
+list.autocomplete-suggestions [selected] [file-type-icon='gdoc'] {
+ background-image: -webkit-image-set(
+ url('../../common/images/file_types/100/gdoc_white.png') 1x,
+ url('../../common/images/file_types/200/gdoc_white.png') 2x);
+}
+
+[file-type-icon='gdraw'] {
+ background-image: -webkit-image-set(
+ url('../../common/images/file_types/100/gdraw.png') 1x,
+ url('../../common/images/file_types/200/gdraw.png') 2x);
+}
+
+tree:focus .tree-item[selected] > .tree-row > [file-type-icon='gdraw'],
+list:focus [selected] [file-type-icon='gdraw'],
+list.autocomplete-suggestions [selected] [file-type-icon='gdraw'] {
+ background-image: -webkit-image-set(
+ url('../../common/images/file_types/100/gdraw_white.png') 1x,
+ url('../../common/images/file_types/200/gdraw_white.png') 2x);
+}
+
+[file-type-icon='glink'] {
+ background-image: -webkit-image-set(
+ url('../../common/images/file_types/100/glink.png') 1x,
+ url('../../common/images/file_types/200/glink.png') 2x);
+}
+
+tree:focus .tree-item[selected] > .tree-row > [file-type-icon='glink'],
+list:focus [selected] [file-type-icon='glink'],
+list.autocomplete-suggestions [selected] [file-type-icon='glink'] {
+ background-image: -webkit-image-set(
+ url('../../common/images/file_types/100/glink_white.png') 1x,
+ url('../../common/images/file_types/200/glink_white.png') 2x);
+}
+
+[file-type-icon='gsheet'] {
+ background-image: -webkit-image-set(
+ url('../../common/images/file_types/100/gsheet.png') 1x,
+ url('../../common/images/file_types/200/gsheet.png') 2x);
+}
+
+tree:focus .tree-item[selected] > .tree-row > [file-type-icon='gsheet'],
+list:focus [selected] [file-type-icon='gsheet'],
+list.autocomplete-suggestions [selected] [file-type-icon='gsheet'] {
+ background-image: -webkit-image-set(
+ url('../../common/images/file_types/100/gsheet_white.png') 1x,
+ url('../../common/images/file_types/200/gsheet_white.png') 2x);
+}
+
+[file-type-icon='gslides'] {
+ background-image: -webkit-image-set(
+ url('../../common/images/file_types/100/gslides.png') 1x,
+ url('../../common/images/file_types/200/gslides.png') 2x);
+}
+
+tree:focus .tree-item[selected] > .tree-row > [file-type-icon='gslides'],
+list:focus [selected] [file-type-icon='gslides'],
+list.autocomplete-suggestions [selected] [file-type-icon='gslides'] {
+ background-image: -webkit-image-set(
+ url('../../common/images/file_types/100/gslides_white.png') 1x,
+ url('../../common/images/file_types/200/gslides_white.png') 2x);
+}
+
+[file-type-icon='gtable'] {
+ background-image: -webkit-image-set(
+ url('../../common/images/file_types/100/gtable.png') 1x,
+ url('../../common/images/file_types/200/gtable.png') 2x);
+}
+
+tree:focus .tree-item[selected] > .tree-row > [file-type-icon='gtable'],
+list:focus [selected] [file-type-icon='gtable'],
+list.autocomplete-suggestions [selected] [file-type-icon='gtable'] {
+ background-image: -webkit-image-set(
+ url('../../common/images/file_types/100/gtable_white.png') 1x,
+ url('../../common/images/file_types/200/gtable_white.png') 2x);
+}
+
+[file-type-icon='gform'] {
+ background-image: -webkit-image-set(
+ url('../../common/images/file_types/100/form.png') 1x,
+ url('../../common/images/file_types/200/form.png') 2x);
+}
+
+tree:focus .tree-item[selected] > .tree-row > [file-type-icon='gform'],
+list:focus [selected] [file-type-icon='gform'],
+list.autocomplete-suggestions [selected] [file-type-icon='gform'] {
+ background-image: -webkit-image-set(
+ url('../../common/images/file_types/100/form_white.png') 1x,
+ url('../../common/images/file_types/200/form_white.png') 2x);
+}
+
+[file-type-icon='image'] {
+ background-image: -webkit-image-set(
+ url('../../common/images/file_types/100/image.png') 1x,
+ url('../../common/images/file_types/200/image.png') 2x);
+}
+
+tree:focus .tree-item[selected] > .tree-row > [file-type-icon='image'],
+list:focus [selected] [file-type-icon='image'],
+list.autocomplete-suggestions [selected] [file-type-icon='image'] {
+ background-image: -webkit-image-set(
+ url('../../common/images/file_types/100/image_white.png') 1x,
+ url('../../common/images/file_types/200/image_white.png') 2x);
+}
+
+[file-type-icon='pdf'] {
+ background-image: -webkit-image-set(
+ url('../../common/images/file_types/100/pdf.png') 1x,
+ url('../../common/images/file_types/200/pdf.png') 2x);
+}
+
+tree:focus .tree-item[selected] > .tree-row > [file-type-icon='pdf'],
+list:focus [selected] [file-type-icon='pdf'],
+list.autocomplete-suggestions [selected] [file-type-icon='pdf'] {
+ background-image: -webkit-image-set(
+ url('../../common/images/file_types/100/pdf_white.png') 1x,
+ url('../../common/images/file_types/200/pdf_white.png') 2x);
+}
+
+[file-type-icon='ppt'] {
+ background-image: -webkit-image-set(
+ url('../../common/images/file_types/100/ppt.png') 1x,
+ url('../../common/images/file_types/200/ppt.png') 2x);
+}
+
+tree:focus .tree-item[selected] > .tree-row > [file-type-icon='ppt'],
+list:focus [selected] [file-type-icon='ppt'],
+list.autocomplete-suggestions [selected] [file-type-icon='ppt'] {
+ background-image: -webkit-image-set(
+ url('../../common/images/file_types/100/ppt_white.png') 1x,
+ url('../../common/images/file_types/200/ppt_white.png') 2x);
+}
+
+[file-type-icon='script'] {
+ background-image: -webkit-image-set(
+ url('../../common/images/file_types/100/script.png') 1x,
+ url('../../common/images/file_types/200/script.png') 2x);
+}
+
+tree:focus .tree-item[selected] > .tree-row > [file-type-icon='script'],
+list:focus [selected] [file-type-icon='script'],
+list.autocomplete-suggestions [selected] [file-type-icon='script'] {
+ background-image: -webkit-image-set(
+ url('../../common/images/file_types/100/script_white.png') 1x,
+ url('../../common/images/file_types/200/script_white.png') 2x);
+}
+
+[file-type-icon='sites'] {
+ background-image: -webkit-image-set(
+ url('../../common/images/file_types/100/sites.png') 1x,
+ url('../../common/images/file_types/200/sites.png') 2x);
+}
+
+tree:focus .tree-item[selected] > .tree-row > [file-type-icon='sites'],
+list:focus [selected] [file-type-icon='sites'],
+list.autocomplete-suggestions [selected] [file-type-icon='sites'] {
+ background-image: -webkit-image-set(
+ url('../../common/images/file_types/100/sites_white.png') 1x,
+ url('../../common/images/file_types/200/sites_white.png') 2x);
+}
+
+[file-type-icon='video'] {
+ background-image: -webkit-image-set(
+ url('../../common/images/file_types/100/video.png') 1x,
+ url('../../common/images/file_types/200/video.png') 2x);
+}
+
+tree:focus .tree-item[selected] > .tree-row > [file-type-icon='video'],
+list:focus [selected] [file-type-icon='video'],
+list.autocomplete-suggestions [selected] [file-type-icon='video'] {
+ background-image: -webkit-image-set(
+ url('../../common/images/file_types/100/video_white.png') 1x,
+ url('../../common/images/file_types/200/video_white.png') 2x);
+}
+
+[file-type-icon='word'] {
+ background-image: -webkit-image-set(
+ url('../../common/images/file_types/100/word.png') 1x,
+ url('../../common/images/file_types/200/word.png') 2x);
+}
+
+tree:focus .tree-item[selected] > .tree-row > [file-type-icon='word'],
+list:focus [selected] [file-type-icon='word'],
+list.autocomplete-suggestions [selected] [file-type-icon='word'] {
+ background-image: -webkit-image-set(
+ url('../../common/images/file_types/100/word_white.png') 1x,
+ url('../../common/images/file_types/200/word_white.png') 2x);
+}
+
+[file-type-icon='drive'] {
+ background-image: -webkit-image-set(
+ url('chrome://theme/IDR_FILE_MANAGER_IMG_FILETYPE_DRIVE') 1x,
+ url('chrome://theme/IDR_FILE_MANAGER_IMG_FILETYPE_DRIVE@2x') 2x);
+}
+
+/* Large generic thumbnails, used when a file does not have a thumbnail. */
+[generic-thumbnail] {
+ background-image: -webkit-image-set(
+ url('chrome://theme/IDR_FILE_MANAGER_IMG_FILETYPE_LARGE_GENERIC') 1x,
+ url('chrome://theme/IDR_FILE_MANAGER_IMG_FILETYPE_LARGE_GENERIC@2x') 2x);
+ background-position: center center;
+ background-repeat: no-repeat;
+}
+
+[generic-thumbnail='audio'] {
+ background-image: -webkit-image-set(
+ url('chrome://theme/IDR_FILE_MANAGER_IMG_FILETYPE_LARGE_AUDIO') 1x,
+ url('chrome://theme/IDR_FILE_MANAGER_IMG_FILETYPE_LARGE_AUDIO@2x') 2x);
+}
+
+[generic-thumbnail='folder'] {
+ background-image: -webkit-image-set(
+ url('chrome://theme/IDR_FILE_MANAGER_IMG_FILETYPE_LARGE_FOLDER') 1x,
+ url('chrome://theme/IDR_FILE_MANAGER_IMG_FILETYPE_LARGE_FOLDER@2x') 2x);
+}
+
+[generic-thumbnail='image'] {
+ background-image: -webkit-image-set(
+ url('chrome://theme/IDR_FILE_MANAGER_IMG_FILETYPE_LARGE_IMAGE') 1x,
+ url('chrome://theme/IDR_FILE_MANAGER_IMG_FILETYPE_LARGE_IMAGE@2x') 2x);
+}
+
+[generic-thumbnail='video'] {
+ background-image: -webkit-image-set(
+ url('chrome://theme/IDR_FILE_MANAGER_IMG_FILETYPE_LARGE_VIDEO') 1x,
+ url('chrome://theme/IDR_FILE_MANAGER_IMG_FILETYPE_LARGE_VIDEO@2x') 2x);
+}
+
+/* Icons for volume types. */
+
+[volume-type-icon='archive'] {
+ background-image: -webkit-image-set(
+ url('../images/volumes/black_archive.png') 1x,
+ url('../images/volumes/2x/black_archive.png') 2x);
+}
+
+list:focus li[selected] [volume-type-icon='archive'],
+tree:focus .tree-item[selected] > .tree-row > [volume-type-icon='archive'] {
+ background-image: -webkit-image-set(
+ url('../images/volumes/white_archive.png') 1x,
+ url('../images/volumes/2x/white_archive.png') 2x);
+}
+
+[volume-type-icon='downloads'] {
+ background-image: -webkit-image-set(
+ url('../images/volumes/black_downloads.png') 1x,
+ url('../images/volumes/2x/black_downloads.png') 2x);
+}
+
+list:focus li[selected] [volume-type-icon='downloads'],
+tree:focus .tree-item[selected] > .tree-row > [volume-type-icon='downloads'] {
+ background-image: -webkit-image-set(
+ url('../images/volumes/white_downloads.png') 1x,
+ url('../images/volumes/2x/white_downloads.png') 2x);
+}
+
+[volume-type-icon='drive'] {
+ background-image: -webkit-image-set(
+ url('../images/volumes/black_drive.png') 1x,
+ url('../images/volumes/2x/black_drive.png') 2x);
+}
+
+list:focus li[selected] [volume-type-icon='drive'],
+tree:focus .tree-item[selected] > .tree-row > [volume-type-icon='drive'] {
+ background-image: -webkit-image-set(
+ url('../images/volumes/white_drive.png') 1x,
+ url('../images/volumes/2x/white_drive.png') 2x);
+}
+
+[volume-type-icon='drive_offline'] {
+ background-image: -webkit-image-set(
+ url('../images/volumes/black_offline.png') 1x,
+ url('../images/volumes/2x/black_offline.png') 2x);
+}
+
+list:focus li[selected] [volume-type-icon='drive_offline'],
+tree:focus .tree-item[selected] > .tree-row >
+ [volume-type-icon='drive_offline'] {
+ background-image: -webkit-image-set(
+ url('../images/volumes/white_offline.png') 1x,
+ url('../images/volumes/2x/white_offline.png') 2x);
+}
+
+[volume-type-icon='drive_shared_with_me'] {
+ background-image: -webkit-image-set(
+ url('../images/volumes/black_shared.png') 1x,
+ url('../images/volumes/2x/black_shared.png') 2x);
+}
+
+list:focus li[selected] [volume-type-icon='drive_shared_with_me'],
+tree:focus .tree-item[selected] > .tree-row >
+ [volume-type-icon='drive_shared_with_me'] {
+ background-image: -webkit-image-set(
+ url('../images/volumes/white_shared.png') 1x,
+ url('../images/volumes/2x/white_shared.png') 2x);
+}
+
+[volume-type-icon='drive_recent'] {
+ background-image: -webkit-image-set(
+ url('../images/volumes/black_recent.png') 1x,
+ url('../images/volumes/2x/black_recent.png') 2x);
+}
+
+list:focus li[selected] [volume-type-icon='drive_recent'],
+tree:focus .tree-item[selected] > .tree-row >
+ [volume-type-icon='drive_recent'] {
+ background-image: -webkit-image-set(
+ url('../images/volumes/white_recent.png') 1x,
+ url('../images/volumes/2x/white_recent.png') 2x);
+}
+
+[volume-type-icon='removable'] {
+ background-image: -webkit-image-set(
+ url('../images/volumes/black_usb.png') 1x,
+ url('../images/volumes/2x/black_usb.png') 2x);
+}
+
+list:focus li[selected] [volume-type-icon='removable'],
+tree:focus .tree-item[selected] > .tree-row >
+ [volume-type-icon='removable'] {
+ background-image: -webkit-image-set(
+ url('../images/volumes/white_usb.png') 1x,
+ url('../images/volumes/2x/white_usb.png') 2x);
+}
+
+[volume-type-icon='removable'][volume-subtype='sd'] {
+ background-image: -webkit-image-set(
+ url('../images/volumes/black_sd.png') 1x,
+ url('../images/volumes/2x/black_sd.png') 2x);
+}
+
+list:focus li[selected] [volume-type-icon='removable'][volume-subtype='sd'],
+tree:focus .tree-item[selected] > .tree-row >
+ [volume-type-icon='removable'][volume-subtype='sd'] {
+ background-image: -webkit-image-set(
+ url('../images/volumes/white_sd.png') 1x,
+ url('../images/volumes/2x/white_sd.png') 2x);
+}
+
+[volume-type-icon='removable'][volume-subtype='optical'] {
+ background-image: -webkit-image-set(
+ url('../images/volumes/black_optical.png') 1x,
+ url('../images/volumes/2x/black_optical.png') 2x);
+}
+
+list:focus div[selected]
+ [volume-type-icon='removable'][volume-subtype='optical'],
+tree:focus .tree-item[selected] > .tree-row >
+ [volume-type-icon='removable'][volume-subtype='optical'] {
+ background-image: -webkit-image-set(
+ url('../images/volumes/white_optical.png') 1x,
+ url('../images/volumes/2x/white_optical.png') 2x);
+}
+
+list:focus li[selected]
+ [volume-type-icon='removable'][volume-subtype='optical'],
+tree:focus .tree-item[selected] > .tree-row >
+ [volume-type-icon='removable'][volume-subtype='optical'] {
+ background-image: -webkit-image-set(
+ url('../images/volumes/white_optical.png') 1x,
+ url('../images/volumes/2x/white_optical.png') 2x);
+}
+
+/* TODO(kaznacheev): consider a better icon for volume-subtype=unknown.
+ Also find out if we need an icon for volume-subtype=mobile */
+[volume-type-icon='removable'][volume-subtype='unknown'] {
+ background-image: -webkit-image-set(
+ url('../images/volumes/black_hdd.png') 1x,
+ url('../images/volumes/2x/black_hdd.png') 2x);
+}
+
+list:focus li[selected]
+ [volume-type-icon='removable'][volume-subtype='unknown'],
+tree:focus .tree-item[selected] > .tree-row >
+ [volume-type-icon='removable'][volume-subtype='unknown'] {
+ background-image: -webkit-image-set(
+ url('../images/volumes/white_hdd.png') 1x,
+ url('../images/volumes/2x/white_hdd.png') 2x);
+}
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/css/gallery.css b/chromium/chrome/browser/resources/file_manager/foreground/css/gallery.css
new file mode 100644
index 00000000000..7680f4d3100
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/css/gallery.css
@@ -0,0 +1,1374 @@
+/* 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. */
+
+body {
+ -webkit-user-select: none;
+ font-family: Open Sans, Droid Sans Fallback, sans-serif;
+ font-size: 84%;
+ margin: 0;
+}
+
+.gallery,
+.gallery .content {
+ bottom: 0;
+ left: 0;
+ overflow: hidden;
+ position: absolute;
+ right: 0;
+ top: 0;
+}
+
+/* Common background for both mosaic and slide mode. */
+.gallery .content {
+ background-color: black;
+}
+
+/* Close button */
+
+/* We actually want (left,top) to be (0,0) but for some weird reason
+ this triggers :hover style on page reload which is ugly. */
+.gallery > .back-button {
+ cursor: pointer;
+ left: 1px;
+ position: absolute;
+ top: 1px;
+ z-index: 200;
+}
+
+/* The close icon is in a nested div so that its opacity can be manipulated
+ independently from its parent (which can be dimmed when the crop frame
+ overlaps it) */
+.gallery > .back-button div {
+ background-image: -webkit-image-set(
+ url('../images/gallery/back_to_files.png') 1x,
+ url('../images/gallery/2x/back_to_files.png') 2x);
+ background-position: center center;
+ background-repeat: no-repeat;
+ height: 40px;
+ opacity: 0;
+ width: 64px;
+}
+
+.gallery[tools] > .back-button div {
+ opacity: 0.5;
+}
+
+.gallery[tools] > .back-button div:hover {
+ opacity: 1;
+}
+
+/* Image container and canvas elements */
+
+.gallery .image-container {
+ cursor: none; /* Only visible when the toolbar is active */
+ height: 100%;
+ position: absolute;
+ width: 100%;
+}
+
+.gallery[tools] .image-container[cursor='default'] {
+ cursor: default;
+}
+
+.gallery[tools] .image-container[cursor='move'] {
+ cursor: -webkit-image-set(
+ url('../images/gallery/cursor_move.png') 1x,
+ url('../images/gallery/2x/cursor_move.png') 2x) 15 15, auto;
+}
+
+.gallery[tools] .image-container[cursor='crop'] {
+ cursor: -webkit-image-set(
+ url('../images/gallery/cursor_crop.png') 1x,
+ url('../images/gallery/2x/cursor_crop.png') 2x) 15 15, auto;
+}
+
+.gallery[tools] .image-container[cursor='n-resize'],
+.gallery[tools] .image-container[cursor='s-resize'] {
+ cursor: -webkit-image-set(
+ url('../images/gallery/cursor_updown.png') 1x,
+ url('../images/gallery/2x/cursor_updown.png') 2x) 15 15, auto;
+}
+
+.gallery[tools] .image-container[cursor='e-resize'],
+.gallery[tools] .image-container[cursor='w-resize'] {
+ cursor: -webkit-image-set(
+ url('../images/gallery/cursor_leftright.png') 1x,
+ url('../images/gallery/2x/cursor_leftright.png') 2x) 15 15, auto;
+}
+
+.gallery[tools] .image-container[cursor='nw-resize'],
+.gallery[tools] .image-container[cursor='se-resize'] {
+ cursor: -webkit-image-set(
+ url('../images/gallery/cursor_nwse.png') 1x,
+ url('../images/gallery/2x/cursor_nwse.png') 2x) 15 15, auto;
+}
+
+.gallery[tools] .image-container[cursor='ne-resize'],
+.gallery[tools] .image-container[cursor='sw-resize'] {
+ cursor: -webkit-image-set(
+ url('../images/gallery/cursor_swne.png') 1x,
+ url('../images/gallery/2x/cursor_swne.png') 2x) 15 15, auto;
+}
+
+.gallery .image-container > .image {
+ pointer-events: none;
+ position: absolute;
+ /* Duration and timing function are set in Javascript. */
+ transition-property: -webkit-transform, opacity;
+}
+
+.gallery .image-container > .image[fade] {
+ opacity: 0;
+}
+
+/* Full resolution image is invisible unless printing. */
+.gallery .image-container > canvas.fullres {
+ display: none;
+}
+
+@media print {
+ /* Do not print anything but the image content. */
+ .gallery > :not(.content) {
+ display: none !important;
+ }
+
+ /* Center the printed image. */
+ .gallery .image-container {
+ -webkit-box-align: center;
+ -webkit-box-orient: horizontal;
+ -webkit-box-pack: center;
+ display: -webkit-box;
+ }
+
+ /* Do not print the screen resolution image. */
+ .gallery .image-container > canvas.image {
+ display: none !important;
+ }
+
+ /* Print the full resolution image instead. */
+ .gallery .image-container > canvas.fullres {
+ display: block !important;
+ max-height: 100%;
+ max-width: 100%;
+ }
+
+ /* Print video at the center of the page */
+ .gallery .image-container > video.image {
+ position: auto !important;
+ }
+}
+
+/* Toolbar */
+
+.gallery > .header,
+.gallery > .toolbar {
+ -webkit-box-align: stretch;
+ -webkit-box-orient: horizontal;
+ -webkit-box-pack: start;
+ background-color: rgba(30, 30, 30, 0.8);
+ display: -webkit-box;
+ left: 0;
+ opacity: 0;
+ padding: 0 10px;
+ pointer-events: none;
+ position: absolute;
+ right: 0;
+ transition: opacity 300ms ease;
+}
+
+.gallery > .header {
+ -webkit-box-align: center;
+ -webkit-box-pack: end;
+ border-bottom: 1px solid rgba(50, 50, 50, 0.8);
+ display: -webkit-box;
+ height: 45px;
+ top: 0;
+}
+
+.gallery > .toolbar {
+ border-top: 1px solid rgba(50, 50, 50, 0.8);
+ bottom: 0;
+ height: 55px;
+ min-width: 800px;
+}
+
+.gallery[tools]:not([slideshow]) > .header,
+.gallery[tools]:not([slideshow]) > .toolbar {
+ opacity: 1;
+ pointer-events: auto;
+}
+
+/* Hide immediately when entering the slideshow. */
+.gallery[tools][slideshow] > .toolbar {
+ transition-duration: 0;
+}
+
+.gallery[tools][locked] > .toolbar {
+ pointer-events: none;
+}
+
+.gallery .arrow-box {
+ -webkit-box-align: center;
+ -webkit-box-orient: horizontal;
+ -webkit-box-pack: center;
+ display: -webkit-box;
+ height: 100%;
+ pointer-events: none;
+ position: absolute;
+ width: 100%;
+ z-index: 100;
+}
+
+.gallery .arrow-box .arrow {
+ opacity: 0;
+ pointer-events: none;
+}
+
+.gallery .arrow-box .arrow-spacer {
+ -webkit-box-flex: 1;
+ pointer-events: none;
+}
+
+.gallery[tools] .arrow-box[active] .arrow {
+ cursor: pointer;
+ opacity: 1;
+ pointer-events: auto;
+}
+
+/* The arrow icons are in nested divs so that their opacity can be manipulated
+ * independently from their parent (which can be dimmed when the crop frame
+ * overlaps it) */
+.gallery .arrow div {
+ background-position: center center;
+ background-repeat: no-repeat;
+ height: 193px;
+ opacity: 0;
+ width: 105px;
+}
+
+.gallery[tools] .arrow-box[active] .arrow div {
+ opacity: 0.25;
+}
+
+.gallery[tools] .arrow-box[active] .arrow div:hover {
+ opacity: 1;
+}
+
+.gallery .arrow.left div {
+ background-image: -webkit-image-set(
+ url('../images/gallery/arrow_left.png') 1x,
+ url('../images/gallery/2x/arrow_left.png') 2x);
+}
+
+.gallery .arrow.right div {
+ background-image: -webkit-image-set(
+ url('../images/gallery/arrow_right.png') 1x,
+ url('../images/gallery/2x/arrow_right.png') 2x);
+}
+
+/* Special behavior on mouse drag.
+ Redundant .gallery attributes included to make the rules more specific */
+
+/* Everything but the image container should become mouse-transparent */
+.gallery[tools][editing][mousedrag] * {
+ pointer-events: none;
+}
+
+.gallery[tools][editing][mousedrag] .image-container {
+ pointer-events: auto;
+}
+
+/* The editor marks elements with 'dimmed' attribute to get them out of the way
+ of the crop frame */
+.gallery[tools][editing] [dimmed],
+.gallery[tools][editing] [dimmed] * {
+ pointer-events: none;
+}
+
+.gallery[tools][editing] [dimmed] {
+ opacity: 0.2;
+}
+
+/* Filename */
+
+.gallery .filename-spacer {
+ position: relative;
+ width: 270px;
+}
+
+.gallery .filename-spacer > * {
+ background-color: transparent;
+ overflow: hidden;
+ position: absolute;
+ transition: visibility 0 linear 180ms, all 180ms linear;
+ width: 260px;
+}
+
+.gallery .filename-spacer * {
+ color: white;
+}
+
+.gallery .filename-spacer .namebox {
+ height: 22px;
+ top: 15px;
+}
+
+.gallery[editing] .filename-spacer .namebox {
+ height: 21px;
+ top: 5px;
+}
+
+
+.gallery .filename-spacer .namebox {
+ background-color: transparent;
+ border: none;
+ box-sizing: border-box;
+ cursor: pointer;
+ display: block;
+ font-size: 120%;
+ outline: none;
+ overflow: hidden;
+ padding: 0 3px;
+ position: absolute;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.gallery .filename-spacer .namebox[disabled] {
+ -webkit-user-select: none;
+ cursor: default;
+}
+
+.gallery .filename-spacer .namebox:not([disabled]):not(:focus):hover {
+ background-color: rgba(48, 48, 48, 1.0);
+}
+
+.gallery .filename-spacer .namebox:focus {
+ background-color: white;
+ color: black;
+ cursor: text;
+}
+
+.gallery .filename-spacer .options {
+ -webkit-box-align: center;
+ -webkit-box-orient: horizontal;
+ -webkit-box-pack: start;
+ display: -webkit-box;
+ opacity: 0;
+ top: 50px;
+ visibility: hidden;
+}
+
+.gallery[editing] .filename-spacer .options {
+ opacity: 1;
+ top: 28px;
+ visibility: visible;
+}
+
+.gallery .filename-spacer .saved,
+.gallery .filename-spacer .overwrite-original {
+ cursor: inherit;
+ font-size: 90%;
+ margin-left: 3px;
+ margin-right: 18px;
+ opacity: 0;
+ pointer-events: none;
+ transition: all linear 120ms;
+}
+
+.gallery[editing] .filename-spacer .saved {
+ color: white;
+ opacity: 0.5;
+}
+
+.gallery[editing] .filename-spacer .overwrite-original,
+.gallery[editing] .filename-spacer .overwrite-original > * {
+ cursor: pointer;
+ opacity: 1;
+ pointer-events: auto;
+}
+
+.gallery[editing] .options[saved] .overwrite-original {
+ opacity: 0.5;
+}
+
+.gallery[editing] .options[saved] .overwrite-original,
+.gallery[editing] .options[saved] .overwrite-original > * {
+ cursor: default;
+ pointer-events: none;
+}
+
+.gallery .filename-spacer .overwrite-original input {
+ margin-bottom: -2px;
+ margin-right: 6px;
+}
+
+.gallery .filename-spacer .saved[highlighted] {
+ -webkit-transform: scaleX(1.1) scaleY(1.1) rotate(0);
+ opacity: 1;
+}
+
+/* Bubble */
+.gallery .toolbar .bubble {
+ bottom: 65px;
+ font-size: 85%;
+ left: 50px;
+ position: absolute;
+ width: 220px;
+}
+
+.gallery:not([editing]) .toolbar .bubble {
+ display: none;
+}
+
+/* Toolbar buttons */
+
+.gallery .button-spacer {
+ -webkit-box-flex: 1;
+ display: -webkit-box;
+}
+
+/* Thumbnails */
+
+.gallery .ribbon-spacer {
+ -webkit-box-align: center;
+ -webkit-box-orient: horizontal;
+ -webkit-box-pack: center;
+ display: -webkit-box;
+ height: 100%;
+ left: 280px;
+ position: absolute;
+ right: 280px;
+}
+
+.gallery .toolbar .ribbon {
+ -webkit-box-flex: 0;
+ -webkit-box-orient: horizontal;
+ -webkit-box-pack: start;
+ display: -webkit-box;
+ height: 100%;
+ overflow: hidden;
+ transition: opacity 180ms linear, visibility 0 linear;
+ z-index: 0;
+}
+
+.gallery[editing] .toolbar .ribbon {
+ opacity: 0;
+ transition-delay: 0, 180ms;
+ visibility: hidden;
+}
+
+.gallery .ribbon-image {
+ -webkit-box-align: center;
+ -webkit-box-orient: horizontal;
+ -webkit-box-pack: center;
+ border: 2px solid rgba(255, 255, 255, 0); /* transparent white */
+ cursor: pointer;
+ display: -webkit-box;
+ height: 47px;
+ margin: 2px;
+ overflow: hidden;
+ transition: all 180ms linear;
+ width: 47px;
+}
+
+.ribbon-image[vanishing='smooth'] {
+ border-left-width: 0;
+ border-right-width: 0;
+ margin-left: 0;
+ margin-right: 0;
+ width: 0;
+}
+
+.gallery .ribbon-image[selected] {
+ border: 2px solid rgba(255, 233, 168, 1);
+}
+
+.gallery .toolbar .ribbon.fade-left {
+ -webkit-mask-image: linear-gradient(to right, rgba(0, 0, 0, 0) 0,
+ rgba(0, 0, 0, 1) 40px);
+}
+
+.gallery .toolbar .ribbon.fade-right {
+ -webkit-mask-image: linear-gradient(to left, rgba(0, 0, 0, 0) 0,
+ rgba(0, 0, 0, 1) 40px);
+}
+
+.gallery .toolbar .ribbon.fade-left.fade-right {
+ -webkit-mask-image: linear-gradient(to right, rgba(0, 0, 0, 0) 0,
+ rgba(0, 0, 0, 1) 40px,
+ rgba(0, 0, 0, 1) 230px,
+ rgba(0, 0, 0, 0) 100%);
+}
+
+.gallery .image-wrapper {
+ background-size: 45px 45px;
+ border: 1px solid rgba(0, 0, 0, 0); /* transparent black */
+ height: 45px;
+ overflow: hidden;
+ position: relative;
+ width: 45px;
+}
+
+.gallery .image-wrapper > img {
+ position: absolute;
+}
+
+.gallery .image-wrapper > img:not(.cached) {
+ -webkit-animation: fadeIn 500ms ease-in;
+}
+
+/* Editor buttons */
+
+.gallery .edit-bar-spacer {
+ -webkit-box-align: center;
+ -webkit-box-orient: horizontal;
+ -webkit-box-pack: center;
+ display: -webkit-box;
+ height: 100%;
+ left: 280px;
+ opacity: 0;
+ position: absolute;
+ right: 280px;
+ transition: opacity 180ms linear, visibility 0 linear 180ms;
+ visibility: hidden;
+}
+
+.gallery .toolbar .edit-main {
+ -webkit-box-orient: horizontal;
+ -webkit-box-pack: center;
+ color: white;
+ display: -webkit-box;
+ height: 55px;
+ overflow: visible;
+}
+
+.gallery[editing] .edit-bar-spacer {
+ opacity: 1.0;
+ pointer-events: auto;
+ transition-delay: 100ms, 100ms;
+ visibility: visible;
+}
+
+.gallery .header button,
+.gallery .toolbar button,
+.gallery .header button[disabled],
+.gallery .toolbar button[disabled] {
+ -webkit-box-align: center;
+ -webkit-box-flex: 0;
+ -webkit-box-orient: horizontal;
+ -webkit-box-pack: end;
+ background-color: rgba(0, 0, 0, 0);
+ background-position: center;
+ background-repeat: no-repeat;
+ border: none;
+ box-shadow: none;
+ color: white;
+ cursor: pointer;
+ display: -webkit-box;
+ opacity: 0.99; /* Workaround for http://crosbug.com/21065 */
+ padding: 1px; /* Instead of a border. */
+ position: relative;
+ z-index: 10;
+}
+
+.gallery .header button,
+.gallery .toolbar button {
+ height: 40px;
+ margin: 6px 0;
+ min-width: 40px; /* Reset. */
+ width: 40px;
+}
+
+/* By default, labels are hidden. */
+.gallery > .toolbar button span {
+ display: none;
+}
+
+/* Show labels if there is enough space. */
+@media (min-width: 1180px) {
+
+ .gallery .edit-main button,
+ .gallery .edit-main button[disabled] {
+ background-position: 5px center;
+ max-width: 60px;
+ min-width: 0; /* Reset. */
+ padding: 0 10px 0 35px;
+ width: auto;
+ }
+
+ .gallery > .toolbar button span {
+ display: inline;
+ }
+
+}
+
+.gallery .header button:hover,
+.gallery .toolbar button:hover {
+ background-color: rgba(31, 31, 31, 1);
+ color: white;
+}
+
+.gallery .header button:active,
+.gallery .toolbar button:active,
+.gallery .header button[pressed],
+.gallery .toolbar button[pressed],
+.gallery .header button[pressed]:hover,
+.gallery .toolbar button[pressed]:hover {
+ background-color: rgba(240, 240, 240, 1);
+ color: black;
+}
+
+.gallery > .toolbar button.autofix {
+ background-image: -webkit-image-set(
+ url('../images/gallery/icon_autofix.png') 1x,
+ url('../images/gallery/2x/icon_autofix.png') 2x);
+}
+
+.gallery > .toolbar button.autofix:active,
+.gallery > .toolbar button.autofix[pressed] {
+ background-image: -webkit-image-set(
+ url('../images/gallery/icon_autofix_selected.png') 1x,
+ url('../images/gallery/2x/icon_autofix_selected.png') 2x);
+}
+
+.gallery > .toolbar button.crop {
+ background-image: -webkit-image-set(
+ url('../images/gallery/icon_crop.png') 1x,
+ url('../images/gallery/2x/icon_crop.png') 2x);
+}
+
+.gallery > .toolbar button.crop:active,
+.gallery > .toolbar button.crop[pressed] {
+ background-image: -webkit-image-set(
+ url('../images/gallery/icon_crop_selected.png') 1x,
+ url('../images/gallery/2x/icon_crop_selected.png') 2x);
+}
+
+.gallery > .toolbar button.exposure {
+ background-image: -webkit-image-set(
+ url('../images/gallery/icon_brightness.png') 1x,
+ url('../images/gallery/2x/icon_brightness.png') 2x);
+}
+
+.gallery > .toolbar button.exposure:active,
+.gallery > .toolbar button.exposure[pressed] {
+ background-image: -webkit-image-set(
+ url('../images/gallery/icon_brightness_selected.png') 1x,
+ url('../images/gallery/2x/icon_brightness_selected.png') 2x);
+}
+
+.gallery > .toolbar button.rotate_right {
+ background-image: -webkit-image-set(
+ url('../images/gallery/icon_rotate.png') 1x,
+ url('../images/gallery/2x/icon_rotate.png') 2x);
+}
+
+.gallery > .toolbar button.rotate_right:active,
+.gallery > .toolbar button.rotate_right[pressed] {
+ background-image: -webkit-image-set(
+ url('../images/gallery/icon_rotate_selected.png') 1x,
+ url('../images/gallery/2x/icon_rotate_selected.png') 2x);
+}
+
+.gallery > .toolbar button.rotate_left {
+ background-image: -webkit-image-set(
+ url('../images/gallery/icon_rotate_left.png') 1x,
+ url('../images/gallery/2x/icon_rotate_left.png') 2x);
+}
+
+.gallery > .toolbar button.rotate_left:active,
+.gallery > .toolbar button.rotate_left[pressed] {
+ background-image: -webkit-image-set(
+ url('../images/gallery/icon_rotate_left_selected.png') 1x,
+ url('../images/gallery/2x/icon_rotate_left_selected.png') 2x);
+}
+
+.gallery > .toolbar button.undo {
+ background-image: -webkit-image-set(
+ url('../images/gallery/icon_undo.png') 1x,
+ url('../images/gallery/2x/icon_undo.png') 2x);
+}
+
+.gallery > .toolbar button.undo:active,
+.gallery > .toolbar button.undo[pressed] {
+ background-image: -webkit-image-set(
+ url('../images/gallery/icon_undo_selected.png') 1x,
+ url('../images/gallery/2x/icon_undo_selected.png') 2x);
+}
+
+.gallery > .toolbar button.redo {
+ background-image: -webkit-image-set(
+ url('../images/gallery/icon_redo.png') 1x,
+ url('../images/gallery/2x/icon_redo.png') 2x);
+ position: absolute; /* Exclude from center-packing*/
+}
+
+.gallery > .toolbar button.redo:active,
+.gallery > .toolbar button.redo[pressed] {
+ background-image: -webkit-image-set(
+ url('../images/gallery/icon_redo_selected.png') 1x,
+ url('../images/gallery/2x/icon_redo_selected.png') 2x);
+}
+
+.gallery > .toolbar button[disabled],
+.gallery[tools][locked] > .toolbar button {
+ opacity: 0.5;
+ pointer-events: none;
+}
+
+.gallery > .toolbar button[hidden] {
+ display: none;
+}
+
+.gallery[mode='slide'] > .toolbar > button.mode {
+ background-image: -webkit-image-set(
+ url('../images/gallery/icon_mosaic.png') 1x,
+ url('../images/gallery/2x/icon_mosaic.png') 2x);
+}
+
+.gallery[mode='slide'] > .toolbar > button.mode:active {
+ background-image: -webkit-image-set(
+ url('../images/gallery/icon_mosaic_selected.png') 1x,
+ url('../images/gallery/2x/icon_mosaic_selected.png') 2x);
+}
+
+.gallery[mode='mosaic'] > .toolbar > button.mode {
+ background-image: -webkit-image-set(
+ url('../images/gallery/icon_1up.png') 1x,
+ url('../images/gallery/2x/icon_1up.png') 2x);
+}
+
+.gallery[mode='mosaic'] > .toolbar > button.mode:active {
+ background-image: -webkit-image-set(
+ url('../images/gallery/icon_1up_selected.png') 1x,
+ url('../images/gallery/2x/icon_1up_selected.png') 2x);
+}
+
+.gallery > .toolbar > button.slideshow {
+ background-image: -webkit-image-set(
+ url('../images/gallery/icon_slideshow.png') 1x,
+ url('../images/gallery/2x/icon_slideshow.png') 2x);
+}
+
+.gallery > .toolbar > button.slideshow:active,
+.gallery > .toolbar > button.slideshow[pressed] {
+ background-image: -webkit-image-set(
+ url('../images/gallery/icon_slideshow_selected.png') 1x,
+ url('../images/gallery/2x/icon_slideshow_selected.png') 2x);
+}
+
+.gallery > .toolbar > button.delete {
+ background-image: -webkit-image-set(
+ url('../images/gallery/icon_delete.png') 1x,
+ url('../images/gallery/2x/icon_delete.png') 2x);
+}
+
+.gallery > .toolbar > button.delete:active {
+ background-image: -webkit-image-set(
+ url('../images/gallery/icon_delete_selected.png') 1x,
+ url('../images/gallery/2x/icon_delete_selected.png') 2x);
+}
+
+.gallery > .toolbar > button.edit {
+ background-image: -webkit-image-set(
+ url('../images/gallery/icon_edit.png') 1x,
+ url('../images/gallery/2x/icon_edit.png') 2x);
+}
+
+.gallery > .toolbar > button.edit:active,
+.gallery > .toolbar > button.edit[pressed] {
+ background-image: -webkit-image-set(
+ url('../images/gallery/icon_edit_selected.png') 1x,
+ url('../images/gallery/2x/icon_edit_selected.png') 2x);
+}
+
+.gallery > .toolbar > button.print {
+ background-image: -webkit-image-set(
+ url('../images/gallery/icon_print.png') 1x,
+ url('../images/gallery/2x/icon_print.png') 2x);
+}
+
+.gallery > .toolbar > button.print:active,
+.gallery > .toolbar > button.print[pressed] {
+ background-image: -webkit-image-set(
+ url('../images/gallery/icon_print_selected.png') 1x,
+ url('../images/gallery/2x/icon_print_selected.png') 2x);
+}
+
+.gallery > .toolbar > button.share {
+ background-image: -webkit-image-set(
+ url('../images/gallery/icon_share.png') 1x,
+ url('../images/gallery/2x/icon_share.png') 2x);
+}
+
+.gallery > .toolbar > button.share:active,
+.gallery > .toolbar > button.share[pressed] {
+ background-image: -webkit-image-set(
+ url('../images/gallery/icon_share_selected.png') 1x,
+ url('../images/gallery/2x/icon_share_selected.png') 2x);
+}
+
+.gallery > .toolbar > button.share[disabled] {
+ display: none;
+}
+
+/* Secondary toolbar (mode-specific tools) */
+
+.gallery .edit-modal {
+ -webkit-box-orient: horizontal;
+ -webkit-box-pack: center;
+ bottom: 80px;
+ display: -webkit-box;
+ height: 40px;
+ pointer-events: none;
+ position: absolute;
+ width: 100%;
+}
+
+.gallery .edit-modal-wrapper[hidden] {
+ display: none;
+}
+
+.gallery .edit-modal-wrapper {
+ -webkit-box-align: center;
+ -webkit-box-orient: horizontal;
+ -webkit-box-pack: center;
+ background-color: rgba(0, 0, 0, 0.75);
+ color: white;
+ display: -webkit-box;
+ padding-right: 5px;
+ pointer-events: auto;
+}
+
+.gallery .edit-modal .label {
+ -webkit-box-align: center;
+ -webkit-box-orient: horizontal;
+ background-position: 20px center;
+ background-repeat: no-repeat;
+ display: -webkit-box;
+ height: 20px;
+ padding-left: 50px;
+ padding-right: 10px;
+}
+
+.gallery .edit-modal .label.brightness {
+ background-image: -webkit-image-set(
+ url('../images/gallery/icon_brightness.png') 1x,
+ url('../images/gallery/2x/icon_brightness.png') 2x);
+}
+
+.gallery .edit-modal .label.contrast {
+ background-image: -webkit-image-set(
+ url('../images/gallery/icon_contrast.png') 1x,
+ url('../images/gallery/2x/icon_contrast.png') 2x);
+ height: 24px;
+ margin-left: 15px;
+}
+
+.gallery .edit-modal .range {
+ -webkit-appearance: none !important;
+ height: 3px;
+ margin-right: 10px;
+ margin-top: 1px;
+}
+
+.gallery .edit-modal .range::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ background-image: -webkit-image-set(
+ url('../images/gallery/slider_thumb.png') 1x,
+ url('../images/gallery/2x/slider_thumb.png') 2x);
+ height: 29px;
+ width: 16px;
+}
+
+/* Crop frame */
+
+.gallery .crop-overlay {
+ -webkit-box-orient: vertical;
+ display: -webkit-box;
+ pointer-events: none;
+ position: absolute;
+}
+
+.gallery .crop-overlay .shadow {
+ background-color: rgba(0, 0, 0, 0.65);
+}
+
+.gallery .crop-overlay .middle-box {
+ -webkit-box-flex: 1;
+ -webkit-box-orient: horizontal;
+ display: -webkit-box;
+}
+
+.gallery .crop-frame {
+ -webkit-box-flex: 1;
+ display: -webkit-box;
+ position: relative;
+}
+
+.gallery .crop-frame div {
+ background-color: rgba(255, 255, 255, 1);
+ box-shadow: 0 0 3px rgba(0, 0, 0, 0.75);
+ position: absolute;
+}
+
+.gallery .crop-frame .horizontal {
+ height: 1px;
+ left: 7px;
+ right: 7px;
+}
+
+.gallery .crop-frame .horizontal.top {
+ top: 0;
+}
+
+.gallery .crop-frame .horizontal.bottom {
+ bottom: 0;
+}
+
+.gallery .crop-frame .vertical {
+ bottom: 7px;
+ top: 7px;
+ width: 1px;
+}
+
+.gallery .crop-frame .vertical.left {
+ left: 0;
+}
+
+.gallery .crop-frame .vertical.right {
+ right: 0;
+}
+
+.gallery .crop-frame .corner {
+ border-radius: 6px;
+ height: 13px;
+ width: 13px;
+}
+
+.gallery .crop-frame .corner.left {
+ left: -6px;
+}
+
+.gallery .crop-frame .corner.right {
+ right: -6px;
+}
+
+.gallery .crop-frame .corner.top {
+ top: -6px;
+}
+
+.gallery .crop-frame .corner.bottom {
+ bottom: -6px;
+}
+
+/* Prompt/notification panel */
+
+.gallery .prompt-wrapper {
+ -webkit-box-orient: horizontal;
+ -webkit-box-pack: center;
+ display: -webkit-box;
+ height: 100%;
+ pointer-events: none;
+ position: absolute;
+ width: 100%;
+}
+
+.gallery .prompt-wrapper[pos=top] {
+ -webkit-box-align: start;
+}
+
+.gallery .prompt-wrapper[pos=center] {
+ -webkit-box-align: center;
+}
+
+.gallery .prompt-wrapper[pos=center] .back-button {
+ display: none;
+}
+
+.gallery .prompt-wrapper > div.dimmable {
+ opacity: 1;
+ transition: opacity 220ms ease;
+}
+
+.gallery .prompt {
+ -webkit-box-align: center;
+ -webkit-box-orient: horizontal;
+ background-color: rgba(0, 0, 0, 0.8);
+ color: white;
+ display: -webkit-box;
+ font-size: 120%;
+ height: 40px;
+ opacity: 0;
+ padding: 0 20px;
+ position: relative;
+ top: 5px;
+ transition: all 180ms ease;
+}
+
+.gallery .prompt[state='fadein'] {
+ opacity: 1;
+ top: 0;
+}
+
+.gallery .prompt[state='fadeout'] {
+ opacity: 0;
+ top: 0;
+}
+
+.gallery .prompt-wrapper[pos=top] .prompt {
+ padding-right: 10px;
+}
+
+.gallery .prompt .back-button {
+ background-image: -webkit-image-set(
+ url('../images/gallery/butterbar_close_button.png') 1x,
+ url('../images/gallery/2x/butterbar_close_button.png') 2x);
+ background-position: center center;
+ background-repeat: no-repeat;
+ height: 16px;
+ margin-left: 16px;
+ opacity: 0.65;
+ pointer-events: auto;
+ width: 16px;
+}
+
+.gallery .prompt .back-button:hover {
+ background-color: rgba(81, 81, 81, 1);
+ opacity: 1.0;
+}
+
+.gallery .share-menu {
+ -webkit-box-align: stretch;
+ -webkit-box-orient: vertical;
+ -webkit-box-pack: start;
+ background-color: white;
+ border: 1px solid #7f7f7f;
+ border-radius: 1px;
+ bottom: 60px;
+ display: -webkit-box;
+ opacity: 1.0;
+ padding: 8px;
+ position: absolute;
+ right: 10px;
+ transition: opacity 500ms ease-in-out;
+}
+
+.gallery .share-menu .bubble-point {
+ background-image: -webkit-image-set(
+ url('../images/gallery/bubble_point.png') 1x,
+ url('../images/gallery/2x/bubble_point.png') 2x);
+ background-position: center top;
+ background-repeat: no-repeat;
+ bottom: -8px;
+ height: 8px;
+ padding: 0;
+ position: absolute;
+ right: 20px;
+ width: 20px;
+}
+
+.gallery .share-menu[hidden] {
+ bottom: -100%; /* Offscreen so that 'dimmed' attribute does not show it. */
+ opacity: 0;
+ pointer-events: none;
+}
+
+.gallery .share-menu > .item {
+ background-color: rgba(0, 0, 0, 0);
+ background-position: 5px center;
+ background-repeat: no-repeat;
+ cursor: pointer;
+ padding: 5px;
+ padding-left: 26px;
+}
+
+.gallery .share-menu > .item:hover {
+ background-color: rgba(240, 240, 240, 1);
+}
+
+.gallery .share-menu > div > img {
+ display: block;
+ margin-right: 5px;
+}
+
+/* Load spinner and error banner. */
+
+.gallery .spinner {
+ background-image: url(../images/common/spinner.svg);
+ background-size: 100%;
+ height: 16px;
+ left: 50%;
+ margin-left: -8px;
+ margin-top: -8px;
+ position: absolute;
+ top: 50%;
+ width: 16px;
+}
+
+.gallery:not([spinner]) .spinner {
+ display: none;
+}
+
+.gallery .error-banner {
+ -webkit-box-align: center;
+ -webkit-box-orient: horizontal;
+ -webkit-box-pack: center;
+ background-color: rgba(24, 24, 24, 1);
+ background-image: -webkit-image-set(
+ url('../images/media/error.png') 1x,
+ url('../images/media/2x/error.png') 2x);
+ background-position: 25px center;
+ background-repeat: no-repeat;
+ color: white;
+ display: -webkit-box;
+ height: 54px;
+ padding-left: 70px;
+ padding-right: 35px;
+}
+
+.gallery:not([error]) .error-banner {
+ display: none;
+}
+
+/* Video playback support. */
+
+.gallery video {
+ height: 100%;
+ position: absolute;
+ width: 100%;
+}
+
+.gallery .video-controls-spacer {
+ -webkit-box-align: center;
+ -webkit-box-orient: horizontal;
+ -webkit-box-pack: center;
+ bottom: 60px; /* Just above the toolbar */
+ display: -webkit-box;
+ height: 30px;
+ left: 0;
+ opacity: 0;
+ pointer-events: none;
+ position: absolute;
+ right: 0;
+}
+
+.gallery[video] .video-controls-spacer {
+ /* Animate opacity on 'tools' attribute toggle. */
+ /* Change opacity immediately on 'video' attribute change. */
+ transition: opacity 280ms ease;
+}
+
+.gallery[video][tools] .video-controls-spacer {
+ opacity: 1;
+}
+
+.gallery .video-controls {
+ display: none;
+ max-width: 800px;
+}
+
+.gallery[video] .video-controls {
+ -webkit-box-flex: 1;
+ display: -webkit-box;
+}
+
+.gallery[video] > .toolbar .edit-main {
+ display: none;
+}
+
+/* Mosaic view. */
+.mosaic {
+ bottom: 55px; /* Toolbar height. */
+ left: 0;
+ overflow-x: scroll;
+ overflow-y: hidden;
+ position: absolute;
+ right: 0;
+ top: 0;
+
+ /* transition-duration is set in Javascript. */
+ transition-property: -webkit-transform;
+ transition-timing-function: linear;
+}
+
+.mosaic::-webkit-scrollbar {
+ background: transparent;
+}
+
+.mosaic::-webkit-scrollbar-thumb {
+ background: rgb(31, 31, 31);
+}
+
+.gallery:not([mode='mosaic']) .mosaic::-webkit-scrollbar-thumb {
+ background: transparent;
+}
+
+.mosaic-tile {
+ position: absolute;
+ /* Tile's zoom factor is animated on hover. We apply the transform to
+ the entire tile so that the image outline is included into the animation. */
+ transition: -webkit-transform 150ms linear;
+}
+
+/* Mosaic tile's opacity is controlled by |visible| attribute which changes
+ separately from .gallery[mode] */
+.mosaic:not([visible]) .mosaic-tile .img-border {
+ opacity: 0;
+}
+
+/* Animate tile's opacity, except for the selected tile which should show/hide
+ instantly (this looks better when zooming to/from the slide mode). */
+.mosaic-tile:not([selected]) .img-border {
+ transition: opacity 350ms linear;
+}
+
+/* Must be in sync with mosaic_mode.js.
+ Mosaic.Layout.SPACING should be equal to
+ top + bottom + border-top-width + border-bottom-width AND
+ left + right + border-left-width + border-right-width */
+.mosaic-tile .img-border {
+ border: 1px solid black; /* Space between the outline and the image. */
+ bottom: 4px;
+ left: 4px;
+ outline: 2px solid transparent;
+ overflow: hidden;
+ position: absolute;
+ right: 4px;
+ top: 4px;
+}
+
+/* Selected and hover state are only visible when zoom transition is over. */
+.mosaic[visible='normal'] .mosaic-tile[selected] .img-border {
+ outline-color: rgb(51, 153, 255);
+}
+
+.mosaic[visible='normal'].hover-visible .mosaic-tile:hover {
+ -webkit-transform: scale(1.05);
+ z-index: 50;
+}
+
+.mosaic[visible='normal'].hover-visible
+ .mosaic-tile:hover:not([selected]) .img-border {
+ outline-color: rgb(182, 212, 252);
+}
+
+.mosaic-tile .img-wrapper {
+ bottom: 0;
+ left: 0;
+ position: absolute;
+ right: 0;
+ top: 0;
+}
+
+.mosaic-tile .img-wrapper[generic-thumbnail],
+.mosaic-tile .img-wrapper.animated:not([generic-thumbnail])
+ canvas:not(.cached) {
+ -webkit-animation: fadeIn ease-in 1;
+ -webkit-animation-duration: 500ms;
+ -webkit-animation-fill-mode: forwards;
+}
+
+@-webkit-keyframes fadeIn {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+}
+
+/* In order to do mode animated transitions smoothly we keep both mosaic and
+ image-container but transparent. */
+.gallery:not([mode='mosaic']) .mosaic,
+.gallery:not([mode='slide']) .image-container {
+ pointer-events: none;
+}
+
+.gallery:not([mode='slide']) .ribbon,
+.gallery:not([mode='slide']) .arrow-box {
+ opacity: 0;
+ pointer-events: none;
+}
+
+/* Temporary. Remove this along with the delete confirmation dialog
+ when Undo delete is implemented. */
+.cr-dialog-shield {
+ background-color: black;
+}
+
+/* Slideshow controls */
+
+.slideshow-toolbar {
+ -webkit-box-align: center;
+ -webkit-box-orient: horizontal;
+ -webkit-box-pack: center;
+ bottom: 0;
+ display: none;
+ left: 0;
+ padding-bottom: 6px;
+ pointer-events: none;
+ position: absolute;
+ right: 0;
+}
+
+.gallery[tools][slideshow] .slideshow-toolbar {
+ display: -webkit-box;
+}
+
+.slideshow-toolbar > div {
+ background-position: center;
+ background-repeat: no-repeat;
+ height: 68px;
+ opacity: 0.5;
+ pointer-events: auto;
+ width: 68px;
+}
+
+.slideshow-toolbar > div:hover {
+ opacity: 1;
+}
+
+.slideshow-toolbar > .slideshow-play {
+ background-image: -webkit-image-set(
+ url('../images/gallery/slideshow-play.png') 1x,
+ url('../images/gallery/2x/slideshow-play.png') 2x);
+ margin-right: -2px;
+}
+
+.gallery[slideshow='playing'] .slideshow-toolbar > .slideshow-play {
+ background-image: -webkit-image-set(
+ url('../images/gallery/slideshow-pause.png') 1x,
+ url('../images/gallery/2x/slideshow-pause.png') 2x);
+}
+
+.slideshow-toolbar > .slideshow-end {
+ background-image: -webkit-image-set(
+ url('../images/gallery/slideshow-end.png') 1x,
+ url('../images/gallery/2x/slideshow-end.png') 2x);
+ margin-left: -2px;
+}
+
+.gallery > .header > button {
+ -webkit-margin-start: 10px;
+ cursor: default;
+ height: 32px;
+ min-width: 32px;
+ width: 32px;
+}
+
+.gallery > .header > .maximize-button {
+ background: -webkit-image-set(
+ url('chrome://resources/images/apps/topbar_button_maximize.png') 1x,
+ url('chrome://resources/images/2x/apps/topbar_button_maximize.png') 2x)
+ center;
+}
+
+.gallery > .header > .close-button {
+ background: -webkit-image-set(
+ url('chrome://resources/images/apps/topbar_button_close.png') 1x,
+ url('chrome://resources/images/2x/apps/topbar_button_close.png') 2x)
+ center;
+}
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/css/list.css b/chromium/chrome/browser/resources/file_manager/foreground/css/list.css
new file mode 100644
index 00000000000..d102d696dcd
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/css/list.css
@@ -0,0 +1,73 @@
+/* Copyright 2013 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. */
+
+/* Derived from /ui/webui/resources/css/list.css. */
+
+list,
+grid {
+ display: block;
+ outline: none;
+ overflow: auto;
+ position: relative; /* Make sure that item offsets are relative to the
+ list. */
+}
+
+list > *,
+grid > * {
+ -webkit-user-select: none;
+ position: relative; /* to allow overlap */
+ text-overflow: ellipsis;
+ white-space: pre;
+}
+
+list > * {
+ display: block;
+}
+
+grid > * {
+ display: inline-block;
+}
+
+list:focus > [lead],
+grid:focus > [lead] {
+ z-index: 2;
+}
+
+list:not([disabled]) > :hover,
+grid:not([disabled]) > :hover {
+ z-index: 1;
+}
+
+list > [selected],
+grid > [selected] {
+ z-index: 2;
+}
+
+list > .spacer,
+grid > .spacer {
+ box-sizing: border-box;
+ display: block;
+ overflow: hidden;
+ visibility: hidden;
+}
+
+list :-webkit-any(
+ input[type='input'],
+ input[type='password'],
+ input[type='search'],
+ input[type='text'],
+ input[type='url']),
+list :-webkit-any(
+ button,
+ input[type='button'],
+ input[type='submit'],
+ select):not(.custom-appearance):not(.link-button) {
+ line-height: normal;
+ vertical-align: middle;
+}
+
+list > [hidden],
+grid > [hidden] {
+ display: none;
+}
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/css/media_controls.css b/chromium/chrome/browser/resources/file_manager/foreground/css/media_controls.css
new file mode 100644
index 00000000000..ba167eec138
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/css/media_controls.css
@@ -0,0 +1,605 @@
+/* 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. */
+
+.media-button {
+ height: 28px;
+ position: relative;
+ width: 26px;
+}
+
+.media-button > div {
+ height: 100%;
+ opacity: 0;
+ pointer-events: none;
+ position: absolute;
+ transition: opacity 100ms linear;
+ width: 100%;
+}
+
+.media-button[state='default']:not(.disabled):not(:hover):not(:active) >
+ .default.normal,
+.media-button[state='default']:not(.disabled):hover > .default.hover,
+.media-button[state='default']:not(.disabled):active > .default.active,
+.media-button[state='playing']:not(.disabled):not(:hover):not(:active) >
+ .playing.normal,
+.media-button[state='playing']:not(.disabled):hover > .playing.hover,
+.media-button[state='playing']:not(.disabled):active > .playing.active,
+.media-button[state='ended']:not(.disabled):not(:hover):not(:active) >
+ .ended.normal,
+.media-button[state='ended']:not(.disabled):hover > .ended.hover,
+.media-button[state='ended']:not(.disabled):active > .ended.active,
+.media-button.disabled > .disabled {
+ opacity: 1;
+}
+
+/* Custom sliders for progress and volume. */
+
+/* Customize the standard input[type='range']. */
+.custom-slider > input[type='range'] {
+ -webkit-appearance: none !important; /* Hide the default thumb icon. */
+ background: transparent; /* Hide the standard slider bar */
+ height: 100%;
+ left: -2px; /* Required to align the input element with the parent. */
+ outline: none;
+ position: absolute;
+ top: -2px;
+ width: 100%;
+}
+
+/* Custom thumb icon. */
+.custom-slider > input[type='range']::-webkit-slider-thumb {
+ -webkit-appearance: none;
+ background-position: center center;
+ background-repeat: no-repeat;
+ height: 24px;
+ position: relative;
+ z-index: 2;
+}
+
+/* Custom slider bar (we hide the standard one). */
+.custom-slider > .bar {
+ /* In order to match the horizontal position of the standard slider bar
+ left and right must be equal to 1/2 of the thumb icon width. */
+ border-bottom-style: solid;
+ border-top-style: solid;
+ border-width: 1px;
+ bottom: 11px;
+ pointer-events: none; /* Mouse events pass through to the standard input. */
+ position: absolute;
+ top: 11px;
+}
+
+.custom-slider > .bar > .filled,
+.custom-slider > .bar > .cap {
+ border-style: solid;
+ border-width: 1px;
+ bottom: -1px;
+ position: absolute;
+ top: -1px;
+}
+
+/* The filled portion of the slider bar to the left of the thumb. */
+.custom-slider > .bar > .filled {
+ border-left-style: none;
+ border-right-style: none;
+ left: 0;
+ width: 0; /* The element style.width is manipulated from the code. */
+}
+
+/* Rounded caps to the left and right of the slider bar. */
+.custom-slider > .bar > .cap {
+ width: 4px;
+}
+
+/* Left cap is always filled, should be the same color as .filled. */
+.custom-slider > .bar > .cap.left {
+ border-bottom-left-radius: 4px;
+ border-right-style: none;
+ border-top-left-radius: 4px;
+ right: 100%;
+}
+
+/* Right cap is always not filled. */
+.custom-slider > .bar > .cap.right {
+ border-bottom-right-radius: 4px;
+ border-left-style: none;
+ border-top-right-radius: 4px;
+ left: 100%;
+}
+
+.custom-slider > .bar,
+.custom-slider > .bar > .cap.right {
+ background-color: rgba(0, 0, 0, 0.5);
+ border-color: #808080;
+}
+
+.custom-slider > .bar > .filled,
+.custom-slider > .bar > .cap.left {
+ background-image: linear-gradient(#c3c3c3, #d9d9d9);
+ border-color: #d9d9d9;
+}
+
+.custom-slider.disabled > .bar > .filled,
+.custom-slider.disabled > .bar > .cap.left {
+ background-color: rgba(0, 0, 0, 0.5);
+ background-image: none;
+}
+
+.custom-slider.disabled > .bar,
+.custom-slider.disabled > .bar > .filled,
+.custom-slider.disabled > .bar > .cap {
+ border-color: #404040;
+}
+
+.media-button.disabled,
+.custom-slider.disabled,
+.custom-slider.readonly {
+ pointer-events: none;
+}
+
+/* Progress seek marker (precise time shown on mouse hover. */
+
+/* Thin vertical line across the slider bar */
+.custom-slider > .bar > .seek-mark {
+ background-color: #202020;
+ bottom: -1px;
+ left: 0;
+ position: absolute;
+ top: -1px;
+ width: 0;
+}
+
+.custom-slider > .bar > .seek-mark.visible {
+ width: 1px;
+}
+
+.custom-slider > .bar > .seek-mark.inverted {
+ background-color: #808080;
+}
+
+/* Text label giving the precise time corresponding to the hover position. */
+.custom-slider > .bar > .seek-mark > .seek-label {
+ -webkit-box-align: center;
+ -webkit-box-orient: horizontal;
+ -webkit-box-pack: center;
+ background: #202020;
+ border-top-left-radius: 2px;
+ border-top-right-radius: 2px;
+ bottom: 19px;
+ color: white;
+ display: -webkit-box;
+ font-size: 13px;
+ height: 15px;
+ left: 0;
+ opacity: 0;
+ overflow: hidden;
+ position: absolute;
+ transition: opacity 150ms ease;
+}
+
+.custom-slider > .bar > .seek-mark.visible > .seek-label {
+ opacity: 1;
+}
+
+/* Media controls in order of appearance. */
+
+/* Play/pause button. */
+
+.media-button.play {
+ margin-left: -7px;
+ margin-right: -7px;
+}
+
+.media-button.play > .default.normal {
+ background-image: -webkit-image-set(
+ url('../images/media/media_play.png') 1x,
+ url('../images/media/2x/media_play.png') 2x);
+}
+
+.media-button.play > .default.hover {
+ background-image: -webkit-image-set(
+ url('../images/media/media_play_hover.png') 1x,
+ url('../images/media/2x/media_play_hover.png') 2x);
+}
+
+.media-button.play > .default.active {
+ background-image: -webkit-image-set(
+ url('../images/media/media_play_down.png') 1x,
+ url('../images/media/2x/media_play_down.png') 2x);
+}
+
+.media-button.play > .playing.normal {
+ background-image: -webkit-image-set(
+ url('../images/media/media_pause.png') 1x,
+ url('../images/media/2x/media_pause.png') 2x);
+}
+
+.media-button.play > .playing.hover {
+ background-image: -webkit-image-set(
+ url('../images/media/media_pause_hover.png') 1x,
+ url('../images/media/2x/media_pause_hover.png') 2x);
+}
+
+.media-button.play > .playing.active {
+ background-image: -webkit-image-set(
+ url('../images/media/media_pause_down.png') 1x,
+ url('../images/media/2x/media_pause_down.png') 2x);
+}
+
+.media-button.play > .ended.normal {
+ background-image: -webkit-image-set(
+ url('../images/media/media_loop.png') 1x,
+ url('../images/media/2x/media_loop.png') 2x);
+}
+
+.media-button.play > .ended.hover {
+ background-image: -webkit-image-set(
+ url('../images/media/media_loop_hover.png') 1x,
+ url('../images/media/2x/media_loop_hover.png') 2x);
+}
+
+.media-button.play > .ended.active {
+ background-image: -webkit-image-set(
+ url('../images/media/media_loop_down.png') 1x,
+ url('../images/media/2x/media_loop_down.png') 2x);
+}
+
+.media-button.play > .disabled {
+ background-image: -webkit-image-set(
+ url('../images/media/media_play_disabled.png') 1x,
+ url('../images/media/2x/media_play_disabled.png') 2x);
+}
+
+/* Time controls: a slider and a text time display. */
+
+.time-controls {
+ -webkit-box-align: center;
+ -webkit-box-flex: 1;
+ -webkit-box-orient: horizontal;
+ -webkit-box-pack: center;
+ display: -webkit-box;
+ height: 100%;
+}
+
+.custom-slider.progress {
+ -webkit-box-flex: 1;
+ display: -webkit-box;
+ height: 100%;
+ margin-left: -9px; /* Set the margins at the edges of the slider bar. */
+ margin-right: -9px;
+ position: relative;
+}
+
+.custom-slider.progress > input[type='range']::-webkit-slider-thumb {
+ background-image: -webkit-image-set(
+ url('../images/media/media_slider_thumb.png') 1x,
+ url('../images/media/2x/media_slider_thumb.png') 2x);
+ width: 28px;
+}
+
+.custom-slider.progress > input[type='range']::-webkit-slider-thumb:hover {
+ background-image: -webkit-image-set(
+ url('../images/media/media_slider_thumb_hover.png') 1x,
+ url('../images/media/2x/media_slider_thumb_hover.png') 2x);
+}
+
+.custom-slider.progress > input[type='range']::-webkit-slider-thumb:active {
+ background-image: -webkit-image-set(
+ url('../images/media/media_slider_thumb_down.png') 1x,
+ url('../images/media/2x/media_slider_thumb_down.png') 2x);
+}
+
+.custom-slider.progress.disabled > input[type='range']::-webkit-slider-thumb {
+ background-image: none;
+}
+
+.custom-slider.progress > .bar {
+ left: 14px; /* Exactly 1/2 of the thumb width */
+ right: 14px;
+}
+
+/* Time display. */
+
+.time-controls > .time {
+ cursor: default;
+ height: 100%;
+ margin-left: 15px;
+ position: relative;
+}
+
+.time-controls > .time.disabled {
+ opacity: 0;
+}
+
+/* Invisible div used to compute the width required for the elapsed time. */
+.time-controls > .time > .duration {
+ color: transparent;
+}
+
+.time-controls > .time > .current {
+ -webkit-box-align: center;
+ -webkit-box-orient: horizontal;
+ -webkit-box-pack: end;
+ color: white;
+ display: -webkit-box;
+ height: 100%;
+ position: absolute;
+ right: 0;
+ top: -1px;
+}
+
+/* Volume controls: sound button and volume slider */
+
+.volume-controls {
+ -webkit-box-align: center;
+ -webkit-box-orient: horizontal;
+ -webkit-box-pack: center;
+ display: -webkit-box;
+ height: 100%;
+}
+
+/* Sound button */
+
+.media-button.sound {
+ margin-left: -4px;
+ width: 31px;
+}
+
+.media-button.sound[level='0'] > .normal {
+ background-image: -webkit-image-set(
+ url('../images/media/media_sound_disabled.png') 1x,
+ url('../images/media/2x/media_sound_disabled.png') 2x);
+}
+
+.media-button.sound[level='0'] > .hover {
+ background-image: -webkit-image-set(
+ url('../images/media/media_sound_disabled_hover.png') 1x,
+ url('../images/media/2x/media_sound_disabled_hover.png') 2x);
+}
+
+.media-button.sound[level='0'] > .active {
+ background-image: -webkit-image-set(
+ url('../images/media/media_sound_disabled_down.png') 1x,
+ url('../images/media/2x/media_sound_disabled_down.png') 2x);
+}
+
+
+.media-button.sound[level='1'] > .normal {
+ background-image: -webkit-image-set(
+ url('../images/media/media_sound_level1.png') 1x,
+ url('../images/media/2x/media_sound_level1.png') 2x);
+}
+
+.media-button.sound[level='1'] > .hover {
+ background-image: -webkit-image-set(
+ url('../images/media/media_sound_level1_hover.png') 1x,
+ url('../images/media/2x/media_sound_level1_hover.png') 2x);
+}
+
+.media-button.sound[level='1'] > .active {
+ background-image: -webkit-image-set(
+ url('../images/media/media_sound_level1_down.png') 1x,
+ url('../images/media/2x/media_sound_level1_down.png') 2x);
+}
+
+
+.media-button.sound[level='2'] > .normal {
+ background-image: -webkit-image-set(
+ url('../images/media/media_sound_level2.png') 1x,
+ url('../images/media/2x/media_sound_level2.png') 2x);
+}
+
+.media-button.sound[level='2'] > .hover {
+ background-image: -webkit-image-set(
+ url('../images/media/media_sound_level2_hover.png') 1x,
+ url('../images/media/2x/media_sound_level2_hover.png') 2x);
+}
+
+.media-button.sound[level='2'] > .active {
+ background-image: -webkit-image-set(
+ url('../images/media/media_sound_level2_down.png') 1x,
+ url('../images/media/2x/media_sound_level2_down.png') 2x);
+}
+
+
+.media-button.sound[level='3'] > .normal {
+ background-image: -webkit-image-set(
+ url('../images/media/media_sound_full.png') 1x,
+ url('../images/media/2x/media_sound_full.png') 2x);
+}
+
+.media-button.sound[level='3'] > .hover {
+ background-image: -webkit-image-set(
+ url('../images/media/media_sound_full_hover.png') 1x,
+ url('../images/media/2x/media_sound_full_hover.png') 2x);
+}
+
+.media-button.sound[level='3'] > .active {
+ background-image: -webkit-image-set(
+ url('../images/media/media_sound_full_down.png') 1x,
+ url('../images/media/2x/media_sound_full_down.png') 2x);
+}
+
+
+.media-button.sound > .disabled {
+ background-image: -webkit-image-set(
+ url('../images/media/media_sound_full_disabled.png') 1x,
+ url('../images/media/2x/media_sound_full_disabled.png') 2x);
+}
+
+/* Volume slider. */
+
+.custom-slider.volume {
+ height: 100%;
+ margin-left: -4px;
+ margin-right: -4px;
+ position: relative;
+ width: 60px;
+}
+
+.custom-slider.volume > input[type='range']::-webkit-slider-thumb {
+ background-image: -webkit-image-set(
+ url('../images/media/media_volume_slider_thumb.png') 1x,
+ url('../images/media/2x/media_volume_slider_thumb.png') 2x);
+ width: 20px;
+}
+
+.custom-slider.volume > input[type='range']::-webkit-slider-thumb:hover {
+ background-image: -webkit-image-set(
+ url('../images/media/media_volume_slider_thumb_hover.png') 1x,
+ url('../images/media/2x/media_volume_slider_thumb_hover.png') 2x);
+}
+
+.custom-slider.volume > input[type='range']::-webkit-slider-thumb:active {
+ background-image: -webkit-image-set(
+ url('../images/media/media_volume_slider_thumb_down.png') 1x,
+ url('../images/media/2x/media_volume_slider_thumb_down.png') 2x);
+}
+
+.custom-slider.volume.disabled > input[type='range']::-webkit-slider-thumb {
+ background-image: none;
+}
+
+.custom-slider.volume > .bar {
+ left: 10px; /* Exactly 1/2 of the thumb width */
+ right: 10px;
+}
+
+/* Horizontal video control bar, all controls in a row. */
+
+.video-controls {
+ -webkit-box-align: center;
+ -webkit-box-orient: horizontal;
+ -webkit-box-pack: center;
+ background: #202020;
+ border-radius: 5px;
+ display: -webkit-box;
+ font-size: 15px;
+ height: 30px;
+ opacity: 0.8;
+ padding-left: 15px;
+ padding-right: 15px;
+ pointer-events: auto;
+}
+
+.video-controls .time-controls,
+.video-controls .volume-controls {
+ margin-left: 15px;
+}
+
+/* Fullscreen button. */
+/* There is no final decision whether we need a separate icon when toggled. */
+
+.media-button.fullscreen {
+ margin-left: 9px; /* 15px visible margin - 6px whitespace in the icon. */
+ margin-right: -6px;
+}
+
+.media-button.fullscreen > .normal {
+ background-image: -webkit-image-set(
+ url('../images/media/media_fullscreen.png') 1x,
+ url('../images/media/2x/media_fullscreen.png') 2x);
+}
+
+.media-button.fullscreen > .hover {
+ background-image: -webkit-image-set(
+ url('../images/media/media_fullscreen_hover.png') 1x,
+ url('../images/media/2x/media_fullscreen_hover.png') 2x);
+}
+
+.media-button.fullscreen > .active {
+ background-image: -webkit-image-set(
+ url('../images/media/media_fullscreen_down.png') 1x,
+ url('../images/media/2x/media_fullscreen_down.png') 2x);
+}
+
+.media-button.fullscreen > .disabled {
+ background-image: -webkit-image-set(
+ url('../images/media/media_fullscreen_disabled.png') 1x,
+ url('../images/media/2x/media_fullscreen_disabled.png') 2x);
+}
+
+.playback-state-icon {
+ background-color: #202020;
+ background-position: center center;
+ background-repeat: no-repeat;
+ border-radius: 2.5px;
+ height: 32px;
+ left: 50%;
+ margin-left: -16px;
+ margin-top: -16px;
+ opacity: 0;
+ pointer-events: none;
+ position: absolute;
+ top: 50%;
+ width: 32px;
+ z-index: 2;
+}
+
+.text-banner {
+ background-color: black;
+ border-radius: 10px;
+ color: white;
+ font-size: 18px;
+ left: 50%;
+ margin-left: -250px;
+ opacity: 0;
+ padding: 10px;
+ pointer-events: none;
+ position: absolute;
+ text-align: center;
+ text-shadow: 0 0 10px black;
+ top: 20%;
+ width: 500px;
+ z-index: 2;
+}
+
+.text-banner[visible] {
+ -webkit-animation: text-banner-blowup 3000ms;
+}
+
+.playback-state-icon[state] {
+ -webkit-animation: blowup 500ms;
+}
+
+@-webkit-keyframes blowup {
+ from {
+ opacity: 1;
+ }
+ to {
+ -webkit-transform: scale(3);
+ opacity: 0;
+ }
+}
+
+@-webkit-keyframes text-banner-blowup {
+ from {
+ -webkit-transform: scale(0.5);
+ opacity: 0;
+ }
+ 20% {
+ -webkit-transform: scale(1);
+ opacity: 0.75;
+ }
+ 80% {
+ -webkit-transform: scale(1);
+ opacity: 0.75;
+ }
+ to {
+ -webkit-transform: scale(3);
+ opacity: 0;
+ }
+}
+
+.playback-state-icon[state='play'] {
+ background-image: -webkit-image-set(
+ url('../images/media/media_play.png') 1x,
+ url('../images/media/2x/media_play.png') 2x);
+}
+
+.playback-state-icon[state='pause'] {
+ background-image: -webkit-image-set(
+ url('../images/media/media_pause.png') 1x,
+ url('../images/media/2x/media_pause.png') 2x);
+}
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/css/menu.css b/chromium/chrome/browser/resources/file_manager/foreground/css/menu.css
new file mode 100644
index 00000000000..2f32d05752f
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/css/menu.css
@@ -0,0 +1,38 @@
+/* Copyright 2013 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. */
+
+/* Derived from /ui/webui/resources/css/menu.css. */
+
+menu {
+ position: fixed;
+ white-space: nowrap;
+ z-index: 3;
+}
+
+menu:not(.decorated) {
+ display: none;
+}
+
+menu > * {
+ box-sizing: border-box;
+ display: block;
+ text-align: start;
+ width: 100%;
+}
+
+menu > hr {
+ border: 0;
+ height: 1px;
+}
+
+menu > [hidden] {
+ display: none;
+}
+
+menu > [shortcutText]::after {
+ -webkit-padding-start: 30px;
+ color: #999;
+ content: attr(shortcutText);
+ float: right;
+}
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/css/table.css b/chromium/chrome/browser/resources/file_manager/foreground/css/table.css
new file mode 100644
index 00000000000..83516993103
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/css/table.css
@@ -0,0 +1,64 @@
+/* Copyright 2013 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. */
+
+/* Derived from /ui/webui/resources/css/table.css. */
+
+html.col-resize * {
+ cursor: col-resize !important;
+}
+
+.table[hasElementFocus] > list > [lead] {
+ z-index: 2;
+}
+
+.table-row {
+ display: -webkit-box;
+ text-align: start;
+ width: 100%;
+}
+
+.table-row-cell {
+ display: -webkit-box;
+ overflow: hidden;
+}
+
+.table-row-cell > * {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.table-header {
+ overflow: hidden;
+ position: relative;
+}
+
+.table-header-inner {
+ -webkit-user-select: none;
+ cursor: default;
+ display: -webkit-box;
+ position: relative;
+ text-align: start;
+}
+
+.table-header-cell {
+ font-weight: normal;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.table-header-label {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.table-header-splitter {
+ cursor: col-resize;
+ height: 100%;
+ left: 0;
+ position: absolute;
+ top: 0;
+}
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/css/tree.css b/chromium/chrome/browser/resources/file_manager/foreground/css/tree.css
new file mode 100644
index 00000000000..e151977f754
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/css/tree.css
@@ -0,0 +1,83 @@
+/* Copyright 2013 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. */
+
+/* Derived from /ui/webui/resources/css/table.css. */
+
+tree {
+ display: block;
+ outline: none;
+ overflow: auto;
+}
+
+.tree-item > .tree-row {
+ -webkit-user-select: none;
+ cursor: default;
+ position: relative;
+ white-space: nowrap;
+}
+
+.expand-icon {
+ -webkit-transform: rotate(-90deg);
+ -webkit-transition: all 150ms;
+ background-image: -webkit-canvas(tree-triangle);
+ background-position: 50% 50%;
+ background-repeat: no-repeat;
+ background-size: 8px 5px;
+ display: inline-block;
+ height: 16px;
+ position: relative;
+ vertical-align: top;
+}
+
+html[dir=rtl] .expand-icon {
+ -webkit-transform: rotate(90deg);
+}
+
+.tree-item[expanded] > .tree-row > .expand-icon {
+ -webkit-transform: rotate(0);
+ background-image: -webkit-canvas(tree-triangle);
+}
+
+.tree-row .expand-icon {
+ visibility: hidden;
+}
+
+.tree-row[may-have-children] .expand-icon {
+ visibility: visible;
+}
+
+.tree-row[has-children=false] .expand-icon {
+ visibility: hidden;
+}
+
+.tree-row[selected] {
+ z-index: 2;
+}
+
+.tree-children[expanded] {
+ display: block;
+}
+
+.tree-children {
+ display: none;
+}
+
+.tree-item > .tree-row > * {
+ display: inline-block;
+}
+
+.tree-label {
+ white-space: pre;
+}
+
+/* We need to ensure that even empty labels take up space */
+.tree-label:empty::after {
+ content: ' ';
+ white-space: pre;
+}
+
+.tree-rename > .tree-row > .tree-label {
+ -webkit-user-modify: read-write-plaintext-only;
+ -webkit-user-select: auto;
+}
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/css/video_player.css b/chromium/chrome/browser/resources/file_manager/foreground/css/video_player.css
new file mode 100644
index 00000000000..decb63dfee8
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/css/video_player.css
@@ -0,0 +1,103 @@
+/* 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. */
+
+body {
+ -webkit-user-select: none;
+ background: black;
+ font-family: Noto Sans UI,Droid Sans Fallback,sans-serif;
+ font-size: 84%;
+ margin: 0;
+ overflow: hidden;
+}
+
+#video-player {
+ height: 100%;
+ left: 0;
+ position: absolute;
+ top: 0;
+ width: 100%;
+}
+
+#video-container {
+ height: 100%;
+ left: 0;
+ position: absolute;
+ top: 0;
+ width: 100%;
+}
+
+video {
+ height: 100%;
+ left: 0;
+ pointer-events: none;
+ position: absolute;
+ top: 0;
+ width: 100%;
+}
+
+#controls-wrapper {
+ -webkit-box-align: center;
+ -webkit-box-orient: horizontal;
+ -webkit-box-pack: center;
+ bottom: 0;
+ display: -webkit-box;
+ left: 0;
+ position: absolute;
+ right: 0;
+}
+
+#controls {
+ -webkit-box-flex: 1;
+ display: -webkit-box;
+}
+
+#video-player:not([tools]) .tool {
+ opacity: 0;
+}
+
+#video-player:not([tools]) {
+ cursor: none;
+}
+
+#video-player[disabled] .tool {
+ display: none;
+}
+
+.tool {
+ transition: opacity 180ms linear;
+}
+
+#error-wrapper {
+ -webkit-box-align: center;
+ -webkit-box-orient: horizontal;
+ -webkit-box-pack: center;
+ display: -webkit-box;
+ height: 100%;
+ left: 0;
+ pointer-events: none;
+ position: absolute;
+ top: 0;
+ width: 100%;
+}
+
+#error {
+ -webkit-box-align: center;
+ -webkit-box-orient: horizontal;
+ -webkit-box-pack: center;
+ background-color: rgba(24, 24, 24, 1);
+ background-image: -webkit-image-set(
+ url('../images/media/error.png') 1x,
+ url('../images/media/2x/error.png') 2x);
+ background-position: 25px center;
+ background-repeat: no-repeat;
+ color: white;
+ display: -webkit-box;
+ height: 54px;
+ padding-left: 70px;
+ padding-right: 35px;
+}
+
+#error:not([visible]) {
+ display: none;
+}
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/common/2x/bubble_point_white.png b/chromium/chrome/browser/resources/file_manager/foreground/images/common/2x/bubble_point_white.png
new file mode 100644
index 00000000000..643847baee3
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/common/2x/bubble_point_white.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/common/2x/check_no_box.png b/chromium/chrome/browser/resources/file_manager/foreground/images/common/2x/check_no_box.png
new file mode 100644
index 00000000000..8dddff8fa17
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/common/2x/check_no_box.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/common/2x/checkbox_white_checked.png b/chromium/chrome/browser/resources/file_manager/foreground/images/common/2x/checkbox_white_checked.png
new file mode 100644
index 00000000000..466fa2266b3
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/common/2x/checkbox_white_checked.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/common/2x/checkbox_white_unchecked.png b/chromium/chrome/browser/resources/file_manager/foreground/images/common/2x/checkbox_white_unchecked.png
new file mode 100644
index 00000000000..88d97d7dd15
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/common/2x/checkbox_white_unchecked.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/common/2x/close_x_gray.png b/chromium/chrome/browser/resources/file_manager/foreground/images/common/2x/close_x_gray.png
new file mode 100644
index 00000000000..8a2930af8bd
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/common/2x/close_x_gray.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/common/2x/disclosure_arrow_dk_grey.png b/chromium/chrome/browser/resources/file_manager/foreground/images/common/2x/disclosure_arrow_dk_grey.png
new file mode 100644
index 00000000000..8f3531966e3
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/common/2x/disclosure_arrow_dk_grey.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/common/bubble_point_white.png b/chromium/chrome/browser/resources/file_manager/foreground/images/common/bubble_point_white.png
new file mode 100644
index 00000000000..ec5dbe9580e
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/common/bubble_point_white.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/common/check_no_box.png b/chromium/chrome/browser/resources/file_manager/foreground/images/common/check_no_box.png
new file mode 100644
index 00000000000..3006dcd62f9
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/common/check_no_box.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/common/checkbox_white_checked.png b/chromium/chrome/browser/resources/file_manager/foreground/images/common/checkbox_white_checked.png
new file mode 100644
index 00000000000..b24de30f8c8
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/common/checkbox_white_checked.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/common/checkbox_white_unchecked.png b/chromium/chrome/browser/resources/file_manager/foreground/images/common/checkbox_white_unchecked.png
new file mode 100644
index 00000000000..c92eaaa2d91
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/common/checkbox_white_unchecked.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/common/close_x_gray.png b/chromium/chrome/browser/resources/file_manager/foreground/images/common/close_x_gray.png
new file mode 100644
index 00000000000..3ee375a32b5
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/common/close_x_gray.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/common/disclosure_arrow_dk_grey.png b/chromium/chrome/browser/resources/file_manager/foreground/images/common/disclosure_arrow_dk_grey.png
new file mode 100644
index 00000000000..dd2f925b573
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/common/disclosure_arrow_dk_grey.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/common/spinner.svg b/chromium/chrome/browser/resources/file_manager/foreground/images/common/spinner.svg
new file mode 100644
index 00000000000..66b05c38ad2
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/common/spinner.svg
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg viewBox="0 0 32 32" version="1.1"
+ xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+
+<g>
+ <animateTransform attributeName="transform" type="translate"
+ values="0 0; -32 0; -64 0; -96 0; -128 0; -160 0; -192 0; -224 0; -256 0; -288 0; -320 0; -352 0; -384 0; -416 0; -448 0; -480 0; -512 0; -544 0; -576 0; -608 0; -640 0; -672 0; -704 0; -736 0; -768 0; -800 0; -832 0; -864 0; -896 0; -928 0; -960 0; -992 0; -1024 0; -1056 0; -1088 0; -1120 0"
+ dur="1s" calcMode="discrete" repeatCount="indefinite" />
+ <image width="1152" height="32" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABIAAAAAgCAYAAABq+wOnAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAQa9JREFUeNrsnQd8FFX+wKduTTaFQAghhBZ6R5oCFkBUBJWTZgHP3u487Hf27p2eXTw8CzZAxIKCgBRBOJVDEAXpoff0um3K//dLXnAz+ybZ3ew87q87Hx5JdmfmO6/+yrz3e3xGRgsuwoOH5IBkgyRnDX8iN6XLpc8JkqM/fGXjdK1M1/X9nK7+xAnCMp6XFsF5FY3ddOuMNjHxIUn4We64D/o6W552B8+Lybqu/KyrgVW6pi2QnOkVkdy0qXzynQ5JgRTsMHFpji2tg86Ltq3kc6Z8SAFIvm43H9BPRf7r+JHkPcojwU/wE/wEP8FP8BP8BD9mPuhGUcNBTzql/GDVcZdoS7qe0/Ucb8HPn+9fMHkHS76/eKdNcmVcIMjuqYBtpVYXfLbzvSHvsOIHyg86IP8X8pJ9JOj63TlBbAs/m6PtoQWrPt332cRbfQWbqyNtf9E+g+ItTuYF4SLQ688C26YX2DgteU5Iwa90XS0B++cY/NwJ9bMJymamnNLW35B+HS1fC1Rm6Jx2MS/IZ/KC1IXj+RaQ/wz4SgRmqa4pe3UtuEFX/SvUQNVimyfH15CeHwO/GfAnAP8M4PerYfNCGhSzDv/KgJ8P9tdPuuJfrqmBL2V3ZnVj9kY0z6Ap3tZQxsCXzoG67w58MFx5d02b07ViKPvjkP8fdTW4QtfUzxqz//A5ouFDuXaCPI6H/J8Fdd8N2kBOyLdB+G432r6a4v8K6uYz0eYpidT2iuQ5IE9DOF0Zz/ES1L/QjeQ99IQCeIaNUAYroF7mCpLzYCz2n8mz8MAfDvyJwD+bF8Q8Mv6FPkAl2N2/6Jp/KZTNPOgnv8Rqf1KeAfnDgD8Fyv9syB/yBQPfD/zNcM438P0cOO+Hptq/oc8B5ToA/rsS8j+K5F80nK7C97vgvK/hnNlwztp42d/4HCh/2l78UVtny35vQ95OJzLI2Ejygb8K+JB/cWUkMliK4BnxnCRIzhCBw6V0ufR5aGTDfq0iIYPnORiQxP7w19XA9kKF/Bse5B/w9+EmCH4qH4+M/rdmuLPPmAvs5FoNwTaAk1zXANsHQmG25it9QkrO3tNExcOUb1BObJ2v3fIydPzJpMNuhXHhWV50oJDUWPBJwoFBP7zsVq3FoLsdsqfNflb5D+VD8kKqhMarNLEDNIlPFJNTlv8EP8FP8BP8BD/BT/CjN/xAPzil+Q+W7WstJWW93O2mfedoqv+Xyr1L7z207M+72fH354iujDtkV/OrQM9FhwPnzh5yS+64OZfu/3zKRkb8u+1pHa8AfurJRpGcPajD5OXF+XNHLrSSr3iLOgqS4x6bp/VEwHho5wiye3zOuTO+3fXBsDnx5qu+km5gzP5FcqZNMuPzvJAOPzpAIZxR84GLGwXt9sq48P1lfYB/u2BzT6p50U5/gEw4B9NgTnbfItpTyzXFuwiMwefAHvmhSfxA+WmCaL8P+BfQ+TzymwO7Odhfg8H+ukHUtTK0vxRfyTO25NZ7m8KH+5zFC9J9YGueE2701xwytMtMHpMg94LRaho6I+C6D8H+e7Kp9p+mVI+B+97Di/ahkEzGH16Gc7rCo3QVRQfYf/qrcN08Leh7SnKm72iK01tXfVPg9n8DO7p7uM8h9EwB62A0JvjrKV31L4bx6iGo/41N4UM7mswLtoeB36lBPscnwTmDeME1CP54ANreal0NPA59c0UT+ROBfz/cu0cjfDuccxqcA4m7HfjfA/9h4C9tSv2j/Ol8zeYHRHvKg42cKkIddIE20gV+vwns/826FngU2u38pspfeIYa+edo3uM1dAA30AY6AL8D/HZNrTMq8CgvOj5oyBEkNFSikHDAa14zpBmELwzK/RqpOydUyJ/hl3xITxMBHlXlN8THIzXvkjPqnD+GSx1Q8VdLya22a4HKf4IQt8fS+Brjhx65F83tX+f8qa0LEQSH4y1oCBuhIfa0mh96gGAelz3q5d2yp80+4H8H/B4s+eRcvKY5NF4PeXt2SvjkHgl+gp/gJ/gJfoL/u+ejA6axdKrzjzMuFG/hTR0vW31TcttRGaz5VUe+k1R/6X1ySu52UKovBoXOA8r8kOT2F7zMgu87sVlWAxUPyylttoEue1ud86fO2AFjYLKVfNSZCX8n8G8Ndf6ctEzcmcOt4uOMI9DdnwUDeivwrzVzvpwE2pLaxTX/VcddWrDqedGR9hPwr2uMb3BIXdis7w2tm8KvmXGj+t4Hw3MjtLsrTZ0/dEMQ2+oUUU5aXr7rs7xY9G/Ie3Nd9c8Dm2ZdTfuPjp8CZXCTLTl7K+TjHzh7KQbHSxbwP4f7fA38kY3YqmHOCLjuGrD/fgH2U9iWouVD2bfRteBXguRaCEb3sOjqkHfCddMkZ9pm4D+Hs9ei5/s7gu32LRrwtc6fqA4RyuxCqLv1UI6v41gaY/5XQzuaXev8iU524UwpqIPl2IZiqX9ss9AGXwD+3FrnT5QPIMiDgb8E+J9iW45F/qLt6kjv1Eq0e+6Jni/2hGf/CMpwJZRlbjzkPy/Z+0bxAHnQdt6DNrQWyiAvWgcQvnXBSksya/ia4ovUs4jOFyhAfR387Bip16sxPh6BykP7GikFWbAl3S4lZ/8XOkHnaLxukfDruaGdzXNMGkJvXpTXQSP4o5X8uiOl49gke3reP3EQJPzBwF/Pih96dLryu+u63rRnR9cb9+7QFO8k1nxybRK5l2Si6DLjn+r8J/gJfoKf4Cf4CX4jyreU1Obs7FPFB4OtJRi+GyRnxgxbaruXWo9+bYGr1SA7K36gdE+2K7Pf16I99XHjUgtetPWwmh8sP5Brb9Z5rWhLfihsqUedgaap1Zbxy/ZDA2hVxzc1XvVA5S4r+KirQ72vA939DtThI7hE9Rfv+CpefDVQ3k9yNfsFDMi/RNlv60rGW7X/6/JY+WAwDxFk189gwF3epP7HCymOjJ7nR80PVp0JxutPvGifEJ3jJewBHFCHd/Gy4/vKA197ouCfI4iOX4A/lmvSUcO/F9rS99HYfzWzfkT7Jl6QRzWRj/bndDmp5bpg5ZEOUfAvhHHmR7TdmsbnBEFyXYdjqeor7hpF/8/hBdtG4vhqWglAG4L63wB9qn808seZ2b8V9IGb4sC/GNryZjVQMTAm+SvYsP+JsXdB+Wwoyx+hTY9sqvzXg751MTiiToe2hI7AMWagMF8GpGYmHV/laqdW+mGQmgYF+zr8jm8BIvCw8jgLBtfF4YD0Y0O+lEj4KCcOLJy2pNtN+x6BXN7X0ACHUwMlR+p/YBA4T3KmNzYlMmI+9+vSLlENVH4OVfQI0CjeTt4Jg/mbuur3QIN80Qo+SXZP3kUdwpUG3lHLD+A0wX9YyedqZ3qJ7ScsOkdKzn6EiOGWvOCcA/nPgvy/wIIfelFqp0vS03pOHQqK5QZ7eqcDrPnkntguirjadfIJfoKf4Cf4Cf7vkN/QEihQfk9p/sFQGNT1pj1zeV7K1VXf+hPrX7i2aOOMIlb8QPnBtmCwLQPFtWOI06Vb1vAnRubPHfm51fxg5dE8OaXNcsh/GxPHywZyH0v4SvWJ7lJyqyXAb92Ag+EwpytPwi9HreDLnhyMI5LZiJNjvqZrfyd1Ecf2VzIYdPUva+PLNHjgvTC8QYGmeF/fv2Dy0rjwA+WjRFvyp2aON0NfxPvhMi8PuX9NEYIBfb+veKcSCx/sqrGCzT2vIceb4RnKCZ9qpAarj++NgT8/qhk/jVl+vNTBltR6Ioxt7zTGhzHnMjD8Z0Xo+IvUCO8rOdN+hLY1QnSkfdcI/wpBcr3F0WKsxM7vJblbrFW8RedLzmabGnb+eCcD//0InQ56JA5CjFkj2lPXRGL/ovzJm/r9lXBRs8b8RKTty8RR0VD9t4E+tVL1l40V7SnfRCJ/9WCFSNq3WT1gjKsS0udTG3RUwlgm2pJWQN++RLR5lkcj/32FW4Kqr/QdaDfXGM4r4GpjHNvJ+Y4G+GnQphdB35og2JI+j1X/KN4862/N+lwvgTwcAn9Wk2fA81BWNDBe8imC5PwU2hb6bOY05ACSTOB1A403RHnBgaXOQ4oV0I44gy6GdKZJw0ShggM1BjHabeKQapBf90HIczwM6RVIZ0NCj/VFVIcUNGgQLEuUisMDG1gXGjE/9CBxbg6RfD8HaTS1H4j256ESThgroal8oowo8BwojGGA0/cBrm043/a0rgWLYEB6M958UgbIL4dBxCknZ59Gyz8MsKW86JgVLz5pBwpJJ/l1QjH3orkD3K0Gv8XVrNPWgzgTiqyLjJlPUeBN+aHeeHLvwtC4SIYYSE2q/zo+UYJM+Zz5uvQEP8FP8BP8BP8U8Em8G1M+yA1TPlzbZL7vxOYMe/Ou8+qcHyAnB2b0u/n+oo0z/mzGJ/IrLvn3FWxpbs/oshL47cIqxt2iCvjHTeRmXPiBkvxWcmruauBnmZTz12BMXUP0rNBniA+/4lA7Oanlsgb4aLy9BjUzT3K1qCTBQePGBwM1T3SmL2/A+YMbmzwDCYwYvtjmyakLUBoXPhjo3cFAXFJ/uVu9A2OJvgrpY0g7T95YctbpZH54lpj5YKCdDobqAnxha8Ivg4ROjPmkLkL7AxpgvSDt2vXB8JKY+Djzxeb+uAHnRymk2ZAw7hKuwDhBnAAisb+GQsJZQyOwOJXqEy/sXzB5QxT8EY3wsWw/gfQFyf8xrjboNua1C7F/cKZ/2GwPNVhV3Shfqb5AkFzvNuD8wPzPg/QlpE2ErxHnFs6wOQvSZZAos10wJIl8B7SPyWZxSXHmDfBnNcBHBxraDjjbbDOkI2Rsxp2UukE6h5R/O4oTpKXkTF+jBirQwfi9SfmfKciudxrgo42Jz7cK0s+kLPHc5oQ/ivBbm9q/3qLBkrPZ7obknx6oKjX5fj3J/3LS/+qceVj/uDwJJ3dMg9SSUv4e0e75QvUVDxYd6dsak7/oQFWqC16Cce7ukO/XQML2sZr4D+pi2zgJ/0JIUyFlU/hJUO6fqYHyM0WbZ0M08j9/zlkPdLjsm9UwNnUm9b6SOF9Cr0P+BZCupZY/x9ugb82FMWakYEv6Nhb9I6P/rUcIg3Zg/7uEw/g/tX3ByJcFyfEOtLFjuKwydFA6eQYZxASKp+1EA8pPXcfEWT0vEkdMP9JJaUdzMoAbB9mm8LEjfARpIimID008cc1Ed4uPg5VHaQN8U/ihAvI8Ugb51CeQambidLOIT5QB/lzSUMPHIUF6VdeCfSzkozD2CnYP1dMOSuVM4Pe1ml93jatlv/trnT+1nYAX7W8CvzcrfujnLc94sHX2iOd6mqzJjhufnEu7RiCMBD/BT/AT/N81nxLzxpSPY3qEsXMa5dPGf4wT0eW6be92u3n/vq435n/b7pJPTzfy45V/Gr9g/fOCvVne+8aZL2Bcd2HGT+/4Ec35g7qMaE/91Eo+xtyRPTmfmThfjhG9Dg28vVbwMeaO7M5cYMIvI4r9AEg4O6Ey3vyaXbbsnvloqJro97dA6k0M0OJ488HYc4Nh9LGJ8wf7AMbhwFlhT4U6f+I1/mCcEDC+PzFx/gQJtxWk24gharx3CTFMj5jpfw3ylepW0Nc+NHG+oGPtCUi5pB4WQzoeYgCrxCCeRZwAzRVvcfrOWac9GQU/q/bFtCn/ScL/I3ECHSDOnzrH0H+Jc/A04ojZElK2yw4smvpNo/kXHWYzX7AcH4KEY9MNkBZwtbO//KRucCYcri55HBLGy8GlY2HBl3Vd9Zm1P9xlDPL/ngkfZ3rcCSmH1P8ikn+F1AHWBRrVD5A2OoU8X5gTghfEh2h8/AxssydMZl6hs+FqSG0JYwX5rG73vaPks3uJ8+lG0mfD7V+75zOTmEQn5d+h5bctgqL6b8h3O4lzZyCx8X/h6s/kKidt/17SRv5MxqwwJ5BgS1mAfT0S+Q/t90V/8c5xnK5NJ05FnGDyBjpZufqBjbF9oFPlb8T5cZsJ3w19fL5JTCRT+a94S46J9hRsG/cT30IB5QUQOsceIeU/3YTv5GXHnGj5Ecrf7WSM6kLqv4TuBHLOgf7YItTjVHfgQxk7fxU9I40e6J0dQ4TWDC58Rk4vUlh3W8BHAT2ZDBKzjGxekPtAJ3iSVBJnUf5XkYFwPvHG12sEHC++Db8MNjTiePJ3kc46m3gFQ/l2aGNvEWVCtYiPHu95ZCC+3PCVjedF7Ex9DG8C4sqHDqPXdAJdSzPmH/gfsOLDwK4KkiM576ofnq0LEu7pOHY+aaMRl38MW6ee5HP1p2giIxnuV04EjyX5r+OTPIbxidCwqv8l+Al+gp/gR8V3pHdyGpZu4KwTJnzRkfpXQXZPITpKrrNl33+n5I3rkj3qlWIW/LQeU2/hRcfocJtBXkJkmaX89J5XTQc+bYcTnDE+MUoHYNR8ObXtA7xoG0C5Dxo7FxCDz7r6d2Y8DmXd00SxR+Nrn5V8yZn+dM0uSuEHGn8XEp3SMr5gc78EfFqcFjSkcVb/T1bywfh+3WTmE26njasaotpNKVT/i4jPSzhLnRYs9wDh/xgFvgjqs252VqT8t3EnKRN7alyoQyeCA50BOAlgeKBkt7h7zjlbIuSnmbQ/zP+2KOp9IXGI/Puk/aFrBZX7lv/LnC/CuXwqzflM+JHuJoYzkuZytRMg3iVt12B/mco/mmNkJRn/iiLko/yaCWkJVztb5wzDeN5dcqQ9ThxaVPmLy57y546akHvh+/3klFwfeYZAhHw872VSB/NJOwjhi3mC7Ho8Uvvbnt7piyj7PToFX+JqZ6l9QRyCoe2srSA7MAzKTZHwY5D/WP4vECfh58QhE8pvEw0f9Z8obb+6+l9KnqGbQaBnQh28ROzPk94midL4vE1QvuoOXGo0iaOv+/wL9+tUJSv4c0jnC2u4oGjdoniLQyObW8EvJQPnf8KVKnGgpnjHW8zHjjuBdEQjv6+u+q60mK8Tr/USiie6u64Fr7OYX8f6wIR/PQs+Olo6TFo2vN4OcaL9UuBfxYpPUZ7dZLmB9eVPXzrhNjifE/wEP8FP8E8J31+4rUWX67fPbz95+Z6uN+avyR03B5VWbxOcP43yQ8ffin3LkkXZfaNBRqVmj3olnQUfl36BUfCIifHxkNX8QPGuLNGRSuPgW+jxFGMpvvyKQ+1EW9KdlGvR6XJmDM6fqPg1W53b3H+mXIuzOobH4PyJkl/c2STgKnJHxOD8iYqv+st6465JlGtxycuwGJw/UfExQGvtTlem/I2W8muCDttGmzifhkbp/Ila/8OlZyb8XYS/JQY02nwrbGkdv2ps/AX+ecA/l3KPLYS/LQY+Mq+ANChYfuDKo6vuHnZ4xfQ9JvyzgH8e5R7rSf3vibHe/8DVvoSvM4kUf9GON4zlT353K5XHjKFB0Il0fhTOH6PjdCRHWYmDY40WqBzakPwNlO2vkFNyPyP2WyAGPjoOz4K0jmJ/34rLTRuT/02Uv3tJ3YWthIGx5lpDUHAr+LvI2L2bwr8uEn4T9R8cu8+hjd286JioBspP40I6gXG3B5WjTSGL7fiM0zXcxnKG4XP0duG0zust5GPjvZ2rjRFUjy3IDpwudpXF+cd1r+i9xfWazerrdxJOl/vYYj7eZxoRoPXXJfIy8mdZzMeB41Ku9i1abn1PqHgn8VRqFre/J4kjsLeBfwf8+BcDPid7csKmVQP/IVL+qtV8ci9bmzHv9HVlDbgR4C61unAmg/ZXj8/9Or22bneM0gQ/wU/wf598ULBKiQJM5ccw67HeETK7scH8yyltHgelbFStXJbbu7IHzcy9+MNcVuXvatn/TwBuYbjmCEePkxh3vuxpfSvl7TsaHRhPw2c1X0zKBB2NN06Lx2VOk4gOZSlfcqTdS1n64yMGXJHVfEF2PUhZ+hEgumOB9XzHXyn8INHdDlnOlxyPceFLb5B/CXGCWMoH45/mfMSZBOM46lKeOPMF+X4T22Eco/w/ZuLAGEvGIav591KuwdlbF8ah/f931/tD0ZHTwrz8pdsp1x0n5d8UIxzl2BW6rsxXqgt7eY+u/+bQV7fsMJO/uz4Y9n77CYuOyJ42Z4u25PXwYG9yjW9a0NBRNwkA4zV1DTX+OEHECRhrLZb/FaQPI79VqMMD+jyMOTUTBKzkYxu6gDhQM+rxZQe2uT9azC8gDjx0IIfKN1GQ7NjmbrCYf5z0Ycy/s575KchY/n+QCNhJ6fw6F6+DF/6la+olvCAat9W7ovM1P9+1481eVvJfJY1wRH0vnHOS6i+9jfs1aJ1VfBxAHyTPEVIk8mm6GuhFPJRW8nEKOTq73q3PFzvrWnAQV/uWzUo+TmPD6X6fGNpEe2gTZ3G1a2et5KMgx7drqyl89JAvY9D+0RH5z3qdnBdyCP8rq/k4Hbn6yDqXq9XAj3Adck37T3GfA3z0kH/fEL+pRhg5dBKcMdTIcIYIV2vLv/ZeCX6Cn+BHyI9x2Y+ZI4bKh89r+B0mLRsvp7S5mtO1ssr9K58FJfn7ePLN8l/H73rTnnp6Cc9LrdythqDCeIwJ/8b8qyjX3NVE5T8ifvMB0/mM0/50HeWaR4gCaSk/9+IPRXfWoKmUa3CHqX1W8ztdtcEluTIup1zzHNe0mR8R8TtfuyUVjL1LKde8yDVh5kek/C7Xbk0XbO5JlGtwmcAGy/nXbWshyG7a7ItXiOFoKb/rDbs786JtKOWaJ1mUP/C7A3+wCX9TvPQ/mv5Vy9/ViRftp1Muw/AcO6zn72zDi47hlMumx8n51iAff+ly/Y4kSlC62+I0/usgTz7d9c7AJZHI3z0fjcFZlx9HGXOtoQMdiTi+rq/3qaYKIbGIrJT/GJ/oZg4ngoTa37Lr0mDl0VuIk8pKPjqw8WX/Owb7/1KluuBWUj5W6l/4EuevnGESCuT/csVbjJMgKi3mYx++n9ifIfl3jFUDFem4BMxB8T7Fq/H92gkEETuUZvjcqauBixjw76FY5ZBvfhyj/L/B/bpl56+FoivjGfFxGVT4NEZdY8XHzh8euE9XJjLiYwC6NRT+pYz4OIV1aThfncSIz7laDTq7zvlDDhHyP5UVnwg0Nef8f3fNm7ruhvYTFo2Q3S2drPjknqExl3jCTvAT/AQ/hB9H5bNRft609aPszTq/AQrZGWAIXuDpMOYj4MvM+JevOQ3jAhjO3RNH50fD/CvWDuAFOY+itM5jwU/tOvlMdHgZzi0mOovlfHtqh1GU2U+VxAFhOZ8X5YsoW35XEQeQ9f1P1y+mzD7Cc59mwdc51AHDthxnxudql+nIFKP1MSb515UplHOLjQaThfyJlHOPsOOrfzAxWt+0Qv8z8jXFN5QLDwqNm+m8x4KPKVi69z2Dwb0k3uNvQ3wG8hcdqf8KMTwqq47+dyZD/oJw+4tPFmTnRYz4WL+GZaQYkFsaw0j/wrI2vEnn3TxfMymFBR+dT4cNfBns/3E4A8hOGSisOLbpWnA1KDtn13eCaPj3cov5+CYDPaD1gvxBA8DB5wsG+cfpvLgW9I56fF4cxKj8NeIEeqB+GxCGMeLj4DqLq32rEcKXBjNsfzigD6ufffkshnws//pvunhxCEN+2FReyP8IhnwOjL2RsjuzZptLOSmL63DZ14O3/7vr/TR+nGYe1dcqZrTBe4c6wewmipJVR4Kf4P9P8V1ZA+XsUS/fIto9/VV/+Q9HVt7+PEs+KIGjuPozI3GpNMrlL5nw7ckjKOfN4eI7A8ucb0ui8XFprsKG7x5JOe9TzjonZD0+LzlHUc7D4JnlTPiijZZ/1AmLGPHPNcl/MRO+INP4+MKukA1fGkY5D4PHljDK/1km/b+KEZ+Wf9wNy8eEz4tnUM57k9X4EyjO3+PM6q8bjOAXuPDJApbJ3/x55y3NvWjuJfb0zqN4XtgrOtJeZTX+M5T/NwXL9n8B9nZexd6la45/9+Qxxvy3jPaXIDkxDu0SRvbnDOKICTG/bDjz7CsGfOxLr3G1u3SF8O0Yo2cVI/8Dbjx1f32+PBwdQJLhZL9V2p+uBecbHUDwGL0Z8T8LcwDV7vrAKv/LwhxAtbs+sOKvNDqAQPh2Zcj/JtwBIXRhyP+aC3+ADq6Wp7mqj/3Agr8mHC928nQY4ynPX6Qw4H9LyX9eWrcpaSVb5/hZ9H/Z3QKn9Z5c6y/Irmuh/J9kVP4195bdLVNaDLm3bfXhb0+UbJtHC25nKd+gAEgJfoJvPMcK5yceW2e0CePnjHnrcdHmub62P7pHt7nwXVz6dCOr/IMBkk057ygzvmjvTTlvBTO+5Ogfiaywii+INhp/FUN+n1Na/oLcj3LeV+z4Eq39LWbHt9F2HlvCsPxPo5y3iGH59zSxFViNfz0o533ETP7Qdz5jlv+9n16yK+/yNffKnpy7OZ736JryOrSJt1jL3/0LJuMEAUxFIH/9rPks9I9dHwzDcXXTKeJ/ThwhIUzdwdD+wzaNs6D4EPt/IEP+4jAHkCCz5C8McwDxUneaA0jhrDu+p7iF2jDih7Fh8G3DMP+bKQZ4luRMsyneEhZ8SiR/PjW53eiUir1LvQz4Wyl8e3LuOZ6K/St9DPi09cxCUtuRWdXHfjjKgI/rmXFqsyuU78oa2Ko8f9EBBnx8o4pe/3oBqZ2ZfXNKts7Zzab/8x5j/Xs6jsmB8j/Igp83bX2m5ExfBgNvp5RO45WMAbc/suvdwW8wHP+M95YiOCfB/43zs0e+0MGZ0bOn98SPmw6vvPM4S74gu84zKCWXW+gACuPzgtiKct52hvw2lPN2M+PzUjsTWcGm/fFiu8hktUV8gcrfzK78qfxt7Nqf1J5y3iaG7b8D5bwNLPio+8KAQ+v//2XB97Q/H/QRnrb1+EYWfGeL3k7K1u9BlvUfrDo+x5bcOsQJrK+GMmE2/hLHBM54el90pAqqr/Rot5sPqKdS/odsHvC75FvELuZ07RVo73+p4/iLd7/B0P4+wdUurc4J0XX6M7S/UaZhzKeUX60/IYth/sPjmQlCdl0Q6NBDs6r1cbp+mPKphxH/EMUBk8Yw/yeoDoics5JLd35awYBPndJrS22L69+9DPjYy3Bgr7fbgzNrYLOK/SsPM+ArYR0QDtmVgX8fZdL+a8vAVW8EdjXDNniAEb/A6AAS7J4UZv2fsqOC5MzA/B9kwZfdmegB71SHlpNaPdis381fFG2cEbrThGbhDAxj3iixBy0t/wQ/Aj7L+u84ZeU4W1pHnHIu2tLzFE/n8eiAmccs/7ruNZRCgG35CzQDzMuMz4vNKOeVs+PzKZHKaov4nlPJ5zkhlXJeEbv8C54IdbW4892thmAcStoyjKMs+J4OY5JMjMBjLPgpXaekmYzBTPLvzh6SbqKjFLPgOzP7uk3yrrBq/7vfO312h0lflcvJrc9Vg5W7ecH2pORMPyXyV/WVagn947fL3/pa29vbT1i0QXRmdPCe2LTm0JIbtjH1P9TO8Mr51R2hHFC8Jcblh5qFZY+KZc9fhZ+YyjD/ASLXM0L4yRLH8Kg68HVhcocLw9xCjPCU7UR1lWH2qYN69fGN1Yz41MCagZI9rPg2LnyrT4xGrzOsA0cYPljtZ8h3hTdBnWX+KQoHzzPkO8M+EW02hvxexj7hTO+MU6ALWMA7/XFTEgw59wi2pB6g7Kzbv2Dia4Gy/WHjgoUOCC5xmAwM6Z2kQPkBVVN8TLlySu4dIeMizgjB4KvzGD6CUeFIYloAPC+bjFNVrJ6A8pmHa9r2v03ly8yKnxNoDgg7w/qn6aBMZKLoSBNMvmKiEwh2j2jyFRO9VLS5xWh01bjnX3Ka2R9BFvxA1THvqap7PIo3zyrLPOOB3bwgdwz5eBVr2Zf/4bm45K5u2V2lVfpHQk9JHGSXs5Wngq0p3r/wvNiRF229dF054T22cTrjRzC8XOPdjoweNl/hliAjPupUGSG2p18iwjZUCRGsEkC2tE60t13Gt21W8WnGdwXD/GdR9JwKMACDhGk1vzlN0FbsW1bJiN+C2iIPrS1kVP/NaYotGH2ljPjJXP1tIGs1reqCYkZ8dLS0DtM0vYVFjPiobLY/hXw8csPK31dcxoovOdPegqGmZstfIcl5TrtLv8jY8WavB1mNv4Z+Hmpo/U/xrZoCDoplGF9OyhLbT1zyuOhIvRzkgU/X1Xu52oCBbPLP80YHaDvSVwNM+LpaYvA32IisOsqIX0Hxd+Bbuu1s+DrN0YTLUg4y4tOC3eIszV0s+DqnlfHhs7AyWfFryp/nkyj532s1X/WVqJyulZCZ4EZd5bDV/Iq9S6u4sLgYNQcq6UVW8ysPrqmk6L944JvpMqv53mMbzBiY/xOWl3/+l2WZg+81zkpPs9AAC2v/FfuWX5Oce87zvCDnasHqb4OVR6bb0zsx49PkL8jJ37X+8Xvn/1brf/vrnXEjnPOb9b42DexetL0DbMtfP1YPpWvlvsItfkb2Nx7u+sJfLRW4yNYFxqf2bUmU9cb8Pkb8vHD/j7qXYf67hfE1ZQ9Dfv9wvnpK+fAAR6uOfFfJiE8J9qiXFP30xnFGfEqwQ726ePOs/Qz5RmNTLd+9iFX/w4DjxhlAWsXe5azyn8sZlr9h24f8H2TEB6OCHx/6gWjzjGU5/na+5ufzu1y79dXOV//0WNbwJ9oQNjM+R1/vfEr5uRfPHys60q6q2RaTF5JBEX+ZtFUmfF3x/4dy3mBmfE2lOTqGMuPr2sHIxkqr+ArtdfcQdu1foxm6A5jxNe1IZLLSqvKntr++7Pg6bblbb4btj1b+3Vnwg5VH/aCD0RxdPVjwKw+sqkQdkHJeRxb8QNk+H5S/If+6Vcu/qPV/aMkNW7fNzBu79bW2vba/0e3G/Lkjq1jyf+/yP8H//fHB5ishM++Z8mG8fRjGuzp5owSrjj/EkI+2T4bB/j4ikQcINQxxloQl0yAFyUnZclD/xfCBVfzTw/0PwZ8Z5j9su1NdDWxgyD87PP9M+edSyp8lP3ztoaZsZMi/iML/OVBes+OAiwH//HC+uq3q8LdVjPjjKPydlQe+rmTEv4TGrz72QzXD/Nd7AwLGzyGW/U+0p+JODDWvIFK7TzkvULZ3IAjDKiPfql0wts5oY6coAJzsbulsN2Hhn+D5YIzQ0Sl/F2dNIN4wvuhIMc5Kwzq6mLMmEGwYX1P8Pwqy+zLD51M5yq6JVvB11f8zVMAkSl/5iAlf8W3iJNcfDJ+jo/R9NvkHHUAKG5tHc7VbEVtf/2rgF1F0GMfmMZCeY5J/0IF40WbUjS6A9BIbvrKNshMU1scMNvzABl5wGl8OngVpFgs+p6k/c6JkXPOCW9N/wqj+t/Ki3TgzeARnzU5cNP5m4GdR+N+y4CvVhY+C/PkXMbxU+Pt+ydXcKhuQKv9Y2V8JfoL//4FPdiv9zeV/17uDd3ran396eq+re3qP/7jv+HdP7mHIR71fC7VBdF0rkggs1ADCt/SWBEEUZOellOdaZvjACj5mfiLFAbOSUf6xnC+jOECWMeJjo5pC4X/NiO+klr+mLGXEd1PLXw0sZ8THKe5XnML6x9hH14R9qisrGPFxgLuBwl/IiI9rTG6i8Fcx4uOge2s4X1vNcPx9mAvdApOXWmX0vxUdQEsY8ft1vubn67Wgt7Bww0vvl/zyQXmdoGv7hwV3Sa4Wd5Hz8O07voHuwsV/KqxxBppfC1R9Kdo8dxk+x7byPCSf5Xxv0UouPOjmZEh3Qiq1nO+vWCY60p8wfI4OGSuWgYXx1WD1CuA/RnGA4DKwg5bnP1i9WrSnPEBxAOGM3a1W83XFt4qzee40fI4va3AWxG7L+agDye4bKQY4OiUOMOB/zUnOKyn5x2Vox63mwxj8H9LfjO0fx+tKy/OvKV/zot34cmoCJIwN5mXEN76cQ10Rl8IGGPBXUPjogH7MAhkUxt/1zsDZ2SOe3+Fq2f+06qPr1x9eecduC2PghLe/2qOe/AUDuPxU8jnrguAn+An+75pfvmdxFaTvTwH/qBqomCfakoms032qr/QViSi5oeuARfIQcRU+MKg5u928v4/hY1WQ7AtJAVjJv4ALm1aqV+matoBR/i/jQqKPE+OvDIwhVvxbuLDlL7qP0/X5jPg3c8bpZ5yOsY9Y8THYl9HKUjhe+IAR/3ZIYbvN8Lz4HiP+vRwtBhUvvMuI/ydiUBj5sxm2v/CF9bw0l8avHasOxJM/laMtK+ClTxjxcUZF2LIWQU7aFcrPPP3+rJROF0/WVZ+fFx2vx9EAHAZpuWhPtUHiWg59ZLz3xM/n+Qo21zhYREfqMMP5OFZfDenf8ZQ/XP1YD5hn3653B63uesPubbxoC132lUP67JNW83fPHfFj1xvzd/CC3DnkO3RYPwrpz1bzS3d+siGj/617gR+6HTY6bHF6cjy3g6fyy7Z/tC7jtD8d4nmptYGPBuBVVvMD5ftWy+7MEzAWhcaow754P0d5aRBvvh6sXg66QCnwUw38xymOibjzNTWwSOT0akCGKqGoF6JT7Dqr+SADP4dfA8APfQuKDvu7iRPEUn6tDqI/V7ME9NcjifT/Ry3PvyCiDvQMV392KOpK10N6kQF/Dvx8wsBvScbff1le/rU62ONc/UBgeRYZf7T65w6vmJ4PP/ZYKH8b5LOwvxL8BD/Bj44f52DkpzT/OKYJkuPOduM/+0pwpLXzHvth+aGvbllTFwTaa/BCeUIMg3jAMXMeTfFtFCRnqLK/ZsebvcpI5q3iyzRBCs/yieRMr9t+3cr8o+H/93C+d5aclOVlwM8hylx9frB6tmhPKWXAR2PuYVr5C7L7GAM+vsm9z/ihrvoXQ1s8yIDfx4S/nBftuxjwBxAHkMH/GFwPRt9GRvxHw/nqRuD/aMaHMcMHSlg8+L1ohjwufwPld63Z+BdHPiqzL1D4vwD/Wwb5R8P6NWr5i7Ytdfxmva/NhrQYFPK6+e84YwuXZuxvIh/v9x4XMs0VnS2tR77Yy5bWscbBpAYqNhAnUejxLCR8O9/UWRiCrivj8qau61a5b/nKo9/cV+fU8taVr+ItfhNkwbOG69ABgjPk1sVL/hk+PslXfcVviY70v1OclgvIM1jJ11VfyduiI83YR9EA/RLS53Fogw3zA+XvijbP3yhOUzSOF1rMV6D9vS/akm83fI/OFzSOv7CY79eCVe+BLPyT4XucMTubQflXa0r1PEFyGZ1t6ADAZXirLeYXgS60EGTxeMP3fyb532Ax/xjI4i9BFhuXAd4DCV8Q7LSYfwD4OAtnhOF71Jk+5eLjhG+Ivx/4K4E/0vD94yT/pRbzD0P9fxFa/9Afvt7+7658nOSfKT9Ev6HKXy5+u9FFzY+n/ZXgJ/gJ/u+XH+J/0fPnnbeUfFyN42ud17/SAEMvVGocHVB4L7F06+w7YLBfw9W8cdJRsb2KAR/jDhmDTwe1YHWowmsV306UKOPsmyq1uuBZBnxsVB+F30uvBv5DDPi46xvOcjDu8qHoivdhBvwWxIgybv+O2z0/wIDfmuTfGHxZ19Xggwz4bQjfHu6ACrDgtyV8Vzjfdw8DfntiQIXztcBjIUyr+DnEgEyh8J+g5T+pzdnOTn/c9FjXG/PR8YBvYJs10fmykKPswAf8p0L56T2mnRvi/MEjg5RdiybwPST/Ybuvie7MgrrfAyX5j1OCoeK1i2jXRnFgv1/A89KnclLWE2k9rliRe9Hc/qSsTy7v8B75diaFbyPP3i1e8i+0+EP5vsKtM6FCjMGA8fyPuVoHsqX8YPmh12p2QwpXWtAB0tdqvlJ++EXcEIfCR9nZz2o+yMLnanUSav4HWZ5/X8nz8JGfwp/FxScgb4N8Leh7mgsPRikQB0AHq/m6FnyKouxKRHfJtpyvBp+h8F3EAZnCgP8IhY/XvBZP/TtKPsqdSSz4wbIDN6uB8tmgk2xR/aXvH1/70F1W2B9mfFb2T4Kf4Cf4CX60fJyJ1JRkxsfv6hxAKPyN0eed8RB+APGQe3HH1j5yePvrnSdvnZGbBfoNvnHYbzW/+tgPlRTjc7bkbBa6vt4qPr5FOc/4oeov/7uc0vaQxXw04JbSFFg1UPkMAz46vdDJ19P4hRaofFF0pG+3mI/G/xqOsqOEplS/LtpTfrKYjzNPcAZDO0r7myPYkr6zmI+zN9ZylK3fddW/QJDdSyzmDzHnB74E/vLG+DB2NIWPgU2/pTkQwOBYLUjOOQ3xHRk9PMGqYxg36U8xGiF1+e9M4a8F/lwaP/vclx+TnOnX8YKMM6duIH0olinxPUn7603hfwP8+XV/d7v5gMJJjhMmbZg2OyeSAw3HVbTxB2NfibbkkzNrnJl9ipTqortNHIjrOUoA+QjHn6+4esHfeZezea9pWNY1eSZHcocLK1VvyQMmDjQcQ0bHwMfthF/QNXVD52u3vNKs1x9DFYp6fHfroWWKt+ivlHtg+1/NUQLYxyJ/zfiOFj0LVX/ZE5TL0QjGOHkXWMm3Z3Q9ATLxWZP8L4+x/EMdiWH8UIeHLbX9YZCJtKDLuBQPZ0GdH29+aP5tya33aoGqV03aEOZ/qJV8GG92gEycadKHvuaatitWo3zR5vkBZOK7lGvbkfbXw0o+yOL/AP9DkzH0Cy58++J489cAfy7l2vMp11rBXwv8D4wXBquOa3Ewfhrv/806H9/xRo+bts3sOHrHm73uKdk2ryIO8j/i/m+l/ZHgJ/gJfoL/v8oPFWw46AYpChDGTuFj8EbxkFCBMc7+QEZFiHcqKn4UimcNf98n44+o/tKTyoWuK+j4eIhySdz5YOiMoBj/q3xFW5+ymI8Fu5GmOILx/b2vcMvjFvLx3HHEcKRsPR/cqHoL77OQj236GpL/ThT+VtVfcaeFfJxtg8bc9xwl7g20v72a4r/FQn4yVxtTAB1MOVx4ARzRNeW6SPnQjtPJFMJID5wx8gpxfmRT+AVQBtdGw9eCVS2IERDJFonocHqJGM6ZFH4xdIJpDfFFR6rQdvzHs2V3yzfIvdBZeR9HmUlEObJI/r+h1T/yofwxtohO4wuy60zD52h8obPyWeKQaOxAx+8/IP1AdxzpJbT8q1Un5sHYtMKkPNEIe5GjxZEKP9DRgTMKNnPU2SN6GfDD6n/XOwNmQz3PM3HCLIb0NhfZbAQUdti/tlAdV4IgkbKud+yc1f9tMII/o9wvnfDfpPan8APftFxJ8n8bL4j9wMC9rPmgu18MlX8m/MUmAhyN0HeIQyyS8XcEtLOHgmX7LzOTv8aLijfPel5X/WtM6hNnkb0bYf3Xk7+R8kEmPgmyaZ2JEwSdMDMibP8n+eTaiPiat+hxkA2bTOof8/8cFxbHrtF6iJivBqseAP52yn0yiRPmMcq94sbXgr6/wrhMW+6JbR4d6RhHT7aMrwamE93MeKAMx3ZxdZQ6UFR8XVVuNeEP0zV1SAyKd7T5vwX4BwyyomjXOwPdUcrfGOvfi/zdIXriliMrpq+IQf7HxI+j/hFXfqz6f4Kf4Cf4Cf7/Oj/UoEKDpIQoOaGOIQcx6jAydaSBiZxEcRWNcoYwaGvb4sYnAS9P8ne82euvrUe9/Lac3CZDDZR+ldTm7HIWfF3x7+Ntct8Q4/9AoHT/ZHerIYqVfFDk7+FFew7N+QBGwXgL+VjeaERR31hj/oOVx/5gS2nrt4CPbysvI86fbib8o8AfY/PkVMeZLxBD/RLCb0WvGK1YC1ReJNpTS6Pl484QDQQmRCMNZ1qMJWWQZtIyytUg8G2eghj5WK64JAXjsuA22aWkr2NbQyX5PFIGdhO+VwtWXyzYko5Gys+7/Jupgux6mtwTFWScubMKfQZot5JBMpOU/xhSBiaGiu7XFO9kQXbvb4jfevS/ThMkZ+jWyDiIotP0LtK+VxAHQxlp85nE2YEzVUZHwA/b3QjX40IZl+hqcCcvSa0o4ykGRMW4MLhzHM7e+gXSCTKgY9vH2TqjSB2YvDXWg8C/UpBc+4zfOFr0DFYe+Hq8O3vIYl50GLeFRjmBMTlwd6ylJG0ifIHkvwthn9uAo0wF/hTgH6DlH/jXubPPyONFW19K/7qKq91JbylxCKCDpYAIszTSLtHhM5Eznb6r+zlNe4YWW4LwpwF/NfD7UIQpGqDTiDNoEXGwocHoJ0K6E2l742mOR6jzsyRnGqd4S6jyD/ne45uudDTvvoYX5K6Uh8eYOFNI3WMbxJlR2I4qSf7xmuFcbeya7hwvcHJKLtfpjz++vPPtvk83Jn+bD5iuBYp3TZLT2q3jeSmHkn90auHuRJ+QMkBnEfbjABkjOpPy76D6ir8hToOI5T/KpED5wclycta3wM+i1P9NpAwwLgrOXsPZbYUh3+MzVNPkfyR8GWSS4i2aKDnSvoOya0bhTydtAGdqfEScEpWx6D+09ie7M6sVb/HFkiP1v8D3UPofBqW+kTgisQzwBYsSLz7GQ1T9ZReLdg/UHZ9s+NpFHGB/4WrjCb7bFP2PxgeZVKIGyi8RbcnId1L4b5L8NsaOiQ8yqUgNVIwXbUnLgV+v/KuPfO/c//mUaAIDx5Z/f9k4yP9SqP9M0BXKfYXb7gxWHbNFIP+bzneklVcd+W6A7GwxRVcDwcMr71jmK9wSjFD/iFr/pwRYbVT/+P9ifyT4CX6Cn+D/f+DzGRlhIR7QeGnG0ae9quQh/EQYayEKkkSMNCcFXAcvoni5mPJBiAUNzhrL+LirTnrPaW/wor0Pvl3T/OWTREf6Oqv5Xa7d+jooNOPq+x6Cu5TqwgvkpKzdVvE7Xvnthbbk1vNNnC/7laqCUcDfFW9+pz9ubCc5m30Pzdn0DTE6f1RvybmSq/mWePG73rC7CxiLb3G1M53EBp1yyPeVjwVFe0NT+MltR6qtz5v5LBiJfyDGl5OLZJq4rpWCgj1OtKeuiYXf8Yr/TLV5cp6M0RuND1AJCvbFoOiuiJSf1nVictbZz/xs2CEmxkOv1gJVk6BfLGyMn3vR3JHu7NO/5OJ66D41UHkpKPiLaPy6rWeDlUc6SM6Mz6Bd9Ygvn0PnzxWC5Jxn5IduewtGQIYrs++XvOgYYAF/mmHpXdjhPbqhhaNFz+WQ/55xLn/g+8Lybzz8xTvb2FLbLYb+1S2udBj/tr3WPg/lj7HMQw+l4nB7MSlzJc9LuXEi+3d/MLxToGx/IFT+mvGDVce7SK5mK4DfKkIA3jesfwbKD/5t9/tnvGMm/03z7y3sJTnSlxl25TI7qgg/raaIteB/Cje8ckPB+udLI9E/aM+g+ooHwBi5BPjpjRUscUDtAoP5xW0zOy6IVP9paJtp1V86FMbIRRQnkPE4AW3qFmhTH0erfzXMLztTtHs+NzpB6p0TqLh8xxvdP4xV/2uQHyg/B8bILwy7kpEXW75V22Z2uqip+mc05a8Fq1bufLvvVAygGS/9tyF+6bYPkyV35sCy7R8dLNv9RaUV+ndDfBibftP6f4Kf4Cf4CX6CX3vQllQEiWKTxoW/ycYbJ3HRTUWuu2cJZ/7Gihk/gq3d4sY//u3jRyGN8XQYI5XnLzoSuu7ZSr6/JP8jZ2bvsXXGOiio/w2U5I/FWAtW8nlNoRotoLitCZTun4Brva3h89MbdP6o/jVK5bEr5JTcA3Hl8zwq/Y0uS4Hy36BUHh0fD37WWU/fDsbptKhMQC24TcO3m/XjLkXFtyVn3xWr80fX1F1asOIPYFhtjobvaNEnPR7OH5zargW9l4Fhsb6xc4mBvqzL9dsXC5LrfC4eh64d0YLVk4C/trFT5aRW+RX5C4e4c856VbAlTY0T/4SmeCcIsvubxk51txpSCPxz3G3OfgvOnxAnfjHwLzfEnaK/usjqf6LqyHfDnS16z45j+ZcA/7JI+Pb0Tgd8JzafaW+W9yEvOs6JV/nrin+a8eUD7ZCSs/cEy/YNlZJafcyLtoHxoGPEb9KnGpU/sjtze6Di0FD4+SmMM5HEfrHR23HLq7jaZWtRyX/JmfGz4i0eJtqTFwC/S2PNlaTa0ViQhzbrfd2jBeuf/3Os+geMketVX8lwAfm81NDYjmMhypzmWE+tz5s5+tCSG7Y3Vf+BMXItOmEEmxv5DSkrLXhe/KBZv5t3FG2cURQv/Uu0p6xWA+UjBNn1sRmf5/hLudpZmHHX/0SbZyXwzwT+p8CvFz9OUwMFVuufWP6Kt7CfrimXav7y0sPL/7KEOH+Y6L+pXSdheITVVurfjejAv2n9P8FP8BP8BD/B/9VzRDsU8hDG6NTRK5+19yiMMPO/SX55/qKDETp/4sLf+/HYld5jGyeDIjlLDVQ8AmlEhM6fJvFLt3+8Ateth2S/HJTp2ws3vnZWhM6fWPm8meGp+kvvrdy37OwInS8R89O6X+4Bg6MR549eCfx7Kvd9NShefDBio9gVSPdpgcq/ByuP9YvQ+UPlC5KD56KL/1DHD2rBqpdAoe4TofOnHv/o6r/9oqu+b5vQ/5D/glJd1DsS50+IE0ip2r9ybLDy6BW6FtzTBL6iKdVvqMGqnhhoM9KLMCjx9je6XVXD15XDTfJ9qb7ZmuLtGYnzpx7/310nKdUnpuHMtSZ5H9TAF5rq7x2J8yXECVVa9OPMC1Vf8W2U3ami5S/VtUCfaPgYFBnGrFHQd/9K2Z0qWv5iTfX1BX7E22ljgP7q4xvOgHH7IcruUFEdGPA/UH7weDTyB4MSK9WFQ7DvRCk3Q1teIFb5KznTd6q+0tOg77wWLZ+X7D2aKv9FR9ovMG72h77zToTyR3K26DMsXvqHaE/ZpAWr+2DfbZjP25JyhreLt/6DQZm1QFVv4L9H48N4st9K/Yvwe0L947IvrbY5Kccq8hc/z0L/k5wZ+bveGfiP/Lkj5/gKtwTirX9GsGtLQv9O8BP8BD/BT/At5UuNXIzrzqqJx8nJRT4DAK/1ErjShIf/zfAjmHkUV/7eTy7GHXA+idLx1CR+wYYXC6SkzLGeDhdcwYt2TVP8L6My33zAdEv5lfuWf5DaZcKVddO2MZgjKJBvBssOvILGHBi0ceeX7fi4ouWwR3fzgtwx3PYBfrD6XdzeGB1v8eSDUfYTGJPnNmx7Kce1oHe25i1+AR1PNltSk/ia4ktSvIUzJVeLOyO7UisEI+FDxVfyTzQm4XmjhpM4BeW+E5svklNz7xJEx2hetPWOkF8O/LeB/2KsfKgznG75QcH65z9O6375NaI99bqI+ZyO/E90xff3KB1vxvx/UJG/cIEza9C1oiP1JmhrnSLkB6DOvsDtlcGY2sCLMfPfBf4nruzTbxXsnluMb+Qb8jvoqn+5rilPoeMjlmljGJMGfrwE9T8b61+Uk66G/h1pEF5cDrQG0tOC5FrMxc5/2l+8c5acnH2fIDlxfIl4VwZgfw/pMeB/yXPRT2IjcdoeDZQffBfG0PsF2XU5dH9HFPw1uhp4RLQlr2hoyYepEyopC+XXdNVf+haM5Q9C/nHpTaRO4GCw+gTGHSpA+ROD7OMkV3Nc3nWz6it+iZecjwAfl7w22pJhfFwfD/kP/R3je12lBSpf50X5figDjO1luhuUUnl0ezz1D4wJAz8uVwPl/xRE+8PAHxPG17UCGJ82W6H/iPYUXEY3VQ1UvCqItrth7BsL7U+Gfr2x6MfX3rBa/yL8axVv0dOar6TLsbUP/1J58JtInaEN8iNsjwn9N8FP8BP8BD/B/83yaTGATM/lagMS2YnjSAp5IJ2AMKGQ9jXRc5Xg/z/n0wIN0o4ojIMG+a3OfiYzqe2oUbwgngiWHZiDQW2t5rce9XJecvvRT3OC1B6MrR/A4F8Hhvfq0m0f/ocYkHHnJ+UMT84ePeMRMAjHcJqGHRw4WglYvMd1NbhNC1avCJTvW2sS6Dtqfp3xiDtxdLrqh/N4QR4IqT0niNk8L6bXlolWpuMyF03ZDPzVmr98tT29U6ApfKPRivz2ExZ1kdwtzwNjsB8vSrkcL7aoKRddLSL8nbriX6MHq76S6YHGG+WbGcvI7zBxaXfRnQl8e18ogzyO51Mh2Thd98EzHNY19RdoB6s1NbAYg7pGw2/MSEd+3hVrBwi25NG8ZB/A8xLUgZDKc4JN19UKeIZjYPRvgTJYC93hc2LARsSPxEFQkb9QdGT2x6UpI8Ag7A9lnwf1n0KcAgG9Jv/KNniG1bwgfS5IriPxHHx8JzbLoitjKPDPF0THcGC043iheW39Q/vntGJo/5vg9zXw3UIwmPPjyQ9WHXdBvY8DQxj7QB9g5NWLU6Jrh3GHQSiHVVA3n8B525ta5vU8av4ybGsTgX0OL9j6wTjXjgt9gVPT/oObgP8d1MscyP+Ohtp6tHwtWIWBrSdAmZ8piPbB8LOlgQ99UN0E7X85PNtcXnTsa6iPR81XvNnAmALlPrJmaVot/+TXNcssVf+qsh3z7zi6+r4CElQ9orEtIo1K9eVC/4byl87moO/xgtCGBCtWVF/J27veG3IfjH3BSORvLA45yH9ryCPyh8HY2wM+OF51+LtHDyyctjla+R8TP1jVPFhxuNW+Ty45oAYqxKboH7HwyU5QCf0rwU/wE/wEP8FP8OPA/z8BBgB0NyAnicwKswAAAABJRU5ErkJggg==" />
+</g>
+
+</svg>
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/black_folder.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/black_folder.png
new file mode 100644
index 00000000000..01ac4929616
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/black_folder.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/breadcrumb-separator.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/breadcrumb-separator.png
new file mode 100644
index 00000000000..bbbfbffcf82
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/breadcrumb-separator.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/button_list_view.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/button_list_view.png
new file mode 100644
index 00000000000..09aa2fd3a58
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/button_list_view.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/button_list_view_white.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/button_list_view_white.png
new file mode 100644
index 00000000000..f89199a7e40
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/button_list_view_white.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/button_mosaic_view.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/button_mosaic_view.png
new file mode 100644
index 00000000000..1961e46e121
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/button_mosaic_view.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/button_mosaic_view_white.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/button_mosaic_view_white.png
new file mode 100644
index 00000000000..ad6f856c651
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/button_mosaic_view_white.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/close_bar.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/close_bar.png
new file mode 100644
index 00000000000..3f9e4528983
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/close_bar.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/drive_logo.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/drive_logo.png
new file mode 100644
index 00000000000..be900c88b2b
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/drive_logo.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/eject.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/eject.png
new file mode 100644
index 00000000000..f764c0c4c0f
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/eject.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/hashed_bg.gif b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/hashed_bg.gif
new file mode 100644
index 00000000000..03651e6ef82
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/hashed_bg.gif
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/icon_search.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/icon_search.png
new file mode 100644
index 00000000000..7f2ff27ceef
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/icon_search.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/offline.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/offline.png
new file mode 100644
index 00000000000..6fe2d25de02
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/offline.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/onbutton_trash.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/onbutton_trash.png
new file mode 100644
index 00000000000..593f53fdba4
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/onbutton_trash.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/process_drawer_button_closed.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/process_drawer_button_closed.png
new file mode 100644
index 00000000000..92e9e784015
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/process_drawer_button_closed.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/process_drawer_button_closed_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/process_drawer_button_closed_hover.png
new file mode 100644
index 00000000000..326f26f6ce2
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/process_drawer_button_closed_hover.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/process_drawer_button_closed_pressed.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/process_drawer_button_closed_pressed.png
new file mode 100644
index 00000000000..98aca06f061
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/process_drawer_button_closed_pressed.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/process_drawer_button_opened.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/process_drawer_button_opened.png
new file mode 100644
index 00000000000..2e019075159
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/process_drawer_button_opened.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/process_drawer_button_opened_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/process_drawer_button_opened_hover.png
new file mode 100644
index 00000000000..18481ce2989
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/process_drawer_button_opened_hover.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/process_drawer_button_opened_pressed.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/process_drawer_button_opened_pressed.png
new file mode 100644
index 00000000000..6f4e637d152
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/process_drawer_button_opened_pressed.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/search_clear.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/search_clear.png
new file mode 100644
index 00000000000..a76574afb43
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/search_clear.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/search_clear_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/search_clear_hover.png
new file mode 100644
index 00000000000..ed7155a8362
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/search_clear_hover.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/search_clear_pressed.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/search_clear_pressed.png
new file mode 100644
index 00000000000..2237a085992
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/search_clear_pressed.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/search_icon_active.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/search_icon_active.png
new file mode 100644
index 00000000000..d58cd100d67
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/search_icon_active.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/search_icon_inactive.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/search_icon_inactive.png
new file mode 100644
index 00000000000..553ebbb1a68
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/search_icon_inactive.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/select_checkbox.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/select_checkbox.png
new file mode 100644
index 00000000000..e85e010f33b
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/select_checkbox.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/select_checkbox_checked.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/select_checkbox_checked.png
new file mode 100644
index 00000000000..e8792487a26
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/select_checkbox_checked.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/sort_asc.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/sort_asc.png
new file mode 100644
index 00000000000..419b29ed735
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/sort_asc.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/sort_desc.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/sort_desc.png
new file mode 100644
index 00000000000..e7e74c58769
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/sort_desc.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/vertical_separator.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/vertical_separator.png
new file mode 100644
index 00000000000..9d14b688ebf
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/vertical_separator.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/warning_icon_square.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/warning_icon_square.png
new file mode 100644
index 00000000000..52a8b5a4298
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/warning_icon_square.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/white_folder.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/white_folder.png
new file mode 100644
index 00000000000..9124621632c
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/2x/white_folder.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/black_folder.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/black_folder.png
new file mode 100644
index 00000000000..539f67759a5
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/black_folder.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/breadcrumb-separator.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/breadcrumb-separator.png
new file mode 100644
index 00000000000..ed941270f61
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/breadcrumb-separator.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/button_list_view.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/button_list_view.png
new file mode 100644
index 00000000000..c553781c5e1
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/button_list_view.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/button_list_view_white.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/button_list_view_white.png
new file mode 100644
index 00000000000..798df51a9c7
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/button_list_view_white.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/button_mosaic_view.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/button_mosaic_view.png
new file mode 100644
index 00000000000..5690834ac3b
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/button_mosaic_view.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/button_mosaic_view_white.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/button_mosaic_view_white.png
new file mode 100644
index 00000000000..9d4d518bfaa
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/button_mosaic_view_white.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/close_bar.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/close_bar.png
new file mode 100644
index 00000000000..f7232012fc1
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/close_bar.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/drive_logo.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/drive_logo.png
new file mode 100644
index 00000000000..b9ec5168cac
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/drive_logo.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/eject.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/eject.png
new file mode 100644
index 00000000000..dbbdb7f6fcb
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/eject.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/hashed_bg.gif b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/hashed_bg.gif
new file mode 100644
index 00000000000..1e5bcfa09f8
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/hashed_bg.gif
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/offline.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/offline.png
new file mode 100644
index 00000000000..259d3b20e78
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/offline.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/onbutton_trash.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/onbutton_trash.png
new file mode 100644
index 00000000000..20ab756dc43
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/onbutton_trash.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/process_drawer_button_closed.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/process_drawer_button_closed.png
new file mode 100644
index 00000000000..020e2e65f7c
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/process_drawer_button_closed.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/process_drawer_button_closed_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/process_drawer_button_closed_hover.png
new file mode 100644
index 00000000000..193a49b37c4
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/process_drawer_button_closed_hover.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/process_drawer_button_closed_pressed.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/process_drawer_button_closed_pressed.png
new file mode 100644
index 00000000000..bfee4b01376
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/process_drawer_button_closed_pressed.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/process_drawer_button_opened.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/process_drawer_button_opened.png
new file mode 100644
index 00000000000..c03e33708fb
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/process_drawer_button_opened.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/process_drawer_button_opened_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/process_drawer_button_opened_hover.png
new file mode 100644
index 00000000000..bf1aad74d5d
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/process_drawer_button_opened_hover.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/process_drawer_button_opened_pressed.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/process_drawer_button_opened_pressed.png
new file mode 100644
index 00000000000..dbce527cd8b
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/process_drawer_button_opened_pressed.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/search_clear.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/search_clear.png
new file mode 100644
index 00000000000..6755c4e8b17
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/search_clear.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/search_clear_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/search_clear_hover.png
new file mode 100644
index 00000000000..bbfd15f4867
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/search_clear_hover.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/search_clear_pressed.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/search_clear_pressed.png
new file mode 100644
index 00000000000..1825168c5c9
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/search_clear_pressed.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/search_icon_active.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/search_icon_active.png
new file mode 100644
index 00000000000..f59dfe340a7
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/search_icon_active.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/search_icon_inactive.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/search_icon_inactive.png
new file mode 100644
index 00000000000..2f17e7c8c16
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/search_icon_inactive.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/select_checkbox.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/select_checkbox.png
new file mode 100644
index 00000000000..4010b5c7212
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/select_checkbox.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/select_checkbox_checked.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/select_checkbox_checked.png
new file mode 100644
index 00000000000..d63f1a44143
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/select_checkbox_checked.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/sort_asc.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/sort_asc.png
new file mode 100644
index 00000000000..ed8c7d5b8af
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/sort_asc.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/sort_desc.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/sort_desc.png
new file mode 100644
index 00000000000..f0b2af0a667
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/sort_desc.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/vertical_separator.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/vertical_separator.png
new file mode 100644
index 00000000000..512f37f09d6
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/vertical_separator.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/view_thumbs_black.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/view_thumbs_black.png
new file mode 100644
index 00000000000..54448aee6a0
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/view_thumbs_black.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/warning_icon_square.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/warning_icon_square.png
new file mode 100644
index 00000000000..90d6b608fdb
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/warning_icon_square.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/white_folder.png b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/white_folder.png
new file mode 100644
index 00000000000..07b6aa21f7f
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/files/ui/white_folder.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/arrow_left.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/arrow_left.png
new file mode 100644
index 00000000000..6e4fb6659a7
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/arrow_left.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/arrow_right.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/arrow_right.png
new file mode 100644
index 00000000000..22a6c739432
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/arrow_right.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/back_to_files.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/back_to_files.png
new file mode 100644
index 00000000000..e04d9d317c2
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/back_to_files.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/bubble_point.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/bubble_point.png
new file mode 100644
index 00000000000..161e4c06be4
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/bubble_point.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/butterbar_close_button.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/butterbar_close_button.png
new file mode 100644
index 00000000000..aa6c816938d
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/butterbar_close_button.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/cursor_crop.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/cursor_crop.png
new file mode 100644
index 00000000000..6202fa978a8
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/cursor_crop.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/cursor_leftright.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/cursor_leftright.png
new file mode 100644
index 00000000000..a7ee09c632e
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/cursor_leftright.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/cursor_move.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/cursor_move.png
new file mode 100644
index 00000000000..faa3c8ac398
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/cursor_move.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/cursor_nwse.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/cursor_nwse.png
new file mode 100644
index 00000000000..0cd6399da7e
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/cursor_nwse.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/cursor_swne.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/cursor_swne.png
new file mode 100644
index 00000000000..04d9dc02702
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/cursor_swne.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/cursor_updown.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/cursor_updown.png
new file mode 100644
index 00000000000..1e9adfb5165
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/cursor_updown.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_1up.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_1up.png
new file mode 100644
index 00000000000..58cbc28c4cb
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_1up.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_1up_selected.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_1up_selected.png
new file mode 100644
index 00000000000..a0ca726fa0d
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_1up_selected.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_autofix.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_autofix.png
new file mode 100644
index 00000000000..8ce4917d4d2
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_autofix.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_autofix_selected.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_autofix_selected.png
new file mode 100644
index 00000000000..8838d5ce5fd
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_autofix_selected.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_brightness.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_brightness.png
new file mode 100644
index 00000000000..ffcd3852947
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_brightness.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_brightness_selected.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_brightness_selected.png
new file mode 100644
index 00000000000..90bf03bbd82
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_brightness_selected.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_contrast.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_contrast.png
new file mode 100644
index 00000000000..eec931d56de
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_contrast.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_crop.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_crop.png
new file mode 100644
index 00000000000..7c12fb54231
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_crop.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_crop_selected.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_crop_selected.png
new file mode 100644
index 00000000000..bb2e9e6cc0f
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_crop_selected.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_delete.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_delete.png
new file mode 100644
index 00000000000..a55ac6c8086
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_delete.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_delete_selected.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_delete_selected.png
new file mode 100644
index 00000000000..af54168a2e6
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_delete_selected.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_edit.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_edit.png
new file mode 100644
index 00000000000..288bc5b7e47
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_edit.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_edit_selected.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_edit_selected.png
new file mode 100644
index 00000000000..bcf993396dc
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_edit_selected.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_mosaic.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_mosaic.png
new file mode 100644
index 00000000000..3e1a621b559
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_mosaic.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_mosaic_selected.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_mosaic_selected.png
new file mode 100644
index 00000000000..d9e329d9070
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_mosaic_selected.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_print.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_print.png
new file mode 100644
index 00000000000..b5a9be0c6ba
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_print.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_print_selected.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_print_selected.png
new file mode 100644
index 00000000000..048a34172ec
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_print_selected.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_redo.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_redo.png
new file mode 100644
index 00000000000..075275da898
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_redo.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_redo_selected.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_redo_selected.png
new file mode 100644
index 00000000000..beed584ffe3
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_redo_selected.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_rotate.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_rotate.png
new file mode 100644
index 00000000000..db2c0b09f9f
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_rotate.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_rotate_left.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_rotate_left.png
new file mode 100644
index 00000000000..da57329d898
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_rotate_left.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_rotate_left_selected.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_rotate_left_selected.png
new file mode 100644
index 00000000000..d1b00a7e732
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_rotate_left_selected.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_rotate_selected.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_rotate_selected.png
new file mode 100644
index 00000000000..b3a9bf60a28
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_rotate_selected.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_share.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_share.png
new file mode 100644
index 00000000000..b1da6d96d47
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_share.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_share_selected.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_share_selected.png
new file mode 100644
index 00000000000..b3cd00f1118
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_share_selected.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_slideshow.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_slideshow.png
new file mode 100644
index 00000000000..fec87c0ae9a
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_slideshow.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_slideshow_selected.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_slideshow_selected.png
new file mode 100644
index 00000000000..4e1ed5aabfa
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_slideshow_selected.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_undo.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_undo.png
new file mode 100644
index 00000000000..c51fd6258ec
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_undo.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_undo_selected.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_undo_selected.png
new file mode 100644
index 00000000000..92d3a01d8e1
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/icon_undo_selected.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/slider_thumb.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/slider_thumb.png
new file mode 100644
index 00000000000..e100da62618
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/slider_thumb.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/slideshow-end.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/slideshow-end.png
new file mode 100644
index 00000000000..5e19f8d002b
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/slideshow-end.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/slideshow-pause.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/slideshow-pause.png
new file mode 100644
index 00000000000..13457d999fa
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/slideshow-pause.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/slideshow-play.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/slideshow-play.png
new file mode 100644
index 00000000000..aa9bd88fc8d
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/2x/slideshow-play.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/arrow_left.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/arrow_left.png
new file mode 100644
index 00000000000..85e687a1168
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/arrow_left.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/arrow_right.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/arrow_right.png
new file mode 100644
index 00000000000..0361556bd20
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/arrow_right.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/back_to_files.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/back_to_files.png
new file mode 100644
index 00000000000..96e420a657f
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/back_to_files.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/bubble_point.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/bubble_point.png
new file mode 100644
index 00000000000..a4a44e0cf09
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/bubble_point.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/butterbar_close_button.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/butterbar_close_button.png
new file mode 100644
index 00000000000..3c65c230151
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/butterbar_close_button.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/cursor_crop.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/cursor_crop.png
new file mode 100644
index 00000000000..6084188e0a7
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/cursor_crop.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/cursor_leftright.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/cursor_leftright.png
new file mode 100644
index 00000000000..30eeb03accf
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/cursor_leftright.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/cursor_move.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/cursor_move.png
new file mode 100644
index 00000000000..c5026d1b419
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/cursor_move.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/cursor_nwse.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/cursor_nwse.png
new file mode 100644
index 00000000000..87fb564ba66
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/cursor_nwse.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/cursor_swne.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/cursor_swne.png
new file mode 100644
index 00000000000..5e34475c863
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/cursor_swne.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/cursor_updown.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/cursor_updown.png
new file mode 100644
index 00000000000..f3a422477c8
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/cursor_updown.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_1up.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_1up.png
new file mode 100644
index 00000000000..546e87ae49f
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_1up.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_1up_selected.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_1up_selected.png
new file mode 100644
index 00000000000..a3043a8a05a
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_1up_selected.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_autofix.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_autofix.png
new file mode 100644
index 00000000000..0fb5b827d76
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_autofix.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_autofix_selected.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_autofix_selected.png
new file mode 100644
index 00000000000..fb5972d4d9d
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_autofix_selected.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_brightness.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_brightness.png
new file mode 100644
index 00000000000..ec9c114d245
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_brightness.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_brightness_selected.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_brightness_selected.png
new file mode 100644
index 00000000000..88ee72290aa
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_brightness_selected.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_contrast.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_contrast.png
new file mode 100644
index 00000000000..0188d483448
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_contrast.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_crop.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_crop.png
new file mode 100644
index 00000000000..efff5ba58ff
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_crop.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_crop_selected.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_crop_selected.png
new file mode 100644
index 00000000000..18b8317cbac
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_crop_selected.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_delete.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_delete.png
new file mode 100644
index 00000000000..efb132aa722
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_delete.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_delete_selected.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_delete_selected.png
new file mode 100644
index 00000000000..f2f88d8d0f9
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_delete_selected.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_edit.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_edit.png
new file mode 100644
index 00000000000..fc72ecf3b60
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_edit.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_edit_selected.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_edit_selected.png
new file mode 100644
index 00000000000..61540b5b467
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_edit_selected.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_mosaic.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_mosaic.png
new file mode 100644
index 00000000000..6e49d3c929a
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_mosaic.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_mosaic_selected.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_mosaic_selected.png
new file mode 100644
index 00000000000..86edb6e1ec0
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_mosaic_selected.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_print.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_print.png
new file mode 100644
index 00000000000..b2355367c25
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_print.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_print_selected.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_print_selected.png
new file mode 100644
index 00000000000..657b9c8e290
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_print_selected.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_redo.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_redo.png
new file mode 100644
index 00000000000..7b4703b6188
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_redo.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_redo_selected.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_redo_selected.png
new file mode 100644
index 00000000000..0022a136d09
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_redo_selected.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_rotate.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_rotate.png
new file mode 100644
index 00000000000..c60f258f6be
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_rotate.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_rotate_left.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_rotate_left.png
new file mode 100644
index 00000000000..ef2f21f4356
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_rotate_left.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_rotate_left_selected.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_rotate_left_selected.png
new file mode 100644
index 00000000000..1e4c1d6d62f
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_rotate_left_selected.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_rotate_selected.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_rotate_selected.png
new file mode 100644
index 00000000000..445350fcaf6
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_rotate_selected.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_share.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_share.png
new file mode 100644
index 00000000000..36bb2218a82
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_share.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_share_selected.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_share_selected.png
new file mode 100644
index 00000000000..438e8a2599a
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_share_selected.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_slideshow.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_slideshow.png
new file mode 100644
index 00000000000..72763d44ed1
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_slideshow.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_slideshow_selected.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_slideshow_selected.png
new file mode 100644
index 00000000000..4f80a48d403
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_slideshow_selected.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_undo.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_undo.png
new file mode 100644
index 00000000000..79e3fddf26b
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_undo.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_undo_selected.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_undo_selected.png
new file mode 100644
index 00000000000..d5d13a71a9f
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/icon_undo_selected.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/slider_thumb.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/slider_thumb.png
new file mode 100644
index 00000000000..cb2d712068f
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/slider_thumb.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/slideshow-end.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/slideshow-end.png
new file mode 100644
index 00000000000..f4371115363
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/slideshow-end.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/slideshow-pause.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/slideshow-pause.png
new file mode 100644
index 00000000000..2170ce90cfc
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/slideshow-pause.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/slideshow-play.png b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/slideshow-play.png
new file mode 100644
index 00000000000..f949121d245
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/gallery/slideshow-play.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/audio_player.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/audio_player.png
new file mode 100644
index 00000000000..ce5fa1ab7a9
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/audio_player.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/drive.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/drive.png
new file mode 100644
index 00000000000..b2cef7191e7
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/drive.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/error.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/error.png
new file mode 100644
index 00000000000..200baf571a3
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/error.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_close.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_close.png
new file mode 100644
index 00000000000..2e19603f5cd
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_close.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_collapse.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_collapse.png
new file mode 100644
index 00000000000..a9400612019
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_collapse.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_expand.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_expand.png
new file mode 100644
index 00000000000..20c234003fd
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_expand.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_fullscreen.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_fullscreen.png
new file mode 100644
index 00000000000..090c8257ae6
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_fullscreen.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_fullscreen_disabled.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_fullscreen_disabled.png
new file mode 100644
index 00000000000..96b3f635a83
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_fullscreen_disabled.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_fullscreen_down.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_fullscreen_down.png
new file mode 100644
index 00000000000..14cbeecaa87
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_fullscreen_down.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_fullscreen_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_fullscreen_hover.png
new file mode 100644
index 00000000000..25daa4fb6e6
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_fullscreen_hover.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_loop.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_loop.png
new file mode 100644
index 00000000000..6bea029799a
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_loop.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_loop_down.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_loop_down.png
new file mode 100644
index 00000000000..0e4b6e26d00
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_loop_down.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_loop_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_loop_hover.png
new file mode 100644
index 00000000000..b3a6c144a3e
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_loop_hover.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_next.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_next.png
new file mode 100644
index 00000000000..012af925e4a
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_next.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_next_down.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_next_down.png
new file mode 100644
index 00000000000..58d5bf724e5
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_next_down.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_next_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_next_hover.png
new file mode 100644
index 00000000000..d8d199575dc
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_next_hover.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_pause.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_pause.png
new file mode 100644
index 00000000000..acebff9d4a0
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_pause.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_pause_audio.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_pause_audio.png
new file mode 100644
index 00000000000..7489a7ed43a
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_pause_audio.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_pause_audio_down.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_pause_audio_down.png
new file mode 100644
index 00000000000..f9242360d7d
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_pause_audio_down.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_pause_audio_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_pause_audio_hover.png
new file mode 100644
index 00000000000..160d3d11145
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_pause_audio_hover.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_pause_down.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_pause_down.png
new file mode 100644
index 00000000000..9caaf7a0e19
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_pause_down.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_pause_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_pause_hover.png
new file mode 100644
index 00000000000..beb86f2e89a
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_pause_hover.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_play.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_play.png
new file mode 100644
index 00000000000..28e8a25c032
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_play.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_play_audio.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_play_audio.png
new file mode 100644
index 00000000000..16f663260c9
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_play_audio.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_play_audio_down.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_play_audio_down.png
new file mode 100644
index 00000000000..01dcc462306
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_play_audio_down.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_play_audio_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_play_audio_hover.png
new file mode 100644
index 00000000000..c64070f4a24
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_play_audio_hover.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_play_disabled.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_play_disabled.png
new file mode 100644
index 00000000000..77ea7c01091
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_play_disabled.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_play_down.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_play_down.png
new file mode 100644
index 00000000000..7c68787119f
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_play_down.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_play_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_play_hover.png
new file mode 100644
index 00000000000..28ca18b0551
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_play_hover.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_previous.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_previous.png
new file mode 100644
index 00000000000..399e5a00e43
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_previous.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_previous_down.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_previous_down.png
new file mode 100644
index 00000000000..94741583314
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_previous_down.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_previous_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_previous_hover.png
new file mode 100644
index 00000000000..0a8a919c330
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_previous_hover.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_slider_thumb.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_slider_thumb.png
new file mode 100644
index 00000000000..cd20aa2967f
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_slider_thumb.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_slider_thumb_down.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_slider_thumb_down.png
new file mode 100644
index 00000000000..931850f3f52
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_slider_thumb_down.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_slider_thumb_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_slider_thumb_hover.png
new file mode 100644
index 00000000000..9fe30ef7442
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_slider_thumb_hover.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_disabled.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_disabled.png
new file mode 100644
index 00000000000..acbaa71be9b
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_disabled.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_disabled_down.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_disabled_down.png
new file mode 100644
index 00000000000..f594f47e774
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_disabled_down.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_disabled_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_disabled_hover.png
new file mode 100644
index 00000000000..f4cc7b350f5
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_disabled_hover.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_full.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_full.png
new file mode 100644
index 00000000000..a093e9f6cd0
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_full.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_full_disabled.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_full_disabled.png
new file mode 100644
index 00000000000..56635f1d9b7
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_full_disabled.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_full_down.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_full_down.png
new file mode 100644
index 00000000000..7d2a2395049
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_full_down.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_full_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_full_hover.png
new file mode 100644
index 00000000000..f183edd7362
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_full_hover.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_level1.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_level1.png
new file mode 100644
index 00000000000..25904c0d176
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_level1.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_level1_down.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_level1_down.png
new file mode 100644
index 00000000000..0ba2886fbfb
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_level1_down.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_level1_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_level1_hover.png
new file mode 100644
index 00000000000..be56fc17e6a
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_level1_hover.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_level2.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_level2.png
new file mode 100644
index 00000000000..24f7ea805c1
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_level2.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_level2_down.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_level2_down.png
new file mode 100644
index 00000000000..34fce547aa9
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_level2_down.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_level2_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_level2_hover.png
new file mode 100644
index 00000000000..2e3f2e80cd5
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_sound_level2_hover.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_volume_slider_thumb.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_volume_slider_thumb.png
new file mode 100644
index 00000000000..d5cf0c56e52
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_volume_slider_thumb.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_volume_slider_thumb_down.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_volume_slider_thumb_down.png
new file mode 100644
index 00000000000..034c7b807c3
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_volume_slider_thumb_down.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_volume_slider_thumb_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_volume_slider_thumb_hover.png
new file mode 100644
index 00000000000..4d4243d0f0c
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/media_volume_slider_thumb_hover.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/watch.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/watch.png
new file mode 100644
index 00000000000..f47751be542
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/2x/watch.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/audio_player.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/audio_player.png
new file mode 100644
index 00000000000..6921db2f33d
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/audio_player.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/drive.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/drive.png
new file mode 100644
index 00000000000..8f4b64ee8bd
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/drive.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/error.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/error.png
new file mode 100644
index 00000000000..125c0b75899
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/error.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_close.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_close.png
new file mode 100644
index 00000000000..5980ea7504a
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_close.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_collapse.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_collapse.png
new file mode 100644
index 00000000000..3ab1add2a9c
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_collapse.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_expand.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_expand.png
new file mode 100644
index 00000000000..a228fbdf275
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_expand.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_fullscreen.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_fullscreen.png
new file mode 100644
index 00000000000..24f2ecf2f1d
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_fullscreen.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_fullscreen_disabled.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_fullscreen_disabled.png
new file mode 100644
index 00000000000..bf74390888c
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_fullscreen_disabled.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_fullscreen_down.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_fullscreen_down.png
new file mode 100644
index 00000000000..dd318cec7d6
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_fullscreen_down.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_fullscreen_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_fullscreen_hover.png
new file mode 100644
index 00000000000..42327397887
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_fullscreen_hover.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_loop.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_loop.png
new file mode 100644
index 00000000000..f3e35dd9d4a
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_loop.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_loop_down.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_loop_down.png
new file mode 100644
index 00000000000..dda8af53979
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_loop_down.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_loop_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_loop_hover.png
new file mode 100644
index 00000000000..a0ed21fccb8
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_loop_hover.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_next.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_next.png
new file mode 100644
index 00000000000..aec4ee676a8
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_next.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_next_down.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_next_down.png
new file mode 100644
index 00000000000..53cc1062da0
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_next_down.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_next_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_next_hover.png
new file mode 100644
index 00000000000..bea50b59347
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_next_hover.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_pause.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_pause.png
new file mode 100644
index 00000000000..0a304e4bd04
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_pause.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_pause_audio.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_pause_audio.png
new file mode 100644
index 00000000000..8e2e87be5c4
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_pause_audio.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_pause_audio_down.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_pause_audio_down.png
new file mode 100644
index 00000000000..11789840f5b
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_pause_audio_down.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_pause_audio_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_pause_audio_hover.png
new file mode 100644
index 00000000000..52b6bbcbf37
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_pause_audio_hover.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_pause_down.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_pause_down.png
new file mode 100644
index 00000000000..6e65195cbea
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_pause_down.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_pause_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_pause_hover.png
new file mode 100644
index 00000000000..993ee50a4f3
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_pause_hover.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_play.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_play.png
new file mode 100644
index 00000000000..47bcdc29b17
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_play.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_play_audio.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_play_audio.png
new file mode 100644
index 00000000000..eceaa5bb82b
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_play_audio.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_play_audio_down.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_play_audio_down.png
new file mode 100644
index 00000000000..98ebeeaa120
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_play_audio_down.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_play_audio_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_play_audio_hover.png
new file mode 100644
index 00000000000..f9d97d7714a
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_play_audio_hover.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_play_disabled.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_play_disabled.png
new file mode 100644
index 00000000000..6e96d4c9c67
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_play_disabled.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_play_down.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_play_down.png
new file mode 100644
index 00000000000..1759ec39afa
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_play_down.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_play_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_play_hover.png
new file mode 100644
index 00000000000..3942d462656
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_play_hover.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_previous.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_previous.png
new file mode 100644
index 00000000000..4fded35ad47
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_previous.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_previous_down.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_previous_down.png
new file mode 100644
index 00000000000..2c63184167e
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_previous_down.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_previous_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_previous_hover.png
new file mode 100644
index 00000000000..45ea61b3b59
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_previous_hover.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_slider_thumb.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_slider_thumb.png
new file mode 100644
index 00000000000..e55b2c25128
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_slider_thumb.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_slider_thumb_down.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_slider_thumb_down.png
new file mode 100644
index 00000000000..f0b2be7d653
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_slider_thumb_down.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_slider_thumb_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_slider_thumb_hover.png
new file mode 100644
index 00000000000..e216ae6ffbd
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_slider_thumb_hover.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_disabled.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_disabled.png
new file mode 100644
index 00000000000..42126de9330
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_disabled.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_disabled_down.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_disabled_down.png
new file mode 100644
index 00000000000..2b494b93652
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_disabled_down.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_disabled_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_disabled_hover.png
new file mode 100644
index 00000000000..5040f8078d3
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_disabled_hover.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_full.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_full.png
new file mode 100644
index 00000000000..4a034029d5c
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_full.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_full_disabled.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_full_disabled.png
new file mode 100644
index 00000000000..cef4bc4c6d0
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_full_disabled.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_full_down.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_full_down.png
new file mode 100644
index 00000000000..55d77e4d496
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_full_down.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_full_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_full_hover.png
new file mode 100644
index 00000000000..881e84305fb
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_full_hover.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_level1.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_level1.png
new file mode 100644
index 00000000000..2f7ceea32e4
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_level1.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_level1_down.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_level1_down.png
new file mode 100644
index 00000000000..9777c9b5a0b
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_level1_down.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_level1_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_level1_hover.png
new file mode 100644
index 00000000000..fdf3bc1331b
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_level1_hover.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_level2.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_level2.png
new file mode 100644
index 00000000000..9379a038ba9
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_level2.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_level2_down.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_level2_down.png
new file mode 100644
index 00000000000..422b43518be
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_level2_down.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_level2_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_level2_hover.png
new file mode 100644
index 00000000000..8bf6157edf4
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_sound_level2_hover.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_volume_slider_thumb.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_volume_slider_thumb.png
new file mode 100644
index 00000000000..a1bf0577905
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_volume_slider_thumb.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_volume_slider_thumb_down.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_volume_slider_thumb_down.png
new file mode 100644
index 00000000000..a4560f0ffc2
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_volume_slider_thumb_down.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_volume_slider_thumb_hover.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_volume_slider_thumb_hover.png
new file mode 100644
index 00000000000..159e2e71f3e
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/media_volume_slider_thumb_hover.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/media/watch.png b/chromium/chrome/browser/resources/file_manager/foreground/images/media/watch.png
new file mode 100644
index 00000000000..92b628497fb
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/media/watch.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/archive.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/archive.png
new file mode 100644
index 00000000000..b5821d4da5b
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/archive.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_archive.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_archive.png
new file mode 100644
index 00000000000..6927bdd188e
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_archive.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_downloads.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_downloads.png
new file mode 100644
index 00000000000..ab37f07104c
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_downloads.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_drive.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_drive.png
new file mode 100644
index 00000000000..a17a0b19330
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_drive.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_hdd.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_hdd.png
new file mode 100644
index 00000000000..e0ec6965c2a
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_hdd.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_offline.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_offline.png
new file mode 100644
index 00000000000..87029a9355c
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_offline.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_optical.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_optical.png
new file mode 100644
index 00000000000..5077474e3b1
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_optical.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_phone.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_phone.png
new file mode 100644
index 00000000000..c4521dec634
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_phone.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_recent.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_recent.png
new file mode 100644
index 00000000000..e1b86212136
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_recent.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_sd.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_sd.png
new file mode 100644
index 00000000000..5679089adcf
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_sd.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_shared.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_shared.png
new file mode 100644
index 00000000000..ec7c3e4779a
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_shared.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_trash.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_trash.png
new file mode 100644
index 00000000000..67f261f460a
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_trash.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_usb.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_usb.png
new file mode 100644
index 00000000000..2cceca66e9c
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/black_usb.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/device_hd.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/device_hd.png
new file mode 100644
index 00000000000..e0ec6965c2a
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/device_hd.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/device_optical.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/device_optical.png
new file mode 100644
index 00000000000..5077474e3b1
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/device_optical.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/device_sd.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/device_sd.png
new file mode 100644
index 00000000000..5679089adcf
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/device_sd.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/device_sd_large.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/device_sd_large.png
new file mode 100644
index 00000000000..6fa0c756dce
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/device_sd_large.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/device_usb.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/device_usb.png
new file mode 100644
index 00000000000..70e9686f3a2
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/device_usb.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/device_usb_large.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/device_usb_large.png
new file mode 100644
index 00000000000..17a8fee01f7
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/device_usb_large.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/downloads.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/downloads.png
new file mode 100644
index 00000000000..22e66179190
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/downloads.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/drive.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/drive.png
new file mode 100644
index 00000000000..a17a0b19330
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/drive.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/drive_offline.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/drive_offline.png
new file mode 100644
index 00000000000..75981854cd6
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/drive_offline.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/drive_recent.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/drive_recent.png
new file mode 100644
index 00000000000..756e36a2e38
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/drive_recent.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/drive_shared.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/drive_shared.png
new file mode 100644
index 00000000000..c2eb5dafb30
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/drive_shared.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/folder.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/folder.png
new file mode 100644
index 00000000000..01ac4929616
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/folder.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_archive.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_archive.png
new file mode 100644
index 00000000000..481c40b89f1
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_archive.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_downloads.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_downloads.png
new file mode 100644
index 00000000000..5262bb910a8
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_downloads.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_drive.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_drive.png
new file mode 100644
index 00000000000..5f43989d923
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_drive.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_hdd.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_hdd.png
new file mode 100644
index 00000000000..bc5e8c417d9
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_hdd.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_offline.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_offline.png
new file mode 100644
index 00000000000..9d65b392094
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_offline.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_optical.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_optical.png
new file mode 100644
index 00000000000..c67537714dd
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_optical.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_phone.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_phone.png
new file mode 100644
index 00000000000..2dfb8f27d61
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_phone.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_recent.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_recent.png
new file mode 100644
index 00000000000..a80eae2c8b3
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_recent.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_sd.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_sd.png
new file mode 100644
index 00000000000..282de9aa9af
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_sd.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_shared.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_shared.png
new file mode 100644
index 00000000000..24ea1676b15
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_shared.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_trash.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_trash.png
new file mode 100644
index 00000000000..f6c5ccb03f4
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_trash.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_usb.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_usb.png
new file mode 100644
index 00000000000..f296f4e0245
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/2x/white_usb.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_archive.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_archive.png
new file mode 100644
index 00000000000..e4514206fbd
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_archive.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_downloads.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_downloads.png
new file mode 100644
index 00000000000..e25c8756b32
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_downloads.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_drive.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_drive.png
new file mode 100644
index 00000000000..891ec480c47
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_drive.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_folder.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_folder.png
new file mode 100644
index 00000000000..539f67759a5
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_folder.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_hdd.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_hdd.png
new file mode 100644
index 00000000000..53c02b4c48c
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_hdd.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_offline.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_offline.png
new file mode 100644
index 00000000000..a07ef0cb483
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_offline.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_optical.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_optical.png
new file mode 100644
index 00000000000..30a905fbc9c
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_optical.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_phone.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_phone.png
new file mode 100644
index 00000000000..aa3aa9deb7c
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_phone.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_recent.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_recent.png
new file mode 100644
index 00000000000..6e5d3636bff
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_recent.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_sd.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_sd.png
new file mode 100644
index 00000000000..f24b4cefb74
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_sd.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_shared.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_shared.png
new file mode 100644
index 00000000000..10a1553b9b0
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_shared.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_trash.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_trash.png
new file mode 100644
index 00000000000..7708ae555a6
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_trash.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_usb.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_usb.png
new file mode 100644
index 00000000000..ba7ef472471
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/black_usb.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/device_sd_large.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/device_sd_large.png
new file mode 100644
index 00000000000..39d2fe90a49
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/device_sd_large.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/device_usb_large.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/device_usb_large.png
new file mode 100644
index 00000000000..6b68508f7d8
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/device_usb_large.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_archive.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_archive.png
new file mode 100644
index 00000000000..3b542deac45
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_archive.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_downloads.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_downloads.png
new file mode 100644
index 00000000000..8b1cfdf7400
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_downloads.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_drive.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_drive.png
new file mode 100644
index 00000000000..0226fbd14ed
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_drive.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_folder.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_folder.png
new file mode 100644
index 00000000000..07b6aa21f7f
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_folder.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_hdd.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_hdd.png
new file mode 100644
index 00000000000..b7c4a485c94
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_hdd.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_offline.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_offline.png
new file mode 100644
index 00000000000..503ed67f82f
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_offline.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_optical.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_optical.png
new file mode 100644
index 00000000000..3f13f723af4
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_optical.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_phone.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_phone.png
new file mode 100644
index 00000000000..d187ef0951b
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_phone.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_recent.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_recent.png
new file mode 100644
index 00000000000..23c526ad8b2
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_recent.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_sd.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_sd.png
new file mode 100644
index 00000000000..f7f465c430b
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_sd.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_shared.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_shared.png
new file mode 100644
index 00000000000..051191e6de1
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_shared.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_trash.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_trash.png
new file mode 100644
index 00000000000..f95d0215ee4
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_trash.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_usb.png b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_usb.png
new file mode 100644
index 00000000000..954a3c6dae3
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/images/volumes/white_usb.png
Binary files differ
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/app_installer.js b/chromium/chrome/browser/resources/file_manager/foreground/js/app_installer.js
new file mode 100644
index 00000000000..7914d6e458f
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/app_installer.js
@@ -0,0 +1,79 @@
+// Copyright 2013 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.
+
+'use strict';
+
+/**
+ * Manage the installation of apps.
+ *
+ * @param {string} itemId Item id to be installed.
+ * @constructor
+ * @extends {cr.EventType}
+ */
+function AppInstaller(itemId) {
+ this.itemId_ = itemId;
+ this.callback_ = null;
+
+ Object.seal(this);
+}
+
+AppInstaller.prototype = {
+};
+
+/**
+ * Type of result.
+ *
+ * @enum {string}
+ * @const
+ */
+AppInstaller.Result = {
+ SUCCESS: 'AppInstaller.success',
+ CANCELLED: 'AppInstaller.cancelled',
+ ERROR: 'AppInstaller.error'
+};
+Object.freeze(AppInstaller.Result);
+
+/**
+ * Error message for user cancellation. This must be match with the constant
+ * 'kUserCancelledError' in C/B/extensions/webstore_standalone_installer.cc.
+ * @type {string}
+ * @const
+ * @private
+ */
+AppInstaller.USER_CANCELLED_ERROR_STR_ = 'User cancelled install';
+
+/**
+ * Start an installation.
+ * @param {function(boolean, string)} callback Called when the installation is
+ * finished.
+ */
+AppInstaller.prototype.install = function(callback) {
+ this.callback_ = callback;
+ chrome.fileBrowserPrivate.installWebstoreItem(
+ this.itemId_,
+ function() {
+ this.onInstallCompleted_(chrome.runtime.lastError);
+ }.bind(this));
+};
+
+/**
+ * Called when the installation is completed.
+ *
+ * @param {{message: string}?} error Null if the installation is success,
+ * otherwise an object which contains error message.
+ * @private
+ */
+AppInstaller.prototype.onInstallCompleted_ = function(error) {
+ var installerResult = AppInstaller.Result.SUCCESS;
+ var errorMessage = '';
+ if (error) {
+ installerResult =
+ error.message == AppInstaller.USER_CANCELLED_ERROR_STR_ ?
+ AppInstaller.Result.CANCELLED :
+ AppInstaller.Result.ERROR;
+ errorMessage = error.message;
+ }
+ this.callback_(installerResult, errorMessage);
+ this.callback_ = null;
+};
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/combobutton.js b/chromium/chrome/browser/resources/file_manager/foreground/js/combobutton.js
new file mode 100644
index 00000000000..59ae9f2b6ed
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/combobutton.js
@@ -0,0 +1,154 @@
+// 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.
+
+'use strict';
+
+/**
+ * @fileoverview This implements a combobutton control.
+ */
+
+cr.define('cr.ui', function() {
+ /**
+ * Creates a new combobutton element.
+ * @param {Object=} opt_propertyBag Optional properties.
+ * @constructor
+ * @extends {HTMLUListElement}
+ */
+ var ComboButton = cr.ui.define(cr.ui.MenuButton);
+
+
+ ComboButton.prototype = {
+ __proto__: cr.ui.MenuButton.prototype,
+
+ defaultItem_: null,
+
+ /**
+ * Truncates drop-down list.
+ */
+ clear: function() {
+ this.menu.clear();
+ this.multiple = false;
+ },
+
+ addDropDownItem: function(item) {
+ this.multiple = true;
+ var menuitem = this.menu.addMenuItem(item);
+ menuitem.data = item;
+ if (item.iconType) {
+ menuitem.style.backgroundImage = '';
+ menuitem.setAttribute('file-type-icon', item.iconType);
+ }
+ if (item.bold) {
+ menuitem.style.fontWeight = 'bold';
+ }
+ return menuitem;
+ },
+
+ /**
+ * Adds separator to drop-down list.
+ */
+ addSeparator: function() {
+ this.menu.addSeparator();
+ },
+
+ /**
+ * Default item to fire on combobox click
+ */
+ get defaultItem() {
+ return this.defaultItem_;
+ },
+ set defaultItem(defaultItem) {
+ this.defaultItem_ = defaultItem;
+
+ this.actionNode_.textContent = defaultItem.label || '';
+
+ if (defaultItem.iconType) {
+ this.actionNode_.style.backgroundImage = '';
+ this.actionNode_.setAttribute('file-type-icon', defaultItem.iconType);
+ } else if (defaultItem.iconUrl) {
+ this.actionNode_.style.backgroundImage =
+ 'url(' + defaultItem.iconUrl + ')';
+ } else {
+ this.actionNode_.style.backgroundImage = '';
+ }
+ },
+
+ /**
+ * Initializes the element.
+ */
+ decorate: function() {
+ cr.ui.MenuButton.prototype.decorate.call(this);
+
+ this.classList.add('combobutton');
+
+ this.actionNode_ = this.ownerDocument.createElement('div');
+ this.actionNode_.classList.add('action');
+ this.appendChild(this.actionNode_);
+
+ var triggerIcon = this.ownerDocument.createElement('span');
+ triggerIcon.className = 'disclosureindicator';
+ this.trigger_ = this.ownerDocument.createElement('div');
+ this.trigger_.classList.add('trigger');
+ this.trigger_.appendChild(triggerIcon);
+
+ this.appendChild(this.trigger_);
+
+ this.addEventListener('click', this.handleButtonClick_.bind(this));
+
+ this.trigger_.addEventListener('click',
+ this.handleTriggerClicked_.bind(this));
+
+ this.menu.addEventListener('activate',
+ this.handleMenuActivate_.bind(this));
+
+ // Remove mousedown event listener created by MenuButton::decorate,
+ // and move it down to trigger_.
+ this.removeEventListener('mousedown', this);
+ this.trigger_.addEventListener('mousedown', this);
+ },
+
+ /**
+ * Handles the keydown event for the menu button.
+ */
+ handleKeyDown: function(e) {
+ switch (e.keyIdentifier) {
+ case 'Down':
+ case 'Up':
+ if (!this.isMenuShown())
+ this.showMenu();
+ e.preventDefault();
+ break;
+ case 'Esc':
+ case 'U+001B': // Maybe this is remote desktop playing a prank?
+ this.hideMenu();
+ break;
+ }
+ },
+
+ handleTriggerClicked_: function(event) {
+ event.stopPropagation();
+ },
+
+ handleMenuActivate_: function(event) {
+ this.dispatchSelectEvent(event.target.data);
+ },
+
+ handleButtonClick_: function() {
+ this.dispatchSelectEvent(this.defaultItem_);
+ },
+
+ dispatchSelectEvent: function(item) {
+ var selectEvent = new Event('select');
+ selectEvent.item = item;
+ this.dispatchEvent(selectEvent);
+ }
+ };
+
+ cr.defineProperty(ComboButton, 'disabled', cr.PropertyKind.BOOL_ATTR);
+ cr.defineProperty(ComboButton, 'multiple', cr.PropertyKind.BOOL_ATTR);
+
+ return {
+ ComboButton: ComboButton
+ };
+});
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/commandbutton.js b/chromium/chrome/browser/resources/file_manager/foreground/js/commandbutton.js
new file mode 100644
index 00000000000..b6f2b916cd3
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/commandbutton.js
@@ -0,0 +1,136 @@
+// 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.
+
+'use strict';
+
+/**
+ * @fileoverview This implements a common button control, bound to command.
+ */
+
+/**
+ * Creates a new button element.
+ * @param {Object=} opt_propertyBag Optional properties.
+ * @constructor
+ * @extends {HTMLDivElement}
+ */
+ var CommandButton = cr.ui.define('button');
+
+/** @override */
+CommandButton.prototype.__proto__ = HTMLButtonElement.prototype;
+
+/**
+ * Associated command.
+ * @type {Command}
+ * @private
+ */
+CommandButton.prototype.command_ = null;
+
+/**
+ * Initializes the menu item.
+ */
+CommandButton.prototype.decorate = function() {
+ var commandId;
+ if ((commandId = this.getAttribute('command')))
+ this.setCommand(commandId);
+
+ this.addEventListener('click', this.handleClick_.bind(this));
+};
+
+/**
+ * Returns associated command.
+ * @return {cr.ui.Command} associated command.
+ */
+CommandButton.prototype.getCommand = function() {
+ return this.command_;
+};
+
+/**
+ * Associates command with this button.
+ * @param {string|cr.ui.Command} command Command id, or command object to
+ * associate with this button.
+ */
+CommandButton.prototype.setCommand = function(command) {
+ if (this.command_) {
+ this.command_.removeEventListener('labelChange', this);
+ this.command_.removeEventListener('disabledChange', this);
+ this.command_.removeEventListener('hiddenChange', this);
+ }
+
+ if (typeof command == 'string' && command[0] == '#') {
+ command = this.ownerDocument.getElementById(command.slice(1));
+ cr.ui.decorate(command, cr.ui.Command);
+ }
+
+ this.command_ = command;
+ if (command) {
+ if (command.id)
+ this.setAttribute('command', '#' + command.id);
+
+ this.setLabel(command.label);
+ this.disabled = command.disabled;
+ this.hidden = command.hidden;
+
+ this.command_.addEventListener('labelChange', this);
+ this.command_.addEventListener('disabledChange', this);
+ this.command_.addEventListener('hiddenChange', this);
+ }
+};
+
+/**
+ * Returns button label
+ * @return {string} Button label.
+ */
+CommandButton.prototype.getLabel = function() {
+ return this.textContent;
+};
+
+/**
+ * Sets button label.
+ * @param {string} label New button label.
+ */
+CommandButton.prototype.setLabel = function(label) {
+ this.textContent = label;
+};
+
+/**
+ * Handles click event and dispatches associated command.
+ * @param {Event} e The mouseup event object.
+ * @private
+ */
+CommandButton.prototype.handleClick_ = function(e) {
+ if (!this.disabled && this.command_)
+ this.command_.execute(this);
+};
+
+/**
+ * Handles changes to the associated command.
+ * @param {Event} e The event object.
+ */
+CommandButton.prototype.handleEvent = function(e) {
+ switch (e.type) {
+ case 'disabledChange':
+ this.disabled = this.command_.disabled;
+ break;
+ case 'hiddenChange':
+ this.hidden = this.command_.hidden;
+ break;
+ case 'labelChange':
+ this.setLabel(this.command_.label);
+ break;
+ }
+};
+
+/**
+ * Whether the button is disabled or not.
+ * @type {boolean}
+ */
+cr.defineProperty(CommandButton, 'disabled',
+ cr.PropertyKind.BOOL_ATTR);
+
+/**
+ * Whether the button is hidden or not.
+ * @type {boolean}
+ */
+cr.defineProperty(CommandButton, 'hidden',
+ cr.PropertyKind.BOOL_ATTR);
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/cws_container_client.js b/chromium/chrome/browser/resources/file_manager/foreground/js/cws_container_client.js
new file mode 100644
index 00000000000..fae6787a5ad
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/cws_container_client.js
@@ -0,0 +1,249 @@
+// Copyright 2013 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.
+
+'use strict';
+
+/**
+ * @param {WebView} webView Web View tag.
+ * @param {?string} ext File extension.
+ * @param {?string} mime File mime type.
+ * @param {?string} searchQuery Search query.
+ * @param {number} width Width of the CWS widget.
+ * @param {number} height Height of the CWS widget.
+ * @param {string} url Share Url for an entry.
+ * @param {string} target Target (scheme + host + port) of the widget.
+ * @constructor
+ */
+function CWSContainerClient(
+ webView, ext, mime, searchQuery, width, height, url, target) {
+ this.webView_ = webView;
+ this.ext_ = (ext && ext[0] == '.') ? ext.substr(1) : ext;
+ this.mime_ = mime;
+ this.searchQuery_ = searchQuery;
+ this.width_ = width;
+ this.height_ = height;
+ this.url_ = url;
+ this.target_ = target;
+
+ this.loaded_ = false;
+ this.loading_ = false;
+
+ this.onMessageBound_ = this.onMessage_.bind(this);
+ this.onLoadStopBound_ = this.onLoadStop_.bind(this);
+ this.onLoadAbortBound_ = this.onLoadAbort_.bind(this);
+}
+
+CWSContainerClient.prototype = {
+ __proto__: cr.EventTarget.prototype
+};
+
+/**
+ * Events CWSContainerClient fires
+ *
+ * @enum {string}
+ * @const
+ */
+CWSContainerClient.Events = {
+ LOADED: 'CWSContainerClient.Events.LOADED',
+ LOAD_FAILED: 'CWSContainerClient.Events.LOAD_FAILED',
+ REQUEST_INSTALL: 'CWSContainerClient.Events.REQUEST_INSTALL'
+};
+Object.freeze(CWSContainerClient.Events);
+
+/**
+ * Handles messages from the widget
+ * @param {Event} event Message event.
+ * @private
+ */
+CWSContainerClient.prototype.onMessage_ = function(event) {
+ if (event.origin != this.target_)
+ return;
+
+ var data = event.data;
+ switch (data['message']) {
+ case 'widget_loaded':
+ this.onWidgetLoaded_();
+ break;
+ case 'widget_load_failed':
+ this.onWidgetLoadFailed_();
+ break;
+ case 'before_install':
+ this.sendInstallRequest_(data['item_id']);
+ break;
+ default:
+ console.error('Unexpected message: ' + data['message'], data);
+ }
+};
+
+/**
+ * Called when receiving 'loadstop' event from the <wevview>.
+ * @param {Event} event Message event.
+ * @private
+ */
+CWSContainerClient.prototype.onLoadStop_ = function(event) {
+ if (this.url_ == this.webView_.src && !this.loaded_) {
+ this.loaded_ = true;
+ this.postInitializeMessage_();
+ }
+};
+
+/**
+ * Called when the widget is loaded successfully.
+ * @private
+ */
+CWSContainerClient.prototype.onWidgetLoaded_ = function() {
+ cr.dispatchSimpleEvent(this, CWSContainerClient.Events.LOADED);
+};
+
+/**
+ * Called when the widget is failed to load.
+ * @private
+ */
+CWSContainerClient.prototype.onWidgetLoadFailed_ = function() {
+ this.sendWidgetLoadFailed_();
+};
+
+/**
+ * Called when receiving the 'loadabort' event from <webview>.
+ * @param {Event} event Message event.
+ * @private
+ */
+CWSContainerClient.prototype.onLoadAbort_ = function(event) {
+ this.sendWidgetLoadFailed_();
+};
+
+/**
+ * Called when the installation is completed from the suggest-app dialog.
+ *
+ * @param {boolean} result True if the installation is success, false if failed.
+ * @param {string} itemId Item id to be installed.
+ */
+CWSContainerClient.prototype.onInstallCompleted = function(result, itemId) {
+ if (result)
+ this.postInstallSuccessMessage_(itemId);
+ else
+ this.postInstallFailureMessage_(itemId);
+};
+
+/**
+ * Send the fail message to the suggest-app dialog.
+ * @private
+ */
+CWSContainerClient.prototype.sendWidgetLoadFailed_ = function() {
+ cr.dispatchSimpleEvent(this, CWSContainerClient.Events.LOAD_FAILED);
+};
+
+/**
+ * Send the install request to the suggest-app dialog.
+ *
+ * @param {string} itemId Item id to be installed.
+ * @private
+ */
+CWSContainerClient.prototype.sendInstallRequest_ = function(itemId) {
+ var event = new Event(CWSContainerClient.Events.REQUEST_INSTALL);
+ event.itemId = itemId;
+ this.dispatchEvent(event);
+};
+
+/**
+ * Send the 'install_failure' message to the widget.
+ *
+ * @param {string} itemId Item id to be installed.
+ * @private
+ */
+CWSContainerClient.prototype.postInstallFailureMessage_ = function(itemId) {
+ var message = {
+ message: 'install_failure',
+ item_id: itemId,
+ v: 1
+ };
+
+ this.postMessage_(message);
+};
+
+/**
+ * Send the 'install_success' message to the widget.
+ *
+ * @param {string} itemId Item id to be installed.
+ * @private
+ */
+CWSContainerClient.prototype.postInstallSuccessMessage_ = function(itemId) {
+ var message = {
+ message: 'install_success',
+ item_id: itemId,
+ v: 1
+ };
+
+ this.postMessage_(message);
+};
+
+/**
+ * Send the 'initialize' message to the widget.
+ * @private
+ */
+CWSContainerClient.prototype.postInitializeMessage_ = function() {
+ var message = {
+ message: 'initialize',
+ hl: util.getCurrentLocaleOrDefault(),
+ widgth: this.width_,
+ height: this.height_,
+ v: 1
+ };
+
+ if (this.searchQuery_) {
+ message['search_query'] = this.searchQuery_;
+ } else {
+ message['file_extension'] = this.ext_;
+ message['mime_type'] = this.mime_;
+ }
+
+ this.postMessage_(message);
+};
+
+/**
+ * Send a message to the widget. This method shouldn't be called directly,
+ * should from more specified posting function (eg. postXyzMessage_()).
+ *
+ * @param {object} message Message object to be posted.
+ * @private
+ */
+CWSContainerClient.prototype.postMessage_ = function(message) {
+ if (!this.webView_.contentWindow)
+ return;
+
+ this.webView_.contentWindow.postMessage(message, this.target_);
+};
+
+/**
+ * Loads the page to <webview>. Can be called only once.
+ */
+CWSContainerClient.prototype.load = function() {
+ if (this.loading_ || this.loaded_)
+ throw new Error('Already loaded.');
+ this.loading_ = true;
+ this.loaded_ = false;
+
+ window.addEventListener('message', this.onMessageBound_);
+ this.webView_.addEventListener('loadstop', this.onLoadStopBound_);
+ this.webView_.addEventListener('loadabort', this.onLoadAbortBound_);
+ this.webView_.setAttribute('src', this.url_);
+};
+
+/**
+ * Aborts loading of the embedded dialog and performs cleanup.
+ */
+CWSContainerClient.prototype.abort = function() {
+ window.removeEventListener('message', this.onMessageBound_);
+ this.webView_.removeEventListener('loadstop', this.onLoadStopBound_);
+ this.webView_.removeEventListener(
+ 'loadabort', this.onLoadAbortBound_);
+ this.webView_.stop();
+};
+
+/**
+ * Cleans the dialog by removing all handlers.
+ */
+CWSContainerClient.prototype.dispose = function() {
+ this.abort();
+};
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/default_action_dialog.js b/chromium/chrome/browser/resources/file_manager/foreground/js/default_action_dialog.js
new file mode 100644
index 00000000000..a5c8290a239
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/default_action_dialog.js
@@ -0,0 +1,157 @@
+// 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.
+
+'use strict';
+
+
+/**
+ * DefaultActionDialog contains a message, a list box, an ok button, and a
+ * cancel button.
+ * This dialog should be used as action picker for file operations.
+ */
+cr.define('cr.filebrowser', function() {
+
+ /**
+ * Creates dialog in DOM tree.
+ *
+ * @param {HTMLElement} parentNode Node to be parent for this dialog.
+ * @constructor
+ * @extends {FileManagerDialogBase}
+ */
+ function DefaultActionDialog(parentNode) {
+ FileManagerDialogBase.call(this, parentNode);
+
+ this.frame_.id = 'default-action-dialog';
+
+ this.list_ = new cr.ui.List();
+ this.list_.id = 'default-actions-list';
+ this.frame_.insertBefore(this.list_, this.text_.nextSibling);
+
+ this.selectionModel_ = this.list_.selectionModel =
+ new cr.ui.ListSingleSelectionModel();
+ this.dataModel_ = this.list_.dataModel = new cr.ui.ArrayDataModel([]);
+
+ // List has max-height defined at css, so that list grows automatically,
+ // but doesn't exceed predefined size.
+ this.list_.autoExpands = true;
+ this.list_.activateItemAtIndex = this.activateItemAtIndex_.bind(this);
+
+ this.initialFocusElement_ = this.list_;
+
+ var self = this;
+
+ // Binding stuff doesn't work with constructors, so we have to create
+ // closure here.
+ this.list_.itemConstructor = function(item) {
+ return self.renderItem(item);
+ };
+ }
+
+ DefaultActionDialog.prototype = {
+ __proto__: FileManagerDialogBase.prototype
+ };
+
+ /**
+ * @override
+ */
+ DefaultActionDialog.prototype.onInputFocus = function() {
+ this.list_.select();
+ };
+
+ /**
+ * Renders item for list.
+ * @param {Object} item Item to render.
+ */
+ DefaultActionDialog.prototype.renderItem = function(item) {
+ var result = this.document_.createElement('li');
+
+ var div = this.document_.createElement('div');
+ div.textContent = item.label;
+
+ if (item.iconType) {
+ div.setAttribute('file-type-icon', item.iconType);
+ } else if (item.iconUrl) {
+ div.style.backgroundImage = 'url(' + item.iconUrl + ')';
+ }
+
+ if (item.class)
+ div.classList.add(item.class);
+
+ result.appendChild(div);
+
+ cr.defineProperty(result, 'lead', cr.PropertyKind.BOOL_ATTR);
+ cr.defineProperty(result, 'selected', cr.PropertyKind.BOOL_ATTR);
+
+ return result;
+ };
+
+ /**
+ * Shows dialog.
+ *
+ * @param {string} title Title in dialog caption.
+ * @param {string} message Message in dialog caption.
+ * @param {Array.<Object>} items Items to render in the list.
+ * @param {number} defaultIndex Item to select by default.
+ * @param {function(Object=)} opt_onOk OK callback with the selected item.
+ * @param {function()=} opt_onCancel Cancel callback.
+ */
+ DefaultActionDialog.prototype.show = function(title, message, items,
+ defaultIndex, opt_onOk, opt_onCancel) {
+
+ var show = FileManagerDialogBase.prototype.showOkCancelDialog.call(
+ this, title, message, opt_onOk, opt_onCancel);
+
+ if (!show) {
+ console.error('DefaultActionDialog can\'t be shown.');
+ return;
+ }
+
+ if (!message) {
+ this.text_.setAttribute('hidden', 'hidden');
+ } else {
+ this.text_.removeAttribute('hidden');
+ }
+
+ this.list_.startBatchUpdates();
+ this.dataModel_.splice(0, this.dataModel_.length);
+ for (var i = 0; i < items.length; i++) {
+ this.dataModel_.push(items[i]);
+ }
+ this.selectionModel_.selectedIndex = defaultIndex;
+ this.list_.endBatchUpdates();
+ };
+
+ /**
+ * List activation handler. Closes dialog and calls 'ok' callback.
+ * @param {number} index Activated index.
+ */
+ DefaultActionDialog.prototype.activateItemAtIndex_ = function(index) {
+ this.hide();
+ this.onOk_(this.dataModel_.item(index));
+ };
+
+ /**
+ * Closes dialog and invokes callback with currently-selected item.
+ * @override
+ */
+ DefaultActionDialog.prototype.onOkClick_ = function() {
+ this.activateItemAtIndex_(this.selectionModel_.selectedIndex);
+ };
+
+ /**
+ * @override
+ */
+ DefaultActionDialog.prototype.onContainerKeyDown_ = function(event) {
+ // Handle Escape.
+ if (event.keyCode == 27) {
+ this.onCancelClick_(event);
+ event.preventDefault();
+ } else if (event.keyCode == 32 || event.keyCode == 13) {
+ this.onOkClick_();
+ event.preventDefault();
+ }
+ };
+
+ return {DefaultActionDialog: DefaultActionDialog};
+});
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/directory_contents.js b/chromium/chrome/browser/resources/file_manager/foreground/js/directory_contents.js
new file mode 100644
index 00000000000..be69b66af90
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/directory_contents.js
@@ -0,0 +1,770 @@
+// 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.
+
+'use strict';
+
+/**
+ * Scanner of the entries.
+ * @constructor
+ */
+function ContentScanner() {
+ this.cancelled_ = false;
+}
+
+/**
+ * Starts to scan the entries. For example, starts to read the entries in a
+ * directory, or starts to search with some query on a file system.
+ * Derived classes must override this method.
+ *
+ * @param {function(Array.<Entry>)} entriesCallback Called when some chunk of
+ * entries are read. This can be called a couple of times until the
+ * completion.
+ * @param {function()} successCallback Called when the scan is completed
+ * successfully.
+ * @param {function(FileError)} errorCallback Called an error occurs.
+ */
+ContentScanner.prototype.scan = function(
+ entriesCallback, successCallback, errorCallback) {
+};
+
+/**
+ * Request cancelling of the running scan. When the cancelling is done,
+ * an error will be reported from errorCallback passed to scan().
+ */
+ContentScanner.prototype.cancel = function() {
+ this.cancelled_ = true;
+};
+
+/**
+ * Scanner of the entries in a directory.
+ * @param {DirectoryEntry} entry The directory to be read.
+ * @constructor
+ * @extends {ContentScanner}
+ */
+function DirectoryContentScanner(entry) {
+ ContentScanner.call(this);
+ this.entry_ = entry;
+}
+
+/**
+ * Extends ContentScanner.
+ */
+DirectoryContentScanner.prototype.__proto__ = ContentScanner.prototype;
+
+/**
+ * Starts to read the entries in the directory.
+ * @override
+ */
+DirectoryContentScanner.prototype.scan = function(
+ entriesCallback, successCallback, errorCallback) {
+ if (!this.entry_ || this.entry_ === DirectoryModel.fakeDriveEntry_) {
+ // If entry is not specified or a fake, we cannot read it.
+ errorCallback(util.createFileError(FileError.INVALID_MODIFICATION_ERR));
+ return;
+ }
+
+ metrics.startInterval('DirectoryScan');
+ var reader = this.entry_.createReader();
+ var readEntries = function() {
+ reader.readEntries(
+ function(entries) {
+ if (this.cancelled_) {
+ errorCallback(util.createFileError(FileError.ABORT_ERR));
+ return;
+ }
+
+ if (entries.length === 0) {
+ // All entries are read.
+ metrics.recordInterval('DirectoryScan');
+ successCallback();
+ return;
+ }
+
+ entriesCallback(entries);
+ readEntries();
+ }.bind(this),
+ errorCallback);
+ }.bind(this);
+ readEntries();
+};
+
+/**
+ * Scanner of the entries for the search results on Drive File System.
+ * @param {string} query The query string.
+ * @constructor
+ * @extends {ContentScanner}
+ */
+function DriveSearchContentScanner(query) {
+ ContentScanner.call(this);
+ this.query_ = query;
+}
+
+/**
+ * Extends ContentScanner.
+ */
+DriveSearchContentScanner.prototype.__proto__ = ContentScanner.prototype;
+
+/**
+ * Delay in milliseconds to be used for drive search scan, in order to reduce
+ * the number of server requests while user is typing the query.
+ * @type {number}
+ * @private
+ * @const
+ */
+DriveSearchContentScanner.SCAN_DELAY_ = 200;
+
+/**
+ * Maximum number of results which is shown on the search.
+ * @type {number}
+ * @private
+ * @const
+ */
+DriveSearchContentScanner.MAX_RESULTS_ = 100;
+
+/**
+ * Starts to search on Drive File System.
+ * @override
+ */
+DriveSearchContentScanner.prototype.scan = function(
+ entriesCallback, successCallback, errorCallback) {
+ var numReadEntries = 0;
+ var readEntries = function(nextFeed) {
+ chrome.fileBrowserPrivate.searchDrive(
+ {query: this.query_, nextFeed: nextFeed},
+ function(entries, nextFeed) {
+ if (this.cancelled_) {
+ errorCallback(util.createFileError(FileError.ABORT_ERR));
+ return;
+ }
+
+ // TODO(tbarzic): Improve error handling.
+ if (!entries) {
+ console.error('Drive search encountered an error.');
+ errorCallback(util.createFileError(
+ FileError.INVALID_MODIFICATION_ERR));
+ return;
+ }
+
+ var numRemainingEntries =
+ DriveSearchContentScanner.MAX_RESULTS_ - numReadEntries;
+ if (entries.length >= numRemainingEntries) {
+ // The limit is hit, so quit the scan here.
+ entries = entries.slice(0, numRemainingEntries);
+ nextFeed = '';
+ }
+
+ numReadEntries += entries.length;
+ if (entries.length > 0)
+ entriesCallback(entries);
+
+ if (nextFeed === '')
+ successCallback();
+ else
+ readEntries(nextFeed);
+ }.bind(this));
+ }.bind(this);
+
+ // Let's give another search a chance to cancel us before we begin.
+ setTimeout(
+ function() {
+ // Check cancelled state before read the entries.
+ if (this.cancelled_) {
+ errorCallback(util.createFileError(FileError.ABORT_ERR));
+ return;
+ }
+ readEntries('');
+ }.bind(this),
+ DriveSearchContentScanner.SCAN_DELAY_);
+};
+
+/**
+ * Scanner of the entries of the file name search on the directory tree, whose
+ * root is entry.
+ * @param {DirectoryEntry} entry The root of the search target directory tree.
+ * @param {string} query The query of the search.
+ * @constructor
+ * @extends {ContentScanner}
+ */
+function LocalSearchContentScanner(entry, query) {
+ ContentScanner.call(this);
+ this.entry_ = entry;
+ this.query_ = query.toLowerCase();
+}
+
+/**
+ * Extedns ContentScanner.
+ */
+LocalSearchContentScanner.prototype.__proto__ = ContentScanner.prototype;
+
+/**
+ * Starts the file name search.
+ * @override
+ */
+LocalSearchContentScanner.prototype.scan = function(
+ entriesCallback, successCallback, errorCallback) {
+ var numRunningTasks = 0;
+ var error = null;
+ var maybeRunCallback = function() {
+ if (numRunningTasks === 0) {
+ if (this.cancelled_)
+ errorCallback(util.createFileError(FileError.ABORT_ERR));
+ else if (error)
+ errorCallback(error);
+ else
+ successCallback();
+ }
+ }.bind(this);
+
+ var processEntry = function(entry) {
+ numRunningTasks++;
+ var onError = function(fileError) {
+ if (!error)
+ error = fileError;
+ numRunningTasks--;
+ maybeRunCallback();
+ };
+
+ var onSuccess = function(entries) {
+ if (this.cancelled_ || error || entries.length === 0) {
+ numRunningTasks--;
+ maybeRunCallback();
+ return;
+ }
+
+ // Filters by the query, and if found, run entriesCallback.
+ var foundEntries = entries.filter(function(entry) {
+ return entry.name.toLowerCase().indexOf(this.query_) >= 0;
+ }.bind(this));
+ if (foundEntries.length > 0)
+ entriesCallback(foundEntries);
+
+ // Start to process sub directories.
+ for (var i = 0; i < entries.length; i++) {
+ if (entries[i].isDirectory)
+ processEntry(entries[i]);
+ }
+
+ // Read remaining entries.
+ reader.readEntries(onSuccess, onError);
+ }.bind(this);
+
+ var reader = entry.createReader();
+ reader.readEntries(onSuccess, onError);
+ }.bind(this);
+
+ processEntry(this.entry_);
+};
+
+/**
+ * Scanner of the entries for the metadata search on Drive File System.
+ * @param {string} query The query of the search.
+ * @param {DriveMetadataSearchContentScanner.SearchType} searchType The option
+ * of the search.
+ * @constructor
+ * @extends {ContentScanner}
+ */
+function DriveMetadataSearchContentScanner(query, searchType) {
+ ContentScanner.call(this);
+ this.query_ = query;
+ this.searchType_ = searchType;
+}
+
+/**
+ * Extends ContentScanner.
+ */
+DriveMetadataSearchContentScanner.prototype.__proto__ =
+ ContentScanner.prototype;
+
+/**
+ * The search types on the Drive File System.
+ * @enum {string}
+ */
+DriveMetadataSearchContentScanner.SearchType = Object.freeze({
+ SEARCH_ALL: 'ALL',
+ SEARCH_SHARED_WITH_ME: 'SHARED_WITH_ME',
+ SEARCH_RECENT_FILES: 'EXCLUDE_DIRECTORIES',
+ SEARCH_OFFLINE: 'OFFLINE'
+});
+
+/**
+ * Starts to metadata-search on Drive File System.
+ * @override
+ */
+DriveMetadataSearchContentScanner.prototype.scan = function(
+ entriesCallback, successCallback, errorCallback) {
+ chrome.fileBrowserPrivate.searchDriveMetadata(
+ {query: this.query_, types: this.searchType_, maxResults: 500},
+ function(results) {
+ if (this.cancelled_) {
+ errorCallback(util.createFileError(FileError.ABORT_ERR));
+ return;
+ }
+
+ if (!results) {
+ console.error('Drive search encountered an error.');
+ errorCallback(util.createFileError(
+ FileError.INVALID_MODIFICATION_ERR));
+ return;
+ }
+
+ var entries = results.map(function(result) { return result.entry; });
+ if (entries.length > 0)
+ entriesCallback(entries);
+ successCallback();
+ }.bind(this));
+};
+
+/**
+ * This class manages filters and determines a file should be shown or not.
+ * When filters are changed, a 'changed' event is fired.
+ *
+ * @param {MetadataCache} metadataCache Metadata cache service.
+ * @param {boolean} showHidden If files starting with '.' are shown.
+ * @constructor
+ * @extends {cr.EventTarget}
+ */
+function FileFilter(metadataCache, showHidden) {
+ /**
+ * @type {MetadataCache}
+ * @private
+ */
+ this.metadataCache_ = metadataCache;
+
+ /**
+ * @type Object.<string, Function>
+ * @private
+ */
+ this.filters_ = {};
+ this.setFilterHidden(!showHidden);
+
+ // Do not show entries marked as 'deleted'.
+ this.addFilter('deleted', function(entry) {
+ var internal = this.metadataCache_.getCached(entry, 'internal');
+ return !(internal && internal.deleted);
+ }.bind(this));
+}
+
+/*
+ * FileFilter extends cr.EventTarget.
+ */
+FileFilter.prototype = {__proto__: cr.EventTarget.prototype};
+
+/**
+ * @param {string} name Filter identifier.
+ * @param {function(Entry)} callback A filter — a function receiving an Entry,
+ * and returning bool.
+ */
+FileFilter.prototype.addFilter = function(name, callback) {
+ this.filters_[name] = callback;
+ cr.dispatchSimpleEvent(this, 'changed');
+};
+
+/**
+ * @param {string} name Filter identifier.
+ */
+FileFilter.prototype.removeFilter = function(name) {
+ delete this.filters_[name];
+ cr.dispatchSimpleEvent(this, 'changed');
+};
+
+/**
+ * @param {boolean} value If do not show hidden files.
+ */
+FileFilter.prototype.setFilterHidden = function(value) {
+ if (value) {
+ this.addFilter(
+ 'hidden',
+ function(entry) { return entry.name.substr(0, 1) !== '.'; }
+ );
+ } else {
+ this.removeFilter('hidden');
+ }
+};
+
+/**
+ * @return {boolean} If the files with names starting with "." are not shown.
+ */
+FileFilter.prototype.isFilterHiddenOn = function() {
+ return 'hidden' in this.filters_;
+};
+
+/**
+ * @param {Entry} entry File entry.
+ * @return {boolean} True if the file should be shown, false otherwise.
+ */
+FileFilter.prototype.filter = function(entry) {
+ for (var name in this.filters_) {
+ if (!this.filters_[name](entry))
+ return false;
+ }
+ return true;
+};
+
+/**
+ * A context of DirectoryContents.
+ * TODO(yoshiki): remove this. crbug.com/224869.
+ *
+ * @param {FileFilter} fileFilter The file-filter context.
+ * @param {MetadataCache} metadataCache Metadata cache service.
+ * @constructor
+ */
+function FileListContext(fileFilter, metadataCache) {
+ /**
+ * @type {cr.ui.ArrayDataModel}
+ */
+ this.fileList = new cr.ui.ArrayDataModel([]);
+
+ /**
+ * @type {MetadataCache}
+ */
+ this.metadataCache = metadataCache;
+
+ /**
+ * @type {FileFilter}
+ */
+ this.fileFilter = fileFilter;
+}
+
+/**
+ * This class is responsible for scanning directory (or search results),
+ * and filling the fileList. Different descendants handle various types of
+ * directory contents shown: basic directory, drive search results, local search
+ * results.
+ * TODO(hidehiko): Remove EventTarget from this.
+ *
+ * @param {FileListContext} context The file list context.
+ * @param {boolean} isSearch True for search directory contents, otherwise
+ * false.
+ * @param {DirectoryEntry} directoryEntry The entry of the current directory.
+ * @param {DirectoryEntry} lastNonSearchDirectoryEntry The entry of the last
+ * non-search directory.
+ * @param {function():ContentScanner} scannerFactory The factory to create
+ * ContentScanner instance.
+ * @constructor
+ * @extends {cr.EventTarget}
+ */
+function DirectoryContents(context, isSearch, directoryEntry,
+ lastNonSearchDirectoryEntry,
+ scannerFactory) {
+ this.context_ = context;
+ this.fileList_ = context.fileList;
+
+ this.isSearch_ = isSearch;
+ this.directoryEntry_ = directoryEntry;
+ this.lastNonSearchDirectoryEntry_ = lastNonSearchDirectoryEntry;
+
+ this.scannerFactory_ = scannerFactory;
+ this.scanner_ = null;
+ this.prefetchMetadataQueue_ = new AsyncUtil.Queue();
+ this.scanCancelled_ = false;
+}
+
+/**
+ * DirectoryContents extends cr.EventTarget.
+ */
+DirectoryContents.prototype.__proto__ = cr.EventTarget.prototype;
+
+/**
+ * Create the copy of the object, but without scan started.
+ * @return {DirectoryContents} Object copy.
+ */
+DirectoryContents.prototype.clone = function() {
+ return new DirectoryContents(
+ this.context_, this.isSearch_, this.directoryEntry_,
+ this.lastNonSearchDirectoryEntry_, this.scannerFactory_);
+};
+
+/**
+ * Use a given fileList instead of the fileList from the context.
+ * @param {Array|cr.ui.ArrayDataModel} fileList The new file list.
+ */
+DirectoryContents.prototype.setFileList = function(fileList) {
+ if (fileList instanceof cr.ui.ArrayDataModel)
+ this.fileList_ = fileList;
+ else
+ this.fileList_ = new cr.ui.ArrayDataModel(fileList);
+ this.context_.metadataCache.setCacheSize(this.fileList_.length);
+};
+
+/**
+ * Use the filelist from the context and replace its contents with the entries
+ * from the current fileList.
+ */
+DirectoryContents.prototype.replaceContextFileList = function() {
+ if (this.context_.fileList !== this.fileList_) {
+ var spliceArgs = this.fileList_.slice();
+ var fileList = this.context_.fileList;
+ spliceArgs.unshift(0, fileList.length);
+ fileList.splice.apply(fileList, spliceArgs);
+ this.fileList_ = fileList;
+ this.context_.metadataCache.setCacheSize(this.fileList_.length);
+ }
+};
+
+/**
+ * @return {boolean} If the scan is active.
+ */
+DirectoryContents.prototype.isScanning = function() {
+ return this.scanner_ || this.prefetchMetadataQueue_.isRunning();
+};
+
+/**
+ * @return {boolean} True if search results (drive or local).
+ */
+DirectoryContents.prototype.isSearch = function() {
+ return this.isSearch_;
+};
+
+/**
+ * @return {DirectoryEntry} A DirectoryEntry for current directory. In case of
+ * search -- the top directory from which search is run.
+ */
+DirectoryContents.prototype.getDirectoryEntry = function() {
+ return this.directoryEntry_;
+};
+
+/**
+ * @return {DirectoryEntry} A DirectoryEntry for the last non search contents.
+ */
+DirectoryContents.prototype.getLastNonSearchDirectoryEntry = function() {
+ return this.lastNonSearchDirectoryEntry_;
+};
+
+/**
+ * Start directory scan/search operation. Either 'scan-completed' or
+ * 'scan-failed' event will be fired upon completion.
+ */
+DirectoryContents.prototype.scan = function() {
+ // TODO(hidehiko,mtomasz): this scan method must be called at most once.
+ // Remove such a limitation.
+ this.scanner_ = this.scannerFactory_();
+ this.scanner_.scan(this.onNewEntries_.bind(this),
+ this.onScanCompleted_.bind(this),
+ this.onScanError_.bind(this));
+};
+
+/**
+ * Cancels the running scan.
+ */
+DirectoryContents.prototype.cancelScan = function() {
+ if (this.scanCancelled_)
+ return;
+ this.scanCancelled_ = true;
+ if (this.scanner_)
+ this.scanner_.cancel();
+
+ this.prefetchMetadataQueue_.cancel();
+ cr.dispatchSimpleEvent(this, 'scan-cancelled');
+};
+
+/**
+ * Called when the scanning by scanner_ is done.
+ * @private
+ */
+DirectoryContents.prototype.onScanCompleted_ = function() {
+ this.scanner_ = null;
+ if (this.scanCancelled_)
+ return;
+
+ this.prefetchMetadataQueue_.run(function(callback) {
+ // Call callback first, so isScanning() returns false in the event handlers.
+ callback();
+ cr.dispatchSimpleEvent(this, 'scan-completed');
+ }.bind(this));
+};
+
+/**
+ * Called in case scan has failed. Should send the event.
+ * @private
+ */
+DirectoryContents.prototype.onScanError_ = function() {
+ this.scanner_ = null;
+ if (this.scanCancelled_)
+ return;
+
+ this.prefetchMetadataQueue_.run(function(callback) {
+ // Call callback first, so isScanning() returns false in the event handlers.
+ callback();
+ cr.dispatchSimpleEvent(this, 'scan-failed');
+ }.bind(this));
+};
+
+/**
+ * Called when some chunk of entries are read by scanner.
+ * @param {Array.<Entry>} entries The list of the scanned entries.
+ * @private
+ */
+DirectoryContents.prototype.onNewEntries_ = function(entries) {
+ if (this.scanCancelled_)
+ return;
+
+ var entriesFiltered = [].filter.call(
+ entries, this.context_.fileFilter.filter.bind(this.context_.fileFilter));
+
+ // Update the filelist without waiting the metadata.
+ this.fileList_.push.apply(this.fileList_, entriesFiltered);
+ cr.dispatchSimpleEvent(this, 'scan-updated');
+
+ this.context_.metadataCache.setCacheSize(this.fileList_.length);
+
+ // Because the prefetchMetadata can be slow, throttling by splitting entries
+ // into smaller chunks to reduce UI latency.
+ // TODO(hidehiko,mtomasz): This should be handled in MetadataCache.
+ var MAX_CHUNK_SIZE = 50;
+ for (var i = 0; i < entriesFiltered.length; i += MAX_CHUNK_SIZE) {
+ var chunk = entriesFiltered.slice(i, i + MAX_CHUNK_SIZE);
+ this.prefetchMetadataQueue_.run(function(chunk, callback) {
+ this.prefetchMetadata(chunk, function() {
+ if (this.scanCancelled_) {
+ // Do nothing if the scanning is cancelled.
+ callback();
+ return;
+ }
+
+ // TODO(yoshiki): Here we should fire the update event of changed
+ // items. Currently we have a method this.fileList_.updateIndex() to
+ // fire an event, but this method takes only 1 argument and invokes sort
+ // one by one. It is obviously time wasting. Instead, we call sort
+ // directory.
+ // In future, we should implement a good method like updateIndexes and
+ // use it here.
+ var status = this.fileList_.sortStatus;
+ this.fileList_.sort(status.field, status.direction);
+
+ cr.dispatchSimpleEvent(this, 'scan-updated');
+ callback();
+ }.bind(this));
+ }.bind(this, chunk));
+ }
+};
+
+/**
+ * @param {Array.<Entry>} entries Files.
+ * @param {function(Object)} callback Callback on done.
+ */
+DirectoryContents.prototype.prefetchMetadata = function(entries, callback) {
+ this.context_.metadataCache.get(entries, 'filesystem', callback);
+};
+
+/**
+ * @param {Array.<Entry>} entries Files.
+ * @param {function(Object)} callback Callback on done.
+ */
+DirectoryContents.prototype.reloadMetadata = function(entries, callback) {
+ this.context_.metadataCache.clear(entries, '*');
+ this.context_.metadataCache.get(entries, 'filesystem', callback);
+};
+
+/**
+ * @param {string} name Directory name.
+ * @param {function(DirectoryEntry)} successCallback Called on success.
+ * @param {function(FileError)} errorCallback On error.
+ */
+DirectoryContents.prototype.createDirectory = function(
+ name, successCallback, errorCallback) {
+ // TODO(hidehiko): createDirectory should not be the part of
+ // DirectoryContent.
+ if (this.isSearch_ || !this.directoryEntry_) {
+ errorCallback(util.createFileError(FileError.INVALID_MODIFICATION_ERR));
+ return;
+ }
+
+ var onSuccess = function(newEntry) {
+ this.reloadMetadata([newEntry], function() {
+ successCallback(newEntry);
+ });
+ };
+
+ this.directoryEntry_.getDirectory(name, {create: true, exclusive: true},
+ onSuccess.bind(this), errorCallback);
+};
+
+/**
+ * Creates a DirectoryContents instance to show entries in a directory.
+ *
+ * @param {FileListContext} context File list context.
+ * @param {DirectoryEntry} directoryEntry The current directory entry.
+ * @return {DirectoryContents} Created DirectoryContents instance.
+ */
+DirectoryContents.createForDirectory = function(context, directoryEntry) {
+ return new DirectoryContents(
+ context,
+ false, // Non search.
+ directoryEntry,
+ directoryEntry,
+ function() {
+ return new DirectoryContentScanner(directoryEntry);
+ });
+};
+
+/**
+ * Creates a DirectoryContents instance to show the result of the search on
+ * Drive File System.
+ *
+ * @param {FileListContext} context File list context.
+ * @param {DirectoryEntry} directoryEntry The current directory entry.
+ * @param {DirectoryEntry} previousDirectoryEntry The DirectoryEntry that was
+ * current before the search.
+ * @param {string} query Search query.
+ * @return {DirectoryContents} Created DirectoryContents instance.
+ */
+DirectoryContents.createForDriveSearch = function(
+ context, directoryEntry, previousDirectoryEntry, query) {
+ return new DirectoryContents(
+ context,
+ true, // Search.
+ directoryEntry,
+ previousDirectoryEntry,
+ function() {
+ return new DriveSearchContentScanner(query);
+ });
+};
+
+/**
+ * Creates a DirectoryContents instance to show the result of the search on
+ * Local File System.
+ *
+ * @param {FileListContext} context File list context.
+ * @param {DirectoryEntry} directoryEntry The current directory entry.
+ * @param {string} query Search query.
+ * @return {DirectoryContents} Created DirectoryContents instance.
+ */
+DirectoryContents.createForLocalSearch = function(
+ context, directoryEntry, query) {
+ return new DirectoryContents(
+ context,
+ true, // Search.
+ directoryEntry,
+ directoryEntry,
+ function() {
+ return new LocalSearchContentScanner(directoryEntry, query);
+ });
+};
+
+/**
+ * Creates a DirectoryContents instance to show the result of metadata search
+ * on Drive File System.
+ *
+ * @param {FileListContext} context File list context.
+ * @param {DirectoryEntry} fakeDirectoryEntry Fake directory entry representing
+ * the set of result entries. This serves as a top directory for the
+ * search.
+ * @param {DirectoryEntry} driveDirectoryEntry Directory for the actual drive.
+ * @param {string} query Search query.
+ * @param {DriveMetadataSearchContentScanner.SearchType} searchType The type of
+ * the search. The scanner will restricts the entries based on the given
+ * type.
+ * @return {DirectoryContents} Created DirectoryContents instance.
+ */
+DirectoryContents.createForDriveMetadataSearch = function(
+ context, fakeDirectoryEntry, driveDirectoryEntry, query, searchType) {
+ return new DirectoryContents(
+ context,
+ true, // Search
+ fakeDirectoryEntry,
+ driveDirectoryEntry,
+ function() {
+ return new DriveMetadataSearchContentScanner(query, searchType);
+ });
+};
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/directory_model.js b/chromium/chrome/browser/resources/file_manager/foreground/js/directory_model.js
new file mode 100644
index 00000000000..fde41e8c321
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/directory_model.js
@@ -0,0 +1,1186 @@
+// 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.
+
+'use strict';
+
+// If directory files changes too often, don't rescan directory more than once
+// per specified interval
+var SIMULTANEOUS_RESCAN_INTERVAL = 1000;
+// Used for operations that require almost instant rescan.
+var SHORT_RESCAN_INTERVAL = 100;
+
+/**
+ * Data model of the file manager.
+ *
+ * @param {boolean} singleSelection True if only one file could be selected
+ * at the time.
+ * @param {FileFilter} fileFilter Instance of FileFilter.
+ * @param {FileWatcher} fileWatcher Instance of FileWatcher.
+ * @param {MetadataCache} metadataCache The metadata cache service.
+ * @param {VolumeManagerWrapper} volumeManager The volume manager.
+ * @constructor
+ */
+function DirectoryModel(singleSelection, fileFilter, fileWatcher,
+ metadataCache, volumeManager) {
+ this.fileListSelection_ = singleSelection ?
+ new cr.ui.ListSingleSelectionModel() : new cr.ui.ListSelectionModel();
+
+ this.runningScan_ = null;
+ this.pendingScan_ = null;
+ this.rescanTime_ = null;
+ this.scanFailures_ = 0;
+ this.changeDirectorySequence_ = 0;
+
+ this.fileFilter_ = fileFilter;
+ this.fileFilter_.addEventListener('changed',
+ this.onFilterChanged_.bind(this));
+
+ this.currentFileListContext_ = new FileListContext(
+ fileFilter, metadataCache);
+ this.currentDirContents_ =
+ DirectoryContents.createForDirectory(this.currentFileListContext_, null);
+
+ this.volumeManager_ = volumeManager;
+ this.volumeManager_.volumeInfoList.addEventListener(
+ 'splice', this.onVolumeInfoListUpdated_.bind(this));
+
+ this.fileWatcher_ = fileWatcher;
+ this.fileWatcher_.addEventListener(
+ 'watcher-directory-changed',
+ this.onWatcherDirectoryChanged_.bind(this));
+}
+
+/**
+ * Fake entry to be used in currentDirEntry_ when current directory is
+ * unmounted DRIVE. TODO(haruki): Support "drive/root" and "drive/other".
+ * @type {Object}
+ * @const
+ * @private
+ */
+DirectoryModel.fakeDriveEntry_ = {
+ fullPath: RootDirectory.DRIVE + '/' + DriveSubRootDirectory.ROOT,
+ isDirectory: true,
+ rootType: RootType.DRIVE
+};
+
+/**
+ * Fake entry representing a psuedo directory, which contains Drive files
+ * available offline. This entry works as a trigger to start a search for
+ * offline files.
+ * @type {Object}
+ * @const
+ * @private
+ */
+DirectoryModel.fakeDriveOfflineEntry_ = {
+ fullPath: RootDirectory.DRIVE_OFFLINE,
+ isDirectory: true,
+ rootType: RootType.DRIVE_OFFLINE
+};
+
+/**
+ * Fake entry representing a pseudo directory, which contains shared-with-me
+ * Drive files. This entry works as a trigger to start a search for
+ * shared-with-me files.
+ * @type {Object}
+ * @const
+ * @private
+ */
+DirectoryModel.fakeDriveSharedWithMeEntry_ = {
+ fullPath: RootDirectory.DRIVE_SHARED_WITH_ME,
+ isDirectory: true,
+ rootType: RootType.DRIVE_SHARED_WITH_ME
+};
+
+/**
+ * Fake entry representing a pseudo directory, which contains Drive files
+ * accessed recently. This entry works as a trigger to start a metadata search
+ * implemented as DirectoryContentsDriveRecent.
+ * DirectoryModel is responsible to start the search when the UI tries to open
+ * this fake entry (e.g. changeDirectory()).
+ * @type {Object}
+ * @const
+ * @private
+ */
+DirectoryModel.fakeDriveRecentEntry_ = {
+ fullPath: RootDirectory.DRIVE_RECENT,
+ isDirectory: true,
+ rootType: RootType.DRIVE_RECENT
+};
+
+/**
+ * List of fake entries for special searches.
+ *
+ * @type {Array.<Object>}
+ * @const
+ */
+DirectoryModel.FAKE_DRIVE_SPECIAL_SEARCH_ENTRIES = [
+ DirectoryModel.fakeDriveSharedWithMeEntry_,
+ DirectoryModel.fakeDriveRecentEntry_,
+ DirectoryModel.fakeDriveOfflineEntry_
+];
+
+/**
+ * DirectoryModel extends cr.EventTarget.
+ */
+DirectoryModel.prototype.__proto__ = cr.EventTarget.prototype;
+
+/**
+ * Disposes the directory model by removing file watchers.
+ */
+DirectoryModel.prototype.dispose = function() {
+ this.fileWatcher_.dispose();
+};
+
+/**
+ * @return {cr.ui.ArrayDataModel} Files in the current directory.
+ */
+DirectoryModel.prototype.getFileList = function() {
+ return this.currentFileListContext_.fileList;
+};
+
+/**
+ * Sort the file list.
+ * @param {string} sortField Sort field.
+ * @param {string} sortDirection "asc" or "desc".
+ */
+DirectoryModel.prototype.sortFileList = function(sortField, sortDirection) {
+ this.getFileList().sort(sortField, sortDirection);
+};
+
+/**
+ * @return {cr.ui.ListSelectionModel|cr.ui.ListSingleSelectionModel} Selection
+ * in the fileList.
+ */
+DirectoryModel.prototype.getFileListSelection = function() {
+ return this.fileListSelection_;
+};
+
+/**
+ * @return {RootType} Root type of current root.
+ */
+DirectoryModel.prototype.getCurrentRootType = function() {
+ var entry = this.currentDirContents_.getDirectoryEntry();
+ return PathUtil.getRootType(entry ? entry.fullPath : '');
+};
+
+/**
+ * @return {string} Root path.
+ */
+DirectoryModel.prototype.getCurrentRootPath = function() {
+ var entry = this.currentDirContents_.getDirectoryEntry();
+ return entry ? PathUtil.getRootPath(entry.fullPath) : '';
+};
+
+/**
+ * @return {string} Filesystem URL representing the mountpoint for the current
+ * contents.
+ */
+DirectoryModel.prototype.getCurrentMountPointUrl = function() {
+ var rootPath = this.getCurrentRootPath();
+ // Special search roots are just showing a search results from DRIVE.
+ if (PathUtil.getRootType(rootPath) == RootType.DRIVE ||
+ PathUtil.isSpecialSearchRoot(rootPath))
+ return util.makeFilesystemUrl(RootDirectory.DRIVE);
+
+ return util.makeFilesystemUrl(rootPath);
+};
+
+/**
+ * @return {boolean} on True if offline.
+ */
+DirectoryModel.prototype.isDriveOffline = function() {
+ var connection = this.volumeManager_.getDriveConnectionState();
+ return connection.type == util.DriveConnectionType.OFFLINE;
+};
+
+/**
+ * TODO(haruki): This actually checks the current root. Fix the method name and
+ * related code.
+ * @return {boolean} True if the root for the current directory is read only.
+ */
+DirectoryModel.prototype.isReadOnly = function() {
+ return this.isPathReadOnly(this.getCurrentRootPath());
+};
+
+/**
+ * @return {boolean} True if the a scan is active.
+ */
+DirectoryModel.prototype.isScanning = function() {
+ return this.currentDirContents_.isScanning();
+};
+
+/**
+ * @return {boolean} True if search is in progress.
+ */
+DirectoryModel.prototype.isSearching = function() {
+ return this.currentDirContents_.isSearch();
+};
+
+/**
+ * @param {string} path Path to check.
+ * @return {boolean} True if the |path| is read only.
+ */
+DirectoryModel.prototype.isPathReadOnly = function(path) {
+ // TODO(hidehiko): Migrate this into VolumeInfo.
+ switch (PathUtil.getRootType(path)) {
+ case RootType.REMOVABLE:
+ var volumeInfo = this.volumeManager_.getVolumeInfo(path);
+ // Returns true if the volume is actually read only, or if an error
+ // is found during the mounting.
+ // TODO(hidehiko): Remove "error" check here, by removing error'ed volume
+ // info from VolumeManager.
+ return volumeInfo && (volumeInfo.isReadOnly || !!volumeInfo.error);
+ case RootType.ARCHIVE:
+ return true;
+ case RootType.DOWNLOADS:
+ return false;
+ case RootType.DRIVE:
+ // TODO(haruki): Maybe add DRIVE_OFFLINE as well to allow renaming in the
+ // offline tab.
+ return this.isDriveOffline();
+ default:
+ return true;
+ }
+};
+
+/**
+ * Updates the selection by using the updateFunc and publish the change event.
+ * If updateFunc returns true, it force to dispatch the change event even if the
+ * selection index is not changed.
+ *
+ * @param {cr.ui.ListSelectionModel|cr.ui.ListSingleSelectionModel} selection
+ * Selection to be updated.
+ * @param {function(): boolean} updateFunc Function updating the selection.
+ * @private
+ */
+DirectoryModel.prototype.updateSelectionAndPublishEvent_ =
+ function(selection, updateFunc) {
+ // Begin change.
+ selection.beginChange();
+
+ // If dispatchNeeded is true, we should ensure the change event is
+ // dispatched.
+ var dispatchNeeded = updateFunc();
+
+ // Check if the change event is dispatched in the endChange function
+ // or not.
+ var eventDispatched = function() { dispatchNeeded = false; };
+ selection.addEventListener('change', eventDispatched);
+ selection.endChange();
+ selection.removeEventListener('change', eventDispatched);
+
+ // If the change event have been already dispatched, dispatchNeeded is false.
+ if (dispatchNeeded) {
+ var event = new Event('change');
+ // The selection status (selected or not) is not changed because
+ // this event is caused by the change of selected item.
+ event.changes = [];
+ selection.dispatchEvent(event);
+ }
+};
+
+/**
+ * Invoked when a change in the directory is detected by the watcher.
+ * @private
+ */
+DirectoryModel.prototype.onWatcherDirectoryChanged_ = function() {
+ this.rescanSoon();
+};
+
+/**
+ * Invoked when filters are changed.
+ * @private
+ */
+DirectoryModel.prototype.onFilterChanged_ = function() {
+ this.rescanSoon();
+};
+
+/**
+ * Returns the filter.
+ * @return {FileFilter} The file filter.
+ */
+DirectoryModel.prototype.getFileFilter = function() {
+ return this.fileFilter_;
+};
+
+/**
+ * @return {DirectoryEntry} Current directory.
+ */
+DirectoryModel.prototype.getCurrentDirEntry = function() {
+ return this.currentDirContents_.getDirectoryEntry();
+};
+
+/**
+ * @return {string} URL of the current directory. or null if unavailable.
+ */
+DirectoryModel.prototype.getCurrentDirectoryURL = function() {
+ var entry = this.currentDirContents_.getDirectoryEntry();
+ if (!entry)
+ return null;
+ if (entry === DirectoryModel.fakeDriveOfflineEntry_)
+ return util.makeFilesystemUrl(entry.fullPath);
+ return entry.toURL();
+};
+
+/**
+ * @return {string} Path for the current directory, or empty string if the
+ * current directory is not yet set.
+ */
+DirectoryModel.prototype.getCurrentDirPath = function() {
+ var entry = this.currentDirContents_.getDirectoryEntry();
+ return entry ? entry.fullPath : '';
+};
+
+/**
+ * @return {Array.<string>} File paths of selected files.
+ * @private
+ */
+DirectoryModel.prototype.getSelectedPaths_ = function() {
+ var indexes = this.fileListSelection_.selectedIndexes;
+ var fileList = this.getFileList();
+ if (fileList) {
+ return indexes.map(function(i) {
+ return fileList.item(i).fullPath;
+ });
+ }
+ return [];
+};
+
+/**
+ * @param {Array.<string>} value List of file paths of selected files.
+ * @private
+ */
+DirectoryModel.prototype.setSelectedPaths_ = function(value) {
+ var indexes = [];
+ var fileList = this.getFileList();
+
+ var safeKey = function(key) {
+ // The transformation must:
+ // 1. Never generate a reserved name ('__proto__')
+ // 2. Keep different keys different.
+ return '#' + key;
+ };
+
+ var hash = {};
+
+ for (var i = 0; i < value.length; i++)
+ hash[safeKey(value[i])] = 1;
+
+ for (var i = 0; i < fileList.length; i++) {
+ if (hash.hasOwnProperty(safeKey(fileList.item(i).fullPath)))
+ indexes.push(i);
+ }
+ this.fileListSelection_.selectedIndexes = indexes;
+};
+
+/**
+ * @return {string} Lead item file path.
+ * @private
+ */
+DirectoryModel.prototype.getLeadPath_ = function() {
+ var index = this.fileListSelection_.leadIndex;
+ return index >= 0 && this.getFileList().item(index).fullPath;
+};
+
+/**
+ * @param {string} value The name of new lead index.
+ * @private
+ */
+DirectoryModel.prototype.setLeadPath_ = function(value) {
+ var fileList = this.getFileList();
+ for (var i = 0; i < fileList.length; i++) {
+ if (fileList.item(i).fullPath === value) {
+ this.fileListSelection_.leadIndex = i;
+ return;
+ }
+ }
+};
+
+/**
+ * Schedule rescan with short delay.
+ */
+DirectoryModel.prototype.rescanSoon = function() {
+ this.scheduleRescan(SHORT_RESCAN_INTERVAL);
+};
+
+/**
+ * Schedule rescan with delay. Designed to handle directory change
+ * notification.
+ */
+DirectoryModel.prototype.rescanLater = function() {
+ this.scheduleRescan(SIMULTANEOUS_RESCAN_INTERVAL);
+};
+
+/**
+ * Schedule rescan with delay. If another rescan has been scheduled does
+ * nothing. File operation may cause a few notifications what should cause
+ * a single refresh.
+ * @param {number} delay Delay in ms after which the rescan will be performed.
+ */
+DirectoryModel.prototype.scheduleRescan = function(delay) {
+ if (this.rescanTime_) {
+ if (this.rescanTime_ <= Date.now() + delay)
+ return;
+ clearTimeout(this.rescanTimeoutId_);
+ }
+
+ this.rescanTime_ = Date.now() + delay;
+ this.rescanTimeoutId_ = setTimeout(this.rescan.bind(this), delay);
+};
+
+/**
+ * Cancel a rescan on timeout if it is scheduled.
+ * @private
+ */
+DirectoryModel.prototype.clearRescanTimeout_ = function() {
+ this.rescanTime_ = null;
+ if (this.rescanTimeoutId_) {
+ clearTimeout(this.rescanTimeoutId_);
+ this.rescanTimeoutId_ = null;
+ }
+};
+
+/**
+ * Rescan current directory. May be called indirectly through rescanLater or
+ * directly in order to reflect user action. Will first cache all the directory
+ * contents in an array, then seamlessly substitute the fileList contents,
+ * preserving the select element etc.
+ *
+ * This should be to scan the contents of current directory (or search).
+ */
+DirectoryModel.prototype.rescan = function() {
+ this.clearRescanTimeout_();
+ if (this.runningScan_) {
+ this.pendingRescan_ = true;
+ return;
+ }
+
+ var dirContents = this.currentDirContents_.clone();
+ dirContents.setFileList([]);
+
+ var successCallback = (function() {
+ this.replaceDirectoryContents_(dirContents);
+ cr.dispatchSimpleEvent(this, 'rescan-completed');
+ }).bind(this);
+
+ this.scan_(dirContents,
+ successCallback, function() {}, function() {}, function() {});
+};
+
+/**
+ * Run scan on the current DirectoryContents. The active fileList is cleared and
+ * the entries are added directly.
+ *
+ * This should be used when changing directory or initiating a new search.
+ *
+ * @param {DirectoryContentes} newDirContents New DirectoryContents instance to
+ * replace currentDirContents_.
+ * @param {function()=} opt_callback Called on success.
+ * @private
+ */
+DirectoryModel.prototype.clearAndScan_ = function(newDirContents,
+ opt_callback) {
+ if (this.currentDirContents_.isScanning())
+ this.currentDirContents_.cancelScan();
+ this.currentDirContents_ = newDirContents;
+ this.clearRescanTimeout_();
+
+ if (this.pendingScan_)
+ this.pendingScan_ = false;
+
+ if (this.runningScan_) {
+ if (this.runningScan_.isScanning())
+ this.runningScan_.cancelScan();
+ this.runningScan_ = null;
+ }
+
+ var onDone = function() {
+ cr.dispatchSimpleEvent(this, 'scan-completed');
+ if (opt_callback)
+ opt_callback();
+ }.bind(this);
+
+ var onFailed = function() {
+ cr.dispatchSimpleEvent(this, 'scan-failed');
+ }.bind(this);
+
+ var onUpdated = function() {
+ cr.dispatchSimpleEvent(this, 'scan-updated');
+ }.bind(this);
+
+ var onCancelled = function() {
+ cr.dispatchSimpleEvent(this, 'scan-cancelled');
+ }.bind(this);
+
+ // Clear the table, and start scanning.
+ cr.dispatchSimpleEvent(this, 'scan-started');
+ var fileList = this.getFileList();
+ fileList.splice(0, fileList.length);
+ this.scan_(this.currentDirContents_,
+ onDone, onFailed, onUpdated, onCancelled);
+};
+
+/**
+ * Perform a directory contents scan. Should be called only from rescan() and
+ * clearAndScan_().
+ *
+ * @param {DirectoryContents} dirContents DirectoryContents instance on which
+ * the scan will be run.
+ * @param {function()} successCallback Callback on success.
+ * @param {function()} failureCallback Callback on failure.
+ * @param {function()} updatedCallback Callback on update. Only on the last
+ * update, {@code successCallback} is called instead of this.
+ * @param {function()} cancelledCallback Callback on cancel.
+ * @private
+ */
+DirectoryModel.prototype.scan_ = function(
+ dirContents,
+ successCallback, failureCallback, updatedCallback, cancelledCallback) {
+ var self = this;
+
+ /**
+ * Runs pending scan if there is one.
+ *
+ * @return {boolean} Did pending scan exist.
+ */
+ var maybeRunPendingRescan = function() {
+ if (this.pendingRescan_) {
+ this.rescanSoon();
+ this.pendingRescan_ = false;
+ return true;
+ }
+ return false;
+ }.bind(this);
+
+ var onSuccess = function() {
+ // Record metric for Downloads directory.
+ if (!dirContents.isSearch()) {
+ var locationInfo =
+ this.volumeManager_.getLocationInfo(dirContents.getDirectoryEntry());
+ if (locationInfo.volumeInfo.volumeType === util.VolumeType.DOWNLOADS &&
+ locationInfo.isRootEntry) {
+ metrics.recordMediumCount('DownloadsCount',
+ dirContents.fileList_.length);
+ }
+ }
+
+ this.runningScan_ = null;
+ successCallback();
+ this.scanFailures_ = 0;
+ maybeRunPendingRescan();
+ }.bind(this);
+
+ var onFailure = function() {
+ this.runningScan_ = null;
+ this.scanFailures_++;
+ failureCallback();
+
+ if (maybeRunPendingRescan())
+ return;
+
+ if (this.scanFailures_ <= 1)
+ this.rescanLater();
+ }.bind(this);
+
+ this.runningScan_ = dirContents;
+
+ dirContents.addEventListener('scan-completed', onSuccess);
+ dirContents.addEventListener('scan-updated', updatedCallback);
+ dirContents.addEventListener('scan-failed', onFailure);
+ dirContents.addEventListener('scan-cancelled', cancelledCallback);
+ dirContents.scan();
+};
+
+/**
+ * @param {DirectoryContents} dirContents DirectoryContents instance.
+ * @private
+ */
+DirectoryModel.prototype.replaceDirectoryContents_ = function(dirContents) {
+ cr.dispatchSimpleEvent(this, 'begin-update-files');
+ this.updateSelectionAndPublishEvent_(this.fileListSelection_, function() {
+ var selectedPaths = this.getSelectedPaths_();
+ var selectedIndices = this.fileListSelection_.selectedIndexes;
+
+ // Restore leadIndex in case leadName no longer exists.
+ var leadIndex = this.fileListSelection_.leadIndex;
+ var leadPath = this.getLeadPath_();
+
+ this.currentDirContents_ = dirContents;
+ dirContents.replaceContextFileList();
+
+ this.setSelectedPaths_(selectedPaths);
+ this.fileListSelection_.leadIndex = leadIndex;
+ this.setLeadPath_(leadPath);
+
+ // If nothing is selected after update, then select file next to the
+ // latest selection
+ var forceChangeEvent = false;
+ if (this.fileListSelection_.selectedIndexes.length == 0 &&
+ selectedIndices.length != 0) {
+ var maxIdx = Math.max.apply(null, selectedIndices);
+ this.selectIndex(Math.min(maxIdx - selectedIndices.length + 2,
+ this.getFileList().length) - 1);
+ forceChangeEvent = true;
+ }
+ return forceChangeEvent;
+ }.bind(this));
+
+ cr.dispatchSimpleEvent(this, 'end-update-files');
+};
+
+/**
+ * Callback when an entry is changed.
+ * @param {util.EntryChangedKind} kind How the entry is changed.
+ * @param {Entry} entry The changed entry.
+ */
+DirectoryModel.prototype.onEntryChanged = function(kind, entry) {
+ // TODO(hidehiko): We should update directory model even the search result
+ // is shown.
+ var rootType = this.getCurrentRootType();
+ if ((rootType === RootType.DRIVE ||
+ rootType === RootType.DRIVE_SHARED_WITH_ME ||
+ rootType === RootType.DRIVE_RECENT ||
+ rootType === RootType.DRIVE_OFFLINE) &&
+ this.isSearching())
+ return;
+
+ if (kind == util.EntryChangedKind.CREATED) {
+ entry.getParent(function(parentEntry) {
+ if (this.getCurrentDirEntry().fullPath != parentEntry.fullPath) {
+ // Do nothing if current directory changed during async operations.
+ return;
+ }
+ this.currentDirContents_.prefetchMetadata([entry], function() {
+ if (this.getCurrentDirEntry().fullPath != parentEntry.fullPath) {
+ // Do nothing if current directory changed during async operations.
+ return;
+ }
+
+ var index = this.findIndexByEntry_(entry);
+ if (index >= 0)
+ this.getFileList().splice(index, 1, entry);
+ else
+ this.getFileList().push(entry);
+ }.bind(this));
+ }.bind(this));
+ } else {
+ // This is the delete event.
+ var index = this.findIndexByEntry_(entry);
+ if (index >= 0)
+ this.getFileList().splice(index, 1);
+ }
+};
+
+/**
+ * @param {Entry} entry The entry to be searched.
+ * @return {number} The index in the fileList, or -1 if not found.
+ * @private
+ */
+DirectoryModel.prototype.findIndexByEntry_ = function(entry) {
+ var fileList = this.getFileList();
+ for (var i = 0; i < fileList.length; i++) {
+ if (util.isSameEntry(fileList.item(i), entry))
+ return i;
+ }
+ return -1;
+};
+
+/**
+ * Called when rename is done successfully.
+ * Note: conceptually, DirectoryModel should work without this, because entries
+ * can be renamed by other systems anytime and Files.app should reflect it
+ * correctly.
+ * TODO(hidehiko): investigate more background, and remove this if possible.
+ *
+ * @param {Entry} oldEntry The old entry.
+ * @param {Entry} newEntry The new entry.
+ * @param {function()} opt_callback Called on completion.
+ */
+DirectoryModel.prototype.onRenameEntry = function(
+ oldEntry, newEntry, opt_callback) {
+ this.currentDirContents_.prefetchMetadata([newEntry], function() {
+ // If the current directory is the old entry, then quietly change to the
+ // new one.
+ if (util.isSameEntry(oldEntry, this.getCurrentDirEntry()))
+ this.changeDirectory(newEntry.fullPath);
+
+ // Look for the old entry.
+ // If the entry doesn't exist in the list, it has been updated from
+ // outside (probably by directory rescan).
+ var index = this.findIndexByEntry_(oldEntry);
+ if (index >= 0) {
+ // Update the content list and selection status.
+ var wasSelected = this.fileListSelection_.getIndexSelected(index);
+ this.updateSelectionAndPublishEvent_(this.fileListSelection_, function() {
+ this.fileListSelection_.setIndexSelected(index, false);
+ this.getFileList().splice(index, 1, newEntry);
+ if (wasSelected) {
+ // We re-search the index, because splice may trigger sorting so that
+ // index may be stale.
+ this.fileListSelection_.setIndexSelected(
+ this.findIndexByEntry_(newEntry), true);
+ }
+ return true;
+ }.bind(this));
+ }
+
+ // Run callback, finally.
+ if (opt_callback)
+ opt_callback();
+ }.bind(this));
+};
+
+/**
+ * Creates directory and updates the file list.
+ *
+ * @param {string} name Directory name.
+ * @param {function(DirectoryEntry)} successCallback Callback on success.
+ * @param {function(FileError)} errorCallback Callback on failure.
+ */
+DirectoryModel.prototype.createDirectory = function(name, successCallback,
+ errorCallback) {
+ var entry = this.getCurrentDirEntry();
+ if (!entry) {
+ errorCallback(util.createFileError(FileError.INVALID_MODIFICATION_ERR));
+ return;
+ }
+
+ var tracker = this.createDirectoryChangeTracker();
+ tracker.start();
+
+ var onSuccess = function(newEntry) {
+ // Do not change anything or call the callback if current
+ // directory changed.
+ tracker.stop();
+ if (tracker.hasChanged)
+ return;
+
+ var existing = this.getFileList().slice().filter(
+ function(e) {return e.name == name;});
+
+ if (existing.length) {
+ this.selectEntry(newEntry);
+ successCallback(existing[0]);
+ } else {
+ this.fileListSelection_.beginChange();
+ this.getFileList().splice(0, 0, newEntry);
+ this.selectEntry(newEntry);
+ this.fileListSelection_.endChange();
+ successCallback(newEntry);
+ }
+ };
+
+ this.currentDirContents_.createDirectory(name, onSuccess.bind(this),
+ errorCallback);
+};
+
+/**
+ * Changes directory. Causes 'directory-change' event.
+ *
+ * The directory will not be changed, if another request is started before it is
+ * finished. The error callback will not be called, and the event for the first
+ * request will not be invoked.
+ *
+ * @param {string} path New current directory path.
+ * @param {function(FileError)=} opt_errorCallback Executed if the change
+ * directory failed.
+ */
+DirectoryModel.prototype.changeDirectory = function(path, opt_errorCallback) {
+ this.changeDirectorySequence_++;
+
+ if (PathUtil.isSpecialSearchRoot(path)) {
+ this.specialSearch(path, '');
+ return;
+ }
+
+ this.resolveDirectory(
+ path,
+ function(sequence, directoryEntry) {
+ if (this.changeDirectorySequence_ === sequence)
+ this.changeDirectoryEntry(directoryEntry);
+ }.bind(this, this.changeDirectorySequence_),
+ function(error) {
+ console.error('Error changing directory to ' + path + ': ', error);
+ if (opt_errorCallback)
+ opt_errorCallback(error);
+ });
+};
+
+/**
+ * Resolves absolute directory path. Handles Drive stub. If the drive is
+ * mounting, callbacks will be called after the mount is completed.
+ *
+ * @param {string} path Path to the directory.
+ * @param {function(DirectoryEntry)} successCallback Success callback.
+ * @param {function(FileError)} errorCallback Error callback.
+ */
+DirectoryModel.prototype.resolveDirectory = function(
+ path, successCallback, errorCallback) {
+ if (PathUtil.getRootType(path) == RootType.DRIVE) {
+ if (!this.volumeManager_.getVolumeInfo(RootDirectory.DRIVE)) {
+ errorCallback(util.createFileError(FileError.NOT_FOUND_ERR));
+ return;
+ }
+ }
+
+ var onError = function(error) {
+ // Handle the special case, when in offline mode, and there are no cached
+ // contents on the C++ side. In such case, let's display the stub.
+ // The INVALID_STATE_ERR error code is returned from the drive filesystem
+ // in such situation.
+ //
+ // TODO(mtomasz, hashimoto): Consider rewriting this logic.
+ // crbug.com/253464.
+ if (PathUtil.getRootType(path) == RootType.DRIVE &&
+ error.code == FileError.INVALID_STATE_ERR) {
+ successCallback(DirectoryModel.fakeDriveEntry_);
+ return;
+ }
+ errorCallback(error);
+ }.bind(this);
+
+ // TODO(mtomasz): Use Entry instead of a path.
+ this.volumeManager_.resolveAbsolutePath(
+ path,
+ function(entry) {
+ if (entry.isFile) {
+ onError(util.createFileError(FileError.TYPE_MISMATCH_ERR));
+ return;
+ }
+ successCallback(entry);
+ },
+ onError);
+};
+
+/**
+ * @param {DirectoryEntry} dirEntry The absolute path to the new directory.
+ * @param {function()=} opt_callback Executed if the directory loads
+ * successfully.
+ * @private
+ */
+DirectoryModel.prototype.changeDirectoryEntrySilent_ = function(dirEntry,
+ opt_callback) {
+ var onScanComplete = function() {
+ if (opt_callback)
+ opt_callback();
+ // For tests that open the dialog to empty directories, everything
+ // is loaded at this point.
+ chrome.test.sendMessage('directory-change-complete');
+ };
+ this.clearAndScan_(
+ DirectoryContents.createForDirectory(this.currentFileListContext_,
+ dirEntry),
+ onScanComplete.bind(this));
+};
+
+/**
+ * Change the current directory to the directory represented by a
+ * DirectoryEntry.
+ *
+ * Dispatches the 'directory-changed' event when the directory is successfully
+ * changed.
+ *
+ * @param {DirectoryEntry} dirEntry The absolute path to the new directory.
+ * @param {function()=} opt_callback Executed if the directory loads
+ * successfully.
+ */
+DirectoryModel.prototype.changeDirectoryEntry = function(
+ dirEntry, opt_callback) {
+ this.fileWatcher_.changeWatchedDirectory(dirEntry, function(sequence) {
+ if (this.changeDirectorySequence_ !== sequence)
+ return;
+ var previous = this.currentDirContents_.getDirectoryEntry();
+ this.clearSearch_();
+ this.changeDirectoryEntrySilent_(dirEntry, opt_callback);
+
+ var e = new Event('directory-changed');
+ e.previousDirEntry = previous;
+ e.newDirEntry = dirEntry;
+ this.dispatchEvent(e);
+ }.bind(this, this.changeDirectorySequence_));
+};
+
+/**
+ * Creates an object which could say whether directory has changed while it has
+ * been active or not. Designed for long operations that should be cancelled
+ * if the used change current directory.
+ * @return {Object} Created object.
+ */
+DirectoryModel.prototype.createDirectoryChangeTracker = function() {
+ var tracker = {
+ dm_: this,
+ active_: false,
+ hasChanged: false,
+
+ start: function() {
+ if (!this.active_) {
+ this.dm_.addEventListener('directory-changed',
+ this.onDirectoryChange_);
+ this.active_ = true;
+ this.hasChanged = false;
+ }
+ },
+
+ stop: function() {
+ if (this.active_) {
+ this.dm_.removeEventListener('directory-changed',
+ this.onDirectoryChange_);
+ this.active_ = false;
+ }
+ },
+
+ onDirectoryChange_: function(event) {
+ tracker.stop();
+ tracker.hasChanged = true;
+ }
+ };
+ return tracker;
+};
+
+/**
+ * @param {Entry} entry Entry to be selected.
+ */
+DirectoryModel.prototype.selectEntry = function(entry) {
+ var fileList = this.getFileList();
+ for (var i = 0; i < fileList.length; i++) {
+ if (fileList.item(i).toURL() === entry.toURL()) {
+ this.selectIndex(i);
+ return;
+ }
+ }
+};
+
+/**
+ * @param {Array.<string>} entries Array of entries.
+ */
+DirectoryModel.prototype.selectEntries = function(entries) {
+ // URLs are needed here, since we are comparing Entries by URLs.
+ var urls = util.entriesToURLs(entries);
+ var fileList = this.getFileList();
+ this.fileListSelection_.beginChange();
+ this.fileListSelection_.unselectAll();
+ for (var i = 0; i < fileList.length; i++) {
+ if (urls.indexOf(fileList.item(i).toURL()) >= 0)
+ this.fileListSelection_.setIndexSelected(i, true);
+ }
+ this.fileListSelection_.endChange();
+};
+
+/**
+ * @param {number} index Index of file.
+ */
+DirectoryModel.prototype.selectIndex = function(index) {
+ // this.focusCurrentList_();
+ if (index >= this.getFileList().length)
+ return;
+
+ // If a list bound with the model it will do scrollIndexIntoView(index).
+ this.fileListSelection_.selectedIndex = index;
+};
+
+/**
+ * Called when VolumeInfoList is updated.
+ *
+ * @param {Event} event Event of VolumeInfoList's 'splice'.
+ * @private
+ */
+DirectoryModel.prototype.onVolumeInfoListUpdated_ = function(event) {
+ var driveVolume = this.volumeManager_.getVolumeInfo(RootDirectory.DRIVE);
+ if (driveVolume && !driveVolume.error) {
+ var currentDirEntry = this.getCurrentDirEntry();
+ if (currentDirEntry) {
+ if (currentDirEntry === DirectoryModel.fakeDriveEntry_) {
+ // Replace the fake entry by real DirectoryEntry silently.
+ this.volumeManager_.resolveAbsolutePath(
+ DirectoryModel.fakeDriveEntry_.fullPath,
+ function(entry) {
+ // If the current entry is still fake drive entry, replace it.
+ if (this.getCurrentDirEntry() === DirectoryModel.fakeDriveEntry_)
+ this.changeDirectoryEntrySilent_(entry);
+ },
+ function(error) {});
+ } else if (PathUtil.isSpecialSearchRoot(currentDirEntry.fullPath)) {
+ for (var i = 0; i < event.added.length; i++) {
+ if (event.added[i].volumeType == util.VolumeType.DRIVE) {
+ // If the Drive volume is newly mounted, rescan it.
+ this.rescan();
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ // When the volume where we are is unmounted, fallback to
+ // DEFAULT_MOUNT_POINT. If current directory path is empty, stop the fallback
+ // since the current directory is initializing now.
+ // TODO(mtomasz): DEFAULT_MOUNT_POINT is deprecated. Use VolumeManager::
+ // getDefaultVolume() after it is implemented.
+ if (this.getCurrentDirPath() &&
+ !this.volumeManager_.getVolumeInfo(this.getCurrentDirPath()))
+ this.changeDirectory(PathUtil.DEFAULT_MOUNT_POINT);
+};
+
+/**
+ * Check if the root of the given path is mountable or not.
+ *
+ * @param {string} path Path.
+ * @return {boolean} Return true, if the given path is under mountable root.
+ * Otherwise, return false.
+ */
+DirectoryModel.isMountableRoot = function(path) {
+ var rootType = PathUtil.getRootType(path);
+ switch (rootType) {
+ case RootType.DOWNLOADS:
+ return false;
+ case RootType.ARCHIVE:
+ case RootType.REMOVABLE:
+ case RootType.DRIVE:
+ return true;
+ default:
+ throw new Error('Unknown root type!');
+ }
+};
+
+/**
+ * Performs search and displays results. The search type is dependent on the
+ * current directory. If we are currently on drive, server side content search
+ * over drive mount point. If the current directory is not on the drive, file
+ * name search over current directory will be performed.
+ *
+ * @param {string} query Query that will be searched for.
+ * @param {function(Event)} onSearchRescan Function that will be called when the
+ * search directory is rescanned (i.e. search results are displayed).
+ * @param {function()} onClearSearch Function to be called when search state
+ * gets cleared.
+ * TODO(olege): Change callbacks to events.
+ */
+DirectoryModel.prototype.search = function(query,
+ onSearchRescan,
+ onClearSearch) {
+ query = query.trimLeft();
+
+ this.clearSearch_();
+
+ var currentDirEntry = this.getCurrentDirEntry();
+ if (!currentDirEntry) {
+ // Not yet initialized. Do nothing.
+ return;
+ }
+
+ if (!query) {
+ if (this.isSearching()) {
+ var newDirContents = DirectoryContents.createForDirectory(
+ this.currentFileListContext_,
+ this.currentDirContents_.getLastNonSearchDirectoryEntry());
+ this.clearAndScan_(newDirContents);
+ }
+ return;
+ }
+
+ this.onSearchCompleted_ = onSearchRescan;
+ this.onClearSearch_ = onClearSearch;
+
+ this.addEventListener('scan-completed', this.onSearchCompleted_);
+
+ // If we are offline, let's fallback to file name search inside dir.
+ // A search initiated from directories in Drive or special search results
+ // should trigger Drive search.
+ var newDirContents;
+ if (!this.isDriveOffline() &&
+ PathUtil.isDriveBasedPath(currentDirEntry.fullPath)) {
+ // Drive search is performed over the whole drive, so pass drive root as
+ // |directoryEntry|.
+ newDirContents = DirectoryContents.createForDriveSearch(
+ this.currentFileListContext_,
+ currentDirEntry,
+ this.currentDirContents_.getLastNonSearchDirectoryEntry(),
+ query);
+ } else {
+ newDirContents = DirectoryContents.createForLocalSearch(
+ this.currentFileListContext_, currentDirEntry, query);
+ }
+ this.clearAndScan_(newDirContents);
+};
+
+/**
+ * Performs special search and displays results. e.g. Drive files available
+ * offline, shared-with-me files, recently modified files.
+ * @param {string} path Path string representing special search. See fake
+ * entries in PathUtil.RootDirectory.
+ * @param {string=} opt_query Query string used for the search.
+ */
+DirectoryModel.prototype.specialSearch = function(path, opt_query) {
+ var query = opt_query || '';
+
+ this.clearSearch_();
+
+ this.onSearchCompleted_ = null;
+ this.onClearSearch_ = null;
+
+ var onDriveDirectoryResolved = function(sequence, driveRoot) {
+ if (this.changeDirectorySequence_ !== sequence)
+ return;
+ if (!driveRoot || driveRoot == DirectoryModel.fakeDriveEntry_) {
+ // Drive root not available or not ready. onVolumeInfoListUpdated_()
+ // handles the rescan if necessary.
+ driveRoot = null;
+ }
+
+ var specialSearchType = PathUtil.getRootType(path);
+ var searchOption;
+ var dirEntry;
+ if (specialSearchType == RootType.DRIVE_OFFLINE) {
+ dirEntry = DirectoryModel.fakeDriveOfflineEntry_;
+ searchOption =
+ DriveMetadataSearchContentScanner.SearchType.SEARCH_OFFLINE;
+ } else if (specialSearchType == RootType.DRIVE_SHARED_WITH_ME) {
+ dirEntry = DirectoryModel.fakeDriveSharedWithMeEntry_;
+ searchOption =
+ DriveMetadataSearchContentScanner.SearchType.SEARCH_SHARED_WITH_ME;
+ } else if (specialSearchType == RootType.DRIVE_RECENT) {
+ dirEntry = DirectoryModel.fakeDriveRecentEntry_;
+ searchOption =
+ DriveMetadataSearchContentScanner.SearchType.SEARCH_RECENT_FILES;
+ } else {
+ // Unknown path.
+ throw new Error('Unknown path for special search.');
+ }
+
+ var newDirContents = DirectoryContents.createForDriveMetadataSearch(
+ this.currentFileListContext_,
+ dirEntry, driveRoot, query, searchOption);
+ var previous = this.currentDirContents_.getDirectoryEntry();
+ this.clearAndScan_(newDirContents);
+
+ var e = new Event('directory-changed');
+ e.previousDirEntry = previous;
+ e.newDirEntry = dirEntry;
+ this.dispatchEvent(e);
+ }.bind(this, this.changeDirectorySequence_);
+
+ this.resolveDirectory(DirectoryModel.fakeDriveEntry_.fullPath,
+ onDriveDirectoryResolved /* success */,
+ function() {} /* failed */);
+};
+
+/**
+ * In case the search was active, remove listeners and send notifications on
+ * its canceling.
+ * @private
+ */
+DirectoryModel.prototype.clearSearch_ = function() {
+ if (!this.isSearching())
+ return;
+
+ if (this.onSearchCompleted_) {
+ this.removeEventListener('scan-completed', this.onSearchCompleted_);
+ this.onSearchCompleted_ = null;
+ }
+
+ if (this.onClearSearch_) {
+ this.onClearSearch_();
+ this.onClearSearch_ = null;
+ }
+};
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/directory_tree.js b/chromium/chrome/browser/resources/file_manager/foreground/js/directory_tree.js
new file mode 100644
index 00000000000..5c9de7aa0fe
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/directory_tree.js
@@ -0,0 +1,676 @@
+// Copyright (c) 2013 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.
+
+'use strict';
+
+////////////////////////////////////////////////////////////////////////////////
+// DirectoryTreeUtil
+
+/**
+ * Utility methods. They are intended for use only in this file.
+ */
+var DirectoryTreeUtil = {};
+
+/**
+ * Generate a list of the directory entries for the top level on the tree.
+ * @return {Array.<DirectoryEntry>} Entries for the top level on the tree.
+ */
+DirectoryTreeUtil.generateTopLevelEntries = function() {
+ var entries = [
+ DirectoryModel.fakeDriveEntry_,
+ DirectoryModel.fakeDriveOfflineEntry_,
+ DirectoryModel.fakeDriveSharedWithMeEntry_,
+ DirectoryModel.fakeDriveRecentEntry_,
+ ];
+
+ for (var i = 0; i < entries.length; i++) {
+ entries[i]['label'] = PathUtil.getRootLabel(entries[i].fullPath);
+ }
+
+ return entries;
+};
+
+/**
+ * Checks if the given directory can be on the tree or not.
+ *
+ * @param {string} path Path to be checked.
+ * @return {boolean} True if the path is eligible for the directory tree.
+ * Otherwise, false.
+ */
+DirectoryTreeUtil.isEligiblePathForDirectoryTree = function(path) {
+ return PathUtil.isDriveBasedPath(path);
+};
+
+Object.freeze(DirectoryTreeUtil);
+
+////////////////////////////////////////////////////////////////////////////////
+// DirectoryTreeBase
+
+/**
+ * Implementation of methods for DirectoryTree and DirectoryItem. These classes
+ * inherits cr.ui.Tree/TreeItem so we can't make them inherit this class.
+ * Instead, we separate their implementations to this separate object and call
+ * it with setting 'this' from DirectoryTree/Item.
+ */
+var DirectoryItemTreeBaseMethods = {};
+
+/**
+ * Updates sub-elements of {@code this} reading {@code DirectoryEntry}.
+ * The list of {@code DirectoryEntry} are not updated by this method.
+ *
+ * @param {boolean} recursive True if the all visible sub-directories are
+ * updated recursively including left arrows. If false, the update walks
+ * only immediate child directories without arrows.
+ */
+DirectoryItemTreeBaseMethods.updateSubElementsFromList = function(recursive) {
+ var index = 0;
+ var tree = this.parentTree_ || this; // If no parent, 'this' itself is tree.
+ while (this.entries_[index]) {
+ var currentEntry = this.entries_[index];
+ var currentElement = this.items[index];
+
+ if (index >= this.items.length) {
+ var item = new DirectoryItem(currentEntry, this, tree);
+ this.add(item);
+ index++;
+ } else if (currentEntry.fullPath == currentElement.fullPath) {
+ if (recursive && this.expanded)
+ currentElement.updateSubDirectories(true /* recursive */);
+
+ index++;
+ } else if (currentEntry.fullPath < currentElement.fullPath) {
+ var item = new DirectoryItem(currentEntry, this, tree);
+ this.addAt(item, index);
+ index++;
+ } else if (currentEntry.fullPath > currentElement.fullPath) {
+ this.remove(currentElement);
+ }
+ }
+
+ var removedChild;
+ while (removedChild = this.items[index]) {
+ this.remove(removedChild);
+ }
+
+ if (index == 0) {
+ this.hasChildren = false;
+ this.expanded = false;
+ } else {
+ this.hasChildren = true;
+ }
+};
+
+/**
+ * Finds a parent directory of the {@code entry} in {@code this}, and
+ * invokes the DirectoryItem.selectByEntry() of the found directory.
+ *
+ * @param {DirectoryEntry|Object} entry The entry to be searched for. Can be
+ * a fake.
+ * @return {boolean} True if the parent item is found.
+ */
+DirectoryItemTreeBaseMethods.searchAndSelectByEntry = function(entry) {
+ for (var i = 0; i < this.items.length; i++) {
+ var item = this.items[i];
+ if (util.isParentEntry(item.entry, entry)) {
+ item.selectByEntry(entry);
+ return true;
+ }
+ }
+ return false;
+};
+
+Object.freeze(DirectoryItemTreeBaseMethods);
+
+////////////////////////////////////////////////////////////////////////////////
+// DirectoryItem
+
+/**
+ * A directory in the tree. Each element represents one directory.
+ *
+ * @param {DirectoryEntry} dirEntry DirectoryEntry of this item.
+ * @param {DirectoryItem|DirectoryTree} parentDirItem Parent of this item.
+ * @param {DirectoryTree} tree Current tree, which contains this item.
+ * @extends {cr.ui.TreeItem}
+ * @constructor
+ */
+function DirectoryItem(dirEntry, parentDirItem, tree) {
+ var item = cr.doc.createElement('div');
+ DirectoryItem.decorate(item, dirEntry, parentDirItem, tree);
+ return item;
+}
+
+/**
+ * @param {HTMLElement} el Element to be DirectoryItem.
+ * @param {DirectoryEntry} dirEntry DirectoryEntry of this item.
+ * @param {DirectoryItem|DirectoryTree} parentDirItem Parent of this item.
+ * @param {DirectoryTree} tree Current tree, which contains this item.
+ */
+DirectoryItem.decorate =
+ function(el, dirEntry, parentDirItem, tree) {
+ el.__proto__ = DirectoryItem.prototype;
+ (/** @type {DirectoryItem} */ el).decorate(
+ dirEntry, parentDirItem, tree);
+};
+
+DirectoryItem.prototype = {
+ __proto__: cr.ui.TreeItem.prototype,
+
+ /**
+ * The DirectoryEntry corresponding to this DirectoryItem. This may be
+ * a dummy DirectoryEntry.
+ * @type {DirectoryEntry|Object}
+ */
+ get entry() {
+ return this.dirEntry_;
+ },
+
+ /**
+ * The element containing the label text and the icon.
+ * @type {!HTMLElement}
+ * @override
+ */
+ get labelElement() {
+ return this.firstElementChild.querySelector('.label');
+ }
+};
+
+/**
+ * Calls DirectoryItemTreeBaseMethods.updateSubElementsFromList().
+ *
+ * @param {boolean} recursive True if the all visible sub-directories are
+ * updated recursively including left arrows. If false, the update walks
+ * only immediate child directories without arrows.
+ */
+DirectoryItem.prototype.updateSubElementsFromList = function(recursive) {
+ DirectoryItemTreeBaseMethods.updateSubElementsFromList.call(this, recursive);
+};
+
+/**
+ * Calls DirectoryItemTreeBaseMethods.updateSubElementsFromList().
+ * @param {DirectoryEntry|Object} entry The entry to be searched for. Can be
+ * a fake.
+ * @return {boolean} True if the parent item is found.
+ */
+DirectoryItem.prototype.searchAndSelectByEntry = function(entry) {
+ return DirectoryItemTreeBaseMethods.searchAndSelectByEntry.call(this, entry);
+};
+
+/**
+ * @param {DirectoryEntry} dirEntry DirectoryEntry of this item.
+ * @param {DirectoryItem|DirectoryTree} parentDirItem Parent of this item.
+ * @param {DirectoryTree} tree Current tree, which contains this item.
+ */
+DirectoryItem.prototype.decorate = function(
+ dirEntry, parentDirItem, tree) {
+ var path = dirEntry.fullPath;
+ var label;
+ label = dirEntry.label ? dirEntry.label : dirEntry.name;
+
+ this.className = 'tree-item';
+ this.innerHTML =
+ '<div class="tree-row">' +
+ ' <span class="expand-icon"></span>' +
+ ' <span class="icon"></span>' +
+ ' <span class="label"></span>' +
+ '</div>' +
+ '<div class="tree-children"></div>';
+ this.setAttribute('role', 'treeitem');
+
+ this.parentTree_ = tree;
+ this.directoryModel_ = tree.directoryModel;
+ this.parent_ = parentDirItem;
+ this.label = label;
+ this.fullPath = path;
+ this.dirEntry_ = dirEntry;
+ this.fileFilter_ = this.directoryModel_.getFileFilter();
+
+ // Sets hasChildren=false tentatively. This will be overridden after
+ // scanning sub-directories in DirectoryTreeUtil.updateSubElementsFromList.
+ this.hasChildren = false;
+
+ this.addEventListener('expand', this.onExpand_.bind(this), false);
+ var icon = this.querySelector('.icon');
+ icon.classList.add('volume-icon');
+ var iconType = PathUtil.getRootType(path);
+ if (iconType && PathUtil.isRootPath(path))
+ icon.setAttribute('volume-type-icon', iconType);
+ else
+ icon.setAttribute('file-type-icon', 'folder');
+
+ if (this.parentTree_.contextMenuForSubitems)
+ this.setContextMenu(this.parentTree_.contextMenuForSubitems);
+ // Adds handler for future change.
+ this.parentTree_.addEventListener(
+ 'contextMenuForSubitemsChange',
+ function(e) { this.setContextMenu(e.newValue); }.bind(this));
+
+ if (parentDirItem.expanded)
+ this.updateSubDirectories(false /* recursive */);
+};
+
+/**
+ * Overrides WebKit's scrollIntoViewIfNeeded, which doesn't work well with
+ * a complex layout. This call is not necessary, so we are ignoring it.
+ *
+ * @param {boolean} unused Unused.
+ * @override
+ */
+DirectoryItem.prototype.scrollIntoViewIfNeeded = function(unused) {
+};
+
+/**
+ * Removes the child node, but without selecting the parent item, to avoid
+ * unintended changing of directories. Removing is done externally, and other
+ * code will navigate to another directory.
+ *
+ * @param {!cr.ui.TreeItem} child The tree item child to remove.
+ * @override
+ */
+DirectoryItem.prototype.remove = function(child) {
+ this.lastElementChild.removeChild(child);
+ if (this.items.length == 0)
+ this.hasChildren = false;
+};
+
+/**
+ * Invoked when the item is being expanded.
+ * @param {!UIEvent} e Event.
+ * @private
+ **/
+DirectoryItem.prototype.onExpand_ = function(e) {
+ this.updateSubDirectories(
+ true /* recursive */,
+ function() {},
+ function() {
+ this.expanded = false;
+ }.bind(this));
+
+ e.stopPropagation();
+};
+
+/**
+ * Retrieves the latest subdirectories and update them on the tree.
+ * @param {boolean} recursive True if the update is recursively.
+ * @param {function()=} opt_successCallback Callback called on success.
+ * @param {function()=} opt_errorCallback Callback called on error.
+ */
+DirectoryItem.prototype.updateSubDirectories = function(
+ recursive, opt_successCallback, opt_errorCallback) {
+ if (util.isFakeEntry(this.entry)) {
+ if (opt_errorCallback)
+ opt_errorCallback();
+ return;
+ }
+
+ var sortEntries = function(fileFilter, entries) {
+ entries.sort(function(a, b) {
+ return (a.name.toLowerCase() > b.name.toLowerCase()) ? 1 : -1;
+ });
+ return entries.filter(fileFilter.filter.bind(fileFilter));
+ };
+
+ var onSuccess = function(entries) {
+ this.entries_ = entries;
+ this.redrawSubDirectoryList_(recursive);
+ opt_successCallback && opt_successCallback();
+ }.bind(this);
+
+ var reader = this.entry.createReader();
+ var entries = [];
+ var readEntry = function() {
+ reader.readEntries(function(results) {
+ if (!results.length) {
+ onSuccess(sortEntries(this.fileFilter_, entries));
+ return;
+ }
+
+ for (var i = 0; i < results.length; i++) {
+ var entry = results[i];
+ if (entry.isDirectory)
+ entries.push(entry);
+ }
+ readEntry();
+ }.bind(this));
+ }.bind(this);
+ readEntry();
+};
+
+/**
+ * Updates sub-elements of {@code parentElement} reading {@code DirectoryEntry}
+ * with calling {@code iterator}.
+ *
+ * @param {string} changedDirectryPath The path of the changed directory.
+ */
+DirectoryItem.prototype.updateItemByPath = function(changedDirectryPath) {
+ if (changedDirectryPath === this.entry.fullPath) {
+ this.updateSubDirectories(false /* recursive */);
+ return;
+ }
+
+ for (var i = 0; i < this.items.length; i++) {
+ var item = this.items[i];
+ if (PathUtil.isParentPath(item.entry.fullPath, changedDirectryPath)) {
+ item.updateItemByPath(changedDirectryPath);
+ break;
+ }
+ }
+};
+
+/**
+ * Redraw subitems with the latest information. The items are sorted in
+ * alphabetical order, case insensitive.
+ * @param {boolean} recursive True if the update is recursively.
+ * @private
+ */
+DirectoryItem.prototype.redrawSubDirectoryList_ = function(recursive) {
+ this.updateSubElementsFromList(recursive);
+};
+
+/**
+ * Select the item corresponding to the given {@code entry}.
+ * @param {DirectoryEntry|Object} entry The entry to be selected. Can be a fake.
+ */
+DirectoryItem.prototype.selectByEntry = function(entry) {
+ if (util.isSameEntry(entry, this.entry)) {
+ this.selected = true;
+ return;
+ }
+
+ if (this.searchAndSelectByEntry(entry))
+ return;
+
+ // If the path doesn't exist, updates sub directories and tryes again.
+ this.updateSubDirectories(
+ false /* recursive */,
+ this.searchAndSelectByEntry.bind(this, entry));
+};
+
+/**
+ * Executes the assigned action as a drop target.
+ */
+DirectoryItem.prototype.doDropTargetAction = function() {
+ this.expanded = true;
+};
+
+/**
+ * Executes the assigned action. DirectoryItem performs changeDirectory.
+ */
+DirectoryItem.prototype.doAction = function() {
+ if (this.fullPath != this.directoryModel_.getCurrentDirPath())
+ this.directoryModel_.changeDirectory(this.fullPath);
+};
+
+/**
+ * Sets the context menu for directory tree.
+ * @param {cr.ui.Menu} menu Menu to be set.
+ */
+DirectoryItem.prototype.setContextMenu = function(menu) {
+ if (this.entry && PathUtil.isEligibleForFolderShortcut(this.entry.fullPath))
+ cr.ui.contextMenuHandler.setContextMenu(this, menu);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+// DirectoryTree
+
+/**
+ * Tree of directories on the middle bar. This element is also the root of
+ * items, in other words, this is the parent of the top-level items.
+ *
+ * @constructor
+ * @extends {cr.ui.Tree}
+ */
+function DirectoryTree() {}
+
+/**
+ * Decorates an element.
+ * @param {HTMLElement} el Element to be DirectoryTree.
+ * @param {DirectoryModel} directoryModel Current DirectoryModel.
+ * @param {VolumeManagerWrapper} volumeManager VolumeManager of the system.
+ */
+DirectoryTree.decorate = function(el, directoryModel, volumeManager) {
+ el.__proto__ = DirectoryTree.prototype;
+ (/** @type {DirectoryTree} */ el).decorate(directoryModel, volumeManager);
+};
+
+DirectoryTree.prototype = {
+ __proto__: cr.ui.Tree.prototype,
+
+ // DirectoryTree is always expanded.
+ get expanded() { return true; },
+ /**
+ * @param {boolean} value Not used.
+ */
+ set expanded(value) {},
+
+ /**
+ * The DirectoryEntry corresponding to this DirectoryItem. This may be
+ * a dummy DirectoryEntry.
+ * @type {DirectoryEntry|Object}
+ * @override
+ **/
+ get entry() {
+ return this.dirEntry_;
+ },
+
+ /**
+ * The DirectoryModel this tree corresponds to.
+ * @type {DirectoryModel}
+ */
+ get directoryModel() {
+ return this.directoryModel_;
+ },
+
+ /**
+ * The VolumeManager instance of the system.
+ * @type {VolumeManager}
+ */
+ get volumeManager() {
+ return this.volumeManager_;
+ },
+};
+
+cr.defineProperty(DirectoryTree, 'contextMenuForSubitems', cr.PropertyKind.JS);
+
+/**
+ * Calls DirectoryItemTreeBaseMethods.updateSubElementsFromList().
+ *
+ * @param {boolean} recursive True if the all visible sub-directories are
+ * updated recursively including left arrows. If false, the update walks
+ * only immediate child directories without arrows.
+ */
+DirectoryTree.prototype.updateSubElementsFromList = function(recursive) {
+ DirectoryItemTreeBaseMethods.updateSubElementsFromList.call(this, recursive);
+};
+
+/**
+ * Calls DirectoryItemTreeBaseMethods.updateSubElementsFromList().
+ * @param {DirectoryEntry|Object} entry The entry to be searched for. Can be
+ * a fake.
+ * @return {boolean} True if the parent item is found.
+ */
+DirectoryTree.prototype.searchAndSelectByEntry = function(entry) {
+ return DirectoryItemTreeBaseMethods.searchAndSelectByEntry.call(this, entry);
+};
+
+/**
+ * Decorates an element.
+ * @param {DirectoryModel} directoryModel Current DirectoryModel.
+ * @param {VolumeManagerWrapper} volumeManager VolumeManager of the system.
+ */
+DirectoryTree.prototype.decorate = function(directoryModel, volumeManager) {
+ cr.ui.Tree.prototype.decorate.call(this);
+
+ this.directoryModel_ = directoryModel;
+ this.volumeManager_ = volumeManager;
+ this.entries_ = DirectoryTreeUtil.generateTopLevelEntries();
+
+ this.fileFilter_ = this.directoryModel_.getFileFilter();
+ this.fileFilter_.addEventListener('changed',
+ this.onFilterChanged_.bind(this));
+
+ this.directoryModel_.addEventListener('directory-changed',
+ this.onCurrentDirectoryChanged_.bind(this));
+
+ // Add a handler for directory change.
+ this.addEventListener('change', function() {
+ if (this.selectedItem &&
+ (!this.currentEntry_ ||
+ !util.isSameEntry(this.currentEntry_, this.selectedItem.entry))) {
+ this.currentEntry_ = this.selectedItem.entry;
+ this.selectedItem.doAction();
+ return;
+ }
+ }.bind(this));
+
+ this.privateOnDirectoryChangedBound_ =
+ this.onDirectoryContentChanged_.bind(this);
+ chrome.fileBrowserPrivate.onDirectoryChanged.addListener(
+ this.privateOnDirectoryChangedBound_);
+
+ this.scrollBar_ = MainPanelScrollBar();
+ this.scrollBar_.initialize(this.parentNode, this);
+
+ // Once, draws the list with the fake '/drive/' entry.
+ this.redraw(false /* recursive */);
+ // Resolves 'My Drive' entry and replaces the fake with the true one.
+ this.maybeResolveMyDriveRoot_(function() {
+ // After the true entry is resolved, draws the list again.
+ this.redraw(true /* recursive */);
+ }.bind(this));
+};
+
+/**
+ * Select the item corresponding to the given entry.
+ * @param {DirectoryEntry|Object} entry The directory entry to be selected. Can
+ * be a fake.
+ */
+DirectoryTree.prototype.selectByEntry = function(entry) {
+ // If the target directory is not in the tree, do nothing.
+ if (!DirectoryTreeUtil.isEligiblePathForDirectoryTree(entry.fullPath))
+ return;
+
+ this.maybeResolveMyDriveRoot_(function() {
+ if (this.selectedItem && util.isSameEntry(entry, this.selectedItem.entry))
+ return;
+
+ if (this.searchAndSelectByEntry(entry))
+ return;
+
+ this.selectedItem = null;
+ this.updateSubDirectories(
+ false /* recursive */,
+ // Success callback, failure is not handled.
+ function() {
+ if (!this.searchAndSelectByEntry(entry))
+ this.selectedItem = null;
+ }.bind(this));
+ }.bind(this));
+};
+
+/**
+ * Resolves the My Drive root's entry, if it is a fake. If the entry is already
+ * resolved to a DirectoryEntry, completionCallback() will be called
+ * immediately.
+ * @param {function()} completionCallback Called when the resolving is
+ * done (or the entry is already resolved), regardless if it is
+ * successfully done or not.
+ * @private
+ */
+DirectoryTree.prototype.maybeResolveMyDriveRoot_ = function(
+ completionCallback) {
+ var myDriveItem = this.items[0];
+ if (!util.isFakeEntry(myDriveItem.entry)) {
+ // The entry is already resolved. Don't need to try again.
+ completionCallback();
+ return;
+ }
+
+ // The entry is a fake.
+ this.directoryModel_.resolveDirectory(
+ myDriveItem.fullPath,
+ function(entry) {
+ if (!util.isFakeEntry(entry))
+ myDriveItem.dirEntry_ = entry;
+
+ completionCallback();
+ },
+ completionCallback);
+};
+
+/**
+ * Retrieves the latest subdirectories and update them on the tree.
+ * @param {boolean} recursive True if the update is recursively.
+ * @param {function()=} opt_successCallback Callback called on success.
+ * @param {function()=} opt_errorCallback Callback called on error.
+ */
+DirectoryTree.prototype.updateSubDirectories = function(
+ recursive, opt_successCallback, opt_errorCallback) {
+ this.entries_ = DirectoryTreeUtil.generateTopLevelEntries();
+ this.redraw(recursive);
+ if (opt_successCallback)
+ opt_successCallback();
+};
+
+/**
+ * Redraw the list.
+ * @param {boolean} recursive True if the update is recursively. False if the
+ * only root items are updated.
+ */
+DirectoryTree.prototype.redraw = function(recursive) {
+ this.updateSubElementsFromList(recursive);
+};
+
+/**
+ * Invoked when the filter is changed.
+ * @private
+ */
+DirectoryTree.prototype.onFilterChanged_ = function() {
+ // Returns immediately, if the tree is hidden.
+ if (this.hidden)
+ return;
+
+ this.redraw(true /* recursive */);
+};
+
+/**
+ * Invoked when a directory is changed.
+ * @param {!UIEvent} event Event.
+ * @private
+ */
+DirectoryTree.prototype.onDirectoryContentChanged_ = function(event) {
+ if (event.eventType == 'changed') {
+ // TODO: Use Entry instead of urls. This will stop working once migrating
+ // to separate file systems. See: crbug.com/325052.
+ if (!DirectoryTreeUtil.isEligiblePathForDirectoryTree(event.entry.fullPath))
+ return;
+
+ var myDriveItem = this.items[0];
+ myDriveItem.updateItemByPath(event.entry.fullPath);
+ }
+};
+
+/**
+ * Invoked when the current directory is changed.
+ * @param {!UIEvent} event Event.
+ * @private
+ */
+DirectoryTree.prototype.onCurrentDirectoryChanged_ = function(event) {
+ this.selectByEntry(event.newDirEntry);
+};
+
+/**
+ * Sets the margin height for the transparent preview panel at the bottom.
+ * @param {number} margin Margin to be set in px.
+ */
+DirectoryTree.prototype.setBottomMarginForPanel = function(margin) {
+ this.style.paddingBottom = margin + 'px';
+ this.scrollBar_.setBottomMarginForPanel(margin);
+};
+
+/**
+ * Updates the UI after the layout has changed.
+ */
+DirectoryTree.prototype.relayout = function() {
+ cr.dispatchSimpleEvent(this, 'relayout');
+};
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/drag_selector.js b/chromium/chrome/browser/resources/file_manager/foreground/js/drag_selector.js
new file mode 100644
index 00000000000..edc55165dfb
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/drag_selector.js
@@ -0,0 +1,244 @@
+// Copyright 2013 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.
+
+'use strict';
+
+/**
+ * Drag selector used on the file list or the grid table.
+ * TODO(hirono): Support drag selection for grid view. crbug.com/224832
+ * @constructor
+ */
+function DragSelector() {
+ /**
+ * Target list of drag selection.
+ * @type {cr.ui.List}
+ * @private
+ */
+ this.target_ = null;
+
+ /**
+ * Border element of drag handle.
+ * @type {HtmlElement}
+ * @private
+ */
+ this.border_ = null;
+
+ /**
+ * Start point of dragging.
+ * @type {number?}
+ * @private
+ */
+ this.startX_ = null;
+
+ /**
+ * Start point of dragging.
+ * @type {number?}
+ * @private
+ */
+ this.startY_ = null;
+
+ /**
+ * Indexes of selected items by dragging at the last update.
+ * @type {Array.<number>!}
+ * @private
+ */
+ this.lastSelection_ = [];
+
+ /**
+ * Indexes of selected items at the start of dragging.
+ * @type {Array.<number>!}
+ * @private
+ */
+ this.originalSelection_ = [];
+
+ // Bind handlers to make them removable.
+ this.onMouseMoveBound_ = this.onMouseMove_.bind(this);
+ this.onMouseUpBound_ = this.onMouseUp_.bind(this);
+
+ Object.seal(this);
+}
+
+/**
+ * Flag that shows whether the item is included in the selection or not.
+ * @enum {number}
+ * @private
+ */
+DragSelector.SelectionFlag_ = {
+ IN_LAST_SELECTION: 1 << 0,
+ IN_CURRENT_SELECTION: 1 << 1
+};
+
+/**
+ * Obtains the scrolled position in the element of mouse pointer from the mouse
+ * event.
+ *
+ * @param {HTMLElement} element Element that has the scroll bars.
+ * @param {Event} event The mouse event.
+ * @return {object} Scrolled position.
+ */
+DragSelector.getScrolledPosition = function(element, event) {
+ if (!element.cachedBounds) {
+ element.cachedBounds = element.getBoundingClientRect();
+ if (!element.cachedBounds)
+ return null;
+ }
+ var rect = element.cachedBounds;
+ return {
+ x: event.clientX - rect.left + element.scrollLeft,
+ y: event.clientY - rect.top + element.scrollTop
+ };
+};
+
+/**
+ * Starts drag selection by reacting dragstart event.
+ * This function must be called from handlers of dragstart event.
+ *
+ * @this {DragSelector}
+ * @param {cr.ui.List} list List where the drag selection starts.
+ * @param {Event} event The dragstart event.
+ */
+DragSelector.prototype.startDragSelection = function(list, event) {
+ // Precondition check
+ if (!list.selectionModel_.multiple || this.target_)
+ return;
+
+ // Set the target of the drag selection
+ this.target_ = list;
+
+ // Prevent the default action.
+ event.preventDefault();
+
+ // Save the start state.
+ var startPos = DragSelector.getScrolledPosition(list, event);
+ if (!startPos)
+ return;
+ this.startX_ = startPos.x;
+ this.startY_ = startPos.y;
+ this.lastSelection_ = [];
+ this.originalSelection_ = this.target_.selectionModel_.selectedIndexes;
+
+ // Create and add the border element
+ if (!this.border_) {
+ this.border_ = this.target_.ownerDocument.createElement('div');
+ this.border_.className = 'drag-selection-border';
+ }
+ this.border_.style.left = this.startX_ + 'px';
+ this.border_.style.top = this.startY_ + 'px';
+ this.border_.style.width = '0';
+ this.border_.style.height = '0';
+ list.appendChild(this.border_);
+
+ // If no modifier key is pressed, clear the original selection.
+ if (!event.shiftKey && !event.ctrlKey)
+ this.target_.selectionModel_.unselectAll();
+
+ // Register event handlers.
+ // The handlers are bounded at the constructor.
+ this.target_.ownerDocument.addEventListener(
+ 'mousemove', this.onMouseMoveBound_, true);
+ this.target_.ownerDocument.addEventListener(
+ 'mouseup', this.onMouseUpBound_, true);
+};
+
+/**
+ * Handles the mousemove event.
+ * @private
+ * @param {MouseEvent} event The mousemove event.
+ */
+DragSelector.prototype.onMouseMove_ = function(event) {
+ // Get the selection bounds.
+ var pos = DragSelector.getScrolledPosition(this.target_, event);
+ var borderBounds = {
+ left: Math.max(Math.min(this.startX_, pos.x), 0),
+ top: Math.max(Math.min(this.startY_, pos.y), 0),
+ right: Math.min(Math.max(this.startX_, pos.x), this.target_.scrollWidth),
+ bottom: Math.min(Math.max(this.startY_, pos.y), this.target_.scrollHeight)
+ };
+ borderBounds.width = borderBounds.right - borderBounds.left;
+ borderBounds.height = borderBounds.bottom - borderBounds.top;
+
+ // Collect items within the selection rect.
+ var currentSelection = this.target_.getHitElements(
+ borderBounds.left,
+ borderBounds.top,
+ borderBounds.width,
+ borderBounds.height);
+ var pointedElements = this.target_.getHitElements(pos.x, pos.y);
+ var leadIndex = pointedElements.length ? pointedElements[0] : -1;
+
+ // Diff the selection between currentSelection and this.lastSelection_.
+ var selectionFlag = [];
+ for (var i = 0; i < this.lastSelection_.length; i++) {
+ var index = this.lastSelection_[i];
+ // Bit operator can be used for undefined value.
+ selectionFlag[index] =
+ selectionFlag[index] | DragSelector.SelectionFlag_.IN_LAST_SELECTION;
+ }
+ for (var i = 0; i < currentSelection.length; i++) {
+ var index = currentSelection[i];
+ // Bit operator can be used for undefined value.
+ selectionFlag[index] =
+ selectionFlag[index] | DragSelector.SelectionFlag_.IN_CURRENT_SELECTION;
+ }
+
+ // Update the selection
+ this.target_.selectionModel_.beginChange();
+ for (var name in selectionFlag) {
+ var index = parseInt(name);
+ var flag = selectionFlag[name];
+ // The flag may be one of followings:
+ // - IN_LAST_SELECTION | IN_CURRENT_SELECTION
+ // - IN_LAST_SELECTION
+ // - IN_CURRENT_SELECTION
+ // - undefined
+
+ // If the flag equals to (IN_LAST_SELECTION | IN_CURRENT_SELECTION),
+ // this is included in both the last selection and the current selection.
+ // We have nothing to do for this item.
+
+ if (flag == DragSelector.SelectionFlag_.IN_LAST_SELECTION) {
+ // If the flag equals to IN_LAST_SELECTION,
+ // then the item is included in lastSelection but not in currentSelection.
+ // Revert the selection state to this.originalSelection_.
+ this.target_.selectionModel_.setIndexSelected(
+ index, this.originalSelection_.indexOf(index) != -1);
+ } else if (flag == DragSelector.SelectionFlag_.IN_CURRENT_SELECTION) {
+ // If the flag equals to IN_CURRENT_SELECTION,
+ // this is included in currentSelection but not in lastSelection.
+ this.target_.selectionModel_.setIndexSelected(index, true);
+ }
+ }
+ if (leadIndex != -1) {
+ this.target_.selectionModel_.leadIndex = leadIndex;
+ this.target_.selectionModel_.anchorIndex = leadIndex;
+ }
+ this.target_.selectionModel_.endChange();
+ this.lastSelection_ = currentSelection;
+
+ // Update the size of border
+ this.border_.style.left = borderBounds.left + 'px';
+ this.border_.style.top = borderBounds.top + 'px';
+ this.border_.style.width = borderBounds.width + 'px';
+ this.border_.style.height = borderBounds.height + 'px';
+};
+
+/**
+ * Handle the mouseup event.
+ * @private
+ * @param {MouseEvent} event The mouseup event.
+ */
+DragSelector.prototype.onMouseUp_ = function(event) {
+ this.onMouseMove_(event);
+ this.target_.removeChild(this.border_);
+ this.target_.ownerDocument.removeEventListener(
+ 'mousemove', this.onMouseMoveBound_, true);
+ this.target_.ownerDocument.removeEventListener(
+ 'mouseup', this.onMouseUpBound_, true);
+ cr.dispatchSimpleEvent(this.target_, 'dragselectionend');
+ this.target_.cachedBounds = null;
+ this.target_ = null;
+ // The target may select an item by reacting to the mouseup event.
+ // This suppress to the selecting behavior.
+ event.stopPropagation();
+};
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/drive_banners.js b/chromium/chrome/browser/resources/file_manager/foreground/js/drive_banners.js
new file mode 100644
index 00000000000..dc1cd924d7a
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/drive_banners.js
@@ -0,0 +1,660 @@
+// 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.
+
+'use strict';
+
+/**
+ * Responsible for showing following banners in the file list.
+ * - WelcomeBanner
+ * - AuthFailBanner
+ * @param {DirectoryModel} directoryModel The model.
+ * @param {VolumeManagerWrapper} volumeManager The manager.
+ * @param {DOMDocument} document HTML document.
+ * @param {boolean} showOffers True if we should show offer banners.
+ * @constructor
+ */
+function FileListBannerController(
+ directoryModel, volumeManager, document, showOffers) {
+ this.directoryModel_ = directoryModel;
+ this.volumeManager_ = volumeManager;
+ this.document_ = document;
+ this.showOffers_ = showOffers;
+ this.driveEnabled_ = false;
+
+ this.initializeWelcomeBanner_();
+ this.privateOnDirectoryChangedBound_ =
+ this.privateOnDirectoryChanged_.bind(this);
+
+ var handler = this.checkSpaceAndMaybeShowWelcomeBanner_.bind(this);
+ this.directoryModel_.addEventListener('scan-completed', handler);
+ this.directoryModel_.addEventListener('rescan-completed', handler);
+ this.directoryModel_.addEventListener('directory-changed',
+ this.onDirectoryChanged_.bind(this));
+
+ this.unmountedPanel_ = this.document_.querySelector('#unmounted-panel');
+ this.volumeManager_.volumeInfoList.addEventListener(
+ 'splice', this.onVolumeInfoListSplice_.bind(this));
+ this.volumeManager_.addEventListener('drive-connection-changed',
+ this.onDriveConnectionChanged_.bind(this));
+
+ chrome.storage.onChanged.addListener(this.onStorageChange_.bind(this));
+ this.welcomeHeaderCounter_ = WELCOME_HEADER_COUNTER_LIMIT;
+ this.warningDismissedCounter_ = 0;
+ chrome.storage.local.get([WELCOME_HEADER_COUNTER_KEY, WARNING_DISMISSED_KEY],
+ function(values) {
+ this.welcomeHeaderCounter_ =
+ parseInt(values[WELCOME_HEADER_COUNTER_KEY]) || 0;
+ this.warningDismissedCounter_ =
+ parseInt(values[WARNING_DISMISSED_KEY]) || 0;
+ }.bind(this));
+
+ this.authFailedBanner_ =
+ this.document_.querySelector('#drive-auth-failed-warning');
+ var authFailedText = this.authFailedBanner_.querySelector('.drive-text');
+ authFailedText.innerHTML = util.htmlUnescape(str('DRIVE_NOT_REACHED'));
+ authFailedText.querySelector('a').addEventListener('click', function(e) {
+ chrome.fileBrowserPrivate.logoutUserForReauthentication();
+ e.preventDefault();
+ });
+ this.maybeShowAuthFailBanner_();
+}
+
+/**
+ * FileListBannerController extends cr.EventTarget.
+ */
+FileListBannerController.prototype.__proto__ = cr.EventTarget.prototype;
+
+/**
+ * Key in localStorage to keep number of times the Drive Welcome
+ * banner has shown.
+ */
+var WELCOME_HEADER_COUNTER_KEY = 'driveWelcomeHeaderCounter';
+
+// If the warning was dismissed before, this key stores the quota value
+// (as of the moment of dismissal).
+// If the warning was never dismissed or was reset this key stores 0.
+var WARNING_DISMISSED_KEY = 'driveSpaceWarningDismissed';
+
+/**
+ * Maximum times Drive Welcome banner could have shown.
+ */
+var WELCOME_HEADER_COUNTER_LIMIT = 25;
+
+/**
+ * Initializes the banner to promote DRIVE.
+ * This method must be called before any of showing banner functions, and
+ * also before registering them as callbacks.
+ * @private
+ */
+FileListBannerController.prototype.initializeWelcomeBanner_ = function() {
+ this.usePromoWelcomeBanner_ = !util.boardIs('x86-mario') &&
+ !util.boardIs('x86-zgb') &&
+ !util.boardIs('x86-alex');
+};
+
+/**
+ * @param {number} value How many times the Drive Welcome header banner
+ * has shown.
+ * @private
+ */
+FileListBannerController.prototype.setWelcomeHeaderCounter_ = function(value) {
+ var values = {};
+ values[WELCOME_HEADER_COUNTER_KEY] = value;
+ chrome.storage.local.set(values);
+};
+
+/**
+ * @param {number} value How many times the low space warning has dismissed.
+ * @private
+ */
+FileListBannerController.prototype.setWarningDismissedCounter_ =
+ function(value) {
+ var values = {};
+ values[WARNING_DISMISSED_KEY] = value;
+ chrome.storage.local.set(values);
+};
+
+/**
+ * chrome.storage.onChanged event handler.
+ * @param {Object.<string, Object>} changes Changes values.
+ * @param {string} areaName "local" or "sync".
+ * @private
+ */
+FileListBannerController.prototype.onStorageChange_ = function(changes,
+ areaName) {
+ if (areaName == 'local' && WELCOME_HEADER_COUNTER_KEY in changes) {
+ this.welcomeHeaderCounter_ = changes[WELCOME_HEADER_COUNTER_KEY].newValue;
+ }
+ if (areaName == 'local' && WARNING_DISMISSED_KEY in changes) {
+ this.warningDismissedCounter_ = changes[WARNING_DISMISSED_KEY].newValue;
+ }
+};
+
+/**
+ * Invoked when the drive connection status is change in the volume manager.
+ * @private
+ */
+FileListBannerController.prototype.onDriveConnectionChanged_ = function() {
+ this.maybeShowAuthFailBanner_();
+};
+
+/**
+ * @param {string} type 'none'|'page'|'header'.
+ * @param {string} messageId Resource ID of the message.
+ * @private
+ */
+FileListBannerController.prototype.prepareAndShowWelcomeBanner_ =
+ function(type, messageId) {
+ this.showWelcomeBanner_(type);
+
+ var container = this.document_.querySelector('.drive-welcome.' + type);
+ if (container.firstElementChild)
+ return; // Do not re-create.
+
+ if (!this.document_.querySelector('link[drive-welcome-style]')) {
+ var style = this.document_.createElement('link');
+ style.rel = 'stylesheet';
+ style.href = 'foreground/css/drive_welcome.css';
+ style.setAttribute('drive-welcome-style', '');
+ this.document_.head.appendChild(style);
+ }
+
+ var wrapper = util.createChild(container, 'drive-welcome-wrapper');
+ util.createChild(wrapper, 'drive-welcome-icon');
+
+ var close = util.createChild(wrapper, 'cr-dialog-close');
+ close.addEventListener('click', this.closeWelcomeBanner_.bind(this));
+
+ var message = util.createChild(wrapper, 'drive-welcome-message');
+
+ var title = util.createChild(message, 'drive-welcome-title');
+
+ var text = util.createChild(message, 'drive-welcome-text');
+ text.innerHTML = str(messageId);
+
+ var links = util.createChild(message, 'drive-welcome-links');
+
+ var more;
+ if (this.usePromoWelcomeBanner_) {
+ var welcomeTitle = str('DRIVE_WELCOME_TITLE_ALTERNATIVE');
+ if (util.boardIs('link'))
+ welcomeTitle = str('DRIVE_WELCOME_TITLE_ALTERNATIVE_1TB');
+ title.textContent = welcomeTitle;
+ more = util.createChild(links,
+ 'drive-welcome-button drive-welcome-start', 'a');
+ more.textContent = str('DRIVE_WELCOME_CHECK_ELIGIBILITY');
+ more.href = str('GOOGLE_DRIVE_REDEEM_URL');
+ } else {
+ title.textContent = str('DRIVE_WELCOME_TITLE');
+ more = util.createChild(links, 'plain-link', 'a');
+ more.textContent = str('DRIVE_LEARN_MORE');
+ more.href = str('GOOGLE_DRIVE_OVERVIEW_URL');
+ }
+ more.tabIndex = '13'; // See: go/filesapp-tabindex.
+ more.target = '_blank';
+
+ var dismiss;
+ if (this.usePromoWelcomeBanner_)
+ dismiss = util.createChild(links, 'drive-welcome-button');
+ else
+ dismiss = util.createChild(links, 'plain-link');
+
+ dismiss.classList.add('drive-welcome-dismiss');
+ dismiss.textContent = str('DRIVE_WELCOME_DISMISS');
+ dismiss.addEventListener('click', this.closeWelcomeBanner_.bind(this));
+
+ this.previousDirWasOnDrive_ = false;
+};
+
+/**
+ * Show or hide the "Low Google Drive space" warning.
+ * @param {boolean} show True if the box need to be shown.
+ * @param {Object} sizeStats Size statistics. Should be defined when showing the
+ * warning.
+ * @private
+ */
+FileListBannerController.prototype.showLowDriveSpaceWarning_ =
+ function(show, sizeStats) {
+ var box = this.document_.querySelector('#volume-space-warning');
+
+ // Avoid showing two banners.
+ // TODO(kaznacheev): Unify the low space warning and the promo header.
+ if (show)
+ this.cleanupWelcomeBanner_();
+
+ if (box.hidden == !show)
+ return;
+
+ if (this.warningDismissedCounter_) {
+ if (this.warningDismissedCounter_ ==
+ sizeStats.totalSize && // Quota had not changed
+ sizeStats.remainingSize / sizeStats.totalSize < 0.15) {
+ // Since the last dismissal decision the quota has not changed AND
+ // the user did not free up significant space. Obey the dismissal.
+ show = false;
+ } else {
+ // Forget the dismissal. Warning will be shown again.
+ this.setWarningDismissedCounter_(0);
+ }
+ }
+
+ box.textContent = '';
+ if (show) {
+ var icon = this.document_.createElement('div');
+ icon.className = 'drive-icon';
+ box.appendChild(icon);
+
+ var text = this.document_.createElement('div');
+ text.className = 'drive-text';
+ text.textContent = strf('DRIVE_SPACE_AVAILABLE_LONG',
+ util.bytesToString(sizeStats.remainingSize));
+ box.appendChild(text);
+
+ var link = this.document_.createElement('a');
+ link.className = 'plain-link';
+ link.textContent = str('DRIVE_BUY_MORE_SPACE_LINK');
+ link.href = str('GOOGLE_DRIVE_BUY_STORAGE_URL');
+ link.target = '_blank';
+ box.appendChild(link);
+
+ var close = this.document_.createElement('div');
+ close.className = 'cr-dialog-close';
+ box.appendChild(close);
+ close.addEventListener('click', function(total) {
+ window.localStorage[WARNING_DISMISSED_KEY] = total;
+ box.hidden = true;
+ this.requestRelayout_(100);
+ }.bind(this, sizeStats.totalSize));
+ }
+
+ if (box.hidden != !show) {
+ box.hidden = !show;
+ this.requestRelayout_(100);
+ }
+};
+/**
+ * Closes the Drive Welcome banner.
+ * @private
+ */
+FileListBannerController.prototype.closeWelcomeBanner_ = function() {
+ this.cleanupWelcomeBanner_();
+ // Stop showing the welcome banner.
+ this.setWelcomeHeaderCounter_(WELCOME_HEADER_COUNTER_LIMIT);
+};
+
+/**
+ * Shows or hides the welcome banner for drive.
+ * @private
+ */
+FileListBannerController.prototype.checkSpaceAndMaybeShowWelcomeBanner_ =
+ function() {
+ if (!this.isOnCurrentProfileDrive()) {
+ // We are not on the drive file system. Do not show (close) the welcome
+ // banner.
+ this.cleanupWelcomeBanner_();
+ this.previousDirWasOnDrive_ = false;
+ return;
+ }
+
+ var driveVolume = this.volumeManager_.getCurrentProfileVolumeInfo(
+ util.VolumeType.DRIVE);
+ if (this.welcomeHeaderCounter_ >= WELCOME_HEADER_COUNTER_LIMIT ||
+ !driveVolume || driveVolume.error) {
+ // The banner is already shown enough times or the drive FS is not mounted.
+ // So, do nothing here.
+ return;
+ }
+
+ if (!this.showOffers_) {
+ // Because it is not necessary to show the offer, set
+ // |usePromoWelcomeBanner_| false here. Note that it probably should be able
+ // to do this in the constructor, but there remains non-trivial path,
+ // which may be causes |usePromoWelcomeBanner_| == true's behavior even
+ // if |showOffers_| is false.
+ // TODO(hidehiko): Make sure if it is expected or not, and simplify
+ // |showOffers_| if possible.
+ this.usePromoWelcomeBanner_ = false;
+ }
+
+ // Perform asynchronous tasks in parallel.
+ var group = new AsyncUtil.Group();
+
+ // Choose the offer basing on the board name. The default one is 100 GB.
+ var offerSize = 100; // In GB.
+ var offerServiceId = 'drive.cros.echo.1';
+
+ if (util.boardIs('link')) {
+ offerSize = 1024; // 1 TB.
+ offerServiceId = 'drive.cros.echo.2';
+ }
+
+ // If the offer has been checked, then do not show the promo anymore.
+ group.add(function(onCompleted) {
+ chrome.echoPrivate.getOfferInfo(offerServiceId, function(offerInfo) {
+ // If the offer has not been checked, then an error is raised.
+ if (!chrome.runtime.lastError)
+ this.usePromoWelcomeBanner_ = false;
+ onCompleted();
+ }.bind(this));
+ }.bind(this));
+
+ if (this.usePromoWelcomeBanner_) {
+ // getSizeStats for Drive file system accesses to the server, so we should
+ // minimize the invocation.
+ group.add(function(onCompleted) {
+ chrome.fileBrowserPrivate.getSizeStats(
+ util.makeFilesystemUrl(this.directoryModel_.getCurrentRootPath()),
+ function(result) {
+ if (result && result.totalSize >= offerSize * 1024 * 1024 * 1024)
+ this.usePromoWelcomeBanner_ = false;
+ onCompleted();
+ }.bind(this));
+ }.bind(this));
+ }
+
+ group.run(this.maybeShowWelcomeBanner_.bind(this));
+};
+
+/**
+ * Decides which banner should be shown, and show it. This method is designed
+ * to be called only from checkSpaceAndMaybeShowWelcomeBanner_.
+ * @private
+ */
+FileListBannerController.prototype.maybeShowWelcomeBanner_ = function() {
+ if (this.directoryModel_.getFileList().length == 0 &&
+ this.welcomeHeaderCounter_ == 0) {
+ // Only show the full page banner if the header banner was never shown.
+ // Do not increment the counter.
+ // The timeout below is required because sometimes another
+ // 'rescan-completed' event arrives shortly with non-empty file list.
+ setTimeout(function() {
+ if (this.isOnCurrentProfileDrive() && this.welcomeHeaderCounter_ == 0) {
+ this.prepareAndShowWelcomeBanner_('page', 'DRIVE_WELCOME_TEXT_LONG');
+ }
+ }.bind(this), 2000);
+ } else {
+ // We do not want to increment the counter when the user navigates
+ // between different directories on Drive, but we increment the counter
+ // once anyway to prevent the full page banner from showing.
+ if (!this.previousDirWasOnDrive_ || this.welcomeHeaderCounter_ == 0) {
+ this.setWelcomeHeaderCounter_(this.welcomeHeaderCounter_ + 1);
+ this.prepareAndShowWelcomeBanner_('header', 'DRIVE_WELCOME_TEXT_SHORT');
+ }
+ }
+ this.previousDirWasOnDrive_ = true;
+};
+
+/**
+ * @return {boolean} True if current directory is on Drive root of current
+ * profile.
+ */
+FileListBannerController.prototype.isOnCurrentProfileDrive = function() {
+ var entry = this.directoryModel_.getCurrentDirEntry();
+ if (!entry || util.isFakeEntry(entry))
+ return false;
+ var locationInfo = this.volumeManager_.getLocationInfo(entry);
+ return locationInfo &&
+ locationInfo.rootType === RootType.DRIVE &&
+ locationInfo.volumeInfo.profile.isCurrentProfile;
+};
+
+/**
+ * Shows the Drive Welcome banner.
+ * @param {string} type 'page'|'head'|'none'.
+ * @private
+ */
+FileListBannerController.prototype.showWelcomeBanner_ = function(type) {
+ var container = this.document_.querySelector('.dialog-container');
+ if (container.getAttribute('drive-welcome') != type) {
+ container.setAttribute('drive-welcome', type);
+ this.requestRelayout_(200); // Resize only after the animation is done.
+ }
+};
+
+/**
+ * Update the UI when the current directory changes.
+ *
+ * @param {Event} event The directory-changed event.
+ * @private
+ */
+FileListBannerController.prototype.onDirectoryChanged_ = function(event) {
+ var rootVolume = this.volumeManager_.getVolumeInfo(event.newDirEntry);
+ var previousRootVolume = event.previousDirEntry ?
+ this.volumeManager_.getVolumeInfo(event.previousDirEntry) : null;
+
+ // Show (or hide) the low space warning.
+ this.maybeShowLowSpaceWarning_(rootVolume);
+
+ // Add or remove listener to show low space warning, if necessary.
+ var isLowSpaceWarningTarget = this.isLowSpaceWarningTarget_(rootVolume);
+ if (isLowSpaceWarningTarget !==
+ this.isLowSpaceWarningTarget_(previousRootVolume)) {
+ if (isLowSpaceWarningTarget) {
+ chrome.fileBrowserPrivate.onDirectoryChanged.addListener(
+ this.privateOnDirectoryChangedBound_);
+ } else {
+ chrome.fileBrowserPrivate.onDirectoryChanged.removeListener(
+ this.privateOnDirectoryChangedBound_);
+ }
+ }
+
+ if (!this.isOnCurrentProfileDrive()) {
+ this.cleanupWelcomeBanner_();
+ this.authFailedBanner_.hidden = true;
+ }
+
+ this.updateDriveUnmountedPanel_();
+ if (this.isOnCurrentProfileDrive()) {
+ this.unmountedPanel_.classList.remove('retry-enabled');
+ this.maybeShowAuthFailBanner_();
+ }
+};
+
+/**
+ * @param {VolumeInfo} volumeInfo Volume info to be checked.
+ * @return {boolean} true if the file system specified by |root| is a target
+ * to show low space warning. Otherwise false.
+ * @private
+ */
+FileListBannerController.prototype.isLowSpaceWarningTarget_ =
+ function(volumeInfo) {
+ return volumeInfo &&
+ volumeInfo.profile.isCurrentProfile &&
+ (volumeInfo.volumeType === util.VolumeType.DOWNLOADS ||
+ volumeInfo.volumeType === util.VolumeType.DRIVE);
+};
+
+/**
+ * Callback which is invoked when the file system has been changed.
+ * @param {Object} event chrome.fileBrowserPrivate.onDirectoryChanged event.
+ * @private
+ */
+FileListBannerController.prototype.privateOnDirectoryChanged_ = function(
+ event) {
+ if (!this.directoryModel_.getCurrentDirEntry())
+ return;
+
+ var currentDirEntry = this.directoryModel_.getCurrentDirEntry();
+ var currentVolume = currentDirEntry &&
+ this.volumeManager_.getVolumeInfo(currentDirEntry);
+ var eventVolume = this.volumeManager_.getVolumeInfo(event.entry);
+ if (currentVolume === eventVolume) {
+ // The file system we are currently on is changed.
+ // So, check the free space.
+ this.maybeShowLowSpaceWarning_(currentVolume);
+ }
+};
+
+/**
+ * Shows or hides the low space warning.
+ * @param {VolumeInfo} volume Type of volume, which we are interested in.
+ * @private
+ */
+FileListBannerController.prototype.maybeShowLowSpaceWarning_ = function(
+ volume) {
+ // TODO(kaznacheev): Unify the two low space warning.
+ var threshold = 0;
+ switch (volume.volumeType) {
+ case util.VolumeType.DOWNLOADS:
+ this.showLowDriveSpaceWarning_(false);
+ threshold = 0.2;
+ break;
+ case util.VolumeType.DRIVE:
+ this.showLowDownloadsSpaceWarning_(false);
+ threshold = 0.1;
+ break;
+ default:
+ // If the current file system is neither the DOWNLOAD nor the DRIVE,
+ // just hide the warning.
+ this.showLowDownloadsSpaceWarning_(false);
+ this.showLowDriveSpaceWarning_(false);
+ return;
+ }
+
+ chrome.fileBrowserPrivate.getSizeStats(
+ volume.getDisplayRootDirectoryURL(),
+ function(sizeStats) {
+ var currentVolume = this.volumeManager_.getVolumeInfo(
+ this.directoryModel_.getCurrentDirEntry());
+ if (volume !== currentVolume) {
+ // This happens when the current directory is moved during requesting
+ // the file system size. Just ignore it.
+ return;
+ }
+ // sizeStats is undefined, if some error occurs.
+ if (!sizeStats || sizeStats.totalSize == 0)
+ return;
+
+ var remainingRatio = sizeStats.remainingSize / sizeStats.totalSize;
+ var isLowDiskSpace = remainingRatio < threshold;
+ if (volume.volumeType === util.VolumeType.DOWNLOADS)
+ this.showLowDownloadsSpaceWarning_(isLowDiskSpace);
+ else
+ this.showLowDriveSpaceWarning_(isLowDiskSpace, sizeStats);
+ }.bind(this));
+};
+
+/**
+ * removes the Drive Welcome banner.
+ * @private
+ */
+FileListBannerController.prototype.cleanupWelcomeBanner_ = function() {
+ this.showWelcomeBanner_('none');
+};
+
+/**
+ * Notifies the file manager what layout must be recalculated.
+ * @param {number} delay In milliseconds.
+ * @private
+ */
+FileListBannerController.prototype.requestRelayout_ = function(delay) {
+ var self = this;
+ setTimeout(function() {
+ cr.dispatchSimpleEvent(self, 'relayout');
+ }, delay);
+};
+
+/**
+ * Show or hide the "Low disk space" warning.
+ * @param {boolean} show True if the box need to be shown.
+ * @private
+ */
+FileListBannerController.prototype.showLowDownloadsSpaceWarning_ =
+ function(show) {
+ var box = this.document_.querySelector('.downloads-warning');
+
+ if (box.hidden == !show) return;
+
+ if (show) {
+ var html = util.htmlUnescape(str('DOWNLOADS_DIRECTORY_WARNING'));
+ box.innerHTML = html;
+ var link = box.querySelector('a');
+ link.href = str('DOWNLOADS_LOW_SPACE_WARNING_HELP_URL');
+ link.target = '_blank';
+ } else {
+ box.innerHTML = '';
+ }
+
+ box.hidden = !show;
+ this.requestRelayout_(100);
+};
+
+/**
+ * Creates contents for the DRIVE unmounted panel.
+ * @private
+ */
+FileListBannerController.prototype.ensureDriveUnmountedPanelInitialized_ =
+ function() {
+ var panel = this.unmountedPanel_;
+ if (panel.firstElementChild)
+ return;
+
+ var create = function(parent, tag, className, opt_textContent) {
+ var div = panel.ownerDocument.createElement(tag);
+ div.className = className;
+ div.textContent = opt_textContent || '';
+ parent.appendChild(div);
+ return div;
+ };
+
+ var loading = create(panel, 'div', 'loading', str('DRIVE_LOADING'));
+ var spinnerBox = create(loading, 'div', 'spinner-box');
+ create(spinnerBox, 'div', 'spinner');
+ create(panel, 'div', 'error', str('DRIVE_CANNOT_REACH'));
+
+ var learnMore = create(panel, 'a', 'learn-more plain-link',
+ str('DRIVE_LEARN_MORE'));
+ learnMore.href = str('GOOGLE_DRIVE_ERROR_HELP_URL');
+ learnMore.target = '_blank';
+};
+
+/**
+ * Called when volume info list is updated.
+ * @param {Event} event Splice event data on volume info list.
+ * @private
+ */
+FileListBannerController.prototype.onVolumeInfoListSplice_ = function(event) {
+ var isDriveVolume = function(volumeInfo) {
+ return volumeInfo.volumeType === util.VolumeType.DRIVE;
+ };
+ if (event.removed.some(isDriveVolume) || event.added.some(isDriveVolume))
+ this.updateDriveUnmountedPanel_();
+};
+
+/**
+ * Shows the panel when current directory is DRIVE and it's unmounted.
+ * Hides it otherwise. The panel shows spinner if DRIVE is mounting or
+ * an error message if it failed.
+ * @private
+ */
+FileListBannerController.prototype.updateDriveUnmountedPanel_ = function() {
+ var node = this.document_.body;
+ if (this.isOnCurrentProfileDrive()) {
+ var driveVolume = this.volumeManager_.getCurrentProfileVolumeInfo(
+ util.VolumeType.DRIVE);
+ if (driveVolume && driveVolume.error) {
+ this.ensureDriveUnmountedPanelInitialized_();
+ this.unmountedPanel_.classList.add('retry-enabled');
+ } else {
+ this.unmountedPanel_.classList.remove('retry-enabled');
+ }
+ node.setAttribute('drive', status);
+ } else {
+ node.removeAttribute('drive');
+ }
+};
+
+/**
+ * Updates the visibility of Drive Connection Warning banner, retrieving the
+ * current connection information.
+ * @private
+ */
+FileListBannerController.prototype.maybeShowAuthFailBanner_ = function() {
+ var connection = this.volumeManager_.getDriveConnectionState();
+ var showDriveNotReachedMessage =
+ this.isOnCurrentProfileDrive() &&
+ connection.type == util.DriveConnectionType.OFFLINE &&
+ connection.reason == util.DriveConnectionReason.NOT_READY;
+ this.authFailedBanner_.hidden = !showDriveNotReachedMessage;
+};
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/error_counter.js b/chromium/chrome/browser/resources/file_manager/foreground/js/error_counter.js
new file mode 100644
index 00000000000..8cec3bba03f
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/error_counter.js
@@ -0,0 +1,16 @@
+// Copyright 2013 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.
+
+'use strict';
+
+/**
+ * This variable is checked in SelectFileDialogExtensionBrowserTest.
+ * @type {number}
+ */
+window.JSErrorCount = 0;
+
+/**
+ * Count uncaught exceptions.
+ */
+window.onerror = function() { window.JSErrorCount++; };
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/error_dialog.js b/chromium/chrome/browser/resources/file_manager/foreground/js/error_dialog.js
new file mode 100644
index 00000000000..5dd3545d3c7
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/error_dialog.js
@@ -0,0 +1,29 @@
+// Copyright 2013 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.
+
+'use strict';
+
+/**
+ * @param {HTMLElement} parentNode Node to be parent for this dialog.
+ * @constructor
+ */
+function ErrorDialog(parentNode) {
+ cr.ui.dialogs.BaseDialog.call(this, parentNode);
+}
+
+ErrorDialog.prototype = {
+ __proto__: cr.ui.dialogs.BaseDialog.prototype
+};
+
+/**
+ * One-time initialization of DOM.
+ * @private
+ */
+ErrorDialog.prototype.initDom_ = function() {
+ cr.ui.dialogs.BaseDialog.prototype.initDom_.call(this);
+ this.frame_.classList.add('error-dialog-frame');
+ var img = this.document_.createElement('div');
+ img.className = 'error-dialog-img';
+ this.frame_.insertBefore(img, this.text_);
+};
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/file_grid.js b/chromium/chrome/browser/resources/file_manager/foreground/js/file_grid.js
new file mode 100644
index 00000000000..b275cd0e9f5
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/file_grid.js
@@ -0,0 +1,302 @@
+// 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.
+
+'use strict';
+
+/**
+ * FileGrid constructor.
+ *
+ * Represents grid for the Grid Vew in the File Manager.
+ * @constructor
+ * @extends {cr.ui.Grid}
+ */
+
+function FileGrid() {
+ throw new Error('Use FileGrid.decorate');
+}
+
+/**
+ * Thumbnail quality.
+ * @enum {number}
+ */
+FileGrid.ThumbnailQuality = {
+ LOW: 0,
+ HIGH: 1
+};
+
+/**
+ * Inherits from cr.ui.Grid.
+ */
+FileGrid.prototype.__proto__ = cr.ui.Grid.prototype;
+
+/**
+ * Decorates an HTML element to be a FileGrid.
+ * @param {HTMLElement} self The grid to decorate.
+ * @param {MetadataCache} metadataCache Metadata cache to find entries
+ * metadata.
+ */
+FileGrid.decorate = function(self, metadataCache) {
+ cr.ui.Grid.decorate(self);
+ self.__proto__ = FileGrid.prototype;
+ self.metadataCache_ = metadataCache;
+
+ self.scrollBar_ = new MainPanelScrollBar();
+ self.scrollBar_.initialize(self.parentNode, self);
+ self.setBottomMarginForPanel(0);
+
+ self.itemConstructor = function(entry) {
+ var item = self.ownerDocument.createElement('LI');
+ FileGrid.Item.decorate(item, entry, self);
+ return item;
+ };
+
+ self.relayoutAggregation_ =
+ new AsyncUtil.Aggregation(self.relayoutImmediately_.bind(self));
+};
+
+/**
+ * Updates items to reflect metadata changes.
+ * @param {string} type Type of metadata changed.
+ * @param {Object.<string, Object>} props Map from entry URLs to metadata props.
+ */
+FileGrid.prototype.updateListItemsMetadata = function(type, props) {
+ var boxes = this.querySelectorAll('.img-container');
+ for (var i = 0; i < boxes.length; i++) {
+ var box = boxes[i];
+ var entry = this.dataModel.item(this.getListItemAncestor(box));
+ if (!entry || !(entry.toURL() in props))
+ continue;
+
+ FileGrid.decorateThumbnailBox(box,
+ entry,
+ this.metadataCache_,
+ ThumbnailLoader.FillMode.FIT,
+ FileGrid.ThumbnailQuality.HIGH);
+ }
+};
+
+/**
+ * Redraws the UI. Skips multiple consecutive calls.
+ */
+FileGrid.prototype.relayout = function() {
+ this.relayoutAggregation_.run();
+};
+
+/**
+ * Redraws the UI immediately.
+ * @private
+ */
+FileGrid.prototype.relayoutImmediately_ = function() {
+ this.startBatchUpdates();
+ this.columns = 0;
+ this.redraw();
+ this.endBatchUpdates();
+ cr.dispatchSimpleEvent(this, 'relayout');
+};
+
+/**
+ * Decorates thumbnail.
+ * @param {HTMLElement} li List item.
+ * @param {Entry} entry Entry to render a thumbnail for.
+ * @param {MetadataCache} metadataCache To retrieve metadata.
+ */
+FileGrid.decorateThumbnail = function(li, entry, metadataCache) {
+ li.className = 'thumbnail-item';
+ if (entry)
+ filelist.decorateListItem(li, entry, metadataCache);
+
+ var frame = li.ownerDocument.createElement('div');
+ frame.className = 'thumbnail-frame';
+ li.appendChild(frame);
+
+ var box = li.ownerDocument.createElement('div');
+ if (entry) {
+ FileGrid.decorateThumbnailBox(box,
+ entry,
+ metadataCache,
+ ThumbnailLoader.FillMode.AUTO,
+ FileGrid.ThumbnailQuality.HIGH);
+ }
+ frame.appendChild(box);
+
+ var bottom = li.ownerDocument.createElement('div');
+ bottom.className = 'thumbnail-bottom';
+ bottom.appendChild(filelist.renderFileNameLabel(li.ownerDocument, entry));
+ frame.appendChild(bottom);
+};
+
+/**
+ * Decorates the box containing a centered thumbnail image.
+ *
+ * @param {HTMLDivElement} box Box to decorate.
+ * @param {Entry} entry Entry which thumbnail is generating for.
+ * @param {MetadataCache} metadataCache To retrieve metadata.
+ * @param {ThumbnailLoader.FillMode} fillMode Fill mode.
+ * @param {FileGrid.ThumbnailQuality} quality Thumbnail quality.
+ * @param {function(HTMLElement)=} opt_imageLoadCallback Callback called when
+ * the image has been loaded before inserting it into the DOM.
+ */
+FileGrid.decorateThumbnailBox = function(
+ box, entry, metadataCache, fillMode, quality, opt_imageLoadCallback) {
+ box.className = 'img-container';
+ if (entry.isDirectory) {
+ box.setAttribute('generic-thumbnail', 'folder');
+ if (opt_imageLoadCallback)
+ setTimeout(opt_imageLoadCallback, 0, null /* callback parameter */);
+ return;
+ }
+
+ var metadataTypes = 'thumbnail|filesystem';
+
+ if (FileType.isOnDrive(entry)) {
+ metadataTypes += '|drive';
+ } else {
+ // TODO(dgozman): If we ask for 'media' for a Drive file we fall into an
+ // infinite loop.
+ metadataTypes += '|media';
+ }
+
+ // Drive provides high quality thumbnails via USE_EMBEDDED, however local
+ // images usually provide very tiny thumbnails, therefore USE_EMBEDDE can't
+ // be used to obtain high quality output.
+ var useEmbedded;
+ switch (quality) {
+ case FileGrid.ThumbnailQuality.LOW:
+ useEmbedded = ThumbnailLoader.UseEmbedded.USE_EMBEDDED;
+ break;
+ case FileGrid.ThumbnailQuality.HIGH:
+ useEmbedded = FileType.isOnDrive(entry) ?
+ ThumbnailLoader.UseEmbedded.USE_EMBEDDED :
+ ThumbnailLoader.UseEmbedded.NO_EMBEDDED;
+ break;
+ }
+
+ metadataCache.get(entry, metadataTypes,
+ function(metadata) {
+ new ThumbnailLoader(entry.toURL(),
+ ThumbnailLoader.LoaderType.IMAGE,
+ metadata,
+ undefined, // opt_mediaType
+ useEmbedded).
+ load(box,
+ fillMode,
+ ThumbnailLoader.OptimizationMode.DISCARD_DETACHED,
+ opt_imageLoadCallback);
+ });
+};
+
+/**
+ * Item for the Grid View.
+ * @constructor
+ */
+FileGrid.Item = function() {
+ throw new Error();
+};
+
+/**
+ * Inherits from cr.ui.ListItem.
+ */
+FileGrid.Item.prototype.__proto__ = cr.ui.ListItem.prototype;
+
+Object.defineProperty(FileGrid.Item.prototype, 'label', {
+ /**
+ * @this {FileGrid.Item}
+ * @return {string} Label of the item.
+ */
+ get: function() {
+ return this.querySelector('filename-label').textContent;
+ }
+});
+
+/**
+ * @param {Element} li List item element.
+ * @param {Entry} entry File entry.
+ * @param {FileGrid} grid Owner.
+ */
+FileGrid.Item.decorate = function(li, entry, grid) {
+ li.__proto__ = FileGrid.Item.prototype;
+ FileGrid.decorateThumbnail(li, entry, grid.metadataCache_, true);
+
+ // Override the default role 'listitem' to 'option' to match the parent's
+ // role (listbox).
+ li.setAttribute('role', 'option');
+};
+
+/**
+ * Sets the margin height for the transparent preview panel at the bottom.
+ * @param {number} margin Margin to be set in px.
+ */
+FileGrid.prototype.setBottomMarginForPanel = function(margin) {
+ // +20 bottom margin is needed to match the bottom margin size with the
+ // margin between its items.
+ this.style.paddingBottom = (margin + 20) + 'px';
+ this.scrollBar_.setBottomMarginForPanel(margin);
+};
+
+/**
+ * Obtains if the drag selection should be start or not by referring the mouse
+ * event.
+ * @param {MouseEvent} event Drag start event.
+ * @return {boolean} True if the mouse is hit to the background of the list.
+ */
+FileGrid.prototype.shouldStartDragSelection = function(event) {
+ var pos = DragSelector.getScrolledPosition(this, event);
+ return this.getHitElements(pos.x, pos.y).length == 0;
+};
+
+/**
+ * Obtains the column/row index that the coordinate points.
+ * @param {number} coordinate Vertical/horizontal coodinate value that points
+ * column/row.
+ * @param {number} step Length from a column/row to the next one.
+ * @param {number} threshold Threshold that determinds whether 1 offset is added
+ * to the return value or not. This is used in order to handle the margin of
+ * column/row.
+ * @return {number} Index of hit column/row.
+ * @private
+ */
+FileGrid.prototype.getHitIndex_ = function(coordinate, step, threshold) {
+ var index = ~~(coordinate / step);
+ return (coordinate % step >= threshold) ? index + 1 : index;
+};
+
+/**
+ * Obtains the index list of elements that are hit by the point or the
+ * rectangle.
+ *
+ * We should match its argument interface with FileList.getHitElements.
+ *
+ * @param {number} x X coordinate value.
+ * @param {number} y Y coordinate value.
+ * @param {=number} opt_width Width of the coordinate.
+ * @param {=number} opt_height Height of the coordinate.
+ * @return {Array.<number>} Index list of hit elements.
+ */
+FileGrid.prototype.getHitElements = function(x, y, opt_width, opt_height) {
+ var currentSelection = [];
+ var right = x + (opt_width || 0);
+ var bottom = y + (opt_height || 0);
+ var itemMetrics = this.measureItem();
+ var horizontalStartIndex = this.getHitIndex_(
+ x, itemMetrics.width, itemMetrics.width - itemMetrics.marginRight);
+ var horizontalEndIndex = Math.min(this.columns, this.getHitIndex_(
+ right, itemMetrics.width, itemMetrics.marginLeft));
+ var verticalStartIndex = this.getHitIndex_(
+ y, itemMetrics.height, itemMetrics.height - itemMetrics.bottom);
+ var verticalEndIndex = this.getHitIndex_(
+ bottom, itemMetrics.height, itemMetrics.marginTop);
+ for (var verticalIndex = verticalStartIndex;
+ verticalIndex < verticalEndIndex;
+ verticalIndex++) {
+ var indexBase = this.getFirstItemInRow(verticalIndex);
+ for (var horizontalIndex = horizontalStartIndex;
+ horizontalIndex < horizontalEndIndex;
+ horizontalIndex++) {
+ var index = indexBase + horizontalIndex;
+ if (0 <= index && index < this.dataModel.length)
+ currentSelection.push(index);
+ }
+ }
+ return currentSelection;
+};
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/file_manager.js b/chromium/chrome/browser/resources/file_manager/foreground/js/file_manager.js
new file mode 100644
index 00000000000..201a4f6ad2d
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/file_manager.js
@@ -0,0 +1,3688 @@
+// 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.
+
+'use strict';
+
+/**
+ * FileManager constructor.
+ *
+ * FileManager objects encapsulate the functionality of the file selector
+ * dialogs, as well as the full screen file manager application (though the
+ * latter is not yet implemented).
+ *
+ * @constructor
+ */
+function FileManager() {
+ this.initializeQueue_ = new AsyncUtil.Group();
+
+ /**
+ * Current list type.
+ * @type {ListType}
+ * @private
+ */
+ this.listType_ = null;
+
+ /**
+ * Whether to suppress the focus moving or not.
+ * This is used to filter out focusing by mouse.
+ * @type {boolean}
+ * @private
+ */
+ this.suppressFocus_ = false;
+
+ /**
+ * SelectionHandler.
+ * @type {SelectionHandler}
+ * @private
+ */
+ this.selectionHandler_ = null;
+}
+
+/**
+ * Maximum delay in milliseconds for updating thumbnails in the bottom panel
+ * to mitigate flickering. If images load faster then the delay they replace
+ * old images smoothly. On the other hand we don't want to keep old images
+ * too long.
+ *
+ * @type {number}
+ * @const
+ */
+FileManager.THUMBNAIL_SHOW_DELAY = 100;
+
+FileManager.prototype = {
+ __proto__: cr.EventTarget.prototype,
+ get directoryModel() {
+ return this.directoryModel_;
+ },
+ get navigationList() {
+ return this.navigationList_;
+ },
+ get document() {
+ return this.document_;
+ },
+ get fileTransferController() {
+ return this.fileTransferController_;
+ },
+ get backgroundPage() {
+ return this.backgroundPage_;
+ },
+ get volumeManager() {
+ return this.volumeManager_;
+ }
+};
+
+/**
+ * Unload the file manager.
+ * Used by background.js (when running in the packaged mode).
+ */
+function unload() {
+ fileManager.onBeforeUnload_();
+ fileManager.onUnload_();
+}
+
+/**
+ * List of dialog types.
+ *
+ * Keep this in sync with FileManagerDialog::GetDialogTypeAsString, except
+ * FULL_PAGE which is specific to this code.
+ *
+ * @enum {string}
+ */
+var DialogType = {
+ SELECT_FOLDER: 'folder',
+ SELECT_UPLOAD_FOLDER: 'upload-folder',
+ SELECT_SAVEAS_FILE: 'saveas-file',
+ SELECT_OPEN_FILE: 'open-file',
+ SELECT_OPEN_MULTI_FILE: 'open-multi-file',
+ FULL_PAGE: 'full-page'
+};
+
+/**
+ * @param {string} type Dialog type.
+ * @return {boolean} Whether the type is modal.
+ */
+DialogType.isModal = function(type) {
+ return type == DialogType.SELECT_FOLDER ||
+ type == DialogType.SELECT_UPLOAD_FOLDER ||
+ type == DialogType.SELECT_SAVEAS_FILE ||
+ type == DialogType.SELECT_OPEN_FILE ||
+ type == DialogType.SELECT_OPEN_MULTI_FILE;
+};
+
+/**
+ * @param {string} type Dialog type.
+ * @return {boolean} Whether the type is open dialog.
+ */
+DialogType.isOpenDialog = function(type) {
+ return type == DialogType.SELECT_OPEN_FILE ||
+ type == DialogType.SELECT_OPEN_MULTI_FILE;
+};
+
+/**
+ * @param {string} type Dialog type.
+ * @return {boolean} Whether the type is folder selection dialog.
+ */
+DialogType.isFolderDialog = function(type) {
+ return type == DialogType.SELECT_FOLDER ||
+ type == DialogType.SELECT_UPLOAD_FOLDER;
+};
+
+/**
+ * Bottom margin of the list and tree for transparent preview panel.
+ * @const
+ */
+var BOTTOM_MARGIN_FOR_PREVIEW_PANEL_PX = 52;
+
+// Anonymous "namespace".
+(function() {
+
+ // Private variables and helper functions.
+
+ /**
+ * Number of milliseconds in a day.
+ */
+ var MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000;
+
+ /**
+ * Some UI elements react on a single click and standard double click handling
+ * leads to confusing results. We ignore a second click if it comes soon
+ * after the first.
+ */
+ var DOUBLE_CLICK_TIMEOUT = 200;
+
+ /**
+ * Update the element to display the information about remaining space for
+ * the storage.
+ * @param {!Element} spaceInnerBar Block element for a percentage bar
+ * representing the remaining space.
+ * @param {!Element} spaceInfoLabel Inline element to contain the message.
+ * @param {!Element} spaceOuterBar Block element around the percentage bar.
+ */
+ var updateSpaceInfo = function(
+ sizeStatsResult, spaceInnerBar, spaceInfoLabel, spaceOuterBar) {
+ spaceInnerBar.removeAttribute('pending');
+ if (sizeStatsResult) {
+ var sizeStr = util.bytesToString(sizeStatsResult.remainingSize);
+ spaceInfoLabel.textContent = strf('SPACE_AVAILABLE', sizeStr);
+
+ var usedSpace =
+ sizeStatsResult.totalSize - sizeStatsResult.remainingSize;
+ spaceInnerBar.style.width =
+ (100 * usedSpace / sizeStatsResult.totalSize) + '%';
+
+ spaceOuterBar.hidden = false;
+ } else {
+ spaceOuterBar.hidden = true;
+ spaceInfoLabel.textContent = str('FAILED_SPACE_INFO');
+ }
+ };
+
+ // Public statics.
+
+ FileManager.ListType = {
+ DETAIL: 'detail',
+ THUMBNAIL: 'thumb'
+ };
+
+ FileManager.prototype.initPreferences_ = function(callback) {
+ var group = new AsyncUtil.Group();
+
+ // DRIVE preferences should be initialized before creating DirectoryModel
+ // to rebuild the roots list.
+ group.add(this.getPreferences_.bind(this));
+
+ // Get startup preferences.
+ this.viewOptions_ = {};
+ group.add(function(done) {
+ util.platform.getPreference(this.startupPrefName_, function(value) {
+ // Load the global default options.
+ try {
+ this.viewOptions_ = JSON.parse(value);
+ } catch (ignore) {}
+ // Override with window-specific options.
+ if (window.appState && window.appState.viewOptions) {
+ for (var key in window.appState.viewOptions) {
+ if (window.appState.viewOptions.hasOwnProperty(key))
+ this.viewOptions_[key] = window.appState.viewOptions[key];
+ }
+ }
+ done();
+ }.bind(this));
+ }.bind(this));
+
+ // Get the command line option.
+ group.add(function(done) {
+ chrome.commandLinePrivate.hasSwitch(
+ 'file-manager-show-checkboxes', function(flag) {
+ this.showCheckboxes_ = flag;
+ done();
+ }.bind(this));
+ }.bind(this));
+
+ // TODO(yoshiki): Remove the flag when the feature is launched.
+ this.enableExperimentalWebstoreIntegration_ = true;
+
+ group.run(callback);
+ };
+
+ /**
+ * One time initialization for the file system and related things.
+ *
+ * @param {function()} callback Completion callback.
+ * @private
+ */
+ FileManager.prototype.initFileSystemUI_ = function(callback) {
+ this.table_.startBatchUpdates();
+ this.grid_.startBatchUpdates();
+
+ this.initFileList_();
+ this.setupCurrentDirectory_();
+
+ // PyAuto tests monitor this state by polling this variable
+ this.__defineGetter__('workerInitialized_', function() {
+ return this.metadataCache_.isInitialized();
+ }.bind(this));
+
+ this.initDateTimeFormatters_();
+
+ var self = this;
+
+ // Get the 'allowRedeemOffers' preference before launching
+ // FileListBannerController.
+ this.getPreferences_(function(pref) {
+ /** @type {boolean} */
+ var showOffers = pref['allowRedeemOffers'];
+ self.bannersController_ = new FileListBannerController(
+ self.directoryModel_, self.volumeManager_, self.document_,
+ showOffers);
+ self.bannersController_.addEventListener('relayout',
+ self.onResize_.bind(self));
+ });
+
+ var dm = this.directoryModel_;
+ dm.addEventListener('directory-changed',
+ this.onDirectoryChanged_.bind(this));
+ dm.addEventListener('begin-update-files', function() {
+ self.currentList_.startBatchUpdates();
+ });
+ dm.addEventListener('end-update-files', function() {
+ self.restoreItemBeingRenamed_();
+ self.currentList_.endBatchUpdates();
+ });
+ dm.addEventListener('scan-started', this.onScanStarted_.bind(this));
+ dm.addEventListener('scan-completed', this.onScanCompleted_.bind(this));
+ dm.addEventListener('scan-failed', this.onScanCancelled_.bind(this));
+ dm.addEventListener('scan-cancelled', this.onScanCancelled_.bind(this));
+ dm.addEventListener('scan-updated', this.onScanUpdated_.bind(this));
+ dm.addEventListener('rescan-completed',
+ this.onRescanCompleted_.bind(this));
+
+ this.directoryTree_.addEventListener('change', function() {
+ this.ensureDirectoryTreeItemNotBehindPreviewPanel_();
+ }.bind(this));
+
+ var stateChangeHandler =
+ this.onPreferencesChanged_.bind(this);
+ chrome.fileBrowserPrivate.onPreferencesChanged.addListener(
+ stateChangeHandler);
+ stateChangeHandler();
+
+ var driveConnectionChangedHandler =
+ this.onDriveConnectionChanged_.bind(this);
+ this.volumeManager_.addEventListener('drive-connection-changed',
+ driveConnectionChangedHandler);
+ driveConnectionChangedHandler();
+
+ // Set the initial focus.
+ this.refocus();
+ // Set it as a fallback when there is no focus.
+ this.document_.addEventListener('focusout', function(e) {
+ setTimeout(function() {
+ // When there is no focus, the active element is the <body>.
+ if (this.document_.activeElement == this.document_.body)
+ this.refocus();
+ }.bind(this), 0);
+ }.bind(this));
+
+ this.initDataTransferOperations_();
+
+ this.initContextMenus_();
+ this.initCommands_();
+
+ this.updateFileTypeFilter_();
+
+ this.selectionHandler_.onFileSelectionChanged();
+
+ this.table_.endBatchUpdates();
+ this.grid_.endBatchUpdates();
+
+ callback();
+ };
+
+ /**
+ * If |item| in the directory tree is behind the preview panel, scrolls up the
+ * parent view and make the item visible. This should be called when:
+ * - the selected item is changed in the directory tree.
+ * - the visibility of the the preview panel is changed.
+ *
+ * @private
+ */
+ FileManager.prototype.ensureDirectoryTreeItemNotBehindPreviewPanel_ =
+ function() {
+ var selectedSubTree = this.directoryTree_.selectedItem;
+ if (!selectedSubTree)
+ return;
+ var item = selectedSubTree.rowElement;
+ var parentView = this.directoryTree_;
+
+ var itemRect = item.getBoundingClientRect();
+ if (!itemRect)
+ return;
+
+ var listRect = parentView.getBoundingClientRect();
+ if (!listRect)
+ return;
+
+ var previewPanel = this.dialogDom_.querySelector('.preview-panel');
+ var previewPanelRect = previewPanel.getBoundingClientRect();
+ var panelHeight = previewPanelRect ? previewPanelRect.height : 0;
+
+ var itemBottom = itemRect.bottom;
+ var listBottom = listRect.bottom - panelHeight;
+
+ if (itemBottom > listBottom) {
+ var scrollOffset = itemBottom - listBottom;
+ parentView.scrollTop += scrollOffset;
+ }
+ };
+
+ /**
+ * @private
+ */
+ FileManager.prototype.initDateTimeFormatters_ = function() {
+ var use12hourClock = !this.preferences_['use24hourClock'];
+ this.table_.setDateTimeFormat(use12hourClock);
+ };
+
+ /**
+ * @private
+ */
+ FileManager.prototype.initDataTransferOperations_ = function() {
+ this.fileOperationManager_ = FileOperationManagerWrapper.getInstance(
+ this.backgroundPage_);
+
+ // CopyManager are required for 'Delete' operation in
+ // Open and Save dialogs. But drag-n-drop and copy-paste are not needed.
+ if (this.dialogType != DialogType.FULL_PAGE) return;
+
+ // TODO(hidehiko): Extract FileOperationManager related code from
+ // FileManager to simplify it.
+ this.onCopyProgressBound_ = this.onCopyProgress_.bind(this);
+ this.fileOperationManager_.addEventListener(
+ 'copy-progress', this.onCopyProgressBound_);
+
+ this.onEntryChangedBound_ = this.onEntryChanged_.bind(this);
+ this.fileOperationManager_.addEventListener(
+ 'entry-changed', this.onEntryChangedBound_);
+
+ var controller = this.fileTransferController_ =
+ new FileTransferController(this.document_,
+ this.fileOperationManager_,
+ this.metadataCache_,
+ this.directoryModel_);
+ controller.attachDragSource(this.table_.list);
+ controller.attachFileListDropTarget(this.table_.list);
+ controller.attachDragSource(this.grid_);
+ controller.attachFileListDropTarget(this.grid_);
+ controller.attachTreeDropTarget(this.directoryTree_);
+ controller.attachNavigationListDropTarget(this.navigationList_, true);
+ controller.attachCopyPasteHandlers();
+ controller.addEventListener('selection-copied',
+ this.blinkSelection.bind(this));
+ controller.addEventListener('selection-cut',
+ this.blinkSelection.bind(this));
+ };
+
+ /**
+ * One-time initialization of context menus.
+ * @private
+ */
+ FileManager.prototype.initContextMenus_ = function() {
+ this.fileContextMenu_ = this.dialogDom_.querySelector('#file-context-menu');
+ cr.ui.Menu.decorate(this.fileContextMenu_);
+
+ cr.ui.contextMenuHandler.setContextMenu(this.grid_, this.fileContextMenu_);
+ cr.ui.contextMenuHandler.setContextMenu(this.table_.querySelector('.list'),
+ this.fileContextMenu_);
+ cr.ui.contextMenuHandler.setContextMenu(
+ this.document_.querySelector('.drive-welcome.page'),
+ this.fileContextMenu_);
+
+ this.rootsContextMenu_ =
+ this.dialogDom_.querySelector('#roots-context-menu');
+ cr.ui.Menu.decorate(this.rootsContextMenu_);
+ this.navigationList_.setContextMenu(this.rootsContextMenu_);
+
+ this.directoryTreeContextMenu_ =
+ this.dialogDom_.querySelector('#directory-tree-context-menu');
+ cr.ui.Menu.decorate(this.directoryTreeContextMenu_);
+ this.directoryTree_.contextMenuForSubitems = this.directoryTreeContextMenu_;
+
+ this.textContextMenu_ =
+ this.dialogDom_.querySelector('#text-context-menu');
+ cr.ui.Menu.decorate(this.textContextMenu_);
+
+ this.gearButton_ = this.dialogDom_.querySelector('#gear-button');
+ this.gearButton_.addEventListener('menushow',
+ this.refreshRemainingSpace_.bind(this,
+ false /* Without loading caption. */));
+ this.dialogDom_.querySelector('#gear-menu').menuItemSelector =
+ 'menuitem, hr';
+ cr.ui.decorate(this.gearButton_, cr.ui.MenuButton);
+
+ if (this.dialogType == DialogType.FULL_PAGE) {
+ // This is to prevent the buttons from stealing focus on mouse down.
+ var preventFocus = function(event) {
+ event.preventDefault();
+ };
+
+ var maximizeButton = this.dialogDom_.querySelector('#maximize-button');
+ maximizeButton.addEventListener('click', this.onMaximize.bind(this));
+ maximizeButton.addEventListener('mousedown', preventFocus);
+
+ var closeButton = this.dialogDom_.querySelector('#close-button');
+ closeButton.addEventListener('click', this.onClose.bind(this));
+ closeButton.addEventListener('mousedown', preventFocus);
+ }
+
+ this.syncButton.checkable = true;
+ this.hostedButton.checkable = true;
+ this.detailViewButton_.checkable = true;
+ this.thumbnailViewButton_.checkable = true;
+
+ if (util.platform.runningInBrowser()) {
+ // Suppresses the default context menu.
+ this.dialogDom_.addEventListener('contextmenu', function(e) {
+ e.preventDefault();
+ e.stopPropagation();
+ });
+ }
+ };
+
+ FileManager.prototype.onMaximize = function() {
+ var appWindow = chrome.app.window.current();
+ if (appWindow.isMaximized())
+ appWindow.restore();
+ else
+ appWindow.maximize();
+ };
+
+ FileManager.prototype.onClose = function() {
+ window.close();
+ };
+
+ /**
+ * One-time initialization of commands.
+ * @private
+ */
+ FileManager.prototype.initCommands_ = function() {
+ this.commandHandler = new CommandHandler(this);
+
+ // TODO(hirono): Move the following block to the UI part.
+ var commandButtons = this.dialogDom_.querySelectorAll('button[command]');
+ for (var j = 0; j < commandButtons.length; j++)
+ CommandButton.decorate(commandButtons[j]);
+
+ var inputs = this.dialogDom_.querySelectorAll(
+ 'input[type=text], input[type=search], textarea');
+ for (var i = 0; i < inputs.length; i++) {
+ cr.ui.contextMenuHandler.setContextMenu(inputs[i], this.textContextMenu_);
+ this.registerInputCommands_(inputs[i]);
+ }
+
+ cr.ui.contextMenuHandler.setContextMenu(this.renameInput_,
+ this.textContextMenu_);
+ this.registerInputCommands_(this.renameInput_);
+ this.document_.addEventListener('command',
+ this.setNoHover_.bind(this, true));
+ };
+
+ /**
+ * Registers cut, copy, paste and delete commands on input element.
+ *
+ * @param {Node} node Text input element to register on.
+ * @private
+ */
+ FileManager.prototype.registerInputCommands_ = function(node) {
+ CommandUtil.forceDefaultHandler(node, 'cut');
+ CommandUtil.forceDefaultHandler(node, 'copy');
+ CommandUtil.forceDefaultHandler(node, 'paste');
+ CommandUtil.forceDefaultHandler(node, 'delete');
+ node.addEventListener('keydown', function(e) {
+ var key = util.getKeyModifiers(e) + e.keyCode;
+ if (key === '190' /* '/' */ || key === '191' /* '.' */) {
+ // If this key event is propagated, this is handled search command,
+ // which calls 'preventDefault' method.
+ e.stopPropagation();
+ }
+ });
+ };
+
+ /**
+ * Entry point of the initialization.
+ * This method is called from main.js.
+ */
+ FileManager.prototype.initializeCore = function() {
+ this.initializeQueue_.add(this.initGeneral_.bind(this), [], 'initGeneral');
+ this.initializeQueue_.add(this.initBackgroundPage_.bind(this),
+ [], 'initBackgroundPage');
+ this.initializeQueue_.add(this.initPreferences_.bind(this),
+ ['initGeneral'], 'initPreferences');
+ this.initializeQueue_.add(this.initVolumeManager_.bind(this),
+ ['initGeneral', 'initBackgroundPage'],
+ 'initVolumeManager');
+
+ this.initializeQueue_.run();
+ };
+
+ FileManager.prototype.initializeUI = function(dialogDom, callback) {
+ this.dialogDom_ = dialogDom;
+ this.document_ = this.dialogDom_.ownerDocument;
+
+ this.initializeQueue_.add(
+ this.initEssentialUI_.bind(this),
+ ['initGeneral', 'initBackgroundPage'],
+ 'initEssentialUI');
+ this.initializeQueue_.add(this.initAdditionalUI_.bind(this),
+ ['initEssentialUI'], 'initAdditionalUI');
+ this.initializeQueue_.add(
+ this.initFileSystemUI_.bind(this),
+ ['initAdditionalUI', 'initPreferences'], 'initFileSystemUI');
+
+ // Run again just in case if all pending closures have completed and the
+ // queue has stopped and monitor the completion.
+ this.initializeQueue_.run(callback);
+ };
+
+ /**
+ * Initializes general purpose basic things, which are used by other
+ * initializing methods.
+ *
+ * @param {function()} callback Completion callback.
+ * @private
+ */
+ FileManager.prototype.initGeneral_ = function(callback) {
+ // Initialize the application state.
+ if (window.appState) {
+ this.params_ = window.appState.params || {};
+ this.defaultPath = window.appState.defaultPath;
+ } else {
+ this.params_ = location.search ?
+ JSON.parse(decodeURIComponent(location.search.substr(1))) :
+ {};
+ this.defaultPath = this.params_.defaultPath;
+ }
+
+ // Initialize the member variables that depend this.params_.
+ this.dialogType = this.params_.type || DialogType.FULL_PAGE;
+ this.startupPrefName_ = 'file-manager-' + this.dialogType;
+ this.fileTypes_ = this.params_.typeList || [];
+
+ callback();
+ };
+
+ /**
+ * Initialize the background page.
+ * @param {function()} callback Completion callback.
+ * @private
+ */
+ FileManager.prototype.initBackgroundPage_ = function(callback) {
+ chrome.runtime.getBackgroundPage(function(backgroundPage) {
+ this.backgroundPage_ = backgroundPage;
+ this.backgroundPage_.background.ready(function() {
+ loadTimeData.data = this.backgroundPage_.background.stringData;
+ callback();
+ }.bind(this));
+ }.bind(this));
+ };
+
+ /**
+ * Initializes the VolumeManager instance.
+ * @param {function()} callback Completion callback.
+ * @private
+ */
+ FileManager.prototype.initVolumeManager_ = function(callback) {
+ // Auto resolving to local path does not work for folders (e.g., dialog for
+ // loading unpacked extensions).
+ var noLocalPathResolution = DialogType.isFolderDialog(this.params_.type);
+
+ // If this condition is false, VolumeManagerWrapper hides all drive
+ // related event and data, even if Drive is enabled on preference.
+ // In other words, even if Drive is disabled on preference but Files.app
+ // should show Drive when it is re-enabled, then the value should be set to
+ // true.
+ // Note that the Drive enabling preference change is listened by
+ // DriveIntegrationService, so here we don't need to take care about it.
+ var driveEnabled =
+ !noLocalPathResolution || !this.params_.shouldReturnLocalPath;
+ this.volumeManager_ = new VolumeManagerWrapper(
+ driveEnabled, this.backgroundPage_);
+ callback();
+ };
+
+ /**
+ * One time initialization of the Files.app's essential UI elements. These
+ * elements will be shown to the user. Only visible elements should be
+ * initialized here. Any heavy operation should be avoided. Files.app's
+ * window is shown at the end of this routine.
+ *
+ * @param {function()} callback Completion callback.
+ * @private
+ */
+ FileManager.prototype.initEssentialUI_ = function(callback) {
+ // Optional list of file types.
+ metrics.recordEnum('Create', this.dialogType,
+ [DialogType.SELECT_FOLDER,
+ DialogType.SELECT_UPLOAD_FOLDER,
+ DialogType.SELECT_SAVEAS_FILE,
+ DialogType.SELECT_OPEN_FILE,
+ DialogType.SELECT_OPEN_MULTI_FILE,
+ DialogType.FULL_PAGE]);
+
+ // Create the metadata cache.
+ this.metadataCache_ = MetadataCache.createFull();
+
+ // Create the root view of FileManager.
+ this.ui_ = new FileManagerUI(this.dialogDom_, this.dialogType);
+ this.fileTypeSelector_ = this.ui_.fileTypeSelector;
+ this.okButton_ = this.ui_.okButton;
+ this.cancelButton_ = this.ui_.cancelButton;
+
+ // Show the window as soon as the UI pre-initialization is done.
+ if (this.dialogType == DialogType.FULL_PAGE &&
+ !util.platform.runningInBrowser()) {
+ chrome.app.window.current().show();
+ setTimeout(callback, 100); // Wait until the animation is finished.
+ } else {
+ callback();
+ }
+ };
+
+ /**
+ * One-time initialization of dialogs.
+ * @private
+ */
+ FileManager.prototype.initDialogs_ = function() {
+ // Initialize the dialog.
+ this.ui_.initDialogs();
+ FileManagerDialogBase.setFileManager(this);
+
+ // Obtains the dialog instances from FileManagerUI.
+ // TODO(hirono): Remove the properties from the FileManager class.
+ this.error = this.ui_.errorDialog;
+ this.alert = this.ui_.alertDialog;
+ this.confirm = this.ui_.confirmDialog;
+ this.prompt = this.ui_.promptDialog;
+ this.shareDialog_ = this.ui_.shareDialog;
+ this.defaultTaskPicker = this.ui_.defaultTaskPicker;
+ this.suggestAppsDialog = this.ui_.suggestAppsDialog;
+ };
+
+ /**
+ * One-time initialization of various DOM nodes. Loads the additional DOM
+ * elements visible to the user. Initialize here elements, which are expensive
+ * or hidden in the beginning.
+ *
+ * @param {function()} callback Completion callback.
+ * @private
+ */
+ FileManager.prototype.initAdditionalUI_ = function(callback) {
+ this.initDialogs_();
+ this.ui_.initAdditionalUI();
+
+ this.dialogDom_.addEventListener('drop', function(e) {
+ // Prevent opening an URL by dropping it onto the page.
+ e.preventDefault();
+ });
+
+ this.dialogDom_.addEventListener('click',
+ this.onExternalLinkClick_.bind(this));
+ // Cache nodes we'll be manipulating.
+ var dom = this.dialogDom_;
+
+ this.filenameInput_ = dom.querySelector('#filename-input-box input');
+ this.taskItems_ = dom.querySelector('#tasks');
+
+ this.table_ = dom.querySelector('.detail-table');
+ this.grid_ = dom.querySelector('.thumbnail-grid');
+ this.spinner_ = dom.querySelector('#list-container > .spinner-layer');
+ this.showSpinner_(true);
+
+ // Check the option to hide the selecting checkboxes.
+ this.table_.showCheckboxes = this.showCheckboxes_;
+
+ var fullPage = this.dialogType == DialogType.FULL_PAGE;
+ FileTable.decorate(this.table_, this.metadataCache_, fullPage);
+ FileGrid.decorate(this.grid_, this.metadataCache_);
+
+ this.previewPanel_ = new PreviewPanel(
+ dom.querySelector('.preview-panel'),
+ DialogType.isOpenDialog(this.dialogType) ?
+ PreviewPanel.VisibilityType.ALWAYS_VISIBLE :
+ PreviewPanel.VisibilityType.AUTO,
+ this.metadataCache_,
+ this.volumeManager_);
+ this.previewPanel_.addEventListener(
+ PreviewPanel.Event.VISIBILITY_CHANGE,
+ this.onPreviewPanelVisibilityChange_.bind(this));
+ this.previewPanel_.initialize();
+
+ this.previewPanel_.breadcrumbs.addEventListener(
+ 'pathclick', this.onBreadcrumbClick_.bind(this));
+
+ // Initialize progress center panel.
+ this.progressCenterPanel_ = new ProgressCenterPanel(
+ dom.querySelector('#progress-center'));
+ this.backgroundPage_.background.progressCenter.addPanel(
+ this.progressCenterPanel_);
+
+ this.document_.addEventListener('keydown', this.onKeyDown_.bind(this));
+
+ // This capturing event is only used to distinguish focusing using
+ // keyboard from focusing using mouse.
+ this.document_.addEventListener('mousedown', function() {
+ this.suppressFocus_ = true;
+ }.bind(this), true);
+
+ this.renameInput_ = this.document_.createElement('input');
+ this.renameInput_.className = 'rename';
+
+ this.renameInput_.addEventListener(
+ 'keydown', this.onRenameInputKeyDown_.bind(this));
+ this.renameInput_.addEventListener(
+ 'blur', this.onRenameInputBlur_.bind(this));
+
+ // TODO(hirono): Rename the handler after creating the DialogFooter class.
+ this.filenameInput_.addEventListener(
+ 'input', this.onFilenameInputInput_.bind(this));
+ this.filenameInput_.addEventListener(
+ 'keydown', this.onFilenameInputKeyDown_.bind(this));
+ this.filenameInput_.addEventListener(
+ 'focus', this.onFilenameInputFocus_.bind(this));
+
+ this.listContainer_ = this.dialogDom_.querySelector('#list-container');
+ this.listContainer_.addEventListener(
+ 'keydown', this.onListKeyDown_.bind(this));
+ this.listContainer_.addEventListener(
+ 'keypress', this.onListKeyPress_.bind(this));
+ this.listContainer_.addEventListener(
+ 'mousemove', this.onListMouseMove_.bind(this));
+
+ this.okButton_.addEventListener('click', this.onOk_.bind(this));
+ this.onCancelBound_ = this.onCancel_.bind(this);
+ this.cancelButton_.addEventListener('click', this.onCancelBound_);
+
+ this.decorateSplitter(
+ this.dialogDom_.querySelector('#navigation-list-splitter'));
+ this.decorateSplitter(
+ this.dialogDom_.querySelector('#middlebar-splitter'));
+
+ this.dialogContainer_ = this.dialogDom_.querySelector('.dialog-container');
+
+ this.syncButton = this.dialogDom_.querySelector('#drive-sync-settings');
+ this.syncButton.addEventListener('click', this.onDrivePrefClick_.bind(
+ this, 'cellularDisabled', false /* not inverted */));
+
+ this.hostedButton = this.dialogDom_.querySelector('#drive-hosted-settings');
+ this.hostedButton.addEventListener('click', this.onDrivePrefClick_.bind(
+ this, 'hostedFilesDisabled', true /* inverted */));
+
+ this.detailViewButton_ =
+ this.dialogDom_.querySelector('#detail-view');
+ this.detailViewButton_.addEventListener('activate',
+ this.onDetailViewButtonClick_.bind(this));
+
+ this.thumbnailViewButton_ =
+ this.dialogDom_.querySelector('#thumbnail-view');
+ this.thumbnailViewButton_.addEventListener('activate',
+ this.onThumbnailViewButtonClick_.bind(this));
+
+ cr.ui.ComboButton.decorate(this.taskItems_);
+ this.taskItems_.showMenu = function(shouldSetFocus) {
+ // Prevent the empty menu from opening.
+ if (!this.menu.length)
+ return;
+ cr.ui.ComboButton.prototype.showMenu.call(this, shouldSetFocus);
+ };
+ this.taskItems_.addEventListener('select',
+ this.onTaskItemClicked_.bind(this));
+
+ this.dialogDom_.ownerDocument.defaultView.addEventListener(
+ 'resize', this.onResize_.bind(this));
+
+ this.filePopup_ = null;
+
+ this.searchBoxWrapper_ = this.ui_.searchBox.element;
+ this.searchBox_ = this.ui_.searchBox.inputElement;
+ this.searchBox_.addEventListener(
+ 'input', this.onSearchBoxUpdate_.bind(this));
+ this.ui_.searchBox.clearButton.addEventListener(
+ 'click', this.onSearchClearButtonClick_.bind(this));
+
+ this.lastSearchQuery_ = '';
+
+ this.autocompleteList_ = this.ui_.searchBox.autocompleteList;
+ this.autocompleteList_.requestSuggestions =
+ this.requestAutocompleteSuggestions_.bind(this);
+
+ // Instead, open the suggested item when Enter key is pressed or
+ // mouse-clicked.
+ this.autocompleteList_.handleEnterKeydown = function(event) {
+ this.openAutocompleteSuggestion_();
+ this.lastAutocompleteQuery_ = '';
+ this.autocompleteList_.suggestions = [];
+ }.bind(this);
+ this.autocompleteList_.addEventListener('mousedown', function(event) {
+ this.openAutocompleteSuggestion_();
+ this.lastAutocompleteQuery_ = '';
+ this.autocompleteList_.suggestions = [];
+ }.bind(this));
+
+ this.defaultActionMenuItem_ =
+ this.dialogDom_.querySelector('#default-action');
+
+ this.openWithCommand_ =
+ this.dialogDom_.querySelector('#open-with');
+
+ this.driveBuyMoreStorageCommand_ =
+ this.dialogDom_.querySelector('#drive-buy-more-space');
+
+ this.defaultActionMenuItem_.addEventListener('click',
+ this.dispatchSelectionAction_.bind(this));
+
+ this.initFileTypeFilter_();
+
+ util.addIsFocusedMethod();
+
+ // Populate the static localized strings.
+ i18nTemplate.process(this.document_, loadTimeData);
+
+ // Arrange the file list.
+ this.table_.normalizeColumns();
+ this.table_.redraw();
+
+ callback();
+ };
+
+ /**
+ * @private
+ */
+ FileManager.prototype.onBreadcrumbClick_ = function(event) {
+ // TODO(hirono): Use directoryModel#changeDirectoryEntry after implementing
+ // it.
+ if (event.entry === RootType.DRIVE_SHARED_WITH_ME)
+ this.directoryModel_.changeDirectory(RootDirectory.DRIVE_SHARED_WITH_ME);
+ else
+ this.directoryModel_.changeDirectory(event.entry.fullPath);
+ };
+
+ /**
+ * Constructs table and grid (heavy operation).
+ * @private
+ **/
+ FileManager.prototype.initFileList_ = function() {
+ // Always sharing the data model between the detail/thumb views confuses
+ // them. Instead we maintain this bogus data model, and hook it up to the
+ // view that is not in use.
+ this.emptyDataModel_ = new cr.ui.ArrayDataModel([]);
+ this.emptySelectionModel_ = new cr.ui.ListSelectionModel();
+
+ var singleSelection =
+ this.dialogType == DialogType.SELECT_OPEN_FILE ||
+ this.dialogType == DialogType.SELECT_FOLDER ||
+ this.dialogType == DialogType.SELECT_UPLOAD_FOLDER ||
+ this.dialogType == DialogType.SELECT_SAVEAS_FILE;
+
+ this.fileFilter_ = new FileFilter(
+ this.metadataCache_,
+ false /* Don't show dot files by default. */);
+
+ this.fileWatcher_ = new FileWatcher(this.metadataCache_);
+ this.fileWatcher_.addEventListener(
+ 'watcher-metadata-changed',
+ this.onWatcherMetadataChanged_.bind(this));
+
+ this.directoryModel_ = new DirectoryModel(
+ singleSelection,
+ this.fileFilter_,
+ this.fileWatcher_,
+ this.metadataCache_,
+ this.volumeManager_);
+
+ this.folderShortcutsModel_ = new FolderShortcutsDataModel();
+
+ this.selectionHandler_ = new FileSelectionHandler(this);
+
+ var dataModel = this.directoryModel_.getFileList();
+
+ this.table_.setupCompareFunctions(dataModel);
+
+ dataModel.addEventListener('permuted',
+ this.updateStartupPrefs_.bind(this));
+
+ this.directoryModel_.getFileListSelection().addEventListener('change',
+ this.selectionHandler_.onFileSelectionChanged.bind(
+ this.selectionHandler_));
+
+ this.initList_(this.grid_);
+ this.initList_(this.table_.list);
+
+ var fileListFocusBound = this.onFileListFocus_.bind(this);
+ var fileListBlurBound = this.onFileListBlur_.bind(this);
+
+ this.table_.list.addEventListener('focus', fileListFocusBound);
+ this.grid_.addEventListener('focus', fileListFocusBound);
+
+ this.table_.list.addEventListener('blur', fileListBlurBound);
+ this.grid_.addEventListener('blur', fileListBlurBound);
+
+ var dragStartBound = this.onDragStart_.bind(this);
+ this.table_.list.addEventListener('dragstart', dragStartBound);
+ this.grid_.addEventListener('dragstart', dragStartBound);
+
+ var dragEndBound = this.onDragEnd_.bind(this);
+ this.table_.list.addEventListener('dragend', dragEndBound);
+ this.grid_.addEventListener('dragend', dragEndBound);
+ // This event is published by DragSelector because drag end event is not
+ // published at the end of drag selection.
+ this.table_.list.addEventListener('dragselectionend', dragEndBound);
+ this.grid_.addEventListener('dragselectionend', dragEndBound);
+
+ // TODO(mtomasz, yoshiki): Create navigation list earlier, and here just
+ // attach the directory model.
+ this.initNavigationList_();
+
+ this.table_.addEventListener('column-resize-end',
+ this.updateStartupPrefs_.bind(this));
+
+ // Restore preferences.
+ this.directoryModel_.sortFileList(
+ this.viewOptions_.sortField || 'modificationTime',
+ this.viewOptions_.sortDirection || 'desc');
+ if (this.viewOptions_.columns) {
+ var cm = this.table_.columnModel;
+ for (var i = 0; i < cm.totalSize; i++) {
+ if (this.viewOptions_.columns[i] > 0)
+ cm.setWidth(i, this.viewOptions_.columns[i]);
+ }
+ }
+ this.setListType(this.viewOptions_.listType || FileManager.ListType.DETAIL);
+
+ this.textSearchState_ = {text: '', date: new Date()};
+ this.closeOnUnmount_ = (this.params_.action == 'auto-open');
+
+ if (this.closeOnUnmount_) {
+ this.volumeManager_.addEventListener('externally-unmounted',
+ this.onExternallyUnmounted_.bind(this));
+ }
+
+ // Update metadata to change 'Today' and 'Yesterday' dates.
+ var today = new Date();
+ today.setHours(0);
+ today.setMinutes(0);
+ today.setSeconds(0);
+ today.setMilliseconds(0);
+ setTimeout(this.dailyUpdateModificationTime_.bind(this),
+ today.getTime() + MILLISECONDS_IN_DAY - Date.now() + 1000);
+ };
+
+ /**
+ * @private
+ */
+ FileManager.prototype.initNavigationList_ = function() {
+ this.directoryTree_ = this.dialogDom_.querySelector('#directory-tree');
+ DirectoryTree.decorate(this.directoryTree_, this.directoryModel_);
+
+ this.navigationList_ = this.dialogDom_.querySelector('#navigation-list');
+ NavigationList.decorate(this.navigationList_,
+ this.volumeManager_,
+ this.directoryModel_);
+ this.navigationList_.fileManager = this;
+ this.navigationList_.dataModel = new NavigationListModel(
+ this.volumeManager_, this.folderShortcutsModel_);
+ };
+
+ /**
+ * @private
+ */
+ FileManager.prototype.updateMiddleBarVisibility_ = function() {
+ var entry = this.directoryModel_.getCurrentDirEntry();
+ if (!entry)
+ return;
+
+ var driveVolume = this.volumeManager_.getVolumeInfo(entry);
+ var visible =
+ DirectoryTreeUtil.isEligiblePathForDirectoryTree(entry.fullPath) &&
+ driveVolume && !driveVolume.error;
+ this.dialogDom_.
+ querySelector('.dialog-middlebar-contents').hidden = !visible;
+ this.dialogDom_.querySelector('#middlebar-splitter').hidden = !visible;
+ this.onResize_();
+ };
+
+ /**
+ * @private
+ */
+ FileManager.prototype.updateStartupPrefs_ = function() {
+ var sortStatus = this.directoryModel_.getFileList().sortStatus;
+ var prefs = {
+ sortField: sortStatus.field,
+ sortDirection: sortStatus.direction,
+ columns: [],
+ listType: this.listType_
+ };
+ var cm = this.table_.columnModel;
+ for (var i = 0; i < cm.totalSize; i++) {
+ prefs.columns.push(cm.getWidth(i));
+ }
+ // Save the global default.
+ util.platform.setPreference(this.startupPrefName_, JSON.stringify(prefs));
+
+ // Save the window-specific preference.
+ if (window.appState) {
+ window.appState.viewOptions = prefs;
+ util.saveAppState();
+ }
+ };
+
+ FileManager.prototype.refocus = function() {
+ var targetElement;
+ if (this.dialogType == DialogType.SELECT_SAVEAS_FILE)
+ targetElement = this.filenameInput_;
+ else
+ targetElement = this.currentList_;
+
+ // Hack: if the tabIndex is disabled, we can assume a modal dialog is
+ // shown. Focus to a button on the dialog instead.
+ if (!targetElement.hasAttribute('tabIndex') || targetElement.tabIndex == -1)
+ targetElement = document.querySelector('button:not([tabIndex="-1"])');
+
+ if (targetElement)
+ targetElement.focus();
+ };
+
+ /**
+ * File list focus handler. Used to select the top most element on the list
+ * if nothing was selected.
+ *
+ * @private
+ */
+ FileManager.prototype.onFileListFocus_ = function() {
+ // Do not select default item if focused using mouse.
+ if (this.suppressFocus_)
+ return;
+
+ var selection = this.getSelection();
+ if (!selection || selection.totalCount != 0)
+ return;
+
+ this.directoryModel_.selectIndex(0);
+ };
+
+ /**
+ * File list blur handler.
+ *
+ * @private
+ */
+ FileManager.prototype.onFileListBlur_ = function() {
+ this.suppressFocus_ = false;
+ };
+
+ /**
+ * Index of selected item in the typeList of the dialog params.
+ *
+ * @return {number} 1-based index of selected type or 0 if no type selected.
+ * @private
+ */
+ FileManager.prototype.getSelectedFilterIndex_ = function() {
+ var index = Number(this.fileTypeSelector_.selectedIndex);
+ if (index < 0) // Nothing selected.
+ return 0;
+ if (this.params_.includeAllFiles) // Already 1-based.
+ return index;
+ return index + 1; // Convert to 1-based;
+ };
+
+ FileManager.prototype.setListType = function(type) {
+ if (type && type == this.listType_)
+ return;
+
+ this.table_.list.startBatchUpdates();
+ this.grid_.startBatchUpdates();
+
+ // TODO(dzvorygin): style.display and dataModel setting order shouldn't
+ // cause any UI bugs. Currently, the only right way is first to set display
+ // style and only then set dataModel.
+
+ if (type == FileManager.ListType.DETAIL) {
+ this.table_.dataModel = this.directoryModel_.getFileList();
+ this.table_.selectionModel = this.directoryModel_.getFileListSelection();
+ this.table_.hidden = false;
+ this.grid_.hidden = true;
+ this.grid_.selectionModel = this.emptySelectionModel_;
+ this.grid_.dataModel = this.emptyDataModel_;
+ this.table_.hidden = false;
+ /** @type {cr.ui.List} */
+ this.currentList_ = this.table_.list;
+ this.detailViewButton_.setAttribute('checked', '');
+ this.thumbnailViewButton_.removeAttribute('checked');
+ this.detailViewButton_.setAttribute('disabled', '');
+ this.thumbnailViewButton_.removeAttribute('disabled');
+ } else if (type == FileManager.ListType.THUMBNAIL) {
+ this.grid_.dataModel = this.directoryModel_.getFileList();
+ this.grid_.selectionModel = this.directoryModel_.getFileListSelection();
+ this.grid_.hidden = false;
+ this.table_.hidden = true;
+ this.table_.selectionModel = this.emptySelectionModel_;
+ this.table_.dataModel = this.emptyDataModel_;
+ this.grid_.hidden = false;
+ /** @type {cr.ui.List} */
+ this.currentList_ = this.grid_;
+ this.thumbnailViewButton_.setAttribute('checked', '');
+ this.detailViewButton_.removeAttribute('checked');
+ this.thumbnailViewButton_.setAttribute('disabled', '');
+ this.detailViewButton_.removeAttribute('disabled');
+ } else {
+ throw new Error('Unknown list type: ' + type);
+ }
+
+ this.listType_ = type;
+ this.updateStartupPrefs_();
+ this.onResize_();
+
+ this.table_.list.endBatchUpdates();
+ this.grid_.endBatchUpdates();
+ };
+
+ /**
+ * Initialize the file list table or grid.
+ *
+ * @param {cr.ui.List} list The list.
+ * @private
+ */
+ FileManager.prototype.initList_ = function(list) {
+ // Overriding the default role 'list' to 'listbox' for better accessibility
+ // on ChromeOS.
+ list.setAttribute('role', 'listbox');
+ list.addEventListener('click', this.onDetailClick_.bind(this));
+ list.id = 'file-list';
+ };
+
+ /**
+ * @private
+ */
+ FileManager.prototype.onCopyProgress_ = function(event) {
+ if (event.reason == 'ERROR' &&
+ event.error.code == util.FileOperationErrorType.FILESYSTEM_ERROR &&
+ event.error.data.toDrive &&
+ event.error.data.code == FileError.QUOTA_EXCEEDED_ERR) {
+ this.alert.showHtml(
+ strf('DRIVE_SERVER_OUT_OF_SPACE_HEADER'),
+ strf('DRIVE_SERVER_OUT_OF_SPACE_MESSAGE',
+ decodeURIComponent(
+ event.error.data.sourceFileUrl.split('/').pop()),
+ str('GOOGLE_DRIVE_BUY_STORAGE_URL')));
+ }
+ };
+
+ /**
+ * Handler of file manager operations. Called when an entry has been
+ * changed.
+ * This updates directory model to reflect operation result immediately (not
+ * waiting for directory update event). Also, preloads thumbnails for the
+ * images of new entries.
+ * See also FileOperationManager.EventRouter.
+ *
+ * @param {Event} event An event for the entry change.
+ * @private
+ */
+ FileManager.prototype.onEntryChanged_ = function(event) {
+ var kind = event.kind;
+ var entry = event.entry;
+ this.directoryModel_.onEntryChanged(kind, entry);
+ this.selectionHandler_.onFileSelectionChanged();
+
+ if (kind == util.EntryChangedKind.CREATE && FileType.isImage(entry)) {
+ // Preload a thumbnail if the new copied entry an image.
+ var metadata = entry.getMetadata(function(metadata) {
+ var url = entry.toURL();
+ var thumbnailLoader_ = new ThumbnailLoader(
+ url,
+ ThumbnailLoader.LoaderType.CANVAS,
+ metadata,
+ undefined, // Media type.
+ FileType.isOnDrive(url) ?
+ ThumbnailLoader.UseEmbedded.USE_EMBEDDED :
+ ThumbnailLoader.UseEmbedded.NO_EMBEDDED,
+ 10); // Very low priority.
+ thumbnailLoader_.loadDetachedImage(function(success) {});
+ });
+ }
+ };
+
+ /**
+ * Fills the file type list or hides it.
+ * @private
+ */
+ FileManager.prototype.initFileTypeFilter_ = function() {
+ if (this.params_.includeAllFiles) {
+ var option = this.document_.createElement('option');
+ option.innerText = str('ALL_FILES_FILTER');
+ this.fileTypeSelector_.appendChild(option);
+ option.value = 0;
+ }
+
+ for (var i = 0; i < this.fileTypes_.length; i++) {
+ var fileType = this.fileTypes_[i];
+ var option = this.document_.createElement('option');
+ var description = fileType.description;
+ if (!description) {
+ // See if all the extensions in the group have the same description.
+ for (var j = 0; j != fileType.extensions.length; j++) {
+ var currentDescription =
+ FileType.getTypeString('.' + fileType.extensions[j]);
+ if (!description) // Set the first time.
+ description = currentDescription;
+ else if (description != currentDescription) {
+ // No single description, fall through to the extension list.
+ description = null;
+ break;
+ }
+ }
+
+ if (!description)
+ // Convert ['jpg', 'png'] to '*.jpg, *.png'.
+ description = fileType.extensions.map(function(s) {
+ return '*.' + s;
+ }).join(', ');
+ }
+ option.innerText = description;
+
+ option.value = i + 1;
+
+ if (fileType.selected)
+ option.selected = true;
+
+ this.fileTypeSelector_.appendChild(option);
+ }
+
+ var options = this.fileTypeSelector_.querySelectorAll('option');
+ if (options.length >= 2) {
+ // There is in fact no choice, show the selector.
+ this.fileTypeSelector_.hidden = false;
+
+ this.fileTypeSelector_.addEventListener('change',
+ this.updateFileTypeFilter_.bind(this));
+ }
+ };
+
+ /**
+ * Filters file according to the selected file type.
+ * @private
+ */
+ FileManager.prototype.updateFileTypeFilter_ = function() {
+ this.fileFilter_.removeFilter('fileType');
+ var selectedIndex = this.getSelectedFilterIndex_();
+ if (selectedIndex > 0) { // Specific filter selected.
+ var regexp = new RegExp('.*(' +
+ this.fileTypes_[selectedIndex - 1].extensions.join('|') + ')$', 'i');
+ var filter = function(entry) {
+ return entry.isDirectory || regexp.test(entry.name);
+ };
+ this.fileFilter_.addFilter('fileType', filter);
+ }
+ };
+
+ /**
+ * Resize details and thumb views to fit the new window size.
+ * @private
+ */
+ FileManager.prototype.onResize_ = function() {
+ if (this.listType_ == FileManager.ListType.THUMBNAIL)
+ this.grid_.relayout();
+ else
+ this.table_.relayout();
+
+ // May not be available during initialization.
+ if (this.directoryTree_)
+ this.directoryTree_.relayout();
+
+ // TODO(mtomasz, yoshiki): Initialize navigation list earlier, before
+ // file system is available.
+ if (this.navigationList_)
+ this.navigationList_.redraw();
+
+ this.ui_.searchBox.updateSizeRelatedStyle();
+
+ this.previewPanel_.breadcrumbs.truncate();
+ };
+
+ /**
+ * Handles local metadata changes in the currect directory.
+ * @param {Event} event Change event.
+ * @private
+ */
+ FileManager.prototype.onWatcherMetadataChanged_ = function(event) {
+ this.updateMetadataInUI_(
+ event.metadataType, event.entries, event.properties);
+ };
+
+ /**
+ * Resize details and thumb views to fit the new window size.
+ * @private
+ */
+ FileManager.prototype.onPreviewPanelVisibilityChange_ = function() {
+ // This method may be called on initialization. Some object may not be
+ // initialized.
+
+ var panelHeight = this.previewPanel_.visible ?
+ this.previewPanel_.height : 0;
+ if (this.grid_)
+ this.grid_.setBottomMarginForPanel(panelHeight);
+ if (this.table_)
+ this.table_.setBottomMarginForPanel(panelHeight);
+
+ if (this.directoryTree_) {
+ this.directoryTree_.setBottomMarginForPanel(panelHeight);
+ this.ensureDirectoryTreeItemNotBehindPreviewPanel_();
+ }
+ };
+
+ /**
+ * Invoked when the drag is started on the list or the grid.
+ * @private
+ */
+ FileManager.prototype.onDragStart_ = function() {
+ // On open file dialog, the preview panel is always shown.
+ if (DialogType.isOpenDialog(this.dialogType))
+ return;
+ this.previewPanel_.visibilityType =
+ PreviewPanel.VisibilityType.ALWAYS_HIDDEN;
+ };
+
+ /**
+ * Invoked when the drag is ended on the list or the grid.
+ * @private
+ */
+ FileManager.prototype.onDragEnd_ = function() {
+ // On open file dialog, the preview panel is always shown.
+ if (DialogType.isOpenDialog(this.dialogType))
+ return;
+ this.previewPanel_.visibilityType = PreviewPanel.VisibilityType.AUTO;
+ };
+
+ /**
+ * Restores current directory and may be a selected item after page load (or
+ * reload) or popping a state (after click on back/forward). defaultPath
+ * primarily is used with save/open dialogs.
+ * Default path may also contain a file name. Freshly opened file manager
+ * window has neither.
+ *
+ * @private
+ */
+ FileManager.prototype.setupCurrentDirectory_ = function() {
+ var tracker = this.directoryModel_.createDirectoryChangeTracker();
+ var queue = new AsyncUtil.Queue();
+
+ // Wait until the volume manager is initialized.
+ queue.run(function(callback) {
+ tracker.start();
+ this.volumeManager_.ensureInitialized(callback);
+ }.bind(this));
+
+ // Resolve the default path.
+ var defaultFullPath;
+ var candidateFullPath;
+ var candidateEntry;
+ queue.run(function(callback) {
+ // Cancel this sequence if the current directory has already changed.
+ if (tracker.hasChanged) {
+ callback();
+ return;
+ }
+
+ // Resolve the absolute path in case only the file name or an empty string
+ // is passed.
+ if (!this.defaultPath) {
+ defaultFullPath = PathUtil.DEFAULT_MOUNT_POINT;
+ } else if (this.defaultPath.indexOf('/') === -1) {
+ // Path is a file name.
+ defaultFullPath = PathUtil.DEFAULT_MOUNT_POINT + '/' + this.defaultPath;
+ } else {
+ defaultFullPath = this.defaultPath;
+ }
+
+ // If Drive is disabled but the path points to Drive's entry, fallback to
+ // DEFAULT_MOUNT_POINT.
+ if (PathUtil.isDriveBasedPath(defaultFullPath) &&
+ !this.volumeManager_.getVolumeInfo(RootDirectory.DRIVE)) {
+ candidateFullPath = PathUtil.DEFAULT_MOUNT_POINT + '/' +
+ PathUtil.basename(defaultFullPath);
+ } else {
+ candidateFullPath = defaultFullPath;
+ }
+
+ // If the path points a fake entry, use the entry directly.
+ var fakeEntries = DirectoryModel.FAKE_DRIVE_SPECIAL_SEARCH_ENTRIES;
+ for (var i = 0; i < fakeEntries.length; i++) {
+ if (candidateFullPath === fakeEntries[i].fullPath) {
+ candidateEntry = fakeEntries[i];
+ callback();
+ return;
+ }
+ }
+
+ // Convert the path to the directory entry and an optional selection
+ // entry.
+ // TODO(hirono): There may be a race here. The path on Drive, may not
+ // be available yet.
+ this.volumeManager_.resolveAbsolutePath(candidateFullPath,
+ function(inEntry) {
+ candidateEntry = inEntry;
+ callback();
+ }, function() {
+ callback();
+ });
+ }.bind(this));
+
+ // Check the obtained entry.
+ var nextCurrentDirEntry;
+ var selectionEntry = null;
+ var suggestedName = null;
+ var error = null;
+ queue.run(function(callback) {
+ // Cancel this sequence if the current directory has already changed.
+ if (tracker.hasChanged) {
+ callback();
+ return;
+ }
+
+ if (candidateEntry) {
+ // The entry is directory. Use it.
+ if (candidateEntry.isDirectory) {
+ nextCurrentDirEntry = candidateEntry;
+ callback();
+ return;
+ }
+ // The entry exists, but it is not a directory. Therefore use a
+ // parent.
+ candidateEntry.getParent(function(parentEntry) {
+ nextCurrentDirEntry = parentEntry;
+ selectionEntry = candidateEntry;
+ callback();
+ }, function() {
+ error = new Error('Unable to resolve parent for: ' +
+ candidateEntry.fullPath);
+ callback();
+ });
+ return;
+ }
+
+ // If the entry doesn't exist, most probably because the path contains a
+ // suggested name. Therefore try to open its parent. However, the parent
+ // may also not exist. In such situation, fallback.
+ var pathNodes = candidateFullPath.split('/');
+ var baseName = pathNodes.pop();
+ var parentPath = pathNodes.join('/');
+ this.volumeManager_.resolveAbsolutePath(
+ parentPath,
+ function(parentEntry) {
+ nextCurrentDirEntry = parentEntry;
+ suggestedName = baseName;
+ callback();
+ },
+ callback); // In case of an error, continue.
+ }.bind(this));
+
+ // If the directory is not set at this stage, fallback to the default
+ // mount point.
+ queue.run(function(callback) {
+ // Cancel this sequence if the current directory has already changed,
+ // or the next current directory is already set.
+ if (tracker.hasChanged || nextCurrentDirEntry) {
+ callback();
+ return;
+ }
+ this.volumeManager_.resolveAbsolutePath(
+ PathUtil.DEFAULT_MOUNT_POINT,
+ function(fallbackEntry) {
+ nextCurrentDirEntry = fallbackEntry;
+ callback();
+ },
+ function() {
+ // Fallback directory not available? Throw an error.
+ error = new Error('Unable to resolve the fallback directory: ' +
+ PathUtil.DEFAULT_MOUNT_POINT);
+ callback();
+ });
+ }.bind(this));
+
+ queue.run(function(callback) {
+ // Check error.
+ if (error) {
+ callback();
+ throw error;
+ }
+ // Check directory change.
+ tracker.stop();
+ if (tracker.hasChanged) {
+ callback();
+ return;
+ }
+ // Finish setup current directory.
+ this.finishSetupCurrentDirectory_(
+ nextCurrentDirEntry, selectionEntry, suggestedName);
+ callback();
+ }.bind(this));
+ };
+
+ /**
+ * @param {DirectoryEntry} directoryEntry Directory to be opened.
+ * @param {Entry=} opt_selectionEntry Entry to be selected.
+ * @param {string=} opt_suggestedName Suggested name for a non-existing\
+ * selection.
+ * @private
+ */
+ FileManager.prototype.finishSetupCurrentDirectory_ = function(
+ directoryEntry, opt_selectionEntry, opt_suggestedName) {
+ // Open the directory, and select the selection (if passed).
+ if (util.isFakeEntry(directoryEntry)) {
+ this.directoryModel_.specialSearch(directoryEntry.fullPath, '');
+ } else {
+ this.directoryModel_.changeDirectoryEntry(directoryEntry, function() {
+ if (opt_selectionEntry)
+ this.directoryModel_.selectEntry(opt_selectionEntry);
+ }.bind(this));
+ }
+
+ if (this.dialogType == DialogType.FULL_PAGE) {
+ // In the FULL_PAGE mode if the restored path points to a file we might
+ // have to invoke a task after selecting it.
+ if (this.params_.action == 'select')
+ return;
+
+ var task = null;
+ if (opt_suggestedName) {
+ // Non-existent file or a directory.
+ if (this.params_.gallery) {
+ // Reloading while the Gallery is open with empty or multiple
+ // selection. Open the Gallery when the directory is scanned.
+ task = function() {
+ new FileTasks(this, this.params_).openGallery([]);
+ }.bind(this);
+ }
+ } else if (opt_selectionEntry) {
+ // There is a file to be selected. It means, that we are recovering
+ // the Files app.
+ // We call the appropriate methods of FileTasks directly as we do
+ // not need any of the preparations that |execute| method does.
+ // TODO(mtomasz): Change Entry.fullPath to Entry.
+ var mediaType = FileType.getMediaType(opt_selectionEntry.fullPath);
+ if (mediaType == 'image' || mediaType == 'video') {
+ task = function() {
+ // TODO(mtomasz): Replace the url with an entry.
+ new FileTasks(this, this.params_).openGallery([opt_selectionEntry]);
+ }.bind(this);
+ } else if (mediaType == 'archive') {
+ task = function() {
+ new FileTasks(this, this.params_).mountArchives(
+ [opt_selectionEntry]);
+ }.bind(this);
+ }
+ }
+
+ // If there is a task to be run, run it after the scan is completed.
+ if (task) {
+ var listener = function() {
+ this.directoryModel_.removeEventListener(
+ 'scan-completed', listener);
+ task();
+ }.bind(this);
+ this.directoryModel_.addEventListener('scan-completed', listener);
+ }
+ } else if (this.dialogType == DialogType.SELECT_SAVEAS_FILE) {
+ this.filenameInput_.value = opt_suggestedName || '';
+ this.selectDefaultPathInFilenameInput_();
+ }
+ };
+
+ /**
+ * Unmounts device.
+ * @param {string} path Path to a volume to unmount.
+ */
+ FileManager.prototype.unmountVolume = function(path) {
+ var onError = function(error) {
+ this.alert.showHtml('', str('UNMOUNT_FAILED'));
+ };
+ this.volumeManager_.unmount(path, function() {}, onError.bind(this));
+ };
+
+ /**
+ * @private
+ */
+ FileManager.prototype.refreshCurrentDirectoryMetadata_ = function() {
+ var entries = this.directoryModel_.getFileList().slice();
+ var directoryEntry = this.directoryModel_.getCurrentDirEntry();
+ if (!directoryEntry)
+ return;
+ // We don't pass callback here. When new metadata arrives, we have an
+ // observer registered to update the UI.
+
+ // TODO(dgozman): refresh content metadata only when modificationTime
+ // changed.
+ var isFakeEntry = util.isFakeEntry(directoryEntry);
+ var getEntries = (isFakeEntry ? [] : [directoryEntry]).concat(entries);
+ if (!isFakeEntry)
+ this.metadataCache_.clearRecursively(directoryEntry, '*');
+ this.metadataCache_.get(getEntries, 'filesystem', null);
+
+ if (this.isOnDrive())
+ this.metadataCache_.get(getEntries, 'drive', null);
+
+ var visibleItems = this.currentList_.items;
+ var visibleEntries = [];
+ for (var i = 0; i < visibleItems.length; i++) {
+ var index = this.currentList_.getIndexOfListItem(visibleItems[i]);
+ var entry = this.directoryModel_.getFileList().item(index);
+ // The following check is a workaround for the bug in list: sometimes item
+ // does not have listIndex, and therefore is not found in the list.
+ if (entry) visibleEntries.push(entry);
+ }
+ this.metadataCache_.get(visibleEntries, 'thumbnail', null);
+ };
+
+ /**
+ * @private
+ */
+ FileManager.prototype.dailyUpdateModificationTime_ = function() {
+ var fileList = this.directoryModel_.getFileList();
+ var entries = [];
+ for (var i = 0; i < fileList.length; i++) {
+ entries.push(fileList.item(i));
+ }
+ this.metadataCache_.get(
+ entries,
+ 'filesystem',
+ this.updateMetadataInUI_.bind(this, 'filesystem', entries));
+
+ setTimeout(this.dailyUpdateModificationTime_.bind(this),
+ MILLISECONDS_IN_DAY);
+ };
+
+ /**
+ * @param {string} type Type of metadata changed.
+ * @param {Array.<Entry>} entries Array of entries.
+ * @param {Object.<string, Object>} props Map from entry URLs to metadata
+ * props.
+ * @private
+ */
+ FileManager.prototype.updateMetadataInUI_ = function(
+ type, entries, properties) {
+ if (this.listType_ == FileManager.ListType.DETAIL)
+ this.table_.updateListItemsMetadata(type, properties);
+ else
+ this.grid_.updateListItemsMetadata(type, properties);
+ // TODO: update bottom panel thumbnails.
+ };
+
+ /**
+ * Restore the item which is being renamed while refreshing the file list. Do
+ * nothing if no item is being renamed or such an item disappeared.
+ *
+ * While refreshing file list it gets repopulated with new file entries.
+ * There is not a big difference whether DOM items stay the same or not.
+ * Except for the item that the user is renaming.
+ *
+ * @private
+ */
+ FileManager.prototype.restoreItemBeingRenamed_ = function() {
+ if (!this.isRenamingInProgress())
+ return;
+
+ var dm = this.directoryModel_;
+ var leadIndex = dm.getFileListSelection().leadIndex;
+ if (leadIndex < 0)
+ return;
+
+ var leadEntry = dm.getFileList().item(leadIndex);
+ if (this.renameInput_.currentEntry.fullPath != leadEntry.fullPath)
+ return;
+
+ var leadListItem = this.findListItemForNode_(this.renameInput_);
+ if (this.currentList_ == this.table_.list) {
+ this.table_.updateFileMetadata(leadListItem, leadEntry);
+ }
+ this.currentList_.restoreLeadItem(leadListItem);
+ };
+
+ /**
+ * @return {boolean} True if the current directory content is from Google
+ * Drive.
+ */
+ FileManager.prototype.isOnDrive = function() {
+ var rootType = this.directoryModel_.getCurrentRootType();
+ return rootType === RootType.DRIVE ||
+ rootType === RootType.DRIVE_SHARED_WITH_ME ||
+ rootType === RootType.DRIVE_RECENT ||
+ rootType === RootType.DRIVE_OFFLINE;
+ };
+
+ /**
+ * Overrides default handling for clicks on hyperlinks.
+ * In a packaged apps links with targer='_blank' open in a new tab by
+ * default, other links do not open at all.
+ *
+ * @param {Event} event Click event.
+ * @private
+ */
+ FileManager.prototype.onExternalLinkClick_ = function(event) {
+ if (event.target.tagName != 'A' || !event.target.href)
+ return;
+
+ if (this.dialogType != DialogType.FULL_PAGE)
+ this.onCancel_();
+ };
+
+ /**
+ * Task combobox handler.
+ *
+ * @param {Object} event Event containing task which was clicked.
+ * @private
+ */
+ FileManager.prototype.onTaskItemClicked_ = function(event) {
+ var selection = this.getSelection();
+ if (!selection.tasks) return;
+
+ if (event.item.task) {
+ // Task field doesn't exist on change-default dropdown item.
+ selection.tasks.execute(event.item.task.taskId);
+ } else {
+ var extensions = [];
+
+ for (var i = 0; i < selection.entries.length; i++) {
+ var match = /\.(\w+)$/g.exec(selection.entries[i].toURL());
+ if (match) {
+ var ext = match[1].toUpperCase();
+ if (extensions.indexOf(ext) == -1) {
+ extensions.push(ext);
+ }
+ }
+ }
+
+ var format = '';
+
+ if (extensions.length == 1) {
+ format = extensions[0];
+ }
+
+ // Change default was clicked. We should open "change default" dialog.
+ selection.tasks.showTaskPicker(this.defaultTaskPicker,
+ loadTimeData.getString('CHANGE_DEFAULT_MENU_ITEM'),
+ strf('CHANGE_DEFAULT_CAPTION', format),
+ this.onDefaultTaskDone_.bind(this));
+ }
+ };
+
+ /**
+ * Sets the given task as default, when this task is applicable.
+ *
+ * @param {Object} task Task to set as default.
+ * @private
+ */
+ FileManager.prototype.onDefaultTaskDone_ = function(task) {
+ // TODO(dgozman): move this method closer to tasks.
+ var selection = this.getSelection();
+ chrome.fileBrowserPrivate.setDefaultTask(
+ task.taskId,
+ util.entriesToURLs(selection.entries),
+ selection.mimeTypes);
+ selection.tasks = new FileTasks(this);
+ selection.tasks.init(selection.entries, selection.mimeTypes);
+ selection.tasks.display(this.taskItems_);
+ this.refreshCurrentDirectoryMetadata_();
+ this.selectionHandler_.onFileSelectionChanged();
+ };
+
+ /**
+ * @private
+ */
+ FileManager.prototype.onPreferencesChanged_ = function() {
+ var self = this;
+ this.getPreferences_(function(prefs) {
+ self.initDateTimeFormatters_();
+ self.refreshCurrentDirectoryMetadata_();
+
+ if (prefs.cellularDisabled)
+ self.syncButton.setAttribute('checked', '');
+ else
+ self.syncButton.removeAttribute('checked');
+
+ if (self.hostedButton.hasAttribute('checked') !=
+ prefs.hostedFilesDisabled && self.isOnDrive()) {
+ self.directoryModel_.rescan();
+ }
+
+ if (!prefs.hostedFilesDisabled)
+ self.hostedButton.setAttribute('checked', '');
+ else
+ self.hostedButton.removeAttribute('checked');
+ },
+ true /* refresh */);
+ };
+
+ FileManager.prototype.onDriveConnectionChanged_ = function() {
+ var connection = this.volumeManager_.getDriveConnectionState();
+ if (this.commandHandler)
+ this.commandHandler.updateAvailability();
+ if (this.dialogContainer_)
+ this.dialogContainer_.setAttribute('connection', connection.type);
+ this.shareDialog_.hideWithResult(ShareDialog.Result.NETWORK_ERROR);
+ this.suggestAppsDialog.onDriveConnectionChanged(connection.type);
+ };
+
+ /**
+ * Get the metered status of Drive connection.
+ *
+ * @return {boolean} Returns true if drive should limit the traffic because
+ * the connection is metered and the 'disable-sync-on-metered' setting is
+ * enabled. Otherwise, returns false.
+ */
+ FileManager.prototype.isDriveOnMeteredConnection = function() {
+ var connection = this.volumeManager_.getDriveConnectionState();
+ return connection.type == util.DriveConnectionType.METERED;
+ };
+
+ /**
+ * Get the online/offline status of drive.
+ *
+ * @return {boolean} Returns true if the connection is offline. Otherwise,
+ * returns false.
+ */
+ FileManager.prototype.isDriveOffline = function() {
+ var connection = this.volumeManager_.getDriveConnectionState();
+ return connection.type == util.DriveConnectionType.OFFLINE;
+ };
+
+ FileManager.prototype.isOnReadonlyDirectory = function() {
+ return this.directoryModel_.isReadOnly();
+ };
+
+ /**
+ * @param {Event} Unmount event.
+ * @private
+ */
+ FileManager.prototype.onExternallyUnmounted_ = function(event) {
+ if (event.mountPath == this.directoryModel_.getCurrentRootPath()) {
+ if (this.closeOnUnmount_) {
+ // If the file manager opened automatically when a usb drive inserted,
+ // user have never changed current volume (that implies the current
+ // directory is still on the device) then close this window.
+ window.close();
+ }
+ }
+ };
+
+ /**
+ * Show a modal-like file viewer/editor on top of the File Manager UI.
+ *
+ * @param {HTMLElement} popup Popup element.
+ * @param {function()} closeCallback Function to call after the popup is
+ * closed.
+ */
+ FileManager.prototype.openFilePopup = function(popup, closeCallback) {
+ this.closeFilePopup();
+ this.filePopup_ = popup;
+ this.filePopupCloseCallback_ = closeCallback;
+ this.dialogDom_.insertBefore(
+ this.filePopup_, this.dialogDom_.querySelector('#iframe-drag-area'));
+ this.filePopup_.focus();
+ this.document_.body.setAttribute('overlay-visible', '');
+ this.document_.querySelector('#iframe-drag-area').hidden = false;
+ };
+
+ /**
+ * Closes the modal-like file viewer/editor popup.
+ */
+ FileManager.prototype.closeFilePopup = function() {
+ if (this.filePopup_) {
+ this.document_.body.removeAttribute('overlay-visible');
+ this.document_.querySelector('#iframe-drag-area').hidden = true;
+ // The window resize would not be processed properly while the relevant
+ // divs had 'display:none', force resize after the layout fired.
+ setTimeout(this.onResize_.bind(this), 0);
+ if (this.filePopup_.contentWindow &&
+ this.filePopup_.contentWindow.unload) {
+ this.filePopup_.contentWindow.unload();
+ }
+
+ if (this.filePopupCloseCallback_) {
+ this.filePopupCloseCallback_();
+ this.filePopupCloseCallback_ = null;
+ }
+
+ // These operations have to be in the end, otherwise v8 crashes on an
+ // assert. See: crbug.com/224174.
+ this.dialogDom_.removeChild(this.filePopup_);
+ this.filePopup_ = null;
+ }
+ };
+
+ /**
+ * Updates visibility of the draggable app region in the modal-like file
+ * viewer/editor.
+ *
+ * @param {boolean} visible True for visible, false otherwise.
+ */
+ FileManager.prototype.onFilePopupAppRegionChanged = function(visible) {
+ if (!this.filePopup_)
+ return;
+
+ this.document_.querySelector('#iframe-drag-area').hidden = !visible;
+ };
+
+ /**
+ * @return {Array.<Entry>} List of all entries in the current directory.
+ */
+ FileManager.prototype.getAllEntriesInCurrentDirectory = function() {
+ return this.directoryModel_.getFileList().slice();
+ };
+
+ FileManager.prototype.isRenamingInProgress = function() {
+ return !!this.renameInput_.currentEntry;
+ };
+
+ /**
+ * @private
+ */
+ FileManager.prototype.focusCurrentList_ = function() {
+ if (this.listType_ == FileManager.ListType.DETAIL)
+ this.table_.focus();
+ else // this.listType_ == FileManager.ListType.THUMBNAIL)
+ this.grid_.focus();
+ };
+
+ /**
+ * Return full path of the current directory or null.
+ * @return {?string} The full path of the current directory.
+ */
+ FileManager.prototype.getCurrentDirectory = function() {
+ return this.directoryModel_ && this.directoryModel_.getCurrentDirPath();
+ };
+
+ /**
+ * Return URL of the current directory or null.
+ * @return {string} URL representing the current directory.
+ */
+ FileManager.prototype.getCurrentDirectoryURL = function() {
+ return this.directoryModel_ &&
+ this.directoryModel_.getCurrentDirectoryURL();
+ };
+
+ /**
+ * Return DirectoryEntry of the current directory or null.
+ * @return {DirectoryEntry} DirectoryEntry of the current directory. Returns
+ * null if the directory model is not ready or the current directory is
+ * not set.
+ */
+ FileManager.prototype.getCurrentDirectoryEntry = function() {
+ return this.directoryModel_ && this.directoryModel_.getCurrentDirEntry();
+ };
+
+ /**
+ * Deletes the selected file and directories recursively.
+ */
+ FileManager.prototype.deleteSelection = function() {
+ // TODO(mtomasz): Remove this temporary dialog. crbug.com/167364
+ var entries = this.getSelection().entries;
+ var message = entries.length == 1 ?
+ strf('GALLERY_CONFIRM_DELETE_ONE', entries[0].name) :
+ strf('GALLERY_CONFIRM_DELETE_SOME', entries.length);
+ this.confirm.show(message, function() {
+ this.fileOperationManager_.deleteEntries(entries);
+ }.bind(this));
+ };
+
+ /**
+ * Shows the share dialog for the selected file or directory.
+ */
+ FileManager.prototype.shareSelection = function() {
+ var entries = this.getSelection().entries;
+ if (entries.length != 1) {
+ console.warn('Unable to share multiple items at once.');
+ return;
+ }
+ // Add the overlapped class to prevent the applicaiton window from
+ // captureing mouse events.
+ this.shareDialog_.show(entries[0], function(result) {
+ if (result == ShareDialog.Result.NETWORK_ERROR)
+ this.error.show(str('SHARE_ERROR'));
+ }.bind(this));
+ };
+
+ /**
+ * Creates a folder shortcut.
+ * @param {string} path A shortcut which refers to |path| to be created.
+ */
+ FileManager.prototype.createFolderShortcut = function(path) {
+ // Duplicate entry.
+ if (this.folderShortcutExists(path))
+ return;
+
+ this.folderShortcutsModel_.add(path);
+ };
+
+ /**
+ * Checkes if the shortcut which refers to the given folder exists or not.
+ * @param {string} path Path of the folder to be checked.
+ */
+ FileManager.prototype.folderShortcutExists = function(path) {
+ return this.folderShortcutsModel_.exists(path);
+ };
+
+ /**
+ * Removes the folder shortcut.
+ * @param {string} path The shortcut which refers to |path| is to be removed.
+ */
+ FileManager.prototype.removeFolderShortcut = function(path) {
+ this.folderShortcutsModel_.remove(path);
+ };
+
+ /**
+ * Blinks the selection. Used to give feedback when copying or cutting the
+ * selection.
+ */
+ FileManager.prototype.blinkSelection = function() {
+ var selection = this.getSelection();
+ if (!selection || selection.totalCount == 0)
+ return;
+
+ for (var i = 0; i < selection.entries.length; i++) {
+ var selectedIndex = selection.indexes[i];
+ var listItem = this.currentList_.getListItemByIndex(selectedIndex);
+ if (listItem)
+ this.blinkListItem_(listItem);
+ }
+ };
+
+ /**
+ * @param {Element} listItem List item element.
+ * @private
+ */
+ FileManager.prototype.blinkListItem_ = function(listItem) {
+ listItem.classList.add('blink');
+ setTimeout(function() {
+ listItem.classList.remove('blink');
+ }, 100);
+ };
+
+ /**
+ * @private
+ */
+ FileManager.prototype.selectDefaultPathInFilenameInput_ = function() {
+ var input = this.filenameInput_;
+ input.focus();
+ var selectionEnd = input.value.lastIndexOf('.');
+ if (selectionEnd == -1) {
+ input.select();
+ } else {
+ input.selectionStart = 0;
+ input.selectionEnd = selectionEnd;
+ }
+ // Clear, so we never do this again.
+ this.defaultPath = '';
+ };
+
+ /**
+ * Handles mouse click or tap.
+ *
+ * @param {Event} event The click event.
+ * @private
+ */
+ FileManager.prototype.onDetailClick_ = function(event) {
+ if (this.isRenamingInProgress()) {
+ // Don't pay attention to clicks during a rename.
+ return;
+ }
+
+ var listItem = this.findListItemForEvent_(event);
+ var selection = this.getSelection();
+ if (!listItem || !listItem.selected || selection.totalCount != 1) {
+ return;
+ }
+
+ // React on double click, but only if both clicks hit the same item.
+ // TODO(mtomasz): Simplify it, and use a double click handler if possible.
+ var clickNumber = (this.lastClickedItem_ == listItem) ? 2 : undefined;
+ this.lastClickedItem_ = listItem;
+
+ if (event.detail != clickNumber)
+ return;
+
+ var entry = selection.entries[0];
+ if (entry.isDirectory) {
+ this.onDirectoryAction_(entry);
+ } else {
+ this.dispatchSelectionAction_();
+ }
+ };
+
+ /**
+ * @private
+ */
+ FileManager.prototype.dispatchSelectionAction_ = function() {
+ if (this.dialogType == DialogType.FULL_PAGE) {
+ var selection = this.getSelection();
+ var tasks = selection.tasks;
+ var urls = selection.urls;
+ var mimeTypes = selection.mimeTypes;
+ if (tasks)
+ tasks.executeDefault();
+ return true;
+ }
+ if (!this.okButton_.disabled) {
+ this.onOk_();
+ return true;
+ }
+ return false;
+ };
+
+ /**
+ * Opens the suggest file dialog.
+ *
+ * @param {Entry} entry Entry of the file.
+ * @param {function()} onSuccess Success callback.
+ * @param {function()} onCancelled User-cancelled callback.
+ * @param {function()} onFailure Failure callback.
+ * @private
+ */
+ FileManager.prototype.openSuggestAppsDialog =
+ function(entry, onSuccess, onCancelled, onFailure) {
+ if (!url) {
+ onFailure();
+ return;
+ }
+
+ this.metadataCache_.get([entry], 'drive', function(props) {
+ if (!props || !props[0] || !props[0].contentMimeType) {
+ onFailure();
+ return;
+ }
+
+ var basename = entry.name;
+ var splitted = PathUtil.splitExtension(basename);
+ var filename = splitted[0];
+ var extension = splitted[1];
+ var mime = props[0].contentMimeType;
+
+ // Returns with failure if the file has neither extension nor mime.
+ if (!extension || !mime) {
+ onFailure();
+ return;
+ }
+
+ var onDialogClosed = function(result) {
+ switch (result) {
+ case SuggestAppsDialog.Result.INSTALL_SUCCESSFUL:
+ onSuccess();
+ break;
+ case SuggestAppsDialog.Result.FAILED:
+ onFailure();
+ break;
+ default:
+ onCancelled();
+ }
+ };
+
+ if (FileTasks.EXECUTABLE_EXTENSIONS.indexOf(extension) !== -1) {
+ this.suggestAppsDialog.showByFilename(filename, onDialogClosed);
+ } else {
+ this.suggestAppsDialog.showByExtensionAndMime(
+ extension, mime, onDialogClosed);
+ }
+ }.bind(this));
+ };
+
+ /**
+ * Called when a dialog is shown or hidden.
+ * @param {boolean} flag True if a dialog is shown, false if hidden. */
+ FileManager.prototype.onDialogShownOrHidden = function(show) {
+ // Set/unset a flag to disable dragging on the title area.
+ this.dialogContainer_.classList.toggle('disable-header-drag', show);
+ };
+
+ /**
+ * Executes directory action (i.e. changes directory).
+ *
+ * @param {DirectoryEntry} entry Directory entry to which directory should be
+ * changed.
+ * @private
+ */
+ FileManager.prototype.onDirectoryAction_ = function(entry) {
+ return this.directoryModel_.changeDirectory(entry.fullPath);
+ };
+
+ /**
+ * Update the window title.
+ * @private
+ */
+ FileManager.prototype.updateTitle_ = function() {
+ if (this.dialogType != DialogType.FULL_PAGE)
+ return;
+
+ var path = this.getCurrentDirectory();
+ var rootPath = PathUtil.getRootPath(path);
+ this.document_.title = PathUtil.getRootLabel(rootPath) +
+ path.substring(rootPath.length);
+ };
+
+ /**
+ * Update the gear menu.
+ * @private
+ */
+ FileManager.prototype.updateGearMenu_ = function() {
+ var hideItemsForDrive = !this.isOnDrive();
+ this.syncButton.hidden = hideItemsForDrive;
+ this.hostedButton.hidden = hideItemsForDrive;
+ this.document_.getElementById('drive-separator').hidden =
+ hideItemsForDrive;
+
+ // If volume has changed, then fetch remaining space data.
+ if (this.previousRootUrl_ != this.directoryModel_.getCurrentMountPointUrl())
+ this.refreshRemainingSpace_(true); // Show loading caption.
+
+ this.previousRootUrl_ = this.directoryModel_.getCurrentMountPointUrl();
+ };
+
+ /**
+ * Refreshes space info of the current volume.
+ * @param {boolean} showLoadingCaption Whether show loading caption or not.
+ * @private
+ */
+ FileManager.prototype.refreshRemainingSpace_ = function(showLoadingCaption) {
+ var volumeSpaceInfoLabel =
+ this.dialogDom_.querySelector('#volume-space-info-label');
+ var volumeSpaceInnerBar =
+ this.dialogDom_.querySelector('#volume-space-info-bar');
+ var volumeSpaceOuterBar =
+ this.dialogDom_.querySelector('#volume-space-info-bar').parentNode;
+
+ volumeSpaceInnerBar.setAttribute('pending', '');
+
+ if (showLoadingCaption) {
+ volumeSpaceInfoLabel.innerText = str('WAITING_FOR_SPACE_INFO');
+ volumeSpaceInnerBar.style.width = '100%';
+ }
+
+ var currentMountPointUrl = this.directoryModel_.getCurrentMountPointUrl();
+ chrome.fileBrowserPrivate.getSizeStats(
+ currentMountPointUrl, function(result) {
+ if (this.directoryModel_.getCurrentMountPointUrl() !=
+ currentMountPointUrl)
+ return;
+ updateSpaceInfo(result,
+ volumeSpaceInnerBar,
+ volumeSpaceInfoLabel,
+ volumeSpaceOuterBar);
+ }.bind(this));
+ };
+
+ /**
+ * Update the UI when the current directory changes.
+ *
+ * @param {Event} event The directory-changed event.
+ * @private
+ */
+ FileManager.prototype.onDirectoryChanged_ = function(event) {
+ this.selectionHandler_.onFileSelectionChanged();
+ this.ui_.searchBox.clear();
+ util.updateAppState(this.getCurrentDirectory());
+
+ // If the current directory is moved from the device's volume, do not
+ // automatically close the window on device removal.
+ if (event.previousDirEntry &&
+ PathUtil.getRootPath(event.previousDirEntry.fullPath) !=
+ PathUtil.getRootPath(event.newDirEntry.fullPath))
+ this.closeOnUnmount_ = false;
+
+ if (this.commandHandler)
+ this.commandHandler.updateAvailability();
+ this.updateUnformattedVolumeStatus_();
+ this.updateTitle_();
+ this.updateGearMenu_();
+ var currentEntry = this.getCurrentDirectoryEntry();
+ this.previewPanel_.currentEntry = util.isFakeEntry(currentEntry) ?
+ null : currentEntry;
+ };
+
+ FileManager.prototype.updateUnformattedVolumeStatus_ = function() {
+ var volumeInfo = this.volumeManager_.getVolumeInfo(
+ this.directoryModel_.getCurrentDirEntry());
+
+ if (volumeInfo && volumeInfo.error) {
+ this.dialogDom_.setAttribute('unformatted', '');
+
+ var errorNode = this.dialogDom_.querySelector('#format-panel > .error');
+ if (volumeInfo.error == util.VolumeError.UNSUPPORTED_FILESYSTEM) {
+ errorNode.textContent = str('UNSUPPORTED_FILESYSTEM_WARNING');
+ } else {
+ errorNode.textContent = str('UNKNOWN_FILESYSTEM_WARNING');
+ }
+
+ // Update 'canExecute' for format command so the format button's disabled
+ // property is properly set.
+ if (this.commandHandler)
+ this.commandHandler.updateAvailability();
+ } else {
+ this.dialogDom_.removeAttribute('unformatted');
+ }
+ };
+
+ FileManager.prototype.findListItemForEvent_ = function(event) {
+ return this.findListItemForNode_(event.touchedElement || event.srcElement);
+ };
+
+ FileManager.prototype.findListItemForNode_ = function(node) {
+ var item = this.currentList_.getListItemAncestor(node);
+ // TODO(serya): list should check that.
+ return item && this.currentList_.isItem(item) ? item : null;
+ };
+
+ /**
+ * Unload handler for the page. May be called manually for the file picker
+ * dialog, because it closes by calling extension API functions that do not
+ * return.
+ *
+ * TODO(hirono): This method is not called when Files.app is opend as a dialog
+ * and is closed by the close button in the dialog frame. crbug.com/309967
+ * @private
+ */
+ FileManager.prototype.onUnload_ = function() {
+ if (this.directoryModel_)
+ this.directoryModel_.dispose();
+ if (this.volumeManager_)
+ this.volumeManager_.dispose();
+ if (this.filePopup_ &&
+ this.filePopup_.contentWindow &&
+ this.filePopup_.contentWindow.unload)
+ this.filePopup_.contentWindow.unload(true /* exiting */);
+ if (this.progressCenterPanel_)
+ this.backgroundPage_.background.progressCenter.removePanel(
+ this.progressCenterPanel_);
+ if (this.fileOperationManager_) {
+ if (this.onCopyProgressBound_) {
+ this.fileOperationManager_.removeEventListener(
+ 'copy-progress', this.onCopyProgressBound_);
+ }
+ if (this.onEntryChangedBound_) {
+ this.fileOperationManager_.removeEventListener(
+ 'entry-changed', this.onEntryChangedBound_);
+ }
+ }
+ window.closing = true;
+ if (this.backgroundPage_ && util.platform.runningInBrowser())
+ this.backgroundPage_.background.tryClose();
+ };
+
+ FileManager.prototype.initiateRename = function() {
+ var item = this.currentList_.ensureLeadItemExists();
+ if (!item)
+ return;
+ var label = item.querySelector('.filename-label');
+ var input = this.renameInput_;
+
+ input.value = label.textContent;
+ label.parentNode.setAttribute('renaming', '');
+ label.parentNode.appendChild(input);
+ input.focus();
+ var selectionEnd = input.value.lastIndexOf('.');
+ if (selectionEnd == -1) {
+ input.select();
+ } else {
+ input.selectionStart = 0;
+ input.selectionEnd = selectionEnd;
+ }
+
+ // This has to be set late in the process so we don't handle spurious
+ // blur events.
+ input.currentEntry = this.currentList_.dataModel.item(item.listIndex);
+ };
+
+ /**
+ * @type {Event} Key event.
+ * @private
+ */
+ FileManager.prototype.onRenameInputKeyDown_ = function(event) {
+ if (!this.isRenamingInProgress())
+ return;
+
+ // Do not move selection or lead item in list during rename.
+ if (event.keyIdentifier == 'Up' || event.keyIdentifier == 'Down') {
+ event.stopPropagation();
+ }
+
+ switch (util.getKeyModifiers(event) + event.keyCode) {
+ case '27': // Escape
+ this.cancelRename_();
+ event.preventDefault();
+ break;
+
+ case '13': // Enter
+ this.commitRename_();
+ event.preventDefault();
+ break;
+ }
+ };
+
+ /**
+ * @type {Event} Blur event.
+ * @private
+ */
+ FileManager.prototype.onRenameInputBlur_ = function(event) {
+ if (this.isRenamingInProgress() && !this.renameInput_.validation_)
+ this.commitRename_();
+ };
+
+ /**
+ * @private
+ */
+ FileManager.prototype.commitRename_ = function() {
+ var input = this.renameInput_;
+ var entry = input.currentEntry;
+ var newName = input.value;
+
+ if (newName == entry.name) {
+ this.cancelRename_();
+ return;
+ }
+
+ var nameNode = this.findListItemForNode_(this.renameInput_).
+ querySelector('.filename-label');
+
+ input.validation_ = true;
+ var validationDone = function(valid) {
+ input.validation_ = false;
+ // Alert dialog restores focus unless the item removed from DOM.
+ if (this.document_.activeElement != input)
+ this.cancelRename_();
+ if (!valid)
+ return;
+
+ // Validation succeeded. Do renaming.
+
+ this.cancelRename_();
+ // Optimistically apply new name immediately to avoid flickering in
+ // case of success.
+ nameNode.textContent = newName;
+
+ util.rename(
+ entry, newName,
+ function(newEntry) {
+ this.directoryModel_.onRenameEntry(entry, newEntry);
+ }.bind(this),
+ function(error) {
+ // Write back to the old name.
+ nameNode.textContent = entry.name;
+
+ // Show error dialog.
+ var message;
+ if (error.code == FileError.PATH_EXISTS_ERR ||
+ error.code == FileError.TYPE_MISMATCH_ERR) {
+ // Check the existing entry is file or not.
+ // 1) If the entry is a file:
+ // a) If we get PATH_EXISTS_ERR, a file exists.
+ // b) If we get TYPE_MISMATCH_ERR, a directory exists.
+ // 2) If the entry is a directory:
+ // a) If we get PATH_EXISTS_ERR, a directory exists.
+ // b) If we get TYPE_MISMATCH_ERR, a file exists.
+ message = strf(
+ (entry.isFile && error.code == FileError.PATH_EXISTS_ERR) ||
+ (!entry.isFile && error.code == FileError.TYPE_MISMATCH_ERR) ?
+ 'FILE_ALREADY_EXISTS' :
+ 'DIRECTORY_ALREADY_EXISTS',
+ newName);
+ } else {
+ message = strf('ERROR_RENAMING', entry.name,
+ util.getFileErrorString(err.code));
+ }
+
+ this.alert.show(message);
+ }.bind(this));
+ };
+
+ // TODO(haruki): this.getCurrentDirectoryURL() might not return the actual
+ // parent if the directory content is a search result. Fix it to do proper
+ // validation.
+ this.validateFileName_(this.getCurrentDirectoryURL(),
+ newName,
+ validationDone.bind(this));
+ };
+
+ /**
+ * @private
+ */
+ FileManager.prototype.cancelRename_ = function() {
+ this.renameInput_.currentEntry = null;
+
+ var parent = this.renameInput_.parentNode;
+ if (parent) {
+ parent.removeAttribute('renaming');
+ parent.removeChild(this.renameInput_);
+ }
+ };
+
+ /**
+ * @param {Event} Key event.
+ * @private
+ */
+ FileManager.prototype.onFilenameInputInput_ = function() {
+ this.selectionHandler_.updateOkButton();
+ };
+
+ /**
+ * @param {Event} Key event.
+ * @private
+ */
+ FileManager.prototype.onFilenameInputKeyDown_ = function(event) {
+ if ((util.getKeyModifiers(event) + event.keyCode) === '13' /* Enter */)
+ this.okButton_.click();
+ };
+
+ /**
+ * @param {Event} Focus event.
+ * @private
+ */
+ FileManager.prototype.onFilenameInputFocus_ = function(event) {
+ var input = this.filenameInput_;
+
+ // On focus we want to select everything but the extension, but
+ // Chrome will select-all after the focus event completes. We
+ // schedule a timeout to alter the focus after that happens.
+ setTimeout(function() {
+ var selectionEnd = input.value.lastIndexOf('.');
+ if (selectionEnd == -1) {
+ input.select();
+ } else {
+ input.selectionStart = 0;
+ input.selectionEnd = selectionEnd;
+ }
+ }, 0);
+ };
+
+ /**
+ * @private
+ */
+ FileManager.prototype.onScanStarted_ = function() {
+ if (this.scanInProgress_) {
+ this.table_.list.endBatchUpdates();
+ this.grid_.endBatchUpdates();
+ }
+
+ if (this.commandHandler)
+ this.commandHandler.updateAvailability();
+ this.table_.list.startBatchUpdates();
+ this.grid_.startBatchUpdates();
+ this.scanInProgress_ = true;
+
+ this.scanUpdatedAtLeastOnceOrCompleted_ = false;
+ if (this.scanCompletedTimer_) {
+ clearTimeout(this.scanCompletedTimer_);
+ this.scanCompletedTimer_ = null;
+ }
+
+ if (this.scanUpdatedTimer_) {
+ clearTimeout(this.scanUpdatedTimer_);
+ this.scanUpdatedTimer_ = null;
+ }
+
+ if (this.spinner_.hidden) {
+ this.cancelSpinnerTimeout_();
+ this.showSpinnerTimeout_ =
+ setTimeout(this.showSpinner_.bind(this, true), 500);
+ }
+ };
+
+ /**
+ * @private
+ */
+ FileManager.prototype.onScanCompleted_ = function() {
+ if (!this.scanInProgress_) {
+ console.error('Scan-completed event recieved. But scan is not started.');
+ return;
+ }
+
+ if (this.commandHandler)
+ this.commandHandler.updateAvailability();
+ this.hideSpinnerLater_();
+
+ if (this.scanUpdatedTimer_) {
+ clearTimeout(this.scanUpdatedTimer_);
+ this.scanUpdatedTimer_ = null;
+ }
+
+ // To avoid flickering postpone updating the ui by a small amount of time.
+ // There is a high chance, that metadata will be received within 50 ms.
+ this.scanCompletedTimer_ = setTimeout(function() {
+ // Check if batch updates are already finished by onScanUpdated_().
+ if (!this.scanUpdatedAtLeastOnceOrCompleted_) {
+ this.scanUpdatedAtLeastOnceOrCompleted_ = true;
+ this.updateMiddleBarVisibility_();
+ }
+
+ this.scanInProgress_ = false;
+ this.table_.list.endBatchUpdates();
+ this.grid_.endBatchUpdates();
+ this.scanCompletedTimer_ = null;
+ }.bind(this), 50);
+ };
+
+ /**
+ * @private
+ */
+ FileManager.prototype.onScanUpdated_ = function() {
+ if (!this.scanInProgress_) {
+ console.error('Scan-updated event recieved. But scan is not started.');
+ return;
+ }
+
+ if (this.scanUpdatedTimer_ || this.scanCompletedTimer_)
+ return;
+
+ // Show contents incrementally by finishing batch updated, but only after
+ // 200ms elapsed, to avoid flickering when it is not necessary.
+ this.scanUpdatedTimer_ = setTimeout(function() {
+ // We need to hide the spinner only once.
+ if (!this.scanUpdatedAtLeastOnceOrCompleted_) {
+ this.scanUpdatedAtLeastOnceOrCompleted_ = true;
+ this.hideSpinnerLater_();
+ this.updateMiddleBarVisibility_();
+ }
+
+ // Update the UI.
+ if (this.scanInProgress_) {
+ this.table_.list.endBatchUpdates();
+ this.grid_.endBatchUpdates();
+ this.table_.list.startBatchUpdates();
+ this.grid_.startBatchUpdates();
+ }
+ this.scanUpdatedTimer_ = null;
+ }.bind(this), 200);
+ };
+
+ /**
+ * @private
+ */
+ FileManager.prototype.onScanCancelled_ = function() {
+ if (!this.scanInProgress_) {
+ console.error('Scan-cancelled event recieved. But scan is not started.');
+ return;
+ }
+
+ if (this.commandHandler)
+ this.commandHandler.updateAvailability();
+ this.hideSpinnerLater_();
+ if (this.scanCompletedTimer_) {
+ clearTimeout(this.scanCompletedTimer_);
+ this.scanCompletedTimer_ = null;
+ }
+ if (this.scanUpdatedTimer_) {
+ clearTimeout(this.scanUpdatedTimer_);
+ this.scanUpdatedTimer_ = null;
+ }
+ // Finish unfinished batch updates.
+ if (!this.scanUpdatedAtLeastOnceOrCompleted_) {
+ this.scanUpdatedAtLeastOnceOrCompleted_ = true;
+ this.updateMiddleBarVisibility_();
+ }
+
+ this.scanInProgress_ = false;
+ this.table_.list.endBatchUpdates();
+ this.grid_.endBatchUpdates();
+ };
+
+ /**
+ * Handle the 'rescan-completed' from the DirectoryModel.
+ * @private
+ */
+ FileManager.prototype.onRescanCompleted_ = function() {
+ this.selectionHandler_.onFileSelectionChanged();
+ };
+
+ /**
+ * @private
+ */
+ FileManager.prototype.cancelSpinnerTimeout_ = function() {
+ if (this.showSpinnerTimeout_) {
+ clearTimeout(this.showSpinnerTimeout_);
+ this.showSpinnerTimeout_ = null;
+ }
+ };
+
+ /**
+ * @private
+ */
+ FileManager.prototype.hideSpinnerLater_ = function() {
+ this.cancelSpinnerTimeout_();
+ this.showSpinner_(false);
+ };
+
+ /**
+ * @param {boolean} on True to show, false to hide.
+ * @private
+ */
+ FileManager.prototype.showSpinner_ = function(on) {
+ if (on && this.directoryModel_ && this.directoryModel_.isScanning())
+ this.spinner_.hidden = false;
+
+ if (!on && (!this.directoryModel_ ||
+ !this.directoryModel_.isScanning() ||
+ this.directoryModel_.getFileList().length != 0)) {
+ this.spinner_.hidden = true;
+ }
+ };
+
+ FileManager.prototype.createNewFolder = function() {
+ var defaultName = str('DEFAULT_NEW_FOLDER_NAME');
+
+ // Find a name that doesn't exist in the data model.
+ var files = this.directoryModel_.getFileList();
+ var hash = {};
+ for (var i = 0; i < files.length; i++) {
+ var name = files.item(i).name;
+ // Filtering names prevents from conflicts with prototype's names
+ // and '__proto__'.
+ if (name.substring(0, defaultName.length) == defaultName)
+ hash[name] = 1;
+ }
+
+ var baseName = defaultName;
+ var separator = '';
+ var suffix = '';
+ var index = '';
+
+ var advance = function() {
+ separator = ' (';
+ suffix = ')';
+ index++;
+ };
+
+ var current = function() {
+ return baseName + separator + index + suffix;
+ };
+
+ // Accessing hasOwnProperty is safe since hash properties filtered.
+ while (hash.hasOwnProperty(current())) {
+ advance();
+ }
+
+ var self = this;
+ var list = self.currentList_;
+ var tryCreate = function() {
+ self.directoryModel_.createDirectory(current(),
+ onSuccess, onError);
+ };
+
+ var onSuccess = function(entry) {
+ metrics.recordUserAction('CreateNewFolder');
+ list.selectedItem = entry;
+ self.initiateRename();
+ };
+
+ var onError = function(error) {
+ self.alert.show(strf('ERROR_CREATING_FOLDER', current(),
+ util.getFileErrorString(error.code)));
+ };
+
+ tryCreate();
+ };
+
+ /**
+ * @param {Event} event Click event.
+ * @private
+ */
+ FileManager.prototype.onDetailViewButtonClick_ = function(event) {
+ // Stop propagate and hide the menu manually, in order to prevent the focus
+ // from being back to the button. (cf. http://crbug.com/248479)
+ event.stopPropagation();
+ this.gearButton_.hideMenu();
+
+ this.setListType(FileManager.ListType.DETAIL);
+ this.currentList_.focus();
+ };
+
+ /**
+ * @param {Event} event Click event.
+ * @private
+ */
+ FileManager.prototype.onThumbnailViewButtonClick_ = function(event) {
+ // Stop propagate and hide the menu manually, in order to prevent the focus
+ // from being back to the button. (cf. http://crbug.com/248479)
+ event.stopPropagation();
+ this.gearButton_.hideMenu();
+
+ this.setListType(FileManager.ListType.THUMBNAIL);
+ this.currentList_.focus();
+ };
+
+ /**
+ * KeyDown event handler for the document.
+ * @param {Event} event Key event.
+ * @private
+ */
+ FileManager.prototype.onKeyDown_ = function(event) {
+ if (event.srcElement === this.renameInput_) {
+ // Ignore keydown handler in the rename input box.
+ return;
+ }
+
+ switch (util.getKeyModifiers(event) + event.keyCode) {
+ case 'Ctrl-190': // Ctrl-. => Toggle filter files.
+ this.fileFilter_.setFilterHidden(
+ !this.fileFilter_.isFilterHiddenOn());
+ event.preventDefault();
+ return;
+
+ case '27': // Escape => Cancel dialog.
+ if (this.dialogType != DialogType.FULL_PAGE) {
+ // If there is nothing else for ESC to do, then cancel the dialog.
+ event.preventDefault();
+ this.cancelButton_.click();
+ }
+ break;
+ }
+ };
+
+ /**
+ * KeyDown event handler for the div#list-container element.
+ * @param {Event} event Key event.
+ * @private
+ */
+ FileManager.prototype.onListKeyDown_ = function(event) {
+ if (event.srcElement.tagName == 'INPUT') {
+ // Ignore keydown handler in the rename input box.
+ return;
+ }
+
+ switch (util.getKeyModifiers(event) + event.keyCode) {
+ case '8': // Backspace => Up one directory.
+ event.preventDefault();
+ var path = this.getCurrentDirectory();
+ if (path && !PathUtil.isRootPath(path)) {
+ var path = path.replace(/\/[^\/]+$/, '');
+ this.directoryModel_.changeDirectory(path);
+ }
+ break;
+
+ case '13': // Enter => Change directory or perform default action.
+ // TODO(dgozman): move directory action to dispatchSelectionAction.
+ var selection = this.getSelection();
+ if (selection.totalCount == 1 &&
+ selection.entries[0].isDirectory &&
+ !DialogType.isFolderDialog(this.dialogType)) {
+ event.preventDefault();
+ this.onDirectoryAction_(selection.entries[0]);
+ } else if (this.dispatchSelectionAction_()) {
+ event.preventDefault();
+ }
+ break;
+ }
+
+ switch (event.keyIdentifier) {
+ case 'Home':
+ case 'End':
+ case 'Up':
+ case 'Down':
+ case 'Left':
+ case 'Right':
+ // When navigating with keyboard we hide the distracting mouse hover
+ // highlighting until the user moves the mouse again.
+ this.setNoHover_(true);
+ break;
+ }
+ };
+
+ /**
+ * Suppress/restore hover highlighting in the list container.
+ * @param {boolean} on True to temporarity hide hover state.
+ * @private
+ */
+ FileManager.prototype.setNoHover_ = function(on) {
+ if (on) {
+ this.listContainer_.classList.add('nohover');
+ } else {
+ this.listContainer_.classList.remove('nohover');
+ }
+ };
+
+ /**
+ * KeyPress event handler for the div#list-container element.
+ * @param {Event} event Key event.
+ * @private
+ */
+ FileManager.prototype.onListKeyPress_ = function(event) {
+ if (event.srcElement.tagName == 'INPUT') {
+ // Ignore keypress handler in the rename input box.
+ return;
+ }
+
+ if (event.ctrlKey || event.metaKey || event.altKey)
+ return;
+
+ var now = new Date();
+ var char = String.fromCharCode(event.charCode).toLowerCase();
+ var text = now - this.textSearchState_.date > 1000 ? '' :
+ this.textSearchState_.text;
+ this.textSearchState_ = {text: text + char, date: now};
+
+ this.doTextSearch_();
+ };
+
+ /**
+ * Mousemove event handler for the div#list-container element.
+ * @param {Event} event Mouse event.
+ * @private
+ */
+ FileManager.prototype.onListMouseMove_ = function(event) {
+ // The user grabbed the mouse, restore the hover highlighting.
+ this.setNoHover_(false);
+ };
+
+ /**
+ * Performs a 'text search' - selects a first list entry with name
+ * starting with entered text (case-insensitive).
+ * @private
+ */
+ FileManager.prototype.doTextSearch_ = function() {
+ var text = this.textSearchState_.text;
+ if (!text)
+ return;
+
+ var dm = this.directoryModel_.getFileList();
+ for (var index = 0; index < dm.length; ++index) {
+ var name = dm.item(index).name;
+ if (name.substring(0, text.length).toLowerCase() == text) {
+ this.currentList_.selectionModel.selectedIndexes = [index];
+ return;
+ }
+ }
+
+ this.textSearchState_.text = '';
+ };
+
+ /**
+ * Handle a click of the cancel button. Closes the window.
+ * TODO(jamescook): Make unload handler work automatically, crbug.com/104811
+ *
+ * @param {Event} event The click event.
+ * @private
+ */
+ FileManager.prototype.onCancel_ = function(event) {
+ chrome.fileBrowserPrivate.cancelDialog();
+ this.onUnload_();
+ window.close();
+ };
+
+ /**
+ * Resolves selected file urls returned from an Open dialog.
+ *
+ * For drive files this involves some special treatment.
+ * Starts getting drive files if needed.
+ *
+ * @param {Array.<string>} fileUrls Drive URLs.
+ * @param {function(Array.<string>)} callback To be called with fixed URLs.
+ * @private
+ */
+ FileManager.prototype.resolveSelectResults_ = function(fileUrls, callback) {
+ if (this.isOnDrive()) {
+ chrome.fileBrowserPrivate.getDriveFiles(
+ fileUrls,
+ function(localPaths) {
+ callback(fileUrls);
+ });
+ } else {
+ callback(fileUrls);
+ }
+ };
+
+ /**
+ * Closes this modal dialog with some files selected.
+ * TODO(jamescook): Make unload handler work automatically, crbug.com/104811
+ * @param {Object} selection Contains urls, filterIndex and multiple fields.
+ * @private
+ */
+ FileManager.prototype.callSelectFilesApiAndClose_ = function(selection) {
+ var self = this;
+ function callback() {
+ self.onUnload_();
+ window.close();
+ }
+ if (selection.multiple) {
+ chrome.fileBrowserPrivate.selectFiles(
+ selection.urls, this.params_.shouldReturnLocalPath, callback);
+ } else {
+ var forOpening = (this.dialogType != DialogType.SELECT_SAVEAS_FILE);
+ chrome.fileBrowserPrivate.selectFile(
+ selection.urls[0], selection.filterIndex, forOpening,
+ this.params_.shouldReturnLocalPath, callback);
+ }
+ };
+
+ /**
+ * Tries to close this modal dialog with some files selected.
+ * Performs preprocessing if needed (e.g. for Drive).
+ * @param {Object} selection Contains urls, filterIndex and multiple fields.
+ * @private
+ */
+ FileManager.prototype.selectFilesAndClose_ = function(selection) {
+ if (!this.isOnDrive() ||
+ this.dialogType == DialogType.SELECT_SAVEAS_FILE) {
+ setTimeout(this.callSelectFilesApiAndClose_.bind(this, selection), 0);
+ return;
+ }
+
+ var shade = this.document_.createElement('div');
+ shade.className = 'shade';
+ var footer = this.dialogDom_.querySelector('.button-panel');
+ var progress = footer.querySelector('.progress-track');
+ progress.style.width = '0%';
+ var cancelled = false;
+
+ var progressMap = {};
+ var filesStarted = 0;
+ var filesTotal = selection.urls.length;
+ for (var index = 0; index < selection.urls.length; index++) {
+ progressMap[selection.urls[index]] = -1;
+ }
+ var lastPercent = 0;
+ var bytesTotal = 0;
+ var bytesDone = 0;
+
+ var onFileTransfersUpdated = function(statusList) {
+ for (var index = 0; index < statusList.length; index++) {
+ var status = statusList[index];
+ var escaped = encodeURI(status.fileUrl);
+ if (!(escaped in progressMap)) continue;
+ if (status.total == -1) continue;
+
+ var old = progressMap[escaped];
+ if (old == -1) {
+ // -1 means we don't know file size yet.
+ bytesTotal += status.total;
+ filesStarted++;
+ old = 0;
+ }
+ bytesDone += status.processed - old;
+ progressMap[escaped] = status.processed;
+ }
+
+ var percent = bytesTotal == 0 ? 0 : bytesDone / bytesTotal;
+ // For files we don't have information about, assume the progress is zero.
+ percent = percent * filesStarted / filesTotal * 100;
+ // Do not decrease the progress. This may happen, if first downloaded
+ // file is small, and the second one is large.
+ lastPercent = Math.max(lastPercent, percent);
+ progress.style.width = lastPercent + '%';
+ }.bind(this);
+
+ var setup = function() {
+ this.document_.querySelector('.dialog-container').appendChild(shade);
+ setTimeout(function() { shade.setAttribute('fadein', 'fadein') }, 100);
+ footer.setAttribute('progress', 'progress');
+ this.cancelButton_.removeEventListener('click', this.onCancelBound_);
+ this.cancelButton_.addEventListener('click', onCancel);
+ chrome.fileBrowserPrivate.onFileTransfersUpdated.addListener(
+ onFileTransfersUpdated);
+ }.bind(this);
+
+ var cleanup = function() {
+ shade.parentNode.removeChild(shade);
+ footer.removeAttribute('progress');
+ this.cancelButton_.removeEventListener('click', onCancel);
+ this.cancelButton_.addEventListener('click', this.onCancelBound_);
+ chrome.fileBrowserPrivate.onFileTransfersUpdated.removeListener(
+ onFileTransfersUpdated);
+ }.bind(this);
+
+ var onCancel = function() {
+ cancelled = true;
+ // According to API cancel may fail, but there is no proper UI to reflect
+ // this. So, we just silently assume that everything is cancelled.
+ chrome.fileBrowserPrivate.cancelFileTransfers(
+ selection.urls, function(response) {});
+ cleanup();
+ }.bind(this);
+
+ var onResolved = function(resolvedUrls) {
+ if (cancelled) return;
+ cleanup();
+ selection.urls = resolvedUrls;
+ // Call next method on a timeout, as it's unsafe to
+ // close a window from a callback.
+ setTimeout(this.callSelectFilesApiAndClose_.bind(this, selection), 0);
+ }.bind(this);
+
+ var onProperties = function(properties) {
+ for (var i = 0; i < properties.length; i++) {
+ if (!properties[i] || properties[i].present) {
+ // For files already in GCache, we don't get any transfer updates.
+ filesTotal--;
+ }
+ }
+ this.resolveSelectResults_(selection.urls, onResolved);
+ }.bind(this);
+
+ setup();
+
+ // TODO(mtomasz): Use Entry instead of URLs, if possible.
+ util.URLsToEntries(selection.urls, function(entries) {
+ this.metadataCache_.get(entries, 'drive', onProperties);
+ }.bind(this));
+ };
+
+ /**
+ * Handle a click of the ok button.
+ *
+ * The ok button has different UI labels depending on the type of dialog, but
+ * in code it's always referred to as 'ok'.
+ *
+ * @param {Event} event The click event.
+ * @private
+ */
+ FileManager.prototype.onOk_ = function(event) {
+ if (this.dialogType == DialogType.SELECT_SAVEAS_FILE) {
+ // Save-as doesn't require a valid selection from the list, since
+ // we're going to take the filename from the text input.
+ var filename = this.filenameInput_.value;
+ if (!filename)
+ throw new Error('Missing filename!');
+
+ var directory = this.getCurrentDirectoryEntry();
+ var currentDirUrl = directory.toURL();
+ if (currentDirUrl.charAt(currentDirUrl.length - 1) != '/')
+ currentDirUrl += '/';
+ this.validateFileName_(currentDirUrl, filename, function(isValid) {
+ if (!isValid)
+ return;
+
+ if (util.isFakeEntry(directory)) {
+ // Can't save a file into a fake directory.
+ return;
+ }
+
+ var selectFileAndClose = function() {
+ this.selectFilesAndClose_({
+ urls: [currentDirUrl + encodeURIComponent(filename)],
+ multiple: false,
+ filterIndex: this.getSelectedFilterIndex_(filename)
+ });
+ }.bind(this);
+
+ directory.getFile(
+ filename, {create: false},
+ function(entry) {
+ // An existing file is found. Show confirmation dialog to
+ // overwrite it. If the user select "OK" on the dialog, save it.
+ this.confirm.show(strf('CONFIRM_OVERWRITE_FILE', filename),
+ selectFileAndClose);
+ }.bind(this),
+ function(error) {
+ if (error.code == FileError.NOT_FOUND_ERR) {
+ // The file does not exist, so it should be ok to create a
+ // new file.
+ selectFileAndClose();
+ return;
+ }
+ if (error.code == FileError.TYPE_MISMATCH_ERR) {
+ // An directory is found.
+ // Do not allow to overwrite directory.
+ this.alert.show(strf('DIRECTORY_ALREADY_EXISTS', filename));
+ return;
+ }
+
+ // Unexpected error.
+ console.error('File save failed: ' + error.code);
+ }.bind(this));
+ }.bind(this));
+ return;
+ }
+
+ var files = [];
+ var selectedIndexes = this.currentList_.selectionModel.selectedIndexes;
+
+ if (DialogType.isFolderDialog(this.dialogType) &&
+ selectedIndexes.length == 0) {
+ var url = this.getCurrentDirectoryURL();
+ var singleSelection = {
+ urls: [url],
+ multiple: false,
+ filterIndex: this.getSelectedFilterIndex_()
+ };
+ this.selectFilesAndClose_(singleSelection);
+ return;
+ }
+
+ // All other dialog types require at least one selected list item.
+ // The logic to control whether or not the ok button is enabled should
+ // prevent us from ever getting here, but we sanity check to be sure.
+ if (!selectedIndexes.length)
+ throw new Error('Nothing selected!');
+
+ var dm = this.directoryModel_.getFileList();
+ for (var i = 0; i < selectedIndexes.length; i++) {
+ var entry = dm.item(selectedIndexes[i]);
+ if (!entry) {
+ console.error('Error locating selected file at index: ' + i);
+ continue;
+ }
+
+ files.push(entry.toURL());
+ }
+
+ // Multi-file selection has no other restrictions.
+ if (this.dialogType == DialogType.SELECT_OPEN_MULTI_FILE) {
+ var multipleSelection = {
+ urls: files,
+ multiple: true
+ };
+ this.selectFilesAndClose_(multipleSelection);
+ return;
+ }
+
+ // Everything else must have exactly one.
+ if (files.length > 1)
+ throw new Error('Too many files selected!');
+
+ var selectedEntry = dm.item(selectedIndexes[0]);
+
+ if (DialogType.isFolderDialog(this.dialogType)) {
+ if (!selectedEntry.isDirectory)
+ throw new Error('Selected entry is not a folder!');
+ } else if (this.dialogType == DialogType.SELECT_OPEN_FILE) {
+ if (!selectedEntry.isFile)
+ throw new Error('Selected entry is not a file!');
+ }
+
+ var singleSelection = {
+ urls: [files[0]],
+ multiple: false,
+ filterIndex: this.getSelectedFilterIndex_()
+ };
+ this.selectFilesAndClose_(singleSelection);
+ };
+
+ /**
+ * Verifies the user entered name for file or folder to be created or
+ * renamed to. Name restrictions must correspond to File API restrictions
+ * (see DOMFilePath::isValidPath). Curernt WebKit implementation is
+ * out of date (spec is
+ * http://dev.w3.org/2009/dap/file-system/file-dir-sys.html, 8.3) and going to
+ * be fixed. Shows message box if the name is invalid.
+ *
+ * It also verifies if the name length is in the limit of the filesystem.
+ *
+ * @param {string} parentUrl The URL of the parent directory entry.
+ * @param {string} name New file or folder name.
+ * @param {function} onDone Function to invoke when user closes the
+ * warning box or immediatelly if file name is correct. If the name was
+ * valid it is passed true, and false otherwise.
+ * @private
+ */
+ FileManager.prototype.validateFileName_ = function(parentUrl, name, onDone) {
+ var msg;
+ var testResult = /[\/\\\<\>\:\?\*\"\|]/.exec(name);
+ if (testResult) {
+ msg = strf('ERROR_INVALID_CHARACTER', testResult[0]);
+ } else if (/^\s*$/i.test(name)) {
+ msg = str('ERROR_WHITESPACE_NAME');
+ } else if (/^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/i.test(name)) {
+ msg = str('ERROR_RESERVED_NAME');
+ } else if (this.fileFilter_.isFilterHiddenOn() && name[0] == '.') {
+ msg = str('ERROR_HIDDEN_NAME');
+ }
+
+ if (msg) {
+ this.alert.show(msg, function() {
+ onDone(false);
+ });
+ return;
+ }
+
+ var self = this;
+ chrome.fileBrowserPrivate.validatePathNameLength(
+ parentUrl, name, function(valid) {
+ if (!valid) {
+ self.alert.show(str('ERROR_LONG_NAME'),
+ function() { onDone(false); });
+ } else {
+ onDone(true);
+ }
+ });
+ };
+
+ /**
+ * Handler invoked on preference setting in drive context menu.
+ *
+ * @param {string} pref The preference to alter.
+ * @param {boolean} inverted Invert the value if true.
+ * @param {Event} event The click event.
+ * @private
+ */
+ FileManager.prototype.onDrivePrefClick_ = function(pref, inverted, event) {
+ var newValue = !event.target.hasAttribute('checked');
+ if (newValue)
+ event.target.setAttribute('checked', 'checked');
+ else
+ event.target.removeAttribute('checked');
+
+ var changeInfo = {};
+ changeInfo[pref] = inverted ? !newValue : newValue;
+ chrome.fileBrowserPrivate.setPreferences(changeInfo);
+ };
+
+ /**
+ * Invoked when the search box is changed.
+ *
+ * @param {Event} event The changed event.
+ * @private
+ */
+ FileManager.prototype.onSearchBoxUpdate_ = function(event) {
+ var searchString = this.searchBox_.value;
+
+ if (this.isOnDrive()) {
+ // When the search text is changed, finishes the search and showes back
+ // the last directory by passing an empty string to
+ // {@code DirectoryModel.search()}.
+ if (this.directoryModel_.isSearching() &&
+ this.lastSearchQuery_ != searchString) {
+ this.doSearch('');
+ }
+
+ // On drive, incremental search is not invoked since we have an auto-
+ // complete suggestion instead.
+ return;
+ }
+
+ this.search_(searchString);
+ };
+
+ /**
+ * Handle the search clear button click.
+ * @private
+ */
+ FileManager.prototype.onSearchClearButtonClick_ = function() {
+ this.ui_.searchBox.clear();
+ this.onSearchBoxUpdate_();
+ };
+
+ /**
+ * Search files and update the list with the search result.
+ *
+ * @param {string} searchString String to be searched with.
+ * @private
+ */
+ FileManager.prototype.search_ = function(searchString) {
+ var noResultsDiv = this.document_.getElementById('no-search-results');
+
+ var reportEmptySearchResults = function() {
+ if (this.directoryModel_.getFileList().length === 0) {
+ // The string 'SEARCH_NO_MATCHING_FILES_HTML' may contain HTML tags,
+ // hence we escapes |searchString| here.
+ var html = strf('SEARCH_NO_MATCHING_FILES_HTML',
+ util.htmlEscape(searchString));
+ noResultsDiv.innerHTML = html;
+ noResultsDiv.setAttribute('show', 'true');
+ } else {
+ noResultsDiv.removeAttribute('show');
+ }
+ };
+
+ var hideNoResultsDiv = function() {
+ noResultsDiv.removeAttribute('show');
+ };
+
+ this.doSearch(searchString,
+ reportEmptySearchResults.bind(this),
+ hideNoResultsDiv.bind(this));
+ };
+
+ /**
+ * Performs search and displays results.
+ *
+ * @param {string} query Query that will be searched for.
+ * @param {function()=} opt_onSearchRescan Function that will be called when
+ * the search directory is rescanned (i.e. search results are displayed).
+ * @param {function()=} opt_onClearSearch Function to be called when search
+ * state gets cleared.
+ */
+ FileManager.prototype.doSearch = function(
+ searchString, opt_onSearchRescan, opt_onClearSearch) {
+ var onSearchRescan = opt_onSearchRescan || function() {};
+ var onClearSearch = opt_onClearSearch || function() {};
+
+ this.lastSearchQuery_ = searchString;
+ this.directoryModel_.search(searchString, onSearchRescan, onClearSearch);
+ };
+
+ /**
+ * Requests autocomplete suggestions for files on Drive.
+ * Once the suggestions are returned, the autocomplete popup will show up.
+ *
+ * @param {string} query The text to autocomplete from.
+ * @private
+ */
+ FileManager.prototype.requestAutocompleteSuggestions_ = function(query) {
+ query = query.trimLeft();
+
+ // Only Drive supports auto-compelete
+ if (!this.isOnDrive())
+ return;
+
+ // Remember the most recent query. If there is an other request in progress,
+ // then it's result will be discarded and it will call a new request for
+ // this query.
+ this.lastAutocompleteQuery_ = query;
+ if (this.autocompleteSuggestionsBusy_)
+ return;
+
+ // The autocomplete list should be resized and repositioned here as the
+ // search box is resized when it's focused.
+ this.autocompleteList_.syncWidthAndPositionToInput();
+
+ if (!query) {
+ this.autocompleteList_.suggestions = [];
+ return;
+ }
+
+ var headerItem = {isHeaderItem: true, searchQuery: query};
+ if (!this.autocompleteList_.dataModel ||
+ this.autocompleteList_.dataModel.length == 0)
+ this.autocompleteList_.suggestions = [headerItem];
+ else
+ // Updates only the head item to prevent a flickering on typing.
+ this.autocompleteList_.dataModel.splice(0, 1, headerItem);
+
+ this.autocompleteSuggestionsBusy_ = true;
+
+ var searchParams = {
+ 'query': query,
+ 'types': 'ALL',
+ 'maxResults': 4
+ };
+ chrome.fileBrowserPrivate.searchDriveMetadata(
+ searchParams,
+ function(suggestions) {
+ this.autocompleteSuggestionsBusy_ = false;
+
+ // Discard results for previous requests and fire a new search
+ // for the most recent query.
+ if (query != this.lastAutocompleteQuery_) {
+ this.requestAutocompleteSuggestions_(this.lastAutocompleteQuery_);
+ return;
+ }
+
+ // Keeps the items in the suggestion list.
+ this.autocompleteList_.suggestions = [headerItem].concat(suggestions);
+ }.bind(this));
+ };
+
+ /**
+ * Opens the currently selected suggestion item.
+ * @private
+ */
+ FileManager.prototype.openAutocompleteSuggestion_ = function() {
+ var selectedItem = this.autocompleteList_.selectedItem;
+
+ // If the entry is the search item or no entry is selected, just change to
+ // the search result.
+ if (!selectedItem || selectedItem.isHeaderItem) {
+ var query = selectedItem ?
+ selectedItem.searchQuery : this.searchBox_.value;
+ this.search_(query);
+ return;
+ }
+
+ var entry = selectedItem.entry;
+ // If the entry is a directory, just change the directory.
+ if (entry.isDirectory) {
+ this.onDirectoryAction_(entry);
+ return;
+ }
+
+ var entries = [entry];
+ var self = this;
+
+ // To open a file, first get the mime type.
+ this.metadataCache_.get(entries, 'drive', function(props) {
+ var mimeType = props[0].contentMimeType || '';
+ var mimeTypes = [mimeType];
+ var openIt = function() {
+ if (self.dialogType == DialogType.FULL_PAGE) {
+ var tasks = new FileTasks(self);
+ tasks.init(entries, mimeTypes);
+ tasks.executeDefault();
+ } else {
+ self.onOk_();
+ }
+ };
+
+ // Change the current directory to the directory that contains the
+ // selected file. Note that this is necessary for an image or a video,
+ // which should be opened in the gallery mode, as the gallery mode
+ // requires the entry to be in the current directory model. For
+ // consistency, the current directory is always changed regardless of
+ // the file type.
+ entry.getParent(function(parent) {
+ var onDirectoryChanged = function(event) {
+ self.directoryModel_.removeEventListener('scan-completed',
+ onDirectoryChanged);
+ self.directoryModel_.selectEntry(entry);
+ openIt();
+ };
+ // changeDirectory() returns immediately. We should wait until the
+ // directory scan is complete.
+ self.directoryModel_.addEventListener('scan-completed',
+ onDirectoryChanged);
+ self.directoryModel_.changeDirectory(
+ parent.fullPath,
+ function() {
+ // Remove the listner if the change directory failed.
+ self.directoryModel_.removeEventListener('scan-completed',
+ onDirectoryChanged);
+ });
+ });
+ });
+ };
+
+ FileManager.prototype.decorateSplitter = function(splitterElement) {
+ var self = this;
+
+ var Splitter = cr.ui.Splitter;
+
+ var customSplitter = cr.ui.define('div');
+
+ customSplitter.prototype = {
+ __proto__: Splitter.prototype,
+
+ handleSplitterDragStart: function(e) {
+ Splitter.prototype.handleSplitterDragStart.apply(this, arguments);
+ this.ownerDocument.documentElement.classList.add('col-resize');
+ },
+
+ handleSplitterDragMove: function(deltaX) {
+ Splitter.prototype.handleSplitterDragMove.apply(this, arguments);
+ self.onResize_();
+ },
+
+ handleSplitterDragEnd: function(e) {
+ Splitter.prototype.handleSplitterDragEnd.apply(this, arguments);
+ this.ownerDocument.documentElement.classList.remove('col-resize');
+ }
+ };
+
+ customSplitter.decorate(splitterElement);
+ };
+
+ /**
+ * Updates default action menu item to match passed taskItem (icon,
+ * label and action).
+ *
+ * @param {Object} defaultItem - taskItem to match.
+ * @param {boolean} isMultiple - if multiple tasks available.
+ */
+ FileManager.prototype.updateContextMenuActionItems = function(defaultItem,
+ isMultiple) {
+ if (defaultItem) {
+ if (defaultItem.iconType) {
+ this.defaultActionMenuItem_.style.backgroundImage = '';
+ this.defaultActionMenuItem_.setAttribute('file-type-icon',
+ defaultItem.iconType);
+ } else if (defaultItem.iconUrl) {
+ this.defaultActionMenuItem_.style.backgroundImage =
+ 'url(' + defaultItem.iconUrl + ')';
+ } else {
+ this.defaultActionMenuItem_.style.backgroundImage = '';
+ }
+
+ this.defaultActionMenuItem_.label = defaultItem.title;
+ this.defaultActionMenuItem_.disabled = !!defaultItem.disabled;
+ this.defaultActionMenuItem_.taskId = defaultItem.taskId;
+ }
+
+ var defaultActionSeparator =
+ this.dialogDom_.querySelector('#default-action-separator');
+
+ this.openWithCommand_.canExecuteChange();
+ this.openWithCommand_.setHidden(!(defaultItem && isMultiple));
+ this.openWithCommand_.disabled = defaultItem && !!defaultItem.disabled;
+
+ this.defaultActionMenuItem_.hidden = !defaultItem;
+ defaultActionSeparator.hidden = !defaultItem;
+ };
+
+ /**
+ * Window beforeunload handler.
+ * @return {string} Message to show. Ignored when running as a packaged app.
+ * @private
+ */
+ FileManager.prototype.onBeforeUnload_ = function() {
+ if (this.filePopup_ &&
+ this.filePopup_.contentWindow &&
+ this.filePopup_.contentWindow.beforeunload) {
+ // The gallery might want to prevent the unload if it is busy.
+ return this.filePopup_.contentWindow.beforeunload();
+ }
+ return null;
+ };
+
+ /**
+ * @return {FileSelection} Selection object.
+ */
+ FileManager.prototype.getSelection = function() {
+ return this.selectionHandler_.selection;
+ };
+
+ /**
+ * @return {ArrayDataModel} File list.
+ */
+ FileManager.prototype.getFileList = function() {
+ return this.directoryModel_.getFileList();
+ };
+
+ /**
+ * @return {cr.ui.List} Current list object.
+ */
+ FileManager.prototype.getCurrentList = function() {
+ return this.currentList_;
+ };
+
+ /**
+ * Retrieve the preferences of the files.app. This method caches the result
+ * and returns it unless opt_update is true.
+ * @param {function(Object.<string, *>)} callback Callback to get the
+ * preference.
+ * @param {boolean=} opt_update If is's true, don't use the cache and
+ * retrieve latest preference. Default is false.
+ * @private
+ */
+ FileManager.prototype.getPreferences_ = function(callback, opt_update) {
+ if (!opt_update && this.preferences_ !== undefined) {
+ callback(this.preferences_);
+ return;
+ }
+
+ chrome.fileBrowserPrivate.getPreferences(function(prefs) {
+ this.preferences_ = prefs;
+ callback(prefs);
+ }.bind(this));
+ };
+})();
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/file_manager_commands.js b/chromium/chrome/browser/resources/file_manager/foreground/js/file_manager_commands.js
new file mode 100644
index 00000000000..52a26b92a64
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/file_manager_commands.js
@@ -0,0 +1,828 @@
+// 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.
+
+'use strict';
+
+/**
+ * TODO(dzvorygin): Here we use this hack, since 'hidden' is standard
+ * attribute and we can't use it's setter as usual.
+ * @param {boolean} value New value of hidden property.
+ */
+cr.ui.Command.prototype.setHidden = function(value) {
+ this.__lookupSetter__('hidden').call(this, value);
+};
+
+/**
+ * A command.
+ * @interface
+ */
+var Command = function() {};
+
+/**
+ * Handles the execute event.
+ * @param {Event} event Command event.
+ * @param {FileManager} fileManager FileManager.
+ */
+Command.prototype.execute = function(event, fileManager) {};
+
+/**
+ * Handles the can execute event.
+ * @param {Event} event Can execute event.
+ * @param {FileManager} fileManager FileManager.
+ */
+Command.prototype.canExecute = function(event, fileManager) {};
+
+/**
+ * Utility for commands.
+ */
+var CommandUtil = {};
+
+/**
+ * Extracts entry on which command event was dispatched.
+ *
+ * @param {DirectoryTree|DirectoryItem|NavigationList|HTMLLIElement|cr.ui.List}
+ * element Directory to extract a path from.
+ * @return {Entry} Entry of the found node.
+ */
+CommandUtil.getCommandEntry = function(element) {
+ if (element instanceof NavigationList) {
+ // element is a NavigationList.
+
+ /** @type {NavigationModelItem} */
+ var selectedItem = element.selectedItem;
+ return selectedItem && selectedItem.getCachedEntry();
+ } else if (element instanceof NavigationListItem) {
+ // element is a subitem of NavigationList.
+ /** @type {NavigationList} */
+ var navigationList = element.parentElement;
+ var index = navigationList.getIndexOfListItem(element);
+ /** @type {NavigationModelItem} */
+ var item = (index != -1) ? navigationList.dataModel.item(index) : null;
+ return item && item.getCachedEntry();
+ } else if (element instanceof DirectoryTree) {
+ // element is a DirectoryTree.
+ return element.selectedItem;
+ } else if (element instanceof DirectoryItem) {
+ // element is a sub item in DirectoryTree.
+
+ // DirectoryItem.fullPath is set on initialization, but entry is lazily.
+ // We may use fullPath just in case that the entry has not been set yet.
+ return element.entry;
+ } else if (element instanceof cr.ui.List) {
+ // element is a normal List (eg. the file list on the right panel).
+ var entry = element.selectedItem;
+ // Check if it is Entry or not by referring the fullPath member variable.
+ return entry && entry.fullPath ? entry : null;
+ } else {
+ console.warn('Unsupported element');
+ return null;
+ }
+};
+
+/**
+ * @param {NavigationList} navigationList navigation list to extract root node.
+ * @return {?RootType} Type of the found root.
+ */
+CommandUtil.getCommandRootType = function(navigationList) {
+ var root = CommandUtil.getCommandEntry(navigationList);
+ return root &&
+ PathUtil.isRootPath(root.fullPath) &&
+ PathUtil.getRootType(root.fullPath);
+};
+
+/**
+ * Checks if command can be executed on drive.
+ * @param {Event} event Command event to mark.
+ * @param {FileManager} fileManager FileManager to use.
+ */
+CommandUtil.canExecuteEnabledOnDriveOnly = function(event, fileManager) {
+ event.canExecute = fileManager.isOnDrive();
+};
+
+/**
+ * Checks if command should be visible on drive.
+ * @param {Event} event Command event to mark.
+ * @param {FileManager} fileManager FileManager to use.
+ */
+CommandUtil.canExecuteVisibleOnDriveOnly = function(event, fileManager) {
+ event.canExecute = fileManager.isOnDrive();
+ event.command.setHidden(!fileManager.isOnDrive());
+};
+
+/**
+ * Sets as the command as always enabled.
+ * @param {Event} event Command event to mark.
+ */
+CommandUtil.canExecuteAlways = function(event) {
+ event.canExecute = true;
+};
+
+/**
+ * Returns a single selected/passed entry or null.
+ * @param {Event} event Command event.
+ * @param {FileManager} fileManager FileManager to use.
+ * @return {FileEntry} The entry or null.
+ */
+CommandUtil.getSingleEntry = function(event, fileManager) {
+ if (event.target.entry) {
+ return event.target.entry;
+ }
+ var selection = fileManager.getSelection();
+ if (selection.totalCount == 1) {
+ return selection.entries[0];
+ }
+ return null;
+};
+
+/**
+ * Obtains target entries that can be pinned from the selection.
+ * If directories are included in the selection, it just returns an empty
+ * array to avoid confusing because pinning directory is not supported
+ * currently.
+ *
+ * @return {Array.<Entry>} Target entries.
+ */
+CommandUtil.getPinTargetEntries = function() {
+ var hasDirectory = false;
+ var results = fileManager.getSelection().entries.filter(function(entry) {
+ hasDirectory = hasDirectory || entry.isDirectory;
+ if (!entry || hasDirectory)
+ return false;
+ var metadata = fileManager.metadataCache_.getCached(entry, 'drive');
+ if (!metadata || metadata.hosted)
+ return false;
+ entry.pinned = metadata.pinned;
+ return true;
+ });
+ return hasDirectory ? [] : results;
+};
+
+/**
+ * Sets the default handler for the commandId and prevents handling
+ * the keydown events for this command. Not doing that breaks relationship
+ * of original keyboard event and the command. WebKit would handle it
+ * differently in some cases.
+ * @param {Node} node to register command handler on.
+ * @param {string} commandId Command id to respond to.
+ */
+CommandUtil.forceDefaultHandler = function(node, commandId) {
+ var doc = node.ownerDocument;
+ var command = doc.querySelector('command[id="' + commandId + '"]');
+ node.addEventListener('keydown', function(e) {
+ if (command.matchesEvent(e)) {
+ // Prevent cr.ui.CommandManager of handling it and leave it
+ // for the default handler.
+ e.stopPropagation();
+ }
+ });
+ node.addEventListener('command', function(event) {
+ if (event.command.id !== commandId)
+ return;
+ document.execCommand(event.command.id);
+ event.cancelBubble = true;
+ });
+ node.addEventListener('canExecute', function(event) {
+ if (event.command.id === commandId)
+ event.canExecute = document.queryCommandEnabled(event.command.id);
+ });
+};
+
+/**
+ * Default command.
+ * @type {Command}
+ */
+CommandUtil.defaultCommand = {
+ execute: function(event, fileManager) {
+ fileManager.document.execCommand(event.command.id);
+ },
+ canExecute: function(event, fileManager) {
+ event.canExecute = fileManager.document.queryCommandEnabled(
+ event.command.id);
+ }
+};
+
+/**
+ * Creates the volume switch command with index.
+ * @param {number} index Volume index from 1 to 9.
+ * @return {Command} Volume switch command.
+ */
+CommandUtil.createVolumeSwitchCommand = function(index) {
+ return {
+ execute: function(event, fileManager) {
+ fileManager.navigationList.selectByIndex(index - 1);
+ },
+ canExecute: function(event, fileManager) {
+ event.canExecute = index > 0 &&
+ index <= fileManager.navigationList.dataModel.length;
+ }
+ };
+};
+
+/**
+ * Handle of the command events.
+ * @param {FileManager} fileManager FileManager.
+ * @constructor
+ */
+var CommandHandler = function(fileManager) {
+ /**
+ * FileManager.
+ * @type {FileManager}
+ * @private
+ */
+ this.fileManager_ = fileManager;
+
+ /**
+ * Command elements.
+ * @type {Object.<string, cr.ui.Command>}
+ * @private
+ */
+ this.commands_ = {};
+
+ /**
+ * Whether the ctrl key is pressed or not.
+ * @type {boolean}
+ * @private
+ */
+ this.ctrlKeyPressed_ = false;
+
+ Object.seal(this);
+
+ // Decorate command tags in the document.
+ var commands = fileManager.document.querySelectorAll('command');
+ for (var i = 0; i < commands.length; i++) {
+ cr.ui.Command.decorate(commands[i]);
+ this.commands_[commands[i].id] = commands[i];
+ }
+
+ // Register events.
+ fileManager.document.addEventListener('command', this.onCommand_.bind(this));
+ fileManager.document.addEventListener('canExecute',
+ this.onCanExecute_.bind(this));
+ fileManager.document.addEventListener('keydown', this.onKeyDown_.bind(this));
+ fileManager.document.addEventListener('keyup', this.onKeyUp_.bind(this));
+};
+
+/**
+ * Updates the availability of all commands.
+ */
+CommandHandler.prototype.updateAvailability = function() {
+ for (var id in this.commands_) {
+ this.commands_[id].canExecuteChange();
+ }
+};
+
+/**
+ * Checks if the handler should ignore the current event, eg. since there is
+ * a popup dialog currently opened.
+ *
+ * @return {boolean} True if the event should be ignored, false otherwise.
+ * @private
+ */
+CommandHandler.prototype.shouldIgnoreEvents_ = function() {
+ // Do not handle commands, when a dialog is shown.
+ if (this.fileManager_.document.querySelector('.cr-dialog-container.shown'))
+ return true;
+
+ return false; // Do not ignore.
+};
+
+/**
+ * Handles command events.
+ * @param {Event} event Command event.
+ * @private
+ */
+CommandHandler.prototype.onCommand_ = function(event) {
+ if (this.shouldIgnoreEvents_())
+ return;
+ var handler = CommandHandler.COMMANDS_[event.command.id];
+ handler.execute.call(this, event, this.fileManager_);
+};
+
+/**
+ * Handles canExecute events.
+ * @param {Event} event Can execute event.
+ * @private
+ */
+CommandHandler.prototype.onCanExecute_ = function(event) {
+ if (this.shouldIgnoreEvents_())
+ return;
+ var handler = CommandHandler.COMMANDS_[event.command.id];
+ handler.canExecute.call(this, event, this.fileManager_);
+};
+
+/**
+ * Handle key down event.
+ * @param {Event} event Key down event.
+ * @private
+ */
+CommandHandler.prototype.onKeyDown_ = function(event) {
+ // 17 is the keycode of Ctrl key and it means the event is not for other keys
+ // with Ctrl modifier but for ctrl key itself.
+ if (util.getKeyModifiers(event) + event.keyCode == 'Ctrl-17') {
+ this.ctrlKeyPressed_ = true;
+ this.updateAvailability();
+ }
+};
+
+/**
+ * Handle key up event.
+ * @param {Event} event Key up event.
+ * @private
+ */
+CommandHandler.prototype.onKeyUp_ = function(event) {
+ // 17 is the keycode of Ctrl key and it means the event is not for other keys
+ // with Ctrl modifier but for ctrl key itself.
+ if (util.getKeyModifiers(event) + event.keyCode == '17') {
+ this.ctrlKeyPressed_ = false;
+ this.updateAvailability();
+ }
+};
+
+/**
+ * Commands.
+ * @type {Object.<string, Command>}
+ * @const
+ * @private
+ */
+CommandHandler.COMMANDS_ = {};
+
+/**
+ * Unmounts external drive.
+ * @type {Command}
+ */
+CommandHandler.COMMANDS_['unmount'] = {
+ /**
+ * @param {Event} event Command event.
+ * @param {FileManager} fileManager The file manager instance.
+ */
+ execute: function(event, fileManager) {
+ var root = CommandUtil.getCommandEntry(event.target);
+ if (root)
+ fileManager.unmountVolume(PathUtil.getRootPath(root.fullPath));
+ },
+ /**
+ * @param {Event} event Command event.
+ */
+ canExecute: function(event, fileManager) {
+ var rootType = CommandUtil.getCommandRootType(event.target);
+
+ event.canExecute = (rootType == RootType.ARCHIVE ||
+ rootType == RootType.REMOVABLE);
+ event.command.setHidden(!event.canExecute);
+ event.command.label = rootType == RootType.ARCHIVE ?
+ str('CLOSE_ARCHIVE_BUTTON_LABEL') :
+ str('UNMOUNT_DEVICE_BUTTON_LABEL');
+ }
+};
+
+/**
+ * Formats external drive.
+ * @type {Command}
+ */
+CommandHandler.COMMANDS_['format'] = {
+ /**
+ * @param {Event} event Command event.
+ * @param {FileManager} fileManager The file manager instance.
+ */
+ execute: function(event, fileManager) {
+ var directoryModel = fileManager.directoryModel;
+ var root = CommandUtil.getCommandEntry(event.target);
+ // 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();
+
+ // TODO(satorux): Stop assuming fullPath to be unique. crbug.com/320967
+ var mountPath = root.fullPath;
+ var volumeInfo = fileManager.volumeManager.getVolumeInfo(mountPath);
+ if (volumeInfo) {
+ fileManager.confirm.show(
+ loadTimeData.getString('FORMATTING_WARNING'),
+ chrome.fileBrowserPrivate.formatVolume.bind(null,
+ volumeInfo.volumeId));
+ }
+ },
+ /**
+ * @param {Event} event Command event.
+ * @param {FileManager} fileManager The file manager instance.
+ */
+ canExecute: function(event, fileManager) {
+ var directoryModel = fileManager.directoryModel;
+ var root = CommandUtil.getCommandEntry(event.target);
+ // See the comment in execute() for why doing this.
+ if (!root)
+ root = directoryModel.getCurrentDirEntry();
+ var removable = root &&
+ PathUtil.getRootType(root.fullPath) == RootType.REMOVABLE;
+ // Don't check if the volume is read-only. Unformatted volume is
+ // considered read-only per directoryModel.isPathReadOnly(), but can be
+ // formatted. An error will be raised if formatting failed anyway.
+ event.canExecute = removable;
+ event.command.setHidden(!removable);
+ }
+};
+
+/**
+ * Initiates new folder creation.
+ * @type {Command}
+ */
+CommandHandler.COMMANDS_['new-folder'] = {
+ execute: function(event, fileManager) {
+ fileManager.createNewFolder();
+ },
+ canExecute: function(event, fileManager) {
+ var directoryModel = fileManager.directoryModel;
+ event.canExecute = !fileManager.isOnReadonlyDirectory() &&
+ !fileManager.isRenamingInProgress() &&
+ !directoryModel.isSearching() &&
+ !directoryModel.isScanning();
+ }
+};
+
+/**
+ * Initiates new window creation.
+ * @type {Command}
+ */
+CommandHandler.COMMANDS_['new-window'] = {
+ execute: function(event, fileManager) {
+ fileManager.backgroundPage.launchFileManager({
+ defaultPath: fileManager.getCurrentDirectory()
+ });
+ },
+ canExecute: function(event, fileManager) {
+ event.canExecute =
+ fileManager.getCurrentDirectoryEntry() &&
+ (fileManager.dialogType === DialogType.FULL_PAGE);
+ }
+};
+
+/**
+ * Deletes selected files.
+ * @type {Command}
+ */
+CommandHandler.COMMANDS_['delete'] = {
+ execute: function(event, fileManager) {
+ fileManager.deleteSelection();
+ },
+ canExecute: function(event, fileManager) {
+ var allowDeletingWhileOffline =
+ fileManager.directoryModel.getCurrentRootType() === RootType.DRIVE;
+ var selection = fileManager.getSelection();
+ event.canExecute = (!fileManager.isOnReadonlyDirectory() ||
+ allowDeletingWhileOffline) &&
+ selection &&
+ selection.totalCount > 0;
+ }
+};
+
+/**
+ * Pastes files from clipboard.
+ * @type {Command}
+ */
+CommandHandler.COMMANDS_['paste'] = {
+ execute: function() {
+ document.execCommand(event.command.id);
+ },
+ canExecute: function(event, fileManager) {
+ var document = fileManager.document;
+ var fileTransferController = fileManager.fileTransferController;
+ event.canExecute = (fileTransferController &&
+ fileTransferController.queryPasteCommandEnabled());
+ }
+};
+
+CommandHandler.COMMANDS_['cut'] = CommandUtil.defaultCommand;
+CommandHandler.COMMANDS_['copy'] = CommandUtil.defaultCommand;
+
+/**
+ * Initiates file renaming.
+ * @type {Command}
+ */
+CommandHandler.COMMANDS_['rename'] = {
+ execute: function(event, fileManager) {
+ fileManager.initiateRename();
+ },
+ canExecute: function(event, fileManager) {
+ var allowRenamingWhileOffline =
+ fileManager.directoryModel.getCurrentRootType() === RootType.DRIVE;
+ var selection = fileManager.getSelection();
+ event.canExecute =
+ !fileManager.isRenamingInProgress() &&
+ (!fileManager.isOnReadonlyDirectory() || allowRenamingWhileOffline) &&
+ selection &&
+ selection.totalCount == 1;
+ }
+};
+
+/**
+ * Opens drive help.
+ * @type {Command}
+ */
+CommandHandler.COMMANDS_['volume-help'] = {
+ execute: function(event, fileManager) {
+ if (fileManager.isOnDrive())
+ util.visitURL(str('GOOGLE_DRIVE_HELP_URL'));
+ else
+ util.visitURL(str('FILES_APP_HELP_URL'));
+ },
+ canExecute: CommandUtil.canExecuteAlways
+};
+
+/**
+ * Opens drive buy-more-space url.
+ * @type {Command}
+ */
+CommandHandler.COMMANDS_['drive-buy-more-space'] = {
+ execute: function(event, fileManager) {
+ util.visitURL(str('GOOGLE_DRIVE_BUY_STORAGE_URL'));
+ },
+ canExecute: CommandUtil.canExecuteVisibleOnDriveOnly
+};
+
+/**
+ * Opens drive.google.com.
+ * @type {Command}
+ */
+CommandHandler.COMMANDS_['drive-go-to-drive'] = {
+ execute: function(event, fileManager) {
+ util.visitURL(str('GOOGLE_DRIVE_ROOT_URL'));
+ },
+ canExecute: CommandUtil.canExecuteVisibleOnDriveOnly
+};
+
+/**
+ * Displays open with dialog for current selection.
+ * @type {Command}
+ */
+CommandHandler.COMMANDS_['open-with'] = {
+ execute: function(event, fileManager) {
+ var tasks = fileManager.getSelection().tasks;
+ if (tasks) {
+ tasks.showTaskPicker(fileManager.defaultTaskPicker,
+ str('OPEN_WITH_BUTTON_LABEL'),
+ null,
+ function(task) {
+ tasks.execute(task.taskId);
+ });
+ }
+ },
+ canExecute: function(event, fileManager) {
+ var tasks = fileManager.getSelection().tasks;
+ event.canExecute = tasks && tasks.size() > 1;
+ }
+};
+
+/**
+ * Focuses search input box.
+ * @type {Command}
+ */
+CommandHandler.COMMANDS_['search'] = {
+ execute: function(event, fileManager) {
+ var element = fileManager.document.querySelector('#search-box input');
+ element.focus();
+ element.select();
+ },
+ canExecute: function(event, fileManager) {
+ event.canExecute = !fileManager.isRenamingInProgress();
+ }
+};
+
+/**
+ * Activates the n-th volume.
+ * @type {Command}
+ */
+CommandHandler.COMMANDS_['volume-switch-1'] =
+ CommandUtil.createVolumeSwitchCommand(1);
+CommandHandler.COMMANDS_['volume-switch-2'] =
+ CommandUtil.createVolumeSwitchCommand(2);
+CommandHandler.COMMANDS_['volume-switch-3'] =
+ CommandUtil.createVolumeSwitchCommand(3);
+CommandHandler.COMMANDS_['volume-switch-4'] =
+ CommandUtil.createVolumeSwitchCommand(4);
+CommandHandler.COMMANDS_['volume-switch-5'] =
+ CommandUtil.createVolumeSwitchCommand(5);
+CommandHandler.COMMANDS_['volume-switch-6'] =
+ CommandUtil.createVolumeSwitchCommand(6);
+CommandHandler.COMMANDS_['volume-switch-7'] =
+ CommandUtil.createVolumeSwitchCommand(7);
+CommandHandler.COMMANDS_['volume-switch-8'] =
+ CommandUtil.createVolumeSwitchCommand(8);
+CommandHandler.COMMANDS_['volume-switch-9'] =
+ CommandUtil.createVolumeSwitchCommand(9);
+
+/**
+ * Flips 'available offline' flag on the file.
+ * @type {Command}
+ */
+CommandHandler.COMMANDS_['toggle-pinned'] = {
+ execute: function(event, fileManager) {
+ var pin = !event.command.checked;
+ event.command.checked = pin;
+ var entries = CommandUtil.getPinTargetEntries();
+ var currentEntry;
+ var error = false;
+ var steps = {
+ // Pick an entry and pin it.
+ start: function() {
+ // Check if all the entries are pinned or not.
+ if (entries.length == 0)
+ return;
+ currentEntry = entries.shift();
+ chrome.fileBrowserPrivate.pinDriveFile(
+ currentEntry.toURL(),
+ pin,
+ steps.entryPinned);
+ },
+
+ // Check the result of pinning
+ entryPinned: function() {
+ // Convert to boolean.
+ error = !!chrome.runtime.lastError;
+ if (error && pin) {
+ fileManager.metadataCache_.get(
+ currentEntry, 'filesystem', steps.showError);
+ }
+ fileManager.metadataCache_.clear(currentEntry, 'drive');
+ fileManager.metadataCache_.get(
+ currentEntry, 'drive', steps.updateUI.bind(this));
+ },
+
+ // Update the user interface accoding to the cache state.
+ updateUI: function(drive) {
+ fileManager.updateMetadataInUI_(
+ 'drive', [currentEntry.toURL()], [drive]);
+ if (!error)
+ steps.start();
+ },
+
+ // Show the error
+ showError: function(filesystem) {
+ fileManager.alert.showHtml(str('DRIVE_OUT_OF_SPACE_HEADER'),
+ strf('DRIVE_OUT_OF_SPACE_MESSAGE',
+ unescape(currentEntry.name),
+ util.bytesToString(filesystem.size)));
+ }
+ };
+ steps.start();
+ },
+
+ canExecute: function(event, fileManager) {
+ var entries = CommandUtil.getPinTargetEntries();
+ var checked = true;
+ for (var i = 0; i < entries.length; i++) {
+ checked = checked && entries[i].pinned;
+ }
+ if (entries.length > 0) {
+ event.canExecute = true;
+ event.command.setHidden(false);
+ event.command.checked = checked;
+ } else {
+ event.canExecute = false;
+ event.command.setHidden(true);
+ }
+ }
+};
+
+/**
+ * Creates zip file for current selection.
+ * @type {Command}
+ */
+CommandHandler.COMMANDS_['zip-selection'] = {
+ execute: function(event, fileManager) {
+ var dirEntry = fileManager.getCurrentDirectoryEntry();
+ var selectionEntries = fileManager.getSelection().entries;
+ fileManager.fileOperationManager_.zipSelection(dirEntry, selectionEntries);
+ },
+ canExecute: function(event, fileManager) {
+ var dirEntry = fileManager.getCurrentDirectoryEntry();
+ var selection = fileManager.getSelection();
+ event.canExecute =
+ dirEntry &&
+ !fileManager.isOnReadonlyDirectory() &&
+ !fileManager.isOnDrive() &&
+ selection && selection.totalCount > 0;
+ }
+};
+
+/**
+ * Shows the share dialog for the current selection (single only).
+ * @type {Command}
+ */
+CommandHandler.COMMANDS_['share'] = {
+ execute: function(event, fileManager) {
+ fileManager.shareSelection();
+ },
+ canExecute: function(event, fileManager) {
+ var selection = fileManager.getSelection();
+ event.canExecute = fileManager.isOnDrive() &&
+ !fileManager.isDriveOffline() &&
+ selection && selection.totalCount == 1;
+ event.command.setHidden(!fileManager.isOnDrive());
+ }
+};
+
+/**
+ * Creates a shortcut of the selected folder (single only).
+ * @type {Command}
+ */
+CommandHandler.COMMANDS_['create-folder-shortcut'] = {
+ /**
+ * @param {Event} event Command event.
+ * @param {FileManager} fileManager The file manager instance.
+ */
+ execute: function(event, fileManager) {
+ var entry = CommandUtil.getCommandEntry(event.target);
+ if (entry)
+ fileManager.createFolderShortcut(entry.fullPath);
+ },
+
+ /**
+ * @param {Event} event Command event.
+ * @param {FileManager} fileManager The file manager instance.
+ */
+ canExecute: function(event, fileManager) {
+ var entry = CommandUtil.getCommandEntry(event.target);
+ var folderShortcutExists = entry &&
+ fileManager.folderShortcutExists(entry.fullPath);
+
+ var onlyOneFolderSelected = true;
+ // Only on list, user can select multiple files. The command is enabled only
+ // when a single file is selected.
+ if (event.target instanceof cr.ui.List &&
+ !(event.target instanceof NavigationList)) {
+ var items = event.target.selectedItems;
+ onlyOneFolderSelected = (items.length == 1 && items[0].isDirectory);
+ }
+
+ var eligible = entry &&
+ PathUtil.isEligibleForFolderShortcut(entry.fullPath);
+ event.canExecute =
+ eligible && onlyOneFolderSelected && !folderShortcutExists;
+ event.command.setHidden(!eligible || !onlyOneFolderSelected);
+ }
+};
+
+/**
+ * Removes the folder shortcut.
+ * @type {Command}
+ */
+CommandHandler.COMMANDS_['remove-folder-shortcut'] = {
+ /**
+ * @param {Event} event Command event.
+ * @param {FileManager} fileManager The file manager instance.
+ */
+ execute: function(event, fileManager) {
+ var entry = CommandUtil.getCommandEntry(event.target);
+ if (entry && entry.fullPath)
+ fileManager.removeFolderShortcut(entry.fullPath);
+ },
+
+ /**
+ * @param {Event} event Command event.
+ * @param {FileManager} fileManager The file manager instance.
+ */
+ canExecute: function(event, fileManager) {
+ var entry = CommandUtil.getCommandEntry(event.target);
+ var path = entry && entry.fullPath;
+
+ var eligible = path && PathUtil.isEligibleForFolderShortcut(path);
+ var isShortcut = path && fileManager.folderShortcutExists(path);
+ event.canExecute = isShortcut && eligible;
+ event.command.setHidden(!event.canExecute);
+ }
+};
+
+/**
+ * Zoom in to the Files.app.
+ * @type {Command}
+ */
+CommandHandler.COMMANDS_['zoom-in'] = {
+ execute: function(event, fileManager) {
+ chrome.fileBrowserPrivate.zoom('in');
+ },
+ canExecute: CommandUtil.canExecuteAlways
+};
+
+/**
+ * Zoom out from the Files.app.
+ * @type {Command}
+ */
+CommandHandler.COMMANDS_['zoom-out'] = {
+ execute: function(event, fileManager) {
+ chrome.fileBrowserPrivate.zoom('out');
+ },
+ canExecute: CommandUtil.canExecuteAlways
+};
+
+/**
+ * Reset the zoom factor.
+ * @type {Command}
+ */
+CommandHandler.COMMANDS_['zoom-reset'] = {
+ execute: function(event, fileManager) {
+ chrome.fileBrowserPrivate.zoom('reset');
+ },
+ canExecute: CommandUtil.canExecuteAlways
+};
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/file_operation_manager_wrapper.js b/chromium/chrome/browser/resources/file_manager/foreground/js/file_operation_manager_wrapper.js
new file mode 100644
index 00000000000..583b9bf549e
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/file_operation_manager_wrapper.js
@@ -0,0 +1,56 @@
+// Copyright 2013 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.
+
+'use strict';
+
+/**
+ * While FileOperationManager is run in the background page, this class is
+ * used to communicate with it.
+ * @param {DOMWindow} backgroundPage Window object of the background page.
+ * @constructor
+ */
+function FileOperationManagerWrapper(backgroundPage) {
+ this.fileOperationManager_ =
+ backgroundPage.FileOperationManager.getInstance();
+}
+
+/**
+ * Create a new instance or get existing instance of FCMW.
+ * @param {DOMWindow} backgroundPage Window object of the background page.
+ * @return {FileOperationManagerWrapper} FileOperationManagerWrapper instance.
+ */
+FileOperationManagerWrapper.getInstance = function(backgroundPage) {
+ if (!FileOperationManagerWrapper.instance_)
+ FileOperationManagerWrapper.instance_ =
+ new FileOperationManagerWrapper(backgroundPage);
+
+ return FileOperationManagerWrapper.instance_;
+};
+
+/**
+ * @return {boolean} True if there is a running task.
+ */
+FileOperationManagerWrapper.prototype.isRunning = function() {
+ return this.fileOperationManager_.hasQueuedTasks();
+};
+
+/**
+ * Decorates a FileOperationManager method, so it will be executed after
+ * initializing the FileOperationManager instance in background page.
+ * @param {string} method The method name.
+ */
+FileOperationManagerWrapper.decorateAsyncMethod = function(method) {
+ FileOperationManagerWrapper.prototype[method] = function() {
+ this.fileOperationManager_[method].apply(
+ this.fileOperationManager_, arguments);
+ };
+};
+
+FileOperationManagerWrapper.decorateAsyncMethod('paste');
+FileOperationManagerWrapper.decorateAsyncMethod('deleteEntries');
+FileOperationManagerWrapper.decorateAsyncMethod('forceDeleteTask');
+FileOperationManagerWrapper.decorateAsyncMethod('cancelDeleteTask');
+FileOperationManagerWrapper.decorateAsyncMethod('zipSelection');
+FileOperationManagerWrapper.decorateAsyncMethod('addEventListener');
+FileOperationManagerWrapper.decorateAsyncMethod('removeEventListener');
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/file_selection.js b/chromium/chrome/browser/resources/file_manager/foreground/js/file_selection.js
new file mode 100644
index 00000000000..945a48bfa0e
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/file_selection.js
@@ -0,0 +1,360 @@
+// 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.
+
+'use strict';
+
+/**
+ * The current selection object.
+ *
+ * @param {FileManager} fileManager FileManager instance.
+ * @param {Array.<number>} indexes Selected indexes.
+ * @constructor
+ */
+function FileSelection(fileManager, indexes) {
+ this.fileManager_ = fileManager;
+ this.computeBytesSequence_ = 0;
+ this.indexes = indexes;
+ this.entries = [];
+ this.totalCount = 0;
+ this.fileCount = 0;
+ this.directoryCount = 0;
+ this.bytes = 0;
+ this.showBytes = false;
+ this.allDriveFilesPresent = false,
+ this.iconType = null;
+ this.bytesKnown = false;
+ this.mustBeHidden_ = false;
+ this.mimeTypes = null;
+
+ // Synchronously compute what we can.
+ for (var i = 0; i < this.indexes.length; i++) {
+ var entry = fileManager.getFileList().item(this.indexes[i]);
+ if (!entry)
+ continue;
+
+ this.entries.push(entry);
+
+ if (this.iconType == null) {
+ this.iconType = FileType.getIcon(entry);
+ } else if (this.iconType != 'unknown') {
+ var iconType = FileType.getIcon(entry);
+ if (this.iconType != iconType)
+ this.iconType = 'unknown';
+ }
+
+ if (entry.isFile) {
+ this.fileCount += 1;
+ } else {
+ this.directoryCount += 1;
+ }
+ this.totalCount++;
+ }
+
+ this.tasks = new FileTasks(this.fileManager_);
+
+ Object.seal(this);
+}
+
+/**
+ * Computes data required to get file tasks and requests the tasks.
+ *
+ * @param {function} callback The callback.
+ */
+FileSelection.prototype.createTasks = function(callback) {
+ if (!this.fileManager_.isOnDrive()) {
+ this.tasks.init(this.entries);
+ callback();
+ return;
+ }
+
+ this.fileManager_.metadataCache_.get(this.entries, 'drive', function(props) {
+ var present = props.filter(function(p) { return p && p.availableOffline });
+ this.allDriveFilesPresent = present.length == props.length;
+
+ // Collect all of the mime types and push that info into the selection.
+ this.mimeTypes = props.map(function(value) {
+ return (value && value.contentMimeType) || '';
+ });
+
+ this.tasks.init(this.entries, this.mimeTypes);
+ callback();
+ }.bind(this));
+};
+
+/**
+ * Computes the total size of selected files.
+ *
+ * @param {function} callback Completion callback. Not called when cancelled,
+ * or a new call has been invoked in the meantime.
+ */
+FileSelection.prototype.computeBytes = function(callback) {
+ if (this.entries.length == 0) {
+ this.bytesKnown = true;
+ this.showBytes = false;
+ this.bytes = 0;
+ return;
+ }
+
+ var computeBytesSequence = ++this.computeBytesSequence_;
+ var pendingMetadataCount = 0;
+
+ var maybeDone = function() {
+ if (pendingMetadataCount == 0) {
+ this.bytesKnown = true;
+ callback();
+ }
+ }.bind(this);
+
+ var onProps = function(properties) {
+ // Ignore if the call got cancelled, or there is another new one fired.
+ if (computeBytesSequence != this.computeBytesSequence_)
+ return;
+
+ // It may happen that the metadata is not available because a file has been
+ // deleted in the meantime.
+ if (properties)
+ this.bytes += properties.size;
+ pendingMetadataCount--;
+ maybeDone();
+ }.bind(this);
+
+ for (var index = 0; index < this.entries.length; index++) {
+ var entry = this.entries[index];
+ if (entry.isFile) {
+ this.showBytes |= !FileType.isHosted(entry);
+ pendingMetadataCount++;
+ this.fileManager_.metadataCache_.get(entry, 'filesystem', onProps);
+ } else if (entry.isDirectory) {
+ // Don't compute the directory size as it's expensive.
+ // crbug.com/179073.
+ this.showBytes = false;
+ break;
+ }
+ }
+ maybeDone();
+};
+
+/**
+ * Cancels any async computation by increasing the sequence number. Results
+ * of any previous call to computeBytes() will be discarded.
+ *
+ * @private
+ */
+FileSelection.prototype.cancelComputing_ = function() {
+ this.computeBytesSequence_++;
+};
+
+/**
+ * This object encapsulates everything related to current selection.
+ *
+ * @param {FileManager} fileManager File manager instance.
+ * @extends {cr.EventTarget}
+ * @constructor
+ */
+function FileSelectionHandler(fileManager) {
+ this.fileManager_ = fileManager;
+ // TODO(dgozman): create a shared object with most of UI elements.
+ this.okButton_ = fileManager.okButton_;
+ this.filenameInput_ = fileManager.filenameInput_;
+ this.previewPanel_ = fileManager.previewPanel_;
+ this.taskItems_ = fileManager.taskItems_;
+}
+
+/**
+ * Create the temporary disabled action menu item.
+ * @return {Object} Created disabled item.
+ * @private
+ */
+FileSelectionHandler.createTemporaryDisabledActionMenuItem_ = function() {
+ if (!FileSelectionHandler.cachedDisabledActionMenuItem_) {
+ FileSelectionHandler.cachedDisabledActionMenuItem_ = {
+ label: str('ACTION_OPEN'),
+ disabled: true
+ };
+ }
+
+ return FileSelectionHandler.cachedDisabledActionMenuItem_;
+};
+
+/**
+ * Cached the temporary disabled action menu item. Used inside
+ * FileSelectionHandler.createTemporaryDisabledActionMenuItem_().
+ * @private
+ */
+FileSelectionHandler.cachedDisabledActionMenuItem_ = null;
+
+/**
+ * FileSelectionHandler extends cr.EventTarget.
+ */
+FileSelectionHandler.prototype.__proto__ = cr.EventTarget.prototype;
+
+/**
+ * Maximum amount of thumbnails in the preview pane.
+ *
+ * @const
+ * @type {number}
+ */
+FileSelectionHandler.MAX_PREVIEW_THUMBNAIL_COUNT = 4;
+
+/**
+ * Maximum width or height of an image what pops up when the mouse hovers
+ * thumbnail in the bottom panel (in pixels).
+ *
+ * @const
+ * @type {number}
+ */
+FileSelectionHandler.IMAGE_HOVER_PREVIEW_SIZE = 200;
+
+/**
+ * Update the UI when the selection model changes.
+ *
+ * @param {Event} event The change event.
+ */
+FileSelectionHandler.prototype.onFileSelectionChanged = function(event) {
+ var indexes =
+ this.fileManager_.getCurrentList().selectionModel.selectedIndexes;
+ if (this.selection) this.selection.cancelComputing_();
+ var selection = new FileSelection(this.fileManager_, indexes);
+ this.selection = selection;
+
+ if (this.fileManager_.dialogType == DialogType.SELECT_SAVEAS_FILE) {
+ // If this is a save-as dialog, copy the selected file into the filename
+ // input text box.
+ if (this.selection.totalCount == 1 &&
+ this.selection.entries[0].isFile &&
+ this.filenameInput_.value != this.selection.entries[0].name) {
+ this.filenameInput_.value = this.selection.entries[0].name;
+ }
+ }
+
+ this.updateOkButton();
+
+ if (this.selectionUpdateTimer_) {
+ clearTimeout(this.selectionUpdateTimer_);
+ this.selectionUpdateTimer_ = null;
+ }
+
+ // The rest of the selection properties are computed via (sometimes lengthy)
+ // asynchronous calls. We initiate these calls after a timeout. If the
+ // selection is changing quickly we only do this once when it slows down.
+
+ var updateDelay = 200;
+ var now = Date.now();
+ if (now > (this.lastFileSelectionTime_ || 0) + updateDelay) {
+ // The previous selection change happened a while ago. Update the UI soon.
+ updateDelay = 0;
+ }
+ this.lastFileSelectionTime_ = now;
+
+ if (this.fileManager_.dialogType === DialogType.FULL_PAGE &&
+ selection.directoryCount === 0 && selection.fileCount > 0) {
+ // Show disabled items for position calculation of the menu. They will be
+ // overridden in this.updateFileSelectionAsync().
+ this.fileManager_.updateContextMenuActionItems(
+ FileSelectionHandler.createTemporaryDisabledActionMenuItem_(), true);
+ } else {
+ // Update context menu.
+ this.fileManager_.updateContextMenuActionItems(null, false);
+ }
+
+ this.selectionUpdateTimer_ = setTimeout(function() {
+ this.selectionUpdateTimer_ = null;
+ if (this.selection == selection)
+ this.updateFileSelectionAsync(selection);
+ }.bind(this), updateDelay);
+};
+
+/**
+ * Updates the Ok button enabled state.
+ *
+ * @return {boolean} Whether button is enabled.
+ */
+FileSelectionHandler.prototype.updateOkButton = function() {
+ var selectable;
+ var dialogType = this.fileManager_.dialogType;
+
+ if (DialogType.isFolderDialog(dialogType)) {
+ // In SELECT_FOLDER mode, we allow to select current directory
+ // when nothing is selected.
+ selectable = this.selection.directoryCount <= 1 &&
+ this.selection.fileCount == 0;
+ } else if (dialogType == DialogType.SELECT_OPEN_FILE) {
+ selectable = (this.isFileSelectionAvailable() &&
+ this.selection.directoryCount == 0 &&
+ this.selection.fileCount == 1);
+ } else if (dialogType == DialogType.SELECT_OPEN_MULTI_FILE) {
+ selectable = (this.isFileSelectionAvailable() &&
+ this.selection.directoryCount == 0 &&
+ this.selection.fileCount >= 1);
+ } else if (dialogType == DialogType.SELECT_SAVEAS_FILE) {
+ if (this.fileManager_.isOnReadonlyDirectory()) {
+ selectable = false;
+ } else {
+ selectable = !!this.filenameInput_.value;
+ }
+ } else if (dialogType == DialogType.FULL_PAGE) {
+ // No "select" buttons on the full page UI.
+ selectable = true;
+ } else {
+ throw new Error('Unknown dialog type');
+ }
+
+ this.okButton_.disabled = !selectable;
+ return selectable;
+};
+
+/**
+ * Check if all the files in the current selection are available. The only
+ * case when files might be not available is when the selection contains
+ * uncached Drive files and the browser is offline.
+ *
+ * @return {boolean} True if all files in the current selection are
+ * available.
+ */
+FileSelectionHandler.prototype.isFileSelectionAvailable = function() {
+ return !this.fileManager_.isOnDrive() ||
+ !this.fileManager_.isDriveOffline() ||
+ this.selection.allDriveFilesPresent;
+};
+
+/**
+ * Calculates async selection stats and updates secondary UI elements.
+ *
+ * @param {FileSelection} selection The selection object.
+ */
+FileSelectionHandler.prototype.updateFileSelectionAsync = function(selection) {
+ if (this.selection != selection) return;
+
+ // Update the file tasks.
+ if (this.fileManager_.dialogType === DialogType.FULL_PAGE &&
+ selection.directoryCount === 0 && selection.fileCount > 0) {
+ selection.createTasks(function() {
+ if (this.selection != selection)
+ return;
+ selection.tasks.display(this.taskItems_);
+ selection.tasks.updateMenuItem();
+ }.bind(this));
+ } else {
+ this.taskItems_.hidden = true;
+ }
+
+ // Update preview panels.
+ var wasVisible = this.previewPanel_.visible;
+ this.previewPanel_.setSelection(selection);
+
+ // Scroll to item
+ if (!wasVisible && this.selection.totalCount == 1) {
+ var list = this.fileManager_.getCurrentList();
+ list.scrollIndexIntoView(list.selectionModel.selectedIndex);
+ }
+
+ // Sync the commands availability.
+ if (this.fileManager_.commandHandler)
+ this.fileManager_.commandHandler.updateAvailability();
+
+ // Inform tests it's OK to click buttons now.
+ if (selection.totalCount > 0) {
+ chrome.test.sendMessage('selection-change-complete');
+ }
+};
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/file_table.js b/chromium/chrome/browser/resources/file_manager/foreground/js/file_table.js
new file mode 100644
index 00000000000..3280de32953
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/file_table.js
@@ -0,0 +1,1036 @@
+// 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.
+
+'use strict';
+
+/**
+ * Namespace for utility functions.
+ */
+var filelist = {};
+
+/**
+ * Custom column model for advanced auto-resizing.
+ *
+ * @param {Array.<cr.ui.table.TableColumn>} tableColumns Table columns.
+ * @extends {cr.ui.table.TableColumnModel}
+ * @constructor
+ */
+function FileTableColumnModel(tableColumns) {
+ cr.ui.table.TableColumnModel.call(this, tableColumns);
+}
+
+/**
+ * The columns whose index is less than the constant are resizable.
+ * @const
+ * @type {number}
+ * @private
+ */
+FileTableColumnModel.RESIZABLE_LENGTH_ = 4;
+
+/**
+ * Inherits from cr.ui.TableColumnModel.
+ */
+FileTableColumnModel.prototype.__proto__ =
+ cr.ui.table.TableColumnModel.prototype;
+
+/**
+ * Minimum width of column.
+ * @const
+ * @type {number}
+ * @private
+ */
+FileTableColumnModel.MIN_WIDTH_ = 10;
+
+/**
+ * Sets column width so that the column dividers move to the specified position.
+ * This function also check the width of each column and keep the width larger
+ * than MIN_WIDTH_.
+ *
+ * @private
+ * @param {Array.<number>} newPos Positions of each column dividers.
+ */
+FileTableColumnModel.prototype.applyColumnPositions_ = function(newPos) {
+ // Check the minimum width and adjust the positions.
+ for (var i = 0; i < newPos.length - 2; i++) {
+ if (newPos[i + 1] - newPos[i] < FileTableColumnModel.MIN_WIDTH_) {
+ newPos[i + 1] = newPos[i] + FileTableColumnModel.MIN_WIDTH_;
+ }
+ }
+ for (var i = newPos.length - 1; i >= 2; i--) {
+ if (newPos[i] - newPos[i - 1] < FileTableColumnModel.MIN_WIDTH_) {
+ newPos[i - 1] = newPos[i] - FileTableColumnModel.MIN_WIDTH_;
+ }
+ }
+ // Set the new width of columns
+ for (var i = 0; i < FileTableColumnModel.RESIZABLE_LENGTH_; i++) {
+ this.columns_[i].width = newPos[i + 1] - newPos[i];
+ }
+};
+
+/**
+ * Normalizes widths to make their sum 100% if possible. Uses the proportional
+ * approach with some additional constraints.
+ *
+ * @param {number} contentWidth Target width.
+ * @override
+ */
+FileTableColumnModel.prototype.normalizeWidths = function(contentWidth) {
+ var totalWidth = 0;
+ var fixedWidth = 0;
+ // Some columns have fixed width.
+ for (var i = 0; i < this.columns_.length; i++) {
+ if (i < FileTableColumnModel.RESIZABLE_LENGTH_)
+ totalWidth += this.columns_[i].width;
+ else
+ fixedWidth += this.columns_[i].width;
+ }
+ var newTotalWidth = Math.max(contentWidth - fixedWidth, 0);
+ var positions = [0];
+ var sum = 0;
+ for (var i = 0; i < FileTableColumnModel.RESIZABLE_LENGTH_; i++) {
+ var column = this.columns_[i];
+ sum += column.width;
+ // Faster alternative to Math.floor for non-negative numbers.
+ positions[i + 1] = ~~(newTotalWidth * sum / totalWidth);
+ }
+ this.applyColumnPositions_(positions);
+};
+
+/**
+ * Handles to the start of column resizing by splitters.
+ */
+FileTableColumnModel.prototype.handleSplitterDragStart = function() {
+ this.columnPos_ = [0];
+ for (var i = 0; i < this.columns_.length; i++) {
+ this.columnPos_[i + 1] = this.columns_[i].width + this.columnPos_[i];
+ }
+};
+
+/**
+ * Handles to the end of column resizing by splitters.
+ */
+FileTableColumnModel.prototype.handleSplitterDragEnd = function() {
+ this.columnPos_ = null;
+};
+
+/**
+ * Sets the width of column with keeping the total width of table.
+ * @param {number} columnIndex Index of column that is resized.
+ * @param {number} columnWidth New width of the column.
+ */
+FileTableColumnModel.prototype.setWidthAndKeepTotal = function(
+ columnIndex, columnWidth) {
+ // Skip to resize 'selection' column
+ if (columnIndex < 0 ||
+ columnIndex >= FileTableColumnModel.RESIZABLE_LENGTH_ ||
+ !this.columnPos_) {
+ return;
+ }
+
+ // Calculate new positions of column splitters.
+ var newPosStart =
+ this.columnPos_[columnIndex] + Math.max(columnWidth,
+ FileTableColumnModel.MIN_WIDTH_);
+ var newPos = [];
+ var posEnd = this.columnPos_[FileTableColumnModel.RESIZABLE_LENGTH_];
+ for (var i = 0; i < columnIndex + 1; i++) {
+ newPos[i] = this.columnPos_[i];
+ }
+ for (var i = columnIndex + 1;
+ i < FileTableColumnModel.RESIZABLE_LENGTH_;
+ i++) {
+ var posStart = this.columnPos_[columnIndex + 1];
+ newPos[i] = (posEnd - newPosStart) *
+ (this.columnPos_[i] - posStart) /
+ (posEnd - posStart) +
+ newPosStart;
+ // Faster alternative to Math.floor for non-negative numbers.
+ newPos[i] = ~~newPos[i];
+ }
+ newPos[columnIndex] = this.columnPos_[columnIndex];
+ newPos[FileTableColumnModel.RESIZABLE_LENGTH_] = posEnd;
+ this.applyColumnPositions_(newPos);
+
+ // Notifiy about resizing
+ cr.dispatchSimpleEvent(this, 'resize');
+};
+
+/**
+ * Custom splitter that resizes column with retaining the sum of all the column
+ * width.
+ */
+var FileTableSplitter = cr.ui.define('div');
+
+/**
+ * Inherits from cr.ui.TableSplitter.
+ */
+FileTableSplitter.prototype.__proto__ = cr.ui.TableSplitter.prototype;
+
+/**
+ * Handles the drag start event.
+ */
+FileTableSplitter.prototype.handleSplitterDragStart = function() {
+ cr.ui.TableSplitter.prototype.handleSplitterDragStart.call(this);
+ this.table_.columnModel.handleSplitterDragStart();
+};
+
+/**
+ * Handles the drag move event.
+ * @param {number} deltaX Horizontal mouse move offset.
+ */
+FileTableSplitter.prototype.handleSplitterDragMove = function(deltaX) {
+ this.table_.columnModel.setWidthAndKeepTotal(this.columnIndex,
+ this.columnWidth_ + deltaX,
+ true);
+};
+
+/**
+ * Handles the drag end event.
+ */
+FileTableSplitter.prototype.handleSplitterDragEnd = function() {
+ cr.ui.TableSplitter.prototype.handleSplitterDragEnd.call(this);
+ this.table_.columnModel.handleSplitterDragEnd();
+};
+
+/**
+ * File list Table View.
+ * @constructor
+ */
+function FileTable() {
+ throw new Error('Designed to decorate elements');
+}
+
+/**
+ * Inherits from cr.ui.Table.
+ */
+FileTable.prototype.__proto__ = cr.ui.Table.prototype;
+
+/**
+ * Decorates the element.
+ * @param {HTMLElement} self Table to decorate.
+ * @param {MetadataCache} metadataCache To retrieve metadata.
+ * @param {boolean} fullPage True if it's full page File Manager,
+ * False if a file open/save dialog.
+ */
+FileTable.decorate = function(self, metadataCache, fullPage) {
+ cr.ui.Table.decorate(self);
+ self.__proto__ = FileTable.prototype;
+ self.metadataCache_ = metadataCache;
+ self.collator_ = Intl.Collator([], {numeric: true, sensitivity: 'base'});
+
+ var columns = [
+ new cr.ui.table.TableColumn('name', str('NAME_COLUMN_LABEL'),
+ fullPage ? 386 : 324),
+ new cr.ui.table.TableColumn('size', str('SIZE_COLUMN_LABEL'),
+ 110, true),
+ new cr.ui.table.TableColumn('type', str('TYPE_COLUMN_LABEL'),
+ fullPage ? 110 : 110),
+ new cr.ui.table.TableColumn('modificationTime',
+ str('DATE_COLUMN_LABEL'),
+ fullPage ? 150 : 210)
+ ];
+
+ columns[0].renderFunction = self.renderName_.bind(self);
+ columns[1].renderFunction = self.renderSize_.bind(self);
+ columns[1].defaultOrder = 'desc';
+ columns[2].renderFunction = self.renderType_.bind(self);
+ columns[3].renderFunction = self.renderDate_.bind(self);
+ columns[3].defaultOrder = 'desc';
+
+ var tableColumnModelClass;
+ tableColumnModelClass = FileTableColumnModel;
+ if (self.showCheckboxes) {
+ columns.push(new cr.ui.table.TableColumn('selection',
+ '',
+ 50, true));
+ columns[4].renderFunction = self.renderSelection_.bind(self);
+ columns[4].headerRenderFunction =
+ self.renderSelectionColumnHeader_.bind(self);
+ columns[4].fixed = true;
+ }
+
+ var columnModel = Object.create(tableColumnModelClass.prototype, {
+ /**
+ * The number of columns.
+ * @type {number}
+ */
+ size: {
+ /**
+ * @this {FileTableColumnModel}
+ * @return {number} Number of columns.
+ */
+ get: function() {
+ return this.totalSize;
+ }
+ },
+
+ /**
+ * The number of columns.
+ * @type {number}
+ */
+ totalSize: {
+ /**
+ * @this {FileTableColumnModel}
+ * @return {number} Number of columns.
+ */
+ get: function() {
+ return columns.length;
+ }
+ },
+
+ /**
+ * Obtains a column by the specified horizontal position.
+ */
+ getHitColumn: {
+ /**
+ * @this {FileTableColumnModel}
+ * @param {number} x Horizontal position.
+ * @return {object} The object that contains column index, column width,
+ * and hitPosition where the horizontal position is hit in the column.
+ */
+ value: function(x) {
+ for (var i = 0; x >= this.columns_[i].width; i++) {
+ x -= this.columns_[i].width;
+ }
+ if (i >= this.columns_.length)
+ return null;
+ return {index: i, hitPosition: x, width: this.columns_[i].width};
+ }
+ }
+ });
+
+ tableColumnModelClass.call(columnModel, columns);
+ self.columnModel = columnModel;
+ self.setDateTimeFormat(true);
+ self.setRenderFunction(self.renderTableRow_.bind(self,
+ self.getRenderFunction()));
+
+ self.scrollBar_ = MainPanelScrollBar();
+ self.scrollBar_.initialize(self, self.list);
+ // Keep focus on the file list when clicking on the header.
+ self.header.addEventListener('mousedown', function(e) {
+ self.list.focus();
+ e.preventDefault();
+ });
+
+ var handleSelectionChange = function() {
+ var selectAll = self.querySelector('#select-all-checkbox');
+ if (selectAll)
+ self.updateSelectAllCheckboxState_(selectAll);
+ };
+
+ self.relayoutAggregation_ =
+ new AsyncUtil.Aggregation(self.relayoutImmediately_.bind(self));
+
+ Object.defineProperty(self.list_, 'selectionModel', {
+ /**
+ * @this {cr.ui.List}
+ * @return {cr.ui.ListSelectionModel} The current selection model.
+ */
+ get: function() {
+ return this.selectionModel_;
+ },
+ /**
+ * @this {cr.ui.List}
+ */
+ set: function(value) {
+ var sm = this.selectionModel;
+ if (sm)
+ sm.removeEventListener('change', handleSelectionChange);
+
+ util.callInheritedSetter(this, 'selectionModel', value);
+ sm = value;
+
+ if (sm)
+ sm.addEventListener('change', handleSelectionChange);
+ handleSelectionChange();
+ }
+ });
+
+ // Override header#redraw to use FileTableSplitter.
+ self.header_.redraw = function() {
+ this.__proto__.redraw.call(this);
+ // Extend table splitters
+ var splitters = this.querySelectorAll('.table-header-splitter');
+ for (var i = 0; i < splitters.length; i++) {
+ if (splitters[i] instanceof FileTableSplitter)
+ continue;
+ FileTableSplitter.decorate(splitters[i]);
+ }
+ };
+
+ // Save the last selection. This is used by shouldStartDragSelection.
+ self.list.addEventListener('mousedown', function(e) {
+ this.lastSelection_ = this.selectionModel.selectedIndexes;
+ }.bind(self), true);
+ self.list.shouldStartDragSelection =
+ self.shouldStartDragSelection_.bind(self);
+
+ /**
+ * Obtains the index list of elements that are hit by the point or the
+ * rectangle.
+ *
+ * @param {number} x X coordinate value.
+ * @param {number} y Y coordinate value.
+ * @param {=number} opt_width Width of the coordinate.
+ * @param {=number} opt_height Height of the coordinate.
+ * @return {Array.<number>} Index list of hit elements.
+ */
+ self.list.getHitElements = function(x, y, opt_width, opt_height) {
+ var currentSelection = [];
+ var bottom = y + (opt_height || 0);
+ for (var i = 0; i < this.selectionModel_.length; i++) {
+ var itemMetrics = this.getHeightsForIndex_(i);
+ if (itemMetrics.top < bottom && itemMetrics.top + itemMetrics.height >= y)
+ currentSelection.push(i);
+ }
+ return currentSelection;
+ };
+};
+
+/**
+ * Sets date and time format.
+ * @param {boolean} use12hourClock True if 12 hours clock, False if 24 hours.
+ */
+FileTable.prototype.setDateTimeFormat = function(use12hourClock) {
+ this.timeFormatter_ = Intl.DateTimeFormat(
+ [] /* default locale */,
+ {hour: 'numeric', minute: 'numeric',
+ hour12: use12hourClock});
+ this.dateFormatter_ = Intl.DateTimeFormat(
+ [] /* default locale */,
+ {year: 'numeric', month: 'short', day: 'numeric',
+ hour: 'numeric', minute: 'numeric',
+ hour12: use12hourClock});
+};
+
+/**
+ * Obtains if the drag selection should be start or not by referring the mouse
+ * event.
+ * @param {MouseEvent} event Drag start event.
+ * @return {boolean} True if the mouse is hit to the background of the list.
+ * @private
+ */
+FileTable.prototype.shouldStartDragSelection_ = function(event) {
+ // If the shift key is pressed, it should starts drag selection.
+ if (event.shiftKey)
+ return true;
+
+ // If the position values are negative, it points the out of list.
+ // It should start the drag selection.
+ var pos = DragSelector.getScrolledPosition(this.list, event);
+ if (!pos)
+ return false;
+ if (pos.x < 0 || pos.y < 0)
+ return true;
+
+ // If the item index is out of range, it should start the drag selection.
+ var itemHeight = this.list.measureItem().height;
+ // Faster alternative to Math.floor for non-negative numbers.
+ var itemIndex = ~~(pos.y / itemHeight);
+ if (itemIndex >= this.list.dataModel.length)
+ return true;
+
+ // If the pointed item is already selected, it should not start the drag
+ // selection.
+ if (this.lastSelection_.indexOf(itemIndex) != -1)
+ return false;
+
+ // If the horizontal value is not hit to column, it should start the drag
+ // selection.
+ var hitColumn = this.columnModel.getHitColumn(pos.x);
+ if (!hitColumn)
+ return true;
+
+ // Check if the point is on the column contents or not.
+ var item = this.list.getListItemByIndex(itemIndex);
+ switch (this.columnModel.columns_[hitColumn.index].id) {
+ case 'name':
+ var spanElement = item.querySelector('.filename-label span');
+ var spanRect = spanElement.getBoundingClientRect();
+ // The this.list.cachedBounds_ object is set by
+ // DragSelector.getScrolledPosition.
+ if (!this.list.cachedBounds)
+ return true;
+ var textRight =
+ spanRect.left - this.list.cachedBounds.left + spanRect.width;
+ return textRight <= hitColumn.hitPosition;
+ default:
+ return true;
+ }
+};
+
+/**
+ * Update check and disable states of the 'Select all' checkbox.
+ * @param {HTMLInputElement} checkbox The checkbox. If not passed, using
+ * the default one.
+ * @private
+ */
+FileTable.prototype.updateSelectAllCheckboxState_ = function(checkbox) {
+ // TODO(serya): introduce this.selectionModel.selectedCount.
+ checkbox.checked = this.dataModel.length > 0 &&
+ this.dataModel.length == this.selectionModel.selectedIndexes.length;
+ checkbox.disabled = this.dataModel.length == 0;
+};
+
+/**
+ * Prepares the data model to be sorted by columns.
+ * @param {cr.ui.ArrayDataModel} dataModel Data model to prepare.
+ */
+FileTable.prototype.setupCompareFunctions = function(dataModel) {
+ dataModel.setCompareFunction('name',
+ this.compareName_.bind(this));
+ dataModel.setCompareFunction('modificationTime',
+ this.compareMtime_.bind(this));
+ dataModel.setCompareFunction('size',
+ this.compareSize_.bind(this));
+ dataModel.setCompareFunction('type',
+ this.compareType_.bind(this));
+};
+
+/**
+ * Render the Name column of the detail table.
+ *
+ * Invoked by cr.ui.Table when a file needs to be rendered.
+ *
+ * @param {Entry} entry The Entry object to render.
+ * @param {string} columnId The id of the column to be rendered.
+ * @param {cr.ui.Table} table The table doing the rendering.
+ * @return {HTMLDivElement} Created element.
+ * @private
+ */
+FileTable.prototype.renderName_ = function(entry, columnId, table) {
+ var label = this.ownerDocument.createElement('div');
+ label.appendChild(this.renderIconType_(entry, columnId, table));
+ label.entry = entry;
+ label.className = 'detail-name';
+ label.appendChild(filelist.renderFileNameLabel(this.ownerDocument, entry));
+ return label;
+};
+
+/**
+ * Render the Selection column of the detail table.
+ *
+ * Invoked by cr.ui.Table when a file needs to be rendered.
+ *
+ * @param {Entry} entry The Entry object to render.
+ * @param {string} columnId The id of the column to be rendered.
+ * @param {cr.ui.Table} table The table doing the rendering.
+ * @return {HTMLDivElement} Created element.
+ * @private
+ */
+FileTable.prototype.renderSelection_ = function(entry, columnId, table) {
+ var label = this.ownerDocument.createElement('div');
+ label.className = 'selection-label';
+ if (this.selectionModel.multiple) {
+ var checkBox = this.ownerDocument.createElement('input');
+ filelist.decorateSelectionCheckbox(checkBox, entry, this.list);
+ label.appendChild(checkBox);
+ }
+ return label;
+};
+
+/**
+ * Render the Size column of the detail table.
+ *
+ * @param {Entry} entry The Entry object to render.
+ * @param {string} columnId The id of the column to be rendered.
+ * @param {cr.ui.Table} table The table doing the rendering.
+ * @return {HTMLDivElement} Created element.
+ * @private
+ */
+FileTable.prototype.renderSize_ = function(entry, columnId, table) {
+ var div = this.ownerDocument.createElement('div');
+ div.className = 'size';
+ this.updateSize_(
+ div, entry, this.metadataCache_.getCached(entry, 'filesystem'));
+
+ return div;
+};
+
+/**
+ * Sets up or updates the size cell.
+ *
+ * @param {HTMLDivElement} div The table cell.
+ * @param {Entry} entry The corresponding entry.
+ * @param {Object} filesystemProps Metadata.
+ * @private
+ */
+FileTable.prototype.updateSize_ = function(div, entry, filesystemProps) {
+ if (!filesystemProps) {
+ div.textContent = '...';
+ } else if (filesystemProps.size == -1) {
+ div.textContent = '--';
+ } else if (filesystemProps.size == 0 &&
+ FileType.isHosted(entry)) {
+ div.textContent = '--';
+ } else {
+ div.textContent = util.bytesToString(filesystemProps.size);
+ }
+};
+
+/**
+ * Render the Type column of the detail table.
+ *
+ * @param {Entry} entry The Entry object to render.
+ * @param {string} columnId The id of the column to be rendered.
+ * @param {cr.ui.Table} table The table doing the rendering.
+ * @return {HTMLDivElement} Created element.
+ * @private
+ */
+FileTable.prototype.renderType_ = function(entry, columnId, table) {
+ var div = this.ownerDocument.createElement('div');
+ div.className = 'type';
+ div.textContent = FileType.getTypeString(entry);
+ return div;
+};
+
+/**
+ * Render the Date column of the detail table.
+ *
+ * @param {Entry} entry The Entry object to render.
+ * @param {string} columnId The id of the column to be rendered.
+ * @param {cr.ui.Table} table The table doing the rendering.
+ * @return {HTMLDivElement} Created element.
+ * @private
+ */
+FileTable.prototype.renderDate_ = function(entry, columnId, table) {
+ var div = this.ownerDocument.createElement('div');
+ div.className = 'date';
+
+ this.updateDate_(div,
+ this.metadataCache_.getCached(entry, 'filesystem'));
+ return div;
+};
+
+/**
+ * Sets up or updates the date cell.
+ *
+ * @param {HTMLDivElement} div The table cell.
+ * @param {Object} filesystemProps Metadata.
+ * @private
+ */
+FileTable.prototype.updateDate_ = function(div, filesystemProps) {
+ if (!filesystemProps) {
+ div.textContent = '...';
+ return;
+ }
+
+ var modTime = filesystemProps.modificationTime;
+ var today = new Date();
+ today.setHours(0);
+ today.setMinutes(0);
+ today.setSeconds(0);
+ today.setMilliseconds(0);
+
+ /**
+ * Number of milliseconds in a day.
+ */
+ var MILLISECONDS_IN_DAY = 24 * 60 * 60 * 1000;
+
+ if (modTime >= today &&
+ modTime < today.getTime() + MILLISECONDS_IN_DAY) {
+ div.textContent = strf('TIME_TODAY', this.timeFormatter_.format(modTime));
+ } else if (modTime >= today - MILLISECONDS_IN_DAY && modTime < today) {
+ div.textContent = strf('TIME_YESTERDAY',
+ this.timeFormatter_.format(modTime));
+ } else {
+ div.textContent =
+ this.dateFormatter_.format(filesystemProps.modificationTime);
+ }
+};
+
+/**
+ * Updates the file metadata in the table item.
+ *
+ * @param {Element} item Table item.
+ * @param {Entry} entry File entry.
+ */
+FileTable.prototype.updateFileMetadata = function(item, entry) {
+ var props = this.metadataCache_.getCached(entry, 'filesystem');
+ this.updateDate_(item.querySelector('.date'), props);
+ this.updateSize_(item.querySelector('.size'), entry, props);
+};
+
+/**
+ * Updates list items 'in place' on metadata change.
+ * @param {string} type Type of metadata change.
+ * @param {Object.<sting, Object>} propsMap Map from entry URLs to metadata
+ * properties.
+ */
+FileTable.prototype.updateListItemsMetadata = function(type, propsMap) {
+ var forEachCell = function(selector, callback) {
+ var cells = this.querySelectorAll(selector);
+ for (var i = 0; i < cells.length; i++) {
+ var cell = cells[i];
+ var listItem = this.list_.getListItemAncestor(cell);
+ var entry = this.dataModel.item(listItem.listIndex);
+ if (entry) {
+ var props = propsMap[entry.toURL()];
+ if (props)
+ callback.call(this, cell, entry, props, listItem);
+ }
+ }
+ }.bind(this);
+ if (type == 'filesystem') {
+ forEachCell('.table-row-cell > .date', function(item, entry, props) {
+ this.updateDate_(item, props);
+ });
+ forEachCell('.table-row-cell > .size', function(item, entry, props) {
+ this.updateSize_(item, entry, props);
+ });
+ } else if (type == 'drive') {
+ // The cell name does not matter as the entire list item is needed.
+ forEachCell('.table-row-cell > .date',
+ function(item, entry, props, listItem) {
+ filelist.updateListItemDriveProps(listItem, props);
+ });
+ }
+};
+
+/**
+ * Compare by mtime first, then by name.
+ * @param {Entry} a First entry.
+ * @param {Entry} b Second entry.
+ * @return {number} Compare result.
+ * @private
+ */
+FileTable.prototype.compareName_ = function(a, b) {
+ return this.collator_.compare(a.name, b.name);
+};
+
+/**
+ * Compare by mtime first, then by name.
+ * @param {Entry} a First entry.
+ * @param {Entry} b Second entry.
+ * @return {number} Compare result.
+ * @private
+ */
+FileTable.prototype.compareMtime_ = function(a, b) {
+ var aCachedFilesystem = this.metadataCache_.getCached(a, 'filesystem');
+ var aTime = aCachedFilesystem ? aCachedFilesystem.modificationTime : 0;
+
+ var bCachedFilesystem = this.metadataCache_.getCached(b, 'filesystem');
+ var bTime = bCachedFilesystem ? bCachedFilesystem.modificationTime : 0;
+
+ if (aTime > bTime)
+ return 1;
+
+ if (aTime < bTime)
+ return -1;
+
+ return this.collator_.compare(a.name, b.name);
+};
+
+/**
+ * Compare by size first, then by name.
+ * @param {Entry} a First entry.
+ * @param {Entry} b Second entry.
+ * @return {number} Compare result.
+ * @private
+ */
+FileTable.prototype.compareSize_ = function(a, b) {
+ var aCachedFilesystem = this.metadataCache_.getCached(a, 'filesystem');
+ var aSize = aCachedFilesystem ? aCachedFilesystem.size : 0;
+
+ var bCachedFilesystem = this.metadataCache_.getCached(b, 'filesystem');
+ var bSize = bCachedFilesystem ? bCachedFilesystem.size : 0;
+
+ if (aSize != bSize) return aSize - bSize;
+ return this.collator_.compare(a.name, b.name);
+};
+
+/**
+ * Compare by type first, then by subtype and then by name.
+ * @param {Entry} a First entry.
+ * @param {Entry} b Second entry.
+ * @return {number} Compare result.
+ * @private
+ */
+FileTable.prototype.compareType_ = function(a, b) {
+ // Directories precede files.
+ if (a.isDirectory != b.isDirectory)
+ return Number(b.isDirectory) - Number(a.isDirectory);
+
+ var aType = FileType.getTypeString(a);
+ var bType = FileType.getTypeString(b);
+
+ var result = this.collator_.compare(aType, bType);
+ if (result != 0)
+ return result;
+
+ return this.collator_.compare(a.name, b.name);
+};
+
+/**
+ * Renders table row.
+ * @param {function(Entry, cr.ui.Table)} baseRenderFunction Base renderer.
+ * @param {Entry} entry Corresponding entry.
+ * @return {HTMLLiElement} Created element.
+ * @private
+ */
+FileTable.prototype.renderTableRow_ = function(baseRenderFunction, entry) {
+ var item = baseRenderFunction(entry, this);
+ filelist.decorateListItem(item, entry, this.metadataCache_);
+ return item;
+};
+
+/**
+ * Renders the name column header.
+ * @param {string} name Localized column name.
+ * @return {HTMLLiElement} Created element.
+ * @private
+ */
+FileTable.prototype.renderNameColumnHeader_ = function(name) {
+ if (!this.selectionModel.multiple)
+ return this.ownerDocument.createTextNode(name);
+
+ var input = this.ownerDocument.createElement('input');
+ input.setAttribute('type', 'checkbox');
+ input.setAttribute('tabindex', -1);
+ input.id = 'select-all-checkbox';
+ input.className = 'common';
+
+ this.updateSelectAllCheckboxState_(input);
+
+ input.addEventListener('click', function(event) {
+ if (input.checked)
+ this.selectionModel.selectAll();
+ else
+ this.selectionModel.unselectAll();
+ event.stopPropagation();
+ }.bind(this));
+
+ var fragment = this.ownerDocument.createDocumentFragment();
+ fragment.appendChild(input);
+ fragment.appendChild(this.ownerDocument.createTextNode(name));
+ return fragment;
+};
+
+/**
+ * Renders the selection column header.
+ * @param {string} name Localized column name.
+ * @return {HTMLLiElement} Created element.
+ * @private
+ */
+FileTable.prototype.renderSelectionColumnHeader_ = function(name) {
+ if (!this.selectionModel.multiple)
+ return this.ownerDocument.createTextNode('');
+
+ var input = this.ownerDocument.createElement('input');
+ input.setAttribute('type', 'checkbox');
+ input.setAttribute('tabindex', -1);
+ input.id = 'select-all-checkbox';
+ input.className = 'common';
+
+ this.updateSelectAllCheckboxState_(input);
+
+ input.addEventListener('click', function(event) {
+ if (input.checked)
+ this.selectionModel.selectAll();
+ else
+ this.selectionModel.unselectAll();
+ event.stopPropagation();
+ }.bind(this));
+
+ var fragment = this.ownerDocument.createDocumentFragment();
+ fragment.appendChild(input);
+ return fragment;
+};
+
+/**
+ * Render the type column of the detail table.
+ *
+ * Invoked by cr.ui.Table when a file needs to be rendered.
+ *
+ * @param {Entry} entry The Entry object to render.
+ * @param {string} columnId The id of the column to be rendered.
+ * @param {cr.ui.Table} table The table doing the rendering.
+ * @return {HTMLDivElement} Created element.
+ * @private
+ */
+FileTable.prototype.renderIconType_ = function(entry, columnId, table) {
+ var icon = this.ownerDocument.createElement('div');
+ icon.className = 'detail-icon';
+ icon.setAttribute('file-type-icon', FileType.getIcon(entry));
+ return icon;
+};
+
+/**
+ * Sets the margin height for the transparent preview panel at the bottom.
+ * @param {number} margin Margin to be set in px.
+ */
+FileTable.prototype.setBottomMarginForPanel = function(margin) {
+ this.list_.style.paddingBottom = margin + 'px';
+ this.scrollBar_.setBottomMarginForPanel(margin);
+};
+
+/**
+ * Redraws the UI. Skips multiple consecutive calls.
+ */
+FileTable.prototype.relayout = function() {
+ this.relayoutAggregation_.run();
+};
+
+/**
+ * Redraws the UI immediately.
+ * @private
+ */
+FileTable.prototype.relayoutImmediately_ = function() {
+ if (this.clientWidth > 0)
+ this.normalizeColumns();
+ this.redraw();
+ cr.dispatchSimpleEvent(this.list, 'relayout');
+};
+
+/**
+ * Decorates (and wire up) a checkbox to be used in either a detail or a
+ * thumbnail list item.
+ * @param {HTMLInputElement} input Element to decorate.
+ */
+filelist.decorateCheckbox = function(input) {
+ var stopEventPropagation = function(event) {
+ if (!event.shiftKey)
+ event.stopPropagation();
+ };
+ input.setAttribute('type', 'checkbox');
+ input.setAttribute('tabindex', -1);
+ input.classList.add('common');
+ input.addEventListener('mousedown', stopEventPropagation);
+ input.addEventListener('mouseup', stopEventPropagation);
+
+ input.addEventListener(
+ 'click',
+ /**
+ * @this {HTMLInputElement}
+ */
+ function(event) {
+ // Revert default action and swallow the event
+ // if this is a multiple click or Shift is pressed.
+ if (event.detail > 1 || event.shiftKey) {
+ this.checked = !this.checked;
+ stopEventPropagation(event);
+ }
+ });
+};
+
+/**
+ * Decorates selection checkbox.
+ * @param {HTMLInputElement} input Element to decorate.
+ * @param {Entry} entry Corresponding entry.
+ * @param {cr.ui.List} list Owner list.
+ */
+filelist.decorateSelectionCheckbox = function(input, entry, list) {
+ filelist.decorateCheckbox(input);
+ input.classList.add('file-checkbox');
+ input.addEventListener('click', function(e) {
+ var sm = list.selectionModel;
+ var listIndex = list.getListItemAncestor(this).listIndex;
+ sm.setIndexSelected(listIndex, this.checked);
+ sm.leadIndex = listIndex;
+ if (sm.anchorIndex == -1)
+ sm.anchorIndex = listIndex;
+
+ });
+ // Since we do not want to open the item when tap on checkbox, we need to
+ // stop propagation of TAP event dispatched by checkbox ideally. But all
+ // touch events from touch_handler are dispatched to the list control. So we
+ // have to stop propagation of native touchstart event to prevent list
+ // control from generating TAP event here. The synthetic click event will
+ // select the touched checkbox/item.
+ input.addEventListener('touchstart',
+ function(e) { e.stopPropagation() });
+
+ var index = list.dataModel.indexOf(entry);
+ // Our DOM nodes get discarded as soon as we're scrolled out of view,
+ // so we have to make sure the check state is correct when we're brought
+ // back to life.
+ input.checked = list.selectionModel.getIndexSelected(index);
+};
+
+/**
+ * Common item decoration for table's and grid's items.
+ * @param {ListItem} li List item.
+ * @param {Entry} entry The entry.
+ * @param {MetadataCache} metadataCache Cache to retrieve metadada.
+ */
+filelist.decorateListItem = function(li, entry, metadataCache) {
+ li.classList.add(entry.isDirectory ? 'directory' : 'file');
+ if (FileType.isOnDrive(entry)) {
+ // The metadata may not yet be ready. In that case, the list item will be
+ // updated when the metadata is ready via updateListItemsMetadata.
+ var driveProps = metadataCache.getCached(entry, 'drive');
+ if (driveProps)
+ filelist.updateListItemDriveProps(li, driveProps);
+ }
+
+ // Overriding the default role 'list' to 'listbox' for better
+ // accessibility on ChromeOS.
+ li.setAttribute('role', 'option');
+
+ Object.defineProperty(li, 'selected', {
+ /**
+ * @this {ListItem}
+ * @return {boolean} True if the list item is selected.
+ */
+ get: function() {
+ return this.hasAttribute('selected');
+ },
+
+ /**
+ * @this {ListItem}
+ */
+ set: function(v) {
+ if (v)
+ this.setAttribute('selected', '');
+ else
+ this.removeAttribute('selected');
+ var checkBox = this.querySelector('input.file-checkbox');
+ if (checkBox)
+ checkBox.checked = !!v;
+ }
+ });
+};
+
+/**
+ * Render filename label for grid and list view.
+ * @param {HTMLDocument} doc Owner document.
+ * @param {Entry} entry The Entry object to render.
+ * @return {HTMLDivElement} The label.
+ */
+filelist.renderFileNameLabel = function(doc, entry) {
+ // Filename need to be in a '.filename-label' container for correct
+ // work of inplace renaming.
+ var box = doc.createElement('div');
+ box.className = 'filename-label';
+ var fileName = doc.createElement('span');
+ fileName.textContent = entry.name;
+ box.appendChild(fileName);
+
+ return box;
+};
+
+/**
+ * Updates grid item or table row for the driveProps.
+ * @param {cr.ui.ListItem} li List item.
+ * @param {Object} driveProps Metadata.
+ */
+filelist.updateListItemDriveProps = function(li, driveProps) {
+ if (li.classList.contains('file')) {
+ if (driveProps.availableOffline)
+ li.classList.remove('dim-offline');
+ else
+ li.classList.add('dim-offline');
+ // TODO(mtomasz): Consider adding some vidual indication for files which
+ // are not cached on LTE. Currently we show them as normal files.
+ // crbug.com/246611.
+ }
+
+ if (driveProps.customIconUrl) {
+ var iconDiv = li.querySelector('.detail-icon');
+ if (!iconDiv)
+ return;
+ iconDiv.style.backgroundImage = 'url(' + driveProps.customIconUrl + ')';
+ }
+};
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/file_tasks.js b/chromium/chrome/browser/resources/file_manager/foreground/js/file_tasks.js
new file mode 100644
index 00000000000..7bde88ca9c2
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/file_tasks.js
@@ -0,0 +1,834 @@
+// 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.
+
+'use strict';
+
+/**
+ * This object encapsulates everything related to tasks execution.
+ *
+ * @param {FileManager} fileManager FileManager instance.
+ * @param {Object=} opt_params File manager load parameters.
+ * @constructor
+ */
+function FileTasks(fileManager, opt_params) {
+ this.fileManager_ = fileManager;
+ this.params_ = opt_params;
+ this.tasks_ = null;
+ this.defaultTask_ = null;
+ this.entries_ = null;
+
+ /**
+ * List of invocations to be called once tasks are available.
+ *
+ * @private
+ * @type {Array.<Object>}
+ */
+ this.pendingInvocations_ = [];
+}
+
+/**
+ * Location of the FAQ about the file actions.
+ *
+ * @const
+ * @type {string}
+ */
+FileTasks.NO_ACTION_FOR_FILE_URL = 'http://support.google.com/chromeos/bin/' +
+ 'answer.py?answer=1700055&topic=29026&ctx=topic';
+
+/**
+ * Location of the Chrome Web Store.
+ *
+ * @const
+ * @type {string}
+ */
+FileTasks.CHROME_WEB_STORE_URL = 'https://chrome.google.com/webstore';
+
+/**
+ * Base URL of apps list in the Chrome Web Store. This constant is used in
+ * FileTasks.createWebStoreLink().
+ *
+ * @const
+ * @type {string}
+ */
+FileTasks.WEB_STORE_HANDLER_BASE_URL =
+ 'https://chrome.google.com/webstore/category/collection/file_handlers';
+
+/**
+ * Returns URL of the Chrome Web Store which show apps supporting the given
+ * file-extension and mime-type.
+ *
+ * @param {string} extension Extension of the file (with the first dot).
+ * @param {string} mimeType Mime type of the file.
+ * @return {string} URL
+ */
+FileTasks.createWebStoreLink = function(extension, mimeType) {
+ if (!extension)
+ return FileTasks.CHROME_WEB_STORE_URL;
+
+ if (extension[0] === '.')
+ extension = extension.substr(1);
+ else
+ console.warn('Please pass an extension with a dot to createWebStoreLink.');
+
+ var url = FileTasks.WEB_STORE_HANDLER_BASE_URL;
+ url += '?_fe=' + extension.toLowerCase().replace(/[^\w]/g, '');
+
+ // If a mime is given, add it into the URL.
+ if (mimeType)
+ url += '&_fmt=' + mimeType.replace(/[^-\w\/]/g, '');
+ return url;
+};
+
+/**
+ * Complete the initialization.
+ *
+ * @param {Array.<Entry>} entries List of file entries.
+ * @param {Array.<string>=} opt_mimeTypes List of MIME types for each
+ * of the files.
+ */
+FileTasks.prototype.init = function(entries, opt_mimeTypes) {
+ this.entries_ = entries;
+ this.mimeTypes_ = opt_mimeTypes || [];
+
+ // TODO(mtomasz): Move conversion from entry to url to custom bindings.
+ var urls = util.entriesToURLs(entries);
+ if (urls.length > 0) {
+ chrome.fileBrowserPrivate.getFileTasks(urls, this.mimeTypes_,
+ this.onTasks_.bind(this));
+ }
+};
+
+/**
+ * Returns amount of tasks.
+ *
+ * @return {number} amount of tasks.
+ */
+FileTasks.prototype.size = function() {
+ return (this.tasks_ && this.tasks_.length) || 0;
+};
+
+/**
+ * Callback when tasks found.
+ *
+ * @param {Array.<Object>} tasks The tasks.
+ * @private
+ */
+FileTasks.prototype.onTasks_ = function(tasks) {
+ this.processTasks_(tasks);
+ for (var index = 0; index < this.pendingInvocations_.length; index++) {
+ var name = this.pendingInvocations_[index][0];
+ var args = this.pendingInvocations_[index][1];
+ this[name].apply(this, args);
+ }
+ this.pendingInvocations_ = [];
+};
+
+/**
+ * The list of known extensions to record UMA.
+ * Note: Because the data is recorded by the index, so new item shouldn't be
+ * inserted.
+ *
+ * @const
+ * @type {Array.<string>}
+ * @private
+ */
+FileTasks.knownExtensions_ = [
+ 'other', '.3ga', '.3gp', '.aac', '.alac', '.asf', '.avi', '.bmp', '.csv',
+ '.doc', '.docx', '.flac', '.gif', '.ico', '.jpeg', '.jpg', '.log', '.m3u',
+ '.m3u8', '.m4a', '.m4v', '.mid', '.mkv', '.mov', '.mp3', '.mp4', '.mpg',
+ '.odf', '.odp', '.ods', '.odt', '.oga', '.ogg', '.ogv', '.pdf', '.png',
+ '.ppt', '.pptx', '.ra', '.ram', '.rar', '.rm', '.rtf', '.wav', '.webm',
+ '.webp', '.wma', '.wmv', '.xls', '.xlsx',
+];
+
+/**
+ * The list of excutable file extensions.
+ *
+ * @const
+ * @type {Array.<string>}
+ */
+FileTasks.EXECUTABLE_EXTENSIONS = Object.freeze([
+ '.exe', '.lnk', '.deb', '.dmg', '.jar', '.msi',
+]);
+
+/**
+ * The list of extensions to skip the suggest app dialog.
+ * @const
+ * @type {Array.<string>}
+ * @private
+ */
+FileTasks.EXTENSIONS_TO_SKIP_SUGGEST_APPS_ = Object.freeze([
+ '.crdownload', '.dsc', '.inf', '.crx',
+]);
+
+/**
+ * Records trial of opening file grouped by extensions.
+ *
+ * @param {Array.<Entry>} entries The entries to be opened.
+ * @private
+ */
+FileTasks.recordViewingFileTypeUMA_ = function(entries) {
+ for (var i = 0; i < entries.length; i++) {
+ var entry = entries[i];
+ var extension = FileType.getExtension(entry).toLowerCase();
+ if (FileTasks.knownExtensions_.indexOf(extension) < 0) {
+ extension = 'other';
+ }
+ metrics.recordEnum(
+ 'ViewingFileType', extension, FileTasks.knownExtensions_);
+ }
+};
+
+/**
+ * Returns true if the taskId is for an internal task.
+ *
+ * @param {string} taskId Task identifier.
+ * @return {boolean} True if the task ID is for an internal task.
+ * @private
+ */
+FileTasks.isInternalTask_ = function(taskId) {
+ var taskParts = taskId.split('|');
+ var appId = taskParts[0];
+ var taskType = taskParts[1];
+ var actionId = taskParts[2];
+ // The action IDs here should match ones used in executeInternalTask_().
+ return (appId === chrome.runtime.id &&
+ taskType === 'file' &&
+ (actionId === 'play' ||
+ actionId === 'watch' ||
+ actionId === 'mount-archive' ||
+ actionId === 'gallery'));
+};
+
+/**
+ * Processes internal tasks.
+ *
+ * @param {Array.<Object>} tasks The tasks.
+ * @private
+ */
+FileTasks.prototype.processTasks_ = function(tasks) {
+ this.tasks_ = [];
+ var id = chrome.runtime.id;
+ var isOnDrive = false;
+ for (var index = 0; index < this.entries_.length; ++index) {
+ if (FileType.isOnDrive(this.entries_[index])) {
+ isOnDrive = true;
+ break;
+ }
+ }
+
+ for (var i = 0; i < tasks.length; i++) {
+ var task = tasks[i];
+ var taskParts = task.taskId.split('|');
+
+ // Skip internal Files.app's handlers.
+ if (taskParts[0] === id && (taskParts[2] === 'auto-open' ||
+ taskParts[2] === 'select' || taskParts[2] === 'open')) {
+ continue;
+ }
+
+ // Tweak images, titles of internal tasks.
+ if (taskParts[0] === id && taskParts[1] === 'file') {
+ if (taskParts[2] === 'play') {
+ // TODO(serya): This hack needed until task.iconUrl is working
+ // (see GetFileTasksFileBrowserFunction::RunImpl).
+ task.iconType = 'audio';
+ task.title = loadTimeData.getString('ACTION_LISTEN');
+ } else if (taskParts[2] === 'mount-archive') {
+ task.iconType = 'archive';
+ task.title = loadTimeData.getString('MOUNT_ARCHIVE');
+ } else if (taskParts[2] === 'gallery') {
+ task.iconType = 'image';
+ task.title = loadTimeData.getString('ACTION_OPEN');
+ } else if (taskParts[2] === 'watch') {
+ task.iconType = 'video';
+ task.title = loadTimeData.getString('ACTION_WATCH');
+ } else if (taskParts[2] === 'open-hosted-generic') {
+ if (this.entries_.length > 1)
+ task.iconType = 'generic';
+ else // Use specific icon.
+ task.iconType = FileType.getIcon(this.entries_[0]);
+ task.title = loadTimeData.getString('ACTION_OPEN');
+ } else if (taskParts[2] === 'open-hosted-gdoc') {
+ task.iconType = 'gdoc';
+ task.title = loadTimeData.getString('ACTION_OPEN_GDOC');
+ } else if (taskParts[2] === 'open-hosted-gsheet') {
+ task.iconType = 'gsheet';
+ task.title = loadTimeData.getString('ACTION_OPEN_GSHEET');
+ } else if (taskParts[2] === 'open-hosted-gslides') {
+ task.iconType = 'gslides';
+ task.title = loadTimeData.getString('ACTION_OPEN_GSLIDES');
+ } else if (taskParts[2] === 'view-swf') {
+ // Do not render this task if disabled.
+ if (!loadTimeData.getBoolean('SWF_VIEW_ENABLED'))
+ continue;
+ task.iconType = 'generic';
+ task.title = loadTimeData.getString('ACTION_VIEW');
+ } else if (taskParts[2] === 'view-pdf') {
+ // Do not render this task if disabled.
+ if (!loadTimeData.getBoolean('PDF_VIEW_ENABLED'))
+ continue;
+ task.iconType = 'pdf';
+ task.title = loadTimeData.getString('ACTION_VIEW');
+ } else if (taskParts[2] === 'view-in-browser') {
+ task.iconType = 'generic';
+ task.title = loadTimeData.getString('ACTION_VIEW');
+ }
+ }
+
+ if (!task.iconType && taskParts[1] === 'web-intent') {
+ task.iconType = 'generic';
+ }
+
+ this.tasks_.push(task);
+ if (this.defaultTask_ === null && task.isDefault) {
+ this.defaultTask_ = task;
+ }
+ }
+ if (!this.defaultTask_ && this.tasks_.length > 0) {
+ // If we haven't picked a default task yet, then just pick the first one.
+ // This is not the preferred way we want to pick this, but better this than
+ // no default at all if the C++ code didn't set one.
+ this.defaultTask_ = this.tasks_[0];
+ }
+};
+
+/**
+ * Executes default task.
+ *
+ * @param {function(boolean, Array.<string>)=} opt_callback Called when the
+ * default task is executed, or the error is occurred.
+ * @private
+ */
+FileTasks.prototype.executeDefault_ = function(opt_callback) {
+ FileTasks.recordViewingFileTypeUMA_(this.entries_);
+ this.executeDefaultInternal_(this.entries_, opt_callback);
+};
+
+/**
+ * Executes default task.
+ *
+ * @param {Array.<Entry>} entries Entries to execute.
+ * @param {function(boolean, Array.<Entry>)=} opt_callback Called when the
+ * default task is executed, or the error is occurred.
+ * @private
+ */
+FileTasks.prototype.executeDefaultInternal_ = function(entries, opt_callback) {
+ var callback = opt_callback || function(arg1, arg2) {};
+
+ if (this.defaultTask_ !== null) {
+ this.executeInternal_(this.defaultTask_.taskId, entries);
+ callback(true, entries);
+ return;
+ }
+
+ // We don't have tasks, so try to show a file in a browser tab.
+ // We only do that for single selection to avoid confusion.
+ if (entries.length !== 1 || !entries[0])
+ return;
+
+ var filename = entries[0].name;
+ var extension = PathUtil.splitExtension(filename)[1];
+ var mimeType = this.mimeTypes_[0];
+
+ var showAlert = function() {
+ var textMessageId;
+ var titleMessageId;
+ switch (extension) {
+ case '.exe':
+ textMessageId = 'NO_ACTION_FOR_EXECUTABLE';
+ break;
+ case '.crx':
+ textMessageId = 'NO_ACTION_FOR_CRX';
+ titleMessageId = 'NO_ACTION_FOR_CRX_TITLE';
+ break;
+ default:
+ textMessageId = 'NO_ACTION_FOR_FILE';
+ }
+
+ var webStoreUrl = FileTasks.createWebStoreLink(extension, mimeType);
+ var text = strf(textMessageId,
+ webStoreUrl,
+ FileTasks.NO_ACTION_FOR_FILE_URL);
+ var title = titleMessageId ? str(titleMessageId) : filename;
+ this.fileManager_.alert.showHtml(title, text, function() {});
+ callback(false, urls);
+ }.bind(this);
+
+ var onViewFilesFailure = function() {
+ var fm = this.fileManager_;
+ if (fm.enableExperimentalWebStoreIntegration_) {
+ showAlert();
+ return;
+ }
+
+ if (!fm.isOnDrive() ||
+ !entries[0] ||
+ FileTasks.EXTENSIONS_TO_SKIP_SUGGEST_APPS_.indexOf(extension) !== -1) {
+ showAlert();
+ return;
+ }
+
+ fm.openSuggestAppsDialog(
+ entries[0],
+ function() {
+ var newTasks = new FileTasks(fm);
+ newTasks.init(entries, this.mimeTypes_);
+ newTasks.executeDefault();
+ callback(true, entries);
+ }.bind(this),
+ // Cancelled callback.
+ function() {
+ callback(false, entries);
+ },
+ showAlert);
+ }.bind(this);
+
+ var onViewFiles = function(success) {
+ if (success)
+ callback(success, entries);
+ else
+ onViewFilesFailure();
+ }.bind(this);
+
+ this.checkAvailability_(function() {
+ // TODO(mtomasz): Pass entries instead.
+ var urls = util.entriesToURLs(entries);
+ util.viewFilesInBrowser(urls, onViewFiles);
+ }.bind(this));
+};
+
+/**
+ * Executes a single task.
+ *
+ * @param {string} taskId Task identifier.
+ * @param {Array.<Entry>=} opt_entries Entries to xecute on instead of
+ * this.entries_|.
+ * @private
+ */
+FileTasks.prototype.execute_ = function(taskId, opt_entries) {
+ var entries = opt_entries || this.entries_;
+ FileTasks.recordViewingFileTypeUMA_(entries);
+ this.executeInternal_(taskId, entries);
+};
+
+/**
+ * The core implementation to execute a single task.
+ *
+ * @param {string} taskId Task identifier.
+ * @param {Array.<Entry>} entries Entries to execute.
+ * @private
+ */
+FileTasks.prototype.executeInternal_ = function(taskId, entries) {
+ this.checkAvailability_(function() {
+ if (FileTasks.isInternalTask_(taskId)) {
+ var taskParts = taskId.split('|');
+ this.executeInternalTask_(taskParts[2], entries);
+ } else {
+ // TODO(mtomasz): Pass entries instead.
+ var urls = util.entriesToURLs(entries);
+ chrome.fileBrowserPrivate.executeTask(taskId, urls);
+ }
+ }.bind(this));
+};
+
+/**
+ * Checks whether the remote files are available right now.
+ *
+ * @param {function} callback The callback.
+ * @private
+ */
+FileTasks.prototype.checkAvailability_ = function(callback) {
+ var areAll = function(props, name) {
+ var isOne = function(e) {
+ // If got no properties, we safely assume that item is unavailable.
+ return e && e[name];
+ };
+ return props.filter(isOne).length === props.length;
+ };
+
+ var fm = this.fileManager_;
+ var entries = this.entries_;
+
+ if (fm.isOnDrive() && fm.isDriveOffline()) {
+ fm.metadataCache_.get(entries, 'drive', function(props) {
+ if (areAll(props, 'availableOffline')) {
+ callback();
+ return;
+ }
+
+ fm.alert.showHtml(
+ loadTimeData.getString('OFFLINE_HEADER'),
+ props[0].hosted ?
+ loadTimeData.getStringF(
+ entries.length === 1 ?
+ 'HOSTED_OFFLINE_MESSAGE' :
+ 'HOSTED_OFFLINE_MESSAGE_PLURAL') :
+ loadTimeData.getStringF(
+ entries.length === 1 ?
+ 'OFFLINE_MESSAGE' :
+ 'OFFLINE_MESSAGE_PLURAL',
+ loadTimeData.getString('OFFLINE_COLUMN_LABEL')));
+ });
+ return;
+ }
+
+ if (fm.isOnDrive() && fm.isDriveOnMeteredConnection()) {
+ fm.metadataCache_.get(entries, 'drive', function(driveProps) {
+ if (areAll(driveProps, 'availableWhenMetered')) {
+ callback();
+ return;
+ }
+
+ fm.metadataCache_.get(entries, 'filesystem', function(fileProps) {
+ var sizeToDownload = 0;
+ for (var i = 0; i !== entries.length; i++) {
+ if (!driveProps[i].availableWhenMetered)
+ sizeToDownload += fileProps[i].size;
+ }
+ fm.confirm.show(
+ loadTimeData.getStringF(
+ entries.length === 1 ?
+ 'CONFIRM_MOBILE_DATA_USE' :
+ 'CONFIRM_MOBILE_DATA_USE_PLURAL',
+ util.bytesToString(sizeToDownload)),
+ callback);
+ });
+ });
+ return;
+ }
+
+ callback();
+};
+
+/**
+ * Executes an internal task.
+ *
+ * @param {string} id The short task id.
+ * @param {Array.<Entry>} entries The entries to execute on.
+ * @private
+ */
+FileTasks.prototype.executeInternalTask_ = function(id, entries) {
+ var fm = this.fileManager_;
+
+ if (id === 'play') {
+ var position = 0;
+ if (entries.length === 1) {
+ // If just a single audio file is selected pass along every audio file
+ // in the directory.
+ var selectedEntries = entries[0];
+ entries = fm.getAllEntriesInCurrentDirectory().filter(FileType.isAudio);
+ position = entries.indexOf(selectedEntries);
+ }
+ // TODO(mtomasz): Pass entries instead.
+ var urls = util.entriesToURLs(entries);
+ fm.backgroundPage.launchAudioPlayer({items: urls, position: position});
+ return;
+ }
+
+ if (id === 'watch') {
+ console.assert(entries.length === 1, 'Cannot open multiple videos');
+ // TODO(mtomasz): Pass an entry instead.
+ fm.backgroundPage.launchVideoPlayer(entries[0].toURL());
+ return;
+ }
+
+ if (id === 'mount-archive') {
+ this.mountArchivesInternal_(entries);
+ return;
+ }
+
+ if (id === 'gallery') {
+ this.openGalleryInternal_(entries);
+ return;
+ }
+
+ console.error('Unexpected action ID: ' + id);
+};
+
+/**
+ * Mounts archives.
+ *
+ * @param {Array.<Entry>} entries Mount file entries list.
+ */
+FileTasks.prototype.mountArchives = function(entries) {
+ FileTasks.recordViewingFileTypeUMA_(entries);
+ this.mountArchivesInternal_(entries);
+};
+
+/**
+ * The core implementation of mounts archives.
+ *
+ * @param {Array.<Entry>} entries Mount file entries list.
+ * @private
+ */
+FileTasks.prototype.mountArchivesInternal_ = function(entries) {
+ var fm = this.fileManager_;
+
+ var tracker = fm.directoryModel.createDirectoryChangeTracker();
+ tracker.start();
+
+ // TODO(mtomasz): Pass Entries instead of URLs.
+ var urls = util.entriesToURLs(entries);
+ fm.resolveSelectResults_(urls, function(resolvedURLs) {
+ for (var index = 0; index < resolvedURLs.length; ++index) {
+ // TODO(mtomasz): Pass Entry instead of URL.
+ fm.volumeManager.mountArchive(resolvedURLs[index],
+ function(mountPath) {
+ tracker.stop();
+ if (!tracker.hasChanged)
+ fm.directoryModel.changeDirectory(mountPath);
+ }, function(url, error) {
+ tracker.stop();
+ var path = util.extractFilePath(url);
+ var namePos = path.lastIndexOf('/');
+ fm.alert.show(strf('ARCHIVE_MOUNT_FAILED',
+ path.substr(namePos + 1), error));
+ }.bind(null, resolvedURLs[index]));
+ }
+ });
+};
+
+/**
+ * Open the Gallery.
+ *
+ * @param {Array.<Entry>} entries List of selected entries.
+ */
+FileTasks.prototype.openGallery = function(entries) {
+ FileTasks.recordViewingFileTypeUMA_(entries);
+ this.openGalleryInternal_(entries);
+};
+
+/**
+ * The core implementation to open the Gallery.
+ *
+ * @param {Array.<Entry>} entries List of selected entries.
+ * @private
+ */
+FileTasks.prototype.openGalleryInternal_ = function(entries) {
+ var fm = this.fileManager_;
+
+ var allEntries =
+ fm.getAllEntriesInCurrentDirectory().filter(FileType.isImageOrVideo);
+
+ var galleryFrame = fm.document_.createElement('iframe');
+ galleryFrame.className = 'overlay-pane';
+ galleryFrame.scrolling = 'no';
+ galleryFrame.setAttribute('webkitallowfullscreen', true);
+
+ if (this.params_ && this.params_.gallery) {
+ // Remove the Gallery state from the location, we do not need it any more.
+ util.updateAppState(null /* keep path */, '' /* remove search. */);
+ }
+
+ var savedAppState = window.appState;
+ var savedTitle = document.title;
+
+ // Push a temporary state which will be replaced every time the selection
+ // changes in the Gallery and popped when the Gallery is closed.
+ util.updateAppState();
+
+ var onBack = function(selectedEntries) {
+ fm.directoryModel.selectEntries(selectedEntries);
+ fm.closeFilePopup(); // Will call Gallery.unload.
+ window.appState = savedAppState;
+ util.saveAppState();
+ document.title = savedTitle;
+ };
+
+ var onClose = function() {
+ fm.onClose();
+ };
+
+ var onMaximize = function() {
+ fm.onMaximize();
+ };
+
+ var onAppRegionChanged = function(visible) {
+ fm.onFilePopupAppRegionChanged(visible);
+ };
+
+ galleryFrame.onload = function() {
+ galleryFrame.contentWindow.ImageUtil.metrics = metrics;
+
+ // TODO(haruki): isOnReadonlyDirectory() only checks the permission for the
+ // root. We should check more granular permission to know whether the file
+ // is writable or not.
+ var readonly = fm.isOnReadonlyDirectory();
+ var currentDir = fm.getCurrentDirectoryEntry();
+ var downloadsVolume =
+ fm.volumeManager.getCurrentProfileVolumeInfo(RootType.DOWNLOADS);
+ var downloadsDir = downloadsVolume && downloadsVolume.root;
+ var readonlyDirName = null;
+ if (readonly && currentDir) {
+ var rootPath = PathUtil.getRootPath(currentDir.fullPath);
+ readonlyDirName = fm.isOnDrive() ?
+ PathUtil.getRootLabel(rootPath) :
+ PathUtil.basename(rootPath);
+ }
+
+ var context = {
+ // We show the root label in readonly warning (e.g. archive name).
+ readonlyDirName: readonlyDirName,
+ curDirEntry: currentDir,
+ saveDirEntry: readonly ? downloadsDir : null,
+ searchResults: fm.directoryModel.isSearching(),
+ metadataCache: fm.metadataCache_,
+ pageState: this.params_,
+ appWindow: chrome.app.window.current(),
+ onBack: onBack,
+ onClose: onClose,
+ onMaximize: onMaximize,
+ onAppRegionChanged: onAppRegionChanged,
+ displayStringFunction: strf
+ };
+ galleryFrame.contentWindow.Gallery.open(
+ context, fm.volumeManager, allEntries, entries);
+ }.bind(this);
+
+ galleryFrame.src = 'gallery.html';
+ fm.openFilePopup(galleryFrame, fm.updateTitle_.bind(fm));
+};
+
+/**
+ * Displays the list of tasks in a task picker combobutton.
+ *
+ * @param {cr.ui.ComboButton} combobutton The task picker element.
+ * @private
+ */
+FileTasks.prototype.display_ = function(combobutton) {
+ if (this.tasks_.length === 0) {
+ combobutton.hidden = true;
+ return;
+ }
+
+ combobutton.clear();
+ combobutton.hidden = false;
+ combobutton.defaultItem = this.createCombobuttonItem_(this.defaultTask_);
+
+ var items = this.createItems_();
+
+ if (items.length > 1) {
+ var defaultIdx = 0;
+
+ for (var j = 0; j < items.length; j++) {
+ combobutton.addDropDownItem(items[j]);
+ if (items[j].task.taskId === this.defaultTask_.taskId)
+ defaultIdx = j;
+ }
+
+ combobutton.addSeparator();
+ var changeDefaultMenuItem = combobutton.addDropDownItem({
+ label: loadTimeData.getString('CHANGE_DEFAULT_MENU_ITEM')
+ });
+ changeDefaultMenuItem.classList.add('change-default');
+ }
+};
+
+/**
+ * Creates sorted array of available task descriptions such as title and icon.
+ *
+ * @return {Array} created array can be used to feed combobox, menus and so on.
+ * @private
+ */
+FileTasks.prototype.createItems_ = function() {
+ var items = [];
+ var title = this.defaultTask_.title + ' ' +
+ loadTimeData.getString('DEFAULT_ACTION_LABEL');
+ items.push(this.createCombobuttonItem_(this.defaultTask_, title, true));
+
+ for (var index = 0; index < this.tasks_.length; index++) {
+ var task = this.tasks_[index];
+ if (task !== this.defaultTask_)
+ items.push(this.createCombobuttonItem_(task));
+ }
+
+ items.sort(function(a, b) {
+ return a.label.localeCompare(b.label);
+ });
+
+ return items;
+};
+
+/**
+ * Updates context menu with default item.
+ * @private
+ */
+
+FileTasks.prototype.updateMenuItem_ = function() {
+ this.fileManager_.updateContextMenuActionItems(this.defaultTask_,
+ this.tasks_.length > 1);
+};
+
+/**
+ * Creates combobutton item based on task.
+ *
+ * @param {Object} task Task to convert.
+ * @param {string=} opt_title Title.
+ * @param {boolean=} opt_bold Make a menu item bold.
+ * @return {Object} Item appendable to combobutton drop-down list.
+ * @private
+ */
+FileTasks.prototype.createCombobuttonItem_ = function(task, opt_title,
+ opt_bold) {
+ return {
+ label: opt_title || task.title,
+ iconUrl: task.iconUrl,
+ iconType: task.iconType,
+ task: task,
+ bold: opt_bold || false
+ };
+};
+
+
+/**
+ * Decorates a FileTasks method, so it will be actually executed after the tasks
+ * are available.
+ * This decorator expects an implementation called |method + '_'|.
+ *
+ * @param {string} method The method name.
+ */
+FileTasks.decorate = function(method) {
+ var privateMethod = method + '_';
+ FileTasks.prototype[method] = function() {
+ if (this.tasks_) {
+ this[privateMethod].apply(this, arguments);
+ } else {
+ this.pendingInvocations_.push([privateMethod, arguments]);
+ }
+ return this;
+ };
+};
+
+/**
+ * Shows modal action picker dialog with currently available list of tasks.
+ *
+ * @param {DefaultActionDialog} actionDialog Action dialog to show and update.
+ * @param {string} title Title to use.
+ * @param {string} message Message to use.
+ * @param {function(Object)} onSuccess Callback to pass selected task.
+ */
+FileTasks.prototype.showTaskPicker = function(actionDialog, title, message,
+ onSuccess) {
+ var items = this.createItems_();
+
+ var defaultIdx = 0;
+ for (var j = 0; j < items.length; j++) {
+ if (items[j].task.taskId === this.defaultTask_.taskId)
+ defaultIdx = j;
+ }
+
+ actionDialog.show(
+ title,
+ message,
+ items, defaultIdx,
+ function(item) {
+ onSuccess(item.task);
+ });
+};
+
+FileTasks.decorate('display');
+FileTasks.decorate('updateMenuItem');
+FileTasks.decorate('execute');
+FileTasks.decorate('executeDefault');
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/file_transfer_controller.js b/chromium/chrome/browser/resources/file_manager/foreground/js/file_transfer_controller.js
new file mode 100644
index 00000000000..3f8900c88d1
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/file_transfer_controller.js
@@ -0,0 +1,860 @@
+// 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.
+
+'use strict';
+
+/**
+ * Global (placed in the window object) variable name to hold internal
+ * file dragging information. Needed to show visual feedback while dragging
+ * since DataTransfer object is in protected state. Reachable from other
+ * file manager instances.
+ */
+var DRAG_AND_DROP_GLOBAL_DATA = '__drag_and_drop_global_data';
+
+/**
+ * @param {HTMLDocument} doc Owning document.
+ * @param {FileOperationManager} fileOperationManager File operation manager
+ * instance.
+ * @param {MetadataCache} metadataCache Metadata cache service.
+ * @param {DirectoryModel} directoryModel Directory model instance.
+ * @constructor
+ */
+function FileTransferController(doc,
+ fileOperationManager,
+ metadataCache,
+ directoryModel) {
+ this.document_ = doc;
+ this.fileOperationManager_ = fileOperationManager;
+ this.metadataCache_ = metadataCache;
+ this.directoryModel_ = directoryModel;
+
+ this.directoryModel_.getFileListSelection().addEventListener('change',
+ this.onSelectionChanged_.bind(this));
+
+ /**
+ * DOM element to represent selected file in drag operation. Used if only
+ * one element is selected.
+ * @type {HTMLElement}
+ * @private
+ */
+ this.preloadedThumbnailImageNode_ = null;
+
+ /**
+ * File objects for selected files.
+ *
+ * @type {Array.<File>}
+ * @private
+ */
+ this.selectedFileObjects_ = [];
+
+ /**
+ * Drag selector.
+ * @type {DragSelector}
+ * @private
+ */
+ this.dragSelector_ = new DragSelector();
+
+ /**
+ * Whether a user is touching the device or not.
+ * @type {boolean}
+ * @private
+ */
+ this.touching_ = false;
+}
+
+FileTransferController.prototype = {
+ __proto__: cr.EventTarget.prototype,
+
+ /**
+ * @this {FileTransferController}
+ * @param {cr.ui.List} list Items in the list will be draggable.
+ */
+ attachDragSource: function(list) {
+ list.style.webkitUserDrag = 'element';
+ list.addEventListener('dragstart', this.onDragStart_.bind(this, list));
+ list.addEventListener('dragend', this.onDragEnd_.bind(this, list));
+ list.addEventListener('touchstart', this.onTouchStart_.bind(this));
+ list.addEventListener('touchend', this.onTouchEnd_.bind(this));
+ },
+
+ /**
+ * @this {FileTransferController}
+ * @param {cr.ui.List} list List itself and its directory items will could
+ * be drop target.
+ * @param {boolean=} opt_onlyIntoDirectories If true only directory list
+ * items could be drop targets. Otherwise any other place of the list
+ * accetps files (putting it into the current directory).
+ */
+ attachFileListDropTarget: function(list, opt_onlyIntoDirectories) {
+ list.addEventListener('dragover', this.onDragOver_.bind(this,
+ !!opt_onlyIntoDirectories, list));
+ list.addEventListener('dragenter',
+ this.onDragEnterFileList_.bind(this, list));
+ list.addEventListener('dragleave', this.onDragLeave_.bind(this, list));
+ list.addEventListener('drop',
+ this.onDrop_.bind(this, !!opt_onlyIntoDirectories));
+ },
+
+ /**
+ * @this {FileTransferController}
+ * @param {DirectoryTree} tree Its sub items will could be drop target.
+ */
+ attachTreeDropTarget: function(tree) {
+ tree.addEventListener('dragover', this.onDragOver_.bind(this, true, tree));
+ tree.addEventListener('dragenter', this.onDragEnterTree_.bind(this, tree));
+ tree.addEventListener('dragleave', this.onDragLeave_.bind(this, tree));
+ tree.addEventListener('drop', this.onDrop_.bind(this, true));
+ },
+
+ /**
+ * @this {FileTransferController}
+ * @param {NavigationList} tree Its sub items will could be drop target.
+ */
+ attachNavigationListDropTarget: function(list) {
+ list.addEventListener('dragover',
+ this.onDragOver_.bind(this, true /* onlyIntoDirectories */, list));
+ list.addEventListener('dragenter',
+ this.onDragEnterVolumesList_.bind(this, list));
+ list.addEventListener('dragleave', this.onDragLeave_.bind(this, list));
+ list.addEventListener('drop',
+ this.onDrop_.bind(this, true /* onlyIntoDirectories */));
+ },
+
+ /**
+ * Attach handlers of copy, cut and paste operations to the document.
+ *
+ * @this {FileTransferController}
+ */
+ attachCopyPasteHandlers: function() {
+ this.document_.addEventListener('beforecopy',
+ this.onBeforeCopy_.bind(this));
+ this.document_.addEventListener('copy',
+ this.onCopy_.bind(this));
+ this.document_.addEventListener('beforecut',
+ this.onBeforeCut_.bind(this));
+ this.document_.addEventListener('cut',
+ this.onCut_.bind(this));
+ this.document_.addEventListener('beforepaste',
+ this.onBeforePaste_.bind(this));
+ this.document_.addEventListener('paste',
+ this.onPaste_.bind(this));
+ this.copyCommand_ = this.document_.querySelector('command#copy');
+ },
+
+ /**
+ * Write the current selection to system clipboard.
+ *
+ * @this {FileTransferController}
+ * @param {DataTransfer} dataTransfer DataTransfer from the event.
+ * @param {string} effectAllowed Value must be valid for the
+ * |dataTransfer.effectAllowed| property ('move', 'copy', 'copyMove').
+ */
+ cutOrCopy_: function(dataTransfer, effectAllowed) {
+ // Tag to check it's filemanager data.
+ dataTransfer.setData('fs/tag', 'filemanager-data');
+ dataTransfer.setData('fs/sourceRoot',
+ this.directoryModel_.getCurrentRootPath());
+ var sourcePaths =
+ this.selectedEntries_.map(function(e) { return e.fullPath; });
+ dataTransfer.setData('fs/sources', sourcePaths.join('\n'));
+ dataTransfer.effectAllowed = effectAllowed;
+ dataTransfer.setData('fs/effectallowed', effectAllowed);
+
+ for (var i = 0; i < this.selectedFileObjects_.length; i++) {
+ dataTransfer.items.add(this.selectedFileObjects_[i]);
+ }
+ },
+
+ /**
+ * Extracts source root from the |dataTransfer| object.
+ *
+ * @this {FileTransferController}
+ * @param {DataTransfer} dataTransfer DataTransfer object from the event.
+ * @return {string} Path or empty string (if unknown).
+ */
+ getSourceRoot_: function(dataTransfer) {
+ var sourceRoot = dataTransfer.getData('fs/sourceRoot');
+ if (sourceRoot)
+ return sourceRoot;
+
+ // |dataTransfer| in protected mode.
+ if (window[DRAG_AND_DROP_GLOBAL_DATA])
+ return window[DRAG_AND_DROP_GLOBAL_DATA].sourceRoot;
+
+ // Dragging from other tabs/windows.
+ var views = chrome && chrome.extension ? chrome.extension.getViews() : [];
+ for (var i = 0; i < views.length; i++) {
+ if (views[i][DRAG_AND_DROP_GLOBAL_DATA])
+ return views[i][DRAG_AND_DROP_GLOBAL_DATA].sourceRoot;
+ }
+
+ // Unknown source.
+ return '';
+ },
+
+ /**
+ * Queue up a file copy operation based on the current system clipboard.
+ *
+ * @this {FileTransferController}
+ * @param {DataTransfer} dataTransfer System data transfer object.
+ * @param {string=} opt_destinationPath Paste destination.
+ * @param {string=} opt_effect Desired drop/paste effect. Could be
+ * 'move'|'copy' (default is copy). Ignored if conflicts with
+ * |dataTransfer.effectAllowed|.
+ * @return {string} Either "copy" or "move".
+ */
+ paste: function(dataTransfer, opt_destinationPath, opt_effect) {
+ var sourcePaths = (dataTransfer.getData('fs/sources') || '').split('\n');
+ var destinationPath = opt_destinationPath ||
+ this.currentDirectoryContentPath;
+ // effectAllowed set in copy/paste handlers stay uninitialized. DnD handlers
+ // work fine.
+ var effectAllowed = dataTransfer.effectAllowed != 'uninitialized' ?
+ dataTransfer.effectAllowed : dataTransfer.getData('fs/effectallowed');
+ var toMove = effectAllowed == 'move' ||
+ (effectAllowed == 'copyMove' && opt_effect == 'move');
+
+ // Start the pasting operation.
+ this.fileOperationManager_.paste(sourcePaths, destinationPath, toMove);
+ return toMove ? 'move' : 'copy';
+ },
+
+ /**
+ * Preloads an image thumbnail for the specified file entry.
+ *
+ * @this {FileTransferController}
+ * @param {Entry} entry Entry to preload a thumbnail for.
+ */
+ preloadThumbnailImage_: function(entry) {
+ var metadataTypes = 'thumbnail|filesystem';
+ var thumbnailContainer = this.document_.createElement('div');
+ this.preloadedThumbnailImageNode_ = thumbnailContainer;
+ this.preloadedThumbnailImageNode_.className = 'img-container';
+ this.metadataCache_.get(
+ entry,
+ metadataTypes,
+ function(metadata) {
+ new ThumbnailLoader(entry.toURL(),
+ ThumbnailLoader.LoaderType.IMAGE,
+ metadata).
+ load(thumbnailContainer,
+ ThumbnailLoader.FillMode.FILL);
+ }.bind(this));
+ },
+
+ /**
+ * Renders a drag-and-drop thumbnail.
+ *
+ * @this {FileTransferController}
+ * @return {HTMLElement} Element containing the thumbnail.
+ */
+ renderThumbnail_: function() {
+ var length = this.selectedEntries_.length;
+
+ var container = this.document_.querySelector('#drag-container');
+ var contents = this.document_.createElement('div');
+ contents.className = 'drag-contents';
+ container.appendChild(contents);
+
+ var thumbnailImage;
+ if (this.preloadedThumbnailImageNode_)
+ thumbnailImage = this.preloadedThumbnailImageNode_.querySelector('img');
+
+ // Option 1. Multiple selection, render only a label.
+ if (length > 1) {
+ var label = this.document_.createElement('div');
+ label.className = 'label';
+ label.textContent = strf('DRAGGING_MULTIPLE_ITEMS', length);
+ contents.appendChild(label);
+ return container;
+ }
+
+ // Option 2. Thumbnail image available, then render it without
+ // a label.
+ if (thumbnailImage) {
+ thumbnailImage.classList.add('drag-thumbnail');
+ contents.classList.add('for-image');
+ contents.appendChild(this.preloadedThumbnailImageNode_);
+ return container;
+ }
+
+ // Option 3. Thumbnail not available. Render an icon and a label.
+ var entry = this.selectedEntries_[0];
+ var icon = this.document_.createElement('div');
+ icon.className = 'detail-icon';
+ icon.setAttribute('file-type-icon', FileType.getIcon(entry));
+ contents.appendChild(icon);
+ var label = this.document_.createElement('div');
+ label.className = 'label';
+ label.textContent = entry.name;
+ contents.appendChild(label);
+ return container;
+ },
+
+ /**
+ * @this {FileTransferController}
+ * @param {cr.ui.List} list Drop target list
+ * @param {Event} event A dragstart event of DOM.
+ */
+ onDragStart_: function(list, event) {
+ // If a user is touching, Files.app does not receive drag operations.
+ if (this.touching_) {
+ event.preventDefault();
+ return;
+ }
+
+ // Check if a drag selection should be initiated or not.
+ if (list.shouldStartDragSelection(event)) {
+ this.dragSelector_.startDragSelection(list, event);
+ return;
+ }
+
+ // Nothing selected.
+ if (!this.selectedEntries_.length) {
+ event.preventDefault();
+ return;
+ }
+
+ var dt = event.dataTransfer;
+
+ if (this.canCopyOrDrag_(dt)) {
+ if (this.canCutOrDrag_(dt))
+ this.cutOrCopy_(dt, 'copyMove');
+ else
+ this.cutOrCopy_(dt, 'copy');
+ } else {
+ event.preventDefault();
+ return;
+ }
+
+ var dragThumbnail = this.renderThumbnail_();
+ dt.setDragImage(dragThumbnail, 1000, 1000);
+
+ window[DRAG_AND_DROP_GLOBAL_DATA] = {
+ sourceRoot: this.directoryModel_.getCurrentRootPath()
+ };
+ },
+
+ /**
+ * @this {FileTransferController}
+ * @param {cr.ui.List} list Drop target list.
+ * @param {Event} event A dragend event of DOM.
+ */
+ onDragEnd_: function(list, event) {
+ var container = this.document_.querySelector('#drag-container');
+ container.textContent = '';
+ this.clearDropTarget_();
+ delete window[DRAG_AND_DROP_GLOBAL_DATA];
+ },
+
+ /**
+ * @this {FileTransferController}
+ * @param {boolean} onlyIntoDirectories True if the drag is only into
+ * directories.
+ * @param {cr.ui.List} list Drop target list.
+ * @param {Event} event A dragover event of DOM.
+ */
+ onDragOver_: function(onlyIntoDirectories, list, event) {
+ event.preventDefault();
+ var path = this.destinationPath_ ||
+ (!onlyIntoDirectories && this.currentDirectoryContentPath);
+ event.dataTransfer.dropEffect = this.selectDropEffect_(event, path);
+ event.preventDefault();
+ },
+
+ /**
+ * @this {FileTransferController}
+ * @param {cr.ui.List} list Drop target list.
+ * @param {Event} event A dragenter event of DOM.
+ */
+ onDragEnterFileList_: function(list, event) {
+ event.preventDefault(); // Required to prevent the cursor flicker.
+ this.lastEnteredTarget_ = event.target;
+ var item = list.getListItemAncestor(event.target);
+ item = item && list.isItem(item) ? item : null;
+ if (item == this.dropTarget_)
+ return;
+
+ var entry = item && list.dataModel.item(item.listIndex);
+ if (entry) {
+ this.setDropTarget_(item, entry.isDirectory, event.dataTransfer,
+ entry.fullPath);
+ } else {
+ this.clearDropTarget_();
+ }
+ },
+
+ /**
+ * @this {FileTransferController}
+ * @param {DirectoryTree} tree Drop target tree.
+ * @param {Event} event A dragenter event of DOM.
+ */
+ onDragEnterTree_: function(tree, event) {
+ event.preventDefault(); // Required to prevent the cursor flicker.
+ this.lastEnteredTarget_ = event.target;
+ var item = event.target;
+ while (item && !(item instanceof DirectoryItem)) {
+ item = item.parentNode;
+ }
+
+ if (item == this.dropTarget_)
+ return;
+
+ var entry = item && item.entry;
+ if (entry) {
+ this.setDropTarget_(item, entry.isDirectory, event.dataTransfer,
+ entry.fullPath);
+ } else {
+ this.clearDropTarget_();
+ }
+ },
+
+ /**
+ * @this {FileTransferController}
+ * @param {NavigationList} list Drop target list.
+ * @param {Event} event A dragenter event of DOM.
+ */
+ onDragEnterVolumesList_: function(list, event) {
+ event.preventDefault(); // Required to prevent the cursor flicker.
+ this.lastEnteredTarget_ = event.target;
+ var item = list.getListItemAncestor(event.target);
+ item = item && list.isItem(item) ? item : null;
+ if (item == this.dropTarget_)
+ return;
+
+ var path = item && list.dataModel.item(item.listIndex).path;
+ if (path)
+ this.setDropTarget_(item, true /* directory */, event.dataTransfer, path);
+ else
+ this.clearDropTarget_();
+ },
+
+ /**
+ * @this {FileTransferController}
+ * @param {cr.ui.List} list Drop target list.
+ * @param {Event} event A dragleave event of DOM.
+ */
+ onDragLeave_: function(list, event) {
+ // If mouse moves from one element to another the 'dragenter'
+ // event for the new element comes before the 'dragleave' event for
+ // the old one. In this case event.target != this.lastEnteredTarget_
+ // and handler of the 'dragenter' event has already caried of
+ // drop target. So event.target == this.lastEnteredTarget_
+ // could only be if mouse goes out of listened element.
+ if (event.target == this.lastEnteredTarget_) {
+ this.clearDropTarget_();
+ this.lastEnteredTarget_ = null;
+ }
+ },
+
+ /**
+ * @this {FileTransferController}
+ * @param {boolean} onlyIntoDirectories True if the drag is only into
+ * directories.
+ * @param {Event} event A dragleave event of DOM.
+ */
+ onDrop_: function(onlyIntoDirectories, event) {
+ if (onlyIntoDirectories && !this.dropTarget_)
+ return;
+ var destinationPath = this.destinationPath_ ||
+ this.currentDirectoryContentPath;
+ if (!this.canPasteOrDrop_(event.dataTransfer, destinationPath))
+ return;
+ event.preventDefault();
+ this.paste(event.dataTransfer, destinationPath,
+ this.selectDropEffect_(event, destinationPath));
+ this.clearDropTarget_();
+ },
+
+ /**
+ * Sets the drop target.
+ * @this {FileTransferController}
+ * @param {Element} domElement Target of the drop.
+ * @param {boolean} isDirectory If the target is a directory.
+ * @param {DataTransfer} dataTransfer Data transfer object.
+ * @param {string} destinationPath Destination path.
+ */
+ setDropTarget_: function(domElement, isDirectory, dataTransfer,
+ destinationPath) {
+ if (this.dropTarget_ == domElement)
+ return;
+
+ // Remove the old drop target.
+ this.clearDropTarget_();
+
+ // Set the new drop target.
+ this.dropTarget_ = domElement;
+
+ if (!domElement ||
+ !isDirectory ||
+ !this.canPasteOrDrop_(dataTransfer, destinationPath)) {
+ return;
+ }
+
+ // Add accept class if the domElement can accept the drag.
+ domElement.classList.add('accepts');
+ this.destinationPath_ = destinationPath;
+
+ // Start timer changing the directory.
+ this.navigateTimer_ = setTimeout(function() {
+ if (domElement instanceof DirectoryItem)
+ // Do custom action.
+ (/** @type {DirectoryItem} */ domElement).doDropTargetAction();
+ this.directoryModel_.changeDirectory(destinationPath);
+ }.bind(this), 2000);
+ },
+
+ /**
+ * Handles touch start.
+ */
+ onTouchStart_: function() {
+ this.touching_ = true;
+ },
+
+ /**
+ * Handles touch end.
+ */
+ onTouchEnd_: function(event) {
+ if (event.touches.length === 0)
+ this.touching_ = false;
+ },
+
+ /**
+ * Clears the drop target.
+ * @this {FileTransferController}
+ */
+ clearDropTarget_: function() {
+ if (this.dropTarget_ && this.dropTarget_.classList.contains('accepts'))
+ this.dropTarget_.classList.remove('accepts');
+ this.dropTarget_ = null;
+ this.destinationPath_ = null;
+ if (this.navigateTimer_ !== undefined) {
+ clearTimeout(this.navigateTimer_);
+ this.navigateTimer_ = undefined;
+ }
+ },
+
+ /**
+ * @this {FileTransferController}
+ * @return {boolean} Returns false if {@code <input type="text">} element is
+ * currently active. Otherwise, returns true.
+ */
+ isDocumentWideEvent_: function() {
+ return this.document_.activeElement.nodeName.toLowerCase() != 'input' ||
+ this.document_.activeElement.type.toLowerCase() != 'text';
+ },
+
+ /**
+ * @this {FileTransferController}
+ */
+ onCopy_: function(event) {
+ if (!this.isDocumentWideEvent_() ||
+ !this.canCopyOrDrag_()) {
+ return;
+ }
+ event.preventDefault();
+ this.cutOrCopy_(event.clipboardData, 'copy');
+ this.notify_('selection-copied');
+ },
+
+ /**
+ * @this {FileTransferController}
+ */
+ onBeforeCopy_: function(event) {
+ if (!this.isDocumentWideEvent_())
+ return;
+
+ // queryCommandEnabled returns true if event.defaultPrevented is true.
+ if (this.canCopyOrDrag_())
+ event.preventDefault();
+ },
+
+ /**
+ * @this {FileTransferController}
+ * @return {boolean} Returns true if some files are selected and all the file
+ * on drive is available to be copied. Otherwise, returns false.
+ */
+ canCopyOrDrag_: function() {
+ if (this.isOnDrive &&
+ this.directoryModel_.isDriveOffline() &&
+ !this.allDriveFilesAvailable)
+ return false;
+ return this.selectedEntries_.length > 0;
+ },
+
+ /**
+ * @this {FileTransferController}
+ */
+ onCut_: function(event) {
+ if (!this.isDocumentWideEvent_() ||
+ !this.canCutOrDrag_()) {
+ return;
+ }
+ event.preventDefault();
+ this.cutOrCopy_(event.clipboardData, 'move');
+ this.notify_('selection-cut');
+ },
+
+ /**
+ * @this {FileTransferController}
+ */
+ onBeforeCut_: function(event) {
+ if (!this.isDocumentWideEvent_())
+ return;
+ // queryCommandEnabled returns true if event.defaultPrevented is true.
+ if (this.canCutOrDrag_())
+ event.preventDefault();
+ },
+
+ /**
+ * @this {FileTransferController}
+ * @return {boolean} Returns true if some files are selected and all the file
+ * on drive is available to be cut. Otherwise, returns false.
+ */
+ canCutOrDrag_: function() {
+ return !this.readonly && this.canCopyOrDrag_();
+ },
+
+ /**
+ * @this {FileTransferController}
+ */
+ onPaste_: function(event) {
+ // Need to update here since 'beforepaste' doesn't fire.
+ if (!this.isDocumentWideEvent_() ||
+ !this.canPasteOrDrop_(event.clipboardData,
+ this.currentDirectoryContentPath)) {
+ return;
+ }
+ event.preventDefault();
+ var effect = this.paste(event.clipboardData);
+
+ // On cut, we clear the clipboard after the file is pasted/moved so we don't
+ // try to move/delete the original file again.
+ if (effect == 'move') {
+ this.simulateCommand_('cut', function(event) {
+ event.preventDefault();
+ event.clipboardData.setData('fs/clear', '');
+ });
+ }
+ },
+
+ /**
+ * @this {FileTransferController}
+ */
+ onBeforePaste_: function(event) {
+ if (!this.isDocumentWideEvent_())
+ return;
+ // queryCommandEnabled returns true if event.defaultPrevented is true.
+ if (this.canPasteOrDrop_(event.clipboardData,
+ this.currentDirectoryContentPath)) {
+ event.preventDefault();
+ }
+ },
+
+ /**
+ * @this {FileTransferController}
+ * @param {DataTransfer} dataTransfer Data transfer object.
+ * @param {string?} destinationPath Destination path.
+ * @return {boolean} Returns true if items stored in {@code dataTransfer} can
+ * be pasted to {@code destinationPath}. Otherwise, returns false.
+ */
+ canPasteOrDrop_: function(dataTransfer, destinationPath) {
+ if (!destinationPath) {
+ return false;
+ }
+ if (this.directoryModel_.isPathReadOnly(destinationPath)) {
+ return false;
+ }
+ if (!dataTransfer.types || dataTransfer.types.indexOf('fs/tag') == -1) {
+ return false; // Unsupported type of content.
+ }
+ if (dataTransfer.getData('fs/tag') == '') {
+ // Data protected. Other checks are not possible but it makes sense to
+ // let the user try.
+ return true;
+ }
+
+ var directories = dataTransfer.getData('fs/directories').split('\n').
+ filter(function(d) { return d != ''; });
+
+ for (var i = 0; i < directories.length; i++) {
+ if (destinationPath.substr(0, directories[i].length) == directories[i])
+ return false; // recursive paste.
+ }
+
+ return true;
+ },
+
+ /**
+ * Execute paste command.
+ *
+ * @this {FileTransferController}
+ * @return {boolean} Returns true, the paste is success. Otherwise, returns
+ * false.
+ */
+ queryPasteCommandEnabled: function() {
+ if (!this.isDocumentWideEvent_()) {
+ return false;
+ }
+
+ // HACK(serya): return this.document_.queryCommandEnabled('paste')
+ // should be used.
+ var result;
+ this.simulateCommand_('paste', function(event) {
+ result = this.canPasteOrDrop_(event.clipboardData,
+ this.currentDirectoryContentPath);
+ }.bind(this));
+ return result;
+ },
+
+ /**
+ * Allows to simulate commands to get access to clipboard.
+ *
+ * @this {FileTransferController}
+ * @param {string} command 'copy', 'cut' or 'paste'.
+ * @param {function} handler Event handler.
+ */
+ simulateCommand_: function(command, handler) {
+ var iframe = this.document_.querySelector('#command-dispatcher');
+ var doc = iframe.contentDocument;
+ doc.addEventListener(command, handler);
+ doc.execCommand(command);
+ doc.removeEventListener(command, handler);
+ },
+
+ /**
+ * @this {FileTransferController}
+ */
+ onSelectionChanged_: function(event) {
+ var entries = this.selectedEntries_;
+ var files = this.selectedFileObjects_ = [];
+ this.preloadedThumbnailImageNode_ = null;
+
+ var fileEntries = [];
+ for (var i = 0; i < entries.length; i++) {
+ if (entries[i].isFile)
+ fileEntries.push(entries[i]);
+ }
+
+ if (entries.length == 1) {
+ // For single selection, the dragged element is created in advance,
+ // otherwise an image may not be loaded at the time the 'dragstart' event
+ // comes.
+ this.preloadThumbnailImage_(entries[0]);
+ }
+
+ // File object must be prepeared in advance for clipboard operations
+ // (copy, paste and drag). DataTransfer object closes for write after
+ // returning control from that handlers so they may not have
+ // asynchronous operations.
+ var prepareFileObjects = function() {
+ for (var i = 0; i < fileEntries.length; i++) {
+ fileEntries[i].file(function(file) { files.push(file); });
+ }
+ };
+
+ if (this.isOnDrive) {
+ this.allDriveFilesAvailable = false;
+ this.metadataCache_.get(
+ entries, 'drive', function(props) {
+ // We consider directories not available offline for the purposes of
+ // file transfer since we cannot afford to recursive traversal.
+ this.allDriveFilesAvailable =
+ entries.filter(function(e) {return e.isDirectory}).length == 0 &&
+ props.filter(function(p) {return !p.availableOffline}).length == 0;
+ // |Copy| is the only menu item affected by allDriveFilesAvailable.
+ // It could be open right now, update its UI.
+ this.copyCommand_.disabled = !this.canCopyOrDrag_();
+
+ if (this.allDriveFilesAvailable)
+ prepareFileObjects();
+ }.bind(this));
+ } else {
+ prepareFileObjects();
+ }
+ },
+
+ /**
+ * Path of directory that is displaying now.
+ * If search result is displaying now, this is null.
+ * @this {FileTransferController}
+ * @return {string} Path of directry that is displaying now.
+ */
+ get currentDirectoryContentPath() {
+ return this.directoryModel_.isSearching() ?
+ null : this.directoryModel_.getCurrentDirPath();
+ },
+
+ /**
+ * @this {FileTransferController}
+ * @return {boolean} True if the current directory is read only.
+ */
+ get readonly() {
+ return this.directoryModel_.isReadOnly();
+ },
+
+ /**
+ * @this {FileTransferController}
+ * @return {boolean} True if the current directory is on Drive.
+ */
+ get isOnDrive() {
+ return PathUtil.isDriveBasedPath(this.directoryModel_.getCurrentRootPath());
+ },
+
+ /**
+ * @this {FileTransferController}
+ */
+ notify_: function(eventName) {
+ var self = this;
+ // Set timeout to avoid recursive events.
+ setTimeout(function() {
+ cr.dispatchSimpleEvent(self, eventName);
+ }, 0);
+ },
+
+ /**
+ * @this {FileTransferController}
+ * @return {Array.<Entry>} Array of the selected entries.
+ */
+ get selectedEntries_() {
+ var list = this.directoryModel_.getFileList();
+ var selectedIndexes = this.directoryModel_.getFileListSelection().
+ selectedIndexes;
+ var entries = selectedIndexes.map(function(index) {
+ return list.item(index);
+ });
+
+ // TODO(serya): Diagnostics for http://crbug/129642
+ if (entries.indexOf(undefined) != -1) {
+ var index = entries.indexOf(undefined);
+ entries = entries.filter(function(e) { return !!e; });
+ console.error('Invalid selection found: list items: ', list.length,
+ 'wrong indexe value: ', selectedIndexes[index],
+ 'Stack trace: ', new Error().stack);
+ }
+ return entries;
+ },
+
+ /**
+ * @this {FileTransferController}
+ * @return {string} Returns the appropriate drop query type ('none', 'move'
+ * or copy') to the current modifiers status and the destination.
+ */
+ selectDropEffect_: function(event, destinationPath) {
+ if (!destinationPath ||
+ this.directoryModel_.isPathReadOnly(destinationPath))
+ return 'none';
+ if (event.dataTransfer.effectAllowed == 'copyMove' &&
+ this.getSourceRoot_(event.dataTransfer) ==
+ PathUtil.getRootPath(destinationPath) &&
+ !event.ctrlKey) {
+ return 'move';
+ }
+ if (event.dataTransfer.effectAllowed == 'copyMove' &&
+ event.shiftKey) {
+ return 'move';
+ }
+ return 'copy';
+ },
+};
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/file_type.js b/chromium/chrome/browser/resources/file_manager/foreground/js/file_type.js
new file mode 100644
index 00000000000..ea0ae9278ee
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/file_type.js
@@ -0,0 +1,294 @@
+// 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.
+
+'use strict';
+
+/**
+ * Namespace object for file type utility functions.
+ */
+var FileType = {};
+
+/**
+ * Description of known file types.
+ * Pair type-subtype defines order when sorted by file type.
+ */
+FileType.types = [
+ // Images
+ {type: 'image', name: 'IMAGE_FILE_TYPE', subtype: 'JPEG',
+ pattern: /\.jpe?g$/i},
+ {type: 'image', name: 'IMAGE_FILE_TYPE', subtype: 'BMP',
+ pattern: /\.bmp$/i},
+ {type: 'image', name: 'IMAGE_FILE_TYPE', subtype: 'GIF',
+ pattern: /\.gif$/i},
+ {type: 'image', name: 'IMAGE_FILE_TYPE', subtype: 'ICO',
+ pattern: /\.ico$/i},
+ {type: 'image', name: 'IMAGE_FILE_TYPE', subtype: 'PNG',
+ pattern: /\.png$/i},
+ {type: 'image', name: 'IMAGE_FILE_TYPE', subtype: 'WebP',
+ pattern: /\.webp$/i},
+ {type: 'image', name: 'IMAGE_FILE_TYPE', subtype: 'TIFF',
+ pattern: /\.tiff?$/i},
+
+ // Video
+ {type: 'video', name: 'VIDEO_FILE_TYPE', subtype: '3GP',
+ pattern: /\.3gp$/i},
+ {type: 'video', name: 'VIDEO_FILE_TYPE', subtype: 'AVI',
+ pattern: /\.avi$/i},
+ {type: 'video', name: 'VIDEO_FILE_TYPE', subtype: 'QuickTime',
+ pattern: /\.mov$/i},
+ {type: 'video', name: 'VIDEO_FILE_TYPE', subtype: 'MKV',
+ pattern: /\.mkv$/i},
+ {type: 'video', name: 'VIDEO_FILE_TYPE', subtype: 'MPEG',
+ pattern: /\.m(p4|4v|pg|peg|pg4|peg4)$/i},
+ {type: 'video', name: 'VIDEO_FILE_TYPE', subtype: 'OGG',
+ pattern: /\.og(m|v|x)$/i},
+ {type: 'video', name: 'VIDEO_FILE_TYPE', subtype: 'WebM',
+ pattern: /\.webm$/i},
+
+ // Audio
+ {type: 'audio', name: 'AUDIO_FILE_TYPE', subtype: 'AMR',
+ pattern: /\.amr$/i},
+ {type: 'audio', name: 'AUDIO_FILE_TYPE', subtype: 'FLAC',
+ pattern: /\.flac$/i},
+ {type: 'audio', name: 'AUDIO_FILE_TYPE', subtype: 'MP3',
+ pattern: /\.mp3$/i},
+ {type: 'audio', name: 'AUDIO_FILE_TYPE', subtype: 'MPEG',
+ pattern: /\.m4a$/i},
+ {type: 'audio', name: 'AUDIO_FILE_TYPE', subtype: 'OGG',
+ pattern: /\.og(a|g)$/i},
+ {type: 'audio', name: 'AUDIO_FILE_TYPE', subtype: 'WAV',
+ pattern: /\.wav$/i},
+
+ // Text
+ {type: 'text', name: 'PLAIN_TEXT_FILE_TYPE', subtype: 'TXT',
+ pattern: /\.txt$/i},
+
+ // Archive
+ {type: 'archive', name: 'ZIP_ARCHIVE_FILE_TYPE', subtype: 'ZIP',
+ pattern: /\.zip$/i},
+ {type: 'archive', name: 'RAR_ARCHIVE_FILE_TYPE', subtype: 'RAR',
+ pattern: /\.rar$/i},
+ {type: 'archive', name: 'TAR_ARCHIVE_FILE_TYPE', subtype: 'TAR',
+ pattern: /\.tar$/i},
+ {type: 'archive', name: 'TAR_BZIP2_ARCHIVE_FILE_TYPE', subtype: 'TBZ2',
+ pattern: /\.(tar\.bz2|tbz|tbz2)$/i},
+ {type: 'archive', name: 'TAR_GZIP_ARCHIVE_FILE_TYPE', subtype: 'TGZ',
+ pattern: /\.(tar\.|t)gz$/i},
+
+ // Hosted docs.
+ {type: 'hosted', icon: 'gdoc', name: 'GDOC_DOCUMENT_FILE_TYPE',
+ subtype: 'doc', pattern: /\.gdoc$/i},
+ {type: 'hosted', icon: 'gsheet', name: 'GSHEET_DOCUMENT_FILE_TYPE',
+ subtype: 'sheet', pattern: /\.gsheet$/i},
+ {type: 'hosted', icon: 'gslides', name: 'GSLIDES_DOCUMENT_FILE_TYPE',
+ subtype: 'slides', pattern: /\.gslides$/i},
+ {type: 'hosted', icon: 'gdraw', name: 'GDRAW_DOCUMENT_FILE_TYPE',
+ subtype: 'draw', pattern: /\.gdraw$/i},
+ {type: 'hosted', icon: 'gtable', name: 'GTABLE_DOCUMENT_FILE_TYPE',
+ subtype: 'table', pattern: /\.gtable$/i},
+ {type: 'hosted', icon: 'glink', name: 'GLINK_DOCUMENT_FILE_TYPE',
+ subtype: 'glink', pattern: /\.glink$/i},
+ {type: 'hosted', icon: 'gform', name: 'GFORM_DOCUMENT_FILE_TYPE',
+ subtype: 'form', pattern: /\.gform$/i},
+
+ // Others
+ {type: 'document', icon: 'pdf', name: 'PDF_DOCUMENT_FILE_TYPE',
+ subtype: 'PDF', pattern: /\.pdf$/i},
+ {type: 'document', name: 'HTML_DOCUMENT_FILE_TYPE',
+ subtype: 'HTML', pattern: /\.(html?|mht|mhtml)$/i},
+ {type: 'document', icon: 'word', name: 'WORD_DOCUMENT_FILE_TYPE',
+ subtype: 'Word', pattern: /\.(doc|docx)$/i},
+ {type: 'document', icon: 'ppt', name: 'POWERPOINT_PRESENTATION_FILE_TYPE',
+ subtype: 'PPT', pattern: /\.(ppt|pptx)$/i},
+ {type: 'document', icon: 'excel', name: 'EXCEL_FILE_TYPE',
+ subtype: 'Excel', pattern: /\.(xls|xlsx)$/i}
+];
+
+/**
+ * A special type for directory.
+ */
+FileType.DIRECTORY = {name: 'FOLDER', type: '.folder', icon: 'folder'};
+
+/**
+ * Returns the file path extension for a given file.
+ *
+ * @param {string|Entry} file Reference to the file.
+ * Can be a name, a path, a url or a File API Entry.
+ * @return {string} The extension including a leading '.', or empty string if
+ * not found.
+ */
+FileType.getExtension = function(file) {
+ var fileName;
+ if (typeof file == 'object') {
+ if (file.isDirectory) {
+ // No extension for a directory.
+ return '';
+ } else {
+ fileName = file.name;
+ }
+ } else {
+ fileName = file;
+ }
+
+ var extensionStartIndex = fileName.lastIndexOf('.');
+ if (extensionStartIndex == -1 || extensionStartIndex == fileName.length - 1) {
+ return '';
+ }
+ return fileName.substr(extensionStartIndex);
+};
+
+/**
+ * Get the file type object for a given file.
+ *
+ * @param {string|Entry} file Reference to the file.
+ * Can be a name, a path, a url or a File API Entry.
+ * @return {Object} The matching file type object or an empty object.
+ */
+FileType.getType = function(file) {
+ if (typeof file == 'object') {
+ if (file.isDirectory)
+ return FileType.DIRECTORY;
+ else
+ file = file.name;
+ }
+ var types = FileType.types;
+ for (var i = 0; i < types.length; i++) {
+ if (types[i].pattern.test(file)) {
+ return types[i];
+ }
+ }
+
+ // Unknown file type.
+ var extension = FileType.getExtension(file);
+ if (extension == '') {
+ return { name: 'NO_EXTENSION_FILE_TYPE', type: 'UNKNOWN', icon: '' };
+ }
+ // subtype is the extension excluding the first dot.
+ return { name: 'GENERIC_FILE_TYPE', type: 'UNKNOWN',
+ subtype: extension.substr(1).toUpperCase(), icon: '' };
+};
+
+/**
+ * @param {string|Entry} file Reference to the file.
+ * Can be a name, a path, a url or a File API Entry.
+ * @return {string} Localized string representation of file type.
+ */
+FileType.getTypeString = function(file) {
+ var fileType = FileType.getType(file);
+ if (fileType.subtype)
+ return strf(fileType.name, fileType.subtype);
+ else
+ return str(fileType.name);
+};
+
+/**
+ * Pattern for urls pointing to Google Drive files.
+ */
+FileType.DRIVE_URL_PATTERN =
+ new RegExp('^filesystem:[^/]*://[^/]*/[^/]*/drive/(.*)');
+
+/**
+ * Pattern for file paths pointing to Google Drive files.
+ */
+FileType.DRIVE_PATH_PATTERN =
+ new RegExp('^/drive/');
+
+/**
+ * @param {string|Entry} file The url string or entry.
+ * @return {boolean} Whether this provider supports the url.
+ */
+FileType.isOnDrive = function(file) {
+ return typeof file == 'string' ?
+ FileType.DRIVE_URL_PATTERN.test(file) :
+ FileType.DRIVE_PATH_PATTERN.test(file.fullPath);
+};
+
+
+/**
+ * Get the media type for a given file.
+ *
+ * @param {string|Entry} file Reference to the file.
+ * @return {string} The value of 'type' property from one of the elements in
+ * FileType.types or undefined.
+ */
+FileType.getMediaType = function(file) {
+ return FileType.getType(file).type;
+};
+
+/**
+ * @param {string|Entry} file Reference to the file.
+ * @return {boolean} True if audio file.
+ */
+FileType.isAudio = function(file) {
+ return FileType.getMediaType(file) == 'audio';
+};
+
+/**
+ * @param {string|Entry} file Reference to the file.
+ * @return {boolean} True if image file.
+ */
+FileType.isImage = function(file) {
+ return FileType.getMediaType(file) == 'image';
+};
+
+/**
+ * @param {string|Entry} file Reference to the file.
+ * @return {boolean} True if video file.
+ */
+FileType.isVideo = function(file) {
+ return FileType.getMediaType(file) == 'video';
+};
+
+
+/**
+ * Files with more pixels won't have preview.
+ * @param {string|Entry} file Reference to the file.
+ * @return {boolean} True if image or video.
+ */
+FileType.isImageOrVideo = function(file) {
+ var type = FileType.getMediaType(file);
+ return type == 'image' || type == 'video';
+};
+
+/**
+ * @param {string|Entry} file Reference to the file.
+ * @return {boolean} Returns true if the file is hosted.
+ */
+FileType.isHosted = function(file) {
+ return FileType.getType(file).type === 'hosted';
+};
+
+/**
+ * @param {string|Entry} file Reference to the file.
+ * @return {boolean} Returns true if the file is not hidden, and we should
+ * display it.
+ */
+FileType.isVisible = function(file) {
+ if (typeof file == 'object') {
+ file = file.name;
+ }
+
+ var path = util.extractFilePath(file);
+ if (path) file = path;
+
+ file = file.split('/').pop();
+ return file.indexOf('.') != 0 && !(file in FileType.HIDDEN_NAMES);
+};
+
+/**
+ * File/directory names that we know are usually hidden.
+ */
+FileType.HIDDEN_NAMES = {
+ 'RECYCLED': true
+};
+
+/**
+ * @param {string|Entry} file Reference to the file.
+ * @return {string} Returns string that represents the file icon.
+ * It refers to a file 'images/filetype_' + icon + '.png'.
+ */
+FileType.getIcon = function(file) {
+ var fileType = FileType.getType(file);
+ return fileType.icon || fileType.type || 'unknown';
+};
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/file_watcher.js b/chromium/chrome/browser/resources/file_manager/foreground/js/file_watcher.js
new file mode 100644
index 00000000000..a6414e61f4a
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/file_watcher.js
@@ -0,0 +1,227 @@
+// Copyright 2013 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.
+
+'use strict';
+
+/**
+ * Watches for changes in the tracked directory, including local metadata
+ * changes.
+ *
+ * @param {MetadataCache} metadataCache Instance of MetadataCache.
+ * @extends {cr.EventTarget}
+ * @constructor
+ */
+function FileWatcher(metadataCache) {
+ this.queue_ = new AsyncUtil.Queue();
+ this.metadataCache_ = metadataCache;
+ this.watchedDirectoryEntry_ = null;
+
+ this.onDirectoryChangedBound_ = this.onDirectoryChanged_.bind(this);
+ chrome.fileBrowserPrivate.onDirectoryChanged.addListener(
+ this.onDirectoryChangedBound_);
+
+ this.filesystemMetadataObserverId_ = null;
+ this.thumbnailMetadataObserverId_ = null;
+ this.driveMetadataObserverId_ = null;
+}
+
+/**
+ * FileWatcher extends cr.EventTarget.
+ */
+FileWatcher.prototype.__proto__ = cr.EventTarget.prototype;
+
+/**
+ * Stops watching (must be called before page unload).
+ */
+FileWatcher.prototype.dispose = function() {
+ chrome.fileBrowserPrivate.onDirectoryChanged.removeListener(
+ this.onDirectoryChangedBound_);
+ if (this.watchedDirectoryEntry_)
+ this.resetWatchedEntry_(function() {}, function() {});
+};
+
+/**
+ * Called when a file in the watched directory is changed.
+ * @param {Event} event Change event.
+ * @private
+ */
+FileWatcher.prototype.onDirectoryChanged_ = function(event) {
+ if (this.watchedDirectoryEntry_ &&
+ event.entry.toURL() === this.watchedDirectoryEntry_.toURL()) {
+ var e = new Event('watcher-directory-changed');
+ this.dispatchEvent(e);
+ }
+};
+
+/**
+ * Called when general metadata in the watched directory has been changed.
+ *
+ * @param {Array.<Entry>} entries Array of entries.
+ * @param {Object.<string, Object>} properties Map from entry URLs to metadata
+ * properties.
+ * @private
+ */
+FileWatcher.prototype.onFilesystemMetadataChanged_ = function(
+ entries, properties) {
+ this.dispatchMetadataEvent_('filesystem', entries, properties);
+};
+
+/**
+ * Called when thumbnail metadata in the watched directory has been changed.
+ *
+ * @param {Array.<Entry>} entries Arrray of entries.
+ * @param {Object.<string, Object>} properties Map from entry URLs to metadata
+ * properties.
+ * @private
+ */
+FileWatcher.prototype.onThumbnailMetadataChanged_ = function(
+ entries, properties) {
+ this.dispatchMetadataEvent_('thumbnail', entries, properties);
+};
+
+/**
+ * Called when drive metadata in the watched directory has been changed.
+ *
+ * @param {Array.<Entry>} entries Array of entries.
+ * @param {Object.<string, Object>} properties Map from entry URLs to metadata
+ * properties.
+ * @private
+ */
+FileWatcher.prototype.onDriveMetadataChanged_ = function(
+ entries, properties) {
+ this.dispatchMetadataEvent_('drive', entries, properties);
+};
+
+/**
+ * Dispatches an event about detected change in metadata within the tracked
+ * directory.
+ *
+ * @param {string} type Type of the metadata change.
+ * @param {Array.<Entry>} entries Array of entries.
+ * @param {Object.<string, Object>} properties Map from entry URLs to metadata
+ * properties.
+ * @private
+ */
+FileWatcher.prototype.dispatchMetadataEvent_ = function(
+ type, entries, properties) {
+ var e = new Event('watcher-metadata-changed');
+ e.metadataType = type;
+ e.entries = entries;
+ e.properties = properties;
+ this.dispatchEvent(e);
+};
+
+/**
+ * Changes the watched directory. In case of a fake entry, the watch is
+ * just released, since there is no reason to track a fake directory.
+ *
+ * @param {!DirectoryEntry|!Object} entry Directory entry to be tracked, or the
+ * fake entry.
+ * @param {function()} callback Completion callback.
+ */
+FileWatcher.prototype.changeWatchedDirectory = function(entry, callback) {
+ if (entry && entry.toURL) {
+ this.changeWatchedEntry_(
+ entry,
+ callback,
+ function() {
+ console.error(
+ 'Unable to change the watched directory to: ' + entry.toURL());
+ callback();
+ });
+ } else {
+ this.resetWatchedEntry_(
+ callback,
+ function() {
+ console.error('Unable to reset the watched directory.');
+ callback();
+ });
+ }
+};
+
+/**
+ * Resets the watched entry to the passed directory.
+ *
+ * @param {function()} onSuccess Success callback.
+ * @param {function()} onError Error callback.
+ * @private
+ */
+FileWatcher.prototype.resetWatchedEntry_ = function(onSuccess, onError) {
+ // Run the tasks in the queue to avoid races.
+ this.queue_.run(function(callback) {
+ // Release the watched directory.
+ if (this.watchedDirectoryEntry_) {
+ chrome.fileBrowserPrivate.removeFileWatch(
+ this.watchedDirectoryEntry_.toURL(),
+ function(result) {
+ this.watchedDirectoryEntry_ = null;
+ if (result)
+ onSuccess();
+ else
+ onError();
+ callback();
+ }.bind(this));
+ this.metadataCache_.removeObserver(this.filesystemMetadataObserverId_);
+ this.metadataCache_.removeObserver(this.thumbnailMetadataObserverId_);
+ this.metadataCache_.removeObserver(this.driveMetadataObserverId_);
+ } else {
+ onSuccess();
+ callback();
+ }
+ }.bind(this));
+};
+
+/**
+ * Sets the watched entry to the passed directory.
+ *
+ * @param {!DirectoryEntry} entry Directory to be watched.
+ * @param {function()} onSuccess Success callback.
+ * @param {function()} onError Error callback.
+ * @private
+ */
+FileWatcher.prototype.changeWatchedEntry_ = function(
+ entry, onSuccess, onError) {
+ var setEntryClosure = function() {
+ // Run the tasks in the queue to avoid races.
+ this.queue_.run(function(callback) {
+ chrome.fileBrowserPrivate.addFileWatch(
+ entry.toURL(),
+ function(result) {
+ if (!result) {
+ this.watchedDirectoryEntry_ = null;
+ onError();
+ } else {
+ this.watchedDirectoryEntry_ = entry;
+ onSuccess();
+ }
+ callback();
+ }.bind(this));
+ this.filesystemMetadataObserverId_ = this.metadataCache_.addObserver(
+ entry,
+ MetadataCache.CHILDREN,
+ 'filesystem',
+ this.onFilesystemMetadataChanged_.bind(this));
+ this.thumbnailMetadataObserverId_ = this.metadataCache_.addObserver(
+ entry,
+ MetadataCache.CHILDREN,
+ 'thumbnail',
+ this.onThumbnailMetadataChanged_.bind(this));
+ this.driveMetadataObserverId_ = this.metadataCache_.addObserver(
+ entry,
+ MetadataCache.CHILDREN,
+ 'drive',
+ this.onDriveMetadataChanged_.bind(this));
+ }.bind(this));
+ }.bind(this);
+
+ // Reset the watched directory first, then set the new watched directory.
+ this.resetWatchedEntry_(setEntryClosure, onError);
+};
+
+/**
+ * @return {DirectoryEntry} Current watched directory entry.
+ */
+FileWatcher.prototype.getWatchedDirectoryEntry = function() {
+ return this.watchedDirectoryEntry_;
+};
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/folder_shortcuts_data_model.js b/chromium/chrome/browser/resources/file_manager/foreground/js/folder_shortcuts_data_model.js
new file mode 100644
index 00000000000..9467f5eec07
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/folder_shortcuts_data_model.js
@@ -0,0 +1,293 @@
+// Copyright (c) 2013 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.
+
+/**
+ * Model for the folder shortcuts. This object is cr.ui.ArrayDataModel-like
+ * object with additional methods for the folder shortcut feature.
+ * This uses chrome.storage as backend. Items are always sorted by file path.
+ *
+ * @constructor
+ * @extends {cr.EventTarget}
+ */
+function FolderShortcutsDataModel() {
+ this.array_ = [];
+
+ /**
+ * Eliminate unsupported folders from the list.
+ *
+ * @param {Array.<string>} array Folder array which may contain the
+ * unsupported folders.
+ * @return {Array.<string>} Folder list without unsupported folder.
+ */
+ var filter = function(array) {
+ return array.filter(PathUtil.isEligibleForFolderShortcut);
+ };
+
+ // Loads the contents from the storage to initialize the array.
+ chrome.storage.sync.get(FolderShortcutsDataModel.NAME, function(value) {
+ if (!(FolderShortcutsDataModel.NAME in value))
+ return;
+
+ // Since the value comes from outer resource, we have to check it just in
+ // case.
+ var list = value[FolderShortcutsDataModel.NAME];
+ if (list instanceof Array) {
+ list = filter(list);
+
+ // Record metrics.
+ metrics.recordSmallCount('FolderShortcut.Count', list.length);
+
+ var permutation = this.calculatePermutation_(this.array_, list);
+ this.array_ = list;
+ this.firePermutedEvent_(permutation);
+ }
+ }.bind(this));
+
+ // Listening for changes in the storage.
+ chrome.storage.onChanged.addListener(function(changes, namespace) {
+ if (!(FolderShortcutsDataModel.NAME in changes) || namespace != 'sync')
+ return;
+
+ var list = changes[FolderShortcutsDataModel.NAME].newValue;
+ // Since the value comes from outer resource, we have to check it just in
+ // case.
+ if (list instanceof Array) {
+ list = filter(list);
+
+ // If the list is not changed, do nothing and just return.
+ if (this.array_.length == list.length) {
+ var changed = false;
+ for (var i = 0; i < this.array_.length; i++) {
+ // Same item check: must be exact match.
+ if (this.array_[i] != list[i]) {
+ changed = true;
+ break;
+ }
+ }
+ if (!changed)
+ return;
+ }
+
+ var permutation = this.calculatePermutation_(this.array_, list);
+ this.array_ = list;
+ this.firePermutedEvent_(permutation);
+ }
+ }.bind(this));
+}
+
+/**
+ * Key name in chrome.storage. The array are stored with this name.
+ * @type {string}
+ * @const
+ */
+FolderShortcutsDataModel.NAME = 'folder-shortcuts-list';
+
+FolderShortcutsDataModel.prototype = {
+ __proto__: cr.EventTarget.prototype,
+
+ /**
+ * @return {number} Number of elements in the array.
+ */
+ get length() {
+ return this.array_.length;
+ },
+
+ /**
+ * Returns the paths in the given range as a new array instance. The
+ * arguments and return value are compatible with Array.slice().
+ *
+ * @param {number} start Where to start the selection.
+ * @param {number=} opt_end Where to end the selection.
+ * @return {Array.<string>} Paths in the selected range.
+ */
+ slice: function(begin, opt_end) {
+ return this.array_.slice(begin, opt_end);
+ },
+
+ /**
+ * @param {number} index Index of the element to be retrieved.
+ * @return {string} The value of the |index|-th element.
+ */
+ item: function(index) {
+ return this.array_[index];
+ },
+
+ /**
+ * @param {string} value Value of the element to be retrieved.
+ * @return {number} Index of the element with the specified |value|.
+ */
+ getIndex: function(value) {
+ for (var i = 0; i < this.length; i++) {
+ // Same item check: must be exact match.
+ if (this.array_[i] == value) {
+ return i;
+ }
+ }
+ return -1;
+ },
+
+ /**
+ * Compares 2 strings and returns a number indicating one string comes before
+ * or after or is the same as the other string in sort order.
+ *
+ * @param {string} a String1.
+ * @param {string} b String2.
+ * @return {boolean} Return -1, if String1 < String2. Return 0, if String1 ==
+ * String2. Otherwise, return 1.
+ */
+ compare: function(a, b) {
+ return a.localeCompare(b,
+ undefined, // locale parameter, use default locale.
+ {usage: 'sort', numeric: true});
+ },
+
+ /**
+ * Adds the given item to the array. If there were already same item in the
+ * list, return the index of the existing item without adding a duplicate
+ * item.
+ *
+ * @param {string} value Value to be added into the array.
+ * @return {number} Index in the list which the element added to.
+ */
+ add: function(value) {
+ var oldArray = this.array_.slice(0); // Shallow copy.
+ var addedIndex = -1;
+ for (var i = 0; i < this.length; i++) {
+ // Same item check: must be exact match.
+ if (this.array_[i] == value)
+ return i;
+
+ // Since the array is sorted, new item will be added just before the first
+ // larger item.
+ if (this.compare(this.array_[i], value) >= 0) {
+ this.array_.splice(i, 0, value);
+ addedIndex = i;
+ break;
+ }
+ }
+ // If value is not added yet, add it at the last.
+ if (addedIndex == -1) {
+ this.array_.push(value);
+ addedIndex = this.length;
+ }
+
+ this.firePermutedEvent_(
+ this.calculatePermutation_(oldArray, this.array_));
+ this.save_();
+ metrics.recordUserAction('FolderShortcut.Add');
+ return addedIndex;
+ },
+
+ /**
+ * Removes the given item from the array.
+ * @param {string} value Value to be removed from the array.
+ * @return {number} Index in the list which the element removed from.
+ */
+ remove: function(value) {
+ var removedIndex = -1;
+ var oldArray = this.array_.slice(0); // Shallow copy.
+ for (var i = 0; i < this.length; i++) {
+ // Same item check: must be exact match.
+ if (this.array_[i] == value) {
+ this.array_.splice(i, 1);
+ removedIndex = i;
+ break;
+ }
+ }
+
+ if (removedIndex != -1) {
+ this.firePermutedEvent_(
+ this.calculatePermutation_(oldArray, this.array_));
+ this.save_();
+ metrics.recordUserAction('FolderShortcut.Remove');
+ return removedIndex;
+ }
+
+ // No item is removed.
+ return -1;
+ },
+
+ /**
+ * @param {string} path Path to be checked.
+ * @return {boolean} True if the given |path| exists in the array. False
+ * otherwise.
+ */
+ exists: function(path) {
+ var index = this.getIndex(path);
+ return (index >= 0);
+ },
+
+ /**
+ * Saves the current array to chrome.storage.
+ * @private
+ */
+ save_: function() {
+ var obj = {};
+ obj[FolderShortcutsDataModel.NAME] = this.array_;
+ chrome.storage.sync.set(obj, function() {});
+ },
+
+ /**
+ * Creates a permutation array for 'permuted' event, which is compatible with
+ * a permutation array used in cr/ui/array_data_model.js.
+ *
+ * @param {array} oldArray Previous array before changing.
+ * @param {array} newArray New array after changing.
+ * @return {Array.<number>} Created permutation array.
+ * @private
+ */
+ calculatePermutation_: function(oldArray, newArray) {
+ var oldIndex = 0; // Index of oldArray.
+ var newIndex = 0; // Index of newArray.
+
+ // Note that both new and old arrays are sorted.
+ var permutation = [];
+ for (; oldIndex < oldArray.length; oldIndex++) {
+ if (newIndex >= newArray.length) {
+ // oldArray[oldIndex] is deleted, which is not in the new array.
+ permutation[oldIndex] = -1;
+ continue;
+ }
+
+ while (newIndex < newArray.length) {
+ // Unchanged item, which exists in both new and old array. But the
+ // index may be changed.
+ if (oldArray[oldIndex] == newArray[newIndex]) {
+ permutation[oldIndex] = newIndex;
+ newIndex++;
+ break;
+ }
+
+ // oldArray[oldIndex] is deleted, which is not in the new array.
+ if (this.compare(oldArray[oldIndex], newArray[newIndex]) < 0) {
+ permutation[oldIndex] = -1;
+ break;
+ }
+
+ // In the case of this.compare(oldArray[oldIndex]) > 0:
+ // newArray[newIndex] is added, which is not in the old array.
+ newIndex++;
+ }
+ }
+ return permutation;
+ },
+
+ /**
+ * Fires a 'permuted' event, which is compatible with cr.ui.ArrayDataModel.
+ * @param {Array.<number>} Permutation array.
+ */
+ firePermutedEvent_: function(permutation) {
+ var permutedEvent = new Event('permuted');
+ permutedEvent.newLength = this.length;
+ permutedEvent.permutation = permutation;
+ this.dispatchEvent(permutedEvent);
+
+ // Note: This model only fires 'permuted' event, because:
+ // 1) 'change' event is not necessary to fire since it is covered by
+ // 'permuted' event.
+ // 2) 'splice' and 'sorted' events are not implemented. These events are
+ // not used in NavigationListModel. We have to implement them when
+ // necessary.
+ }
+};
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/commands.js b/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/commands.js
new file mode 100644
index 00000000000..f05f0f227d6
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/commands.js
@@ -0,0 +1,455 @@
+// 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.
+
+'use strict';
+
+/**
+ * Command queue is the only way to modify images.
+ * Supports undo/redo.
+ * Command execution is asynchronous (callback-based).
+ *
+ * @param {Document} document Document to create canvases in.
+ * @param {HTMLCanvasElement} canvas The canvas with the original image.
+ * @param {function(callback)} saveFunction Function to save the image.
+ * @constructor
+ */
+function CommandQueue(document, canvas, saveFunction) {
+ this.document_ = document;
+ this.undo_ = [];
+ this.redo_ = [];
+ this.subscribers_ = [];
+ this.currentImage_ = canvas;
+
+ // Current image may be null or not-null but with width = height = 0.
+ // Copying an image with zero dimensions causes js errors.
+ if (this.currentImage_) {
+ this.baselineImage_ = document.createElement('canvas');
+ this.baselineImage_.width = this.currentImage_.width;
+ this.baselineImage_.height = this.currentImage_.height;
+ if (this.currentImage_.width > 0 && this.currentImage_.height > 0) {
+ var context = this.baselineImage_.getContext('2d');
+ context.drawImage(this.currentImage_, 0, 0);
+ }
+ } else {
+ this.baselineImage_ = null;
+ }
+
+ this.previousImage_ = document.createElement('canvas');
+ this.previousImageAvailable_ = false;
+
+ this.saveFunction_ = saveFunction;
+ this.busy_ = false;
+ this.UIContext_ = {};
+}
+
+/**
+ * Attach the UI elements to the command queue.
+ * Once the UI is attached the results of image manipulations are displayed.
+ *
+ * @param {ImageView} imageView The ImageView object to display the results.
+ * @param {ImageEditor.Prompt} prompt Prompt to use with this CommandQueue.
+ * @param {function(boolean)} lock Function to enable/disable buttons etc.
+ */
+CommandQueue.prototype.attachUI = function(imageView, prompt, lock) {
+ this.UIContext_ = {
+ imageView: imageView,
+ prompt: prompt,
+ lock: lock
+ };
+};
+
+/**
+ * Execute the action when the queue is not busy.
+ * @param {function} callback Callback.
+ */
+CommandQueue.prototype.executeWhenReady = function(callback) {
+ if (this.isBusy())
+ this.subscribers_.push(callback);
+ else
+ setTimeout(callback, 0);
+};
+
+/**
+ * @return {boolean} True if the command queue is busy.
+ */
+CommandQueue.prototype.isBusy = function() { return this.busy_ };
+
+/**
+ * Set the queue state to busy. Lock the UI.
+ * @private
+ */
+CommandQueue.prototype.setBusy_ = function() {
+ if (this.busy_)
+ throw new Error('CommandQueue already busy');
+
+ this.busy_ = true;
+
+ if (this.UIContext_.lock)
+ this.UIContext_.lock(true);
+
+ ImageUtil.trace.resetTimer('command-busy');
+};
+
+/**
+ * Set the queue state to not busy. Unlock the UI and execute pending actions.
+ * @private
+ */
+CommandQueue.prototype.clearBusy_ = function() {
+ if (!this.busy_)
+ throw new Error('Inconsistent CommandQueue already not busy');
+
+ this.busy_ = false;
+
+ // Execute the actions requested while the queue was busy.
+ while (this.subscribers_.length)
+ this.subscribers_.shift()();
+
+ if (this.UIContext_.lock)
+ this.UIContext_.lock(false);
+
+ ImageUtil.trace.reportTimer('command-busy');
+};
+
+/**
+ * Commit the image change: save and unlock the UI.
+ * @param {number=} opt_delay Delay in ms (to avoid disrupting the animation).
+ * @private
+ */
+CommandQueue.prototype.commit_ = function(opt_delay) {
+ setTimeout(this.saveFunction_.bind(null, this.clearBusy_.bind(this)),
+ opt_delay || 0);
+};
+
+/**
+ * Internal function to execute the command in a given context.
+ *
+ * @param {Command} command The command to execute.
+ * @param {Object} uiContext The UI context.
+ * @param {function} callback Completion callback.
+ * @private
+ */
+CommandQueue.prototype.doExecute_ = function(command, uiContext, callback) {
+ if (!this.currentImage_)
+ throw new Error('Cannot operate on null image');
+
+ // Remember one previous image so that the first undo is as fast as possible.
+ this.previousImage_.width = this.currentImage_.width;
+ this.previousImage_.height = this.currentImage_.height;
+ this.previousImageAvailable_ = true;
+ var context = this.previousImage_.getContext('2d');
+ context.drawImage(this.currentImage_, 0, 0);
+
+ command.execute(
+ this.document_,
+ this.currentImage_,
+ function(result, opt_delay) {
+ this.currentImage_ = result;
+ callback(opt_delay);
+ }.bind(this),
+ uiContext);
+};
+
+/**
+ * Executes the command.
+ *
+ * @param {Command} command Command to execute.
+ * @param {boolean=} opt_keep_redo True if redo stack should not be cleared.
+ */
+CommandQueue.prototype.execute = function(command, opt_keep_redo) {
+ this.setBusy_();
+
+ if (!opt_keep_redo)
+ this.redo_ = [];
+
+ this.undo_.push(command);
+
+ this.doExecute_(command, this.UIContext_, this.commit_.bind(this));
+};
+
+/**
+ * @return {boolean} True if Undo is applicable.
+ */
+CommandQueue.prototype.canUndo = function() {
+ return this.undo_.length != 0;
+};
+
+/**
+ * Undo the most recent command.
+ */
+CommandQueue.prototype.undo = function() {
+ if (!this.canUndo())
+ throw new Error('Cannot undo');
+
+ this.setBusy_();
+
+ var command = this.undo_.pop();
+ this.redo_.push(command);
+
+ var self = this;
+
+ function complete() {
+ var delay = command.revertView(
+ self.currentImage_, self.UIContext_.imageView);
+ self.commit_(delay);
+ }
+
+ if (this.previousImageAvailable_) {
+ // First undo after an execute call.
+ this.currentImage_.width = this.previousImage_.width;
+ this.currentImage_.height = this.previousImage_.height;
+ var context = this.currentImage_.getContext('2d');
+ context.drawImage(this.previousImage_, 0, 0);
+
+ // Free memory.
+ this.previousImage_.width = 0;
+ this.previousImage_.height = 0;
+ this.previousImageAvailable_ = false;
+
+ complete();
+ // TODO(kaznacheev) Consider recalculating previousImage_ right here
+ // by replaying the commands in the background.
+ } else {
+ this.currentImage_.width = this.baselineImage_.width;
+ this.currentImage_.height = this.baselineImage_.height;
+ var context = this.currentImage_.getContext('2d');
+ context.drawImage(this.baselineImage_, 0, 0);
+
+ var replay = function(index) {
+ if (index < self.undo_.length)
+ self.doExecute_(self.undo_[index], {}, replay.bind(null, index + 1));
+ else {
+ complete();
+ }
+ };
+
+ replay(0);
+ }
+};
+
+/**
+ * @return {boolean} True if Redo is applicable.
+ */
+CommandQueue.prototype.canRedo = function() {
+ return this.redo_.length != 0;
+};
+
+/**
+ * Repeat the command that was recently un-done.
+ */
+CommandQueue.prototype.redo = function() {
+ if (!this.canRedo())
+ throw new Error('Cannot redo');
+
+ this.execute(this.redo_.pop(), true);
+};
+
+/**
+ * Closes internal buffers. Call to ensure, that internal buffers are freed
+ * as soon as possible.
+ */
+CommandQueue.prototype.close = function() {
+ // Free memory used by the undo buffer.
+ this.previousImage_.width = 0;
+ this.previousImage_.height = 0;
+ this.previousImageAvailable_ = false;
+
+ if (this.baselineImage_) {
+ this.baselineImage_.width = 0;
+ this.baselineImage_.height = 0;
+ }
+};
+
+/**
+ * Command object encapsulates an operation on an image and a way to visualize
+ * its result.
+ *
+ * @param {string} name Command name.
+ * @constructor
+ */
+function Command(name) {
+ this.name_ = name;
+}
+
+/**
+ * @return {string} String representation of the command.
+ */
+Command.prototype.toString = function() {
+ return 'Command ' + this.name_;
+};
+
+/**
+ * Execute the command and visualize its results.
+ *
+ * The two actions are combined into one method because sometimes it is nice
+ * to be able to show partial results for slower operations.
+ *
+ * @param {Document} document Document on which to execute command.
+ * @param {HTMLCanvasElement} srcCanvas Canvas to execute on.
+ * @param {function(HTMLCanvasElement, number)} callback Callback to call on
+ * completion.
+ * @param {Object} uiContext Context to work in.
+ */
+Command.prototype.execute = function(document, srcCanvas, callback, uiContext) {
+ console.error('Command.prototype.execute not implemented');
+};
+
+/**
+ * Visualize reversion of the operation.
+ *
+ * @param {HTMLCanvasElement} canvas Image data to use.
+ * @param {ImageView} imageView ImageView to revert.
+ * @return {number} Animation duration in ms.
+ */
+Command.prototype.revertView = function(canvas, imageView) {
+ imageView.replace(canvas);
+ return 0;
+};
+
+/**
+ * Creates canvas to render on.
+ *
+ * @param {Document} document Document to create canvas in.
+ * @param {HTMLCanvasElement} srcCanvas to copy optional dimensions from.
+ * @param {number=} opt_width new canvas width.
+ * @param {number=} opt_height new canvas height.
+ * @return {HTMLCanvasElement} Newly created canvas.
+ * @private
+ */
+Command.prototype.createCanvas_ = function(
+ document, srcCanvas, opt_width, opt_height) {
+ var result = document.createElement('canvas');
+ result.width = opt_width || srcCanvas.width;
+ result.height = opt_height || srcCanvas.height;
+ return result;
+};
+
+
+/**
+ * Rotate command
+ * @param {number} rotate90 Rotation angle in 90 degree increments (signed).
+ * @constructor
+ * @extends {Command}
+ */
+Command.Rotate = function(rotate90) {
+ Command.call(this, 'rotate(' + rotate90 * 90 + 'deg)');
+ this.rotate90_ = rotate90;
+};
+
+Command.Rotate.prototype = { __proto__: Command.prototype };
+
+/** @override */
+Command.Rotate.prototype.execute = function(
+ document, srcCanvas, callback, uiContext) {
+ var result = this.createCanvas_(
+ document,
+ srcCanvas,
+ (this.rotate90_ & 1) ? srcCanvas.height : srcCanvas.width,
+ (this.rotate90_ & 1) ? srcCanvas.width : srcCanvas.height);
+ ImageUtil.drawImageTransformed(
+ result, srcCanvas, 1, 1, this.rotate90_ * Math.PI / 2);
+ var delay;
+ if (uiContext.imageView) {
+ delay = uiContext.imageView.replaceAndAnimate(result, null, this.rotate90_);
+ }
+ setTimeout(callback, 0, result, delay);
+};
+
+/** @override */
+Command.Rotate.prototype.revertView = function(canvas, imageView) {
+ return imageView.replaceAndAnimate(canvas, null, -this.rotate90_);
+};
+
+
+/**
+ * Crop command.
+ *
+ * @param {Rect} imageRect Crop rectangle in image coordinates.
+ * @constructor
+ * @extends {Command}
+ */
+Command.Crop = function(imageRect) {
+ Command.call(this, 'crop' + imageRect.toString());
+ this.imageRect_ = imageRect;
+};
+
+Command.Crop.prototype = { __proto__: Command.prototype };
+
+/** @override */
+Command.Crop.prototype.execute = function(
+ document, srcCanvas, callback, uiContext) {
+ var result = this.createCanvas_(
+ document, srcCanvas, this.imageRect_.width, this.imageRect_.height);
+ Rect.drawImage(result.getContext('2d'), srcCanvas, null, this.imageRect_);
+ var delay;
+ if (uiContext.imageView) {
+ delay = uiContext.imageView.replaceAndAnimate(result, this.imageRect_, 0);
+ }
+ setTimeout(callback, 0, result, delay);
+};
+
+/** @override */
+Command.Crop.prototype.revertView = function(canvas, imageView) {
+ return imageView.animateAndReplace(canvas, this.imageRect_);
+};
+
+
+/**
+ * Filter command.
+ *
+ * @param {string} name Command name.
+ * @param {function(ImageData,ImageData,number,number)} filter Filter function.
+ * @param {string} message Message to display when done.
+ * @constructor
+ * @extends {Command}
+ */
+Command.Filter = function(name, filter, message) {
+ Command.call(this, name);
+ this.filter_ = filter;
+ this.message_ = message;
+};
+
+Command.Filter.prototype = { __proto__: Command.prototype };
+
+/** @override */
+Command.Filter.prototype.execute = function(
+ document, srcCanvas, callback, uiContext) {
+ var result = this.createCanvas_(document, srcCanvas);
+
+ var self = this;
+
+ var previousRow = 0;
+
+ function onProgressVisible(updatedRow, rowCount) {
+ if (updatedRow == rowCount) {
+ uiContext.imageView.replace(result);
+ if (self.message_)
+ uiContext.prompt.show(self.message_, 2000);
+ callback(result);
+ } else {
+ var viewport = uiContext.imageView.viewport_;
+
+ var imageStrip = new Rect(viewport.getImageBounds());
+ imageStrip.top = previousRow;
+ imageStrip.height = updatedRow - previousRow;
+
+ var screenStrip = new Rect(viewport.getImageBoundsOnScreen());
+ screenStrip.top = Math.round(viewport.imageToScreenY(previousRow));
+ screenStrip.height =
+ Math.round(viewport.imageToScreenY(updatedRow)) - screenStrip.top;
+
+ uiContext.imageView.paintDeviceRect(
+ viewport.screenToDeviceRect(screenStrip), result, imageStrip);
+ previousRow = updatedRow;
+ }
+ }
+
+ function onProgressInvisible(updatedRow, rowCount) {
+ if (updatedRow == rowCount) {
+ callback(result);
+ }
+ }
+
+ filter.applyByStrips(result, srcCanvas, this.filter_,
+ uiContext.imageView ? onProgressVisible : onProgressInvisible);
+};
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/exif_encoder.js b/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/exif_encoder.js
new file mode 100644
index 00000000000..e81b8fba899
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/exif_encoder.js
@@ -0,0 +1,569 @@
+// 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.
+
+'use strict';
+
+// TODO:(kaznacheev) Share the EXIF constants with exif_parser.js
+var EXIF_MARK_SOS = 0xffda; // Start of "stream" (the actual image data).
+var EXIF_MARK_SOI = 0xffd8; // Start of image data.
+var EXIF_MARK_EOI = 0xffd9; // End of image data.
+
+var EXIF_MARK_APP0 = 0xffe0; // APP0 block, most commonly JFIF data.
+var EXIF_MARK_EXIF = 0xffe1; // Start of exif block.
+
+var EXIF_ALIGN_LITTLE = 0x4949; // Indicates little endian exif data.
+var EXIF_ALIGN_BIG = 0x4d4d; // Indicates big endian exif data.
+
+var EXIF_TAG_TIFF = 0x002a; // First directory containing TIFF data.
+var EXIF_TAG_GPSDATA = 0x8825; // Pointer from TIFF to the GPS directory.
+var EXIF_TAG_EXIFDATA = 0x8769; // Pointer from TIFF to the EXIF IFD.
+
+var EXIF_TAG_JPG_THUMB_OFFSET = 0x0201; // Pointer from TIFF to thumbnail.
+var EXIF_TAG_JPG_THUMB_LENGTH = 0x0202; // Length of thumbnail data.
+
+var EXIF_TAG_IMAGE_WIDTH = 0x0100;
+var EXIF_TAG_IMAGE_HEIGHT = 0x0101;
+
+var EXIF_TAG_ORIENTATION = 0x0112;
+var EXIF_TAG_X_DIMENSION = 0xA002;
+var EXIF_TAG_Y_DIMENSION = 0xA003;
+
+/**
+ * The Exif metadata encoder.
+ * Uses the metadata format as defined by ExifParser.
+ * @param {Object} original_metadata Metadata to encode.
+ * @constructor
+ * @extends {ImageEncoder.MetadataEncoder}
+ */
+function ExifEncoder(original_metadata) {
+ ImageEncoder.MetadataEncoder.apply(this, arguments);
+
+ this.ifd_ = this.metadata_.ifd;
+ if (!this.ifd_)
+ this.ifd_ = this.metadata_.ifd = {};
+}
+
+ExifEncoder.prototype = {__proto__: ImageEncoder.MetadataEncoder.prototype};
+
+ImageEncoder.registerMetadataEncoder(ExifEncoder, 'image/jpeg');
+
+/**
+ * @param {HTMLCanvasElement|Object} canvas Canvas or anything with
+ * width and height properties.
+ */
+ExifEncoder.prototype.setImageData = function(canvas) {
+ var image = this.ifd_.image;
+ if (!image)
+ image = this.ifd_.image = {};
+
+ // Only update width/height in this directory if they are present.
+ if (image[EXIF_TAG_IMAGE_WIDTH] && image[EXIF_TAG_IMAGE_HEIGHT]) {
+ image[EXIF_TAG_IMAGE_WIDTH].value = canvas.width;
+ image[EXIF_TAG_IMAGE_HEIGHT].value = canvas.height;
+ }
+
+ var exif = this.ifd_.exif;
+ if (!exif)
+ exif = this.ifd_.exif = {};
+ ExifEncoder.findOrCreateTag(image, EXIF_TAG_EXIFDATA);
+ ExifEncoder.findOrCreateTag(exif, EXIF_TAG_X_DIMENSION).value = canvas.width;
+ ExifEncoder.findOrCreateTag(exif, EXIF_TAG_Y_DIMENSION).value = canvas.height;
+
+ this.metadata_.width = canvas.width;
+ this.metadata_.height = canvas.height;
+
+ // Always save in default orientation.
+ delete this.metadata_.imageTransform;
+ ExifEncoder.findOrCreateTag(image, EXIF_TAG_ORIENTATION).value = 1;
+};
+
+
+/**
+ * @param {HTMLCanvasElement} canvas Thumbnail canvas.
+ * @param {number} quality (0..1] Thumbnail encoding quality.
+ */
+ExifEncoder.prototype.setThumbnailData = function(canvas, quality) {
+ // Empirical formula with reasonable behavior:
+ // 10K for 1Mpix, 30K for 5Mpix, 50K for 9Mpix and up.
+ var pixelCount = this.metadata_.width * this.metadata_.height;
+ var maxEncodedSize = 5000 * Math.min(10, 1 + pixelCount / 1000000);
+
+ var DATA_URL_PREFIX = 'data:' + this.mimeType + ';base64,';
+ var BASE64_BLOAT = 4 / 3;
+ var maxDataURLLength =
+ DATA_URL_PREFIX.length + Math.ceil(maxEncodedSize * BASE64_BLOAT);
+
+ for (;; quality *= 0.8) {
+ ImageEncoder.MetadataEncoder.prototype.setThumbnailData.call(
+ this, canvas, quality);
+ if (this.metadata_.thumbnailURL.length <= maxDataURLLength || quality < 0.2)
+ break;
+ }
+
+ if (this.metadata_.thumbnailURL.length <= maxDataURLLength) {
+ var thumbnail = this.ifd_.thumbnail;
+ if (!thumbnail)
+ thumbnail = this.ifd_.thumbnail = {};
+
+ ExifEncoder.findOrCreateTag(thumbnail, EXIF_TAG_IMAGE_WIDTH).value =
+ canvas.width;
+
+ ExifEncoder.findOrCreateTag(thumbnail, EXIF_TAG_IMAGE_HEIGHT).value =
+ canvas.height;
+
+ // The values for these tags will be set in ExifWriter.encode.
+ ExifEncoder.findOrCreateTag(thumbnail, EXIF_TAG_JPG_THUMB_OFFSET);
+ ExifEncoder.findOrCreateTag(thumbnail, EXIF_TAG_JPG_THUMB_LENGTH);
+
+ // Always save in default orientation.
+ ExifEncoder.findOrCreateTag(thumbnail, EXIF_TAG_ORIENTATION).value = 1;
+ } else {
+ console.warn(
+ 'Thumbnail URL too long: ' + this.metadata_.thumbnailURL.length);
+ // Delete thumbnail ifd so that it is not written out to a file, but
+ // keep thumbnailURL for display purposes.
+ if (this.ifd_.thumbnail) {
+ delete this.ifd_.thumbnail;
+ }
+ }
+ delete this.metadata_.thumbnailTransform;
+};
+
+/**
+ * Return a range where the metadata is (or should be) located.
+ * @param {string} encodedImage Raw image data to look for metadata.
+ * @return {Object} An object with from and to properties.
+ */
+ExifEncoder.prototype.findInsertionRange = function(encodedImage) {
+ function getWord(pos) {
+ if (pos + 2 > encodedImage.length)
+ throw 'Reading past the buffer end @' + pos;
+ return encodedImage.charCodeAt(pos) << 8 | encodedImage.charCodeAt(pos + 1);
+ }
+
+ if (getWord(0) != EXIF_MARK_SOI)
+ throw new Error('Jpeg data starts from 0x' + getWord(0).toString(16));
+
+ var sectionStart = 2;
+
+ // Default: an empty range right after SOI.
+ // Will be returned in absence of APP0 or Exif sections.
+ var range = {from: sectionStart, to: sectionStart};
+
+ for (;;) {
+ var tag = getWord(sectionStart);
+
+ if (tag == EXIF_MARK_SOS)
+ break;
+
+ var nextSectionStart = sectionStart + 2 + getWord(sectionStart + 2);
+ if (nextSectionStart <= sectionStart ||
+ nextSectionStart > encodedImage.length)
+ throw new Error('Invalid section size in jpeg data');
+
+ if (tag == EXIF_MARK_APP0) {
+ // Assert that we have not seen the Exif section yet.
+ if (range.from != range.to)
+ throw new Error('APP0 section found after EXIF section');
+ // An empty range right after the APP0 segment.
+ range.from = range.to = nextSectionStart;
+ } else if (tag == EXIF_MARK_EXIF) {
+ // A range containing the existing EXIF section.
+ range.from = sectionStart;
+ range.to = nextSectionStart;
+ }
+ sectionStart = nextSectionStart;
+ }
+
+ return range;
+};
+
+/**
+ * @return {ArrayBuffer} serialized metadata ready to write to an image file.
+ */
+ExifEncoder.prototype.encode = function() {
+ var HEADER_SIZE = 10;
+
+ // Allocate the largest theoretically possible size.
+ var bytes = new Uint8Array(0x10000);
+
+ // Serialize header
+ var hw = new ByteWriter(bytes.buffer, 0, HEADER_SIZE);
+ hw.writeScalar(EXIF_MARK_EXIF, 2);
+ hw.forward('size', 2);
+ hw.writeString('Exif\0\0'); // Magic string.
+
+ // First serialize the content of the exif section.
+ // Use a ByteWriter starting at HEADER_SIZE offset so that tell() positions
+ // can be directly mapped to offsets as encoded in the dictionaries.
+ var bw = new ByteWriter(bytes.buffer, HEADER_SIZE);
+
+ if (this.metadata_.littleEndian) {
+ bw.setByteOrder(ByteWriter.LITTLE_ENDIAN);
+ bw.writeScalar(EXIF_ALIGN_LITTLE, 2);
+ } else {
+ bw.setByteOrder(ByteWriter.BIG_ENDIAN);
+ bw.writeScalar(EXIF_ALIGN_BIG, 2);
+ }
+
+ bw.writeScalar(EXIF_TAG_TIFF, 2);
+
+ bw.forward('image-dir', 4); // The pointer should point right after itself.
+ bw.resolveOffset('image-dir');
+
+ ExifEncoder.encodeDirectory(bw, this.ifd_.image,
+ [EXIF_TAG_EXIFDATA, EXIF_TAG_GPSDATA], 'thumb-dir');
+
+ if (this.ifd_.exif) {
+ bw.resolveOffset(EXIF_TAG_EXIFDATA);
+ ExifEncoder.encodeDirectory(bw, this.ifd_.exif);
+ } else {
+ if (EXIF_TAG_EXIFDATA in this.ifd_.image)
+ throw new Error('Corrupt exif dictionary reference');
+ }
+
+ if (this.ifd_.gps) {
+ bw.resolveOffset(EXIF_TAG_GPSDATA);
+ ExifEncoder.encodeDirectory(bw, this.ifd_.gps);
+ } else {
+ if (EXIF_TAG_GPSDATA in this.ifd_.image)
+ throw new Error('Missing gps dictionary reference');
+ }
+
+ if (this.ifd_.thumbnail) {
+ bw.resolveOffset('thumb-dir');
+ ExifEncoder.encodeDirectory(
+ bw,
+ this.ifd_.thumbnail,
+ [EXIF_TAG_JPG_THUMB_OFFSET, EXIF_TAG_JPG_THUMB_LENGTH]);
+
+ var thumbnailDecoded =
+ ImageEncoder.decodeDataURL(this.metadata_.thumbnailURL);
+ bw.resolveOffset(EXIF_TAG_JPG_THUMB_OFFSET);
+ bw.resolve(EXIF_TAG_JPG_THUMB_LENGTH, thumbnailDecoded.length);
+ bw.writeString(thumbnailDecoded);
+ } else {
+ bw.resolve('thumb-dir', 0);
+ }
+
+ bw.checkResolved();
+
+ var totalSize = HEADER_SIZE + bw.tell();
+ hw.resolve('size', totalSize - 2); // The marker is excluded.
+ hw.checkResolved();
+
+ var subarray = new Uint8Array(totalSize);
+ for (var i = 0; i != totalSize; i++) {
+ subarray[i] = bytes[i];
+ }
+ return subarray.buffer;
+};
+
+/*
+ * Static methods.
+ */
+
+/**
+ * Write the contents of an IFD directory.
+ * @param {ByteWriter} bw ByteWriter to use.
+ * @param {Object} directory A directory map as created by ExifParser.
+ * @param {Array} resolveLater An array of tag ids for which the values will be
+ * resolved later.
+ * @param {string} nextDirPointer A forward key for the pointer to the next
+ * directory. If omitted the pointer is set to 0.
+ */
+ExifEncoder.encodeDirectory = function(
+ bw, directory, resolveLater, nextDirPointer) {
+
+ var longValues = [];
+
+ bw.forward('dir-count', 2);
+ var count = 0;
+
+ for (var key in directory) {
+ var tag = directory[key];
+ bw.writeScalar(tag.id, 2);
+ bw.writeScalar(tag.format, 2);
+ bw.writeScalar(tag.componentCount, 4);
+
+ var width = ExifEncoder.getComponentWidth(tag) * tag.componentCount;
+
+ if (resolveLater && (resolveLater.indexOf(tag.id) >= 0)) {
+ // The actual value depends on further computations.
+ if (tag.componentCount != 1 || width > 4)
+ throw new Error('Cannot forward the pointer for ' + tag.id);
+ bw.forward(tag.id, width);
+ } else if (width <= 4) {
+ // The value fits into 4 bytes, write it immediately.
+ ExifEncoder.writeValue(bw, tag);
+ } else {
+ // The value does not fit, forward the 4 byte offset to the actual value.
+ width = 4;
+ bw.forward(tag.id, width);
+ longValues.push(tag);
+ }
+ bw.skip(4 - width); // Align so that the value take up exactly 4 bytes.
+ count++;
+ }
+
+ bw.resolve('dir-count', count);
+
+ if (nextDirPointer) {
+ bw.forward(nextDirPointer, 4);
+ } else {
+ bw.writeScalar(0, 4);
+ }
+
+ // Write out the long values and resolve pointers.
+ for (var i = 0; i != longValues.length; i++) {
+ var longValue = longValues[i];
+ bw.resolveOffset(longValue.id);
+ ExifEncoder.writeValue(bw, longValue);
+ }
+};
+
+/**
+ * @param {{format:number, id:number}} tag EXIF tag object.
+ * @return {number} Width in bytes of the data unit associated with this tag.
+ * TODO(kaznacheev): Share with ExifParser?
+ */
+ExifEncoder.getComponentWidth = function(tag) {
+ switch (tag.format) {
+ case 1: // Byte
+ case 2: // String
+ case 7: // Undefined
+ return 1;
+
+ case 3: // Short
+ return 2;
+
+ case 4: // Long
+ case 9: // Signed Long
+ return 4;
+
+ case 5: // Rational
+ case 10: // Signed Rational
+ return 8;
+
+ default: // ???
+ console.warn('Unknown tag format 0x' +
+ Number(tag.id).toString(16) + ': ' + tag.format);
+ return 4;
+ }
+};
+
+/**
+ * Writes out the tag value.
+ * @param {ByteWriter} bw Writer to use.
+ * @param {Object} tag Tag, which value to write.
+ */
+ExifEncoder.writeValue = function(bw, tag) {
+ if (tag.format == 2) { // String
+ if (tag.componentCount != tag.value.length) {
+ throw new Error(
+ 'String size mismatch for 0x' + Number(tag.id).toString(16));
+ }
+ bw.writeString(tag.value);
+ } else { // Scalar or rational
+ var width = ExifEncoder.getComponentWidth(tag);
+
+ var writeComponent = function(value, signed) {
+ if (width == 8) {
+ bw.writeScalar(value[0], 4, signed);
+ bw.writeScalar(value[1], 4, signed);
+ } else {
+ bw.writeScalar(value, width, signed);
+ }
+ };
+
+ var signed = (tag.format == 9 || tag.format == 10);
+ if (tag.componentCount == 1) {
+ writeComponent(tag.value, signed);
+ } else {
+ for (var i = 0; i != tag.componentCount; i++) {
+ writeComponent(tag.value[i], signed);
+ }
+ }
+ }
+};
+
+/**
+ * @param {{Object.<number,Object>}} directory EXIF directory.
+ * @param {number} id Tag id.
+ * @param {number} format Tag format
+ * (used in {@link ExifEncoder#getComponentWidth}).
+ * @param {number} componentCount Number of components in this tag.
+ * @return {{id:number, format:number, componentCount:number}}
+ * Tag found or created.
+ */
+ExifEncoder.findOrCreateTag = function(directory, id, format, componentCount) {
+ if (!(id in directory)) {
+ directory[id] = {
+ id: id,
+ format: format || 3, // Short
+ componentCount: componentCount || 1
+ };
+ }
+ return directory[id];
+};
+
+/**
+ * ByteWriter class.
+ * @param {ArrayBuffer} arrayBuffer Underlying buffer to use.
+ * @param {number} offset Offset at which to start writing.
+ * @param {number} length Maximum length to use.
+ * @class
+ * @constructor
+ */
+function ByteWriter(arrayBuffer, offset, length) {
+ length = length || (arrayBuffer.byteLength - offset);
+ this.view_ = new DataView(arrayBuffer, offset, length);
+ this.littleEndian_ = false;
+ this.pos_ = 0;
+ this.forwards_ = {};
+}
+
+/**
+ * Little endian byte order.
+ * @type {number}
+ */
+ByteWriter.LITTLE_ENDIAN = 0;
+
+/**
+ * Bug endian byte order.
+ * @type {number}
+ */
+ByteWriter.BIG_ENDIAN = 1;
+
+/**
+ * Set the byte ordering for future writes.
+ * @param {number} order ByteOrder to use {ByteWriter.LITTLE_ENDIAN}
+ * or {ByteWriter.BIG_ENDIAN}.
+ */
+ByteWriter.prototype.setByteOrder = function(order) {
+ this.littleEndian_ = (order == ByteWriter.LITTLE_ENDIAN);
+};
+
+/**
+ * @return {number} the current write position.
+ */
+ByteWriter.prototype.tell = function() { return this.pos_ };
+
+/**
+ * Skips desired amount of bytes in output stream.
+ * @param {number} count Byte count to skip.
+ */
+ByteWriter.prototype.skip = function(count) {
+ this.validateWrite(count);
+ this.pos_ += count;
+};
+
+/**
+ * Check if the buffer has enough room to read 'width' bytes. Throws an error
+ * if it has not.
+ * @param {number} width Amount of bytes to check.
+ */
+ByteWriter.prototype.validateWrite = function(width) {
+ if (this.pos_ + width > this.view_.byteLength)
+ throw new Error('Writing past the end of the buffer');
+};
+
+/**
+ * Writes scalar value to output stream.
+ * @param {number} value Value to write.
+ * @param {number} width Desired width of written value.
+ * @param {boolean=} opt_signed True if value represents signed number.
+ */
+ByteWriter.prototype.writeScalar = function(value, width, opt_signed) {
+ var method;
+// The below switch is so verbose for two reasons:
+// 1. V8 is faster on method names which are 'symbols'.
+// 2. Method names are discoverable by full text search.
+ switch (width) {
+ case 1:
+ method = opt_signed ? 'setInt8' : 'setUint8';
+ break;
+
+ case 2:
+ method = opt_signed ? 'setInt16' : 'setUint16';
+ break;
+
+ case 4:
+ method = opt_signed ? 'setInt32' : 'setUint32';
+ break;
+
+ case 8:
+ method = opt_signed ? 'setInt64' : 'setUint64';
+ break;
+
+ default:
+ throw new Error('Invalid width: ' + width);
+ break;
+ }
+
+ this.validateWrite(width);
+ this.view_[method](this.pos_, value, this.littleEndian_);
+ this.pos_ += width;
+};
+
+/**
+ * Writes string.
+ * @param {string} str String to write.
+ */
+ByteWriter.prototype.writeString = function(str) {
+ this.validateWrite(str.length);
+ for (var i = 0; i != str.length; i++) {
+ this.view_.setUint8(this.pos_++, str.charCodeAt(i));
+ }
+};
+
+/**
+ * Allocate the space for 'width' bytes for the value that will be set later.
+ * To be followed by a 'resolve' call with the same key.
+ * @param {string} key A key to identify the value.
+ * @param {number} width Width of the value in bytes.
+ */
+ByteWriter.prototype.forward = function(key, width) {
+ if (key in this.forwards_)
+ throw new Error('Duplicate forward key ' + key);
+ this.validateWrite(width);
+ this.forwards_[key] = {
+ pos: this.pos_,
+ width: width
+ };
+ this.pos_ += width;
+};
+
+/**
+ * Set the value previously allocated with a 'forward' call.
+ * @param {string} key A key to identify the value.
+ * @param {number} value value to write in pre-allocated space.
+ */
+ByteWriter.prototype.resolve = function(key, value) {
+ if (!(key in this.forwards_))
+ throw new Error('Undeclared forward key ' + key.toString(16));
+ var forward = this.forwards_[key];
+ var curPos = this.pos_;
+ this.pos_ = forward.pos;
+ this.writeScalar(value, forward.width);
+ this.pos_ = curPos;
+ delete this.forwards_[key];
+};
+
+/**
+ * A shortcut to resolve the value to the current write position.
+ * @param {string} key A key to identify pre-allocated position.
+ */
+ByteWriter.prototype.resolveOffset = function(key) {
+ this.resolve(key, this.tell());
+};
+
+/**
+ * Check if every forward has been resolved, throw and error if not.
+ */
+ByteWriter.prototype.checkResolved = function() {
+ for (var key in this.forwards_) {
+ throw new Error('Unresolved forward pointer ' + key.toString(16));
+ }
+};
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/filter.js b/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/filter.js
new file mode 100644
index 00000000000..e06d4ef2dd8
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/filter.js
@@ -0,0 +1,612 @@
+// 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.
+
+'use strict';
+
+/**
+ * A namespace for image filter utilities.
+ */
+var filter = {};
+
+/**
+ * Create a filter from name and options.
+ *
+ * @param {string} name Maps to a filter method name.
+ * @param {Object} options A map of filter-specific options.
+ * @return {function(ImageData,ImageData,number,number)} created function.
+ */
+filter.create = function(name, options) {
+ var filterFunc = filter[name](options);
+ return function() {
+ var time = Date.now();
+ filterFunc.apply(null, arguments);
+ var dst = arguments[0];
+ var mPixPerSec = dst.width * dst.height / 1000 / (Date.now() - time);
+ ImageUtil.trace.report(name, Math.round(mPixPerSec * 10) / 10 + 'Mps');
+ }
+};
+
+/**
+ * Apply a filter to a image by splitting it into strips.
+ *
+ * To be used with large images to avoid freezing up the UI.
+ *
+ * @param {HTMLCanvasElement} dstCanvas Destination canvas.
+ * @param {HTMLCanvasElement} srcCanvas Source canvas.
+ * @param {function(ImageData,ImageData,number,number)} filterFunc Filter.
+ * @param {function(number, number)} progressCallback Progress callback.
+ * @param {number} maxPixelsPerStrip Pixel number to process at once.
+ */
+filter.applyByStrips = function(
+ dstCanvas, srcCanvas, filterFunc, progressCallback, maxPixelsPerStrip) {
+ var dstContext = dstCanvas.getContext('2d');
+ var srcContext = srcCanvas.getContext('2d');
+ var source = srcContext.getImageData(0, 0, srcCanvas.width, srcCanvas.height);
+
+ var stripCount = Math.ceil(srcCanvas.width * srcCanvas.height /
+ (maxPixelsPerStrip || 1000000)); // 1 Mpix is a reasonable default.
+
+ var strip = srcContext.getImageData(0, 0,
+ srcCanvas.width, Math.ceil(srcCanvas.height / stripCount));
+
+ var offset = 0;
+
+ function filterStrip() {
+ // If the strip overlaps the bottom of the source image we cannot shrink it
+ // and we cannot fill it partially (since canvas.putImageData always draws
+ // the entire buffer).
+ // Instead we move the strip up several lines (converting those lines
+ // twice is a small price to pay).
+ if (offset > source.height - strip.height) {
+ offset = source.height - strip.height;
+ }
+
+ filterFunc(strip, source, 0, offset);
+ dstContext.putImageData(strip, 0, offset);
+
+ offset += strip.height;
+
+ if (offset < source.height) {
+ setTimeout(filterStrip, 0);
+ } else {
+ ImageUtil.trace.reportTimer('filter-commit');
+ }
+
+ progressCallback(offset, source.height);
+ }
+
+ ImageUtil.trace.resetTimer('filter-commit');
+ filterStrip();
+};
+
+/**
+ * Return a color histogram for an image.
+ *
+ * @param {HTMLCanvasElement|ImageData} source Image data to analyze.
+ * @return {{r: Array.<number>, g: Array.<number>, b: Array.<number>}}
+ * histogram.
+ */
+filter.getHistogram = function(source) {
+ var imageData;
+ if (source.constructor.name == 'HTMLCanvasElement') {
+ imageData = source.getContext('2d').
+ getImageData(0, 0, source.width, source.height);
+ } else {
+ imageData = source;
+ }
+
+ var r = [];
+ var g = [];
+ var b = [];
+
+ for (var i = 0; i != 256; i++) {
+ r.push(0);
+ g.push(0);
+ b.push(0);
+ }
+
+ var data = imageData.data;
+ var maxIndex = 4 * imageData.width * imageData.height;
+ for (var index = 0; index != maxIndex;) {
+ r[data[index++]]++;
+ g[data[index++]]++;
+ b[data[index++]]++;
+ index++;
+ }
+
+ return { r: r, g: g, b: b };
+};
+
+/**
+ * Compute the function for every integer value from 0 up to maxArg.
+ *
+ * Rounds and clips the results to fit the [0..255] range.
+ * Useful to speed up pixel manipulations.
+ *
+ * @param {number} maxArg Maximum argument value (inclusive).
+ * @param {function(number): number} func Function to precompute.
+ * @return {Uint8Array} Computed results.
+ */
+filter.precompute = function(maxArg, func) {
+ var results = new Uint8Array(maxArg + 1);
+ for (var arg = 0; arg <= maxArg; arg++) {
+ results[arg] = Math.max(0, Math.min(0xFF, Math.round(func(arg))));
+ }
+ return results;
+};
+
+/**
+ * Convert pixels by applying conversion tables to each channel individually.
+ *
+ * @param {Array.<number>} rMap Red channel conversion table.
+ * @param {Array.<number>} gMap Green channel conversion table.
+ * @param {Array.<number>} bMap Blue channel conversion table.
+ * @param {ImageData} dst Destination image data. Can be smaller than the
+ * source, must completely fit inside the source.
+ * @param {ImageData} src Source image data.
+ * @param {number} offsetX Horizontal offset of dst relative to src.
+ * @param {number} offsetY Vertical offset of dst relative to src.
+ */
+filter.mapPixels = function(rMap, gMap, bMap, dst, src, offsetX, offsetY) {
+ var dstData = dst.data;
+ var dstWidth = dst.width;
+ var dstHeight = dst.height;
+
+ var srcData = src.data;
+ var srcWidth = src.width;
+ var srcHeight = src.height;
+
+ if (offsetX < 0 || offsetX + dstWidth > srcWidth ||
+ offsetY < 0 || offsetY + dstHeight > srcHeight)
+ throw new Error('Invalid offset');
+
+ var dstIndex = 0;
+ for (var y = 0; y != dstHeight; y++) {
+ var srcIndex = (offsetX + (offsetY + y) * srcWidth) * 4;
+ for (var x = 0; x != dstWidth; x++) {
+ dstData[dstIndex++] = rMap[srcData[srcIndex++]];
+ dstData[dstIndex++] = gMap[srcData[srcIndex++]];
+ dstData[dstIndex++] = bMap[srcData[srcIndex++]];
+ dstIndex++;
+ srcIndex++;
+ }
+ }
+};
+
+/**
+ * Number of digits after period(in binary form) to preserve.
+ * @type {number}
+ */
+filter.FIXED_POINT_SHIFT = 16;
+
+/**
+ * Maximum value that can be represented in fixed point without overflow.
+ * @type {number}
+ */
+filter.MAX_FLOAT_VALUE = 0x7FFFFFFF >> filter.FIXED_POINT_SHIFT;
+
+/**
+ * Converts floating point to fixed.
+ * @param {number} x Number to convert.
+ * @return {number} Converted number.
+ */
+filter.floatToFixedPoint = function(x) {
+ // Math.round on negative arguments causes V8 to deoptimize the calling
+ // function, so we are using >> 0 instead.
+ return (x * (1 << filter.FIXED_POINT_SHIFT)) >> 0;
+};
+
+/**
+ * Perform an image convolution with a symmetrical 5x5 matrix:
+ *
+ * 0 0 w3 0 0
+ * 0 w2 w1 w2 0
+ * w3 w1 w0 w1 w3
+ * 0 w2 w1 w2 0
+ * 0 0 w3 0 0
+ *
+ * @param {Array.<number>} weights See the picture above.
+ * @param {ImageData} dst Destination image data. Can be smaller than the
+ * source, must completely fit inside the source.
+ * @param {ImageData} src Source image data.
+ * @param {number} offsetX Horizontal offset of dst relative to src.
+ * @param {number} offsetY Vertical offset of dst relative to src.
+ */
+filter.convolve5x5 = function(weights, dst, src, offsetX, offsetY) {
+ var w0 = filter.floatToFixedPoint(weights[0]);
+ var w1 = filter.floatToFixedPoint(weights[1]);
+ var w2 = filter.floatToFixedPoint(weights[2]);
+ var w3 = filter.floatToFixedPoint(weights[3]);
+
+ var dstData = dst.data;
+ var dstWidth = dst.width;
+ var dstHeight = dst.height;
+ var dstStride = dstWidth * 4;
+
+ var srcData = src.data;
+ var srcWidth = src.width;
+ var srcHeight = src.height;
+ var srcStride = srcWidth * 4;
+ var srcStride2 = srcStride * 2;
+
+ if (offsetX < 0 || offsetX + dstWidth > srcWidth ||
+ offsetY < 0 || offsetY + dstHeight > srcHeight)
+ throw new Error('Invalid offset');
+
+ // Javascript is not very good at inlining constants.
+ // We inline manually and assert that the constant is equal to the variable.
+ if (filter.FIXED_POINT_SHIFT != 16)
+ throw new Error('Wrong fixed point shift');
+
+ var margin = 2;
+
+ var startX = Math.max(0, margin - offsetX);
+ var endX = Math.min(dstWidth, srcWidth - margin - offsetX);
+
+ var startY = Math.max(0, margin - offsetY);
+ var endY = Math.min(dstHeight, srcHeight - margin - offsetY);
+
+ for (var y = startY; y != endY; y++) {
+ var dstIndex = y * dstStride + startX * 4;
+ var srcIndex = (y + offsetY) * srcStride + (startX + offsetX) * 4;
+
+ for (var x = startX; x != endX; x++) {
+ for (var c = 0; c != 3; c++) {
+ var sum = w0 * srcData[srcIndex] +
+ w1 * (srcData[srcIndex - 4] +
+ srcData[srcIndex + 4] +
+ srcData[srcIndex - srcStride] +
+ srcData[srcIndex + srcStride]) +
+ w2 * (srcData[srcIndex - srcStride - 4] +
+ srcData[srcIndex + srcStride - 4] +
+ srcData[srcIndex - srcStride + 4] +
+ srcData[srcIndex + srcStride + 4]) +
+ w3 * (srcData[srcIndex - 8] +
+ srcData[srcIndex + 8] +
+ srcData[srcIndex - srcStride2] +
+ srcData[srcIndex + srcStride2]);
+ if (sum < 0)
+ dstData[dstIndex++] = 0;
+ else if (sum > 0xFF0000)
+ dstData[dstIndex++] = 0xFF;
+ else
+ dstData[dstIndex++] = sum >> 16;
+ srcIndex++;
+ }
+ srcIndex++;
+ dstIndex++;
+ }
+ }
+};
+
+/**
+ * Compute the average color for the image.
+ *
+ * @param {ImageData} imageData Image data to analyze.
+ * @return {{r: number, g: number, b: number}} average color.
+ */
+filter.getAverageColor = function(imageData) {
+ var data = imageData.data;
+ var width = imageData.width;
+ var height = imageData.height;
+
+ var total = 0;
+ var r = 0;
+ var g = 0;
+ var b = 0;
+
+ var maxIndex = 4 * width * height;
+ for (var i = 0; i != maxIndex;) {
+ total++;
+ r += data[i++];
+ g += data[i++];
+ b += data[i++];
+ i++;
+ }
+ if (total == 0) return { r: 0, g: 0, b: 0 };
+ return { r: r / total, g: g / total, b: b / total };
+};
+
+/**
+ * Compute the average color with more weight given to pixes at the center.
+ *
+ * @param {ImageData} imageData Image data to analyze.
+ * @return {{r: number, g: number, b: number}} weighted average color.
+ */
+filter.getWeightedAverageColor = function(imageData) {
+ var data = imageData.data;
+ var width = imageData.width;
+ var height = imageData.height;
+
+ var total = 0;
+ var r = 0;
+ var g = 0;
+ var b = 0;
+
+ var center = Math.floor(width / 2);
+ var maxDist = center * Math.sqrt(2);
+ maxDist *= 2; // Weaken the effect of distance
+
+ var i = 0;
+ for (var x = 0; x != width; x++) {
+ for (var y = 0; y != height; y++) {
+ var dist = Math.sqrt(
+ (x - center) * (x - center) + (y - center) * (y - center));
+ var weight = (maxDist - dist) / maxDist;
+
+ total += weight;
+ r += data[i++] * weight;
+ g += data[i++] * weight;
+ b += data[i++] * weight;
+ i++;
+ }
+ }
+ if (total == 0) return { r: 0, g: 0, b: 0 };
+ return { r: r / total, g: g / total, b: b / total };
+};
+
+/**
+ * Copy part of src image to dst, applying matrix color filter on-the-fly.
+ *
+ * The copied part of src should completely fit into dst (there is no clipping
+ * on either side).
+ *
+ * @param {Array.<number>} matrix 3x3 color matrix.
+ * @param {ImageData} dst Destination image data.
+ * @param {ImageData} src Source image data.
+ * @param {number} offsetX X offset in source to start processing.
+ * @param {number} offsetY Y offset in source to start processing.
+ */
+filter.colorMatrix3x3 = function(matrix, dst, src, offsetX, offsetY) {
+ var c11 = filter.floatToFixedPoint(matrix[0]);
+ var c12 = filter.floatToFixedPoint(matrix[1]);
+ var c13 = filter.floatToFixedPoint(matrix[2]);
+ var c21 = filter.floatToFixedPoint(matrix[3]);
+ var c22 = filter.floatToFixedPoint(matrix[4]);
+ var c23 = filter.floatToFixedPoint(matrix[5]);
+ var c31 = filter.floatToFixedPoint(matrix[6]);
+ var c32 = filter.floatToFixedPoint(matrix[7]);
+ var c33 = filter.floatToFixedPoint(matrix[8]);
+
+ var dstData = dst.data;
+ var dstWidth = dst.width;
+ var dstHeight = dst.height;
+
+ var srcData = src.data;
+ var srcWidth = src.width;
+ var srcHeight = src.height;
+
+ if (offsetX < 0 || offsetX + dstWidth > srcWidth ||
+ offsetY < 0 || offsetY + dstHeight > srcHeight)
+ throw new Error('Invalid offset');
+
+ // Javascript is not very good at inlining constants.
+ // We inline manually and assert that the constant is equal to the variable.
+ if (filter.FIXED_POINT_SHIFT != 16)
+ throw new Error('Wrong fixed point shift');
+
+ var dstIndex = 0;
+ for (var y = 0; y != dstHeight; y++) {
+ var srcIndex = (offsetX + (offsetY + y) * srcWidth) * 4;
+ for (var x = 0; x != dstWidth; x++) {
+ var r = srcData[srcIndex++];
+ var g = srcData[srcIndex++];
+ var b = srcData[srcIndex++];
+ srcIndex++;
+
+ var rNew = r * c11 + g * c12 + b * c13;
+ var gNew = r * c21 + g * c22 + b * c23;
+ var bNew = r * c31 + g * c32 + b * c33;
+
+ if (rNew < 0) {
+ dstData[dstIndex++] = 0;
+ } else if (rNew > 0xFF0000) {
+ dstData[dstIndex++] = 0xFF;
+ } else {
+ dstData[dstIndex++] = rNew >> 16;
+ }
+
+ if (gNew < 0) {
+ dstData[dstIndex++] = 0;
+ } else if (gNew > 0xFF0000) {
+ dstData[dstIndex++] = 0xFF;
+ } else {
+ dstData[dstIndex++] = gNew >> 16;
+ }
+
+ if (bNew < 0) {
+ dstData[dstIndex++] = 0;
+ } else if (bNew > 0xFF0000) {
+ dstData[dstIndex++] = 0xFF;
+ } else {
+ dstData[dstIndex++] = bNew >> 16;
+ }
+
+ dstIndex++;
+ }
+ }
+};
+
+/**
+ * Return a convolution filter function bound to specific weights.
+ *
+ * @param {Array.<number>} weights Weights for the convolution matrix
+ * (not normalized).
+ * @return {function(ImageData,ImageData,number,number)} Convolution filter.
+ */
+filter.createConvolutionFilter = function(weights) {
+ // Normalize the weights to sum to 1.
+ var total = 0;
+ for (var i = 0; i != weights.length; i++) {
+ total += weights[i] * (i ? 4 : 1);
+ }
+
+ var normalized = [];
+ for (i = 0; i != weights.length; i++) {
+ normalized.push(weights[i] / total);
+ }
+ for (; i < 4; i++) {
+ normalized.push(0);
+ }
+
+ var maxWeightedSum = 0xFF *
+ Math.abs(normalized[0]) +
+ Math.abs(normalized[1]) * 4 +
+ Math.abs(normalized[2]) * 4 +
+ Math.abs(normalized[3]) * 4;
+ if (maxWeightedSum > filter.MAX_FLOAT_VALUE)
+ throw new Error('convolve5x5 cannot convert the weights to fixed point');
+
+ return filter.convolve5x5.bind(null, normalized);
+};
+
+/**
+ * Creates matrix filter.
+ * @param {Array.<number>} matrix Color transformation matrix.
+ * @return {function(ImageData,ImageData,number,number)} Matrix filter.
+ */
+filter.createColorMatrixFilter = function(matrix) {
+ for (var r = 0; r != 3; r++) {
+ var maxRowSum = 0;
+ for (var c = 0; c != 3; c++) {
+ maxRowSum += 0xFF * Math.abs(matrix[r * 3 + c]);
+ }
+ if (maxRowSum > filter.MAX_FLOAT_VALUE)
+ throw new Error(
+ 'colorMatrix3x3 cannot convert the matrix to fixed point');
+ }
+ return filter.colorMatrix3x3.bind(null, matrix);
+};
+
+/**
+ * Return a blur filter.
+ * @param {Object} options Blur options.
+ * @return {function(ImageData,ImageData,number,number)} Blur filter.
+ */
+filter.blur = function(options) {
+ if (options.radius == 1)
+ return filter.createConvolutionFilter(
+ [1, options.strength]);
+ else if (options.radius == 2)
+ return filter.createConvolutionFilter(
+ [1, options.strength, options.strength]);
+ else
+ return filter.createConvolutionFilter(
+ [1, options.strength, options.strength, options.strength]);
+};
+
+/**
+ * Return a sharpen filter.
+ * @param {Object} options Sharpen options.
+ * @return {function(ImageData,ImageData,number,number)} Sharpen filter.
+ */
+filter.sharpen = function(options) {
+ if (options.radius == 1)
+ return filter.createConvolutionFilter(
+ [5, -options.strength]);
+ else if (options.radius == 2)
+ return filter.createConvolutionFilter(
+ [10, -options.strength, -options.strength]);
+ else
+ return filter.createConvolutionFilter(
+ [15, -options.strength, -options.strength, -options.strength]);
+};
+
+/**
+ * Return an exposure filter.
+ * @param {Object} options exposure options.
+ * @return {function(ImageData,ImageData,number,number)} Exposure filter.
+ */
+filter.exposure = function(options) {
+ var pixelMap = filter.precompute(
+ 255,
+ function(value) {
+ if (options.brightness > 0) {
+ value *= (1 + options.brightness);
+ } else {
+ value += (0xFF - value) * options.brightness;
+ }
+ return 0x80 +
+ (value - 0x80) * Math.tan((options.contrast + 1) * Math.PI / 4);
+ });
+
+ return filter.mapPixels.bind(null, pixelMap, pixelMap, pixelMap);
+};
+
+/**
+ * Return a color autofix filter.
+ * @param {Object} options Histogram for autofix.
+ * @return {function(ImageData,ImageData,number,number)} Autofix filter.
+ */
+filter.autofix = function(options) {
+ return filter.mapPixels.bind(null,
+ filter.autofix.stretchColors(options.histogram.r),
+ filter.autofix.stretchColors(options.histogram.g),
+ filter.autofix.stretchColors(options.histogram.b));
+};
+
+/**
+ * Return a conversion table that stretches the range of colors used
+ * in the image to 0..255.
+ * @param {Array.<number>} channelHistogram Histogram to calculate range.
+ * @return {Uint8Array} Color mapping array.
+ */
+filter.autofix.stretchColors = function(channelHistogram) {
+ var range = filter.autofix.getRange(channelHistogram);
+ return filter.precompute(
+ 255,
+ function(x) {
+ return (x - range.first) / (range.last - range.first) * 255;
+ }
+ );
+};
+
+/**
+ * Return a range that encloses non-zero elements values in a histogram array.
+ * @param {Array.<number>} channelHistogram Histogram to analyze.
+ * @return {{first: number, last: number}} Channel range in histogram.
+ */
+filter.autofix.getRange = function(channelHistogram) {
+ var first = 0;
+ while (first < channelHistogram.length && channelHistogram[first] == 0)
+ first++;
+
+ var last = channelHistogram.length - 1;
+ while (last >= 0 && channelHistogram[last] == 0)
+ last--;
+
+ if (first >= last) // Stretching does not make sense
+ return {first: 0, last: channelHistogram.length - 1};
+ else
+ return {first: first, last: last};
+};
+
+/**
+ * Minimum channel offset that makes visual difference. If autofix calculated
+ * offset is less than SENSITIVITY, probably autofix is not needed.
+ * Reasonable empirical value.
+ * @type {number}
+ */
+filter.autofix.SENSITIVITY = 8;
+
+/**
+ * @param {Array.<number>} channelHistogram Histogram to analyze.
+ * @return {boolean} True if stretching this range to 0..255 would make
+ * a visible difference.
+ */
+filter.autofix.needsStretching = function(channelHistogram) {
+ var range = filter.autofix.getRange(channelHistogram);
+ return (range.first >= filter.autofix.SENSITIVITY ||
+ range.last <= 255 - filter.autofix.SENSITIVITY);
+};
+
+/**
+ * @param {{r: Array.<number>, g: Array.<number>, b: Array.<number>}} histogram
+ * @return {boolean} True if the autofix would make a visible difference.
+ */
+filter.autofix.isApplicable = function(histogram) {
+ return filter.autofix.needsStretching(histogram.r) ||
+ filter.autofix.needsStretching(histogram.g) ||
+ filter.autofix.needsStretching(histogram.b);
+};
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/image_adjust.js b/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/image_adjust.js
new file mode 100644
index 00000000000..2abb10ed2f5
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/image_adjust.js
@@ -0,0 +1,248 @@
+// 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.
+
+'use strict';
+
+/**
+ * The base class for simple filters that only modify the image content
+ * but do not modify the image dimensions.
+ * @constructor
+ * @extends ImageEditor.Mode
+ */
+ImageEditor.Mode.Adjust = function() {
+ ImageEditor.Mode.apply(this, arguments);
+ this.implicitCommit = true;
+ this.doneMessage_ = null;
+ this.viewportGeneration_ = 0;
+};
+
+ImageEditor.Mode.Adjust.prototype = {__proto__: ImageEditor.Mode.prototype};
+
+/** @override */
+ImageEditor.Mode.Adjust.prototype.getCommand = function() {
+ if (!this.filter_) return null;
+
+ return new Command.Filter(this.name, this.filter_, this.doneMessage_);
+};
+
+/** @override */
+ImageEditor.Mode.Adjust.prototype.cleanUpUI = function() {
+ ImageEditor.Mode.prototype.cleanUpUI.apply(this, arguments);
+ this.hidePreview();
+};
+
+/**
+ * TODO(JSDOC)
+ */
+ImageEditor.Mode.Adjust.prototype.hidePreview = function() {
+ if (this.canvas_) {
+ this.canvas_.parentNode.removeChild(this.canvas_);
+ this.canvas_ = null;
+ }
+};
+
+/**
+ * TODO(JSDOC)
+ */
+ImageEditor.Mode.Adjust.prototype.cleanUpCaches = function() {
+ this.filter_ = null;
+ this.previewImageData_ = null;
+};
+
+/**
+ * TODO(JSDOC)
+ */
+ImageEditor.Mode.Adjust.prototype.reset = function() {
+ ImageEditor.Mode.prototype.reset.call(this);
+ this.hidePreview();
+ this.cleanUpCaches();
+};
+
+/**
+ * TODO(JSDOC)
+ * @param {Object} options // TODO(JSDOC).
+ */
+ImageEditor.Mode.Adjust.prototype.update = function(options) {
+ ImageEditor.Mode.prototype.update.apply(this, arguments);
+
+ // We assume filter names are used in the UI directly.
+ // This will have to change with i18n.
+ this.filter_ = this.createFilter(options);
+ this.updatePreviewImage();
+ ImageUtil.trace.resetTimer('preview');
+ this.filter_(this.previewImageData_, this.originalImageData, 0, 0);
+ ImageUtil.trace.reportTimer('preview');
+ this.canvas_.getContext('2d').putImageData(
+ this.previewImageData_, 0, 0);
+};
+
+/**
+ * Copy the source image data for the preview.
+ * Use the cached copy if the viewport has not changed.
+ */
+ImageEditor.Mode.Adjust.prototype.updatePreviewImage = function() {
+ if (!this.previewImageData_ ||
+ this.viewportGeneration_ != this.getViewport().getCacheGeneration()) {
+ this.viewportGeneration_ = this.getViewport().getCacheGeneration();
+
+ if (!this.canvas_) {
+ this.canvas_ = this.getImageView().createOverlayCanvas();
+ }
+
+ this.getImageView().setupDeviceBuffer(this.canvas_);
+
+ this.originalImageData = this.getImageView().copyScreenImageData();
+ this.previewImageData_ = this.getImageView().copyScreenImageData();
+ }
+};
+
+/*
+ * Own methods
+ */
+
+/**
+ * TODO(JSDOC)
+ * @param {Object} options // TODO(JSDOC).
+ * @return {function(ImageData,ImageData,number,number)} Created function.
+ */
+ImageEditor.Mode.Adjust.prototype.createFilter = function(options) {
+ return filter.create(this.name, options);
+};
+
+/**
+ * A base class for color filters that are scale independent.
+ * @constructor
+ */
+ImageEditor.Mode.ColorFilter = function() {
+ ImageEditor.Mode.Adjust.apply(this, arguments);
+};
+
+ImageEditor.Mode.ColorFilter.prototype =
+ {__proto__: ImageEditor.Mode.Adjust.prototype};
+
+/**
+ * TODO(JSDOC)
+ * @return {{r: Array.<number>, g: Array.<number>, b: Array.<number>}}
+ * histogram.
+ */
+ImageEditor.Mode.ColorFilter.prototype.getHistogram = function() {
+ return filter.getHistogram(this.getImageView().getThumbnail());
+};
+
+/**
+ * Exposure/contrast filter.
+ * @constructor
+ */
+ImageEditor.Mode.Exposure = function() {
+ ImageEditor.Mode.ColorFilter.call(this, 'exposure', 'GALLERY_EXPOSURE');
+};
+
+ImageEditor.Mode.Exposure.prototype =
+ {__proto__: ImageEditor.Mode.ColorFilter.prototype};
+
+/**
+ * TODO(JSDOC)
+ * @param {ImageEditor.Toolbar} toolbar The toolbar to populate.
+ */
+ImageEditor.Mode.Exposure.prototype.createTools = function(toolbar) {
+ toolbar.addRange('brightness', 'GALLERY_BRIGHTNESS', -1, 0, 1, 100);
+ toolbar.addRange('contrast', 'GALLERY_CONTRAST', -1, 0, 1, 100);
+};
+
+/**
+ * Autofix.
+ * @constructor
+ */
+ImageEditor.Mode.Autofix = function() {
+ ImageEditor.Mode.ColorFilter.call(this, 'autofix', 'GALLERY_AUTOFIX');
+ this.doneMessage_ = 'GALLERY_FIXED';
+};
+
+ImageEditor.Mode.Autofix.prototype =
+ {__proto__: ImageEditor.Mode.ColorFilter.prototype};
+
+/**
+ * TODO(JSDOC)
+ * @param {ImageEditor.Toolbar} toolbar The toolbar to populate.
+ */
+ImageEditor.Mode.Autofix.prototype.createTools = function(toolbar) {
+ var self = this;
+ toolbar.addButton('Apply', this.apply.bind(this));
+};
+
+/**
+ * TODO(JSDOC)
+ * @return {boolean} // TODO(JSDOC).
+ */
+ImageEditor.Mode.Autofix.prototype.isApplicable = function() {
+ return this.getImageView().hasValidImage() &&
+ filter.autofix.isApplicable(this.getHistogram());
+};
+
+/**
+ * TODO(JSDOC)
+ */
+ImageEditor.Mode.Autofix.prototype.apply = function() {
+ this.update({histogram: this.getHistogram()});
+};
+
+/**
+ * Instant Autofix.
+ * @constructor
+ */
+ImageEditor.Mode.InstantAutofix = function() {
+ ImageEditor.Mode.Autofix.apply(this, arguments);
+ this.instant = true;
+};
+
+ImageEditor.Mode.InstantAutofix.prototype =
+ {__proto__: ImageEditor.Mode.Autofix.prototype};
+
+/**
+ * TODO(JSDOC)
+ */
+ImageEditor.Mode.InstantAutofix.prototype.setUp = function() {
+ ImageEditor.Mode.Autofix.prototype.setUp.apply(this, arguments);
+ this.apply();
+};
+
+/**
+ * Blur filter.
+ * @constructor
+ */
+ImageEditor.Mode.Blur = function() {
+ ImageEditor.Mode.Adjust.call(this, 'blur');
+};
+
+ImageEditor.Mode.Blur.prototype =
+ {__proto__: ImageEditor.Mode.Adjust.prototype};
+
+/**
+ * TODO(JSDOC)
+ * @param {ImageEditor.Toolbar} toolbar The toolbar to populate.
+ */
+ImageEditor.Mode.Blur.prototype.createTools = function(toolbar) {
+ toolbar.addRange('strength', 'GALLERY_STRENGTH', 0, 0, 1, 100);
+ toolbar.addRange('radius', 'GALLERY_RADIUS', 1, 1, 3);
+};
+
+/**
+ * Sharpen filter.
+ * @constructor
+ */
+ImageEditor.Mode.Sharpen = function() {
+ ImageEditor.Mode.Adjust.call(this, 'sharpen');
+};
+
+ImageEditor.Mode.Sharpen.prototype =
+ {__proto__: ImageEditor.Mode.Adjust.prototype};
+
+/**
+ * TODO(JSDOC)
+ * @param {ImageEditor.Toolbar} toolbar The toolbar to populate.
+ */
+ImageEditor.Mode.Sharpen.prototype.createTools = function(toolbar) {
+ toolbar.addRange('strength', 'GALLERY_STRENGTH', 0, 0, 1, 100);
+ toolbar.addRange('radius', 'GALLERY_RADIUS', 1, 1, 3);
+};
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/image_buffer.js b/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/image_buffer.js
new file mode 100644
index 00000000000..8e894a8a0f2
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/image_buffer.js
@@ -0,0 +1,184 @@
+// 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.
+
+'use strict';
+
+/**
+ * A stack of overlays that display itself and handle mouse events.
+ * TODO(kaznacheev) Consider disbanding this class and moving
+ * the functionality to individual objects that display anything or handle
+ * mouse events.
+ * @constructor
+ */
+function ImageBuffer() {
+ this.overlays_ = [];
+}
+
+/**
+ * TODO(JSDOC).
+ * @param {ImageBuffer.Overlay} overlay // TODO(JSDOC).
+ */
+ImageBuffer.prototype.addOverlay = function(overlay) {
+ var zIndex = overlay.getZIndex();
+ // Store the overlays in the ascending Z-order.
+ var i;
+ for (i = 0; i != this.overlays_.length; i++) {
+ if (zIndex < this.overlays_[i].getZIndex()) break;
+ }
+ this.overlays_.splice(i, 0, overlay);
+};
+
+/**
+ * TODO(JSDOC).
+ * @param {ImageBuffer.Overlay} overlay // TODO(JSDOC).
+ */
+ImageBuffer.prototype.removeOverlay = function(overlay) {
+ for (var i = 0; i != this.overlays_.length; i++) {
+ if (this.overlays_[i] == overlay) {
+ this.overlays_.splice(i, 1);
+ return;
+ }
+ }
+ throw new Error('Cannot remove overlay ' + overlay);
+};
+
+/**
+ * Draws overlays in the ascending Z-order.
+ */
+ImageBuffer.prototype.draw = function() {
+ for (var i = 0; i != this.overlays_.length; i++) {
+ this.overlays_[i].draw();
+ }
+};
+
+/**
+ * Searches for a cursor style in the descending Z-order.
+ * @param {number} x X coordinate for cursor.
+ * @param {number} y Y coordinate for cursor.
+ * @param {boolean} mouseDown If mouse button is down.
+ * @return {string} A value for style.cursor CSS property.
+ */
+ImageBuffer.prototype.getCursorStyle = function(x, y, mouseDown) {
+ for (var i = this.overlays_.length - 1; i >= 0; i--) {
+ var style = this.overlays_[i].getCursorStyle(x, y, mouseDown);
+ if (style) return style;
+ }
+ return 'default';
+};
+
+/**
+ * Searches for a click handler in the descending Z-order.
+ * @param {number} x X coordinate for click event.
+ * @param {number} y Y coordinate for click event.
+ * @return {boolean} True if handled.
+ */
+ImageBuffer.prototype.onClick = function(x, y) {
+ for (var i = this.overlays_.length - 1; i >= 0; i--) {
+ if (this.overlays_[i].onClick(x, y)) return true;
+ }
+ return false;
+};
+
+/**
+ * Searches for a drag handler in the descending Z-order.
+ * @param {number} x Event X coordinate.
+ * @param {number} y Event Y coordinate.
+ * @param {boolean} touch True if it's a touch event, false if mouse.
+ * @return {function(number,number)} A function to be called on mouse drag.
+ */
+ImageBuffer.prototype.getDragHandler = function(x, y, touch) {
+ for (var i = this.overlays_.length - 1; i >= 0; i--) {
+ var handler = this.overlays_[i].getDragHandler(x, y, touch);
+ if (handler)
+ return handler;
+ }
+ return null;
+};
+
+/**
+ * Searches for an action for the double tap enumerating
+ * layers in the descending Z-order.
+ * @param {number} x X coordinate of the event.
+ * @param {number} y Y coordinate of the event.
+ * @return {ImageBuffer.DoubleTapAction} Action to perform as result.
+ */
+ImageBuffer.prototype.getDoubleTapAction = function(x, y) {
+ for (var i = this.overlays_.length - 1; i >= 0; i--) {
+ var action = this.overlays_[i].getDoubleTapAction(x, y);
+ if (action != ImageBuffer.DoubleTapAction.NOTHING)
+ return action;
+ }
+ return ImageBuffer.DoubleTapAction.NOTHING;
+};
+
+/**
+ * Possible double tap actions.
+ * @enum
+ */
+ImageBuffer.DoubleTapAction = {
+ NOTHING: 0,
+ COMMIT: 1,
+ CANCEL: 2
+};
+
+/**
+ * ImageBuffer.Overlay is a pluggable extension that modifies the outlook
+ * and the behavior of the ImageBuffer instance.
+ * @class
+ */
+ImageBuffer.Overlay = function() {};
+
+/**
+ * TODO(JSDOC).
+ * @return {number} // TODO(JSDOC).
+ */
+ImageBuffer.Overlay.prototype.getZIndex = function() { return 0 };
+
+/**
+ * TODO(JSDOC).
+ */
+ImageBuffer.Overlay.prototype.draw = function() {};
+
+/**
+ * TODO(JSDOC).
+ * @param {number} x X coordinate for cursor.
+ * @param {number} y Y coordinate for cursor.
+ * @param {boolean} mouseDown If mouse button is down.
+ * @return {?string} A value for style.cursor CSS property or null for
+ * default.
+ */
+ImageBuffer.Overlay.prototype.getCursorStyle = function(x, y, mouseDown) {
+ return null;
+};
+
+/**
+ * TODO(JSDOC).
+ * @param {number} x // TODO(JSDOC).
+ * @param {number} y // TODO(JSDOC).
+ * @return {boolean} // TODO(JSDOC).
+ */
+ImageBuffer.Overlay.prototype.onClick = function(x, y) {
+ return false;
+};
+
+/**
+ * TODO(JSDOC).
+ * @param {number} x Event X coordinate.
+ * @param {number} y Event Y coordinate.
+ * @param {boolean} touch True if it's a touch event, false if mouse.
+ * @return {function(number,number)} A function to be called on mouse drag.
+ */
+ImageBuffer.Overlay.prototype.getDragHandler = function(x, y, touch) {
+ return null;
+};
+
+/**
+ * TODO(JSDOC).
+ * @param {number} x // TODO(JSDOC).
+ * @param {number} y // TODO(JSDOC).
+ * @return {ImageBuffer.DoubleTapAction} // TODO(JSDOC).
+ */
+ImageBuffer.Overlay.prototype.getDoubleTapAction = function(x, y) {
+ return ImageBuffer.DoubleTapAction.NOTHING;
+};
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/image_editor.js b/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/image_editor.js
new file mode 100644
index 00000000000..090155494bd
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/image_editor.js
@@ -0,0 +1,1177 @@
+// 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.
+
+'use strict';
+
+/**
+ * ImageEditor is the top level object that holds together and connects
+ * everything needed for image editing.
+ *
+ * @param {Viewport} viewport The viewport.
+ * @param {ImageView} imageView The ImageView containing the images to edit.
+ * @param {ImageEditor.Prompt} prompt Prompt instance.
+ * @param {Object} DOMContainers Various DOM containers required for the editor.
+ * @param {Array.<ImageEditor.Mode>} modes Available editor modes.
+ * @param {function} displayStringFunction String formatting function.
+ * @param {function()} onToolsVisibilityChanged Callback to be called, when
+ * some of the UI elements have been dimmed or revealed.
+ * @constructor
+ */
+function ImageEditor(
+ viewport, imageView, prompt, DOMContainers, modes, displayStringFunction,
+ onToolsVisibilityChanged) {
+ this.rootContainer_ = DOMContainers.root;
+ this.container_ = DOMContainers.image;
+ this.modes_ = modes;
+ this.displayStringFunction_ = displayStringFunction;
+ this.onToolsVisibilityChanged_ = onToolsVisibilityChanged;
+
+ ImageUtil.removeChildren(this.container_);
+
+ var document = this.container_.ownerDocument;
+
+ this.viewport_ = viewport;
+ this.viewport_.sizeByFrame(this.container_);
+
+ this.buffer_ = new ImageBuffer();
+ this.viewport_.addRepaintCallback(this.buffer_.draw.bind(this.buffer_));
+
+ this.imageView_ = imageView;
+ this.imageView_.addContentCallback(this.onContentUpdate_.bind(this));
+ this.buffer_.addOverlay(this.imageView_);
+
+ this.panControl_ = new ImageEditor.MouseControl(
+ this.rootContainer_, this.container_, this.getBuffer());
+
+ this.panControl_.setDoubleTapCallback(this.onDoubleTap_.bind(this));
+
+ this.mainToolbar_ = new ImageEditor.Toolbar(
+ DOMContainers.toolbar, displayStringFunction);
+
+ this.modeToolbar_ = new ImageEditor.Toolbar(
+ DOMContainers.mode, displayStringFunction,
+ this.onOptionsChange.bind(this));
+
+ this.prompt_ = prompt;
+
+ this.createToolButtons();
+
+ this.commandQueue_ = null;
+}
+
+/**
+ * @return {boolean} True if no user commands are to be accepted.
+ */
+ImageEditor.prototype.isLocked = function() {
+ return !this.commandQueue_ || this.commandQueue_.isBusy();
+};
+
+/**
+ * @return {boolean} True if the command queue is busy.
+ */
+ImageEditor.prototype.isBusy = function() {
+ return this.commandQueue_ && this.commandQueue_.isBusy();
+};
+
+/**
+ * Reflect the locked state of the editor in the UI.
+ * @param {boolean} on True if locked.
+ */
+ImageEditor.prototype.lockUI = function(on) {
+ ImageUtil.setAttribute(this.rootContainer_, 'locked', on);
+};
+
+/**
+ * Report the tool use to the metrics subsystem.
+ * @param {string} name Action name.
+ */
+ImageEditor.prototype.recordToolUse = function(name) {
+ ImageUtil.metrics.recordEnum(
+ ImageUtil.getMetricName('Tool'), name, this.actionNames_);
+};
+
+/**
+ * Content update handler.
+ * @private
+ */
+ImageEditor.prototype.onContentUpdate_ = function() {
+ for (var i = 0; i != this.modes_.length; i++) {
+ var mode = this.modes_[i];
+ ImageUtil.setAttribute(mode.button_, 'disabled', !mode.isApplicable());
+ }
+};
+
+/**
+ * Open the editing session for a new image.
+ *
+ * @param {string} url Image url.
+ * @param {Object} metadata Metadata.
+ * @param {Object} effect Transition effect object.
+ * @param {function(function)} saveFunction Image save function.
+ * @param {function} displayCallback Display callback.
+ * @param {function} loadCallback Load callback.
+ */
+ImageEditor.prototype.openSession = function(
+ url, metadata, effect, saveFunction, displayCallback, loadCallback) {
+ if (this.commandQueue_)
+ throw new Error('Session not closed');
+
+ this.lockUI(true);
+
+ var self = this;
+ this.imageView_.load(
+ url, metadata, effect, displayCallback, function(loadType, delay, error) {
+ self.lockUI(false);
+ self.commandQueue_ = new CommandQueue(
+ self.container_.ownerDocument,
+ self.imageView_.getCanvas(),
+ saveFunction);
+ self.commandQueue_.attachUI(
+ self.getImageView(), self.getPrompt(), self.lockUI.bind(self));
+ self.updateUndoRedo();
+ loadCallback(loadType, delay, error);
+ });
+};
+
+/**
+ * Close the current image editing session.
+ * @param {function} callback Callback.
+ */
+ImageEditor.prototype.closeSession = function(callback) {
+ this.getPrompt().hide();
+ if (this.imageView_.isLoading()) {
+ if (this.commandQueue_) {
+ console.warn('Inconsistent image editor state');
+ this.commandQueue_ = null;
+ }
+ this.imageView_.cancelLoad();
+ this.lockUI(false);
+ callback();
+ return;
+ }
+ if (!this.commandQueue_) {
+ // Session is already closed.
+ callback();
+ return;
+ }
+
+ this.executeWhenReady(callback);
+ this.commandQueue_.close();
+ this.commandQueue_ = null;
+};
+
+/**
+ * Commit the current operation and execute the action.
+ *
+ * @param {function} callback Callback.
+ */
+ImageEditor.prototype.executeWhenReady = function(callback) {
+ if (this.commandQueue_) {
+ this.leaveModeGently();
+ this.commandQueue_.executeWhenReady(callback);
+ } else {
+ if (!this.imageView_.isLoading())
+ console.warn('Inconsistent image editor state');
+ callback();
+ }
+};
+
+/**
+ * @return {boolean} True if undo queue is not empty.
+ */
+ImageEditor.prototype.canUndo = function() {
+ return this.commandQueue_ && this.commandQueue_.canUndo();
+};
+
+/**
+ * Undo the recently executed command.
+ */
+ImageEditor.prototype.undo = function() {
+ if (this.isLocked()) return;
+ this.recordToolUse('undo');
+
+ // First undo click should dismiss the uncommitted modifications.
+ if (this.currentMode_ && this.currentMode_.isUpdated()) {
+ this.currentMode_.reset();
+ return;
+ }
+
+ this.getPrompt().hide();
+ this.leaveMode(false);
+ this.commandQueue_.undo();
+ this.updateUndoRedo();
+};
+
+/**
+ * Redo the recently un-done command.
+ */
+ImageEditor.prototype.redo = function() {
+ if (this.isLocked()) return;
+ this.recordToolUse('redo');
+ this.getPrompt().hide();
+ this.leaveMode(false);
+ this.commandQueue_.redo();
+ this.updateUndoRedo();
+};
+
+/**
+ * Update Undo/Redo buttons state.
+ */
+ImageEditor.prototype.updateUndoRedo = function() {
+ var canUndo = this.commandQueue_ && this.commandQueue_.canUndo();
+ var canRedo = this.commandQueue_ && this.commandQueue_.canRedo();
+ ImageUtil.setAttribute(this.undoButton_, 'disabled', !canUndo);
+ this.redoButton_.hidden = !canRedo;
+};
+
+/**
+ * @return {HTMLCanvasElement} The current image canvas.
+ */
+ImageEditor.prototype.getCanvas = function() {
+ return this.getImageView().getCanvas();
+};
+
+/**
+ * @return {ImageBuffer} ImageBuffer instance.
+ */
+ImageEditor.prototype.getBuffer = function() { return this.buffer_ };
+
+/**
+ * @return {ImageView} ImageView instance.
+ */
+ImageEditor.prototype.getImageView = function() { return this.imageView_ };
+
+/**
+ * @return {Viewport} Viewport instance.
+ */
+ImageEditor.prototype.getViewport = function() { return this.viewport_ };
+
+/**
+ * @return {ImageEditor.Prompt} Prompt instance.
+ */
+ImageEditor.prototype.getPrompt = function() { return this.prompt_ };
+
+/**
+ * Handle the toolbar controls update.
+ * @param {Object} options A map of options.
+ */
+ImageEditor.prototype.onOptionsChange = function(options) {
+ ImageUtil.trace.resetTimer('update');
+ if (this.currentMode_) {
+ this.currentMode_.update(options);
+ }
+ ImageUtil.trace.reportTimer('update');
+};
+
+/**
+ * ImageEditor.Mode represents a modal state dedicated to a specific operation.
+ * Inherits from ImageBuffer. Overlay to simplify the drawing of mode-specific
+ * tools.
+ *
+ * @param {string} name The mode name.
+ * @param {string} title The mode title.
+ * @constructor
+ */
+
+ImageEditor.Mode = function(name, title) {
+ this.name = name;
+ this.title = title;
+ this.message_ = 'GALLERY_ENTER_WHEN_DONE';
+};
+
+ImageEditor.Mode.prototype = {__proto__: ImageBuffer.Overlay.prototype };
+
+/**
+ * @return {Viewport} Viewport instance.
+ */
+ImageEditor.Mode.prototype.getViewport = function() { return this.viewport_ };
+
+/**
+ * @return {ImageView} ImageView instance.
+ */
+ImageEditor.Mode.prototype.getImageView = function() { return this.imageView_ };
+
+/**
+ * @return {string} The mode-specific message to be displayed when entering.
+ */
+ImageEditor.Mode.prototype.getMessage = function() { return this.message_ };
+
+/**
+ * @return {boolean} True if the mode is applicable in the current context.
+ */
+ImageEditor.Mode.prototype.isApplicable = function() { return true };
+
+/**
+ * Called once after creating the mode button.
+ *
+ * @param {ImageEditor} editor The editor instance.
+ * @param {HTMLElement} button The mode button.
+ */
+
+ImageEditor.Mode.prototype.bind = function(editor, button) {
+ this.editor_ = editor;
+ this.editor_.registerAction_(this.name);
+ this.button_ = button;
+ this.viewport_ = editor.getViewport();
+ this.imageView_ = editor.getImageView();
+};
+
+/**
+ * Called before entering the mode.
+ */
+ImageEditor.Mode.prototype.setUp = function() {
+ this.editor_.getBuffer().addOverlay(this);
+ this.updated_ = false;
+};
+
+/**
+ * Create mode-specific controls here.
+ * @param {ImageEditor.Toolbar} toolbar The toolbar to populate.
+ */
+ImageEditor.Mode.prototype.createTools = function(toolbar) {};
+
+/**
+ * Called before exiting the mode.
+ */
+ImageEditor.Mode.prototype.cleanUpUI = function() {
+ this.editor_.getBuffer().removeOverlay(this);
+};
+
+/**
+ * Called after exiting the mode.
+ */
+ImageEditor.Mode.prototype.cleanUpCaches = function() {};
+
+/**
+ * Called when any of the controls changed its value.
+ * @param {Object} options A map of options.
+ */
+ImageEditor.Mode.prototype.update = function(options) {
+ this.markUpdated();
+};
+
+/**
+ * Mark the editor mode as updated.
+ */
+ImageEditor.Mode.prototype.markUpdated = function() {
+ this.updated_ = true;
+};
+
+/**
+ * @return {boolean} True if the mode controls changed.
+ */
+ImageEditor.Mode.prototype.isUpdated = function() { return this.updated_ };
+
+/**
+ * Resets the mode to a clean state.
+ */
+ImageEditor.Mode.prototype.reset = function() {
+ this.editor_.modeToolbar_.reset();
+ this.updated_ = false;
+};
+
+/**
+ * One-click editor tool, requires no interaction, just executes the command.
+ *
+ * @param {string} name The mode name.
+ * @param {string} title The mode title.
+ * @param {Command} command The command to execute on click.
+ * @constructor
+ */
+ImageEditor.Mode.OneClick = function(name, title, command) {
+ ImageEditor.Mode.call(this, name, title);
+ this.instant = true;
+ this.command_ = command;
+};
+
+ImageEditor.Mode.OneClick.prototype = {__proto__: ImageEditor.Mode.prototype};
+
+/**
+ * @return {Command} command.
+ */
+ImageEditor.Mode.OneClick.prototype.getCommand = function() {
+ return this.command_;
+};
+
+/**
+ * Register the action name. Required for metrics reporting.
+ * @param {string} name Button name.
+ * @private
+ */
+ImageEditor.prototype.registerAction_ = function(name) {
+ this.actionNames_.push(name);
+};
+
+/**
+ * Populate the toolbar.
+ */
+ImageEditor.prototype.createToolButtons = function() {
+ this.mainToolbar_.clear();
+ this.actionNames_ = [];
+
+ var self = this;
+ function createButton(name, title, handler) {
+ return self.mainToolbar_.addButton(name,
+ title,
+ handler,
+ name /* opt_className */);
+ }
+
+ for (var i = 0; i != this.modes_.length; i++) {
+ var mode = this.modes_[i];
+ mode.bind(this, createButton(mode.name,
+ mode.title,
+ this.enterMode.bind(this, mode)));
+ }
+
+ this.undoButton_ = createButton('undo',
+ 'GALLERY_UNDO',
+ this.undo.bind(this));
+ this.registerAction_('undo');
+
+ this.redoButton_ = createButton('redo',
+ 'GALLERY_REDO',
+ this.redo.bind(this));
+ this.registerAction_('redo');
+};
+
+/**
+ * @return {ImageEditor.Mode} The current mode.
+ */
+ImageEditor.prototype.getMode = function() { return this.currentMode_ };
+
+/**
+ * The user clicked on the mode button.
+ *
+ * @param {ImageEditor.Mode} mode The new mode.
+ */
+ImageEditor.prototype.enterMode = function(mode) {
+ if (this.isLocked()) return;
+
+ if (this.currentMode_ == mode) {
+ // Currently active editor tool clicked, commit if modified.
+ this.leaveMode(this.currentMode_.updated_);
+ return;
+ }
+
+ this.recordToolUse(mode.name);
+
+ this.leaveModeGently();
+ // The above call could have caused a commit which might have initiated
+ // an asynchronous command execution. Wait for it to complete, then proceed
+ // with the mode set up.
+ this.commandQueue_.executeWhenReady(this.setUpMode_.bind(this, mode));
+};
+
+/**
+ * Set up the new editing mode.
+ *
+ * @param {ImageEditor.Mode} mode The mode.
+ * @private
+ */
+ImageEditor.prototype.setUpMode_ = function(mode) {
+ this.currentTool_ = mode.button_;
+
+ ImageUtil.setAttribute(this.currentTool_, 'pressed', true);
+
+ this.currentMode_ = mode;
+ this.currentMode_.setUp();
+
+ if (this.currentMode_.instant) { // Instant tool.
+ this.leaveMode(true);
+ return;
+ }
+
+ this.getPrompt().show(this.currentMode_.getMessage());
+
+ this.modeToolbar_.clear();
+ this.currentMode_.createTools(this.modeToolbar_);
+ this.modeToolbar_.show(true);
+};
+
+/**
+ * The user clicked on 'OK' or 'Cancel' or on a different mode button.
+ * @param {boolean} commit True if commit is required.
+ */
+ImageEditor.prototype.leaveMode = function(commit) {
+ if (!this.currentMode_) return;
+
+ if (!this.currentMode_.instant) {
+ this.getPrompt().hide();
+ }
+
+ this.modeToolbar_.show(false);
+
+ this.currentMode_.cleanUpUI();
+ if (commit) {
+ var self = this;
+ var command = this.currentMode_.getCommand();
+ if (command) { // Could be null if the user did not do anything.
+ this.commandQueue_.execute(command);
+ this.updateUndoRedo();
+ }
+ }
+ this.currentMode_.cleanUpCaches();
+ this.currentMode_ = null;
+
+ ImageUtil.setAttribute(this.currentTool_, 'pressed', false);
+ this.currentTool_ = null;
+};
+
+/**
+ * Leave the mode, commit only if required by the current mode.
+ */
+ImageEditor.prototype.leaveModeGently = function() {
+ this.leaveMode(this.currentMode_ &&
+ this.currentMode_.updated_ &&
+ this.currentMode_.implicitCommit);
+};
+
+/**
+ * Enter the editor mode with the given name.
+ *
+ * @param {string} name Mode name.
+ * @private
+ */
+ImageEditor.prototype.enterModeByName_ = function(name) {
+ for (var i = 0; i != this.modes_.length; i++) {
+ var mode = this.modes_[i];
+ if (mode.name == name) {
+ if (!mode.button_.hasAttribute('disabled'))
+ this.enterMode(mode);
+ return;
+ }
+ }
+ console.error('Mode "' + name + '" not found.');
+};
+
+/**
+ * Key down handler.
+ * @param {Event} event The keydown event.
+ * @return {boolean} True if handled.
+ */
+ImageEditor.prototype.onKeyDown = function(event) {
+ switch (util.getKeyModifiers(event) + event.keyIdentifier) {
+ case 'U+001B': // Escape
+ case 'Enter':
+ if (this.getMode()) {
+ this.leaveMode(event.keyIdentifier == 'Enter');
+ return true;
+ }
+ break;
+
+ case 'Ctrl-U+005A': // Ctrl+Z
+ if (this.commandQueue_.canUndo()) {
+ this.undo();
+ return true;
+ }
+ break;
+
+ case 'Ctrl-U+0059': // Ctrl+Y
+ if (this.commandQueue_.canRedo()) {
+ this.redo();
+ return true;
+ }
+ break;
+
+ case 'U+0041': // 'a'
+ this.enterModeByName_('autofix');
+ return true;
+
+ case 'U+0042': // 'b'
+ this.enterModeByName_('exposure');
+ return true;
+
+ case 'U+0043': // 'c'
+ this.enterModeByName_('crop');
+ return true;
+
+ case 'U+004C': // 'l'
+ this.enterModeByName_('rotate_left');
+ return true;
+
+ case 'U+0052': // 'r'
+ this.enterModeByName_('rotate_right');
+ return true;
+ }
+ return false;
+};
+
+/**
+ * Double tap handler.
+ * @param {number} x X coordinate of the event.
+ * @param {number} y Y coordinate of the event.
+ * @private
+ */
+ImageEditor.prototype.onDoubleTap_ = function(x, y) {
+ if (this.getMode()) {
+ var action = this.buffer_.getDoubleTapAction(x, y);
+ if (action == ImageBuffer.DoubleTapAction.COMMIT)
+ this.leaveMode(true);
+ else if (action == ImageBuffer.DoubleTapAction.CANCEL)
+ this.leaveMode(false);
+ }
+};
+
+/**
+ * Hide the tools that overlap the given rectangular frame.
+ *
+ * @param {Rect} frame Hide the tool that overlaps this rect.
+ * @param {Rect} transparent But do not hide the tool that is completely inside
+ * this rect.
+ */
+ImageEditor.prototype.hideOverlappingTools = function(frame, transparent) {
+ var tools = this.rootContainer_.ownerDocument.querySelectorAll('.dimmable');
+ var changed = false;
+ for (var i = 0; i != tools.length; i++) {
+ var tool = tools[i];
+ var toolRect = tool.getBoundingClientRect();
+ var overlapping =
+ (frame && frame.intersects(toolRect)) &&
+ !(transparent && transparent.contains(toolRect));
+ if (overlapping && !tool.hasAttribute('dimmed') ||
+ !overlapping && tool.hasAttribute('dimmed')) {
+ ImageUtil.setAttribute(tool, 'dimmed', overlapping);
+ changed = true;
+ }
+ }
+ if (changed)
+ this.onToolsVisibilityChanged_();
+};
+
+/**
+ * A helper object for panning the ImageBuffer.
+ *
+ * @param {HTMLElement} rootContainer The top-level container.
+ * @param {HTMLElement} container The container for mouse events.
+ * @param {ImageBuffer} buffer Image buffer.
+ * @constructor
+ */
+ImageEditor.MouseControl = function(rootContainer, container, buffer) {
+ this.rootContainer_ = rootContainer;
+ this.container_ = container;
+ this.buffer_ = buffer;
+
+ var handlers = {
+ 'touchstart': this.onTouchStart,
+ 'touchend': this.onTouchEnd,
+ 'touchcancel': this.onTouchCancel,
+ 'touchmove': this.onTouchMove,
+ 'mousedown': this.onMouseDown,
+ 'mouseup': this.onMouseUp
+ };
+
+ for (var eventName in handlers) {
+ container.addEventListener(
+ eventName, handlers[eventName].bind(this), false);
+ }
+
+ // Mouse move handler has to be attached to the window to receive events
+ // from outside of the window. See: http://crbug.com/155705
+ window.addEventListener('mousemove', this.onMouseMove.bind(this), false);
+};
+
+/**
+ * Maximum movement for touch to be detected as a tap (in pixels).
+ * @private
+ */
+ImageEditor.MouseControl.MAX_MOVEMENT_FOR_TAP_ = 8;
+
+/**
+ * Maximum time for touch to be detected as a tap (in milliseconds).
+ * @private
+ */
+ImageEditor.MouseControl.MAX_TAP_DURATION_ = 500;
+
+/**
+ * Maximum distance from the first tap to the second tap to be considered
+ * as a double tap.
+ * @private
+ */
+ImageEditor.MouseControl.MAX_DISTANCE_FOR_DOUBLE_TAP_ = 32;
+
+/**
+ * Maximum time for touch to be detected as a double tap (in milliseconds).
+ * @private
+ */
+ImageEditor.MouseControl.MAX_DOUBLE_TAP_DURATION_ = 1000;
+
+/**
+ * Returns an event's position.
+ *
+ * @param {MouseEvent|Touch} e Pointer position.
+ * @return {Object} A pair of x,y in page coordinates.
+ * @private
+ */
+ImageEditor.MouseControl.getPosition_ = function(e) {
+ return {
+ x: e.pageX,
+ y: e.pageY
+ };
+};
+
+/**
+ * Returns touch position or null if there is more than one touch position.
+ *
+ * @param {TouchEvent} e Event.
+ * @return {object?} A pair of x,y in page coordinates.
+ * @private
+ */
+ImageEditor.MouseControl.prototype.getTouchPosition_ = function(e) {
+ if (e.targetTouches.length == 1)
+ return ImageEditor.MouseControl.getPosition_(e.targetTouches[0]);
+ else
+ return null;
+};
+
+/**
+ * Touch start handler.
+ * @param {TouchEvent} e Event.
+ */
+ImageEditor.MouseControl.prototype.onTouchStart = function(e) {
+ var position = this.getTouchPosition_(e);
+ if (position) {
+ this.touchStartInfo_ = {
+ x: position.x,
+ y: position.y,
+ time: Date.now()
+ };
+ this.dragHandler_ = this.buffer_.getDragHandler(position.x, position.y,
+ true /* touch */);
+ this.dragHappened_ = false;
+ }
+};
+
+/**
+ * Touch end handler.
+ * @param {TouchEvent} e Event.
+ */
+ImageEditor.MouseControl.prototype.onTouchEnd = function(e) {
+ if (!this.dragHappened_ && Date.now() - this.touchStartInfo_.time <=
+ ImageEditor.MouseControl.MAX_TAP_DURATION_) {
+ this.buffer_.onClick(this.touchStartInfo_.x, this.touchStartInfo_.y);
+ if (this.previousTouchStartInfo_ &&
+ Date.now() - this.previousTouchStartInfo_.time <
+ ImageEditor.MouseControl.MAX_DOUBLE_TAP_DURATION_) {
+ var prevTouchCircle = new Circle(
+ this.previousTouchStartInfo_.x,
+ this.previousTouchStartInfo_.y,
+ ImageEditor.MouseControl.MAX_DISTANCE_FOR_DOUBLE_TAP_);
+ if (prevTouchCircle.inside(this.touchStartInfo_.x,
+ this.touchStartInfo_.y)) {
+ this.doubleTapCallback_(this.touchStartInfo_.x, this.touchStartInfo_.y);
+ }
+ }
+ this.previousTouchStartInfo_ = this.touchStartInfo_;
+ } else {
+ this.previousTouchStartInfo_ = null;
+ }
+ this.onTouchCancel(e);
+};
+
+/**
+ * Default double tap handler.
+ * @param {number} x X coordinate of the event.
+ * @param {number} y Y coordinate of the event.
+ * @private
+ */
+ImageEditor.MouseControl.prototype.doubleTapCallback_ = function(x, y) {};
+
+/**
+ * Sets callback to be called when double tap detected.
+ * @param {function(number, number)} callback New double tap callback.
+ */
+ImageEditor.MouseControl.prototype.setDoubleTapCallback = function(callback) {
+ this.doubleTapCallback_ = callback;
+};
+
+/**
+ * Touch chancel handler.
+ */
+ImageEditor.MouseControl.prototype.onTouchCancel = function() {
+ this.dragHandler_ = null;
+ this.dragHappened_ = false;
+ this.touchStartInfo_ = null;
+ this.lockMouse_(false);
+};
+
+/**
+ * Touch move handler.
+ * @param {TouchEvent} e Event.
+ */
+ImageEditor.MouseControl.prototype.onTouchMove = function(e) {
+ var position = this.getTouchPosition_(e);
+ if (!position)
+ return;
+
+ if (this.touchStartInfo_ && !this.dragHappened_) {
+ var tapCircle = new Circle(this.touchStartInfo_.x, this.touchStartInfo_.y,
+ ImageEditor.MouseControl.MAX_MOVEMENT_FOR_TAP_);
+ this.dragHappened_ = !tapCircle.inside(position.x, position.y);
+ }
+ if (this.dragHandler_ && this.dragHappened_) {
+ this.dragHandler_(position.x, position.y);
+ this.lockMouse_(true);
+ }
+};
+
+/**
+ * Mouse down handler.
+ * @param {MouseEvent} e Event.
+ */
+ImageEditor.MouseControl.prototype.onMouseDown = function(e) {
+ var position = ImageEditor.MouseControl.getPosition_(e);
+
+ this.dragHandler_ = this.buffer_.getDragHandler(position.x, position.y,
+ false /* mouse */);
+ this.dragHappened_ = false;
+ this.updateCursor_(position);
+};
+
+/**
+ * Mouse up handler.
+ * @param {MouseEvent} e Event.
+ */
+ImageEditor.MouseControl.prototype.onMouseUp = function(e) {
+ var position = ImageEditor.MouseControl.getPosition_(e);
+
+ if (!this.dragHappened_) {
+ this.buffer_.onClick(position.x, position.y);
+ }
+ this.dragHandler_ = null;
+ this.dragHappened_ = false;
+ this.lockMouse_(false);
+};
+
+/**
+ * Mouse move handler.
+ * @param {MouseEvent} e Event.
+ */
+ImageEditor.MouseControl.prototype.onMouseMove = function(e) {
+ var position = ImageEditor.MouseControl.getPosition_(e);
+
+ if (this.dragHandler_ && !e.which) {
+ // mouseup must have happened while the mouse was outside our window.
+ this.dragHandler_ = null;
+ this.lockMouse_(false);
+ }
+
+ this.updateCursor_(position);
+ if (this.dragHandler_) {
+ this.dragHandler_(position.x, position.y);
+ this.dragHappened_ = true;
+ this.lockMouse_(true);
+ }
+};
+
+/**
+ * Update the UI to reflect mouse drag state.
+ * @param {boolean} on True if dragging.
+ * @private
+ */
+ImageEditor.MouseControl.prototype.lockMouse_ = function(on) {
+ ImageUtil.setAttribute(this.rootContainer_, 'mousedrag', on);
+};
+
+/**
+ * Update the cursor.
+ *
+ * @param {Object} position An object holding x and y properties.
+ * @private
+ */
+ImageEditor.MouseControl.prototype.updateCursor_ = function(position) {
+ var oldCursor = this.container_.getAttribute('cursor');
+ var newCursor = this.buffer_.getCursorStyle(
+ position.x, position.y, !!this.dragHandler_);
+ if (newCursor != oldCursor) // Avoid flicker.
+ this.container_.setAttribute('cursor', newCursor);
+};
+
+/**
+ * A toolbar for the ImageEditor.
+ * @param {HTMLElement} parent The parent element.
+ * @param {function} displayStringFunction A string formatting function.
+ * @param {function} updateCallback The callback called when controls change.
+ * @constructor
+ */
+ImageEditor.Toolbar = function(parent, displayStringFunction, updateCallback) {
+ this.wrapper_ = parent;
+ this.displayStringFunction_ = displayStringFunction;
+ this.updateCallback_ = updateCallback;
+};
+
+/**
+ * Clear the toolbar.
+ */
+ImageEditor.Toolbar.prototype.clear = function() {
+ ImageUtil.removeChildren(this.wrapper_);
+};
+
+/**
+ * Create a control.
+ * @param {string} tagName The element tag name.
+ * @return {HTMLElement} The created control element.
+ * @private
+ */
+ImageEditor.Toolbar.prototype.create_ = function(tagName) {
+ return this.wrapper_.ownerDocument.createElement(tagName);
+};
+
+/**
+ * Add a control.
+ * @param {HTMLElement} element The control to add.
+ * @return {HTMLElement} The added element.
+ */
+ImageEditor.Toolbar.prototype.add = function(element) {
+ this.wrapper_.appendChild(element);
+ return element;
+};
+
+/**
+ * Add a text label.
+ * @param {string} name Label name.
+ * @return {HTMLElement} The added label.
+ */
+ImageEditor.Toolbar.prototype.addLabel = function(name) {
+ var label = this.create_('span');
+ label.textContent = this.displayStringFunction_(name);
+ return this.add(label);
+};
+
+/**
+ * Add a button.
+ *
+ * @param {string} name Button name.
+ * @param {string} title Button title.
+ * @param {function} handler onClick handler.
+ * @param {string=} opt_class Extra class name.
+ * @return {HTMLElement} The added button.
+ */
+ImageEditor.Toolbar.prototype.addButton = function(
+ name, title, handler, opt_class) {
+ var button = this.create_('button');
+ if (opt_class) button.classList.add(opt_class);
+ var label = this.create_('span');
+ label.textContent = this.displayStringFunction_(title);
+ button.appendChild(label);
+ button.label = this.displayStringFunction_(title);
+ button.addEventListener('click', handler, false);
+ return this.add(button);
+};
+
+/**
+ * Add a range control (scalar value picker).
+ *
+ * @param {string} name An option name.
+ * @param {string} title An option title.
+ * @param {number} min Min value of the option.
+ * @param {number} value Default value of the option.
+ * @param {number} max Max value of the options.
+ * @param {number} scale A number to multiply by when setting
+ * min/value/max in DOM.
+ * @param {boolean=} opt_showNumeric True if numeric value should be displayed.
+ * @return {HTMLElement} Range element.
+ */
+ImageEditor.Toolbar.prototype.addRange = function(
+ name, title, min, value, max, scale, opt_showNumeric) {
+ var self = this;
+
+ scale = scale || 1;
+
+ var range = this.create_('input');
+
+ range.className = 'range';
+ range.type = 'range';
+ range.name = name;
+ range.min = Math.ceil(min * scale);
+ range.max = Math.floor(max * scale);
+
+ var numeric = this.create_('div');
+ numeric.className = 'numeric';
+ function mirror() {
+ numeric.textContent = Math.round(range.getValue() * scale) / scale;
+ }
+
+ range.setValue = function(newValue) {
+ range.value = Math.round(newValue * scale);
+ mirror();
+ };
+
+ range.getValue = function() {
+ return Number(range.value) / scale;
+ };
+
+ range.reset = function() {
+ range.setValue(value);
+ };
+
+ range.addEventListener('change',
+ function() {
+ mirror();
+ self.updateCallback_(self.getOptions());
+ },
+ false);
+
+ range.setValue(value);
+
+ var label = this.create_('div');
+ label.textContent = this.displayStringFunction_(title);
+ label.className = 'label ' + name;
+ this.add(label);
+ this.add(range);
+
+ if (opt_showNumeric)
+ this.add(numeric);
+
+ // Swallow the left and right keys, so they are not handled by other
+ // listeners.
+ range.addEventListener('keydown', function(e) {
+ if (e.keyIdentifier === 'Left' || e.keyIdentifier === 'Right')
+ e.stopPropagation();
+ });
+
+ return range;
+};
+
+/**
+ * @return {Object} options A map of options.
+ */
+ImageEditor.Toolbar.prototype.getOptions = function() {
+ var values = {};
+ for (var child = this.wrapper_.firstChild; child; child = child.nextSibling) {
+ if (child.name)
+ values[child.name] = child.getValue();
+ }
+ return values;
+};
+
+/**
+ * Reset the toolbar.
+ */
+ImageEditor.Toolbar.prototype.reset = function() {
+ for (var child = this.wrapper_.firstChild; child; child = child.nextSibling) {
+ if (child.reset) child.reset();
+ }
+};
+
+/**
+ * Show/hide the toolbar.
+ * @param {boolean} on True if show.
+ */
+ImageEditor.Toolbar.prototype.show = function(on) {
+ if (!this.wrapper_.firstChild)
+ return; // Do not show empty toolbar;
+
+ this.wrapper_.hidden = !on;
+};
+
+/** A prompt panel for the editor.
+ *
+ * @param {HTMLElement} container Container element.
+ * @param {function} displayStringFunction A formatting function.
+ * @constructor
+ */
+ImageEditor.Prompt = function(container, displayStringFunction) {
+ this.container_ = container;
+ this.displayStringFunction_ = displayStringFunction;
+};
+
+/**
+ * Reset the prompt.
+ */
+ImageEditor.Prompt.prototype.reset = function() {
+ this.cancelTimer();
+ if (this.wrapper_) {
+ this.container_.removeChild(this.wrapper_);
+ this.wrapper_ = null;
+ this.prompt_ = null;
+ }
+};
+
+/**
+ * Cancel the delayed action.
+ */
+ImageEditor.Prompt.prototype.cancelTimer = function() {
+ if (this.timer_) {
+ clearTimeout(this.timer_);
+ this.timer_ = null;
+ }
+};
+
+/**
+ * Schedule the delayed action.
+ * @param {function} callback Callback.
+ * @param {number} timeout Timeout.
+ */
+ImageEditor.Prompt.prototype.setTimer = function(callback, timeout) {
+ this.cancelTimer();
+ var self = this;
+ this.timer_ = setTimeout(function() {
+ self.timer_ = null;
+ callback();
+ }, timeout);
+};
+
+/**
+ * Show the prompt.
+ *
+ * @param {string} text The prompt text.
+ * @param {number} timeout Timeout in ms.
+ * @param {Object} formatArgs varArgs for the formatting fuction.
+ */
+ImageEditor.Prompt.prototype.show = function(text, timeout, formatArgs) {
+ this.showAt.apply(this,
+ ['center'].concat(Array.prototype.slice.call(arguments)));
+};
+
+/**
+ *
+ * @param {string} pos The 'pos' attribute value.
+ * @param {string} text The prompt text.
+ * @param {number} timeout Timeout in ms.
+ * @param {Object} formatArgs varArgs for the formatting function.
+ */
+ImageEditor.Prompt.prototype.showAt = function(pos, text, timeout, formatArgs) {
+ this.reset();
+ if (!text) return;
+
+ var document = this.container_.ownerDocument;
+ this.wrapper_ = document.createElement('div');
+ this.wrapper_.className = 'prompt-wrapper';
+ this.wrapper_.setAttribute('pos', pos);
+ this.container_.appendChild(this.wrapper_);
+
+ this.prompt_ = document.createElement('div');
+ this.prompt_.className = 'prompt';
+
+ // Create an extra wrapper which opacity can be manipulated separately.
+ var tool = document.createElement('div');
+ tool.className = 'dimmable';
+ this.wrapper_.appendChild(tool);
+ tool.appendChild(this.prompt_);
+
+ var args = [text].concat(Array.prototype.slice.call(arguments, 3));
+ this.prompt_.textContent = this.displayStringFunction_.apply(null, args);
+
+ var close = document.createElement('div');
+ close.className = 'close';
+ close.addEventListener('click', this.hide.bind(this));
+ this.prompt_.appendChild(close);
+
+ setTimeout(
+ this.prompt_.setAttribute.bind(this.prompt_, 'state', 'fadein'), 0);
+
+ if (timeout)
+ this.setTimer(this.hide.bind(this), timeout);
+};
+
+/**
+ * Hide the prompt.
+ */
+ImageEditor.Prompt.prototype.hide = function() {
+ if (!this.prompt_) return;
+ this.prompt_.setAttribute('state', 'fadeout');
+ // Allow some time for the animation to play out.
+ this.setTimer(this.reset.bind(this), 500);
+};
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/image_encoder.js b/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/image_encoder.js
new file mode 100644
index 00000000000..1c96b1fe326
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/image_encoder.js
@@ -0,0 +1,228 @@
+// 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.
+
+'use strict';
+
+/**
+ * A namespace class for image encoding functions. All methods are static.
+ */
+function ImageEncoder() {}
+
+/**
+ * @type {Array.<Object>}
+ */
+ImageEncoder.metadataEncoders = {};
+
+/**
+ * @param {function(new:ImageEncoder.MetadataEncoder)} constructor
+ * // TODO(JSDOC).
+ * @param {string} mimeType // TODO(JSDOC).
+ */
+ImageEncoder.registerMetadataEncoder = function(constructor, mimeType) {
+ ImageEncoder.metadataEncoders[mimeType] = constructor;
+};
+
+/**
+ * Create a metadata encoder.
+ *
+ * The encoder will own and modify a copy of the original metadata.
+ *
+ * @param {Object} metadata Original metadata.
+ * @return {ImageEncoder.MetadataEncoder} Created metadata encoder.
+ */
+ImageEncoder.createMetadataEncoder = function(metadata) {
+ var constructor = ImageEncoder.metadataEncoders[metadata.mimeType] ||
+ ImageEncoder.MetadataEncoder;
+ return new constructor(metadata);
+};
+
+
+/**
+ * Create a metadata encoder object holding a copy of metadata
+ * modified according to the properties of the supplied image.
+ *
+ * @param {Object} metadata Original metadata.
+ * @param {HTMLCanvasElement} canvas Canvas to use for metadata.
+ * @param {number} quality Encoding quality (defaults to 1).
+ * @return {ImageEncoder.MetadataEncoder} Encoder with encoded metadata.
+ */
+ImageEncoder.encodeMetadata = function(metadata, canvas, quality) {
+ var encoder = ImageEncoder.createMetadataEncoder(metadata);
+ encoder.setImageData(canvas);
+ encoder.setThumbnailData(ImageEncoder.createThumbnail(canvas), quality || 1);
+ return encoder;
+};
+
+
+/**
+ * Return a blob with the encoded image with metadata inserted.
+ * @param {HTMLCanvasElement} canvas The canvas with the image to be encoded.
+ * @param {ImageEncoder.MetadataEncoder} metadataEncoder Encoder to use.
+ * @param {number} quality (0..1], Encoding quality, defaults to 0.9.
+ * @return {Blob} encoded data.
+ */
+ImageEncoder.getBlob = function(canvas, metadataEncoder, quality) {
+ // Contrary to what one might think 1.0 is not a good default. Opening and
+ // saving an typical photo taken with consumer camera increases its file size
+ // by 50-100%.
+ // Experiments show that 0.9 is much better. It shrinks some photos a bit,
+ // keeps others about the same size, but does not visibly lower the quality.
+ quality = quality || 0.9;
+
+ ImageUtil.trace.resetTimer('dataurl');
+ // WebKit does not support canvas.toBlob yet so canvas.toDataURL is
+ // the only way to use the Chrome built-in image encoder.
+ var dataURL =
+ canvas.toDataURL(metadataEncoder.getMetadata().mimeType, quality);
+ ImageUtil.trace.reportTimer('dataurl');
+
+ var encodedImage = ImageEncoder.decodeDataURL(dataURL);
+
+ var encodedMetadata = metadataEncoder.encode();
+
+ var slices = [];
+
+ // TODO(kaznacheev): refactor |stringToArrayBuffer| and |encode| to return
+ // arrays instead of array buffers.
+ function appendSlice(arrayBuffer) {
+ slices.push(new DataView(arrayBuffer));
+ }
+
+ ImageUtil.trace.resetTimer('blob');
+ if (encodedMetadata.byteLength != 0) {
+ var metadataRange = metadataEncoder.findInsertionRange(encodedImage);
+ appendSlice(ImageEncoder.stringToArrayBuffer(
+ encodedImage, 0, metadataRange.from));
+
+ appendSlice(metadataEncoder.encode());
+
+ appendSlice(ImageEncoder.stringToArrayBuffer(
+ encodedImage, metadataRange.to, encodedImage.length));
+ } else {
+ appendSlice(ImageEncoder.stringToArrayBuffer(
+ encodedImage, 0, encodedImage.length));
+ }
+ var blob = new Blob(slices, {type: metadataEncoder.getMetadata().mimeType});
+ ImageUtil.trace.reportTimer('blob');
+ return blob;
+};
+
+/**
+ * Decode a dataURL into a binary string containing the encoded image.
+ *
+ * Why return a string? Calling atob and having the rest of the code deal
+ * with a string is several times faster than decoding base64 in Javascript.
+ *
+ * @param {string} dataURL Data URL to decode.
+ * @return {string} A binary string (char codes are the actual byte values).
+ */
+ImageEncoder.decodeDataURL = function(dataURL) {
+ // Skip the prefix ('data:image/<type>;base64,')
+ var base64string = dataURL.substring(dataURL.indexOf(',') + 1);
+ return atob(base64string);
+};
+
+/**
+ * Return a thumbnail for an image.
+ * @param {HTMLCanvasElement} canvas Original image.
+ * @param {number=} opt_shrinkage Thumbnail should be at least this much smaller
+ * than the original image (in each dimension).
+ * @return {HTMLCanvasElement} Thumbnail canvas.
+ */
+ImageEncoder.createThumbnail = function(canvas, opt_shrinkage) {
+ var MAX_THUMBNAIL_DIMENSION = 320;
+
+ opt_shrinkage = Math.max(opt_shrinkage || 4,
+ canvas.width / MAX_THUMBNAIL_DIMENSION,
+ canvas.height / MAX_THUMBNAIL_DIMENSION);
+
+ var thumbnailCanvas = canvas.ownerDocument.createElement('canvas');
+ thumbnailCanvas.width = Math.round(canvas.width / opt_shrinkage);
+ thumbnailCanvas.height = Math.round(canvas.height / opt_shrinkage);
+
+ var context = thumbnailCanvas.getContext('2d');
+ context.drawImage(canvas,
+ 0, 0, canvas.width, canvas.height,
+ 0, 0, thumbnailCanvas.width, thumbnailCanvas.height);
+
+ return thumbnailCanvas;
+};
+
+/**
+ * TODO(JSDOC)
+ * @param {string} string // TODO(JSDOC).
+ * @param {number} from // TODO(JSDOC).
+ * @param {number} to // TODO(JSDOC).
+ * @return {ArrayBuffer} // TODO(JSDOC).
+ */
+ImageEncoder.stringToArrayBuffer = function(string, from, to) {
+ var size = to - from;
+ var array = new Uint8Array(size);
+ for (var i = 0; i != size; i++) {
+ array[i] = string.charCodeAt(from + i);
+ }
+ return array.buffer;
+};
+
+/**
+ * A base class for a metadata encoder.
+ *
+ * Serves as a default metadata encoder for images that none of the metadata
+ * parsers recognized.
+ *
+ * @param {Object} original_metadata Starting metadata.
+ * @constructor
+ */
+ImageEncoder.MetadataEncoder = function(original_metadata) {
+ this.metadata_ = MetadataCache.cloneMetadata(original_metadata) || {};
+ if (this.metadata_.mimeType != 'image/jpeg') {
+ // Chrome can only encode JPEG and PNG. Force PNG mime type so that we
+ // can save to file and generate a thumbnail.
+ this.metadata_.mimeType = 'image/png';
+ }
+};
+
+/**
+ * TODO(JSDOC)
+ * @return {Object} // TODO(JSDOC).
+ */
+ImageEncoder.MetadataEncoder.prototype.getMetadata = function() {
+ return this.metadata_;
+};
+
+/**
+ * @param {HTMLCanvasElement|Object} canvas Canvas or or anything with
+ * width and height properties.
+ */
+ImageEncoder.MetadataEncoder.prototype.setImageData = function(canvas) {
+ this.metadata_.width = canvas.width;
+ this.metadata_.height = canvas.height;
+};
+
+/**
+ * @param {HTMLCanvasElement} canvas Canvas to use as thumbnail.
+ * @param {number} quality Thumbnail quality.
+ */
+ImageEncoder.MetadataEncoder.prototype.setThumbnailData =
+ function(canvas, quality) {
+ this.metadata_.thumbnailURL =
+ canvas.toDataURL(this.metadata_.mimeType, quality);
+};
+
+/**
+ * Return a range where the metadata is (or should be) located.
+ * @param {string} encodedImage // TODO(JSDOC).
+ * @return {Object} An object with from and to properties.
+ */
+ImageEncoder.MetadataEncoder.prototype.
+ findInsertionRange = function(encodedImage) { return {from: 0, to: 0} };
+
+/**
+ * Return serialized metadata ready to write to an image file.
+ * The return type is optimized for passing to Blob.append.
+ * @return {ArrayBuffer} // TODO(JSDOC).
+ */
+ImageEncoder.MetadataEncoder.prototype.encode = function() {
+ return new Uint8Array(0).buffer;
+};
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/image_transform.js b/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/image_transform.js
new file mode 100644
index 00000000000..6b194baf0fc
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/image_transform.js
@@ -0,0 +1,493 @@
+// 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.
+
+'use strict';
+
+/**
+ * Crop mode.
+ * @constructor
+ */
+ImageEditor.Mode.Crop = function() {
+ ImageEditor.Mode.call(this, 'crop', 'GALLERY_CROP');
+};
+
+ImageEditor.Mode.Crop.prototype = {__proto__: ImageEditor.Mode.prototype};
+
+/**
+ * TODO(JSDOC).
+ */
+ImageEditor.Mode.Crop.prototype.setUp = function() {
+ ImageEditor.Mode.prototype.setUp.apply(this, arguments);
+
+ var container = this.getImageView().container_;
+ var doc = container.ownerDocument;
+
+ this.domOverlay_ = doc.createElement('div');
+ this.domOverlay_.className = 'crop-overlay';
+ container.appendChild(this.domOverlay_);
+
+ this.shadowTop_ = doc.createElement('div');
+ this.shadowTop_.className = 'shadow';
+ this.domOverlay_.appendChild(this.shadowTop_);
+
+ this.middleBox_ = doc.createElement('div');
+ this.middleBox_.className = 'middle-box';
+ this.domOverlay_.appendChild(this.middleBox_);
+
+ this.shadowLeft_ = doc.createElement('div');
+ this.shadowLeft_.className = 'shadow';
+ this.middleBox_.appendChild(this.shadowLeft_);
+
+ this.cropFrame_ = doc.createElement('div');
+ this.cropFrame_.className = 'crop-frame';
+ this.middleBox_.appendChild(this.cropFrame_);
+
+ this.shadowRight_ = doc.createElement('div');
+ this.shadowRight_.className = 'shadow';
+ this.middleBox_.appendChild(this.shadowRight_);
+
+ this.shadowBottom_ = doc.createElement('div');
+ this.shadowBottom_.className = 'shadow';
+ this.domOverlay_.appendChild(this.shadowBottom_);
+
+ var cropFrame = this.cropFrame_;
+ function addCropFrame(className) {
+ var div = doc.createElement('div');
+ div.className = className;
+ cropFrame.appendChild(div);
+ }
+
+ addCropFrame('left top corner');
+ addCropFrame('top horizontal');
+ addCropFrame('right top corner');
+ addCropFrame('left vertical');
+ addCropFrame('right vertical');
+ addCropFrame('left bottom corner');
+ addCropFrame('bottom horizontal');
+ addCropFrame('right bottom corner');
+
+ this.onResizedBound_ = this.onResized_.bind(this);
+ window.addEventListener('resize', this.onResizedBound_);
+
+ this.createDefaultCrop();
+};
+
+/**
+ * Handles resizing of the window and updates the crop rectangle.
+ * @private
+ */
+ImageEditor.Mode.Crop.prototype.onResized_ = function() {
+ this.positionDOM();
+};
+
+/**
+ * TODO(JSDOC).
+ */
+ImageEditor.Mode.Crop.prototype.reset = function() {
+ ImageEditor.Mode.prototype.reset.call(this);
+ this.createDefaultCrop();
+};
+
+/**
+ * TODO(JSDOC).
+ */
+ImageEditor.Mode.Crop.prototype.positionDOM = function() {
+ var screenClipped = this.viewport_.getScreenClipped();
+
+ var screenCrop = this.viewport_.imageToScreenRect(this.cropRect_.getRect());
+ var delta = ImageEditor.Mode.Crop.MOUSE_GRAB_RADIUS;
+ this.editor_.hideOverlappingTools(
+ screenCrop.inflate(delta, delta),
+ screenCrop.inflate(-delta, -delta));
+
+ this.domOverlay_.style.left = screenClipped.left + 'px';
+ this.domOverlay_.style.top = screenClipped.top + 'px';
+ this.domOverlay_.style.width = screenClipped.width + 'px';
+ this.domOverlay_.style.height = screenClipped.height + 'px';
+
+ this.shadowLeft_.style.width = screenCrop.left - screenClipped.left + 'px';
+
+ this.shadowTop_.style.height = screenCrop.top - screenClipped.top + 'px';
+
+ this.shadowRight_.style.width = screenClipped.left + screenClipped.width -
+ (screenCrop.left + screenCrop.width) + 'px';
+
+ this.shadowBottom_.style.height = screenClipped.top + screenClipped.height -
+ (screenCrop.top + screenCrop.height) + 'px';
+};
+
+/**
+ * TODO(JSDOC).
+ */
+ImageEditor.Mode.Crop.prototype.cleanUpUI = function() {
+ ImageEditor.Mode.prototype.cleanUpUI.apply(this, arguments);
+ this.domOverlay_.parentNode.removeChild(this.domOverlay_);
+ this.domOverlay_ = null;
+ this.editor_.hideOverlappingTools();
+ window.removeEventListener(this.onResizedBound_);
+ this.onResizedBound_ = null;
+};
+
+/**
+ * @const
+ * @type {number}
+ */
+ImageEditor.Mode.Crop.MOUSE_GRAB_RADIUS = 6;
+/**
+ * @const
+ * @type {number}
+ */
+ImageEditor.Mode.Crop.TOUCH_GRAB_RADIUS = 20;
+
+/**
+ * TODO(JSDOC).
+ * @return {Command.Crop} // TODO(JSDOC).
+ */
+ImageEditor.Mode.Crop.prototype.getCommand = function() {
+ var cropImageRect = this.cropRect_.getRect();
+ return new Command.Crop(cropImageRect);
+};
+
+/**
+ * TODO(JSDOC).
+ */
+ImageEditor.Mode.Crop.prototype.createDefaultCrop = function() {
+ var rect = new Rect(this.getViewport().getImageClipped());
+ rect = rect.inflate(
+ -Math.round(rect.width / 6), -Math.round(rect.height / 6));
+ this.cropRect_ = new DraggableRect(rect, this.getViewport());
+ this.positionDOM();
+};
+
+/**
+ * TODO(JSDOC).
+ * @param {number} x X coordinate for cursor.
+ * @param {number} y Y coordinate for cursor.
+ * @param {boolean} mouseDown If mouse button is down.
+ * @return {string} A value for style.cursor CSS property.
+ */
+ImageEditor.Mode.Crop.prototype.getCursorStyle = function(x, y, mouseDown) {
+ return this.cropRect_.getCursorStyle(x, y, mouseDown);
+};
+
+/**
+ * TODO(JSDOC).
+ * @param {number} x Event X coordinate.
+ * @param {number} y Event Y coordinate.
+ * @param {boolean} touch True if it's a touch event, false if mouse.
+ * @return {function(number,number)} A function to be called on mouse drag.
+ */
+ImageEditor.Mode.Crop.prototype.getDragHandler = function(x, y, touch) {
+ var cropDragHandler = this.cropRect_.getDragHandler(x, y, touch);
+ if (!cropDragHandler) return null;
+
+ var self = this;
+ return function(x, y) {
+ cropDragHandler(x, y);
+ self.markUpdated();
+ self.positionDOM();
+ };
+};
+
+/**
+ * TODO(JSDOC).
+ * @param {number} x X coordinate of the event.
+ * @param {number} y Y coordinate of the event.
+ * @return {ImageBuffer.DoubleTapAction} Action to perform as result.
+ */
+ImageEditor.Mode.Crop.prototype.getDoubleTapAction = function(x, y) {
+ return this.cropRect_.getDoubleTapAction(x, y);
+};
+
+/*
+ * A draggable rectangle over the image.
+ * @param {Rect} rect // TODO(JSDOC).
+ * @param {Viewport} viewport // TODO(JSDOC).
+ * @constructor
+ */
+function DraggableRect(rect, viewport) {
+ // The bounds are not held in a regular rectangle (with width/height).
+ // left/top/right/bottom held instead for convenience.
+ this.bounds_ = {};
+ this.bounds_[DraggableRect.LEFT] = rect.left;
+ this.bounds_[DraggableRect.RIGHT] = rect.left + rect.width;
+ this.bounds_[DraggableRect.TOP] = rect.top;
+ this.bounds_[DraggableRect.BOTTOM] = rect.top + rect.height;
+
+ this.viewport_ = viewport;
+
+ this.oppositeSide_ = {};
+ this.oppositeSide_[DraggableRect.LEFT] = DraggableRect.RIGHT;
+ this.oppositeSide_[DraggableRect.RIGHT] = DraggableRect.LEFT;
+ this.oppositeSide_[DraggableRect.TOP] = DraggableRect.BOTTOM;
+ this.oppositeSide_[DraggableRect.BOTTOM] = DraggableRect.TOP;
+
+ // Translation table to form CSS-compatible cursor style.
+ this.cssSide_ = {};
+ this.cssSide_[DraggableRect.LEFT] = 'w';
+ this.cssSide_[DraggableRect.TOP] = 'n';
+ this.cssSide_[DraggableRect.RIGHT] = 'e';
+ this.cssSide_[DraggableRect.BOTTOM] = 's';
+ this.cssSide_[DraggableRect.NONE] = '';
+}
+
+// Static members to simplify reflective access to the bounds.
+/**
+ * @const
+ * @type {string}
+ */
+DraggableRect.LEFT = 'left';
+/**
+ * @const
+ * @type {string}
+ */
+DraggableRect.RIGHT = 'right';
+/**
+ * @const
+ * @type {string}
+ */
+DraggableRect.TOP = 'top';
+/**
+ * @const
+ * @type {string}
+ */
+DraggableRect.BOTTOM = 'bottom';
+/**
+ * @const
+ * @type {string}
+ */
+DraggableRect.NONE = 'none';
+
+/**
+ * TODO(JSDOC)
+ * @return {number} // TODO(JSDOC).
+ */
+DraggableRect.prototype.getLeft = function() {
+ return this.bounds_[DraggableRect.LEFT];
+};
+
+/**
+ * TODO(JSDOC)
+ * @return {number} // TODO(JSDOC).
+ */
+DraggableRect.prototype.getRight = function() {
+ return this.bounds_[DraggableRect.RIGHT];
+};
+
+/**
+ * TODO(JSDOC)
+ * @return {number} // TODO(JSDOC).
+ */
+DraggableRect.prototype.getTop = function() {
+ return this.bounds_[DraggableRect.TOP];
+};
+
+/**
+ * TODO(JSDOC)
+ * @return {number} // TODO(JSDOC).
+ */
+DraggableRect.prototype.getBottom = function() {
+ return this.bounds_[DraggableRect.BOTTOM];
+};
+
+/**
+ * TODO(JSDOC)
+ * @return {Rect} // TODO(JSDOC).
+ */
+DraggableRect.prototype.getRect = function() {
+ return new Rect(this.bounds_);
+};
+
+/**
+ * TODO(JSDOC)
+ * @param {number} x X coordinate for cursor.
+ * @param {number} y Y coordinate for cursor.
+ * @param {boolean} touch // TODO(JSDOC).
+ * @return {Object} // TODO(JSDOC).
+ */
+DraggableRect.prototype.getDragMode = function(x, y, touch) {
+ var result = {
+ xSide: DraggableRect.NONE,
+ ySide: DraggableRect.NONE
+ };
+
+ var bounds = this.bounds_;
+ var R = this.viewport_.screenToImageSize(
+ touch ? ImageEditor.Mode.Crop.TOUCH_GRAB_RADIUS :
+ ImageEditor.Mode.Crop.MOUSE_GRAB_RADIUS);
+
+ var circle = new Circle(x, y, R);
+
+ var xBetween = ImageUtil.between(bounds.left, x, bounds.right);
+ var yBetween = ImageUtil.between(bounds.top, y, bounds.bottom);
+
+ if (circle.inside(bounds.left, bounds.top)) {
+ result.xSide = DraggableRect.LEFT;
+ result.ySide = DraggableRect.TOP;
+ } else if (circle.inside(bounds.left, bounds.bottom)) {
+ result.xSide = DraggableRect.LEFT;
+ result.ySide = DraggableRect.BOTTOM;
+ } else if (circle.inside(bounds.right, bounds.top)) {
+ result.xSide = DraggableRect.RIGHT;
+ result.ySide = DraggableRect.TOP;
+ } else if (circle.inside(bounds.right, bounds.bottom)) {
+ result.xSide = DraggableRect.RIGHT;
+ result.ySide = DraggableRect.BOTTOM;
+ } else if (yBetween && Math.abs(x - bounds.left) <= R) {
+ result.xSide = DraggableRect.LEFT;
+ } else if (yBetween && Math.abs(x - bounds.right) <= R) {
+ result.xSide = DraggableRect.RIGHT;
+ } else if (xBetween && Math.abs(y - bounds.top) <= R) {
+ result.ySide = DraggableRect.TOP;
+ } else if (xBetween && Math.abs(y - bounds.bottom) <= R) {
+ result.ySide = DraggableRect.BOTTOM;
+ } else if (xBetween && yBetween) {
+ result.whole = true;
+ } else {
+ result.newcrop = true;
+ result.xSide = DraggableRect.RIGHT;
+ result.ySide = DraggableRect.BOTTOM;
+ }
+
+ return result;
+};
+
+/**
+ * TODO(JSDOC)
+ * @param {number} x X coordinate for cursor.
+ * @param {number} y Y coordinate for cursor.
+ * @param {boolean} mouseDown If mouse button is down.
+ * @return {string} // TODO(JSDOC).
+ */
+DraggableRect.prototype.getCursorStyle = function(x, y, mouseDown) {
+ var mode;
+ if (mouseDown) {
+ mode = this.dragMode_;
+ } else {
+ mode = this.getDragMode(
+ this.viewport_.screenToImageX(x), this.viewport_.screenToImageY(y));
+ }
+ if (mode.whole) return 'move';
+ if (mode.newcrop) return 'crop';
+ return this.cssSide_[mode.ySide] + this.cssSide_[mode.xSide] + '-resize';
+};
+
+/**
+ * TODO(JSDOC)
+ * @param {number} x X coordinate for cursor.
+ * @param {number} y Y coordinate for cursor.
+ * @param {boolean} touch // TODO(JSDOC).
+ * @return {function(number,number)} // TODO(JSDOC).
+ */
+DraggableRect.prototype.getDragHandler = function(x, y, touch) {
+ x = this.viewport_.screenToImageX(x);
+ y = this.viewport_.screenToImageY(y);
+
+ var clipRect = this.viewport_.getImageClipped();
+ if (!clipRect.inside(x, y)) return null;
+
+ this.dragMode_ = this.getDragMode(x, y, touch);
+
+ var self = this;
+
+ var mouseBiasX;
+ var mouseBiasY;
+
+ var fixedWidth = 0;
+ var fixedHeight = 0;
+
+ var resizeFuncX;
+ var resizeFuncY;
+
+ if (this.dragMode_.whole) {
+ mouseBiasX = this.bounds_.left - x;
+ fixedWidth = this.bounds_.right - this.bounds_.left;
+ resizeFuncX = function(x) {
+ self.bounds_.left = x;
+ self.bounds_.right = self.bounds_.left + fixedWidth;
+ };
+ mouseBiasY = this.bounds_.top - y;
+ fixedHeight = this.bounds_.bottom - this.bounds_.top;
+ resizeFuncY = function(y) {
+ self.bounds_.top = y;
+ self.bounds_.bottom = self.bounds_.top + fixedHeight;
+ };
+ } else {
+ var checkNewCrop = function() {
+ if (self.dragMode_.newcrop) {
+ self.dragMode_.newcrop = false;
+ self.bounds_.left = self.bounds_.right = x;
+ self.bounds_.top = self.bounds_.bottom = y;
+ mouseBiasX = 0;
+ mouseBiasY = 0;
+ }
+ };
+
+ var flipSide = function(side) {
+ var opposite = self.oppositeSide_[side];
+ var temp = self.bounds_[side];
+ self.bounds_[side] = self.bounds_[opposite];
+ self.bounds_[opposite] = temp;
+ return opposite;
+ };
+
+ if (this.dragMode_.xSide != DraggableRect.NONE) {
+ mouseBiasX = self.bounds_[this.dragMode_.xSide] - x;
+ resizeFuncX = function(x) {
+ checkNewCrop();
+ self.bounds_[self.dragMode_.xSide] = x;
+ if (self.bounds_.left > self.bounds_.right) {
+ self.dragMode_.xSide = flipSide(self.dragMode_.xSide);
+ }
+ };
+ }
+ if (this.dragMode_.ySide != DraggableRect.NONE) {
+ mouseBiasY = self.bounds_[this.dragMode_.ySide] - y;
+ resizeFuncY = function(y) {
+ checkNewCrop();
+ self.bounds_[self.dragMode_.ySide] = y;
+ if (self.bounds_.top > self.bounds_.bottom) {
+ self.dragMode_.ySide = flipSide(self.dragMode_.ySide);
+ }
+ };
+ }
+ }
+
+ function convertX(x) {
+ return ImageUtil.clamp(
+ clipRect.left,
+ self.viewport_.screenToImageX(x) + mouseBiasX,
+ clipRect.left + clipRect.width - fixedWidth);
+ }
+
+ function convertY(y) {
+ return ImageUtil.clamp(
+ clipRect.top,
+ self.viewport_.screenToImageY(y) + mouseBiasY,
+ clipRect.top + clipRect.height - fixedHeight);
+ }
+
+ return function(x, y) {
+ if (resizeFuncX) resizeFuncX(convertX(x));
+ if (resizeFuncY) resizeFuncY(convertY(y));
+ };
+};
+
+/**
+ * TODO(JSDOC)
+ * @param {number} x X coordinate for cursor.
+ * @param {number} y Y coordinate for cursor.
+ * @param {boolean} touch // TODO(JSDOC).
+ * @return {ImageBuffer.DoubleTapAction} // TODO(JSDOC).
+ */
+DraggableRect.prototype.getDoubleTapAction = function(x, y, touch) {
+ x = this.viewport_.screenToImageX(x);
+ y = this.viewport_.screenToImageY(y);
+
+ var clipRect = this.viewport_.getImageClipped();
+ if (clipRect.inside(x, y))
+ return ImageBuffer.DoubleTapAction.COMMIT;
+ else
+ return ImageBuffer.DoubleTapAction.NOTHING;
+};
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/image_util.js b/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/image_util.js
new file mode 100644
index 00000000000..f088f7c6c83
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/image_util.js
@@ -0,0 +1,701 @@
+// 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.
+
+'use strict';
+
+
+// Namespace object for the utilities.
+function ImageUtil() {}
+
+/**
+ * Performance trace.
+ */
+ImageUtil.trace = (function() {
+ function PerformanceTrace() {
+ this.lines_ = {};
+ this.timers_ = {};
+ this.container_ = null;
+ }
+
+ PerformanceTrace.prototype.bindToDOM = function(container) {
+ this.container_ = container;
+ };
+
+ PerformanceTrace.prototype.report = function(key, value) {
+ if (!(key in this.lines_)) {
+ if (this.container_) {
+ var div = this.lines_[key] = document.createElement('div');
+ this.container_.appendChild(div);
+ } else {
+ this.lines_[key] = {};
+ }
+ }
+ this.lines_[key].textContent = key + ': ' + value;
+ if (ImageUtil.trace.log) this.dumpLine(key);
+ };
+
+ PerformanceTrace.prototype.resetTimer = function(key) {
+ this.timers_[key] = Date.now();
+ };
+
+ PerformanceTrace.prototype.reportTimer = function(key) {
+ this.report(key, (Date.now() - this.timers_[key]) + 'ms');
+ };
+
+ PerformanceTrace.prototype.dump = function() {
+ for (var key in this.lines_)
+ this.dumpLine(key);
+ };
+
+ PerformanceTrace.prototype.dumpLine = function(key) {
+ console.log('trace.' + this.lines_[key].textContent);
+ };
+
+ return new PerformanceTrace();
+})();
+
+/**
+ * @param {number} min Minimum value.
+ * @param {number} value Value to adjust.
+ * @param {number} max Maximum value.
+ * @return {number} The closest to the |value| number in span [min, max].
+ */
+ImageUtil.clamp = function(min, value, max) {
+ return Math.max(min, Math.min(max, value));
+};
+
+/**
+ * @param {number} min Minimum value.
+ * @param {number} value Value to check.
+ * @param {number} max Maximum value.
+ * @return {boolean} True if value is between.
+ */
+ImageUtil.between = function(min, value, max) {
+ return (value - min) * (value - max) <= 0;
+};
+
+/**
+ * Rectangle class.
+ */
+
+/**
+ * Rectangle constructor takes 0, 1, 2 or 4 arguments.
+ * Supports following variants:
+ * new Rect(left, top, width, height)
+ * new Rect(width, height)
+ * new Rect(rect) // anything with left, top, width, height properties
+ * new Rect(bounds) // anything with left, top, right, bottom properties
+ * new Rect(canvas|image) // anything with width and height properties.
+ * new Rect() // empty rectangle.
+ * @constructor
+ */
+function Rect() {
+ switch (arguments.length) {
+ case 4:
+ this.left = arguments[0];
+ this.top = arguments[1];
+ this.width = arguments[2];
+ this.height = arguments[3];
+ return;
+
+ case 2:
+ this.left = 0;
+ this.top = 0;
+ this.width = arguments[0];
+ this.height = arguments[1];
+ return;
+
+ case 1: {
+ var source = arguments[0];
+ if ('left' in source && 'top' in source) {
+ this.left = source.left;
+ this.top = source.top;
+ if ('right' in source && 'bottom' in source) {
+ this.width = source.right - source.left;
+ this.height = source.bottom - source.top;
+ return;
+ }
+ } else {
+ this.left = 0;
+ this.top = 0;
+ }
+ if ('width' in source && 'height' in source) {
+ this.width = source.width;
+ this.height = source.height;
+ return;
+ }
+ break; // Fall through to the error message.
+ }
+
+ case 0:
+ this.left = 0;
+ this.top = 0;
+ this.width = 0;
+ this.height = 0;
+ return;
+ }
+ console.error('Invalid Rect constructor arguments:',
+ Array.apply(null, arguments));
+}
+
+/**
+ * @param {number} factor Factor to scale.
+ * @return {Rect} A rectangle with every dimension scaled.
+ */
+Rect.prototype.scale = function(factor) {
+ return new Rect(
+ this.left * factor,
+ this.top * factor,
+ this.width * factor,
+ this.height * factor);
+};
+
+/**
+ * @param {number} dx Difference in X.
+ * @param {number} dy Difference in Y.
+ * @return {Rect} A rectangle shifted by (dx,dy), same size.
+ */
+Rect.prototype.shift = function(dx, dy) {
+ return new Rect(this.left + dx, this.top + dy, this.width, this.height);
+};
+
+/**
+ * @param {number} x Coordinate of the left top corner.
+ * @param {number} y Coordinate of the left top corner.
+ * @return {Rect} A rectangle with left==x and top==y, same size.
+ */
+Rect.prototype.moveTo = function(x, y) {
+ return new Rect(x, y, this.width, this.height);
+};
+
+/**
+ * @param {number} dx Difference in X.
+ * @param {number} dy Difference in Y.
+ * @return {Rect} A rectangle inflated by (dx, dy), same center.
+ */
+Rect.prototype.inflate = function(dx, dy) {
+ return new Rect(
+ this.left - dx, this.top - dy, this.width + 2 * dx, this.height + 2 * dy);
+};
+
+/**
+ * @param {number} x Coordinate of the point.
+ * @param {number} y Coordinate of the point.
+ * @return {boolean} True if the point lies inside the rectangle.
+ */
+Rect.prototype.inside = function(x, y) {
+ return this.left <= x && x < this.left + this.width &&
+ this.top <= y && y < this.top + this.height;
+};
+
+/**
+ * @param {Rect} rect Rectangle to check.
+ * @return {boolean} True if this rectangle intersects with the |rect|.
+ */
+Rect.prototype.intersects = function(rect) {
+ return (this.left + this.width) > rect.left &&
+ (rect.left + rect.width) > this.left &&
+ (this.top + this.height) > rect.top &&
+ (rect.top + rect.height) > this.top;
+};
+
+/**
+ * @param {Rect} rect Rectangle to check.
+ * @return {boolean} True if this rectangle containing the |rect|.
+ */
+Rect.prototype.contains = function(rect) {
+ return (this.left <= rect.left) &&
+ (rect.left + rect.width) <= (this.left + this.width) &&
+ (this.top <= rect.top) &&
+ (rect.top + rect.height) <= (this.top + this.height);
+};
+
+/**
+ * @return {boolean} True if rectangle is empty.
+ */
+Rect.prototype.isEmpty = function() {
+ return this.width === 0 || this.height === 0;
+};
+
+/**
+ * Clamp the rectangle to the bounds by moving it.
+ * Decrease the size only if necessary.
+ * @param {Rect} bounds Bounds.
+ * @return {Rect} Calculated rectangle.
+ */
+Rect.prototype.clamp = function(bounds) {
+ var rect = new Rect(this);
+
+ if (rect.width > bounds.width) {
+ rect.left = bounds.left;
+ rect.width = bounds.width;
+ } else if (rect.left < bounds.left) {
+ rect.left = bounds.left;
+ } else if (rect.left + rect.width >
+ bounds.left + bounds.width) {
+ rect.left = bounds.left + bounds.width - rect.width;
+ }
+
+ if (rect.height > bounds.height) {
+ rect.top = bounds.top;
+ rect.height = bounds.height;
+ } else if (rect.top < bounds.top) {
+ rect.top = bounds.top;
+ } else if (rect.top + rect.height >
+ bounds.top + bounds.height) {
+ rect.top = bounds.top + bounds.height - rect.height;
+ }
+
+ return rect;
+};
+
+/**
+ * @return {string} String representation.
+ */
+Rect.prototype.toString = function() {
+ return '(' + this.left + ',' + this.top + '):' +
+ '(' + (this.left + this.width) + ',' + (this.top + this.height) + ')';
+};
+/*
+ * Useful shortcuts for drawing (static functions).
+ */
+
+/**
+ * Draw the image in context with appropriate scaling.
+ * @param {CanvasRenderingContext2D} context Context to draw.
+ * @param {Image} image Image to draw.
+ * @param {Rect=} opt_dstRect Rectangle in the canvas (whole canvas by default).
+ * @param {Rect=} opt_srcRect Rectangle in the image (whole image by default).
+ */
+Rect.drawImage = function(context, image, opt_dstRect, opt_srcRect) {
+ opt_dstRect = opt_dstRect || new Rect(context.canvas);
+ opt_srcRect = opt_srcRect || new Rect(image);
+ if (opt_dstRect.isEmpty() || opt_srcRect.isEmpty())
+ return;
+ context.drawImage(image,
+ opt_srcRect.left, opt_srcRect.top, opt_srcRect.width, opt_srcRect.height,
+ opt_dstRect.left, opt_dstRect.top, opt_dstRect.width, opt_dstRect.height);
+};
+
+/**
+ * Draw a box around the rectangle.
+ * @param {CanvasRenderingContext2D} context Context to draw.
+ * @param {Rect} rect Rectangle.
+ */
+Rect.outline = function(context, rect) {
+ context.strokeRect(
+ rect.left - 0.5, rect.top - 0.5, rect.width + 1, rect.height + 1);
+};
+
+/**
+ * Fill the rectangle.
+ * @param {CanvasRenderingContext2D} context Context to draw.
+ * @param {Rect} rect Rectangle.
+ */
+Rect.fill = function(context, rect) {
+ context.fillRect(rect.left, rect.top, rect.width, rect.height);
+};
+
+/**
+ * Fills the space between the two rectangles.
+ * @param {CanvasRenderingContext2D} context Context to draw.
+ * @param {Rect} inner Inner rectangle.
+ * @param {Rect} outer Outer rectangle.
+ */
+Rect.fillBetween = function(context, inner, outer) {
+ var innerRight = inner.left + inner.width;
+ var innerBottom = inner.top + inner.height;
+ var outerRight = outer.left + outer.width;
+ var outerBottom = outer.top + outer.height;
+ if (inner.top > outer.top) {
+ context.fillRect(
+ outer.left, outer.top, outer.width, inner.top - outer.top);
+ }
+ if (inner.left > outer.left) {
+ context.fillRect(
+ outer.left, inner.top, inner.left - outer.left, inner.height);
+ }
+ if (inner.width < outerRight) {
+ context.fillRect(
+ innerRight, inner.top, outerRight - innerRight, inner.height);
+ }
+ if (inner.height < outerBottom) {
+ context.fillRect(
+ outer.left, innerBottom, outer.width, outerBottom - innerBottom);
+ }
+};
+
+/**
+ * Circle class.
+ * @param {number} x X coordinate of circle center.
+ * @param {number} y Y coordinate of circle center.
+ * @param {number} r Radius.
+ * @constructor
+ */
+function Circle(x, y, r) {
+ this.x = x;
+ this.y = y;
+ this.squaredR = r * r;
+}
+
+/**
+ * Check if the point is inside the circle.
+ * @param {number} x X coordinate of the point.
+ * @param {number} y Y coordinate of the point.
+ * @return {boolean} True if the point is inside.
+ */
+Circle.prototype.inside = function(x, y) {
+ x -= this.x;
+ y -= this.y;
+ return x * x + y * y <= this.squaredR;
+};
+
+/**
+ * Copy an image applying scaling and rotation.
+ *
+ * @param {HTMLCanvasElement} dst Destination.
+ * @param {HTMLCanvasElement|HTMLImageElement} src Source.
+ * @param {number} scaleX Y scale transformation.
+ * @param {number} scaleY X scale transformation.
+ * @param {number} angle (in radians).
+ */
+ImageUtil.drawImageTransformed = function(dst, src, scaleX, scaleY, angle) {
+ var context = dst.getContext('2d');
+ context.save();
+ context.translate(context.canvas.width / 2, context.canvas.height / 2);
+ context.rotate(angle);
+ context.scale(scaleX, scaleY);
+ context.drawImage(src, -src.width / 2, -src.height / 2);
+ context.restore();
+};
+
+/**
+ * Adds or removes an attribute to/from an HTML element.
+ * @param {HTMLElement} element To be applied to.
+ * @param {string} attribute Name of attribute.
+ * @param {boolean} on True if add, false if remove.
+ */
+ImageUtil.setAttribute = function(element, attribute, on) {
+ if (on)
+ element.setAttribute(attribute, '');
+ else
+ element.removeAttribute(attribute);
+};
+
+/**
+ * Adds or removes CSS class to/from an HTML element.
+ * @param {HTMLElement} element To be applied to.
+ * @param {string} className Name of CSS class.
+ * @param {boolean} on True if add, false if remove.
+ */
+ImageUtil.setClass = function(element, className, on) {
+ var cl = element.classList;
+ if (on)
+ cl.add(className);
+ else
+ cl.remove(className);
+};
+
+/**
+ * ImageLoader loads an image from a given Entry into a canvas in two steps:
+ * 1. Loads the image into an HTMLImageElement.
+ * 2. Copies pixels from HTMLImageElement to HTMLCanvasElement. This is done
+ * stripe-by-stripe to avoid freezing up the UI. The transform is taken into
+ * account.
+ *
+ * @param {HTMLDocument} document Owner document.
+ * @param {MetadataCache=} opt_metadataCache Metadata cache. Required for
+ * caching. If not passed, caching will be disabled.
+ * @constructor
+ */
+ImageUtil.ImageLoader = function(document, opt_metadataCache) {
+ this.document_ = document;
+ this.metadataCache_ = opt_metadataCache || null;
+ this.image_ = new Image();
+ this.generation_ = 0;
+};
+
+/**
+ * Max size of image to be displayed (in pixels)
+ */
+ImageUtil.ImageLoader.IMAGE_SIZE_LIMIT = 25 * 1000 * 1000;
+
+/**
+ * @param {number} width Width of the image.
+ * @param {number} height Height of the image.
+ * @return {boolean} True if the image is too large to be loaded.
+ */
+ImageUtil.ImageLoader.isTooLarge = function(width, height) {
+ return width * height > ImageUtil.ImageLoader.IMAGE_SIZE_LIMIT;
+};
+
+/**
+ * Loads an image.
+ * TODO(mtomasz): Simplify, or even get rid of this class and merge with the
+ * ThumbnaiLoader class.
+ *
+ * @param {FileEntry} entry Image entry to be loaded.
+ * @param {function(function(object))} transformFetcher function to get
+ * the image transform (which we need for the image orientation).
+ * @param {function(HTMLCanvasElement, string=)} callback Callback to be
+ * called when loaded. The second optional argument is an error identifier.
+ * @param {number=} opt_delay Load delay in milliseconds, useful to let the
+ * animations play out before the computation heavy image loading starts.
+ */
+ImageUtil.ImageLoader.prototype.load = function(
+ entry, transformFetcher, callback, opt_delay) {
+ this.cancel();
+
+ this.entry_ = entry;
+ this.callback_ = callback;
+
+ // The transform fetcher is not cancellable so we need a generation counter.
+ var generation = ++this.generation_;
+ var onTransform = function(image, transform) {
+ if (generation === this.generation_) {
+ this.convertImage_(
+ image, transform || { scaleX: 1, scaleY: 1, rotate90: 0});
+ }
+ };
+
+ var onError = function(opt_error) {
+ this.image_.onerror = null;
+ this.image_.onload = null;
+ var tmpCallback = this.callback_;
+ this.callback_ = null;
+ var emptyCanvas = this.document_.createElement('canvas');
+ emptyCanvas.width = 0;
+ emptyCanvas.height = 0;
+ tmpCallback(emptyCanvas, opt_error);
+ }.bind(this);
+
+ var loadImage = function(opt_metadata) {
+ ImageUtil.metrics.startInterval(ImageUtil.getMetricName('LoadTime'));
+ this.timeout_ = null;
+
+ this.image_.onload = function(e) {
+ this.image_.onerror = null;
+ this.image_.onload = null;
+ if (ImageUtil.ImageLoader.isTooLarge(this.image_.width,
+ this.image_.height)) {
+ onError('GALLERY_IMAGE_TOO_BIG_ERROR');
+ return;
+ }
+ transformFetcher(entry, onTransform.bind(this, e.target));
+ }.bind(this);
+
+ // The error callback has an optional error argument, which in case of a
+ // general error should not be specified
+ this.image_.onerror = onError.bind(this, 'GALLERY_IMAGE_ERROR');
+
+ // Extract the last modification date to determine if the cached image
+ // is outdated.
+ var modificationTime = opt_metadata &&
+ opt_metadata.modificationTime &&
+ opt_metadata.modificationTime.getTime();
+
+ // Load the image directly.
+ this.image_.src = entry.toURL();
+ }.bind(this);
+
+ // Loads the image. If already loaded, then forces a reload.
+ var startLoad = this.resetImage_.bind(this, function() {
+ // Fetch metadata to detect last modification time for the caching purpose.
+ if (this.metadataCache_)
+ this.metadataCache_.get(entry, 'filesystem', loadImage);
+ else
+ loadImage();
+ }.bind(this), onError);
+
+ if (opt_delay) {
+ this.timeout_ = setTimeout(startLoad, opt_delay);
+ } else {
+ startLoad();
+ }
+};
+
+/**
+ * Resets the image by forcing the garbage collection and clearing the src
+ * attribute.
+ *
+ * @param {function()} onSuccess Success callback.
+ * @param {function(opt_string)} onError Failure callback with an optional
+ * error identifier.
+ * @private
+ */
+ImageUtil.ImageLoader.prototype.resetImage_ = function(onSuccess, onError) {
+ var clearSrc = function() {
+ this.image_.onload = onSuccess;
+ this.image_.onerror = onSuccess;
+ this.image_.src = '';
+ }.bind(this);
+
+ var emptyImage = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAA' +
+ 'AAABAAEAAAICTAEAOw==';
+
+ if (this.image_.src !== emptyImage) {
+ // Load an empty image, then clear src.
+ this.image_.onload = clearSrc;
+ this.image_.onerror = onError.bind(this, 'GALLERY_IMAGE_ERROR');
+ this.image_.src = emptyImage;
+ } else {
+ // Empty image already loaded, so clear src immediately.
+ clearSrc();
+ }
+};
+
+/**
+ * @return {boolean} True if an image is loading.
+ */
+ImageUtil.ImageLoader.prototype.isBusy = function() {
+ return !!this.callback_;
+};
+
+/**
+ * @param {Entry} entry Image entry.
+ * @return {boolean} True if loader loads this image.
+ */
+ImageUtil.ImageLoader.prototype.isLoading = function(entry) {
+ return this.isBusy() && util.isSameEntry(this.entry_, entry);
+};
+
+/**
+ * @param {function} callback To be called when the image loaded.
+ */
+ImageUtil.ImageLoader.prototype.setCallback = function(callback) {
+ this.callback_ = callback;
+};
+
+/**
+ * Stops loading image.
+ */
+ImageUtil.ImageLoader.prototype.cancel = function() {
+ if (!this.callback_) return;
+ this.callback_ = null;
+ if (this.timeout_) {
+ clearTimeout(this.timeout_);
+ this.timeout_ = null;
+ }
+ if (this.image_) {
+ this.image_.onload = function() {};
+ this.image_.onerror = function() {};
+ this.image_.src = '';
+ }
+ this.generation_++; // Silence the transform fetcher if it is in progress.
+};
+
+/**
+ * @param {HTMLImageElement} image Image to be transformed.
+ * @param {Object} transform transformation description to apply to the image.
+ * @private
+ */
+ImageUtil.ImageLoader.prototype.convertImage_ = function(image, transform) {
+ var canvas = this.document_.createElement('canvas');
+
+ if (transform.rotate90 & 1) { // Rotated +/-90deg, swap the dimensions.
+ canvas.width = image.height;
+ canvas.height = image.width;
+ } else {
+ canvas.width = image.width;
+ canvas.height = image.height;
+ }
+
+ var context = canvas.getContext('2d');
+ context.save();
+ context.translate(canvas.width / 2, canvas.height / 2);
+ context.rotate(transform.rotate90 * Math.PI / 2);
+ context.scale(transform.scaleX, transform.scaleY);
+
+ var stripCount = Math.ceil(image.width * image.height / (1 << 21));
+ var step = Math.max(16, Math.ceil(image.height / stripCount)) & 0xFFFFF0;
+
+ this.copyStrip_(context, image, 0, step);
+};
+
+/**
+ * @param {CanvasRenderingContext2D} context Context to draw.
+ * @param {HTMLImageElement} image Image to draw.
+ * @param {number} firstRow Number of the first pixel row to draw.
+ * @param {number} rowCount Count of pixel rows to draw.
+ * @private
+ */
+ImageUtil.ImageLoader.prototype.copyStrip_ = function(
+ context, image, firstRow, rowCount) {
+ var lastRow = Math.min(firstRow + rowCount, image.height);
+
+ context.drawImage(
+ image, 0, firstRow, image.width, lastRow - firstRow,
+ -image.width / 2, firstRow - image.height / 2,
+ image.width, lastRow - firstRow);
+
+ if (lastRow === image.height) {
+ context.restore();
+ if (this.entry_.toURL().substr(0, 5) !== 'data:') { // Ignore data urls.
+ ImageUtil.metrics.recordInterval(ImageUtil.getMetricName('LoadTime'));
+ }
+ try {
+ setTimeout(this.callback_, 0, context.canvas);
+ } catch (e) {
+ console.error(e);
+ }
+ this.callback_ = null;
+ } else {
+ var self = this;
+ this.timeout_ = setTimeout(
+ function() {
+ self.timeout_ = null;
+ self.copyStrip_(context, image, lastRow, rowCount);
+ }, 0);
+ }
+};
+
+/**
+ * @param {HTMLElement} element To remove children from.
+ */
+ImageUtil.removeChildren = function(element) {
+ element.textContent = '';
+};
+
+/**
+ * @param {string} name File name (with extension).
+ * @return {string} File name without extension.
+ */
+ImageUtil.getDisplayNameFromName = function(name) {
+ var index = name.lastIndexOf('.');
+ if (index !== -1)
+ return name.substr(0, index);
+ else
+ return name;
+};
+
+/**
+ * @param {string} name File name.
+ * @return {string} File extension.
+ */
+ImageUtil.getExtensionFromFullName = function(name) {
+ var index = name.lastIndexOf('.');
+ if (index !== -1)
+ return name.substring(index);
+ else
+ return '';
+};
+
+/**
+ * Metrics (from metrics.js) itnitialized by the File Manager from owner frame.
+ * @type {Object?}
+ */
+ImageUtil.metrics = null;
+
+/**
+ * @param {string} name Local name.
+ * @return {string} Full name.
+ */
+ImageUtil.getMetricName = function(name) {
+ return 'PhotoEditor.' + name;
+};
+
+/**
+ * Used for metrics reporting, keep in sync with the histogram description.
+ */
+ImageUtil.FILE_TYPES = ['jpg', 'png', 'gif', 'bmp', 'webp'];
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/image_view.js b/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/image_view.js
new file mode 100644
index 00000000000..24d70045798
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/image_view.js
@@ -0,0 +1,1065 @@
+// 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.
+
+'use strict';
+
+/**
+ * The overlay displaying the image.
+ *
+ * @param {HTMLElement} container The container element.
+ * @param {Viewport} viewport The viewport.
+ * @param {MetadataCache} metadataCache The metadataCache.
+ * @constructor
+ */
+function ImageView(container, viewport, metadataCache) {
+ this.container_ = container;
+ this.viewport_ = viewport;
+ this.document_ = container.ownerDocument;
+ this.contentGeneration_ = 0;
+ this.displayedContentGeneration_ = 0;
+ this.displayedViewportGeneration_ = 0;
+
+ this.imageLoader_ = new ImageUtil.ImageLoader(this.document_, metadataCache);
+ // We have a separate image loader for prefetch which does not get cancelled
+ // when the selection changes.
+ this.prefetchLoader_ = new ImageUtil.ImageLoader(
+ this.document_, metadataCache);
+
+ // The content cache is used for prefetching the next image when going
+ // through the images sequentially. The real life photos can be large
+ // (18Mpix = 72Mb pixel array) so we want only the minimum amount of caching.
+ this.contentCache_ = new ImageView.Cache(2);
+
+ // We reuse previously generated screen-scale images so that going back to
+ // a recently loaded image looks instant even if the image is not in
+ // the content cache any more. Screen-scale images are small (~1Mpix)
+ // so we can afford to cache more of them.
+ this.screenCache_ = new ImageView.Cache(5);
+ this.contentCallbacks_ = [];
+
+ /**
+ * The element displaying the current content.
+ *
+ * @type {HTMLCanvasElement|HTMLVideoElement}
+ * @private
+ */
+ this.screenImage_ = null;
+
+ this.localImageTransformFetcher_ = function(entry, callback) {
+ metadataCache.get(entry, 'fetchedMedia', function(fetchedMedia) {
+ callback(fetchedMedia.imageTransform);
+ });
+ };
+}
+
+/**
+ * Duration of transition between modes in ms.
+ */
+ImageView.MODE_TRANSITION_DURATION = 350;
+
+/**
+ * If the user flips though images faster than this interval we do not apply
+ * the slide-in/slide-out transition.
+ */
+ImageView.FAST_SCROLL_INTERVAL = 300;
+
+/**
+ * Image load type: full resolution image loaded from cache.
+ */
+ImageView.LOAD_TYPE_CACHED_FULL = 0;
+
+/**
+ * Image load type: screen resolution preview loaded from cache.
+ */
+ImageView.LOAD_TYPE_CACHED_SCREEN = 1;
+
+/**
+ * Image load type: image read from file.
+ */
+ImageView.LOAD_TYPE_IMAGE_FILE = 2;
+
+/**
+ * Image load type: video loaded.
+ */
+ImageView.LOAD_TYPE_VIDEO_FILE = 3;
+
+/**
+ * Image load type: error occurred.
+ */
+ImageView.LOAD_TYPE_ERROR = 4;
+
+/**
+ * Image load type: the file contents is not available offline.
+ */
+ImageView.LOAD_TYPE_OFFLINE = 5;
+
+/**
+ * The total number of load types.
+ */
+ImageView.LOAD_TYPE_TOTAL = 6;
+
+ImageView.prototype = {__proto__: ImageBuffer.Overlay.prototype};
+
+/**
+ * Draws below overlays with the default zIndex.
+ * @return {number} Z-index.
+ */
+ImageView.prototype.getZIndex = function() { return -1 };
+
+/**
+ * Draws the image on screen.
+ */
+ImageView.prototype.draw = function() {
+ if (!this.contentCanvas_) // Do nothing if the image content is not set.
+ return;
+
+ var forceRepaint = false;
+
+ if (this.displayedViewportGeneration_ !==
+ this.viewport_.getCacheGeneration()) {
+ this.displayedViewportGeneration_ = this.viewport_.getCacheGeneration();
+
+ this.setupDeviceBuffer(this.screenImage_);
+
+ forceRepaint = true;
+ }
+
+ if (forceRepaint ||
+ this.displayedContentGeneration_ !== this.contentGeneration_) {
+ this.displayedContentGeneration_ = this.contentGeneration_;
+
+ ImageUtil.trace.resetTimer('paint');
+ this.paintDeviceRect(this.viewport_.getDeviceClipped(),
+ this.contentCanvas_, this.viewport_.getImageClipped());
+ ImageUtil.trace.reportTimer('paint');
+ }
+};
+
+/**
+ * @param {number} x X pointer position.
+ * @param {number} y Y pointer position.
+ * @param {boolean} mouseDown True if mouse is down.
+ * @return {string} CSS cursor style.
+ */
+ImageView.prototype.getCursorStyle = function(x, y, mouseDown) {
+ // Indicate that the image is draggable.
+ if (this.viewport_.isClipped() &&
+ this.viewport_.getScreenClipped().inside(x, y))
+ return 'move';
+
+ return null;
+};
+
+/**
+ * @param {number} x X pointer position.
+ * @param {number} y Y pointer position.
+ * @return {function} The closure to call on drag.
+ */
+ImageView.prototype.getDragHandler = function(x, y) {
+ var cursor = this.getCursorStyle(x, y);
+ if (cursor === 'move') {
+ // Return the handler that drags the entire image.
+ return this.viewport_.createOffsetSetter(x, y);
+ }
+
+ return null;
+};
+
+/**
+ * @return {number} The cache generation.
+ */
+ImageView.prototype.getCacheGeneration = function() {
+ return this.contentGeneration_;
+};
+
+/**
+ * Invalidates the caches to force redrawing the screen canvas.
+ */
+ImageView.prototype.invalidateCaches = function() {
+ this.contentGeneration_++;
+};
+
+/**
+ * @return {HTMLCanvasElement} The content canvas element.
+ */
+ImageView.prototype.getCanvas = function() { return this.contentCanvas_ };
+
+/**
+ * @return {boolean} True if the a valid image is currently loaded.
+ */
+ImageView.prototype.hasValidImage = function() {
+ return !this.preview_ && this.contentCanvas_ && this.contentCanvas_.width;
+};
+
+/**
+ * @return {HTMLVideoElement} The video element.
+ */
+ImageView.prototype.getVideo = function() { return this.videoElement_ };
+
+/**
+ * @return {HTMLCanvasElement} The cached thumbnail image.
+ */
+ImageView.prototype.getThumbnail = function() { return this.thumbnailCanvas_ };
+
+/**
+ * @return {number} The content revision number.
+ */
+ImageView.prototype.getContentRevision = function() {
+ return this.contentRevision_;
+};
+
+/**
+ * Copies an image fragment from a full resolution canvas to a device resolution
+ * canvas.
+ *
+ * @param {Rect} deviceRect Rectangle in the device coordinates.
+ * @param {HTMLCanvasElement} canvas Full resolution canvas.
+ * @param {Rect} imageRect Rectangle in the full resolution canvas.
+ */
+ImageView.prototype.paintDeviceRect = function(deviceRect, canvas, imageRect) {
+ // Map screen canvas (0,0) to (deviceBounds.left, deviceBounds.top)
+ var deviceBounds = this.viewport_.getDeviceClipped();
+ deviceRect = deviceRect.shift(-deviceBounds.left, -deviceBounds.top);
+
+ // The source canvas may have different physical size than the image size
+ // set at the viewport. Adjust imageRect accordingly.
+ var bounds = this.viewport_.getImageBounds();
+ var scaleX = canvas.width / bounds.width;
+ var scaleY = canvas.height / bounds.height;
+ imageRect = new Rect(imageRect.left * scaleX, imageRect.top * scaleY,
+ imageRect.width * scaleX, imageRect.height * scaleY);
+ Rect.drawImage(
+ this.screenImage_.getContext('2d'), canvas, deviceRect, imageRect);
+};
+
+/**
+ * Creates an overlay canvas with properties similar to the screen canvas.
+ * Useful for showing quick feedback when editing.
+ *
+ * @return {HTMLCanvasElement} Overlay canvas.
+ */
+ImageView.prototype.createOverlayCanvas = function() {
+ var canvas = this.document_.createElement('canvas');
+ canvas.className = 'image';
+ this.container_.appendChild(canvas);
+ return canvas;
+};
+
+/**
+ * Sets up the canvas as a buffer in the device resolution.
+ *
+ * @param {HTMLCanvasElement} canvas The buffer canvas.
+ */
+ImageView.prototype.setupDeviceBuffer = function(canvas) {
+ var deviceRect = this.viewport_.getDeviceClipped();
+
+ // Set the canvas position and size in device pixels.
+ if (canvas.width !== deviceRect.width)
+ canvas.width = deviceRect.width;
+
+ if (canvas.height !== deviceRect.height)
+ canvas.height = deviceRect.height;
+
+ canvas.style.left = deviceRect.left + 'px';
+ canvas.style.top = deviceRect.top + 'px';
+
+ // Scale the canvas down to screen pixels.
+ this.setTransform(canvas);
+};
+
+/**
+ * @return {ImageData} A new ImageData object with a copy of the content.
+ */
+ImageView.prototype.copyScreenImageData = function() {
+ return this.screenImage_.getContext('2d').getImageData(
+ 0, 0, this.screenImage_.width, this.screenImage_.height);
+};
+
+/**
+ * @return {boolean} True if the image is currently being loaded.
+ */
+ImageView.prototype.isLoading = function() {
+ return this.imageLoader_.isBusy();
+};
+
+/**
+ * Cancels the current image loading operation. The callbacks will be ignored.
+ */
+ImageView.prototype.cancelLoad = function() {
+ this.imageLoader_.cancel();
+};
+
+/**
+ * Loads and display a new image.
+ *
+ * Loads the thumbnail first, then replaces it with the main image.
+ * Takes into account the image orientation encoded in the metadata.
+ *
+ * @param {FileEntry} entry Image entry.
+ * @param {Object} metadata Metadata.
+ * @param {Object} effect Transition effect object.
+ * @param {function(number} displayCallback Called when the image is displayed
+ * (possibly as a prevew).
+ * @param {function(number} loadCallback Called when the image is fully loaded.
+ * The parameter is the load type.
+ */
+ImageView.prototype.load = function(entry, metadata, effect,
+ displayCallback, loadCallback) {
+ if (effect) {
+ // Skip effects when reloading repeatedly very quickly.
+ var time = Date.now();
+ if (this.lastLoadTime_ &&
+ (time - this.lastLoadTime_) < ImageView.FAST_SCROLL_INTERVAL) {
+ effect = null;
+ }
+ this.lastLoadTime_ = time;
+ }
+
+ metadata = metadata || {};
+
+ ImageUtil.metrics.startInterval(ImageUtil.getMetricName('DisplayTime'));
+
+ var self = this;
+
+ this.contentEntry_ = entry;
+ this.contentRevision_ = -1;
+
+ var loadingVideo = FileType.getMediaType(entry) === 'video';
+ if (loadingVideo) {
+ var video = this.document_.createElement('video');
+ var videoPreview = !!(metadata.thumbnail && metadata.thumbnail.url);
+ if (videoPreview) {
+ var thumbnailLoader = new ThumbnailLoader(
+ metadata.thumbnail.url,
+ ThumbnailLoader.LoaderType.CANVAS,
+ metadata);
+ thumbnailLoader.loadDetachedImage(function(success) {
+ if (success) {
+ var canvas = thumbnailLoader.getImage();
+ video.setAttribute('poster', canvas.toDataURL('image/jpeg'));
+ this.replace(video, effect); // Show the poster immediately.
+ if (displayCallback) displayCallback();
+ }
+ }.bind(this));
+ }
+
+ var onVideoLoad = function(error) {
+ video.removeEventListener('loadedmetadata', onVideoLoadSuccess);
+ video.removeEventListener('error', onVideoLoadError);
+ displayMainImage(ImageView.LOAD_TYPE_VIDEO_FILE, videoPreview, video,
+ error);
+ };
+ var onVideoLoadError = onVideoLoad.bind(this, 'GALLERY_VIDEO_ERROR');
+ var onVideoLoadSuccess = onVideoLoad.bind(this, null);
+
+ video.addEventListener('loadedmetadata', onVideoLoadSuccess);
+ video.addEventListener('error', onVideoLoadError);
+
+ video.src = entry.toURL();
+ video.load();
+ return;
+ }
+
+ // Cache has to be evicted in advance, so the returned cached image is not
+ // evicted later by the prefetched image.
+ this.contentCache_.evictLRU();
+
+ var cached = this.contentCache_.getItem(this.contentEntry_);
+ if (cached) {
+ displayMainImage(ImageView.LOAD_TYPE_CACHED_FULL,
+ false /* no preview */, cached);
+ } else {
+ var cachedScreen = this.screenCache_.getItem(this.contentEntry_);
+ var imageWidth = metadata.media && metadata.media.width ||
+ metadata.drive && metadata.drive.imageWidth;
+ var imageHeight = metadata.media && metadata.media.height ||
+ metadata.drive && metadata.drive.imageHeight;
+ if (cachedScreen) {
+ // We have a cached screen-scale canvas, use it instead of a thumbnail.
+ displayThumbnail(ImageView.LOAD_TYPE_CACHED_SCREEN, cachedScreen);
+ // As far as the user can tell the image is loaded. We still need to load
+ // the full res image to make editing possible, but we can report now.
+ ImageUtil.metrics.recordInterval(ImageUtil.getMetricName('DisplayTime'));
+ } else if ((!effect || (effect.constructor.name === 'Slide')) &&
+ metadata.thumbnail && metadata.thumbnail.url &&
+ !(imageWidth && imageHeight &&
+ ImageUtil.ImageLoader.isTooLarge(imageWidth, imageHeight))) {
+ // Only show thumbnails if there is no effect or the effect is Slide.
+ // Also no thumbnail if the image is too large to be loaded.
+ var thumbnailLoader = new ThumbnailLoader(
+ metadata.thumbnail.url,
+ ThumbnailLoader.LoaderType.CANVAS,
+ metadata);
+ thumbnailLoader.loadDetachedImage(function(success) {
+ displayThumbnail(ImageView.LOAD_TYPE_IMAGE_FILE,
+ success ? thumbnailLoader.getImage() : null);
+ });
+ } else {
+ loadMainImage(ImageView.LOAD_TYPE_IMAGE_FILE, entry,
+ false /* no preview*/, 0 /* delay */);
+ }
+ }
+
+ function displayThumbnail(loadType, canvas) {
+ if (canvas) {
+ self.replace(
+ canvas,
+ effect,
+ metadata.media.width || metadata.drive.imageWidth,
+ metadata.media.height || metadata.drive.imageHeight,
+ true /* preview */);
+ if (displayCallback) displayCallback();
+ }
+ loadMainImage(loadType, entry, !!canvas,
+ (effect && canvas) ? effect.getSafeInterval() : 0);
+ }
+
+ function loadMainImage(loadType, contentEntry, previewShown, delay) {
+ if (self.prefetchLoader_.isLoading(contentEntry)) {
+ // The image we need is already being prefetched. Initiating another load
+ // would be a waste. Hijack the load instead by overriding the callback.
+ self.prefetchLoader_.setCallback(
+ displayMainImage.bind(null, loadType, previewShown));
+
+ // Swap the loaders so that the self.isLoading works correctly.
+ var temp = self.prefetchLoader_;
+ self.prefetchLoader_ = self.imageLoader_;
+ self.imageLoader_ = temp;
+ return;
+ }
+ self.prefetchLoader_.cancel(); // The prefetch was doing something useless.
+
+ self.imageLoader_.load(
+ contentEntry,
+ self.localImageTransformFetcher_,
+ displayMainImage.bind(null, loadType, previewShown),
+ delay);
+ }
+
+ function displayMainImage(loadType, previewShown, content, opt_error) {
+ if (opt_error)
+ loadType = ImageView.LOAD_TYPE_ERROR;
+
+ // If we already displayed the preview we should not replace the content if:
+ // 1. The full content failed to load.
+ // or
+ // 2. We are loading a video (because the full video is displayed in the
+ // same HTML element as the preview).
+ var animationDuration = 0;
+ if (!(previewShown &&
+ (loadType === ImageView.LOAD_TYPE_ERROR ||
+ loadType === ImageView.LOAD_TYPE_VIDEO_FILE))) {
+ var replaceEffect = previewShown ? null : effect;
+ animationDuration = replaceEffect ? replaceEffect.getSafeInterval() : 0;
+ self.replace(content, replaceEffect);
+ if (!previewShown && displayCallback) displayCallback();
+ }
+
+ if (loadType !== ImageView.LOAD_TYPE_ERROR &&
+ loadType !== ImageView.LOAD_TYPE_CACHED_SCREEN) {
+ ImageUtil.metrics.recordInterval(ImageUtil.getMetricName('DisplayTime'));
+ }
+ ImageUtil.metrics.recordEnum(ImageUtil.getMetricName('LoadMode'),
+ loadType, ImageView.LOAD_TYPE_TOTAL);
+
+ if (loadType === ImageView.LOAD_TYPE_ERROR &&
+ !navigator.onLine && metadata.streaming) {
+ // |streaming| is set only when the file is not locally cached.
+ loadType = ImageView.LOAD_TYPE_OFFLINE;
+ }
+ if (loadCallback) loadCallback(loadType, animationDuration, opt_error);
+ }
+};
+
+/**
+ * Prefetches an image.
+ * @param {FileEntry} entry The image entry.
+ * @param {number} delay Image load delay in ms.
+ */
+ImageView.prototype.prefetch = function(entry, delay) {
+ var self = this;
+ function prefetchDone(canvas) {
+ if (canvas.width)
+ self.contentCache_.putItem(entry, canvas);
+ }
+
+ var cached = this.contentCache_.getItem(entry);
+ if (cached) {
+ prefetchDone(cached);
+ } else if (FileType.getMediaType(entry) === 'image') {
+ // Evict the LRU item before we allocate the new canvas to avoid unneeded
+ // strain on memory.
+ this.contentCache_.evictLRU();
+
+ this.prefetchLoader_.load(
+ entry,
+ this.localImageTransformFetcher_,
+ prefetchDone,
+ delay);
+ }
+};
+
+/**
+ * Renames the current image.
+ * @param {FileEntry} newEntry The new image Entry.
+ */
+ImageView.prototype.changeEntry = function(newEntry) {
+ this.contentCache_.renameItem(this.contentEntry_, newEntry);
+ this.screenCache_.renameItem(this.contentEntry_, newEntry);
+ this.contentEntry_ = newEntry;
+};
+
+/**
+ * Unloads content.
+ * @param {Rect} zoomToRect Target rectangle for zoom-out-effect.
+ */
+ImageView.prototype.unload = function(zoomToRect) {
+ if (this.unloadTimer_) {
+ clearTimeout(this.unloadTimer_);
+ this.unloadTimer_ = null;
+ }
+ if (zoomToRect && this.screenImage_) {
+ var effect = this.createZoomEffect(zoomToRect);
+ this.setTransform(this.screenImage_, effect);
+ this.screenImage_.setAttribute('fade', true);
+ this.unloadTimer_ = setTimeout(function() {
+ this.unloadTimer_ = null;
+ this.unload(null /* force unload */);
+ }.bind(this),
+ effect.getSafeInterval());
+ return;
+ }
+ this.container_.textContent = '';
+ this.contentCanvas_ = null;
+ this.screenImage_ = null;
+ this.videoElement_ = null;
+};
+
+/**
+ * @param {HTMLCanvasElement|HTMLVideoElement} content The image element.
+ * @param {number=} opt_width Image width.
+ * @param {number=} opt_height Image height.
+ * @param {boolean=} opt_preview True if the image is a preview (not full res).
+ * @private
+ */
+ImageView.prototype.replaceContent_ = function(
+ content, opt_width, opt_height, opt_preview) {
+
+ if (this.contentCanvas_ && this.contentCanvas_.parentNode === this.container_)
+ this.container_.removeChild(this.contentCanvas_);
+
+ if (content.constructor.name === 'HTMLVideoElement') {
+ this.contentCanvas_ = null;
+ this.videoElement_ = content;
+ this.screenImage_ = content;
+ this.screenImage_.className = 'image';
+ this.container_.appendChild(this.screenImage_);
+ this.videoElement_.play();
+ return;
+ }
+
+ this.screenImage_ = this.document_.createElement('canvas');
+ this.screenImage_.className = 'image';
+
+ this.videoElement_ = null;
+ this.contentCanvas_ = content;
+ this.invalidateCaches();
+ this.viewport_.setImageSize(
+ opt_width || this.contentCanvas_.width,
+ opt_height || this.contentCanvas_.height);
+ this.viewport_.fitImage();
+ this.viewport_.update();
+ this.draw();
+
+ this.container_.appendChild(this.screenImage_);
+
+ this.preview_ = opt_preview;
+ // If this is not a thumbnail, cache the content and the screen-scale image.
+ if (this.hasValidImage()) {
+ // Insert the full resolution canvas into DOM so that it can be printed.
+ this.container_.appendChild(this.contentCanvas_);
+ this.contentCanvas_.classList.add('fullres');
+
+ this.contentCache_.putItem(this.contentEntry_, this.contentCanvas_, true);
+ this.screenCache_.putItem(this.contentEntry_, this.screenImage_);
+
+ // TODO(kaznacheev): It is better to pass screenImage_ as it is usually
+ // much smaller than contentCanvas_ and still contains the entire image.
+ // Once we implement zoom/pan we should pass contentCanvas_ instead.
+ this.updateThumbnail_(this.screenImage_);
+
+ this.contentRevision_++;
+ for (var i = 0; i !== this.contentCallbacks_.length; i++) {
+ try {
+ this.contentCallbacks_[i]();
+ } catch (e) {
+ console.error(e);
+ }
+ }
+ }
+};
+
+/**
+ * Adds a listener for content changes.
+ * @param {function} callback Callback.
+ */
+ImageView.prototype.addContentCallback = function(callback) {
+ this.contentCallbacks_.push(callback);
+};
+
+/**
+ * Updates the cached thumbnail image.
+ *
+ * @param {HTMLCanvasElement} canvas The source canvas.
+ * @private
+ */
+ImageView.prototype.updateThumbnail_ = function(canvas) {
+ ImageUtil.trace.resetTimer('thumb');
+ var pixelCount = 10000;
+ var downScale =
+ Math.max(1, Math.sqrt(canvas.width * canvas.height / pixelCount));
+
+ this.thumbnailCanvas_ = canvas.ownerDocument.createElement('canvas');
+ this.thumbnailCanvas_.width = Math.round(canvas.width / downScale);
+ this.thumbnailCanvas_.height = Math.round(canvas.height / downScale);
+ Rect.drawImage(this.thumbnailCanvas_.getContext('2d'), canvas);
+ ImageUtil.trace.reportTimer('thumb');
+};
+
+/**
+ * Replaces the displayed image, possibly with slide-in animation.
+ *
+ * @param {HTMLCanvasElement|HTMLVideoElement} content The image element.
+ * @param {Object=} opt_effect Transition effect object.
+ * @param {number=} opt_width Image width.
+ * @param {number=} opt_height Image height.
+ * @param {boolean=} opt_preview True if the image is a preview (not full res).
+ */
+ImageView.prototype.replace = function(
+ content, opt_effect, opt_width, opt_height, opt_preview) {
+ var oldScreenImage = this.screenImage_;
+
+ this.replaceContent_(content, opt_width, opt_height, opt_preview);
+ if (!opt_effect) {
+ if (oldScreenImage)
+ oldScreenImage.parentNode.removeChild(oldScreenImage);
+ return;
+ }
+
+ var newScreenImage = this.screenImage_;
+
+ if (oldScreenImage)
+ ImageUtil.setAttribute(newScreenImage, 'fade', true);
+ this.setTransform(newScreenImage, opt_effect, 0 /* instant */);
+
+ setTimeout(function() {
+ this.setTransform(newScreenImage, null,
+ opt_effect && opt_effect.getDuration());
+ if (oldScreenImage) {
+ ImageUtil.setAttribute(newScreenImage, 'fade', false);
+ ImageUtil.setAttribute(oldScreenImage, 'fade', true);
+ console.assert(opt_effect.getReverse, 'Cannot revert an effect.');
+ var reverse = opt_effect.getReverse();
+ this.setTransform(oldScreenImage, reverse);
+ setTimeout(function() {
+ if (oldScreenImage.parentNode)
+ oldScreenImage.parentNode.removeChild(oldScreenImage);
+ }, reverse.getSafeInterval());
+ }
+ }.bind(this), 0);
+};
+
+/**
+ * @param {HTMLCanvasElement|HTMLVideoElement} element The element to transform.
+ * @param {ImageView.Effect=} opt_effect The effect to apply.
+ * @param {number=} opt_duration Transition duration.
+ */
+ImageView.prototype.setTransform = function(element, opt_effect, opt_duration) {
+ if (!opt_effect)
+ opt_effect = new ImageView.Effect.None();
+ if (typeof opt_duration !== 'number')
+ opt_duration = opt_effect.getDuration();
+ element.style.webkitTransitionDuration = opt_duration + 'ms';
+ element.style.webkitTransitionTimingFunction = opt_effect.getTiming();
+ element.style.webkitTransform = opt_effect.transform(element, this.viewport_);
+};
+
+/**
+ * @param {Rect} screenRect Target rectangle in screen coordinates.
+ * @return {ImageView.Effect.Zoom} Zoom effect object.
+ */
+ImageView.prototype.createZoomEffect = function(screenRect) {
+ return new ImageView.Effect.Zoom(
+ this.viewport_.screenToDeviceRect(screenRect),
+ null /* use viewport */,
+ ImageView.MODE_TRANSITION_DURATION);
+};
+
+/**
+ * Visualizes crop or rotate operation. Hide the old image instantly, animate
+ * the new image to visualize the operation.
+ *
+ * @param {HTMLCanvasElement} canvas New content canvas.
+ * @param {Rect} imageCropRect The crop rectangle in image coordinates.
+ * Null for rotation operations.
+ * @param {number} rotate90 Rotation angle in 90 degree increments.
+ * @return {number} Animation duration.
+ */
+ImageView.prototype.replaceAndAnimate = function(
+ canvas, imageCropRect, rotate90) {
+ var oldScale = this.viewport_.getScale();
+ var deviceCropRect = imageCropRect && this.viewport_.screenToDeviceRect(
+ this.viewport_.imageToScreenRect(imageCropRect));
+
+ var oldScreenImage = this.screenImage_;
+ this.replaceContent_(canvas);
+ var newScreenImage = this.screenImage_;
+
+ // Display the new canvas, initially transformed.
+ var deviceFullRect = this.viewport_.getDeviceClipped();
+
+ var effect = rotate90 ?
+ new ImageView.Effect.Rotate(
+ oldScale / this.viewport_.getScale(), -rotate90) :
+ new ImageView.Effect.Zoom(deviceCropRect, deviceFullRect);
+
+ this.setTransform(newScreenImage, effect, 0 /* instant */);
+
+ oldScreenImage.parentNode.appendChild(newScreenImage);
+ oldScreenImage.parentNode.removeChild(oldScreenImage);
+
+ // Let the layout fire, then animate back to non-transformed state.
+ setTimeout(
+ this.setTransform.bind(
+ this, newScreenImage, null, effect.getDuration()),
+ 0);
+
+ return effect.getSafeInterval();
+};
+
+/**
+ * Visualizes "undo crop". Shrink the current image to the given crop rectangle
+ * while fading in the new image.
+ *
+ * @param {HTMLCanvasElement} canvas New content canvas.
+ * @param {Rect} imageCropRect The crop rectangle in image coordinates.
+ * @return {number} Animation duration.
+ */
+ImageView.prototype.animateAndReplace = function(canvas, imageCropRect) {
+ var deviceFullRect = this.viewport_.getDeviceClipped();
+ var oldScale = this.viewport_.getScale();
+
+ var oldScreenImage = this.screenImage_;
+ this.replaceContent_(canvas);
+ var newScreenImage = this.screenImage_;
+
+ var deviceCropRect = this.viewport_.screenToDeviceRect(
+ this.viewport_.imageToScreenRect(imageCropRect));
+
+ var setFade = ImageUtil.setAttribute.bind(null, newScreenImage, 'fade');
+ setFade(true);
+ oldScreenImage.parentNode.insertBefore(newScreenImage, oldScreenImage);
+
+ var effect = new ImageView.Effect.Zoom(deviceCropRect, deviceFullRect);
+ // Animate to the transformed state.
+ this.setTransform(oldScreenImage, effect);
+
+ setTimeout(setFade.bind(null, false), 0);
+
+ setTimeout(function() {
+ if (oldScreenImage.parentNode)
+ oldScreenImage.parentNode.removeChild(oldScreenImage);
+ }, effect.getSafeInterval());
+
+ return effect.getSafeInterval();
+};
+
+
+/**
+ * Generic cache with a limited capacity and LRU eviction.
+ * @param {number} capacity Maximum number of cached item.
+ * @constructor
+ */
+ImageView.Cache = function(capacity) {
+ this.capacity_ = capacity;
+ this.map_ = {};
+ this.order_ = [];
+};
+
+/**
+ * Fetches the item from the cache.
+ * @param {FileEntry} entry The entry.
+ * @return {Object} The cached item.
+ */
+ImageView.Cache.prototype.getItem = function(entry) {
+ return this.map_[entry.toURL()];
+};
+
+/**
+ * Puts the item into the cache.
+ *
+ * @param {FileEntry} entry The entry.
+ * @param {Object} item The item object.
+ * @param {boolean=} opt_keepLRU True if the LRU order should not be modified.
+ */
+ImageView.Cache.prototype.putItem = function(entry, item, opt_keepLRU) {
+ var pos = this.order_.indexOf(entry.toURL());
+
+ if ((pos >= 0) !== (entry.toURL() in this.map_))
+ throw new Error('Inconsistent cache state');
+
+ if (entry.toURL() in this.map_) {
+ if (!opt_keepLRU) {
+ // Move to the end (most recently used).
+ this.order_.splice(pos, 1);
+ this.order_.push(entry.toURL());
+ }
+ } else {
+ this.evictLRU();
+ this.order_.push(entry.toURL());
+ }
+
+ if ((pos >= 0) && (item !== this.map_[entry.toURL()]))
+ this.deleteItem_(this.map_[entry.toURL()]);
+ this.map_[entry.toURL()] = item;
+
+ if (this.order_.length > this.capacity_)
+ throw new Error('Exceeded cache capacity');
+};
+
+/**
+ * Evicts the least recently used items.
+ */
+ImageView.Cache.prototype.evictLRU = function() {
+ if (this.order_.length === this.capacity_) {
+ var url = this.order_.shift();
+ this.deleteItem_(this.map_[url]);
+ delete this.map_[url];
+ }
+};
+
+/**
+ * Changes the Entry.
+ * @param {FileEntry} oldEntry The old Entry.
+ * @param {FileEntry} newEntry The new Entry.
+ */
+ImageView.Cache.prototype.renameItem = function(oldEntry, newEntry) {
+ if (util.isSameEntry(oldEntry, newEntry))
+ return; // No need to rename.
+
+ var pos = this.order_.indexOf(oldEntry.toURL());
+ if (pos < 0)
+ return; // Not cached.
+
+ this.order_[pos] = newEntry.toURL();
+ this.map_[newEntry.toURL()] = this.map_[oldEntry.toURL()];
+ delete this.map_[oldEntry.toURL()];
+};
+
+/**
+ * Disposes an object.
+ *
+ * @param {Object} item The item object.
+ * @private
+ */
+ImageView.Cache.prototype.deleteItem_ = function(item) {
+ // Trick to reduce memory usage without waiting for gc.
+ if (item instanceof HTMLCanvasElement) {
+ // If the canvas is being used somewhere else (eg. displayed on the screen),
+ // it will be cleared.
+ item.width = 0;
+ item.height = 0;
+ }
+};
+
+/* Transition effects */
+
+/**
+ * Base class for effects.
+ *
+ * @param {number} duration Duration in ms.
+ * @param {string=} opt_timing CSS transition timing function name.
+ * @constructor
+ */
+ImageView.Effect = function(duration, opt_timing) {
+ this.duration_ = duration;
+ this.timing_ = opt_timing || 'linear';
+};
+
+/**
+ *
+ */
+ImageView.Effect.DEFAULT_DURATION = 180;
+
+/**
+ *
+ */
+ImageView.Effect.MARGIN = 100;
+
+/**
+ * @return {number} Effect duration in ms.
+ */
+ImageView.Effect.prototype.getDuration = function() { return this.duration_ };
+
+/**
+ * @return {number} Delay in ms since the beginning of the animation after which
+ * it is safe to perform CPU-heavy operations without disrupting the animation.
+ */
+ImageView.Effect.prototype.getSafeInterval = function() {
+ return this.getDuration() + ImageView.Effect.MARGIN;
+};
+
+/**
+ * @return {string} CSS transition timing function name.
+ */
+ImageView.Effect.prototype.getTiming = function() { return this.timing_ };
+
+/**
+ * @param {HTMLCanvasElement|HTMLVideoElement} element Element.
+ * @return {number} Preferred pixel ration to use with this element.
+ * @private
+ */
+ImageView.Effect.getPixelRatio_ = function(element) {
+ if (element.constructor.name === 'HTMLCanvasElement')
+ return Viewport.getDevicePixelRatio();
+ else
+ return 1;
+};
+
+/**
+ * Default effect. It is not a no-op as it needs to adjust a canvas scale
+ * for devicePixelRatio.
+ *
+ * @constructor
+ */
+ImageView.Effect.None = function() {
+ ImageView.Effect.call(this, 0);
+};
+
+/**
+ * Inherits from ImageView.Effect.
+ */
+ImageView.Effect.None.prototype = { __proto__: ImageView.Effect.prototype };
+
+/**
+ * @param {HTMLCanvasElement|HTMLVideoElement} element Element.
+ * @return {string} Transform string.
+ */
+ImageView.Effect.None.prototype.transform = function(element) {
+ var ratio = ImageView.Effect.getPixelRatio_(element);
+ return 'scale(' + (1 / ratio) + ')';
+};
+
+/**
+ * Slide effect.
+ *
+ * @param {number} direction -1 for left, 1 for right.
+ * @param {boolean=} opt_slow True if slow (as in slideshow).
+ * @constructor
+ */
+ImageView.Effect.Slide = function Slide(direction, opt_slow) {
+ ImageView.Effect.call(this,
+ opt_slow ? 800 : ImageView.Effect.DEFAULT_DURATION, 'ease-in-out');
+ this.direction_ = direction;
+ this.slow_ = opt_slow;
+ this.shift_ = opt_slow ? 100 : 40;
+ if (this.direction_ < 0) this.shift_ = -this.shift_;
+};
+
+/**
+ * Inherits from ImageView.Effect.
+ */
+ImageView.Effect.Slide.prototype = { __proto__: ImageView.Effect.prototype };
+
+/**
+ * @return {ImageView.Effect.Slide} Reverse Slide effect.
+ */
+ImageView.Effect.Slide.prototype.getReverse = function() {
+ return new ImageView.Effect.Slide(-this.direction_, this.slow_);
+};
+
+/**
+ * @param {HTMLCanvasElement|HTMLVideoElement} element Element.
+ * @return {string} Transform string.
+ */
+ImageView.Effect.Slide.prototype.transform = function(element) {
+ var ratio = ImageView.Effect.getPixelRatio_(element);
+ return 'scale(' + (1 / ratio) + ') translate(' + this.shift_ + 'px, 0px)';
+};
+
+/**
+ * Zoom effect.
+ *
+ * Animates the original rectangle to the target rectangle. Both parameters
+ * should be given in device coordinates (accounting for devicePixelRatio).
+ *
+ * @param {Rect} deviceTargetRect Target rectangle.
+ * @param {Rect=} opt_deviceOriginalRect Original rectangle. If omitted,
+ * the full viewport will be used at the time of |transform| call.
+ * @param {number=} opt_duration Duration in ms.
+ * @constructor
+ */
+ImageView.Effect.Zoom = function(
+ deviceTargetRect, opt_deviceOriginalRect, opt_duration) {
+ ImageView.Effect.call(this,
+ opt_duration || ImageView.Effect.DEFAULT_DURATION);
+ this.target_ = deviceTargetRect;
+ this.original_ = opt_deviceOriginalRect;
+};
+
+/**
+ * Inherits from ImageView.Effect.
+ */
+ImageView.Effect.Zoom.prototype = { __proto__: ImageView.Effect.prototype };
+
+/**
+ * @param {HTMLCanvasElement|HTMLVideoElement} element Element.
+ * @param {Viewport} viewport Viewport.
+ * @return {string} Transform string.
+ */
+ImageView.Effect.Zoom.prototype.transform = function(element, viewport) {
+ if (!this.original_)
+ this.original_ = viewport.getDeviceClipped();
+
+ var ratio = ImageView.Effect.getPixelRatio_(element);
+
+ var dx = (this.target_.left + this.target_.width / 2) -
+ (this.original_.left + this.original_.width / 2);
+ var dy = (this.target_.top + this.target_.height / 2) -
+ (this.original_.top + this.original_.height / 2);
+
+ var scaleX = this.target_.width / this.original_.width;
+ var scaleY = this.target_.height / this.original_.height;
+
+ return 'translate(' + (dx / ratio) + 'px,' + (dy / ratio) + 'px) ' +
+ 'scaleX(' + (scaleX / ratio) + ') scaleY(' + (scaleY / ratio) + ')';
+};
+
+/**
+ * Rotate effect.
+ *
+ * @param {number} scale Scale.
+ * @param {number} rotate90 Rotation in 90 degrees increments.
+ * @constructor
+ */
+ImageView.Effect.Rotate = function(scale, rotate90) {
+ ImageView.Effect.call(this, ImageView.Effect.DEFAULT_DURATION);
+ this.scale_ = scale;
+ this.rotate90_ = rotate90;
+};
+
+/**
+ * Inherits from ImageView.Effect.
+ */
+ImageView.Effect.Rotate.prototype = { __proto__: ImageView.Effect.prototype };
+
+/**
+ * @param {HTMLCanvasElement|HTMLVideoElement} element Element.
+ * @return {string} Transform string.
+ */
+ImageView.Effect.Rotate.prototype.transform = function(element) {
+ var ratio = ImageView.Effect.getPixelRatio_(element);
+ return 'rotate(' + (this.rotate90_ * 90) + 'deg) ' +
+ 'scale(' + (this.scale_ / ratio) + ')';
+};
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/viewport.js b/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/viewport.js
new file mode 100644
index 00000000000..4d6b8ea0874
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/image_editor/viewport.js
@@ -0,0 +1,430 @@
+// 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.
+
+'use strict';
+
+/**
+ * Viewport class controls the way the image is displayed (scale, offset etc).
+ * @constructor
+ */
+function Viewport() {
+ this.imageBounds_ = new Rect();
+ this.screenBounds_ = new Rect();
+
+ this.scale_ = 1;
+ this.offsetX_ = 0;
+ this.offsetY_ = 0;
+
+ this.generation_ = 0;
+
+ this.scaleControl_ = null;
+ this.repaintCallbacks_ = [];
+ this.update();
+}
+
+/*
+ * Viewport modification.
+ */
+
+/**
+ * @param {Object} scaleControl The UI object responsible for scaling.
+ */
+Viewport.prototype.setScaleControl = function(scaleControl) {
+ this.scaleControl_ = scaleControl;
+};
+
+/**
+ * @param {number} width Image width.
+ * @param {number} height Image height.
+ */
+Viewport.prototype.setImageSize = function(width, height) {
+ this.imageBounds_ = new Rect(width, height);
+ if (this.scaleControl_) this.scaleControl_.displayImageSize(width, height);
+ this.invalidateCaches();
+};
+
+/**
+ * @param {number} width Screen width.
+ * @param {number} height Screen height.
+ */
+Viewport.prototype.setScreenSize = function(width, height) {
+ this.screenBounds_ = new Rect(width, height);
+ if (this.scaleControl_)
+ this.scaleControl_.setMinScale(this.getFittingScale());
+ this.invalidateCaches();
+};
+
+/**
+ * Set the size by an HTML element.
+ *
+ * @param {HTMLElement} frame The element acting as the "screen".
+ */
+Viewport.prototype.sizeByFrame = function(frame) {
+ this.setScreenSize(frame.clientWidth, frame.clientHeight);
+};
+
+/**
+ * Set the size and scale to fit an HTML element.
+ *
+ * @param {HTMLElement} frame The element acting as the "screen".
+ */
+Viewport.prototype.sizeByFrameAndFit = function(frame) {
+ var wasFitting = this.getScale() == this.getFittingScale();
+ this.sizeByFrame(frame);
+ var minScale = this.getFittingScale();
+ if (wasFitting || (this.getScale() < minScale)) {
+ this.setScale(minScale, true);
+ }
+};
+
+/**
+ * @return {number} Scale.
+ */
+Viewport.prototype.getScale = function() { return this.scale_ };
+
+/**
+ * @param {number} scale The new scale.
+ * @param {boolean} notify True if the change should be reflected in the UI.
+ */
+Viewport.prototype.setScale = function(scale, notify) {
+ if (this.scale_ == scale) return;
+ this.scale_ = scale;
+ if (notify && this.scaleControl_) this.scaleControl_.displayScale(scale);
+ this.invalidateCaches();
+};
+
+/**
+ * @return {number} Best scale to fit the current image into the current screen.
+ */
+Viewport.prototype.getFittingScale = function() {
+ var scaleX = this.screenBounds_.width / this.imageBounds_.width;
+ var scaleY = this.screenBounds_.height / this.imageBounds_.height;
+ // Scales > (1 / this.getDevicePixelRatio()) do not look good. Also they are
+ // not really useful as we do not have any pixel-level operations.
+ return Math.min(1 / Viewport.getDevicePixelRatio(), scaleX, scaleY);
+};
+
+/**
+ * Set the scale to fit the image into the screen.
+ */
+Viewport.prototype.fitImage = function() {
+ var scale = this.getFittingScale();
+ if (this.scaleControl_) this.scaleControl_.setMinScale(scale);
+ this.setScale(scale, true);
+};
+
+/**
+ * @return {number} X-offset of the viewport.
+ */
+Viewport.prototype.getOffsetX = function() { return this.offsetX_ };
+
+/**
+ * @return {number} Y-offset of the viewport.
+ */
+Viewport.prototype.getOffsetY = function() { return this.offsetY_ };
+
+/**
+ * Set the image offset in the viewport.
+ * @param {number} x X-offset.
+ * @param {number} y Y-offset.
+ * @param {boolean} ignoreClipping True if no clipping should be applied.
+ */
+Viewport.prototype.setOffset = function(x, y, ignoreClipping) {
+ if (!ignoreClipping) {
+ x = this.clampOffsetX_(x);
+ y = this.clampOffsetY_(y);
+ }
+ if (this.offsetX_ == x && this.offsetY_ == y) return;
+ this.offsetX_ = x;
+ this.offsetY_ = y;
+ this.invalidateCaches();
+};
+
+/**
+ * Return a closure that can be called to pan the image.
+ * Useful for implementing non-trivial variants of panning (overview etc).
+ * @param {number} originalX The x coordinate on the screen canvas that
+ * corresponds to zero change to offsetX.
+ * @param {number} originalY The y coordinate on the screen canvas that
+ * corresponds to zero change to offsetY.
+ * @param {function():number} scaleFunc returns the image to screen scale.
+ * @param {function(number,number):boolean} hitFunc returns true if (x,y) is
+ * in the valid region.
+ * @return {function} The closure to pan the image.
+ */
+Viewport.prototype.createOffsetSetter = function(
+ originalX, originalY, scaleFunc, hitFunc) {
+ var originalOffsetX = this.offsetX_;
+ var originalOffsetY = this.offsetY_;
+ if (!hitFunc) hitFunc = function() { return true };
+ if (!scaleFunc) scaleFunc = this.getScale.bind(this);
+
+ var self = this;
+ return function(x, y) {
+ if (hitFunc(x, y)) {
+ var scale = scaleFunc();
+ self.setOffset(
+ originalOffsetX + (x - originalX) / scale,
+ originalOffsetY + (y - originalY) / scale);
+ self.repaint();
+ }
+ };
+};
+
+/*
+ * Access to the current viewport state.
+ */
+
+/**
+ * @return {Rect} The image bounds in image coordinates.
+ */
+Viewport.prototype.getImageBounds = function() { return this.imageBounds_ };
+
+/**
+* @return {Rect} The screen bounds in screen coordinates.
+*/
+Viewport.prototype.getScreenBounds = function() { return this.screenBounds_ };
+
+/**
+ * @return {Rect} The visible part of the image, in image coordinates.
+ */
+Viewport.prototype.getImageClipped = function() { return this.imageClipped_ };
+
+/**
+ * @return {Rect} The visible part of the image, in screen coordinates.
+ */
+Viewport.prototype.getScreenClipped = function() { return this.screenClipped_ };
+
+/**
+ * A counter that is incremented with each viewport state change.
+ * Clients that cache anything that depends on the viewport state should keep
+ * track of this counter.
+ * @return {number} counter.
+ */
+Viewport.prototype.getCacheGeneration = function() { return this.generation_ };
+
+/**
+ * Called on event view port state change (even if repaint has not been called).
+ */
+Viewport.prototype.invalidateCaches = function() { this.generation_++ };
+
+/**
+ * @return {Rect} The image bounds in screen coordinates.
+ */
+Viewport.prototype.getImageBoundsOnScreen = function() {
+ return this.imageOnScreen_;
+};
+
+/*
+ * Conversion between the screen and image coordinate spaces.
+ */
+
+/**
+ * @param {number} size Size in screen coordinates.
+ * @return {number} Size in image coordinates.
+ */
+Viewport.prototype.screenToImageSize = function(size) {
+ return size / this.getScale();
+};
+
+/**
+ * @param {number} x X in screen coordinates.
+ * @return {number} X in image coordinates.
+ */
+Viewport.prototype.screenToImageX = function(x) {
+ return Math.round((x - this.imageOnScreen_.left) / this.getScale());
+};
+
+/**
+ * @param {number} y Y in screen coordinates.
+ * @return {number} Y in image coordinates.
+ */
+Viewport.prototype.screenToImageY = function(y) {
+ return Math.round((y - this.imageOnScreen_.top) / this.getScale());
+};
+
+/**
+ * @param {Rect} rect Rectangle in screen coordinates.
+ * @return {Rect} Rectangle in image coordinates.
+ */
+Viewport.prototype.screenToImageRect = function(rect) {
+ return new Rect(
+ this.screenToImageX(rect.left),
+ this.screenToImageY(rect.top),
+ this.screenToImageSize(rect.width),
+ this.screenToImageSize(rect.height));
+};
+
+/**
+ * @param {number} size Size in image coordinates.
+ * @return {number} Size in screen coordinates.
+ */
+Viewport.prototype.imageToScreenSize = function(size) {
+ return size * this.getScale();
+};
+
+/**
+ * @param {number} x X in image coordinates.
+ * @return {number} X in screen coordinates.
+ */
+Viewport.prototype.imageToScreenX = function(x) {
+ return Math.round(this.imageOnScreen_.left + x * this.getScale());
+};
+
+/**
+ * @param {number} y Y in image coordinates.
+ * @return {number} Y in screen coordinates.
+ */
+Viewport.prototype.imageToScreenY = function(y) {
+ return Math.round(this.imageOnScreen_.top + y * this.getScale());
+};
+
+/**
+ * @param {Rect} rect Rectangle in image coordinates.
+ * @return {Rect} Rectangle in screen coordinates.
+ */
+Viewport.prototype.imageToScreenRect = function(rect) {
+ return new Rect(
+ this.imageToScreenX(rect.left),
+ this.imageToScreenY(rect.top),
+ Math.round(this.imageToScreenSize(rect.width)),
+ Math.round(this.imageToScreenSize(rect.height)));
+};
+
+/**
+ * @return {number} The number of physical pixels in one CSS pixel.
+ */
+Viewport.getDevicePixelRatio = function() { return window.devicePixelRatio };
+
+/**
+ * Convert a rectangle from screen coordinates to 'device' coordinates.
+ *
+ * This conversion enlarges the original rectangle devicePixelRatio times
+ * with the screen center as a fixed point.
+ *
+ * @param {Rect} rect Rectangle in screen coordinates.
+ * @return {Rect} Rectangle in device coordinates.
+ */
+Viewport.prototype.screenToDeviceRect = function(rect) {
+ var ratio = Viewport.getDevicePixelRatio();
+ var screenCenterX = Math.round(
+ this.screenBounds_.left + this.screenBounds_.width / 2);
+ var screenCenterY = Math.round(
+ this.screenBounds_.top + this.screenBounds_.height / 2);
+ return new Rect(screenCenterX + (rect.left - screenCenterX) * ratio,
+ screenCenterY + (rect.top - screenCenterY) * ratio,
+ rect.width * ratio,
+ rect.height * ratio);
+};
+
+/**
+ * @return {Rect} The visible part of the image, in device coordinates.
+ */
+Viewport.prototype.getDeviceClipped = function() {
+ return this.screenToDeviceRect(this.getScreenClipped());
+};
+
+/**
+ * @return {boolean} True if some part of the image is clipped by the screen.
+ */
+Viewport.prototype.isClipped = function() {
+ return this.getMarginX_() < 0 || this.getMarginY_() < 0;
+};
+
+/**
+ * @return {number} Horizontal margin.
+ * Negative if the image is clipped horizontally.
+ * @private
+ */
+Viewport.prototype.getMarginX_ = function() {
+ return Math.round(
+ (this.screenBounds_.width - this.imageBounds_.width * this.scale_) / 2);
+};
+
+/**
+ * @return {number} Vertical margin.
+ * Negative if the image is clipped vertically.
+ * @private
+ */
+Viewport.prototype.getMarginY_ = function() {
+ return Math.round(
+ (this.screenBounds_.height - this.imageBounds_.height * this.scale_) / 2);
+};
+
+/**
+ * @param {number} x X-offset.
+ * @return {number} X-offset clamped to the valid range.
+ * @private
+ */
+Viewport.prototype.clampOffsetX_ = function(x) {
+ var limit = Math.round(Math.max(0, -this.getMarginX_() / this.getScale()));
+ return ImageUtil.clamp(-limit, x, limit);
+};
+
+/**
+ * @param {number} y Y-offset.
+ * @return {number} Y-offset clamped to the valid range.
+ * @private
+ */
+Viewport.prototype.clampOffsetY_ = function(y) {
+ var limit = Math.round(Math.max(0, -this.getMarginY_() / this.getScale()));
+ return ImageUtil.clamp(-limit, y, limit);
+};
+
+/**
+ * Recalculate the viewport parameters.
+ */
+Viewport.prototype.update = function() {
+ var scale = this.getScale();
+
+ // Image bounds in screen coordinates.
+ this.imageOnScreen_ = new Rect(
+ this.getMarginX_(),
+ this.getMarginY_(),
+ Math.round(this.imageBounds_.width * scale),
+ Math.round(this.imageBounds_.height * scale));
+
+ // A visible part of the image in image coordinates.
+ this.imageClipped_ = new Rect(this.imageBounds_);
+
+ // A visible part of the image in screen coordinates.
+ this.screenClipped_ = new Rect(this.screenBounds_);
+
+ // Adjust for the offset.
+ if (this.imageOnScreen_.left < 0) {
+ this.imageOnScreen_.left +=
+ Math.round(this.clampOffsetX_(this.offsetX_) * scale);
+ this.imageClipped_.left = Math.round(-this.imageOnScreen_.left / scale);
+ this.imageClipped_.width = Math.round(this.screenBounds_.width / scale);
+ } else {
+ this.screenClipped_.left = this.imageOnScreen_.left;
+ this.screenClipped_.width = this.imageOnScreen_.width;
+ }
+
+ if (this.imageOnScreen_.top < 0) {
+ this.imageOnScreen_.top +=
+ Math.round(this.clampOffsetY_(this.offsetY_) * scale);
+ this.imageClipped_.top = Math.round(-this.imageOnScreen_.top / scale);
+ this.imageClipped_.height = Math.round(this.screenBounds_.height / scale);
+ } else {
+ this.screenClipped_.top = this.imageOnScreen_.top;
+ this.screenClipped_.height = this.imageOnScreen_.height;
+ }
+};
+
+/**
+ * @param {function} callback Repaint callback.
+ */
+Viewport.prototype.addRepaintCallback = function(callback) {
+ this.repaintCallbacks_.push(callback);
+};
+
+/**
+ * Repaint all clients.
+ */
+Viewport.prototype.repaint = function() {
+ this.update();
+ for (var i = 0; i != this.repaintCallbacks_.length; i++)
+ this.repaintCallbacks_[i]();
+};
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/main.js b/chromium/chrome/browser/resources/file_manager/foreground/js/main.js
new file mode 100644
index 00000000000..f32c0a1ef47
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/main.js
@@ -0,0 +1,41 @@
+// 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.
+
+'use strict';
+
+/**
+ * @type {FileManager}
+ */
+var fileManager;
+
+/**
+ * Indicates if the DOM and scripts have been already loaded.
+ * @type {boolean}
+ */
+var pageLoaded = false;
+
+/**
+ * Kick off the file manager dialog.
+ * Called by main.html after the DOM has been parsed.
+ */
+function init() {
+ // Initializes UI and starts the File Manager dialog.
+ fileManager.initializeUI(document.body, function() {
+ chrome.test.sendMessage('ready');
+ metrics.recordInterval('Load.Total');
+ });
+}
+
+// Create the File Manager object. Note, that the DOM, nor any external
+// scripts may not be ready yet.
+fileManager = new FileManager();
+
+// Initialize the core stuff, which doesn't require access to DOM nor to
+// additional scripts.
+fileManager.initializeCore();
+
+// Final initialization is performed after all scripts and Dom is loaded.
+util.addPageLoadHandler(init);
+
+metrics.recordInterval('Load.Script'); // Must be the last line.
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/main_scripts.js b/chromium/chrome/browser/resources/file_manager/foreground/js/main_scripts.js
new file mode 100644
index 00000000000..768d35ce8fd
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/main_scripts.js
@@ -0,0 +1,132 @@
+// 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.
+
+// The include directives are put into Javascript-style comments to prevent
+// parsing errors in non-flattened mode. The flattener still sees them.
+// Note that this makes the flattener to comment out the first line of the
+// included file but that's all right since any javascript file should start
+// with a copyright comment anyway.
+
+// If you add a new dependency, you should update build files by rerunning
+// gyp. Otherwise, you'll be bitten by a dependency issue like:
+//
+// 1) You add a new dependency to "whatever.js"
+// 2) You make changes in "whatever.js"
+// 3) Rebuild "resources.pak" and open Files.app
+// 4) You don't see the changes in "whatever.js". Why is that?
+//
+// Because the dependencies are computed at gyp time, the existing build
+// files don't know that "resources.pak" now has a dependency to
+// "whatever.js". You should rerun gyp to let the build files know.
+//
+// //metrics.js initiates load performance tracking
+// //so we want to parse it as early as possible.
+//<include src="metrics.js"/>
+//
+//<include src="../../../image_loader/image_loader_client.js"/>
+//
+//<include src="../../../../../../ui/webui/resources/js/load_time_data.js"/>
+//<include src="../../../../../../ui/webui/resources/js/cr.js"/>
+//<include src="../../../../../../ui/webui/resources/js/util.js"/>
+//<include src="../../../../../../ui/webui/resources/js/i18n_template_no_process.js"/>
+//
+//<include src="../../../../../../ui/webui/resources/js/event_tracker.js"/>
+//<include src="../../../../../../ui/webui/resources/js/cr/ui.js"/>
+//<include src="../../../../../../ui/webui/resources/js/cr/event_target.js"/>
+//<include src="../../../../../../ui/webui/resources/js/cr/ui/touch_handler.js"/>
+//<include src="../../../../../../ui/webui/resources/js/cr/ui/array_data_model.js"/>
+//<include src="../../../../../../ui/webui/resources/js/cr/ui/dialogs.js"/>
+//<include src="../../../../../../ui/webui/resources/js/cr/ui/list_item.js"/>
+//<include src="../../../../../../ui/webui/resources/js/cr/ui/list_selection_model.js"/>
+//<include src="../../../../../../ui/webui/resources/js/cr/ui/list_single_selection_model.js"/>
+//<include src="../../../../../../ui/webui/resources/js/cr/ui/list_selection_controller.js"/>
+//<include src="../../../../../../ui/webui/resources/js/cr/ui/list.js"/>
+//<include src="../../../../../../ui/webui/resources/js/cr/ui/tree.js"/>
+//<include src="../../../../../../ui/webui/resources/js/cr/ui/autocomplete_list.js"/>
+//
+//<include src="../../../../../../ui/webui/resources/js/cr/ui/splitter.js"/>
+//<include src="../../../../../../ui/webui/resources/js/cr/ui/table/table_splitter.js"/>
+//
+//<include src="../../../../../../ui/webui/resources/js/cr/ui/table/table_column.js"/>
+//<include src="../../../../../../ui/webui/resources/js/cr/ui/table/table_column_model.js"/>
+//<include src="../../../../../../ui/webui/resources/js/cr/ui/table/table_header.js"/>
+//<include src="../../../../../../ui/webui/resources/js/cr/ui/table/table_list.js"/>
+//<include src="../../../../../../ui/webui/resources/js/cr/ui/table.js"/>
+//
+//<include src="../../../../../../ui/webui/resources/js/cr/ui/grid.js"/>
+//
+//<include src="../../../../../../ui/webui/resources/js/cr/ui/command.js"/>
+//<include src="../../../../../../ui/webui/resources/js/cr/ui/position_util.js"/>
+//<include src="../../../../../../ui/webui/resources/js/cr/ui/menu_item.js"/>
+//<include src="../../../../../../ui/webui/resources/js/cr/ui/menu.js"/>
+//<include src="../../../../../../ui/webui/resources/js/cr/ui/menu_button.js"/>
+//<include src="../../../../../../ui/webui/resources/js/cr/ui/context_menu_handler.js"/>
+
+(function() {
+// 'strict mode' is invoked for this scope.
+
+// // This script must be loaded before all other Files.app's scripts.
+//<include src="error_counter.js"/>
+//
+//<include src="../../common/js/async_util.js"/>
+//<include src="../../common/js/path_util.js"/>
+//<include src="../../common/js/util.js"/>
+//<include src="../../common/js/progress_center_common.js">
+//
+//<include src="combobutton.js"/>
+//<include src="commandbutton.js"/>
+//<include src="ui/file_manager_dialog_base.js"/>
+//
+//<include src="app_installer.js"/>
+//<include src="cws_container_client.js"/>
+//<include src="directory_contents.js"/>
+//<include src="directory_model.js"/>
+//<include src="directory_tree.js"/>
+//<include src="drag_selector.js"/>
+//<include src="drive_banners.js" />
+//<include src="error_dialog.js"/>
+//<include src="file_operation_manager_wrapper.js"/>
+//<include src="file_grid.js"/>
+//<include src="file_manager.js"/>
+//<include src="file_selection.js"/>
+//<include src="file_table.js"/>
+//<include src="file_tasks.js"/>
+//<include src="file_transfer_controller.js"/>
+//<include src="file_type.js"/>
+//<include src="file_watcher.js"/>
+//<include src="folder_shortcuts_data_model.js"/>
+//<include src="navigation_list_model.js"/>
+//<include src="scrollbar.js"/>
+//<include src="share_client.js"/>
+//<include src="share_dialog.js"/>
+//<include src="suggest_apps_dialog.js"/>
+//<include src="text_measure.js"/>
+//<include src="tree.css.js"/>
+//<include src="ui/breadcrumbs_controller.js"/>
+//<include src="ui/conflict_dialog.js"/>
+//<include src="ui/file_manager_ui.js"/>
+//<include src="ui/navigation_list.js"/>
+//<include src="ui/preview_panel.js"/>
+//<include src="ui/progress_center_panel.js"/>
+//<include src="ui/search_box.js"/>
+//<include src="volume_manager_wrapper.js"/>
+//<include src="media/media_util.js"/>
+//<include src="metadata/metadata_cache.js"/>
+//<include src="default_action_dialog.js"/>
+//<include src="file_manager_commands.js"/>
+
+// // For accurate load performance tracking place main.js should be
+// // the last include to include.
+//<include src="main.js"/>
+
+// Global fileManager reference useful for poking at from the console.
+window.fileManager = fileManager;
+
+// Exports
+window.util = util;
+window.FileOperationManagerWrapper = FileOperationManagerWrapper;
+
+window.unload = unload;
+
+})();
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/media/audio_player.js b/chromium/chrome/browser/resources/file_manager/foreground/js/media/audio_player.js
new file mode 100644
index 00000000000..4791c31f92b
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/media/audio_player.js
@@ -0,0 +1,628 @@
+// 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.
+
+'use strict';
+
+/**
+ * TODO(mtomasz): Rewrite the entire audio player.
+ *
+ * @param {HTMLElement} container Container element.
+ * @constructor
+ */
+function AudioPlayer(container) {
+ this.container_ = container;
+ this.metadataCache_ = MetadataCache.createFull();
+ this.currentTrack_ = -1;
+ this.playlistGeneration_ = 0;
+ this.selectedEntry_ = null;
+ this.volumeManager_ = new VolumeManagerWrapper(
+ VolumeManagerWrapper.DriveEnabledStatus.DRIVE_ENABLED);
+
+ this.container_.classList.add('collapsed');
+
+ function createChild(opt_className, opt_tag) {
+ var child = container.ownerDocument.createElement(opt_tag || 'div');
+ if (opt_className)
+ child.className = opt_className;
+ container.appendChild(child);
+ return child;
+ }
+
+ // We create two separate containers (for expanded and compact view) and keep
+ // two sets of TrackInfo instances. We could fiddle with a single set instead
+ // but it would make keeping the list scroll position very tricky.
+ this.trackList_ = createChild('track-list');
+ this.trackStack_ = createChild('track-stack');
+
+ createChild('title-button collapse').addEventListener(
+ 'click', this.onExpandCollapse_.bind(this));
+
+ this.audioControls_ = new FullWindowAudioControls(
+ createChild(), this.advance_.bind(this), this.onError_.bind(this));
+
+ this.audioControls_.attachMedia(createChild('', 'audio'));
+
+ chrome.fileBrowserPrivate.getStrings(function(strings) {
+ container.ownerDocument.title = strings['AUDIO_PLAYER_TITLE'];
+ this.errorString_ = strings['AUDIO_ERROR'];
+ this.offlineString_ = strings['AUDIO_OFFLINE'];
+ AudioPlayer.TrackInfo.DEFAULT_ARTIST =
+ strings['AUDIO_PLAYER_DEFAULT_ARTIST'];
+ }.bind(this));
+
+ this.volumeManager_.addEventListener('externally-unmounted',
+ this.onExternallyUnmounted_.bind(this));
+
+ window.addEventListener('resize', this.onResize_.bind(this));
+
+ // Show the window after DOM is processed.
+ var currentWindow = chrome.app.window.current();
+ setTimeout(currentWindow.show.bind(currentWindow), 0);
+}
+
+/**
+ * Initial load method (static).
+ */
+AudioPlayer.load = function() {
+ document.ondragstart = function(e) { e.preventDefault() };
+
+ // TODO(mtomasz): Consider providing an exact size icon, instead of relying
+ // on downsampling by ash.
+ chrome.app.window.current().setIcon(
+ 'foreground/images/media/2x/audio_player.png');
+
+ AudioPlayer.instance =
+ new AudioPlayer(document.querySelector('.audio-player'));
+ reload();
+};
+
+util.addPageLoadHandler(AudioPlayer.load);
+
+/**
+ * Unload the player.
+ */
+function unload() {
+ if (AudioPlayer.instance)
+ AudioPlayer.instance.onUnload();
+}
+
+/**
+ * Reload the player.
+ */
+function reload() {
+ if (window.appState) {
+ util.saveAppState();
+ AudioPlayer.instance.load(window.appState);
+ return;
+ }
+}
+
+/**
+ * Load a new playlist.
+ * @param {Playlist} playlist Playlist object passed via mediaPlayerPrivate.
+ */
+AudioPlayer.prototype.load = function(playlist) {
+ this.playlistGeneration_++;
+ this.audioControls_.pause();
+ this.currentTrack_ = -1;
+
+ // Save the app state, in case of restart.
+ window.appState = playlist;
+ util.saveAppState();
+
+ util.URLsToEntries(playlist.items, function(entries) {
+ this.entries_ = entries;
+ this.invalidTracks_ = {};
+ this.cancelAutoAdvance_();
+
+ if (this.entries_.length <= 1)
+ this.container_.classList.add('single-track');
+ else
+ this.container_.classList.remove('single-track');
+
+ this.syncHeight_();
+
+ this.trackList_.textContent = '';
+ this.trackStack_.textContent = '';
+
+ this.trackListItems_ = [];
+ this.trackStackItems_ = [];
+
+ if (this.entries_.length == 0)
+ return;
+
+ for (var i = 0; i != this.entries_.length; i++) {
+ var entry = this.entries_[i];
+ var onClick = this.select_.bind(this, i, false /* no restore */);
+ this.trackListItems_.push(
+ new AudioPlayer.TrackInfo(this.trackList_, entry, onClick));
+ this.trackStackItems_.push(
+ new AudioPlayer.TrackInfo(this.trackStack_, entry, onClick));
+ }
+
+ this.select_(playlist.position, !!playlist.time);
+
+ // This class will be removed if at least one track has art.
+ this.container_.classList.add('noart');
+
+ // Load the selected track metadata first, then load the rest.
+ this.loadMetadata_(playlist.position);
+ for (i = 0; i != this.entries_.length; i++) {
+ if (i != playlist.position)
+ this.loadMetadata_(i);
+ }
+ }.bind(this));
+};
+
+/**
+ * Load metadata for a track.
+ * @param {number} track Track number.
+ * @private
+ */
+AudioPlayer.prototype.loadMetadata_ = function(track) {
+ this.fetchMetadata_(
+ this.entries_[track], this.displayMetadata_.bind(this, track));
+};
+
+/**
+ * Display track's metadata.
+ * @param {number} track Track number.
+ * @param {Object} metadata Metadata object.
+ * @param {string=} opt_error Error message.
+ * @private
+ */
+AudioPlayer.prototype.displayMetadata_ = function(track, metadata, opt_error) {
+ this.trackListItems_[track].
+ setMetadata(metadata, this.container_, opt_error);
+ this.trackStackItems_[track].
+ setMetadata(metadata, this.container_, opt_error);
+};
+
+/**
+ * Closes audio player when a volume containing the selected item is unmounted.
+ * @param {Event} event The unmount event.
+ * @private
+ */
+AudioPlayer.prototype.onExternallyUnmounted_ = function(event) {
+ if (!this.selectedEntry_)
+ return;
+
+ if (this.volumeManager_.getVolumeInfo(this.selectedEntry_) ===
+ event.volumeInfo) {
+ window.close();
+ }
+};
+
+/**
+ * Called on window is being unloaded.
+ */
+AudioPlayer.prototype.onUnload = function() {
+ this.audioControls_.cleanup();
+ this.volumeManager_.dispose();
+};
+
+/**
+ * Select a new track to play.
+ * @param {number} newTrack New track number.
+ * @param {boolean=} opt_restoreState True if restoring the play state from URL.
+ * @private
+ */
+AudioPlayer.prototype.select_ = function(newTrack, opt_restoreState) {
+ if (this.currentTrack_ == newTrack) return;
+
+ this.changeSelectionInList_(this.currentTrack_, newTrack);
+ this.changeSelectionInStack_(this.currentTrack_, newTrack);
+
+ this.currentTrack_ = newTrack;
+
+ if (window.appState) {
+ window.appState.position = this.currentTrack_;
+ window.appState.time = 0;
+ util.saveAppState();
+ } else {
+ util.platform.setPreference(AudioPlayer.TRACK_KEY, this.currentTrack_);
+ }
+
+ this.scrollToCurrent_(false);
+
+ var currentTrack = this.currentTrack_;
+ var entry = this.entries_[currentTrack];
+ this.fetchMetadata_(entry, function(metadata) {
+ if (this.currentTrack_ != currentTrack)
+ return;
+ this.audioControls_.load(entry, opt_restoreState);
+
+ // Resolve real filesystem path of the current audio file.
+ this.selectedEntry_ = entry;
+ }.bind(this));
+};
+
+/**
+ * @param {Entry} entry Track file entry.
+ * @param {function(object)} callback Callback.
+ * @private
+ */
+AudioPlayer.prototype.fetchMetadata_ = function(entry, callback) {
+ this.metadataCache_.get(entry, 'thumbnail|media|streaming',
+ function(generation, metadata) {
+ // Do nothing if another load happened since the metadata request.
+ if (this.playlistGeneration_ == generation)
+ callback(metadata);
+ }.bind(this, this.playlistGeneration_));
+};
+
+/**
+ * @param {number} oldTrack Old track number.
+ * @param {number} newTrack New track number.
+ * @private
+ */
+AudioPlayer.prototype.changeSelectionInList_ = function(oldTrack, newTrack) {
+ this.trackListItems_[newTrack].getBox().classList.add('selected');
+
+ if (oldTrack >= 0) {
+ this.trackListItems_[oldTrack].getBox().classList.remove('selected');
+ }
+};
+
+/**
+ * @param {number} oldTrack Old track number.
+ * @param {number} newTrack New track number.
+ * @private
+ */
+AudioPlayer.prototype.changeSelectionInStack_ = function(oldTrack, newTrack) {
+ var newBox = this.trackStackItems_[newTrack].getBox();
+ newBox.classList.add('selected'); // Put on top immediately.
+ newBox.classList.add('visible'); // Start fading in.
+
+ if (oldTrack >= 0) {
+ var oldBox = this.trackStackItems_[oldTrack].getBox();
+ oldBox.classList.remove('selected'); // Put under immediately.
+ setTimeout(function() {
+ if (!oldBox.classList.contains('selected')) {
+ // This will start fading out which is not really necessary because
+ // oldBox is already completely obscured by newBox.
+ oldBox.classList.remove('visible');
+ }
+ }, 300);
+ }
+};
+
+/**
+ * Scrolls the current track into the viewport.
+ *
+ * @param {boolean} keepAtBottom If true, make the selected track the last
+ * of the visible (if possible). If false, perform minimal scrolling.
+ * @private
+ */
+AudioPlayer.prototype.scrollToCurrent_ = function(keepAtBottom) {
+ var box = this.trackListItems_[this.currentTrack_].getBox();
+ this.trackList_.scrollTop = Math.max(
+ keepAtBottom ? 0 : Math.min(box.offsetTop, this.trackList_.scrollTop),
+ box.offsetTop + box.offsetHeight - this.trackList_.clientHeight);
+};
+
+/**
+ * @return {boolean} True if the player is be displayed in compact mode.
+ * @private
+ */
+AudioPlayer.prototype.isCompact_ = function() {
+ return this.container_.classList.contains('collapsed') ||
+ this.container_.classList.contains('single-track');
+};
+
+/**
+ * Go to the previous or the next track.
+ * @param {boolean} forward True if next, false if previous.
+ * @param {boolean=} opt_onlyIfValid True if invalid tracks should be selected.
+ * @private
+ */
+AudioPlayer.prototype.advance_ = function(forward, opt_onlyIfValid) {
+ this.cancelAutoAdvance_();
+
+ var newTrack = this.currentTrack_ + (forward ? 1 : -1);
+ if (newTrack < 0) newTrack = this.entries_.length - 1;
+ if (newTrack == this.entries_.length) newTrack = 0;
+ if (opt_onlyIfValid && this.invalidTracks_[newTrack])
+ return;
+ this.select_(newTrack);
+};
+
+/**
+ * Media error handler.
+ * @private
+ */
+AudioPlayer.prototype.onError_ = function() {
+ var track = this.currentTrack_;
+
+ this.invalidTracks_[track] = true;
+
+ this.fetchMetadata_(
+ this.entries_[track],
+ function(metadata) {
+ var error = (!navigator.onLine && metadata.streaming) ?
+ this.offlineString_ : this.errorString_;
+ this.displayMetadata_(track, metadata, error);
+ this.scheduleAutoAdvance_();
+ }.bind(this));
+};
+
+/**
+ * Schedule automatic advance to the next track after a timeout.
+ * @private
+ */
+AudioPlayer.prototype.scheduleAutoAdvance_ = function() {
+ this.cancelAutoAdvance_();
+ this.autoAdvanceTimer_ = setTimeout(
+ function() {
+ this.autoAdvanceTimer_ = null;
+ // We are advancing only if the next track is not known to be invalid.
+ // This prevents an endless auto-advancing in the case when all tracks
+ // are invalid (we will only visit each track once).
+ this.advance_(true /* forward */, true /* only if valid */);
+ }.bind(this),
+ 3000);
+};
+
+/**
+ * Cancel the scheduled auto advance.
+ * @private
+ */
+AudioPlayer.prototype.cancelAutoAdvance_ = function() {
+ if (this.autoAdvanceTimer_) {
+ clearTimeout(this.autoAdvanceTimer_);
+ this.autoAdvanceTimer_ = null;
+ }
+};
+
+/**
+ * Expand/collapse button click handler. Toggles the mode and updates the
+ * height of the window.
+ *
+ * @private
+ */
+AudioPlayer.prototype.onExpandCollapse_ = function() {
+ if (!this.isCompact_()) {
+ this.setExpanded_(false);
+ this.lastExpandedHeight_ = window.innerHeight;
+ } else {
+ this.setExpanded_(true);
+ }
+ this.syncHeight_();
+};
+
+/**
+ * Toggles the current expand mode.
+ *
+ * @param {boolean} on True if on, false otherwise.
+ * @private
+ */
+AudioPlayer.prototype.setExpanded_ = function(on) {
+ if (on) {
+ this.container_.classList.remove('collapsed');
+ this.scrollToCurrent_(true);
+ } else {
+ this.container_.classList.add('collapsed');
+ }
+};
+
+/**
+ * Toggles the expanded mode when resizing.
+ *
+ * @param {Event} event Resize event.
+ * @private
+ */
+AudioPlayer.prototype.onResize_ = function(event) {
+ if (this.isCompact_() &&
+ window.innerHeight >= AudioPlayer.EXPANDED_MODE_MIN_HEIGHT) {
+ this.setExpanded_(true);
+ } else if (!this.isCompact_() &&
+ window.innerHeight < AudioPlayer.EXPANDED_MODE_MIN_HEIGHT) {
+ this.setExpanded_(false);
+ }
+};
+
+/* Keep the below constants in sync with the CSS. */
+
+/**
+ * Window header size in pixels.
+ * @type {number}
+ * @const
+ */
+AudioPlayer.HEADER_HEIGHT = 28;
+
+/**
+ * Track height in pixels.
+ * @type {number}
+ * @const
+ */
+AudioPlayer.TRACK_HEIGHT = 58;
+
+/**
+ * Controls bar height in pixels.
+ * @type {number}
+ * @const
+ */
+AudioPlayer.CONTROLS_HEIGHT = 35;
+
+/**
+ * Default number of items in the expanded mode.
+ * @type {number}
+ * @const
+ */
+AudioPlayer.DEFAULT_EXPANDED_ITEMS = 5;
+
+/**
+ * Minimum size of the window in the expanded mode in pixels.
+ * @type {number}
+ * @const
+ */
+AudioPlayer.EXPANDED_MODE_MIN_HEIGHT = AudioPlayer.CONTROLS_HEIGHT +
+ AudioPlayer.TRACK_HEIGHT * 2;
+
+/**
+ * Set the correct player window height.
+ * @private
+ */
+AudioPlayer.prototype.syncHeight_ = function() {
+ var targetHeight;
+
+ if (!this.isCompact_()) {
+ // Expanded.
+ if (this.lastExpandedHeight_) {
+ targetHeight = this.lastExpandedHeight_;
+ } else {
+ var expandedListHeight =
+ Math.min(this.entries_.length, AudioPlayer.DEFAULT_EXPANDED_ITEMS) *
+ AudioPlayer.TRACK_HEIGHT;
+ targetHeight = AudioPlayer.CONTROLS_HEIGHT + expandedListHeight;
+ }
+ } else {
+ // Not expaned.
+ targetHeight = AudioPlayer.CONTROLS_HEIGHT + AudioPlayer.TRACK_HEIGHT;
+ }
+
+ window.resizeTo(window.innerWidth, targetHeight + AudioPlayer.HEADER_HEIGHT);
+};
+
+/**
+ * Create a TrackInfo object encapsulating the information about one track.
+ *
+ * @param {HTMLElement} container Container element.
+ * @param {Entry} entry Track entry.
+ * @param {function} onClick Click handler.
+ * @constructor
+ */
+AudioPlayer.TrackInfo = function(container, entry, onClick) {
+ this.entry_ = entry;
+
+ var doc = container.ownerDocument;
+
+ this.box_ = doc.createElement('div');
+ this.box_.className = 'track';
+ this.box_.addEventListener('click', onClick);
+ container.appendChild(this.box_);
+
+ this.art_ = doc.createElement('div');
+ this.art_.className = 'art blank';
+ this.box_.appendChild(this.art_);
+
+ this.img_ = doc.createElement('img');
+ this.art_.appendChild(this.img_);
+
+ this.data_ = doc.createElement('div');
+ this.data_.className = 'data';
+ this.box_.appendChild(this.data_);
+
+ this.title_ = doc.createElement('div');
+ this.title_.className = 'data-title';
+ this.data_.appendChild(this.title_);
+
+ this.artist_ = doc.createElement('div');
+ this.artist_.className = 'data-artist';
+ this.data_.appendChild(this.artist_);
+};
+
+/**
+ * @return {HTMLDivElement} The wrapper element for the track.
+ */
+AudioPlayer.TrackInfo.prototype.getBox = function() { return this.box_ };
+
+/**
+ * @return {string} Default track title (file name extracted from the entry).
+ */
+AudioPlayer.TrackInfo.prototype.getDefaultTitle = function() {
+ // TODO(mtomasz): Reuse ImageUtil.getDisplayNameFromName().
+ var name = this.entry_.name;
+ var dotIndex = name.lastIndexOf('.');
+ var title = dotIndex >= 0 ? name.substr(0, dotIndex) : name;
+ return title;
+};
+
+/**
+ * TODO(kaznacheev): Localize.
+ */
+AudioPlayer.TrackInfo.DEFAULT_ARTIST = 'Unknown Artist';
+
+/**
+ * @return {string} 'Unknown artist' string.
+ */
+AudioPlayer.TrackInfo.prototype.getDefaultArtist = function() {
+ return AudioPlayer.TrackInfo.DEFAULT_ARTIST;
+};
+
+/**
+ * @param {Object} metadata The metadata object.
+ * @param {HTMLElement} container The container for the tracks.
+ * @param {string} error Error string.
+ */
+AudioPlayer.TrackInfo.prototype.setMetadata = function(
+ metadata, container, error) {
+ if (error) {
+ this.art_.classList.add('blank');
+ this.art_.classList.add('error');
+ container.classList.remove('noart');
+ } else if (metadata.thumbnail && metadata.thumbnail.url) {
+ this.img_.onload = function() {
+ // Only display the image if the thumbnail loaded successfully.
+ this.art_.classList.remove('blank');
+ container.classList.remove('noart');
+ }.bind(this);
+ this.img_.src = metadata.thumbnail.url;
+ }
+ this.title_.textContent = (metadata.media && metadata.media.title) ||
+ this.getDefaultTitle();
+ this.artist_.textContent = error ||
+ (metadata.media && metadata.media.artist) || this.getDefaultArtist();
+};
+
+/**
+ * Audio controls specific for the Audio Player.
+ *
+ * @param {HTMLElement} container Parent container.
+ * @param {function(boolean)} advanceTrack Parameter: true=forward.
+ * @param {function} onError Error handler.
+ * @constructor
+ */
+function FullWindowAudioControls(container, advanceTrack, onError) {
+ AudioControls.apply(this, arguments);
+
+ document.addEventListener('keydown', function(e) {
+ if (e.keyIdentifier == 'U+0020') {
+ this.togglePlayState();
+ e.preventDefault();
+ }
+ }.bind(this));
+}
+
+FullWindowAudioControls.prototype = { __proto__: AudioControls.prototype };
+
+/**
+ * Enable play state restore from the location hash.
+ * @param {FileEntry} entry Source Entry.
+ * @param {boolean} restore True if need to restore the play state.
+ */
+FullWindowAudioControls.prototype.load = function(entry, restore) {
+ this.media_.src = entry.toURL();
+ this.media_.load();
+ this.restoreWhenLoaded_ = restore;
+};
+
+/**
+ * Save the current state so that it survives page/app reload.
+ */
+FullWindowAudioControls.prototype.onPlayStateChanged = function() {
+ this.encodeState();
+};
+
+/**
+ * Restore the state after page/app reload.
+ */
+FullWindowAudioControls.prototype.restorePlayState = function() {
+ if (this.restoreWhenLoaded_) {
+ this.restoreWhenLoaded_ = false; // This should only work once.
+ if (this.decodeState())
+ return;
+ }
+ this.play();
+};
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/media/media_controls.js b/chromium/chrome/browser/resources/file_manager/foreground/js/media/media_controls.js
new file mode 100644
index 00000000000..a335bdc4c52
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/media/media_controls.js
@@ -0,0 +1,1245 @@
+// 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.
+
+'use strict';
+
+/**
+ * @fileoverview MediaControls class implements media playback controls
+ * that exist outside of the audio/video HTML element.
+ */
+
+/**
+ * @param {HTMLElement} containerElement The container for the controls.
+ * @param {function} onMediaError Function to display an error message.
+ * @constructor
+ */
+function MediaControls(containerElement, onMediaError) {
+ this.container_ = containerElement;
+ this.document_ = this.container_.ownerDocument;
+ this.media_ = null;
+
+ this.onMediaPlayBound_ = this.onMediaPlay_.bind(this, true);
+ this.onMediaPauseBound_ = this.onMediaPlay_.bind(this, false);
+ this.onMediaDurationBound_ = this.onMediaDuration_.bind(this);
+ this.onMediaProgressBound_ = this.onMediaProgress_.bind(this);
+ this.onMediaError_ = onMediaError || function() {};
+}
+
+/**
+ * Button's state types. Values are used as CSS class names.
+ * @enum {string}
+ */
+MediaControls.ButtonStateType = {
+ DEFAULT: 'default',
+ PLAYING: 'playing',
+ ENDED: 'ended'
+};
+
+/**
+ * @return {HTMLAudioElement|HTMLVideoElement} The media element.
+ */
+MediaControls.prototype.getMedia = function() { return this.media_ };
+
+/**
+ * Format the time in hh:mm:ss format (omitting redundant leading zeros).
+ *
+ * @param {number} timeInSec Time in seconds.
+ * @return {string} Formatted time string.
+ * @private
+ */
+MediaControls.formatTime_ = function(timeInSec) {
+ var seconds = Math.floor(timeInSec % 60);
+ var minutes = Math.floor((timeInSec / 60) % 60);
+ var hours = Math.floor(timeInSec / 60 / 60);
+ var result = '';
+ if (hours) result += hours + ':';
+ if (hours && (minutes < 10)) result += '0';
+ result += minutes + ':';
+ if (seconds < 10) result += '0';
+ result += seconds;
+ return result;
+};
+
+/**
+ * Create a custom control.
+ *
+ * @param {string} className Class name.
+ * @param {HTMLElement=} opt_parent Parent element or container if undefined.
+ * @return {HTMLElement} The new control element.
+ */
+MediaControls.prototype.createControl = function(className, opt_parent) {
+ var parent = opt_parent || this.container_;
+ var control = this.document_.createElement('div');
+ control.className = className;
+ parent.appendChild(control);
+ return control;
+};
+
+/**
+ * Create a custom button.
+ *
+ * @param {string} className Class name.
+ * @param {function(Event)} handler Click handler.
+ * @param {HTMLElement=} opt_parent Parent element or container if undefined.
+ * @param {number=} opt_numStates Number of states, default: 1.
+ * @return {HTMLElement} The new button element.
+ */
+MediaControls.prototype.createButton = function(
+ className, handler, opt_parent, opt_numStates) {
+ opt_numStates = opt_numStates || 1;
+
+ var button = this.createControl(className, opt_parent);
+ button.classList.add('media-button');
+ button.addEventListener('click', handler);
+
+ var stateTypes = Object.keys(MediaControls.ButtonStateType);
+ for (var state = 0; state != opt_numStates; state++) {
+ var stateClass = MediaControls.ButtonStateType[stateTypes[state]];
+ this.createControl('normal ' + stateClass, button);
+ this.createControl('hover ' + stateClass, button);
+ this.createControl('active ' + stateClass, button);
+ }
+ this.createControl('disabled', button);
+
+ button.setAttribute('state', MediaControls.ButtonStateType.DEFAULT);
+ button.addEventListener('click', handler);
+ return button;
+};
+
+/**
+ * Enable/disable controls matching a given selector.
+ *
+ * @param {string} selector CSS selector.
+ * @param {boolean} on True if enable, false if disable.
+ * @private
+ */
+MediaControls.prototype.enableControls_ = function(selector, on) {
+ var controls = this.container_.querySelectorAll(selector);
+ for (var i = 0; i != controls.length; i++) {
+ var classList = controls[i].classList;
+ if (on)
+ classList.remove('disabled');
+ else
+ classList.add('disabled');
+ }
+};
+
+/*
+ * Playback control.
+ */
+
+/**
+ * Play the media.
+ */
+MediaControls.prototype.play = function() {
+ this.media_.play();
+};
+
+/**
+ * Pause the media.
+ */
+MediaControls.prototype.pause = function() {
+ this.media_.pause();
+};
+
+/**
+ * @return {boolean} True if the media is currently playing.
+ */
+MediaControls.prototype.isPlaying = function() {
+ return !this.media_.paused && !this.media_.ended;
+};
+
+/**
+ * Toggle play/pause.
+ */
+MediaControls.prototype.togglePlayState = function() {
+ if (this.isPlaying())
+ this.pause();
+ else
+ this.play();
+};
+
+/**
+ * Toggle play/pause state on a mouse click on the play/pause button. Can be
+ * called externally. TODO(mtomasz): Remove it. http://www.crbug.com/254318.
+ *
+ * @param {Event=} opt_event Mouse click event.
+ */
+MediaControls.prototype.onPlayButtonClicked = function(opt_event) {
+ this.togglePlayState();
+};
+
+/**
+ * @param {HTMLElement=} opt_parent Parent container.
+ */
+MediaControls.prototype.initPlayButton = function(opt_parent) {
+ this.playButton_ = this.createButton('play media-control',
+ this.onPlayButtonClicked.bind(this), opt_parent, 3 /* States. */);
+};
+
+/*
+ * Time controls
+ */
+
+/**
+ * The default range of 100 is too coarse for the media progress slider.
+ */
+MediaControls.PROGRESS_RANGE = 5000;
+
+/**
+ * @param {boolean=} opt_seekMark True if the progress slider should have
+ * a seek mark.
+ * @param {HTMLElement=} opt_parent Parent container.
+ */
+MediaControls.prototype.initTimeControls = function(opt_seekMark, opt_parent) {
+ var timeControls = this.createControl('time-controls', opt_parent);
+
+ var sliderConstructor =
+ opt_seekMark ? MediaControls.PreciseSlider : MediaControls.Slider;
+
+ this.progressSlider_ = new sliderConstructor(
+ this.createControl('progress media-control', timeControls),
+ 0, /* value */
+ MediaControls.PROGRESS_RANGE,
+ this.onProgressChange_.bind(this),
+ this.onProgressDrag_.bind(this));
+
+ var timeBox = this.createControl('time media-control', timeControls);
+
+ this.duration_ = this.createControl('duration', timeBox);
+ // Set the initial width to the minimum to reduce the flicker.
+ this.duration_.textContent = MediaControls.formatTime_(0);
+
+ this.currentTime_ = this.createControl('current', timeBox);
+};
+
+/**
+ * @param {number} current Current time is seconds.
+ * @param {number} duration Duration in seconds.
+ * @private
+ */
+MediaControls.prototype.displayProgress_ = function(current, duration) {
+ var ratio = current / duration;
+ this.progressSlider_.setValue(ratio);
+ this.currentTime_.textContent = MediaControls.formatTime_(current);
+};
+
+/**
+ * @param {number} value Progress [0..1].
+ * @private
+ */
+MediaControls.prototype.onProgressChange_ = function(value) {
+ if (!this.media_.seekable || !this.media_.duration) {
+ console.error('Inconsistent media state');
+ return;
+ }
+
+ var current = this.media_.duration * value;
+ this.media_.currentTime = current;
+ this.currentTime_.textContent = MediaControls.formatTime_(current);
+};
+
+/**
+ * @param {boolean} on True if dragging.
+ * @private
+ */
+MediaControls.prototype.onProgressDrag_ = function(on) {
+ if (on) {
+ this.resumeAfterDrag_ = this.isPlaying();
+ this.media_.pause();
+ } else {
+ if (this.resumeAfterDrag_) {
+ if (this.media_.ended)
+ this.onMediaPlay_(false);
+ else
+ this.media_.play();
+ }
+ this.updatePlayButtonState_(this.isPlaying());
+ }
+};
+
+/*
+ * Volume controls
+ */
+
+/**
+ * @param {HTMLElement=} opt_parent Parent element for the controls.
+ */
+MediaControls.prototype.initVolumeControls = function(opt_parent) {
+ var volumeControls = this.createControl('volume-controls', opt_parent);
+
+ this.soundButton_ = this.createButton('sound media-control',
+ this.onSoundButtonClick_.bind(this), volumeControls);
+ this.soundButton_.setAttribute('level', 3); // max level.
+
+ this.volume_ = new MediaControls.AnimatedSlider(
+ this.createControl('volume media-control', volumeControls),
+ 1, /* value */
+ 100 /* range */,
+ this.onVolumeChange_.bind(this),
+ this.onVolumeDrag_.bind(this));
+};
+
+/**
+ * Click handler for the sound level button.
+ * @private
+ */
+MediaControls.prototype.onSoundButtonClick_ = function() {
+ if (this.media_.volume == 0) {
+ this.volume_.setValue(this.savedVolume_ || 1);
+ } else {
+ this.savedVolume_ = this.media_.volume;
+ this.volume_.setValue(0);
+ }
+ this.onVolumeChange_(this.volume_.getValue());
+};
+
+/**
+ * @param {number} value Volume [0..1].
+ * @return {number} The rough level [0..3] used to pick an icon.
+ * @private
+ */
+MediaControls.getVolumeLevel_ = function(value) {
+ if (value == 0) return 0;
+ if (value <= 1 / 3) return 1;
+ if (value <= 2 / 3) return 2;
+ return 3;
+};
+
+/**
+ * @param {number} value Volume [0..1].
+ * @private
+ */
+MediaControls.prototype.onVolumeChange_ = function(value) {
+ this.media_.volume = value;
+ this.soundButton_.setAttribute('level', MediaControls.getVolumeLevel_(value));
+};
+
+/**
+ * @param {boolean} on True if dragging is in progress.
+ * @private
+ */
+MediaControls.prototype.onVolumeDrag_ = function(on) {
+ if (on && (this.media_.volume != 0)) {
+ this.savedVolume_ = this.media_.volume;
+ }
+};
+
+/*
+ * Media event handlers.
+ */
+
+/**
+ * Attach a media element.
+ *
+ * @param {HTMLMediaElement} mediaElement The media element to control.
+ */
+MediaControls.prototype.attachMedia = function(mediaElement) {
+ this.media_ = mediaElement;
+
+ this.media_.addEventListener('play', this.onMediaPlayBound_);
+ this.media_.addEventListener('pause', this.onMediaPauseBound_);
+ this.media_.addEventListener('durationchange', this.onMediaDurationBound_);
+ this.media_.addEventListener('timeupdate', this.onMediaProgressBound_);
+ this.media_.addEventListener('error', this.onMediaError_);
+
+ // Reflect the media state in the UI.
+ this.onMediaDuration_();
+ this.onMediaPlay_(this.isPlaying());
+ this.onMediaProgress_();
+ if (this.volume_) {
+ /* Copy the user selected volume to the new media element. */
+ this.media_.volume = this.volume_.getValue();
+ }
+};
+
+/**
+ * Detach media event handlers.
+ */
+MediaControls.prototype.detachMedia = function() {
+ if (!this.media_)
+ return;
+
+ this.media_.removeEventListener('play', this.onMediaPlayBound_);
+ this.media_.removeEventListener('pause', this.onMediaPauseBound_);
+ this.media_.removeEventListener('durationchange', this.onMediaDurationBound_);
+ this.media_.removeEventListener('timeupdate', this.onMediaProgressBound_);
+ this.media_.removeEventListener('error', this.onMediaError_);
+
+ this.media_ = null;
+};
+
+/**
+ * Force-empty the media pipeline. This is a workaround for crbug.com/149957.
+ * The document is not going to be GC-ed until the last Files app window closes,
+ * but we want the media pipeline to deinitialize ASAP to minimize leakage.
+ */
+MediaControls.prototype.cleanup = function() {
+ this.media_.src = '';
+ this.media_.load();
+ this.detachMedia();
+};
+
+/**
+ * 'play' and 'pause' event handler.
+ * @param {boolean} playing True if playing.
+ * @private
+ */
+MediaControls.prototype.onMediaPlay_ = function(playing) {
+ if (this.progressSlider_.isDragging())
+ return;
+
+ this.updatePlayButtonState_(playing);
+ this.onPlayStateChanged();
+};
+
+/**
+ * 'durationchange' event handler.
+ * @private
+ */
+MediaControls.prototype.onMediaDuration_ = function() {
+ if (!this.media_.duration) {
+ this.enableControls_('.media-control', false);
+ return;
+ }
+
+ this.enableControls_('.media-control', true);
+
+ var sliderContainer = this.progressSlider_.getContainer();
+ if (this.media_.seekable)
+ sliderContainer.classList.remove('readonly');
+ else
+ sliderContainer.classList.add('readonly');
+
+ var valueToString = function(value) {
+ return MediaControls.formatTime_(this.media_.duration * value);
+ }.bind(this);
+
+ this.duration_.textContent = valueToString(1);
+
+ if (this.progressSlider_.setValueToStringFunction)
+ this.progressSlider_.setValueToStringFunction(valueToString);
+
+ if (this.media_.seekable)
+ this.restorePlayState();
+};
+
+/**
+ * 'timeupdate' event handler.
+ * @private
+ */
+MediaControls.prototype.onMediaProgress_ = function() {
+ if (!this.media_.duration) {
+ this.displayProgress_(0, 1);
+ return;
+ }
+
+ var current = this.media_.currentTime;
+ var duration = this.media_.duration;
+
+ if (this.progressSlider_.isDragging())
+ return;
+
+ this.displayProgress_(current, duration);
+
+ if (current == duration) {
+ this.onMediaComplete();
+ }
+ this.onPlayStateChanged();
+};
+
+/**
+ * Called when the media playback is complete.
+ */
+MediaControls.prototype.onMediaComplete = function() {};
+
+/**
+ * Called when play/pause state is changed or on playback progress.
+ * This is the right moment to save the play state.
+ */
+MediaControls.prototype.onPlayStateChanged = function() {};
+
+/**
+ * Updates the play button state.
+ * @param {boolean} playing If the video is playing.
+ * @private
+ */
+MediaControls.prototype.updatePlayButtonState_ = function(playing) {
+ if (playing) {
+ this.playButton_.setAttribute('state',
+ MediaControls.ButtonStateType.PLAYING);
+ } else if (!this.media_.ended) {
+ this.playButton_.setAttribute('state',
+ MediaControls.ButtonStateType.DEFAULT);
+ } else {
+ this.playButton_.setAttribute('state',
+ MediaControls.ButtonStateType.ENDED);
+ }
+};
+
+/**
+ * Restore play state. Base implementation is empty.
+ */
+MediaControls.prototype.restorePlayState = function() {};
+
+/**
+ * Encode current state into the page URL or the app state.
+ */
+MediaControls.prototype.encodeState = function() {
+ if (!this.media_.duration)
+ return;
+
+ if (window.appState) {
+ window.appState.time = this.media_.currentTime;
+ util.saveAppState();
+ return;
+ }
+
+ var playState = JSON.stringify({
+ play: this.isPlaying(),
+ time: this.media_.currentTime
+ });
+
+ var newLocation = document.location.origin + document.location.pathname +
+ document.location.search + '#' + playState;
+
+ document.location.href = newLocation;
+};
+
+/**
+ * Decode current state from the page URL or the app state.
+ * @return {boolean} True if decode succeeded.
+ */
+MediaControls.prototype.decodeState = function() {
+ if (window.appState) {
+ if (!('time' in window.appState))
+ return false;
+ // There is no page reload for apps v2, only app restart.
+ // Always restart in paused state.
+ this.media_.currentTime = appState.time;
+ this.pause();
+ return true;
+ }
+
+ var hash = document.location.hash.substring(1);
+ if (hash) {
+ try {
+ var playState = JSON.parse(hash);
+ if (!('time' in playState))
+ return false;
+
+ this.media_.currentTime = playState.time;
+
+ if (playState.play)
+ this.play();
+ else
+ this.pause();
+
+ return true;
+ } catch (e) {
+ console.warn('Cannot decode play state');
+ }
+ }
+ return false;
+};
+
+/**
+ * Remove current state from the page URL or the app state.
+ */
+MediaControls.prototype.clearState = function() {
+ if (window.appState) {
+ if ('time' in window.appState)
+ delete window.appState.time;
+ util.saveAppState();
+ return;
+ }
+
+ var newLocation = document.location.origin + document.location.pathname +
+ document.location.search + '#';
+
+ document.location.href = newLocation;
+};
+
+/**
+ * Create a customized slider control.
+ *
+ * @param {HTMLElement} container The containing div element.
+ * @param {number} value Initial value [0..1].
+ * @param {number} range Number of distinct slider positions to be supported.
+ * @param {function(number)} onChange Value change handler.
+ * @param {function(boolean)} onDrag Drag begin/end handler.
+ * @constructor
+ */
+
+MediaControls.Slider = function(container, value, range, onChange, onDrag) {
+ this.container_ = container;
+ this.onChange_ = onChange;
+ this.onDrag_ = onDrag;
+
+ var document = this.container_.ownerDocument;
+
+ this.container_.classList.add('custom-slider');
+
+ this.input_ = document.createElement('input');
+ this.input_.type = 'range';
+ this.input_.min = 0;
+ this.input_.max = range;
+ this.input_.value = value * range;
+ this.container_.appendChild(this.input_);
+
+ this.input_.addEventListener(
+ 'change', this.onInputChange_.bind(this));
+ this.input_.addEventListener(
+ 'mousedown', this.onInputDrag_.bind(this, true));
+ this.input_.addEventListener(
+ 'mouseup', this.onInputDrag_.bind(this, false));
+
+ this.bar_ = document.createElement('div');
+ this.bar_.className = 'bar';
+ this.container_.appendChild(this.bar_);
+
+ this.filled_ = document.createElement('div');
+ this.filled_.className = 'filled';
+ this.bar_.appendChild(this.filled_);
+
+ var leftCap = document.createElement('div');
+ leftCap.className = 'cap left';
+ this.bar_.appendChild(leftCap);
+
+ var rightCap = document.createElement('div');
+ rightCap.className = 'cap right';
+ this.bar_.appendChild(rightCap);
+
+ this.value_ = value;
+ this.setFilled_(value);
+};
+
+/**
+ * @return {HTMLElement} The container element.
+ */
+MediaControls.Slider.prototype.getContainer = function() {
+ return this.container_;
+};
+
+/**
+ * @return {HTMLElement} The standard input element.
+ * @private
+ */
+MediaControls.Slider.prototype.getInput_ = function() {
+ return this.input_;
+};
+
+/**
+ * @return {HTMLElement} The slider bar element.
+ */
+MediaControls.Slider.prototype.getBar = function() {
+ return this.bar_;
+};
+
+/**
+ * @return {number} [0..1] The current value.
+ */
+MediaControls.Slider.prototype.getValue = function() {
+ return this.value_;
+};
+
+/**
+ * @param {number} value [0..1].
+ */
+MediaControls.Slider.prototype.setValue = function(value) {
+ this.value_ = value;
+ this.setValueToUI_(value);
+};
+
+/**
+ * Fill the given proportion the slider bar (from the left).
+ *
+ * @param {number} proportion [0..1].
+ * @private
+ */
+MediaControls.Slider.prototype.setFilled_ = function(proportion) {
+ this.filled_.style.width = proportion * 100 + '%';
+};
+
+/**
+ * Get the value from the input element.
+ *
+ * @return {number} Value [0..1].
+ * @private
+ */
+MediaControls.Slider.prototype.getValueFromUI_ = function() {
+ return this.input_.value / this.input_.max;
+};
+
+/**
+ * Update the UI with the current value.
+ *
+ * @param {number} value [0..1].
+ * @private
+ */
+MediaControls.Slider.prototype.setValueToUI_ = function(value) {
+ this.input_.value = value * this.input_.max;
+ this.setFilled_(value);
+};
+
+/**
+ * Compute the proportion in which the given position divides the slider bar.
+ *
+ * @param {number} position in pixels.
+ * @return {number} [0..1] proportion.
+ */
+MediaControls.Slider.prototype.getProportion = function(position) {
+ var rect = this.bar_.getBoundingClientRect();
+ return Math.max(0, Math.min(1, (position - rect.left) / rect.width));
+};
+
+/**
+ * 'change' event handler.
+ * @private
+ */
+MediaControls.Slider.prototype.onInputChange_ = function() {
+ this.value_ = this.getValueFromUI_();
+ this.setFilled_(this.value_);
+ this.onChange_(this.value_);
+};
+
+/**
+ * @return {boolean} True if dragging is in progress.
+ */
+MediaControls.Slider.prototype.isDragging = function() {
+ return this.isDragging_;
+};
+
+/**
+ * Mousedown/mouseup handler.
+ * @param {boolean} on True if the mouse is down.
+ * @private
+ */
+MediaControls.Slider.prototype.onInputDrag_ = function(on) {
+ this.isDragging_ = on;
+ this.onDrag_(on);
+};
+
+/**
+ * Create a customized slider with animated thumb movement.
+ *
+ * @param {HTMLElement} container The containing div element.
+ * @param {number} value Initial value [0..1].
+ * @param {number} range Number of distinct slider positions to be supported.
+ * @param {function(number)} onChange Value change handler.
+ * @param {function(boolean)} onDrag Drag begin/end handler.
+ * @param {function(number):string} formatFunction Value formatting function.
+ * @constructor
+ */
+MediaControls.AnimatedSlider = function(
+ container, value, range, onChange, onDrag, formatFunction) {
+ MediaControls.Slider.apply(this, arguments);
+};
+
+MediaControls.AnimatedSlider.prototype = {
+ __proto__: MediaControls.Slider.prototype
+};
+
+/**
+ * Number of animation steps.
+ */
+MediaControls.AnimatedSlider.STEPS = 10;
+
+/**
+ * Animation duration.
+ */
+MediaControls.AnimatedSlider.DURATION = 100;
+
+/**
+ * @param {number} value [0..1].
+ * @private
+ */
+MediaControls.AnimatedSlider.prototype.setValueToUI_ = function(value) {
+ if (this.animationInterval_) {
+ clearInterval(this.animationInterval_);
+ }
+ var oldValue = this.getValueFromUI_();
+ var step = 0;
+ this.animationInterval_ = setInterval(function() {
+ step++;
+ var currentValue = oldValue +
+ (value - oldValue) * (step / MediaControls.AnimatedSlider.STEPS);
+ MediaControls.Slider.prototype.setValueToUI_.call(this, currentValue);
+ if (step == MediaControls.AnimatedSlider.STEPS) {
+ clearInterval(this.animationInterval_);
+ }
+ }.bind(this),
+ MediaControls.AnimatedSlider.DURATION / MediaControls.AnimatedSlider.STEPS);
+};
+
+/**
+ * Create a customized slider with a precise time feedback.
+ *
+ * The time value is shown above the slider bar at the mouse position.
+ *
+ * @param {HTMLElement} container The containing div element.
+ * @param {number} value Initial value [0..1].
+ * @param {number} range Number of distinct slider positions to be supported.
+ * @param {function(number)} onChange Value change handler.
+ * @param {function(boolean)} onDrag Drag begin/end handler.
+ * @param {function(number):string} formatFunction Value formatting function.
+ * @constructor
+ */
+MediaControls.PreciseSlider = function(
+ container, value, range, onChange, onDrag, formatFunction) {
+ MediaControls.Slider.apply(this, arguments);
+
+ var doc = this.container_.ownerDocument;
+
+ /**
+ * @type {function(number):string}
+ * @private
+ */
+ this.valueToString_ = null;
+
+ this.seekMark_ = doc.createElement('div');
+ this.seekMark_.className = 'seek-mark';
+ this.getBar().appendChild(this.seekMark_);
+
+ this.seekLabel_ = doc.createElement('div');
+ this.seekLabel_.className = 'seek-label';
+ this.seekMark_.appendChild(this.seekLabel_);
+
+ this.getContainer().addEventListener(
+ 'mousemove', this.onMouseMove_.bind(this));
+ this.getContainer().addEventListener(
+ 'mouseout', this.onMouseOut_.bind(this));
+};
+
+MediaControls.PreciseSlider.prototype = {
+ __proto__: MediaControls.Slider.prototype
+};
+
+/**
+ * Show the seek mark after a delay.
+ */
+MediaControls.PreciseSlider.SHOW_DELAY = 200;
+
+/**
+ * Hide the seek mark for this long after changing the position with a click.
+ */
+MediaControls.PreciseSlider.HIDE_AFTER_MOVE_DELAY = 2500;
+
+/**
+ * Hide the seek mark for this long after changing the position with a drag.
+ */
+MediaControls.PreciseSlider.HIDE_AFTER_DRAG_DELAY = 750;
+
+/**
+ * Default hide timeout (no hiding).
+ */
+MediaControls.PreciseSlider.NO_AUTO_HIDE = 0;
+
+/**
+ * @param {function(number):string} func Value formatting function.
+ */
+MediaControls.PreciseSlider.prototype.setValueToStringFunction =
+ function(func) {
+ this.valueToString_ = func;
+
+ /* It is not completely accurate to assume that the max value corresponds
+ to the longest string, but generous CSS padding will compensate for that. */
+ var labelWidth = this.valueToString_(1).length / 2 + 1;
+ this.seekLabel_.style.width = labelWidth + 'em';
+ this.seekLabel_.style.marginLeft = -labelWidth / 2 + 'em';
+};
+
+/**
+ * Show the time above the slider.
+ *
+ * @param {number} ratio [0..1] The proportion of the duration.
+ * @param {number} timeout Timeout in ms after which the label should be hidden.
+ * MediaControls.PreciseSlider.NO_AUTO_HIDE means show until the next call.
+ * @private
+ */
+MediaControls.PreciseSlider.prototype.showSeekMark_ =
+ function(ratio, timeout) {
+ // Do not update the seek mark for the first 500ms after the drag is finished.
+ if (this.latestMouseUpTime_ && (this.latestMouseUpTime_ + 500 > Date.now()))
+ return;
+
+ this.seekMark_.style.left = ratio * 100 + '%';
+
+ if (ratio < this.getValue()) {
+ this.seekMark_.classList.remove('inverted');
+ } else {
+ this.seekMark_.classList.add('inverted');
+ }
+ this.seekLabel_.textContent = this.valueToString_(ratio);
+
+ this.seekMark_.classList.add('visible');
+
+ if (this.seekMarkTimer_) {
+ clearTimeout(this.seekMarkTimer_);
+ this.seekMarkTimer_ = null;
+ }
+ if (timeout != MediaControls.PreciseSlider.NO_AUTO_HIDE) {
+ this.seekMarkTimer_ = setTimeout(this.hideSeekMark_.bind(this), timeout);
+ }
+};
+
+/**
+ * @private
+ */
+MediaControls.PreciseSlider.prototype.hideSeekMark_ = function() {
+ this.seekMarkTimer_ = null;
+ this.seekMark_.classList.remove('visible');
+};
+
+/**
+ * 'mouseout' event handler.
+ * @param {Event} e Event.
+ * @private
+ */
+MediaControls.PreciseSlider.prototype.onMouseMove_ = function(e) {
+ this.latestSeekRatio_ = this.getProportion(e.clientX);
+
+ var self = this;
+ function showMark() {
+ if (!self.isDragging()) {
+ self.showSeekMark_(self.latestSeekRatio_,
+ MediaControls.PreciseSlider.HIDE_AFTER_MOVE_DELAY);
+ }
+ }
+
+ if (this.seekMark_.classList.contains('visible')) {
+ showMark();
+ } else if (!this.seekMarkTimer_) {
+ this.seekMarkTimer_ =
+ setTimeout(showMark, MediaControls.PreciseSlider.SHOW_DELAY);
+ }
+};
+
+/**
+ * 'mouseout' event handler.
+ * @param {Event} e Event.
+ * @private
+ */
+MediaControls.PreciseSlider.prototype.onMouseOut_ = function(e) {
+ for (var element = e.relatedTarget; element; element = element.parentNode) {
+ if (element == this.getContainer())
+ return;
+ }
+ if (this.seekMarkTimer_) {
+ clearTimeout(this.seekMarkTimer_);
+ this.seekMarkTimer_ = null;
+ }
+ this.hideSeekMark_();
+};
+
+/**
+ * 'change' event handler.
+ * @private
+ */
+MediaControls.PreciseSlider.prototype.onInputChange_ = function() {
+ MediaControls.Slider.prototype.onInputChange_.apply(this, arguments);
+ if (this.isDragging()) {
+ this.showSeekMark_(
+ this.getValue(), MediaControls.PreciseSlider.NO_AUTO_HIDE);
+ }
+};
+
+/**
+ * Mousedown/mouseup handler.
+ * @param {boolean} on True if the mouse is down.
+ * @private
+ */
+MediaControls.PreciseSlider.prototype.onInputDrag_ = function(on) {
+ MediaControls.Slider.prototype.onInputDrag_.apply(this, arguments);
+
+ if (on) {
+ // Dragging started, align the seek mark with the thumb position.
+ this.showSeekMark_(
+ this.getValue(), MediaControls.PreciseSlider.NO_AUTO_HIDE);
+ } else {
+ // Just finished dragging.
+ // Show the label for the last time with a shorter timeout.
+ this.showSeekMark_(
+ this.getValue(), MediaControls.PreciseSlider.HIDE_AFTER_DRAG_DELAY);
+ this.latestMouseUpTime_ = Date.now();
+ }
+};
+
+/**
+ * Create video controls.
+ *
+ * @param {HTMLElement} containerElement The container for the controls.
+ * @param {function} onMediaError Function to display an error message.
+ * @param {function(string):string} stringFunction Function providing localized
+ * strings.
+ * @param {function=} opt_fullScreenToggle Function to toggle fullscreen mode.
+ * @param {HTMLElement=} opt_stateIconParent The parent for the icon that
+ * gives visual feedback when the playback state changes.
+ * @constructor
+ */
+function VideoControls(containerElement, onMediaError, stringFunction,
+ opt_fullScreenToggle, opt_stateIconParent) {
+ MediaControls.call(this, containerElement, onMediaError);
+ this.stringFunction_ = stringFunction;
+
+ this.container_.classList.add('video-controls');
+ this.initPlayButton();
+ this.initTimeControls(true /* show seek mark */);
+ this.initVolumeControls();
+
+ if (opt_fullScreenToggle) {
+ this.fullscreenButton_ =
+ this.createButton('fullscreen', opt_fullScreenToggle);
+ }
+
+ if (opt_stateIconParent) {
+ this.stateIcon_ = this.createControl(
+ 'playback-state-icon', opt_stateIconParent);
+ this.textBanner_ = this.createControl('text-banner', opt_stateIconParent);
+ }
+
+ var videoControls = this;
+ chrome.mediaPlayerPrivate.onTogglePlayState.addListener(
+ function() { videoControls.togglePlayStateWithFeedback(); });
+}
+
+/**
+ * No resume if we are within this margin from the start or the end.
+ */
+VideoControls.RESUME_MARGIN = 0.03;
+
+/**
+ * No resume for videos shorter than this.
+ */
+VideoControls.RESUME_THRESHOLD = 5 * 60; // 5 min.
+
+/**
+ * When resuming rewind back this much.
+ */
+VideoControls.RESUME_REWIND = 5; // seconds.
+
+VideoControls.prototype = { __proto__: MediaControls.prototype };
+
+/**
+ * Shows icon feedback for the current state of the video player.
+ * @private
+ */
+VideoControls.prototype.showIconFeedback_ = function() {
+ this.stateIcon_.removeAttribute('state');
+ setTimeout(function() {
+ this.stateIcon_.setAttribute('state', this.isPlaying() ? 'play' : 'pause');
+ }.bind(this), 0);
+};
+
+/**
+ * Shows a text banner.
+ *
+ * @param {string} identifier String identifier.
+ * @private
+ */
+VideoControls.prototype.showTextBanner_ = function(identifier) {
+ this.textBanner_.removeAttribute('visible');
+ this.textBanner_.textContent = this.stringFunction_(identifier);
+ setTimeout(function() {
+ this.textBanner_.setAttribute('visible', 'true');
+ }.bind(this), 0);
+};
+
+/**
+ * Toggle play/pause state on a mouse click on the play/pause button. Can be
+ * called externally.
+ *
+ * @param {Event} event Mouse click event.
+ */
+VideoControls.prototype.onPlayButtonClicked = function(event) {
+ if (event.ctrlKey) {
+ this.toggleLoopedModeWithFeedback(true);
+ if (!this.isPlaying())
+ this.togglePlayState();
+ } else {
+ this.togglePlayState();
+ }
+};
+
+/**
+ * Media completion handler.
+ */
+VideoControls.prototype.onMediaComplete = function() {
+ this.onMediaPlay_(false); // Just update the UI.
+ this.savePosition(); // This will effectively forget the position.
+};
+
+/**
+ * Toggles the looped mode with feedback.
+ * @param {boolean} on Whether enabled or not.
+ */
+VideoControls.prototype.toggleLoopedModeWithFeedback = function(on) {
+ if (!this.getMedia().duration)
+ return;
+ this.toggleLoopedMode(on);
+ if (on) {
+ // TODO(mtomasz): Simplify, crbug.com/254318.
+ this.showTextBanner_('GALLERY_VIDEO_LOOPED_MODE');
+ }
+};
+
+/**
+ * Toggles the looped mode.
+ * @param {boolean} on Whether enabled or not.
+ */
+VideoControls.prototype.toggleLoopedMode = function(on) {
+ this.getMedia().loop = on;
+};
+
+/**
+ * Toggles play/pause state and flash an icon over the video.
+ */
+VideoControls.prototype.togglePlayStateWithFeedback = function() {
+ if (!this.getMedia().duration)
+ return;
+
+ this.togglePlayState();
+ this.showIconFeedback_();
+};
+
+/**
+ * Toggles play/pause state.
+ */
+VideoControls.prototype.togglePlayState = function() {
+ if (this.isPlaying()) {
+ // User gave the Pause command. Save the state and reset the loop mode.
+ this.toggleLoopedMode(false);
+ this.savePosition();
+ }
+ MediaControls.prototype.togglePlayState.apply(this, arguments);
+};
+
+/**
+ * Saves the playback position to the persistent storage.
+ * @param {boolean=} opt_sync True if the position must be saved synchronously
+ * (required when closing app windows).
+ */
+VideoControls.prototype.savePosition = function(opt_sync) {
+ if (!this.media_.duration ||
+ this.media_.duration < VideoControls.RESUME_THRESHOLD) {
+ return;
+ }
+
+ var ratio = this.media_.currentTime / this.media_.duration;
+ var position;
+ if (ratio < VideoControls.RESUME_MARGIN ||
+ ratio > (1 - VideoControls.RESUME_MARGIN)) {
+ // We are too close to the beginning or the end.
+ // Remove the resume position so that next time we start from the beginning.
+ position = null;
+ } else {
+ position = Math.floor(
+ Math.max(0, this.media_.currentTime - VideoControls.RESUME_REWIND));
+ }
+
+ if (opt_sync) {
+ // Packaged apps cannot save synchronously.
+ // Pass the data to the background page.
+ if (!window.saveOnExit)
+ window.saveOnExit = [];
+ window.saveOnExit.push({ key: this.media_.src, value: position });
+ } else {
+ util.AppCache.update(this.media_.src, position);
+ }
+};
+
+/**
+ * Resumes the playback position saved in the persistent storage.
+ */
+VideoControls.prototype.restorePlayState = function() {
+ if (this.media_.duration >= VideoControls.RESUME_THRESHOLD) {
+ util.AppCache.getValue(this.media_.src, function(position) {
+ if (position)
+ this.media_.currentTime = position;
+ }.bind(this));
+ }
+};
+
+/**
+ * Updates style to best fit the size of the container.
+ */
+VideoControls.prototype.updateStyle = function() {
+ // We assume that the video controls element fills the parent container.
+ // This is easier than adding margins to this.container_.clientWidth.
+ var width = this.container_.parentNode.clientWidth;
+
+ // Set the margin to 5px for width >= 400, 0px for width < 160,
+ // interpolate linearly in between.
+ this.container_.style.margin =
+ Math.ceil((Math.max(160, Math.min(width, 400)) - 160) / 48) + 'px';
+
+ var hideBelow = function(selector, limit) {
+ this.container_.querySelector(selector).style.display =
+ width < limit ? 'none' : '-webkit-box';
+ }.bind(this);
+
+ hideBelow('.time', 350);
+ hideBelow('.volume', 275);
+ hideBelow('.volume-controls', 210);
+ hideBelow('.fullscreen', 150);
+};
+
+/**
+ * Creates audio controls.
+ *
+ * @param {HTMLElement} container Parent container.
+ * @param {function(boolean)} advanceTrack Parameter: true=forward.
+ * @param {function} onError Error handler.
+ * @constructor
+ */
+function AudioControls(container, advanceTrack, onError) {
+ MediaControls.call(this, container, onError);
+
+ this.container_.classList.add('audio-controls');
+
+ this.advanceTrack_ = advanceTrack;
+
+ this.initPlayButton();
+ this.initTimeControls(false /* no seek mark */);
+ /* No volume controls */
+ this.createButton('previous', this.onAdvanceClick_.bind(this, false));
+ this.createButton('next', this.onAdvanceClick_.bind(this, true));
+
+ var audioControls = this;
+ chrome.mediaPlayerPrivate.onNextTrack.addListener(
+ function() { audioControls.onAdvanceClick_(true); });
+ chrome.mediaPlayerPrivate.onPrevTrack.addListener(
+ function() { audioControls.onAdvanceClick_(false); });
+ chrome.mediaPlayerPrivate.onTogglePlayState.addListener(
+ function() { audioControls.togglePlayState(); });
+}
+
+AudioControls.prototype = { __proto__: MediaControls.prototype };
+
+/**
+ * Media completion handler. Advances to the next track.
+ */
+AudioControls.prototype.onMediaComplete = function() {
+ this.advanceTrack_(true);
+};
+
+/**
+ * The track position after which "previous" button acts as "restart".
+ */
+AudioControls.TRACK_RESTART_THRESHOLD = 5; // seconds.
+
+/**
+ * @param {boolean} forward True if advancing forward.
+ * @private
+ */
+AudioControls.prototype.onAdvanceClick_ = function(forward) {
+ if (!forward &&
+ (this.getMedia().currentTime > AudioControls.TRACK_RESTART_THRESHOLD)) {
+ // We are far enough from the beginning of the current track.
+ // Restart it instead of than skipping to the previous one.
+ this.getMedia().currentTime = 0;
+ } else {
+ this.advanceTrack_(forward);
+ }
+};
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/media/media_util.js b/chromium/chrome/browser/resources/file_manager/foreground/js/media/media_util.js
new file mode 100644
index 00000000000..a4c28348048
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/media/media_util.js
@@ -0,0 +1,421 @@
+// 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.
+
+'use strict';
+
+/**
+ * Loads a thumbnail using provided url. In CANVAS mode, loaded images
+ * are attached as <canvas> element, while in IMAGE mode as <img>.
+ * <canvas> renders faster than <img>, however has bigger memory overhead.
+ *
+ * @param {string} url File URL.
+ * @param {ThumbnailLoader.LoaderType=} opt_loaderType Canvas or Image loader,
+ * default: IMAGE.
+ * @param {Object=} opt_metadata Metadata object.
+ * @param {string=} opt_mediaType Media type.
+ * @param {ThumbnailLoader.UseEmbedded=} opt_useEmbedded If to use embedded
+ * jpeg thumbnail if available. Default: USE_EMBEDDED.
+ * @param {number=} opt_priority Priority, the highest is 0. default: 2.
+ * @constructor
+ */
+function ThumbnailLoader(url, opt_loaderType, opt_metadata, opt_mediaType,
+ opt_useEmbedded, opt_priority) {
+ opt_useEmbedded = opt_useEmbedded || ThumbnailLoader.UseEmbedded.USE_EMBEDDED;
+
+ this.mediaType_ = opt_mediaType || FileType.getMediaType(url);
+ this.loaderType_ = opt_loaderType || ThumbnailLoader.LoaderType.IMAGE;
+ this.metadata_ = opt_metadata;
+ this.priority_ = (opt_priority !== undefined) ? opt_priority : 2;
+ this.transform_ = null;
+
+ if (!opt_metadata) {
+ this.thumbnailUrl_ = url; // Use the URL directly.
+ return;
+ }
+
+ this.fallbackUrl_ = null;
+ this.thumbnailUrl_ = null;
+ if (opt_metadata.drive && opt_metadata.drive.customIconUrl)
+ this.fallbackUrl_ = opt_metadata.drive.customIconUrl;
+
+ // Fetch the rotation from the Drive metadata (if available).
+ var driveTransform;
+ if (opt_metadata.drive && opt_metadata.drive.imageRotation !== undefined) {
+ driveTransform = {
+ scaleX: 1,
+ scaleY: 1,
+ rotate90: opt_metadata.drive.imageRotation / 90
+ };
+ }
+
+ if (opt_metadata.thumbnail && opt_metadata.thumbnail.url &&
+ opt_useEmbedded == ThumbnailLoader.UseEmbedded.USE_EMBEDDED) {
+ this.thumbnailUrl_ = opt_metadata.thumbnail.url;
+ this.transform_ = driveTransform !== undefined ? driveTransform :
+ opt_metadata.thumbnail.transform;
+ } else if (FileType.isImage(url)) {
+ this.thumbnailUrl_ = url;
+ this.transform_ = driveTransform !== undefined ? driveTransform :
+ opt_metadata.media && opt_metadata.media.imageTransform;
+ } else if (this.fallbackUrl_) {
+ // Use fallback as the primary thumbnail.
+ this.thumbnailUrl_ = this.fallbackUrl_;
+ this.fallbackUrl_ = null;
+ } // else the generic thumbnail based on the media type will be used.
+}
+
+/**
+ * In percents (0.0 - 1.0), how much area can be cropped to fill an image
+ * in a container, when loading a thumbnail in FillMode.AUTO mode.
+ * The specified 30% value allows to fill 16:9, 3:2 pictures in 4:3 element.
+ * @type {number}
+ */
+ThumbnailLoader.AUTO_FILL_THRESHOLD = 0.3;
+
+/**
+ * Type of displaying a thumbnail within a box.
+ * @enum {number}
+ */
+ThumbnailLoader.FillMode = {
+ FILL: 0, // Fill whole box. Image may be cropped.
+ FIT: 1, // Keep aspect ratio, do not crop.
+ OVER_FILL: 2, // Fill whole box with possible stretching.
+ AUTO: 3 // Try to fill, but if incompatible aspect ratio, then fit.
+};
+
+/**
+ * Optimization mode for downloading thumbnails.
+ * @enum {number}
+ */
+ThumbnailLoader.OptimizationMode = {
+ NEVER_DISCARD: 0, // Never discards downloading. No optimization.
+ DISCARD_DETACHED: 1 // Canceled if the container is not attached anymore.
+};
+
+/**
+ * Type of element to store the image.
+ * @enum {number}
+ */
+ThumbnailLoader.LoaderType = {
+ IMAGE: 0,
+ CANVAS: 1
+};
+
+/**
+ * Whether to use the embedded thumbnail, or not. The embedded thumbnail may
+ * be small.
+ * @enum {number}
+ */
+ThumbnailLoader.UseEmbedded = {
+ USE_EMBEDDED: 0,
+ NO_EMBEDDED: 1
+};
+
+/**
+ * Maximum thumbnail's width when generating from the full resolution image.
+ * @const
+ * @type {number}
+ */
+ThumbnailLoader.THUMBNAIL_MAX_WIDTH = 500;
+
+/**
+ * Maximum thumbnail's height when generating from the full resolution image.
+ * @const
+ * @type {number}
+ */
+ThumbnailLoader.THUMBNAIL_MAX_HEIGHT = 500;
+
+/**
+ * Loads and attaches an image.
+ *
+ * @param {HTMLElement} box Container element.
+ * @param {ThumbnailLoader.FillMode} fillMode Fill mode.
+ * @param {ThumbnailLoader.OptimizationMode=} opt_optimizationMode Optimization
+ * for downloading thumbnails. By default optimizations are disabled.
+ * @param {function(Image, Object)} opt_onSuccess Success callback,
+ * accepts the image and the transform.
+ * @param {function} opt_onError Error callback.
+ * @param {function} opt_onGeneric Callback for generic image used.
+ */
+ThumbnailLoader.prototype.load = function(box, fillMode, opt_optimizationMode,
+ opt_onSuccess, opt_onError, opt_onGeneric) {
+ opt_optimizationMode = opt_optimizationMode ||
+ ThumbnailLoader.OptimizationMode.NEVER_DISCARD;
+
+ if (!this.thumbnailUrl_) {
+ // Relevant CSS rules are in file_types.css.
+ box.setAttribute('generic-thumbnail', this.mediaType_);
+ if (opt_onGeneric) opt_onGeneric();
+ return;
+ }
+
+ this.cancel();
+ this.canvasUpToDate_ = false;
+ this.image_ = new Image();
+ this.image_.onload = function() {
+ this.attachImage(box, fillMode);
+ if (opt_onSuccess)
+ opt_onSuccess(this.image_, this.transform_);
+ }.bind(this);
+ this.image_.onerror = function() {
+ if (opt_onError)
+ opt_onError();
+ if (this.fallbackUrl_) {
+ new ThumbnailLoader(this.fallbackUrl_,
+ this.loaderType_,
+ null, // No metadata.
+ this.mediaType_,
+ undefined, // Default value for use-embedded.
+ this.priority_).
+ load(box, fillMode, opt_optimizationMode, opt_onSuccess);
+ } else {
+ box.setAttribute('generic-thumbnail', this.mediaType_);
+ }
+ }.bind(this);
+
+ if (this.image_.src) {
+ console.warn('Thumbnail already loaded: ' + this.thumbnailUrl_);
+ return;
+ }
+
+ // TODO(mtomasz): Smarter calculation of the requested size.
+ var wasAttached = box.ownerDocument.contains(box);
+ var modificationTime = this.metadata_ &&
+ this.metadata_.filesystem &&
+ this.metadata_.filesystem.modificationTime &&
+ this.metadata_.filesystem.modificationTime.getTime();
+ this.taskId_ = util.loadImage(
+ this.image_,
+ this.thumbnailUrl_,
+ { maxWidth: ThumbnailLoader.THUMBNAIL_MAX_WIDTH,
+ maxHeight: ThumbnailLoader.THUMBNAIL_MAX_HEIGHT,
+ cache: true,
+ priority: this.priority_,
+ timestamp: modificationTime },
+ function() {
+ if (opt_optimizationMode ==
+ ThumbnailLoader.OptimizationMode.DISCARD_DETACHED &&
+ !box.ownerDocument.contains(box)) {
+ // If the container is not attached, then invalidate the download.
+ return false;
+ }
+ return true;
+ });
+};
+
+/**
+ * Cancels loading the current image.
+ */
+ThumbnailLoader.prototype.cancel = function() {
+ if (this.taskId_) {
+ this.image_.onload = function() {};
+ this.image_.onerror = function() {};
+ util.cancelLoadImage(this.taskId_);
+ this.taskId_ = null;
+ }
+};
+
+/**
+ * @return {boolean} True if a valid image is loaded.
+ */
+ThumbnailLoader.prototype.hasValidImage = function() {
+ return !!(this.image_ && this.image_.width && this.image_.height);
+};
+
+/**
+ * @return {boolean} True if the image is rotated 90 degrees left or right.
+ * @private
+ */
+ThumbnailLoader.prototype.isRotated_ = function() {
+ return this.transform_ && (this.transform_.rotate90 % 2 == 1);
+};
+
+/**
+ * @return {number} Image width (corrected for rotation).
+ */
+ThumbnailLoader.prototype.getWidth = function() {
+ return this.isRotated_() ? this.image_.height : this.image_.width;
+};
+
+/**
+ * @return {number} Image height (corrected for rotation).
+ */
+ThumbnailLoader.prototype.getHeight = function() {
+ return this.isRotated_() ? this.image_.width : this.image_.height;
+};
+
+/**
+ * Load an image but do not attach it.
+ *
+ * @param {function(boolean)} callback Callback, parameter is true if the image
+ * has loaded successfully or a stock icon has been used.
+ */
+ThumbnailLoader.prototype.loadDetachedImage = function(callback) {
+ if (!this.thumbnailUrl_) {
+ callback(true);
+ return;
+ }
+
+ this.cancel();
+ this.canvasUpToDate_ = false;
+ this.image_ = new Image();
+ this.image_.onload = callback.bind(null, true);
+ this.image_.onerror = callback.bind(null, false);
+
+ // TODO(mtomasz): Smarter calculation of the requested size.
+ var modificationTime = this.metadata_ &&
+ this.metadata_.filesystem &&
+ this.metadata_.filesystem.modificationTime &&
+ this.metadata_.filesystem.modificationTime.getTime();
+ this.taskId_ = util.loadImage(
+ this.image_,
+ this.thumbnailUrl_,
+ { maxWidth: ThumbnailLoader.THUMBNAIL_MAX_WIDTH,
+ maxHeight: ThumbnailLoader.THUMBNAIL_MAX_HEIGHT,
+ cache: true,
+ priority: this.priority_,
+ timestamp: modificationTime });
+};
+
+/**
+ * Renders the thumbnail into either canvas or an image element.
+ * @private
+ */
+ThumbnailLoader.prototype.renderMedia_ = function() {
+ if (this.loaderType_ != ThumbnailLoader.LoaderType.CANVAS)
+ return;
+
+ if (!this.canvas_)
+ this.canvas_ = document.createElement('canvas');
+
+ // Copy the image to a canvas if the canvas is outdated.
+ if (!this.canvasUpToDate_) {
+ this.canvas_.width = this.image_.width;
+ this.canvas_.height = this.image_.height;
+ var context = this.canvas_.getContext('2d');
+ context.drawImage(this.image_, 0, 0);
+ this.canvasUpToDate_ = true;
+ }
+};
+
+/**
+ * Attach the image to a given element.
+ * @param {Element} container Parent element.
+ * @param {ThumbnailLoader.FillMode} fillMode Fill mode.
+ */
+ThumbnailLoader.prototype.attachImage = function(container, fillMode) {
+ if (!this.hasValidImage()) {
+ container.setAttribute('generic-thumbnail', this.mediaType_);
+ return;
+ }
+
+ this.renderMedia_();
+ util.applyTransform(container, this.transform_);
+ var attachableMedia = this.loaderType_ == ThumbnailLoader.LoaderType.CANVAS ?
+ this.canvas_ : this.image_;
+
+ ThumbnailLoader.centerImage_(
+ container, attachableMedia, fillMode, this.isRotated_());
+
+ if (attachableMedia.parentNode != container) {
+ container.textContent = '';
+ container.appendChild(attachableMedia);
+ }
+
+ if (!this.taskId_)
+ attachableMedia.classList.add('cached');
+};
+
+/**
+ * Gets the loaded image.
+ * TODO(mtomasz): Apply transformations.
+ *
+ * @return {Image|HTMLCanvasElement} Either image or a canvas object.
+ */
+ThumbnailLoader.prototype.getImage = function() {
+ this.renderMedia_();
+ return this.loaderType_ == ThumbnailLoader.LoaderType.CANVAS ? this.canvas_ :
+ this.image_;
+};
+
+/**
+ * Update the image style to fit/fill the container.
+ *
+ * Using webkit center packing does not align the image properly, so we need
+ * to wait until the image loads and its dimensions are known, then manually
+ * position it at the center.
+ *
+ * @param {HTMLElement} box Containing element.
+ * @param {Image|HTMLCanvasElement} img Element containing an image.
+ * @param {ThumbnailLoader.FillMode} fillMode Fill mode.
+ * @param {boolean} rotate True if the image should be rotated 90 degrees.
+ * @private
+ */
+ThumbnailLoader.centerImage_ = function(box, img, fillMode, rotate) {
+ var imageWidth = img.width;
+ var imageHeight = img.height;
+
+ var fractionX;
+ var fractionY;
+
+ var boxWidth = box.clientWidth;
+ var boxHeight = box.clientHeight;
+
+ var fill;
+ switch (fillMode) {
+ case ThumbnailLoader.FillMode.FILL:
+ case ThumbnailLoader.FillMode.OVER_FILL:
+ fill = true;
+ break;
+ case ThumbnailLoader.FillMode.FIT:
+ fill = false;
+ break;
+ case ThumbnailLoader.FillMode.AUTO:
+ var imageRatio = imageWidth / imageHeight;
+ var boxRatio = 1.0;
+ if (boxWidth && boxHeight)
+ boxRatio = boxWidth / boxHeight;
+ // Cropped area in percents.
+ var ratioFactor = boxRatio / imageRatio;
+ fill = (ratioFactor >= 1.0 - ThumbnailLoader.AUTO_FILL_THRESHOLD) &&
+ (ratioFactor <= 1.0 + ThumbnailLoader.AUTO_FILL_THRESHOLD);
+ break;
+ }
+
+ if (boxWidth && boxHeight) {
+ // When we know the box size we can position the image correctly even
+ // in a non-square box.
+ var fitScaleX = (rotate ? boxHeight : boxWidth) / imageWidth;
+ var fitScaleY = (rotate ? boxWidth : boxHeight) / imageHeight;
+
+ var scale = fill ?
+ Math.max(fitScaleX, fitScaleY) :
+ Math.min(fitScaleX, fitScaleY);
+
+ if (fillMode != ThumbnailLoader.FillMode.OVER_FILL)
+ scale = Math.min(scale, 1); // Never overscale.
+
+ fractionX = imageWidth * scale / boxWidth;
+ fractionY = imageHeight * scale / boxHeight;
+ } else {
+ // We do not know the box size so we assume it is square.
+ // Compute the image position based only on the image dimensions.
+ // First try vertical fit or horizontal fill.
+ fractionX = imageWidth / imageHeight;
+ fractionY = 1;
+ if ((fractionX < 1) == !!fill) { // Vertical fill or horizontal fit.
+ fractionY = 1 / fractionX;
+ fractionX = 1;
+ }
+ }
+
+ function percent(fraction) {
+ return (fraction * 100).toFixed(2) + '%';
+ }
+
+ img.style.width = percent(fractionX);
+ img.style.height = percent(fractionY);
+ img.style.left = percent((1 - fractionX) / 2);
+ img.style.top = percent((1 - fractionY) / 2);
+};
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/media/mediaplayer_scripts.js b/chromium/chrome/browser/resources/file_manager/foreground/js/media/mediaplayer_scripts.js
new file mode 100644
index 00000000000..496a8d09d98
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/media/mediaplayer_scripts.js
@@ -0,0 +1,33 @@
+// 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.
+
+// The include directives are put into Javascript-style comments to prevent
+// parsing errors in non-flattened mode. The flattener still sees them.
+// Note that this makes the flattener to comment out the first line of the
+// included file but that's all right since any javascript file should start
+// with a copyright comment anyway.
+
+
+//<include src="../../../../../../../ui/webui/resources/js/cr.js"/>
+//<include src="../../../../../../../ui/webui/resources/js/cr/event_target.js"/>
+//<include src="../../../../../../../ui/webui/resources/js/cr/ui/array_data_model.js"/>
+
+(function() {
+// 'strict mode' is invoked for this scope.
+
+//<include src="../../../common/js/async_util.js"/>
+//<include src="../../../common/js/util.js"/>
+//<include src="../../../common/js/path_util.js"/>
+//<include src="../file_type.js"/>
+//<include src="../volume_manager_wrapper.js">
+//<include src="../metadata/metadata_cache.js"/>
+
+//<include src="media_controls.js"/>
+//<include src="audio_player.js"/>
+//<include src="player_testapi.js"/>
+
+window.reload = reload;
+window.unload = unload;
+
+})();
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/media/player_testapi.js b/chromium/chrome/browser/resources/file_manager/foreground/js/media/player_testapi.js
new file mode 100644
index 00000000000..75f0b302502
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/media/player_testapi.js
@@ -0,0 +1,193 @@
+// 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.
+
+'use strict';
+
+/**
+ * Test API for Chrome OS Video Player and Audio Player.
+ *
+ * To test the Video Player open a tab with the URL:
+ * chrome-extension://hhaomjibdihmijegdhdafkllkbggdgoj/video_player.html
+ *
+ * To test the Audio Player open a tab with the URL:
+ * chrome-extension://hhaomjibdihmijegdhdafkllkbggdgoj/mediaplayer.html
+ *
+ */
+var playerTestAPI = {
+
+ /* Methods common for audio and video players */
+
+ /**
+ * Respond with the path to the current media source.
+ */
+ getSrc: function() {
+ playerTestAPI.respond_(util.extractFilePath(playerTestAPI.getMedia_().src));
+ },
+
+ /**
+ * Respond with a boolean value, true if the media is playing.
+ */
+ isPlaying: function() {
+ playerTestAPI.respond_(playerTestAPI.getControls_().isPlaying());
+ },
+
+ /**
+ * Play the media.
+ */
+ play: function() {
+ playerTestAPI.getControls_().play();
+ },
+
+ /**
+ * Pause the playback.
+ */
+ pause: function() {
+ playerTestAPI.getControls_().pause();
+ },
+
+ /**
+ * Respond with a number, duration of the media in seconds.
+ */
+ getDuration: function() {
+ playerTestAPI.respond_(playerTestAPI.getMedia_().duration);
+ },
+
+ /**
+ * Respond with a number, current media position in seconds.
+ */
+ getCurrentTime: function() {
+ playerTestAPI.respond_(playerTestAPI.getMedia_().currentTime);
+ },
+
+ /**
+ * Change media position.
+ * @param {number} time Media positions.
+ */
+ seekTo: function(time) {
+ playerTestAPI.getMedia_().currentTime = time;
+ },
+
+ /* Video player-specific methods.
+ *
+ * To test the video player open a tab with the url:
+ * chrome-extension://hhaomjibdihmijegdhdafkllkbggdgoj/mediaplayer.html
+ *
+ */
+
+ /**
+ * Load the specified file in the video player,
+ * Starts playing immediately.
+ * @param {string} filePath File path.
+ */
+ loadVideo: function(filePath) {
+ var url = util.makeFilesystemUrl(filePath);
+ location.href = location.origin + location.pathname + '?' + url;
+ reload();
+ },
+
+ /**
+ * Respond with a number, current volume [0..100].
+ */
+ getVolume: function() {
+ playerTestAPI.respond_(playerTestAPI.getMedia_().volume * 100);
+ },
+
+ /**
+ * Change volume.
+ * @param {number} volume Volume [0..100].
+ */
+ setVolume: function(volume) {
+ playerTestAPI.respond_(
+ playerTestAPI.getControls_().onVolumeChange_(volume / 100));
+ },
+
+ /**
+ * Respond with a boolean, true if the volume is muted.
+ */
+ isMuted: function() {
+ playerTestAPI.respond_(playerTestAPI.getMedia_().volume == 0);
+ },
+
+ /**
+ * Mute the volume. No-op if already muted.
+ */
+ mute: function() {
+ if (playerTestAPI.getMedia_().volume != 0)
+ playerTestAPI.getControls_().onSoundButtonClick_();
+ },
+
+ /**
+ * Unmute the volume. No-op if not muted.
+ */
+ unmute: function() {
+ if (playerTestAPI.getMedia_().volume == 0)
+ playerTestAPI.getControls_().onSoundButtonClick_();
+ },
+
+ /* Audio player-specific methods. */
+
+ /**
+ * Load a group of tracks into the audio player.
+ * Starts playing one of the tracks immediately.
+ * @param {Array.<string>} filePaths Array of file paths.
+ * @param {number} firstTrack Number of the file to play first (0-based).
+ */
+ loadAudio: function(filePaths, firstTrack) {
+ AudioPlayer.instance.load({
+ items: filePaths.map(util.makeFilesystemUrl),
+ position: firstTrack
+ });
+ },
+
+ /**
+ * Respond with a current track number,
+ */
+ getTrackNumber: function() {
+ playerTestAPI.respond_(AudioPlayer.instance.currentTrack_);
+ },
+
+ /**
+ * Play the next track.
+ */
+ forward: function() {
+ playerTestAPI.getControls_().onAdvanceClick_(true /* forward */);
+ },
+
+ /**
+ * Go back. Will restart the current track if the current position is > 5 sec
+ * or play the previous track otherwise.
+ */
+ back: function() {
+ playerTestAPI.getControls_().onAdvanceClick_(false /* back */);
+ },
+
+ /* Utility methods */
+
+ /**
+ * @return {AudioControls|VideoControls} Media controls.
+ * @private
+ */
+ getControls_: function() {
+ return window.controls || window.AudioPlayer.instance.audioControls_;
+ },
+
+ /**
+ * @return {HTMLVideoElement|HTMLAudioElement} Media element.
+ * @private
+ */
+ getMedia_: function() {
+ return playerTestAPI.getControls_().getMedia();
+ },
+
+ /**
+ * @param {string|boolean|number} value Value to send back.
+ * @private
+ */
+ respond_: function(value) {
+ if (window.domAutomationController)
+ window.domAutomationController.send(value);
+ else
+ console.log('playerTestAPI response: ' + value);
+ }
+};
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/media/util.js b/chromium/chrome/browser/resources/file_manager/foreground/js/media/util.js
new file mode 100644
index 00000000000..4d3c953dd55
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/media/util.js
@@ -0,0 +1,179 @@
+// 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.
+
+'use strict';
+
+/**
+ * A controller class detects mouse inactivity and hides "tool" elements.
+ *
+ * @param {Element} container The main DOM container.
+ * @param {number=} opt_timeout Hide timeout in ms.
+ * @param {function():boolean=} opt_toolsActive Function that returns |true|
+ * if the tools are active and should not be hidden.
+ * @constructor
+ */
+function MouseInactivityWatcher(container, opt_timeout, opt_toolsActive) {
+ this.container_ = container;
+ this.timeout_ = opt_timeout || MouseInactivityWatcher.DEFAULT_TIMEOUT;
+ this.toolsActive_ = opt_toolsActive || function() { return false };
+
+ this.onTimeoutBound_ = this.onTimeout_.bind(this);
+ this.timeoutID_ = null;
+ this.mouseOverTool_ = false;
+
+ this.clientX_ = 0;
+ this.clientY_ = 0;
+
+ /**
+ * Indicates if the inactivity watcher is enabled or disabled. Use getters
+ * and setters.
+ * @type {boolean}
+ * @private
+ **/
+ this.disabled_ = false;
+ this.__defineSetter__('disabled', function(value) {
+ this.disabled_ = value;
+ if (value)
+ this.kick();
+ else
+ this.check();
+ });
+ this.__defineGetter__('disabled', function() {
+ return this.disabled_;
+ });
+
+ this.container_.addEventListener('mousemove', this.onMouseMove_.bind(this));
+ var tools = this.container_.querySelector('.tool');
+ for (var i = 0; i < tools.length; i++) {
+ tools[i].addEventListener('mouseover', this.onToolMouseOver_.bind(this));
+ tools[i].addEventListener('mouseout', this.onToolMouseOut_.bind(this));
+ }
+
+ // Show tools when the user touches the screen.
+ this.container_.addEventListener(
+ 'touchstart', this.activityStarted_.bind(this));
+ var initiateFading = this.activityStopped_.bind(this, this.timeout_);
+ this.container_.addEventListener('touchend', initiateFading);
+ this.container_.addEventListener('touchcancel', initiateFading);
+}
+
+/**
+ * Default inactivity timeout.
+ */
+MouseInactivityWatcher.DEFAULT_TIMEOUT = 3000;
+
+/**
+ * @param {boolean} on True if show, false if hide.
+ */
+MouseInactivityWatcher.prototype.showTools = function(on) {
+ if (on)
+ this.container_.setAttribute('tools', 'true');
+ else
+ this.container_.removeAttribute('tools');
+};
+
+/**
+ * To be called when the user started activity. Shows the tools
+ * and cancels the countdown.
+ * @private
+ */
+MouseInactivityWatcher.prototype.activityStarted_ = function() {
+ this.showTools(true);
+
+ if (this.timeoutID_) {
+ clearTimeout(this.timeoutID_);
+ this.timeoutID_ = null;
+ }
+};
+
+/**
+ * Called when user activity has stopped. Re-starts the countdown.
+ * @param {number=} opt_timeout Timeout.
+ * @private
+ */
+MouseInactivityWatcher.prototype.activityStopped_ = function(opt_timeout) {
+ if (this.disabled_ || this.mouseOverTool_ || this.toolsActive_())
+ return;
+
+ if (this.timeoutID_)
+ clearTimeout(this.timeoutID_);
+
+ this.timeoutID_ = setTimeout(
+ this.onTimeoutBound_, opt_timeout || this.timeout_);
+};
+
+/**
+ * Called when a user performed a short action (such as a click or a key press)
+ * that should show the tools if they are not visible.
+ * @param {number=} opt_timeout Timeout.
+ */
+MouseInactivityWatcher.prototype.kick = function(opt_timeout) {
+ this.activityStarted_();
+ this.activityStopped_(opt_timeout);
+};
+
+/**
+ * Check if the tools are active and update the tools visibility accordingly.
+ */
+MouseInactivityWatcher.prototype.check = function() {
+ if (this.toolsActive_())
+ this.activityStarted_();
+ else
+ this.activityStopped_();
+};
+
+/**
+ * Mouse move handler.
+ *
+ * @param {Event} e Event.
+ * @private
+ */
+MouseInactivityWatcher.prototype.onMouseMove_ = function(e) {
+ if (this.clientX_ == e.clientX && this.clientY_ == e.clientY) {
+ // The mouse has not moved, must be the cursor change triggered by
+ // some of the attributes on the root container. Ignore the event.
+ return;
+ }
+ this.clientX_ = e.clientX;
+ this.clientY_ = e.clientY;
+
+ if (this.disabled_)
+ return;
+
+ this.kick();
+};
+
+/**
+ * Mouse over handler on a tool element.
+ *
+ * @param {Event} e Event.
+ * @private
+ */
+MouseInactivityWatcher.prototype.onToolMouseOver_ = function(e) {
+ this.mouseOverTool_ = true;
+ if (!this.disabled_)
+ this.kick();
+};
+
+/**
+ * Mouse out handler on a tool element.
+ *
+ * @param {Event} e Event.
+ * @private
+ */
+MouseInactivityWatcher.prototype.onToolMouseOut_ = function(e) {
+ this.mouseOverTool_ = false;
+ if (!this.disabled_)
+ this.kick();
+};
+
+/**
+ * Timeout handler.
+ * @private
+ */
+MouseInactivityWatcher.prototype.onTimeout_ = function() {
+ this.timeoutID_ = null;
+ if (!this.disabled_ && !this.toolsActive_())
+ this.showTools(false);
+};
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/media/video_player.js b/chromium/chrome/browser/resources/file_manager/foreground/js/media/video_player.js
new file mode 100644
index 00000000000..9b4b663fdc1
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/media/video_player.js
@@ -0,0 +1,289 @@
+// 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.
+
+'use strict';
+
+/**
+ * Display error message.
+ * @param {string} message Message id.
+ */
+function showErrorMessage(message) {
+ var errorBanner = document.querySelector('#error');
+ errorBanner.textContent =
+ loadTimeData.getString(message);
+ errorBanner.setAttribute('visible', 'true');
+
+ // The window is hidden if the video has not loaded yet.
+ chrome.app.window.current().show();
+}
+
+/**
+ * Handles playback (decoder) errors.
+ */
+function onPlaybackError() {
+ showErrorMessage('GALLERY_VIDEO_DECODING_ERROR');
+ decodeErrorOccured = true;
+
+ // Disable inactivity watcher, and disable the ui, by hiding tools manually.
+ controls.inactivityWatcher.disabled = true;
+ document.querySelector('#video-player').setAttribute('disabled', 'true');
+
+ // Detach the video element, since it may be unreliable and reset stored
+ // current playback time.
+ controls.cleanup();
+ controls.clearState();
+
+ // Avoid reusing a video element.
+ video.parentNode.removeChild(video);
+ video = null;
+}
+
+/**
+ * @param {Element} playerContainer Main container.
+ * @param {Element} videoContainer Container for the video element.
+ * @param {Element} controlsContainer Container for video controls.
+ * @constructor
+ */
+function FullWindowVideoControls(
+ playerContainer, videoContainer, controlsContainer) {
+ VideoControls.call(this,
+ controlsContainer,
+ onPlaybackError,
+ loadTimeData.getString.bind(loadTimeData),
+ this.toggleFullScreen_.bind(this),
+ videoContainer);
+
+ this.playerContainer_ = playerContainer;
+
+ this.updateStyle();
+ window.addEventListener('resize', this.updateStyle.bind(this));
+
+ document.addEventListener('keydown', function(e) {
+ if (e.keyIdentifier == 'U+0020') { // Space
+ this.togglePlayStateWithFeedback();
+ e.preventDefault();
+ }
+ if (e.keyIdentifier == 'U+001B') { // Escape
+ util.toggleFullScreen(
+ chrome.app.window.current(),
+ false); // Leave the full screen mode.
+ e.preventDefault();
+ }
+ }.bind(this));
+
+ // TODO(mtomasz): Simplify. crbug.com/254318.
+ videoContainer.addEventListener('click', function(e) {
+ if (e.ctrlKey) {
+ this.toggleLoopedModeWithFeedback(true);
+ if (!this.isPlaying())
+ this.togglePlayStateWithFeedback();
+ } else {
+ this.togglePlayStateWithFeedback();
+ }
+ }.bind(this));
+
+ this.inactivityWatcher_ = new MouseInactivityWatcher(playerContainer);
+ this.__defineGetter__('inactivityWatcher', function() {
+ return this.inactivityWatcher_;
+ });
+
+ this.inactivityWatcher_.check();
+}
+
+FullWindowVideoControls.prototype = { __proto__: VideoControls.prototype };
+
+/**
+ * Save the current state so that it survives page/app reload.
+ */
+FullWindowVideoControls.prototype.onPlayStateChanged = function() {
+ this.encodeState();
+};
+
+/**
+ * Restore the state after the video is loaded.
+ */
+FullWindowVideoControls.prototype.restorePlayState = function() {
+ if (!this.decodeState()) {
+ VideoControls.prototype.restorePlayState.apply(this, arguments);
+ this.play();
+ }
+};
+
+/**
+ * Toggles the full screen mode.
+ * @private
+ */
+FullWindowVideoControls.prototype.toggleFullScreen_ = function() {
+ var appWindow = chrome.app.window.current();
+ util.toggleFullScreen(appWindow, !util.isFullScreen(appWindow));
+};
+
+// TODO(mtomasz): Convert it to class members: crbug.com/171191.
+var decodeErrorOccured;
+var video;
+var controls;
+var metadataCache;
+var volumeManager;
+var selectedEntry;
+
+/**
+ * Initialize the video player window.
+ */
+function loadVideoPlayer() {
+ document.ondragstart = function(e) { e.preventDefault() };
+
+ chrome.fileBrowserPrivate.getStrings(function(strings) {
+ loadTimeData.data = strings;
+
+ controls = new FullWindowVideoControls(
+ document.querySelector('#video-player'),
+ document.querySelector('#video-container'),
+ document.querySelector('#controls'));
+
+ metadataCache = MetadataCache.createFull();
+ volumeManager = new VolumeManagerWrapper(
+ VolumeManagerWrapper.DriveEnabledStatus.DRIVE_ENABLED);
+ volumeManager.addEventListener('externally-unmounted',
+ onExternallyUnmounted);
+
+ // If the video player is starting before the first instance of the File
+ // Manager then it does not have access to filesystem URLs.
+ // Request it now.
+ volumeManager.ensureInitialized(reload);
+ var reloadVideo = function(e) {
+ if (decodeErrorOccured &&
+ // Ignore shortcut keys
+ !e.ctrlKey && !e.altKey && !e.shiftKey && !e.metaKey) {
+ reload();
+ e.preventDefault();
+ }
+ };
+
+ document.addEventListener('keydown', reloadVideo, true);
+ document.addEventListener('click', reloadVideo, true);
+ });
+}
+
+/**
+ * Closes video player when a volume containing the played item is unmounted.
+ * @param {Event} event The unmount event.
+ */
+function onExternallyUnmounted(event) {
+ if (!selectedEntry)
+ return;
+
+ if (volumeManager.getVolumeInfo(selectedEntry) === event.volumeInfo)
+ window.close();
+}
+
+/**
+ * Unload the player.
+ */
+function unload() {
+ if (volumeManager)
+ volumeManager.dispose();
+
+ if (!controls.getMedia())
+ return;
+
+ controls.savePosition(true /* exiting */);
+ controls.cleanup();
+}
+
+/**
+ * Reload the player.
+ */
+function reload() {
+ // Re-enable ui and hide error message if already displayed.
+ document.querySelector('#video-player').removeAttribute('disabled');
+ document.querySelector('#error').removeAttribute('visible');
+ controls.inactivityWatcher.disabled = false;
+ decodeErrorOccured = false;
+
+ var url;
+ if (window.appState) {
+ util.saveAppState();
+ url = window.appState.url;
+ } else {
+ url = document.location.search.substr(1);
+ }
+
+ document.title = decodeURIComponent(url.split('/').pop());
+ var queue = new AsyncUtil.Queue();
+
+ queue.run(function(callback) {
+ webkitResolveLocalFileSystemURL(url,
+ function(entry) {
+ selectedEntry = entry;
+ callback();
+ }, function() {
+ console.warn('Failed to resolve entry for: ' + url);
+ callback();
+ });
+ });
+
+
+ queue.run(function(callback) {
+ if (!selectedEntry) {
+ showErrorMessage('GALLERY_VIDEO_ERROR');
+ return;
+ }
+ metadataCache.get(selectedEntry, 'streaming', function(streaming) {
+ if (streaming && !navigator.onLine) {
+ showErrorMessage('GALLERY_VIDEO_OFFLINE');
+ return;
+ }
+
+ // Detach the previous video element, if exists.
+ if (video)
+ video.parentNode.removeChild(video);
+
+ video = document.createElement('video');
+ document.querySelector('#video-container').appendChild(video);
+ controls.attachMedia(video);
+
+ video.src = selectedEntry.toURL();
+ video.load();
+ video.addEventListener('loadedmetadata', function() {
+ // TODO: chrome.app.window soon will be able to resize the content area.
+ // Until then use approximate title bar height.
+ var TITLE_HEIGHT = 28;
+
+ var aspect = video.videoWidth / video.videoHeight;
+ var newWidth = video.videoWidth;
+ var newHeight = video.videoHeight + TITLE_HEIGHT;
+
+ var shrinkX = newWidth / window.screen.availWidth;
+ var shrinkY = newHeight / window.screen.availHeight;
+ if (shrinkX > 1 || shrinkY > 1) {
+ if (shrinkY > shrinkX) {
+ newHeight = newHeight / shrinkY;
+ newWidth = (newHeight - TITLE_HEIGHT) * aspect;
+ } else {
+ newWidth = newWidth / shrinkX;
+ newHeight = newWidth / aspect + TITLE_HEIGHT;
+ }
+ }
+
+ var oldLeft = window.screenX;
+ var oldTop = window.screenY;
+ var oldWidth = window.outerWidth;
+ var oldHeight = window.outerHeight;
+
+ if (!oldWidth && !oldHeight) {
+ oldLeft = window.screen.availWidth / 2;
+ oldTop = window.screen.availHeight / 2;
+ }
+
+ var appWindow = chrome.app.window.current();
+ appWindow.resizeTo(newWidth, newHeight);
+ appWindow.moveTo(oldLeft - (newWidth - oldWidth) / 2,
+ oldTop - (newHeight - oldHeight) / 2);
+ appWindow.show();
+ });
+ });
+ });
+}
+
+util.addPageLoadHandler(loadVideoPlayer);
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/media/video_player_scripts.js b/chromium/chrome/browser/resources/file_manager/foreground/js/media/video_player_scripts.js
new file mode 100644
index 00000000000..54ea178f2fe
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/media/video_player_scripts.js
@@ -0,0 +1,34 @@
+// 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.
+
+// The include directives are put into Javascript-style comments to prevent
+// parsing errors in non-flattened mode. The flattener still sees them.
+// Note that this makes the flattener to comment out the first line of the
+// included file but that's all right since any javascript file should start
+// with a copyright comment anyway.
+
+//<include src="../../../../../../../ui/webui/resources/js/cr.js"/>
+//<include src="../../../../../../../ui/webui/resources/js/cr/event_target.js"/>
+//<include src="../../../../../../../ui/webui/resources/js/cr/ui/array_data_model.js"/>
+//<include src="../../../../../../../ui/webui/resources/js/load_time_data.js"/>
+
+(function() {
+// 'strict mode' is invoked for this scope.
+
+//<include src="../../../common/js/async_util.js"/>
+//<include src="../../../common/js/util.js"/>
+//<include src="../../../common/js/path_util.js"/>
+//<include src="../file_type.js"/>
+//<include src="../volume_manager_wrapper.js">
+//<include src="../metadata/metadata_cache.js"/>
+
+//<include src="media_controls.js"/>
+//<include src="util.js"/>
+//<include src="video_player.js"/>
+//<include src="player_testapi.js"/>
+
+window.reload = reload;
+window.unload = unload;
+
+})();
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/byte_reader.js b/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/byte_reader.js
new file mode 100644
index 00000000000..09cf306b53c
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/byte_reader.js
@@ -0,0 +1,535 @@
+// 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.
+
+'use strict';
+
+/**
+ * @constructor
+ * @param {ArrayBuffer} arrayBuffer // TODO(JSDOC).
+ * @param {number=} opt_offset // TODO(JSDOC).
+ * @param {number=} opt_length // TODO(JSDOC).
+ */
+function ByteReader(arrayBuffer, opt_offset, opt_length) {
+ opt_offset = opt_offset || 0;
+ opt_length = opt_length || (arrayBuffer.byteLength - opt_offset);
+ this.view_ = new DataView(arrayBuffer, opt_offset, opt_length);
+ this.pos_ = 0;
+ this.seekStack_ = [];
+ this.setByteOrder(ByteReader.BIG_ENDIAN);
+}
+
+// Static constants and methods.
+
+/**
+ * Intel, 0x1234 is [0x34, 0x12]
+ * @const
+ * @type {number}
+ */
+ByteReader.LITTLE_ENDIAN = 0;
+/**
+ * Motorola, 0x1234 is [0x12, 0x34]
+ * @const
+ * @type {number}
+ */
+ByteReader.BIG_ENDIAN = 1;
+
+/**
+ * Seek relative to the beginning of the buffer.
+ * @const
+ * @type {number}
+ */
+ByteReader.SEEK_BEG = 0;
+/**
+ * Seek relative to the current position.
+ * @const
+ * @type {number}
+ */
+ByteReader.SEEK_CUR = 1;
+/**
+ * Seek relative to the end of the buffer.
+ * @const
+ * @type {number}
+ */
+ByteReader.SEEK_END = 2;
+
+/**
+ * Throw an error if (0 > pos >= end) or if (pos + size > end).
+ *
+ * Static utility function.
+ *
+ * @param {number} pos // TODO(JSDOC).
+ * @param {number} size // TODO(JSDOC).
+ * @param {number} end // TODO(JSDOC).
+ */
+ByteReader.validateRead = function(pos, size, end) {
+ if (pos < 0 || pos >= end)
+ throw new Error('Invalid read position');
+
+ if (pos + size > end)
+ throw new Error('Read past end of buffer');
+};
+
+/**
+ * Read as a sequence of characters, returning them as a single string.
+ *
+ * This is a static utility function. There is a member function with the
+ * same name which side-effects the current read position.
+ *
+ * @param {DataView} dataView // TODO(JSDOC).
+ * @param {number} pos // TODO(JSDOC).
+ * @param {number} size // TODO(JSDOC).
+ * @param {number=} opt_end // TODO(JSDOC).
+ * @return {string} // TODO(JSDOC).
+ */
+ByteReader.readString = function(dataView, pos, size, opt_end) {
+ ByteReader.validateRead(pos, size, opt_end || dataView.byteLength);
+
+ var codes = [];
+
+ for (var i = 0; i < size; ++i)
+ codes.push(dataView.getUint8(pos + i));
+
+ return String.fromCharCode.apply(null, codes);
+};
+
+/**
+ * Read as a sequence of characters, returning them as a single string.
+ *
+ * This is a static utility function. There is a member function with the
+ * same name which side-effects the current read position.
+ *
+ * @param {DataView} dataView // TODO(JSDOC).
+ * @param {number} pos // TODO(JSDOC).
+ * @param {number} size // TODO(JSDOC).
+ * @param {number=} opt_end // TODO(JSDOC).
+ * @return {string} // TODO(JSDOC).
+ */
+ByteReader.readNullTerminatedString = function(dataView, pos, size, opt_end) {
+ ByteReader.validateRead(pos, size, opt_end || dataView.byteLength);
+
+ var codes = [];
+
+ for (var i = 0; i < size; ++i) {
+ var code = dataView.getUint8(pos + i);
+ if (code == 0) break;
+ codes.push(code);
+ }
+
+ return String.fromCharCode.apply(null, codes);
+};
+
+/**
+ * Read as a sequence of UTF16 characters, returning them as a single string.
+ *
+ * This is a static utility function. There is a member function with the
+ * same name which side-effects the current read position.
+ *
+ * @param {DataView} dataView // TODO(JSDOC).
+ * @param {number} pos // TODO(JSDOC).
+ * @param {boolean} bom // TODO(JSDOC).
+ * @param {number} size // TODO(JSDOC).
+ * @param {number=} opt_end // TODO(JSDOC).
+ * @return {string} // TODO(JSDOC).
+ */
+ByteReader.readNullTerminatedStringUTF16 = function(
+ dataView, pos, bom, size, opt_end) {
+ ByteReader.validateRead(pos, size, opt_end || dataView.byteLength);
+
+ var littleEndian = false;
+ var start = 0;
+
+ if (bom) {
+ littleEndian = (dataView.getUint8(pos) == 0xFF);
+ start = 2;
+ }
+
+ var codes = [];
+
+ for (var i = start; i < size; i += 2) {
+ var code = dataView.getUint16(pos + i, littleEndian);
+ if (code == 0) break;
+ codes.push(code);
+ }
+
+ return String.fromCharCode.apply(null, codes);
+};
+
+/**
+ * @const
+ * @type {Array.<string>}
+ * @private
+ */
+ByteReader.base64Alphabet_ =
+ ('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/').
+ split('');
+
+/**
+ * Read as a sequence of bytes, returning them as a single base64 encoded
+ * string.
+ *
+ * This is a static utility function. There is a member function with the
+ * same name which side-effects the current read position.
+ *
+ * @param {DataView} dataView // TODO(JSDOC).
+ * @param {number} pos // TODO(JSDOC).
+ * @param {number} size // TODO(JSDOC).
+ * @param {number=} opt_end // TODO(JSDOC).
+ * @return {string} // TODO(JSDOC).
+ */
+ByteReader.readBase64 = function(dataView, pos, size, opt_end) {
+ ByteReader.validateRead(pos, size, opt_end || dataView.byteLength);
+
+ var rv = [];
+ var chars = [];
+ var padding = 0;
+
+ for (var i = 0; i < size; /* incremented inside */) {
+ var bits = dataView.getUint8(pos + (i++)) << 16;
+
+ if (i < size) {
+ bits |= dataView.getUint8(pos + (i++)) << 8;
+
+ if (i < size) {
+ bits |= dataView.getUint8(pos + (i++));
+ } else {
+ padding = 1;
+ }
+ } else {
+ padding = 2;
+ }
+
+ chars[3] = ByteReader.base64Alphabet_[bits & 63];
+ chars[2] = ByteReader.base64Alphabet_[(bits >> 6) & 63];
+ chars[1] = ByteReader.base64Alphabet_[(bits >> 12) & 63];
+ chars[0] = ByteReader.base64Alphabet_[(bits >> 18) & 63];
+
+ rv.push.apply(rv, chars);
+ }
+
+ if (padding > 0)
+ rv[rv.length - 1] = '=';
+ if (padding > 1)
+ rv[rv.length - 2] = '=';
+
+ return rv.join('');
+};
+
+/**
+ * Read as an image encoded in a data url.
+ *
+ * This is a static utility function. There is a member function with the
+ * same name which side-effects the current read position.
+ *
+ * @param {DataView} dataView // TODO(JSDOC).
+ * @param {number} pos // TODO(JSDOC).
+ * @param {number} size // TODO(JSDOC).
+ * @param {number=} opt_end // TODO(JSDOC).
+ * @return {string} // TODO(JSDOC).
+ */
+ByteReader.readImage = function(dataView, pos, size, opt_end) {
+ opt_end = opt_end || dataView.byteLength;
+ ByteReader.validateRead(pos, size, opt_end);
+
+ // Two bytes is enough to identify the mime type.
+ var prefixToMime = {
+ '\x89P' : 'png',
+ '\xFF\xD8' : 'jpeg',
+ 'BM' : 'bmp',
+ 'GI' : 'gif'
+ };
+
+ var prefix = ByteReader.readString(dataView, pos, 2, opt_end);
+ var mime = prefixToMime[prefix] ||
+ dataView.getUint16(pos, false).toString(16); // For debugging.
+
+ var b64 = ByteReader.readBase64(dataView, pos, size, opt_end);
+ return 'data:image/' + mime + ';base64,' + b64;
+};
+
+// Instance methods.
+
+/**
+ * Return true if the requested number of bytes can be read from the buffer.
+ *
+ * @param {number} size // TODO(JSDOC).
+ * @return {boolean} // TODO(JSDOC).
+ */
+ByteReader.prototype.canRead = function(size) {
+ return this.pos_ + size <= this.view_.byteLength;
+};
+
+/**
+ * Return true if the current position is past the end of the buffer.
+ * @return {boolean} // TODO(JSDOC).
+ */
+ByteReader.prototype.eof = function() {
+ return this.pos_ >= this.view_.byteLength;
+};
+
+/**
+ * Return true if the current position is before the beginning of the buffer.
+ * @return {boolean} // TODO(JSDOC).
+ */
+ByteReader.prototype.bof = function() {
+ return this.pos_ < 0;
+};
+
+/**
+ * Return true if the current position is outside the buffer.
+ * @return {boolean} // TODO(JSDOC).
+ */
+ByteReader.prototype.beof = function() {
+ return this.pos_ >= this.view_.byteLength || this.pos_ < 0;
+};
+
+/**
+ * Set the expected byte ordering for future reads.
+ * @param {number} order // TODO(JSDOC).
+ */
+ByteReader.prototype.setByteOrder = function(order) {
+ this.littleEndian_ = order == ByteReader.LITTLE_ENDIAN;
+};
+
+/**
+ * Throw an error if the reader is at an invalid position, or if a read a read
+ * of |size| would put it in one.
+ *
+ * You may optionally pass opt_end to override what is considered to be the
+ * end of the buffer.
+ *
+ * @param {number} size // TODO(JSDOC).
+ * @param {number=} opt_end // TODO(JSDOC).
+ */
+ByteReader.prototype.validateRead = function(size, opt_end) {
+ if (typeof opt_end == 'undefined')
+ opt_end = this.view_.byteLength;
+
+ ByteReader.validateRead(this.view_, this.pos_, size, opt_end);
+};
+
+/**
+ * @param {number} width // TODO(JSDOC).
+ * @param {boolean=} opt_signed // TODO(JSDOC).
+ * @param {number=} opt_end // TODO(JSDOC).
+ * @return {string} // TODO(JSDOC).
+ */
+ByteReader.prototype.readScalar = function(width, opt_signed, opt_end) {
+ var method = opt_signed ? 'getInt' : 'getUint';
+
+ switch (width) {
+ case 1:
+ method += '8';
+ break;
+
+ case 2:
+ method += '16';
+ break;
+
+ case 4:
+ method += '32';
+ break;
+
+ case 8:
+ method += '64';
+ break;
+
+ default:
+ throw new Error('Invalid width: ' + width);
+ break;
+ }
+
+ this.validateRead(width, opt_end);
+ var rv = this.view_[method](this.pos_, this.littleEndian_);
+ this.pos_ += width;
+ return rv;
+};
+
+/**
+ * Read as a sequence of characters, returning them as a single string.
+ *
+ * Adjusts the current position on success. Throws an exception if the
+ * read would go past the end of the buffer.
+ *
+ * @param {number} size // TODO(JSDOC).
+ * @param {number=} opt_end // TODO(JSDOC).
+ * @return {string} // TODO(JSDOC).
+ */
+ByteReader.prototype.readString = function(size, opt_end) {
+ var rv = ByteReader.readString(this.view_, this.pos_, size, opt_end);
+ this.pos_ += size;
+ return rv;
+};
+
+
+/**
+ * Read as a sequence of characters, returning them as a single string.
+ *
+ * Adjusts the current position on success. Throws an exception if the
+ * read would go past the end of the buffer.
+ *
+ * @param {number} size // TODO(JSDOC).
+ * @param {number=} opt_end // TODO(JSDOC).
+ * @return {string} // TODO(JSDOC).
+ */
+ByteReader.prototype.readNullTerminatedString = function(size, opt_end) {
+ var rv = ByteReader.readNullTerminatedString(this.view_,
+ this.pos_,
+ size,
+ opt_end);
+ this.pos_ += rv.length;
+
+ if (rv.length < size) {
+ // If we've stopped reading because we found '0' but didn't hit size limit
+ // then we should skip additional '0' character
+ this.pos_++;
+ }
+
+ return rv;
+};
+
+
+/**
+ * Read as a sequence of UTF16 characters, returning them as a single string.
+ *
+ * Adjusts the current position on success. Throws an exception if the
+ * read would go past the end of the buffer.
+ *
+ * @param {boolean} bom // TODO(JSDOC).
+ * @param {number} size // TODO(JSDOC).
+ * @param {number=} opt_end // TODO(JSDOC).
+ * @return {string} // TODO(JSDOC).
+ */
+ByteReader.prototype.readNullTerminatedStringUTF16 =
+ function(bom, size, opt_end) {
+ var rv = ByteReader.readNullTerminatedStringUTF16(
+ this.view_, this.pos_, bom, size, opt_end);
+
+ if (bom) {
+ // If the BOM word was present advance the position.
+ this.pos_ += 2;
+ }
+
+ this.pos_ += rv.length;
+
+ if (rv.length < size) {
+ // If we've stopped reading because we found '0' but didn't hit size limit
+ // then we should skip additional '0' character
+ this.pos_ += 2;
+ }
+
+ return rv;
+};
+
+
+/**
+ * Read as an array of numbers.
+ *
+ * Adjusts the current position on success. Throws an exception if the
+ * read would go past the end of the buffer.
+ *
+ * @param {number} size // TODO(JSDOC).
+ * @param {number=} opt_end // TODO(JSDOC).
+ * @param {function(new:Array.<*>)=} opt_arrayConstructor // TODO(JSDOC).
+ * @return {Array.<*>} // TODO(JSDOC).
+ */
+ByteReader.prototype.readSlice = function(size, opt_end,
+ opt_arrayConstructor) {
+ this.validateRead(size, opt_end);
+
+ var arrayConstructor = opt_arrayConstructor || Uint8Array;
+ var slice = new arrayConstructor(
+ this.view_.buffer, this.view_.byteOffset + this.pos, size);
+ this.pos_ += size;
+
+ return slice;
+};
+
+/**
+ * Read as a sequence of bytes, returning them as a single base64 encoded
+ * string.
+ *
+ * Adjusts the current position on success. Throws an exception if the
+ * read would go past the end of the buffer.
+ *
+ * @param {number} size // TODO(JSDOC).
+ * @param {number=} opt_end // TODO(JSDOC).
+ * @return {string} // TODO(JSDOC).
+ */
+ByteReader.prototype.readBase64 = function(size, opt_end) {
+ var rv = ByteReader.readBase64(this.view_, this.pos_, size, opt_end);
+ this.pos_ += size;
+ return rv;
+};
+
+/**
+ * Read an image returning it as a data url.
+ *
+ * Adjusts the current position on success. Throws an exception if the
+ * read would go past the end of the buffer.
+ *
+ * @param {number} size // TODO(JSDOC).
+ * @param {number=} opt_end // TODO(JSDOC).
+ * @return {string} // TODO(JSDOC).
+ */
+ByteReader.prototype.readImage = function(size, opt_end) {
+ var rv = ByteReader.readImage(this.view_, this.pos_, size, opt_end);
+ this.pos_ += size;
+ return rv;
+};
+
+/**
+ * Seek to a give position relative to opt_seekStart.
+ *
+ * @param {number} pos // TODO(JSDOC).
+ * @param {number=} opt_seekStart // TODO(JSDOC).
+ * @param {number=} opt_end // TODO(JSDOC).
+ */
+ByteReader.prototype.seek = function(pos, opt_seekStart, opt_end) {
+ opt_end = opt_end || this.view_.byteLength;
+
+ var newPos;
+ if (opt_seekStart == ByteReader.SEEK_CUR) {
+ newPos = this.pos_ + pos;
+ } else if (opt_seekStart == ByteReader.SEEK_END) {
+ newPos = opt_end + pos;
+ } else {
+ newPos = pos;
+ }
+
+ if (newPos < 0 || newPos > this.view_.byteLength)
+ throw new Error('Seek outside of buffer: ' + (newPos - opt_end));
+
+ this.pos_ = newPos;
+};
+
+/**
+ * Seek to a given position relative to opt_seekStart, saving the current
+ * position.
+ *
+ * Recover the current position with a call to seekPop.
+ *
+ * @param {number} pos // TODO(JSDOC).
+ * @param {number=} opt_seekStart // TODO(JSDOC).
+ */
+ByteReader.prototype.pushSeek = function(pos, opt_seekStart) {
+ var oldPos = this.pos_;
+ this.seek(pos, opt_seekStart);
+ // Alter the seekStack_ after the call to seek(), in case it throws.
+ this.seekStack_.push(oldPos);
+};
+
+/**
+ * Undo a previous seekPush.
+ */
+ByteReader.prototype.popSeek = function() {
+ this.seek(this.seekStack_.pop());
+};
+
+/**
+ * Return the current read position.
+ * @return {number} // TODO(JSDOC).
+ */
+ByteReader.prototype.tell = function() {
+ return this.pos_;
+};
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/exif_parser.js b/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/exif_parser.js
new file mode 100644
index 00000000000..f81e5cc0b9f
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/exif_parser.js
@@ -0,0 +1,439 @@
+// 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.
+
+'use strict';
+
+var EXIF_MARK_SOI = 0xffd8; // Start of image data.
+var EXIF_MARK_SOS = 0xffda; // Start of "stream" (the actual image data).
+var EXIF_MARK_SOF = 0xffc0; // Start of "frame"
+var EXIF_MARK_EXIF = 0xffe1; // Start of exif block.
+
+var EXIF_ALIGN_LITTLE = 0x4949; // Indicates little endian exif data.
+var EXIF_ALIGN_BIG = 0x4d4d; // Indicates big endian exif data.
+
+var EXIF_TAG_TIFF = 0x002a; // First directory containing TIFF data.
+var EXIF_TAG_GPSDATA = 0x8825; // Pointer from TIFF to the GPS directory.
+var EXIF_TAG_EXIFDATA = 0x8769; // Pointer from TIFF to the EXIF IFD.
+var EXIF_TAG_SUBIFD = 0x014a; // Pointer from TIFF to "Extra" IFDs.
+
+var EXIF_TAG_JPG_THUMB_OFFSET = 0x0201; // Pointer from TIFF to thumbnail.
+var EXIF_TAG_JPG_THUMB_LENGTH = 0x0202; // Length of thumbnail data.
+
+var EXIF_TAG_ORIENTATION = 0x0112;
+var EXIF_TAG_X_DIMENSION = 0xA002;
+var EXIF_TAG_Y_DIMENSION = 0xA003;
+
+function ExifParser(parent) {
+ ImageParser.call(this, parent, 'jpeg', /\.jpe?g$/i);
+}
+
+ExifParser.prototype = {__proto__: ImageParser.prototype};
+
+/**
+ * @param {File} file // TODO(JSDOC).
+ * @param {Object} metadata // TODO(JSDOC).
+ * @param {function} callback // TODO(JSDOC).
+ * @param {function} errorCallback // TODO(JSDOC).
+ */
+ExifParser.prototype.parse = function(file, metadata, callback, errorCallback) {
+ this.requestSlice(file, callback, errorCallback, metadata, 0);
+};
+
+/**
+ * @param {File} file // TODO(JSDOC).
+ * @param {function} callback // TODO(JSDOC).
+ * @param {function} errorCallback // TODO(JSDOC).
+ * @param {Object} metadata // TODO(JSDOC).
+ * @param {number} filePos // TODO(JSDOC).
+ * @param {number=} opt_length // TODO(JSDOC).
+ */
+ExifParser.prototype.requestSlice = function(
+ file, callback, errorCallback, metadata, filePos, opt_length) {
+ // Read at least 1Kb so that we do not issue too many read requests.
+ opt_length = Math.max(1024, opt_length || 0);
+
+ var self = this;
+ var reader = new FileReader();
+ reader.onerror = errorCallback;
+ reader.onload = function() { self.parseSlice(
+ file, callback, errorCallback, metadata, filePos, reader.result);
+ };
+ reader.readAsArrayBuffer(file.slice(filePos, filePos + opt_length));
+};
+
+/**
+ * @param {File} file // TODO(JSDOC).
+ * @param {function} callback // TODO(JSDOC).
+ * @param {function} errorCallback // TODO(JSDOC).
+ * @param {Object} metadata // TODO(JSDOC).
+ * @param {number} filePos // TODO(JSDOC).
+ * @param {ArrayBuffer} buf // TODO(JSDOC).
+ */
+ExifParser.prototype.parseSlice = function(
+ file, callback, errorCallback, metadata, filePos, buf) {
+ try {
+ var br = new ByteReader(buf);
+
+ if (!br.canRead(4)) {
+ // We never ask for less than 4 bytes. This can only mean we reached EOF.
+ throw new Error('Unexpected EOF @' + (filePos + buf.byteLength));
+ }
+
+ if (filePos == 0) {
+ // First slice, check for the SOI mark.
+ var firstMark = this.readMark(br);
+ if (firstMark != EXIF_MARK_SOI)
+ throw new Error('Invalid file header: ' + firstMark.toString(16));
+ }
+
+ var self = this;
+ var reread = function(opt_offset, opt_bytes) {
+ self.requestSlice(file, callback, errorCallback, metadata,
+ filePos + br.tell() + (opt_offset || 0), opt_bytes);
+ };
+
+ while (true) {
+ if (!br.canRead(4)) {
+ // Cannot read the mark and the length, request a minimum-size slice.
+ reread();
+ return;
+ }
+
+ var mark = this.readMark(br);
+ if (mark == EXIF_MARK_SOS)
+ throw new Error('SOS marker found before SOF');
+
+ var markLength = this.readMarkLength(br);
+
+ var nextSectionStart = br.tell() + markLength;
+ if (!br.canRead(markLength)) {
+ // Get the entire section.
+ if (filePos + br.tell() + markLength > file.size) {
+ throw new Error(
+ 'Invalid section length @' + (filePos + br.tell() - 2));
+ }
+ reread(-4, markLength + 4);
+ return;
+ }
+
+ if (mark == EXIF_MARK_EXIF) {
+ this.parseExifSection(metadata, buf, br);
+ } else if (ExifParser.isSOF_(mark)) {
+ // The most reliable size information is encoded in the SOF section.
+ br.seek(1, ByteReader.SEEK_CUR); // Skip the precision byte.
+ var height = br.readScalar(2);
+ var width = br.readScalar(2);
+ ExifParser.setImageSize(metadata, width, height);
+ callback(metadata); // We are done!
+ return;
+ }
+
+ br.seek(nextSectionStart, ByteReader.SEEK_BEG);
+ }
+ } catch (e) {
+ errorCallback(e.toString());
+ }
+};
+
+/**
+ * @private
+ * @param {number} mark // TODO(JSDOC).
+ * @return {boolean} // TODO(JSDOC).
+ */
+ExifParser.isSOF_ = function(mark) {
+ // There are 13 variants of SOF fragment format distinguished by the last
+ // hex digit of the mark, but the part we want is always the same.
+ if ((mark & ~0xF) != EXIF_MARK_SOF) return false;
+
+ // If the last digit is 4, 8 or 12 it is not really a SOF.
+ var type = mark & 0xF;
+ return (type != 4 && type != 8 && type != 12);
+};
+
+/**
+ * @param {Object} metadata // TODO(JSDOC).
+ * @param {ArrayBuffer} buf // TODO(JSDOC).
+ * @param {ByteReader} br // TODO(JSDOC).
+ */
+ExifParser.prototype.parseExifSection = function(metadata, buf, br) {
+ var magic = br.readString(6);
+ if (magic != 'Exif\0\0') {
+ // Some JPEG files may have sections marked with EXIF_MARK_EXIF
+ // but containing something else (e.g. XML text). Ignore such sections.
+ this.vlog('Invalid EXIF magic: ' + magic + br.readString(100));
+ return;
+ }
+
+ // Offsets inside the EXIF block are based after the magic string.
+ // Create a new ByteReader based on the current position to make offset
+ // calculations simpler.
+ br = new ByteReader(buf, br.tell());
+
+ var order = br.readScalar(2);
+ if (order == EXIF_ALIGN_LITTLE) {
+ br.setByteOrder(ByteReader.LITTLE_ENDIAN);
+ } else if (order != EXIF_ALIGN_BIG) {
+ this.log('Invalid alignment value: ' + order.toString(16));
+ return;
+ }
+
+ var tag = br.readScalar(2);
+ if (tag != EXIF_TAG_TIFF) {
+ this.log('Invalid TIFF tag: ' + tag.toString(16));
+ return;
+ }
+
+ metadata.littleEndian = (order == EXIF_ALIGN_LITTLE);
+ metadata.ifd = {
+ image: {},
+ thumbnail: {}
+ };
+ var directoryOffset = br.readScalar(4);
+
+ // Image directory.
+ this.vlog('Read image directory.');
+ br.seek(directoryOffset);
+ directoryOffset = this.readDirectory(br, metadata.ifd.image);
+ metadata.imageTransform = this.parseOrientation(metadata.ifd.image);
+
+ // Thumbnail Directory chained from the end of the image directory.
+ if (directoryOffset) {
+ this.vlog('Read thumbnail directory.');
+ br.seek(directoryOffset);
+ this.readDirectory(br, metadata.ifd.thumbnail);
+ // If no thumbnail orientation is encoded, assume same orientation as
+ // the primary image.
+ metadata.thumbnailTransform =
+ this.parseOrientation(metadata.ifd.thumbnail) ||
+ metadata.imageTransform;
+ }
+
+ // EXIF Directory may be specified as a tag in the image directory.
+ if (EXIF_TAG_EXIFDATA in metadata.ifd.image) {
+ this.vlog('Read EXIF directory.');
+ directoryOffset = metadata.ifd.image[EXIF_TAG_EXIFDATA].value;
+ br.seek(directoryOffset);
+ metadata.ifd.exif = {};
+ this.readDirectory(br, metadata.ifd.exif);
+ }
+
+ // GPS Directory may also be linked from the image directory.
+ if (EXIF_TAG_GPSDATA in metadata.ifd.image) {
+ this.vlog('Read GPS directory.');
+ directoryOffset = metadata.ifd.image[EXIF_TAG_GPSDATA].value;
+ br.seek(directoryOffset);
+ metadata.ifd.gps = {};
+ this.readDirectory(br, metadata.ifd.gps);
+ }
+
+ // Thumbnail may be linked from the image directory.
+ if (EXIF_TAG_JPG_THUMB_OFFSET in metadata.ifd.thumbnail &&
+ EXIF_TAG_JPG_THUMB_LENGTH in metadata.ifd.thumbnail) {
+ this.vlog('Read thumbnail image.');
+ br.seek(metadata.ifd.thumbnail[EXIF_TAG_JPG_THUMB_OFFSET].value);
+ metadata.thumbnailURL = br.readImage(
+ metadata.ifd.thumbnail[EXIF_TAG_JPG_THUMB_LENGTH].value);
+ } else {
+ this.vlog('Image has EXIF data, but no JPG thumbnail.');
+ }
+};
+
+/**
+ * @param {Object} metadata // TODO(JSDOC).
+ * @param {number} width // TODO(JSDOC).
+ * @param {number} height // TODO(JSDOC).
+ */
+ExifParser.setImageSize = function(metadata, width, height) {
+ if (metadata.imageTransform && metadata.imageTransform.rotate90) {
+ metadata.width = height;
+ metadata.height = width;
+ } else {
+ metadata.width = width;
+ metadata.height = height;
+ }
+};
+
+/**
+ * @param {ByteReader} br // TODO(JSDOC).
+ * @return {number} // TODO(JSDOC).
+ */
+ExifParser.prototype.readMark = function(br) {
+ return br.readScalar(2);
+};
+
+/**
+ * @param {ByteReader} br // TODO(JSDOC).
+ * @return {number} // TODO(JSDOC).
+ */
+ExifParser.prototype.readMarkLength = function(br) {
+ // Length includes the 2 bytes used to store the length.
+ return br.readScalar(2) - 2;
+};
+
+/**
+ * @param {ByteReader} br // TODO(JSDOC).
+ * @param {Array.<Object>} tags // TODO(JSDOC).
+ * @return {number} // TODO(JSDOC).
+ */
+ExifParser.prototype.readDirectory = function(br, tags) {
+ var entryCount = br.readScalar(2);
+ for (var i = 0; i < entryCount; i++) {
+ var tagId = br.readScalar(2);
+ var tag = tags[tagId] = {id: tagId};
+ tag.format = br.readScalar(2);
+ tag.componentCount = br.readScalar(4);
+ this.readTagValue(br, tag);
+ }
+
+ return br.readScalar(4);
+};
+
+/**
+ * @param {ByteReader} br // TODO(JSDOC).
+ * @param {Object} tag // TODO(JSDOC).
+ */
+ExifParser.prototype.readTagValue = function(br, tag) {
+ var self = this;
+
+ function safeRead(size, readFunction, signed) {
+ try {
+ unsafeRead(size, readFunction, signed);
+ } catch (ex) {
+ self.log('error reading tag 0x' + tag.id.toString(16) + '/' +
+ tag.format + ', size ' + tag.componentCount + '*' + size + ' ' +
+ (ex.stack || '<no stack>') + ': ' + ex);
+ tag.value = null;
+ }
+ }
+
+ function unsafeRead(size, readFunction, signed) {
+ if (!readFunction)
+ readFunction = function(size) { return br.readScalar(size, signed) };
+
+ var totalSize = tag.componentCount * size;
+ if (totalSize < 1) {
+ // This is probably invalid exif data, skip it.
+ tag.componentCount = 1;
+ tag.value = br.readScalar(4);
+ return;
+ }
+
+ if (totalSize > 4) {
+ // If the total size is > 4, the next 4 bytes will be a pointer to the
+ // actual data.
+ br.pushSeek(br.readScalar(4));
+ }
+
+ if (tag.componentCount == 1) {
+ tag.value = readFunction(size);
+ } else {
+ // Read multiple components into an array.
+ tag.value = [];
+ for (var i = 0; i < tag.componentCount; i++)
+ tag.value[i] = readFunction(size);
+ }
+
+ if (totalSize > 4) {
+ // Go back to the previous position if we had to jump to the data.
+ br.popSeek();
+ } else if (totalSize < 4) {
+ // Otherwise, if the value wasn't exactly 4 bytes, skip over the
+ // unread data.
+ br.seek(4 - totalSize, ByteReader.SEEK_CUR);
+ }
+ }
+
+ switch (tag.format) {
+ case 1: // Byte
+ case 7: // Undefined
+ safeRead(1);
+ break;
+
+ case 2: // String
+ safeRead(1);
+ if (tag.componentCount == 0) {
+ tag.value = '';
+ } else if (tag.componentCount == 1) {
+ tag.value = String.fromCharCode(tag.value);
+ } else {
+ tag.value = String.fromCharCode.apply(null, tag.value);
+ }
+ break;
+
+ case 3: // Short
+ safeRead(2);
+ break;
+
+ case 4: // Long
+ safeRead(4);
+ break;
+
+ case 9: // Signed Long
+ safeRead(4, null, true);
+ break;
+
+ case 5: // Rational
+ safeRead(8, function() {
+ return [br.readScalar(4), br.readScalar(4)];
+ });
+ break;
+
+ case 10: // Signed Rational
+ safeRead(8, function() {
+ return [br.readScalar(4, true), br.readScalar(4, true)];
+ });
+ break;
+
+ default: // ???
+ this.vlog('Unknown tag format 0x' + Number(tag.id).toString(16) +
+ ': ' + tag.format);
+ safeRead(4);
+ break;
+ }
+
+ this.vlog('Read tag: 0x' + tag.id.toString(16) + '/' + tag.format + ': ' +
+ tag.value);
+};
+
+/**
+ * TODO(JSDOC)
+ * @const
+ * @type {Array.<number>}
+ */
+ExifParser.SCALEX = [1, -1, -1, 1, 1, 1, -1, -1];
+
+/**
+ * TODO(JSDOC)
+ * @const
+ * @type {Array.<number>}
+ */
+ExifParser.SCALEY = [1, 1, -1, -1, -1, 1, 1, -1];
+
+/**
+ * TODO(JSDOC)
+ * @const
+ * @type {Array.<number>}
+ */
+ExifParser.ROTATE90 = [0, 0, 0, 0, 1, 1, 1, 1];
+
+/**
+ * Transform exif-encoded orientation into a set of parameters compatible with
+ * CSS and canvas transforms (scaleX, scaleY, rotation).
+ *
+ * @param {Object} ifd exif property dictionary (image or thumbnail).
+ * @return {Object} // TODO(JSDOC).
+ */
+ExifParser.prototype.parseOrientation = function(ifd) {
+ if (ifd[EXIF_TAG_ORIENTATION]) {
+ var index = (ifd[EXIF_TAG_ORIENTATION].value || 1) - 1;
+ return {
+ scaleX: ExifParser.SCALEX[index],
+ scaleY: ExifParser.SCALEY[index],
+ rotate90: ExifParser.ROTATE90[index]
+ };
+ }
+ return null;
+};
+
+MetadataDispatcher.registerParserClass(ExifParser);
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/function_parallel.js b/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/function_parallel.js
new file mode 100644
index 00000000000..1691a89a1fa
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/function_parallel.js
@@ -0,0 +1,82 @@
+// 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.
+
+'use strict';
+
+/**
+ * @class FunctionSequence to invoke steps in sequence
+ *
+ * @param {string} name // TODO(JSDOC).
+ * @param {Array.<function>} steps Array of functions to invoke in parallel.
+ * @param {Object} logger // TODO(JSDOC).
+ * @param {function()} callback Callback to invoke on success.
+ * @param {function(string)} failureCallback Callback to invoke on failure.
+ * @constructor
+ */
+function FunctionParallel(name, steps, logger, callback, failureCallback) {
+ // Private variables hidden in closure
+ this.currentStepIdx_ = -1;
+ this.failed_ = false;
+ this.steps_ = steps;
+ this.callback_ = callback;
+ this.failureCallback_ = failureCallback;
+ this.logger = logger;
+ this.name = name;
+
+ this.remaining = this.steps_.length;
+
+ this.nextStep = this.nextStep_.bind(this);
+ this.onError = this.onError_.bind(this);
+ this.apply = this.start.bind(this);
+}
+
+
+/**
+ * Error handling function, which fires error callback.
+ *
+ * @param {string} err Error message.
+ * @private
+ */
+FunctionParallel.prototype.onError_ = function(err) {
+ if (!this.failed_) {
+ this.failed_ = true;
+ this.failureCallback_(err);
+ }
+};
+
+/**
+ * Advances to next step. This method should not be used externally. In external
+ * cases should be used nextStep function, which is defined in closure and thus
+ * has access to internal variables of functionsequence.
+ *
+ * @private
+ */
+FunctionParallel.prototype.nextStep_ = function() {
+ if (--this.remaining == 0 && !this.failed_) {
+ this.callback_();
+ }
+};
+
+/**
+ * This function should be called only once on start, so start all the children
+ * at once
+ * @param {...} var_args // TODO(JSDOC).
+ */
+FunctionParallel.prototype.start = function(var_args) {
+ this.logger.vlog('Starting [' + this.steps_.length + '] parallel tasks ' +
+ 'with ' + arguments.length + ' argument(s)');
+ if (this.logger.verbose) {
+ for (var j = 0; j < arguments.length; j++) {
+ this.logger.vlog(arguments[j]);
+ }
+ }
+ for (var i = 0; i < this.steps_.length; i++) {
+ this.logger.vlog('Attempting to start step [' + this.steps_[i].name + ']');
+ try {
+ this.steps_[i].apply(this, arguments);
+ } catch (e) {
+ this.onError(e.toString());
+ }
+ }
+};
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/function_sequence.js b/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/function_sequence.js
new file mode 100644
index 00000000000..a2372f9f146
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/function_sequence.js
@@ -0,0 +1,133 @@
+// 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.
+
+'use strict';
+
+/**
+ * @class FunctionSequence to invoke steps in sequence
+ *
+ * @param {string} name // TODO(JSDOC).
+ * @param {Array} steps array of functions to invoke in sequence.
+ * @param {Object} logger logger.
+ * @param {function} callback callback to invoke on success.
+ * @param {function} failureCallback callback to invoke on failure.
+ * @constructor
+ */
+function FunctionSequence(name, steps, logger, callback, failureCallback) {
+ // Private variables hidden in closure
+ this.currentStepIdx_ = -1;
+ this.failed_ = false;
+ this.steps_ = steps;
+ this.callback_ = callback;
+ this.failureCallback_ = failureCallback;
+ this.logger = logger;
+ this.name = name;
+
+ this.onError = this.onError_.bind(this);
+ this.finish = this.finish_.bind(this);
+ this.nextStep = this.nextStep_.bind(this);
+ this.apply = this.apply_.bind(this);
+}
+
+/**
+ * Sets new callback
+ *
+ * @param {function} callback new callback to call on succeed.
+ */
+FunctionSequence.prototype.setCallback = function(callback) {
+ this.callback_ = callback;
+};
+
+/**
+ * Sets new error callback
+ *
+ * @param {function} failureCallback new callback to call on failure.
+ */
+FunctionSequence.prototype.setFailureCallback = function(failureCallback) {
+ this.failureCallback_ = failureCallback;
+};
+
+
+/**
+ * Error handling function, which traces current error step, stops sequence
+ * advancing and fires error callback.
+ *
+ * @param {string} err Error message.
+ * @private
+ */
+FunctionSequence.prototype.onError_ = function(err) {
+ this.logger.vlog('Failed step: ' + this.steps_[this.currentStepIdx_].name +
+ ': ' + err);
+ if (!this.failed_) {
+ this.failed_ = true;
+ this.failureCallback_(err);
+ }
+};
+
+/**
+ * Finishes sequence processing and jumps to the last step.
+ * This method should not be used externally. In external
+ * cases should be used finish function, which is defined in closure and thus
+ * has access to internal variables of functionsequence.
+ * @private
+ */
+FunctionSequence.prototype.finish_ = function() {
+ if (!this.failed_ && this.currentStepIdx_ < this.steps_.length) {
+ this.currentStepIdx_ = this.steps_.length;
+ this.callback_();
+ }
+};
+
+/**
+ * Advances to next step.
+ * This method should not be used externally. In external
+ * cases should be used nextStep function, which is defined in closure and thus
+ * has access to internal variables of functionsequence.
+ * @private
+ * @param {...} var_args // TODO(JSDOC).
+ */
+FunctionSequence.prototype.nextStep_ = function(var_args) {
+ if (this.failed_) {
+ return;
+ }
+
+ if (++this.currentStepIdx_ >= this.steps_.length) {
+ this.logger.vlog('Sequence ended');
+ this.callback_.apply(this, arguments);
+ } else {
+ this.logger.vlog('Attempting to start step [' +
+ this.steps_[this.currentStepIdx_].name +
+ ']');
+ try {
+ this.steps_[this.currentStepIdx_].apply(this, arguments);
+ } catch (e) {
+ this.onError(e.toString());
+ }
+ }
+};
+
+/**
+ * This function should be called only once on start, so start sequence pipeline
+ * @param {...} var_args // TODO(JSDOC).
+ */
+FunctionSequence.prototype.start = function(var_args) {
+ if (this.started) {
+ throw new Error('"Start" method of FunctionSequence was called twice');
+ }
+
+ this.logger.log('Starting sequence with ' + arguments.length + ' arguments');
+
+ this.started = true;
+ this.nextStep.apply(this, arguments);
+};
+
+/**
+ * Add Function object mimics to FunctionSequence
+ * @private
+ * @param {*} obj // TODO(JSDOC).
+ * @param {Array.*} args // TODO(JSDOC).
+ */
+FunctionSequence.prototype.apply_ = function(obj, args) {
+ this.start.apply(this, args);
+};
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/id3_parser.js b/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/id3_parser.js
new file mode 100644
index 00000000000..4037d02e43e
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/id3_parser.js
@@ -0,0 +1,708 @@
+// 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.
+
+'use strict';
+
+importScripts('function_sequence.js');
+importScripts('function_parallel.js');
+
+function Id3Parser(parent) {
+ MetadataParser.call(this, parent, 'id3', /\.(mp3)$/i);
+}
+
+Id3Parser.prototype = {__proto__: MetadataParser.prototype};
+
+/**
+ * Reads synchsafe integer.
+ * 'SynchSafe' term is taken from id3 documentation.
+ *
+ * @param {ByteReader} reader - reader to use.
+ * @param {number} length - bytes to read.
+ * @return {number} // TODO(JSDOC).
+ * @private
+ */
+Id3Parser.readSynchSafe_ = function(reader, length) {
+ var rv = 0;
+
+ switch (length) {
+ case 4:
+ rv = reader.readScalar(1, false) << 21;
+ case 3:
+ rv |= reader.readScalar(1, false) << 14;
+ case 2:
+ rv |= reader.readScalar(1, false) << 7;
+ case 1:
+ rv |= reader.readScalar(1, false);
+ }
+
+ return rv;
+};
+
+/**
+ * Reads 3bytes integer.
+ *
+ * @param {ByteReader} reader - reader to use.
+ * @return {number} // TODO(JSDOC).
+ * @private
+ */
+Id3Parser.readUInt24_ = function(reader) {
+ return reader.readScalar(2, false) << 16 | reader.readScalar(1, false);
+};
+
+/**
+ * Reads string from reader with specified encoding
+ *
+ * @param {ByteReader} reader reader to use.
+ * @param {number} encoding string encoding.
+ * @param {number} size maximum string size. Actual result may be shorter.
+ * @return {string} // TODO(JSDOC).
+ * @private
+ */
+Id3Parser.prototype.readString_ = function(reader, encoding, size) {
+ switch (encoding) {
+ case Id3Parser.v2.ENCODING.ISO_8859_1:
+ return reader.readNullTerminatedString(size);
+
+ case Id3Parser.v2.ENCODING.UTF_16:
+ return reader.readNullTerminatedStringUTF16(true, size);
+
+ case Id3Parser.v2.ENCODING.UTF_16BE:
+ return reader.readNullTerminatedStringUTF16(false, size);
+
+ case Id3Parser.v2.ENCODING.UTF_8:
+ // TODO: implement UTF_8.
+ this.log('UTF8 encoding not supported, used ISO_8859_1 instead');
+ return reader.readNullTerminatedString(size);
+
+ default: {
+ this.log('Unsupported encoding in ID3 tag: ' + encoding);
+ return '';
+ }
+ }
+};
+
+/**
+ * Reads text frame from reader.
+ *
+ * @param {ByteReader} reader reader to use.
+ * @param {number} majorVersion major id3 version to use.
+ * @param {Object} frame frame so store data at.
+ * @param {number} end frame end position in reader.
+ * @private
+ */
+Id3Parser.prototype.readTextFrame_ = function(reader,
+ majorVersion,
+ frame,
+ end) {
+ frame.encoding = reader.readScalar(1, false, end);
+ frame.value = this.readString_(reader, frame.encoding, end - reader.tell());
+};
+
+/**
+ * Reads user defined text frame from reader.
+ *
+ * @param {ByteReader} reader reader to use.
+ * @param {number} majorVersion major id3 version to use.
+ * @param {Object} frame frame so store data at.
+ * @param {number} end frame end position in reader.
+ * @private
+ */
+Id3Parser.prototype.readUserDefinedTextFrame_ = function(reader,
+ majorVersion,
+ frame,
+ end) {
+ frame.encoding = reader.readScalar(1, false, end);
+
+ frame.description = this.readString_(
+ reader,
+ frame.encoding,
+ end - reader.tell());
+
+ frame.value = this.readString_(
+ reader,
+ frame.encoding,
+ end - reader.tell());
+};
+
+/**
+ * @param {ByteReader} reader Reader to use.
+ * @param {number} majorVersion Major id3 version to use.
+ * @param {Object} frame Frame so store data at.
+ * @param {number} end Frame end position in reader.
+ * @private
+ */
+Id3Parser.prototype.readPIC_ = function(reader, majorVersion, frame, end) {
+ frame.encoding = reader.readScalar(1, false, end);
+ frame.format = reader.readNullTerminatedString(3, end - reader.tell());
+ frame.pictureType = reader.readScalar(1, false, end);
+ frame.description = this.readString_(reader,
+ frame.encoding,
+ end - reader.tell());
+
+
+ if (frame.format == '-->') {
+ frame.imageUrl = reader.readNullTerminatedString(end - reader.tell());
+ } else {
+ frame.imageUrl = reader.readImage(end - reader.tell());
+ }
+};
+
+/**
+ * @param {ByteReader} reader Reader to use.
+ * @param {number} majorVersion Major id3 version to use.
+ * @param {Object} frame Frame so store data at.
+ * @param {number} end Frame end position in reader.
+ * @private
+ */
+Id3Parser.prototype.readAPIC_ = function(reader, majorVersion, frame, end) {
+ this.vlog('Extracting picture');
+ frame.encoding = reader.readScalar(1, false, end);
+ frame.mime = reader.readNullTerminatedString(end - reader.tell());
+ frame.pictureType = reader.readScalar(1, false, end);
+ frame.description = this.readString_(
+ reader,
+ frame.encoding,
+ end - reader.tell());
+
+ if (frame.mime == '-->') {
+ frame.imageUrl = reader.readNullTerminatedString(end - reader.tell());
+ } else {
+ frame.imageUrl = reader.readImage(end - reader.tell());
+ }
+};
+
+/**
+ * Reads string from reader with specified encoding
+ *
+ * @param {ByteReader} reader reader to use.
+ * @param {number} majorVersion // TODO(JSDOC).
+ * @return {Object} frame read.
+ * @private
+ */
+Id3Parser.prototype.readFrame_ = function(reader, majorVersion) {
+ if (reader.eof())
+ return null;
+
+ var frame = {};
+
+ reader.pushSeek(reader.tell(), ByteReader.SEEK_BEG);
+
+ var position = reader.tell();
+
+ frame.name = (majorVersion == 2) ? reader.readNullTerminatedString(3) :
+ reader.readNullTerminatedString(4);
+
+ if (frame.name == '')
+ return null;
+
+ this.vlog('Found frame ' + (frame.name) + ' at position ' + position);
+
+ switch (majorVersion) {
+ case 2:
+ frame.size = Id3Parser.readUInt24_(reader);
+ frame.headerSize = 6;
+ break;
+ case 3:
+ frame.size = reader.readScalar(4, false);
+ frame.headerSize = 10;
+ frame.flags = reader.readScalar(2, false);
+ break;
+ case 4:
+ frame.size = Id3Parser.readSynchSafe_(reader, 4);
+ frame.headerSize = 10;
+ frame.flags = reader.readScalar(2, false);
+ break;
+ }
+
+ this.vlog('Found frame [' + frame.name + '] with size [' + frame.size + ']');
+
+ if (Id3Parser.v2.HANDLERS[frame.name]) {
+ Id3Parser.v2.HANDLERS[frame.name].call(
+ this,
+ reader,
+ majorVersion,
+ frame,
+ reader.tell() + frame.size);
+ } else if (frame.name.charAt(0) == 'T' || frame.name.charAt(0) == 'W') {
+ this.readTextFrame_(
+ reader,
+ majorVersion,
+ frame,
+ reader.tell() + frame.size);
+ }
+
+ reader.popSeek();
+
+ reader.seek(frame.size + frame.headerSize, ByteReader.SEEK_CUR);
+
+ return frame;
+};
+
+/**
+ * @param {File} file // TODO(JSDOC).
+ * @param {Object} metadata // TODO(JSDOC).
+ * @param {function(Object)} callback // TODO(JSDOC).
+ * @param {function(etring)} onError // TODO(JSDOC).
+ */
+Id3Parser.prototype.parse = function(file, metadata, callback, onError) {
+ var self = this;
+
+ this.log('Starting id3 parser for ' + file.name);
+
+ var id3v1Parser = new FunctionSequence(
+ 'id3v1parser',
+ [
+ /**
+ * Reads last 128 bytes of file in bytebuffer,
+ * which passes further.
+ * In last 128 bytes should be placed ID3v1 tag if available.
+ * @param {File} file File which bytes to read.
+ */
+ function readTail(file) {
+ util.readFileBytes(file, file.size - 128, file.size,
+ this.nextStep, this.onError, this);
+ },
+
+ /**
+ * Attempts to extract ID3v1 tag from 128 bytes long ByteBuffer
+ * @param {File} file File which tags are being extracted. Could be used
+ * for logging purposes.
+ * @param {ByteReader} reader ByteReader of 128 bytes.
+ */
+ function extractId3v1(file, reader) {
+ if (reader.readString(3) == 'TAG') {
+ this.logger.vlog('id3v1 found');
+ var id3v1 = metadata.id3v1 = {};
+
+ var title = reader.readNullTerminatedString(30).trim();
+
+ if (title.length > 0) {
+ metadata.title = title;
+ }
+
+ reader.seek(3 + 30, ByteReader.SEEK_BEG);
+
+ var artist = reader.readNullTerminatedString(30).trim();
+ if (artist.length > 0) {
+ metadata.artist = artist;
+ }
+
+ reader.seek(3 + 30 + 30, ByteReader.SEEK_BEG);
+
+ var album = reader.readNullTerminatedString(30).trim();
+ if (album.length > 0) {
+ metadata.album = album;
+ }
+ }
+ this.nextStep();
+ }
+ ],
+ this
+ );
+
+ var id3v2Parser = new FunctionSequence(
+ 'id3v2parser',
+ [
+ function readHead(file) {
+ util.readFileBytes(file, 0, 10, this.nextStep, this.onError,
+ this);
+ },
+
+ /**
+ * Check if passed array of 10 bytes contains ID3 header.
+ * @param {File} file File to check and continue reading if ID3
+ * metadata found.
+ * @param {ByteReader} reader Reader to fill with stream bytes.
+ */
+ function checkId3v2(file, reader) {
+ if (reader.readString(3) == 'ID3') {
+ this.logger.vlog('id3v2 found');
+ var id3v2 = metadata.id3v2 = {};
+ id3v2.major = reader.readScalar(1, false);
+ id3v2.minor = reader.readScalar(1, false);
+ id3v2.flags = reader.readScalar(1, false);
+ id3v2.size = Id3Parser.readSynchSafe_(reader, 4);
+
+ util.readFileBytes(file, 10, 10 + id3v2.size, this.nextStep,
+ this.onError, this);
+ } else {
+ this.finish();
+ }
+ },
+
+ /**
+ * Extracts all ID3v2 frames from given bytebuffer.
+ * @param {File} file File being parsed.
+ * @param {ByteReader} reader Reader to use for metadata extraction.
+ */
+ function extractFrames(file, reader) {
+ var id3v2 = metadata.id3v2;
+
+ if ((id3v2.major > 2) &&
+ (id3v2.flags & Id3Parser.v2.FLAG_EXTENDED_HEADER != 0)) {
+ // Skip extended header if found
+ if (id3v2.major == 3) {
+ reader.seek(reader.readScalar(4, false) - 4);
+ } else if (id3v2.major == 4) {
+ reader.seek(Id3Parser.readSynchSafe_(reader, 4) - 4);
+ }
+ }
+
+ var frame;
+
+ while (frame = self.readFrame_(reader, id3v2.major)) {
+ metadata.id3v2[frame.name] = frame;
+ }
+
+ this.nextStep();
+ },
+
+ /**
+ * Adds 'description' object to metadata.
+ * 'description' used to unify different parsers and make
+ * metadata parser-aware.
+ * Description is array if value-type pairs. Type should be used
+ * to properly format value before displaying to user.
+ */
+ function prepareDescription() {
+ var id3v2 = metadata.id3v2;
+
+ if (id3v2['APIC'])
+ metadata.thumbnailURL = id3v2['APIC'].imageUrl;
+ else if (id3v2['PIC'])
+ metadata.thumbnailURL = id3v2['PIC'].imageUrl;
+
+ metadata.description = [];
+
+ for (var key in id3v2) {
+ if (typeof(Id3Parser.v2.MAPPERS[key]) != 'undefined' &&
+ id3v2[key].value.trim().length > 0) {
+ metadata.description.push({
+ key: Id3Parser.v2.MAPPERS[key],
+ value: id3v2[key].value.trim()
+ });
+ }
+ }
+
+ function extract(propName, tags) {
+ for (var i = 1; i != arguments.length; i++) {
+ var tag = id3v2[arguments[i]];
+ if (tag && tag.value) {
+ metadata[propName] = tag.value;
+ break;
+ }
+ }
+ }
+
+ extract('album', 'TALB', 'TAL');
+ extract('title', 'TIT2', 'TT2');
+ extract('artist', 'TPE1', 'TP1');
+
+ metadata.description.sort(function(a, b) {
+ return Id3Parser.METADATA_ORDER.indexOf(a.key) -
+ Id3Parser.METADATA_ORDER.indexOf(b.key);
+ });
+ this.nextStep();
+ }
+ ],
+ this
+ );
+
+ var metadataParser = new FunctionParallel(
+ 'mp3metadataParser',
+ [id3v1Parser, id3v2Parser],
+ this,
+ function() {
+ callback.call(null, metadata);
+ },
+ onError
+ );
+
+ id3v1Parser.setCallback(metadataParser.nextStep);
+ id3v2Parser.setCallback(metadataParser.nextStep);
+
+ id3v1Parser.setFailureCallback(metadataParser.onError);
+ id3v2Parser.setFailureCallback(metadataParser.onError);
+
+ this.vlog('Passed argument : ' + file);
+
+ metadataParser.start(file);
+};
+
+
+/**
+ * Metadata order to use for metadata generation
+ */
+Id3Parser.METADATA_ORDER = [
+ 'ID3_TITLE',
+ 'ID3_LEAD_PERFORMER',
+ 'ID3_YEAR',
+ 'ID3_ALBUM',
+ 'ID3_TRACK_NUMBER',
+ 'ID3_BPM',
+ 'ID3_COMPOSER',
+ 'ID3_DATE',
+ 'ID3_PLAYLIST_DELAY',
+ 'ID3_LYRICIST',
+ 'ID3_FILE_TYPE',
+ 'ID3_TIME',
+ 'ID3_LENGTH',
+ 'ID3_FILE_OWNER',
+ 'ID3_BAND',
+ 'ID3_COPYRIGHT',
+ 'ID3_OFFICIAL_AUDIO_FILE_WEBPAGE',
+ 'ID3_OFFICIAL_ARTIST',
+ 'ID3_OFFICIAL_AUDIO_SOURCE_WEBPAGE',
+ 'ID3_PUBLISHERS_OFFICIAL_WEBPAGE'
+];
+
+
+/**
+ * id3v1 constants
+ */
+Id3Parser.v1 = {
+ /**
+ * Genres list as described in id3 documentation. We aren't going to
+ * localize this list, because at least in Russian (and I think most
+ * other languages), translation exists at least for 10% and most time
+ * translation would degrade to transliteration.
+ */
+ GENRES: [
+ 'Blues',
+ 'Classic Rock',
+ 'Country',
+ 'Dance',
+ 'Disco',
+ 'Funk',
+ 'Grunge',
+ 'Hip-Hop',
+ 'Jazz',
+ 'Metal',
+ 'New Age',
+ 'Oldies',
+ 'Other',
+ 'Pop',
+ 'R&B',
+ 'Rap',
+ 'Reggae',
+ 'Rock',
+ 'Techno',
+ 'Industrial',
+ 'Alternative',
+ 'Ska',
+ 'Death Metal',
+ 'Pranks',
+ 'Soundtrack',
+ 'Euro-Techno',
+ 'Ambient',
+ 'Trip-Hop',
+ 'Vocal',
+ 'Jazz+Funk',
+ 'Fusion',
+ 'Trance',
+ 'Classical',
+ 'Instrumental',
+ 'Acid',
+ 'House',
+ 'Game',
+ 'Sound Clip',
+ 'Gospel',
+ 'Noise',
+ 'AlternRock',
+ 'Bass',
+ 'Soul',
+ 'Punk',
+ 'Space',
+ 'Meditative',
+ 'Instrumental Pop',
+ 'Instrumental Rock',
+ 'Ethnic',
+ 'Gothic',
+ 'Darkwave',
+ 'Techno-Industrial',
+ 'Electronic',
+ 'Pop-Folk',
+ 'Eurodance',
+ 'Dream',
+ 'Southern Rock',
+ 'Comedy',
+ 'Cult',
+ 'Gangsta',
+ 'Top 40',
+ 'Christian Rap',
+ 'Pop/Funk',
+ 'Jungle',
+ 'Native American',
+ 'Cabaret',
+ 'New Wave',
+ 'Psychadelic',
+ 'Rave',
+ 'Showtunes',
+ 'Trailer',
+ 'Lo-Fi',
+ 'Tribal',
+ 'Acid Punk',
+ 'Acid Jazz',
+ 'Polka',
+ 'Retro',
+ 'Musical',
+ 'Rock & Roll',
+ 'Hard Rock',
+ 'Folk',
+ 'Folk-Rock',
+ 'National Folk',
+ 'Swing',
+ 'Fast Fusion',
+ 'Bebob',
+ 'Latin',
+ 'Revival',
+ 'Celtic',
+ 'Bluegrass',
+ 'Avantgarde',
+ 'Gothic Rock',
+ 'Progressive Rock',
+ 'Psychedelic Rock',
+ 'Symphonic Rock',
+ 'Slow Rock',
+ 'Big Band',
+ 'Chorus',
+ 'Easy Listening',
+ 'Acoustic',
+ 'Humour',
+ 'Speech',
+ 'Chanson',
+ 'Opera',
+ 'Chamber Music',
+ 'Sonata',
+ 'Symphony',
+ 'Booty Bass',
+ 'Primus',
+ 'Porn Groove',
+ 'Satire',
+ 'Slow Jam',
+ 'Club',
+ 'Tango',
+ 'Samba',
+ 'Folklore',
+ 'Ballad',
+ 'Power Ballad',
+ 'Rhythmic Soul',
+ 'Freestyle',
+ 'Duet',
+ 'Punk Rock',
+ 'Drum Solo',
+ 'A capella',
+ 'Euro-House',
+ 'Dance Hall',
+ 'Goa',
+ 'Drum & Bass',
+ 'Club-House',
+ 'Hardcore',
+ 'Terror',
+ 'Indie',
+ 'BritPop',
+ 'Negerpunk',
+ 'Polsk Punk',
+ 'Beat',
+ 'Christian Gangsta Rap',
+ 'Heavy Metal',
+ 'Black Metal',
+ 'Crossover',
+ 'Contemporary Christian',
+ 'Christian Rock',
+ 'Merengue',
+ 'Salsa',
+ 'Thrash Metal',
+ 'Anime',
+ 'Jpop',
+ 'Synthpop'
+ ]
+};
+
+/**
+ * id3v2 constants
+ */
+Id3Parser.v2 = {
+ FLAG_EXTENDED_HEADER: 1 << 5,
+
+ ENCODING: {
+ /**
+ * ISO-8859-1 [ISO-8859-1]. Terminated with $00.
+ *
+ * @const
+ * @type {number}
+ */
+ ISO_8859_1: 0,
+
+
+ /**
+ * [UTF-16] encoded Unicode [UNICODE] with BOM. All
+ * strings in the same frame SHALL have the same byteorder.
+ * Terminated with $00 00.
+ *
+ * @const
+ * @type {number}
+ */
+ UTF_16: 1,
+
+ /**
+ * UTF-16BE [UTF-16] encoded Unicode [UNICODE] without BOM.
+ * Terminated with $00 00.
+ *
+ * @const
+ * @type {number}
+ */
+ UTF_16BE: 2,
+
+ /**
+ * UTF-8 [UTF-8] encoded Unicode [UNICODE]. Terminated with $00.
+ *
+ * @const
+ * @type {number}
+ */
+ UTF_8: 3
+ },
+ HANDLERS: {
+ //User defined text information frame
+ TXX: Id3Parser.prototype.readUserDefinedTextFrame_,
+ //User defined URL link frame
+ WXX: Id3Parser.prototype.readUserDefinedTextFrame_,
+
+ //User defined text information frame
+ TXXX: Id3Parser.prototype.readUserDefinedTextFrame_,
+
+ //User defined URL link frame
+ WXXX: Id3Parser.prototype.readUserDefinedTextFrame_,
+
+ //User attached image
+ PIC: Id3Parser.prototype.readPIC_,
+
+ //User attached image
+ APIC: Id3Parser.prototype.readAPIC_
+ },
+ MAPPERS: {
+ TALB: 'ID3_ALBUM',
+ TBPM: 'ID3_BPM',
+ TCOM: 'ID3_COMPOSER',
+ TDAT: 'ID3_DATE',
+ TDLY: 'ID3_PLAYLIST_DELAY',
+ TEXT: 'ID3_LYRICIST',
+ TFLT: 'ID3_FILE_TYPE',
+ TIME: 'ID3_TIME',
+ TIT2: 'ID3_TITLE',
+ TLEN: 'ID3_LENGTH',
+ TOWN: 'ID3_FILE_OWNER',
+ TPE1: 'ID3_LEAD_PERFORMER',
+ TPE2: 'ID3_BAND',
+ TRCK: 'ID3_TRACK_NUMBER',
+ TYER: 'ID3_YEAR',
+ WCOP: 'ID3_COPYRIGHT',
+ WOAF: 'ID3_OFFICIAL_AUDIO_FILE_WEBPAGE',
+ WOAR: 'ID3_OFFICIAL_ARTIST',
+ WOAS: 'ID3_OFFICIAL_AUDIO_SOURCE_WEBPAGE',
+ WPUB: 'ID3_PUBLISHERS_OFFICIAL_WEBPAGE'
+ }
+};
+
+MetadataDispatcher.registerParserClass(Id3Parser);
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/image_parsers.js b/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/image_parsers.js
new file mode 100644
index 00000000000..52c4fc4e8a8
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/image_parsers.js
@@ -0,0 +1,198 @@
+// 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.
+
+'use strict';
+
+/* Base class for image metadata parsers that only need to look at a short
+ fragment at the start of the file */
+function SimpleImageParser(parent, type, urlFilter, headerSize) {
+ ImageParser.call(this, parent, type, urlFilter);
+ this.headerSize = headerSize;
+}
+
+SimpleImageParser.prototype = {__proto__: ImageParser.prototype};
+
+/**
+ * @param {File} file // TODO(JSDOC).
+ * @param {Object} metadata // TODO(JSDOC).
+ * @param {function(Object)} callback // TODO(JSDOC).
+ * @param {function(string)} errorCallback // TODO(JSDOC).
+ */
+SimpleImageParser.prototype.parse = function(
+ file, metadata, callback, errorCallback) {
+ var self = this;
+ util.readFileBytes(file, 0, this.headerSize,
+ function(file, br) {
+ try {
+ self.parseHeader(metadata, br);
+ callback(metadata);
+ } catch (e) {
+ errorCallback(e.toString());
+ }
+ },
+ errorCallback);
+};
+
+
+function PngParser(parent) {
+ SimpleImageParser.call(this, parent, 'png', /\.png$/i, 24);
+}
+
+PngParser.prototype = {__proto__: SimpleImageParser.prototype};
+
+/**
+ * @param {Object} metadata // TODO(JSDOC).
+ * @param {ByteReader} br // TODO(JSDOC).
+ */
+PngParser.prototype.parseHeader = function(metadata, br) {
+ br.setByteOrder(ByteReader.BIG_ENDIAN);
+
+ var signature = br.readString(8);
+ if (signature != '\x89PNG\x0D\x0A\x1A\x0A')
+ throw new Error('Invalid PNG signature: ' + signature);
+
+ br.seek(12);
+ var ihdr = br.readString(4);
+ if (ihdr != 'IHDR')
+ throw new Error('Missing IHDR chunk');
+
+ metadata.width = br.readScalar(4);
+ metadata.height = br.readScalar(4);
+};
+
+MetadataDispatcher.registerParserClass(PngParser);
+
+
+function BmpParser(parent) {
+ SimpleImageParser.call(this, parent, 'bmp', /\.bmp$/i, 28);
+}
+
+BmpParser.prototype = {__proto__: SimpleImageParser.prototype};
+
+/**
+ * @param {Object} metadata // TODO(JSDOC).
+ * @param {ByteReader} br // TODO(JSDOC).
+ */
+BmpParser.prototype.parseHeader = function(metadata, br) {
+ br.setByteOrder(ByteReader.LITTLE_ENDIAN);
+
+ var signature = br.readString(2);
+ if (signature != 'BM')
+ throw new Error('Invalid BMP signature: ' + signature);
+
+ br.seek(18);
+ metadata.width = br.readScalar(4);
+ metadata.height = br.readScalar(4);
+};
+
+MetadataDispatcher.registerParserClass(BmpParser);
+
+
+function GifParser(parent) {
+ SimpleImageParser.call(this, parent, 'gif', /\.Gif$/i, 10);
+}
+
+GifParser.prototype = {__proto__: SimpleImageParser.prototype};
+
+/**
+ * @param {Object} metadata // TODO(JSDOC).
+ * @param {ByteReader} br // TODO(JSDOC).
+ */
+GifParser.prototype.parseHeader = function(metadata, br) {
+ br.setByteOrder(ByteReader.LITTLE_ENDIAN);
+
+ var signature = br.readString(6);
+ if (!signature.match(/GIF8(7|9)a/))
+ throw new Error('Invalid GIF signature: ' + signature);
+
+ metadata.width = br.readScalar(2);
+ metadata.height = br.readScalar(2);
+};
+
+MetadataDispatcher.registerParserClass(GifParser);
+
+
+function WebpParser(parent) {
+ SimpleImageParser.call(this, parent, 'webp', /\.webp$/i, 30);
+}
+
+WebpParser.prototype = {__proto__: SimpleImageParser.prototype};
+
+/**
+ * @param {Object} metadata // TODO(JSDOC).
+ * @param {ByteReader} br // TODO(JSDOC).
+ */
+WebpParser.prototype.parseHeader = function(metadata, br) {
+ br.setByteOrder(ByteReader.LITTLE_ENDIAN);
+
+ var riffSignature = br.readString(4);
+ if (riffSignature != 'RIFF')
+ throw new Error('Invalid RIFF signature: ' + riffSignature);
+
+ br.seek(8);
+ var webpSignature = br.readString(4);
+ if (webpSignature != 'WEBP')
+ throw new Error('Invalid WEBP signature: ' + webpSignature);
+
+ var chunkFormat = br.readString(4);
+ if (chunkFormat != 'VP8 ' && chunkFormat != 'VP8L')
+ throw new Error('Invalid chunk format: ' + chunkFormat);
+
+ if (chunkFormat == 'VP8 ') {
+ // VP8 lossy bitstream format.
+ br.seek(23);
+ var lossySignature = br.readScalar(2) | (br.readScalar(1) << 16);
+ if (lossySignature != 0x2a019d)
+ throw new Error('Invalid VP8 lossy bitstream signature: ' +
+ lossySignature);
+
+ var dimensionBits = br.readScalar(4);
+ metadata.width = dimensionBits & 0x3fff;
+ metadata.height = (dimensionBits >> 16) & 0x3fff;
+ } else {
+ // VP8 lossless bitstream format.
+ br.seek(20);
+ var losslessSignature = br.readScalar(1);
+ if (losslessSignature != 0x2f)
+ throw new Error('Invalid VP8 lossless bitstream signature: ' +
+ losslessSignature);
+
+ var dimensionBits = br.readScalar(4);
+ metadata.width = (dimensionBits & 0x3fff) + 1;
+ metadata.height = ((dimensionBits >> 14) & 0x3fff) + 1;
+ }
+};
+
+MetadataDispatcher.registerParserClass(WebpParser);
+
+/**
+ * Parser for the header of .ico icon files.
+ * @param {MetadataDispatcher} parent Parent metadata dispatcher object.
+ * @constructor
+ * @extends SimpleImageParser
+ */
+function IcoParser(parent) {
+ SimpleImageParser.call(this, parent, 'ico', /\.ico$/i, 8);
+}
+
+IcoParser.prototype = {__proto__: SimpleImageParser.prototype};
+
+/**
+ * Parse the binary data as a ico header and stores to metadata.
+ * @param {Object} metadata Dictionary to store the parser metadata.
+ * @param {ByteReader} byteReader Reader for header binary data.
+ */
+IcoParser.prototype.parseHeader = function(metadata, byteReader) {
+ byteReader.setByteOrder(ByteReader.LITTLE_ENDIAN);
+
+ var signature = byteReader.readString(4);
+ if (signature !== '\x00\x00\x00\x01')
+ throw new Error('Invalid ICO signature: ' + signature);
+
+ byteReader.seek(2);
+ metadata.width = byteReader.readScalar(1);
+ metadata.height = byteReader.readScalar(1);
+};
+
+MetadataDispatcher.registerParserClass(IcoParser);
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/metadata_cache.js b/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/metadata_cache.js
new file mode 100644
index 00000000000..4bbbe182958
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/metadata_cache.js
@@ -0,0 +1,1042 @@
+// 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.
+
+'use strict';
+
+/**
+ * MetadataCache is a map from Entry to an object containing properties.
+ * Properties are divided by types, and all properties of one type are accessed
+ * at once.
+ * Some of the properties:
+ * {
+ * filesystem: size, modificationTime
+ * internal: presence
+ * drive: pinned, present, hosted, availableOffline
+ * streaming: (no property)
+ *
+ * Following are not fetched for non-present drive files.
+ * media: artist, album, title, width, height, imageTransform, etc.
+ * thumbnail: url, transform
+ *
+ * Following are always fetched from content, and so force the downloading
+ * of remote drive files. One should use this for required content metadata,
+ * i.e. image orientation.
+ * fetchedMedia: width, height, etc.
+ * }
+ *
+ * Typical usages:
+ * {
+ * cache.get([entry1, entry2], 'drive|filesystem', function(metadata) {
+ * if (metadata[0].drive.pinned && metadata[1].filesystem.size == 0)
+ * alert("Pinned and empty!");
+ * });
+ *
+ * cache.set(entry, 'internal', {presence: 'deleted'});
+ *
+ * cache.clear([fileEntry1, fileEntry2], 'filesystem');
+ *
+ * // Getting fresh value.
+ * cache.clear(entry, 'thumbnail');
+ * cache.get(entry, 'thumbnail', function(thumbnail) {
+ * img.src = thumbnail.url;
+ * });
+ *
+ * var cached = cache.getCached(entry, 'filesystem');
+ * var size = (cached && cached.size) || UNKNOWN_SIZE;
+ * }
+ *
+ * @constructor
+ */
+function MetadataCache() {
+ /**
+ * Map from Entry (using Entry.toURL) to metadata. Metadata contains
+ * |properties| - an hierarchical object of values, and an object for each
+ * metadata provider: <prodiver-id>: {time, callbacks}
+ * @private
+ */
+ this.cache_ = {};
+
+ /**
+ * List of metadata providers.
+ * @private
+ */
+ this.providers_ = [];
+
+ /**
+ * List of observers added. Each one is an object with fields:
+ * re - regexp of urls;
+ * type - metadata type;
+ * callback - the callback.
+ * @private
+ */
+ this.observers_ = [];
+ this.observerId_ = 0;
+
+ this.batchCount_ = 0;
+ this.totalCount_ = 0;
+
+ this.currentCacheSize_ = 0;
+
+ /**
+ * Time of first get query of the current batch. Items updated later than this
+ * will not be evicted.
+ * @private
+ */
+ this.lastBatchStart_ = new Date();
+}
+
+/**
+ * Observer type: it will be notified if the changed Entry is exactly the same
+ * as the observed Entry.
+ */
+MetadataCache.EXACT = 0;
+
+/**
+ * Observer type: it will be notified if the changed Entry is an immediate child
+ * of the observed Entry.
+ */
+MetadataCache.CHILDREN = 1;
+
+/**
+ * Observer type: it will be notified if the changed Entry is a descendant of
+ * of the observer Entry.
+ */
+MetadataCache.DESCENDANTS = 2;
+
+/**
+ * Margin of the cache size. This amount of caches may be kept in addition.
+ */
+MetadataCache.EVICTION_THRESHOLD_MARGIN = 500;
+
+/**
+ * @return {MetadataCache!} The cache with all providers.
+ */
+MetadataCache.createFull = function() {
+ var cache = new MetadataCache();
+ cache.providers_.push(new FilesystemProvider());
+ cache.providers_.push(new DriveProvider());
+ cache.providers_.push(new ContentProvider());
+ return cache;
+};
+
+/**
+ * Clones metadata entry. Metadata entries may contain scalars, arrays,
+ * hash arrays and Date object. Other objects are not supported.
+ * @param {Object} metadata Metadata object.
+ * @return {Object} Cloned entry.
+ */
+MetadataCache.cloneMetadata = function(metadata) {
+ if (metadata instanceof Array) {
+ var result = [];
+ for (var index = 0; index < metadata.length; index++) {
+ result[index] = MetadataCache.cloneMetadata(metadata[index]);
+ }
+ return result;
+ } else if (metadata instanceof Date) {
+ var result = new Date();
+ result.setTime(metadata.getTime());
+ return result;
+ } else if (metadata instanceof Object) { // Hash array only.
+ var result = {};
+ for (var property in metadata) {
+ if (metadata.hasOwnProperty(property))
+ result[property] = MetadataCache.cloneMetadata(metadata[property]);
+ }
+ return result;
+ } else {
+ return metadata;
+ }
+};
+
+/**
+ * @return {boolean} Whether all providers are ready.
+ */
+MetadataCache.prototype.isInitialized = function() {
+ for (var index = 0; index < this.providers_.length; index++) {
+ if (!this.providers_[index].isInitialized()) return false;
+ }
+ return true;
+};
+
+/**
+ * Sets the size of cache. The actual cache size may be larger than the given
+ * value.
+ * @param {number} size The cache size to be set.
+ */
+MetadataCache.prototype.setCacheSize = function(size) {
+ this.currentCacheSize_ = size;
+
+ if (this.totalCount_ > this.currentEvictionThreshold_())
+ this.evict_();
+};
+
+/**
+ * Returns the current threshold to evict caches. When the number of caches
+ * exceeds this, the cache should be evicted.
+ * @return {number} Threshold to evict caches.
+ * @private
+ */
+MetadataCache.prototype.currentEvictionThreshold_ = function() {
+ return this.currentCacheSize_ * 2 + MetadataCache.EVICTION_THRESHOLD_MARGIN;
+};
+
+/**
+ * Fetches the metadata, puts it in the cache, and passes to callback.
+ * If required metadata is already in the cache, does not fetch it again.
+ * @param {Entry|Array.<Entry>} entries The list of entries. May be just a
+ * single item.
+ * @param {string} type The metadata type.
+ * @param {function(Object)} callback The metadata is passed to callback.
+ */
+MetadataCache.prototype.get = function(entries, type, callback) {
+ if (!(entries instanceof Array)) {
+ this.getOne(entries, type, callback);
+ return;
+ }
+
+ if (entries.length == 0) {
+ if (callback) callback([]);
+ return;
+ }
+
+ var result = [];
+ var remaining = entries.length;
+ this.startBatchUpdates();
+
+ var onOneItem = function(index, value) {
+ result[index] = value;
+ remaining--;
+ if (remaining == 0) {
+ this.endBatchUpdates();
+ if (callback) setTimeout(callback, 0, result);
+ }
+ };
+
+ for (var index = 0; index < entries.length; index++) {
+ result.push(null);
+ this.getOne(entries[index], type, onOneItem.bind(this, index));
+ }
+};
+
+/**
+ * Fetches the metadata for one Entry. See comments to |get|.
+ * @param {Entry} entry The entry.
+ * @param {string} type Metadata type.
+ * @param {function(Object)} callback The callback.
+ */
+MetadataCache.prototype.getOne = function(entry, type, callback) {
+ if (type.indexOf('|') != -1) {
+ var types = type.split('|');
+ var result = {};
+ var typesLeft = types.length;
+
+ var onOneType = function(requestedType, metadata) {
+ result[requestedType] = metadata;
+ typesLeft--;
+ if (typesLeft == 0) callback(result);
+ };
+
+ for (var index = 0; index < types.length; index++) {
+ this.getOne(entry, types[index], onOneType.bind(null, types[index]));
+ }
+ return;
+ }
+
+ callback = callback || function() {};
+
+ var entryURL = entry.toURL();
+ if (!(entryURL in this.cache_)) {
+ this.cache_[entryURL] = this.createEmptyItem_();
+ this.totalCount_++;
+ }
+
+ var item = this.cache_[entryURL];
+
+ if (type in item.properties) {
+ callback(item.properties[type]);
+ return;
+ }
+
+ this.startBatchUpdates();
+ var providers = this.providers_.slice();
+ var currentProvider;
+ var self = this;
+
+ var onFetched = function() {
+ if (type in item.properties) {
+ self.endBatchUpdates();
+ // Got properties from provider.
+ callback(item.properties[type]);
+ } else {
+ tryNextProvider();
+ }
+ };
+
+ var onProviderProperties = function(properties) {
+ var id = currentProvider.getId();
+ var fetchedCallbacks = item[id].callbacks;
+ delete item[id].callbacks;
+ item.time = new Date();
+ self.mergeProperties_(entry, properties);
+
+ for (var index = 0; index < fetchedCallbacks.length; index++) {
+ fetchedCallbacks[index]();
+ }
+ };
+
+ var queryProvider = function() {
+ var id = currentProvider.getId();
+ if ('callbacks' in item[id]) {
+ // We are querying this provider now.
+ item[id].callbacks.push(onFetched);
+ } else {
+ item[id].callbacks = [onFetched];
+ currentProvider.fetch(entry, type, onProviderProperties);
+ }
+ };
+
+ var tryNextProvider = function() {
+ if (providers.length == 0) {
+ self.endBatchUpdates();
+ callback(item.properties[type] || null);
+ return;
+ }
+
+ currentProvider = providers.shift();
+ if (currentProvider.supportsEntry(entry) &&
+ currentProvider.providesType(type)) {
+ queryProvider();
+ } else {
+ tryNextProvider();
+ }
+ };
+
+ tryNextProvider();
+};
+
+/**
+ * Returns the cached metadata value, or |null| if not present.
+ * @param {Entry|Array.<Entry>} entries The list of entries. May be just a
+ * single entry.
+ * @param {string} type The metadata type.
+ * @return {Object} The metadata or null.
+ */
+MetadataCache.prototype.getCached = function(entries, type) {
+ var single = false;
+ if (!(entries instanceof Array)) {
+ single = true;
+ entries = [entries];
+ }
+
+ var result = [];
+ for (var index = 0; index < entries.length; index++) {
+ var entryURL = entries[index].toURL();
+ result.push(entryURL in this.cache_ ?
+ (this.cache_[entryURL].properties[type] || null) : null);
+ }
+
+ return single ? result[0] : result;
+};
+
+/**
+ * Puts the metadata into cache
+ * @param {Entry|Array.<Entry>} entries The list of entries. May be just a
+ * single entry.
+ * @param {string} type The metadata type.
+ * @param {Array.<Object>} values List of corresponding metadata values.
+ */
+MetadataCache.prototype.set = function(entries, type, values) {
+ if (!(entries instanceof Array)) {
+ entries = [entries];
+ values = [values];
+ }
+
+ this.startBatchUpdates();
+ for (var index = 0; index < entries.length; index++) {
+ var entryURL = entries[index].toURL();
+ if (!(entryURL in this.cache_)) {
+ this.cache_[entryURL] = this.createEmptyItem_();
+ this.totalCount_++;
+ }
+ this.cache_[entryURL].properties[type] = values[index];
+ this.notifyObservers_(entries[index], type);
+ }
+ this.endBatchUpdates();
+};
+
+/**
+ * Clears the cached metadata values.
+ * @param {Entry|Array.<Entry>} entries The list of entries. May be just a
+ * single entry.
+ * @param {string} type The metadata types or * for any type.
+ */
+MetadataCache.prototype.clear = function(entries, type) {
+ if (!(entries instanceof Array))
+ entries = [entries];
+
+ var types = type.split('|');
+
+ for (var index = 0; index < entries.length; index++) {
+ var entry = entries[index];
+ var entryURL = entry.toURL();
+ if (entryURL in this.cache_) {
+ if (type === '*') {
+ this.cache_[entryURL].properties = {};
+ } else {
+ for (var j = 0; j < types.length; j++) {
+ var type = types[j];
+ delete this.cache_[entryURL].properties[type];
+ }
+ }
+ }
+ }
+};
+
+/**
+ * Clears the cached metadata values recursively.
+ * @param {Entry} entry An entry to be cleared recursively from cache.
+ * @param {string} type The metadata types or * for any type.
+ */
+MetadataCache.prototype.clearRecursively = function(entry, type) {
+ var types = type.split('|');
+ var keys = Object.keys(this.cache_);
+ var entryURL = entry.toURL();
+
+ for (var index = 0; index < keys.length; index++) {
+ var cachedEntryURL = keys[index];
+ if (cachedEntryURL.substring(0, entryURL.length) === entryURL) {
+ if (type === '*') {
+ this.cache_[cachedEntryURL].properties = {};
+ } else {
+ for (var j = 0; j < types.length; j++) {
+ var type = types[j];
+ delete this.cache_[cachedEntryURL].properties[type];
+ }
+ }
+ }
+ }
+};
+
+/**
+ * Adds an observer, which will be notified when metadata changes.
+ * @param {Entry} entry The root entry to look at.
+ * @param {number} relation This defines, which items will trigger the observer.
+ * See comments to |MetadataCache.EXACT| and others.
+ * @param {string} type The metadata type.
+ * @param {function(Array.<Entry>, Array.<Object>)} observer List of entries
+ * and corresponding metadata values are passed to this callback.
+ * @return {number} The observer id, which can be used to remove it.
+ */
+MetadataCache.prototype.addObserver = function(
+ entry, relation, type, observer) {
+ var entryURL = entry.toURL();
+ var re;
+ if (relation == MetadataCache.CHILDREN)
+ re = entryURL + '(/[^/]*)?';
+ else if (relation == MetadataCache.DESCENDANTS)
+ re = entryURL + '(/.*)?';
+ else
+ re = entryURL;
+
+ var id = ++this.observerId_;
+ this.observers_.push({
+ re: new RegExp('^' + re + '$'),
+ type: type,
+ callback: observer,
+ id: id,
+ pending: {}
+ });
+
+ return id;
+};
+
+/**
+ * Removes the observer.
+ * @param {number} id Observer id.
+ * @return {boolean} Whether observer was removed or not.
+ */
+MetadataCache.prototype.removeObserver = function(id) {
+ for (var index = 0; index < this.observers_.length; index++) {
+ if (this.observers_[index].id == id) {
+ this.observers_.splice(index, 1);
+ return true;
+ }
+ }
+ return false;
+};
+
+/**
+ * Start batch updates.
+ */
+MetadataCache.prototype.startBatchUpdates = function() {
+ this.batchCount_++;
+ if (this.batchCount_ == 1)
+ this.lastBatchStart_ = new Date();
+};
+
+/**
+ * End batch updates. Notifies observers if all nested updates are finished.
+ */
+MetadataCache.prototype.endBatchUpdates = function() {
+ this.batchCount_--;
+ if (this.batchCount_ != 0) return;
+ if (this.totalCount_ > this.currentEvictionThreshold_())
+ this.evict_();
+ for (var index = 0; index < this.observers_.length; index++) {
+ var observer = this.observers_[index];
+ var entries = [];
+ var properties = [];
+ for (var entryURL in observer.pending) {
+ if (observer.pending.hasOwnProperty(entryURL) &&
+ entryURL in this.cache_) {
+ var entry = observer.pending[entryURL];
+ entries.push(entry);
+ properties.push(
+ this.cache_[entryURL].properties[observer.type] || null);
+ }
+ }
+ observer.pending = {};
+ if (entries.length > 0) {
+ observer.callback(entries, properties);
+ }
+ }
+};
+
+/**
+ * Notifies observers or puts the data to pending list.
+ * @param {Entry} entry Changed entry.
+ * @param {string} type Metadata type.
+ * @private
+ */
+MetadataCache.prototype.notifyObservers_ = function(entry, type) {
+ var entryURL = entry.toURL();
+ for (var index = 0; index < this.observers_.length; index++) {
+ var observer = this.observers_[index];
+ if (observer.type == type && observer.re.test(entryURL)) {
+ if (this.batchCount_ == 0) {
+ // Observer expects array of urls and array of properties.
+ observer.callback(
+ [entry], [this.cache_[entryURL].properties[type] || null]);
+ } else {
+ observer.pending[entryURL] = entry;
+ }
+ }
+ }
+};
+
+/**
+ * Removes the oldest items from the cache.
+ * This method never removes the items from last batch.
+ * @private
+ */
+MetadataCache.prototype.evict_ = function() {
+ var toRemove = [];
+
+ // We leave only a half of items, so we will not call evict_ soon again.
+ var desiredCount = this.currentEvictionThreshold_();
+ var removeCount = this.totalCount_ - desiredCount;
+ for (var url in this.cache_) {
+ if (this.cache_.hasOwnProperty(url) &&
+ this.cache_[url].time < this.lastBatchStart_) {
+ toRemove.push(url);
+ }
+ }
+
+ toRemove.sort(function(a, b) {
+ var aTime = this.cache_[a].time;
+ var bTime = this.cache_[b].time;
+ return aTime < bTime ? -1 : aTime > bTime ? 1 : 0;
+ }.bind(this));
+
+ removeCount = Math.min(removeCount, toRemove.length);
+ this.totalCount_ -= removeCount;
+ for (var index = 0; index < removeCount; index++) {
+ delete this.cache_[toRemove[index]];
+ }
+};
+
+/**
+ * @return {Object} Empty cache item.
+ * @private
+ */
+MetadataCache.prototype.createEmptyItem_ = function() {
+ var item = {properties: {}};
+ for (var index = 0; index < this.providers_.length; index++) {
+ item[this.providers_[index].getId()] = {};
+ }
+ return item;
+};
+
+/**
+ * Caches all the properties from data to cache entry for the entry.
+ * @param {Entry} entry The file entry.
+ * @param {Object} data The properties.
+ * @private
+ */
+MetadataCache.prototype.mergeProperties_ = function(entry, data) {
+ if (data == null) return;
+ var properties = this.cache_[entry.toURL()].properties;
+ for (var type in data) {
+ if (data.hasOwnProperty(type) && !properties.hasOwnProperty(type)) {
+ properties[type] = data[type];
+ this.notifyObservers_(entry, type);
+ }
+ }
+};
+
+/**
+ * Base class for metadata providers.
+ * @constructor
+ */
+function MetadataProvider() {
+}
+
+/**
+ * @param {Entry} entry The entry.
+ * @return {boolean} Whether this provider supports the entry.
+ */
+MetadataProvider.prototype.supportsEntry = function(entry) { return false; };
+
+/**
+ * @param {string} type The metadata type.
+ * @return {boolean} Whether this provider provides this metadata.
+ */
+MetadataProvider.prototype.providesType = function(type) { return false; };
+
+/**
+ * @return {string} Unique provider id.
+ */
+MetadataProvider.prototype.getId = function() { return ''; };
+
+/**
+ * @return {boolean} Whether provider is ready.
+ */
+MetadataProvider.prototype.isInitialized = function() { return true; };
+
+/**
+ * Fetches the metadata. It's suggested to return all the metadata this provider
+ * can fetch at once.
+ * @param {Entry} entry File entry.
+ * @param {string} type Requested metadata type.
+ * @param {function(Object)} callback Callback expects a map from metadata type
+ * to metadata value.
+ */
+MetadataProvider.prototype.fetch = function(entry, type, callback) {
+ throw new Error('Default metadata provider cannot fetch.');
+};
+
+
+/**
+ * Provider of filesystem metadata.
+ * This provider returns the following objects:
+ * filesystem: { size, modificationTime }
+ * @constructor
+ */
+function FilesystemProvider() {
+ MetadataProvider.call(this);
+}
+
+FilesystemProvider.prototype = {
+ __proto__: MetadataProvider.prototype
+};
+
+/**
+ * @param {Entry} entry The entry.
+ * @return {boolean} Whether this provider supports the entry.
+ */
+FilesystemProvider.prototype.supportsEntry = function(entry) {
+ return true;
+};
+
+/**
+ * @param {string} type The metadata type.
+ * @return {boolean} Whether this provider provides this metadata.
+ */
+FilesystemProvider.prototype.providesType = function(type) {
+ return type == 'filesystem';
+};
+
+/**
+ * @return {string} Unique provider id.
+ */
+FilesystemProvider.prototype.getId = function() { return 'filesystem'; };
+
+/**
+ * Fetches the metadata.
+ * @param {Entry} entry File entry.
+ * @param {string} type Requested metadata type.
+ * @param {function(Object)} callback Callback expects a map from metadata type
+ * to metadata value.
+ */
+FilesystemProvider.prototype.fetch = function(
+ entry, type, callback) {
+ function onError(error) {
+ callback(null);
+ }
+
+ function onMetadata(entry, metadata) {
+ callback({
+ filesystem: {
+ size: entry.isFile ? (metadata.size || 0) : -1,
+ modificationTime: metadata.modificationTime
+ }
+ });
+ }
+
+ entry.getMetadata(onMetadata.bind(null, entry), onError);
+};
+
+/**
+ * Provider of drive metadata.
+ * This provider returns the following objects:
+ * drive: { pinned, hosted, present, customIconUrl, etc. }
+ * thumbnail: { url, transform }
+ * streaming: { }
+ * @constructor
+ */
+function DriveProvider() {
+ MetadataProvider.call(this);
+
+ // We batch metadata fetches into single API call.
+ this.entries_ = [];
+ this.callbacks_ = [];
+ this.scheduled_ = false;
+
+ this.callApiBound_ = this.callApi_.bind(this);
+}
+
+DriveProvider.prototype = {
+ __proto__: MetadataProvider.prototype
+};
+
+/**
+ * @param {Entry} entry The entry.
+ * @return {boolean} Whether this provider supports the entry.
+ */
+DriveProvider.prototype.supportsEntry = function(entry) {
+ return FileType.isOnDrive(entry);
+};
+
+/**
+ * @param {string} type The metadata type.
+ * @return {boolean} Whether this provider provides this metadata.
+ */
+DriveProvider.prototype.providesType = function(type) {
+ return type == 'drive' || type == 'thumbnail' ||
+ type == 'streaming' || type == 'media';
+};
+
+/**
+ * @return {string} Unique provider id.
+ */
+DriveProvider.prototype.getId = function() { return 'drive'; };
+
+/**
+ * Fetches the metadata.
+ * @param {Entry} entry File entry.
+ * @param {string} type Requested metadata type.
+ * @param {function(Object)} callback Callback expects a map from metadata type
+ * to metadata value.
+ */
+DriveProvider.prototype.fetch = function(entry, type, callback) {
+ this.entries_.push(entry);
+ this.callbacks_.push(callback);
+ if (!this.scheduled_) {
+ this.scheduled_ = true;
+ setTimeout(this.callApiBound_, 0);
+ }
+};
+
+/**
+ * Schedules the API call.
+ * @private
+ */
+DriveProvider.prototype.callApi_ = function() {
+ this.scheduled_ = false;
+
+ var entries = this.entries_;
+ var callbacks = this.callbacks_;
+ this.entries_ = [];
+ this.callbacks_ = [];
+ var self = this;
+
+ var task = function(entry, callback) {
+ // TODO(mtomasz): Make getDriveEntryProperties accept Entry instead of URL.
+ var entryURL = entry.toURL();
+ chrome.fileBrowserPrivate.getDriveEntryProperties(entryURL,
+ function(properties) {
+ callback(self.convert_(properties, entry));
+ });
+ };
+
+ for (var i = 0; i < entries.length; i++)
+ task(entries[i], callbacks[i]);
+};
+
+/**
+ * @param {DriveEntryProperties} data Drive entry properties.
+ * @param {Entry} entry File entry.
+ * @return {boolean} True if the file is available offline.
+ */
+DriveProvider.isAvailableOffline = function(data, entry) {
+ if (data.isPresent)
+ return true;
+
+ if (!data.isHosted)
+ return false;
+
+ // What's available offline? See the 'Web' column at:
+ // http://support.google.com/drive/bin/answer.py?hl=en&answer=1628467
+ var subtype = FileType.getType(entry).subtype;
+ return (subtype == 'doc' ||
+ subtype == 'draw' ||
+ subtype == 'sheet' ||
+ subtype == 'slides');
+};
+
+/**
+ * @param {DriveEntryProperties} data Drive entry properties.
+ * @return {boolean} True if opening the file does not require downloading it
+ * via a metered connection.
+ */
+DriveProvider.isAvailableWhenMetered = function(data) {
+ return data.isPresent || data.isHosted;
+};
+
+/**
+ * Converts API metadata to internal format.
+ * @param {Object} data Metadata from API call.
+ * @param {Entry} entry File entry.
+ * @return {Object} Metadata in internal format.
+ * @private
+ */
+DriveProvider.prototype.convert_ = function(data, entry) {
+ var result = {};
+ result.drive = {
+ present: data.isPresent,
+ pinned: data.isPinned,
+ hosted: data.isHosted,
+ imageWidth: data.imageWidth,
+ imageHeight: data.imageHeight,
+ imageRotation: data.imageRotation,
+ availableOffline: DriveProvider.isAvailableOffline(data, entry),
+ availableWhenMetered: DriveProvider.isAvailableWhenMetered(data),
+ customIconUrl: data.customIconUrl || '',
+ contentMimeType: data.contentMimeType || '',
+ sharedWithMe: data.sharedWithMe,
+ shared: data.shared
+ };
+
+ if (!data.isPresent) {
+ // Block the local fetch for drive files, which require downloading.
+ result.thumbnail = {url: '', transform: null};
+ result.media = {};
+ }
+
+ if ('thumbnailUrl' in data) {
+ result.thumbnail = {
+ url: data.thumbnailUrl,
+ transform: null
+ };
+ }
+ if (!data.isPresent) {
+ // Indicate that the data is not available in local cache.
+ // It used to have a field 'url' for streaming play, but it is
+ // derprecated. See crbug.com/174560.
+ result.streaming = {};
+ }
+ return result;
+};
+
+
+/**
+ * Provider of content metadata.
+ * This provider returns the following objects:
+ * thumbnail: { url, transform }
+ * media: { artist, album, title, width, height, imageTransform, etc. }
+ * fetchedMedia: { same fields here }
+ * @constructor
+ */
+function ContentProvider() {
+ MetadataProvider.call(this);
+
+ // Pass all URLs to the metadata reader until we have a correct filter.
+ this.urlFilter_ = /.*/;
+
+ var path = document.location.pathname;
+ var workerPath = document.location.origin +
+ path.substring(0, path.lastIndexOf('/') + 1) +
+ 'foreground/js/metadata/metadata_dispatcher.js';
+
+ this.dispatcher_ = new SharedWorker(workerPath).port;
+ this.dispatcher_.start();
+
+ this.dispatcher_.onmessage = this.onMessage_.bind(this);
+ this.dispatcher_.postMessage({verb: 'init'});
+
+ // Initialization is not complete until the Worker sends back the
+ // 'initialized' message. See below.
+ this.initialized_ = false;
+
+ // Map from Entry.toURL() to callback.
+ // Note that simultaneous requests for same url are handled in MetadataCache.
+ this.callbacks_ = {};
+}
+
+ContentProvider.prototype = {
+ __proto__: MetadataProvider.prototype
+};
+
+/**
+ * @param {Entry} entry The entry.
+ * @return {boolean} Whether this provider supports the entry.
+ */
+ContentProvider.prototype.supportsEntry = function(entry) {
+ return entry.toURL().match(this.urlFilter_);
+};
+
+/**
+ * @param {string} type The metadata type.
+ * @return {boolean} Whether this provider provides this metadata.
+ */
+ContentProvider.prototype.providesType = function(type) {
+ return type == 'thumbnail' || type == 'fetchedMedia' || type == 'media';
+};
+
+/**
+ * @return {string} Unique provider id.
+ */
+ContentProvider.prototype.getId = function() { return 'content'; };
+
+/**
+ * Fetches the metadata.
+ * @param {Entry} entry File entry.
+ * @param {string} type Requested metadata type.
+ * @param {function(Object)} callback Callback expects a map from metadata type
+ * to metadata value.
+ */
+ContentProvider.prototype.fetch = function(entry, type, callback) {
+ if (entry.isDirectory) {
+ callback({});
+ return;
+ }
+ var entryURL = entry.toURL();
+ this.callbacks_[entryURL] = callback;
+ this.dispatcher_.postMessage({verb: 'request', arguments: [entryURL]});
+};
+
+/**
+ * Dispatch a message from a metadata reader to the appropriate on* method.
+ * @param {Object} event The event.
+ * @private
+ */
+ContentProvider.prototype.onMessage_ = function(event) {
+ var data = event.data;
+
+ var methodName =
+ 'on' + data.verb.substr(0, 1).toUpperCase() + data.verb.substr(1) + '_';
+
+ if (!(methodName in this)) {
+ console.error('Unknown message from metadata reader: ' + data.verb, data);
+ return;
+ }
+
+ this[methodName].apply(this, data.arguments);
+};
+
+/**
+ * @return {boolean} Whether provider is ready.
+ */
+ContentProvider.prototype.isInitialized = function() {
+ return this.initialized_;
+};
+
+/**
+ * Handles the 'initialized' message from the metadata reader Worker.
+ * @param {Object} regexp Regexp of supported urls.
+ * @private
+ */
+ContentProvider.prototype.onInitialized_ = function(regexp) {
+ this.urlFilter_ = regexp;
+
+ // Tests can monitor for this state with
+ // ExtensionTestMessageListener listener("worker-initialized");
+ // ASSERT_TRUE(listener.WaitUntilSatisfied());
+ // Automated tests need to wait for this, otherwise we crash in
+ // browser_test cleanup because the worker process still has
+ // URL requests in-flight.
+ var test = chrome.test || window.top.chrome.test;
+ test.sendMessage('worker-initialized');
+ this.initialized_ = true;
+};
+
+/**
+ * Converts content metadata from parsers to the internal format.
+ * @param {Object} metadata The content metadata.
+ * @param {Object=} opt_result The internal metadata object ot put result in.
+ * @return {Object!} Converted metadata.
+ */
+ContentProvider.ConvertContentMetadata = function(metadata, opt_result) {
+ var result = opt_result || {};
+
+ if ('thumbnailURL' in metadata) {
+ metadata.thumbnailTransform = metadata.thumbnailTransform || null;
+ result.thumbnail = {
+ url: metadata.thumbnailURL,
+ transform: metadata.thumbnailTransform
+ };
+ }
+
+ for (var key in metadata) {
+ if (metadata.hasOwnProperty(key)) {
+ if (!('media' in result)) result.media = {};
+ result.media[key] = metadata[key];
+ }
+ }
+
+ if ('media' in result) {
+ result.fetchedMedia = result.media;
+ }
+
+ return result;
+};
+
+/**
+ * Handles the 'result' message from the worker.
+ * @param {string} url File url.
+ * @param {Object} metadata The metadata.
+ * @private
+ */
+ContentProvider.prototype.onResult_ = function(url, metadata) {
+ var callback = this.callbacks_[url];
+ delete this.callbacks_[url];
+ callback(ContentProvider.ConvertContentMetadata(metadata));
+};
+
+/**
+ * Handles the 'error' message from the worker.
+ * @param {string} url File entry.
+ * @param {string} step Step failed.
+ * @param {string} error Error description.
+ * @param {Object?} metadata The metadata, if available.
+ * @private
+ */
+ContentProvider.prototype.onError_ = function(url, step, error, metadata) {
+ if (MetadataCache.log) // Avoid log spam by default.
+ console.warn('metadata: ' + url + ': ' + step + ': ' + error);
+ metadata = metadata || {};
+ // Prevent asking for thumbnail again.
+ metadata.thumbnailURL = '';
+ this.onResult_(url, metadata);
+};
+
+/**
+ * Handles the 'log' message from the worker.
+ * @param {Array.<*>} arglist Log arguments.
+ * @private
+ */
+ContentProvider.prototype.onLog_ = function(arglist) {
+ if (MetadataCache.log) // Avoid log spam by default.
+ console.log.apply(console, ['metadata:'].concat(arglist));
+};
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/metadata_dispatcher.js b/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/metadata_dispatcher.js
new file mode 100644
index 00000000000..b711d7559b7
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/metadata_dispatcher.js
@@ -0,0 +1,226 @@
+// 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.
+
+'use strict';
+
+// All of these scripts could be imported with a single call to importScripts,
+// but then load and compile time errors would all be reported from the same
+// line.
+importScripts('metadata_parser.js');
+importScripts('byte_reader.js');
+importScripts('../../../common/js/util.js');
+
+/**
+ * Dispatches metadata requests to the correct parser.
+ *
+ * @param {Object} port Worker port.
+ * @constructor
+ */
+function MetadataDispatcher(port) {
+ this.port_ = port;
+ this.port_.onmessage = this.onMessage.bind(this);
+
+ // Make sure to update component_extension_resources.grd
+ // when adding new parsers.
+ importScripts('exif_parser.js');
+ importScripts('image_parsers.js');
+ importScripts('mpeg_parser.js');
+ importScripts('id3_parser.js');
+
+ var patterns = [];
+
+ this.parserInstances_ = [];
+ for (var i = 0; i < MetadataDispatcher.parserClasses_.length; i++) {
+ var parserClass = MetadataDispatcher.parserClasses_[i];
+ var parser = new parserClass(this);
+ this.parserInstances_.push(parser);
+ patterns.push(parser.urlFilter.source);
+ }
+
+ this.parserRegexp_ = new RegExp('(' + patterns.join('|') + ')', 'i');
+
+ this.messageHandlers_ = {
+ init: this.init_.bind(this),
+ request: this.request_.bind(this)
+ };
+}
+
+/**
+ * List of registered parser classes.
+ * @private
+ */
+MetadataDispatcher.parserClasses_ = [];
+
+/**
+ * @param {function} parserClass Parser constructor function.
+ */
+MetadataDispatcher.registerParserClass = function(parserClass) {
+ MetadataDispatcher.parserClasses_.push(parserClass);
+};
+
+/**
+ * Verbose logging for the dispatcher.
+ *
+ * Individual parsers also take this as their default verbosity setting.
+ */
+MetadataDispatcher.prototype.verbose = false;
+
+/**
+ * |init| message handler.
+ * @private
+ */
+MetadataDispatcher.prototype.init_ = function() {
+ // Inform our owner that we're done initializing.
+ // If we need to pass more data back, we can add it to the param array.
+ this.postMessage('initialized', [this.parserRegexp_]);
+ this.log('initialized with URL filter ' + this.parserRegexp_);
+};
+
+/**
+ * |request| message handler.
+ * @param {string} fileURL File URL.
+ * @private
+ */
+MetadataDispatcher.prototype.request_ = function(fileURL) {
+ try {
+ this.processOneFile(fileURL, function callback(metadata) {
+ this.postMessage('result', [fileURL, metadata]);
+ }.bind(this));
+ } catch (ex) {
+ this.error(fileURL, ex);
+ }
+};
+
+/**
+ * Indicate to the caller that an operation has failed.
+ *
+ * No other messages relating to the failed operation should be sent.
+ * @param {...Object} var_args Arguments.
+ */
+MetadataDispatcher.prototype.error = function(var_args) {
+ var ary = Array.apply(null, arguments);
+ this.postMessage('error', ary);
+};
+
+/**
+ * Send a log message to the caller.
+ *
+ * Callers must not parse log messages for control flow.
+ * @param {...Object} var_args Arguments.
+ */
+MetadataDispatcher.prototype.log = function(var_args) {
+ var ary = Array.apply(null, arguments);
+ this.postMessage('log', ary);
+};
+
+/**
+ * Send a log message to the caller only if this.verbose is true.
+ * @param {...Object} var_args Arguments.
+ */
+MetadataDispatcher.prototype.vlog = function(var_args) {
+ if (this.verbose)
+ this.log.apply(this, arguments);
+};
+
+/**
+ * Post a properly formatted message to the caller.
+ * @param {string} verb Message type descriptor.
+ * @param {Array.<Object>} args Arguments array.
+ */
+MetadataDispatcher.prototype.postMessage = function(verb, args) {
+ this.port_.postMessage({verb: verb, arguments: args});
+};
+
+/**
+ * Message handler.
+ * @param {Event} event Event object.
+ */
+MetadataDispatcher.prototype.onMessage = function(event) {
+ var data = event.data;
+
+ if (this.messageHandlers_.hasOwnProperty(data.verb)) {
+ this.messageHandlers_[data.verb].apply(this, data.arguments);
+ } else {
+ this.log('Unknown message from client: ' + data.verb, data);
+ }
+};
+
+/**
+ * @param {string} fileURL File URL.
+ * @param {function(Object)} callback Completion callback.
+ */
+MetadataDispatcher.prototype.processOneFile = function(fileURL, callback) {
+ var self = this;
+ var currentStep = -1;
+
+ function nextStep(var_args) {
+ self.vlog('nextStep: ' + steps[currentStep + 1].name);
+ steps[++currentStep].apply(self, arguments);
+ }
+
+ var metadata;
+
+ function onError(err, stepName) {
+ self.error(fileURL, stepName || steps[currentStep].name, err.toString(),
+ metadata);
+ }
+
+ var steps =
+ [ // Step one, find the parser matching the url.
+ function detectFormat() {
+ for (var i = 0; i != self.parserInstances_.length; i++) {
+ var parser = self.parserInstances_[i];
+ if (fileURL.match(parser.urlFilter)) {
+ // Create the metadata object as early as possible so that we can
+ // pass it with the error message.
+ metadata = parser.createDefaultMetadata();
+ nextStep(parser);
+ return;
+ }
+ }
+ onError('unsupported format');
+ },
+
+ // Step two, turn the url into an entry.
+ function getEntry(parser) {
+ webkitResolveLocalFileSystemURL(
+ fileURL,
+ function(entry) { nextStep(entry, parser) },
+ onError);
+ },
+
+ // Step three, turn the entry into a file.
+ function getFile(entry, parser) {
+ entry.file(function(file) { nextStep(file, parser) }, onError);
+ },
+
+ // Step four, parse the file content.
+ function parseContent(file, parser) {
+ metadata.fileSize = file.size;
+ try {
+ parser.parse(file, metadata, callback, onError);
+ } catch (e) {
+ onError(e.stack);
+ }
+ }
+ ];
+
+ nextStep();
+};
+
+// Webworker spec says that the worker global object is called self. That's
+// a terrible name since we use it all over the chrome codebase to capture
+// the 'this' keyword in lambdas.
+var global = self;
+
+if (global.constructor.name == 'SharedWorkerGlobalScope') {
+ global.addEventListener('connect', function(e) {
+ var port = e.ports[0];
+ new MetadataDispatcher(port);
+ port.start();
+ });
+} else {
+ // Non-shared worker.
+ new MetadataDispatcher(global);
+}
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/metadata_parser.js b/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/metadata_parser.js
new file mode 100644
index 00000000000..087eb42a9aa
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/metadata_parser.js
@@ -0,0 +1,62 @@
+// 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.
+
+'use strict';
+
+/**
+ * @param {MetadataDispatcher} parent Parent object.
+ * @param {string} type Parser type.
+ * @param {RegExp} urlFilter RegExp to match URLs.
+ * @constructor
+ */
+function MetadataParser(parent, type, urlFilter) {
+ this.parent_ = parent;
+ this.type = type;
+ this.urlFilter = urlFilter;
+ this.verbose = parent.verbose;
+ this.mimeType = 'unknown';
+}
+
+/**
+ * Output an error message.
+ * @param {...Object} var_args Arguments.
+ */
+MetadataParser.prototype.error = function(var_args) {
+ this.parent_.error.apply(this.parent_, arguments);
+};
+
+/**
+ * Output a log message.
+ * @param {...Object} var_args Arguments.
+ */
+MetadataParser.prototype.log = function(var_args) {
+ this.parent_.log.apply(this.parent_, arguments);
+};
+
+/**
+ * Output a log message if |verbose| flag is on.
+ * @param {...Object} var_args Arguments.
+ */
+MetadataParser.prototype.vlog = function(var_args) {
+ if (this.verbose)
+ this.parent_.log.apply(this.parent_, arguments);
+};
+
+/**
+ * @return {Object} Metadata object with the minimal set of properties.
+ */
+MetadataParser.prototype.createDefaultMetadata = function() {
+ return {
+ type: this.type,
+ mimeType: this.mimeType
+ };
+};
+
+/* Base class for image metadata parsers */
+function ImageParser(parent, type, urlFilter) {
+ MetadataParser.apply(this, arguments);
+ this.mimeType = 'image/' + this.type;
+}
+
+ImageParser.prototype = {__proto__: MetadataParser.prototype};
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/mpeg_parser.js b/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/mpeg_parser.js
new file mode 100644
index 00000000000..03637cff6ad
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/metadata/mpeg_parser.js
@@ -0,0 +1,317 @@
+// 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.
+
+'use strict';
+
+/**
+ * @param {MetadataDispatcher} parent Parent object.
+ * @constructor
+ */
+function MpegParser(parent) {
+ MetadataParser.call(this, parent, 'mpeg', /\.(mp4|m4v|m4a|mpe?g4?)$/i);
+ this.mimeType = 'video/mpeg';
+}
+
+MpegParser.prototype = {__proto__: MetadataParser.prototype};
+
+/**
+ * Size of the atom header.
+ */
+MpegParser.HEADER_SIZE = 8;
+
+/**
+ * @param {ByteReader} br ByteReader instance.
+ * @param {number=} opt_end End of atom position.
+ * @return {number} Atom size.
+ */
+MpegParser.readAtomSize = function(br, opt_end) {
+ var pos = br.tell();
+
+ if (opt_end) {
+ // Assert that opt_end <= buffer end.
+ // When supplied, opt_end is the end of the enclosing atom and is used to
+ // check the correct nesting.
+ br.validateRead(opt_end - pos);
+ }
+
+ var size = br.readScalar(4, false, opt_end);
+
+ if (size < MpegParser.HEADER_SIZE)
+ throw 'atom too short (' + size + ') @' + pos;
+
+ if (opt_end && pos + size > opt_end)
+ throw 'atom too long (' + size + '>' + (opt_end - pos) + ') @' + pos;
+
+ return size;
+};
+
+/**
+ * @param {ByteReader} br ByteReader instance.
+ * @param {number=} opt_end End of atom position.
+ * @return {string} Atom name.
+ */
+MpegParser.readAtomName = function(br, opt_end) {
+ return br.readString(4, opt_end).toLowerCase();
+};
+
+/**
+ * @param {Object} metadata Metadata object.
+ * @return {Object} Root of the parser tree.
+ */
+MpegParser.createRootParser = function(metadata) {
+ function findParentAtom(atom, name) {
+ for (;;) {
+ atom = atom.parent;
+ if (!atom) return null;
+ if (atom.name == name) return atom;
+ }
+ }
+
+ function parseFtyp(br, atom) {
+ metadata.brand = br.readString(4, atom.end);
+ }
+
+ function parseMvhd(br, atom) {
+ var version = br.readScalar(4, false, atom.end);
+ var offset = (version == 0) ? 8 : 16;
+ br.seek(offset, ByteReader.SEEK_CUR);
+ var timescale = br.readScalar(4, false, atom.end);
+ var duration = br.readScalar(4, false, atom.end);
+ metadata.duration = duration / timescale;
+ }
+
+ function parseHdlr(br, atom) {
+ br.seek(8, ByteReader.SEEK_CUR);
+ findParentAtom(atom, 'trak').trackType = br.readString(4, atom.end);
+ }
+
+ function parseStsd(br, atom) {
+ var track = findParentAtom(atom, 'trak');
+ if (track && track.trackType == 'vide') {
+ br.seek(40, ByteReader.SEEK_CUR);
+ metadata.width = br.readScalar(2, false, atom.end);
+ metadata.height = br.readScalar(2, false, atom.end);
+ }
+ }
+
+ function parseDataString(name, br, atom) {
+ br.seek(8, ByteReader.SEEK_CUR);
+ metadata[name] = br.readString(atom.end - br.tell(), atom.end);
+ }
+
+ function parseCovr(br, atom) {
+ br.seek(8, ByteReader.SEEK_CUR);
+ metadata.thumbnailURL = br.readImage(atom.end - br.tell(), atom.end);
+ }
+
+ // 'meta' atom can occur at one of the several places in the file structure.
+ var parseMeta = {
+ ilst: {
+ '©nam': { data: parseDataString.bind(null, 'title') },
+ '©alb': { data: parseDataString.bind(null, 'album') },
+ '©art': { data: parseDataString.bind(null, 'artist') },
+ 'covr': { data: parseCovr }
+ },
+ versioned: true
+ };
+
+ // main parser for the entire file structure.
+ return {
+ ftyp: parseFtyp,
+ moov: {
+ mvhd: parseMvhd,
+ trak: {
+ mdia: {
+ hdlr: parseHdlr,
+ minf: {
+ stbl: {
+ stsd: parseStsd
+ }
+ }
+ },
+ meta: parseMeta
+ },
+ udta: {
+ meta: parseMeta
+ },
+ meta: parseMeta
+ },
+ meta: parseMeta
+ };
+};
+
+/**
+ *
+ * @param {File} file File.
+ * @param {Object} metadata Metadata.
+ * @param {function(Object)} callback Success callback.
+ * @param {function} onError Error callback.
+ */
+MpegParser.prototype.parse = function(file, metadata, callback, onError) {
+ this.rootParser_ = MpegParser.createRootParser(metadata);
+
+ // Kick off the processing by reading the first atom's header.
+ this.requestRead(file, 0, MpegParser.HEADER_SIZE, null,
+ onError, callback.bind(null, metadata));
+};
+
+/**
+ * @param {function(ByteReader, Object)|Object} parser Parser tree node.
+ * @param {ByteReader} br ByteReader instance.
+ * @param {Object} atom Atom descriptor.
+ * @param {number} filePos File position of the atom start.
+ */
+MpegParser.prototype.applyParser = function(parser, br, atom, filePos) {
+ if (this.verbose) {
+ var path = atom.name;
+ for (var p = atom.parent; p && p.name; p = p.parent) {
+ path = p.name + '.' + path;
+ }
+
+ var action;
+ if (!parser) {
+ action = 'skipping ';
+ } else if (parser instanceof Function) {
+ action = 'parsing ';
+ } else {
+ action = 'recursing';
+ }
+
+ var start = atom.start - MpegParser.HEADER_SIZE;
+ this.vlog(path + ': ' +
+ '@' + (filePos + start) + ':' + (atom.end - start),
+ action);
+ }
+
+ if (parser) {
+ if (parser instanceof Function) {
+ br.pushSeek(atom.start);
+ parser(br, atom);
+ br.popSeek();
+ } else {
+ if (parser.versioned) {
+ atom.start += 4;
+ }
+ this.parseMpegAtomsInRange(parser, br, atom, filePos);
+ }
+ }
+};
+
+/**
+ * @param {function(ByteReader, Object)|Object} parser Parser tree node.
+ * @param {ByteReader} br ByteReader instance.
+ * @param {Object} parentAtom Parent atom descriptor.
+ * @param {number} filePos File position of the atom start.
+ */
+MpegParser.prototype.parseMpegAtomsInRange = function(
+ parser, br, parentAtom, filePos) {
+ var count = 0;
+ for (var offset = parentAtom.start; offset != parentAtom.end;) {
+ if (count++ > 100) // Most likely we are looping through a corrupt file.
+ throw 'too many child atoms in ' + parentAtom.name + ' @' + offset;
+
+ br.seek(offset);
+ var size = MpegParser.readAtomSize(br, parentAtom.end);
+ var name = MpegParser.readAtomName(br, parentAtom.end);
+
+ this.applyParser(
+ parser[name],
+ br,
+ { start: offset + MpegParser.HEADER_SIZE,
+ end: offset + size,
+ name: name,
+ parent: parentAtom
+ },
+ filePos
+ );
+
+ offset += size;
+ }
+};
+
+/**
+ * @param {File} file File.
+ * @param {number} filePos Start position in the file.
+ * @param {number} size Atom size.
+ * @param {string} name Atom name.
+ * @param {function} onError Error callback.
+ * @param {function} onSuccess Success callback.
+ */
+MpegParser.prototype.requestRead = function(
+ file, filePos, size, name, onError, onSuccess) {
+ var self = this;
+ var reader = new FileReader();
+ reader.onerror = onError;
+ reader.onload = function(event) {
+ self.processTopLevelAtom(
+ reader.result, file, filePos, size, name, onError, onSuccess);
+ };
+ this.vlog('reading @' + filePos + ':' + size);
+ reader.readAsArrayBuffer(file.slice(filePos, filePos + size));
+};
+
+/**
+ * @param {ArrayBuffer} buf Data buffer.
+ * @param {File} file File.
+ * @param {number} filePos Start position in the file.
+ * @param {number} size Atom size.
+ * @param {string} name Atom name.
+ * @param {function} onError Error callback.
+ * @param {function} onSuccess Success callback.
+ */
+MpegParser.prototype.processTopLevelAtom = function(
+ buf, file, filePos, size, name, onError, onSuccess) {
+ try {
+ var br = new ByteReader(buf);
+
+ // the header has already been read.
+ var atomEnd = size - MpegParser.HEADER_SIZE;
+
+ var bufLength = buf.byteLength;
+
+ // Check the available data size. It should be either exactly
+ // what we requested or HEADER_SIZE bytes less (for the last atom).
+ if (bufLength != atomEnd && bufLength != size) {
+ throw 'Read failure @' + filePos + ', ' +
+ 'requested ' + size + ', read ' + bufLength;
+ }
+
+ // Process the top level atom.
+ if (name) { // name is null only the first time.
+ this.applyParser(
+ this.rootParser_[name],
+ br,
+ {start: 0, end: atomEnd, name: name},
+ filePos
+ );
+ }
+
+ filePos += bufLength;
+ if (bufLength == size) {
+ // The previous read returned everything we asked for, including
+ // the next atom header at the end of the buffer.
+ // Parse this header and schedule the next read.
+ br.seek(-MpegParser.HEADER_SIZE, ByteReader.SEEK_END);
+ var nextSize = MpegParser.readAtomSize(br);
+ var nextName = MpegParser.readAtomName(br);
+
+ // If we do not have a parser for the next atom, skip the content and
+ // read only the header (the one after the next).
+ if (!this.rootParser_[nextName]) {
+ filePos += nextSize - MpegParser.HEADER_SIZE;
+ nextSize = MpegParser.HEADER_SIZE;
+ }
+
+ this.requestRead(file, filePos, nextSize, nextName, onError, onSuccess);
+ } else {
+ // The previous read did not return the next atom header, EOF reached.
+ this.vlog('EOF @' + filePos);
+ onSuccess();
+ }
+ } catch (e) {
+ onError(e.toString());
+ }
+};
+
+MetadataDispatcher.registerParserClass(MpegParser);
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/metrics.js b/chromium/chrome/browser/resources/file_manager/foreground/js/metrics.js
new file mode 100644
index 00000000000..3f34e51a299
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/metrics.js
@@ -0,0 +1,131 @@
+// 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.
+
+/**
+ * @fileoverview Utility methods for accessing chrome.metricsPrivate API.
+ *
+ * To be included as a first script in main.html
+ */
+
+var metrics = {};
+
+/**
+ * A map from interval name to interval start timestamp.
+ */
+metrics.intervals = {};
+
+/**
+ * Start the named time interval.
+ * Should be followed by a call to recordInterval with the same name.
+ *
+ * @param {string} name Unique interval name.
+ */
+metrics.startInterval = function(name) {
+ metrics.intervals[name] = Date.now();
+};
+
+metrics.startInterval('Load.Total');
+metrics.startInterval('Load.Script');
+
+/**
+ * Convert a short metric name to the full format.
+ *
+ * @param {string} name Short metric name.
+ * @return {string} Full metric name.
+ * @private
+ */
+metrics.convertName_ = function(name) {
+ return 'FileBrowser.' + name;
+};
+
+/**
+ * Wrapper method for calling chrome.fileBrowserPrivate safely.
+ * @param {string} name Method name.
+ * @param {Array.<Object>} args Arguments.
+ * @private
+ */
+metrics.call_ = function(name, args) {
+ try {
+ chrome.metricsPrivate[name].apply(chrome.metricsPrivate, args);
+ } catch (e) {
+ console.error(e.stack);
+ }
+};
+
+/**
+ * Create a decorator function that calls a chrome.metricsPrivate function
+ * with the same name and correct parameters.
+ *
+ * @param {string} name Method name.
+ */
+metrics.decorate = function(name) {
+ metrics[name] = function() {
+ var args = Array.apply(null, arguments);
+ args[0] = metrics.convertName_(args[0]);
+ metrics.call_(name, args);
+ if (metrics.log) {
+ console.log('chrome.metricsPrivate.' + name, args);
+ }
+ };
+};
+
+metrics.decorate('recordMediumCount');
+metrics.decorate('recordSmallCount');
+metrics.decorate('recordTime');
+metrics.decorate('recordUserAction');
+
+/**
+ * Complete the time interval recording.
+ *
+ * Should be preceded by a call to startInterval with the same name. *
+ *
+ * @param {string} name Unique interval name.
+ */
+metrics.recordInterval = function(name) {
+ if (name in metrics.intervals) {
+ metrics.recordTime(name, Date.now() - metrics.intervals[name]);
+ } else {
+ console.error('Unknown interval: ' + name);
+ }
+};
+
+/**
+ * Record an enum value.
+ *
+ * @param {string} name Metric name.
+ * @param {Object} value Enum value.
+ * @param {Array.<Object>|number} validValues Array of valid values
+ * or a boundary number value.
+ */
+metrics.recordEnum = function(name, value, validValues) {
+ var boundaryValue;
+ var index;
+ if (validValues.constructor.name == 'Array') {
+ index = validValues.indexOf(value);
+ boundaryValue = validValues.length;
+ } else {
+ index = value;
+ boundaryValue = validValues;
+ }
+ // Collect invalid values in the overflow bucket at the end.
+ if (index < 0 || index > boundaryValue)
+ index = boundaryValue;
+
+ // Setting min to 1 looks strange but this is exactly the recommended way
+ // of using histograms for enum-like types. Bucket #0 works as a regular
+ // bucket AND the underflow bucket.
+ // (Source: UMA_HISTOGRAM_ENUMERATION definition in base/metrics/histogram.h)
+ var metricDescr = {
+ 'metricName': metrics.convertName_(name),
+ 'type': 'histogram-linear',
+ 'min': 1,
+ 'max': boundaryValue,
+ 'buckets': boundaryValue + 1
+ };
+ metrics.call_('recordValue', [metricDescr, index]);
+ if (metrics.log) {
+ console.log('chrome.metricsPrivate.recordValue',
+ [metricDescr.metricName, index, value]);
+ }
+};
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/navigation_list_model.js b/chromium/chrome/browser/resources/file_manager/foreground/js/navigation_list_model.js
new file mode 100644
index 00000000000..f1d31e6966d
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/navigation_list_model.js
@@ -0,0 +1,350 @@
+// 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.
+
+'use strict';
+
+/**
+ * Entry of NavigationListModel. This constructor should be called only from
+ * the helper methods (NavigationModelItem.create).
+ *
+ * @param {string} path Path.
+ * @param {DirectoryEntry} entry Entry. Can be null.
+ * @param {string} label Label.
+ * @constructor
+ */
+function NavigationModelItem(path, entry, label) {
+ this.path_ = path;
+ this.entry_ = entry;
+ this.label_ = label;
+ this.resolvingQueue_ = new AsyncUtil.Queue();
+
+ Object.seal(this);
+}
+
+NavigationModelItem.prototype = {
+ get path() { return this.path_; },
+ get label() { return this.label_; }
+};
+
+/**
+ * Returns the cached entry of the item. This may return NULL if the target is
+ * not available on the filesystem, is not resolved or is under resolving the
+ * entry.
+ *
+ * @return {Entry} Cached entry.
+ */
+NavigationModelItem.prototype.getCachedEntry = function() {
+ return this.entry_;
+};
+
+/**
+ * TODO(mtomasz): Use Entry instead of path.
+ * @param {VolumeManagerWrapper} volumeManager VolumeManagerWrapper instance.
+ * @param {string} path Path.
+ * @param {DirectoryEntry} entry Entry. Can be null.
+ * @param {string} label Label.
+ * @param {function(FileError)} errorCallback Called when the resolving is
+ * failed with the error.
+ * @return {NavigationModelItem} Created NavigationModelItem.
+ */
+NavigationModelItem.create = function(
+ volumeManager, path, entry, label, errorCallback) {
+ var item = new NavigationModelItem(path, entry, label);
+
+ // If the given entry is null, try to resolve path to get an entry.
+ if (!entry) {
+ item.resolvingQueue_.run(function(continueCallback) {
+ volumeManager.resolveAbsolutePath(
+ path,
+ function(entry) {
+ if (entry.isDirectory)
+ item.entry_ = entry;
+ else
+ errorCallback(util.createFileError(FileError.TYPE_MISMATCH_ERR));
+ continueCallback();
+ },
+ function(error) {
+ errorCallback(error);
+ continueCallback();
+ });
+ });
+ }
+ return item;
+};
+
+/**
+ * Retrieves the entry. If the entry is being retrieved, waits until it
+ * finishes.
+ * @param {function(Entry)} callback Called with the resolved entry. The entry
+ * may be NULL if resolving is failed.
+ */
+NavigationModelItem.prototype.getEntryAsync = function(callback) {
+ // If resolving the entry is running, wait until it finishes.
+ this.resolvingQueue_.run(function(continueCallback) {
+ callback(this.entry_);
+ continueCallback();
+ }.bind(this));
+};
+
+/**
+ * Returns if this item is a shortcut or a volume root.
+ * @return {boolean} True if a shortcut, false if a volume root.
+ */
+NavigationModelItem.prototype.isShortcut = function() {
+ return !PathUtil.isRootPath(this.path_);
+};
+
+/**
+ * A navigation list model. This model combines the 2 lists.
+ * @param {VolumeManagerWrapper} volumeManager VolumeManagerWrapper instance.
+ * @param {cr.ui.ArrayDataModel} shortcutListModel The list of folder shortcut.
+ * @constructor
+ * @extends {cr.EventTarget}
+ */
+function NavigationListModel(volumeManager, shortcutListModel) {
+ cr.EventTarget.call(this);
+
+ this.volumeManager_ = volumeManager;
+ this.shortcutListModel_ = shortcutListModel;
+
+ var volumeInfoToModelItem = function(volumeInfo) {
+ if (volumeInfo.volumeType == util.VolumeType.DRIVE) {
+ // For drive volume, we assign the path to "My Drive".
+ return NavigationModelItem.create(
+ this.volumeManager_,
+ volumeInfo.mountPath + '/root',
+ null,
+ volumeInfo.getLabel(),
+ function() {});
+ } else {
+ return NavigationModelItem.create(
+ this.volumeManager_,
+ volumeInfo.mountPath,
+ volumeInfo.root,
+ volumeInfo.getLabel(),
+ function() {});
+ }
+ }.bind(this);
+
+ var pathToModelItem = function(path) {
+ var item = NavigationModelItem.create(
+ this.volumeManager_,
+ path,
+ null, // Entry will be resolved.
+ PathUtil.getFolderLabel(path),
+ function(error) {
+ if (error.code == FileError.NOT_FOUND_ERR)
+ this.onItemNotFoundError(item);
+ }.bind(this));
+ return item;
+ }.bind(this);
+
+ /**
+ * Type of updated list.
+ * @enum {number}
+ * @const
+ */
+ var ListType = {
+ VOLUME_LIST: 1,
+ SHORTCUT_LIST: 2
+ };
+ Object.freeze(ListType);
+
+ // Generates this.volumeList_ and this.shortcutList_ from the models.
+ this.volumeList_ =
+ this.volumeManager_.volumeInfoList.slice().map(volumeInfoToModelItem);
+
+ this.shortcutList_ = [];
+ for (var i = 0; i < this.shortcutListModel_.length; i++) {
+ var shortcutPath = this.shortcutListModel_.item(i);
+ var volumeInfo = this.volumeManager_.getVolumeInfo(shortcutPath);
+ var isMounted = volumeInfo && !volumeInfo.error;
+ if (isMounted)
+ this.shortcutList_.push(pathToModelItem(shortcutPath));
+ }
+
+ // Generates a combined 'permuted' event from an event of either list.
+ var permutedHandler = function(listType, event) {
+ var permutation;
+
+ // Build the volumeList.
+ if (listType == ListType.VOLUME_LIST) {
+ // The volume is mounted or unmounted.
+ var newList = [];
+
+ // Use the old instances if they just move.
+ for (var i = 0; i < event.permutation.length; i++) {
+ if (event.permutation[i] >= 0)
+ newList[event.permutation[i]] = this.volumeList_[i];
+ }
+
+ // Create missing instances.
+ for (var i = 0; i < event.newLength; i++) {
+ if (!newList[i]) {
+ newList[i] = volumeInfoToModelItem(
+ this.volumeManager_.volumeInfoList.item(i));
+ }
+ }
+ this.volumeList_ = newList;
+
+ permutation = event.permutation.slice();
+ } else {
+ // volumeList part has not been changed, so the permutation should be
+ // idenetity mapping.
+ permutation = [];
+ for (var i = 0; i < this.volumeList_.length; i++)
+ permutation[i] = i;
+ }
+
+ // Build the shortcutList. Even if the event is for the volumeInfoList
+ // update, the short cut path may be unmounted or newly mounted. So, here
+ // shortcutList will always be re-built.
+ // Currently this code may be redundant, as shortcut folder is supported
+ // only on Drive File System and we can assume single-profile, but
+ // multi-profile will be supported later.
+ // The shortcut list is sorted in case-insensitive lexicographical order.
+ // So we just can traverse the two list linearly.
+ var modelIndex = 0;
+ var oldListIndex = 0;
+ var newList = [];
+ while (modelIndex < this.shortcutListModel_.length &&
+ oldListIndex < this.shortcutList_.length) {
+ var shortcutPath = this.shortcutListModel_.item(modelIndex);
+ var cmp = this.shortcutListModel_.compare(
+ shortcutPath, this.shortcutList_[oldListIndex].path);
+ if (cmp > 0) {
+ // The shortcut at shortcutList_[oldListIndex] is removed.
+ permutation.push(-1);
+ oldListIndex++;
+ continue;
+ }
+
+ // Check if the volume where the shortcutPath is is mounted or not.
+ var volumeInfo = this.volumeManager_.getVolumeInfo(shortcutPath);
+ var isMounted = volumeInfo && !volumeInfo.error;
+ if (cmp == 0) {
+ // There exists an old NavigationModelItem instance.
+ if (isMounted) {
+ // Reuse the old instance.
+ permutation.push(newList.length + this.volumeList_.length);
+ newList.push(this.shortcutList_[oldListIndex]);
+ } else {
+ permutation.push(-1);
+ }
+ oldListIndex++;
+ } else {
+ // We needs to create a new instance for the shortcut path.
+ if (isMounted)
+ newList.push(pathToModelItem(shortcutPath));
+ }
+ modelIndex++;
+ }
+
+ // Add remaining (new) shortcuts if necessary.
+ for (; modelIndex < this.shortcutListModel_.length; modelIndex++) {
+ var shortcutPath = this.shortcutListModel_.item(modelIndex);
+ var volumeInfo = this.volumeManager_.getVolumeInfo(shortcutPath);
+ var isMounted = volumeInfo && !volumeInfo.error;
+ if (isMounted)
+ newList.push(pathToModelItem(shortcutPath));
+ }
+
+ // Fill remaining permutation if necessary.
+ for (; oldListIndex < this.shortcutList_.length; oldListIndex++)
+ permutation.push(-1);
+
+ this.shortcutList_ = newList;
+
+ // Dispatch permuted event.
+ var 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.
+}
+
+/**
+ * NavigationList inherits cr.EventTarget.
+ */
+NavigationListModel.prototype = {
+ __proto__: cr.EventTarget.prototype,
+ get length() { return this.length_(); },
+ get folderShortcutList() { return this.shortcutList_; }
+};
+
+/**
+ * Returns the item at the given index.
+ * @param {number} index The index of the entry to get.
+ * @return {?string} The path at the given index.
+ */
+NavigationListModel.prototype.item = function(index) {
+ var offset = this.volumeList_.length;
+ if (index < offset)
+ return this.volumeList_[index];
+ return this.shortcutList_[index - offset];
+};
+
+/**
+ * Returns the number of items in the model.
+ * @return {number} The length of the model.
+ * @private
+ */
+NavigationListModel.prototype.length_ = function() {
+ return this.volumeList_.length + this.shortcutList_.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.
+ */
+NavigationListModel.prototype.indexOf = function(modelItem, opt_fromIndex) {
+ for (var i = opt_fromIndex || 0; i < this.length; i++) {
+ if (modelItem === this.item(i))
+ return i;
+ }
+ return -1;
+};
+
+/**
+ * Called when one od the items is not found on the filesystem.
+ * @param {NavigationModelItem} modelItem The entry which is not found.
+ */
+NavigationListModel.prototype.onItemNotFoundError = function(modelItem) {
+ var index = this.indexOf(modelItem);
+ if (index === -1) {
+ // Invalid modelItem.
+ } else if (index < this.volumeList_.length) {
+ // The item is in the volume list.
+ // Not implemented.
+ // TODO(yoshiki): Implement it when necessary.
+ } else {
+ // The item is in the folder shortcut list.
+ if (this.isDriveMounted())
+ this.shortcutListModel_.remove(modelItem.path);
+ }
+};
+
+/**
+ * Returns if the drive is mounted or not.
+ * @return {boolean} True if the drive is mounted, false otherwise.
+ */
+NavigationListModel.prototype.isDriveMounted = function() {
+ var volumeInfo =
+ this.volumeManager_.getCurrentProfileVolumeInfo(RootType.DRIVE);
+ return !!volumeInfo && volumeInfo.root;
+};
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/photo/gallery.js b/chromium/chrome/browser/resources/file_manager/foreground/js/photo/gallery.js
new file mode 100644
index 00000000000..91171b6d3f1
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/photo/gallery.js
@@ -0,0 +1,867 @@
+// 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.
+
+'use strict';
+
+/**
+ * Called from the main frame when unloading.
+ * @return {string?} User-visible message on null if it is OK to close.
+ */
+function beforeunload() { return Gallery.instance.onBeforeUnload() }
+
+/**
+ * Called from the main frame when unloading.
+ * @param {boolean=} opt_exiting True if the app is exiting.
+ */
+function unload(opt_exiting) { Gallery.instance.onUnload(opt_exiting) }
+
+/**
+ * Gallery for viewing and editing image files.
+ *
+ * @param {Object} context Object containing the following:
+ * {function(string)} onNameChange Called every time a selected
+ * item name changes (on rename and on selection change).
+ * {AppWindow} appWindow
+ * {function(string)} onBack
+ * {function()} onClose
+ * {function()} onMaximize
+ * {function(boolean)} onAppRegionChanged
+ * {MetadataCache} metadataCache
+ * {Array.<Object>} shareActions
+ * {string} readonlyDirName Directory name for readonly warning or null.
+ * {DirEntry} saveDirEntry Directory to save to.
+ * {function(string)} displayStringFunction.
+ * @param {VolumeManagerWrapper} volumeManager The VolumeManager instance of
+ * the system.
+ * @class
+ * @constructor
+ */
+function Gallery(context, volumeManager) {
+ this.container_ = document.querySelector('.gallery');
+ this.document_ = document;
+ this.context_ = context;
+ this.metadataCache_ = context.metadataCache;
+ this.volumeManager_ = volumeManager;
+ this.selectedEntry_ = null;
+
+ this.dataModel_ = new cr.ui.ArrayDataModel([]);
+ this.selectionModel_ = new cr.ui.ListSelectionModel();
+ this.displayStringFunction_ = context.displayStringFunction;
+
+ this.initDom_();
+ this.initListeners_();
+}
+
+/**
+ * Gallery extends cr.EventTarget.
+ */
+Gallery.prototype.__proto__ = cr.EventTarget.prototype;
+
+/**
+ * Creates and initializes a Gallery object based on a context.
+ *
+ * @param {Object} context Gallery context.
+ * @param {VolumeManagerWrapper} volumeManager VolumeManager of the system.
+ * @param {Array.<Entry>} entries Array of entries.
+ * @param {Array.<Entry>} selectedEntries Array of selected entries.
+ */
+Gallery.open = function(context, volumeManager, entries, selectedEntries) {
+ Gallery.instance = new Gallery(context, volumeManager);
+ Gallery.instance.load(entries, selectedEntries);
+};
+
+/**
+ * Tools fade-out timeout im milliseconds.
+ * @const
+ * @type {number}
+ */
+Gallery.FADE_TIMEOUT = 3000;
+
+/**
+ * First time tools fade-out timeout im milliseconds.
+ * @const
+ * @type {number}
+ */
+Gallery.FIRST_FADE_TIMEOUT = 1000;
+
+/**
+ * Time until mosaic is initialized in the background. Used to make gallery
+ * in the slide mode load faster. In miiliseconds.
+ * @const
+ * @type {number}
+ */
+Gallery.MOSAIC_BACKGROUND_INIT_DELAY = 1000;
+
+/**
+ * Types of metadata Gallery uses (to query the metadata cache).
+ * @const
+ * @type {string}
+ */
+Gallery.METADATA_TYPE = 'thumbnail|filesystem|media|streaming|drive';
+
+/**
+ * Initializes listeners.
+ * @private
+ */
+Gallery.prototype.initListeners_ = function() {
+ this.document_.oncontextmenu = function(e) { e.preventDefault(); };
+ this.keyDownBound_ = this.onKeyDown_.bind(this);
+ this.document_.body.addEventListener('keydown', this.keyDownBound_);
+
+ this.inactivityWatcher_ = new MouseInactivityWatcher(
+ this.container_, Gallery.FADE_TIMEOUT, this.hasActiveTool.bind(this));
+
+ // Search results may contain files from different subdirectories so
+ // the observer is not going to work.
+ if (!this.context_.searchResults && this.context_.curDirEntry) {
+ this.thumbnailObserverId_ = this.metadataCache_.addObserver(
+ this.context_.curDirEntry,
+ MetadataCache.CHILDREN,
+ 'thumbnail',
+ this.updateThumbnails_.bind(this));
+ }
+
+ this.volumeManager_.addEventListener('externally-unmounted',
+ this.onExternallyUnmounted_.bind(this));
+};
+
+/**
+ * Closes gallery when a volume containing the selected item is unmounted.
+ * @param {Event} event The unmount event.
+ * @private
+ */
+Gallery.prototype.onExternallyUnmounted_ = function(event) {
+ if (!this.selectedEntry_)
+ return;
+
+ if (this.volumeManager_.getVolumeInfo(this.selectedEntry_) ===
+ event.volumeInfo) {
+ this.onBack_();
+ }
+};
+
+/**
+ * Beforeunload handler.
+ * @return {string?} User-visible message on null if it is OK to close.
+ */
+Gallery.prototype.onBeforeUnload = function() {
+ return this.slideMode_.onBeforeUnload();
+};
+
+/**
+ * Unloads the Gallery.
+ * @param {boolean} exiting True if the app is exiting.
+ */
+Gallery.prototype.onUnload = function(exiting) {
+ if (!this.context_.searchResults) {
+ this.metadataCache_.removeObserver(this.thumbnailObserverId_);
+ }
+ this.slideMode_.onUnload(exiting);
+};
+
+/**
+ * Initializes DOM UI
+ * @private
+ */
+Gallery.prototype.initDom_ = function() {
+ var content = util.createChild(this.container_, 'content');
+ content.addEventListener('click', this.onContentClick_.bind(this));
+
+ this.header_ = util.createChild(this.container_, 'header tool dimmable');
+ this.toolbar_ = util.createChild(this.container_, 'toolbar tool dimmable');
+
+ var backButton = util.createChild(this.container_,
+ 'back-button tool dimmable');
+ util.createChild(backButton);
+ backButton.addEventListener('click', this.onBack_.bind(this));
+
+ var preventDefault = function(event) { event.preventDefault(); };
+
+ var maximizeButton = util.createChild(this.header_,
+ 'maximize-button tool dimmable',
+ 'button');
+ maximizeButton.tabIndex = -1;
+ maximizeButton.addEventListener('click', this.onMaximize_.bind(this));
+ maximizeButton.addEventListener('mousedown', preventDefault);
+
+ var closeButton = util.createChild(this.header_,
+ 'close-button tool dimmable',
+ 'button');
+ closeButton.tabIndex = -1;
+ closeButton.addEventListener('click', this.onClose_.bind(this));
+ closeButton.addEventListener('mousedown', preventDefault);
+
+ this.filenameSpacer_ = util.createChild(this.toolbar_, 'filename-spacer');
+ this.filenameEdit_ = util.createChild(this.filenameSpacer_,
+ 'namebox', 'input');
+
+ this.filenameEdit_.setAttribute('type', 'text');
+ this.filenameEdit_.addEventListener('blur',
+ this.onFilenameEditBlur_.bind(this));
+
+ this.filenameEdit_.addEventListener('focus',
+ this.onFilenameFocus_.bind(this));
+
+ this.filenameEdit_.addEventListener('keydown',
+ this.onFilenameEditKeydown_.bind(this));
+
+ util.createChild(this.toolbar_, 'button-spacer');
+
+ this.prompt_ = new ImageEditor.Prompt(
+ this.container_, this.displayStringFunction_);
+
+ this.modeButton_ = util.createChild(this.toolbar_, 'button mode', 'button');
+ this.modeButton_.addEventListener('click',
+ this.toggleMode_.bind(this, null));
+
+ this.mosaicMode_ = new MosaicMode(content,
+ this.dataModel_,
+ this.selectionModel_,
+ this.metadataCache_,
+ this.toggleMode_.bind(this, null));
+
+ this.slideMode_ = new SlideMode(this.container_,
+ content,
+ this.toolbar_,
+ this.prompt_,
+ this.dataModel_,
+ this.selectionModel_,
+ this.context_,
+ this.toggleMode_.bind(this),
+ this.displayStringFunction_);
+
+ this.slideMode_.addEventListener('image-displayed', function() {
+ cr.dispatchSimpleEvent(this, 'image-displayed');
+ }.bind(this));
+ this.slideMode_.addEventListener('image-saved', function() {
+ cr.dispatchSimpleEvent(this, 'image-saved');
+ }.bind(this));
+
+ var deleteButton = this.createToolbarButton_('delete', 'GALLERY_DELETE');
+ deleteButton.addEventListener('click', this.delete_.bind(this));
+
+ this.shareButton_ = this.createToolbarButton_('share', 'GALLERY_SHARE');
+ this.shareButton_.setAttribute('disabled', '');
+ this.shareButton_.addEventListener('click', this.toggleShare_.bind(this));
+
+ this.shareMenu_ = util.createChild(this.container_, 'share-menu');
+ this.shareMenu_.hidden = true;
+ util.createChild(this.shareMenu_, 'bubble-point');
+
+ this.dataModel_.addEventListener('splice', this.onSplice_.bind(this));
+ this.dataModel_.addEventListener('content', this.onContentChange_.bind(this));
+
+ this.selectionModel_.addEventListener('change', this.onSelection_.bind(this));
+ this.slideMode_.addEventListener('useraction', this.onUserAction_.bind(this));
+};
+
+/**
+ * Creates toolbar button.
+ *
+ * @param {string} className Class to add.
+ * @param {string} title Button title.
+ * @return {HTMLElement} Newly created button.
+ * @private
+ */
+Gallery.prototype.createToolbarButton_ = function(className, title) {
+ var button = util.createChild(this.toolbar_, className, 'button');
+ button.title = this.displayStringFunction_(title);
+ return button;
+};
+
+/**
+ * Loads the content.
+ *
+ * @param {Array.<Entry>} entries Array of entries.
+ * @param {Array.<Entry>} selectedEntries Array of selected entries. Must be a
+ * subset of {@code entries}.
+ */
+Gallery.prototype.load = function(entries, selectedEntries) {
+ var items = [];
+ for (var index = 0; index < entries.length; ++index) {
+ items.push(new Gallery.Item(entries[index]));
+ }
+ this.dataModel_.push.apply(this.dataModel_, items);
+
+ this.selectionModel_.adjustLength(this.dataModel_.length);
+
+ for (var i = 0; i !== selectedEntries.length; i++) {
+ var selectedIndex = entries.indexOf(selectedEntries[i]);
+ if (selectedIndex >= 0)
+ this.selectionModel_.setIndexSelected(selectedIndex, true);
+ else
+ console.error('Cannot select ' + selectedEntries[i]);
+ }
+
+ if (this.selectionModel_.selectedIndexes.length === 0)
+ this.onSelection_();
+
+ var mosaic = this.mosaicMode_ && this.mosaicMode_.getMosaic();
+
+ // Mosaic view should show up if most of the selected files are images.
+ var imagesCount = 0;
+ for (var i = 0; i !== selectedEntries.length; i++) {
+ if (FileType.getMediaType(selectedEntries[i]) === 'image')
+ imagesCount++;
+ }
+ var mostlyImages = imagesCount > (selectedEntries.length / 2.0);
+
+ var forcedMosaic = (this.context_.pageState &&
+ this.context_.pageState.gallery === 'mosaic');
+
+ var showMosaic = (mostlyImages && selectedEntries.length > 1) || forcedMosaic;
+ if (mosaic && showMosaic) {
+ this.setCurrentMode_(this.mosaicMode_);
+ mosaic.init();
+ mosaic.show();
+ this.inactivityWatcher_.check(); // Show the toolbar.
+ cr.dispatchSimpleEvent(this, 'loaded');
+ } else {
+ this.setCurrentMode_(this.slideMode_);
+ var maybeLoadMosaic = function() {
+ if (mosaic)
+ mosaic.init();
+ cr.dispatchSimpleEvent(this, 'loaded');
+ }.bind(this);
+ /* TODO: consider nice blow-up animation for the first image */
+ this.slideMode_.enter(null, function() {
+ // Flash the toolbar briefly to show it is there.
+ this.inactivityWatcher_.kick(Gallery.FIRST_FADE_TIMEOUT);
+ }.bind(this),
+ maybeLoadMosaic);
+ }
+};
+
+/**
+ * Closes the Gallery and go to Files.app.
+ * @private
+ */
+Gallery.prototype.back_ = function() {
+ if (util.isFullScreen(this.context_.appWindow)) {
+ util.toggleFullScreen(this.context_.appWindow,
+ false); // Leave the full screen mode.
+ }
+ this.context_.onBack(this.getSelectedEntries());
+};
+
+/**
+ * Handles user's 'Back' action (Escape or a click on the X icon).
+ * @private
+ */
+Gallery.prototype.onBack_ = function() {
+ this.executeWhenReady(this.back_.bind(this));
+};
+
+/**
+ * Handles user's 'Close' action.
+ * @private
+ */
+Gallery.prototype.onClose_ = function() {
+ this.executeWhenReady(this.context_.onClose);
+};
+
+/**
+ * Handles user's 'Maximize' action (Escape or a click on the X icon).
+ * @private
+ */
+Gallery.prototype.onMaximize_ = function() {
+ this.executeWhenReady(this.context_.onMaximize);
+};
+
+/**
+ * Executes a function when the editor is done with the modifications.
+ * @param {function} callback Function to execute.
+ */
+Gallery.prototype.executeWhenReady = function(callback) {
+ this.currentMode_.executeWhenReady(callback);
+};
+
+/**
+ * @return {Object} File browser private API.
+ */
+Gallery.getFileBrowserPrivate = function() {
+ return chrome.fileBrowserPrivate || window.top.chrome.fileBrowserPrivate;
+};
+
+/**
+ * @return {boolean} True if some tool is currently active.
+ */
+Gallery.prototype.hasActiveTool = function() {
+ return this.currentMode_.hasActiveTool() ||
+ this.isSharing_() || this.isRenaming_();
+};
+
+/**
+* External user action event handler.
+* @private
+*/
+Gallery.prototype.onUserAction_ = function() {
+ this.closeShareMenu_();
+ // Show the toolbar and hide it after the default timeout.
+ this.inactivityWatcher_.kick();
+};
+
+/**
+ * Sets the current mode, update the UI.
+ * @param {Object} mode Current mode.
+ * @private
+ */
+Gallery.prototype.setCurrentMode_ = function(mode) {
+ if (mode !== this.slideMode_ && mode !== this.mosaicMode_)
+ console.error('Invalid Gallery mode');
+
+ this.currentMode_ = mode;
+ this.container_.setAttribute('mode', this.currentMode_.getName());
+ this.updateSelectionAndState_();
+ this.updateButtons_();
+};
+
+/**
+ * Mode toggle event handler.
+ * @param {function=} opt_callback Callback.
+ * @param {Event=} opt_event Event that caused this call.
+ * @private
+ */
+Gallery.prototype.toggleMode_ = function(opt_callback, opt_event) {
+ if (!this.modeButton_)
+ return;
+
+ if (this.changingMode_) // Do not re-enter while changing the mode.
+ return;
+
+ if (opt_event)
+ this.onUserAction_();
+
+ this.changingMode_ = true;
+
+ var onModeChanged = function() {
+ this.changingMode_ = false;
+ if (opt_callback) opt_callback();
+ }.bind(this);
+
+ var tileIndex = Math.max(0, this.selectionModel_.selectedIndex);
+
+ var mosaic = this.mosaicMode_.getMosaic();
+ var tileRect = mosaic.getTileRect(tileIndex);
+
+ if (this.currentMode_ === this.slideMode_) {
+ this.setCurrentMode_(this.mosaicMode_);
+ mosaic.transform(
+ tileRect, this.slideMode_.getSelectedImageRect(), true /* instant */);
+ this.slideMode_.leave(tileRect,
+ function() {
+ // Animate back to normal position.
+ mosaic.transform();
+ mosaic.show();
+ onModeChanged();
+ }.bind(this));
+ } else {
+ this.setCurrentMode_(this.slideMode_);
+ this.slideMode_.enter(tileRect,
+ function() {
+ // Animate to zoomed position.
+ mosaic.transform(tileRect, this.slideMode_.getSelectedImageRect());
+ mosaic.hide();
+ }.bind(this),
+ onModeChanged);
+ }
+};
+
+/**
+ * Deletes the selected items.
+ * @private
+ */
+Gallery.prototype.delete_ = function() {
+ this.onUserAction_();
+
+ // Clone the sorted selected indexes array.
+ var indexesToRemove = this.selectionModel_.selectedIndexes.slice();
+ if (!indexesToRemove.length)
+ return;
+
+ /* TODO(dgozman): Implement Undo delete, Remove the confirmation dialog. */
+
+ var itemsToRemove = this.getSelectedItems();
+ var plural = itemsToRemove.length > 1;
+ var param = plural ? itemsToRemove.length : itemsToRemove[0].getFileName();
+
+ function deleteNext() {
+ if (!itemsToRemove.length)
+ return; // All deleted.
+
+ // TODO(hirono): Use fileOperationManager.
+ var entry = itemsToRemove.pop().getEntry();
+ entry.remove(deleteNext, function() {
+ util.flog('Error deleting: ' + entry.fullPath, deleteNext);
+ });
+ }
+
+ // Prevent the Gallery from handling Esc and Enter.
+ this.document_.body.removeEventListener('keydown', this.keyDownBound_);
+ var restoreListener = function() {
+ this.document_.body.addEventListener('keydown', this.keyDownBound_);
+ }.bind(this);
+
+ cr.ui.dialogs.BaseDialog.OK_LABEL = this.displayStringFunction_(
+ 'GALLERY_OK_LABEL');
+ cr.ui.dialogs.BaseDialog.CANCEL_LABEL =
+ this.displayStringFunction_('GALLERY_CANCEL_LABEL');
+ var confirm = new cr.ui.dialogs.ConfirmDialog(this.container_);
+ confirm.show(
+ this.displayStringFunction_(plural ? 'GALLERY_CONFIRM_DELETE_SOME' :
+ 'GALLERY_CONFIRM_DELETE_ONE', param),
+ function() {
+ restoreListener();
+ this.selectionModel_.unselectAll();
+ this.selectionModel_.leadIndex = -1;
+ // Remove items from the data model, starting from the highest index.
+ while (indexesToRemove.length)
+ this.dataModel_.splice(indexesToRemove.pop(), 1);
+ // Delete actual files.
+ deleteNext();
+ }.bind(this),
+ function() {
+ // Restore the listener after a timeout so that ESC is processed.
+ setTimeout(restoreListener, 0);
+ });
+};
+
+/**
+ * @return {Array.<Gallery.Item>} Current selection.
+ */
+Gallery.prototype.getSelectedItems = function() {
+ return this.selectionModel_.selectedIndexes.map(
+ this.dataModel_.item.bind(this.dataModel_));
+};
+
+/**
+ * @return {Array.<Entry>} Array of currently selected entries.
+ */
+Gallery.prototype.getSelectedEntries = function() {
+ return this.selectionModel_.selectedIndexes.map(function(index) {
+ return this.dataModel_.item(index).getEntry();
+ }.bind(this));
+};
+
+/**
+ * @return {Gallery.Item} Current single selection.
+ */
+Gallery.prototype.getSingleSelectedItem = function() {
+ var items = this.getSelectedItems();
+ if (items.length > 1)
+ throw new Error('Unexpected multiple selection');
+ return items[0];
+};
+
+/**
+ * Selection change event handler.
+ * @private
+ */
+Gallery.prototype.onSelection_ = function() {
+ this.updateSelectionAndState_();
+ this.updateShareMenu_();
+};
+
+/**
+ * Data model splice event handler.
+ * @private
+ */
+Gallery.prototype.onSplice_ = function() {
+ this.selectionModel_.adjustLength(this.dataModel_.length);
+};
+
+/**
+ * Content change event handler.
+ * @param {Event} event Event.
+ * @private
+*/
+Gallery.prototype.onContentChange_ = function(event) {
+ var index = this.dataModel_.indexOf(event.item);
+ if (index !== this.selectionModel_.selectedIndex)
+ console.error('Content changed for unselected item');
+ this.updateSelectionAndState_();
+};
+
+/**
+ * Keydown handler.
+ *
+ * @param {Event} event Event.
+ * @private
+ */
+Gallery.prototype.onKeyDown_ = function(event) {
+ var wasSharing = this.isSharing_();
+ this.closeShareMenu_();
+
+ if (this.currentMode_.onKeyDown(event))
+ return;
+
+ switch (util.getKeyModifiers(event) + event.keyIdentifier) {
+ case 'U+0008': // Backspace.
+ // The default handler would call history.back and close the Gallery.
+ event.preventDefault();
+ break;
+
+ case 'U+001B': // Escape
+ // Swallow Esc if it closed the Share menu, otherwise close the Gallery.
+ if (!wasSharing)
+ this.onBack_();
+ break;
+
+ case 'U+004D': // 'm' switches between Slide and Mosaic mode.
+ this.toggleMode_(null, event);
+ break;
+
+ case 'U+0056': // 'v'
+ this.slideMode_.startSlideshow(SlideMode.SLIDESHOW_INTERVAL_FIRST, event);
+ break;
+
+ case 'U+007F': // Delete
+ case 'Shift-U+0033': // Shift+'3' (Delete key might be missing).
+ this.delete_();
+ break;
+ }
+};
+
+// Name box and rename support.
+
+/**
+ * Updates the UI related to the selected item and the persistent state.
+ *
+ * @private
+ */
+Gallery.prototype.updateSelectionAndState_ = function() {
+ var path;
+ var displayName = '';
+
+ var selectedItems = this.getSelectedItems();
+ if (selectedItems.length === 1) {
+ var item = selectedItems[0];
+ var entry = item.getEntry();
+ window.top.document.title = entry.name;
+ displayName = ImageUtil.getDisplayNameFromName(entry.name);
+ } else if (selectedItems.length > 1 && this.context_.curDirEntry) {
+ // If the Gallery was opened on search results the search query will not be
+ // recorded in the app state and the relaunch will just open the gallery
+ // in the curDirEntry directory.
+ path = this.context_.curDirEntry.fullPath;
+ window.top.document.title = this.context_.curDirEntry.name;
+ displayName =
+ this.displayStringFunction_('GALLERY_ITEMS_SELECTED',
+ selectedItems.length);
+ }
+
+ window.top.util.updateAppState(path,
+ {gallery: (this.currentMode_ === this.mosaicMode_ ? 'mosaic' : 'slide')});
+
+ // We can't rename files in readonly directory.
+ // We can only rename a single file.
+ this.filenameEdit_.disabled = selectedItems.length !== 1 ||
+ this.context_.readonlyDirName;
+
+ this.filenameEdit_.value = displayName;
+
+ // Resolve real filesystem path of the current file.
+ if (this.selectionModel_.selectedIndexes.length) {
+ var selectedIndex = this.selectionModel_.selectedIndex;
+ var selectedItem =
+ this.dataModel_.item(this.selectionModel_.selectedIndex);
+ this.selectedEntry_ = selectedItem.getEntry();
+ }
+};
+
+/**
+ * Click event handler on filename edit box
+ * @private
+ */
+Gallery.prototype.onFilenameFocus_ = function() {
+ ImageUtil.setAttribute(this.filenameSpacer_, 'renaming', true);
+ this.filenameEdit_.originalValue = this.filenameEdit_.value;
+ setTimeout(this.filenameEdit_.select.bind(this.filenameEdit_), 0);
+ this.onUserAction_();
+};
+
+/**
+ * Blur event handler on filename edit box.
+ *
+ * @param {Event} event Blur event.
+ * @return {boolean} if default action should be prevented.
+ * @private
+ */
+Gallery.prototype.onFilenameEditBlur_ = function(event) {
+ if (this.filenameEdit_.value && this.filenameEdit_.value[0] === '.') {
+ this.prompt_.show('GALLERY_FILE_HIDDEN_NAME', 5000);
+ this.filenameEdit_.focus();
+ event.stopPropagation();
+ event.preventDefault();
+ return false;
+ }
+
+ var item = this.getSingleSelectedItem();
+ var oldEntry = item.getEntry();
+
+ var onFileExists = function() {
+ this.prompt_.show('GALLERY_FILE_EXISTS', 3000);
+ this.filenameEdit_.value = name;
+ this.filenameEdit_.focus();
+ }.bind(this);
+
+ var onSuccess = function() {
+ var event = new Event('content');
+ event.item = item;
+ event.oldEntry = oldEntry;
+ event.metadata = null; // Metadata unchanged.
+ this.dataModel_.dispatchEvent(event);
+ }.bind(this);
+
+ if (this.filenameEdit_.value) {
+ this.getSingleSelectedItem().rename(
+ this.filenameEdit_.value, onSuccess, onFileExists);
+ }
+
+ ImageUtil.setAttribute(this.filenameSpacer_, 'renaming', false);
+ this.onUserAction_();
+};
+
+/**
+ * Keydown event handler on filename edit box
+ * @private
+ */
+Gallery.prototype.onFilenameEditKeydown_ = function() {
+ switch (event.keyCode) {
+ case 27: // Escape
+ this.filenameEdit_.value = this.filenameEdit_.originalValue;
+ this.filenameEdit_.blur();
+ break;
+
+ case 13: // Enter
+ this.filenameEdit_.blur();
+ break;
+ }
+ event.stopPropagation();
+};
+
+/**
+ * @return {boolean} True if file renaming is currently in progress.
+ * @private
+ */
+Gallery.prototype.isRenaming_ = function() {
+ return this.filenameSpacer_.hasAttribute('renaming');
+};
+
+/**
+ * Content area click handler.
+ * @private
+ */
+Gallery.prototype.onContentClick_ = function() {
+ this.closeShareMenu_();
+ this.filenameEdit_.blur();
+};
+
+// Share button support.
+
+/**
+ * @return {boolean} True if the Share menu is active.
+ * @private
+ */
+Gallery.prototype.isSharing_ = function() {
+ return !this.shareMenu_.hidden;
+};
+
+/**
+ * Close Share menu if it is open.
+ * @private
+ */
+Gallery.prototype.closeShareMenu_ = function() {
+ if (this.isSharing_())
+ this.toggleShare_();
+};
+
+/**
+ * Share button handler.
+ * @private
+ */
+Gallery.prototype.toggleShare_ = function() {
+ if (!this.shareButton_.hasAttribute('disabled'))
+ this.shareMenu_.hidden = !this.shareMenu_.hidden;
+ this.inactivityWatcher_.check();
+};
+
+/**
+ * Updates available actions list based on the currently selected urls.
+ * @private.
+ */
+Gallery.prototype.updateShareMenu_ = function() {
+ var entries = this.getSelectedEntries();
+
+ function isShareAction(task) {
+ var taskParts = task.taskId.split('|');
+ return taskParts[0] !== chrome.runtime.id;
+ }
+
+ var api = Gallery.getFileBrowserPrivate();
+ var mimeTypes = []; // TODO(kaznacheev) Collect mime types properly.
+
+ var createShareMenu = function(tasks) {
+ var wasHidden = this.shareMenu_.hidden;
+ this.shareMenu_.hidden = true;
+ var items = this.shareMenu_.querySelectorAll('.item');
+ for (var i = 0; i !== items.length; i++) {
+ items[i].parentNode.removeChild(items[i]);
+ }
+
+ for (var t = 0; t !== tasks.length; t++) {
+ var task = tasks[t];
+ if (!isShareAction(task)) continue;
+
+ var item = util.createChild(this.shareMenu_, 'item');
+ item.textContent = task.title;
+ item.style.backgroundImage = 'url(' + task.iconUrl + ')';
+ item.addEventListener('click', function(taskId) {
+ this.toggleShare_(); // Hide the menu.
+ this.executeWhenReady(api.executeTask.bind(api, taskId, entries));
+ }.bind(this, task.taskId));
+ }
+
+ var empty = this.shareMenu_.querySelector('.item') === null;
+ ImageUtil.setAttribute(this.shareButton_, 'disabled', empty);
+ this.shareMenu_.hidden = wasHidden || empty;
+ }.bind(this);
+
+ // Create or update the share menu with a list of sharing tasks and show
+ // or hide the share button.
+ // TODO(mtomasz): Pass Entries directly, instead of URLs.
+ if (!entries.length)
+ createShareMenu([]); // Empty list of tasks, since there is no selection.
+ else
+ api.getFileTasks(util.entriesToURLs(entries), mimeTypes, createShareMenu);
+};
+
+/**
+ * Updates thumbnails.
+ * @private
+ */
+Gallery.prototype.updateThumbnails_ = function() {
+ if (this.currentMode_ === this.slideMode_)
+ this.slideMode_.updateThumbnails();
+
+ if (this.mosaicMode_) {
+ var mosaic = this.mosaicMode_.getMosaic();
+ if (mosaic.isInitialized())
+ mosaic.reload();
+ }
+};
+
+/**
+ * Updates buttons.
+ * @private
+ */
+Gallery.prototype.updateButtons_ = function() {
+ if (this.modeButton_) {
+ var oppositeMode =
+ this.currentMode_ === this.slideMode_ ? this.mosaicMode_ :
+ this.slideMode_;
+ this.modeButton_.title =
+ this.displayStringFunction_(oppositeMode.getTitle());
+ }
+};
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/photo/gallery_item.js b/chromium/chrome/browser/resources/file_manager/foreground/js/photo/gallery_item.js
new file mode 100644
index 00000000000..2ae80c92dd7
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/photo/gallery_item.js
@@ -0,0 +1,227 @@
+// 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.
+
+'use strict';
+
+/**
+ * Object representing an image item (a photo or a video).
+ *
+ * @param {FileEntry} entry Image entry.
+ * @constructor
+ */
+Gallery.Item = function(entry) {
+ this.entry_ = entry;
+ this.original_ = true;
+};
+
+/**
+ * @return {FileEntry} Image entry.
+ */
+Gallery.Item.prototype.getEntry = function() { return this.entry_ };
+
+/**
+ * @return {string} File name.
+ */
+Gallery.Item.prototype.getFileName = function() {
+ return this.entry_.name;
+};
+
+/**
+ * @return {boolean} True if this image has not been created in this session.
+ */
+Gallery.Item.prototype.isOriginal = function() { return this.original_ };
+
+// TODO: Localize?
+/**
+ * @type {string} Suffix for a edited copy file name.
+ */
+Gallery.Item.COPY_SIGNATURE = ' - Edited';
+
+/**
+ * Regular expression to match '... - Edited'.
+ * @type {RegExp}
+ */
+Gallery.Item.REGEXP_COPY_0 =
+ new RegExp('^(.+)' + Gallery.Item.COPY_SIGNATURE + '$');
+
+/**
+ * Regular expression to match '... - Edited (N)'.
+ * @type {RegExp}
+ */
+Gallery.Item.REGEXP_COPY_N =
+ new RegExp('^(.+)' + Gallery.Item.COPY_SIGNATURE + ' \\((\\d+)\\)$');
+
+/**
+ * Creates a name for an edited copy of the file.
+ *
+ * @param {Entry} dirEntry Entry.
+ * @param {function} callback Callback.
+ * @private
+ */
+Gallery.Item.prototype.createCopyName_ = function(dirEntry, callback) {
+ var name = this.getFileName();
+
+ // If the item represents a file created during the current Gallery session
+ // we reuse it for subsequent saves instead of creating multiple copies.
+ if (!this.original_) {
+ callback(name);
+ return;
+ }
+
+ var ext = '';
+ var index = name.lastIndexOf('.');
+ if (index != -1) {
+ ext = name.substr(index);
+ name = name.substr(0, index);
+ }
+
+ if (!ext.match(/jpe?g/i)) {
+ // Chrome can natively encode only two formats: JPEG and PNG.
+ // All non-JPEG images are saved in PNG, hence forcing the file extension.
+ ext = '.png';
+ }
+
+ function tryNext(tries) {
+ // All the names are used. Let's overwrite the last one.
+ if (tries == 0) {
+ setTimeout(callback, 0, name + ext);
+ return;
+ }
+
+ // If the file name contains the copy signature add/advance the sequential
+ // number.
+ var matchN = Gallery.Item.REGEXP_COPY_N.exec(name);
+ var match0 = Gallery.Item.REGEXP_COPY_0.exec(name);
+ if (matchN && matchN[1] && matchN[2]) {
+ var copyNumber = parseInt(matchN[2], 10) + 1;
+ name = matchN[1] + Gallery.Item.COPY_SIGNATURE + ' (' + copyNumber + ')';
+ } else if (match0 && match0[1]) {
+ name = match0[1] + Gallery.Item.COPY_SIGNATURE + ' (1)';
+ } else {
+ name += Gallery.Item.COPY_SIGNATURE;
+ }
+
+ dirEntry.getFile(name + ext, {create: false, exclusive: false},
+ tryNext.bind(null, tries - 1),
+ callback.bind(null, name + ext));
+ }
+
+ tryNext(10);
+};
+
+/**
+ * Writes the new item content to the file.
+ *
+ * @param {Entry} overrideDir Directory to save to. If null, save to the same
+ * directory as the original.
+ * @param {boolean} overwrite True if overwrite, false if copy.
+ * @param {HTMLCanvasElement} canvas Source canvas.
+ * @param {ImageEncoder.MetadataEncoder} metadataEncoder MetadataEncoder.
+ * @param {function(boolean)=} opt_callback Callback accepting true for success.
+ */
+Gallery.Item.prototype.saveToFile = function(
+ overrideDir, overwrite, canvas, metadataEncoder, opt_callback) {
+ ImageUtil.metrics.startInterval(ImageUtil.getMetricName('SaveTime'));
+
+ var name = this.getFileName();
+
+ var onSuccess = function(entry) {
+ ImageUtil.metrics.recordEnum(ImageUtil.getMetricName('SaveResult'), 1, 2);
+ ImageUtil.metrics.recordInterval(ImageUtil.getMetricName('SaveTime'));
+ this.entry_ = entry;
+ if (opt_callback) opt_callback(true);
+ }.bind(this);
+
+ function onError(error) {
+ console.error('Error saving from gallery', name, error);
+ ImageUtil.metrics.recordEnum(ImageUtil.getMetricName('SaveResult'), 0, 2);
+ if (opt_callback) opt_callback(false);
+ }
+
+ function doSave(newFile, fileEntry) {
+ fileEntry.createWriter(function(fileWriter) {
+ function writeContent() {
+ fileWriter.onwriteend = onSuccess.bind(null, fileEntry);
+ fileWriter.write(ImageEncoder.getBlob(canvas, metadataEncoder));
+ }
+ fileWriter.onerror = function(error) {
+ onError(error);
+ // Disable all callbacks on the first error.
+ fileWriter.onerror = null;
+ fileWriter.onwriteend = null;
+ };
+ if (newFile) {
+ writeContent();
+ } else {
+ fileWriter.onwriteend = writeContent;
+ fileWriter.truncate(0);
+ }
+ }, onError);
+ }
+
+ function getFile(dir, newFile) {
+ dir.getFile(name, {create: newFile, exclusive: newFile},
+ doSave.bind(null, newFile), onError);
+ }
+
+ function checkExistence(dir) {
+ dir.getFile(name, {create: false, exclusive: false},
+ getFile.bind(null, dir, false /* existing file */),
+ getFile.bind(null, dir, true /* create new file */));
+ }
+
+ var saveToDir = function(dir) {
+ if (overwrite) {
+ checkExistence(dir);
+ } else {
+ this.createCopyName_(dir, function(copyName) {
+ this.original_ = false;
+ name = copyName;
+ checkExistence(dir);
+ }.bind(this));
+ }
+ }.bind(this);
+
+ if (overrideDir) {
+ saveToDir(overrideDir);
+ } else {
+ this.entry_.getParent(saveToDir, onError);
+ }
+};
+
+/**
+ * Renames the file.
+ *
+ * @param {string} displayName New display name (without the extension).
+ * @param {function()} onSuccess Success callback.
+ * @param {function()} onExists Called if the file with the new name exists.
+ */
+Gallery.Item.prototype.rename = function(displayName, onSuccess, onExists) {
+ var fileName = this.entry_.name.replace(
+ ImageUtil.getDisplayNameFromName(this.entry_.name), displayName);
+
+ if (name === this.entry_.name)
+ return;
+
+ var onRenamed = function(entry) {
+ this.entry_ = entry;
+ onSuccess();
+ }.bind(this);
+
+ var onError = function() {
+ console.error('Rename error: "' + oldName + '" to "' + newName + '"');
+ };
+
+ var moveIfDoesNotExist = function(parentDir) {
+ parentDir.getFile(
+ fileName,
+ {create: false, exclusive: false},
+ onExists,
+ function() {
+ this.entry_.moveTo(parentDir, fileName, onRenamed, onError);
+ }.bind(this));
+ }.bind(this);
+
+ this.entry_.getParent(moveIfDoesNotExist, onError);
+};
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/photo/gallery_scripts.js b/chromium/chrome/browser/resources/file_manager/foreground/js/photo/gallery_scripts.js
new file mode 100644
index 00000000000..336fa207938
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/photo/gallery_scripts.js
@@ -0,0 +1,70 @@
+// 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.
+
+// The include directives are put into Javascript-style comments to prevent
+// parsing errors in non-flattened mode. The flattener still sees them.
+// Note that this makes the flattener to comment out the first line of the
+// included file but that's all right since any javascript file should start
+// with a copyright comment anyway.
+
+//<include src="../metrics.js">
+
+//<include src="../../../../image_loader/image_loader_client.js"/>
+
+//<include src="../../../../../../../ui/webui/resources/js/cr.js">
+//<include src="../../../../../../../ui/webui/resources/js/event_tracker.js">
+//<include src="../../../../../../../ui/webui/resources/js/load_time_data.js">
+
+//<include src="../../../../../../../ui/webui/resources/js/cr/ui.js">
+//<include src="../../../../../../../ui/webui/resources/js/cr/event_target.js">
+//<include src="../../../../../../../ui/webui/resources/js/cr/ui/touch_handler.js">
+//<include src="../../../../../../../ui/webui/resources/js/cr/ui/array_data_model.js">
+//<include src="../../../../../../../ui/webui/resources/js/cr/ui/dialogs.js">
+//<include src="../../../../../../../ui/webui/resources/js/cr/ui/list_item.js">
+//<include src="../../../../../../../ui/webui/resources/js/cr/ui/list_selection_model.js">
+//<include src="../../../../../../../ui/webui/resources/js/cr/ui/list_single_selection_model.js">
+//<include src="../../../../../../../ui/webui/resources/js/cr/ui/list_selection_controller.js">
+//<include src="../../../../../../../ui/webui/resources/js/cr/ui/list.js">
+//<include src="../../../../../../../ui/webui/resources/js/cr/ui/grid.js">
+
+(function() {
+// 'strict mode' is invoked for this scope.
+
+//<include src="../../../common/js/async_util.js">
+//<include src="../../../common/js/util.js">
+//<include src="../../../common/js/path_util.js">
+//<include src="../file_type.js">
+//<include src="../volume_manager_wrapper.js">
+
+//<include src="../image_editor/image_util.js"/>
+//<include src="../image_editor/viewport.js"/>
+//<include src="../image_editor/image_buffer.js"/>
+//<include src="../image_editor/image_view.js"/>
+//<include src="../image_editor/commands.js"/>
+//<include src="../image_editor/image_editor.js"/>
+//<include src="../image_editor/image_transform.js"/>
+//<include src="../image_editor/image_adjust.js"/>
+//<include src="../image_editor/filter.js"/>
+//<include src="../image_editor/image_encoder.js"/>
+//<include src="../image_editor/exif_encoder.js"/>
+
+//<include src="../media/media_controls.js"/>
+//<include src="../media/media_util.js"/>
+//<include src="../media/util.js"/>
+
+//<include src="../metadata/metadata_cache.js"/>
+
+//<include src="gallery.js">
+//<include src="gallery_item.js">
+//<include src="mosaic_mode.js">
+//<include src="slide_mode.js">
+//<include src="ribbon.js">
+
+// Exports
+window.ImageUtil = ImageUtil;
+window.Gallery = Gallery;
+window.beforeunload = beforeunload;
+window.unload = unload;
+
+})();
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/photo/mosaic_mode.js b/chromium/chrome/browser/resources/file_manager/foreground/js/photo/mosaic_mode.js
new file mode 100644
index 00000000000..6231864bbd8
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/photo/mosaic_mode.js
@@ -0,0 +1,2012 @@
+// 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.
+
+'use strict';
+
+/**
+ * @param {Element} container Content container.
+ * @param {cr.ui.ArrayDataModel} dataModel Data model.
+ * @param {cr.ui.ListSelectionModel} selectionModel Selection model.
+ * @param {MetadataCache} metadataCache Metadata cache.
+ * @param {function} toggleMode Function to switch to the Slide mode.
+ * @constructor
+ */
+function MosaicMode(
+ container, dataModel, selectionModel, metadataCache, toggleMode) {
+ this.mosaic_ = new Mosaic(
+ container.ownerDocument, dataModel, selectionModel, metadataCache);
+ container.appendChild(this.mosaic_);
+
+ this.toggleMode_ = toggleMode;
+ this.mosaic_.addEventListener('dblclick', this.toggleMode_);
+ this.showingTimeoutID_ = null;
+}
+
+/**
+ * @return {Mosaic} The mosaic control.
+ */
+MosaicMode.prototype.getMosaic = function() { return this.mosaic_ };
+
+/**
+ * @return {string} Mode name.
+ */
+MosaicMode.prototype.getName = function() { return 'mosaic' };
+
+/**
+ * @return {string} Mode title.
+ */
+MosaicMode.prototype.getTitle = function() { return 'GALLERY_MOSAIC' };
+
+/**
+ * Execute an action (this mode has no busy state).
+ * @param {function} action Action to execute.
+ */
+MosaicMode.prototype.executeWhenReady = function(action) { action() };
+
+/**
+ * @return {boolean} Always true (no toolbar fading in this mode).
+ */
+MosaicMode.prototype.hasActiveTool = function() { return true };
+
+/**
+ * Keydown handler.
+ *
+ * @param {Event} event Event.
+ * @return {boolean} True if processed.
+ */
+MosaicMode.prototype.onKeyDown = function(event) {
+ switch (util.getKeyModifiers(event) + event.keyIdentifier) {
+ case 'Enter':
+ this.toggleMode_();
+ return true;
+ }
+ return this.mosaic_.onKeyDown(event);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+/**
+ * Mosaic control.
+ *
+ * @param {Document} document Document.
+ * @param {cr.ui.ArrayDataModel} dataModel Data model.
+ * @param {cr.ui.ListSelectionModel} selectionModel Selection model.
+ * @param {MetadataCache} metadataCache Metadata cache.
+ * @return {Element} Mosaic element.
+ * @constructor
+ */
+function Mosaic(document, dataModel, selectionModel, metadataCache) {
+ var self = document.createElement('div');
+ Mosaic.decorate(self, dataModel, selectionModel, metadataCache);
+ return self;
+}
+
+/**
+ * Inherit from HTMLDivElement.
+ */
+Mosaic.prototype.__proto__ = HTMLDivElement.prototype;
+
+/**
+ * Default layout delay in ms.
+ * @const
+ * @type {number}
+ */
+Mosaic.LAYOUT_DELAY = 200;
+
+/**
+ * Smooth scroll animation duration when scrolling using keyboard or
+ * clicking on a partly visible tile. In ms.
+ * @const
+ * @type {number}
+ */
+Mosaic.ANIMATED_SCROLL_DURATION = 500;
+
+/**
+ * Decorate a Mosaic instance.
+ *
+ * @param {Mosaic} self Self pointer.
+ * @param {cr.ui.ArrayDataModel} dataModel Data model.
+ * @param {cr.ui.ListSelectionModel} selectionModel Selection model.
+ * @param {MetadataCache} metadataCache Metadata cache.
+ */
+Mosaic.decorate = function(self, dataModel, selectionModel, metadataCache) {
+ self.__proto__ = Mosaic.prototype;
+ self.className = 'mosaic';
+
+ self.dataModel_ = dataModel;
+ self.selectionModel_ = selectionModel;
+ self.metadataCache_ = metadataCache;
+
+ // Initialization is completed lazily on the first call to |init|.
+};
+
+/**
+ * Initialize the mosaic element.
+ */
+Mosaic.prototype.init = function() {
+ if (this.tiles_)
+ return; // Already initialized, nothing to do.
+
+ this.layoutModel_ = new Mosaic.Layout();
+ this.onResize_();
+
+ this.selectionController_ =
+ new Mosaic.SelectionController(this.selectionModel_, this.layoutModel_);
+
+ this.tiles_ = [];
+ for (var i = 0; i != this.dataModel_.length; i++)
+ this.tiles_.push(new Mosaic.Tile(this, this.dataModel_.item(i)));
+
+ this.selectionModel_.selectedIndexes.forEach(function(index) {
+ this.tiles_[index].select(true);
+ }.bind(this));
+
+ this.initTiles_(this.tiles_);
+
+ // The listeners might be called while some tiles are still loading.
+ this.initListeners_();
+};
+
+/**
+ * @return {boolean} Whether mosaic is initialized.
+ */
+Mosaic.prototype.isInitialized = function() {
+ return !!this.tiles_;
+};
+
+/**
+ * Start listening to events.
+ *
+ * We keep listening to events even when the mosaic is hidden in order to
+ * keep the layout up to date.
+ *
+ * @private
+ */
+Mosaic.prototype.initListeners_ = function() {
+ this.ownerDocument.defaultView.addEventListener(
+ 'resize', this.onResize_.bind(this));
+
+ var mouseEventBound = this.onMouseEvent_.bind(this);
+ this.addEventListener('mousemove', mouseEventBound);
+ this.addEventListener('mousedown', mouseEventBound);
+ this.addEventListener('mouseup', mouseEventBound);
+ this.addEventListener('scroll', this.onScroll_.bind(this));
+
+ this.selectionModel_.addEventListener('change', this.onSelection_.bind(this));
+ this.selectionModel_.addEventListener('leadIndexChange',
+ this.onLeadChange_.bind(this));
+
+ this.dataModel_.addEventListener('splice', this.onSplice_.bind(this));
+ this.dataModel_.addEventListener('content', this.onContentChange_.bind(this));
+};
+
+/**
+ * Smoothly scrolls the container to the specified position using
+ * f(x) = sqrt(x) speed function normalized to animation duration.
+ * @param {number} targetPosition Horizontal scroll position in pixels.
+ */
+Mosaic.prototype.animatedScrollTo = function(targetPosition) {
+ if (this.scrollAnimation_) {
+ webkitCancelAnimationFrame(this.scrollAnimation_);
+ this.scrollAnimation_ = null;
+ }
+
+ // Mouse move events are fired without touching the mouse because of scrolling
+ // the container. Therefore, these events have to be suppressed.
+ this.suppressHovering_ = true;
+
+ // Calculates integral area from t1 to t2 of f(x) = sqrt(x) dx.
+ var integral = function(t1, t2) {
+ return 2.0 / 3.0 * Math.pow(t2, 3.0 / 2.0) -
+ 2.0 / 3.0 * Math.pow(t1, 3.0 / 2.0);
+ };
+
+ var delta = targetPosition - this.scrollLeft;
+ var factor = delta / integral(0, Mosaic.ANIMATED_SCROLL_DURATION);
+ var startTime = Date.now();
+ var lastPosition = 0;
+ var scrollOffset = this.scrollLeft;
+
+ var animationFrame = function() {
+ var position = Date.now() - startTime;
+ var step = factor *
+ integral(Math.max(0, Mosaic.ANIMATED_SCROLL_DURATION - position),
+ Math.max(0, Mosaic.ANIMATED_SCROLL_DURATION - lastPosition));
+ scrollOffset += step;
+
+ var oldScrollLeft = this.scrollLeft;
+ var newScrollLeft = Math.round(scrollOffset);
+
+ if (oldScrollLeft != newScrollLeft)
+ this.scrollLeft = newScrollLeft;
+
+ if (step == 0 || this.scrollLeft != newScrollLeft) {
+ this.scrollAnimation_ = null;
+ // Release the hovering lock after a safe delay to avoid hovering
+ // a tile because of altering |this.scrollLeft|.
+ setTimeout(function() {
+ if (!this.scrollAnimation_)
+ this.suppressHovering_ = false;
+ }.bind(this), 100);
+ } else {
+ // Continue the animation.
+ this.scrollAnimation_ = requestAnimationFrame(animationFrame);
+ }
+
+ lastPosition = position;
+ }.bind(this);
+
+ // Start the animation.
+ this.scrollAnimation_ = requestAnimationFrame(animationFrame);
+};
+
+/**
+ * @return {Mosaic.Tile} Selected tile or undefined if no selection.
+ */
+Mosaic.prototype.getSelectedTile = function() {
+ return this.tiles_ && this.tiles_[this.selectionModel_.selectedIndex];
+};
+
+/**
+ * @param {number} index Tile index.
+ * @return {Rect} Tile's image rectangle.
+ */
+Mosaic.prototype.getTileRect = function(index) {
+ var tile = this.tiles_[index];
+ return tile && tile.getImageRect();
+};
+
+/**
+ * @param {number} index Tile index.
+ * Scroll the given tile into the viewport.
+ */
+Mosaic.prototype.scrollIntoView = function(index) {
+ var tile = this.tiles_[index];
+ if (tile) tile.scrollIntoView();
+};
+
+/**
+ * Initializes multiple tiles.
+ *
+ * @param {Array.<Mosaic.Tile>} tiles Array of tiles.
+ * @param {function()=} opt_callback Completion callback.
+ * @private
+ */
+Mosaic.prototype.initTiles_ = function(tiles, opt_callback) {
+ // We do not want to use tile indices in asynchronous operations because they
+ // do not survive data model splices. Copy tile references instead.
+ tiles = tiles.slice();
+
+ // Throttle the metadata access so that we do not overwhelm the file system.
+ var MAX_CHUNK_SIZE = 10;
+
+ var loadChunk = function() {
+ if (!tiles.length) {
+ if (opt_callback) opt_callback();
+ return;
+ }
+ var chunkSize = Math.min(tiles.length, MAX_CHUNK_SIZE);
+ var loaded = 0;
+ for (var i = 0; i != chunkSize; i++) {
+ this.initTile_(tiles.shift(), function() {
+ if (++loaded == chunkSize) {
+ this.layout();
+ loadChunk();
+ }
+ }.bind(this));
+ }
+ }.bind(this);
+
+ loadChunk();
+};
+
+/**
+ * Initializes a single tile.
+ *
+ * @param {Mosaic.Tile} tile Tile.
+ * @param {function()} callback Completion callback.
+ * @private
+ */
+Mosaic.prototype.initTile_ = function(tile, callback) {
+ var onImageMeasured = callback;
+ this.metadataCache_.get(tile.getItem().getEntry(), Gallery.METADATA_TYPE,
+ function(metadata) {
+ tile.init(metadata, onImageMeasured);
+ });
+};
+
+/**
+ * Reload all tiles.
+ */
+Mosaic.prototype.reload = function() {
+ this.layoutModel_.reset_();
+ this.tiles_.forEach(function(t) { t.markUnloaded() });
+ this.initTiles_(this.tiles_);
+};
+
+/**
+ * Layout the tiles in the order of their indices.
+ *
+ * Starts where it last stopped (at #0 the first time).
+ * Stops when all tiles are processed or when the next tile is still loading.
+ */
+Mosaic.prototype.layout = function() {
+ if (this.layoutTimer_) {
+ clearTimeout(this.layoutTimer_);
+ this.layoutTimer_ = null;
+ }
+ while (true) {
+ var index = this.layoutModel_.getTileCount();
+ if (index == this.tiles_.length)
+ break; // All tiles done.
+ var tile = this.tiles_[index];
+ if (!tile.isInitialized())
+ break; // Next layout will try to restart from here.
+ this.layoutModel_.add(tile, index + 1 == this.tiles_.length);
+ }
+ this.loadVisibleTiles_();
+};
+
+/**
+ * Schedule the layout.
+ *
+ * @param {number=} opt_delay Delay in ms.
+ */
+Mosaic.prototype.scheduleLayout = function(opt_delay) {
+ if (!this.layoutTimer_) {
+ this.layoutTimer_ = setTimeout(function() {
+ this.layoutTimer_ = null;
+ this.layout();
+ }.bind(this), opt_delay || 0);
+ }
+};
+
+/**
+ * Resize handler.
+ *
+ * @private
+ */
+Mosaic.prototype.onResize_ = function() {
+ this.layoutModel_.setViewportSize(this.clientWidth, this.clientHeight -
+ (Mosaic.Layout.PADDING_TOP + Mosaic.Layout.PADDING_BOTTOM));
+ this.scheduleLayout();
+};
+
+/**
+ * Mouse event handler.
+ *
+ * @param {Event} event Event.
+ * @private
+ */
+Mosaic.prototype.onMouseEvent_ = function(event) {
+ // Navigating with mouse, enable hover state.
+ if (!this.suppressHovering_)
+ this.classList.add('hover-visible');
+
+ if (event.type == 'mousemove')
+ return;
+
+ var index = -1;
+ for (var target = event.target;
+ target && (target != this);
+ target = target.parentNode) {
+ if (target.classList.contains('mosaic-tile')) {
+ index = this.dataModel_.indexOf(target.getItem());
+ break;
+ }
+ }
+ this.selectionController_.handlePointerDownUp(event, index);
+};
+
+/**
+ * Scroll handler.
+ * @private
+ */
+Mosaic.prototype.onScroll_ = function() {
+ requestAnimationFrame(function() {
+ this.loadVisibleTiles_();
+ }.bind(this));
+};
+
+/**
+ * Selection change handler.
+ *
+ * @param {Event} event Event.
+ * @private
+ */
+Mosaic.prototype.onSelection_ = function(event) {
+ for (var i = 0; i != event.changes.length; i++) {
+ var change = event.changes[i];
+ var tile = this.tiles_[change.index];
+ if (tile) tile.select(change.selected);
+ }
+};
+
+/**
+ * Lead item change handler.
+ *
+ * @param {Event} event Event.
+ * @private
+ */
+Mosaic.prototype.onLeadChange_ = function(event) {
+ var index = event.newValue;
+ if (index >= 0) {
+ var tile = this.tiles_[index];
+ if (tile) tile.scrollIntoView();
+ }
+};
+
+/**
+ * Splice event handler.
+ *
+ * @param {Event} event Event.
+ * @private
+ */
+Mosaic.prototype.onSplice_ = function(event) {
+ var index = event.index;
+ this.layoutModel_.invalidateFromTile_(index);
+
+ if (event.removed.length) {
+ for (var t = 0; t != event.removed.length; t++)
+ this.removeChild(this.tiles_[index + t]);
+
+ this.tiles_.splice(index, event.removed.length);
+ this.scheduleLayout(Mosaic.LAYOUT_DELAY);
+ }
+
+ if (event.added.length) {
+ var newTiles = [];
+ for (var t = 0; t != event.added.length; t++)
+ newTiles.push(new Mosaic.Tile(this, this.dataModel_.item(index + t)));
+
+ this.tiles_.splice.apply(this.tiles_, [index, 0].concat(newTiles));
+ this.initTiles_(newTiles);
+ }
+
+ if (this.tiles_.length != this.dataModel_.length)
+ console.error('Mosaic is out of sync');
+};
+
+/**
+ * Content change handler.
+ *
+ * @param {Event} event Event.
+ * @private
+ */
+Mosaic.prototype.onContentChange_ = function(event) {
+ if (!this.tiles_)
+ return;
+
+ if (!event.metadata)
+ return; // Thumbnail unchanged, nothing to do.
+
+ var index = this.dataModel_.indexOf(event.item);
+ if (index != this.selectionModel_.selectedIndex)
+ console.error('Content changed for unselected item');
+
+ this.layoutModel_.invalidateFromTile_(index);
+ this.tiles_[index].init(event.metadata, function() {
+ this.tiles_[index].unload();
+ this.tiles_[index].load(
+ Mosaic.Tile.LoadMode.HIGH_DPI,
+ this.scheduleLayout.bind(this, Mosaic.LAYOUT_DELAY));
+ }.bind(this));
+};
+
+/**
+ * Keydown event handler.
+ *
+ * @param {Event} event Event.
+ * @return {boolean} True if the event has been consumed.
+ */
+Mosaic.prototype.onKeyDown = function(event) {
+ this.selectionController_.handleKeyDown(event);
+ if (event.defaultPrevented) // Navigating with keyboard, hide hover state.
+ this.classList.remove('hover-visible');
+ return event.defaultPrevented;
+};
+
+/**
+ * @return {boolean} True if the mosaic zoom effect can be applied. It is
+ * too slow if there are to many images.
+ * TODO(kaznacheev): Consider unloading the images that are out of the viewport.
+ */
+Mosaic.prototype.canZoom = function() {
+ return this.tiles_.length < 100;
+};
+
+/**
+ * Show the mosaic.
+ */
+Mosaic.prototype.show = function() {
+ var duration = ImageView.MODE_TRANSITION_DURATION;
+ if (this.canZoom()) {
+ // Fade in in parallel with the zoom effect.
+ this.setAttribute('visible', 'zooming');
+ } else {
+ // Mosaic is not animating but the large image is. Fade in the mosaic
+ // shortly before the large image animation is done.
+ duration -= 100;
+ }
+ this.showingTimeoutID_ = setTimeout(function() {
+ this.showingTimeoutID_ = null;
+ // Make the selection visible.
+ // If the mosaic is not animated it will start fading in now.
+ this.setAttribute('visible', 'normal');
+ this.loadVisibleTiles_();
+ }.bind(this), duration);
+};
+
+/**
+ * Hide the mosaic.
+ */
+Mosaic.prototype.hide = function() {
+ if (this.showingTimeoutID_ != null) {
+ clearTimeout(this.showingTimeoutID_);
+ this.showingTimeoutID_ = null;
+ }
+ this.removeAttribute('visible');
+};
+
+/**
+ * Checks if the mosaic view is visible.
+ * @return {boolean} True if visible, false otherwise.
+ * @private
+ */
+Mosaic.prototype.isVisible_ = function() {
+ return this.hasAttribute('visible');
+};
+
+/**
+ * Loads visible tiles. Ignores consecutive calls. Does not reload already
+ * loaded images.
+ * @private
+ */
+Mosaic.prototype.loadVisibleTiles_ = function() {
+ if (this.loadVisibleTilesSuppressed_) {
+ this.loadVisibleTilesScheduled_ = true;
+ return;
+ }
+
+ this.loadVisibleTilesSuppressed_ = true;
+ this.loadVisibleTilesScheduled_ = false;
+ setTimeout(function() {
+ this.loadVisibleTilesSuppressed_ = false;
+ if (this.loadVisibleTilesScheduled_)
+ this.loadVisibleTiles_();
+ }.bind(this), 100);
+
+ // Tiles only in the viewport (visible).
+ var visibleRect = new Rect(0,
+ 0,
+ this.clientWidth,
+ this.clientHeight);
+
+ // Tiles in the viewport and also some distance on the left and right.
+ var renderableRect = new Rect(-this.clientWidth,
+ 0,
+ 3 * this.clientWidth,
+ this.clientHeight);
+
+ // Unload tiles out of scope.
+ for (var index = 0; index < this.tiles_.length; index++) {
+ var tile = this.tiles_[index];
+ var imageRect = tile.getImageRect();
+ // Unload a thumbnail.
+ if (imageRect && !imageRect.intersects(renderableRect))
+ tile.unload();
+ }
+
+ // Load the visible tiles first.
+ var allVisibleLoaded = true;
+ // Show high-dpi only when the mosaic view is visible.
+ var loadMode = this.isVisible_() ? Mosaic.Tile.LoadMode.HIGH_DPI :
+ Mosaic.Tile.LoadMode.LOW_DPI;
+ for (var index = 0; index < this.tiles_.length; index++) {
+ var tile = this.tiles_[index];
+ var imageRect = tile.getImageRect();
+ // Load a thumbnail.
+ if (!tile.isLoading(loadMode) && !tile.isLoaded(loadMode) && imageRect &&
+ imageRect.intersects(visibleRect)) {
+ tile.load(loadMode, function() {});
+ allVisibleLoaded = false;
+ }
+ }
+
+ // Load also another, nearby, if the visible has been already loaded.
+ if (allVisibleLoaded) {
+ for (var index = 0; index < this.tiles_.length; index++) {
+ var tile = this.tiles_[index];
+ var imageRect = tile.getImageRect();
+ // Load a thumbnail.
+ if (!tile.isLoading() && !tile.isLoaded() && imageRect &&
+ imageRect.intersects(renderableRect)) {
+ tile.load(Mosaic.Tile.LoadMode.LOW_DPI, function() {});
+ }
+ }
+ }
+};
+
+/**
+ * Apply or reset the zoom transform.
+ *
+ * @param {Rect} tileRect Tile rectangle. Reset the transform if null.
+ * @param {Rect} imageRect Large image rectangle. Reset the transform if null.
+ * @param {boolean=} opt_instant True of the transition should be instant.
+ */
+Mosaic.prototype.transform = function(tileRect, imageRect, opt_instant) {
+ if (opt_instant) {
+ this.style.webkitTransitionDuration = '0';
+ } else {
+ this.style.webkitTransitionDuration =
+ ImageView.MODE_TRANSITION_DURATION + 'ms';
+ }
+
+ if (this.canZoom() && tileRect && imageRect) {
+ var scaleX = imageRect.width / tileRect.width;
+ var scaleY = imageRect.height / tileRect.height;
+ var shiftX = (imageRect.left + imageRect.width / 2) -
+ (tileRect.left + tileRect.width / 2);
+ var shiftY = (imageRect.top + imageRect.height / 2) -
+ (tileRect.top + tileRect.height / 2);
+ this.style.webkitTransform =
+ 'translate(' + shiftX * scaleX + 'px, ' + shiftY * scaleY + 'px)' +
+ 'scaleX(' + scaleX + ') scaleY(' + scaleY + ')';
+ } else {
+ this.style.webkitTransform = '';
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+/**
+ * Creates a selection controller that is to be used with grid.
+ * @param {cr.ui.ListSelectionModel} selectionModel The selection model to
+ * interact with.
+ * @param {Mosaic.Layout} layoutModel The layout model to use.
+ * @constructor
+ * @extends {!cr.ui.ListSelectionController}
+ */
+Mosaic.SelectionController = function(selectionModel, layoutModel) {
+ cr.ui.ListSelectionController.call(this, selectionModel);
+ this.layoutModel_ = layoutModel;
+};
+
+/**
+ * Extends cr.ui.ListSelectionController.
+ */
+Mosaic.SelectionController.prototype.__proto__ =
+ cr.ui.ListSelectionController.prototype;
+
+/** @override */
+Mosaic.SelectionController.prototype.getLastIndex = function() {
+ return this.layoutModel_.getLaidOutTileCount() - 1;
+};
+
+/** @override */
+Mosaic.SelectionController.prototype.getIndexBefore = function(index) {
+ return this.layoutModel_.getHorizontalAdjacentIndex(index, -1);
+};
+
+/** @override */
+Mosaic.SelectionController.prototype.getIndexAfter = function(index) {
+ return this.layoutModel_.getHorizontalAdjacentIndex(index, 1);
+};
+
+/** @override */
+Mosaic.SelectionController.prototype.getIndexAbove = function(index) {
+ return this.layoutModel_.getVerticalAdjacentIndex(index, -1);
+};
+
+/** @override */
+Mosaic.SelectionController.prototype.getIndexBelow = function(index) {
+ return this.layoutModel_.getVerticalAdjacentIndex(index, 1);
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+/**
+ * Mosaic layout.
+ *
+ * @param {string=} opt_mode Layout mode.
+ * @param {Mosaic.Density=} opt_maxDensity Layout density.
+ * @constructor
+ */
+Mosaic.Layout = function(opt_mode, opt_maxDensity) {
+ this.mode_ = opt_mode || Mosaic.Layout.MODE_TENTATIVE;
+ this.maxDensity_ = opt_maxDensity || Mosaic.Density.createHighest();
+ this.reset_();
+};
+
+/**
+ * Blank space at the top of the mosaic element. We do not do that in CSS
+ * to make transition effects easier.
+ */
+Mosaic.Layout.PADDING_TOP = 50;
+
+/**
+ * Blank space at the bottom of the mosaic element.
+ */
+Mosaic.Layout.PADDING_BOTTOM = 50;
+
+/**
+ * Horizontal and vertical spacing between images. Should be kept in sync
+ * with the style of .mosaic-item in gallery.css (= 2 * ( 4 + 1))
+ */
+Mosaic.Layout.SPACING = 10;
+
+/**
+ * Margin for scrolling using keyboard. Distance between a selected tile
+ * and window border.
+ */
+Mosaic.Layout.SCROLL_MARGIN = 30;
+
+/**
+ * Layout mode: commit to DOM immediately.
+ */
+Mosaic.Layout.MODE_FINAL = 'final';
+
+/**
+ * Layout mode: do not commit layout to DOM until it is complete or the viewport
+ * overflows.
+ */
+Mosaic.Layout.MODE_TENTATIVE = 'tentative';
+
+/**
+ * Layout mode: never commit layout to DOM.
+ */
+Mosaic.Layout.MODE_DRY_RUN = 'dry_run';
+
+/**
+ * Reset the layout.
+ *
+ * @private
+ */
+Mosaic.Layout.prototype.reset_ = function() {
+ this.columns_ = [];
+ this.newColumn_ = null;
+ this.density_ = Mosaic.Density.createLowest();
+ if (this.mode_ != Mosaic.Layout.MODE_DRY_RUN) // DRY_RUN is sticky.
+ this.mode_ = Mosaic.Layout.MODE_TENTATIVE;
+};
+
+/**
+ * @param {number} width Viewport width.
+ * @param {number} height Viewport height.
+ */
+Mosaic.Layout.prototype.setViewportSize = function(width, height) {
+ this.viewportWidth_ = width;
+ this.viewportHeight_ = height;
+ this.reset_();
+};
+
+/**
+ * @return {number} Total width of the layout.
+ */
+Mosaic.Layout.prototype.getWidth = function() {
+ var lastColumn = this.getLastColumn_();
+ return lastColumn ? lastColumn.getRight() : 0;
+};
+
+/**
+ * @return {number} Total height of the layout.
+ */
+Mosaic.Layout.prototype.getHeight = function() {
+ var firstColumn = this.columns_[0];
+ return firstColumn ? firstColumn.getHeight() : 0;
+};
+
+/**
+ * @return {Array.<Mosaic.Tile>} All tiles in the layout.
+ */
+Mosaic.Layout.prototype.getTiles = function() {
+ return Array.prototype.concat.apply([],
+ this.columns_.map(function(c) { return c.getTiles() }));
+};
+
+/**
+ * @return {number} Total number of tiles added to the layout.
+ */
+Mosaic.Layout.prototype.getTileCount = function() {
+ return this.getLaidOutTileCount() +
+ (this.newColumn_ ? this.newColumn_.getTileCount() : 0);
+};
+
+/**
+ * @return {Mosaic.Column} The last column or null for empty layout.
+ * @private
+ */
+Mosaic.Layout.prototype.getLastColumn_ = function() {
+ return this.columns_.length ? this.columns_[this.columns_.length - 1] : null;
+};
+
+/**
+ * @return {number} Total number of tiles in completed columns.
+ */
+Mosaic.Layout.prototype.getLaidOutTileCount = function() {
+ var lastColumn = this.getLastColumn_();
+ return lastColumn ? lastColumn.getNextTileIndex() : 0;
+};
+
+/**
+ * Add a tile to the layout.
+ *
+ * @param {Mosaic.Tile} tile The tile to be added.
+ * @param {boolean} isLast True if this tile is the last.
+ */
+Mosaic.Layout.prototype.add = function(tile, isLast) {
+ var layoutQueue = [tile];
+
+ // There are two levels of backtracking in the layout algorithm.
+ // |Mosaic.Layout.density_| tracks the state of the 'global' backtracking
+ // which aims to use as much of the viewport space as possible.
+ // It starts with the lowest density and increases it until the layout
+ // fits into the viewport. If it does not fit even at the highest density,
+ // the layout continues with the highest density.
+ //
+ // |Mosaic.Column.density_| tracks the state of the 'local' backtracking
+ // which aims to avoid producing unnaturally looking columns.
+ // It starts with the current global density and decreases it until the column
+ // looks nice.
+
+ while (layoutQueue.length) {
+ if (!this.newColumn_) {
+ var lastColumn = this.getLastColumn_();
+ this.newColumn_ = new Mosaic.Column(
+ this.columns_.length,
+ lastColumn ? lastColumn.getNextRowIndex() : 0,
+ lastColumn ? lastColumn.getNextTileIndex() : 0,
+ lastColumn ? lastColumn.getRight() : 0,
+ this.viewportHeight_,
+ this.density_.clone());
+ }
+
+ this.newColumn_.add(layoutQueue.shift());
+
+ var isFinalColumn = isLast && !layoutQueue.length;
+
+ if (!this.newColumn_.prepareLayout(isFinalColumn))
+ continue; // Column is incomplete.
+
+ if (this.newColumn_.isSuboptimal()) {
+ layoutQueue = this.newColumn_.getTiles().concat(layoutQueue);
+ this.newColumn_.retryWithLowerDensity();
+ continue;
+ }
+
+ this.columns_.push(this.newColumn_);
+ this.newColumn_ = null;
+
+ if (this.mode_ == Mosaic.Layout.MODE_FINAL) {
+ this.getLastColumn_().layout();
+ continue;
+ }
+
+ if (this.getWidth() > this.viewportWidth_) {
+ // Viewport completely filled.
+ if (this.density_.equals(this.maxDensity_)) {
+ // Max density reached, commit if tentative, just continue if dry run.
+ if (this.mode_ == Mosaic.Layout.MODE_TENTATIVE)
+ this.commit_();
+ continue;
+ }
+
+ // Rollback the entire layout, retry with higher density.
+ layoutQueue = this.getTiles().concat(layoutQueue);
+ this.columns_ = [];
+ this.density_.increase();
+ continue;
+ }
+
+ if (isFinalColumn && this.mode_ == Mosaic.Layout.MODE_TENTATIVE) {
+ // The complete tentative layout fits into the viewport.
+ var stretched = this.findHorizontalLayout_();
+ if (stretched)
+ this.columns_ = stretched.columns_;
+ // Center the layout in the viewport and commit.
+ this.commit_((this.viewportWidth_ - this.getWidth()) / 2,
+ (this.viewportHeight_ - this.getHeight()) / 2);
+ }
+ }
+};
+
+/**
+ * Commit the tentative layout.
+ *
+ * @param {number=} opt_offsetX Horizontal offset.
+ * @param {number=} opt_offsetY Vertical offset.
+ * @private
+ */
+Mosaic.Layout.prototype.commit_ = function(opt_offsetX, opt_offsetY) {
+ console.assert(this.mode_ != Mosaic.Layout.MODE_FINAL,
+ 'Did not expect final layout');
+ for (var i = 0; i != this.columns_.length; i++) {
+ this.columns_[i].layout(opt_offsetX, opt_offsetY);
+ }
+ this.mode_ = Mosaic.Layout.MODE_FINAL;
+};
+
+/**
+ * Find the most horizontally stretched layout built from the same tiles.
+ *
+ * The main layout algorithm fills the entire available viewport height.
+ * If there is too few tiles this results in a layout that is unnaturally
+ * stretched in the vertical direction.
+ *
+ * This method tries a number of smaller heights and returns the most
+ * horizontally stretched layout that still fits into the viewport.
+ *
+ * @return {Mosaic.Layout} A horizontally stretched layout.
+ * @private
+ */
+Mosaic.Layout.prototype.findHorizontalLayout_ = function() {
+ // If the layout aspect ratio is not dramatically different from
+ // the viewport aspect ratio then there is no need to optimize.
+ if (this.getWidth() / this.getHeight() >
+ this.viewportWidth_ / this.viewportHeight_ * 0.9)
+ return null;
+
+ var tiles = this.getTiles();
+ if (tiles.length == 1)
+ return null; // Single tile layout is always the same.
+
+ var tileHeights = tiles.map(function(t) { return t.getMaxContentHeight() });
+ var minTileHeight = Math.min.apply(null, tileHeights);
+
+ for (var h = minTileHeight; h < this.viewportHeight_; h += minTileHeight) {
+ var layout = new Mosaic.Layout(
+ Mosaic.Layout.MODE_DRY_RUN, this.density_.clone());
+ layout.setViewportSize(this.viewportWidth_, h);
+ for (var t = 0; t != tiles.length; t++)
+ layout.add(tiles[t], t + 1 == tiles.length);
+
+ if (layout.getWidth() <= this.viewportWidth_)
+ return layout;
+ }
+
+ return null;
+};
+
+/**
+ * Invalidate the layout after the given tile was modified (added, deleted or
+ * changed dimensions).
+ *
+ * @param {number} index Tile index.
+ * @private
+ */
+Mosaic.Layout.prototype.invalidateFromTile_ = function(index) {
+ var columnIndex = this.getColumnIndexByTile_(index);
+ if (columnIndex < 0)
+ return; // Index not in the layout, probably already invalidated.
+
+ if (this.columns_[columnIndex].getLeft() >= this.viewportWidth_) {
+ // The columns to the right cover the entire viewport width, so there is no
+ // chance that the modified layout would fit into the viewport.
+ // No point in restarting the entire layout, keep the columns to the right.
+ console.assert(this.mode_ == Mosaic.Layout.MODE_FINAL,
+ 'Expected FINAL layout mode');
+ this.columns_ = this.columns_.slice(0, columnIndex);
+ this.newColumn_ = null;
+ } else {
+ // There is a chance that the modified layout would fit into the viewport.
+ this.reset_();
+ this.mode_ = Mosaic.Layout.MODE_TENTATIVE;
+ }
+};
+
+/**
+ * Get the index of the tile to the left or to the right from the given tile.
+ *
+ * @param {number} index Tile index.
+ * @param {number} direction -1 for left, 1 for right.
+ * @return {number} Adjacent tile index.
+ */
+Mosaic.Layout.prototype.getHorizontalAdjacentIndex = function(
+ index, direction) {
+ var column = this.getColumnIndexByTile_(index);
+ if (column < 0) {
+ console.error('Cannot find column for tile #' + index);
+ return -1;
+ }
+
+ var row = this.columns_[column].getRowByTileIndex(index);
+ if (!row) {
+ console.error('Cannot find row for tile #' + index);
+ return -1;
+ }
+
+ var sameRowNeighbourIndex = index + direction;
+ if (row.hasTile(sameRowNeighbourIndex))
+ return sameRowNeighbourIndex;
+
+ var adjacentColumn = column + direction;
+ if (adjacentColumn < 0 || adjacentColumn == this.columns_.length)
+ return -1;
+
+ return this.columns_[adjacentColumn].
+ getEdgeTileIndex_(row.getCenterY(), -direction);
+};
+
+/**
+ * Get the index of the tile to the top or to the bottom from the given tile.
+ *
+ * @param {number} index Tile index.
+ * @param {number} direction -1 for above, 1 for below.
+ * @return {number} Adjacent tile index.
+ */
+Mosaic.Layout.prototype.getVerticalAdjacentIndex = function(
+ index, direction) {
+ var column = this.getColumnIndexByTile_(index);
+ if (column < 0) {
+ console.error('Cannot find column for tile #' + index);
+ return -1;
+ }
+
+ var row = this.columns_[column].getRowByTileIndex(index);
+ if (!row) {
+ console.error('Cannot find row for tile #' + index);
+ return -1;
+ }
+
+ // Find the first item in the next row, or the last item in the previous row.
+ var adjacentRowNeighbourIndex =
+ row.getEdgeTileIndex_(direction) + direction;
+
+ if (adjacentRowNeighbourIndex < 0 ||
+ adjacentRowNeighbourIndex > this.getTileCount() - 1)
+ return -1;
+
+ if (!this.columns_[column].hasTile(adjacentRowNeighbourIndex)) {
+ // It is not in the current column, so return it.
+ return adjacentRowNeighbourIndex;
+ } else {
+ // It is in the current column, so we have to find optically the closest
+ // tile in the adjacent row.
+ var adjacentRow = this.columns_[column].getRowByTileIndex(
+ adjacentRowNeighbourIndex);
+ var previousTileCenterX = row.getTileByIndex(index).getCenterX();
+
+ // Find the closest one.
+ var closestIndex = -1;
+ var closestDistance;
+ var adjacentRowTiles = adjacentRow.getTiles();
+ for (var t = 0; t != adjacentRowTiles.length; t++) {
+ var distance =
+ Math.abs(adjacentRowTiles[t].getCenterX() - previousTileCenterX);
+ if (closestIndex == -1 || distance < closestDistance) {
+ closestIndex = adjacentRow.getEdgeTileIndex_(-1) + t;
+ closestDistance = distance;
+ }
+ }
+ return closestIndex;
+ }
+};
+
+/**
+ * @param {number} index Tile index.
+ * @return {number} Index of the column containing the given tile.
+ * @private
+ */
+Mosaic.Layout.prototype.getColumnIndexByTile_ = function(index) {
+ for (var c = 0; c != this.columns_.length; c++) {
+ if (this.columns_[c].hasTile(index))
+ return c;
+ }
+ return -1;
+};
+
+/**
+ * Scale the given array of size values to satisfy 3 conditions:
+ * 1. The new sizes must be integer.
+ * 2. The new sizes must sum up to the given |total| value.
+ * 3. The relative proportions of the sizes should be as close to the original
+ * as possible.
+ *
+ * @param {Array.<number>} sizes Array of sizes.
+ * @param {number} newTotal New total size.
+ */
+Mosaic.Layout.rescaleSizesToNewTotal = function(sizes, newTotal) {
+ var total = 0;
+
+ var partialTotals = [0];
+ for (var i = 0; i != sizes.length; i++) {
+ total += sizes[i];
+ partialTotals.push(total);
+ }
+
+ var scale = newTotal / total;
+
+ for (i = 0; i != sizes.length; i++) {
+ sizes[i] = Math.round(partialTotals[i + 1] * scale) -
+ Math.round(partialTotals[i] * scale);
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+/**
+ * Representation of the layout density.
+ *
+ * @param {number} horizontal Horizontal density, number tiles per row.
+ * @param {number} vertical Vertical density, frequency of rows forced to
+ * contain a single tile.
+ * @constructor
+ */
+Mosaic.Density = function(horizontal, vertical) {
+ this.horizontal = horizontal;
+ this.vertical = vertical;
+};
+
+/**
+ * Minimal horizontal density (tiles per row).
+ */
+Mosaic.Density.MIN_HORIZONTAL = 1;
+
+/**
+ * Minimal horizontal density (tiles per row).
+ */
+Mosaic.Density.MAX_HORIZONTAL = 3;
+
+/**
+ * Minimal vertical density: force 1 out of 2 rows to containt a single tile.
+ */
+Mosaic.Density.MIN_VERTICAL = 2;
+
+/**
+ * Maximal vertical density: force 1 out of 3 rows to containt a single tile.
+ */
+Mosaic.Density.MAX_VERTICAL = 3;
+
+/**
+ * @return {Mosaic.Density} Lowest density.
+ */
+Mosaic.Density.createLowest = function() {
+ return new Mosaic.Density(
+ Mosaic.Density.MIN_HORIZONTAL,
+ Mosaic.Density.MIN_VERTICAL /* ignored when horizontal is at min */);
+};
+
+/**
+ * @return {Mosaic.Density} Highest density.
+ */
+Mosaic.Density.createHighest = function() {
+ return new Mosaic.Density(
+ Mosaic.Density.MAX_HORIZONTAL,
+ Mosaic.Density.MAX_VERTICAL);
+};
+
+/**
+ * @return {Mosaic.Density} A clone of this density object.
+ */
+Mosaic.Density.prototype.clone = function() {
+ return new Mosaic.Density(this.horizontal, this.vertical);
+};
+
+/**
+ * @param {Mosaic.Density} that The other object.
+ * @return {boolean} True if equal.
+ */
+Mosaic.Density.prototype.equals = function(that) {
+ return this.horizontal == that.horizontal &&
+ this.vertical == that.vertical;
+};
+
+/**
+ * Increase the density to the next level.
+ */
+Mosaic.Density.prototype.increase = function() {
+ if (this.horizontal == Mosaic.Density.MIN_HORIZONTAL ||
+ this.vertical == Mosaic.Density.MAX_VERTICAL) {
+ console.assert(this.horizontal < Mosaic.Density.MAX_HORIZONTAL);
+ this.horizontal++;
+ this.vertical = Mosaic.Density.MIN_VERTICAL;
+ } else {
+ this.vertical++;
+ }
+};
+
+/**
+ * Decrease horizontal density.
+ */
+Mosaic.Density.prototype.decreaseHorizontal = function() {
+ console.assert(this.horizontal > Mosaic.Density.MIN_HORIZONTAL);
+ this.horizontal--;
+};
+
+/**
+ * @param {number} tileCount Number of tiles in the row.
+ * @param {number} rowIndex Global row index.
+ * @return {boolean} True if the row is complete.
+ */
+Mosaic.Density.prototype.isRowComplete = function(tileCount, rowIndex) {
+ return (tileCount == this.horizontal) || (rowIndex % this.vertical) == 0;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+/**
+ * A column in a mosaic layout. Contains rows.
+ *
+ * @param {number} index Column index.
+ * @param {number} firstRowIndex Global row index.
+ * @param {number} firstTileIndex Index of the first tile in the column.
+ * @param {number} left Left edge coordinate.
+ * @param {number} maxHeight Maximum height.
+ * @param {Mosaic.Density} density Layout density.
+ * @constructor
+ */
+Mosaic.Column = function(index, firstRowIndex, firstTileIndex, left, maxHeight,
+ density) {
+ this.index_ = index;
+ this.firstRowIndex_ = firstRowIndex;
+ this.firstTileIndex_ = firstTileIndex;
+ this.left_ = left;
+ this.maxHeight_ = maxHeight;
+ this.density_ = density;
+
+ this.reset_();
+};
+
+/**
+ * Reset the layout.
+ * @private
+ */
+Mosaic.Column.prototype.reset_ = function() {
+ this.tiles_ = [];
+ this.rows_ = [];
+ this.newRow_ = null;
+};
+
+/**
+ * @return {number} Number of tiles in the column.
+ */
+Mosaic.Column.prototype.getTileCount = function() { return this.tiles_.length };
+
+/**
+ * @return {number} Index of the last tile + 1.
+ */
+Mosaic.Column.prototype.getNextTileIndex = function() {
+ return this.firstTileIndex_ + this.getTileCount();
+};
+
+/**
+ * @return {number} Global index of the last row + 1.
+ */
+Mosaic.Column.prototype.getNextRowIndex = function() {
+ return this.firstRowIndex_ + this.rows_.length;
+};
+
+/**
+ * @return {Array.<Mosaic.Tile>} Array of tiles in the column.
+ */
+Mosaic.Column.prototype.getTiles = function() { return this.tiles_ };
+
+/**
+ * @param {number} index Tile index.
+ * @return {boolean} True if this column contains the tile with the given index.
+ */
+Mosaic.Column.prototype.hasTile = function(index) {
+ return this.firstTileIndex_ <= index &&
+ index < (this.firstTileIndex_ + this.getTileCount());
+};
+
+/**
+ * @param {number} y Y coordinate.
+ * @param {number} direction -1 for left, 1 for right.
+ * @return {number} Index of the tile lying on the edge of the column at the
+ * given y coordinate.
+ * @private
+ */
+Mosaic.Column.prototype.getEdgeTileIndex_ = function(y, direction) {
+ for (var r = 0; r < this.rows_.length; r++) {
+ if (this.rows_[r].coversY(y))
+ return this.rows_[r].getEdgeTileIndex_(direction);
+ }
+ return -1;
+};
+
+/**
+ * @param {number} index Tile index.
+ * @return {Mosaic.Row} The row containing the tile with a given index.
+ */
+Mosaic.Column.prototype.getRowByTileIndex = function(index) {
+ for (var r = 0; r != this.rows_.length; r++)
+ if (this.rows_[r].hasTile(index))
+ return this.rows_[r];
+
+ return null;
+};
+
+/**
+ * Add a tile to the column.
+ *
+ * @param {Mosaic.Tile} tile The tile to add.
+ */
+Mosaic.Column.prototype.add = function(tile) {
+ var rowIndex = this.getNextRowIndex();
+
+ if (!this.newRow_)
+ this.newRow_ = new Mosaic.Row(this.getNextTileIndex());
+
+ this.tiles_.push(tile);
+ this.newRow_.add(tile);
+
+ if (this.density_.isRowComplete(this.newRow_.getTileCount(), rowIndex)) {
+ this.rows_.push(this.newRow_);
+ this.newRow_ = null;
+ }
+};
+
+/**
+ * Prepare the column layout.
+ *
+ * @param {boolean=} opt_force True if the layout must be performed even for an
+ * incomplete column.
+ * @return {boolean} True if the layout was performed.
+ */
+Mosaic.Column.prototype.prepareLayout = function(opt_force) {
+ if (opt_force && this.newRow_) {
+ this.rows_.push(this.newRow_);
+ this.newRow_ = null;
+ }
+
+ if (this.rows_.length == 0)
+ return false;
+
+ this.width_ = Math.min.apply(
+ null, this.rows_.map(function(row) { return row.getMaxWidth() }));
+
+ this.height_ = 0;
+
+ this.rowHeights_ = [];
+ for (var r = 0; r != this.rows_.length; r++) {
+ var rowHeight = this.rows_[r].getHeightForWidth(this.width_);
+ this.height_ += rowHeight;
+ this.rowHeights_.push(rowHeight);
+ }
+
+ var overflow = this.height_ / this.maxHeight_;
+ if (!opt_force && (overflow < 1))
+ return false;
+
+ if (overflow > 1) {
+ // Scale down the column width and height.
+ this.width_ = Math.round(this.width_ / overflow);
+ this.height_ = this.maxHeight_;
+ Mosaic.Layout.rescaleSizesToNewTotal(this.rowHeights_, this.maxHeight_);
+ }
+
+ return true;
+};
+
+/**
+ * Retry the column layout with less tiles per row.
+ */
+Mosaic.Column.prototype.retryWithLowerDensity = function() {
+ this.density_.decreaseHorizontal();
+ this.reset_();
+};
+
+/**
+ * @return {number} Column left edge coordinate.
+ */
+Mosaic.Column.prototype.getLeft = function() { return this.left_ };
+
+/**
+ * @return {number} Column right edge coordinate after the layout.
+ */
+Mosaic.Column.prototype.getRight = function() {
+ return this.left_ + this.width_;
+};
+
+/**
+ * @return {number} Column height after the layout.
+ */
+Mosaic.Column.prototype.getHeight = function() { return this.height_ };
+
+/**
+ * Perform the column layout.
+ * @param {number=} opt_offsetX Horizontal offset.
+ * @param {number=} opt_offsetY Vertical offset.
+ */
+Mosaic.Column.prototype.layout = function(opt_offsetX, opt_offsetY) {
+ opt_offsetX = opt_offsetX || 0;
+ opt_offsetY = opt_offsetY || 0;
+ var rowTop = Mosaic.Layout.PADDING_TOP;
+ for (var r = 0; r != this.rows_.length; r++) {
+ this.rows_[r].layout(
+ opt_offsetX + this.left_,
+ opt_offsetY + rowTop,
+ this.width_,
+ this.rowHeights_[r]);
+ rowTop += this.rowHeights_[r];
+ }
+};
+
+/**
+ * Check if the column layout is too ugly to be displayed.
+ *
+ * @return {boolean} True if the layout is suboptimal.
+ */
+Mosaic.Column.prototype.isSuboptimal = function() {
+ var tileCounts =
+ this.rows_.map(function(row) { return row.getTileCount() });
+
+ var maxTileCount = Math.max.apply(null, tileCounts);
+ if (maxTileCount == 1)
+ return false; // Every row has exactly 1 tile, as optimal as it gets.
+
+ var sizes =
+ this.tiles_.map(function(tile) { return tile.getMaxContentHeight() });
+
+ // Ugly layout #1: all images are small and some are one the same row.
+ var allSmall = Math.max.apply(null, sizes) <= Mosaic.Tile.SMALL_IMAGE_SIZE;
+ if (allSmall)
+ return true;
+
+ // Ugly layout #2: all images are large and none occupies an entire row.
+ var allLarge = Math.min.apply(null, sizes) > Mosaic.Tile.SMALL_IMAGE_SIZE;
+ var allCombined = Math.min.apply(null, tileCounts) != 1;
+ if (allLarge && allCombined)
+ return true;
+
+ // Ugly layout #3: some rows have too many tiles for the resulting width.
+ if (this.width_ / maxTileCount < 100)
+ return true;
+
+ return false;
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+/**
+ * A row in a mosaic layout. Contains tiles.
+ *
+ * @param {number} firstTileIndex Index of the first tile in the row.
+ * @constructor
+ */
+Mosaic.Row = function(firstTileIndex) {
+ this.firstTileIndex_ = firstTileIndex;
+ this.tiles_ = [];
+};
+
+/**
+ * @param {Mosaic.Tile} tile The tile to add.
+ */
+Mosaic.Row.prototype.add = function(tile) {
+ console.assert(this.getTileCount() < Mosaic.Density.MAX_HORIZONTAL);
+ this.tiles_.push(tile);
+};
+
+/**
+ * @return {Array.<Mosaic.Tile>} Array of tiles in the row.
+ */
+Mosaic.Row.prototype.getTiles = function() { return this.tiles_ };
+
+/**
+ * Get a tile by index.
+ * @param {number} index Tile index.
+ * @return {Mosaic.Tile} Requested tile or null if not found.
+ */
+Mosaic.Row.prototype.getTileByIndex = function(index) {
+ if (!this.hasTile(index))
+ return null;
+ return this.tiles_[index - this.firstTileIndex_];
+};
+
+/**
+ *
+ * @return {number} Number of tiles in the row.
+ */
+Mosaic.Row.prototype.getTileCount = function() { return this.tiles_.length };
+
+/**
+ * @param {number} index Tile index.
+ * @return {boolean} True if this row contains the tile with the given index.
+ */
+Mosaic.Row.prototype.hasTile = function(index) {
+ return this.firstTileIndex_ <= index &&
+ index < (this.firstTileIndex_ + this.tiles_.length);
+};
+
+/**
+ * @param {number} y Y coordinate.
+ * @return {boolean} True if this row covers the given Y coordinate.
+ */
+Mosaic.Row.prototype.coversY = function(y) {
+ return this.top_ <= y && y < (this.top_ + this.height_);
+};
+
+/**
+ * @return {number} Y coordinate of the tile center.
+ */
+Mosaic.Row.prototype.getCenterY = function() {
+ return this.top_ + Math.round(this.height_ / 2);
+};
+
+/**
+ * Get the first or the last tile.
+ *
+ * @param {number} direction -1 for the first tile, 1 for the last tile.
+ * @return {number} Tile index.
+ * @private
+ */
+Mosaic.Row.prototype.getEdgeTileIndex_ = function(direction) {
+ if (direction < 0)
+ return this.firstTileIndex_;
+ else
+ return this.firstTileIndex_ + this.getTileCount() - 1;
+};
+
+/**
+ * @return {number} Aspect ration of the combined content box of this row.
+ * @private
+ */
+Mosaic.Row.prototype.getTotalContentAspectRatio_ = function() {
+ var sum = 0;
+ for (var t = 0; t != this.tiles_.length; t++)
+ sum += this.tiles_[t].getAspectRatio();
+ return sum;
+};
+
+/**
+ * @return {number} Total horizontal spacing in this row. This includes
+ * the spacing between the tiles and both left and right margins.
+ *
+ * @private
+ */
+Mosaic.Row.prototype.getTotalHorizontalSpacing_ = function() {
+ return Mosaic.Layout.SPACING * this.getTileCount();
+};
+
+/**
+ * @return {number} Maximum width that this row may have without overscaling
+ * any of the tiles.
+ */
+Mosaic.Row.prototype.getMaxWidth = function() {
+ var contentHeight = Math.min.apply(null,
+ this.tiles_.map(function(tile) { return tile.getMaxContentHeight() }));
+
+ var contentWidth =
+ Math.round(contentHeight * this.getTotalContentAspectRatio_());
+ return contentWidth + this.getTotalHorizontalSpacing_();
+};
+
+/**
+ * Compute the height that best fits the supplied row width given
+ * aspect ratios of the tiles in this row.
+ *
+ * @param {number} width Row width.
+ * @return {number} Height.
+ */
+Mosaic.Row.prototype.getHeightForWidth = function(width) {
+ var contentWidth = width - this.getTotalHorizontalSpacing_();
+ var contentHeight =
+ Math.round(contentWidth / this.getTotalContentAspectRatio_());
+ return contentHeight + Mosaic.Layout.SPACING;
+};
+
+/**
+ * Position the row in the mosaic.
+ *
+ * @param {number} left Left position.
+ * @param {number} top Top position.
+ * @param {number} width Width.
+ * @param {number} height Height.
+ */
+Mosaic.Row.prototype.layout = function(left, top, width, height) {
+ this.top_ = top;
+ this.height_ = height;
+
+ var contentWidth = width - this.getTotalHorizontalSpacing_();
+ var contentHeight = height - Mosaic.Layout.SPACING;
+
+ var tileContentWidth = this.tiles_.map(
+ function(tile) { return tile.getAspectRatio() });
+
+ Mosaic.Layout.rescaleSizesToNewTotal(tileContentWidth, contentWidth);
+
+ var tileLeft = left;
+ for (var t = 0; t != this.tiles_.length; t++) {
+ var tileWidth = tileContentWidth[t] + Mosaic.Layout.SPACING;
+ this.tiles_[t].layout(tileLeft, top, tileWidth, height);
+ tileLeft += tileWidth;
+ }
+};
+
+////////////////////////////////////////////////////////////////////////////////
+
+/**
+ * A single tile of the image mosaic.
+ *
+ * @param {Element} container Container element.
+ * @param {Gallery.Item} item Gallery item associated with this tile.
+ * @return {Element} The new tile element.
+ * @constructor
+ */
+Mosaic.Tile = function(container, item) {
+ var self = container.ownerDocument.createElement('div');
+ Mosaic.Tile.decorate(self, container, item);
+ return self;
+};
+
+/**
+ * @param {Element} self Self pointer.
+ * @param {Element} container Container element.
+ * @param {Gallery.Item} item Gallery item associated with this tile.
+ */
+Mosaic.Tile.decorate = function(self, container, item) {
+ self.__proto__ = Mosaic.Tile.prototype;
+ self.className = 'mosaic-tile';
+
+ self.container_ = container;
+ self.item_ = item;
+ self.left_ = null; // Mark as not laid out.
+};
+
+/**
+ * Load mode for the tile's image.
+ * @enum {number}
+ */
+Mosaic.Tile.LoadMode = {
+ LOW_DPI: 0,
+ HIGH_DPI: 1
+};
+
+/**
+* Inherit from HTMLDivElement.
+*/
+Mosaic.Tile.prototype.__proto__ = HTMLDivElement.prototype;
+
+/**
+ * Minimum tile content size.
+ */
+Mosaic.Tile.MIN_CONTENT_SIZE = 64;
+
+/**
+ * Maximum tile content size.
+ */
+Mosaic.Tile.MAX_CONTENT_SIZE = 512;
+
+/**
+ * Default size for a tile with no thumbnail image.
+ */
+Mosaic.Tile.GENERIC_ICON_SIZE = 128;
+
+/**
+ * Max size of an image considered to be 'small'.
+ * Small images are laid out slightly differently.
+ */
+Mosaic.Tile.SMALL_IMAGE_SIZE = 160;
+
+/**
+ * @return {Gallery.Item} The Gallery item.
+ */
+Mosaic.Tile.prototype.getItem = function() { return this.item_ };
+
+/**
+ * @return {number} Maximum content height that this tile can have.
+ */
+Mosaic.Tile.prototype.getMaxContentHeight = function() {
+ return this.maxContentHeight_;
+};
+
+/**
+ * @return {number} The aspect ratio of the tile image.
+ */
+Mosaic.Tile.prototype.getAspectRatio = function() { return this.aspectRatio_ };
+
+/**
+ * @return {boolean} True if the tile is initialized.
+ */
+Mosaic.Tile.prototype.isInitialized = function() {
+ return !!this.maxContentHeight_;
+};
+
+/**
+ * Checks whether the image of specified (or better resolution) has been loaded.
+ *
+ * @param {Mosaic.Tile.LoadMode=} opt_loadMode Loading mode, default: LOW_DPI.
+ * @return {boolean} True if the tile is loaded with the specified dpi or
+ * better.
+ */
+Mosaic.Tile.prototype.isLoaded = function(opt_loadMode) {
+ var loadMode = opt_loadMode || Mosaic.Tile.LoadMode.LOW_DPI;
+ switch (loadMode) {
+ case Mosaic.Tile.LoadMode.LOW_DPI:
+ if (this.imagePreloaded_ || this.imageLoaded_)
+ return true;
+ break;
+ case Mosaic.Tile.LoadMode.HIGH_DPI:
+ if (this.imageLoaded_)
+ return true;
+ break;
+ }
+ return false;
+};
+
+/**
+ * Checks whether the image of specified (or better resolution) is being loaded.
+ *
+ * @param {Mosaic.Tile.LoadMode=} opt_loadMode Loading mode, default: LOW_DPI.
+ * @return {boolean} True if the tile is being loaded with the specified dpi or
+ * better.
+ */
+Mosaic.Tile.prototype.isLoading = function(opt_loadMode) {
+ var loadMode = opt_loadMode || Mosaic.Tile.LoadMode.LOW_DPI;
+ switch (loadMode) {
+ case Mosaic.Tile.LoadMode.LOW_DPI:
+ if (this.imagePreloading_ || this.imageLoading_)
+ return true;
+ break;
+ case Mosaic.Tile.LoadMode.HIGH_DPI:
+ if (this.imageLoading_)
+ return true;
+ break;
+ }
+ return false;
+};
+
+/**
+ * Mark the tile as not loaded to prevent it from participating in the layout.
+ */
+Mosaic.Tile.prototype.markUnloaded = function() {
+ this.maxContentHeight_ = 0;
+ if (this.thumbnailLoader_) {
+ this.thumbnailLoader_.cancel();
+ this.imagePreloaded_ = false;
+ this.imagePreloading_ = false;
+ this.imageLoaded_ = false;
+ this.imageLoading_ = false;
+ }
+};
+
+/**
+ * Initializes the thumbnail in the tile. Does not load an image, but sets
+ * target dimensions using metadata.
+ *
+ * @param {Object} metadata Metadata object.
+ * @param {function()} onImageMeasured Image measured callback.
+ */
+Mosaic.Tile.prototype.init = function(metadata, onImageMeasured) {
+ this.markUnloaded();
+ this.left_ = null; // Mark as not laid out.
+
+ // Set higher priority for the selected elements to load them first.
+ var priority = this.getAttribute('selected') ? 2 : 3;
+
+ // Use embedded thumbnails on Drive, since they have higher resolution.
+ var hidpiEmbedded = FileType.isOnDrive(this.getItem().getEntry());
+ this.thumbnailLoader_ = new ThumbnailLoader(
+ this.getItem().getEntry().toURL(),
+ ThumbnailLoader.LoaderType.CANVAS,
+ metadata,
+ undefined, // Media type.
+ hidpiEmbedded ? ThumbnailLoader.UseEmbedded.USE_EMBEDDED :
+ ThumbnailLoader.UseEmbedded.NO_EMBEDDED,
+ priority);
+
+ // If no hidpi embedded thumbnail available, then use the low resolution
+ // for preloading.
+ if (!hidpiEmbedded) {
+ this.thumbnailPreloader_ = new ThumbnailLoader(
+ this.getItem().getEntry().toURL(),
+ ThumbnailLoader.LoaderType.CANVAS,
+ metadata,
+ undefined, // Media type.
+ ThumbnailLoader.UseEmbedded.USE_EMBEDDED,
+ 2); // Preloaders have always higher priotity, so the preload images
+ // are loaded as soon as possible.
+ }
+
+ var setDimensions = function(width, height) {
+ if (width > height) {
+ if (width > Mosaic.Tile.MAX_CONTENT_SIZE) {
+ height = Math.round(height * Mosaic.Tile.MAX_CONTENT_SIZE / width);
+ width = Mosaic.Tile.MAX_CONTENT_SIZE;
+ }
+ } else {
+ if (height > Mosaic.Tile.MAX_CONTENT_SIZE) {
+ width = Math.round(width * Mosaic.Tile.MAX_CONTENT_SIZE / height);
+ height = Mosaic.Tile.MAX_CONTENT_SIZE;
+ }
+ }
+ this.maxContentHeight_ = Math.max(Mosaic.Tile.MIN_CONTENT_SIZE, height);
+ this.aspectRatio_ = width / height;
+ onImageMeasured();
+ }.bind(this);
+
+ // Dimensions are always acquired from the metadata. For local files, it is
+ // extracted from headers. For Drive files, it is received via the Drive API.
+ // If the dimensions are not available, then the fallback dimensions will be
+ // used (same as for the generic icon).
+ if (metadata.media && metadata.media.width) {
+ setDimensions(metadata.media.width, metadata.media.height);
+ } else if (metadata.drive && metadata.drive.imageWidth &&
+ metadata.drive.imageHeight) {
+ setDimensions(metadata.drive.imageWidth, metadata.drive.imageHeight);
+ } else {
+ // No dimensions in metadata, then use the generic dimensions.
+ setDimensions(Mosaic.Tile.GENERIC_ICON_SIZE,
+ Mosaic.Tile.GENERIC_ICON_SIZE);
+ }
+};
+
+/**
+ * Loads an image into the tile.
+ *
+ * The mode argument is a hint. Use low-dpi for faster response, and high-dpi
+ * for better output, but possibly affecting performance.
+ *
+ * If the mode is high-dpi, then a the high-dpi image is loaded, but also
+ * low-dpi image is loaded for preloading (if available).
+ * For the low-dpi mode, only low-dpi image is loaded. If not available, then
+ * the high-dpi image is loaded as a fallback.
+ *
+ * @param {Mosaic.Tile.LoadMode} loadMode Loading mode.
+ * @param {function(boolean)} onImageLoaded Callback when image is loaded.
+ * The argument is true for success, false for failure.
+ */
+Mosaic.Tile.prototype.load = function(loadMode, onImageLoaded) {
+ // Attaches the image to the tile and finalizes loading process for the
+ // specified loader.
+ var finalizeLoader = function(mode, success, loader) {
+ if (success && this.wrapper_) {
+ // Show the fade-in animation only when previously there was no image
+ // attached in this tile.
+ if (!this.imageLoaded_ && !this.imagePreloaded_)
+ this.wrapper_.classList.add('animated');
+ else
+ this.wrapper_.classList.remove('animated');
+ }
+ loader.attachImage(this.wrapper_, ThumbnailLoader.FillMode.OVER_FILL);
+ onImageLoaded(success);
+ switch (mode) {
+ case Mosaic.Tile.LoadMode.LOW_DPI:
+ this.imagePreloading_ = false;
+ this.imagePreloaded_ = true;
+ break;
+ case Mosaic.Tile.LoadMode.HIGH_DPI:
+ this.imageLoading_ = false;
+ this.imageLoaded_ = true;
+ break;
+ }
+ }.bind(this);
+
+ // Always load the low-dpi image first if it is available for the fastest
+ // feedback.
+ if (!this.imagePreloading_ && this.thumbnailPreloader_) {
+ this.imagePreloading_ = true;
+ this.thumbnailPreloader_.loadDetachedImage(function(success) {
+ // Hi-dpi loaded first, ignore this call then.
+ if (this.imageLoaded_)
+ return;
+ finalizeLoader(Mosaic.Tile.LoadMode.LOW_DPI,
+ success,
+ this.thumbnailPreloader_);
+ }.bind(this));
+ }
+
+ // Load the high-dpi image only when it is requested, or the low-dpi is not
+ // available.
+ if (!this.imageLoading_ &&
+ (loadMode == Mosaic.Tile.LoadMode.HIGH_DPI || !this.imagePreloading_)) {
+ this.imageLoading_ = true;
+ this.thumbnailLoader_.loadDetachedImage(function(success) {
+ // Cancel preloading, since the hi-dpi image is ready.
+ if (this.thumbnailPreloader_)
+ this.thumbnailPreloader_.cancel();
+ finalizeLoader(Mosaic.Tile.LoadMode.HIGH_DPI,
+ success,
+ this.thumbnailLoader_);
+ }.bind(this));
+ }
+};
+
+/**
+ * Unloads an image from the tile.
+ */
+Mosaic.Tile.prototype.unload = function() {
+ this.thumbnailLoader_.cancel();
+ if (this.thumbnailPreloader_)
+ this.thumbnailPreloader_.cancel();
+ this.imagePreloaded_ = false;
+ this.imageLoaded_ = false;
+ this.imagePreloading_ = false;
+ this.imageLoading_ = false;
+ this.wrapper_.innerText = '';
+};
+
+/**
+ * Select/unselect the tile.
+ *
+ * @param {boolean} on True if selected.
+ */
+Mosaic.Tile.prototype.select = function(on) {
+ if (on)
+ this.setAttribute('selected', true);
+ else
+ this.removeAttribute('selected');
+};
+
+/**
+ * Position the tile in the mosaic.
+ *
+ * @param {number} left Left position.
+ * @param {number} top Top position.
+ * @param {number} width Width.
+ * @param {number} height Height.
+ */
+Mosaic.Tile.prototype.layout = function(left, top, width, height) {
+ this.left_ = left;
+ this.top_ = top;
+ this.width_ = width;
+ this.height_ = height;
+
+ this.style.left = left + 'px';
+ this.style.top = top + 'px';
+ this.style.width = width + 'px';
+ this.style.height = height + 'px';
+
+ if (!this.wrapper_) { // First time, create DOM.
+ this.container_.appendChild(this);
+ var border = util.createChild(this, 'img-border');
+ this.wrapper_ = util.createChild(border, 'img-wrapper');
+ }
+ if (this.hasAttribute('selected'))
+ this.scrollIntoView(false);
+
+ if (this.imageLoaded_) {
+ this.thumbnailLoader_.attachImage(this.wrapper_,
+ ThumbnailLoader.FillMode.FILL);
+ }
+};
+
+/**
+ * If the tile is not fully visible scroll the parent to make it fully visible.
+ * @param {boolean=} opt_animated True, if scroll should be animated,
+ * default: true.
+ */
+Mosaic.Tile.prototype.scrollIntoView = function(opt_animated) {
+ if (this.left_ == null) // Not laid out.
+ return;
+
+ var targetPosition;
+ var tileLeft = this.left_ - Mosaic.Layout.SCROLL_MARGIN;
+ if (tileLeft < this.container_.scrollLeft) {
+ targetPosition = tileLeft;
+ } else {
+ var tileRight = this.left_ + this.width_ + Mosaic.Layout.SCROLL_MARGIN;
+ var scrollRight = this.container_.scrollLeft + this.container_.clientWidth;
+ if (tileRight > scrollRight)
+ targetPosition = tileRight - this.container_.clientWidth;
+ }
+
+ if (targetPosition) {
+ if (opt_animated === false)
+ this.container_.scrollLeft = targetPosition;
+ else
+ this.container_.animatedScrollTo(targetPosition);
+ }
+};
+
+/**
+ * @return {Rect} Rectangle occupied by the tile's image,
+ * relative to the viewport.
+ */
+Mosaic.Tile.prototype.getImageRect = function() {
+ if (this.left_ == null) // Not laid out.
+ return null;
+
+ var margin = Mosaic.Layout.SPACING / 2;
+ return new Rect(this.left_ - this.container_.scrollLeft, this.top_,
+ this.width_, this.height_).inflate(-margin, -margin);
+};
+
+/**
+ * @return {number} X coordinate of the tile center.
+ */
+Mosaic.Tile.prototype.getCenterX = function() {
+ return this.left_ + Math.round(this.width_ / 2);
+};
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/photo/ribbon.js b/chromium/chrome/browser/resources/file_manager/foreground/js/photo/ribbon.js
new file mode 100644
index 00000000000..4d1c81c6c3c
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/photo/ribbon.js
@@ -0,0 +1,366 @@
+// 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.
+
+'use strict';
+
+/**
+ * Scrollable thumbnail ribbon at the bottom of the Gallery in the Slide mode.
+ *
+ * @param {Document} document Document.
+ * @param {MetadataCache} metadataCache MetadataCache instance.
+ * @param {cr.ui.ArrayDataModel} dataModel Data model.
+ * @param {cr.ui.ListSelectionModel} selectionModel Selection model.
+ * @return {Element} Ribbon element.
+ * @constructor
+ */
+function Ribbon(document, metadataCache, dataModel, selectionModel) {
+ var self = document.createElement('div');
+ Ribbon.decorate(self, metadataCache, dataModel, selectionModel);
+ return self;
+}
+
+/**
+ * Inherit from HTMLDivElement.
+ */
+Ribbon.prototype.__proto__ = HTMLDivElement.prototype;
+
+/**
+ * Decorate a Ribbon instance.
+ *
+ * @param {Ribbon} self Self pointer.
+ * @param {MetadataCache} metadataCache MetadataCache instance.
+ * @param {cr.ui.ArrayDataModel} dataModel Data model.
+ * @param {cr.ui.ListSelectionModel} selectionModel Selection model.
+ */
+Ribbon.decorate = function(self, metadataCache, dataModel, selectionModel) {
+ self.__proto__ = Ribbon.prototype;
+ self.metadataCache_ = metadataCache;
+ self.dataModel_ = dataModel;
+ self.selectionModel_ = selectionModel;
+
+ self.className = 'ribbon';
+};
+
+/**
+ * Max number of thumbnails in the ribbon.
+ * @type {number}
+ */
+Ribbon.ITEMS_COUNT = 5;
+
+/**
+ * Force redraw the ribbon.
+ */
+Ribbon.prototype.redraw = function() {
+ this.onSelection_();
+};
+
+/**
+ * Clear all cached data to force full redraw on the next selection change.
+ */
+Ribbon.prototype.reset = function() {
+ this.renderCache_ = {};
+ this.firstVisibleIndex_ = 0;
+ this.lastVisibleIndex_ = -1; // Zero thumbnails
+};
+
+/**
+ * Enable the ribbon.
+ */
+Ribbon.prototype.enable = function() {
+ this.onContentBound_ = this.onContentChange_.bind(this);
+ this.dataModel_.addEventListener('content', this.onContentBound_);
+
+ this.onSpliceBound_ = this.onSplice_.bind(this);
+ this.dataModel_.addEventListener('splice', this.onSpliceBound_);
+
+ this.onSelectionBound_ = this.onSelection_.bind(this);
+ this.selectionModel_.addEventListener('change', this.onSelectionBound_);
+
+ this.reset();
+ this.redraw();
+};
+
+/**
+ * Disable ribbon.
+ */
+Ribbon.prototype.disable = function() {
+ this.dataModel_.removeEventListener('content', this.onContentBound_);
+ this.dataModel_.removeEventListener('splice', this.onSpliceBound_);
+ this.selectionModel_.removeEventListener('change', this.onSelectionBound_);
+
+ this.removeVanishing_();
+ this.textContent = '';
+};
+
+/**
+ * Data model splice handler.
+ * @param {Event} event Event.
+ * @private
+ */
+Ribbon.prototype.onSplice_ = function(event) {
+ if (event.removed.length == 0)
+ return;
+
+ if (event.removed.length > 1) {
+ console.error('Cannot remove multiple items');
+ return;
+ }
+
+ var removed = this.renderCache_[event.removed[0].getEntry().toURL()];
+ if (!removed || !removed.parentNode || !removed.hasAttribute('selected')) {
+ console.error('Can only remove the selected item');
+ return;
+ }
+
+ var persistentNodes = this.querySelectorAll('.ribbon-image:not([vanishing])');
+ if (this.lastVisibleIndex_ < this.dataModel_.length) { // Not at the end.
+ var lastNode = persistentNodes[persistentNodes.length - 1];
+ if (lastNode.nextSibling) {
+ // Pull back a vanishing node from the right.
+ lastNode.nextSibling.removeAttribute('vanishing');
+ } else {
+ // Push a new item at the right end.
+ this.appendChild(this.renderThumbnail_(this.lastVisibleIndex_));
+ }
+ } else {
+ // No items to the right, move the window to the left.
+ this.lastVisibleIndex_--;
+ if (this.firstVisibleIndex_) {
+ this.firstVisibleIndex_--;
+ var firstNode = persistentNodes[0];
+ if (firstNode.previousSibling) {
+ // Pull back a vanishing node from the left.
+ firstNode.previousSibling.removeAttribute('vanishing');
+ } else {
+ // Push a new item at the left end.
+ var newThumbnail = this.renderThumbnail_(this.firstVisibleIndex_);
+ newThumbnail.style.marginLeft = -(this.clientHeight - 2) + 'px';
+ this.insertBefore(newThumbnail, this.firstChild);
+ setTimeout(function() {
+ newThumbnail.style.marginLeft = '0';
+ }, 0);
+ }
+ }
+ }
+
+ removed.removeAttribute('selected');
+ removed.setAttribute('vanishing', 'smooth');
+ this.scheduleRemove_();
+};
+
+/**
+ * Selection change handler.
+ * @private
+ */
+Ribbon.prototype.onSelection_ = function() {
+ var indexes = this.selectionModel_.selectedIndexes;
+ if (indexes.length == 0)
+ return; // Ignore temporary empty selection.
+ var selectedIndex = indexes[0];
+
+ var length = this.dataModel_.length;
+
+ // TODO(dgozman): use margin instead of 2 here.
+ var itemWidth = this.clientHeight - 2;
+ var fullItems = Ribbon.ITEMS_COUNT;
+ fullItems = Math.min(fullItems, length);
+ var right = Math.floor((fullItems - 1) / 2);
+
+ var fullWidth = fullItems * itemWidth;
+ this.style.width = fullWidth + 'px';
+
+ var lastIndex = selectedIndex + right;
+ lastIndex = Math.max(lastIndex, fullItems - 1);
+ lastIndex = Math.min(lastIndex, length - 1);
+ var firstIndex = lastIndex - fullItems + 1;
+
+ if (this.firstVisibleIndex_ != firstIndex ||
+ this.lastVisibleIndex_ != lastIndex) {
+
+ if (this.lastVisibleIndex_ == -1) {
+ this.firstVisibleIndex_ = firstIndex;
+ this.lastVisibleIndex_ = lastIndex;
+ }
+
+ this.removeVanishing_();
+
+ this.textContent = '';
+ var startIndex = Math.min(firstIndex, this.firstVisibleIndex_);
+ // All the items except the first one treated equally.
+ for (var index = startIndex + 1;
+ index <= Math.max(lastIndex, this.lastVisibleIndex_);
+ ++index) {
+ // Only add items that are in either old or the new viewport.
+ if (this.lastVisibleIndex_ < index && index < firstIndex ||
+ lastIndex < index && index < this.firstVisibleIndex_)
+ continue;
+ var box = this.renderThumbnail_(index);
+ box.style.marginLeft = '0';
+ this.appendChild(box);
+ if (index < firstIndex || index > lastIndex) {
+ // If the node is not in the new viewport we only need it while
+ // the animation is playing out.
+ box.setAttribute('vanishing', 'slide');
+ }
+ }
+
+ var slideCount = this.childNodes.length + 1 - Ribbon.ITEMS_COUNT;
+ var margin = itemWidth * slideCount;
+ var startBox = this.renderThumbnail_(startIndex);
+ if (startIndex == firstIndex) {
+ // Sliding to the right.
+ startBox.style.marginLeft = -margin + 'px';
+ if (this.firstChild)
+ this.insertBefore(startBox, this.firstChild);
+ else
+ this.appendChild(startBox);
+ setTimeout(function() {
+ startBox.style.marginLeft = '0';
+ }, 0);
+ } else {
+ // Sliding to the left. Start item will become invisible and should be
+ // removed afterwards.
+ startBox.setAttribute('vanishing', 'slide');
+ startBox.style.marginLeft = '0';
+ if (this.firstChild)
+ this.insertBefore(startBox, this.firstChild);
+ else
+ this.appendChild(startBox);
+ setTimeout(function() {
+ startBox.style.marginLeft = -margin + 'px';
+ }, 0);
+ }
+
+ ImageUtil.setClass(this, 'fade-left',
+ firstIndex > 0 && selectedIndex != firstIndex);
+
+ ImageUtil.setClass(this, 'fade-right',
+ lastIndex < length - 1 && selectedIndex != lastIndex);
+
+ this.firstVisibleIndex_ = firstIndex;
+ this.lastVisibleIndex_ = lastIndex;
+
+ this.scheduleRemove_();
+ }
+
+ var oldSelected = this.querySelector('[selected]');
+ if (oldSelected) oldSelected.removeAttribute('selected');
+
+ var newSelected =
+ this.renderCache_[this.dataModel_.item(selectedIndex).getEntry().toURL()];
+ if (newSelected) newSelected.setAttribute('selected', true);
+};
+
+/**
+ * Schedule the removal of thumbnails marked as vanishing.
+ * @private
+ */
+Ribbon.prototype.scheduleRemove_ = function() {
+ if (this.removeTimeout_)
+ clearTimeout(this.removeTimeout_);
+
+ this.removeTimeout_ = setTimeout(function() {
+ this.removeTimeout_ = null;
+ this.removeVanishing_();
+ }.bind(this), 200);
+};
+
+/**
+ * Remove all thumbnails marked as vanishing.
+ * @private
+ */
+Ribbon.prototype.removeVanishing_ = function() {
+ if (this.removeTimeout_) {
+ clearTimeout(this.removeTimeout_);
+ this.removeTimeout_ = 0;
+ }
+ var vanishingNodes = this.querySelectorAll('[vanishing]');
+ for (var i = 0; i != vanishingNodes.length; i++) {
+ vanishingNodes[i].removeAttribute('vanishing');
+ this.removeChild(vanishingNodes[i]);
+ }
+};
+
+/**
+ * Create a DOM element for a thumbnail.
+ *
+ * @param {number} index Item index.
+ * @return {Element} Newly created element.
+ * @private
+ */
+Ribbon.prototype.renderThumbnail_ = function(index) {
+ var item = this.dataModel_.item(index);
+ var url = item.getEntry().toURL();
+
+ var cached = this.renderCache_[url];
+ if (cached) {
+ var img = cached.querySelector('img');
+ if (img)
+ img.classList.add('cached');
+ return cached;
+ }
+
+ var thumbnail = this.ownerDocument.createElement('div');
+ thumbnail.className = 'ribbon-image';
+ thumbnail.addEventListener('click', function() {
+ var index = this.dataModel_.indexOf(item);
+ this.selectionModel_.unselectAll();
+ this.selectionModel_.setIndexSelected(index, true);
+ }.bind(this));
+
+ util.createChild(thumbnail, 'image-wrapper');
+
+ this.metadataCache_.get(item.getEntry(), Gallery.METADATA_TYPE,
+ this.setThumbnailImage_.bind(this, thumbnail, url));
+
+ // TODO: Implement LRU eviction.
+ // Never evict the thumbnails that are currently in the DOM because we rely
+ // on this cache to find them by URL.
+ this.renderCache_[url] = thumbnail;
+ return thumbnail;
+};
+
+/**
+ * Set the thumbnail image.
+ *
+ * @param {Element} thumbnail Thumbnail element.
+ * @param {string} url Image url.
+ * @param {Object} metadata Metadata.
+ * @private
+ */
+Ribbon.prototype.setThumbnailImage_ = function(thumbnail, url, metadata) {
+ new ThumbnailLoader(url, ThumbnailLoader.LoaderType.IMAGE, metadata).load(
+ thumbnail.querySelector('.image-wrapper'),
+ ThumbnailLoader.FillMode.FILL /* fill */,
+ ThumbnailLoader.OptimizationMode.NEVER_DISCARD);
+};
+
+/**
+ * Content change handler.
+ *
+ * @param {Event} event Event.
+ * @private
+ */
+Ribbon.prototype.onContentChange_ = function(event) {
+ var url = event.item.getEntry().toURL();
+ this.remapCache_(event.oldUrl, url);
+
+ var thumbnail = this.renderCache_[url];
+ if (thumbnail && event.metadata)
+ this.setThumbnailImage_(thumbnail, url, event.metadata);
+};
+
+/**
+ * Update the thumbnail element cache.
+ *
+ * @param {string} oldUrl Old url.
+ * @param {string} newUrl New url.
+ * @private
+ */
+Ribbon.prototype.remapCache_ = function(oldUrl, newUrl) {
+ if (oldUrl != newUrl && (oldUrl in this.renderCache_)) {
+ this.renderCache_[newUrl] = this.renderCache_[oldUrl];
+ delete this.renderCache_[oldUrl];
+ }
+};
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/photo/slide_mode.js b/chromium/chrome/browser/resources/file_manager/foreground/js/photo/slide_mode.js
new file mode 100644
index 00000000000..d9b47395bb9
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/photo/slide_mode.js
@@ -0,0 +1,1354 @@
+// 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.
+
+'use strict';
+
+/**
+ * Slide mode displays a single image and has a set of controls to navigate
+ * between the images and to edit an image.
+ *
+ * TODO(kaznacheev): Introduce a parameter object.
+ *
+ * @param {Element} container Main container element.
+ * @param {Element} content Content container element.
+ * @param {Element} toolbar Toolbar element.
+ * @param {ImageEditor.Prompt} prompt Prompt.
+ * @param {cr.ui.ArrayDataModel} dataModel Data model.
+ * @param {cr.ui.ListSelectionModel} selectionModel Selection model.
+ * @param {Object} context Context.
+ * @param {function(function())} toggleMode Function to toggle the Gallery mode.
+ * @param {function(string):string} displayStringFunction String formatting
+ * function.
+ * @constructor
+ */
+function SlideMode(container, content, toolbar, prompt,
+ dataModel, selectionModel, context,
+ toggleMode, displayStringFunction) {
+ this.container_ = container;
+ this.document_ = container.ownerDocument;
+ this.content = content;
+ this.toolbar_ = toolbar;
+ this.prompt_ = prompt;
+ this.dataModel_ = dataModel;
+ this.selectionModel_ = selectionModel;
+ this.context_ = context;
+ this.metadataCache_ = context.metadataCache;
+ this.toggleMode_ = toggleMode;
+ this.displayStringFunction_ = displayStringFunction;
+
+ this.onSelectionBound_ = this.onSelection_.bind(this);
+ this.onSpliceBound_ = this.onSplice_.bind(this);
+ this.onContentBound_ = this.onContentChange_.bind(this);
+
+ // Unique numeric key, incremented per each load attempt used to discard
+ // old attempts. This can happen especially when changing selection fast or
+ // Internet connection is slow.
+ this.currentUniqueKey_ = 0;
+
+ this.initListeners_();
+ this.initDom_();
+}
+
+/**
+ * SlideMode extends cr.EventTarget.
+ */
+SlideMode.prototype.__proto__ = cr.EventTarget.prototype;
+
+/**
+ * List of available editor modes.
+ * @type {Array.<ImageEditor.Mode>}
+ */
+SlideMode.editorModes = [
+ new ImageEditor.Mode.InstantAutofix(),
+ new ImageEditor.Mode.Crop(),
+ new ImageEditor.Mode.Exposure(),
+ new ImageEditor.Mode.OneClick(
+ 'rotate_left', 'GALLERY_ROTATE_LEFT', new Command.Rotate(-1)),
+ new ImageEditor.Mode.OneClick(
+ 'rotate_right', 'GALLERY_ROTATE_RIGHT', new Command.Rotate(1))
+];
+
+/**
+ * @return {string} Mode name.
+ */
+SlideMode.prototype.getName = function() { return 'slide' };
+
+/**
+ * @return {string} Mode title.
+ */
+SlideMode.prototype.getTitle = function() { return 'GALLERY_SLIDE' };
+
+/**
+ * Initialize the listeners.
+ * @private
+ */
+SlideMode.prototype.initListeners_ = function() {
+ window.addEventListener('resize', this.onResize_.bind(this), false);
+};
+
+/**
+ * Initialize the UI.
+ * @private
+ */
+SlideMode.prototype.initDom_ = function() {
+ // Container for displayed image or video.
+ this.imageContainer_ = util.createChild(
+ this.document_.querySelector('.content'), 'image-container');
+ this.imageContainer_.addEventListener('click', this.onClick_.bind(this));
+
+ this.document_.addEventListener('click', this.onDocumentClick_.bind(this));
+
+ // Overwrite options and info bubble.
+ this.options_ = util.createChild(
+ this.toolbar_.querySelector('.filename-spacer'), 'options');
+
+ this.savedLabel_ = util.createChild(this.options_, 'saved');
+ this.savedLabel_.textContent = this.displayStringFunction_('GALLERY_SAVED');
+
+ var overwriteOriginalBox =
+ util.createChild(this.options_, 'overwrite-original');
+
+ this.overwriteOriginal_ = util.createChild(
+ overwriteOriginalBox, 'common white', 'input');
+ this.overwriteOriginal_.type = 'checkbox';
+ this.overwriteOriginal_.id = 'overwrite-checkbox';
+ util.platform.getPreference(SlideMode.OVERWRITE_KEY, function(value) {
+ // Out-of-the box default is 'true'
+ this.overwriteOriginal_.checked =
+ (typeof value !== 'string' || value === 'true');
+ }.bind(this));
+ this.overwriteOriginal_.addEventListener('click',
+ this.onOverwriteOriginalClick_.bind(this));
+
+ var overwriteLabel = util.createChild(overwriteOriginalBox, '', 'label');
+ overwriteLabel.textContent =
+ this.displayStringFunction_('GALLERY_OVERWRITE_ORIGINAL');
+ overwriteLabel.setAttribute('for', 'overwrite-checkbox');
+
+ this.bubble_ = util.createChild(this.toolbar_, 'bubble');
+ this.bubble_.hidden = true;
+
+ var bubbleContent = util.createChild(this.bubble_);
+ bubbleContent.innerHTML = this.displayStringFunction_(
+ 'GALLERY_OVERWRITE_BUBBLE');
+
+ util.createChild(this.bubble_, 'pointer bottom', 'span');
+
+ var bubbleClose = util.createChild(this.bubble_, 'close-x');
+ bubbleClose.addEventListener('click', this.onCloseBubble_.bind(this));
+
+ // Video player controls.
+ this.mediaSpacer_ =
+ util.createChild(this.container_, 'video-controls-spacer');
+ this.mediaToolbar_ = util.createChild(this.mediaSpacer_, 'tool');
+ this.mediaControls_ = new VideoControls(
+ this.mediaToolbar_,
+ this.showErrorBanner_.bind(this, 'GALLERY_VIDEO_ERROR'),
+ this.displayStringFunction_.bind(this),
+ this.toggleFullScreen_.bind(this),
+ this.container_);
+
+ // Ribbon and related controls.
+ this.arrowBox_ = util.createChild(this.container_, 'arrow-box');
+
+ this.arrowLeft_ =
+ util.createChild(this.arrowBox_, 'arrow left tool dimmable');
+ this.arrowLeft_.addEventListener('click',
+ this.advanceManually.bind(this, -1));
+ util.createChild(this.arrowLeft_);
+
+ util.createChild(this.arrowBox_, 'arrow-spacer');
+
+ this.arrowRight_ =
+ util.createChild(this.arrowBox_, 'arrow right tool dimmable');
+ this.arrowRight_.addEventListener('click',
+ this.advanceManually.bind(this, 1));
+ util.createChild(this.arrowRight_);
+
+ this.ribbonSpacer_ = util.createChild(this.toolbar_, 'ribbon-spacer');
+ this.ribbon_ = new Ribbon(this.document_,
+ this.metadataCache_, this.dataModel_, this.selectionModel_);
+ this.ribbonSpacer_.appendChild(this.ribbon_);
+
+ // Error indicator.
+ var errorWrapper = util.createChild(this.container_, 'prompt-wrapper');
+ errorWrapper.setAttribute('pos', 'center');
+
+ this.errorBanner_ = util.createChild(errorWrapper, 'error-banner');
+
+ util.createChild(this.container_, 'spinner');
+
+ var slideShowButton = util.createChild(this.toolbar_,
+ 'button slideshow', 'button');
+ slideShowButton.title = this.displayStringFunction_('GALLERY_SLIDESHOW');
+ slideShowButton.addEventListener('click',
+ this.startSlideshow.bind(this, SlideMode.SLIDESHOW_INTERVAL_FIRST));
+
+ var slideShowToolbar =
+ util.createChild(this.container_, 'tool slideshow-toolbar');
+ util.createChild(slideShowToolbar, 'slideshow-play').
+ addEventListener('click', this.toggleSlideshowPause_.bind(this));
+ util.createChild(slideShowToolbar, 'slideshow-end').
+ addEventListener('click', this.stopSlideshow_.bind(this));
+
+ // Editor.
+
+ this.editButton_ = util.createChild(this.toolbar_, 'button edit', 'button');
+ this.editButton_.title = this.displayStringFunction_('GALLERY_EDIT');
+ this.editButton_.setAttribute('disabled', ''); // Disabled by default.
+ this.editButton_.addEventListener('click', this.toggleEditor.bind(this));
+
+ this.printButton_ = util.createChild(this.toolbar_, 'button print', 'button');
+ this.printButton_.title = this.displayStringFunction_('GALLERY_PRINT');
+ this.printButton_.setAttribute('disabled', ''); // Disabled by default.
+ this.printButton_.addEventListener('click', this.print_.bind(this));
+
+ this.editBarSpacer_ = util.createChild(this.toolbar_, 'edit-bar-spacer');
+ this.editBarMain_ = util.createChild(this.editBarSpacer_, 'edit-main');
+
+ this.editBarMode_ = util.createChild(this.container_, 'edit-modal');
+ this.editBarModeWrapper_ = util.createChild(
+ this.editBarMode_, 'edit-modal-wrapper');
+ this.editBarModeWrapper_.hidden = true;
+
+ // Objects supporting image display and editing.
+ this.viewport_ = new Viewport();
+
+ this.imageView_ = new ImageView(
+ this.imageContainer_,
+ this.viewport_,
+ this.metadataCache_);
+
+ this.editor_ = new ImageEditor(
+ this.viewport_,
+ this.imageView_,
+ this.prompt_,
+ {
+ root: this.container_,
+ image: this.imageContainer_,
+ toolbar: this.editBarMain_,
+ mode: this.editBarModeWrapper_
+ },
+ SlideMode.editorModes,
+ this.displayStringFunction_,
+ this.onToolsVisibilityChanged_.bind(this));
+
+ this.editor_.getBuffer().addOverlay(
+ new SwipeOverlay(this.advanceManually.bind(this)));
+};
+
+/**
+ * Load items, display the selected item.
+ * @param {Rect} zoomFromRect Rectangle for zoom effect.
+ * @param {function} displayCallback Called when the image is displayed.
+ * @param {function} loadCallback Called when the image is displayed.
+ */
+SlideMode.prototype.enter = function(
+ zoomFromRect, displayCallback, loadCallback) {
+ this.sequenceDirection_ = 0;
+ this.sequenceLength_ = 0;
+
+ var loadDone = function(loadType, delay) {
+ this.active_ = true;
+
+ this.selectionModel_.addEventListener('change', this.onSelectionBound_);
+ this.dataModel_.addEventListener('splice', this.onSpliceBound_);
+ this.dataModel_.addEventListener('content', this.onContentBound_);
+
+ ImageUtil.setAttribute(this.arrowBox_, 'active', this.getItemCount_() > 1);
+ this.ribbon_.enable();
+
+ // Wait 1000ms after the animation is done, then prefetch the next image.
+ this.requestPrefetch(1, delay + 1000);
+
+ if (loadCallback) loadCallback();
+ }.bind(this);
+
+ // The latest |leave| call might have left the image animating. Remove it.
+ this.unloadImage_();
+
+ if (this.getItemCount_() === 0) {
+ this.displayedIndex_ = -1;
+ //TODO(kaznacheev) Show this message in the grid mode too.
+ this.showErrorBanner_('GALLERY_NO_IMAGES');
+ loadDone();
+ } else {
+ // Remember the selection if it is empty or multiple. It will be restored
+ // in |leave| if the user did not changing the selection manually.
+ var currentSelection = this.selectionModel_.selectedIndexes;
+ if (currentSelection.length === 1)
+ this.savedSelection_ = null;
+ else
+ this.savedSelection_ = currentSelection;
+
+ // Ensure valid single selection.
+ // Note that the SlideMode object is not listening to selection change yet.
+ this.select(Math.max(0, this.getSelectedIndex()));
+ this.displayedIndex_ = this.getSelectedIndex();
+
+ var selectedItem = this.getSelectedItem();
+ // Show the selected item ASAP, then complete the initialization
+ // (loading the ribbon thumbnails can take some time).
+ this.metadataCache_.get(selectedItem.getEntry(), Gallery.METADATA_TYPE,
+ function(metadata) {
+ this.loadItem_(selectedItem.getEntry(), metadata,
+ zoomFromRect && this.imageView_.createZoomEffect(zoomFromRect),
+ displayCallback, loadDone);
+ }.bind(this));
+
+ }
+};
+
+/**
+ * Leave the mode.
+ * @param {Rect} zoomToRect Rectangle for zoom effect.
+ * @param {function} callback Called when the image is committed and
+ * the zoom-out animation has started.
+ */
+SlideMode.prototype.leave = function(zoomToRect, callback) {
+ var commitDone = function() {
+ this.stopEditing_();
+ this.stopSlideshow_();
+ ImageUtil.setAttribute(this.arrowBox_, 'active', false);
+ this.selectionModel_.removeEventListener(
+ 'change', this.onSelectionBound_);
+ this.dataModel_.removeEventListener('splice', this.onSpliceBound_);
+ this.dataModel_.removeEventListener('content', this.onContentBound_);
+ this.ribbon_.disable();
+ this.active_ = false;
+ if (this.savedSelection_)
+ this.selectionModel_.selectedIndexes = this.savedSelection_;
+ this.unloadImage_(zoomToRect);
+ callback();
+ }.bind(this);
+
+ if (this.getItemCount_() === 0) {
+ this.showErrorBanner_(false);
+ commitDone();
+ } else {
+ this.commitItem_(commitDone);
+ }
+
+ // Disable the slide-mode only buttons when leaving.
+ this.editButton_.setAttribute('disabled', '');
+ this.printButton_.setAttribute('disabled', '');
+};
+
+
+/**
+ * Execute an action when the editor is not busy.
+ *
+ * @param {function} action Function to execute.
+ */
+SlideMode.prototype.executeWhenReady = function(action) {
+ this.editor_.executeWhenReady(action);
+};
+
+/**
+ * @return {boolean} True if the mode has active tools (that should not fade).
+ */
+SlideMode.prototype.hasActiveTool = function() {
+ return this.isEditing();
+};
+
+/**
+ * @return {number} Item count.
+ * @private
+ */
+SlideMode.prototype.getItemCount_ = function() {
+ return this.dataModel_.length;
+};
+
+/**
+ * @param {number} index Index.
+ * @return {Gallery.Item} Item.
+ */
+SlideMode.prototype.getItem = function(index) {
+ return this.dataModel_.item(index);
+};
+
+/**
+ * @return {Gallery.Item} Selected index.
+ */
+SlideMode.prototype.getSelectedIndex = function() {
+ return this.selectionModel_.selectedIndex;
+};
+
+/**
+ * @return {Rect} Screen rectangle of the selected image.
+ */
+SlideMode.prototype.getSelectedImageRect = function() {
+ if (this.getSelectedIndex() < 0)
+ return null;
+ else
+ return this.viewport_.getScreenClipped();
+};
+
+/**
+ * @return {Gallery.Item} Selected item.
+ */
+SlideMode.prototype.getSelectedItem = function() {
+ return this.getItem(this.getSelectedIndex());
+};
+
+/**
+ * Toggles the full screen mode.
+ * @private
+ */
+SlideMode.prototype.toggleFullScreen_ = function() {
+ util.toggleFullScreen(this.context_.appWindow,
+ !util.isFullScreen(this.context_.appWindow));
+};
+
+/**
+ * Selection change handler.
+ *
+ * Commits the current image and displays the newly selected image.
+ * @private
+ */
+SlideMode.prototype.onSelection_ = function() {
+ if (this.selectionModel_.selectedIndexes.length === 0)
+ return; // Temporary empty selection.
+
+ // Forget the saved selection if the user changed the selection manually.
+ if (!this.isSlideshowOn_())
+ this.savedSelection_ = null;
+
+ if (this.getSelectedIndex() === this.displayedIndex_)
+ return; // Do not reselect.
+
+ this.commitItem_(this.loadSelectedItem_.bind(this));
+};
+
+/**
+ * Handles changes in tools visibility, and if the header is dimmed, then
+ * requests disabling the draggable app region.
+ *
+ * @private
+ */
+SlideMode.prototype.onToolsVisibilityChanged_ = function() {
+ var headerDimmed =
+ this.document_.querySelector('.header').hasAttribute('dimmed');
+ this.context_.onAppRegionChanged(!headerDimmed);
+};
+
+/**
+ * Change the selection.
+ *
+ * @param {number} index New selected index.
+ * @param {number=} opt_slideHint Slide animation direction (-1|1).
+ */
+SlideMode.prototype.select = function(index, opt_slideHint) {
+ this.slideHint_ = opt_slideHint;
+ this.selectionModel_.selectedIndex = index;
+ this.selectionModel_.leadIndex = index;
+};
+
+/**
+ * Load the selected item.
+ *
+ * @private
+ */
+SlideMode.prototype.loadSelectedItem_ = function() {
+ var slideHint = this.slideHint_;
+ this.slideHint_ = undefined;
+
+ var index = this.getSelectedIndex();
+ if (index === this.displayedIndex_)
+ return; // Do not reselect.
+
+ var step = slideHint || (index - this.displayedIndex_);
+
+ if (Math.abs(step) != 1) {
+ // Long leap, the sequence is broken, we have no good prefetch candidate.
+ this.sequenceDirection_ = 0;
+ this.sequenceLength_ = 0;
+ } else if (this.sequenceDirection_ === step) {
+ // Keeping going in sequence.
+ this.sequenceLength_++;
+ } else {
+ // Reversed the direction. Reset the counter.
+ this.sequenceDirection_ = step;
+ this.sequenceLength_ = 1;
+ }
+
+ if (this.sequenceLength_ <= 1) {
+ // We have just broke the sequence. Touch the current image so that it stays
+ // in the cache longer.
+ this.imageView_.prefetch(this.imageView_.contentEntry_);
+ }
+
+ this.displayedIndex_ = index;
+
+ function shouldPrefetch(loadType, step, sequenceLength) {
+ // Never prefetch when selecting out of sequence.
+ if (Math.abs(step) != 1)
+ return false;
+
+ // Never prefetch after a video load (decoding the next image can freeze
+ // the UI for a second or two).
+ if (loadType === ImageView.LOAD_TYPE_VIDEO_FILE)
+ return false;
+
+ // Always prefetch if the previous load was from cache.
+ if (loadType === ImageView.LOAD_TYPE_CACHED_FULL)
+ return true;
+
+ // Prefetch if we have been going in the same direction for long enough.
+ return sequenceLength >= 3;
+ }
+
+ var selectedItem = this.getSelectedItem();
+ this.currentUniqueKey_++;
+ var selectedUniqueKey = this.currentUniqueKey_;
+ var onMetadata = function(metadata) {
+ // Discard, since another load has been invoked after this one.
+ if (selectedUniqueKey != this.currentUniqueKey_) return;
+ this.loadItem_(selectedItem.getEntry(), metadata,
+ new ImageView.Effect.Slide(step, this.isSlideshowPlaying_()),
+ function() {} /* no displayCallback */,
+ function(loadType, delay) {
+ // Discard, since another load has been invoked after this one.
+ if (selectedUniqueKey != this.currentUniqueKey_) return;
+ if (shouldPrefetch(loadType, step, this.sequenceLength_)) {
+ this.requestPrefetch(step, delay);
+ }
+ if (this.isSlideshowPlaying_())
+ this.scheduleNextSlide_();
+ }.bind(this));
+ }.bind(this);
+ this.metadataCache_.get(
+ selectedItem.getEntry(), Gallery.METADATA_TYPE, onMetadata);
+};
+
+/**
+ * Unload the current image.
+ *
+ * @param {Rect} zoomToRect Rectangle for zoom effect.
+ * @private
+ */
+SlideMode.prototype.unloadImage_ = function(zoomToRect) {
+ this.imageView_.unload(zoomToRect);
+ this.container_.removeAttribute('video');
+};
+
+/**
+ * Data model 'splice' event handler.
+ * @param {Event} event Event.
+ * @private
+ */
+SlideMode.prototype.onSplice_ = function(event) {
+ ImageUtil.setAttribute(this.arrowBox_, 'active', this.getItemCount_() > 1);
+
+ // Splice invalidates saved indices, drop the saved selection.
+ this.savedSelection_ = null;
+
+ if (event.removed.length != 1)
+ return;
+
+ // Delay the selection to let the ribbon splice handler work first.
+ setTimeout(function() {
+ if (event.index < this.dataModel_.length) {
+ // There is the next item, select it.
+ // The next item is now at the same index as the removed one, so we need
+ // to correct displayIndex_ so that loadSelectedItem_ does not think
+ // we are re-selecting the same item (and does right-to-left slide-in
+ // animation).
+ this.displayedIndex_ = event.index - 1;
+ this.select(event.index);
+ } else if (this.dataModel_.length) {
+ // Removed item is the rightmost, but there are more items.
+ this.select(event.index - 1); // Select the new last index.
+ } else {
+ // No items left. Unload the image and show the banner.
+ this.commitItem_(function() {
+ this.unloadImage_();
+ this.showErrorBanner_('GALLERY_NO_IMAGES');
+ }.bind(this));
+ }
+ }.bind(this), 0);
+};
+
+/**
+ * @param {number} direction -1 for left, 1 for right.
+ * @return {number} Next index in the given direction, with wrapping.
+ * @private
+ */
+SlideMode.prototype.getNextSelectedIndex_ = function(direction) {
+ function advance(index, limit) {
+ index += (direction > 0 ? 1 : -1);
+ if (index < 0)
+ return limit - 1;
+ if (index === limit)
+ return 0;
+ return index;
+ }
+
+ // If the saved selection is multiple the Slideshow should cycle through
+ // the saved selection.
+ if (this.isSlideshowOn_() &&
+ this.savedSelection_ && this.savedSelection_.length > 1) {
+ var pos = advance(this.savedSelection_.indexOf(this.getSelectedIndex()),
+ this.savedSelection_.length);
+ return this.savedSelection_[pos];
+ } else {
+ return advance(this.getSelectedIndex(), this.getItemCount_());
+ }
+};
+
+/**
+ * Advance the selection based on the pressed key ID.
+ * @param {string} keyID Key identifier.
+ */
+SlideMode.prototype.advanceWithKeyboard = function(keyID) {
+ this.advanceManually(keyID === 'Up' || keyID === 'Left' ? -1 : 1);
+};
+
+/**
+ * Advance the selection as a result of a user action (as opposed to an
+ * automatic change in the slideshow mode).
+ * @param {number} direction -1 for left, 1 for right.
+ */
+SlideMode.prototype.advanceManually = function(direction) {
+ if (this.isSlideshowPlaying_()) {
+ this.pauseSlideshow_();
+ cr.dispatchSimpleEvent(this, 'useraction');
+ }
+ this.selectNext(direction);
+};
+
+/**
+ * Select the next item.
+ * @param {number} direction -1 for left, 1 for right.
+ */
+SlideMode.prototype.selectNext = function(direction) {
+ this.select(this.getNextSelectedIndex_(direction), direction);
+};
+
+/**
+ * Select the first item.
+ */
+SlideMode.prototype.selectFirst = function() {
+ this.select(0);
+};
+
+/**
+ * Select the last item.
+ */
+SlideMode.prototype.selectLast = function() {
+ this.select(this.getItemCount_() - 1);
+};
+
+// Loading/unloading
+
+/**
+ * Load and display an item.
+ *
+ * @param {FileEntry} entry Item entry to be loaded.
+ * @param {Object} metadata Item metadata.
+ * @param {Object} effect Transition effect object.
+ * @param {function} displayCallback Called when the image is displayed
+ * (which can happen before the image load due to caching).
+ * @param {function} loadCallback Called when the image is fully loaded.
+ * @private
+ */
+SlideMode.prototype.loadItem_ = function(
+ entry, metadata, effect, displayCallback, loadCallback) {
+ this.selectedImageMetadata_ = MetadataCache.cloneMetadata(metadata);
+
+ this.showSpinner_(true);
+
+ var loadDone = function(loadType, delay, error) {
+ var video = this.isShowingVideo_();
+ ImageUtil.setAttribute(this.container_, 'video', video);
+
+ this.showSpinner_(false);
+ if (loadType === ImageView.LOAD_TYPE_ERROR) {
+ // if we have a specific error, then display it
+ if (error) {
+ this.showErrorBanner_(error);
+ } else {
+ // otherwise try to infer general error
+ this.showErrorBanner_(
+ video ? 'GALLERY_VIDEO_ERROR' : 'GALLERY_IMAGE_ERROR');
+ }
+ } else if (loadType === ImageView.LOAD_TYPE_OFFLINE) {
+ this.showErrorBanner_(
+ video ? 'GALLERY_VIDEO_OFFLINE' : 'GALLERY_IMAGE_OFFLINE');
+ }
+
+ if (video) {
+ // The editor toolbar does not make sense for video, hide it.
+ this.stopEditing_();
+ this.mediaControls_.attachMedia(this.imageView_.getVideo());
+
+ // TODO(kaznacheev): Add metrics for video playback.
+ } else {
+ ImageUtil.metrics.recordUserAction(ImageUtil.getMetricName('View'));
+
+ var toMillions = function(number) {
+ return Math.round(number / (1000 * 1000));
+ };
+
+ ImageUtil.metrics.recordSmallCount(ImageUtil.getMetricName('Size.MB'),
+ toMillions(metadata.filesystem.size));
+
+ var canvas = this.imageView_.getCanvas();
+ ImageUtil.metrics.recordSmallCount(ImageUtil.getMetricName('Size.MPix'),
+ toMillions(canvas.width * canvas.height));
+
+ var extIndex = entry.name.lastIndexOf('.');
+ var ext = extIndex < 0 ? '' :
+ entry.name.substr(extIndex + 1).toLowerCase();
+ if (ext === 'jpeg') ext = 'jpg';
+ ImageUtil.metrics.recordEnum(
+ ImageUtil.getMetricName('FileType'), ext, ImageUtil.FILE_TYPES);
+ }
+
+ // Enable or disable buttons for editing and printing.
+ if (video || error) {
+ this.editButton_.setAttribute('disabled', '');
+ this.printButton_.setAttribute('disabled', '');
+ } else {
+ this.editButton_.removeAttribute('disabled');
+ this.printButton_.removeAttribute('disabled');
+ }
+
+ // For once edited image, disallow the 'overwrite' setting change.
+ ImageUtil.setAttribute(this.options_, 'saved',
+ !this.getSelectedItem().isOriginal());
+
+ util.platform.getPreference(SlideMode.OVERWRITE_BUBBLE_KEY,
+ function(value) {
+ var times = typeof value === 'string' ? parseInt(value, 10) : 0;
+ if (times < SlideMode.OVERWRITE_BUBBLE_MAX_TIMES) {
+ this.bubble_.hidden = false;
+ if (this.isEditing()) {
+ util.platform.setPreference(
+ SlideMode.OVERWRITE_BUBBLE_KEY, times + 1);
+ }
+ }
+ }.bind(this));
+
+ loadCallback(loadType, delay);
+ }.bind(this);
+
+ var displayDone = function() {
+ cr.dispatchSimpleEvent(this, 'image-displayed');
+ displayCallback();
+ }.bind(this);
+
+ this.editor_.openSession(entry, metadata, effect,
+ this.saveCurrentImage_.bind(this), displayDone, loadDone);
+};
+
+/**
+ * Commit changes to the current item and reset all messages/indicators.
+ *
+ * @param {function} callback Callback.
+ * @private
+ */
+SlideMode.prototype.commitItem_ = function(callback) {
+ this.showSpinner_(false);
+ this.showErrorBanner_(false);
+ this.editor_.getPrompt().hide();
+
+ // Detach any media attached to the controls.
+ if (this.mediaControls_.getMedia())
+ this.mediaControls_.detachMedia();
+
+ // If showing the video, then pause it. Note, that it may not be attached
+ // to the media controls yet.
+ if (this.isShowingVideo_()) {
+ this.imageView_.getVideo().pause();
+ // Force stop downloading, if uncached on Drive.
+ this.imageView_.getVideo().src = '';
+ this.imageView_.getVideo().load();
+ }
+
+ this.editor_.closeSession(callback);
+};
+
+/**
+ * Request a prefetch for the next image.
+ *
+ * @param {number} direction -1 or 1.
+ * @param {number} delay Delay in ms. Used to prevent the CPU-heavy image
+ * loading from disrupting the animation that might be still in progress.
+ */
+SlideMode.prototype.requestPrefetch = function(direction, delay) {
+ if (this.getItemCount_() <= 1) return;
+
+ var index = this.getNextSelectedIndex_(direction);
+ var nextItemEntry = this.getItem(index).getEntry();
+ this.imageView_.prefetch(nextItemEntry, delay);
+};
+
+// Event handlers.
+
+/**
+ * Unload handler, to be called from the top frame.
+ * @param {boolean} exiting True if the app is exiting.
+ */
+SlideMode.prototype.onUnload = function(exiting) {
+ if (this.isShowingVideo_() && this.mediaControls_.isPlaying()) {
+ this.mediaControls_.savePosition(exiting);
+ }
+};
+
+/**
+ * beforeunload handler, to be called from the top frame.
+ * @return {string} Message to show if there are unsaved changes.
+ */
+SlideMode.prototype.onBeforeUnload = function() {
+ if (this.editor_.isBusy())
+ return this.displayStringFunction_('GALLERY_UNSAVED_CHANGES');
+ return null;
+};
+
+/**
+ * Click handler for the image container.
+ *
+ * @param {Event} event Mouse click event.
+ * @private
+ */
+SlideMode.prototype.onClick_ = function(event) {
+ if (!this.isShowingVideo_() || !this.mediaControls_.getMedia())
+ return;
+ if (event.ctrlKey) {
+ this.mediaControls_.toggleLoopedModeWithFeedback(true);
+ if (!this.mediaControls_.isPlaying())
+ this.mediaControls_.togglePlayStateWithFeedback();
+ } else {
+ this.mediaControls_.togglePlayStateWithFeedback();
+ }
+};
+
+/**
+ * Click handler for the entire document.
+ * @param {Event} e Mouse click event.
+ * @private
+ */
+SlideMode.prototype.onDocumentClick_ = function(e) {
+ // Close the bubble if clicked outside of it and if it is visible.
+ if (!this.bubble_.contains(e.target) &&
+ !this.editButton_.contains(e.target) &&
+ !this.arrowLeft_.contains(e.target) &&
+ !this.arrowRight_.contains(e.target) &&
+ !this.bubble_.hidden) {
+ this.bubble_.hidden = true;
+ }
+};
+
+/**
+ * Keydown handler.
+ *
+ * @param {Event} event Event.
+ * @return {boolean} True if handled.
+ */
+SlideMode.prototype.onKeyDown = function(event) {
+ var keyID = util.getKeyModifiers(event) + event.keyIdentifier;
+
+ if (this.isSlideshowOn_()) {
+ switch (keyID) {
+ case 'U+001B': // Escape exits the slideshow.
+ this.stopSlideshow_(event);
+ break;
+
+ case 'U+0020': // Space pauses/resumes the slideshow.
+ this.toggleSlideshowPause_();
+ break;
+
+ case 'Up':
+ case 'Down':
+ case 'Left':
+ case 'Right':
+ this.advanceWithKeyboard(keyID);
+ break;
+ }
+ return true; // Consume all keystrokes in the slideshow mode.
+ }
+
+ if (this.isEditing() && this.editor_.onKeyDown(event))
+ return true;
+
+ switch (keyID) {
+ case 'U+0020': // Space toggles the video playback.
+ if (this.isShowingVideo_() && this.mediaControls_.getMedia())
+ this.mediaControls_.togglePlayStateWithFeedback();
+ break;
+
+ case 'Ctrl-U+0050': // Ctrl+'p' prints the current image.
+ if (!this.printButton_.hasAttribute('disabled'))
+ this.print_();
+ break;
+
+ case 'U+0045': // 'e' toggles the editor.
+ if (!this.editButton_.hasAttribute('disabled'))
+ this.toggleEditor(event);
+ break;
+
+ case 'U+001B': // Escape
+ if (!this.isEditing())
+ return false; // Not handled.
+ this.toggleEditor(event);
+ break;
+
+ case 'Home':
+ this.selectFirst();
+ break;
+ case 'End':
+ this.selectLast();
+ break;
+ case 'Up':
+ case 'Down':
+ case 'Left':
+ case 'Right':
+ this.advanceWithKeyboard(keyID);
+ break;
+
+ default: return false;
+ }
+
+ return true;
+};
+
+/**
+ * Resize handler.
+ * @private
+ */
+SlideMode.prototype.onResize_ = function() {
+ this.viewport_.sizeByFrameAndFit(this.container_);
+ this.viewport_.repaint();
+};
+
+/**
+ * Update thumbnails.
+ */
+SlideMode.prototype.updateThumbnails = function() {
+ this.ribbon_.reset();
+ if (this.active_)
+ this.ribbon_.redraw();
+};
+
+// Saving
+
+/**
+ * Save the current image to a file.
+ *
+ * @param {function} callback Callback.
+ * @private
+ */
+SlideMode.prototype.saveCurrentImage_ = function(callback) {
+ var item = this.getSelectedItem();
+ var oldEntry = item.getEntry();
+ var canvas = this.imageView_.getCanvas();
+
+ this.showSpinner_(true);
+ var metadataEncoder = ImageEncoder.encodeMetadata(
+ this.selectedImageMetadata_.media, canvas, 1 /* quality */);
+
+ this.selectedImageMetadata_ = ContentProvider.ConvertContentMetadata(
+ metadataEncoder.getMetadata(), this.selectedImageMetadata_);
+
+ item.saveToFile(
+ this.context_.saveDirEntry,
+ this.shouldOverwriteOriginal_(),
+ canvas,
+ metadataEncoder,
+ function(success) {
+ // TODO(kaznacheev): Implement write error handling.
+ // Until then pretend that the save succeeded.
+ this.showSpinner_(false);
+ this.flashSavedLabel_();
+
+ var event = new Event('content');
+ event.item = item;
+ event.oldEntry = oldEntry;
+ event.metadata = this.selectedImageMetadata_;
+ this.dataModel_.dispatchEvent(event);
+
+ // Allow changing the 'Overwrite original' setting only if the user
+ // used Undo to restore the original image AND it is not a copy.
+ // Otherwise lock the setting in its current state.
+ var mayChangeOverwrite = !this.editor_.canUndo() && item.isOriginal();
+ ImageUtil.setAttribute(this.options_, 'saved', !mayChangeOverwrite);
+
+ if (this.imageView_.getContentRevision() === 1) { // First edit.
+ ImageUtil.metrics.recordUserAction(ImageUtil.getMetricName('Edit'));
+ }
+
+ if (!util.isSameEntry(oldEntry, item.getEntry())) {
+ this.dataModel_.splice(
+ this.getSelectedIndex(), 0, new Gallery.Item(oldEntry));
+ // The ribbon will ignore the splice above and redraw after the
+ // select call below (while being obscured by the Editor toolbar,
+ // so there is no need for nice animation here).
+ // SlideMode will ignore the selection change as the displayed item
+ // index has not changed.
+ this.select(++this.displayedIndex_);
+ }
+ callback();
+ cr.dispatchSimpleEvent(this, 'image-saved');
+ }.bind(this));
+};
+
+/**
+ * Update caches when the selected item has been renamed.
+ * @param {Event} event Event.
+ * @private
+ */
+SlideMode.prototype.onContentChange_ = function(event) {
+ var newEntry = event.item.getEntry();
+ if (util.isSameEntry(newEntry, event.oldEntry))
+ this.imageView_.changeEntry(newEntry);
+ this.metadataCache_.clear(event.oldEntry, Gallery.METADATA_TYPE);
+};
+
+/**
+ * Flash 'Saved' label briefly to indicate that the image has been saved.
+ * @private
+ */
+SlideMode.prototype.flashSavedLabel_ = function() {
+ var setLabelHighlighted =
+ ImageUtil.setAttribute.bind(null, this.savedLabel_, 'highlighted');
+ setTimeout(setLabelHighlighted.bind(null, true), 0);
+ setTimeout(setLabelHighlighted.bind(null, false), 300);
+};
+
+/**
+ * Local storage key for the 'Overwrite original' setting.
+ * @type {string}
+ */
+SlideMode.OVERWRITE_KEY = 'gallery-overwrite-original';
+
+/**
+ * Local storage key for the number of times that
+ * the overwrite info bubble has been displayed.
+ * @type {string}
+ */
+SlideMode.OVERWRITE_BUBBLE_KEY = 'gallery-overwrite-bubble';
+
+/**
+ * Max number that the overwrite info bubble is shown.
+ * @type {number}
+ */
+SlideMode.OVERWRITE_BUBBLE_MAX_TIMES = 5;
+
+/**
+ * @return {boolean} True if 'Overwrite original' is set.
+ * @private
+ */
+SlideMode.prototype.shouldOverwriteOriginal_ = function() {
+ return this.overwriteOriginal_.checked;
+};
+
+/**
+ * 'Overwrite original' checkbox handler.
+ * @param {Event} event Event.
+ * @private
+ */
+SlideMode.prototype.onOverwriteOriginalClick_ = function(event) {
+ util.platform.setPreference(SlideMode.OVERWRITE_KEY, event.target.checked);
+};
+
+/**
+ * Overwrite info bubble close handler.
+ * @private
+ */
+SlideMode.prototype.onCloseBubble_ = function() {
+ this.bubble_.hidden = true;
+ util.platform.setPreference(SlideMode.OVERWRITE_BUBBLE_KEY,
+ SlideMode.OVERWRITE_BUBBLE_MAX_TIMES);
+};
+
+// Slideshow
+
+/**
+ * Slideshow interval in ms.
+ */
+SlideMode.SLIDESHOW_INTERVAL = 5000;
+
+/**
+ * First slideshow interval in ms. It should be shorter so that the user
+ * is not guessing whether the button worked.
+ */
+SlideMode.SLIDESHOW_INTERVAL_FIRST = 1000;
+
+/**
+ * Empirically determined duration of the fullscreen toggle animation.
+ */
+SlideMode.FULLSCREEN_TOGGLE_DELAY = 500;
+
+/**
+ * @return {boolean} True if the slideshow is on.
+ * @private
+ */
+SlideMode.prototype.isSlideshowOn_ = function() {
+ return this.container_.hasAttribute('slideshow');
+};
+
+/**
+ * Start the slideshow.
+ * @param {number=} opt_interval First interval in ms.
+ * @param {Event=} opt_event Event.
+ */
+SlideMode.prototype.startSlideshow = function(opt_interval, opt_event) {
+ // Set the attribute early to prevent the toolbar from flashing when
+ // the slideshow is being started from the mosaic view.
+ this.container_.setAttribute('slideshow', 'playing');
+
+ if (this.active_) {
+ this.stopEditing_();
+ } else {
+ // We are in the Mosaic mode. Toggle the mode but remember to return.
+ this.leaveAfterSlideshow_ = true;
+ this.toggleMode_(this.startSlideshow.bind(
+ this, SlideMode.SLIDESHOW_INTERVAL, opt_event));
+ return;
+ }
+
+ if (opt_event) // Caused by user action, notify the Gallery.
+ cr.dispatchSimpleEvent(this, 'useraction');
+
+ this.fullscreenBeforeSlideshow_ = util.isFullScreen(this.context_.appWindow);
+ if (!this.fullscreenBeforeSlideshow_) {
+ // Wait until the zoom animation from the mosaic mode is done.
+ setTimeout(this.toggleFullScreen_.bind(this),
+ ImageView.ZOOM_ANIMATION_DURATION);
+ opt_interval = (opt_interval || SlideMode.SLIDESHOW_INTERVAL) +
+ SlideMode.FULLSCREEN_TOGGLE_DELAY;
+ }
+
+ this.resumeSlideshow_(opt_interval);
+};
+
+/**
+ * Stop the slideshow.
+ * @param {Event=} opt_event Event.
+ * @private
+ */
+SlideMode.prototype.stopSlideshow_ = function(opt_event) {
+ if (!this.isSlideshowOn_())
+ return;
+
+ if (opt_event) // Caused by user action, notify the Gallery.
+ cr.dispatchSimpleEvent(this, 'useraction');
+
+ this.pauseSlideshow_();
+ this.container_.removeAttribute('slideshow');
+
+ // Do not restore fullscreen if we exited fullscreen while in slideshow.
+ var fullscreen = util.isFullScreen(this.context_.appWindow);
+ var toggleModeDelay = 0;
+ if (!this.fullscreenBeforeSlideshow_ && fullscreen) {
+ this.toggleFullScreen_();
+ toggleModeDelay = SlideMode.FULLSCREEN_TOGGLE_DELAY;
+ }
+ if (this.leaveAfterSlideshow_) {
+ this.leaveAfterSlideshow_ = false;
+ setTimeout(this.toggleMode_.bind(this), toggleModeDelay);
+ }
+};
+
+/**
+ * @return {boolean} True if the slideshow is playing (not paused).
+ * @private
+ */
+SlideMode.prototype.isSlideshowPlaying_ = function() {
+ return this.container_.getAttribute('slideshow') === 'playing';
+};
+
+/**
+ * Pause/resume the slideshow.
+ * @private
+ */
+SlideMode.prototype.toggleSlideshowPause_ = function() {
+ cr.dispatchSimpleEvent(this, 'useraction'); // Show the tools.
+ if (this.isSlideshowPlaying_()) {
+ this.pauseSlideshow_();
+ } else {
+ this.resumeSlideshow_(SlideMode.SLIDESHOW_INTERVAL_FIRST);
+ }
+};
+
+/**
+ * @param {number=} opt_interval Slideshow interval in ms.
+ * @private
+ */
+SlideMode.prototype.scheduleNextSlide_ = function(opt_interval) {
+ console.assert(this.isSlideshowPlaying_(), 'Inconsistent slideshow state');
+
+ if (this.slideShowTimeout_)
+ clearTimeout(this.slideShowTimeout_);
+
+ this.slideShowTimeout_ = setTimeout(function() {
+ this.slideShowTimeout_ = null;
+ this.selectNext(1);
+ }.bind(this),
+ opt_interval || SlideMode.SLIDESHOW_INTERVAL);
+};
+
+/**
+ * Resume the slideshow.
+ * @param {number=} opt_interval Slideshow interval in ms.
+ * @private
+ */
+SlideMode.prototype.resumeSlideshow_ = function(opt_interval) {
+ this.container_.setAttribute('slideshow', 'playing');
+ this.scheduleNextSlide_(opt_interval);
+};
+
+/**
+ * Pause the slideshow.
+ * @private
+ */
+SlideMode.prototype.pauseSlideshow_ = function() {
+ this.container_.setAttribute('slideshow', 'paused');
+ if (this.slideShowTimeout_) {
+ clearTimeout(this.slideShowTimeout_);
+ this.slideShowTimeout_ = null;
+ }
+};
+
+/**
+ * @return {boolean} True if the editor is active.
+ */
+SlideMode.prototype.isEditing = function() {
+ return this.container_.hasAttribute('editing');
+};
+
+/**
+ * Stop editing.
+ * @private
+ */
+SlideMode.prototype.stopEditing_ = function() {
+ if (this.isEditing())
+ this.toggleEditor();
+};
+
+/**
+ * Activate/deactivate editor.
+ * @param {Event=} opt_event Event.
+ */
+SlideMode.prototype.toggleEditor = function(opt_event) {
+ if (opt_event) // Caused by user action, notify the Gallery.
+ cr.dispatchSimpleEvent(this, 'useraction');
+
+ if (!this.active_) {
+ this.toggleMode_(this.toggleEditor.bind(this));
+ return;
+ }
+
+ this.stopSlideshow_();
+ if (!this.isEditing() && this.isShowingVideo_())
+ return; // No editing for videos.
+
+ ImageUtil.setAttribute(this.container_, 'editing', !this.isEditing());
+
+ if (this.isEditing()) { // isEditing has just been flipped to a new value.
+ if (this.context_.readonlyDirName) {
+ this.editor_.getPrompt().showAt(
+ 'top', 'GALLERY_READONLY_WARNING', 0, this.context_.readonlyDirName);
+ }
+ } else {
+ this.editor_.getPrompt().hide();
+ this.editor_.leaveModeGently();
+ }
+};
+
+/**
+ * Prints the current item.
+ * @private
+ */
+SlideMode.prototype.print_ = function() {
+ cr.dispatchSimpleEvent(this, 'useraction');
+ window.print();
+};
+
+/**
+ * Display the error banner.
+ * @param {string} message Message.
+ * @private
+ */
+SlideMode.prototype.showErrorBanner_ = function(message) {
+ if (message) {
+ this.errorBanner_.textContent = this.displayStringFunction_(message);
+ }
+ ImageUtil.setAttribute(this.container_, 'error', !!message);
+};
+
+/**
+ * Show/hide the busy spinner.
+ *
+ * @param {boolean} on True if show, false if hide.
+ * @private
+ */
+SlideMode.prototype.showSpinner_ = function(on) {
+ if (this.spinnerTimer_) {
+ clearTimeout(this.spinnerTimer_);
+ this.spinnerTimer_ = null;
+ }
+
+ if (on) {
+ this.spinnerTimer_ = setTimeout(function() {
+ this.spinnerTimer_ = null;
+ ImageUtil.setAttribute(this.container_, 'spinner', true);
+ }.bind(this), 1000);
+ } else {
+ ImageUtil.setAttribute(this.container_, 'spinner', false);
+ }
+};
+
+/**
+ * @return {boolean} True if the current item is a video.
+ * @private
+ */
+SlideMode.prototype.isShowingVideo_ = function() {
+ return !!this.imageView_.getVideo();
+};
+
+/**
+ * Overlay that handles swipe gestures. Changes to the next or previous file.
+ * @param {function(number)} callback A callback accepting the swipe direction
+ * (1 means left, -1 right).
+ * @constructor
+ * @implements {ImageBuffer.Overlay}
+ */
+function SwipeOverlay(callback) {
+ this.callback_ = callback;
+}
+
+/**
+ * Inherit ImageBuffer.Overlay.
+ */
+SwipeOverlay.prototype.__proto__ = ImageBuffer.Overlay.prototype;
+
+/**
+ * @param {number} x X pointer position.
+ * @param {number} y Y pointer position.
+ * @param {boolean} touch True if dragging caused by touch.
+ * @return {function} The closure to call on drag.
+ */
+SwipeOverlay.prototype.getDragHandler = function(x, y, touch) {
+ if (!touch)
+ return null;
+ var origin = x;
+ var done = false;
+ return function(x, y) {
+ if (!done && origin - x > SwipeOverlay.SWIPE_THRESHOLD) {
+ this.callback_(1);
+ done = true;
+ } else if (!done && x - origin > SwipeOverlay.SWIPE_THRESHOLD) {
+ this.callback_(-1);
+ done = true;
+ }
+ }.bind(this);
+};
+
+/**
+ * If the user touched the image and moved the finger more than SWIPE_THRESHOLD
+ * horizontally it's considered as a swipe gesture (change the current image).
+ */
+SwipeOverlay.SWIPE_THRESHOLD = 100;
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/scrollbar.js b/chromium/chrome/browser/resources/file_manager/foreground/js/scrollbar.js
new file mode 100644
index 00000000000..a81fe8db12f
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/scrollbar.js
@@ -0,0 +1,294 @@
+// Copyright (c) 2013 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.
+
+'use strict';
+
+/**
+ * Creates a new scroll bar element.
+ * @extends {HTMLDivElement}
+ * @constructor
+ */
+var ScrollBar = cr.ui.define('div');
+
+/**
+ * Mode of the scrollbar. As for now, only vertical scrollbars are supported.
+ * @type {number}
+ */
+ScrollBar.Mode = {
+ VERTICAL: 0,
+ HORIZONTAL: 1
+};
+
+ScrollBar.prototype = {
+ set mode(value) {
+ this.mode_ = value;
+ if (this.mode_ == ScrollBar.Mode.VERTICAL) {
+ this.classList.remove('scrollbar-horizontal');
+ this.classList.add('scrollbar-vertical');
+ } else {
+ this.classList.remove('scrollbar-vertical');
+ this.classList.add('scrollbar-horizontal');
+ }
+ this.redraw_();
+ },
+ get mode() {
+ return this.mode_;
+ }
+};
+
+/**
+ * Inherits after HTMLDivElement.
+ */
+ScrollBar.prototype.__proto__ = HTMLDivElement.prototype;
+
+/**
+ * Initializes the DOM structure of the scrollbar.
+ */
+ScrollBar.prototype.decorate = function() {
+ this.classList.add('scrollbar');
+ this.button_ = util.createChild(this, 'scrollbar-button', 'div');
+ this.mode = ScrollBar.Mode.VERTICAL;
+
+ this.button_.addEventListener('mousedown',
+ this.onButtonPressed_.bind(this));
+ window.addEventListener('mouseup', this.onMouseUp_.bind(this));
+ window.addEventListener('mousemove', this.onMouseMove_.bind(this));
+};
+
+/**
+ * Initialize a scrollbar.
+ *
+ * @param {Element} parent Parent element, must have a relative or absolute
+ * positioning.
+ * @param {Element=} opt_scrollableArea Element with scrollable contents.
+ * If not passed, then call attachToView manually when the scrollable
+ * element becomes available.
+ */
+ScrollBar.prototype.initialize = function(parent, opt_scrollableArea) {
+ parent.appendChild(this);
+ if (opt_scrollableArea)
+ this.attachToView(opt_scrollableArea);
+};
+
+/**
+ * Attaches the scrollbar to a scrollable element and attaches handlers.
+ * @param {Element} view Scrollable element.
+ */
+ScrollBar.prototype.attachToView = function(view) {
+ this.view_ = view;
+ this.view_.addEventListener('scroll', this.onScroll_.bind(this));
+ this.view_.addEventListener('relayout', this.onRelayout_.bind(this));
+ this.domObserver_ = new MutationObserver(this.onDomChanged_.bind(this));
+ this.domObserver_.observe(this.view_, {subtree: true, attributes: true});
+ this.onRelayout_();
+};
+
+/**
+ * Scroll handler.
+ * @private
+ */
+ScrollBar.prototype.onScroll_ = function() {
+ this.scrollTop_ = this.view_.scrollTop;
+ this.redraw_();
+};
+
+/**
+ * Relayout handler.
+ * @private
+ */
+ScrollBar.prototype.onRelayout_ = function() {
+ this.scrollHeight_ = this.view_.scrollHeight;
+ this.clientHeight_ = this.view_.clientHeight;
+ this.offsetTop_ = this.view_.offsetTop;
+ this.scrollTop_ = this.view_.scrollTop;
+ this.redraw_();
+};
+
+/**
+ * Pressing on the scrollbar's button handler.
+ *
+ * @param {Event} event Pressing event.
+ * @private
+ */
+ScrollBar.prototype.onButtonPressed_ = function(event) {
+ this.buttonPressed_ = true;
+ this.buttonPressedEvent_ = event;
+ this.buttonPressedPosition_ = this.button_.offsetTop - this.view_.offsetTop;
+ this.button_.classList.add('pressed');
+
+ event.preventDefault();
+};
+
+/**
+ * Releasing the button handler. Note, that it may not be called when releasing
+ * outside of the window. Therefore this is also called from onMouseMove_.
+ *
+ * @param {Event} event Mouse event.
+ * @private
+ */
+ScrollBar.prototype.onMouseUp_ = function(event) {
+ this.buttonPressed_ = false;
+ this.button_.classList.remove('pressed');
+};
+
+/**
+ * Mouse move handler. Updates the scroll position.
+ *
+ * @param {Event} event Mouse event.
+ * @private
+ */
+ScrollBar.prototype.onMouseMove_ = function(event) {
+ if (!this.buttonPressed_)
+ return;
+ if (!event.which) {
+ this.onMouseUp_(event);
+ return;
+ }
+ var clientSize = this.getClientHeight();
+ var totalSize = this.getTotalHeight();
+ // TODO(hirono): Fix the geometric calculation. crbug.com/253779
+ var buttonSize = Math.max(50, clientSize / totalSize * clientSize);
+ var buttonPosition = this.buttonPressedPosition_ +
+ (event.screenY - this.buttonPressedEvent_.screenY);
+ // Ensures the scrollbar is in the view.
+ buttonPosition =
+ Math.max(0, Math.min(buttonPosition, clientSize - buttonSize));
+ var scrollPosition;
+ if (clientSize > buttonSize) {
+ scrollPosition = Math.max(totalSize - clientSize, 0) *
+ buttonPosition / (clientSize - buttonSize);
+ } else {
+ scrollPosition = 0;
+ }
+
+ this.scrollTop_ = scrollPosition;
+ this.view_.scrollTop = scrollPosition;
+ this.redraw_();
+};
+
+/**
+ * Handles changed in Dom by redrawing the scrollbar. Ignores consecutive calls.
+ * @private
+ */
+ScrollBar.prototype.onDomChanged_ = function() {
+ if (this.domChangedTimer_) {
+ clearTimeout(this.domChangedTimer_);
+ this.domChangedTimer_ = null;
+ }
+ this.domChangedTimer_ = setTimeout(function() {
+ this.onRelayout_();
+ this.domChangedTimer_ = null;
+ }.bind(this), 50);
+};
+
+/**
+ * Redraws the scrollbar.
+ * @private
+ */
+ScrollBar.prototype.redraw_ = function() {
+ if (!this.view_)
+ return;
+
+ var clientSize = this.getClientHeight();
+ var clientTop = this.offsetTop_;
+ var scrollPosition = this.scrollTop_;
+ var totalSize = this.getTotalHeight();
+ var hidden = totalSize <= clientSize;
+
+ var buttonSize = Math.max(50, clientSize / totalSize * clientSize);
+ var buttonPosition;
+ if (clientSize - buttonSize > 0) {
+ buttonPosition = scrollPosition / (totalSize - clientSize) *
+ (clientSize - buttonSize);
+ } else {
+ buttonPosition = 0;
+ }
+ var buttonTop = buttonPosition + clientTop;
+
+ var time = Date.now();
+ if (this.hidden != hidden ||
+ this.lastButtonTop_ != buttonTop ||
+ this.lastButtonSize_ != buttonSize) {
+ requestAnimationFrame(function() {
+ this.hidden = hidden;
+ this.button_.style.top = buttonTop + 'px';
+ this.button_.style.height = buttonSize + 'px';
+ }.bind(this));
+ }
+
+ this.lastButtonTop_ = buttonTop;
+ this.lastButtonSize_ = buttonSize;
+};
+
+/**
+ * Returns the viewport height of the view.
+ * @return {number} The viewport height of the view in px.
+ * @protected
+ */
+ScrollBar.prototype.getClientHeight = function() {
+ return this.clientHeight_;
+};
+
+/**
+ * Returns the total height of the view.
+ * @return {number} The total height of the view in px.
+ * @protected
+ */
+ScrollBar.prototype.getTotalHeight = function() {
+ return this.scrollHeight_;
+};
+
+/**
+ * Creates a new scroll bar for elements in the main panel.
+ * @extends {ScrollBar}
+ * @constructor
+ */
+var MainPanelScrollBar = cr.ui.define('div');
+
+/**
+ * Inherits after ScrollBar.
+ */
+MainPanelScrollBar.prototype.__proto__ = ScrollBar.prototype;
+
+/** @override */
+MainPanelScrollBar.prototype.decorate = function() {
+ ScrollBar.prototype.decorate.call(this);
+
+ /**
+ * Margin for the transparent preview panel at the bottom.
+ * @type {number}
+ * @private
+ */
+ this.bottomMarginForPanel_ = 0;
+};
+
+/**
+ * GReturns the viewport height of the view, considering the preview panel.
+ *
+ * @return {number} The viewport height of the view in px.
+ * @override
+ * @protected
+ */
+MainPanelScrollBar.prototype.getClientHeight = function() {
+ return this.clientHeight_ - this.bottomMarginForPanel_;
+};
+
+/**
+ * Returns the total height of the view, considering the preview panel.
+ *
+ * @return {number} The total height of the view in px.
+ * @override
+ * @protected
+ */
+MainPanelScrollBar.prototype.getTotalHeight = function() {
+ return this.scrollHeight_ - this.bottomMarginForPanel_;
+};
+
+/**
+ * Sets the bottom margin height of the view for the transparent preview panel.
+ * @param {number} margin Margin to be set in px.
+ */
+MainPanelScrollBar.prototype.setBottomMarginForPanel = function(margin) {
+ this.bottomMarginForPanel_ = margin;
+};
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/share_client.js b/chromium/chrome/browser/resources/file_manager/foreground/js/share_client.js
new file mode 100644
index 00000000000..3044b21a17e
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/share_client.js
@@ -0,0 +1,188 @@
+// Copyright 2013 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.
+
+'use strict';
+
+/**
+ * @param {WebView} webView Web View tag.
+ * @param {string} url Share Url for an entry.
+ * @param {ShareClient.Observer} observer Observer instance.
+ * @constructor
+ */
+function ShareClient(webView, url, observer) {
+ this.webView_ = webView;
+ this.url_ = url;
+ this.observer_ = observer;
+ this.loaded_ = false;
+ this.loading_ = false;
+ this.onMessageBound_ = this.onMessage_.bind(this);
+ this.onLoadStopBound_ = this.onLoadStop_.bind(this);
+ this.onLoadAbortBound_ = this.onLoadAbort_.bind(this);
+}
+
+/**
+ * Source origin of the client.
+ * @type {string}
+ * @const
+ */
+ShareClient.SHARE_ORIGIN =
+ 'chrome-extension://hhaomjibdihmijegdhdafkllkbggdgoj';
+
+/**
+ * Target origin of the embedded dialog.
+ * @type {string}
+ * @const
+ */
+ShareClient.SHARE_TARGET = 'https://drive.google.com';
+
+/**
+ * Observes for state changes of the embedded dialog.
+ * @interface
+ */
+ShareClient.Observer = function() {
+};
+
+/**
+ * Notifies about the embedded dialog being loaded.
+ */
+ShareClient.Observer.prototype.onLoaded = function() {
+};
+
+/**
+ * Notifies when the the embedded dialog failed to load.
+ */
+ShareClient.Observer.prototype.onLoadingFailed = function() {
+};
+
+/**
+ * Notifies about changed dimensions of the embedded dialog.
+ * @param {number} width Width in pixels.
+ * @param {number} height Height in pixels.
+ * @param {function()} callback Completion callback. Call when finished
+ * handling the resize.
+ */
+ShareClient.Observer.prototype.onResized = function(width, height, callback) {
+};
+
+/**
+ * Notifies about the embedded dialog being closed.
+ */
+ShareClient.Observer.prototype.onClosed = function() {
+};
+
+/**
+ * Handles messages from the embedded dialog.
+ * @param {Event} e Message event.
+ * @private
+ */
+ShareClient.prototype.onMessage_ = function(e) {
+ if (e.origin != ShareClient.SHARE_TARGET && !window.IN_TEST) {
+ // Logs added temporarily to track crbug.com/288783.
+ console.debug('Received a message from an illegal origin: ' + e.origin);
+ return;
+ }
+
+ var data = JSON.parse(e.data);
+ // Logs added temporarily to track crbug.com/288783.
+ console.debug('Received message: ' + data.type);
+
+ switch (data.type) {
+ case 'resize':
+ this.observer_.onResized(data.args.width,
+ data.args.height,
+ this.postMessage_.bind(this, 'resizeComplete'));
+ break;
+ case 'prepareForVisible':
+ this.postMessage_('prepareComplete');
+ if (!this.loaded_) {
+ this.loading_ = false;
+ this.loaded_ = true;
+ this.observer_.onLoaded();
+ }
+ break;
+ case 'setVisible':
+ if (!data.args.visible)
+ this.observer_.onClosed();
+ break;
+ }
+};
+
+/**
+ * Handles completion of the web view request.
+ * @param {Event} e Message event.
+ * @private
+ */
+ShareClient.prototype.onLoadStop_ = function(e) {
+ // Logs added temporarily to track crbug.com/288783.
+ console.debug('Web View loaded.');
+
+ this.postMessage_('makeBodyVisible');
+};
+
+/**
+ * Handles termination of the web view request.
+ * @param {Event} e Message event.
+ * @private
+ */
+ShareClient.prototype.onLoadAbort_ = function(e) {
+ // Logs added temporarily to track crbug.com/288783.
+ console.debug('Web View failed to load with error: ' + e.reason + ', url: ' +
+ e.url + ' while requested: ' + this.url_);
+
+ this.observer_.onLoadFailed();
+};
+
+/**
+ * Sends a message to the embedded dialog.
+ * @param {string} type Message type.
+ * @param {Object=} opt_args Optional arguments.
+ * @private
+ */
+ShareClient.prototype.postMessage_ = function(type, opt_args) {
+ // Logs added temporarily to track crbug.com/288783.
+ console.debug('Sending message: ' + type);
+
+ var message = {
+ type: type,
+ args: opt_args
+ };
+ this.webView_.contentWindow.postMessage(
+ JSON.stringify(message),
+ !window.IN_TEST ? ShareClient.SHARE_TARGET : '*');
+};
+
+/**
+ * Loads the embedded dialog. Can be called only one.
+ */
+ShareClient.prototype.load = function() {
+ if (this.loading_ || this.loaded_)
+ throw new Error('Already loaded.');
+ this.loading_ = true;
+
+ // Logs added temporarily to track crbug.com/288783.
+ console.debug('Loading.');
+
+ window.addEventListener('message', this.onMessageBound_);
+ this.webView_.addEventListener('loadstop', this.onLoadStopBound_);
+ this.webView_.addEventListener('loadabort', this.onLoadAbortBound_);
+ this.webView_.setAttribute('src', this.url_);
+};
+
+/**
+ * Aborts loading of the embedded dialog and performs cleanup.
+ */
+ShareClient.prototype.abort = function() {
+ window.removeEventListener('message', this.onMessageBound_);
+ this.webView_.removeEventListener('loadstop', this.onLoadStopBound_);
+ this.webView_.removeEventListener(
+ 'loadabort', this.onLoadAbortBound_);
+ this.webView_.stop();
+};
+
+/**
+ * Cleans the dialog by removing all handlers.
+ */
+ShareClient.prototype.dispose = function() {
+ this.abort();
+};
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/share_dialog.js b/chromium/chrome/browser/resources/file_manager/foreground/js/share_dialog.js
new file mode 100644
index 00000000000..ff7fbf6b665
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/share_dialog.js
@@ -0,0 +1,314 @@
+// Copyright 2013 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.
+
+'use strict';
+
+/**
+ * @param {HTMLElement} parentNode Node to be parent for this dialog.
+ * @constructor
+ * @extends {FileManagerDialogBase}
+ * @implements {ShareClient.Observer}
+ */
+function ShareDialog(parentNode) {
+ this.queue_ = new AsyncUtil.Queue();
+ this.onQueueTaskFinished_ = null;
+ this.shareClient_ = null;
+ this.spinner_ = null;
+ this.spinnerLayer_ = null;
+ this.webViewWrapper_ = null;
+ this.webView_ = null;
+ this.failureTimeout_ = null;
+ this.callback_ = null;
+
+ FileManagerDialogBase.call(this, parentNode);
+}
+
+/**
+ * Timeout for loading the share dialog before giving up.
+ * @type {number}
+ * @const
+ */
+ShareDialog.FAILURE_TIMEOUT = 10000;
+
+/**
+ * The result of opening the dialog.
+ * @enum {string}
+ * @const
+ */
+ShareDialog.Result = Object.freeze({
+ // The dialog is closed normally. This includes user cancel.
+ SUCCESS: 'success',
+ // The dialog is closed by network error.
+ NETWORK_ERROR: 'networkError',
+ // The dialog is not opened because it is already showing.
+ ALREADY_SHOWING: 'alreadyShowing'
+});
+
+/**
+ * Wraps a Web View element and adds authorization headers to it.
+ * @param {string} urlPattern Pattern of urls to be authorized.
+ * @param {WebView} webView Web View element to be wrapped.
+ * @constructor
+ */
+ShareDialog.WebViewAuthorizer = function(urlPattern, webView) {
+ this.urlPattern_ = urlPattern;
+ this.webView_ = webView;
+ this.initialized_ = false;
+ this.accessToken_ = null;
+};
+
+/**
+ * Initializes the web view by installing hooks injecting the authorization
+ * headers.
+ * @param {function()} callback Completion callback.
+ */
+ShareDialog.WebViewAuthorizer.prototype.initialize = function(callback) {
+ if (this.initialized_) {
+ callback();
+ return;
+ }
+
+ var registerInjectionHooks = function() {
+ this.webView_.removeEventListener('loadstop', registerInjectionHooks);
+ this.webView_.request.onBeforeSendHeaders.addListener(
+ this.authorizeRequest_.bind(this),
+ {urls: [this.urlPattern_]},
+ ['blocking', 'requestHeaders']);
+ this.initialized_ = true;
+ callback();
+ }.bind(this);
+
+ this.webView_.addEventListener('loadstop', registerInjectionHooks);
+ this.webView_.setAttribute('src', 'data:text/html,');
+};
+
+/**
+ * Authorizes the web view by fetching the freshest access tokens.
+ * @param {function()} callback Completion callback.
+ */
+ShareDialog.WebViewAuthorizer.prototype.authorize = function(callback) {
+ // Fetch or update the access token.
+ chrome.fileBrowserPrivate.requestAccessToken(false, // force_refresh
+ function(inAccessToken) {
+ this.accessToken_ = inAccessToken;
+ callback();
+ }.bind(this));
+};
+
+/**
+ * Injects headers into the passed request.
+ * @param {Event} e Request event.
+ * @return {{requestHeaders: HttpHeaders}} Modified headers.
+ * @private
+ */
+ShareDialog.WebViewAuthorizer.prototype.authorizeRequest_ = function(e) {
+ e.requestHeaders.push({
+ name: 'Authorization',
+ value: 'Bearer ' + this.accessToken_
+ });
+ return {requestHeaders: e.requestHeaders};
+};
+
+ShareDialog.prototype = {
+ __proto__: FileManagerDialogBase.prototype
+};
+
+/**
+ * One-time initialization of DOM.
+ * @private
+ */
+ShareDialog.prototype.initDom_ = function() {
+ FileManagerDialogBase.prototype.initDom_.call(this);
+ this.frame_.classList.add('share-dialog-frame');
+
+ this.spinnerLayer_ = this.document_.createElement('div');
+ this.spinnerLayer_.className = 'spinner-layer';
+ this.frame_.appendChild(this.spinnerLayer_);
+
+ this.webViewWrapper_ = this.document_.createElement('div');
+ this.webViewWrapper_.className = 'share-dialog-webview-wrapper';
+ this.cancelButton_.hidden = true;
+ this.okButton_.hidden = true;
+ this.frame_.insertBefore(this.webViewWrapper_,
+ this.frame_.querySelector('.cr-dialog-buttons'));
+};
+
+/**
+ * @override
+ */
+ShareDialog.prototype.onResized = function(width, height, callback) {
+ if (width && height) {
+ this.webViewWrapper_.style.width = width + 'px';
+ this.webViewWrapper_.style.height = height + 'px';
+ this.webView_.style.width = width + 'px';
+ this.webView_.style.height = height + 'px';
+ }
+ setTimeout(callback, 0);
+};
+
+/**
+ * @override
+ */
+ShareDialog.prototype.onClosed = function() {
+ this.hide();
+};
+
+/**
+ * @override
+ */
+ShareDialog.prototype.onLoaded = function() {
+ if (this.failureTimeout_) {
+ clearTimeout(this.failureTimeout_);
+ this.failureTimeout_ = null;
+ }
+
+ // Logs added temporarily to track crbug.com/288783.
+ console.debug('Loaded.');
+
+ this.okButton_.hidden = false;
+ this.spinnerLayer_.hidden = true;
+ this.webViewWrapper_.classList.add('loaded');
+ this.webView_.focus();
+};
+
+/**
+ * @override
+ */
+ShareDialog.prototype.onLoadFailed = function() {
+ this.hideWithResult(ShareDialog.Result.NETWORK_ERROR);
+};
+
+/**
+ * @override
+ */
+ShareDialog.prototype.hide = function(opt_onHide) {
+ this.hideWithResult(ShareDialog.Result.SUCCESS, opt_onHide);
+};
+
+/**
+ * Hide the dialog with the result and the callback.
+ * @param {ShareDialog.Result} result Result passed to the closing callback.
+ * @param {function()=} opt_onHide Callback called at the end of hiding.
+ */
+ShareDialog.prototype.hideWithResult = function(result, opt_onHide) {
+ if (!this.isShowing())
+ return;
+
+ if (this.shareClient_) {
+ this.shareClient_.dispose();
+ this.shareClient_ = null;
+ }
+
+ this.webViewWrapper_.textContent = '';
+ if (this.failureTimeout_) {
+ clearTimeout(this.failureTimeout_);
+ this.failureTimeout_ = null;
+ }
+
+ FileManagerDialogBase.prototype.hide.call(
+ this,
+ function() {
+ if (opt_onHide)
+ opt_onHide();
+ this.callback_(result);
+ this.callback_ = null;
+ }.bind(this));
+};
+
+/**
+ * Shows the dialog.
+ * @param {FileEntry} entry Entry to share.
+ * @param {function(boolean)} callback Callback to be called when the showing
+ * task is completed. The argument is whether to succeed or not. Note that
+ * cancel is regarded as success.
+ */
+ShareDialog.prototype.show = function(entry, callback) {
+ // If the dialog is already showing, return the error.
+ if (this.isShowing()) {
+ callback(ShareDialog.Result.ALREADY_SHOWING);
+ return;
+ }
+
+ // Initialize the variables.
+ this.callback_ = callback;
+ this.spinnerLayer_.hidden = false;
+ this.webViewWrapper_.style.width = '';
+ this.webViewWrapper_.style.height = '';
+
+ // If the embedded share dialog is not started within some time, then
+ // give up and show an error message.
+ this.failureTimeout_ = setTimeout(function() {
+ this.hideWithResult(ShareDialog.Result.NETWORK_ERROR);
+
+ // Logs added temporarily to track crbug.com/288783.
+ console.debug('Timeout. Web View points at: ' + this.webView_.src);
+ }.bind(this), ShareDialog.FAILURE_TIMEOUT);
+
+ // TODO(mtomasz): Move to initDom_() once and reuse <webview> once it gets
+ // fixed. See: crbug.com/260622.
+ this.webView_ = util.createChild(
+ this.webViewWrapper_, 'share-dialog-webview', 'webview');
+ this.webView_.setAttribute('tabIndex', '-1');
+ this.webViewAuthorizer_ = new ShareDialog.WebViewAuthorizer(
+ !window.IN_TEST ? (ShareClient.SHARE_TARGET + '/*') : '<all_urls>',
+ this.webView_);
+ this.webView_.addEventListener('newwindow', function(e) {
+ // Discard the window object and reopen in an external window.
+ e.window.discard();
+ util.visitURL(e.targetUrl);
+ e.preventDefault();
+ });
+ var show = FileManagerDialogBase.prototype.showBlankDialog.call(this);
+ if (!show) {
+ // The code shoundn't get here, since already-showing was handled before.
+ console.error('ShareDialog can\'t be shown.');
+ return;
+ }
+
+ // Initialize and authorize the Web View tag asynchronously.
+ var group = new AsyncUtil.Group();
+
+ // Fetches an url to the sharing dialog.
+ var shareUrl;
+ group.add(function(inCallback) {
+ chrome.fileBrowserPrivate.getShareUrl(
+ entry.toURL(),
+ function(inShareUrl) {
+ if (!chrome.runtime.lastError)
+ shareUrl = inShareUrl;
+ inCallback();
+ });
+ });
+ group.add(this.webViewAuthorizer_.initialize.bind(this.webViewAuthorizer_));
+ group.add(this.webViewAuthorizer_.authorize.bind(this.webViewAuthorizer_));
+
+ // Loads the share widget once all the previous async calls are finished.
+ group.run(function() {
+ // If the url is not obtained, return the network error.
+ if (!shareUrl) {
+ // Logs added temporarily to track crbug.com/288783.
+ console.debug('URL not available.');
+
+ this.hideWithResult(ShareDialog.Result.NETWORK_ERROR);
+ return;
+ }
+ // Already inactive, therefore ignore.
+ if (!this.isShowing())
+ return;
+ this.shareClient_ = new ShareClient(this.webView_,
+ shareUrl,
+ this);
+ this.shareClient_.load();
+ }.bind(this));
+};
+
+/**
+ * Tells whether the share dialog is showing or not.
+ *
+ * @return {boolean} True since the show method is called and until the closing
+ * callback is invoked.
+ */
+ShareDialog.prototype.isShowing = function() {
+ return !!this.callback_;
+};
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/suggest_apps_dialog.js b/chromium/chrome/browser/resources/file_manager/foreground/js/suggest_apps_dialog.js
new file mode 100644
index 00000000000..27d12dfe19b
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/suggest_apps_dialog.js
@@ -0,0 +1,554 @@
+// Copyright (c) 2013 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.
+
+'use strict';
+
+/**
+ * SuggestAppsDialog contains a list box to select an app to be opened the file
+ * with. This dialog should be used as action picker for file operations.
+ */
+
+/**
+ * The width of the widget (in pixel).
+ * @type {number}
+ * @const
+ */
+var WEBVIEW_WIDTH = 735;
+/**
+ * The height of the widget (in pixel).
+ * @type {number}
+ * @const
+ */
+var WEBVIEW_HEIGHT = 480;
+
+/**
+ * The URL of the widget.
+ * @type {string}
+ * @const
+ */
+var CWS_WIDGET_URL =
+ 'https://clients5.google.com/webstore/wall/cros-widget-container';
+/**
+ * The origin of the widget.
+ * @type {string}
+ * @const
+ */
+var CWS_WIDGET_ORIGIN = 'https://clients5.google.com';
+
+/**
+ * Creates dialog in DOM tree.
+ *
+ * @param {HTMLElement} parentNode Node to be parent for this dialog.
+ * @param {Object} state Static state of suggest app dialog.
+ * @constructor
+ * @extends {FileManagerDialogBase}
+ */
+function SuggestAppsDialog(parentNode, state) {
+ FileManagerDialogBase.call(this, parentNode);
+
+ this.frame_.id = 'suggest-app-dialog';
+
+ this.webviewContainer_ = this.document_.createElement('div');
+ this.webviewContainer_.id = 'webview-container';
+ this.webviewContainer_.style.width = WEBVIEW_WIDTH + 'px';
+ this.webviewContainer_.style.height = WEBVIEW_HEIGHT + 'px';
+ this.frame_.insertBefore(this.webviewContainer_, this.text_.nextSibling);
+
+ var spinnerLayer = this.document_.createElement('div');
+ spinnerLayer.className = 'spinner-layer';
+ this.webviewContainer_.appendChild(spinnerLayer);
+
+ this.buttons_ = this.document_.createElement('div');
+ this.buttons_.id = 'buttons';
+ this.frame_.appendChild(this.buttons_);
+
+ this.webstoreButton_ = this.document_.createElement('div');
+ this.webstoreButton_.id = 'webstore-button';
+ this.webstoreButton_.innerHTML = str('SUGGEST_DIALOG_LINK_TO_WEBSTORE');
+ this.webstoreButton_.addEventListener(
+ 'click', this.onWebstoreLinkClicked_.bind(this));
+ this.buttons_.appendChild(this.webstoreButton_);
+
+ this.initialFocusElement_ = this.webviewContainer_;
+
+ this.webview_ = null;
+ this.accessToken_ = null;
+ this.widgetUrl_ =
+ state.overrideCwsContainerUrlForTest || CWS_WIDGET_URL;
+ this.widgetOrigin_ =
+ state.overrideCwsContainerOriginForTest || CWS_WIDGET_ORIGIN;
+
+ this.extension_ = null;
+ this.mime_ = null;
+ this.installingItemId_ = null;
+ this.state_ = SuggestAppsDialog.State.UNINITIALIZED;
+
+ this.initializationTask_ = new AsyncUtil.Group();
+ this.initializationTask_.add(this.retrieveAuthorizeToken_.bind(this));
+ this.initializationTask_.run();
+}
+
+SuggestAppsDialog.prototype = {
+ __proto__: FileManagerDialogBase.prototype
+};
+
+/**
+ * @enum {string}
+ * @const
+ */
+SuggestAppsDialog.State = {
+ UNINITIALIZED: 'SuggestAppsDialog.State.UNINITIALIZED',
+ INITIALIZING: 'SuggestAppsDialog.State.INITIALIZING',
+ INITIALIZE_FAILED_CLOSING:
+ 'SuggestAppsDialog.State.INITIALIZE_FAILED_CLOSING',
+ INITIALIZED: 'SuggestAppsDialog.State.INITIALIZED',
+ INSTALLING: 'SuggestAppsDialog.State.INSTALLING',
+ INSTALLED_CLOSING: 'SuggestAppsDialog.State.INSTALLED_CLOSING',
+ OPENING_WEBSTORE_CLOSING: 'SuggestAppsDialog.State.OPENING_WEBSTORE_CLOSING',
+ CANCELED_CLOSING: 'SuggestAppsDialog.State.CANCELED_CLOSING'
+};
+Object.freeze(SuggestAppsDialog.State);
+
+/**
+ * @enum {string}
+ * @const
+ */
+SuggestAppsDialog.Result = {
+ // Install is done. The install app should be opened.
+ INSTALL_SUCCESSFUL: 'SuggestAppsDialog.Result.INSTALL_SUCCESSFUL',
+ // User cancelled the suggest app dialog. No message should be shown.
+ USER_CANCELL: 'SuggestAppsDialog.Result.USER_CANCELL',
+ // User clicked the link to web store so the dialog is closed.
+ WEBSTORE_LINK_OPENED: 'SuggestAppsDialog.Result.WEBSTORE_LINK_OPENED',
+ // Failed to load the widget. Error message should be shown.
+ FAILED: 'SuggestAppsDialog.Result.FAILED'
+};
+Object.freeze(SuggestAppsDialog.Result);
+
+/**
+ * @override
+ */
+SuggestAppsDialog.prototype.onInputFocus = function() {
+ this.webviewContainer_.select();
+};
+
+/**
+ * Injects headers into the passed request.
+ *
+ * @param {Event} e Request event.
+ * @return {{requestHeaders: HttpHeaders}} Modified headers.
+ * @private
+ */
+SuggestAppsDialog.prototype.authorizeRequest_ = function(e) {
+ e.requestHeaders.push({
+ name: 'Authorization',
+ value: 'Bearer ' + this.accessToken_
+ });
+ return {requestHeaders: e.requestHeaders};
+};
+
+/**
+ * Retrieves the authorize token. This method should be called in
+ * initialization of the dialog.
+ *
+ * @param {function()} callback Called when the token is retrieved.
+ * @private
+ */
+SuggestAppsDialog.prototype.retrieveAuthorizeToken_ = function(callback) {
+ if (window.IN_TEST) {
+ // In test, use a dummy string as token. This must be a non-empty string.
+ this.accessToken_ = 'DUMMY_ACCESS_TOKEN_FOR_TEST';
+ }
+
+ if (this.accessToken_) {
+ callback();
+ return;
+ }
+
+ // Fetch or update the access token.
+ chrome.fileBrowserPrivate.requestWebStoreAccessToken(
+ function(accessToken) {
+ // In case of error, this.accessToken_ will be set to null.
+ this.accessToken_ = accessToken;
+ callback();
+ }.bind(this));
+};
+
+/**
+ * Dummy function for SuggestAppsDialog.show() not to be called unintentionally.
+ */
+SuggestAppsDialog.prototype.show = function() {
+ console.error('SuggestAppsDialog.show() shouldn\'t be called directly.');
+};
+
+/**
+ * Shows suggest-apps dialog by file extension and mime.
+ *
+ * @param {string} extension Extension of the file.
+ * @param {string} mime Mime of the file.
+ * @param {function(boolean)} onDialogClosed Called when the dialog is closed.
+ * The argument is the result of installation: true if an app is installed,
+ * false otherwise.
+ */
+SuggestAppsDialog.prototype.showByExtensionAndMime =
+ function(extension, mime, onDialogClosed) {
+ this.text_.hidden = true;
+ this.dialogText_ = '';
+ this.showInternal_(null, extension, mime, onDialogClosed);
+};
+
+/**
+ * Shows suggest-apps dialog by the filename.
+ *
+ * @param {string} filename Filename (without extension) of the file.
+ * @param {function(boolean)} onDialogClosed Called when the dialog is closed.
+ * The argument is the result of installation: true if an app is installed,
+ * false otherwise.
+ */
+SuggestAppsDialog.prototype.showByFilename =
+ function(filename, onDialogClosed) {
+ this.text_.hidden = false;
+ this.dialogText_ = str('SUGGEST_DIALOG_MESSAGE_FOR_EXECUTABLE');
+ this.showInternal_(filename, null, null, onDialogClosed);
+};
+
+/**
+ * Internal methdo to shows a dialog. This should be called only from 'Suggest.
+ * appDialog.showXxxx()' functions.
+ *
+ * @param {string} filename Filename (without extension) of the file.
+ * @param {string} extension Extension of the file.
+ * @param {string} mime Mime of the file.
+ * @param {function(boolean)} onDialogClosed Called when the dialog is closed.
+ * The argument is the result of installation: true if an app is installed,
+ * false otherwise.
+ * @private
+ */
+SuggestAppsDialog.prototype.showInternal_ =
+ function(filename, extension, mime, onDialogClosed) {
+ if (this.state_ != SuggestAppsDialog.State.UNINITIALIZED) {
+ console.error('Invalid state.');
+ return;
+ }
+
+ this.extension_ = extension;
+ this.mimeType_ = mime;
+ this.onDialogClosed_ = onDialogClosed;
+ this.state_ = SuggestAppsDialog.State.INITIALIZING;
+
+ SuggestAppsDialog.Metrics.recordShowDialog();
+ SuggestAppsDialog.Metrics.startLoad();
+
+ // Makes it sure that the initialization is completed.
+ this.initializationTask_.run(function() {
+ if (!this.accessToken_) {
+ this.state_ = SuggestAppsDialog.State.INITIALIZE_FAILED_CLOSING;
+ this.onHide_();
+ return;
+ }
+
+ var title = str('SUGGEST_DIALOG_TITLE');
+ var show = this.dialogText_ ?
+ FileManagerDialogBase.prototype.showTitleAndTextDialog.call(
+ this, title, this.dialogText_) :
+ FileManagerDialogBase.prototype.showTitleOnlyDialog.call(
+ this, title);
+ if (!show) {
+ console.error('SuggestAppsDialog can\'t be shown');
+ this.state_ = SuggestAppsDialog.State.UNINITIALIZED;
+ this.onHide();
+ return;
+ }
+
+ this.webview_ = this.document_.createElement('webview');
+ this.webview_.id = 'cws-widget';
+ this.webview_.partition = 'persist:cwswidgets';
+ this.webview_.style.width = WEBVIEW_WIDTH + 'px';
+ this.webview_.style.height = WEBVIEW_HEIGHT + 'px';
+ this.webview_.request.onBeforeSendHeaders.addListener(
+ this.authorizeRequest_.bind(this),
+ {urls: [this.widgetOrigin_ + '/*']},
+ ['blocking', 'requestHeaders']);
+ this.webview_.addEventListener('newwindow', function(event) {
+ // Discard the window object and reopen in an external window.
+ event.window.discard();
+ util.visitURL(event.targetUrl);
+ event.preventDefault();
+ });
+ this.webviewContainer_.appendChild(this.webview_);
+
+ this.frame_.classList.add('show-spinner');
+
+ this.webviewClient_ = new CWSContainerClient(
+ this.webview_,
+ extension, mime, filename,
+ WEBVIEW_WIDTH, WEBVIEW_HEIGHT,
+ this.widgetUrl_, this.widgetOrigin_);
+ this.webviewClient_.addEventListener(CWSContainerClient.Events.LOADED,
+ this.onWidgetLoaded_.bind(this));
+ this.webviewClient_.addEventListener(CWSContainerClient.Events.LOAD_FAILED,
+ this.onWidgetLoadFailed_.bind(this));
+ this.webviewClient_.addEventListener(
+ CWSContainerClient.Events.REQUEST_INSTALL,
+ this.onInstallRequest_.bind(this));
+ this.webviewClient_.load();
+ }.bind(this));
+};
+
+/**
+ * Called when the 'See more...' link is clicked to be navigated to Webstore.
+ * @param {Event} e Event.
+ * @private
+ */
+SuggestAppsDialog.prototype.onWebstoreLinkClicked_ = function(e) {
+ var webStoreUrl =
+ FileTasks.createWebStoreLink(this.extension_, this.mimeType_);
+ chrome.windows.create({url: webStoreUrl});
+ this.state_ = SuggestAppsDialog.State.OPENING_WEBSTORE_CLOSING;
+ this.hide();
+};
+
+/**
+ * Called when the widget is loaded successfully.
+ * @param {Event} event Event.
+ * @private
+ */
+SuggestAppsDialog.prototype.onWidgetLoaded_ = function(event) {
+ SuggestAppsDialog.Metrics.finishLoad();
+ SuggestAppsDialog.Metrics.recordLoad(
+ SuggestAppsDialog.Metrics.LOAD.SUCCEEDED);
+
+ this.frame_.classList.remove('show-spinner');
+ this.state_ = SuggestAppsDialog.State.INITIALIZED;
+
+ this.webview_.focus();
+};
+
+/**
+ * Called when the widget is failed to load.
+ * @param {Event} event Event.
+ * @private
+ */
+SuggestAppsDialog.prototype.onWidgetLoadFailed_ = function(event) {
+ SuggestAppsDialog.Metrics.recordLoad(SuggestAppsDialog.Metrics.LOAD.FAILURE);
+
+ this.frame_.classList.remove('show-spinner');
+ this.state_ = SuggestAppsDialog.State.INITIALIZE_FAILED_CLOSING;
+
+ this.hide();
+};
+
+/**
+ * Called when the connection status is changed.
+ * @param {util.DriveConnectionType} connectionType Current connection type.
+ */
+SuggestAppsDialog.prototype.onDriveConnectionChanged =
+ function(connectionType) {
+ if (this.state_ !== SuggestAppsDialog.State.UNINITIALIZED &&
+ connectionType === util.DriveConnectionType.OFFLINE) {
+ this.state_ = SuggestAppsDialog.State.INITIALIZE_FAILED_CLOSING;
+ this.hide();
+ }
+};
+
+/**
+ * Called when receiving the install request from the webview client.
+ * @param {Event} e Event.
+ * @private
+ */
+SuggestAppsDialog.prototype.onInstallRequest_ = function(e) {
+ var itemId = e.itemId;
+ this.installingItemId_ = itemId;
+
+ this.appInstaller_ = new AppInstaller(itemId);
+ this.appInstaller_.install(this.onInstallCompleted_.bind(this));
+
+ this.frame_.classList.add('show-spinner');
+ this.state_ = SuggestAppsDialog.State.INSTALLING;
+};
+
+/**
+ * Called when the installation is completed from the app installer.
+ * @param {AppInstaller.Result} result Result of the installation.
+ * @param {string} error Detail of the error.
+ * @private
+ */
+SuggestAppsDialog.prototype.onInstallCompleted_ = function(result, error) {
+ var success = (result === AppInstaller.Result.SUCCESS);
+
+ this.frame_.classList.remove('show-spinner');
+ this.state_ = success ?
+ SuggestAppsDialog.State.INSTALLED_CLOSING :
+ SuggestAppsDialog.State.INITIALIZED; // Back to normal state.
+ this.webviewClient_.onInstallCompleted(success, this.installingItemId_);
+ this.installingItemId_ = null;
+
+ switch (result) {
+ case AppInstaller.Result.SUCCESS:
+ SuggestAppsDialog.Metrics.recordInstall(
+ SuggestAppsDialog.Metrics.INSTALL.SUCCESS);
+ this.hide();
+ break;
+ case AppInstaller.Result.CANCELLED:
+ SuggestAppsDialog.Metrics.recordInstall(
+ SuggestAppsDialog.Metrics.INSTALL.CANCELLED);
+ // User cancelled the installation. Do nothing.
+ break;
+ case AppInstaller.Result.ERROR:
+ SuggestAppsDialog.Metrics.recordInstall(
+ SuggestAppsDialog.Metrics.INSTALL.FAILED);
+ fileManager.error.show(str('SUGGEST_DIALOG_INSTALLATION_FAILED'));
+ break;
+ }
+};
+
+/**
+ * @override
+ */
+SuggestAppsDialog.prototype.hide = function(opt_originalOnHide) {
+ switch (this.state_) {
+ case SuggestAppsDialog.State.INSTALLING:
+ // Install is being aborted. Send the failure result.
+ // Cancels the install.
+ if (this.webviewClient_)
+ this.webviewClient_.onInstallCompleted(false, this.installingItemId_);
+ this.installingItemId_ = null;
+
+ // Assumes closing the dialog as canceling the install.
+ this.state_ = SuggestAppsDialog.State.CANCELED_CLOSING;
+ break;
+ case SuggestAppsDialog.State.INITIALIZING:
+ SuggestAppsDialog.Metrics.recordLoad(
+ SuggestAppsDialog.Metrics.LOAD.CANCELLED);
+ this.state_ = SuggestAppsDialog.State.CANCELED_CLOSING;
+ break;
+ case SuggestAppsDialog.State.INSTALLED_CLOSING:
+ case SuggestAppsDialog.State.INITIALIZE_FAILED_CLOSING:
+ case SuggestAppsDialog.State.OPENING_WEBSTORE_CLOSING:
+ // Do nothing.
+ break;
+ case SuggestAppsDialog.State.INITIALIZED:
+ this.state_ = SuggestAppsDialog.State.CANCELED_CLOSING;
+ break;
+ default:
+ this.state_ = SuggestAppsDialog.State.CANCELED_CLOSING;
+ console.error('Invalid state.');
+ }
+
+ if (this.webviewClient_) {
+ this.webviewClient_.dispose();
+ this.webviewClient_ = null;
+ }
+
+ this.webviewContainer_.removeChild(this.webview_);
+ this.webview_ = null;
+ this.extension_ = null;
+ this.mime_ = null;
+
+ FileManagerDialogBase.prototype.hide.call(
+ this,
+ this.onHide_.bind(this, opt_originalOnHide));
+};
+
+/**
+ * @param {function()=} opt_originalOnHide Original onHide function passed to
+ * SuggestAppsDialog.hide().
+ * @private
+ */
+SuggestAppsDialog.prototype.onHide_ = function(opt_originalOnHide) {
+ // Calls the callback after the dialog hides.
+ if (opt_originalOnHide)
+ opt_originalOnHide();
+
+ var result;
+ switch (this.state_) {
+ case SuggestAppsDialog.State.INSTALLED_CLOSING:
+ result = SuggestAppsDialog.Result.INSTALL_SUCCESSFUL;
+ SuggestAppsDialog.Metrics.recordCloseDialog(
+ SuggestAppsDialog.Metrics.CLOSE_DIALOG.ITEM_INSTALLED);
+ break;
+ case SuggestAppsDialog.State.INITIALIZE_FAILED_CLOSING:
+ result = SuggestAppsDialog.Result.FAILED;
+ break;
+ case SuggestAppsDialog.State.CANCELED_CLOSING:
+ result = SuggestAppsDialog.Result.USER_CANCELL;
+ SuggestAppsDialog.Metrics.recordCloseDialog(
+ SuggestAppsDialog.Metrics.CLOSE_DIALOG.USER_CANCELL);
+ break;
+ case SuggestAppsDialog.State.OPENING_WEBSTORE_CLOSING:
+ result = SuggestAppsDialog.Result.WEBSTORE_LINK_OPENED;
+ SuggestAppsDialog.Metrics.recordCloseDialog(
+ SuggestAppsDialog.Metrics.CLOSE_DIALOG.WEB_STORE_LINK);
+ break;
+ default:
+ result = SuggestAppsDialog.Result.USER_CANCELL;
+ SuggestAppsDialog.Metrics.recordCloseDialog(
+ SuggestAppsDialog.Metrics.CLOSE_DIALOG.UNKNOWN_ERROR);
+ console.error('Invalid state.');
+ }
+ this.state_ = SuggestAppsDialog.State.UNINITIALIZED;
+
+ this.onDialogClosed_(result);
+};
+
+/**
+ * Utility methods and constants to record histograms.
+ */
+SuggestAppsDialog.Metrics = Object.freeze({
+ LOAD: Object.freeze({
+ SUCCEEDED: 0,
+ CANCELLED: 1,
+ FAILED: 2,
+ }),
+
+ /**
+ * @param {SuggestAppsDialog.Metrics.LOAD} result Result of load.
+ */
+ recordLoad: function(result) {
+ if (0 <= result && result < 3)
+ metrics.recordEnum('SuggestApps.Load', result, 3);
+ },
+
+ CLOSE_DIALOG: Object.freeze({
+ UNKOWN_ERROR: 0,
+ ITEM_INSTALLED: 1,
+ USER_CANCELLED: 2,
+ WEBSTORE_LINK_OPENED: 3,
+ }),
+
+ /**
+ * @param {SuggestAppsDialog.Metrics.CLOSE_DIALOG} reason Reason of closing
+ * dialog.
+ */
+ recordCloseDialog: function(reason) {
+ if (0 <= reason && reason < 4)
+ metrics.recordEnum('SuggestApps.CloseDialog', reason, 4);
+ },
+
+ INSTALL: Object.freeze({
+ SUCCEEDED: 0,
+ CANCELLED: 1,
+ FAILED: 2,
+ }),
+
+ /**
+ * @param {SuggestAppsDialog.Metrics.INSTALL} result Result of installation.
+ */
+ recordInstall: function(result) {
+ if (0 <= result && result < 3)
+ metrics.recordEnum('SuggestApps.Install', result, 3);
+ },
+
+ recordShowDialog: function() {
+ metrics.recordUserAction('SuggestApps.ShowDialog');
+ },
+
+ startLoad: function() {
+ metrics.startInterval('SuggestApps.LoadTime');
+ },
+
+ finishLoad: function() {
+ metrics.recordInterval('SuggestApps.LoadTime');
+ },
+});
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/text_measure.js b/chromium/chrome/browser/resources/file_manager/foreground/js/text_measure.js
new file mode 100644
index 00000000000..a8db83ae582
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/text_measure.js
@@ -0,0 +1,50 @@
+// Copyright 2013 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.
+
+'use strict';
+
+/**
+ * TextMeasure constructor.
+ *
+ * TextMeasure is a measure for text that returns the width of text. This
+ * class has a dummy span element. When measuring the width of text, it sets
+ * the text to the element and obtains the element's size by
+ * getBoundingClientRect.
+ *
+ * @constructor
+ * @param {HTMLElement} element Element that has styles of measured text. The
+ * width of text is measures like as it is rendered in this element.
+ */
+var TextMeasure = function(element) {
+ var doc = element.ownerDocument;
+ this.dummySpan_ = doc.createElement('span');
+ this.dummySpan_ = doc.getElementsByTagName('body')[0].
+ appendChild(this.dummySpan_);
+ this.dummySpan_.style.position = 'absolute';
+ this.dummySpan_.style.visibility = 'hidden';
+ var styles = window.getComputedStyle(element, '');
+ var stylesToBeCopied = [
+ 'fontSize',
+ 'fontStyle',
+ 'fontWeight',
+ 'fontFamily',
+ 'letterSpacing'
+ ];
+ for (var i = 0; i < stylesToBeCopied.length; i++) {
+ this.dummySpan_.style[stylesToBeCopied[i]] = styles[stylesToBeCopied[i]];
+ }
+ Object.seal(this);
+};
+
+/**
+ * Measures the width of text.
+ *
+ * @param {string} text Text that is measured the width.
+ * @return {number} Width of the specified text.
+ */
+TextMeasure.prototype.getWidth = function(text) {
+ this.dummySpan_.innerText = text;
+ var rect = this.dummySpan_.getBoundingClientRect();
+ return rect ? rect.width : 0;
+};
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/tree.css.js b/chromium/chrome/browser/resources/file_manager/foreground/js/tree.css.js
new file mode 100644
index 00000000000..6b89acbdb86
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/tree.css.js
@@ -0,0 +1,59 @@
+// Copyright 2013 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.
+
+'use strict';
+
+/**
+ * Custom version of chrome://resources/css/tree.css.js, adding support for
+ * inverted arrow icons.
+ */
+(function() {
+ /**
+ * @type {number}
+ * @const
+ */
+ var WIDTH = 14;
+
+ /**
+ * @type {number}
+ * @const
+ */
+ var HEIGHT = WIDTH / 2 + 2;
+
+ /**
+ * @type {number}
+ * @const
+ */
+ var MARGIN = 1;
+
+ /**
+ * @param {string} name CSS canvas identifier.
+ * @param {string} backgroundColor Background color.
+ * @param {string} strokeColor Outline color.
+ */
+ function prepareTriangle(name, backgroundColor, strokeColor) {
+ var ctx = document.getCSSCanvasContext('2d',
+ name,
+ WIDTH + MARGIN * 2,
+ HEIGHT + MARGIN * 2);
+
+ ctx.fillStyle = backgroundColor;
+ ctx.strokeStyle = strokeColor;
+ ctx.translate(MARGIN, MARGIN);
+
+ ctx.beginPath();
+ ctx.moveTo(0, 0);
+ ctx.lineTo(0, 2);
+ ctx.lineTo(WIDTH / 2, HEIGHT);
+ ctx.lineTo(WIDTH, 2);
+ ctx.lineTo(WIDTH, 0);
+ ctx.closePath();
+ ctx.fill();
+ ctx.stroke();
+ }
+
+ prepareTriangle(
+ 'tree-triangle', 'rgba(122, 122, 122, 0.6)', 'rgba(0, 0, 0, 0)');
+ prepareTriangle('tree-triangle-inverted', '#ffffff', '#ffffff');
+})();
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/ui/breadcrumbs_controller.js b/chromium/chrome/browser/resources/file_manager/foreground/js/ui/breadcrumbs_controller.js
new file mode 100644
index 00000000000..4189ef9ea0b
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/ui/breadcrumbs_controller.js
@@ -0,0 +1,262 @@
+// Copyright 2013 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.
+
+'use strict';
+
+/**
+ * @extends cr.EventTarget
+ * @param {HTMLDivElement} div Div container for breadcrumbs.
+ * @param {MetadataCache} metadataCache To retrieve metadata.
+ * @param {VolumeManagerWrapper} volumeManager Volume manager.
+ * @constructor
+ */
+function BreadcrumbsController(div, metadataCache, volumeManager) {
+ this.bc_ = div;
+ this.metadataCache_ = metadataCache;
+ this.volumeManager_ = volumeManager;
+ this.entry_ = null;
+
+ /**
+ * Sequence value to skip requests that are out of date.
+ * @type {number}
+ * @private
+ */
+ this.showSequence_ = 0;
+
+ // Register events and seql the object.
+ div.addEventListener('click', this.onClick_.bind(this));
+}
+
+/**
+ * Extends cr.EventTarget.
+ */
+BreadcrumbsController.prototype.__proto__ = cr.EventTarget.prototype;
+
+/**
+ * Shows breadcrumbs.
+ *
+ * @param {Entry} entry Target entry.
+ */
+BreadcrumbsController.prototype.show = function(entry) {
+ if (entry === this.entry_)
+ return;
+
+ this.entry_ = entry;
+ this.bc_.hidden = false;
+ this.bc_.textContent = '';
+ this.showSequence_++;
+
+ var queue = new AsyncUtil.Queue();
+ var entries = [];
+ var error = false;
+
+ // Obtain entries from the target entry to the root.
+ var loop;
+ var resolveParent = function(inEntry, callback) {
+ entries.unshift(inEntry);
+ if (!this.volumeManager_.getLocationInfo(inEntry).isRootEntry) {
+ inEntry.getParent(function(parent) {
+ resolveParent(parent, callback);
+ }, function() {
+ error = true;
+ callback();
+ });
+ } else {
+ callback();
+ }
+ }.bind(this);
+ queue.run(resolveParent.bind(null, entry));
+
+ // Override DRIVE_OTHER root to DRIVE_SHARED_WITH_ME root.
+ queue.run(function(callback) {
+ // If an error was occured, just skip.
+ if (error) {
+ callback();
+ return;
+ }
+
+ // If the path is not under the drive other root, it is not needed to
+ // override root type.
+ var locationInfo = this.volumeManager_.getLocationInfo(entry);
+ if (!locationInfo) {
+ error = true;
+ callback();
+ return;
+ }
+ if (locationInfo.rootType !== RootType.DRIVE_OTHER) {
+ callback();
+ return;
+ }
+
+ // Otherwise check the metadata of the directory localted at just under
+ // drive other.
+ if (!entries[1]) {
+ error = true;
+ callback();
+ return;
+ }
+ this.metadataCache_.getOne(entries[1], 'drive', function(result) {
+ if (result && result.sharedWithMe)
+ entries[0] = RootType.DRIVE_SHARED_WITH_ME;
+ else
+ entries.shift();
+ callback();
+ });
+ }.bind(this));
+
+ // Update DOM element.
+ queue.run(function(sequence, callback) {
+ // Check the sequence number to skip requests that are out of date.
+ if (this.showSequence_ === sequence && !error)
+ this.updateInternal_(entries);
+ callback();
+ }.bind(this, this.showSequence_));
+};
+
+/**
+ * Updates the breadcrumb display.
+ * @param {Array.<Entry|RootType>} entries Location information of target path.
+ * @private
+ */
+BreadcrumbsController.prototype.updateInternal_ = function(entries) {
+ // Make elements.
+ var doc = this.bc_.ownerDocument;
+ for (var i = 0; i < entries.length; i++) {
+ // Add a component.
+ var entry = entries[i];
+ var div = doc.createElement('div');
+ div.className = 'breadcrumb-path';
+ if (entry === RootType.DRIVE_SHARED_WITH_ME) {
+ div.textContent = PathUtil.getRootLabel(RootType.DRIVE_SHARED_WITH_ME);
+ } else {
+ var location = this.volumeManager_.getLocationInfo(entry);
+ div.textContent = (location && location.isRootEntry) ?
+ PathUtil.getRootLabel(entry.fullPath) : entry.name;
+ }
+ div.entry = entry;
+ this.bc_.appendChild(div);
+
+ // If this is the last component, break here.
+ if (i === entries.length - 1) {
+ div.classList.add('breadcrumb-last');
+ break;
+ }
+
+ // Add a separator.
+ var separator = doc.createElement('div');
+ separator.className = 'separator';
+ this.bc_.appendChild(separator);
+ }
+
+ this.truncate();
+};
+
+/**
+ * Updates breadcrumbs widths in order to truncate it properly.
+ */
+BreadcrumbsController.prototype.truncate = function() {
+ if (!this.bc_.firstChild)
+ return;
+
+ // Assume style.width == clientWidth (items have no margins or paddings).
+
+ for (var item = this.bc_.firstChild; item; item = item.nextSibling) {
+ item.removeAttribute('style');
+ item.removeAttribute('collapsed');
+ }
+
+ var containerWidth = this.bc_.clientWidth;
+
+ var pathWidth = 0;
+ var currentWidth = 0;
+ var lastSeparator;
+ for (var item = this.bc_.firstChild; item; item = item.nextSibling) {
+ if (item.className == 'separator') {
+ pathWidth += currentWidth;
+ currentWidth = item.clientWidth;
+ lastSeparator = item;
+ } else {
+ currentWidth += item.clientWidth;
+ }
+ }
+ if (pathWidth + currentWidth <= containerWidth)
+ return;
+ if (!lastSeparator) {
+ this.bc_.lastChild.style.width = Math.min(currentWidth, containerWidth) +
+ 'px';
+ return;
+ }
+ var lastCrumbSeparatorWidth = lastSeparator.clientWidth;
+ // Current directory name may occupy up to 70% of space or even more if the
+ // path is short.
+ var maxPathWidth = Math.max(Math.round(containerWidth * 0.3),
+ containerWidth - currentWidth);
+ maxPathWidth = Math.min(pathWidth, maxPathWidth);
+
+ var parentCrumb = lastSeparator.previousSibling;
+ var collapsedWidth = 0;
+ if (parentCrumb && pathWidth - maxPathWidth > parentCrumb.clientWidth) {
+ // At least one crumb is hidden completely (or almost completely).
+ // Show sign of hidden crumbs like this:
+ // root > some di... > ... > current directory.
+ parentCrumb.setAttribute('collapsed', '');
+ collapsedWidth = Math.min(maxPathWidth, parentCrumb.clientWidth);
+ maxPathWidth -= collapsedWidth;
+ if (parentCrumb.clientWidth != collapsedWidth)
+ parentCrumb.style.width = collapsedWidth + 'px';
+
+ lastSeparator = parentCrumb.previousSibling;
+ if (!lastSeparator)
+ return;
+ collapsedWidth += lastSeparator.clientWidth;
+ maxPathWidth = Math.max(0, maxPathWidth - lastSeparator.clientWidth);
+ }
+
+ pathWidth = 0;
+ for (var item = this.bc_.firstChild; item != lastSeparator;
+ item = item.nextSibling) {
+ // TODO(serya): Mixing access item.clientWidth and modifying style and
+ // attributes could cause multiple layout reflows.
+ if (pathWidth + item.clientWidth <= maxPathWidth) {
+ pathWidth += item.clientWidth;
+ } else if (pathWidth == maxPathWidth) {
+ item.style.width = '0';
+ } else if (item.classList.contains('separator')) {
+ // Do not truncate separator. Instead let the last crumb be longer.
+ item.style.width = '0';
+ maxPathWidth = pathWidth;
+ } else {
+ // Truncate the last visible crumb.
+ item.style.width = (maxPathWidth - pathWidth) + 'px';
+ pathWidth = maxPathWidth;
+ }
+ }
+
+ currentWidth = Math.min(currentWidth,
+ containerWidth - pathWidth - collapsedWidth);
+ this.bc_.lastChild.style.width =
+ (currentWidth - lastCrumbSeparatorWidth) + 'px';
+};
+
+/**
+ * Hide breadcrumbs div.
+ */
+BreadcrumbsController.prototype.hide = function() {
+ this.bc_.hidden = true;
+};
+
+/**
+ * Handle a click event on a breadcrumb element.
+ * @param {Event} event The click event.
+ * @private
+ */
+BreadcrumbsController.prototype.onClick_ = function(event) {
+ if (!event.target.classList.contains('breadcrumb-path') ||
+ event.target.classList.contains('breadcrumb-last'))
+ return;
+
+ var newEvent = new Event('pathclick');
+ newEvent.entry = event.target.entry;
+ this.dispatchEvent(newEvent);
+};
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/ui/conflict_dialog.js b/chromium/chrome/browser/resources/file_manager/foreground/js/ui/conflict_dialog.js
new file mode 100644
index 00000000000..8aecd737228
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/ui/conflict_dialog.js
@@ -0,0 +1,132 @@
+// Copyright 2013 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.
+
+'use strict';
+
+/**
+ * Dialog to confirm the operation for conflicted file operations.
+ *
+ * @param {HTMLElement} parentNode Node to be parent for this dialog.
+ * @constructor
+ * @extends {FileManagerDialogBase}
+ */
+function ConflictDialog(parentNode) {
+ FileManagerDialogBase.call(this, parentNode);
+
+ /**
+ * Callback to be called when the showing task is completed. The first
+ * argument is which button is pressed. The second argument is whether to
+ * apply all or not.
+ *
+ * @type {function(ConflictDialog.Result, boolean)}
+ * @private
+ */
+ this.callback_ = null;
+
+ /**
+ * Checkbox to specify whether to apply the selection to all entries or not.
+ * @type {HTMLElement}
+ * @private
+ */
+ this.applyAllCheckbox_ = parentNode.ownerDocument.createElement('input');
+ this.applyAllCheckbox_.id = 'conflict-confirm-dialog-apply-all-checkbox';
+ this.applyAllCheckbox_.type = 'checkbox';
+
+ // Apply all line.
+ var applyAllLabel = parentNode.ownerDocument.createElement('label');
+ applyAllLabel.textContent = str('CONFLICT_DIALOG_APPLY_TO_ALL');
+ applyAllLabel.setAttribute('for', this.applyAllCheckbox_.id);
+
+ /**
+ * Element of the keep both button.
+ * @type {HTMLElement}
+ * @private
+ */
+ this.keepBothButton_ = parentNode.ownerDocument.createElement('button');
+ this.keepBothButton_.textContent = str('CONFLICT_DIALOG_KEEP_BOTH');
+ this.keepBothButton_.addEventListener(
+ 'click',
+ this.hideWithResult_.bind(this, ConflictDialog.Result.KEEP_BOTH));
+
+ /**
+ * Element of the replace button.
+ * @type {HTMLElement}
+ * @private
+ */
+ this.replaceButton_ = parentNode.ownerDocument.createElement('button');
+ this.replaceButton_.textContent = str('CONFLICT_DIALOG_REPLACE');
+ this.replaceButton_.addEventListener(
+ 'click',
+ this.hideWithResult_.bind(this, ConflictDialog.Result.REPLACE));
+
+ // Buttons line.
+ var buttons = this.okButton_.parentNode;
+ buttons.insertBefore(this.applyAllCheckbox_, this.okButton_);
+ buttons.insertBefore(applyAllLabel, this.okButton_);
+ buttons.replaceChild(this.keepBothButton_, this.okButton_);
+ buttons.appendChild(this.replaceButton_);
+
+ // Frame
+ this.frame_.id = 'conflict-confirm-dialog';
+}
+
+/**
+ * Result of conflict confirm dialogs.
+ * @enum {string}
+ * @const
+ */
+ConflictDialog.Result = Object.freeze({
+ KEEP_BOTH: 'keepBoth',
+ CANCEL: 'cancel',
+ REPLACE: 'replace'
+});
+
+ConflictDialog.prototype = {
+ __proto__: FileManagerDialogBase.prototype
+};
+
+/**
+ * Shows the conflict confirm dialog.
+ *
+ * @param {string} fileName Filename that is conflicted.
+ * @param {function(ConflictDialog.Result, boolean)} callback Complete
+ * callbak. See also ConflictDialog#callback_.
+ * @return {boolean} True if the dialog can show successfully. False if the
+ * dialog failed to show due to an existing dialog.
+ */
+ConflictDialog.prototype.show = function(fileName, callback) {
+ if (this.callback_)
+ return false;
+
+ this.callback_ = callback;
+ FileManagerDialogBase.prototype.showOkCancelDialog.call(
+ this,
+ '', // We dont't show the title for the dialog.
+ strf('CONFLICT_DIALOG_MESSAGE', fileName));
+ return true;
+};
+
+/**
+ * Handles cancellation.
+ * @param {Event} event Click event.
+ * @private
+ */
+ConflictDialog.prototype.onCancelClick_ = function(event) {
+ this.hideWithResult_(ConflictDialog.Result.CANCEL);
+};
+
+/**
+ * Hides the dialog box with the result.
+ * @param {ConflictDialog.Result} result Result.
+ * @private
+ */
+ConflictDialog.prototype.hideWithResult_ = function(result) {
+ this.hide(function() {
+ if (!this.callback_)
+ return;
+ this.callback_(result, this.applyAllCheckbox_.checked);
+ this.callback_ = null;
+ this.applyAllCheckbox_.checked = false;
+ }.bind(this));
+};
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/ui/file_manager_dialog_base.js b/chromium/chrome/browser/resources/file_manager/foreground/js/ui/file_manager_dialog_base.js
new file mode 100644
index 00000000000..63b856f087f
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/ui/file_manager_dialog_base.js
@@ -0,0 +1,122 @@
+// Copyright 2013 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.
+
+'use strict';
+
+/**
+ * This class is an extended class, to manage the status of the dialogs.
+ *
+ * @param {HTMLElement} parentNode Parent node of the dialog.
+ * @extends {cr.ui.dialogs.FileManagerDialogBase}
+ * @constructor
+ */
+var FileManagerDialogBase = function(parentNode) {
+ cr.ui.dialogs.BaseDialog.call(this, parentNode);
+};
+
+FileManagerDialogBase.prototype = {
+ __proto__: cr.ui.dialogs.BaseDialog.prototype
+};
+
+/**
+ * The FileManager object. This is used to notify events of showing or hiding
+ * dialog to file manager.
+ *
+ * @type {FileManager}
+ * @private
+ */
+FileManagerDialogBase.fileManager_ = null;
+
+/**
+ * Setter of FileManagerDialogBase.fileManager_.
+ * @param {FileManager} fileManager The fileManager object.
+ */
+FileManagerDialogBase.setFileManager = function(fileManager) {
+ FileManagerDialogBase.fileManager_ = fileManager;
+};
+
+/**
+ * The flag if any dialog is shown. True if a dialog is visible, false
+ * otherwise.
+ * @type {boolean}
+ */
+FileManagerDialogBase.shown = false;
+
+/**
+ * @param {string} title Title.
+ * @param {string} message Message.
+ * @param {function()} onOk Called when the OK button is pressed.
+ * @param {function()} onCancel Called when the cancel button is pressed.
+ * @return {boolean} True if the dialog can show successfully. False if the
+ * dialog failed to show due to an existing dialog.
+ */
+FileManagerDialogBase.prototype.showOkCancelDialog = function(
+ title, message, onOk, onCancel) {
+ return this.showImpl_(title, message, onOk, onCancel);
+};
+
+/**
+ * @param {string} title Title.
+ * @param {string} message Message.
+ * @param {function()} onOk Called when the OK button is pressed.
+ * @param {function()} onCancel Called when the cancel button is pressed.
+ * @return {boolean} True if the dialog can show successfully. False if the
+ * dialog failed to show due to an existing dialog.
+ * @private
+ */
+FileManagerDialogBase.prototype.showImpl_ = function(
+ title, message, onOk, onCancel) {
+ if (FileManagerDialogBase.shown)
+ return false;
+
+ FileManagerDialogBase.shown = true;
+ if (FileManagerDialogBase.fileManager_)
+ FileManagerDialogBase.fileManager_.onDialogShownOrHidden(true);
+ cr.ui.dialogs.BaseDialog.prototype.showWithTitle.call(
+ this, title, message, onOk, onCancel, null);
+
+ return true;
+};
+
+/**
+ * @return {boolean} True if the dialog can show successfully. False if the
+ * dialog failed to show due to an existing dialog.
+ */
+FileManagerDialogBase.prototype.showBlankDialog = function() {
+ return this.showImpl_('', '', null, null, null);
+};
+
+/**
+ * @param {string} title Title.
+ * @return {boolean} True if the dialog can show successfully. False if the
+ * dialog failed to show due to an existing dialog.
+ */
+FileManagerDialogBase.prototype.showTitleOnlyDialog = function(title) {
+ return this.showImpl_(title, '', null, null, null);
+};
+
+/**
+ * @param {string} title Title.
+ * @param {string} text Text to be shown in the dialog.
+ * @return {boolean} True if the dialog can show successfully. False if the
+ * dialog failed to show due to an existing dialog.
+ */
+FileManagerDialogBase.prototype.showTitleAndTextDialog = function(title, text) {
+ return this.showImpl_(title, text, null, null, null);
+};
+
+/**
+ * @param {function()=} opt_onHide Called when the dialog is hidden.
+ */
+FileManagerDialogBase.prototype.hide = function(opt_onHide) {
+ cr.ui.dialogs.BaseDialog.prototype.hide.call(
+ this,
+ function() {
+ if (opt_onHide)
+ opt_onHide();
+ if (FileManagerDialogBase.fileManager_)
+ FileManagerDialogBase.fileManager_.onDialogShownOrHidden(false);
+ FileManagerDialogBase.shown = false;
+ });
+};
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/ui/file_manager_ui.js b/chromium/chrome/browser/resources/file_manager/foreground/js/ui/file_manager_ui.js
new file mode 100644
index 00000000000..1d460f54cdb
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/ui/file_manager_ui.js
@@ -0,0 +1,190 @@
+// Copyright 2013 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.
+
+'use strict';
+
+/**
+ * The root of the file manager's view managing the DOM of Files.app.
+ *
+ * @param {HTMLElement} element Top level element of Files.app.
+ * @param {DialogType} dialogType Dialog type.
+ * @constructor.
+ */
+var FileManagerUI = function(element, dialogType) {
+ /**
+ * Top level element of Files.app.
+ * @type {HTMLElement}
+ * @private
+ */
+ this.element_ = element;
+
+ /**
+ * Dialog type.
+ * @type {DialogType}
+ * @private
+ */
+ this.dialogType_ = dialogType;
+
+ /**
+ * Error dialog.
+ * @type {ErrorDialog}
+ */
+ this.errorDialog = null;
+
+ /**
+ * Alert dialog.
+ * @type {cr.ui.dialogs.AlertDialog}
+ */
+ this.alertDialog = null;
+
+ /**
+ * Confirm dialog.
+ * @type {cr.ui.dialogs.ConfirmDialog}
+ */
+ this.confirmDialog = null;
+
+ /**
+ * Prompt dialog.
+ * @type {cr.ui.dialogs.PromptDialog}
+ */
+ this.promptDialog = null;
+
+ /**
+ * Share dialog.
+ * @type {ShareDialog}
+ */
+ this.shareDialog = null;
+
+ /**
+ * Default task picker.
+ * @type {DefaultActionDialog}
+ */
+ this.defaultTaskPicker = null;
+
+ /**
+ * Suggest apps dialog.
+ * @type {SuggestAppsDialog}
+ */
+ this.suggestAppsDialog = null;
+
+ /**
+ * Conflict dialog.
+ * @type {ConflictDialog}
+ */
+ this.conflictDialog = null;
+
+ /**
+ * Search box.
+ * @type {SearchBox}
+ */
+ this.searchBox = null;
+
+ /**
+ * File type selector in the footer.
+ * @type {HTMLElement}
+ */
+ this.fileTypeSelector = null;
+
+ /**
+ * OK button in the footer.
+ * @type {HTMLElement}
+ */
+ this.okButton = null;
+
+ /**
+ * Cancel button in the footer.
+ * @type {HTMLElement}
+ */
+ this.cancelButton = null;
+
+ Object.seal(this);
+
+ // Initialize the header.
+ this.element_.querySelector('#app-name').innerText =
+ chrome.runtime.getManifest().name;
+
+ // Initialize dialog type.
+ this.initDialogType_();
+
+ // Pre-populate the static localized strings.
+ i18nTemplate.process(this.element_.ownerDocument, loadTimeData);
+};
+
+/**
+ * Tweak the UI to become a particular kind of dialog, as determined by the
+ * dialog type parameter passed to the constructor.
+ *
+ * @private
+ */
+FileManagerUI.prototype.initDialogType_ = function() {
+ // Obtain elements.
+ var hasFooterPanel =
+ this.dialogType_ == DialogType.SELECT_SAVEAS_FILE ||
+ DialogType.isFolderDialog(this.dialogType_);
+
+ // If the footer panel exists, the buttons are placed there. Otherwise,
+ // the buttons are on the preview panel.
+ var parentPanelOfButtons = this.element_.ownerDocument.querySelector(
+ !hasFooterPanel ? '.preview-panel' : '.dialog-footer');
+ parentPanelOfButtons.classList.add('button-panel');
+ this.fileTypeSelector = parentPanelOfButtons.querySelector('.file-type');
+ this.okButton = parentPanelOfButtons.querySelector('.ok');
+ this.cancelButton = parentPanelOfButtons.querySelector('.cancel');
+
+ // Set attributes.
+ var okLabel = str('OPEN_LABEL');
+
+ switch (this.dialogType_) {
+ case DialogType.SELECT_UPLOAD_FOLDER:
+ okLabel = str('UPLOAD_LABEL');
+ break;
+
+ case DialogType.SELECT_SAVEAS_FILE:
+ okLabel = str('SAVE_LABEL');
+ break;
+
+ case DialogType.SELECT_FOLDER:
+ case DialogType.SELECT_OPEN_FILE:
+ case DialogType.SELECT_OPEN_MULTI_FILE:
+ case DialogType.FULL_PAGE:
+ break;
+
+ default:
+ throw new Error('Unknown dialog type: ' + this.dialogType);
+ }
+
+ this.okButton.textContent = okLabel;
+ this.element_.setAttribute('type', this.dialogType_);
+};
+
+/**
+ * Initialize the dialogs.
+ */
+FileManagerUI.prototype.initDialogs = function() {
+ // Initialize the dialog label.
+ var dialogs = cr.ui.dialogs;
+ dialogs.BaseDialog.OK_LABEL = str('OK_LABEL');
+ dialogs.BaseDialog.CANCEL_LABEL = str('CANCEL_LABEL');
+ var appState = window.appState || {};
+
+ // Create the dialog instances.
+ this.errorDialog = new ErrorDialog(this.element_);
+ this.alertDialog = new dialogs.AlertDialog(this.element_);
+ this.confirmDialog = new dialogs.ConfirmDialog(this.element_);
+ this.promptDialog = new dialogs.PromptDialog(this.element_);
+ this.shareDialog = new ShareDialog(this.element_);
+ this.defaultTaskPicker =
+ new cr.filebrowser.DefaultActionDialog(this.element_);
+ this.suggestAppsDialog = new SuggestAppsDialog(
+ this.element_, appState.suggestAppsDialogState || {});
+ this.conflictDialog = new ConflictDialog(this.element_);
+};
+
+/**
+ * Initialize here elements, which are expensive
+ * or hidden in the beginning.
+ */
+FileManagerUI.prototype.initAdditionalUI = function() {
+ this.searchBox = new SearchBox(this.element_.querySelector('#search-box'));
+};
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/ui/navigation_list.js b/chromium/chrome/browser/resources/file_manager/foreground/js/ui/navigation_list.js
new file mode 100644
index 00000000000..f471aa6c0de
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/ui/navigation_list.js
@@ -0,0 +1,386 @@
+// 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.
+
+'use strict';
+
+/**
+ * A navigation list item.
+ * @constructor
+ * @extends {HTMLLIElement}
+ */
+var NavigationListItem = cr.ui.define('li');
+
+NavigationListItem.prototype = {
+ __proto__: HTMLLIElement.prototype,
+ get modelItem() { return this.modelItem_; }
+};
+
+/**
+ * Decorate the item.
+ */
+NavigationListItem.prototype.decorate = function() {
+ // decorate() may be called twice: from the constructor and from
+ // List.createItem(). This check prevents double-decorating.
+ if (this.className)
+ return;
+
+ this.className = 'root-item';
+ this.setAttribute('role', 'option');
+
+ this.iconDiv_ = cr.doc.createElement('div');
+ this.iconDiv_.className = 'volume-icon';
+ this.appendChild(this.iconDiv_);
+
+ this.label_ = cr.doc.createElement('div');
+ this.label_.className = 'root-label';
+ this.appendChild(this.label_);
+
+ cr.defineProperty(this, 'lead', cr.PropertyKind.BOOL_ATTR);
+ cr.defineProperty(this, 'selected', cr.PropertyKind.BOOL_ATTR);
+};
+
+/**
+ * Associate a path with this item.
+ * @param {NavigationModelItem} modelItem NavigationModelItem of this item.
+ * @param {string=} opt_deviceType The type of the device. Available iff the
+ * path represents removable storage.
+ */
+NavigationListItem.prototype.setModelItem =
+ function(modelItem, opt_deviceType) {
+ if (this.modelItem_)
+ console.warn('NavigationListItem.setModelItem should be called only once.');
+
+ this.modelItem_ = modelItem;
+
+ var rootType = PathUtil.getRootType(modelItem.path);
+ this.iconDiv_.setAttribute('volume-type-icon', rootType);
+ if (opt_deviceType) {
+ this.iconDiv_.setAttribute('volume-subtype', opt_deviceType);
+ }
+
+ this.label_.textContent = modelItem.label;
+
+ if (rootType === RootType.ARCHIVE || rootType === RootType.REMOVABLE) {
+ this.eject_ = cr.doc.createElement('div');
+ // Block other mouse handlers.
+ this.eject_.addEventListener(
+ 'mouseup', function(event) { event.stopPropagation() });
+ this.eject_.addEventListener(
+ 'mousedown', function(event) { event.stopPropagation() });
+
+ this.eject_.className = 'root-eject';
+ this.eject_.addEventListener('click', function(event) {
+ event.stopPropagation();
+ cr.dispatchSimpleEvent(this, 'eject');
+ }.bind(this));
+
+ this.appendChild(this.eject_);
+ }
+};
+
+/**
+ * Associate a context menu with this item.
+ * @param {cr.ui.Menu} menu Menu this item.
+ */
+NavigationListItem.prototype.maybeSetContextMenu = function(menu) {
+ if (!this.modelItem_.path) {
+ console.error('NavigationListItem.maybeSetContextMenu must be called ' +
+ 'after setModelItem().');
+ return;
+ }
+
+ var isRoot = PathUtil.isRootPath(this.modelItem_.path);
+ var rootType = PathUtil.getRootType(this.modelItem_.path);
+ // The context menu is shown on the following items:
+ // - Removable and Archive volumes
+ // - Folder shortcuts
+ if (!isRoot ||
+ (rootType != RootType.DRIVE && rootType != RootType.DOWNLOADS))
+ cr.ui.contextMenuHandler.setContextMenu(this, menu);
+};
+
+/**
+ * A navigation list.
+ * @constructor
+ * @extends {cr.ui.List}
+ */
+function NavigationList() {
+}
+
+/**
+ * NavigationList inherits cr.ui.List.
+ */
+NavigationList.prototype = {
+ __proto__: cr.ui.List.prototype,
+
+ set dataModel(dataModel) {
+ if (!this.onListContentChangedBound_)
+ this.onListContentChangedBound_ = this.onListContentChanged_.bind(this);
+
+ if (this.dataModel_) {
+ this.dataModel_.removeEventListener(
+ 'change', this.onListContentChangedBound_);
+ this.dataModel_.removeEventListener(
+ 'permuted', this.onListContentChangedBound_);
+ }
+
+ var parentSetter = cr.ui.List.prototype.__lookupSetter__('dataModel');
+ parentSetter.call(this, dataModel);
+
+ // This must be placed after the parent method is called, in order to make
+ // it sure that the list was changed.
+ dataModel.addEventListener('change', this.onListContentChangedBound_);
+ dataModel.addEventListener('permuted', this.onListContentChangedBound_);
+ },
+
+ get dataModel() {
+ return this.dataModel_;
+ },
+
+ // TODO(yoshiki): Add a setter of 'directoryModel'.
+};
+
+/**
+ * @param {HTMLElement} el Element to be DirectoryItem.
+ * @param {VolumeManagerWrapper} volumeManager The VolumeManager of the system.
+ * @param {DirectoryModel} directoryModel Current DirectoryModel.
+ * folders.
+ */
+NavigationList.decorate = function(el, volumeManager, directoryModel) {
+ el.__proto__ = NavigationList.prototype;
+ el.decorate(volumeManager, directoryModel);
+};
+
+/**
+ * @param {VolumeManagerWrapper} volumeManager The VolumeManager of the system.
+ * @param {DirectoryModel} directoryModel Current DirectoryModel.
+ */
+NavigationList.prototype.decorate = function(volumeManager, directoryModel) {
+ cr.ui.List.decorate(this);
+ this.__proto__ = NavigationList.prototype;
+
+ this.directoryModel_ = directoryModel;
+ this.volumeManager_ = volumeManager;
+ this.selectionModel = new cr.ui.ListSingleSelectionModel();
+
+ this.directoryModel_.addEventListener('directory-changed',
+ this.onCurrentDirectoryChanged_.bind(this));
+ this.selectionModel.addEventListener(
+ 'change', this.onSelectionChange_.bind(this));
+ this.selectionModel.addEventListener(
+ 'beforeChange', this.onBeforeSelectionChange_.bind(this));
+
+ this.scrollBar_ = new ScrollBar();
+ this.scrollBar_.initialize(this.parentNode, this);
+
+ // Overriding default role 'list' set by cr.ui.List.decorate() to 'listbox'
+ // role for better accessibility on ChromeOS.
+ this.setAttribute('role', 'listbox');
+
+ var self = this;
+ this.itemConstructor = function(modelItem) {
+ return self.renderRoot_(modelItem);
+ };
+};
+
+/**
+ * This overrides cr.ui.List.measureItem().
+ * In the method, a temporary element is added/removed from the list, and we
+ * need to omit animations for such temporary items.
+ *
+ * @param {ListItem=} opt_item The list item to be measured.
+ * @return {{height: number, marginTop: number, marginBottom:number,
+ * width: number, marginLeft: number, marginRight:number}} Size.
+ * @override
+ */
+NavigationList.prototype.measureItem = function(opt_item) {
+ this.measuringTemporaryItemNow_ = true;
+ var result = cr.ui.List.prototype.measureItem.call(this, opt_item);
+ this.measuringTemporaryItemNow_ = false;
+ return result;
+};
+
+/**
+ * Creates an element of a navigation list. This method is called from
+ * cr.ui.List internally.
+ *
+ * @param {NavigationModelItem} modelItem NavigationModelItem to be rendered.
+ * @return {NavigationListItem} Rendered element.
+ * @private
+ */
+NavigationList.prototype.renderRoot_ = function(modelItem) {
+ var item = new NavigationListItem();
+ var volumeInfo =
+ PathUtil.isRootPath(modelItem.path) &&
+ this.volumeManager_.getVolumeInfo(modelItem.path);
+ item.setModelItem(modelItem, volumeInfo && volumeInfo.deviceType);
+
+ var handleClick = function() {
+ if (item.selected &&
+ modelItem.path !== this.directoryModel_.getCurrentDirPath()) {
+ metrics.recordUserAction('FolderShortcut.Navigate');
+ this.changeDirectory_(modelItem);
+ }
+ }.bind(this);
+ item.addEventListener('click', handleClick);
+
+ var handleEject = function() {
+ var unmountCommand = cr.doc.querySelector('command#unmount');
+ // Let's make sure 'canExecute' state of the command is properly set for
+ // the root before executing it.
+ unmountCommand.canExecuteChange(item);
+ unmountCommand.execute(item);
+ };
+ item.addEventListener('eject', handleEject);
+
+ if (this.contextMenu_)
+ item.maybeSetContextMenu(this.contextMenu_);
+
+ return item;
+};
+
+/**
+ * Changes the current directory to the given path.
+ * If the given path is not found, a 'shortcut-target-not-found' event is
+ * fired.
+ *
+ * @param {NavigationModelItem} modelItem Directory to be chagned to.
+ * @private
+ */
+NavigationList.prototype.changeDirectory_ = function(modelItem) {
+ var onErrorCallback = function(error) {
+ if (error.code === FileError.NOT_FOUND_ERR)
+ this.dataModel.onItemNotFoundError(modelItem);
+ }.bind(this);
+
+ this.directoryModel_.changeDirectory(modelItem.path, onErrorCallback);
+};
+
+/**
+ * Sets a context menu. Context menu is enabled only on archive and removable
+ * volumes as of now.
+ *
+ * @param {cr.ui.Menu} menu Context menu.
+ */
+NavigationList.prototype.setContextMenu = function(menu) {
+ this.contextMenu_ = menu;
+
+ for (var i = 0; i < this.dataModel.length; i++) {
+ this.getListItemByIndex(i).maybeSetContextMenu(this.contextMenu_);
+ }
+};
+
+/**
+ * Selects the n-th item from the list.
+ *
+ * @param {number} index Item index.
+ * @return {boolean} True for success, otherwise false.
+ */
+NavigationList.prototype.selectByIndex = function(index) {
+ if (index < 0 || index > this.dataModel.length - 1)
+ return false;
+
+ var newModelItem = this.dataModel.item(index);
+ var newPath = newModelItem.path;
+ if (!newPath)
+ return false;
+
+ // Prevents double-moving to the current directory.
+ // eg. When user clicks the item, changing directory has already been done in
+ // click handler.
+ var entry = this.directoryModel_.getCurrentDirEntry();
+ if (entry && entry.fullPath == newPath)
+ return false;
+
+ metrics.recordUserAction('FolderShortcut.Navigate');
+ this.changeDirectory_(newModelItem);
+ return true;
+};
+
+/**
+ * Handler before root item change.
+ * @param {Event} event The event.
+ * @private
+ */
+NavigationList.prototype.onBeforeSelectionChange_ = function(event) {
+ if (event.changes.length == 1 && !event.changes[0].selected)
+ event.preventDefault();
+};
+
+/**
+ * Handler for root item being clicked.
+ * @param {Event} event The event.
+ * @private
+ */
+NavigationList.prototype.onSelectionChange_ = function(event) {
+ // This handler is invoked even when the navigation list itself changes the
+ // selection. In such case, we shouldn't handle the event.
+ if (this.dontHandleSelectionEvent_)
+ return;
+
+ this.selectByIndex(this.selectionModel.selectedIndex);
+};
+
+/**
+ * Invoked when the current directory is changed.
+ * @param {Event} event The event.
+ * @private
+ */
+NavigationList.prototype.onCurrentDirectoryChanged_ = function(event) {
+ this.selectBestMatchItem_();
+};
+
+/**
+ * Invoked when the content in the data model is changed.
+ * @param {Event} event The event.
+ * @private
+ */
+NavigationList.prototype.onListContentChanged_ = function(event) {
+ this.selectBestMatchItem_();
+};
+
+/**
+ * Synchronizes the volume list selection with the current directory, after
+ * it is changed outside of the volume list.
+ * @private
+ */
+NavigationList.prototype.selectBestMatchItem_ = function() {
+ var entry = this.directoryModel_.getCurrentDirEntry();
+ var path = entry && entry.fullPath;
+ if (!path)
+ return;
+
+ // (1) Select the nearest parent directory (including the shortcut folders).
+ var bestMatchIndex = -1;
+ var bestMatchSubStringLen = 0;
+ for (var i = 0; i < this.dataModel.length; i++) {
+ var itemPath = this.dataModel.item(i).path;
+ if (path.indexOf(itemPath) == 0) {
+ if (bestMatchSubStringLen < itemPath.length) {
+ bestMatchIndex = i;
+ bestMatchSubStringLen = itemPath.length;
+ }
+ }
+ }
+ if (bestMatchIndex != -1) {
+ // Not to invoke the handler of this instance, sets the guard.
+ this.dontHandleSelectionEvent_ = true;
+ this.selectionModel.selectedIndex = bestMatchIndex;
+ this.dontHandleSelectionEvent_ = false;
+ return;
+ }
+
+ // (2) Selects the volume of the current directory.
+ var newRootPath = PathUtil.getRootPath(path);
+ for (var i = 0; i < this.dataModel.length; i++) {
+ var itemPath = this.dataModel.item(i).path;
+ if (PathUtil.getRootPath(itemPath) == newRootPath) {
+ // Not to invoke the handler of this instance, sets the guard.
+ this.dontHandleSelectionEvent_ = true;
+ this.selectionModel.selectedIndex = i;
+ this.dontHandleSelectionEvent_ = false;
+ return;
+ }
+ }
+};
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/ui/preview_panel.js b/chromium/chrome/browser/resources/file_manager/foreground/js/ui/preview_panel.js
new file mode 100644
index 00000000000..a8d0db482df
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/ui/preview_panel.js
@@ -0,0 +1,518 @@
+// Copyright (c) 2013 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.
+
+'use strict';
+
+/**
+ * PreviewPanel UI class.
+ * @param {HTMLElement} element DOM Element of preview panel.
+ * @param {PreviewPanel.VisibilityType} visibilityType Initial value of the
+ * visibility type.
+ * @param {MetadataCache} metadataCache Metadata cache.
+ * @param {VolumeManagerWrapper} volumeManager Volume manager.
+ * @constructor
+ * @extends {cr.EventTarget}
+ */
+var PreviewPanel = function(element,
+ visibilityType,
+ metadataCache,
+ volumeManager) {
+ /**
+ * The cached height of preview panel.
+ * @type {number}
+ * @private
+ */
+ this.height_ = 0;
+
+ /**
+ * Visibility type of the preview panel.
+ * @type {PreviewPanel.VisiblityType}
+ * @private
+ */
+ this.visibilityType_ = visibilityType;
+
+ /**
+ * Current entry to be displayed.
+ * @type {Entry}
+ * @private
+ */
+ this.currentEntry_ = null;
+
+ /**
+ * Dom element of the preview panel.
+ * @type {HTMLElement}
+ * @private
+ */
+ this.element_ = element;
+
+ /**
+ * @type {BreadcrumbsController}
+ */
+ this.breadcrumbs = new BreadcrumbsController(
+ element.querySelector('#search-breadcrumbs'),
+ metadataCache,
+ volumeManager);
+
+ /**
+ * @type {PreviewPanel.Thumbnails}
+ */
+ this.thumbnails = new PreviewPanel.Thumbnails(
+ element.querySelector('.preview-thumbnails'), metadataCache);
+
+ /**
+ * @type {HTMLElement}
+ * @private
+ */
+ this.summaryElement_ = element.querySelector('.preview-summary');
+
+ /**
+ * @type {PreviewPanel.CalculatingSizeLabel}
+ * @private
+ */
+ this.calculatingSizeLabel_ = new PreviewPanel.CalculatingSizeLabel(
+ this.summaryElement_.querySelector('.calculating-size'));
+
+ /**
+ * @type {HTMLElement}
+ * @private
+ */
+ this.previewText_ = element.querySelector('.preview-text');
+
+ /**
+ * FileSelection to be displayed.
+ * @type {FileSelection}
+ * @private
+ */
+ this.selection_ = {entries: [], computeBytes: function() {}};
+
+ /**
+ * Sequence value that is incremented by every selection update and is used to
+ * check if the callback is up to date or not.
+ * @type {number}
+ * @private
+ */
+ this.sequence_ = 0;
+
+ /**
+ * @type {VolumeManager}
+ * @private
+ */
+ this.volumeManager_ = volumeManager;
+
+ cr.EventTarget.call(this);
+};
+
+/**
+ * Name of PreviewPanels's event.
+ * @enum {string}
+ * @const
+ */
+PreviewPanel.Event = Object.freeze({
+ // Event to be triggered at the end of visibility change.
+ VISIBILITY_CHANGE: 'visibilityChange'
+});
+
+/**
+ * Visibility type of the preview panel.
+ */
+PreviewPanel.VisibilityType = Object.freeze({
+ // Preview panel always shows.
+ ALWAYS_VISIBLE: 'alwaysVisible',
+ // Preview panel shows when the selection property are set.
+ AUTO: 'auto',
+ // Preview panel does not show.
+ ALWAYS_HIDDEN: 'alwaysHidden'
+});
+
+/**
+ * @private
+ */
+PreviewPanel.Visibility_ = Object.freeze({
+ VISIBLE: 'visible',
+ HIDING: 'hiding',
+ HIDDEN: 'hidden'
+});
+
+PreviewPanel.prototype = {
+ __proto__: cr.EventTarget.prototype,
+
+ /**
+ * Setter for the current entry.
+ * @param {Entry} entry New entry.
+ */
+ set currentEntry(entry) {
+ if (util.isSameEntry(this.currentEntry_, entry))
+ return;
+ this.currentEntry_ = entry;
+ this.updateVisibility_();
+ this.updatePreviewArea_();
+ },
+
+ /**
+ * Setter for the visibility type.
+ * @param {PreviewPanel.VisibilityType} visibilityType New value of visibility
+ * type.
+ */
+ set visibilityType(visibilityType) {
+ this.visibilityType_ = visibilityType;
+ this.updateVisibility_();
+ },
+
+ get visible() {
+ return this.element_.getAttribute('visibility') ==
+ PreviewPanel.Visibility_.VISIBLE;
+ },
+
+ /**
+ * Obtains the height of preview panel.
+ * @return {number} Height of preview panel.
+ */
+ get height() {
+ this.height_ = this.height_ || this.element_.clientHeight;
+ return this.height_;
+ }
+};
+
+/**
+ * Initializes the element.
+ */
+PreviewPanel.prototype.initialize = function() {
+ this.element_.addEventListener('webkitTransitionEnd',
+ this.onTransitionEnd_.bind(this));
+ this.updatePreviewArea_();
+ this.updateVisibility_();
+};
+
+/**
+ * Apply the selection and update the view of the preview panel.
+ * @param {FileSelection} selection Selection to be applied.
+ */
+PreviewPanel.prototype.setSelection = function(selection) {
+ this.sequence_++;
+ this.selection_ = selection;
+ this.updateVisibility_();
+ // If the previw panel is hiding, does not update the current view.
+ if (this.visible)
+ this.updatePreviewArea_();
+};
+
+/**
+ * Update the visibility of the preview panel.
+ * @private
+ */
+PreviewPanel.prototype.updateVisibility_ = function() {
+ // Get the new visibility value.
+ var visibility = this.element_.getAttribute('visibility');
+ var newVisible = null;
+ switch (this.visibilityType_) {
+ case PreviewPanel.VisibilityType.ALWAYS_VISIBLE:
+ newVisible = true;
+ break;
+ case PreviewPanel.VisibilityType.AUTO:
+ newVisible =
+ this.selection_.entries.length !== 0 ||
+ (this.currentEntry_ &&
+ !this.volumeManager_.getLocationInfo(
+ this.currentEntry_).isRootEntry);
+ break;
+ case PreviewPanel.VisibilityType.ALWAYS_HIDDEN:
+ newVisible = false;
+ break;
+ default:
+ console.error('Invalid visibilityType.');
+ return;
+ }
+
+ // If the visibility has been already the new value, just return.
+ if ((visibility == PreviewPanel.Visibility_.VISIBLE && newVisible) ||
+ (visibility == PreviewPanel.Visibility_.HIDDEN && !newVisible))
+ return;
+
+ // Set the new visibility value.
+ if (newVisible) {
+ this.element_.setAttribute('visibility', PreviewPanel.Visibility_.VISIBLE);
+ cr.dispatchSimpleEvent(this, PreviewPanel.Event.VISIBILITY_CHANGE);
+ } else {
+ this.element_.setAttribute('visibility', PreviewPanel.Visibility_.HIDING);
+ }
+};
+
+/**
+ * Update the text in the preview panel.
+ *
+ * @param {boolean} breadCrumbsVisible Whether the bread crumbs is visible or
+ * not.
+ * @private
+ */
+PreviewPanel.prototype.updatePreviewArea_ = function(breadCrumbsVisible) {
+ var selection = this.selection_;
+
+ // Update thumbnails.
+ this.thumbnails.selection = selection.totalCount !== 0 ?
+ selection : {entries: [this.currentEntry_]};
+
+ // Check if the breadcrumb list should show instead on the preview text.
+ var entry;
+ if (this.selection_.totalCount == 1)
+ entry = this.selection_.entries[0];
+ else if (this.selection_.totalCount == 0)
+ entry = this.currentEntry_;
+
+ if (entry) {
+ this.breadcrumbs.show(entry);
+ this.calculatingSizeLabel_.hidden = true;
+ this.previewText_.textContent = '';
+ return;
+ }
+ this.breadcrumbs.hide();
+
+ // Obtains the preview text.
+ var text;
+ if (selection.directoryCount == 0)
+ text = strf('MANY_FILES_SELECTED', selection.fileCount);
+ else if (selection.fileCount == 0)
+ text = strf('MANY_DIRECTORIES_SELECTED', selection.directoryCount);
+ else
+ text = strf('MANY_ENTRIES_SELECTED', selection.totalCount);
+
+ // Obtains the size of files.
+ this.calculatingSizeLabel_.hidden = selection.bytesKnown;
+ if (selection.bytesKnown && selection.showBytes)
+ text += ', ' + util.bytesToString(selection.bytes);
+
+ // Set the preview text to the element.
+ this.previewText_.textContent = text;
+
+ // Request the byte calculation if needed.
+ if (!selection.bytesKnown) {
+ this.selection_.computeBytes(function(sequence) {
+ // Selection has been already updated.
+ if (this.sequence_ != sequence)
+ return;
+ this.updatePreviewArea_();
+ }.bind(this, this.sequence_));
+ }
+};
+
+/**
+ * Event handler to be called at the end of hiding transition.
+ * @param {Event} event The webkitTransitionEnd event.
+ * @private
+ */
+PreviewPanel.prototype.onTransitionEnd_ = function(event) {
+ if (event.target != this.element_ || event.propertyName != 'opacity')
+ return;
+ var visibility = this.element_.getAttribute('visibility');
+ if (visibility != PreviewPanel.Visibility_.HIDING)
+ return;
+ this.element_.setAttribute('visibility', PreviewPanel.Visibility_.HIDDEN);
+ cr.dispatchSimpleEvent(this, PreviewPanel.Event.VISIBILITY_CHANGE);
+};
+
+/**
+ * Animating label that is shown during the bytes of selection entries is being
+ * calculated.
+ *
+ * This label shows dots and varying the number of dots every
+ * CalculatingSizeLabel.PERIOD milliseconds.
+ * @param {HTMLElement} element DOM element of the label.
+ * @constructor
+ */
+PreviewPanel.CalculatingSizeLabel = function(element) {
+ this.element_ = element;
+ this.count_ = 0;
+ this.intervalID_ = null;
+ Object.seal(this);
+};
+
+/**
+ * Time period in milliseconds.
+ * @const {number}
+ */
+PreviewPanel.CalculatingSizeLabel.PERIOD = 500;
+
+PreviewPanel.CalculatingSizeLabel.prototype = {
+ /**
+ * Set visibility of the label.
+ * When it is displayed, the text is animated.
+ * @param {boolean} hidden Whether to hide the label or not.
+ */
+ set hidden(hidden) {
+ this.element_.hidden = hidden;
+ if (!hidden) {
+ if (this.intervalID_ != null)
+ return;
+ this.count_ = 2;
+ this.intervalID_ =
+ setInterval(this.onStep_.bind(this),
+ PreviewPanel.CalculatingSizeLabel.PERIOD);
+ this.onStep_();
+ } else {
+ if (this.intervalID_ == null)
+ return;
+ clearInterval(this.intervalID_);
+ this.intervalID_ = null;
+ }
+ }
+};
+
+/**
+ * Increments the counter and updates the number of dots.
+ * @private
+ */
+PreviewPanel.CalculatingSizeLabel.prototype.onStep_ = function() {
+ var text = str('CALCULATING_SIZE');
+ for (var i = 0; i < ~~(this.count_ / 2) % 4; i++) {
+ text += '.';
+ }
+ this.element_.textContent = text;
+ this.count_++;
+};
+
+/**
+ * Thumbnails on the preview panel.
+ *
+ * @param {HTMLElement} element DOM Element of thumbnail container.
+ * @param {MetadataCache} metadataCache MetadataCache.
+ * @constructor
+ */
+PreviewPanel.Thumbnails = function(element, metadataCache) {
+ this.element_ = element;
+ this.metadataCache_ = metadataCache;
+ this.sequence_ = 0;
+ Object.seal(this);
+};
+
+/**
+ * Maximum number of thumbnails.
+ * @const {number}
+ */
+PreviewPanel.Thumbnails.MAX_THUMBNAIL_COUNT = 4;
+
+/**
+ * Edge length of the thumbnail square.
+ * @const {number}
+ */
+PreviewPanel.Thumbnails.THUMBNAIL_SIZE = 35;
+
+/**
+ * Longer edge length of zoomed thumbnail rectangle.
+ * @const {number}
+ */
+PreviewPanel.Thumbnails.ZOOMED_THUMBNAIL_SIZE = 200;
+
+PreviewPanel.Thumbnails.prototype = {
+ /**
+ * Sets entries to be displayed in the view.
+ * @param {Array.<Entry>} value Entries.
+ */
+ set selection(value) {
+ this.sequence_++;
+ this.loadThumbnails_(value);
+ }
+};
+
+/**
+ * Loads thumbnail images.
+ * @param {FileSelection} selection Selection containing entries that are
+ * sources of images.
+ * @private
+ */
+PreviewPanel.Thumbnails.prototype.loadThumbnails_ = function(selection) {
+ var entries = selection.entries;
+ this.element_.classList.remove('has-zoom');
+ this.element_.innerText = '';
+ var clickHandler = selection.tasks &&
+ selection.tasks.executeDefault.bind(selection.tasks);
+ var length = Math.min(entries.length,
+ PreviewPanel.Thumbnails.MAX_THUMBNAIL_COUNT);
+ for (var i = 0; i < length; i++) {
+ // Create a box.
+ var box = this.element_.ownerDocument.createElement('div');
+ box.style.zIndex = PreviewPanel.Thumbnails.MAX_THUMBNAIL_COUNT + 1 - i;
+
+ // Load the image.
+ if (entries[i]) {
+ FileGrid.decorateThumbnailBox(box,
+ entries[i],
+ this.metadataCache_,
+ ThumbnailLoader.FillMode.FILL,
+ FileGrid.ThumbnailQuality.LOW,
+ i == 0 && length == 1 &&
+ this.setZoomedImage_.bind(this));
+ }
+
+ // Register the click handler.
+ if (clickHandler)
+ box.addEventListener('click', clickHandler);
+
+ // Append
+ this.element_.appendChild(box);
+ }
+};
+
+/**
+ * Create the zoomed version of image and set it to the DOM element to show the
+ * zoomed image.
+ *
+ * @param {Image} image Image to be source of the zoomed image.
+ * @param {transform} transform Transformation to be applied to the image.
+ * @private
+ */
+PreviewPanel.Thumbnails.prototype.setZoomedImage_ = function(image, transform) {
+ if (!image)
+ return;
+ var width = image.width || 0;
+ var height = image.height || 0;
+ if (width == 0 ||
+ height == 0 ||
+ (width < PreviewPanel.Thumbnails.THUMBNAIL_SIZE * 2 &&
+ height < PreviewPanel.Thumbnails.THUMBNAIL_SIZE * 2))
+ return;
+
+ var scale = Math.min(1,
+ PreviewPanel.Thumbnails.ZOOMED_THUMBNAIL_SIZE /
+ Math.max(width, height));
+ var imageWidth = ~~(width * scale);
+ var imageHeight = ~~(height * scale);
+ var zoomedImage = this.element_.ownerDocument.createElement('img');
+
+ if (scale < 0.3) {
+ // Scaling large images kills animation. Downscale it in advance.
+ // Canvas scales images with liner interpolation. Make a larger
+ // image (but small enough to not kill animation) and let IMAGE
+ // scale it smoothly.
+ var INTERMEDIATE_SCALE = 3;
+ var canvas = this.element_.ownerDocument.createElement('canvas');
+ canvas.width = imageWidth * INTERMEDIATE_SCALE;
+ canvas.height = imageHeight * INTERMEDIATE_SCALE;
+ var ctx = canvas.getContext('2d');
+ ctx.drawImage(image, 0, 0, canvas.width, canvas.height);
+ // Using bigger than default compression reduces image size by
+ // several times. Quality degradation compensated by greater resolution.
+ zoomedImage.src = canvas.toDataURL('image/jpeg', 0.6);
+ } else {
+ zoomedImage.src = image.src;
+ }
+
+ var boxWidth = Math.max(PreviewPanel.Thumbnails.THUMBNAIL_SIZE, imageWidth);
+ var boxHeight = Math.max(PreviewPanel.Thumbnails.THUMBNAIL_SIZE, imageHeight);
+ if (transform && transform.rotate90 % 2 == 1) {
+ var t = boxWidth;
+ boxWidth = boxHeight;
+ boxHeight = t;
+ }
+
+ util.applyTransform(zoomedImage, transform);
+
+ var zoomedBox = this.element_.ownerDocument.createElement('div');
+ zoomedBox.className = 'popup';
+ zoomedBox.style.width = boxWidth + 'px';
+ zoomedBox.style.height = boxHeight + 'px';
+ zoomedBox.appendChild(zoomedImage);
+
+ this.element_.appendChild(zoomedBox);
+ this.element_.classList.add('has-zoom');
+ return;
+};
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/ui/progress_center_panel.js b/chromium/chrome/browser/resources/file_manager/foreground/js/ui/progress_center_panel.js
new file mode 100644
index 00000000000..bcfe6d8e747
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/ui/progress_center_panel.js
@@ -0,0 +1,329 @@
+// Copyright 2013 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.
+
+'use strict';
+
+/**
+ * Progress center panel.
+ *
+ * @param {HTMLElement} element DOM Element of the process center panel.
+ * @constructor
+ */
+var ProgressCenterPanel = function(element) {
+ /**
+ * Root element of the progress center.
+ * @type {!HTMLElement}
+ * @private
+ */
+ this.element_ = element;
+
+ /**
+ * Open view containing multiple progress items.
+ * @type {!HTMLElement}
+ * @private
+ */
+ this.openView_ = this.element_.querySelector('#progress-center-open-view');
+
+ /**
+ * Close view that is a summarized progress item.
+ * @type {!HTMLElement}
+ * @private
+ */
+ this.closeView_ = this.element_.querySelector('#progress-center-close-view');
+
+ /**
+ * Toggle animation rule of the progress center.
+ * @type {!CSSKeyFrameRule}
+ * @private
+ */
+ this.toggleAnimation_ = ProgressCenterPanel.getToggleAnimation_(
+ element.ownerDocument);
+
+ /**
+ * Reset is requested but it is pending until the transition of progress bar
+ * is complete.
+ * @type {boolean}
+ * @private
+ */
+ this.resetRequested_ = false;
+
+ /**
+ * Callback to becalled with the ID of the progress item when the cancel
+ * button is clicked.
+ */
+ this.cancelCallback = null;
+
+ Object.seal(this);
+
+ // Register event handlers.
+ element.addEventListener('click', this.onClick_.bind(this));
+ element.addEventListener(
+ 'webkitAnimationEnd', this.onToggleAnimationEnd_.bind(this));
+ element.addEventListener(
+ 'webkitTransitionEnd', this.onItemTransitionEnd_.bind(this));
+};
+
+/**
+ * Updates attributes of the item element.
+ * @param {!HTMLElement} element Element to be updated.
+ * @param {!ProgressCenterItem} item Progress center item.
+ * @private
+ */
+ProgressCenterPanel.updateItemElement_ = function(element, item) {
+ // Sets element attributes.
+ element.setAttribute('data-progress-id', item.id);
+ element.classList.toggle('error', item.state === ProgressItemState.ERROR);
+ element.classList.toggle('cancelable', item.cancelable);
+
+ // Only when the previousWidthRate is not NaN (when style width is already
+ // set) and the progress rate increases, we use transition animation.
+ var previousWidthRate =
+ parseInt(element.querySelector('.progress-track').style.width);
+ var targetWidthRate = item.progressRateInPercent;
+ var animation = !isNaN(previousWidthRate) &&
+ previousWidthRate < targetWidthRate;
+ if (item.state === ProgressItemState.COMPLETED && animation) {
+ // The attribute pre-complete means that the actual operation is already
+ // done but the UI transition of progress bar is not complete.
+ element.setAttribute('pre-complete', '');
+ } else {
+ element.querySelector('label').textContent = item.message;
+ }
+
+ // To commit the property change and to trigger the transition even if the
+ // change is done synchronously, assign the width value asynchronously.
+ var updateTrackWidth = function() {
+ var track = element.querySelector('.progress-track');
+ track.classList.toggle('animated', animation);
+ track.style.width = targetWidthRate + '%';
+ track.hidden = false;
+ };
+ if (animation)
+ setTimeout(updateTrackWidth);
+ else
+ updateTrackWidth();
+};
+
+/**
+ * Obtains the toggle animation keyframes rule from the document.
+ * @param {HTMLDocument} document Document containing the rule.
+ * @return {CSSKeyFrameRules} Animation rule.
+ * @private
+ */
+ProgressCenterPanel.getToggleAnimation_ = function(document) {
+ for (var i = 0; i < document.styleSheets.length; i++) {
+ var styleSheet = document.styleSheets[i];
+ for (var j = 0; j < styleSheet.cssRules.length; j++) {
+ var rule = styleSheet.cssRules[j];
+ if (rule.type === CSSRule.WEBKIT_KEYFRAMES_RULE &&
+ rule.name === 'progress-center-toggle') {
+ return rule;
+ }
+ }
+ }
+ throw new Error('The progress-center-toggle rules is not found.');
+};
+
+/**
+ * Updates an item to the progress center panel.
+ * @param {!ProgressCenterItem} item Item including new contents.
+ */
+ProgressCenterPanel.prototype.updateItem = function(item) {
+ // If reset is requested, force to reset.
+ if (this.resetRequested_)
+ this.reset(true);
+
+ var itemElement = this.getItemElement_(item.id);
+
+ // Check whether the item should be displayed or not by referring its state.
+ switch (item.state) {
+ // Should show the item.
+ case ProgressItemState.PROGRESSING:
+ case ProgressItemState.ERROR:
+ // If the item has not been added yet, create a new element and add it.
+ if (!itemElement) {
+ itemElement = this.createNewItemElement_();
+ this.openView_.insertBefore(itemElement, this.openView_.firstNode);
+ }
+
+ // Update the element by referring the item model.
+ ProgressCenterPanel.updateItemElement_(itemElement, item);
+ this.element_.hidden = false;
+ break;
+
+ // Should not show the item.
+ case ProgressItemState.COMPLETED:
+ case ProgressItemState.CANCELED:
+ // If itemElement is not shown, just break.
+ if (!itemElement)
+ break;
+
+ // If the item is complete state, once update it because it may turn to
+ // have the pre-complete attribute.
+ if (item.state === ProgressItemState.COMPLETED)
+ ProgressCenterPanel.updateItemElement_(itemElement, item);
+
+ // If the item has the pre-complete attribute, keep showing it. Otherwise,
+ // just remove it.
+ if (item.state !== ProgressItemState.COMPLETED ||
+ !itemElement.hasAttribute('pre-complete')) {
+ this.openView_.removeChild(itemElement);
+ }
+ break;
+ }
+};
+
+/**
+ * Updates close showing summarized item.
+ * @param {!ProgressCenterItem} summarizedItem Item to be displayed in the close
+ * view.
+ */
+ProgressCenterPanel.prototype.updateCloseView = function(summarizedItem) {
+ this.closeView_.classList.toggle('single', !summarizedItem.summarized);
+ ProgressCenterPanel.updateItemElement_(this.closeView_, summarizedItem);
+};
+
+/**
+ * Remove all the items.
+ * @param {boolean=} opt_force True if we force to reset and do not wait the
+ * transition of progress bar. False otherwise. False is default.
+ */
+ProgressCenterPanel.prototype.reset = function(opt_force) {
+ if (!opt_force && this.element_.querySelector('[pre-complete]')) {
+ this.resetRequested_ = true;
+ return;
+ }
+
+ // Clear the flag.
+ this.resetRequested_ = false;
+
+ // Clear the all compete item.
+ this.openView_.innerHTML = '';
+
+ // Clear track width of close view.
+ this.closeView_.querySelector('.progress-track').style.width = '';
+
+ // Hide the progress center.
+ this.element_.hidden = true;
+ this.closeView_.querySelector('.progress-track').hidden = true;
+ this.element_.classList.remove('opened');
+};
+
+/**
+ * Gets an item element having the specified ID.
+ * @param {string} id progress item ID.
+ * @return {HTMLElement} Item element having the ID.
+ * @private
+ */
+ProgressCenterPanel.prototype.getItemElement_ = function(id) {
+ var query = 'li[data-progress-id="' + id + '"]';
+ return this.openView_.querySelector(query);
+};
+
+/**
+ * Creates an item element.
+ * @return {HTMLElement} Created item element.
+ * @private
+ */
+ProgressCenterPanel.prototype.createNewItemElement_ = function() {
+ var label = this.element_.ownerDocument.createElement('label');
+ label.className = 'label';
+
+ var progressBarIndicator = this.element_.ownerDocument.createElement('div');
+ progressBarIndicator.className = 'progress-track';
+
+ var progressBar = this.element_.ownerDocument.createElement('div');
+ progressBar.className = 'progress-bar';
+ progressBar.appendChild(progressBarIndicator);
+
+ var cancelButton = this.element_.ownerDocument.createElement('button');
+ cancelButton.className = 'cancel';
+ cancelButton.setAttribute('tabindex', '-1');
+
+ var progressFrame = this.element_.ownerDocument.createElement('div');
+ progressFrame.className = 'progress-frame';
+ progressFrame.appendChild(progressBar);
+ progressFrame.appendChild(cancelButton);
+
+ var itemElement = this.element_.ownerDocument.createElement('li');
+ itemElement.appendChild(label);
+ itemElement.appendChild(progressFrame);
+
+ return itemElement;
+};
+
+/**
+ * Handles the animation end event of the progress center.
+ * @param {Event} event Animation end event.
+ * @private
+ */
+ProgressCenterPanel.prototype.onToggleAnimationEnd_ = function(event) {
+ // Transition end of the root element's height.
+ if (event.target === this.element_ &&
+ event.animationName === 'progress-center-toggle') {
+ this.element_.classList.remove('animated');
+ return;
+ }
+};
+
+/**
+ * Handles the transition end event of items.
+ * @param {Event} event Transition end event.
+ * @private
+ */
+ProgressCenterPanel.prototype.onItemTransitionEnd_ = function(event) {
+ var itemElement = event.target.parentNode.parentNode.parentNode;
+ if (!itemElement.hasAttribute('pre-complete') ||
+ event.propertyName !== 'width')
+ return;
+ if (itemElement !== this.closeView_)
+ this.openView_.removeChild(itemElement);
+ itemElement.removeAttribute('pre-complete');
+
+ if (this.resetRequested_)
+ this.reset();
+};
+
+/**
+ * Handles the click event.
+ * @param {Event} event Click event.
+ * @private
+ */
+ProgressCenterPanel.prototype.onClick_ = function(event) {
+ // Toggle button.
+ if (event.target.classList.contains('toggle') &&
+ (!this.closeView_.classList.contains('single') ||
+ this.element_.classList.contains('opened'))) {
+
+ // If the progress center has already animated, just return.
+ if (this.element_.classList.contains('animated'))
+ return;
+
+ // Obtains current and target height.
+ var currentHeight;
+ var targetHeight;
+ if (this.element_.classList.contains('opened')) {
+ currentHeight = this.openView_.getBoundingClientRect().height;
+ targetHeight = this.closeView_.getBoundingClientRect().height;
+ } else {
+ currentHeight = this.closeView_.getBoundingClientRect().height;
+ targetHeight = this.openView_.getBoundingClientRect().height;
+ }
+
+ // Set styles for animation.
+ this.toggleAnimation_.cssRules[0].style.height = currentHeight + 'px';
+ this.toggleAnimation_.cssRules[1].style.height = targetHeight + 'px';
+ this.element_.classList.add('animated');
+ this.element_.classList.toggle('opened');
+ return;
+ }
+
+ // Cancel button.
+ if (this.cancelCallback) {
+ var id = event.target.classList.contains('toggle') ?
+ this.closeView_.getAttribute('data-progress-id') :
+ event.target.parentNode.parentNode.getAttribute('data-progress-id');
+ this.cancelCallback(id);
+ }
+};
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/ui/search_box.js b/chromium/chrome/browser/resources/file_manager/foreground/js/ui/search_box.js
new file mode 100644
index 00000000000..30a7ea73faf
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/ui/search_box.js
@@ -0,0 +1,202 @@
+// Copyright 2013 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.
+
+'use strict';
+
+/**
+ * Search box.
+ *
+ * @param {element} element Root element of the search box.
+ * @constructor
+ */
+function SearchBox(element) {
+ /**
+ * Autocomplete List.
+ * @type {AutocompleteList}
+ */
+ this.autocompleteList = new SearchBox.AutocompleteList(element.ownerDocument);
+
+ /**
+ * Root element of the search box.
+ * @type {HTMLElement}
+ */
+ this.element = element;
+
+ /**
+ * Text input of the search box.
+ * @type {HTMLElement}
+ */
+ this.inputElement = element.querySelector('input');
+
+ /**
+ * Clear button of the search box.
+ * @type {HTMLElement}
+ */
+ this.clearButton = element.querySelector('.clear');
+
+ /**
+ * Text measure.
+ * @type {TextMeasure}
+ * @private
+ */
+ this.textMeasure_ = new TextMeasure(this.inputElement);
+
+ Object.freeze(this);
+
+ // Register events.
+ this.inputElement.addEventListener('input', this.updateStyles_.bind(this));
+ this.inputElement.addEventListener('keydown', this.onKeyDown_.bind(this));
+ this.inputElement.addEventListener('focus', this.onFocus_.bind(this));
+ this.inputElement.addEventListener('blur', this.onBlur_.bind(this));
+ element.querySelector('.icon').addEventListener(
+ 'click', this.onIconClick_.bind(this));
+ element.parentNode.appendChild(this.autocompleteList);
+}
+
+/**
+ * Autocomplete list for search box.
+ * @param {HTMLDocument} document Document.
+ * @constructor
+ */
+SearchBox.AutocompleteList = function(document) {
+ var self = cr.ui.AutocompleteList.call(this);
+ self.__proto__ = SearchBox.AutocompleteList.prototype;
+ self.id = 'autocomplete-list';
+ self.autoExpands = true;
+ self.itemConstructor = SearchBox.AutocompleteListItem_.bind(null, document);
+ self.addEventListener('mouseover', self.onMouseOver_.bind(self));
+ return self;
+};
+
+SearchBox.AutocompleteList.prototype = {
+ __proto__: cr.ui.AutocompleteList.prototype
+};
+
+/**
+ * Do nothing when a suggestion is selected.
+ * @override
+ */
+SearchBox.AutocompleteList.prototype.handleSelectedSuggestion = function() {};
+
+/**
+ * Change the selection by a mouse over instead of just changing the
+ * color of moused over element with :hover in CSS. Here's why:
+ *
+ * 1) The user selects an item A with up/down keys (item A is highlighted)
+ * 2) Then the user moves the cursor to another item B
+ *
+ * If we just change the color of moused over element (item B), both
+ * the item A and B are highlighted. This is bad. We should change the
+ * selection so only the item B is highlighted.
+ *
+ * @param {Event} event Event.
+ * @private
+ */
+SearchBox.AutocompleteList.prototype.onMouseOver_ = function(event) {
+ if (event.target.itemInfo)
+ this.selectedItem = event.target.itemInfo;
+};
+
+/**
+ * ListItem element for autocomple.
+ *
+ * @param {HTMLDocument} document Document.
+ * @param {Object} item An object representing a suggestion.
+ * @constructor
+ * @private
+ */
+SearchBox.AutocompleteListItem_ = function(document, item) {
+ var li = new cr.ui.ListItem();
+ li.itemInfo = item;
+
+ var icon = document.createElement('div');
+ icon.className = 'detail-icon';
+
+ var text = document.createElement('div');
+ text.className = 'detail-text';
+
+ if (item.isHeaderItem) {
+ icon.setAttribute('search-icon', '');
+ text.innerHTML =
+ strf('SEARCH_DRIVE_HTML', util.htmlEscape(item.searchQuery));
+ } else {
+ var iconType = FileType.getIcon(item.entry);
+ icon.setAttribute('file-type-icon', iconType);
+ // highlightedBaseName is a piece of HTML with meta characters properly
+ // escaped. See the comment at fileBrowserPrivate.searchDriveMetadata().
+ text.innerHTML = item.highlightedBaseName;
+ }
+ li.appendChild(icon);
+ li.appendChild(text);
+ return li;
+};
+
+/**
+ * Updates the size related style.
+ */
+SearchBox.prototype.updateSizeRelatedStyle = function() {
+ // Hide the search box if there is not enough space.
+ this.element.classList.toggle(
+ 'too-short',
+ this.element.clientWidth < 100);
+};
+
+/**
+ * Clears the search query.
+ */
+SearchBox.prototype.clear = function() {
+ this.inputElement.value = '';
+ this.updateStyles_();
+};
+
+/**
+ * Handles a focus event of the search box.
+ * @private
+ */
+SearchBox.prototype.onFocus_ = function() {
+ this.element.classList.toggle('has-cursor', true);
+ this.inputElement.tabIndex = '99'; // See: go/filesapp-tabindex.
+ this.autocompleteList.attachToInput(this.inputElement);
+};
+
+/**
+ * Handles a blur event of the search box.
+ * @private
+ */
+SearchBox.prototype.onBlur_ = function() {
+ this.element.classList.toggle('has-cursor', false);
+ this.inputElement.tabIndex = '-1';
+ this.autocompleteList.detach();
+};
+
+/**
+ * Handles a keydown event of the search box.
+ * @private
+ */
+SearchBox.prototype.onKeyDown_ = function() {
+ // Handle only Esc key now.
+ if (event.keyCode != 27 || this.inputElement.value)
+ return;
+ this.inputElement.blur();
+};
+
+/**
+ * Handles a click event of the search icon.
+ * @private
+ */
+SearchBox.prototype.onIconClick_ = function() {
+ this.inputElement.focus();
+};
+
+/**
+ * Updates styles of the search box.
+ * @private
+ */
+SearchBox.prototype.updateStyles_ = function() {
+ this.element.classList.toggle('has-text',
+ !!this.inputElement.value);
+ var width = this.textMeasure_.getWidth(this.inputElement.value) +
+ 16 /* Extra space to allow leeway. */;
+ this.inputElement.style.width = width + 'px';
+};
diff --git a/chromium/chrome/browser/resources/file_manager/foreground/js/volume_manager_wrapper.js b/chromium/chrome/browser/resources/file_manager/foreground/js/volume_manager_wrapper.js
new file mode 100644
index 00000000000..a9631dc92d9
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/foreground/js/volume_manager_wrapper.js
@@ -0,0 +1,334 @@
+// Copyright 2013 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.
+
+/**
+ * Thin wrapper for VolumeManager. This should be an interface proxy to talk
+ * to VolumeManager. This class also filters Drive related data/events if
+ * driveEnabled is set to false.
+ *
+ * @param {VolumeManagerWrapper.DriveEnabledStatus} driveEnabled DRIVE_ENABLED
+ * if drive should be available. DRIVE_DISABLED if drive related
+ * data/events should be hidden.
+ * @param {DOMWindow} opt_backgroundPage Window object of the background
+ * page. If this is specified, the class skips to get background page.
+ * TOOD(hirono): Let all clients of the class pass the background page and
+ * make the argument not optional.
+ * @constructor
+ * @extends {cr.EventTarget}
+ */
+function VolumeManagerWrapper(driveEnabled, opt_backgroundPage) {
+ cr.EventTarget.call(this);
+
+ this.driveEnabled_ = driveEnabled;
+ this.volumeInfoList = new cr.ui.ArrayDataModel([]);
+
+ this.volumeManager_ = null;
+ this.pendingTasks_ = [];
+ this.onEventBound_ = this.onEvent_.bind(this);
+ this.onVolumeInfoListUpdatedBound_ =
+ this.onVolumeInfoListUpdated_.bind(this);
+
+ this.disposed_ = false;
+
+ // Start initialize the VolumeManager.
+ var queue = new AsyncUtil.Queue();
+
+ if (opt_backgroundPage) {
+ this.backgroundPage_ = opt_backgroundPage;
+ } else {
+ queue.run(function(callNextStep) {
+ chrome.runtime.getBackgroundPage(function(backgroundPage) {
+ this.backgroundPage_ = backgroundPage;
+ callNextStep();
+ }.bind(this));
+ }.bind(this));
+ }
+
+ queue.run(function(callNextStep) {
+ this.backgroundPage_.VolumeManager.getInstance(function(volumeManager) {
+ this.onReady_(volumeManager);
+ callNextStep();
+ }.bind(this));
+ }.bind(this));
+}
+
+/**
+ * If the drive is enabled on the wrapper.
+ * @enum {boolean}
+ */
+VolumeManagerWrapper.DriveEnabledStatus = {
+ DRIVE_ENABLED: true,
+ DRIVE_DISABLED: false
+};
+
+/**
+ * Extends cr.EventTarget.
+ */
+VolumeManagerWrapper.prototype.__proto__ = cr.EventTarget.prototype;
+
+/**
+ * Called when the VolumeManager gets ready for post initialization.
+ * @param {VolumeManager} volumeManager The initialized VolumeManager instance.
+ * @private
+ */
+VolumeManagerWrapper.prototype.onReady_ = function(volumeManager) {
+ if (this.disposed_)
+ return;
+
+ this.volumeManager_ = volumeManager;
+
+ // Subscribe to VolumeManager.
+ this.volumeManager_.addEventListener(
+ 'drive-connection-changed', this.onEventBound_);
+ this.volumeManager_.addEventListener(
+ 'externally-unmounted', this.onEventBound_);
+
+ // Cache volumeInfoList.
+ var volumeInfoList = [];
+ for (var i = 0; i < this.volumeManager_.volumeInfoList.length; i++) {
+ var volumeInfo = this.volumeManager_.volumeInfoList.item(i);
+ // TODO(hidehiko): Filter mounted volumes located on Drive File System.
+ if (!this.driveEnabled_ && volumeInfo.volumeType === util.VolumeType.DRIVE)
+ continue;
+ volumeInfoList.push(volumeInfo);
+ }
+ this.volumeInfoList.splice.apply(
+ this.volumeInfoList,
+ [0, this.volumeInfoList.length].concat(volumeInfoList));
+
+ // Subscribe to VolumeInfoList.
+ // In VolumeInfoList, we only use 'splice' event.
+ this.volumeManager_.volumeInfoList.addEventListener(
+ 'splice', this.onVolumeInfoListUpdatedBound_);
+
+ // Run pending tasks.
+ var pendingTasks = this.pendingTasks_;
+ this.pendingTasks_ = null;
+ for (var i = 0; i < pendingTasks.length; i++)
+ pendingTasks[i]();
+};
+
+/**
+ * Disposes the instance. After the invocation of this method, any other
+ * method should not be called.
+ */
+VolumeManagerWrapper.prototype.dispose = function() {
+ this.disposed_ = true;
+
+ if (!this.volumeManager_)
+ return;
+ this.volumeManager_.removeEventListener(
+ 'drive-connection-changed', this.onEventBound_);
+ this.volumeManager_.removeEventListener(
+ 'externally-unmounted', this.onEventBound_);
+ this.volumeManager_.volumeInfoList.removeEventListener(
+ 'splice', this.onVolumeInfoListUpdatedBound_);
+};
+
+/**
+ * Called on events sent from VolumeManager. This has responsibility to
+ * re-dispatch the event to the listeners.
+ * @param {Event} event Event object sent from VolumeManager.
+ * @private
+ */
+VolumeManagerWrapper.prototype.onEvent_ = function(event) {
+ if (!this.driveEnabled_) {
+ // If the drive is disabled, ignore all drive related events.
+ if (event.type === 'drive-connection-changed' ||
+ (event.type === 'externally-unmounted' &&
+ event.volumeInfo.volumeType === util.VolumeType.DRIVE))
+ return;
+ }
+
+ this.dispatchEvent(event);
+};
+
+/**
+ * Called on events of modifying VolumeInfoList.
+ * @param {Event} event Event object sent from VolumeInfoList.
+ * @private
+ */
+VolumeManagerWrapper.prototype.onVolumeInfoListUpdated_ = function(event) {
+ if (this.driveEnabled_) {
+ // Apply the splice as is.
+ this.volumeInfoList.splice.apply(
+ this.volumeInfoList,
+ [event.index, event.removed.length].concat(event.added));
+ } else {
+ // Filters drive related volumes.
+ var index = event.index;
+ for (var i = 0; i < event.index; i++) {
+ if (this.volumeManager_.volumeInfoList.item(i).volumeType ===
+ util.VolumeType.DRIVE)
+ index--;
+ }
+
+ var numRemovedVolumes = 0;
+ for (var i = 0; i < event.removed.length; i++) {
+ if (event.removed[i].volumeType !== util.VolumeType.DRIVE)
+ numRemovedVolumes++;
+ }
+
+ var addedVolumes = [];
+ for (var i = 0; i < event.added.length; i++) {
+ var volumeInfo = event.added[i];
+ if (volumeInfo.volumeType !== util.VolumeType.DRIVE)
+ addedVolumes.push(volumeInfo);
+ }
+
+ this.volumeInfoList.splice.apply(
+ this.volumeInfoList,
+ [index, numRemovedVolumes].concat(addedVolumes));
+ }
+};
+
+/**
+ * Ensures the VolumeManager is initialized, and then invokes callback.
+ * If the VolumeManager is already initialized, callback will be called
+ * immediately.
+ * @param {function()} callback Called on initialization completion.
+ */
+VolumeManagerWrapper.prototype.ensureInitialized = function(callback) {
+ if (this.pendingTasks_) {
+ this.pendingTasks_.push(this.ensureInitialized.bind(this, callback));
+ return;
+ }
+
+ callback();
+};
+
+/**
+ * @return {util.DriveConnectionType} Current drive connection state.
+ */
+VolumeManagerWrapper.prototype.getDriveConnectionState = function() {
+ if (!this.driveEnabled_ || !this.volumeManager_) {
+ return {
+ type: util.DriveConnectionType.OFFLINE,
+ reason: util.DriveConnectionReason.NO_SERVICE
+ };
+ }
+
+ return this.volumeManager_.getDriveConnectionState();
+};
+
+/**
+ * @param {string} mountPath The path to mount location of the volume.
+ * @return {VolumeInfo} The VolumeInfo instance for the volume mounted at
+ * mountPath, or null if no volume is found
+ */
+VolumeManagerWrapper.prototype.getVolumeInfo = function(mountPath) {
+ return this.filterDisabledDriveVolume_(
+ this.volumeManager_ && this.volumeManager_.getVolumeInfo(mountPath));
+};
+
+/**
+ * Obtains a volume information from a file entry URL.
+ * TODO(hirono): Check a file system to find a volume.
+ *
+ * @param {string} url URL of entry.
+ * @return {VolumeInfo} Volume info.
+ */
+VolumeManagerWrapper.prototype.getVolumeInfoByURL = function(url) {
+ return this.filterDisabledDriveVolume_(
+ this.volumeManager_ && this.volumeManager_.getVolumeInfoByURL(url));
+};
+
+/**
+ * Obtains a volume infomration of the current profile.
+ *
+ * @param {util.VolumeType} volumeType Volume type.
+ * @return {VolumeInfo} Found volume info.
+ */
+VolumeManagerWrapper.prototype.getCurrentProfileVolumeInfo =
+ function(volumeType) {
+ return this.filterDisabledDriveVolume_(
+ this.volumeManager_ &&
+ this.volumeManager_.getCurrentProfileVolumeInfo(volumeType));
+};
+
+/**
+ * Obtains location information from an entry.
+ *
+ * @param {Entry} entry File or directory entry.
+ * @return {EntryLocation} Location information.
+ */
+VolumeManagerWrapper.prototype.getLocationInfo = function(entry) {
+ return this.volumeManager_ && this.volumeManager_.getLocationInfo(entry);
+};
+
+/**
+ * Requests to mount the archive file.
+ * @param {string} fileUrl The path to the archive file to be mounted.
+ * @param {function(string)} successCallback Called with mount path on success.
+ * @param {function(util.VolumeError)} errorCallback Called when an error
+ * occurs.
+ */
+VolumeManagerWrapper.prototype.mountArchive = function(
+ fileUrl, successCallback, errorCallback) {
+ if (this.pendingTasks_) {
+ this.pendingTasks_.push(
+ this.mountArchive.bind(this, fileUrl, successCallback, errorCallback));
+ return;
+ }
+
+ this.volumeManager_.mountArchive(fileUrl, successCallback, errorCallback);
+};
+
+/**
+ * Requests unmount the volume at mountPath.
+ * @param {string} mountPath The path to the mount location of the volume.
+ * @param {function(string)} successCallback Called with the mount path
+ * on success.
+ * @param {function(util.VolumeError)} errorCallback Called when an error
+ * occurs.
+ */
+VolumeManagerWrapper.prototype.unmount = function(
+ mountPath, successCallback, errorCallback) {
+ if (this.pendingTasks_) {
+ this.pendingTasks_.push(
+ this.unmount.bind(this, mountPath, successCallback, errorCallback));
+ return;
+ }
+
+ this.volumeManager_.unmount(mountPath, successCallback, errorCallback);
+};
+
+/**
+ * Resolves the absolute path to an entry instance.
+ * @param {string} path The path to be resolved.
+ * @param {function(Entry)} successCallback Called with the resolved entry
+ * on success.
+ * @param {function(FileError)} errorCallback Called with the error on error.
+ */
+VolumeManagerWrapper.prototype.resolveAbsolutePath = function(
+ path, successCallback, errorCallback) {
+ if (this.pendingTasks_) {
+ this.pendingTasks_.push(this.resolveAbsolutePath.bind(
+ this, path, successCallback, errorCallback));
+ return;
+ }
+
+ // If the drive is disabled, any resolving the path under drive should be
+ // failed.
+ if (!this.driveEnabled_ && PathUtil.isDriveBasedPath(path)) {
+ errorCallback(util.createFileError(FileError.NOT_FOUND_ERR));
+ return;
+ }
+
+ this.volumeManager_.resolveAbsolutePath(path, successCallback, errorCallback);
+};
+
+/**
+ * Filters volume info by referring driveEnabled.
+ *
+ * @param {VolumeInfo} volumeInfo Volume info.
+ * @return {VolumeInfo} Null if the drive is disabled and the given volume is
+ * drive. Otherwise just returns the volume.
+ * @private
+ */
+VolumeManagerWrapper.prototype.filterDisabledDriveVolume_ =
+ function(volumeInfo) {
+ var isDrive = volumeInfo && volumeInfo.volumeType === util.VolumeType.DRIVE;
+ return this.driveEnabled_ || !isDrive ? volumeInfo : null;
+};
diff --git a/chromium/chrome/browser/resources/file_manager/gallery.html b/chromium/chrome/browser/resources/file_manager/gallery.html
new file mode 100644
index 00000000000..02e3e4fc25e
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/gallery.html
@@ -0,0 +1,79 @@
+<!--
+ -- 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.
+ -->
+<html>
+<head>
+ <link rel="stylesheet" href="foreground/css/list.css">
+ <link rel="stylesheet" href="foreground/css/common.css">
+ <link rel="stylesheet" href="foreground/css/file_types.css">
+ <link rel="stylesheet" href="foreground/css/gallery.css">
+ <link rel="stylesheet" href="foreground/css/media_controls.css">
+
+ <!-- Don't load gallery_scripts.js when flattening is disabled -->
+ <if expr="0"><!-- </if>
+ <script src="foreground/js/photo/gallery_scripts.js"></script>
+ <if expr="0"> --></if>
+
+ <if expr="0">
+ <!-- This section is used when the file manager is loaded with
+ 'filemgr-ext-path' command-line flag. -->
+ <!-- Keep the list in sync with gallery_scripts.js. -->
+ <script src="foreground/js/metrics.js"></script>
+
+ <!-- Loads the client of the image loader extension -->
+ <script src="chrome-extension://pmfjbimdmchhbnneeidfognadeopoehp/image_loader_client.js"></script>
+
+ <script src="chrome://resources/js/cr.js"></script>
+ <script src="chrome://resources/js/event_tracker.js"></script>
+ <script src="chrome://resources/js/load_time_data.js"></script>
+
+ <script src="chrome://resources/js/cr/ui.js"></script>
+ <script src="chrome://resources/js/cr/event_target.js"></script>
+ <script src="chrome://resources/js/cr/ui/touch_handler.js"></script>
+ <script src="chrome://resources/js/cr/ui/array_data_model.js"></script>
+ <script src="chrome://resources/js/cr/ui/dialogs.js"></script>
+ <script src="chrome://resources/js/cr/ui/list_item.js"></script>
+ <script src="chrome://resources/js/cr/ui/list_selection_model.js"></script>
+ <script src="chrome://resources/js/cr/ui/list_single_selection_model.js"></script>
+ <script src="chrome://resources/js/cr/ui/list_selection_controller.js"></script>
+ <script src="chrome://resources/js/cr/ui/list.js"></script>
+ <script src="chrome://resources/js/cr/ui/grid.js"></script>
+
+ <script src="common/js/async_util.js"></script>
+ <script src="common/js/util.js"></script>
+ <script src="common/js/path_util.js"></script>
+
+ <script src="foreground/js/file_type.js"></script>
+ <script src="foreground/js/volume_manager_wrapper.js"></script>
+
+ <script src="foreground/js/image_editor/image_util.js"></script>
+ <script src="foreground/js/image_editor/viewport.js"></script>
+ <script src="foreground/js/image_editor/image_buffer.js"></script>
+ <script src="foreground/js/image_editor/image_view.js"></script>
+ <script src="foreground/js/image_editor/commands.js"></script>
+ <script src="foreground/js/image_editor/image_editor.js"></script>
+ <script src="foreground/js/image_editor/image_transform.js"></script>
+ <script src="foreground/js/image_editor/image_adjust.js"></script>
+ <script src="foreground/js/image_editor/filter.js"></script>
+ <script src="foreground/js/image_editor/image_encoder.js"></script>
+ <script src="foreground/js/image_editor/exif_encoder.js"></script>
+
+ <script src="foreground/js/media/media_controls.js"></script>
+ <script src="foreground/js/media/media_util.js"></script>
+ <script src="foreground/js/media/util.js"></script>
+
+ <script src="foreground/js/metadata/metadata_cache.js"></script>
+
+ <script src="foreground/js/photo/gallery.js"></script>
+ <script src="foreground/js/photo/gallery_item.js"></script>
+ <script src="foreground/js/photo/mosaic_mode.js"></script>
+ <script src="foreground/js/photo/slide_mode.js"></script>
+ <script src="foreground/js/photo/ribbon.js"></script>
+ </if>
+</head>
+<body>
+ <div class="gallery"></div>
+</body>
+</html>
diff --git a/chromium/chrome/browser/resources/file_manager/main.html b/chromium/chrome/browser/resources/file_manager/main.html
new file mode 100644
index 00000000000..7ae3c4ff98e
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/main.html
@@ -0,0 +1,413 @@
+<!DOCTYPE HTML>
+<!--
+ -- 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.
+ -->
+<html i18n-values="dir:textdirection;">
+ <head>
+ <!-- We have to set some default title, or chrome will use the page name.
+ -- As soon as we init and change to a directory, we'll show the directory
+ -- path. Until then, use an invisible non-whitespace character.
+ -->
+ <title>&#xFEFF;</title>
+
+ <meta name="google" value="notranslate">
+
+ <link rel="stylesheet" href="chrome://resources/css/apps/topbutton_bar.css"></link>
+
+ <link rel="stylesheet" href="foreground/css/list.css"></link>
+ <link rel="stylesheet" href="foreground/css/table.css"></link>
+ <link rel="stylesheet" href="foreground/css/tree.css"></link>
+ <link rel="stylesheet" href="foreground/css/menu.css"></link>
+ <link rel="stylesheet" href="foreground/css/combobutton.css"></link>
+ <link rel="stylesheet" href="foreground/css/file_manager.css"></link>
+ <link rel="stylesheet" href="foreground/css/file_types.css"></link>
+ <link rel="stylesheet" href="foreground/css/common.css"></link>
+
+ <!-- Don't load main_scripts.js when flattening is disabled. -->
+ <if expr="0"><!-- </if>
+ <script src="foreground/js/main_scripts.js"></script>
+ <if expr="0"> --></if>
+
+ <if expr="0">
+ <!-- This section is used when the file manager is loaded with
+ 'filemgr-ext-path' command-line flag. -->
+ <!-- Keep the list in sync with js/main_scripts.js. -->
+
+ <!-- metrics.js initiates load performance tracking
+ so we want to parse it as early as possible -->
+ <script src="foreground/js/metrics.js"></script>
+
+ <!-- Loads the client of the image loader extension -->
+ <script src="chrome-extension://pmfjbimdmchhbnneeidfognadeopoehp/image_loader_client.js"></script>
+
+ <script src="chrome://resources/js/load_time_data.js"></script>
+ <script src="chrome://resources/js/cr.js"></script>
+ <script src="chrome://resources/js/util.js"></script>
+ <script src="chrome://resources/js/i18n_template_no_process.js"></script>
+
+ <script src="chrome://resources/js/event_tracker.js"></script>
+ <script src="chrome://resources/js/cr/ui.js"></script>
+ <script src="chrome://resources/js/cr/event_target.js"></script>
+ <script src="chrome://resources/js/cr/ui/touch_handler.js"></script>
+ <script src="chrome://resources/js/cr/ui/array_data_model.js"></script>
+ <script src="chrome://resources/js/cr/ui/dialogs.js"></script>
+ <script src="chrome://resources/js/cr/ui/list_item.js"></script>
+ <script src="chrome://resources/js/cr/ui/list_selection_model.js"></script>
+ <script src="chrome://resources/js/cr/ui/list_single_selection_model.js"></script>
+ <script src="chrome://resources/js/cr/ui/list_selection_controller.js"></script>
+ <script src="chrome://resources/js/cr/ui/list.js"></script>
+ <script src="chrome://resources/js/cr/ui/tree.js"></script>
+ <script src="chrome://resources/js/cr/ui/autocomplete_list.js"></script>
+
+ <script src="chrome://resources/js/cr/ui/splitter.js"></script>
+ <script src="chrome://resources/js/cr/ui/table/table_splitter.js"></script>
+
+ <script src="chrome://resources/js/cr/ui/table/table_column.js"></script>
+ <script src="chrome://resources/js/cr/ui/table/table_column_model.js"></script>
+ <script src="chrome://resources/js/cr/ui/table/table_header.js"></script>
+ <script src="chrome://resources/js/cr/ui/table/table_list.js"></script>
+ <script src="chrome://resources/js/cr/ui/table.js"></script>
+
+ <script src="chrome://resources/js/cr/ui/grid.js"></script>
+
+ <script src="chrome://resources/js/cr/ui/command.js"></script>
+ <script src="chrome://resources/js/cr/ui/position_util.js"></script>
+ <script src="chrome://resources/js/cr/ui/menu_item.js"></script>
+ <script src="chrome://resources/js/cr/ui/menu.js"></script>
+ <script src="chrome://resources/js/cr/ui/menu_button.js"></script>
+ <script src="chrome://resources/js/cr/ui/context_menu_handler.js"></script>
+
+ <!-- This script must be loaded before all other Files.app's scripts. -->
+ <script src="foreground/js/error_counter.js"></script>
+
+ <script src="common/js/async_util.js"></script>
+ <script src="common/js/path_util.js"></script>
+ <script src="common/js/util.js"></script>
+ <script src="common/js/progress_center_common.js"></script>
+
+ <script src="foreground/js/combobutton.js"></script>
+ <script src="foreground/js/commandbutton.js"></script>
+ <script src="foreground/js/ui/file_manager_dialog_base.js"></script>
+
+ <script src="foreground/js/app_installer.js"></script>
+ <script src="foreground/js/cws_container_client.js"></script>
+ <script src="foreground/js/directory_contents.js"></script>
+ <script src="foreground/js/directory_model.js"></script>
+ <script src="foreground/js/directory_tree.js"></script>
+ <script src="foreground/js/drag_selector.js"></script>
+ <script src="foreground/js/drive_banners.js"></script>
+ <script src="foreground/js/error_dialog.js"></script>
+ <script src="foreground/js/file_operation_manager_wrapper.js"></script>
+ <script src="foreground/js/file_grid.js"></script>
+ <script src="foreground/js/file_manager.js"></script>
+ <script src="foreground/js/file_selection.js"></script>
+ <script src="foreground/js/file_table.js"></script>
+ <script src="foreground/js/file_tasks.js"></script>
+ <script src="foreground/js/file_transfer_controller.js"></script>
+ <script src="foreground/js/file_type.js"></script>
+ <script src="foreground/js/file_watcher.js"></script>
+ <script src="foreground/js/folder_shortcuts_data_model.js"></script>
+ <script src="foreground/js/navigation_list_model.js"></script>
+ <script src="foreground/js/scrollbar.js"></script>
+ <script src="foreground/js/share_client.js"></script>
+ <script src="foreground/js/share_dialog.js"></script>
+ <script src="foreground/js/suggest_apps_dialog.js"></script>
+ <script src="foreground/js/text_measure.js"></script>
+ <script src="foreground/js/tree.css.js"></script>
+ <script src="foreground/js/ui/breadcrumbs_controller.js"></script>
+ <script src="foreground/js/ui/conflict_dialog.js"></script>
+ <script src="foreground/js/ui/file_manager_ui.js"></script>
+ <script src="foreground/js/ui/navigation_list.js"></script>
+ <script src="foreground/js/ui/preview_panel.js"></script>
+ <script src="foreground/js/ui/progress_center_panel.js"></script>
+ <script src="foreground/js/ui/search_box.js"></script>
+ <script src="foreground/js/volume_manager_wrapper.js"></script>
+ <script src="foreground/js/media/media_util.js"></script>
+ <script src="foreground/js/metadata/metadata_cache.js"></script>
+ <script src="foreground/js/default_action_dialog.js"></script>
+ <script src="foreground/js/file_manager_commands.js"></script>
+
+ <!-- For accurate load performance tracking main.js should be
+ the last script to include. -->
+ <script src="foreground/js/main.js"></script>
+ </if>
+
+ </head>
+ <body tabindex="-1" i18n-values=".style.fontFamily:WEB_FONT_FAMILY;
+ .style.fontSize:WEB_FONT_SIZE">
+ <commands>
+ <!-- We have to use U+XXXX notation here according to event.keyIdentifier
+ property -->
+ <command id="cut" i18n-values="label:CUT_BUTTON_LABEL"
+ shortcut="U+0058-Ctrl">
+ <command id="copy" i18n-values="label:COPY_BUTTON_LABEL"
+ shortcut="U+0043-Ctrl">
+ <command id="paste" i18n-values="label:PASTE_BUTTON_LABEL"
+ shortcut="U+0056-Ctrl">
+ <command id="rename" i18n-values="label:RENAME_BUTTON_LABEL"
+ shortcut="Enter-Ctrl">
+ <command id="delete" shortcut="U+007F">
+ <command id="create-folder-shortcut"
+ i18n-values="label:CREATE_FOLDER_SHORTCUT_BUTTON_LABEL">
+ <command id="remove-folder-shortcut"
+ i18n-values="label:REMOVE_FOLDER_SHORTCUT_BUTTON_LABEL">
+ <command id="new-folder" i18n-values="label:NEW_FOLDER_BUTTON_LABEL"
+ shortcut="U+0045-Ctrl">
+ <command id="new-window" i18n-values="label:NEW_WINDOW_BUTTON_LABEL"
+ shortcut="U+004E-Ctrl">
+
+ <command id="search" shortcut="U+0046-Ctrl U+00BF">
+
+ <!-- Shortcuts for toggling between volumes (CTRL-1..CTRL-9). -->
+ <command id="volume-switch-1" shortcut="U+0031-Ctrl">
+ <command id="volume-switch-2" shortcut="U+0032-Ctrl">
+ <command id="volume-switch-3" shortcut="U+0033-Ctrl">
+ <command id="volume-switch-4" shortcut="U+0034-Ctrl">
+ <command id="volume-switch-5" shortcut="U+0035-Ctrl">
+ <command id="volume-switch-6" shortcut="U+0036-Ctrl">
+ <command id="volume-switch-7" shortcut="U+0037-Ctrl">
+ <command id="volume-switch-8" shortcut="U+0038-Ctrl">
+ <command id="volume-switch-9" shortcut="U+0039-Ctrl">
+
+ <command id="unmount" i18n-values="label:UNMOUNT_DEVICE_BUTTON_LABEL">
+ <command id="format" i18n-values="label:FORMAT_DEVICE_BUTTON_LABEL">
+
+ <command id="volume-help" i18n-values="label:DRIVE_MENU_HELP">
+ <command id="drive-buy-more-space"
+ i18n-values="label:DRIVE_BUY_MORE_SPACE">
+ <command id="drive-go-to-drive"
+ i18n-values="label:DRIVE_VISIT_DRIVE_GOOGLE_COM">
+
+ <command id="toggle-pinned" i18n-values="label:OFFLINE_COLUMN_LABEL">
+
+ <command id="open-with" i18n-values="label:OPEN_WITH_BUTTON_LABEL">
+ <command id="zip-selection"
+ i18n-values="label:ZIP_SELECTION_BUTTON_LABEL">
+ <command id="share" i18n-values="label:SHARE_BUTTON_LABEL"
+ shortcut="U+00BE" hide-shortcut-text><!-- Shortcut: '.' -->
+
+ <command id="zoom-in" shortcut="U+00BB-Ctrl">
+ <command id="zoom-out" shortcut="U+00BD-Ctrl">
+ <command id="zoom-reset" shortcut="U+0030-Ctrl">
+ </commands>
+
+ <menu id="file-context-menu" class="chrome-menu" showShortcuts>
+ <menuitem id="default-action"
+ visibleif="full-page" hidden></menuitem>
+ <menuitem command="#open-with"
+ visibleif="full-page" hidden></menuitem>
+ <hr id="default-action-separator" visibleif="full-page" hidden>
+ <menuitem command="#toggle-pinned" checkable></menuitem>
+ <menuitem command="#share"></menuitem>
+ <menuitem command="#create-folder-shortcut"></menuitem>
+ <hr command="#share">
+ <menuitem command="#cut" visibleif="full-page"></menuitem>
+ <menuitem command="#copy" visibleif="full-page"></menuitem>
+ <menuitem command="#paste" visibleif="full-page"></menuitem>
+ <hr visibleif="full-page">
+ <menuitem command="#rename"></menuitem>
+ <menuitem command="#delete" i18n-content="DELETE_BUTTON_LABEL"></menuitem>
+ <menuitem command="#zip-selection"></menuitem>
+ <hr visibleif="saveas-file full-page">
+ <menuitem command="#new-folder"
+ visibleif="saveas-file full-page"></menuitem>
+ </menu>
+
+ <menu id="roots-context-menu" class="chrome-menu" >
+ <menuitem command="#unmount"></menuitem>
+ <menuitem command="#format"></menuitem>
+ <menuitem command="#remove-folder-shortcut"></menuitem>
+ </menu>
+
+ <menu id="directory-tree-context-menu" class="chrome-menu" >
+ <menuitem command="#create-folder-shortcut"></menuitem>
+ </menu>
+
+ <menu id="gear-menu" class="chrome-menu" showShortcuts>
+ <menuitem id="gear-menu-newwindow" command="#new-window"></menuitem>
+ <menuitem id="gear-menu-newfolder" command="#new-folder"></menuitem>
+ <hr id="drive-separator">
+ <menuitem id="drive-sync-settings"
+ i18n-content="DRIVE_MOBILE_CONNECTION_OPTION"></menuitem>
+ <menuitem id="drive-hosted-settings"
+ i18n-content="DRIVE_SHOW_HOSTED_FILES_OPTION"></menuitem>
+ <hr>
+ <div i18n-content="VIEW_TYPE_LABEL"></div>
+ <menuitem class="menuitem-button left" id="detail-view"
+ i18n-values="aria-label:DETAIL_VIEW_TOOLTIP">
+ </menuitem>
+ <menuitem class="menuitem-button right" id="thumbnail-view"
+ i18n-values="aria-label:THUMBNAIL_VIEW_TOOLTIP">
+ </menuitem>
+ <hr>
+ <menuitem id="gear-menu-drive-buy-more-space"
+ command="#drive-buy-more-space"></menuitem>
+ <menuitem id="gear-menu-drive-go-to-drive"
+ command="#drive-go-to-drive"></menuitem>
+ <menuitem id="gear-menu-volume-help"
+ command="#volume-help"></menuitem>
+ <hr>
+ <div id="volume-space-info">
+ <div id="volume-space-info-contents">
+ <span id="volume-space-info-label"></span>
+ <div class="progress-bar">
+ <div class="progress-track" id="volume-space-info-bar"></div>
+ </div>
+ </div>
+ </div>
+ </menu>
+
+ <menu id="tasks-menu" class="chrome-menu">
+ </menu>
+
+ <menu id="text-context-menu" class="chrome-menu" showShortcuts>
+ <menuitem command="#cut"></menuitem>
+ <menuitem command="#copy"></menuitem>
+ <menuitem command="#paste"></menuitem>
+ <menuitem command="#delete" i18n-content="DELETE_BUTTON_LABEL"></menuitem>
+ </menu>
+
+ <div class="dialog-container">
+ <div class="dialog-navigation-list">
+ <div class="dialog-navigation-list-header">
+ <span id="app-name"></span>
+ </div>
+ <div class="dialog-navigation-list-contents">
+ <list id="navigation-list" tabindex="14"></list>
+ </div>
+ <div class="dialog-navigation-list-footer">
+ <div id="progress-center" hidden>
+ <li id="progress-center-close-view">
+ <label></label>
+ <div class="progress-frame">
+ <div class="progress-bar">
+ <div class="progress-track" hidden></div>
+ </div>
+ </div>
+ </li>
+ <div id="progress-center-open-view"></div>
+ <button class="toggle" tabindex="-1"></button>
+ </div>
+ </div>
+ </div>
+ <div class="splitter" id="navigation-list-splitter"></div>
+ <div class="dialog-main">
+ <div class="dialog-header">
+ <div id="search-box">
+ <div class="icon"></div>
+ <div class="full-size">
+ <input type="search" tabindex="-1"
+ i18n-values="aria-label:SEARCH_TEXT_LABEL">
+ </div>
+ <button class="clear"></button>
+ </div>
+ <div class="topbutton-bar">
+ <button class="menubutton gear-button" id="gear-button" tabindex="3"
+ menu="#gear-menu"
+ i18n-values="aria-label:GEAR_BUTTON_TOOLTIP"
+ aria-activedescendant="gear-menu">
+ </button>
+ <button class="maximize-button" id="maximize-button"
+ visibleif="full-page" tabindex="-1">
+ </button>
+ <button class="close-button" id="close-button"
+ visibleif="full-page" tabindex="-1">
+ </button>
+ </div>
+ </div>
+ <div class="dialog-body">
+ <div class="main-panel">
+ <!-- The middle bar and spilitter are hidden by default, and will be shown by script if necessary. -->
+ <div class="dialog-middlebar-contents" hidden>
+ <div id="middlebar-header"
+ i18n-content="DRIVE_DIRECTORY_LABEL"></div>
+ <tree id="directory-tree" tabindex="15"></tree>
+ </div>
+ <div class="splitter" id="middlebar-splitter" hidden></div>
+ <div class="filelist-panel">
+ <div class="drive-welcome header"></div>
+ <div class="volume-warning" id="volume-space-warning" hidden></div>
+ <div class="volume-warning" id="drive-auth-failed-warning" hidden>
+ <div class="drive-icon"></div>
+ <div class="drive-text" id="drive-auth-failed-warning-text"></div>
+ </div>
+ <div id="list-container">
+ <div class="detail-table" id="detail-table" tabindex="1" autofocus>
+ </div>
+ <grid class="thumbnail-grid" tabindex="2"></grid>
+ <div class="spinner-layer"></div>
+ <div class="drive-welcome page"></div>
+ <div id="no-search-results"></div>
+ </div>
+ <div class="downloads-warning" hidden></div>
+ </div>
+ </div>
+ <div class="preview-panel progressable" visibility="hidden">
+ <div class="left">
+ <div>
+ <div class="preview-thumbnails"></div>
+ </div>
+ <div id="preview-lines">
+ <div class="preview-summary">
+ <span class="preview-text"></span>
+ <span class="calculating-size"></span>
+ </div>
+ <div id="search-breadcrumbs" class="breadcrumbs"></div>
+ </div>
+ </div>
+ <div class="right buttonbar" visibleif="full-page">
+ <button id="share-button" command="#share" tabindex="4"
+ i18n-values="aria-label:SHARE_BUTTON_LABEL"></button>
+ <button id="tasks" class="combobutton" menu="#tasks-menu"
+ tabindex="5"></button>
+ <button id="delete-button" command="#delete" tabindex="6"
+ i18n-values="aria-label:DELETE_BUTTON_LABEL"></button>
+ </div>
+ <div class="preparing-label" i18n-content="PREPARING_LABEL"></div>
+ <div class="progress-bar">
+ <div class="progress-track"></div>
+ </div>
+ <div class="right buttonbar" id="open-panel"
+ visibleif="open-file open-multi-file">
+ <select class="file-type" hidden></select>
+ <button class="ok" disabled tabindex="7"></button>
+ <button class="cancel" i18n-content="CANCEL_LABEL" tabindex="8"></button>
+ </div>
+ </div>
+ <div id="unmounted-panel"></div>
+ <div id="format-panel">
+ <div class="error"></div>
+ <button id="format-button" command="#format"></button>
+ </div>
+ </div>
+ </div>
+ </div>
+ <div class="dialog-footer progressable" tabindex="-1"
+ visibleif="saveas-file folder upload-folder">
+ <div class="left">
+ <button id="new-folder-button" i18n-content="NEW_FOLDER_BUTTON_LABEL"
+ visibleif="saveas-file folder" command="#new-folder"
+ tabindex="9">
+ </button>
+ <div id="filename-input-box">
+ <div class="filename-label" i18n-content="FILENAME_LABEL"></div>
+ <input type="text" spellcheck="false" tabindex="10">
+ </div>
+ <div class="preparing-label" i18n-content="PREPARING_LABEL"></div>
+ <div class="progress-bar">
+ <div class="progress-track"></div>
+ </div>
+ </div>
+ <div class="right buttonbar">
+ <select class="file-type" hidden></select>
+ <button class="ok" disabled tabindex="11"></button>
+ <button class="cancel" i18n-content="CANCEL_LABEL" tabindex="12"></button>
+ </div>
+ </div>
+ <div id="drag-container"></div>
+ <iframe id="command-dispatcher" hidden></iframe>
+ <div class="overlay-pane" id="iframe-drag-area" hidden></div>
+ </body>
+</html>
diff --git a/chromium/chrome/browser/resources/file_manager/manifest.json b/chromium/chrome/browser/resources/file_manager/manifest.json
new file mode 100644
index 00000000000..a5a58c94645
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/manifest.json
@@ -0,0 +1,231 @@
+{
+ // chrome-extension://hhaomjibdihmijegdhdafkllkbggdgoj/
+ "key": "MIGdMA0GCSqGSIb3DQEBAQUAA4GLADCBhwKBgQDlhpGghtnNJ7pluQN0RDwbUxwwi99oM35ZEaFYvxPLrf0fIEC18cfDdJi6u4aJ+UoSpgzK731L0P/k4LvK2Rz9kVKOy0+IvuRrWkT7lbrLfA1UEBh02OA1AAshjmyRg4IxCqgl8ia8XWq6HKegS1y1KXZYGgb4qp7Bh9VC4cIzswIBIw==",
+ "manifest_version": 2,
+ "name": "Files",
+ "version": "3.0",
+ "description": "File Manager",
+ "incognito" : "split",
+ "icons": {
+ "16": "common/images/icon16.png",
+ "32": "common/images/icon32.png",
+ "48": "common/images/icon48.png",
+ "64": "common/images/icon64.png",
+ "96": "common/images/icon96.png",
+ "128": "common/images/icon128.png",
+ "256": "common/images/icon256.png"
+ },
+ "permissions": [
+ "contextMenus",
+ "experimental",
+ "echoPrivate",
+ "storage",
+ "fileBrowserHandler",
+ "fileBrowserPrivate",
+ "fullscreen",
+ "mediaPlayerPrivate",
+ "mediaGalleriesPrivate",
+ "metricsPrivate",
+ "notifications",
+ "commandLinePrivate",
+ "unlimitedStorage",
+ "webview",
+ // Comment out chrome:// permissions to debug on a desktop browser.
+ "chrome://extension-icon/",
+ "chrome://resources/",
+ "chrome://theme/",
+ "tabs",
+ "clipboardWrite",
+ "clipboardRead",
+ "power",
+ "https://docs.google.com/",
+ "https://*.googleusercontent.com/",
+ "https://drive.google.com/"
+ ],
+ "file_browser_handlers": [
+ {
+ "id": "play",
+ "default_title": "__MSG_PLAY_MEDIA__",
+ "default_icon": "common/images/file_types/200/audio.png",
+ "file_filters": [
+ "filesystem:*.amr",
+ "filesystem:*.flac",
+ "filesystem:*.m4a",
+ "filesystem:*.mp3",
+ "filesystem:*.oga",
+ "filesystem:*.ogg",
+ "filesystem:*.wav"
+ ]
+ },
+ {
+ "id": "watch",
+ "default_title": "__MSG_PLAY_MEDIA__",
+ "default_icon": "common/images/file_types/200/video.png",
+ "file_filters": [
+ "filesystem:*.3gp",
+ "filesystem:*.avi",
+ "filesystem:*.m4v",
+ "filesystem:*.mkv",
+ "filesystem:*.mov",
+ "filesystem:*.mp4",
+ "filesystem:*.mpeg",
+ "filesystem:*.mpeg4",
+ "filesystem:*.mpg",
+ "filesystem:*.mpg4",
+ "filesystem:*.ogm",
+ "filesystem:*.ogv",
+ "filesystem:*.ogx",
+ "filesystem:*.webm"
+ ]
+ },
+ {
+ "id": "mount-archive",
+ "default_title": "__MSG_MOUNT_ARCHIVE__",
+ "default_icon": "common/images/file_types/200/archive.png",
+ "file_filters": [
+ "filesystem:*.rar",
+ "filesystem:*.zip"
+ ]
+ },
+ {
+ "id": "view-pdf",
+ "default_title": "__MSG_OPEN_ACTION__",
+ "default_icon": "common/images/file_types/200/pdf.png",
+ "file_filters": [
+ "filesystem:*.pdf"
+ ]
+ },
+ {
+ "id": "view-swf",
+ "default_title": "__MSG_OPEN_ACTION__",
+ "default_icon": "common/images/file_types/200/generic.png",
+ "file_filters": [
+ "filesystem:*.swf"
+ ]
+ },
+ {
+ "id": "view-in-browser",
+ "default_title": "__MSG_OPEN_ACTION__",
+ "default_icon": "common/images/file_types/200/generic.png",
+ "file_filters": [
+ "filesystem:*.htm",
+ "filesystem:*.html",
+ "filesystem:*.mht",
+ "filesystem:*.mhtml",
+ "filesystem:*.txt"
+ ]
+ },
+ {
+ "id": "gallery",
+ "default_title": "__MSG_OPEN_ACTION__",
+ "default_icon": "common/images/file_types/200/image.png",
+ "file_filters": [
+ // Image formats
+ "filesystem:*.bmp",
+ "filesystem:*.gif",
+ "filesystem:*.ico",
+ "filesystem:*.jpg",
+ "filesystem:*.jpeg",
+ "filesystem:*.png",
+ "filesystem:*.webp",
+ // Video formats
+ "filesystem:*.3gp",
+ "filesystem:*.avi",
+ "filesystem:*.m4v",
+ "filesystem:*.mkv",
+ "filesystem:*.mov",
+ "filesystem:*.mp4",
+ "filesystem:*.mpeg",
+ "filesystem:*.mpeg4",
+ "filesystem:*.mpg",
+ "filesystem:*.mpg4",
+ "filesystem:*.ogm",
+ "filesystem:*.ogv",
+ "filesystem:*.ogx",
+ "filesystem:*.webm"
+ ]
+ },
+ {
+ "id": "open-hosted-generic",
+ "default_title": "__MSG_HOSTED__",
+ "default_icon": "common/images/file_types/200generic.png",
+ "file_filters": [
+ "filesystem:*.gdraw",
+ "filesystem:*.gtable",
+ "filesystem:*.gform"
+ ]
+ },
+ {
+ "id": "open-hosted-gdoc",
+ "default_title": "__MSG_HOSTED__",
+ "default_icon": "common/images/file_types/200/generic.png",
+ "file_filters": [
+ "filesystem:*.gdoc"
+ ]
+ },
+ {
+ "id": "open-hosted-gsheet",
+ "default_title": "__MSG_HOSTED__",
+ "default_icon": "common/images/file_types/200/generic.png",
+ "file_filters": [
+ "filesystem:*.gsheet"
+ ]
+ },
+ {
+ "id": "open-hosted-gslides",
+ "default_title": "__MSG_HOSTED__",
+ "default_icon": "common/images/file_types/200/generic.png",
+ "file_filters": [
+ "filesystem:*.gslides"
+ ]
+ },
+ // The following handlers are used only internally, therefore they do not
+ // have any file filter.
+ // Automatically opens a volume and later close Files.app when unmounted.
+ {
+ "id": "auto-open",
+ "default_title": "__MSG_OPEN_ACTION__",
+ "default_icon": "common/images/file_types/200/generic.png",
+ "file_filters": []
+ },
+ // Selects the passed file after launching Files.app.
+ {
+ "id": "select",
+ "default_title": "__MSG_OPEN_ACTION__",
+ "default_icon": "common/images/file_types/200/generic.png",
+ "file_filters": []
+ },
+ // Opens the passed directory after launching Files.app.
+ {
+ "id": "open",
+ "default_title": "__MSG_OPEN_ACTION__",
+ "default_icon": "common/images/file_types/200/generic.png",
+ "file_filters": []
+ }
+ ],
+ // Required to import scripts in a web worker. Note, that in Apps v2, it is
+ // enough that anything is passed to web_accessible_resources. If there is
+ // at least any file, then all files are allowed. http://crbug.com/179127.
+ "web_accessible_resources": ["foreground/js/metadata/byte_reader.js"],
+ "app": {
+ "background": {
+ "scripts": [
+ "chrome://resources/js/load_time_data.js",
+ "chrome://resources/js/cr.js",
+ "chrome://resources/js/cr/event_target.js",
+ "chrome://resources/js/cr/ui/array_data_model.js",
+ "common/js/async_util.js",
+ "common/js/path_util.js",
+ "common/js/progress_center_common.js",
+ "common/js/util.js",
+ "background/js/volume_manager.js",
+ "background/js/file_operation_handler.js",
+ "background/js/file_operation_manager.js",
+ "background/js/test_util.js",
+ "background/js/progress_center.js",
+ "background/js/background.js"]
+ },
+ // chrome-extension://pmfjbimdmchhbnneeidfognadeopoehp is the image loader extension.
+ "content_security_policy": "default-src 'none'; script-src 'self' chrome://resources chrome-extension://pmfjbimdmchhbnneeidfognadeopoehp; style-src 'self' 'unsafe-inline' chrome://resources; frame-src 'self' about:; img-src 'self' chrome://resources chrome://theme data: https://docs.google.com https://*.googleusercontent.com chrome://extension-icon; media-src 'self' https://*.googleusercontent.com; connect-src https://drive.google.com; object-src 'self'"
+ }
+}
diff --git a/chromium/chrome/browser/resources/file_manager/mediaplayer.html b/chromium/chrome/browser/resources/file_manager/mediaplayer.html
new file mode 100644
index 00000000000..80e2730d09e
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/mediaplayer.html
@@ -0,0 +1,47 @@
+<!--
+ -- 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.
+ -->
+<!DOCTYPE HTML>
+<html>
+<head>
+ <!-- We have to set some default title, or chrome will use the page name.
+ -- As soon as the i18n'd strings are loaded we replace it with the correct
+ -- string. Until then, use an invisible non-whitespace character.
+ -->
+ <title>&#xFEFF;</title>
+ <link rel="icon" type="image/png" href="foreground/images/media/audio_player.png">
+ <link rel="stylesheet" type="text/css" href="foreground/css/media_controls.css">
+ <link rel="stylesheet" type="text/css" href="foreground/css/audio_player.css">
+
+ <!-- Don't load mediaplayer_scripts.js when flattening is disabled -->
+ <if expr="0"><!-- </if>
+ <script src="foreground/js/media/mediaplayer_scripts.js"></script>
+ <if expr="0"> --></if>
+
+ <if expr="0">
+ <!-- This section is used when the file manager is loaded with
+ 'filemgr-ext-path' command-line flag. -->
+ <!-- Keep the list in sync with mediaplayer_scripts.js. -->
+ <script src="chrome://resources/js/cr.js"></script>
+ <script src="chrome://resources/js/cr/event_target.js"></script>
+ <script src="chrome://resources/js/cr/ui/array_data_model.js"></script>
+
+ <script src="common/js/async_util.js"></script>
+ <script src="common/js/util.js"></script>
+ <script src="common/js/path_util.js"></script>
+
+ <script src="foreground/js/file_type.js"></script>
+ <script src="foreground/js/volume_manager_wrapper.js"></script>
+ <script src="foreground/js/metadata/metadata_cache.js"></script>
+
+ <script src="foreground/js/media/media_controls.js"></script>
+ <script src="foreground/js/media/audio_player.js"></script>
+ <script src="foreground/js/media/player_testapi.js"></script>
+ </if>
+</head>
+<body>
+ <div class="audio-player"></div>
+</body>
+</html>
diff --git a/chromium/chrome/browser/resources/file_manager/video_player.html b/chromium/chrome/browser/resources/file_manager/video_player.html
new file mode 100644
index 00000000000..46d4fa82ef8
--- /dev/null
+++ b/chromium/chrome/browser/resources/file_manager/video_player.html
@@ -0,0 +1,57 @@
+<!--
+ -- 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.
+ -->
+<html>
+<head>
+ <!-- We have to set some default title, or chrome will use the page name.
+ -- As soon as the i18n'd strings are loaded we replace it with the correct
+ -- string. Until then, use an invisible non-whitespace character.
+ -->
+ <title>&#xFEFF;</title>
+ <link rel="icon" type="image/png" href="chrome://theme/IDR_FILE_MANAGER_IMG_FILETYPE_VIDEO">
+ <link rel="stylesheet" type="text/css" href="foreground/css/media_controls.css">
+ <link rel="stylesheet" type="text/css" href="foreground/css/video_player.css">
+
+ <!-- Don't load video_player_scripts.js when flattening is disabled -->
+ <if expr="0"><!-- </if>
+ <script src="foreground/js/media/video_player_scripts.js"></script>
+ <if expr="0"> --></if>
+
+ <if expr="0">
+ <!-- This section is used when the file manager is loaded with
+ 'filemgr-ext-path' command-line flag. -->
+ <!-- Keep the list in sync with video_player_scripts.js. -->
+ <script src="chrome://resources/js/cr.js"></script>
+ <script src="chrome://resources/js/cr/event_target.js"></script>
+ <script src="chrome://resources/js/cr/ui/array_data_model.js"></script>
+ <script src="chrome://resources/js/load_time_data.js"></script>
+
+ <script src="common/js/async_util.js"></script>
+ <script src="common/js/util.js"></script>
+ <script src="common/js/path_util.js"></script>
+
+ <script src="foreground/js/file_type.js"></script>
+ <script src="foreground/js/volume_manager_wrapper.js"></script>
+ <script src="foreground/js/metadata/metadata_cache.js"></script>
+
+ <script src="foreground/js/media/media_controls.js"></script>
+ <script src="foreground/js/media/util.js"></script>
+ <script src="foreground/js/media/video_player.js"></script>
+ <script src="foreground/js/media/player_testapi.js"></script>
+ </if>
+</head>
+<body>
+ <div id="video-player" tools>
+ <div id="video-container">
+ </div>
+ <div id="controls-wrapper">
+ <div id="controls" class="tool"></div>
+ </div>
+ <div id="error-wrapper">
+ <div id="error"></div>
+ </div>
+ </div>
+</body>
+</html>