OLD | NEW |
---|---|
(Empty) | |
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | |
2 // Use of this source code is governed by a BSD-style license that can be | |
3 // found in the LICENSE file. | |
4 | |
5 document.addEventListener('DOMContentLoaded', function() { | |
6 if (document.location.hash) // File path passed after the #. | |
7 Gallery.openStandalone(decodeURI(document.location.hash.substr(1))); | |
8 }); | |
9 | |
10 /** | |
11 * Gallery for viewing and editing image files. | |
12 * | |
13 * @param {Object} context Object containing the following: | |
14 * {function(string)} onNameChange Called every time a selected | |
15 * item name changes (on rename and on selection change). | |
16 * {function} onClose | |
17 * {MetadataCache} metadataCache | |
18 * {Array.<Object>} shareActions | |
19 * {string} readonlyDirName Directory name for readonly warning or null. | |
20 * {DirEntry} saveDirEntry Directory to save to. | |
21 * {function(string)} displayStringFunction | |
22 * @class | |
23 * @constructor | |
24 */ | |
25 function Gallery(context) { | |
26 this.container_ = document.querySelector('.gallery'); | |
27 this.document_ = document; | |
28 this.context_ = context; | |
29 this.metadataCache_ = context.metadataCache; | |
30 | |
31 var strf = context.displayStringFunction; | |
32 this.displayStringFunction_ = function(id, formatArgs) { | |
33 var args = Array.prototype.slice.call(arguments); | |
34 args[0] = 'GALLERY_' + id.toUpperCase(); | |
35 return strf.apply(null, args); | |
36 }; | |
37 | |
38 this.initListeners_(); | |
39 this.initDom_(); | |
40 } | |
41 | |
42 /** | |
43 * Create and initialize a Gallery object based on a context. | |
44 * | |
45 * @param {Object} context Gallery context. | |
46 * @param {Array.<string>} urls Array of image urls. | |
47 * @param {string} selectedUrl Selected url. | |
48 */ | |
49 Gallery.open = function(context, urls, selectedUrl) { | |
50 Gallery.instance = new Gallery(context); | |
51 Gallery.instance.load(urls, selectedUrl); | |
52 }; | |
53 | |
54 /** | |
55 * Create a Gallery object in a tab. | |
56 * @param {string} path File system path to a selected file. | |
57 */ | |
58 Gallery.openStandalone = function(path) { | |
59 ImageUtil.metrics = metrics; | |
60 | |
61 var currentDir; | |
62 var urls = []; | |
63 var selectedUrl; | |
64 | |
65 Gallery.getFileBrowserPrivate().requestLocalFileSystem(function(filesystem) { | |
66 // If the path points to the directory scan it. | |
67 filesystem.root.getDirectory(path, {create: false}, scanDirectory, | |
68 function() { | |
69 // Try to scan the parent directory. | |
70 var pathParts = path.split('/'); | |
71 pathParts.pop(); | |
72 var parentPath = pathParts.join('/'); | |
dgozman
2012/08/17 07:11:03
Is there a method in PathUtil?
Vladislav Kaznacheev
2012/08/17 09:20:40
No.
On 2012/08/17 07:11:03, dgozman wrote:
| |
73 filesystem.root.getDirectory(parentPath, {create: false}, | |
74 scanDirectory, open /* no data, just display an error */); | |
75 }); | |
76 }); | |
77 | |
78 function scanDirectory(dirEntry) { | |
79 currentDir = dirEntry; | |
80 util.forEachDirEntry(currentDir, function(entry) { | |
81 if (entry == null) { | |
82 open(); | |
83 } else if (FileType.isImageOrVideo(entry.name)) { | |
dgozman
2012/08/17 07:11:03
You can pass entry instead of name here.
This is
Vladislav Kaznacheev
2012/08/17 09:20:40
Done.
| |
84 var url = entry.toURL(); | |
85 urls.push(url); | |
86 if (entry.fullPath == path) | |
87 selectedUrl = url; | |
88 } | |
89 }); | |
90 } | |
91 | |
92 function onNameChange(name) { | |
93 window.top.document.title = name; | |
94 | |
95 var newPath = currentDir.fullPath + '/' + name; | |
96 var location = document.location.origin + document.location.pathname + | |
97 '#' + encodeURI(newPath); | |
98 history.replaceState(undefined, newPath, location); | |
99 } | |
100 | |
101 function onClose() { | |
102 document.location = 'main.html?' + | |
103 JSON.stringify({defaultPath: document.location.hash.substr(1)}); | |
104 } | |
105 | |
106 function open() { | |
107 Gallery.getFileBrowserPrivate().getStrings(function(strings) { | |
108 loadTimeData.data = strings; | |
109 var context = { | |
110 readonlyDirName: null, | |
111 saveDirEntry: currentDir, | |
112 metadataCache: MetadataCache.createFull(), | |
113 onNameChange: onNameChange, | |
114 onClose: onClose, | |
115 displayStringFunction: strf | |
dgozman
2012/08/17 07:11:03
strf is not defined.
Vladislav Kaznacheev
2012/08/17 09:20:40
It is now. It recently moved from file_manager.js
dgozman
2012/08/17 09:55:00
Well, do we need displayStringFunction anymore? Ju
Vladislav Kaznacheev
2012/08/17 10:30:49
We can avoid passing it to Gallery, but the Galler
| |
116 }; | |
117 Gallery.open(context, urls, selectedUrl || urls[0]); | |
118 }); | |
119 } | |
120 }; | |
121 | |
122 /** | |
123 * Tools fade-out timeout im milliseconds. | |
124 * @type {Number} | |
125 */ | |
126 Gallery.FADE_TIMEOUT = 3000; | |
127 | |
128 /** | |
129 * First time tools fade-out timeout im milliseconds. | |
130 * @type {Number} | |
131 */ | |
132 Gallery.FIRST_FADE_TIMEOUT = 1000; | |
133 | |
134 /** | |
135 * Types of metadata Gallery uses (to query the metadata cache). | |
136 */ | |
137 Gallery.METADATA_TYPE = 'thumbnail|filesystem|media|streaming'; | |
138 | |
139 /** | |
140 * Initialize listeners. | |
141 * @private | |
142 */ | |
143 | |
144 Gallery.prototype.initListeners_ = function() { | |
145 this.document_.oncontextmenu = function(e) { e.preventDefault(); }; | |
146 | |
147 this.document_.body.addEventListener('keydown', this.onKeyDown_.bind(this)); | |
148 | |
149 this.inactivityWatcher_ = new MouseInactivityWatcher( | |
150 this.container_, Gallery.FADE_TIMEOUT, this.hasActiveTool.bind(this)); | |
151 | |
152 // Show tools when the user touches the screen. | |
153 this.document_.body.addEventListener('touchstart', | |
154 this.inactivityWatcher_.startActivity.bind(this.inactivityWatcher_)); | |
155 var initiateFading = | |
156 this.inactivityWatcher_.stopActivity.bind(this.inactivityWatcher_, | |
157 Gallery.FADE_TIMEOUT); | |
158 this.document_.body.addEventListener('touchend', initiateFading); | |
159 this.document_.body.addEventListener('touchcancel', initiateFading); | |
160 }; | |
161 | |
162 /** | |
163 * Initializes DOM UI | |
164 * @private | |
165 */ | |
166 Gallery.prototype.initDom_ = function() { | |
167 var content = createChild(this.container_, 'content'); | |
168 content.addEventListener('click', this.onContentClick_.bind(this)); | |
169 | |
170 var closeButton = createChild(this.container_, 'close tool dimmable'); | |
171 createChild(closeButton); | |
172 closeButton.addEventListener('click', this.onClose_.bind(this)); | |
173 | |
174 this.toolbar_ = createChild(this.container_, 'toolbar tool dimmable'); | |
175 | |
176 this.filenameSpacer_ = createChild(this.toolbar_, 'filename-spacer'); | |
177 | |
178 var nameBox = createChild(this.filenameSpacer_, 'namebox'); | |
179 | |
180 this.filenameText_ = createChild(nameBox); | |
181 this.filenameText_.addEventListener('click', | |
182 this.onFilenameClick_.bind(this)); | |
183 | |
184 this.filenameEdit_ = this.document_.createElement('input'); | |
185 this.filenameEdit_.setAttribute('type', 'text'); | |
186 this.filenameEdit_.addEventListener('blur', | |
187 this.onFilenameEditBlur_.bind(this)); | |
188 this.filenameEdit_.addEventListener('keydown', | |
189 this.onFilenameEditKeydown_.bind(this)); | |
190 nameBox.appendChild(this.filenameEdit_); | |
191 | |
192 createChild(this.toolbar_, 'button-spacer'); | |
193 | |
194 this.prompt_ = new ImageEditor.Prompt( | |
195 this.container_, this.displayStringFunction_); | |
196 | |
197 this.slideMode_ = new SlideMode(this.container_, this.toolbar_, this.prompt_, | |
198 this.context_, this.displayStringFunction_); | |
199 this.slideMode_.addEventListener('edit', this.onEdit_.bind(this)); | |
200 this.slideMode_.addEventListener('selection', this.onSelection_.bind(this)); | |
201 | |
202 this.shareMode_ = new ShareMode( | |
203 this.slideMode_.editor_, this.container_, this.toolbar_, | |
204 this.onShare_.bind(this), this.executeWhenReady.bind(this), | |
205 this.displayStringFunction_); | |
206 | |
207 Gallery.getFileBrowserPrivate().isFullscreen(function(fullscreen) { | |
208 this.originalFullscreen_ = fullscreen; | |
209 }.bind(this)); | |
210 }; | |
211 | |
212 /** | |
213 * Load the content. | |
214 * | |
215 * @param {Array.<string>} urls Array of urls. | |
216 * @param {string} selectedUrl Selected url. | |
217 */ | |
218 Gallery.prototype.load = function(urls, selectedUrl) { | |
219 this.items_ = []; | |
220 for (var index = 0; index < urls.length; ++index) { | |
221 this.items_.push(new Gallery.Item(urls[index])); | |
222 } | |
223 | |
224 var selectedIndex = urls.indexOf(selectedUrl); | |
225 this.slideMode_.load(this.items_, selectedIndex, function() { | |
226 // Flash the toolbar briefly to show it is there. | |
227 this.inactivityWatcher_.startActivity(); | |
228 this.inactivityWatcher_.stopActivity(Gallery.FIRST_FADE_TIMEOUT); | |
229 }.bind(this)); | |
230 }; | |
231 | |
232 /** | |
233 * Close the Gallery. | |
234 * @private | |
235 */ | |
236 Gallery.prototype.close_ = function() { | |
237 Gallery.getFileBrowserPrivate().isFullscreen(function(fullscreen) { | |
238 if (this.originalFullscreen_ != fullscreen) { | |
239 Gallery.toggleFullscreen(); | |
240 } | |
241 this.context_.onClose(); | |
242 }.bind(this)); | |
243 }; | |
244 | |
245 /** | |
246 * Handle user's 'Close' action (Escape or a click on the X icon). | |
247 * @private | |
248 */ | |
249 Gallery.prototype.onClose_ = function() { | |
250 // TODO: handle write errors gracefully (suggest retry or saving elsewhere). | |
dgozman
2012/08/17 07:11:03
I think, this TODO should go to the save code now.
Vladislav Kaznacheev
2012/08/17 09:20:40
We already have it there. Removing here.
On 2012/0
| |
251 this.executeWhenReady(this.close_.bind(this)); | |
252 }; | |
253 | |
254 /** | |
255 * Execute a function when the editor is done with the modifications. | |
256 * @param {function} callback Function to execute. | |
257 */ | |
258 Gallery.prototype.executeWhenReady = function(callback) { | |
259 //TODO(kaznacheev): Execute directly when in grid mode. | |
260 this.slideMode_.editor_.executeWhenReady(callback); | |
261 }; | |
262 | |
263 /** | |
264 * @return {Object} File browser private API. | |
265 */ | |
266 Gallery.getFileBrowserPrivate = function() { | |
267 return chrome.fileBrowserPrivate || window.top.chrome.fileBrowserPrivate; | |
268 }; | |
269 | |
270 /** | |
271 * Switches gallery to fullscreen mode and back. | |
272 */ | |
273 Gallery.toggleFullscreen = function() { | |
274 Gallery.getFileBrowserPrivate().toggleFullscreen(); | |
275 }; | |
276 | |
277 /** | |
278 * @return {boolean} True if some tool is currently active. | |
279 */ | |
280 Gallery.prototype.hasActiveTool = function() { | |
281 return this.slideMode_.isEditing() || this.isSharing_() || this.isRenaming_(); | |
282 }; | |
283 | |
284 /** | |
285 * Check if the tools are active and notify the inactivity watcher. | |
286 * @private | |
287 */ | |
288 Gallery.prototype.checkActivity_ = function() { | |
289 if (this.hasActiveTool()) | |
290 this.inactivityWatcher_.startActivity(); | |
291 else | |
292 this.inactivityWatcher_.stopActivity(); | |
293 }; | |
294 | |
295 /** | |
296 * Edit toggle event handler. | |
297 * @private | |
298 */ | |
299 Gallery.prototype.onEdit_ = function() { | |
300 // The user has just clicked on the Edit button. Dismiss the Share menu. | |
301 if (this.isSharing_()) | |
302 this.onShare_(); | |
303 this.checkActivity_(); | |
304 }; | |
305 | |
306 /** | |
307 * @return {Array.<Gallery.Item>} Current selection. | |
308 */ | |
309 Gallery.prototype.getSelectedItems = function() { | |
310 // TODO(kaznacheev) support multiple selection grid/mosaic mode. | |
311 return [this.slideMode_.getSelectedItem()]; | |
312 }; | |
313 | |
314 /** | |
315 * @return {Gallery.Item} Current single selection. | |
316 */ | |
317 Gallery.prototype.getSingleSelectedItem = function() { | |
318 var items = this.getSelectedItems(); | |
319 if (items.length > 1) | |
320 throw new Error('Unexpected multiple selection'); | |
321 return items[0]; | |
322 }; | |
323 | |
324 /** | |
325 * Selection change event handler. | |
326 * @private | |
327 */ | |
328 Gallery.prototype.onSelection_ = function() { | |
329 this.updateFilename_(); | |
330 this.shareMode_.updateMenu( | |
331 this.getSelectedItems().map(function(item) { return item.getUrl() })); | |
332 }; | |
333 | |
334 /** | |
335 * Keydown handler. | |
336 * @param {Event} event Event. | |
337 * @private | |
338 */ | |
339 Gallery.prototype.onKeyDown_ = function(event) { | |
340 if (this.slideMode_.onKeyDown(event)) | |
341 return; | |
342 | |
343 switch (util.getKeyModifiers(event) + event.keyIdentifier) { | |
344 case 'U+0008': // Backspace. | |
345 // The default handler would call history.back and close the Gallery. | |
346 event.preventDefault(); | |
347 break; | |
348 | |
349 case 'U+001B': // Escape | |
350 if (this.isSharing_()) | |
351 this.onShare_(); | |
352 else | |
353 this.onClose_(); | |
354 break; | |
355 } | |
356 }; | |
357 | |
358 // Name box and rename support. | |
359 | |
360 /** | |
361 * Update the displayed current item file name. | |
362 * | |
363 * @private | |
364 */ | |
365 Gallery.prototype.updateFilename_ = function() { | |
366 var fullName = this.getSingleSelectedItem().getFileName(); | |
367 | |
368 this.context_.onNameChange(fullName); | |
369 | |
370 var displayName = ImageUtil.getFileNameFromFullName(fullName); | |
371 this.filenameEdit_.value = displayName; | |
372 this.filenameText_.textContent = displayName; | |
373 }; | |
374 | |
375 /** | |
376 * Click event handler on filename edit box | |
377 * @private | |
378 */ | |
379 Gallery.prototype.onFilenameClick_ = function() { | |
380 // We can't rename files in readonly directory. | |
381 if (this.context_.readonlyDirName) | |
382 return; | |
383 | |
384 ImageUtil.setAttribute(this.filenameSpacer_, 'renaming', true); | |
385 setTimeout(this.filenameEdit_.select.bind(this.filenameEdit_), 0); | |
386 this.inactivityWatcher_.startActivity(); | |
387 }; | |
388 | |
389 /** | |
390 * Blur event handler on filename edit box | |
391 * @private | |
392 */ | |
393 Gallery.prototype.onFilenameEditBlur_ = function() { | |
394 if (this.filenameEdit_.value && this.filenameEdit_.value[0] == '.') { | |
395 this.prompt_.show('file_hidden_name', 5000); | |
396 this.filenameEdit_.focus(); | |
397 return; | |
398 } | |
399 | |
400 var item = this.getSingleSelectedItem(); | |
401 var oldUrl = item.getUrl(); | |
402 | |
403 var onFileExists = function() { | |
404 this.prompt_.show('file_exists', 3000); | |
405 this.filenameEdit_.value = name; | |
406 this.onFilenameClick_(); | |
407 }.bind(this); | |
408 | |
409 var onSuccess = function() { | |
410 this.slideMode_.updateSelectedUrl_(oldUrl, item.getUrl()); | |
411 }.bind(this); | |
412 | |
413 if (this.filenameEdit_.value) { | |
414 this.getSingleSelectedItem().rename(this.context_.saveDirEntry, | |
415 this.filenameEdit_.value, onSuccess, onFileExists); | |
416 } | |
417 | |
418 ImageUtil.setAttribute(this.filenameSpacer_, 'renaming', false); | |
419 this.checkActivity_(); | |
420 }; | |
421 | |
422 /** | |
423 * Keydown event handler on filename edit box | |
424 * @private | |
425 */ | |
426 Gallery.prototype.onFilenameEditKeydown_ = function() { | |
427 switch (event.keyCode) { | |
428 case 27: // Escape | |
429 this.updateFilename_(); | |
430 this.filenameEdit_.blur(); | |
431 break; | |
432 | |
433 case 13: // Enter | |
434 this.filenameEdit_.blur(); | |
435 break; | |
436 } | |
437 event.stopPropagation(); | |
438 }; | |
439 | |
440 /** | |
441 * @return {Boolean} True if file renaming is currently in progress | |
442 * @private | |
443 */ | |
444 Gallery.prototype.isRenaming_ = function() { | |
445 return this.filenameSpacer_.hasAttribute('renaming'); | |
446 }; | |
447 | |
448 /** | |
449 * Content area click handler. | |
450 * @private | |
451 */ | |
452 Gallery.prototype.onContentClick_ = function() { | |
453 this.filenameEdit_.blur(); | |
454 }; | |
455 | |
456 // Share button support. | |
457 | |
458 /** | |
459 * @return {boolean} True if the Share mode is active. | |
460 * @private | |
461 */ | |
462 Gallery.prototype.isSharing_ = function() { | |
463 return this.shareMode_.isActive(); | |
464 }; | |
465 | |
466 /** | |
467 * Share button handler. | |
468 * @param {Event} event Event. | |
469 * @private | |
470 */ | |
471 Gallery.prototype.onShare_ = function(event) { | |
472 this.shareMode_.toggle(event); | |
473 this.checkActivity_(); | |
474 }; | |
475 | |
476 /** | |
477 * | |
478 * @param {ImageEditor} editor Editor. | |
479 * @param {Element} container Container element. | |
480 * @param {Element} toolbar Toolbar element. | |
481 * @param {function} onClick Click handler. | |
482 * @param {function(function())} actionCallback Function to execute the action. | |
483 * @param {function(string):string} displayStringFunction String formatting | |
484 * function. | |
485 * @constructor | |
486 */ | |
487 function ShareMode(editor, container, toolbar, | |
488 onClick, actionCallback, displayStringFunction) { | |
489 ImageEditor.Mode.call(this, 'share'); | |
490 | |
491 this.message_ = null; | |
492 | |
493 var button = createChild(toolbar, 'button share'); | |
494 button.textContent = displayStringFunction('share'); | |
495 button.addEventListener('click', onClick); | |
496 this.bind(editor, button); | |
497 | |
498 this.actionCallback_ = actionCallback; | |
499 | |
500 this.menu_ = createChild(container, 'share-menu'); | |
501 this.menu_.hidden = true; | |
502 | |
503 createChild(this.menu_, 'bubble-point'); | |
504 } | |
505 | |
506 ShareMode.prototype = { __proto__: ImageEditor.Mode.prototype }; | |
507 | |
508 /** | |
509 * Shows share mode UI. | |
510 */ | |
511 ShareMode.prototype.setUp = function() { | |
512 ImageEditor.Mode.prototype.setUp.apply(this, arguments); | |
513 this.menu_.hidden = false; | |
514 ImageUtil.setAttribute(this.button_, 'pressed', false); | |
515 }; | |
516 | |
517 /** | |
518 * Hides share mode UI. | |
519 */ | |
520 ShareMode.prototype.cleanUpUI = function() { | |
521 ImageEditor.Mode.prototype.cleanUpUI.apply(this, arguments); | |
522 this.menu_.hidden = true; | |
523 }; | |
524 | |
525 /** | |
526 * @return {boolean} True if the menu is currently open. | |
527 */ | |
528 ShareMode.prototype.isActive = function() { | |
529 return !this.menu_.hidden; | |
530 }; | |
531 | |
532 /** | |
533 * Show/hide the menu. | |
534 * @param {Event} event Event. | |
535 */ | |
536 ShareMode.prototype.toggle = function(event) { | |
537 this.editor_.enterMode(this, event); | |
538 }; | |
539 | |
540 /** | |
541 * Update available actions list based on the currently selected urls. | |
542 * | |
543 * @param {Array.<string>} urls Array of urls. | |
544 */ | |
545 ShareMode.prototype.updateMenu = function(urls) { | |
dgozman
2012/08/17 07:11:03
You can use new FileTasks(urls).getExternals(callb
Vladislav Kaznacheev
2012/08/17 09:20:40
My idea is to get rid of FileTasks.getExternals co
| |
546 var internalId = util.getExtensionId(); | |
547 function isShareAction(task) { | |
548 var task_parts = task.taskId.split('|'); | |
549 return task_parts[0] != internalId; | |
550 } | |
551 | |
552 var items = this.menu_.querySelectorAll('.item'); | |
553 for (var i = 0; i != items.length; i++) { | |
554 items[i].parentNode.removeChild(items[i]); | |
555 } | |
556 | |
557 var api = Gallery.getFileBrowserPrivate(); | |
558 api.getFileTasks(urls, function(tasks) { | |
559 for (var i = 0; i != tasks.length; i++) { | |
560 var task = tasks[i]; | |
561 if (!isShareAction(task)) continue; | |
562 | |
563 var item = document.createElement('div'); | |
564 item.className = 'item'; | |
565 this.menu_.appendChild(item); | |
566 | |
567 item.textContent = task.title; | |
568 item.style.backgroundImage = 'url(' + task.iconUrl + ')'; | |
569 item.addEventListener('click', this.actionCallback_.bind(null, | |
570 api.executeTask.bind(api, task.taskId, urls))); | |
571 } | |
572 | |
573 if (this.menu_.firstChild) | |
574 this.button_.removeAttribute('disabled'); | |
575 else | |
576 this.button_.setAttribute('disabled', 'true'); | |
577 }.bind(this)); | |
578 }; | |
OLD | NEW |