From 61cf2db39cf24b3ca8cef64c21a971b239ef8b1e Mon Sep 17 00:00:00 2001 From: "kaznacheev@chromium.org" Date: Mon, 5 Mar 2012 07:33:53 +0000 Subject: [File Manager] Cleanup: Moving js/css/html files to dedicated directories This is pure refactoring patch, no new functionality is introduced. This retires gallery_demo.html which is superceded by harness.html. BUG= TEST= Review URL: https://chromiumcodereview.appspot.com/9583009 git-svn-id: svn://svn.chromium.org/chrome/trunk/src@124926 0039d316-1c4b-4281-b951-d872f2087c98 --- .../resources/component_extension_resources.grd | 22 +- .../browser/resources/file_manager/css/gallery.css | 797 ++++++++++++++++++ .../resources/file_manager/css/media_controls.css | 491 +++++++++++ chrome/browser/resources/file_manager/gallery.html | 37 + .../resources/file_manager/js/audio_player.js | 273 ------ .../resources/file_manager/js/byte_reader.js | 412 --------- .../resources/file_manager/js/exif_parser.js | 365 -------- .../resources/file_manager/js/file_manager.js | 2 +- .../resources/file_manager/js/function_parallel.js | 74 -- .../resources/file_manager/js/function_sequence.js | 124 --- .../resources/file_manager/js/id3_parser.js | 680 --------------- .../file_manager/js/image_editor/gallery.css | 797 ------------------ .../file_manager/js/image_editor/gallery.html | 38 - .../file_manager/js/image_editor/gallery.js | 2 +- .../file_manager/js/image_editor/gallery_demo.html | 70 -- .../file_manager/js/image_editor/image_editor.css | 106 --- .../js/image_editor/media_controls.css | 491 ----------- .../file_manager/js/image_editor/media_controls.js | 932 --------------------- .../resources/file_manager/js/image_parsers.js | 93 -- .../file_manager/js/media/audio_player.js | 273 ++++++ .../file_manager/js/media/media_controls.js | 932 +++++++++++++++++++++ .../file_manager/js/metadata/byte_reader.js | 412 +++++++++ .../file_manager/js/metadata/exif_parser.js | 365 ++++++++ .../file_manager/js/metadata/function_parallel.js | 74 ++ .../file_manager/js/metadata/function_sequence.js | 124 +++ .../file_manager/js/metadata/id3_parser.js | 679 +++++++++++++++ .../file_manager/js/metadata/image_parsers.js | 93 ++ .../js/metadata/metadata_dispatcher.js | 215 +++++ .../file_manager/js/metadata/metadata_parser.js | 43 + .../file_manager/js/metadata/metadata_provider.js | 115 +++ .../file_manager/js/metadata/mpeg_parser.js | 262 ++++++ .../file_manager/js/metadata_dispatcher.js | 214 ----- .../resources/file_manager/js/metadata_parser.js | 43 - .../resources/file_manager/js/metadata_provider.js | 115 --- .../resources/file_manager/js/mpeg_parser.js | 262 ------ chrome/browser/resources/file_manager/main.html | 2 +- .../resources/file_manager/mediaplayer.html | 8 +- .../browser/resources/file_manager/playlist.html | 155 ---- 38 files changed, 4929 insertions(+), 5263 deletions(-) create mode 100644 chrome/browser/resources/file_manager/css/gallery.css create mode 100644 chrome/browser/resources/file_manager/css/media_controls.css create mode 100644 chrome/browser/resources/file_manager/gallery.html delete mode 100644 chrome/browser/resources/file_manager/js/audio_player.js delete mode 100644 chrome/browser/resources/file_manager/js/byte_reader.js delete mode 100644 chrome/browser/resources/file_manager/js/exif_parser.js delete mode 100644 chrome/browser/resources/file_manager/js/function_parallel.js delete mode 100644 chrome/browser/resources/file_manager/js/function_sequence.js delete mode 100644 chrome/browser/resources/file_manager/js/id3_parser.js delete mode 100644 chrome/browser/resources/file_manager/js/image_editor/gallery.css delete mode 100644 chrome/browser/resources/file_manager/js/image_editor/gallery.html delete mode 100644 chrome/browser/resources/file_manager/js/image_editor/gallery_demo.html delete mode 100644 chrome/browser/resources/file_manager/js/image_editor/image_editor.css delete mode 100644 chrome/browser/resources/file_manager/js/image_editor/media_controls.css delete mode 100644 chrome/browser/resources/file_manager/js/image_editor/media_controls.js delete mode 100644 chrome/browser/resources/file_manager/js/image_parsers.js create mode 100644 chrome/browser/resources/file_manager/js/media/audio_player.js create mode 100644 chrome/browser/resources/file_manager/js/media/media_controls.js create mode 100644 chrome/browser/resources/file_manager/js/metadata/byte_reader.js create mode 100644 chrome/browser/resources/file_manager/js/metadata/exif_parser.js create mode 100644 chrome/browser/resources/file_manager/js/metadata/function_parallel.js create mode 100644 chrome/browser/resources/file_manager/js/metadata/function_sequence.js create mode 100644 chrome/browser/resources/file_manager/js/metadata/id3_parser.js create mode 100644 chrome/browser/resources/file_manager/js/metadata/image_parsers.js create mode 100644 chrome/browser/resources/file_manager/js/metadata/metadata_dispatcher.js create mode 100644 chrome/browser/resources/file_manager/js/metadata/metadata_parser.js create mode 100644 chrome/browser/resources/file_manager/js/metadata/metadata_provider.js create mode 100644 chrome/browser/resources/file_manager/js/metadata/mpeg_parser.js delete mode 100644 chrome/browser/resources/file_manager/js/metadata_dispatcher.js delete mode 100644 chrome/browser/resources/file_manager/js/metadata_parser.js delete mode 100644 chrome/browser/resources/file_manager/js/metadata_provider.js delete mode 100644 chrome/browser/resources/file_manager/js/mpeg_parser.js delete mode 100644 chrome/browser/resources/file_manager/playlist.html diff --git a/chrome/browser/resources/component_extension_resources.grd b/chrome/browser/resources/component_extension_resources.grd index 656f8b8..6f8bbdb 100644 --- a/chrome/browser/resources/component_extension_resources.grd +++ b/chrome/browser/resources/component_extension_resources.grd @@ -27,19 +27,17 @@ - - - - - - - - - - - - + + + + + + + + + + diff --git a/chrome/browser/resources/file_manager/css/gallery.css b/chrome/browser/resources/file_manager/css/gallery.css new file mode 100644 index 0000000..0e4c516 --- /dev/null +++ b/chrome/browser/resources/file_manager/css/gallery.css @@ -0,0 +1,797 @@ +/* + * 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 { + margin: 0; + -webkit-user-select: none; + font-family: Open Sans,Droid Sans Fallback,sans-serif; + font-size: 84%; +} + +.gallery { + background: black; + position: absolute; + left: 0; + right: 0; + top: 0; + bottom: 0; + overflow: hidden; +} + +/* Close button */ +.gallery > .close { + position: absolute; + right: 5px; + top: 5px; + cursor: pointer; + 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 > .close div { + opacity: 0; + width: 20px; + height: 20px; + background-image: url(../images/gallery/close_x.png); + background-repeat: no-repeat; + background-position: center center; +} + +.gallery[tools] > .close div { + opacity: 0.5; +} + +.gallery[tools] > .close div:hover { + opacity: 0.7; + background-color: rgba(81,81,81,1); +} + +/* Image container and canvas elements */ + +.gallery > .image-container { + position: absolute; + height: 100%; + width: 100%; + background-color: rgba(0,0,0,1); + cursor: none; /* Only visible when the toolbar is active */ +} + +.gallery[tools][locked] *, +.gallery[tools][locked] .image-container[cursor] { + cursor: wait; +} + +.gallery[tools] .image-container[cursor='default'] { + cursor: default; +} + +.gallery[tools] .image-container[cursor='move'] { + cursor: url(../images/gallery/cursor_move.png) 18 18; +} + +.gallery[tools] .image-container[cursor='crop'] { + cursor: url(../images/gallery/cursor_crop.png) 18 18; +} + +.gallery[tools] .image-container[cursor='n-resize'], +.gallery[tools] .image-container[cursor='s-resize'] { + cursor: url(../images/gallery/cursor_updown.png) 18 18; +} + +.gallery[tools] .image-container[cursor='e-resize'], +.gallery[tools] .image-container[cursor='w-resize'] { + cursor: url(../images/gallery/cursor_leftright.png) 18 18; +} + +.gallery[tools] .image-container[cursor='nw-resize'], +.gallery[tools] .image-container[cursor='se-resize'] { + cursor: url(../images/gallery/cursor_nwse.png) 18 18; +} + +.gallery[tools] .image-container[cursor='ne-resize'], +.gallery[tools] .image-container[cursor='sw-resize'] { + cursor: url(../images/gallery/cursor_swne.png) 18 18; +} + +.gallery > .image-container > .image { + position: absolute; + pointer-events: none; + + -webkit-transition-property: -webkit-transform, opacity; + -webkit-transition-timing-function: ease-in-out; + + /* Keep in sync with ImageView.ANIMATION_DURATION in image_view.js*/ + -webkit-transition-duration: 180ms; +} + +.gallery > .image-container > .image[fade] { + opacity: 0; +} + +.gallery > .image-container > .image[fade='left'] { + -webkit-transform: translate(-40px,0); +} + +.gallery > .image-container > .image[fade='right'] { + -webkit-transform: translate(40px,0); +} + +/* Toolbar */ + +.gallery > .toolbar { + position: absolute; + bottom: 0; + width: 100%; + height: 55px; + display: -webkit-box; + -webkit-box-orient: horizontal; + -webkit-box-pack: start; + -webkit-box-align: stretch; + background-color: rgba(18,18,18,0.75); + border-top: 1px solid rgba(31,31,31,0.75); + + pointer-events: none; + opacity: 0; + -webkit-transform: translate(0, 0); + + -webkit-transition-property: webkit-transform, opacity; + -webkit-transition-duration: 300ms; + -webkit-transition-timing-function: ease; +} + +.gallery[tools] > .toolbar { + pointer-events: auto; + opacity: 1; + -webkit-transform: translate(0, 0); +} + +.gallery[tools][locked] > .toolbar { + pointer-events: none; +} + +.gallery .arrow-box { + position: absolute; + z-index: 100; + width: 100%; + height: 100%; + pointer-events: none; + display: -webkit-box; + -webkit-box-orient: horizontal; + -webkit-box-align: center; + -webkit-box-pack: center; +} + +.gallery .arrow-box .arrow { + pointer-events: none; +} + +.gallery .arrow-box .arrow-spacer { + -webkit-box-flex: 1; + pointer-events: none; +} + +.gallery[tools] .arrow[active] { + pointer-events: auto; + cursor: pointer; +} + +/* 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{ + width: 105px; + height: 193px; + background-repeat: no-repeat; + background-position: center center; + opacity: 0; +} + +.gallery[tools] .arrow[active] div{ + opacity: 0.25; +} + +.gallery[tools] .arrow[active] div:hover{ + opacity: 1; +} + +.gallery .arrow.left div{ + background-image: url(../images/gallery/arrow_left.png); +} + +.gallery .arrow.right div{ + background-image: url(../images/gallery/arrow_right.png); +} + +/* 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[renaming] .filename-spacer > div, +.gallery .filename-spacer > input { + opacity: 0; + z-index: 0; +} + +.gallery .filename-spacer > div, +.gallery[renaming] .filename-spacer > input { + opacity: 1; + z-index: 1; +} + +.gallery .filename-spacer > input, +.gallery .filename-spacer > div { + font-family: Open Sans,Droid Sans Fallback,sans-serif; + position: absolute; + left: 10px; + top: 15px; + background-color: transparent; + color: white; + width: 260px; + height: 21px; + border: none; + outline: none; + font-size: 120%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.gallery .filename-spacer > div:hover { + background-color: rgba(48, 48, 48, 1.0); + cursor: pointer; +} + +.gallery .filename-spacer > input:focus, +.gallery .filename-spacer > input:focus:hover { + background-color: white; + color: black; + border-right: 1px solid white; + border-bottom: 1px solid white; +} + +.gallery .button-spacer { + display: -webkit-box; + -webkit-box-flex: 1; +} + +/* Thumbnails */ + +.gallery .ribbon-spacer { + position: absolute; + left: 280px; + right: 280px; + height: 100%; + display: -webkit-box; + -webkit-box-orient: horizontal; + -webkit-box-pack: center; + -webkit-box-align: center; +} + +.gallery .toolbar .ribbon { + overflow: hidden; + height: 100%; + -webkit-box-flex: 0; + display: -webkit-box; + -webkit-box-orient: horizontal; + -webkit-box-pack: start; + + -webkit-transition: opacity 180ms linear; + z-index: 0; +} + +.gallery[editing] .toolbar .ribbon { + opacity: 0; +} + +.gallery .ribbon-image { + display: -webkit-box; + -webkit-box-orient: horizontal; + -webkit-box-pack: center; + -webkit-box-align: center; + overflow: hidden; + cursor: pointer; + width: 47px; + height: 47px; + margin: 2px; + border: 2px solid rgba(255,255,255,0); /* transparent white */ + -webkit-transition: margin-left 180ms linear; +} + +.gallery .ribbon-image[selected] { + border: 2px solid rgba(255,233,168,1); +} + +.gallery .toolbar .ribbon.fade-left { + -webkit-mask-image: -webkit-linear-gradient(left, rgba(0,0,0,0) 0, rgba(0,0,0,1) 40px); +} + +.gallery .toolbar .ribbon.fade-right { + -webkit-mask-image: -webkit-linear-gradient(right, rgba(0,0,0,0) 0, rgba(0,0,0,1) 40px); +} + +.gallery .toolbar .ribbon.fade-left.fade-right { + -webkit-mask-image: -webkit-linear-gradient(left, 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 { + position: relative; + overflow: hidden; + width: 45px; + height: 45px; + border: 1px solid rgba(0,0,0,0); /* transparent black */ +} + +/* Editor buttons */ + +.gallery .toolbar .edit-bar { + position: absolute; + overflow: hidden; + pointer-events: none; + right: 0; + width: 100%; + opacity: 0; + height: 55px; + color: white; + display: -webkit-box; + -webkit-box-orient: horizontal; + -webkit-box-pack: center; + -webkit-transition: opacity 180ms linear; +} + +.gallery[editing] .toolbar .edit-bar { + opacity: 1; +} + +.gallery .edit-main { + display: -webkit-box; + -webkit-box-orient: horizontal; + opacity: 0; + -webkit-transition: opacity 250ms ease-in-out; +} + +.gallery[editing] .edit-main { + pointer-events: auto; + opacity: 1.0; +} + +.gallery > .toolbar .button { + -webkit-box-flex: 0; + padding: 0 10px 0 35px; + cursor: pointer; + margin: 8px 0 7px 3px; + height: 40px; + + display: -webkit-box; + -webkit-box-orient: horizontal; + -webkit-box-align: center; + -webkit-box-pack: end; + + background-repeat: no-repeat; + background-position: 5px center; + + opacity: 0.99; /* Workaround for http://crosbug.com/21065 */ +} + +.gallery > .toolbar .button { + background-color: rgba(0,0,0,0); + color: white; +} + +.gallery > .toolbar .button:hover { + background-color: rgba(31,31,31,1); + color: white; +} + +.gallery > .toolbar .button[pressed] { + background-color: rgba(240,240,240,1); + color: black; +} + +.gallery > .toolbar .button[pressed]:hover { + background-color: rgba(240,240,240,1); + color: black; +} + +.gallery > .toolbar .button.autofix { + background-image: url(../images/gallery/icon_autofix.png); +} + +.gallery > .toolbar .button.autofix[pressed] { + background-image: url(../images/gallery/icon_autofix_selected.png); +} + +.gallery > .toolbar .button.crop { + background-image: url(../images/gallery/icon_crop.png); +} + +.gallery > .toolbar .button.crop[pressed] { + background-image: url(../images/gallery/icon_crop_selected.png); +} + +.gallery > .toolbar .button.exposure { + background-image: url(../images/gallery/icon_brightness.png); +} + +.gallery > .toolbar .button.exposure[pressed] { + background-image: url(../images/gallery/icon_brightness_selected.png); +} + +.gallery > .toolbar .button.rotate_right { + background-image: url(../images/gallery/icon_rotate.png); +} + +.gallery > .toolbar .button.rotate_right[pressed] { + background-image: url(../images/gallery/icon_rotate_selected.png); +} + +.gallery > .toolbar .button.rotate_left { + background-image: url(../images/gallery/icon_rotate_left.png); +} + +.gallery > .toolbar .button.rotate_left[pressed] { + background-image: url(../images/gallery/icon_rotate_left_selected.png); +} + +.gallery > .toolbar .button.undo { + background-image: url(../images/gallery/icon_undo.png); +} + +.gallery > .toolbar .button.redo { + position: absolute; /* Exclude from center-packing*/ + background-image: url(../images/gallery/icon_redo.png); +} + +.gallery > .toolbar .button[disabled] { + pointer-events: none; + opacity: 0.5; +} + +.gallery > .toolbar .button[hidden] { + display: none; +} + +.gallery > .toolbar > .button.edit { + position: relative; + z-index: 10; + background-image: url(../images/gallery/icon_edit.png); +} + +.gallery > .toolbar > .button.edit[pressed] { + background-image: url(../images/gallery/icon_edit_selected.png); +} + +.gallery > .toolbar > .button.share { + position: relative; + z-index: 10; + background-image: url(../images/gallery/icon_share.png); +} + +.gallery > .toolbar > .button.share[pressed] { + background-image: url(../images/gallery/icon_share_selected.png); +} + +.gallery > .toolbar > .button:last-child { + margin-right: 8px; +} + +/* Secondary toolbar (mode-specific tools) */ + +.gallery .edit-modal { + position: absolute; + width: 100%; + bottom: 80px; + height: 40px; + display: -webkit-box; + -webkit-box-orient: horizontal; + -webkit-box-pack: center; + pointer-events: none; +} + +.gallery .edit-modal-wrapper[hidden] { + display: none; +} + +.gallery .edit-modal-wrapper { + color: white; + padding-right: 5px; + background-color: rgba(0, 0, 0, 0.75); + display: -webkit-box; + -webkit-box-orient: horizontal; + -webkit-box-pack: center; + -webkit-box-align: center; + pointer-events: auto; +} + +.gallery .edit-modal .label { + height: 20px; + padding-left: 50px; + padding-right: 10px; + + display: -webkit-box; + -webkit-box-orient: horizontal; + -webkit-box-align: center; + + background-repeat: no-repeat; + background-position: 20px center; +} + +.gallery .edit-modal .label.brightness { + background-image: url(../images/gallery/icon_brightness.png); +} + +.gallery .edit-modal .label.contrast { + margin-left: 15px; + background-image: url(../images/gallery/icon_contrast.png); +} + +.gallery .edit-modal .range { + -webkit-appearance: none !important; + height: 3px; + margin-top: 1px; + margin-right: 10px; +} + +.gallery .edit-modal .range::-webkit-slider-thumb { + -webkit-appearance: none; + width: 16px; + height: 29px; + background-image: url(../images/gallery/slider_thumb.png); +} + +/* Crop frame */ + +.gallery .crop-overlay { + position: absolute; + pointer-events: none; + display: -webkit-box; + -webkit-box-orient: vertical; +} + +.gallery .crop-overlay .shadow { + background-color: rgba(0,0,0,0.65); +} + +.gallery .crop-overlay .middle-box { + display: -webkit-box; + -webkit-box-orient: horizontal; + -webkit-box-flex: 1; +} + +.gallery .crop-frame { + position: relative; + display: -webkit-box; + -webkit-box-flex: 1; +} + +.gallery .crop-frame div{ + position: absolute; + background-color: rgba(255, 255, 255, 1); + -webkit-box-shadow: 0 0 3px rgba(0, 0, 0, 0.75); +} + +.gallery .crop-frame .horizontal { + left: 7px; + right: 7px; + height: 1px; +} + +.gallery .crop-frame .horizontal.top { + top: 0; +} + +.gallery .crop-frame .horizontal.bottom { + bottom: 0; +} + +.gallery .crop-frame .vertical { + top: 7px; + bottom: 7px; + width: 1px; +} + +.gallery .crop-frame .vertical.left { + left: 0; +} + +.gallery .crop-frame .vertical.right { + right: 0; +} + +.gallery .crop-frame .corner { + border-radius: 6px; + width: 13px; + height: 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 { + position: absolute; + pointer-events: none; + + width: 100%; + height: 100%; + + display: -webkit-box; + -webkit-box-orient: horizontal; + -webkit-box-align: center; + -webkit-box-pack: center; +} + +.gallery .prompt-wrapper > div.dimmable { + opacity: 1; + -webkit-transition: opacity 220ms ease; +} + +.gallery .prompt { + font-size: 120%; + height: 40px; + padding: 0 20px; + color: white; + background-color: rgba(0, 0, 0, 0.8); + + display: -webkit-box; + -webkit-box-orient: horizontal; + -webkit-box-align: center; + + position: relative; + top: 5px; + opacity: 0; + -webkit-transition: all 180ms ease; +} + +.gallery .prompt[state='fadein'] { + top: 0; + opacity: 1; +} + +.gallery .prompt[state='fadeout'] { + top: 0; + opacity: 0; +} + +.gallery .share-menu { + position: absolute; + right: 10px; + bottom: 60px; + background-color: white; + opacity: 1.0; + -webkit-transition: opacity 500ms ease-in-out; + padding: 8px; + display: -webkit-box; + -webkit-box-orient: vertical; + -webkit-box-align: stretch; + -webkit-box-pack: start; + border: 1px solid #7f7f7f; + -webkit-border-radius: 1px; +} + +.gallery .share-menu .bubble-point { + background-image: url(../images/gallery/bubble_point.png); + background-position: center top; + background-repeat: no-repeat; + position: absolute; + width: 20px; + height: 8px; + bottom: -8px; + right: 20px; + padding: 0; +} + +.gallery .share-menu[hidden] { + opacity: 0.0; + bottom: -100%; /* offscreen so that 'dimmed' attribute does not show it*/ + pointer-events: none; +} + +.gallery .share-menu > div { + cursor: pointer; + background-color: rgba(0,0,0,0); + padding: 5px; + display: -webkit-box; + -webkit-box-align: center; + -webkit-box-pack: start; +} + +.gallery .share-menu > div:hover { + background-color: rgba(240,240,240,1); +} + +.gallery .share-menu > div > img { + margin-right: 5px; + display: block; +} + +/* Video playback support. */ + +.gallery video { + position: absolute; + width: 100%; + height: 100%; +} + +.gallery .video-controls-spacer { + position: absolute; + left: 0; + right: 0; + height: 30px; + bottom: 60px; /* Just above the toolbar */ + display: -webkit-box; + -webkit-box-orient: horizontal; + -webkit-box-align: center; + -webkit-box-pack: center; + pointer-events: none; + opacity: 0; +} + +.gallery[video] .video-controls-spacer { + /* Animate opacity on 'tools' attribute toggle. */ + /* Change opacity immediately on 'video' attribute change. */ + -webkit-transition: opacity 0.28s ease; +} + +.gallery[video][tools] .video-controls-spacer { + opacity: 1; +} + +.gallery .video-controls { + max-width: 800px; + display: none; +} + +.gallery[video] .video-controls { + display: -webkit-box; + -webkit-box-flex: 1; +} + +.gallery[video] > .toolbar .button.edit, +.gallery[video] > .toolbar .edit-bar { + display: none; +} diff --git a/chrome/browser/resources/file_manager/css/media_controls.css b/chrome/browser/resources/file_manager/css/media_controls.css new file mode 100644 index 0000000..ca0b5e3 --- /dev/null +++ b/chrome/browser/resources/file_manager/css/media_controls.css @@ -0,0 +1,491 @@ +/* + * 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 { + position: relative; + width: 28px; + height: 28px; + background-repeat: no-repeat; + background-position: center center; +} + +.media-button > div { + position: absolute; + pointer-events: none; + width: 100%; + height: 100%; + -webkit-transition: opacity 100ms linear; + opacity: 0; +} + +.media-button[state='0']:not(.disabled):not(:hover):not(:active) > .state0.normal, +.media-button[state='0']:not(.disabled):hover > .state0.hover, +.media-button[state='0']:not(.disabled):active > .state0.active, +.media-button[state='1']:not(.disabled):not(:hover):not(:active) > .state1.normal, +.media-button[state='1']:not(.disabled):hover > .state1.hover, +.media-button[state='1']:not(.disabled):active > .state1.active, +.media-button.disabled > .disabled { + opacity: 1; +} + +/* Custom sliders for progress and volume. */ + +/* Customize the standard input[type='range']. */ +.custom-slider > input[type='range'] { + position: absolute; + -webkit-appearance: none !important; /* Hide the default thumb icon. */ + left: -2px; /* Required to align the input element with the parent. */ + top: -2px; + width: 100%; + height: 100%; + background: transparent; /* Hide the standard slider bar */ +} + +/* Custom thumb icon. */ +.custom-slider > input[type='range']::-webkit-slider-thumb { + position: relative; + -webkit-appearance: none; + z-index: 2; + height: 24px; + background-position: center center; + background-repeat: no-repeat; +} + +/* Custom slider bar (we hide the standard one). */ +.custom-slider > .bar { + position: absolute; + /* 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. */ + top: 11px; + bottom: 11px; + border-width: 1px; + border-top-style: solid; + border-bottom-style: solid; + pointer-events: none; /* Mouse events pass through to the standard input. */ +} + +.custom-slider > .bar > .filled, +.custom-slider > .bar > .cap { + position: absolute; + top: -1px; + bottom: -1px; + border-width: 1px; + border-style: solid; +} + +/* The filled portion of the slider bar to the left of the thumb. */ +.custom-slider > .bar > .filled { + width: 0; /* The element style.width is manipulated from the code. */ + left: 0; + border-left-style: none; + border-right-style: none; +} + +/* 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 { + right: 100%; + border-right-style: none; + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; +} + +/* Right cap is always not filled. */ +.custom-slider > .bar > .cap.right { + left: 100%; + border-left-style: none; + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; +} + +.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: -webkit-linear-gradient(#c3c3c3, #d9d9d9); + border-color: #d9d9d9; +} + +.custom-slider.disabled > .bar > .filled, +.custom-slider.disabled > .bar > .cap.left { + background-image: none; + background-color: rgba(0, 0, 0, 0.5); +} + +.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 { + position: absolute; + left: 0; + width: 0; + top: -1px; + bottom: -1px; + background-color: #202020; +} + +.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 { + position: absolute; + left: 0; + bottom: 20px; + + font-size: 13px; + + height: 0; + -webkit-transition: height 0.15s ease; + + color: white; + background: #202020; + border-top-left-radius: 2px; + border-top-right-radius: 2px; + display: -webkit-box; + -webkit-box-orient: horizontal; + -webkit-box-align: center; + -webkit-box-pack: center; + + overflow: hidden; +} + +.custom-slider > .bar > .seek-mark.visible > .seek-label { + height: 15px; +} + +/* Media controls in order of appearance. */ + +/* Play/pause button. */ + +.media-button.play { + margin-left: -7px; + margin-right: -7px; +} + +.media-button.play > .state0.normal { + background-image: url(../images/media/media_play.png); +} + +.media-button.play > .state0.hover { + background-image: url(../images/media/media_play_hover.png); +} + +.media-button.play > .state0.active { + background-image: url(../images/media/media_play_down.png); +} + +.media-button.play > .state1.normal{ + background-image: url(../images/media/media_pause.png); +} + +.media-button.play > .state1.hover { + background-image: url(../images/media/media_pause_hover.png); +} + +.media-button.play > .state1.active { + background-image: url(../images/media/media_pause_down.png); +} + +.media-button.play > .disabled { + background-image: url(../images/media/media_play_disabled.png); +} + +/* Time controls: a slider and a text time display. */ + +.time-controls { + height: 100%; + display: -webkit-box; + -webkit-box-orient: horizontal; + -webkit-box-align: center; + -webkit-box-pack: center; + -webkit-box-flex: 1; +} + +.custom-slider.progress { + position: relative; + height: 100%; + display: -webkit-box; + -webkit-box-flex: 1; + margin-left: -9px; /* Set the margins at the edges of the slider bar. */ + margin-right: -9px; +} + +.custom-slider.progress > input[type='range']::-webkit-slider-thumb { + width: 28px; + background-image: url(../images/media/media_slider_thumb.png); +} + +.custom-slider.progress > input[type='range']::-webkit-slider-thumb:hover { + background-image: url(../images/media/media_slider_thumb_hover.png); +} + +.custom-slider.progress > input[type='range']::-webkit-slider-thumb:active { + background-image: url(../images/media/media_slider_thumb_down.png); +} + +.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 { + position: relative; + height: 100%; + margin-left: 15px; +} + +.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 { + position: absolute; + right: 0; + top: 0; + bottom: 0; + display: -webkit-box; + -webkit-box-orient: horizontal; + -webkit-box-align: center; + -webkit-box-pack: end; + color: white; +} + +/* Volume controls: sound button and volume slider */ + +.volume-controls { + height: 100%; + display: -webkit-box; + -webkit-box-orient: horizontal; + -webkit-box-align: center; + -webkit-box-pack: center; +} + +/* Sound button */ + +.media-button.sound { + margin-left: -4px; + margin-right: -2px; + background-position: -2px center; +} + +.media-button.sound[level='0'] > .normal { + background-image: url(../images/media/media_sound_disabled.png); +} + +.media-button.sound[level='0'] > .hover { + background-image: url(../images/media/media_sound_disabled_hover.png); +} + +.media-button.sound[level='0'] > .active { + background-image: url(../images/media/media_sound_disabled_down.png); +} + + +.media-button.sound[level='1'] > .normal { + background-image: url(../images/media/media_sound_level1.png); +} + +.media-button.sound[level='1'] > .hover { + background-image: url(../images/media/media_sound_level1_hover.png); +} + +.media-button.sound[level='1'] > .active { + background-image: url(../images/media/media_sound_level1_down.png); +} + + +.media-button.sound[level='2'] > .normal { + background-image: url(../images/media/media_sound_level2.png); +} + +.media-button.sound[level='2'] > .hover { + background-image: url(../images/media/media_sound_level2_hover.png); +} + +.media-button.sound[level='2'] > .active { + background-image: url(../images/media/media_sound_level2_down.png); +} + + +.media-button.sound[level='3'] > .normal { + background-image: url(../images/media/media_sound_full.png); +} + +.media-button.sound[level='3'] > .hover { + background-image: url(../images/media/media_sound_full_hover.png); +} + +.media-button.sound[level='3'] > .active { + background-image: url(../images/media/media_sound_full_down.png); +} + + +.media-button.sound > .disabled { + background-image: url(../images/media/media_sound_full_disabled.png); +} + +/* Volume slider. */ + +.custom-slider.volume { + position: relative; + height: 100%; + width: 60px; + margin-left: -4px; + margin-right: -4px; +} + +.custom-slider.volume > input[type='range']::-webkit-slider-thumb { + width: 20px; + background-image: url(../images/media/media_volume_slider_thumb.png); +} + +.custom-slider.volume > input[type='range']::-webkit-slider-thumb:hover { + background-image: url(../images/media/media_volume_slider_thumb_hover.png); +} + +.custom-slider.volume > input[type='range']::-webkit-slider-thumb:active { + background-image: url(../images/media/media_volume_slider_thumb_down.png); +} + +.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 { + pointer-events: auto; + + font-size: 15px; + height: 30px; + border-radius: 5px; + + padding-left: 15px; + padding-right: 15px; + + opacity: 0.8; + + background: #202020; + + display: -webkit-box; + -webkit-box-orient: horizontal; + -webkit-box-align: center; + -webkit-box-pack: center; +} + +.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: url(../images/media/media_fullscreen.png); +} + +.media-button.fullscreen > .hover { + background-image: url(../images/media/media_fullscreen_hover.png); +} + +.media-button.fullscreen > .active { + background-image: url(../images/media/media_fullscreen_down.png); +} + +.media-button.fullscreen > .disabled { + background-image: url(../images/media/media_fullscreen_disabled.png); +} + +.playback-state-icon { + position: absolute; + left: 50%; + top: 50%; + width: 32px; + height: 32px; + margin-left: -16px; + margin-top: -16px; + border-radius: 2.5px; + pointer-events: none; + display: none; + + background-color: #202020; + background-repeat: no-repeat; + background-position: center center; + + opacity: 1; + -webkit-transform: scaleX(1) scaleY(1); + + -webkit-transition-property: webkit-transform, opacity; + -webkit-transition-duration: 500ms; + -webkit-transition-timing-function: linear; + + z-index: 2; +} + +.playback-state-icon[visible] { + display: block; +} + +.playback-state-icon[state] { + opacity: 0; + -webkit-transform: scaleX(3) scaleY(3); +} + +.playback-state-icon[state='play'] { + background-image: url(../images/media/media_play.png); +} + +.playback-state-icon[state='pause'] { + background-image: url(../images/media/media_pause.png); +} diff --git a/chrome/browser/resources/file_manager/gallery.html b/chrome/browser/resources/file_manager/gallery.html new file mode 100644 index 0000000..35d088cd --- /dev/null +++ b/chrome/browser/resources/file_manager/gallery.html @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/chrome/browser/resources/file_manager/js/audio_player.js b/chrome/browser/resources/file_manager/js/audio_player.js deleted file mode 100644 index 564055b..0000000 --- a/chrome/browser/resources/file_manager/js/audio_player.js +++ /dev/null @@ -1,273 +0,0 @@ -// 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. - -document.addEventListener('DOMContentLoaded', function() { - // Test harness sets the search string to prevent the automatic load. - // It calls AudioPlayer.load() explicitly after initializing - // the |chrome| variable with an appropriate mock object. - if (!document.location.search) { - AudioPlayer.load(); - } -}); - -/** - * @param {HTMLElement} container - * @constructor - */ -function AudioPlayer(container) { - this.container_ = container; - this.metadataProvider_ = new MetadataProvider(); - this.currentTrack_ = -1; - this.playlistGeneration_ = 0; - - 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 close').addEventListener( - 'click', function() { chrome.mediaPlayerPrivate.closeWindow() }); - - createChild('title-button collapse').addEventListener( - 'click', this.onExpandCollapse_.bind(this)); - - this.audioControls_ = new AudioControls( - createChild(), this.advance_.bind(this)); - - this.audioControls_.attachMedia(createChild('', 'audio')); -} - -AudioPlayer.load = function() { - document.ondragstart = function(e) { e.preventDefault() }; - document.oncontextmenu = function(e) { e.preventDefault(); }; - - var player = new AudioPlayer(document.querySelector('.audio-player')); - function getPlaylist() { - chrome.mediaPlayerPrivate.getPlaylist(player.load.bind(player)); - } - getPlaylist(); - chrome.mediaPlayerPrivate.onPlaylistChanged.addListener(getPlaylist); -}; - -AudioPlayer.prototype.load = function(playlist) { - this.playlistGeneration_++; - - this.audioControls_.pause(); - - this.currentTrack_ = -1; - - this.urls_ = playlist.items; - - if (this.urls_.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_ = []; - - for (var i = 0; i != this.urls_.length; i++) { - var url = this.urls_[i]; - var onClick = this.select_.bind(this, i); - this.trackListItems_.push( - new AudioPlayer.TrackInfo(this.trackList_, url, onClick)); - this.trackStackItems_.push( - new AudioPlayer.TrackInfo(this.trackStack_, url, onClick)); - } - - this.select_(playlist.position); - - // 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.urls_.length; i++) { - if (i != playlist.position) - this.loadMetadata_(i); - } -}; - -AudioPlayer.prototype.loadMetadata_ = function(track) { - this.metadataProvider_.fetch( - this.urls_[track], - function(generation, metadata) { - // Do nothing if another load happened since the metadata request. - if (this.playlistGeneration_ != generation) - return; - - if (metadata.thumbnailURL) { - this.container_.classList.remove('noart'); - } - this.trackListItems_[track].setMetadata(metadata); - this.trackStackItems_[track].setMetadata(metadata); - }.bind(this, this.playlistGeneration_)); -}; - -AudioPlayer.prototype.select_ = function(newTrack) { - if (this.currentTrack_ == newTrack) return; - - this.changeSelectionInList_(this.currentTrack_, newTrack); - this.changeSelectionInStack_(this.currentTrack_, newTrack); - - this.currentTrack_ = newTrack; - this.scrollToCurrent_(false); - - var media = this.audioControls_.getMedia(); - media.src = this.urls_[this.currentTrack_]; - media.load(); - this.audioControls_.play(); -}; - -AudioPlayer.prototype.changeSelectionInList_ = function(oldTrack, newTrack) { - this.trackListItems_[newTrack].getBox().classList.add('selected'); - - if (oldTrack >= 0) { - this.trackListItems_[oldTrack].getBox().classList.remove('selected'); - } -}; - -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. - */ -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); -}; - -AudioPlayer.prototype.isCompact_ = function() { - return this.container_.classList.contains('collapsed') || - this.container_.classList.contains('single-track'); -}; - -AudioPlayer.prototype.advance_ = function(forward) { - var newTrack = this.currentTrack_ + (forward ? 1 : -1); - if (newTrack < 0) newTrack = this.urls_.length - 1; - if (newTrack == this.urls_.length) newTrack = 0; - this.select_(newTrack); -}; - -AudioPlayer.prototype.onExpandCollapse_ = function() { - this.container_.classList.toggle('collapsed'); - this.syncHeight_(); - if (!this.isCompact_()) - this.scrollToCurrent_(true); -}; - -/* Keep the below constants in sync with the CSS. */ -AudioPlayer.HEADER_HEIGHT = 30; -AudioPlayer.TRACK_HEIGHT = 58; -AudioPlayer.CONTROLS_HEIGHT = 35; - -AudioPlayer.prototype.syncHeight_ = function() { - var expandedListHeight = - Math.min(this.urls_.length, 3) * AudioPlayer.TRACK_HEIGHT; - this.trackList_.style.height = expandedListHeight + 'px'; - - chrome.mediaPlayerPrivate.setWindowHeight( - (this.isCompact_() ? - AudioPlayer.TRACK_HEIGHT : - AudioPlayer.HEADER_HEIGHT + expandedListHeight) + - AudioPlayer.CONTROLS_HEIGHT); -}; - - -/** - * Create a TrackInfo object encapsulating the information about one track. - * - * @param {HTMLElement} container - * @param {string} url - * @param {function} onClick - * @constructor - */ -AudioPlayer.TrackInfo = function(container, url, onClick) { - this.url_ = url; - - 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_); -}; - -AudioPlayer.TrackInfo.prototype.getBox = function() { return this.box_ }; - -AudioPlayer.TrackInfo.prototype.getDefaultTitle = function() { - var title = this.url_.split('/').pop(); - var dotIndex = title.lastIndexOf('.'); - if (dotIndex >= 0) title = title.substr(0, dotIndex); - return title; -}; - -AudioPlayer.TrackInfo.prototype.getDefaultArtist = function() { - return 'Unknown Artist'; // TODO(kaznacheev): i18n -}; - -AudioPlayer.TrackInfo.prototype.setMetadata = function(metadata) { - if (metadata.thumbnailURL) { - this.art_.classList.remove('blank'); - this.img_.src = metadata.thumbnailURL; - } - this.title_.textContent = metadata.title || this.getDefaultTitle(); - this.artist_.textContent = metadata.artist || this.getDefaultArtist(); -}; \ No newline at end of file diff --git a/chrome/browser/resources/file_manager/js/byte_reader.js b/chrome/browser/resources/file_manager/js/byte_reader.js deleted file mode 100644 index 505473c..0000000 --- a/chrome/browser/resources/file_manager/js/byte_reader.js +++ /dev/null @@ -1,412 +0,0 @@ -// 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. - -ByteReader = function(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 const and methods. - -ByteReader.LITTLE_ENDIAN = 0; // Intel, 0x1234 is [0x34, 0x12] -ByteReader.BIG_ENDIAN = 1; // Motorola, 0x1234 is [0x12, 0x34] - -ByteReader.SEEK_BEG = 0; // Seek relative to the beginning of the buffer. -ByteReader.SEEK_CUR = 1; // Seek relative to the current position. -ByteReader.SEEK_END = 2; // Seek relative to the end of the buffer. - -/** - * Throw an error if (0 > pos >= end) or if (pos + size > end). - * - * Static utility function. - */ -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. - */ -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. - */ -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. - */ -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); -}; - -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. - */ -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. - */ -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. - */ -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. - */ -ByteReader.prototype.eof = function() { - return this.pos_ >= this.view_.byteLength; -}; - -/** - * Return true if the current position is before the beginning of the buffer. - */ -ByteReader.prototype.bof = function() { - return this.pos_ < 0; -}; - -/** - * Return true if the current position is outside the buffer. - */ -ByteReader.prototype.beof = function() { - return this.pos_ >= this.view_.byteLength || this.pos_ < 0; -}; - -/** - * Set the expected byte ordering for future reads. - */ -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. - */ -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); -}; - -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. - */ -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. - */ -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. - */ -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. - */ -ByteReader.prototype.readSlice = function(size, opt_end, - opt_arrayConstructor) { - this.validateRead(width, 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. - */ -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. - */ -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. - */ -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. - */ -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. - */ -ByteReader.prototype.tell = function() { - return this.pos_; -}; diff --git a/chrome/browser/resources/file_manager/js/exif_parser.js b/chrome/browser/resources/file_manager/js/exif_parser.js deleted file mode 100644 index 8279564..0000000 --- a/chrome/browser/resources/file_manager/js/exif_parser.js +++ /dev/null @@ -1,365 +0,0 @@ -// 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. - -const EXIF_MARK_SOI = 0xffd8; // Start of image data. -const EXIF_MARK_SOS = 0xffda; // Start of "stream" (the actual image data). -const EXIF_MARK_SOF = 0xffc0; // Start of "frame" -const EXIF_MARK_EXIF = 0xffe1; // Start of exif block. - -const EXIF_ALIGN_LITTLE = 0x4949; // Indicates little endian exif data. -const EXIF_ALIGN_BIG = 0x4d4d; // Indicates big endian exif data. - -const EXIF_TAG_TIFF = 0x002a; // First directory containing TIFF data. -const EXIF_TAG_GPSDATA = 0x8825; // Pointer from TIFF to the GPS directory. -const EXIF_TAG_EXIFDATA = 0x8769; // Pointer from TIFF to the EXIF IFD. -const EXIF_TAG_SUBIFD = 0x014a; // Pointer from TIFF to "Extra" IFDs. - -const EXIF_TAG_JPG_THUMB_OFFSET = 0x0201; // Pointer from TIFF to thumbnail. -const EXIF_TAG_JPG_THUMB_LENGTH = 0x0202; // Length of thumbnail data. - -const EXIF_TAG_ORIENTATION = 0x0112; -const EXIF_TAG_X_DIMENSION = 0xA002; -const EXIF_TAG_Y_DIMENSION = 0xA003; - -function ExifParser(parent) { - ImageParser.call(this, parent, 'jpeg', /\.jpe?g$/i); -} - -ExifParser.prototype = {__proto__: ImageParser.prototype}; - -ExifParser.prototype.parse = function(file, metadata, callback, errorCallback) { - this.requestSlice(file, callback, errorCallback, metadata, 0); -}; - -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.webkitSlice(filePos, filePos + opt_length)); -}; - -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; - function reread(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()); - } -}; - -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); -}; - -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.'); - } -}; - -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; - } -}; - -ExifParser.prototype.readMark = function(br) { - return br.readScalar(2); -}; - -ExifParser.prototype.readMarkLength = function(br) { - // Length includes the 2 bytes used to store the length. - return br.readScalar(2) - 2; -}; - -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); -}; - -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 || '') + ': ' + 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); -}; - -ExifParser.SCALEX = [1, -1, -1, 1, 1, 1, -1, -1]; -ExifParser.SCALEY = [1, 1, -1, -1, -1, 1, 1, -1]; -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) - */ -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/chrome/browser/resources/file_manager/js/file_manager.js b/chrome/browser/resources/file_manager/js/file_manager.js index 37a5ea1..001348f 100644 --- a/chrome/browser/resources/file_manager/js/file_manager.js +++ b/chrome/browser/resources/file_manager/js/file_manager.js @@ -2533,7 +2533,7 @@ FileManager.prototype = { str); }; - galleryFrame.src = 'js/image_editor/gallery.html'; + galleryFrame.src = 'gallery.html'; this.openFilePopup_(galleryFrame); }; diff --git a/chrome/browser/resources/file_manager/js/function_parallel.js b/chrome/browser/resources/file_manager/js/function_parallel.js deleted file mode 100644 index a2925ac..0000000 --- a/chrome/browser/resources/file_manager/js/function_parallel.js +++ /dev/null @@ -1,74 +0,0 @@ -// 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. - -/** - * @constructor - * @class FunctionSequence to invoke steps in sequence - * - * @param steps array of functions to invoke in parallel - * @param callback callback to invoke on success - * @param failureCallback callback to invoke on failure - */ -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 err error message - */ -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. - */ -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 - */ -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/chrome/browser/resources/file_manager/js/function_sequence.js b/chrome/browser/resources/file_manager/js/function_sequence.js deleted file mode 100644 index 1541d63..0000000 --- a/chrome/browser/resources/file_manager/js/function_sequence.js +++ /dev/null @@ -1,124 +0,0 @@ -// 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. - -/** - * @constructor - * @class FunctionSequence to invoke steps in sequence - * - * @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 - */ -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 err error message - */ -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. - */ -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. - */ -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 - */ -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 - */ -FunctionSequence.prototype.apply_ = function(obj, args) { - this.start.apply(this, args); -}; - diff --git a/chrome/browser/resources/file_manager/js/id3_parser.js b/chrome/browser/resources/file_manager/js/id3_parser.js deleted file mode 100644 index 273ec4d..0000000 --- a/chrome/browser/resources/file_manager/js/id3_parser.js +++ /dev/null @@ -1,680 +0,0 @@ -// 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. - -importScripts('function_sequence.js'); -importScripts('function_parallel.js'); -importScripts('util.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 {int} length - bytes to read - * @return {int} - */ -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 {int} - */ -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 {int} encoding string encoding. - * @param {int} size maximum string size. Actual result may be shorter. - * - */ -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 {int} majorVersion major id3 version to use - * @param {Object} frame frame so store data at - * @param {int} end frame end position in reader - */ -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 {int} majorVersion major id3 version to use - * @param {Object} frame frame so store data at - * @param {int} end frame end position in reader - */ -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()); -}; - -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()); - } -}; - -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 - * @return {Object} frame read - */ -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; -}; - -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 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 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 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 being parsed. - * @param {ByteReader} 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 fo 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 {int} - */ - 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 {int} - */ - UTF_16 : 1, - - /** - * UTF-16BE [UTF-16] encoded Unicode [UNICODE] without BOM. - * Terminated with $00 00. - * - * @const - * @type {int} - */ - UTF_16BE : 2, - - /** - * UTF-8 [UTF-8] encoded Unicode [UNICODE]. Terminated with $00. - * - * @const - * @type {int} - */ - 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/chrome/browser/resources/file_manager/js/image_editor/gallery.css b/chrome/browser/resources/file_manager/js/image_editor/gallery.css deleted file mode 100644 index 8fc8d3d..0000000 --- a/chrome/browser/resources/file_manager/js/image_editor/gallery.css +++ /dev/null @@ -1,797 +0,0 @@ -/* - * 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 { - margin: 0; - -webkit-user-select: none; - font-family: Open Sans,Droid Sans Fallback,sans-serif; - font-size: 84%; -} - -.gallery { - background: black; - position: absolute; - left: 0; - right: 0; - top: 0; - bottom: 0; - overflow: hidden; -} - -/* Close button */ -.gallery > .close { - position: absolute; - right: 5px; - top: 5px; - cursor: pointer; - 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 > .close div { - opacity: 0; - width: 20px; - height: 20px; - background-image: url(../../images/gallery/close_x.png); - background-repeat: no-repeat; - background-position: center center; -} - -.gallery[tools] > .close div { - opacity: 0.5; -} - -.gallery[tools] > .close div:hover { - opacity: 0.7; - background-color: rgba(81,81,81,1); -} - -/* Image container and canvas elements */ - -.gallery > .image-container { - position: absolute; - height: 100%; - width: 100%; - background-color: rgba(0,0,0,1); - cursor: none; /* Only visible when the toolbar is active */ -} - -.gallery[tools][locked] *, -.gallery[tools][locked] .image-container[cursor] { - cursor: wait; -} - -.gallery[tools] .image-container[cursor='default'] { - cursor: default; -} - -.gallery[tools] .image-container[cursor='move'] { - cursor: url(../../images/gallery/cursor_move.png) 18 18; -} - -.gallery[tools] .image-container[cursor='crop'] { - cursor: url(../../images/gallery/cursor_crop.png) 18 18; -} - -.gallery[tools] .image-container[cursor='n-resize'], -.gallery[tools] .image-container[cursor='s-resize'] { - cursor: url(../../images/gallery/cursor_updown.png) 18 18; -} - -.gallery[tools] .image-container[cursor='e-resize'], -.gallery[tools] .image-container[cursor='w-resize'] { - cursor: url(../../images/gallery/cursor_leftright.png) 18 18; -} - -.gallery[tools] .image-container[cursor='nw-resize'], -.gallery[tools] .image-container[cursor='se-resize'] { - cursor: url(../../images/gallery/cursor_nwse.png) 18 18; -} - -.gallery[tools] .image-container[cursor='ne-resize'], -.gallery[tools] .image-container[cursor='sw-resize'] { - cursor: url(../../images/gallery/cursor_swne.png) 18 18; -} - -.gallery > .image-container > .image { - position: absolute; - pointer-events: none; - - -webkit-transition-property: -webkit-transform, opacity; - -webkit-transition-timing-function: ease-in-out; - - /* Keep in sync with ImageView.ANIMATION_DURATION in image_view.js*/ - -webkit-transition-duration: 180ms; -} - -.gallery > .image-container > .image[fade] { - opacity: 0; -} - -.gallery > .image-container > .image[fade='left'] { - -webkit-transform: translate(-40px,0); -} - -.gallery > .image-container > .image[fade='right'] { - -webkit-transform: translate(40px,0); -} - -/* Toolbar */ - -.gallery > .toolbar { - position: absolute; - bottom: 0; - width: 100%; - height: 55px; - display: -webkit-box; - -webkit-box-orient: horizontal; - -webkit-box-pack: start; - -webkit-box-align: stretch; - background-color: rgba(18,18,18,0.75); - border-top: 1px solid rgba(31,31,31,0.75); - - pointer-events: none; - opacity: 0; - -webkit-transform: translate(0, 0); - - -webkit-transition-property: webkit-transform, opacity; - -webkit-transition-duration: 300ms; - -webkit-transition-timing-function: ease; -} - -.gallery[tools] > .toolbar { - pointer-events: auto; - opacity: 1; - -webkit-transform: translate(0, 0); -} - -.gallery[tools][locked] > .toolbar { - pointer-events: none; -} - -.gallery .arrow-box { - position: absolute; - z-index: 100; - width: 100%; - height: 100%; - pointer-events: none; - display: -webkit-box; - -webkit-box-orient: horizontal; - -webkit-box-align: center; - -webkit-box-pack: center; -} - -.gallery .arrow-box .arrow { - pointer-events: none; -} - -.gallery .arrow-box .arrow-spacer { - -webkit-box-flex: 1; - pointer-events: none; -} - -.gallery[tools] .arrow[active] { - pointer-events: auto; - cursor: pointer; -} - -/* 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{ - width: 105px; - height: 193px; - background-repeat: no-repeat; - background-position: center center; - opacity: 0; -} - -.gallery[tools] .arrow[active] div{ - opacity: 0.25; -} - -.gallery[tools] .arrow[active] div:hover{ - opacity: 1; -} - -.gallery .arrow.left div{ - background-image: url(../../images/gallery/arrow_left.png); -} - -.gallery .arrow.right div{ - background-image: url(../../images/gallery/arrow_right.png); -} - -/* 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[renaming] .filename-spacer > div, -.gallery .filename-spacer > input { - opacity: 0; - z-index: 0; -} - -.gallery .filename-spacer > div, -.gallery[renaming] .filename-spacer > input { - opacity: 1; - z-index: 1; -} - -.gallery .filename-spacer > input, -.gallery .filename-spacer > div { - font-family: Open Sans,Droid Sans Fallback,sans-serif; - position: absolute; - left: 10px; - top: 15px; - background-color: transparent; - color: white; - width: 260px; - height: 21px; - border: none; - outline: none; - font-size: 120%; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.gallery .filename-spacer > div:hover { - background-color: rgba(48, 48, 48, 1.0); - cursor: pointer; -} - -.gallery .filename-spacer > input:focus, -.gallery .filename-spacer > input:focus:hover { - background-color: white; - color: black; - border-right: 1px solid white; - border-bottom: 1px solid white; -} - -.gallery .button-spacer { - display: -webkit-box; - -webkit-box-flex: 1; -} - -/* Thumbnails */ - -.gallery .ribbon-spacer { - position: absolute; - left: 280px; - right: 280px; - height: 100%; - display: -webkit-box; - -webkit-box-orient: horizontal; - -webkit-box-pack: center; - -webkit-box-align: center; -} - -.gallery .toolbar .ribbon { - overflow: hidden; - height: 100%; - -webkit-box-flex: 0; - display: -webkit-box; - -webkit-box-orient: horizontal; - -webkit-box-pack: start; - - -webkit-transition: opacity 180ms linear; - z-index: 0; -} - -.gallery[editing] .toolbar .ribbon { - opacity: 0; -} - -.gallery .ribbon-image { - display: -webkit-box; - -webkit-box-orient: horizontal; - -webkit-box-pack: center; - -webkit-box-align: center; - overflow: hidden; - cursor: pointer; - width: 47px; - height: 47px; - margin: 2px; - border: 2px solid rgba(255,255,255,0); /* transparent white */ - -webkit-transition: margin-left 180ms linear; -} - -.gallery .ribbon-image[selected] { - border: 2px solid rgba(255,233,168,1); -} - -.gallery .toolbar .ribbon.fade-left { - -webkit-mask-image: -webkit-linear-gradient(left, rgba(0,0,0,0) 0, rgba(0,0,0,1) 40px); -} - -.gallery .toolbar .ribbon.fade-right { - -webkit-mask-image: -webkit-linear-gradient(right, rgba(0,0,0,0) 0, rgba(0,0,0,1) 40px); -} - -.gallery .toolbar .ribbon.fade-left.fade-right { - -webkit-mask-image: -webkit-linear-gradient(left, 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 { - position: relative; - overflow: hidden; - width: 45px; - height: 45px; - border: 1px solid rgba(0,0,0,0); /* transparent black */ -} - -/* Editor buttons */ - -.gallery .toolbar .edit-bar { - position: absolute; - overflow: hidden; - pointer-events: none; - right: 0; - width: 100%; - opacity: 0; - height: 55px; - color: white; - display: -webkit-box; - -webkit-box-orient: horizontal; - -webkit-box-pack: center; - -webkit-transition: opacity 180ms linear; -} - -.gallery[editing] .toolbar .edit-bar { - opacity: 1; -} - -.gallery .edit-main { - display: -webkit-box; - -webkit-box-orient: horizontal; - opacity: 0; - -webkit-transition: opacity 250ms ease-in-out; -} - -.gallery[editing] .edit-main { - pointer-events: auto; - opacity: 1.0; -} - -.gallery > .toolbar .button { - -webkit-box-flex: 0; - padding: 0 10px 0 35px; - cursor: pointer; - margin: 8px 0 7px 3px; - height: 40px; - - display: -webkit-box; - -webkit-box-orient: horizontal; - -webkit-box-align: center; - -webkit-box-pack: end; - - background-repeat: no-repeat; - background-position: 5px center; - - opacity: 0.99; /* Workaround for http://crosbug.com/21065 */ -} - -.gallery > .toolbar .button { - background-color: rgba(0,0,0,0); - color: white; -} - -.gallery > .toolbar .button:hover { - background-color: rgba(31,31,31,1); - color: white; -} - -.gallery > .toolbar .button[pressed] { - background-color: rgba(240,240,240,1); - color: black; -} - -.gallery > .toolbar .button[pressed]:hover { - background-color: rgba(240,240,240,1); - color: black; -} - -.gallery > .toolbar .button.autofix { - background-image: url(../../images/gallery/icon_autofix.png); -} - -.gallery > .toolbar .button.autofix[pressed] { - background-image: url(../../images/gallery/icon_autofix_selected.png); -} - -.gallery > .toolbar .button.crop { - background-image: url(../../images/gallery/icon_crop.png); -} - -.gallery > .toolbar .button.crop[pressed] { - background-image: url(../../images/gallery/icon_crop_selected.png); -} - -.gallery > .toolbar .button.exposure { - background-image: url(../../images/gallery/icon_brightness.png); -} - -.gallery > .toolbar .button.exposure[pressed] { - background-image: url(../../images/gallery/icon_brightness_selected.png); -} - -.gallery > .toolbar .button.rotate_right { - background-image: url(../../images/gallery/icon_rotate.png); -} - -.gallery > .toolbar .button.rotate_right[pressed] { - background-image: url(../../images/gallery/icon_rotate_selected.png); -} - -.gallery > .toolbar .button.rotate_left { - background-image: url(../../images/gallery/icon_rotate_left.png); -} - -.gallery > .toolbar .button.rotate_left[pressed] { - background-image: url(../../images/gallery/icon_rotate_left_selected.png); -} - -.gallery > .toolbar .button.undo { - background-image: url(../../images/gallery/icon_undo.png); -} - -.gallery > .toolbar .button.redo { - position: absolute; /* Exclude from center-packing*/ - background-image: url(../../images/gallery/icon_redo.png); -} - -.gallery > .toolbar .button[disabled] { - pointer-events: none; - opacity: 0.5; -} - -.gallery > .toolbar .button[hidden] { - display: none; -} - -.gallery > .toolbar > .button.edit { - position: relative; - z-index: 10; - background-image: url(../../images/gallery/icon_edit.png); -} - -.gallery > .toolbar > .button.edit[pressed] { - background-image: url(../../images/gallery/icon_edit_selected.png); -} - -.gallery > .toolbar > .button.share { - position: relative; - z-index: 10; - background-image: url(../../images/gallery/icon_share.png); -} - -.gallery > .toolbar > .button.share[pressed] { - background-image: url(../../images/gallery/icon_share_selected.png); -} - -.gallery > .toolbar > .button:last-child { - margin-right: 8px; -} - -/* Secondary toolbar (mode-specific tools) */ - -.gallery .edit-modal { - position: absolute; - width: 100%; - bottom: 80px; - height: 40px; - display: -webkit-box; - -webkit-box-orient: horizontal; - -webkit-box-pack: center; - pointer-events: none; -} - -.gallery .edit-modal-wrapper[hidden] { - display: none; -} - -.gallery .edit-modal-wrapper { - color: white; - padding-right: 5px; - background-color: rgba(0, 0, 0, 0.75); - display: -webkit-box; - -webkit-box-orient: horizontal; - -webkit-box-pack: center; - -webkit-box-align: center; - pointer-events: auto; -} - -.gallery .edit-modal .label { - height: 20px; - padding-left: 50px; - padding-right: 10px; - - display: -webkit-box; - -webkit-box-orient: horizontal; - -webkit-box-align: center; - - background-repeat: no-repeat; - background-position: 20px center; -} - -.gallery .edit-modal .label.brightness { - background-image: url(../../images/gallery/icon_brightness.png); -} - -.gallery .edit-modal .label.contrast { - margin-left: 15px; - background-image: url(../../images/gallery/icon_contrast.png); -} - -.gallery .edit-modal .range { - -webkit-appearance: none !important; - height: 3px; - margin-top: 1px; - margin-right: 10px; -} - -.gallery .edit-modal .range::-webkit-slider-thumb { - -webkit-appearance: none; - width: 16px; - height: 29px; - background-image: url(../../images/gallery/slider_thumb.png); -} - -/* Crop frame */ - -.gallery .crop-overlay { - position: absolute; - pointer-events: none; - display: -webkit-box; - -webkit-box-orient: vertical; -} - -.gallery .crop-overlay .shadow { - background-color: rgba(0,0,0,0.65); -} - -.gallery .crop-overlay .middle-box { - display: -webkit-box; - -webkit-box-orient: horizontal; - -webkit-box-flex: 1; -} - -.gallery .crop-frame { - position: relative; - display: -webkit-box; - -webkit-box-flex: 1; -} - -.gallery .crop-frame div{ - position: absolute; - background-color: rgba(255, 255, 255, 1); - -webkit-box-shadow: 0 0 3px rgba(0, 0, 0, 0.75); -} - -.gallery .crop-frame .horizontal { - left: 7px; - right: 7px; - height: 1px; -} - -.gallery .crop-frame .horizontal.top { - top: 0; -} - -.gallery .crop-frame .horizontal.bottom { - bottom: 0; -} - -.gallery .crop-frame .vertical { - top: 7px; - bottom: 7px; - width: 1px; -} - -.gallery .crop-frame .vertical.left { - left: 0; -} - -.gallery .crop-frame .vertical.right { - right: 0; -} - -.gallery .crop-frame .corner { - border-radius: 6px; - width: 13px; - height: 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 { - position: absolute; - pointer-events: none; - - width: 100%; - height: 100%; - - display: -webkit-box; - -webkit-box-orient: horizontal; - -webkit-box-align: center; - -webkit-box-pack: center; -} - -.gallery .prompt-wrapper > div.dimmable { - opacity: 1; - -webkit-transition: opacity 220ms ease; -} - -.gallery .prompt { - font-size: 120%; - height: 40px; - padding: 0 20px; - color: white; - background-color: rgba(0, 0, 0, 0.8); - - display: -webkit-box; - -webkit-box-orient: horizontal; - -webkit-box-align: center; - - position: relative; - top: 5px; - opacity: 0; - -webkit-transition: all 180ms ease; -} - -.gallery .prompt[state='fadein'] { - top: 0; - opacity: 1; -} - -.gallery .prompt[state='fadeout'] { - top: 0; - opacity: 0; -} - -.gallery .share-menu { - position: absolute; - right: 10px; - bottom: 60px; - background-color: white; - opacity: 1.0; - -webkit-transition: opacity 500ms ease-in-out; - padding: 8px; - display: -webkit-box; - -webkit-box-orient: vertical; - -webkit-box-align: stretch; - -webkit-box-pack: start; - border: 1px solid #7f7f7f; - -webkit-border-radius: 1px; -} - -.gallery .share-menu .bubble-point { - background-image: url(../../images/gallery/bubble_point.png); - background-position: center top; - background-repeat: no-repeat; - position: absolute; - width: 20px; - height: 8px; - bottom: -8px; - right: 20px; - padding: 0; -} - -.gallery .share-menu[hidden] { - opacity: 0.0; - bottom: -100%; /* offscreen so that 'dimmed' attribute does not show it*/ - pointer-events: none; -} - -.gallery .share-menu > div { - cursor: pointer; - background-color: rgba(0,0,0,0); - padding: 5px; - display: -webkit-box; - -webkit-box-align: center; - -webkit-box-pack: start; -} - -.gallery .share-menu > div:hover { - background-color: rgba(240,240,240,1); -} - -.gallery .share-menu > div > img { - margin-right: 5px; - display: block; -} - -/* Video playback support. */ - -.gallery video { - position: absolute; - width: 100%; - height: 100%; -} - -.gallery .video-controls-spacer { - position: absolute; - left: 0; - right: 0; - height: 30px; - bottom: 60px; /* Just above the toolbar */ - display: -webkit-box; - -webkit-box-orient: horizontal; - -webkit-box-align: center; - -webkit-box-pack: center; - pointer-events: none; - opacity: 0; -} - -.gallery[video] .video-controls-spacer { - /* Animate opacity on 'tools' attribute toggle. */ - /* Change opacity immediately on 'video' attribute change. */ - -webkit-transition: opacity 0.28s ease; -} - -.gallery[video][tools] .video-controls-spacer { - opacity: 1; -} - -.gallery .video-controls { - max-width: 800px; - display: none; -} - -.gallery[video] .video-controls { - display: -webkit-box; - -webkit-box-flex: 1; -} - -.gallery[video] > .toolbar .button.edit, -.gallery[video] > .toolbar .edit-bar { - display: none; -} diff --git a/chrome/browser/resources/file_manager/js/image_editor/gallery.html b/chrome/browser/resources/file_manager/js/image_editor/gallery.html deleted file mode 100644 index 892e220..0000000 --- a/chrome/browser/resources/file_manager/js/image_editor/gallery.html +++ /dev/null @@ -1,38 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/chrome/browser/resources/file_manager/js/image_editor/gallery.js b/chrome/browser/resources/file_manager/js/image_editor/gallery.js index 5d422c3..4ee6d0a 100644 --- a/chrome/browser/resources/file_manager/js/image_editor/gallery.js +++ b/chrome/browser/resources/file_manager/js/image_editor/gallery.js @@ -1184,7 +1184,7 @@ Ribbon.Item.prototype.setMetadata = function(metadata) { url = this.url_; transform = metadata.imageTransform; } else { - url = '../../' + FileType.getPreviewArt(mediaType); + url = FileType.getPreviewArt(mediaType); } function percent(ratio) { return Math.round(ratio * 100) + '%' } diff --git a/chrome/browser/resources/file_manager/js/image_editor/gallery_demo.html b/chrome/browser/resources/file_manager/js/image_editor/gallery_demo.html deleted file mode 100644 index a71818b..0000000 --- a/chrome/browser/resources/file_manager/js/image_editor/gallery_demo.html +++ /dev/null @@ -1,70 +0,0 @@ - - - - - - - - - - - - - - - -
- - Chromebook size - - -
- -
- - \ No newline at end of file diff --git a/chrome/browser/resources/file_manager/js/image_editor/image_editor.css b/chrome/browser/resources/file_manager/js/image_editor/image_editor.css deleted file mode 100644 index 31c6847..0000000 --- a/chrome/browser/resources/file_manager/js/image_editor/image_editor.css +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Copyright (c) 2011 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. - */ - -/* Main container div for the editor */ -.image-editor { - background: black; - width: 100%; - height: 100%; -} - -/* Editor toolbar */ -.toolbar { - width: 100%; - height: 100%; - display: -webkit-box; - -webkit-box-orient: horizontal; - -webkit-box-align: center; - -webkit-box-pack: center; -} - -.toolbar button { - margin: 3px; - height: 26px; -} - -.toolbar input { - width: 200px; -} - -.toolbar span { - margin-left: 10px; - margin-right: 10px; -} - -/* The container div for the main canvas */ -.canvas-wrapper { - width: 100%; - height: 100%; - background: black; -} - -/* Scaling controls */ -.scale-tool { - position: absolute; - width: 305px; - height: 43px; - right: 0; - bottom: 0; - border: 1px solid black; - border-right-width: 0; - border-bottom-width: 0; - background: white; -} - -.size-div { - position: absolute; - right: 2px; -} - -.scale-div { - position: absolute; - width: 100%; - top: 18px; -} - -.scale-div button{ - position: absolute; - width: 23px; - height: 23px; - top: 1px; -} - -.scale-div .scale-down{ - left: 1px; -} - -.scale-div input{ - position: absolute; - left: 24px; - top: 0; - width: 150px; -} - -.scale-div span{ - position: absolute; - left: 180px; - top: 2px; -} - -.scale-div .scale-up{ - right: 63px; -} - -.scale-div .scale-1to1{ - width: 32px; - right: 29px; -} - -.scale-div .scale-fit{ - width: 26px; - right: 1px; -} - diff --git a/chrome/browser/resources/file_manager/js/image_editor/media_controls.css b/chrome/browser/resources/file_manager/js/image_editor/media_controls.css deleted file mode 100644 index e1a281a..0000000 --- a/chrome/browser/resources/file_manager/js/image_editor/media_controls.css +++ /dev/null @@ -1,491 +0,0 @@ -/* - * 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 { - position: relative; - width: 28px; - height: 28px; - background-repeat: no-repeat; - background-position: center center; -} - -.media-button > div { - position: absolute; - pointer-events: none; - width: 100%; - height: 100%; - -webkit-transition: opacity 100ms linear; - opacity: 0; -} - -.media-button[state='0']:not(.disabled):not(:hover):not(:active) > .state0.normal, -.media-button[state='0']:not(.disabled):hover > .state0.hover, -.media-button[state='0']:not(.disabled):active > .state0.active, -.media-button[state='1']:not(.disabled):not(:hover):not(:active) > .state1.normal, -.media-button[state='1']:not(.disabled):hover > .state1.hover, -.media-button[state='1']:not(.disabled):active > .state1.active, -.media-button.disabled > .disabled { - opacity: 1; -} - -/* Custom sliders for progress and volume. */ - -/* Customize the standard input[type='range']. */ -.custom-slider > input[type='range'] { - position: absolute; - -webkit-appearance: none !important; /* Hide the default thumb icon. */ - left: -2px; /* Required to align the input element with the parent. */ - top: -2px; - width: 100%; - height: 100%; - background: transparent; /* Hide the standard slider bar */ -} - -/* Custom thumb icon. */ -.custom-slider > input[type='range']::-webkit-slider-thumb { - position: relative; - -webkit-appearance: none; - z-index: 2; - height: 24px; - background-position: center center; - background-repeat: no-repeat; -} - -/* Custom slider bar (we hide the standard one). */ -.custom-slider > .bar { - position: absolute; - /* 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. */ - top: 11px; - bottom: 11px; - border-width: 1px; - border-top-style: solid; - border-bottom-style: solid; - pointer-events: none; /* Mouse events pass through to the standard input. */ -} - -.custom-slider > .bar > .filled, -.custom-slider > .bar > .cap { - position: absolute; - top: -1px; - bottom: -1px; - border-width: 1px; - border-style: solid; -} - -/* The filled portion of the slider bar to the left of the thumb. */ -.custom-slider > .bar > .filled { - width: 0; /* The element style.width is manipulated from the code. */ - left: 0; - border-left-style: none; - border-right-style: none; -} - -/* 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 { - right: 100%; - border-right-style: none; - border-top-left-radius: 4px; - border-bottom-left-radius: 4px; -} - -/* Right cap is always not filled. */ -.custom-slider > .bar > .cap.right { - left: 100%; - border-left-style: none; - border-top-right-radius: 4px; - border-bottom-right-radius: 4px; -} - -.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: -webkit-linear-gradient(#c3c3c3, #d9d9d9); - border-color: #d9d9d9; -} - -.custom-slider.disabled > .bar > .filled, -.custom-slider.disabled > .bar > .cap.left { - background-image: none; - background-color: rgba(0, 0, 0, 0.5); -} - -.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 { - position: absolute; - left: 0; - width: 0; - top: -1px; - bottom: -1px; - background-color: #202020; -} - -.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 { - position: absolute; - left: 0; - bottom: 20px; - - font-size: 13px; - - height: 0; - -webkit-transition: height 0.15s ease; - - color: white; - background: #202020; - border-top-left-radius: 2px; - border-top-right-radius: 2px; - display: -webkit-box; - -webkit-box-orient: horizontal; - -webkit-box-align: center; - -webkit-box-pack: center; - - overflow: hidden; -} - -.custom-slider > .bar > .seek-mark.visible > .seek-label { - height: 15px; -} - -/* Media controls in order of appearance. */ - -/* Play/pause button. */ - -.media-button.play { - margin-left: -7px; - margin-right: -7px; -} - -.media-button.play > .state0.normal { - background-image: url(../../images/media/media_play.png); -} - -.media-button.play > .state0.hover { - background-image: url(../../images/media/media_play_hover.png); -} - -.media-button.play > .state0.active { - background-image: url(../../images/media/media_play_down.png); -} - -.media-button.play > .state1.normal{ - background-image: url(../../images/media/media_pause.png); -} - -.media-button.play > .state1.hover { - background-image: url(../../images/media/media_pause_hover.png); -} - -.media-button.play > .state1.active { - background-image: url(../../images/media/media_pause_down.png); -} - -.media-button.play > .disabled { - background-image: url(../../images/media/media_play_disabled.png); -} - -/* Time controls: a slider and a text time display. */ - -.time-controls { - height: 100%; - display: -webkit-box; - -webkit-box-orient: horizontal; - -webkit-box-align: center; - -webkit-box-pack: center; - -webkit-box-flex: 1; -} - -.custom-slider.progress { - position: relative; - height: 100%; - display: -webkit-box; - -webkit-box-flex: 1; - margin-left: -9px; /* Set the margins at the edges of the slider bar. */ - margin-right: -9px; -} - -.custom-slider.progress > input[type='range']::-webkit-slider-thumb { - width: 28px; - background-image: url(../../images/media/media_slider_thumb.png); -} - -.custom-slider.progress > input[type='range']::-webkit-slider-thumb:hover { - background-image: url(../../images/media/media_slider_thumb_hover.png); -} - -.custom-slider.progress > input[type='range']::-webkit-slider-thumb:active { - background-image: url(../../images/media/media_slider_thumb_down.png); -} - -.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 { - position: relative; - height: 100%; - margin-left: 15px; -} - -.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 { - position: absolute; - right: 0; - top: 0; - bottom: 0; - display: -webkit-box; - -webkit-box-orient: horizontal; - -webkit-box-align: center; - -webkit-box-pack: end; - color: white; -} - -/* Volume controls: sound button and volume slider */ - -.volume-controls { - height: 100%; - display: -webkit-box; - -webkit-box-orient: horizontal; - -webkit-box-align: center; - -webkit-box-pack: center; -} - -/* Sound button */ - -.media-button.sound { - margin-left: -4px; - margin-right: -2px; - background-position: -2px center; -} - -.media-button.sound[level='0'] > .normal { - background-image: url(../../images/media/media_sound_disabled.png); -} - -.media-button.sound[level='0'] > .hover { - background-image: url(../../images/media/media_sound_disabled_hover.png); -} - -.media-button.sound[level='0'] > .active { - background-image: url(../../images/media/media_sound_disabled_down.png); -} - - -.media-button.sound[level='1'] > .normal { - background-image: url(../../images/media/media_sound_level1.png); -} - -.media-button.sound[level='1'] > .hover { - background-image: url(../../images/media/media_sound_level1_hover.png); -} - -.media-button.sound[level='1'] > .active { - background-image: url(../../images/media/media_sound_level1_down.png); -} - - -.media-button.sound[level='2'] > .normal { - background-image: url(../../images/media/media_sound_level2.png); -} - -.media-button.sound[level='2'] > .hover { - background-image: url(../../images/media/media_sound_level2_hover.png); -} - -.media-button.sound[level='2'] > .active { - background-image: url(../../images/media/media_sound_level2_down.png); -} - - -.media-button.sound[level='3'] > .normal { - background-image: url(../../images/media/media_sound_full.png); -} - -.media-button.sound[level='3'] > .hover { - background-image: url(../../images/media/media_sound_full_hover.png); -} - -.media-button.sound[level='3'] > .active { - background-image: url(../../images/media/media_sound_full_down.png); -} - - -.media-button.sound > .disabled { - background-image: url(../../images/media/media_sound_full_disabled.png); -} - -/* Volume slider. */ - -.custom-slider.volume { - position: relative; - height: 100%; - width: 60px; - margin-left: -4px; - margin-right: -4px; -} - -.custom-slider.volume > input[type='range']::-webkit-slider-thumb { - width: 20px; - background-image: url(../../images/media/media_volume_slider_thumb.png); -} - -.custom-slider.volume > input[type='range']::-webkit-slider-thumb:hover { - background-image: url(../../images/media/media_volume_slider_thumb_hover.png); -} - -.custom-slider.volume > input[type='range']::-webkit-slider-thumb:active { - background-image: url(../../images/media/media_volume_slider_thumb_down.png); -} - -.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 { - pointer-events: auto; - - font-size: 15px; - height: 30px; - border-radius: 5px; - - padding-left: 15px; - padding-right: 15px; - - opacity: 0.8; - - background: #202020; - - display: -webkit-box; - -webkit-box-orient: horizontal; - -webkit-box-align: center; - -webkit-box-pack: center; -} - -.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: url(../../images/media/media_fullscreen.png); -} - -.media-button.fullscreen > .hover { - background-image: url(../../images/media/media_fullscreen_hover.png); -} - -.media-button.fullscreen > .active { - background-image: url(../../images/media/media_fullscreen_down.png); -} - -.media-button.fullscreen > .disabled { - background-image: url(../../images/media/media_fullscreen_disabled.png); -} - -.playback-state-icon { - position: absolute; - left: 50%; - top: 50%; - width: 32px; - height: 32px; - margin-left: -16px; - margin-top: -16px; - border-radius: 2.5px; - pointer-events: none; - display: none; - - background-color: #202020; - background-repeat: no-repeat; - background-position: center center; - - opacity: 1; - -webkit-transform: scaleX(1) scaleY(1); - - -webkit-transition-property: webkit-transform, opacity; - -webkit-transition-duration: 500ms; - -webkit-transition-timing-function: linear; - - z-index: 2; -} - -.playback-state-icon[visible] { - display: block; -} - -.playback-state-icon[state] { - opacity: 0; - -webkit-transform: scaleX(3) scaleY(3); -} - -.playback-state-icon[state='play'] { - background-image: url(../../images/media/media_play.png); -} - -.playback-state-icon[state='pause'] { - background-image: url(../../images/media/media_pause.png); -} diff --git a/chrome/browser/resources/file_manager/js/image_editor/media_controls.js b/chrome/browser/resources/file_manager/js/image_editor/media_controls.js deleted file mode 100644 index 3994bbd..0000000 --- a/chrome/browser/resources/file_manager/js/image_editor/media_controls.js +++ /dev/null @@ -1,932 +0,0 @@ -// 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 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(){}; -} - -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. - */ -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 - * @param {HTMLElement=} opt_parent Parent element or container if undefined. - * @return {HTMLElement} - */ -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 - * @param {function(Event)} handler - * @param {HTMLElement=} opt_parent Parent element or container if undefined. - * @param {Boolean} opt_toggle True if the button has toggle state. - * @return {HTMLElement} - */ -MediaControls.prototype.createButton = function( - className, handler, opt_parent, opt_toggle) { - var button = this.createControl(className, opt_parent); - button.classList.add('media-button'); - button.addEventListener('click', handler); - - var numStates = opt_toggle ? 2 : 1; - for (var state = 0; state != numStates; state++) { - var stateClass = 'state' + state; - this.createControl('normal ' + stateClass, button); - this.createControl('hover ' + stateClass, button); - this.createControl('active ' + stateClass, button); - } - this.createControl('disabled', button); - - button.setAttribute('state', 0); - button.addEventListener('click', handler); - return button; -}; - -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. - */ - -MediaControls.prototype.play = function() { - this.media_.play(); -}; - -MediaControls.prototype.pause = function() { - this.media_.pause(); -}; - -MediaControls.prototype.isPlaying = function() { - return !this.media_.paused && !this.media_.ended; -}; - -MediaControls.prototype.togglePlayState = function() { - if (this.isPlaying()) - this.pause(); - else - this.play(); -}; - -MediaControls.prototype.initPlayButton = function(opt_parent) { - this.playButton_ = this.createButton('play media-control', - this.togglePlayState.bind(this), opt_parent, true /* toggle */); -}; - -/* - * Time controls - */ - -// The default range of 100 is too coarse for the media progress slider. -// 1000 should be enough as the entire media controls area is never longer -// than 800px. -MediaControls.PROGRESS_RANGE = 1000; - -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); -}; - -MediaControls.prototype.displayProgress_ = function(current, duration) { - var ratio = current / duration; - this.progressSlider_.setValue(ratio); - this.currentTime_.textContent = MediaControls.formatTime_(current); -}; - -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); -}; - -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(); - } - } -}; - -/* - * Volume 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)); -}; - -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()); -}; - -MediaControls.getVolumeLevel_ = function(value) { - if (value == 0) return 0; - if (value <= 1/3) return 1; - if (value <= 2/3) return 2; - return 3; -}; - -MediaControls.prototype.onVolumeChange_ = function(value) { - this.media_.volume = value; - this.soundButton_.setAttribute('level', MediaControls.getVolumeLevel_(value)); -}; - -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_); - - // Reset the UI. - this.enableControls_('.media-control', false); - this.playButton_.setAttribute('state', 0); - this.displayProgress_(0, 1); - 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; -}; - -MediaControls.prototype.onMediaPlay_ = function(playing) { - if (this.progressSlider_.isDragging()) - return; - - this.playButton_.setAttribute('state', playing ? '1' : '0'); -}; - -MediaControls.prototype.onMediaDuration_ = function() { - if (!this.media_.duration) - 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); -}; - -MediaControls.prototype.onMediaProgress_ = function(e) { - if (!this.media_.duration) - 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(); - } -}; - -MediaControls.prototype.onMediaComplete = function() {}; - -/** - * 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 - * @param {function(boolean)} onDrag - * @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. - */ -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] - */ -MediaControls.Slider.prototype.setFilled_ = function(proportion) { - this.filled_.style.width = proportion * 100 + '%'; -}; - -/** - * Get the value from the input element. - * - * @param {number} proportion [0..1] - */ -MediaControls.Slider.prototype.getValueFromUI_ = function() { - return this.input_.value / this.input_.max; -}; - -/** - * Update the UI with the current value. - * - * @param {number} value [0..1] - */ -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)); -}; - -MediaControls.Slider.prototype.onInputChange_ = function() { - this.value_ = this.getValueFromUI_(); - this.setFilled_(this.value_); - this.onChange_(this.value_); -}; - -MediaControls.Slider.prototype.isDragging = function() { - return this.isDragging_; -}; - -MediaControls.Slider.prototype.onInputDrag_ = function(on, event) { - 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 - * @param {function(boolean)} onDrag - */ -MediaControls.AnimatedSlider = function( - container, value, range, onChange, onDrag, formatFunction) { - MediaControls.Slider.apply(this, arguments); -}; - -MediaControls.AnimatedSlider.prototype = { - __proto__: MediaControls.Slider.prototype -}; - -MediaControls.AnimatedSlider.STEPS = 10; -MediaControls.AnimatedSlider.DURATION = 100; - -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 - * @param {function(boolean)} onDrag - */ -MediaControls.PreciseSlider = function( - container, value, range, onChange, onDrag, formatFunction) { - MediaControls.Slider.apply(this, arguments); - - var doc = this.container_.ownerDocument; - - /** - * @type {function(number):string} - */ - 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 -}; - -MediaControls.PreciseSlider.SHOW_DELAY = 200; -MediaControls.PreciseSlider.HIDE_AFTER_MOVE_DELAY = 2500; -MediaControls.PreciseSlider.HIDE_AFTER_DRAG_DELAY = 750; -MediaControls.PreciseSlider.NO_AUTO_HIDE = 0; - -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. - */ -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); - } -}; - -MediaControls.PreciseSlider.prototype.hideSeekMark_ = function() { - this.seekMarkTimer_ = null; - this.seekMark_.classList.remove('visible'); -}; - -MediaControls.PreciseSlider.prototype.onMouseMove_ = function(event) { - this.latestSeekRatio_ = this.getProportion(event.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); - } -}; - -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_(); -}; - -MediaControls.PreciseSlider.prototype.onInputChange_ = function() { - MediaControls.Slider.prototype.onInputChange_.apply(this, arguments); - if (this.isDragging()) { - this.showSeekMark_( - this.getValue(), MediaControls.PreciseSlider.NO_AUTO_HIDE); - } -}; - -MediaControls.PreciseSlider.prototype.onInputDrag_ = function(on, event) { - 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} 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, - opt_fullScreenToggle, opt_stateIconParent) { - MediaControls.call(this, containerElement, onMediaError); - - 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.resumePositions_ = new TimeLimitedMap( - 'VideoResumePosition', - VideoControls.RESUME_POSITIONS_CAPACITY, - VideoControls.RESUME_POSITION_LIFETIME); -} - -VideoControls.RESUME_POSITIONS_CAPACITY = 100; -VideoControls.RESUME_POSITION_LIFETIME = 30 * 24 * 60 * 60 * 1000; // 30 days. -VideoControls.RESUME_MARGIN = 0.03; -VideoControls.RESUME_THRESHOLD = 5 * 60; // No resume for videos < 5 min. -VideoControls.RESUME_REWIND = 5; // Rewind 5 seconds back when resuming. - -VideoControls.prototype = { __proto__: MediaControls.prototype }; - -VideoControls.prototype.onMediaComplete = function() { - this.onMediaPlay_(false); // Just update the UI. - this.savePosition(); // This will effectively forget the position. -}; - -VideoControls.prototype.togglePlayStateWithFeedback = function(e) { - if (!this.getMedia().duration) - return; - - this.togglePlayState(); - - var self = this; - - var delay = function(action, opt_timeout) { - if (self.statusIconTimer_) { - clearTimeout(self.statusIconTimer_); - } - self.statusIconTimer_ = setTimeout(function() { - self.statusIconTimer_ = null; - action(); - }, opt_timeout || 0); - }; - - function hideStatusIcon() { - self.stateIcon_.removeAttribute('visible'); - self.stateIcon_.removeAttribute('state'); - } - - hideStatusIcon(); - - // The delays are required to trigger the layout between attribute changes. - // Otherwise everything just goes to the final state without the animation. - delay(function() { - self.stateIcon_.setAttribute('visible', true); - delay(function(){ - self.stateIcon_.setAttribute( - 'state', self.isPlaying() ? 'play' : 'pause'); - delay(hideStatusIcon, 1000); /* Twice the animation duration. */ - }); - }); -}; - -VideoControls.prototype.onMediaDuration_ = function() { - MediaControls.prototype.onMediaDuration_.apply(this, arguments); - if (this.media_.duration && - this.media_.duration >= VideoControls.RESUME_THRESHOLD && - this.media_.seekable) { - var position = this.resumePositions_.getValue(this.media_.src); - if (position) { - this.media_.currentTime = position; - } - } -}; - -VideoControls.prototype.togglePlayState = function(e) { - if (this.isPlaying()) { - // User gave the Pause command. - this.savePosition(); - } - MediaControls.prototype.togglePlayState.apply(this, arguments); -}; - -VideoControls.prototype.savePosition = function() { - if (!this.media_.duration || - this.media_.duration_ < VideoControls.RESUME_THRESHOLD) - return; - - var ratio = this.media_.currentTime / this.media_.duration; - 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. - this.resumePositions_.removeValue(this.media_.src); - } else { - this.resumePositions_.setValue(this.media_.src, Math.floor(Math.max(0, - this.media_.currentTime - VideoControls.RESUME_REWIND))); - } -}; - -/** - * TimeLimitedMap is 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 OK. - * - * @param {string} localStorageKey A key in the local storage. - * @param {number} capacity Maximim number of items. If exceeded, oldest items - * are removed. - * @param {number} lifetime Maximim time to keep an item (in milliseconds). - */ -function TimeLimitedMap(localStorageKey, capacity, lifetime) { - this.localStorageKey_ = localStorageKey; - this.capacity_ = capacity; - this.lifetime_ = lifetime; -} - -/** - * @param {string} key - * @return {string} value - */ -TimeLimitedMap.prototype.getValue = function(key) { - var map = this.read_(); - var entry = map[key]; - return entry && entry.value; -}; - -/** - * @param {string} key - * @param {string} value - */ -TimeLimitedMap.prototype.setValue = function(key, value) { - var map = this.read_(); - map[key] = { value: value, timestamp: Date.now() }; - this.cleanup_(map); - this.write_(map); -}; - -/** - * @param {string} key - */ -TimeLimitedMap.prototype.removeValue = function(key) { - var map = this.read_(); - if (!(key in map)) - return; // Nothing to do. - - delete map[key]; - this.cleanup_(map); - this.write_(map); -}; - -/** - * @return {Object} A map of timestamped key-value pairs. - */ -TimeLimitedMap.prototype.read_ = function() { - var json = localStorage[this.localStorageKey_]; - if (json) { - try { - return JSON.parse(json); - } catch(e) { - // The localStorage item somehow got messed up, start fresh. - } - } - return {}; -}; - -/** - * @param {Object} map A map of timestamped key-value pairs. - */ -TimeLimitedMap.prototype.write_ = function(map) { - localStorage[this.localStorageKey_] = JSON.stringify(map); -}; - -/** - * Remove over-capacity and obsolete items. - * - * @param {Object} map A map of timestamped key-value pairs. - */ -TimeLimitedMap.prototype.cleanup_ = function(map) { - // Sort keys by ascending timestamps. - var keys = []; - for (var key in map) { - keys.push(key); - } - keys.sort(function(a, b) { return map[a].timestamp > map[b].timestamp }); - - var cutoff = Date.now() - this.lifetime_; - - var obsolete = 0; - while (obsolete < keys.length && - map[keys[obsolete]].timestamp < cutoff) { - obsolete++; - } - - var overCapacity = Math.max(0, keys.length - this.capacity_); - - var itemsToDelete = Math.max(obsolete, overCapacity); - for (var i = 0; i != itemsToDelete; i++) { - delete map[keys[i]]; - } -}; - - -/** - * Create audio controls. - * - * @param {HTMLElement} container - * @param {function(boolean)} advanceTrack Parameter: true=forward. - * @constructor - */ -function AudioControls(container, advanceTrack) { - MediaControls.call(this, container, null /* 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)); -} - -AudioControls.prototype = { __proto__: MediaControls.prototype }; - -AudioControls.prototype.onMediaComplete = function() { - this.advanceTrack_(true); -}; - -AudioControls.TRACK_RESTART_THRESHOLD = 5; // seconds. - -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/chrome/browser/resources/file_manager/js/image_parsers.js b/chrome/browser/resources/file_manager/js/image_parsers.js deleted file mode 100644 index e467a3e..0000000 --- a/chrome/browser/resources/file_manager/js/image_parsers.js +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright (c) 2011 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. - -/* 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}; - -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}; - -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}; - -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}; - -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); \ No newline at end of file diff --git a/chrome/browser/resources/file_manager/js/media/audio_player.js b/chrome/browser/resources/file_manager/js/media/audio_player.js new file mode 100644 index 0000000..564055b --- /dev/null +++ b/chrome/browser/resources/file_manager/js/media/audio_player.js @@ -0,0 +1,273 @@ +// 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. + +document.addEventListener('DOMContentLoaded', function() { + // Test harness sets the search string to prevent the automatic load. + // It calls AudioPlayer.load() explicitly after initializing + // the |chrome| variable with an appropriate mock object. + if (!document.location.search) { + AudioPlayer.load(); + } +}); + +/** + * @param {HTMLElement} container + * @constructor + */ +function AudioPlayer(container) { + this.container_ = container; + this.metadataProvider_ = new MetadataProvider(); + this.currentTrack_ = -1; + this.playlistGeneration_ = 0; + + 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 close').addEventListener( + 'click', function() { chrome.mediaPlayerPrivate.closeWindow() }); + + createChild('title-button collapse').addEventListener( + 'click', this.onExpandCollapse_.bind(this)); + + this.audioControls_ = new AudioControls( + createChild(), this.advance_.bind(this)); + + this.audioControls_.attachMedia(createChild('', 'audio')); +} + +AudioPlayer.load = function() { + document.ondragstart = function(e) { e.preventDefault() }; + document.oncontextmenu = function(e) { e.preventDefault(); }; + + var player = new AudioPlayer(document.querySelector('.audio-player')); + function getPlaylist() { + chrome.mediaPlayerPrivate.getPlaylist(player.load.bind(player)); + } + getPlaylist(); + chrome.mediaPlayerPrivate.onPlaylistChanged.addListener(getPlaylist); +}; + +AudioPlayer.prototype.load = function(playlist) { + this.playlistGeneration_++; + + this.audioControls_.pause(); + + this.currentTrack_ = -1; + + this.urls_ = playlist.items; + + if (this.urls_.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_ = []; + + for (var i = 0; i != this.urls_.length; i++) { + var url = this.urls_[i]; + var onClick = this.select_.bind(this, i); + this.trackListItems_.push( + new AudioPlayer.TrackInfo(this.trackList_, url, onClick)); + this.trackStackItems_.push( + new AudioPlayer.TrackInfo(this.trackStack_, url, onClick)); + } + + this.select_(playlist.position); + + // 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.urls_.length; i++) { + if (i != playlist.position) + this.loadMetadata_(i); + } +}; + +AudioPlayer.prototype.loadMetadata_ = function(track) { + this.metadataProvider_.fetch( + this.urls_[track], + function(generation, metadata) { + // Do nothing if another load happened since the metadata request. + if (this.playlistGeneration_ != generation) + return; + + if (metadata.thumbnailURL) { + this.container_.classList.remove('noart'); + } + this.trackListItems_[track].setMetadata(metadata); + this.trackStackItems_[track].setMetadata(metadata); + }.bind(this, this.playlistGeneration_)); +}; + +AudioPlayer.prototype.select_ = function(newTrack) { + if (this.currentTrack_ == newTrack) return; + + this.changeSelectionInList_(this.currentTrack_, newTrack); + this.changeSelectionInStack_(this.currentTrack_, newTrack); + + this.currentTrack_ = newTrack; + this.scrollToCurrent_(false); + + var media = this.audioControls_.getMedia(); + media.src = this.urls_[this.currentTrack_]; + media.load(); + this.audioControls_.play(); +}; + +AudioPlayer.prototype.changeSelectionInList_ = function(oldTrack, newTrack) { + this.trackListItems_[newTrack].getBox().classList.add('selected'); + + if (oldTrack >= 0) { + this.trackListItems_[oldTrack].getBox().classList.remove('selected'); + } +}; + +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. + */ +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); +}; + +AudioPlayer.prototype.isCompact_ = function() { + return this.container_.classList.contains('collapsed') || + this.container_.classList.contains('single-track'); +}; + +AudioPlayer.prototype.advance_ = function(forward) { + var newTrack = this.currentTrack_ + (forward ? 1 : -1); + if (newTrack < 0) newTrack = this.urls_.length - 1; + if (newTrack == this.urls_.length) newTrack = 0; + this.select_(newTrack); +}; + +AudioPlayer.prototype.onExpandCollapse_ = function() { + this.container_.classList.toggle('collapsed'); + this.syncHeight_(); + if (!this.isCompact_()) + this.scrollToCurrent_(true); +}; + +/* Keep the below constants in sync with the CSS. */ +AudioPlayer.HEADER_HEIGHT = 30; +AudioPlayer.TRACK_HEIGHT = 58; +AudioPlayer.CONTROLS_HEIGHT = 35; + +AudioPlayer.prototype.syncHeight_ = function() { + var expandedListHeight = + Math.min(this.urls_.length, 3) * AudioPlayer.TRACK_HEIGHT; + this.trackList_.style.height = expandedListHeight + 'px'; + + chrome.mediaPlayerPrivate.setWindowHeight( + (this.isCompact_() ? + AudioPlayer.TRACK_HEIGHT : + AudioPlayer.HEADER_HEIGHT + expandedListHeight) + + AudioPlayer.CONTROLS_HEIGHT); +}; + + +/** + * Create a TrackInfo object encapsulating the information about one track. + * + * @param {HTMLElement} container + * @param {string} url + * @param {function} onClick + * @constructor + */ +AudioPlayer.TrackInfo = function(container, url, onClick) { + this.url_ = url; + + 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_); +}; + +AudioPlayer.TrackInfo.prototype.getBox = function() { return this.box_ }; + +AudioPlayer.TrackInfo.prototype.getDefaultTitle = function() { + var title = this.url_.split('/').pop(); + var dotIndex = title.lastIndexOf('.'); + if (dotIndex >= 0) title = title.substr(0, dotIndex); + return title; +}; + +AudioPlayer.TrackInfo.prototype.getDefaultArtist = function() { + return 'Unknown Artist'; // TODO(kaznacheev): i18n +}; + +AudioPlayer.TrackInfo.prototype.setMetadata = function(metadata) { + if (metadata.thumbnailURL) { + this.art_.classList.remove('blank'); + this.img_.src = metadata.thumbnailURL; + } + this.title_.textContent = metadata.title || this.getDefaultTitle(); + this.artist_.textContent = metadata.artist || this.getDefaultArtist(); +}; \ No newline at end of file diff --git a/chrome/browser/resources/file_manager/js/media/media_controls.js b/chrome/browser/resources/file_manager/js/media/media_controls.js new file mode 100644 index 0000000..3994bbd --- /dev/null +++ b/chrome/browser/resources/file_manager/js/media/media_controls.js @@ -0,0 +1,932 @@ +// 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 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(){}; +} + +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. + */ +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 + * @param {HTMLElement=} opt_parent Parent element or container if undefined. + * @return {HTMLElement} + */ +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 + * @param {function(Event)} handler + * @param {HTMLElement=} opt_parent Parent element or container if undefined. + * @param {Boolean} opt_toggle True if the button has toggle state. + * @return {HTMLElement} + */ +MediaControls.prototype.createButton = function( + className, handler, opt_parent, opt_toggle) { + var button = this.createControl(className, opt_parent); + button.classList.add('media-button'); + button.addEventListener('click', handler); + + var numStates = opt_toggle ? 2 : 1; + for (var state = 0; state != numStates; state++) { + var stateClass = 'state' + state; + this.createControl('normal ' + stateClass, button); + this.createControl('hover ' + stateClass, button); + this.createControl('active ' + stateClass, button); + } + this.createControl('disabled', button); + + button.setAttribute('state', 0); + button.addEventListener('click', handler); + return button; +}; + +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. + */ + +MediaControls.prototype.play = function() { + this.media_.play(); +}; + +MediaControls.prototype.pause = function() { + this.media_.pause(); +}; + +MediaControls.prototype.isPlaying = function() { + return !this.media_.paused && !this.media_.ended; +}; + +MediaControls.prototype.togglePlayState = function() { + if (this.isPlaying()) + this.pause(); + else + this.play(); +}; + +MediaControls.prototype.initPlayButton = function(opt_parent) { + this.playButton_ = this.createButton('play media-control', + this.togglePlayState.bind(this), opt_parent, true /* toggle */); +}; + +/* + * Time controls + */ + +// The default range of 100 is too coarse for the media progress slider. +// 1000 should be enough as the entire media controls area is never longer +// than 800px. +MediaControls.PROGRESS_RANGE = 1000; + +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); +}; + +MediaControls.prototype.displayProgress_ = function(current, duration) { + var ratio = current / duration; + this.progressSlider_.setValue(ratio); + this.currentTime_.textContent = MediaControls.formatTime_(current); +}; + +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); +}; + +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(); + } + } +}; + +/* + * Volume 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)); +}; + +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()); +}; + +MediaControls.getVolumeLevel_ = function(value) { + if (value == 0) return 0; + if (value <= 1/3) return 1; + if (value <= 2/3) return 2; + return 3; +}; + +MediaControls.prototype.onVolumeChange_ = function(value) { + this.media_.volume = value; + this.soundButton_.setAttribute('level', MediaControls.getVolumeLevel_(value)); +}; + +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_); + + // Reset the UI. + this.enableControls_('.media-control', false); + this.playButton_.setAttribute('state', 0); + this.displayProgress_(0, 1); + 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; +}; + +MediaControls.prototype.onMediaPlay_ = function(playing) { + if (this.progressSlider_.isDragging()) + return; + + this.playButton_.setAttribute('state', playing ? '1' : '0'); +}; + +MediaControls.prototype.onMediaDuration_ = function() { + if (!this.media_.duration) + 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); +}; + +MediaControls.prototype.onMediaProgress_ = function(e) { + if (!this.media_.duration) + 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(); + } +}; + +MediaControls.prototype.onMediaComplete = function() {}; + +/** + * 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 + * @param {function(boolean)} onDrag + * @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. + */ +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] + */ +MediaControls.Slider.prototype.setFilled_ = function(proportion) { + this.filled_.style.width = proportion * 100 + '%'; +}; + +/** + * Get the value from the input element. + * + * @param {number} proportion [0..1] + */ +MediaControls.Slider.prototype.getValueFromUI_ = function() { + return this.input_.value / this.input_.max; +}; + +/** + * Update the UI with the current value. + * + * @param {number} value [0..1] + */ +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)); +}; + +MediaControls.Slider.prototype.onInputChange_ = function() { + this.value_ = this.getValueFromUI_(); + this.setFilled_(this.value_); + this.onChange_(this.value_); +}; + +MediaControls.Slider.prototype.isDragging = function() { + return this.isDragging_; +}; + +MediaControls.Slider.prototype.onInputDrag_ = function(on, event) { + 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 + * @param {function(boolean)} onDrag + */ +MediaControls.AnimatedSlider = function( + container, value, range, onChange, onDrag, formatFunction) { + MediaControls.Slider.apply(this, arguments); +}; + +MediaControls.AnimatedSlider.prototype = { + __proto__: MediaControls.Slider.prototype +}; + +MediaControls.AnimatedSlider.STEPS = 10; +MediaControls.AnimatedSlider.DURATION = 100; + +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 + * @param {function(boolean)} onDrag + */ +MediaControls.PreciseSlider = function( + container, value, range, onChange, onDrag, formatFunction) { + MediaControls.Slider.apply(this, arguments); + + var doc = this.container_.ownerDocument; + + /** + * @type {function(number):string} + */ + 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 +}; + +MediaControls.PreciseSlider.SHOW_DELAY = 200; +MediaControls.PreciseSlider.HIDE_AFTER_MOVE_DELAY = 2500; +MediaControls.PreciseSlider.HIDE_AFTER_DRAG_DELAY = 750; +MediaControls.PreciseSlider.NO_AUTO_HIDE = 0; + +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. + */ +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); + } +}; + +MediaControls.PreciseSlider.prototype.hideSeekMark_ = function() { + this.seekMarkTimer_ = null; + this.seekMark_.classList.remove('visible'); +}; + +MediaControls.PreciseSlider.prototype.onMouseMove_ = function(event) { + this.latestSeekRatio_ = this.getProportion(event.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); + } +}; + +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_(); +}; + +MediaControls.PreciseSlider.prototype.onInputChange_ = function() { + MediaControls.Slider.prototype.onInputChange_.apply(this, arguments); + if (this.isDragging()) { + this.showSeekMark_( + this.getValue(), MediaControls.PreciseSlider.NO_AUTO_HIDE); + } +}; + +MediaControls.PreciseSlider.prototype.onInputDrag_ = function(on, event) { + 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} 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, + opt_fullScreenToggle, opt_stateIconParent) { + MediaControls.call(this, containerElement, onMediaError); + + 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.resumePositions_ = new TimeLimitedMap( + 'VideoResumePosition', + VideoControls.RESUME_POSITIONS_CAPACITY, + VideoControls.RESUME_POSITION_LIFETIME); +} + +VideoControls.RESUME_POSITIONS_CAPACITY = 100; +VideoControls.RESUME_POSITION_LIFETIME = 30 * 24 * 60 * 60 * 1000; // 30 days. +VideoControls.RESUME_MARGIN = 0.03; +VideoControls.RESUME_THRESHOLD = 5 * 60; // No resume for videos < 5 min. +VideoControls.RESUME_REWIND = 5; // Rewind 5 seconds back when resuming. + +VideoControls.prototype = { __proto__: MediaControls.prototype }; + +VideoControls.prototype.onMediaComplete = function() { + this.onMediaPlay_(false); // Just update the UI. + this.savePosition(); // This will effectively forget the position. +}; + +VideoControls.prototype.togglePlayStateWithFeedback = function(e) { + if (!this.getMedia().duration) + return; + + this.togglePlayState(); + + var self = this; + + var delay = function(action, opt_timeout) { + if (self.statusIconTimer_) { + clearTimeout(self.statusIconTimer_); + } + self.statusIconTimer_ = setTimeout(function() { + self.statusIconTimer_ = null; + action(); + }, opt_timeout || 0); + }; + + function hideStatusIcon() { + self.stateIcon_.removeAttribute('visible'); + self.stateIcon_.removeAttribute('state'); + } + + hideStatusIcon(); + + // The delays are required to trigger the layout between attribute changes. + // Otherwise everything just goes to the final state without the animation. + delay(function() { + self.stateIcon_.setAttribute('visible', true); + delay(function(){ + self.stateIcon_.setAttribute( + 'state', self.isPlaying() ? 'play' : 'pause'); + delay(hideStatusIcon, 1000); /* Twice the animation duration. */ + }); + }); +}; + +VideoControls.prototype.onMediaDuration_ = function() { + MediaControls.prototype.onMediaDuration_.apply(this, arguments); + if (this.media_.duration && + this.media_.duration >= VideoControls.RESUME_THRESHOLD && + this.media_.seekable) { + var position = this.resumePositions_.getValue(this.media_.src); + if (position) { + this.media_.currentTime = position; + } + } +}; + +VideoControls.prototype.togglePlayState = function(e) { + if (this.isPlaying()) { + // User gave the Pause command. + this.savePosition(); + } + MediaControls.prototype.togglePlayState.apply(this, arguments); +}; + +VideoControls.prototype.savePosition = function() { + if (!this.media_.duration || + this.media_.duration_ < VideoControls.RESUME_THRESHOLD) + return; + + var ratio = this.media_.currentTime / this.media_.duration; + 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. + this.resumePositions_.removeValue(this.media_.src); + } else { + this.resumePositions_.setValue(this.media_.src, Math.floor(Math.max(0, + this.media_.currentTime - VideoControls.RESUME_REWIND))); + } +}; + +/** + * TimeLimitedMap is 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 OK. + * + * @param {string} localStorageKey A key in the local storage. + * @param {number} capacity Maximim number of items. If exceeded, oldest items + * are removed. + * @param {number} lifetime Maximim time to keep an item (in milliseconds). + */ +function TimeLimitedMap(localStorageKey, capacity, lifetime) { + this.localStorageKey_ = localStorageKey; + this.capacity_ = capacity; + this.lifetime_ = lifetime; +} + +/** + * @param {string} key + * @return {string} value + */ +TimeLimitedMap.prototype.getValue = function(key) { + var map = this.read_(); + var entry = map[key]; + return entry && entry.value; +}; + +/** + * @param {string} key + * @param {string} value + */ +TimeLimitedMap.prototype.setValue = function(key, value) { + var map = this.read_(); + map[key] = { value: value, timestamp: Date.now() }; + this.cleanup_(map); + this.write_(map); +}; + +/** + * @param {string} key + */ +TimeLimitedMap.prototype.removeValue = function(key) { + var map = this.read_(); + if (!(key in map)) + return; // Nothing to do. + + delete map[key]; + this.cleanup_(map); + this.write_(map); +}; + +/** + * @return {Object} A map of timestamped key-value pairs. + */ +TimeLimitedMap.prototype.read_ = function() { + var json = localStorage[this.localStorageKey_]; + if (json) { + try { + return JSON.parse(json); + } catch(e) { + // The localStorage item somehow got messed up, start fresh. + } + } + return {}; +}; + +/** + * @param {Object} map A map of timestamped key-value pairs. + */ +TimeLimitedMap.prototype.write_ = function(map) { + localStorage[this.localStorageKey_] = JSON.stringify(map); +}; + +/** + * Remove over-capacity and obsolete items. + * + * @param {Object} map A map of timestamped key-value pairs. + */ +TimeLimitedMap.prototype.cleanup_ = function(map) { + // Sort keys by ascending timestamps. + var keys = []; + for (var key in map) { + keys.push(key); + } + keys.sort(function(a, b) { return map[a].timestamp > map[b].timestamp }); + + var cutoff = Date.now() - this.lifetime_; + + var obsolete = 0; + while (obsolete < keys.length && + map[keys[obsolete]].timestamp < cutoff) { + obsolete++; + } + + var overCapacity = Math.max(0, keys.length - this.capacity_); + + var itemsToDelete = Math.max(obsolete, overCapacity); + for (var i = 0; i != itemsToDelete; i++) { + delete map[keys[i]]; + } +}; + + +/** + * Create audio controls. + * + * @param {HTMLElement} container + * @param {function(boolean)} advanceTrack Parameter: true=forward. + * @constructor + */ +function AudioControls(container, advanceTrack) { + MediaControls.call(this, container, null /* 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)); +} + +AudioControls.prototype = { __proto__: MediaControls.prototype }; + +AudioControls.prototype.onMediaComplete = function() { + this.advanceTrack_(true); +}; + +AudioControls.TRACK_RESTART_THRESHOLD = 5; // seconds. + +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/chrome/browser/resources/file_manager/js/metadata/byte_reader.js b/chrome/browser/resources/file_manager/js/metadata/byte_reader.js new file mode 100644 index 0000000..505473c --- /dev/null +++ b/chrome/browser/resources/file_manager/js/metadata/byte_reader.js @@ -0,0 +1,412 @@ +// 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. + +ByteReader = function(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 const and methods. + +ByteReader.LITTLE_ENDIAN = 0; // Intel, 0x1234 is [0x34, 0x12] +ByteReader.BIG_ENDIAN = 1; // Motorola, 0x1234 is [0x12, 0x34] + +ByteReader.SEEK_BEG = 0; // Seek relative to the beginning of the buffer. +ByteReader.SEEK_CUR = 1; // Seek relative to the current position. +ByteReader.SEEK_END = 2; // Seek relative to the end of the buffer. + +/** + * Throw an error if (0 > pos >= end) or if (pos + size > end). + * + * Static utility function. + */ +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. + */ +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. + */ +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. + */ +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); +}; + +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. + */ +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. + */ +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. + */ +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. + */ +ByteReader.prototype.eof = function() { + return this.pos_ >= this.view_.byteLength; +}; + +/** + * Return true if the current position is before the beginning of the buffer. + */ +ByteReader.prototype.bof = function() { + return this.pos_ < 0; +}; + +/** + * Return true if the current position is outside the buffer. + */ +ByteReader.prototype.beof = function() { + return this.pos_ >= this.view_.byteLength || this.pos_ < 0; +}; + +/** + * Set the expected byte ordering for future reads. + */ +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. + */ +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); +}; + +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. + */ +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. + */ +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. + */ +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. + */ +ByteReader.prototype.readSlice = function(size, opt_end, + opt_arrayConstructor) { + this.validateRead(width, 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. + */ +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. + */ +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. + */ +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. + */ +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. + */ +ByteReader.prototype.tell = function() { + return this.pos_; +}; diff --git a/chrome/browser/resources/file_manager/js/metadata/exif_parser.js b/chrome/browser/resources/file_manager/js/metadata/exif_parser.js new file mode 100644 index 0000000..8279564 --- /dev/null +++ b/chrome/browser/resources/file_manager/js/metadata/exif_parser.js @@ -0,0 +1,365 @@ +// 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. + +const EXIF_MARK_SOI = 0xffd8; // Start of image data. +const EXIF_MARK_SOS = 0xffda; // Start of "stream" (the actual image data). +const EXIF_MARK_SOF = 0xffc0; // Start of "frame" +const EXIF_MARK_EXIF = 0xffe1; // Start of exif block. + +const EXIF_ALIGN_LITTLE = 0x4949; // Indicates little endian exif data. +const EXIF_ALIGN_BIG = 0x4d4d; // Indicates big endian exif data. + +const EXIF_TAG_TIFF = 0x002a; // First directory containing TIFF data. +const EXIF_TAG_GPSDATA = 0x8825; // Pointer from TIFF to the GPS directory. +const EXIF_TAG_EXIFDATA = 0x8769; // Pointer from TIFF to the EXIF IFD. +const EXIF_TAG_SUBIFD = 0x014a; // Pointer from TIFF to "Extra" IFDs. + +const EXIF_TAG_JPG_THUMB_OFFSET = 0x0201; // Pointer from TIFF to thumbnail. +const EXIF_TAG_JPG_THUMB_LENGTH = 0x0202; // Length of thumbnail data. + +const EXIF_TAG_ORIENTATION = 0x0112; +const EXIF_TAG_X_DIMENSION = 0xA002; +const EXIF_TAG_Y_DIMENSION = 0xA003; + +function ExifParser(parent) { + ImageParser.call(this, parent, 'jpeg', /\.jpe?g$/i); +} + +ExifParser.prototype = {__proto__: ImageParser.prototype}; + +ExifParser.prototype.parse = function(file, metadata, callback, errorCallback) { + this.requestSlice(file, callback, errorCallback, metadata, 0); +}; + +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.webkitSlice(filePos, filePos + opt_length)); +}; + +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; + function reread(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()); + } +}; + +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); +}; + +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.'); + } +}; + +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; + } +}; + +ExifParser.prototype.readMark = function(br) { + return br.readScalar(2); +}; + +ExifParser.prototype.readMarkLength = function(br) { + // Length includes the 2 bytes used to store the length. + return br.readScalar(2) - 2; +}; + +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); +}; + +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 || '') + ': ' + 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); +}; + +ExifParser.SCALEX = [1, -1, -1, 1, 1, 1, -1, -1]; +ExifParser.SCALEY = [1, 1, -1, -1, -1, 1, 1, -1]; +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) + */ +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/chrome/browser/resources/file_manager/js/metadata/function_parallel.js b/chrome/browser/resources/file_manager/js/metadata/function_parallel.js new file mode 100644 index 0000000..a2925ac --- /dev/null +++ b/chrome/browser/resources/file_manager/js/metadata/function_parallel.js @@ -0,0 +1,74 @@ +// 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. + +/** + * @constructor + * @class FunctionSequence to invoke steps in sequence + * + * @param steps array of functions to invoke in parallel + * @param callback callback to invoke on success + * @param failureCallback callback to invoke on failure + */ +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 err error message + */ +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. + */ +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 + */ +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/chrome/browser/resources/file_manager/js/metadata/function_sequence.js b/chrome/browser/resources/file_manager/js/metadata/function_sequence.js new file mode 100644 index 0000000..1541d63 --- /dev/null +++ b/chrome/browser/resources/file_manager/js/metadata/function_sequence.js @@ -0,0 +1,124 @@ +// 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. + +/** + * @constructor + * @class FunctionSequence to invoke steps in sequence + * + * @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 + */ +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 err error message + */ +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. + */ +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. + */ +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 + */ +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 + */ +FunctionSequence.prototype.apply_ = function(obj, args) { + this.start.apply(this, args); +}; + diff --git a/chrome/browser/resources/file_manager/js/metadata/id3_parser.js b/chrome/browser/resources/file_manager/js/metadata/id3_parser.js new file mode 100644 index 0000000..96a60af --- /dev/null +++ b/chrome/browser/resources/file_manager/js/metadata/id3_parser.js @@ -0,0 +1,679 @@ +// 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. + +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 {int} length - bytes to read + * @return {int} + */ +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 {int} + */ +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 {int} encoding string encoding. + * @param {int} size maximum string size. Actual result may be shorter. + * + */ +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 {int} majorVersion major id3 version to use + * @param {Object} frame frame so store data at + * @param {int} end frame end position in reader + */ +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 {int} majorVersion major id3 version to use + * @param {Object} frame frame so store data at + * @param {int} end frame end position in reader + */ +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()); +}; + +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()); + } +}; + +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 + * @return {Object} frame read + */ +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; +}; + +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 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 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 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 being parsed. + * @param {ByteReader} 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 fo 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 {int} + */ + 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 {int} + */ + UTF_16 : 1, + + /** + * UTF-16BE [UTF-16] encoded Unicode [UNICODE] without BOM. + * Terminated with $00 00. + * + * @const + * @type {int} + */ + UTF_16BE : 2, + + /** + * UTF-8 [UTF-8] encoded Unicode [UNICODE]. Terminated with $00. + * + * @const + * @type {int} + */ + 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/chrome/browser/resources/file_manager/js/metadata/image_parsers.js b/chrome/browser/resources/file_manager/js/metadata/image_parsers.js new file mode 100644 index 0000000..ed8c528 --- /dev/null +++ b/chrome/browser/resources/file_manager/js/metadata/image_parsers.js @@ -0,0 +1,93 @@ +// 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. + +/* 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}; + +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}; + +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}; + +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}; + +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); \ No newline at end of file diff --git a/chrome/browser/resources/file_manager/js/metadata/metadata_dispatcher.js b/chrome/browser/resources/file_manager/js/metadata/metadata_dispatcher.js new file mode 100644 index 0000000..2c3c0dd --- /dev/null +++ b/chrome/browser/resources/file_manager/js/metadata/metadata_dispatcher.js @@ -0,0 +1,215 @@ +// 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. + +// 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; + +// 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('../util.js'); + +/** + * Dispatches metadata requests to the correct parser. + */ +function MetadataDispatcher() { + // Make sure to ipdate 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 = ['blob:']; // We use blob urls in gallery_demo.js + + 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'); +} + +MetadataDispatcher.parserClasses_ = []; + +MetadataDispatcher.registerParserClass = function(parserClass) { + MetadataDispatcher.parserClasses_.push(parserClass); +}; + +MetadataDispatcher.prototype.parserInstances_ = []; + +/** + * Verbose logging for the dispatcher. + * + * Individual parsers also take this as their default verbosity setting. + */ +MetadataDispatcher.prototype.verbose = false; + +MetadataDispatcher.prototype.messageHandlers = { + 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: function(fileURL) { + var self = this; + + try { + this.processOneFile(fileURL, function callback(metadata) { + self.postMessage('result', [fileURL, metadata]); + }); + } 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. + */ +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. + */ +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. + */ +MetadataDispatcher.prototype.vlog = function(var_args) { + if (this.verbose) + this.log.apply(this, arguments); +}; + +/** + * Post a properly formatted message to the caller. + */ +MetadataDispatcher.prototype.postMessage = function(verb, arguments) { + global.postMessage({verb: verb, arguments: arguments}); +}; + +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); + } +}; + +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; + parser.parse(file, metadata, callback, onError); + } + ]; + + if (fileURL.indexOf('blob:') == 0) { + // Blob urls require different steps: + steps = + [ // Read the blob into an array buffer and get the content type + function readBlob() { + var xhr = new XMLHttpRequest(); + xhr.open('GET', fileURL, true); + xhr.responseType = 'arraybuffer'; + xhr.onload = function(e) { + if (xhr.status == 200) { + nextStep(xhr.getResponseHeader('Content-Type'), xhr.response); + } else { + onError('HTTP ' + xhr.status); + } + }; + xhr.send(); + }, + + // Step two, find the parser matching the content type. + function detectFormat(mimeType, arrayBuffer) { + for (var i = 0; i != self.parserInstances_.length; i++) { + var parser = self.parserInstances_[i]; + if (parser.acceptsMimeType(mimeType)) { + metadata = parser.createDefaultMetadata(); + var blobBuilder = new WebKitBlobBuilder(); + blobBuilder.append(arrayBuffer); + nextStep(blobBuilder.getBlob(), parser); + return; + } + } + callback({}); // Unrecognized mime type. + }, + + // Reuse the last step from the standard sequence. + steps[steps.length - 1] + ]; + } + + nextStep(); +}; + +var dispatcher = new MetadataDispatcher(); +global.onmessage = dispatcher.onMessage.bind(dispatcher); diff --git a/chrome/browser/resources/file_manager/js/metadata/metadata_parser.js b/chrome/browser/resources/file_manager/js/metadata/metadata_parser.js new file mode 100644 index 0000000..094775c --- /dev/null +++ b/chrome/browser/resources/file_manager/js/metadata/metadata_parser.js @@ -0,0 +1,43 @@ +// 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. + +function MetadataParser(parent, type, urlFilter) { + this.parent_ = parent; + this.type = type; + this.urlFilter = urlFilter; + this.verbose = parent.verbose; + this.mimeType = 'unknown'; +} + +MetadataParser.prototype.error = function(var_args) { + this.parent_.error.apply(this.parent_, arguments); +}; + +MetadataParser.prototype.log = function(var_args) { + this.parent_.log.apply(this.parent_, arguments); +}; + +MetadataParser.prototype.vlog = function(var_args) { + if (this.verbose) + this.parent_.log.apply(this.parent_, arguments); +}; + +MetadataParser.prototype.createDefaultMetadata = function() { + return { + type: this.type, + mimeType: this.mimeType + }; +}; + +MetadataParser.prototype.acceptsMimeType = function(mimeType) { + return 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/chrome/browser/resources/file_manager/js/metadata/metadata_provider.js b/chrome/browser/resources/file_manager/js/metadata/metadata_provider.js new file mode 100644 index 0000000..e49f452 --- /dev/null +++ b/chrome/browser/resources/file_manager/js/metadata/metadata_provider.js @@ -0,0 +1,115 @@ +// 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. + +/** + * @param {string} opt_workerPath path to the worker source JS file. + */ +function MetadataProvider(opt_workerPath) { + this.cache_ = {}; + + // Pass all URLs to the metadata reader until we have a correct filter. + this.urlFilter = /.*/; + + if (!opt_workerPath) { + var path = document.location.pathname; + opt_workerPath = document.location.origin + + path.substring(0, path.lastIndexOf('/') + 1) + + 'js/metadata/metadata_dispatcher.js'; + } + + this.dispatcher_ = new Worker(opt_workerPath); + 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. +} + +MetadataProvider.prototype.fetch = function(url, callback) { + var cacheValue = this.cache_[url]; + + if (!cacheValue) { + // This is the first time anyone's asked, go get it. + if (url.match(this.urlFilter)) { + this.cache_[url] = [callback]; + this.dispatcher_.postMessage({verb: 'request', arguments: [url]}); + return; + } + // Cannot extract metadata for this file, return an empty map. + setTimeout(function() { callback({}) }, 0); + return; + } + + if (cacheValue instanceof Array) { + // Something is already pending, add to the list of observers. + cacheValue.push(callback); + return; + } + + if (cacheValue instanceof Object) { + // We already know the answer, let the caller know in a fresh call stack. + setTimeout(function() { callback(cacheValue) }, 0); + return; + } + + console.error('Unexpected metadata cache value:' + cacheValue); +}; + +MetadataProvider.prototype.reset = function(url) { + if (this.cache_[url] instanceof Array) { + console.error( + 'Abandoned ' + this.cache_[url].length + ' metadata subscribers', + url); + } + delete this.cache_[url]; +}; + + +/** + * Dispatch a message from a metadata reader to the appropriate on* method. + */ +MetadataProvider.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.log('Unknown message from metadata reader: ' + data.verb, data); + return; + } + + this[methodName].apply(this, data.arguments); +}; + +/** + * Handles the 'initialized' message from the metadata reader Worker. + */ +MetadataProvider.prototype.onInitialized_ = function(regexp) { + this.urlFilter = regexp; +}; + +MetadataProvider.prototype.onResult_ = function(url, metadata) { + var observers = this.cache_[url]; + if (!observers || !(observers instanceof Array)) { + console.error('Missing or invalid metadata observers: ' + url + ': ' + + observers); + return; + } + + console.log('metadata result:', metadata); + for (var i = 0; i < observers.length; i++) { + observers[i](metadata); + } + + this.cache_[url] = metadata; +}; + +MetadataProvider.prototype.onError_ = function(url, step, error, metadata) { + console.warn('metadata: ' + url + ': ' + step + ': ' + error); + this.onResult_(url, metadata || {}); +}; + +MetadataProvider.prototype.onLog_ = function(arglist) { + console.log.apply(console, ['metadata:'].concat(arglist)); +}; diff --git a/chrome/browser/resources/file_manager/js/metadata/mpeg_parser.js b/chrome/browser/resources/file_manager/js/metadata/mpeg_parser.js new file mode 100644 index 0000000..b2458cb --- /dev/null +++ b/chrome/browser/resources/file_manager/js/metadata/mpeg_parser.js @@ -0,0 +1,262 @@ +// 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. + +function MpegParser(parent) { + MetadataParser.call(this, parent, 'mpeg', /\.(mp4|m4v|m4a|mpe?g4?)$/i); + this.mimeType = 'video/mpeg'; +} + +MpegParser.prototype = {__proto__: MetadataParser.prototype}; + +MpegParser.prototype.acceptsMimeType = function(mimeType) { + return mimeType.match(/^video\/(mp4|mpeg)$/); +}; + +MpegParser.HEADER_SIZE = 8; + +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; +}; + +MpegParser.readAtomName = function(br, opt_end) { + return br.readString(4, opt_end).toLowerCase(); +}; + +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 + }; +}; + +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)); +}; + +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); + } + } +}; + +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; + } +} + +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.webkitSlice(filePos, filePos + size)); +} + +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) { + return onError(e.toString()); + } +}; + +MetadataDispatcher.registerParserClass(MpegParser); diff --git a/chrome/browser/resources/file_manager/js/metadata_dispatcher.js b/chrome/browser/resources/file_manager/js/metadata_dispatcher.js deleted file mode 100644 index 144b580..0000000 --- a/chrome/browser/resources/file_manager/js/metadata_dispatcher.js +++ /dev/null @@ -1,214 +0,0 @@ -// Copyright (c) 2011 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. - -// 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; - -// 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'); - -/** - * Dispatches metadata requests to the correct parser. - */ -function MetadataDispatcher() { - // Make sure to ipdate 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 = ['blob:']; // We use blob urls in gallery_demo.js - - 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'); -} - -MetadataDispatcher.parserClasses_ = []; - -MetadataDispatcher.registerParserClass = function(parserClass) { - MetadataDispatcher.parserClasses_.push(parserClass); -}; - -MetadataDispatcher.prototype.parserInstances_ = []; - -/** - * Verbose logging for the dispatcher. - * - * Individual parsers also take this as their default verbosity setting. - */ -MetadataDispatcher.prototype.verbose = false; - -MetadataDispatcher.prototype.messageHandlers = { - 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: function(fileURL) { - var self = this; - - try { - this.processOneFile(fileURL, function callback(metadata) { - self.postMessage('result', [fileURL, metadata]); - }); - } 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. - */ -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. - */ -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. - */ -MetadataDispatcher.prototype.vlog = function(var_args) { - if (this.verbose) - this.log.apply(this, arguments); -}; - -/** - * Post a properly formatted message to the caller. - */ -MetadataDispatcher.prototype.postMessage = function(verb, arguments) { - global.postMessage({verb: verb, arguments: arguments}); -}; - -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); - } -}; - -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; - parser.parse(file, metadata, callback, onError); - } - ]; - - if (fileURL.indexOf('blob:') == 0) { - // Blob urls require different steps: - steps = - [ // Read the blob into an array buffer and get the content type - function readBlob() { - var xhr = new XMLHttpRequest(); - xhr.open('GET', fileURL, true); - xhr.responseType = 'arraybuffer'; - xhr.onload = function(e) { - if (xhr.status == 200) { - nextStep(xhr.getResponseHeader('Content-Type'), xhr.response); - } else { - onError('HTTP ' + xhr.status); - } - }; - xhr.send(); - }, - - // Step two, find the parser matching the content type. - function detectFormat(mimeType, arrayBuffer) { - for (var i = 0; i != self.parserInstances_.length; i++) { - var parser = self.parserInstances_[i]; - if (parser.acceptsMimeType(mimeType)) { - metadata = parser.createDefaultMetadata(); - var blobBuilder = new WebKitBlobBuilder(); - blobBuilder.append(arrayBuffer); - nextStep(blobBuilder.getBlob(), parser); - return; - } - } - callback({}); // Unrecognized mime type. - }, - - // Reuse the last step from the standard sequence. - steps[steps.length - 1] - ]; - } - - nextStep(); -}; - -var dispatcher = new MetadataDispatcher(); -global.onmessage = dispatcher.onMessage.bind(dispatcher); diff --git a/chrome/browser/resources/file_manager/js/metadata_parser.js b/chrome/browser/resources/file_manager/js/metadata_parser.js deleted file mode 100644 index 094775c..0000000 --- a/chrome/browser/resources/file_manager/js/metadata_parser.js +++ /dev/null @@ -1,43 +0,0 @@ -// 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. - -function MetadataParser(parent, type, urlFilter) { - this.parent_ = parent; - this.type = type; - this.urlFilter = urlFilter; - this.verbose = parent.verbose; - this.mimeType = 'unknown'; -} - -MetadataParser.prototype.error = function(var_args) { - this.parent_.error.apply(this.parent_, arguments); -}; - -MetadataParser.prototype.log = function(var_args) { - this.parent_.log.apply(this.parent_, arguments); -}; - -MetadataParser.prototype.vlog = function(var_args) { - if (this.verbose) - this.parent_.log.apply(this.parent_, arguments); -}; - -MetadataParser.prototype.createDefaultMetadata = function() { - return { - type: this.type, - mimeType: this.mimeType - }; -}; - -MetadataParser.prototype.acceptsMimeType = function(mimeType) { - return 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/chrome/browser/resources/file_manager/js/metadata_provider.js b/chrome/browser/resources/file_manager/js/metadata_provider.js deleted file mode 100644 index 7973660..0000000 --- a/chrome/browser/resources/file_manager/js/metadata_provider.js +++ /dev/null @@ -1,115 +0,0 @@ -// Copyright (c) 2011 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. - -/** - * @param {string} opt_workerPath path to the worker source JS file. - */ -function MetadataProvider(opt_workerPath) { - this.cache_ = {}; - - // Pass all URLs to the metadata reader until we have a correct filter. - this.urlFilter = /.*/; - - if (!opt_workerPath) { - var path = document.location.pathname; - opt_workerPath = document.location.origin + - path.substring(0, path.lastIndexOf('/') + 1) + - 'js/metadata_dispatcher.js'; - } - - this.dispatcher_ = new Worker(opt_workerPath); - 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. -} - -MetadataProvider.prototype.fetch = function(url, callback) { - var cacheValue = this.cache_[url]; - - if (!cacheValue) { - // This is the first time anyone's asked, go get it. - if (url.match(this.urlFilter)) { - this.cache_[url] = [callback]; - this.dispatcher_.postMessage({verb: 'request', arguments: [url]}); - return; - } - // Cannot extract metadata for this file, return an empty map. - setTimeout(function() { callback({}) }, 0); - return; - } - - if (cacheValue instanceof Array) { - // Something is already pending, add to the list of observers. - cacheValue.push(callback); - return; - } - - if (cacheValue instanceof Object) { - // We already know the answer, let the caller know in a fresh call stack. - setTimeout(function() { callback(cacheValue) }, 0); - return; - } - - console.error('Unexpected metadata cache value:' + cacheValue); -}; - -MetadataProvider.prototype.reset = function(url) { - if (this.cache_[url] instanceof Array) { - console.error( - 'Abandoned ' + this.cache_[url].length + ' metadata subscribers', - url); - } - delete this.cache_[url]; -}; - - -/** - * Dispatch a message from a metadata reader to the appropriate on* method. - */ -MetadataProvider.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.log('Unknown message from metadata reader: ' + data.verb, data); - return; - } - - this[methodName].apply(this, data.arguments); -}; - -/** - * Handles the 'initialized' message from the metadata reader Worker. - */ -MetadataProvider.prototype.onInitialized_ = function(regexp) { - this.urlFilter = regexp; -}; - -MetadataProvider.prototype.onResult_ = function(url, metadata) { - var observers = this.cache_[url]; - if (!observers || !(observers instanceof Array)) { - console.error('Missing or invalid metadata observers: ' + url + ': ' + - observers); - return; - } - - console.log('metadata result:', metadata); - for (var i = 0; i < observers.length; i++) { - observers[i](metadata); - } - - this.cache_[url] = metadata; -}; - -MetadataProvider.prototype.onError_ = function(url, step, error, metadata) { - console.warn('metadata: ' + url + ': ' + step + ': ' + error); - this.onResult_(url, metadata || {}); -}; - -MetadataProvider.prototype.onLog_ = function(arglist) { - console.log.apply(console, ['metadata:'].concat(arglist)); -}; diff --git a/chrome/browser/resources/file_manager/js/mpeg_parser.js b/chrome/browser/resources/file_manager/js/mpeg_parser.js deleted file mode 100644 index b2458cb..0000000 --- a/chrome/browser/resources/file_manager/js/mpeg_parser.js +++ /dev/null @@ -1,262 +0,0 @@ -// 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. - -function MpegParser(parent) { - MetadataParser.call(this, parent, 'mpeg', /\.(mp4|m4v|m4a|mpe?g4?)$/i); - this.mimeType = 'video/mpeg'; -} - -MpegParser.prototype = {__proto__: MetadataParser.prototype}; - -MpegParser.prototype.acceptsMimeType = function(mimeType) { - return mimeType.match(/^video\/(mp4|mpeg)$/); -}; - -MpegParser.HEADER_SIZE = 8; - -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; -}; - -MpegParser.readAtomName = function(br, opt_end) { - return br.readString(4, opt_end).toLowerCase(); -}; - -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 - }; -}; - -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)); -}; - -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); - } - } -}; - -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; - } -} - -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.webkitSlice(filePos, filePos + size)); -} - -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) { - return onError(e.toString()); - } -}; - -MetadataDispatcher.registerParserClass(MpegParser); diff --git a/chrome/browser/resources/file_manager/main.html b/chrome/browser/resources/file_manager/main.html index 0652bbd..1a351c4 100644 --- a/chrome/browser/resources/file_manager/main.html +++ b/chrome/browser/resources/file_manager/main.html @@ -99,7 +99,7 @@ - + diff --git a/chrome/browser/resources/file_manager/mediaplayer.html b/chrome/browser/resources/file_manager/mediaplayer.html index 0a2ffdd..baa04ff 100644 --- a/chrome/browser/resources/file_manager/mediaplayer.html +++ b/chrome/browser/resources/file_manager/mediaplayer.html @@ -2,12 +2,12 @@ "http://www.w3.org/TR/html4/loose.dtd"> - + - - - + + +
diff --git a/chrome/browser/resources/file_manager/playlist.html b/chrome/browser/resources/file_manager/playlist.html deleted file mode 100644 index 56a6ffa..0000000 --- a/chrome/browser/resources/file_manager/playlist.html +++ /dev/null @@ -1,155 +0,0 @@ - - - - -Media Playlist - - - - -
-
- - -- cgit v1.1