Update To 11.40.268.0
[platform/framework/web/crosswalk.git] / src / ui / file_manager / gallery / js / gallery_item.js
1 // Copyright 2014 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 /**
6  * Object representing an image item (a photo).
7  *
8  * @param {FileEntry} entry Image entry.
9  * @param {EntryLocation} locationInfo Entry location information.
10  * @param {Object} metadata Metadata for the entry.
11  * @param {MetadataCache} metadataCache Metadata cache instance.
12  * @param {boolean} original Whether the entry is original or edited.
13  * @constructor
14  */
15 Gallery.Item = function(
16     entry, locationInfo, metadata, metadataCache, original) {
17   /**
18    * @type {FileEntry}
19    * @private
20    */
21   this.entry_ = entry;
22
23   /**
24    * @type {EntryLocation}
25    * @private
26    */
27   this.locationInfo_ = locationInfo;
28
29   /**
30    * @type {Object}
31    * @private
32    */
33   this.metadata_ = Object.freeze(metadata);
34
35   /**
36    * @type {MetadataCache}
37    * @private
38    */
39   this.metadataCache_ = metadataCache;
40
41   /**
42    * The content cache is used for prefetching the next image when going through
43    * the images sequentially. The real life photos can be large (18Mpix = 72Mb
44    * pixel array) so we want only the minimum amount of caching.
45    * @type {HTMLCanvasElement}
46    */
47   this.screenImage = null;
48
49   /**
50    * We reuse previously generated screen-scale images so that going back to a
51    * recently loaded image looks instant even if the image is not in the content
52    * cache any more. Screen-scale images are small (~1Mpix) so we can afford to
53    * cache more of them.
54    * @type {HTMLCanvasElement}
55    */
56   this.contentImage = null;
57
58   /**
59    * Last accessed date to be used for selecting items whose cache are evicted.
60    * @type {number}
61    * @private
62    */
63   this.lastAccessed_ = Date.now();
64
65   /**
66    * @type {boolean}
67    * @private
68    */
69   this.original_ = original;
70
71   Object.seal(this);
72 };
73
74 /**
75  * @return {FileEntry} Image entry.
76  */
77 Gallery.Item.prototype.getEntry = function() { return this.entry_; };
78
79 /**
80  * @return {EntryLocation} Entry location information.
81  */
82 Gallery.Item.prototype.getLocationInfo = function() {
83   return this.locationInfo_;
84 };
85
86 /**
87  * @return {Object} Metadata.
88  */
89 Gallery.Item.prototype.getMetadata = function() { return this.metadata_; };
90
91 /**
92  * Obtains the latest media metadata.
93  *
94  * This is a heavy operation since it forces to load the image data to obtain
95  * the metadata.
96  * @return {Promise} Promise to be fulfilled with fetched metadata.
97  */
98 Gallery.Item.prototype.getFetchedMedia = function() {
99   return new Promise(function(fulfill, reject) {
100     this.metadataCache_.getLatest(
101         [this.entry_],
102         'fetchedMedia',
103         function(metadata) {
104           if (metadata[0])
105             fulfill(metadata[0]);
106           else
107             reject('Failed to load metadata.');
108         });
109   }.bind(this));
110 };
111
112 /**
113  * Sets the metadata.
114  * @param {Object} metadata New metadata.
115  */
116 Gallery.Item.prototype.setMetadata = function(metadata) {
117   this.metadata_ = Object.freeze(metadata);
118 };
119
120 /**
121  * @return {string} File name.
122  */
123 Gallery.Item.prototype.getFileName = function() {
124   return this.entry_.name;
125 };
126
127 /**
128  * @return {boolean} True if this image has not been created in this session.
129  */
130 Gallery.Item.prototype.isOriginal = function() { return this.original_; };
131
132 /**
133  * Obtains the last accessed date.
134  * @return {number} Last accessed date.
135  */
136 Gallery.Item.prototype.getLastAccessedDate = function() {
137   return this.lastAccessed_;
138 };
139
140 /**
141  * Updates the last accessed date.
142  */
143 Gallery.Item.prototype.touch = function() {
144   this.lastAccessed_ = Date.now();
145 };
146
147 // TODO: Localize?
148 /**
149  * @type {string} Suffix for a edited copy file name.
150  */
151 Gallery.Item.COPY_SIGNATURE = ' - Edited';
152
153 /**
154  * Regular expression to match '... - Edited'.
155  * @type {RegExp}
156  */
157 Gallery.Item.REGEXP_COPY_0 =
158     new RegExp('^(.+)' + Gallery.Item.COPY_SIGNATURE + '$');
159
160 /**
161  * Regular expression to match '... - Edited (N)'.
162  * @type {RegExp}
163  */
164 Gallery.Item.REGEXP_COPY_N =
165     new RegExp('^(.+)' + Gallery.Item.COPY_SIGNATURE + ' \\((\\d+)\\)$');
166
167 /**
168  * Creates a name for an edited copy of the file.
169  *
170  * @param {DirectoryEntry} dirEntry Entry.
171  * @param {function(string)} callback Callback.
172  * @private
173  */
174 Gallery.Item.prototype.createCopyName_ = function(dirEntry, callback) {
175   var name = this.getFileName();
176
177   // If the item represents a file created during the current Gallery session
178   // we reuse it for subsequent saves instead of creating multiple copies.
179   if (!this.original_) {
180     callback(name);
181     return;
182   }
183
184   var ext = '';
185   var index = name.lastIndexOf('.');
186   if (index != -1) {
187     ext = name.substr(index);
188     name = name.substr(0, index);
189   }
190
191   if (!ext.match(/jpe?g/i)) {
192     // Chrome can natively encode only two formats: JPEG and PNG.
193     // All non-JPEG images are saved in PNG, hence forcing the file extension.
194     ext = '.png';
195   }
196
197   function tryNext(tries) {
198     // All the names are used. Let's overwrite the last one.
199     if (tries == 0) {
200       setTimeout(callback, 0, name + ext);
201       return;
202     }
203
204     // If the file name contains the copy signature add/advance the sequential
205     // number.
206     var matchN = Gallery.Item.REGEXP_COPY_N.exec(name);
207     var match0 = Gallery.Item.REGEXP_COPY_0.exec(name);
208     if (matchN && matchN[1] && matchN[2]) {
209       var copyNumber = parseInt(matchN[2], 10) + 1;
210       name = matchN[1] + Gallery.Item.COPY_SIGNATURE + ' (' + copyNumber + ')';
211     } else if (match0 && match0[1]) {
212       name = match0[1] + Gallery.Item.COPY_SIGNATURE + ' (1)';
213     } else {
214       name += Gallery.Item.COPY_SIGNATURE;
215     }
216
217     dirEntry.getFile(name + ext, {create: false, exclusive: false},
218         tryNext.bind(null, tries - 1),
219         callback.bind(null, name + ext));
220   }
221
222   tryNext(10);
223 };
224
225 /**
226  * Writes the new item content to either the existing or a new file.
227  *
228  * @param {VolumeManager} volumeManager Volume manager instance.
229  * @param {string} fallbackDir Fallback directory in case the current directory
230  *     is read only.
231  * @param {boolean} overwrite Whether to overwrite the image to the item or not.
232  * @param {HTMLCanvasElement} canvas Source canvas.
233  * @param {ImageEncoder.MetadataEncoder} metadataEncoder MetadataEncoder.
234  * @param {function(boolean)=} opt_callback Callback accepting true for success.
235  */
236 Gallery.Item.prototype.saveToFile = function(
237     volumeManager, fallbackDir, overwrite, canvas, metadataEncoder,
238     opt_callback) {
239   ImageUtil.metrics.startInterval(ImageUtil.getMetricName('SaveTime'));
240
241   var name = this.getFileName();
242
243   var onSuccess = function(entry, locationInfo) {
244     ImageUtil.metrics.recordEnum(ImageUtil.getMetricName('SaveResult'), 1, 2);
245     ImageUtil.metrics.recordInterval(ImageUtil.getMetricName('SaveTime'));
246
247     this.entry_ = entry;
248     this.locationInfo_ = locationInfo;
249
250     this.metadataCache_.clear([this.entry_], 'fetchedMedia');
251     if (opt_callback)
252       opt_callback(true);
253   }.bind(this);
254
255   var onError = function(error) {
256     console.error('Error saving from gallery', name, error);
257     ImageUtil.metrics.recordEnum(ImageUtil.getMetricName('SaveResult'), 0, 2);
258     if (opt_callback)
259       opt_callback(false);
260   }
261
262   var doSave = function(newFile, fileEntry) {
263     fileEntry.createWriter(function(fileWriter) {
264       function writeContent() {
265         fileWriter.onwriteend = onSuccess.bind(null, fileEntry);
266         fileWriter.write(ImageEncoder.getBlob(canvas, metadataEncoder));
267       }
268       fileWriter.onerror = function(error) {
269         onError(error);
270         // Disable all callbacks on the first error.
271         fileWriter.onerror = null;
272         fileWriter.onwriteend = null;
273       };
274       if (newFile) {
275         writeContent();
276       } else {
277         fileWriter.onwriteend = writeContent;
278         fileWriter.truncate(0);
279       }
280     }, onError);
281   }
282
283   var getFile = function(dir, newFile) {
284     dir.getFile(name, {create: newFile, exclusive: newFile},
285         function(fileEntry) {
286           var locationInfo = volumeManager.getLocationInfo(fileEntry);
287           // If the volume is gone, then abort the saving operation.
288           if (!locationInfo) {
289             onError('NotFound');
290             return;
291           }
292           doSave(newFile, fileEntry, locationInfo);
293         }.bind(this), onError);
294   }.bind(this);
295
296   var checkExistence = function(dir) {
297     dir.getFile(name, {create: false, exclusive: false},
298         getFile.bind(null, dir, false /* existing file */),
299         getFile.bind(null, dir, true /* create new file */));
300   }
301
302   var saveToDir = function(dir) {
303     if (overwrite && !this.locationInfo_.isReadOnly) {
304       checkExistence(dir);
305     } else {
306       this.createCopyName_(dir, function(copyName) {
307         this.original_ = false;
308         name = copyName;
309         checkExistence(dir);
310       }.bind(this));
311     }
312   }.bind(this);
313
314   if (this.locationInfo_.isReadOnly) {
315     saveToDir(fallbackDir);
316   } else {
317     this.entry_.getParent(saveToDir, onError);
318   }
319 };
320
321 /**
322  * Renames the item.
323  *
324  * @param {string} displayName New display name (without the extension).
325  * @return {Promise} Promise fulfilled with when renaming completes, or rejected
326  *     with the error message.
327  */
328 Gallery.Item.prototype.rename = function(displayName) {
329   var newFileName = this.entry_.name.replace(
330       ImageUtil.getDisplayNameFromName(this.entry_.name), displayName);
331
332   if (newFileName === this.entry_.name)
333     return Promise.reject('NOT_CHANGED');
334
335   if (/^\s*$/.test(displayName))
336     return Promise.reject(str('ERROR_WHITESPACE_NAME'));
337
338   var parentDirectoryPromise = new Promise(
339       this.entry_.getParent.bind(this.entry_));
340   return parentDirectoryPromise.then(function(parentDirectory) {
341     var nameValidatingPromise =
342         util.validateFileName(parentDirectory, newFileName, true);
343     return nameValidatingPromise.then(function() {
344       var existingFilePromise = new Promise(parentDirectory.getFile.bind(
345           parentDirectory, newFileName, {create: false, exclusive: false}));
346       return existingFilePromise.then(function() {
347         return Promise.reject(str('GALLERY_FILE_EXISTS'));
348       }, function() {
349         return new Promise(
350             this.entry_.moveTo.bind(this.entry_, parentDirectory, newFileName));
351       }.bind(this));
352     }.bind(this));
353   }.bind(this)).then(function(entry) {
354     this.entry_ = entry;
355   }.bind(this));
356 };