mirror of
				https://github.com/flynx/ImageGrid.git
				synced 2025-11-03 21:00:14 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			771 lines
		
	
	
		
			21 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			771 lines
		
	
	
		
			21 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')
 | 
						|
 | 
						|
	// XXX EXPERIMENTAL: graph...
 | 
						|
	// do this only if browser is loaded...
 | 
						|
	var graph = typeof(window) != 'undefined' ?
 | 
						|
		requirejs('lib/components/ig-image-graph')
 | 
						|
		: null
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
var util = require('lib/util')
 | 
						|
var toggler = require('lib/toggler')
 | 
						|
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 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???
 | 
						|
	// XXX should this set .orientation / .flipped if they are not set???
 | 
						|
	readMetadata: ['- Image/Get metadata data',
 | 
						|
		core.doc`
 | 
						|
 | 
						|
		This will overwrite/update if:
 | 
						|
			- image .metadata is not set
 | 
						|
			- image .metadata.ImageGridMetadata is not 'full'
 | 
						|
			- force is true
 | 
						|
 | 
						|
 | 
						|
		NOTE: also see: .cacheMetadata(..)
 | 
						|
		`,
 | 
						|
		core.sessionQueueHandler('Read image metadata', 
 | 
						|
			{quiet: true}, 
 | 
						|
			function(queue, image, force){
 | 
						|
				return [
 | 
						|
					image == 'all' ?
 | 
						|
							this.images.keys()
 | 
						|
						: image == 'loaded' ?
 | 
						|
							this.data.getImages('loaded')
 | 
						|
						: image,
 | 
						|
					force, 
 | 
						|
				] },
 | 
						|
			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 || {}).ImageGridMetadata == 'full'){
 | 
						|
						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 {
 | 
						|
								// 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....
 | 
						|
								that.images[gid].metadata =
 | 
						|
									Object.assign(
 | 
						|
										// XXX do we need to update or overwrite??
 | 
						|
										that.images[gid].metadata || {},
 | 
						|
										data,
 | 
						|
										{
 | 
						|
											ImageGridMetadataReader: 'exiftool/ImageGrid',
 | 
						|
											// mark metadata as full read...
 | 
						|
											ImageGridMetadata: 'full',
 | 
						|
										})
 | 
						|
								that.markChanged 
 | 
						|
									&& that.markChanged('images', [gid]) }
 | 
						|
 | 
						|
							resolve(data) }) }) }) })],
 | 
						|
	readAllMetadata: ['File/Read all metadata',
 | 
						|
		'readMetadata: "all" ...'],
 | 
						|
 | 
						|
	// XXX take image Metadata and write it to target...
 | 
						|
	writeMetadata: ['- Image/Set metadata data',
 | 
						|
		function(image, target){
 | 
						|
			// XXX
 | 
						|
		}],
 | 
						|
 | 
						|
 | 
						|
	// XXX add undo...
 | 
						|
	ratingToRibbons: ['- Ribbon|Crop/',
 | 
						|
		core.doc`Place images to ribbons by rating
 | 
						|
 | 
						|
			Place images to ribbons by rating in a crop...
 | 
						|
			.ratingToRibbons()
 | 
						|
			.ratingToRibbons('crop')
 | 
						|
 | 
						|
			Place images to ribbons by rating (in-place)...
 | 
						|
			.ratingToRibbons('in-place')
 | 
						|
 | 
						|
 | 
						|
		NOTE: this will override the current ribbon structure.
 | 
						|
		NOTE: this needs .metadata to be loaded.
 | 
						|
		NOTE: we do not care about the actual rating values or their 
 | 
						|
			number, the number of ribbons corresponds to number of used 
 | 
						|
			ratings and thy are sorted by their text value.
 | 
						|
		`,
 | 
						|
		function(mode='crop'){
 | 
						|
			var that = this
 | 
						|
			var images = this.images
 | 
						|
			var index = {}
 | 
						|
 | 
						|
			var ribbons = this.data.order
 | 
						|
				.reduce(function(ribbons, gid, i){
 | 
						|
					var r = ((images[gid] || {}).metadata || {}).rating || 0
 | 
						|
					// NOTE: we can't just use the rating as ribbon gid 
 | 
						|
					// 		because currently number-like ribbon gids 
 | 
						|
					// 		break things -- needs revision...
 | 
						|
					var g = index[r] = index[r] || that.data.newGID()
 | 
						|
					// NOTE: this will create sparse ribbons...
 | 
						|
					;(ribbons[g] = (ribbons[g] || []))[i] = gid
 | 
						|
					return ribbons }, {})
 | 
						|
 | 
						|
			// build the new data...
 | 
						|
			var data = mode == 'in-place' ?
 | 
						|
				this.data
 | 
						|
				: this.data.clone()
 | 
						|
			data.ribbons = ribbons
 | 
						|
			// sort by rating then replace with "gid"...
 | 
						|
			data.ribbon_order = Object.keys(index)
 | 
						|
				.sort()
 | 
						|
				.reverse()
 | 
						|
				.map(function(r){ 
 | 
						|
					return index[r] })
 | 
						|
			this.setBaseRibbon(data.ribbon_order.last())
 | 
						|
 | 
						|
			mode == 'in-place'
 | 
						|
				&& this.markChanged
 | 
						|
				&& this.markChanged('data')
 | 
						|
			mode == 'crop'
 | 
						|
				&& this.crop(data) }],
 | 
						|
 | 
						|
	// shorthands...
 | 
						|
	cropRatingsAsRibbons: ['Ribbon|Crop/Crop ratings to ribbons',
 | 
						|
		'ratingToRibbons: "crop"'],
 | 
						|
	splitRatingsAsRibbons: ['Ribbon/Split ratings to ribbons (in-place)',
 | 
						|
		'ratingToRibbons: "in-place"'],
 | 
						|
})
 | 
						|
 | 
						|
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 focus',
 | 
						|
			'on open',
 | 
						|
		],
 | 
						|
		'metadata-auto-select-mode': 'on focus',
 | 
						|
 | 
						|
		// 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', 
 | 
						|
 | 
						|
			'Rating',
 | 
						|
 | 
						|
			'Artist', 'Copyright',
 | 
						|
 | 
						|
			'Date/time Original', 'Create Date', 'Modify Date',
 | 
						|
 | 
						|
			'Mime Type',
 | 
						|
		],
 | 
						|
 | 
						|
		// XXX EXPERIMENTAL: graph...
 | 
						|
		'metadata-graph': 'on',
 | 
						|
		'metadata-graph-config': {
 | 
						|
			graph: 'waveform',
 | 
						|
			mode: 'luminance',
 | 
						|
		},
 | 
						|
	},
 | 
						|
 | 
						|
	toggleMetadataAutoSelect: ['Interface/Metadata value select',
 | 
						|
		core.makeConfigToggler('metadata-auto-select-mode', 
 | 
						|
			function(){ return this.config['metadata-auto-select-modes'] })],
 | 
						|
 | 
						|
	toggleMetadataGraph: ['Interface/Metadata graph display',
 | 
						|
		{ mode: function(){
 | 
						|
			return (!graph || this.config['browse-advanced-mode'] != 'on') && 'hidden' }},
 | 
						|
		core.makeConfigToggler('metadata-graph', ['on', 'off'])],
 | 
						|
 | 
						|
	// XXX show only graph...
 | 
						|
	// XXX might be a good idea to show directly in main menu...
 | 
						|
	// XXX reuse in .showMetadata(..)
 | 
						|
	// XXX should be floating...
 | 
						|
	showGraph: ['- Image/',
 | 
						|
		function(make){
 | 
						|
			// XXX
 | 
						|
		}],
 | 
						|
 | 
						|
	// 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'
 | 
						|
								},
 | 
						|
							}) }
 | 
						|
					// XXX EXPERIMENTAL: graph
 | 
						|
					// XXX do the calculations in a worker...
 | 
						|
					make.dialog.updateGraph = function(gid, size){
 | 
						|
						// prevent from updating too often...
 | 
						|
						if(this.__graph_updating){
 | 
						|
							// request an update...
 | 
						|
							this.__graph_updating = [gid, size]
 | 
						|
							return this.graph }
 | 
						|
						this.__graph_updating = true
 | 
						|
						setTimeout(function(){
 | 
						|
							// update was requested while we were out -> update now...
 | 
						|
							this.__graph_updating instanceof Array
 | 
						|
								&& this.updateGraph(...this.__graph_updating)
 | 
						|
							delete this.__graph_updating }.bind(this), 200)
 | 
						|
 | 
						|
						// graph disabled...
 | 
						|
						if(!graph || that.config['metadata-graph'] != 'on'){
 | 
						|
							return }
 | 
						|
 | 
						|
						// data...
 | 
						|
						gid = that.data.getImage(gid || 'current')
 | 
						|
						var config = that.config['metadata-graph-config'] || {}
 | 
						|
						var url = that.images[gid].preview ?
 | 
						|
							that.images.getBestPreview(gid, size || 300, null, true).url
 | 
						|
							: that.getImagePath(gid)
 | 
						|
						var flipped = (that.images[gid] || {}).flipped || []
 | 
						|
						flipped = flipped.length == 1 ? 
 | 
						|
								flipped[0]
 | 
						|
							: flipped.length == 2 ?
 | 
						|
								'both'
 | 
						|
							: null
 | 
						|
 | 
						|
						// build the element...
 | 
						|
						var elem = this.graph = 
 | 
						|
							Object.assign(
 | 
						|
								this.graph 
 | 
						|
									|| document.createElement('ig-image-graph'), 
 | 
						|
								config,
 | 
						|
								// orientation....
 | 
						|
								{
 | 
						|
									orientation: (that.images[gid] || {}).orientation || 0,
 | 
						|
									flipped: flipped, 
 | 
						|
								})
 | 
						|
						Object.assign(elem.style, {
 | 
						|
							width: '500px',
 | 
						|
							height: '200px',
 | 
						|
						})
 | 
						|
						// delay drawing a bit...
 | 
						|
						setTimeout(function(){
 | 
						|
							elem.src = url }, 0)
 | 
						|
						return elem }
 | 
						|
 | 
						|
					// preview...
 | 
						|
					make(['Preview:', this.updatePreview()], 
 | 
						|
						{ cls: 'preview' })
 | 
						|
					// XXX EXPERIMENTAL: graph
 | 
						|
					// graph...
 | 
						|
					graph 
 | 
						|
						&& that.config['metadata-graph'] == 'on'
 | 
						|
						&& make(['Graph:', $(this.updateGraph())], 
 | 
						|
							{ cls: 'preview' })
 | 
						|
					// NOTE: these are 1-based and not 0-based...
 | 
						|
					make(['Position: ', 
 | 
						|
						$('<small>')
 | 
						|
							.addClass('text')
 | 
						|
							.css({
 | 
						|
								whiteSpace: 'pre',
 | 
						|
							})
 | 
						|
							.html([
 | 
						|
								// ribbon...
 | 
						|
								that.data.getImageOrder('ribbon', image) + 1
 | 
						|
									+'/'+ 
 | 
						|
									that.data.getImages(image).len
 | 
						|
									+ '<small>R</small>',
 | 
						|
								...((that.crop_stack && that.crop_stack.len > 0) ?
 | 
						|
									// crop...
 | 
						|
									[that.data.getImageOrder('loaded', image) + 1
 | 
						|
										+'/'+ 
 | 
						|
										that.data.getImages('loaded').len
 | 
						|
										+ '<small>C</small>']
 | 
						|
									// global...
 | 
						|
									: [that.data.getImageOrder(image) + 1
 | 
						|
										+'/'+ 
 | 
						|
										that.data.getImages('all').len
 | 
						|
										+ '<small>G</small>']),
 | 
						|
								// ribbon...
 | 
						|
								'<span>R:</span>'+
 | 
						|
									(that.data.getRibbonOrder(image) + 1)
 | 
						|
									+'/'+
 | 
						|
									Object.keys(that.data.ribbons).length,
 | 
						|
							].join('   ')) ],
 | 
						|
						{ cls: 'index' })
 | 
						|
					make.Separator()
 | 
						|
 | 
						|
					// 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])
 | 
						|
							},
 | 
						|
						}) 
 | 
						|
					make.Separator()
 | 
						|
 | 
						|
					// gid...
 | 
						|
					make(['$GID: ', image])
 | 
						|
 | 
						|
 | 
						|
					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})
 | 
						|
					}
 | 
						|
 | 
						|
					// get other sections...
 | 
						|
					that.callSortedAction('metadataSection', make, image, data, mode)
 | 
						|
				}, {
 | 
						|
					cls: 'table-view metadata-view',
 | 
						|
					showDisabled: false,
 | 
						|
				})
 | 
						|
				.on('attached', function(){ 
 | 
						|
					this.updatePreview() 
 | 
						|
					graph
 | 
						|
						&& this.updateGraph()
 | 
						|
				})
 | 
						|
				.on('update', function(){ 
 | 
						|
					this.updatePreview() 
 | 
						|
					graph
 | 
						|
						&& this.updateGraph()
 | 
						|
				})
 | 
						|
				// select value of current item...
 | 
						|
				.on('select', function(evt, elem){
 | 
						|
					that.config['metadata-auto-select-mode'] == 'on focus'
 | 
						|
						&& $(elem).find('.text').last().selectText() })
 | 
						|
				.close(function(){
 | 
						|
					// XXX handle comment and tag changes...
 | 
						|
					// XXX
 | 
						|
 | 
						|
					that.refresh(image)
 | 
						|
 | 
						|
					// XXX EXPERIMENTAL: graph...
 | 
						|
					// save graph settings...
 | 
						|
					this.graph
 | 
						|
						&& (that.config['metadata-graph-config'] = {
 | 
						|
							graph: this.graph.graph,
 | 
						|
							mode: this.graph.mode,
 | 
						|
						}) }) })],
 | 
						|
 | 
						|
	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
 | 
						|
 | 
						|
			// NOTE: this is called on showMetadata.pre in the .handlers 
 | 
						|
			// 		feature section...
 | 
						|
			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() }) } }],
 | 
						|
 | 
						|
		// reload view when .ratingToRibbons('in-place') is called...
 | 
						|
		['ratingToRibbons',
 | 
						|
			function(res, mode){
 | 
						|
				mode == 'in-place'
 | 
						|
					&& this.reload(true) }],
 | 
						|
	],
 | 
						|
})
 | 
						|
 | 
						|
 | 
						|
/**********************************************************************
 | 
						|
* vim:set ts=4 sw=4 :                               */ return module })
 |