mirror of
https://github.com/flynx/ImageGrid.git
synced 2025-10-28 18:00:09 +00:00
614 lines
16 KiB
JavaScript
Executable File
614 lines
16 KiB
JavaScript
Executable File
/**********************************************************************
|
|
*
|
|
*
|
|
*
|
|
**********************************************************************/
|
|
|
|
((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define)
|
|
(function(require){ var module={} // make module AMD/node compatible...
|
|
/*********************************************************************/
|
|
|
|
// XXX this is a hack...
|
|
// ...need a way to escape these so as not to load them in browser...
|
|
if(typeof(process) != 'undefined'){
|
|
var fs = requirejs('fs')
|
|
var path = requirejs('path')
|
|
var exiftool = requirejs('exiftool')
|
|
}
|
|
|
|
|
|
var util = require('lib/util')
|
|
var toggler = require('lib/toggler')
|
|
var tasks = require('lib/tasks')
|
|
var keyboard = require('lib/keyboard')
|
|
|
|
var actions = require('lib/actions')
|
|
var core = require('features/core')
|
|
var base = require('features/base')
|
|
var widgets = require('features/ui-widgets')
|
|
|
|
var browse = require('lib/widget/browse')
|
|
var overlay = require('lib/widget/overlay')
|
|
|
|
|
|
|
|
/*********************************************************************/
|
|
// XXX make metadata a prop of image... (???)
|
|
// XXX Q: should we standardise metadata field names and adapt them to
|
|
// lib???
|
|
|
|
var MetadataActions = actions.Actions({
|
|
// XXX
|
|
metadata: ['- Image/',
|
|
function(target, data){
|
|
// XXX
|
|
}],
|
|
|
|
getMetadata: ['- Image/Get metadata data',
|
|
function(image){
|
|
var gid = this.data.getImage(image)
|
|
|
|
if(this.images && this.images[gid]){
|
|
return this.images[gid].metadata || {}
|
|
}
|
|
return null
|
|
}],
|
|
setMetadata: ['- Image/Set metadata data',
|
|
function(image, metadata, merge){
|
|
var that = this
|
|
var gid = this.data.getImage(image)
|
|
|
|
if(this.images && this.images[gid]){
|
|
if(merge){
|
|
var m = this.images[gid].metadata
|
|
Object.keys(metadata).forEach(function(k){
|
|
m[k] = metadata[k]
|
|
})
|
|
|
|
} else {
|
|
this.images[gid].metadata = metadata
|
|
}
|
|
}
|
|
}]
|
|
})
|
|
|
|
var Metadata =
|
|
module.Metadata = core.ImageGridFeatures.Feature({
|
|
title: '',
|
|
doc: '',
|
|
|
|
tag: 'metadata',
|
|
depends: [
|
|
'base',
|
|
],
|
|
suggested: [
|
|
'fs-metadata',
|
|
'ui-metadata',
|
|
'ui-fs-metadata',
|
|
],
|
|
|
|
actions: MetadataActions,
|
|
})
|
|
|
|
|
|
//---------------------------------------------------------------------
|
|
// Metadata reader/writer...
|
|
|
|
|
|
// XXX add Metadata writer...
|
|
var MetadataReaderActions = actions.Actions({
|
|
// NOTE: this will read both stat and metadata...
|
|
//
|
|
// XXX add support to taskqueue...
|
|
// XXX should this process multiple images???
|
|
// XXX also check the metadata/ folder (???)
|
|
// XXX this uses .markChanged(..) form filesystem.FileSystemWriter
|
|
// feature, but technically does not depend on it...
|
|
// XXX should we store metadata in an image (current) or in fs???
|
|
readMetadata: ['- Image/Get metadata data',
|
|
function(image, force){
|
|
var that = this
|
|
|
|
var gid = this.data.getImage(image)
|
|
var img = this.images && this.images[gid]
|
|
|
|
if(!image && !img){
|
|
return false
|
|
}
|
|
|
|
//var full_path = path.normalize(img.base_path +'/'+ img.path)
|
|
var full_path = this.getImagePath(gid)
|
|
|
|
return new Promise(function(resolve, reject){
|
|
if(!force && img.metadata){
|
|
return resolve(img.metadata)
|
|
}
|
|
|
|
fs.readFile(full_path, function(err, file){
|
|
if(err){
|
|
return reject(err)
|
|
}
|
|
|
|
// read stat...
|
|
if(!that.images[gid].birthtime){
|
|
var img = that.images[gid]
|
|
var stat = fs.statSync(full_path)
|
|
|
|
img.atime = stat.atime
|
|
img.mtime = stat.mtime
|
|
img.ctime = stat.ctime
|
|
img.birthtime = stat.birthtime
|
|
|
|
img.size = stat.size
|
|
}
|
|
|
|
// read image metadata...
|
|
exiftool.metadata(file, function(err, data){
|
|
if(err){
|
|
reject(err)
|
|
|
|
} else if(data.error){
|
|
reject(data)
|
|
|
|
} else {
|
|
// store metadata...
|
|
// XXX
|
|
|
|
// convert to a real dict...
|
|
// NOTE: exiftool appears to return an array
|
|
// object rather than an actual dict/object
|
|
// and that is not JSON compatible....
|
|
var m = {}
|
|
Object.keys(data).forEach(function(k){ m[k] = data[k] })
|
|
|
|
that.images[gid].metadata = m
|
|
|
|
// XXX
|
|
that.markChanged && that.markChanged('images', [gid])
|
|
}
|
|
|
|
resolve(data)
|
|
})
|
|
})
|
|
})
|
|
}],
|
|
|
|
// XXX STUB: add support for this to .readMetadata(..)
|
|
readAllMetadata: ['File/Read all metadata',
|
|
function(){
|
|
var that = this
|
|
// XXX make this a global API...
|
|
var q = this.__reader_queue = this.__reader_queue || tasks.Queue()
|
|
|
|
var logger = this.logger && this.logger.push('Read metadata')
|
|
|
|
// XXX is this the right way to go???
|
|
q.on('taskQueued', function(t){ logger.emit('queued', t) })
|
|
q.on('taskDone', function(t){ logger.emit('done', t) })
|
|
q.on('taskFailed', function(t){ logger.emit('error', t) })
|
|
|
|
var read = function(gid){
|
|
return function(){ return that.readMetadata(gid) } }
|
|
|
|
q.start()
|
|
|
|
this.images
|
|
&& this.images.forEach(function(gid){
|
|
q.enqueue('metadata', read(gid))
|
|
})
|
|
|
|
return q
|
|
}],
|
|
|
|
// XXX take image Metadata and write it to target...
|
|
writeMetadata: ['- Image/Set metadata data',
|
|
function(image, target){
|
|
// XXX
|
|
}]
|
|
})
|
|
|
|
var MetadataReader =
|
|
module.MetadataReader = core.ImageGridFeatures.Feature({
|
|
title: '',
|
|
doc: '',
|
|
|
|
tag: 'fs-metadata',
|
|
depends: [
|
|
'fs-info',
|
|
'metadata',
|
|
],
|
|
|
|
isApplicable: function(){ return this.runtime.node },
|
|
|
|
actions: MetadataReaderActions,
|
|
|
|
handlers: [
|
|
// XXX STUB: need a better strategy to read metadata...
|
|
// Approach 1 (target):
|
|
// read the metadata on demand e.g. on .showMetadata(..)
|
|
// + natural approach
|
|
// - not sync
|
|
// really complicated to organize...
|
|
//
|
|
// Approach 2:
|
|
// lazy read -- timeout and overwrite on next image
|
|
// - hack-ish
|
|
// + simple
|
|
//
|
|
// Approach 3:
|
|
// index a dir
|
|
/*
|
|
['focusImage',
|
|
function(){
|
|
var gid = this.current
|
|
metadata = this.images && this.images[gid] && this.images[gid].metadata
|
|
metadata = metadata && (Object.keys(metadata).length > 0)
|
|
|
|
if(!metadata){
|
|
this.readMetadata(gid)
|
|
}
|
|
}]
|
|
*/
|
|
],
|
|
})
|
|
|
|
|
|
|
|
//---------------------------------------------------------------------
|
|
// Metadata editor/viewer...
|
|
//
|
|
// NOTE: this is by-design platform independent...
|
|
//
|
|
// XXX first instinct is to use browse with editable fields as it will
|
|
// give us:
|
|
// - searchability
|
|
// - navigation
|
|
// - ...
|
|
// missing functionality:
|
|
// - editor/form on open event
|
|
// - inline (preferred)
|
|
// - modal-form
|
|
// - table-like layout
|
|
// - template???
|
|
// - script layout tweaking (post-update)
|
|
//
|
|
// ...need to think about this...
|
|
|
|
// XXX add ability to manually sort fields -- moving a field up/down
|
|
// edits .config...
|
|
// ...not sure how to go about this yet...
|
|
// XXX add combined fields...
|
|
// 'Make' + 'Camera Model Name'
|
|
// XXX add identical fields -- show first available and hide the rest...
|
|
// 'Shutter Speed', 'Exposure Time',
|
|
// 'Lens ID', 'Lens'
|
|
// XXX add field editing... (open)
|
|
// XXX might be good to split this to sections...
|
|
// - base info
|
|
// - general metadata
|
|
// - full metadata
|
|
// - EXIF
|
|
// - IPTC
|
|
// - ...
|
|
var MetadataUIActions = actions.Actions({
|
|
config: {
|
|
'metadata-preview-size': 150,
|
|
|
|
'metadata-auto-select-modes': [
|
|
'none',
|
|
'on select',
|
|
'on open',
|
|
],
|
|
'metadata-auto-select-mode': 'on select',
|
|
|
|
// XXX
|
|
'metadata-editable-fields': [
|
|
//'Artist',
|
|
//'Copyright',
|
|
'Comment',
|
|
'Tags',
|
|
],
|
|
'metadata-field-order': [
|
|
// base
|
|
'GID',
|
|
'File Name', 'Parent Directory', 'Full Path',
|
|
|
|
'Date created', 'ctime', 'mtime', 'atime',
|
|
|
|
'Index (ribbon)', 'Index (crop)', 'Index (global)',
|
|
|
|
// metadata...
|
|
'Make', 'Camera Model Name', 'Lens ID', 'Lens', 'Lens Profile Name', 'Focal Length',
|
|
|
|
'Metering Mode', 'Exposure Program', 'Exposure Compensation',
|
|
'Shutter Speed Value', 'Exposure Time',
|
|
'Aperture Value', 'F Number',
|
|
'Iso',
|
|
'Quality', 'Focus Mode',
|
|
|
|
'Artist', 'Copyright',
|
|
|
|
'Date/time Original', 'Create Date', 'Modify Date',
|
|
|
|
'Mime Type',
|
|
],
|
|
},
|
|
|
|
toggleMetadataAutoSelect: ['Interface/Metadata value auto-select',
|
|
core.makeConfigToggler('metadata-auto-select-mode',
|
|
function(){ return this.config['metadata-auto-select-modes'] })],
|
|
|
|
// NOTE: this will extend the Browse object with .updateMetadata(..)
|
|
// method to enable updating of metadata in the list...
|
|
//
|
|
// XXX should we replace 'mode' with nested set of metadata???
|
|
// XXX make this support multiple images...
|
|
// XXX make things editable only in when edit is loaded...
|
|
// XXX BUG: .dialog.updatePreview() is stealing marks from
|
|
// the original image in ribbon...
|
|
// ...see inside...
|
|
showMetadata: ['Image/Metadata...',
|
|
widgets.makeUIDialog(function(image, mode){
|
|
var that = this
|
|
image = this.data.getImage(image)
|
|
mode = mode || 'disabled'
|
|
data = this.images[image]
|
|
|
|
var preview_size = this.config['metadata-preview-size'] || 150
|
|
|
|
var _normalize = typeof(path) != 'undefined' ?
|
|
path.normalize
|
|
: function(e){ return e.replace(/\/\.\//, '') }
|
|
|
|
return browse.makeLister(null,
|
|
function(p, make){
|
|
// helper...
|
|
// NOTE: we intentionally rewrite this on each update,
|
|
// this is done to keep the ref to make(..) up-to-date...
|
|
make.dialog.wait = function(){
|
|
make.Separator()
|
|
make.Spinner() }
|
|
// XXX BUG: this when attached is stealing marks from
|
|
// the original image in ribbon...
|
|
make.dialog.updatePreview = function(){
|
|
var preview = this.preview = this.preview || that.ribbons.createImage(image)
|
|
return that.ribbons.updateImage(preview, image, preview_size, false, {
|
|
nochrome: true,
|
|
pre_updaters_callback: function([p]){
|
|
p.classList.add('clone', 'preview')
|
|
p.style.height = preview_size +'px'
|
|
p.style.width = preview_size +'px'
|
|
},
|
|
}) }
|
|
|
|
// preview...
|
|
make(['Preview:', this.updatePreview()],
|
|
{ cls: 'preview' })
|
|
make.Separator()
|
|
|
|
// essentials...
|
|
make(['$GID: ', image])
|
|
// NOTE: these are 1-based and not 0-based...
|
|
make(['Index: ',
|
|
// ribbon...
|
|
that.data.getImageOrder('ribbon', image) + 1
|
|
+'/'+
|
|
that.data.getImages(image).len
|
|
+ 'R',
|
|
// crop...
|
|
...((that.crop_stack && that.crop_stack.len > 0) ?
|
|
[that.data.getImageOrder('loaded', image) + 1
|
|
+'/'+
|
|
that.data.getImages('loaded').len
|
|
+ 'C']
|
|
: []),
|
|
// global...
|
|
that.data.getImageOrder(image) + 1
|
|
+'/'+
|
|
that.data.getImages('all').len
|
|
+ 'G', ])
|
|
make(['Ribbon:',
|
|
that.data.getRibbonOrder(image) + 1
|
|
+'/'+
|
|
Object.keys(that.data.ribbons).length])
|
|
|
|
make.Separator()
|
|
|
|
if(data){
|
|
// some abstractions...
|
|
var _basename = typeof(path) != 'undefined' ?
|
|
path.basename
|
|
: function(e){ return e.split(/[\\\/]/g).pop() }
|
|
var _dirname = typeof(path) != 'undefined' ?
|
|
function(e){ return path.normalize(path.dirname(e)) }
|
|
: function(e){
|
|
return _normalize(e.split(/[\\\/]/g).slice(0, -1).join('/')) }
|
|
|
|
// paths...
|
|
data.path
|
|
&& make(['File $Name: ',
|
|
_basename(data.path)])
|
|
&& make(['Parent $Directory: ',
|
|
_dirname((data.base_path || '.') +'/'+ data.path)])
|
|
&& make(['Full $Path: ',
|
|
_normalize((data.base_path || '.') +'/'+ data.path)])
|
|
// times...
|
|
data.birthtime
|
|
&& make(['Date created: ',
|
|
data.birthtime && new Date(data.birthtime).toShortDate()])
|
|
data.ctime
|
|
&& make(['- ctime: ',
|
|
data.ctime && new Date(data.ctime).toShortDate()],
|
|
{disabled: true})
|
|
data.mtime
|
|
&& make(['- mtime: ',
|
|
data.mtime && new Date(data.mtime).toShortDate()],
|
|
{disabled: true})
|
|
data.atime
|
|
&& make(['- atime: ',
|
|
data.atime && new Date(data.atime).toShortDate()],
|
|
{disabled: true})
|
|
}
|
|
|
|
// comment...
|
|
make.Editable(['$Comment: ',
|
|
function(){
|
|
return data && data.comment || '' }],
|
|
{
|
|
start_on: 'open',
|
|
edit_text: 'last',
|
|
multiline: true,
|
|
reset_on_commit: false,
|
|
editdone: function(evt, value){
|
|
if(value.trim() == ''){
|
|
return }
|
|
data = that.images[image] = that.images[image] || {}
|
|
data.comment = value
|
|
// mark image as changed...
|
|
that.markChanged
|
|
&& that.markChanged('images', [image])
|
|
},
|
|
})
|
|
|
|
// get other sections...
|
|
that.callSortedAction('metadataSection', make, image, data, mode)
|
|
}, {
|
|
cls: 'table-view metadata-view',
|
|
showDisabled: false,
|
|
})
|
|
.on('attached', function(){
|
|
this.updatePreview() })
|
|
// select value of current item...
|
|
.on('select', function(evt, elem){
|
|
that.config['metadata-auto-select-mode'] == 'on select'
|
|
&& $(elem).find('.text').last().selectText() })
|
|
.close(function(){
|
|
// XXX handle comment and tag changes...
|
|
// XXX
|
|
|
|
that.refresh(image)
|
|
})
|
|
})],
|
|
|
|
metadataSection: ['- Image/',
|
|
{ sortedActionPriority: 'normal' },
|
|
core.notUserCallable(function(make, gid, image, mode){
|
|
var that = this
|
|
var metadata = this.getMetadata(gid) || {}
|
|
var field_order = this.config['metadata-field-order'] || []
|
|
var x = field_order.length + 1
|
|
|
|
make.dialog.updateMetadata =
|
|
function(metadata){
|
|
metadata = metadata || that.getMetadata()
|
|
|
|
// build new data set and update view...
|
|
//this.options.data = _buildInfoList(image, metadata)
|
|
this.update()
|
|
|
|
return this
|
|
}
|
|
|
|
// build fields...
|
|
var fields = []
|
|
Object.keys(metadata)
|
|
.forEach(function(k){
|
|
var n = k
|
|
// convert camel-case to human-case ;)
|
|
.replace(/([A-Z]+)/g, ' $1')
|
|
.capitalize()
|
|
var opts = {}
|
|
|
|
// skip metadata stuff in short mode...
|
|
if(mode != 'full'
|
|
&& field_order.indexOf(n) == -1){
|
|
if(mode == 'short'){
|
|
return
|
|
|
|
} else if(mode == 'disabled') {
|
|
opts.disabled = true
|
|
}
|
|
}
|
|
|
|
fields.push([
|
|
[ n + ': ', metadata[k] ],
|
|
opts,
|
|
])
|
|
})
|
|
|
|
// make fields...
|
|
fields
|
|
.sort(function(a, b){
|
|
a = field_order.indexOf(a[0][0]
|
|
.replace(/\$(\w)/g, '$1')
|
|
.replace(/^- |: $/g, ''))
|
|
a = a == -1 ? x : a
|
|
b = field_order.indexOf(b[0][0]
|
|
.replace(/\$(\w)/g, '$1')
|
|
.replace(/^- |: $/g, ''))
|
|
b = b == -1 ? x : b
|
|
return a - b })
|
|
.run(function(){
|
|
this.length > 0
|
|
&& make.Separator() })
|
|
.forEach(function(e){
|
|
make(...e) })
|
|
})],
|
|
})
|
|
|
|
var MetadataUI =
|
|
module.MetadataUI = core.ImageGridFeatures.Feature({
|
|
title: '',
|
|
doc: '',
|
|
|
|
tag: 'ui-metadata',
|
|
depends: [
|
|
'ui',
|
|
'metadata',
|
|
],
|
|
|
|
actions: MetadataUIActions,
|
|
})
|
|
|
|
|
|
|
|
// Load etdata on demand...
|
|
//
|
|
var MetadataFSUI =
|
|
module.MetadataFSUI = core.ImageGridFeatures.Feature({
|
|
title: '',
|
|
doc: '',
|
|
|
|
tag: 'ui-fs-metadata',
|
|
depends: [
|
|
'ui',
|
|
'metadata',
|
|
'fs-metadata',
|
|
],
|
|
|
|
handlers: [
|
|
// Read metadata and when done update the list...
|
|
['showMetadata.pre',
|
|
function(image){
|
|
var that = this
|
|
var reader = this.readMetadata(image)
|
|
|
|
return reader
|
|
&& function(client){
|
|
// add a loading indicator...
|
|
// NOTE: this will get overwritten when calling .updateMetadata()
|
|
client.wait()
|
|
|
|
reader
|
|
.then(function(data){
|
|
client.updateMetadata() })
|
|
.catch(function(){
|
|
client.update() })
|
|
} }],
|
|
],
|
|
})
|
|
|
|
|
|
/**********************************************************************
|
|
* vim:set ts=4 sw=4 : */ return module })
|