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