| 
									
										
										
										
											2024-10-27 11:01:12 +03:00
										 |  |  | /********************************************************************** | 
					
						
							|  |  |  | *  | 
					
						
							|  |  |  | * | 
					
						
							|  |  |  | * | 
					
						
							|  |  |  | **********************************************************************/ | 
					
						
							|  |  |  | ((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define) | 
					
						
							|  |  |  | (function(require){ var module={} // make module AMD/node compatible...
 | 
					
						
							|  |  |  | /*********************************************************************/ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | if(typeof(process) != 'undefined'){ | 
					
						
							|  |  |  | 	var fse = requirejs('fs-extra') | 
					
						
							|  |  |  | 	var pathlib = requirejs('path') | 
					
						
							|  |  |  | 	var file = require('imagegrid/file') | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | var util = require('lib/util') | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | var data = require('imagegrid/data') | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | var toggler = require('lib/toggler') | 
					
						
							|  |  |  | var actions = require('lib/actions') | 
					
						
							|  |  |  | var features = require('lib/features') | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | var browse = require('lib/widget/browse') | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | var core = require('features/core') | 
					
						
							|  |  |  | var widgets = require('features/ui-widgets') | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /*********************************************************************/ | 
					
						
							|  |  |  | // 
 | 
					
						
							|  |  |  | // XXX need a way to load a collection directly...
 | 
					
						
							|  |  |  | // 		- reload from .location.collection
 | 
					
						
							|  |  |  | // 		- path syntax 
 | 
					
						
							|  |  |  | // 			<path>:<title-selector>	-- needs to be normalized
 | 
					
						
							|  |  |  | // 			<path>:<gid>
 | 
					
						
							|  |  |  | // XXX might be a good idea to make collection loading part of the 
 | 
					
						
							|  |  |  | // 		.load(..) protocol...
 | 
					
						
							|  |  |  | // 		...this could be done via a url suffix, as a shorthand.
 | 
					
						
							|  |  |  | // 		something like:
 | 
					
						
							|  |  |  | // 			/path/to/index:collection
 | 
					
						
							|  |  |  | // 				-> /path/to/index/sub/path/.ImageGrid/collections/collection
 | 
					
						
							|  |  |  | // XXX loading collections by direct path would require us to look 
 | 
					
						
							|  |  |  | // 		in the containing index for missing parts (*images.json, ...)
 | 
					
						
							|  |  |  | // XXX saving a local collection would require us to save to two 
 | 
					
						
							|  |  |  | // 		locations:
 | 
					
						
							|  |  |  | // 			- collection specific stuff (data) to collection path
 | 
					
						
							|  |  |  | // 			- global stuff (images, tags, ...) to base index...
 | 
					
						
							|  |  |  | // XXX local tags:
 | 
					
						
							|  |  |  | // 		- save		- done
 | 
					
						
							|  |  |  | // 		- load		- done
 | 
					
						
							|  |  |  | // 		- save/merge use...
 | 
					
						
							|  |  |  | // XXX merge data -- collection/crop/all
 | 
					
						
							|  |  |  | // 		- vertical only
 | 
					
						
							|  |  |  | // 		- horizontal only
 | 
					
						
							|  |  |  | // 		- both
 | 
					
						
							|  |  |  | // XXX external / linked collections (.location)...
 | 
					
						
							|  |  |  | // XXX merge collections from multiple indexes -- can we avoid extending the format???
 | 
					
						
							|  |  |  | //
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // XXX should these be in .config???
 | 
					
						
							|  |  |  | var MAIN_COLLECTION_TITLE = | 
					
						
							|  |  |  | module.MAIN_COLLECTION_TITLE = '$ALL' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | var MAIN_COLLECTION_GID = | 
					
						
							|  |  |  | module.MAIN_COLLECTION_GID = '0' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | var COLLECTION_TRANSFER_CHANGES = | 
					
						
							|  |  |  | module.COLLECTION_TRANSFER_CHANGES = [ | 
					
						
							|  |  |  | 	'metadata', | 
					
						
							|  |  |  | 	'data', | 
					
						
							|  |  |  | ] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | //---------------------------------------------------------------------
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // XXX add undo...
 | 
					
						
							|  |  |  | var CollectionActions = actions.Actions({ | 
					
						
							|  |  |  | 	config: { | 
					
						
							|  |  |  | 		// can be:
 | 
					
						
							|  |  |  | 		// 	'all'		- save crop state for all collections (default)
 | 
					
						
							|  |  |  | 		// 	'main'		- save crop state for main state only
 | 
					
						
							|  |  |  | 		// 	'none'		- do not save crop state
 | 
					
						
							|  |  |  | 		'collection-save-crop-state': 'all', | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// XXX should this be in config???
 | 
					
						
							|  |  |  | 		// 		...technically no, but we need this to resolve correctly 
 | 
					
						
							|  |  |  | 		// 		to a relevant feature...
 | 
					
						
							|  |  |  | 		'collection-transfer-changes': COLLECTION_TRANSFER_CHANGES.slice(), | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Global default collections...
 | 
					
						
							|  |  |  | 		//
 | 
					
						
							|  |  |  | 		// NOTE: delete or set to null for none...
 | 
					
						
							|  |  |  | 		//'default-collections': null,
 | 
					
						
							|  |  |  | 	}, | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Format:
 | 
					
						
							|  |  |  | 	// 	{
 | 
					
						
							|  |  |  | 	// 		<title>: {
 | 
					
						
							|  |  |  | 	// 			title: <title>,
 | 
					
						
							|  |  |  | 	// 			gid: <gid>,
 | 
					
						
							|  |  |  | 	// 			count: <number>,
 | 
					
						
							|  |  |  | 	//
 | 
					
						
							|  |  |  | 	// 			crop_stack: [ .. ],
 | 
					
						
							|  |  |  | 	//
 | 
					
						
							|  |  |  | 	// 			// base collection format -- raw data...
 | 
					
						
							|  |  |  | 	// 			data: <data>,
 | 
					
						
							|  |  |  | 	//
 | 
					
						
							|  |  |  | 	// 			...
 | 
					
						
							|  |  |  | 	// 		},
 | 
					
						
							|  |  |  | 	// 		...
 | 
					
						
							|  |  |  | 	// 	}
 | 
					
						
							|  |  |  | 	collections: null, | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	get collectionGIDs(){ | 
					
						
							|  |  |  | 		var res = {} | 
					
						
							|  |  |  | 		var c = this.collections || {} | 
					
						
							|  |  |  | 		Object.keys(c) | 
					
						
							|  |  |  | 			.forEach(function(title){ | 
					
						
							|  |  |  | 				res[c[title].gid || title] = title }) | 
					
						
							|  |  |  | 		return res }, | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	get collection(){ | 
					
						
							|  |  |  | 		return this.location.collection }, | 
					
						
							|  |  |  | 	set collection(value){ | 
					
						
							|  |  |  | 		this.loadCollection(value) }, | 
					
						
							|  |  |  | 	get collectionGID(){ | 
					
						
							|  |  |  | 		return ((this.collections || {})[this.collection] || {}).gid  | 
					
						
							|  |  |  | 			|| MAIN_COLLECTION_GID }, | 
					
						
							|  |  |  | 	set collectionGID(value){ | 
					
						
							|  |  |  | 		this.collection = value }, | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// XXX should this check consistency???
 | 
					
						
							|  |  |  | 	get collection_order(){ | 
					
						
							|  |  |  | 		var collections = this.collections | 
					
						
							|  |  |  | 		var defaults = this.config['default-collections'] || [] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// no collections -> return defaults | []
 | 
					
						
							|  |  |  | 		if(this.collections == null){ | 
					
						
							|  |  |  | 			return defaults.slice() } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		var keys = Object.keys(collections) | 
					
						
							|  |  |  | 		var order = this.__collection_order = this.__collection_order || [] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// add unsorted things to the head of the list...
 | 
					
						
							|  |  |  | 		var res = [ | 
					
						
							|  |  |  | 			...keys, | 
					
						
							|  |  |  | 			...order, | 
					
						
							|  |  |  | 		].tailUnique() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// defaults...
 | 
					
						
							|  |  |  | 		res = res.concat(defaults).unique() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// keep MAIN_COLLECTION_TITLE out of the collection order...
 | 
					
						
							|  |  |  | 		var m = res.indexOf(MAIN_COLLECTION_TITLE) | 
					
						
							|  |  |  | 		m >= 0 | 
					
						
							|  |  |  | 			&& res.splice(m, 1) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// remove stuff not present...
 | 
					
						
							|  |  |  | 		if(res.length > keys.length){ | 
					
						
							|  |  |  | 			res = res.filter(function(e){  | 
					
						
							|  |  |  | 				return e in collections  | 
					
						
							|  |  |  | 					|| defaults.indexOf(e) >= 0 }) } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		this.__collection_order.splice(0, this.__collection_order.length, ...res) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		return this.__collection_order.slice() }, | 
					
						
							|  |  |  | 	set collection_order(value){ | 
					
						
							|  |  |  | 		value  | 
					
						
							|  |  |  | 			&& this.sortCollections(value) }, | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// NOTE: this accounts only for actual collections and does not counts
 | 
					
						
							|  |  |  | 	// 		MAIN_COLLECTION_TITLE that can be contained in .collections,
 | 
					
						
							|  |  |  | 	// 		thus this is NOT the same as:
 | 
					
						
							|  |  |  | 	// 			Object.keys(this.collections).length
 | 
					
						
							|  |  |  | 	// XXX do we need this???
 | 
					
						
							|  |  |  | 	get collections_length(){ | 
					
						
							|  |  |  | 		var c = (this.collections || {}) | 
					
						
							|  |  |  | 		return MAIN_COLLECTION_TITLE in c ?  | 
					
						
							|  |  |  | 			Object.keys(c).length - 1 | 
					
						
							|  |  |  | 			: Object.keys(c).length }, | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Format:
 | 
					
						
							|  |  |  | 	// 	{
 | 
					
						
							|  |  |  | 	// 		// NOTE: this is always the first handler...
 | 
					
						
							|  |  |  | 	// 		'data': <action-name>,
 | 
					
						
							|  |  |  | 	// 		'gid': <gid>,
 | 
					
						
							|  |  |  | 	//
 | 
					
						
							|  |  |  | 	// 		<format>: <action-name>,
 | 
					
						
							|  |  |  | 	// 		...
 | 
					
						
							|  |  |  | 	// 	}
 | 
					
						
							|  |  |  | 	//
 | 
					
						
							|  |  |  | 	// XXX revise doc...
 | 
					
						
							|  |  |  | 	// XXX this is almost the same as .store_handlers...
 | 
					
						
							|  |  |  | 	get collection_handlers(){ | 
					
						
							|  |  |  | 		return this.cache('collection_handlers', function(handlers){ | 
					
						
							|  |  |  | 			// cached value...
 | 
					
						
							|  |  |  | 			if(handlers){ | 
					
						
							|  |  |  | 				return Object.assign({}, handlers) } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			var that = this | 
					
						
							|  |  |  | 			handlers = {} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			handlers['data'] = null | 
					
						
							|  |  |  | 			this.actions.forEach(function(action){ | 
					
						
							|  |  |  | 				var fmt = that.getActionAttr(action, 'collectionFormat') | 
					
						
							|  |  |  | 				handlers[fmt] | 
					
						
							|  |  |  | 					&& console.warn('Multiple handlers for collection format:', store) | 
					
						
							|  |  |  | 				if(fmt){ | 
					
						
							|  |  |  | 					handlers[fmt] = action } }) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// cleanup...
 | 
					
						
							|  |  |  | 			if(handlers['data'] == null){ | 
					
						
							|  |  |  | 				delete handlers['data'] } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			return handlers }) }, | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Collection events...
 | 
					
						
							|  |  |  | 	//
 | 
					
						
							|  |  |  | 	collectionLoading: ['- Collections/', | 
					
						
							|  |  |  | 		core.doc`This is called by .loadCollection(..) or one of the 
 | 
					
						
							|  |  |  | 		overloading actions around the collection load... | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		The .pre phase is called just before the load and the .post phase  | 
					
						
							|  |  |  | 		just after. | 
					
						
							|  |  |  | 		 | 
					
						
							|  |  |  | 		`,
 | 
					
						
							|  |  |  | 		core.Event(function(collection){ | 
					
						
							|  |  |  | 			// This is the window resize event...
 | 
					
						
							|  |  |  | 			//
 | 
					
						
							|  |  |  | 			// Not for direct use.
 | 
					
						
							|  |  |  | 		})], | 
					
						
							|  |  |  | 	collectionUnloaded: ['- Collections/', | 
					
						
							|  |  |  | 		core.doc`This is called when unloading a collection.
 | 
					
						
							|  |  |  | 		`,
 | 
					
						
							|  |  |  | 		core.Event(function(collection){ | 
					
						
							|  |  |  | 			// This is the window resize event...
 | 
					
						
							|  |  |  | 			//
 | 
					
						
							|  |  |  | 			// Not for direct use.
 | 
					
						
							|  |  |  | 		})], | 
					
						
							|  |  |  | 	collectionCreated: ['- Collections/', | 
					
						
							|  |  |  | 		core.doc`This is called when a collection is created.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		NOTE: this is not triggered for the "${MAIN_COLLECTION_TITLE}" collection... | 
					
						
							|  |  |  | 		`,
 | 
					
						
							|  |  |  | 		core.Event(function(collection){ | 
					
						
							|  |  |  | 			// This is the window resize event...
 | 
					
						
							|  |  |  | 			//
 | 
					
						
							|  |  |  | 			// Not for direct use.
 | 
					
						
							|  |  |  | 		})], | 
					
						
							|  |  |  | 	collectionRemoved: ['- Collections/', | 
					
						
							|  |  |  | 		core.doc`This is called when a collection is removed.
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		NOTE: this is not triggered for the "${MAIN_COLLECTION_TITLE}" collection... | 
					
						
							|  |  |  | 		`,
 | 
					
						
							|  |  |  | 		core.Event(function(collection){ | 
					
						
							|  |  |  | 			// This is the window resize event...
 | 
					
						
							|  |  |  | 			//
 | 
					
						
							|  |  |  | 			// Not for direct use.
 | 
					
						
							|  |  |  | 		})], | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// XXX should there be a force arg when we can't actually stop the 
 | 
					
						
							|  |  |  | 	// 		running promise and recover???
 | 
					
						
							|  |  |  | 	// XXX need to figure out error handling for this scheme...
 | 
					
						
							|  |  |  | 	// XXX do we need timeouts here????
 | 
					
						
							|  |  |  | 	ensureCollection: ['- Collections/', | 
					
						
							|  |  |  | 		core.doc`Ensure a collection exists and is consistent...
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			Ensure collection exists and is initialized... | 
					
						
							|  |  |  | 			.ensureCollection(title) | 
					
						
							|  |  |  | 				-> promise(collection) | 
					
						
							|  |  |  | 				NOTE: this will not start a new check until the previous | 
					
						
							|  |  |  | 					is done (i.e. the previous promise is resolved/rejected) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		 | 
					
						
							|  |  |  | 		This will: | 
					
						
							|  |  |  | 			- create a collection if it does not exist | 
					
						
							|  |  |  | 			- initialize if needed | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		While the promise for a specific action is not resolved this  | 
					
						
							|  |  |  | 		will return it and not start a new promise thus queuing all  | 
					
						
							|  |  |  | 		subsequent calls. | 
					
						
							|  |  |  | 		`,
 | 
					
						
							|  |  |  | 		function(collection){ | 
					
						
							|  |  |  | 			var that = this | 
					
						
							|  |  |  | 			collection = collection  | 
					
						
							|  |  |  | 				|| MAIN_COLLECTION_TITLE | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// main collection shorthand...
 | 
					
						
							|  |  |  | 			// XXX revise...
 | 
					
						
							|  |  |  | 			if(this.collection == null  | 
					
						
							|  |  |  | 					&& collection == MAIN_COLLECTION_TITLE){ | 
					
						
							|  |  |  | 				return Promise.resolve(this) } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			var running = this.__running_collection_ensure =  | 
					
						
							|  |  |  | 				this.__running_collection_ensure || {} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// create collection if needed...
 | 
					
						
							|  |  |  | 			;(!this.collections  | 
					
						
							|  |  |  | 					|| !(collection in this.collections)) | 
					
						
							|  |  |  | 				&& this.newCollection(collection) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			var collection_data = this.collections[collection] | 
					
						
							|  |  |  | 			var handlers = this.collection_handlers | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// if a promise has not yet resolved/rejected, return it 
 | 
					
						
							|  |  |  | 			// and do not start a new one...
 | 
					
						
							|  |  |  | 			if(running[collection]){ | 
					
						
							|  |  |  | 				return running[collection] } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// handle collection...
 | 
					
						
							|  |  |  | 			return (running[collection] = new Promise(function(resolve, reject){ | 
					
						
							|  |  |  | 				// NOTE: we do not need to return this as we'll resolve/reject
 | 
					
						
							|  |  |  | 				// 		manually in .then(..) / .catch(..)
 | 
					
						
							|  |  |  | 				Promise | 
					
						
							|  |  |  | 					.all(Object.keys(handlers) | 
					
						
							|  |  |  | 						// filter relevant handlers...
 | 
					
						
							|  |  |  | 						.filter(function(format){  | 
					
						
							|  |  |  | 							return format == '*' || collection_data[format] }) | 
					
						
							|  |  |  | 						// run handlers...
 | 
					
						
							|  |  |  | 						.map(function(format){ | 
					
						
							|  |  |  | 							return that[handlers[format]](collection, collection_data) })) | 
					
						
							|  |  |  | 					.then(function(){ | 
					
						
							|  |  |  | 						delete running[collection] | 
					
						
							|  |  |  | 						resolve(collection_data) }) | 
					
						
							|  |  |  | 					.catch(function(err){ | 
					
						
							|  |  |  | 						delete running[collection] | 
					
						
							|  |  |  | 						reject(err) }) })) }], | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Collection life-cycle...
 | 
					
						
							|  |  |  | 	//
 | 
					
						
							|  |  |  | 	// NOTE: if collection does not exist this will do nothing...
 | 
					
						
							|  |  |  | 	// NOTE: this is not sync, if it is needed to trigger post collection
 | 
					
						
							|  |  |  | 	// 		loading then bind to collectionLoading.post...
 | 
					
						
							|  |  |  | 	loadCollection: ['- Collections/', | 
					
						
							|  |  |  | 		core.doc`Load collection...
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			Load collection... | 
					
						
							|  |  |  | 			.loadCollection(collection) | 
					
						
							|  |  |  | 			.loadCollection(gid) | 
					
						
							|  |  |  | 				-> this | 
					
						
							|  |  |  | 			 | 
					
						
							|  |  |  | 			Force reload current collection... | 
					
						
							|  |  |  | 			.loadCollection('!') | 
					
						
							|  |  |  | 				-> this | 
					
						
							|  |  |  | 				NOTE: this will not call .saveCollection(..) before  | 
					
						
							|  |  |  | 					reloading, thus potentially losing some state that  | 
					
						
							|  |  |  | 					was not explicitly saved. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		When loading a collection, previous state is saved. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		If .data for a collection is not available this will do nothing,  | 
					
						
							|  |  |  | 		this enables extending actions to handle the collection in  | 
					
						
							|  |  |  | 		different ways. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		Protocol: | 
					
						
							|  |  |  | 		- collection format handlers: .collection_handlers | 
					
						
							|  |  |  | 			- built from actions that define .collectionFormat attr to  | 
					
						
							|  |  |  | 			  contain the target format string. | 
					
						
							|  |  |  | 		- format is determined by matching it to a key in .collections[collection] | 
					
						
							|  |  |  | 			e.g. 'data' is applicable if .collections[collection].data is not null | 
					
						
							|  |  |  | 		- the format key's value is passed to the handler action | 
					
						
							|  |  |  | 		- the handler is expected to return a promise | 
					
						
							|  |  |  | 		- only the first matching handler is called | 
					
						
							|  |  |  | 		- the data handler is always first to get checked | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		Example loader action: | 
					
						
							|  |  |  | 			collectionXLoader: [ | 
					
						
							|  |  |  | 				// handle .x
 | 
					
						
							|  |  |  | 				{collectionFormat: 'x'} | 
					
						
							|  |  |  | 				function(title, state){ | 
					
						
							|  |  |  | 					return new Promise(function(resolve){  | 
					
						
							|  |  |  | 						var x = state.x | 
					
						
							|  |  |  | 		 | 
					
						
							|  |  |  | 						// do stuff with .x
 | 
					
						
							|  |  |  | 		 | 
					
						
							|  |  |  | 						resolve()  | 
					
						
							|  |  |  | 					}) }], | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		The .data handler is always first to enable caching, i.e. once some | 
					
						
							|  |  |  | 		non-data handler is done, it can set the .data which will be loaded | 
					
						
							|  |  |  | 		directly the next time. | 
					
						
							|  |  |  | 		To invalidate such a cache .data should simply be deleted. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		NOTE: cached collection state is persistent. | 
					
						
							|  |  |  | 		NOTE: when current collection is removed from .collections this  | 
					
						
							|  |  |  | 			will not save state when loading another collection... | 
					
						
							|  |  |  | 		`,
 | 
					
						
							|  |  |  | 		function(collection){ | 
					
						
							|  |  |  | 			var that = this | 
					
						
							|  |  |  | 			var force = collection == '!' | 
					
						
							|  |  |  | 			collection = collection == '!' ?  | 
					
						
							|  |  |  | 				this.collection  | 
					
						
							|  |  |  | 				: collection | 
					
						
							|  |  |  | 			// if collection is a gid, get the title...
 | 
					
						
							|  |  |  | 			collection = this.collectionGIDs[collection] || collection  | 
					
						
							|  |  |  | 			if(collection == null  | 
					
						
							|  |  |  | 					|| this.collections == null  | 
					
						
							|  |  |  | 					|| !(collection in this.collections)){ | 
					
						
							|  |  |  | 				return } | 
					
						
							|  |  |  | 			var crop_mode = this.config['collection-save-crop-state'] || 'all' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			var current = this.current | 
					
						
							|  |  |  | 			var ribbon = this.current_ribbon | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			var prev = this.collection | 
					
						
							|  |  |  | 			var collection_data = this.collections[collection] | 
					
						
							|  |  |  | 			//var handlers = this.collection_handlers
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// save current collection state...
 | 
					
						
							|  |  |  | 			//
 | 
					
						
							|  |  |  | 			// main view...
 | 
					
						
							|  |  |  | 			// NOTE: we save here unconditionally because MAIN_COLLECTION_TITLE
 | 
					
						
							|  |  |  | 			// 		is stored ONLY when we load some other collection...
 | 
					
						
							|  |  |  | 			if(this.collection == null){ | 
					
						
							|  |  |  | 				this.saveCollection( | 
					
						
							|  |  |  | 					MAIN_COLLECTION_TITLE,  | 
					
						
							|  |  |  | 					crop_mode == 'none' ?  'base' : 'crop',  | 
					
						
							|  |  |  | 					true) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// collection...
 | 
					
						
							|  |  |  | 			// NOTE: we only save if the current collection exists, it 
 | 
					
						
							|  |  |  | 			// 		may not exist if it was just removed...
 | 
					
						
							|  |  |  | 			} else if(this.collection in this.collections | 
					
						
							|  |  |  | 					// prevent saving over changed current state...
 | 
					
						
							|  |  |  | 					&& !force){ | 
					
						
							|  |  |  | 				this.saveCollection( | 
					
						
							|  |  |  | 					this.collection,  | 
					
						
							|  |  |  | 					crop_mode == 'all' ? 'crop': null) } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// load collection...
 | 
					
						
							|  |  |  | 			// XXX should this be sync???
 | 
					
						
							|  |  |  | 			//return this.ensureCollection(collection)
 | 
					
						
							|  |  |  | 			this.ensureCollection(collection) | 
					
						
							|  |  |  | 				.then(function(){ | 
					
						
							|  |  |  | 					var data = collection_data.data | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 					if(!data){ | 
					
						
							|  |  |  | 						return } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 					// current...
 | 
					
						
							|  |  |  | 					data.current = data.getImage(current)  | 
					
						
							|  |  |  | 						// current is not in collection -> try and keep 
 | 
					
						
							|  |  |  | 						// the ribbon context...
 | 
					
						
							|  |  |  | 						|| that.data.getImage( | 
					
						
							|  |  |  | 							current,  | 
					
						
							|  |  |  | 							data.getImages(that.data.getImages(ribbon))) | 
					
						
							|  |  |  | 						// get closest image from collection...
 | 
					
						
							|  |  |  | 						|| that.data.getImage(current, data.order) | 
					
						
							|  |  |  | 						|| data.current | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 					that | 
					
						
							|  |  |  | 						.collectionLoading.chainCall(that,  | 
					
						
							|  |  |  | 							function(){ | 
					
						
							|  |  |  | 								// do the actual load...
 | 
					
						
							|  |  |  | 								that.load.chainCall(that,  | 
					
						
							|  |  |  | 									function(){ | 
					
						
							|  |  |  | 										that.collectionUnloaded( | 
					
						
							|  |  |  | 											prev || MAIN_COLLECTION_TITLE) },  | 
					
						
							|  |  |  | 									{ | 
					
						
							|  |  |  | 										location: that.location, | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 										data: data, | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 										crop_stack: collection_data.crop_stack | 
					
						
							|  |  |  | 											&& collection_data.crop_stack.slice(), | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 										// NOTE: we do not need to pass collections 
 | 
					
						
							|  |  |  | 										// 		and order here as they stay in from 
 | 
					
						
							|  |  |  | 										// 		the last .load(..) in merge mode...
 | 
					
						
							|  |  |  | 										//collections: that.collections,
 | 
					
						
							|  |  |  | 										//collection_order: that.collection_order,
 | 
					
						
							|  |  |  | 									}, true) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 								// maintain the .collection state...
 | 
					
						
							|  |  |  | 								if(collection == MAIN_COLLECTION_TITLE){ | 
					
						
							|  |  |  | 									// no need to maintain the main data in two 
 | 
					
						
							|  |  |  | 									// locations...
 | 
					
						
							|  |  |  | 									delete that.collections[MAIN_COLLECTION_TITLE] | 
					
						
							|  |  |  | 									delete this.location.collection | 
					
						
							|  |  |  | 								} else { | 
					
						
							|  |  |  | 									that.data.collection =  | 
					
						
							|  |  |  | 										that.location.collection =  | 
					
						
							|  |  |  | 										collection | 
					
						
							|  |  |  | 									// cleanup...
 | 
					
						
							|  |  |  | 									if(collection == null){ | 
					
						
							|  |  |  | 										delete this.location.collection } } },  | 
					
						
							|  |  |  | 							collection) }) }], | 
					
						
							|  |  |  | 	// XXX should this call .loadCollection('!') when saving to current
 | 
					
						
							|  |  |  | 	// 		collection???
 | 
					
						
							|  |  |  | 	// 		This would reaload the view to a consistent (just saved) 
 | 
					
						
							|  |  |  | 	// 		state...
 | 
					
						
							|  |  |  | 	// 		...see comments inside...
 | 
					
						
							|  |  |  | 	// XXX it feels like we need two levels of actions, low-level that 
 | 
					
						
							|  |  |  | 	// 		just do their job and user actions that take care of 
 | 
					
						
							|  |  |  | 	// 		consistent state and the like...
 | 
					
						
							|  |  |  | 	saveCollection: ['- Collections/', | 
					
						
							|  |  |  | 		core.doc`Save current state to collection
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			Save current state to current collection | 
					
						
							|  |  |  | 			.saveCollection() | 
					
						
							|  |  |  | 			.saveCollection('current') | 
					
						
							|  |  |  | 				-> this | 
					
						
							|  |  |  | 				NOTE: this will do nothing if no collection is loaded. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			Save state as collection... | 
					
						
							|  |  |  | 			.saveCollection(collection) | 
					
						
							|  |  |  | 				-> this | 
					
						
							|  |  |  | 				NOTE: if saving to self the default mode is 'crop' else | 
					
						
							|  |  |  | 					it is 'current' (see below for info on respective  | 
					
						
							|  |  |  | 					modes)... | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			Save current state as collection ignoring crop stack | 
					
						
							|  |  |  | 			.saveCollection(collection, 0) | 
					
						
							|  |  |  | 			.saveCollection(collection, 'current') | 
					
						
							|  |  |  | 				-> this | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			Save new empty collection | 
					
						
							|  |  |  | 			.saveCollection(collection, 'empty') | 
					
						
							|  |  |  | 				-> this | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			Save current crop state to collection | 
					
						
							|  |  |  | 			.saveCollection(collection, 'crop') | 
					
						
							|  |  |  | 				-> this | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			Save top depth crops from current crop stack to collection | 
					
						
							|  |  |  | 			.saveCollection(collection, depth) | 
					
						
							|  |  |  | 				-> this | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			Save base crop state to collection | 
					
						
							|  |  |  | 			.saveCollection(collection, 'base') | 
					
						
							|  |  |  | 				-> this | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		NOTE: this will overwrite collection .data and .crop_stack only,  | 
					
						
							|  |  |  | 			the rest of the data is untouched... | 
					
						
							|  |  |  | 		NOTE: when saving to current collection and maintain consistent  | 
					
						
							|  |  |  | 			state it may be necessary to .loadCollection('!') | 
					
						
							|  |  |  | 		`,
 | 
					
						
							|  |  |  | 		function(collection, mode, force){ | 
					
						
							|  |  |  | 			var that = this | 
					
						
							|  |  |  | 			collection = collection || this.collection | 
					
						
							|  |  |  | 			collection = collection == 'current' ? this.collection : collection | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			if(!force  | 
					
						
							|  |  |  | 					&& (collection == null  | 
					
						
							|  |  |  | 						|| collection == MAIN_COLLECTION_TITLE)){ | 
					
						
							|  |  |  | 				return } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			var collections = this.collections = this.collections || {} | 
					
						
							|  |  |  | 			var depth = typeof(mode) == typeof(123) ? mode : null | 
					
						
							|  |  |  | 			mode = depth == 0 ? 'current'  | 
					
						
							|  |  |  | 				: depth ? 'crop'  | 
					
						
							|  |  |  | 				: mode | 
					
						
							|  |  |  | 			// default mode -- if saving to self then 'crop' else 'current'
 | 
					
						
							|  |  |  | 			if(!mode){ | 
					
						
							|  |  |  | 				mode = ((collection in collections  | 
					
						
							|  |  |  | 							&& collection == this.collection)  | 
					
						
							|  |  |  | 						|| collection == MAIN_COLLECTION_TITLE) ?  | 
					
						
							|  |  |  | 					'crop'  | 
					
						
							|  |  |  | 					: 'current' } | 
					
						
							|  |  |  | 			var new_collection =  | 
					
						
							|  |  |  | 				!collections[collection]  | 
					
						
							|  |  |  | 					&& collection != MAIN_COLLECTION_TITLE | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// save the data...
 | 
					
						
							|  |  |  | 			// XXX would be nice to be able to add new collections both 
 | 
					
						
							|  |  |  | 			// 		to the start and end of list...
 | 
					
						
							|  |  |  | 			var state = collections[collection] =  | 
					
						
							|  |  |  | 				collections[collection]  | 
					
						
							|  |  |  | 				|| {} | 
					
						
							|  |  |  | 			state.title = state.title  | 
					
						
							|  |  |  | 				|| collection | 
					
						
							|  |  |  | 			state.gid = state.gid  | 
					
						
							|  |  |  | 				// maintain the GID of MAIN_COLLECTION_TITLE as MAIN_COLLECTION_GID...
 | 
					
						
							|  |  |  | 				|| (collection == MAIN_COLLECTION_TITLE ?  | 
					
						
							|  |  |  | 					MAIN_COLLECTION_GID  | 
					
						
							|  |  |  | 					// generate unique gid...
 | 
					
						
							|  |  |  | 					: (function(){ | 
					
						
							|  |  |  | 						var gids = that.collectionGIDs | 
					
						
							|  |  |  | 						do{ | 
					
						
							|  |  |  | 							var gid = that.data.newGID() | 
					
						
							|  |  |  | 						} while(gids[gid] != null) | 
					
						
							|  |  |  | 						return gid })()) | 
					
						
							|  |  |  | 			// NOTE: we do not need to care about tags here as they 
 | 
					
						
							|  |  |  | 			// 		will get overwritten on load...
 | 
					
						
							|  |  |  | 			state.data = (mode == 'empty' ?  | 
					
						
							|  |  |  | 					(new this.data.constructor()) | 
					
						
							|  |  |  | 				: mode == 'base' && this.crop_stack ?  | 
					
						
							|  |  |  | 					(this.crop_stack[0] || this.data.clone()) | 
					
						
							|  |  |  | 				: mode == 'crop' ?  | 
					
						
							|  |  |  | 					this.data.clone() | 
					
						
							|  |  |  | 				// current...
 | 
					
						
							|  |  |  | 				: this.data.clone() | 
					
						
							|  |  |  | 					.run(function(){ | 
					
						
							|  |  |  | 						var d = this | 
					
						
							|  |  |  | 						this.collection = collection }) | 
					
						
							|  |  |  | 					.clear('unloaded')) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// crop mode -> handle crop stack...
 | 
					
						
							|  |  |  | 			if(mode == 'crop' && this.crop_stack && depth != 0){ | 
					
						
							|  |  |  | 				depth = depth || this.crop_stack.length | 
					
						
							|  |  |  | 				depth = this.crop_stack.length - Math.min(depth, this.crop_stack.length) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				state.crop_stack = this.crop_stack.slice(depth) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// other modes...
 | 
					
						
							|  |  |  | 			} else { | 
					
						
							|  |  |  | 				delete state.crop_stack } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// XXX this leads to recursion????
 | 
					
						
							|  |  |  | 			// 		.loadCollection('X')
 | 
					
						
							|  |  |  | 			// 			-> .saveCollection('current')
 | 
					
						
							|  |  |  | 			// 				-> .loadCollection('!')
 | 
					
						
							|  |  |  | 			// XXX should we be doing this here or on case by case basis externally...
 | 
					
						
							|  |  |  | 			//collection == this.collection
 | 
					
						
							|  |  |  | 			//	&& this.loadCollection('!')
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			new_collection | 
					
						
							|  |  |  | 				&& this.collectionCreated(collection) }], | 
					
						
							|  |  |  | 	newCollection: ['- Collections/', | 
					
						
							|  |  |  | 		core.doc` Shorthand to .saveCollection(collection, 'empty')`, | 
					
						
							|  |  |  | 		function(collection){ return this.saveCollection(collection, 'empty') }], | 
					
						
							|  |  |  | 	removeCollection: ['- Collections/', | 
					
						
							|  |  |  | 		core.doc`
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			.removeCollection(collection) | 
					
						
							|  |  |  | 			.removeCollection(gid) | 
					
						
							|  |  |  | 				-> this | 
					
						
							|  |  |  | 		 | 
					
						
							|  |  |  | 		NOTE: when removing the currently loaded collection this will  | 
					
						
							|  |  |  | 			just remove it from .collections and do nothing...`,
 | 
					
						
							|  |  |  | 		function(collection){ | 
					
						
							|  |  |  | 			if(!this.collections || collection == MAIN_COLLECTION_TITLE){ | 
					
						
							|  |  |  | 				return } | 
					
						
							|  |  |  | 			collection = this.collectionGIDs[collection] || collection | 
					
						
							|  |  |  | 			if(collection in this.collections){ | 
					
						
							|  |  |  | 				delete this.collections[collection] | 
					
						
							|  |  |  | 				this.collectionRemoved(collection) } }], | 
					
						
							|  |  |  | 	renameCollection: ['- Collections/', | 
					
						
							|  |  |  | 		function(from, to){ | 
					
						
							|  |  |  | 			if(to == from  | 
					
						
							|  |  |  | 					|| from == MAIN_COLLECTION_TITLE  | 
					
						
							|  |  |  | 					|| to == MAIN_COLLECTION_TITLE  | 
					
						
							|  |  |  | 					|| (this.collections  | 
					
						
							|  |  |  | 						|| {})[from] == null){ | 
					
						
							|  |  |  | 				return } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			var order = this.collection_order | 
					
						
							|  |  |  | 			order.splice(order.indexOf(from), 1, to) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			var data = this.collections[to] = this.collections[from] | 
					
						
							|  |  |  | 			delete this.collections[from] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			data.title = to | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			if(this.collection == from){ | 
					
						
							|  |  |  | 				this.location.collection = to } }], | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Collection list manipulation...
 | 
					
						
							|  |  |  | 	//
 | 
					
						
							|  |  |  | 	sortCollections: ['- Collections/', | 
					
						
							|  |  |  | 		core.doc`Sort collection list...
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			Sort collections... | 
					
						
							|  |  |  | 			.sortCollections()	 | 
					
						
							|  |  |  | 				NOTE: this is equivalent to [].sort() | 
					
						
							|  |  |  | 				-> this | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			Sort collections via cmp function... | 
					
						
							|  |  |  | 			.sortCollections(cmp)	 | 
					
						
							|  |  |  | 				NOTE: this is equivalent to [].sort(cmp) | 
					
						
							|  |  |  | 				-> this | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			Sort collections via list... | 
					
						
							|  |  |  | 			.sortCollections([item, ...])	 | 
					
						
							|  |  |  | 				-> this | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		`,
 | 
					
						
							|  |  |  | 		function(cmp){ | 
					
						
							|  |  |  | 			// XXX handle the case when there's no .__collection_order
 | 
					
						
							|  |  |  | 			if(!this.__collection_order){ | 
					
						
							|  |  |  | 				return } | 
					
						
							|  |  |  | 			// sort via list...
 | 
					
						
							|  |  |  | 			if(cmp instanceof Array){ | 
					
						
							|  |  |  | 				this.__collection_order = cmp.slice() | 
					
						
							|  |  |  | 			// cmp...
 | 
					
						
							|  |  |  | 			} else if(cmp instanceof Function){ | 
					
						
							|  |  |  | 				this.__collection_order.sort(cmp) | 
					
						
							|  |  |  | 			// basic sort...
 | 
					
						
							|  |  |  | 			} else { | 
					
						
							|  |  |  | 				this.__collection_order.sort() } | 
					
						
							|  |  |  | 			this.collection_order }], | 
					
						
							|  |  |  | 	collectionToTop: ['Collections/Bring collection to $top', | 
					
						
							|  |  |  | 		core.doc`Bring collection to top...
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			Bring current collection to top of collection list | 
					
						
							|  |  |  | 			.collectionToTop() | 
					
						
							|  |  |  | 				-> this | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			Bring collection title to top of collection list | 
					
						
							|  |  |  | 			.collectionToTop(title) | 
					
						
							|  |  |  | 				-> this | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			Bring collection gid to top of collection list | 
					
						
							|  |  |  | 			.collectionToTop(gid) | 
					
						
							|  |  |  | 				-> this | 
					
						
							|  |  |  | 		`,
 | 
					
						
							|  |  |  | 		{mode: 'uncollect'}, | 
					
						
							|  |  |  | 		function(collection){ | 
					
						
							|  |  |  | 			collection = collection || this.collection | 
					
						
							|  |  |  | 			collection = this.collectionGIDs[collection] || collection | 
					
						
							|  |  |  | 			var o = this.collection_order  | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			if(!collection || o.indexOf(collection) < 0){ | 
					
						
							|  |  |  | 				return } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			this.collection_order = [collection].concat(o).unique() }], | 
					
						
							|  |  |  | 	 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Introspection...
 | 
					
						
							|  |  |  | 	//
 | 
					
						
							|  |  |  | 	// XXX make this check offline collections -- use .ensureCollection(..)???
 | 
					
						
							|  |  |  | 	inCollections: ['- Image/', | 
					
						
							|  |  |  | 		core.doc`Get list of collections containing item
 | 
					
						
							|  |  |  | 		 | 
					
						
							|  |  |  | 		NOTE: this currently does not load or check offline collections. | 
					
						
							|  |  |  | 		`,
 | 
					
						
							|  |  |  | 		function(gid, collections){ | 
					
						
							|  |  |  | 			var that = this | 
					
						
							|  |  |  | 			gid = this.data.getImage(gid) | 
					
						
							|  |  |  | 			collections = collections || this.collection_order | 
					
						
							|  |  |  | 			collections = collections instanceof Array ? collections : [collections] | 
					
						
							|  |  |  | 			return collections | 
					
						
							|  |  |  | 				.filter(function(c){ | 
					
						
							|  |  |  | 					return c != MAIN_COLLECTION_TITLE | 
					
						
							|  |  |  | 						&& that.collections[c] | 
					
						
							|  |  |  | 						&& that.collections[c].data | 
					
						
							|  |  |  | 						&& (!gid  | 
					
						
							|  |  |  | 							|| that.collections[c].data.getImage(gid)) }) }], | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Collection editing....
 | 
					
						
							|  |  |  | 	//
 | 
					
						
							|  |  |  | 	// NOTE: Currently these are sync, and sequencing collections 
 | 
					
						
							|  |  |  | 	// 		operations happens automatically as everything uses 
 | 
					
						
							|  |  |  | 	// 		.ensureCollection(..) internally...
 | 
					
						
							|  |  |  | 	// 		to explecitly sequence code do:
 | 
					
						
							|  |  |  | 	//			.collect(..)
 | 
					
						
							|  |  |  | 	//			.ensureCollection(..)
 | 
					
						
							|  |  |  | 	//				.then(function(){
 | 
					
						
							|  |  |  | 	//					// this is run strictly after .collect(..) 
 | 
					
						
							|  |  |  | 	//					...
 | 
					
						
							|  |  |  | 	//				})
 | 
					
						
							|  |  |  | 	// NOTE: see .ensureCollection(..) for more details...
 | 
					
						
							|  |  |  | 	//
 | 
					
						
							|  |  |  | 	// XXX undo: need to be able to place collected stuff...
 | 
					
						
							|  |  |  | 	collect: ['Collections|Image/Add $image to collection...', | 
					
						
							|  |  |  | 		core.doc`Add items to collection
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			Add current image to collection... | 
					
						
							|  |  |  | 			.collect('current', collection) | 
					
						
							|  |  |  | 				-> this | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			Add current ribbon to collection... | 
					
						
							|  |  |  | 			.collect('ribbon', collection) | 
					
						
							|  |  |  | 				-> this | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			Add loaded images to collection... | 
					
						
							|  |  |  | 			.collect('loaded', collection) | 
					
						
							|  |  |  | 				-> this | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			 | 
					
						
							|  |  |  | 			Add gid(s) to collection... | 
					
						
							|  |  |  | 			.collect(gid, collection) | 
					
						
							|  |  |  | 			.collect([gid, ,. ], collection) | 
					
						
							|  |  |  | 				-> this | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		NOTE: this will not account for item topology. To merge accounting | 
					
						
							|  |  |  | 			for topology use .joinCollect(..) | 
					
						
							|  |  |  | 		NOTE: if an image gid is not found locally it will be searched in | 
					
						
							|  |  |  | 			base data... | 
					
						
							|  |  |  | 		`,
 | 
					
						
							|  |  |  | 		function(gids, collection){ | 
					
						
							|  |  |  | 			var that = this | 
					
						
							|  |  |  | 			collection = collection || this.collection | 
					
						
							|  |  |  | 			collection = this.collectionGIDs[collection] || collection  | 
					
						
							|  |  |  | 			if(collection == null || collection == MAIN_COLLECTION_TITLE){ | 
					
						
							|  |  |  | 				return } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			gids = gids == 'loaded' ?  | 
					
						
							|  |  |  | 					this.data.getImages('loaded') | 
					
						
							|  |  |  | 				: gids == 'ribbon' ? | 
					
						
							|  |  |  | 					[this.current_ribbon] | 
					
						
							|  |  |  | 				: gids instanceof Array ?  | 
					
						
							|  |  |  | 					gids  | 
					
						
							|  |  |  | 				: [gids] | 
					
						
							|  |  |  | 			gids = gids | 
					
						
							|  |  |  | 				.map(function(gid){  | 
					
						
							|  |  |  | 					return gid in that.data.ribbons ?  | 
					
						
							|  |  |  | 						// when adding a ribbon gid expand to images...
 | 
					
						
							|  |  |  | 						that.data.ribbons[gid].compact() | 
					
						
							|  |  |  | 						: [ that.data.getImage(gid)  | 
					
						
							|  |  |  | 							// check base data for image gid...
 | 
					
						
							|  |  |  | 							|| that.collections[MAIN_COLLECTION_TITLE].data.getImage(gid) ] }) | 
					
						
							|  |  |  | 				.flat() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			this.ensureCollection(collection) | 
					
						
							|  |  |  | 				.then((function(c){ | 
					
						
							|  |  |  | 					var remove = c.data.getImages(gids, 'all') | 
					
						
							|  |  |  | 					// only add gids that do not exist in collection...
 | 
					
						
							|  |  |  | 					gids = gids | 
					
						
							|  |  |  | 						.filter(function(g){  | 
					
						
							|  |  |  | 							return remove.indexOf(g) < 0 }) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 					if(gids.length == 0){ | 
					
						
							|  |  |  | 						return } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 					// add to collection...
 | 
					
						
							|  |  |  | 					var data = this.data.constructor.fromArray(gids) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 					// XXX should we use collection.data.placeImage(..)???
 | 
					
						
							|  |  |  | 					return this.joinCollect(null, collection, data)  | 
					
						
							|  |  |  | 				}).bind(this)) }], | 
					
						
							|  |  |  | 	joinCollect: ['Collections/$Merge view to collection...', | 
					
						
							|  |  |  | 		core.doc`Merge/Join current state to collection
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			Join current state into collection | 
					
						
							|  |  |  | 			.joinCollect(collection) | 
					
						
							|  |  |  | 				-> this | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			Join current state with specific alignment into collection | 
					
						
							|  |  |  | 			.joinCollect(align, collection) | 
					
						
							|  |  |  | 				-> this | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			Join data to collection with specific alignment | 
					
						
							|  |  |  | 			.joinCollect(align, collection, data) | 
					
						
							|  |  |  | 				-> this | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		This is like .collect(..) but will preserve topology. | 
					
						
							|  |  |  | 		 | 
					
						
							|  |  |  | 		NOTE: for align docs see Data.join(..) | 
					
						
							|  |  |  | 		NOTE: if align is set to null or not given then it will be set  | 
					
						
							|  |  |  | 			to default value. | 
					
						
							|  |  |  | 		NOTE: this will join to the left (prepend) of the collections, this is  | 
					
						
							|  |  |  | 			different from how basic .join(..) works (appends) | 
					
						
							|  |  |  | 		`,
 | 
					
						
							|  |  |  | 		function(align, collection, data){ | 
					
						
							|  |  |  | 			collection = arguments.length == 1 ? align : collection | 
					
						
							|  |  |  | 			collection = this.collectionGIDs[collection] || collection  | 
					
						
							|  |  |  | 			if(collection == null || collection == MAIN_COLLECTION_TITLE){ | 
					
						
							|  |  |  | 				return } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// if only collection is given, reset align to null...
 | 
					
						
							|  |  |  | 			align = align === collection ? null : align | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// create collection if it does not exist...
 | 
					
						
							|  |  |  | 			this.ensureCollection(collection) | 
					
						
							|  |  |  | 				.then((function(c){ | 
					
						
							|  |  |  | 					var target = c.crop_stack ?  | 
					
						
							|  |  |  | 						c.crop_stack[0]  | 
					
						
							|  |  |  | 						: c.data | 
					
						
							|  |  |  | 					//this.collections[collection].data.join(align, data || this.data.clone())
 | 
					
						
							|  |  |  | 					var res = (data || this.data) | 
					
						
							|  |  |  | 						.clone() | 
					
						
							|  |  |  | 						.clear('unloaded') | 
					
						
							|  |  |  | 						.join(align, target) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 					var rorder = res.order.slice().reverse() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 					// write to base data...
 | 
					
						
							|  |  |  | 					if(c.crop_stack){ | 
					
						
							|  |  |  | 						c.crop_stack[0] = res | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 						c.crop_stack | 
					
						
							|  |  |  | 							.concat([c.data]) | 
					
						
							|  |  |  | 							.forEach(function(data){ | 
					
						
							|  |  |  | 								data.order = data.order | 
					
						
							|  |  |  | 									.reverse() | 
					
						
							|  |  |  | 									.concat(rorder) | 
					
						
							|  |  |  | 									.unique() | 
					
						
							|  |  |  | 									.reverse()  | 
					
						
							|  |  |  | 								data.updateImagePositions() }) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 					} else { | 
					
						
							|  |  |  | 						c.data = res } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 					// joining into the current collection...
 | 
					
						
							|  |  |  | 					if(collection == this.collection){ | 
					
						
							|  |  |  | 						if(this.crop_stack){ | 
					
						
							|  |  |  | 							this.crop_stack[0] = res | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 							this.crop_stack | 
					
						
							|  |  |  | 								.concat([this.data]) | 
					
						
							|  |  |  | 								.forEach(function(data){ | 
					
						
							|  |  |  | 									data.order = data.order | 
					
						
							|  |  |  | 										.reverse() | 
					
						
							|  |  |  | 										.concat(rorder) | 
					
						
							|  |  |  | 										.unique() | 
					
						
							|  |  |  | 										.reverse()  | 
					
						
							|  |  |  | 									data.updateImagePositions() }) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 						} else { | 
					
						
							|  |  |  | 							var cur = this.current | 
					
						
							|  |  |  | 							this.data = res  | 
					
						
							|  |  |  | 							this.data.current = cur } } | 
					
						
							|  |  |  | 				}).bind(this)) }], | 
					
						
							|  |  |  | 	// XXX undo: see .removeFromCrop(..) for a reference implementation...
 | 
					
						
							|  |  |  | 	// 		this will need:
 | 
					
						
							|  |  |  | 	// 			- .collect(..) to be able to place images...
 | 
					
						
							|  |  |  | 	//			- also store image order as .data.order is cleared of 
 | 
					
						
							|  |  |  | 	//				removed images...
 | 
					
						
							|  |  |  | 	uncollect: ['Collections|Image/Remove from collection', | 
					
						
							|  |  |  | 		core.doc`Remove gid(s) from collection...
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			Remove current image from current collection... | 
					
						
							|  |  |  | 			.uncollect() | 
					
						
							|  |  |  | 			.uncollect('current') | 
					
						
							|  |  |  | 				-> this | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			Remove gid(s) from current collection... | 
					
						
							|  |  |  | 			.uncollect(gid) | 
					
						
							|  |  |  | 			.uncollect([gid, ..]) | 
					
						
							|  |  |  | 				-> this | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			Remove gid(s) from collection... | 
					
						
							|  |  |  | 			.uncollect(gid, collection) | 
					
						
							|  |  |  | 			.uncollect([gid, ..], collection) | 
					
						
							|  |  |  | 				-> this | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		NOTE: this will remove any gid, be it image or ribbon. | 
					
						
							|  |  |  | 		`,
 | 
					
						
							|  |  |  | 		{ | 
					
						
							|  |  |  | 			mode: function(){  | 
					
						
							|  |  |  | 				return !this.collection && 'disabled' }, | 
					
						
							|  |  |  | 			// XXX two ways to go:
 | 
					
						
							|  |  |  | 			//		- .collect(..) + .data.placeImage(..)
 | 
					
						
							|  |  |  | 			//		- rewrite .collect(..) to use .data.placeImage(..) (like: .addToCrop(..))
 | 
					
						
							|  |  |  | 			getUndoState: function(d){ | 
					
						
							|  |  |  | 				d.placements = this.data.getImagePositions(d.args[0]) | 
					
						
							|  |  |  | 				d.collection = d.args[1]  | 
					
						
							|  |  |  | 					|| this.collection }, | 
					
						
							|  |  |  | 			// XXX this does not work yet...
 | 
					
						
							|  |  |  | 			// 		...need to trigger .reload(..), and considering that
 | 
					
						
							|  |  |  | 			// 		this is a UI action, doing so explicitly is logical...
 | 
					
						
							|  |  |  | 			// XXX can we unify .collect(..) and .addToCrop(..)???
 | 
					
						
							|  |  |  | 			// 		...they essentially do the same thing with one exception
 | 
					
						
							|  |  |  | 			// 		a crop retains the full order while a collection has a 
 | 
					
						
							|  |  |  | 			// 		cleared order...
 | 
					
						
							|  |  |  | 			// 		..might also be a good idea to unify much of the 
 | 
					
						
							|  |  |  | 			// 		collection and crop mechanics...
 | 
					
						
							|  |  |  | 			undo: function(d){ | 
					
						
							|  |  |  | 				var that = this | 
					
						
							|  |  |  | 				var gids = d.args[0] || [d.current] | 
					
						
							|  |  |  | 				gids = gids instanceof Array ? gids : [gids] | 
					
						
							|  |  |  | 				var collection = d.collection | 
					
						
							|  |  |  | 				this | 
					
						
							|  |  |  | 					// XXX is this the right approach???
 | 
					
						
							|  |  |  | 					.collect(gids, collection) | 
					
						
							|  |  |  | 					.ensureCollection(collection) | 
					
						
							|  |  |  | 						.then(function(){ | 
					
						
							|  |  |  | 							d.placements.forEach(function(e){ | 
					
						
							|  |  |  | 								// XXX does not place correctly...
 | 
					
						
							|  |  |  | 								that.data.placeImage(e[0], e[1], that.data.order[e[2]])  | 
					
						
							|  |  |  | 							that.reload(true) | 
					
						
							|  |  |  | 						}) }) }, | 
					
						
							|  |  |  | 			//*/
 | 
					
						
							|  |  |  | 		}, | 
					
						
							|  |  |  | 		function(gids, collection){ | 
					
						
							|  |  |  | 			collection = collection || this.collection | 
					
						
							|  |  |  | 			collection = this.collectionGIDs[collection] || collection  | 
					
						
							|  |  |  | 			if(collection == null  | 
					
						
							|  |  |  | 					|| collection == MAIN_COLLECTION_TITLE | 
					
						
							|  |  |  | 					|| !this.collections  | 
					
						
							|  |  |  | 					|| !(collection in this.collections)){ | 
					
						
							|  |  |  | 				return } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			var that = this | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			gids = gids == 'loaded' ? this.data.getImages('loaded') | 
					
						
							|  |  |  | 				: gids instanceof Array ? gids  | 
					
						
							|  |  |  | 				: [gids] | 
					
						
							|  |  |  | 			gids = gids | 
					
						
							|  |  |  | 				.map(function(gid){  | 
					
						
							|  |  |  | 					return gid in that.data.ribbons ?  | 
					
						
							|  |  |  | 						// when adding a ribbon gid expand to images...
 | 
					
						
							|  |  |  | 						that.data.ribbons[gid].compact() | 
					
						
							|  |  |  | 						: [that.data.getImage(gid)] }) | 
					
						
							|  |  |  | 				.flat() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// remove from the loaded state...
 | 
					
						
							|  |  |  | 			this.collection == collection | 
					
						
							|  |  |  | 				&& this.data.clear(gids) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// NOTE: we do both this and the above iff data is cloned...
 | 
					
						
							|  |  |  | 			// NOTE: if tags are saved to the collection it means that 
 | 
					
						
							|  |  |  | 			// 		those tags are local to the collection and we do not 
 | 
					
						
							|  |  |  | 			// 		need to protect them...
 | 
					
						
							|  |  |  | 			if(this.collections[collection].data  | 
					
						
							|  |  |  | 					&& this.data !== this.collections[collection].data){ | 
					
						
							|  |  |  | 				this.collections[collection].data | 
					
						
							|  |  |  | 					.clear(gids) } }], | 
					
						
							|  |  |  | 	uncollectRibbon: ['Collections|Ribbon/Remove ribbon from collection', | 
					
						
							|  |  |  | 		core.doc`Remove ribbons from collection...
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			Remove current ribbon from current collection... | 
					
						
							|  |  |  | 			.uncollectRibbon() | 
					
						
							|  |  |  | 			.uncollectRibbon('current') | 
					
						
							|  |  |  | 				-> this | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			Remove gid(s) from current collection... | 
					
						
							|  |  |  | 			.uncollectRibbon(gid) | 
					
						
							|  |  |  | 			.uncollectRibbon([gid, .. ]) | 
					
						
							|  |  |  | 				-> this | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			Remove gid(s) from collection... | 
					
						
							|  |  |  | 			.uncollectRibbon(gid, collection) | 
					
						
							|  |  |  | 			.uncollectRibbon([gid, .. ], collection) | 
					
						
							|  |  |  | 				-> this | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		NOTE: this is the same as .uncollect(..) but removes whole ribbons,  | 
					
						
							|  |  |  | 			i.e. each gid given will be resolved to a ribbon which will be | 
					
						
							|  |  |  | 			removed. | 
					
						
							|  |  |  | 		`,
 | 
					
						
							|  |  |  | 		{mode: 'uncollect'}, | 
					
						
							|  |  |  | 		function(gids, collection){ | 
					
						
							|  |  |  | 			var that = this | 
					
						
							|  |  |  | 			gids = gids || 'current' | 
					
						
							|  |  |  | 			gids = gids instanceof Array ? gids : [gids] | 
					
						
							|  |  |  | 			gids = gids | 
					
						
							|  |  |  | 				.map(function(gid){  | 
					
						
							|  |  |  | 					return that.data.getRibbon(gid) }) | 
					
						
							|  |  |  | 			return this.uncollect(gids, collection) }], | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Serialization...
 | 
					
						
							|  |  |  | 	//
 | 
					
						
							|  |  |  | 	// NOTE: this will handle collection title and data only, the rest 
 | 
					
						
							|  |  |  | 	// 		is copied in as-is.
 | 
					
						
							|  |  |  | 	// 		It is the responsibility of the extending features to transform
 | 
					
						
							|  |  |  | 	// 		their data on load as needed.
 | 
					
						
							|  |  |  | 	load: [function(json){ | 
					
						
							|  |  |  | 		var that = this | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		var collections = {} | 
					
						
							|  |  |  | 		var c = json.collections || {} | 
					
						
							|  |  |  | 		var order = json.collection_order || Object.keys(c) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		if((json.location || {}).collection){ | 
					
						
							|  |  |  | 			this.location.collection = json.location.collection } | 
					
						
							|  |  |  | 			 | 
					
						
							|  |  |  | 		Object.keys(c) | 
					
						
							|  |  |  | 			.forEach(function(title){ | 
					
						
							|  |  |  | 				if(c[title] === false){ | 
					
						
							|  |  |  | 					return } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				var state = collections[title] = { title: title } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				// load data...
 | 
					
						
							|  |  |  | 				var d = c[title].data == null ? | 
					
						
							|  |  |  | 						null | 
					
						
							|  |  |  | 					: c[title].data instanceof data.Data ? | 
					
						
							|  |  |  | 						c[title].data | 
					
						
							|  |  |  | 					: data.Data.fromJSON(c[title].data) | 
					
						
							|  |  |  | 				if(d){ | 
					
						
							|  |  |  | 					state.data = d } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				// image count...
 | 
					
						
							|  |  |  | 				if(c[title].count){ | 
					
						
							|  |  |  | 					state.count = c[title].count } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				// NOTE: this can be done lazily when loading each collection
 | 
					
						
							|  |  |  | 				// 		but doing so will make the system more complex and 
 | 
					
						
							|  |  |  | 				// 		confuse (or complicate) some code that expects 
 | 
					
						
							|  |  |  | 				// 		.collections[*].crop_stack[*] to be instances of Data.
 | 
					
						
							|  |  |  | 				if(c[title].crop_stack){ | 
					
						
							|  |  |  | 					state.crop_stack = c[title].crop_stack | 
					
						
							|  |  |  | 						.map(function(c){  | 
					
						
							|  |  |  | 							return c instanceof data.Data ?  | 
					
						
							|  |  |  | 								c  | 
					
						
							|  |  |  | 								: data.Data(c) }) } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				// copy the rest of collection data as-is...
 | 
					
						
							|  |  |  | 				Object.keys(c[title]) | 
					
						
							|  |  |  | 					.forEach(function(key){ | 
					
						
							|  |  |  | 						if(key in state){ | 
					
						
							|  |  |  | 							return } | 
					
						
							|  |  |  | 						state[key] = c[title][key] }) }) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		return function(){ | 
					
						
							|  |  |  | 			if(Object.keys(collections).length > 0){ | 
					
						
							|  |  |  | 				this.collections = collections | 
					
						
							|  |  |  | 				this.collection_order = order } } }], | 
					
						
							|  |  |  | 	//
 | 
					
						
							|  |  |  | 	// Supported modes:
 | 
					
						
							|  |  |  | 	// 	current (default) 	- ignore collections
 | 
					
						
							|  |  |  | 	// 	base				- save only base data in each collection and
 | 
					
						
							|  |  |  | 	// 							the main collection is saved as current
 | 
					
						
							|  |  |  | 	// 	full				- full current state.
 | 
					
						
							|  |  |  | 	// 	
 | 
					
						
							|  |  |  | 	// NOTE: we do not store .collection_order here, because we order 
 | 
					
						
							|  |  |  | 	// 		the collections in the object.
 | 
					
						
							|  |  |  | 	// 		...when saving a partial collection set, for example in
 | 
					
						
							|  |  |  | 	// 		.prepareIndexForWrite(..) it would be necessary to add it 
 | 
					
						
							|  |  |  | 	// 		in to maintain the correct order when merging... (XXX)
 | 
					
						
							|  |  |  | 	// NOTE: currently this only stores title and data, it is the 
 | 
					
						
							|  |  |  | 	// 		responsibility of extending features to store their specific 
 | 
					
						
							|  |  |  | 	// 		stuff in collections...
 | 
					
						
							|  |  |  | 	// 		XXX is this the right way to go???
 | 
					
						
							|  |  |  | 	// NOTE: .chnages are handled separately in feature .handlers...
 | 
					
						
							|  |  |  | 	json: [function(mode){ return function(res){ | 
					
						
							|  |  |  | 		mode = mode || 'current' | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		var collections = this.collections || {} | 
					
						
							|  |  |  | 		var order = this.collection_order | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// NOTE: if mode is 'current' ignore collections...
 | 
					
						
							|  |  |  | 		if(mode != 'current' && order.length > 0){ | 
					
						
							|  |  |  | 			// NOTE: .collection_order does not return MAIN_COLLECTION_TITLE 
 | 
					
						
							|  |  |  | 			// 		so we have to add it in manually...
 | 
					
						
							|  |  |  | 			order = MAIN_COLLECTION_TITLE in collections ? | 
					
						
							|  |  |  | 				order.concat([MAIN_COLLECTION_TITLE]) | 
					
						
							|  |  |  | 				: order | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// in base mode save the main view as current...
 | 
					
						
							|  |  |  | 			if(mode == 'base' && this.collection){ | 
					
						
							|  |  |  | 				var main = collections[MAIN_COLLECTION_TITLE] | 
					
						
							|  |  |  | 				res.data =  (main.crop_stack ?  | 
					
						
							|  |  |  | 						(main.crop_stack[0] || main.data) | 
					
						
							|  |  |  | 						: main.data) | 
					
						
							|  |  |  | 					.json() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				delete res.location.collection } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			res.collections = {} | 
					
						
							|  |  |  | 			order.forEach(function(title){ | 
					
						
							|  |  |  | 				// in base mode skip the main collection...
 | 
					
						
							|  |  |  | 				if(mode == 'base'  | 
					
						
							|  |  |  | 						&& title == MAIN_COLLECTION_TITLE){ | 
					
						
							|  |  |  | 					return } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				var state = collections[title] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				// collection does not exist (default collection)...
 | 
					
						
							|  |  |  | 				// XXX
 | 
					
						
							|  |  |  | 				if(state == null){ | 
					
						
							|  |  |  | 					res.collections[title] = false | 
					
						
							|  |  |  | 					return } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				// build the JSON...
 | 
					
						
							|  |  |  | 				var s = res.collections[title] = { title: title } | 
					
						
							|  |  |  | 				if(state.gid){ | 
					
						
							|  |  |  | 					s.gid = state.gid } | 
					
						
							|  |  |  | 				var data = ((mode == 'base' && state.crop_stack) ?  | 
					
						
							|  |  |  | 						(state.crop_stack[0] || state.data) | 
					
						
							|  |  |  | 						: state.data) | 
					
						
							|  |  |  | 				if(data){ | 
					
						
							|  |  |  | 					s.data = data.json() | 
					
						
							|  |  |  | 					s.count = data.length | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				} else if(state.count) { | 
					
						
							|  |  |  | 					s.count = state.count } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				// handle .crop_stack of collection...
 | 
					
						
							|  |  |  | 				// NOTE: in base mode, crop_stack is ignored...
 | 
					
						
							|  |  |  | 				if(mode != 'base' && state.crop_stack){ | 
					
						
							|  |  |  | 					s.crop_stack = state.crop_stack | 
					
						
							|  |  |  | 						.map(function(d){ return d.json() }) } }) } } }], | 
					
						
							|  |  |  | 	clone: [function(full){ | 
					
						
							|  |  |  | 		return function(res){ | 
					
						
							|  |  |  | 			if(this.collections){ | 
					
						
							|  |  |  | 				var cur = this.collections | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				if(this.collection){ | 
					
						
							|  |  |  | 					res.location.collection = this.collection } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				collections = res.collections = {} | 
					
						
							|  |  |  | 				this.collection_order | 
					
						
							|  |  |  | 					.forEach(function(title){ | 
					
						
							|  |  |  | 						var c = collections[title] = { | 
					
						
							|  |  |  | 							title: title, | 
					
						
							|  |  |  | 						} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 						if(cur[title].data){ | 
					
						
							|  |  |  | 							c.data = cur[title].data.clone() } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 						if(cur[title].crop_stack){ | 
					
						
							|  |  |  | 							c.crop_stack = cur[title].crop_stack | 
					
						
							|  |  |  | 								.map(function(d){ return d.clone() }) } }) } } }], | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	clear: [function(){ | 
					
						
							|  |  |  | 		this.collection | 
					
						
							|  |  |  | 			&& this.collectionUnloaded('*') | 
					
						
							|  |  |  | 		delete this.collections | 
					
						
							|  |  |  | 		delete this.__collection_order | 
					
						
							|  |  |  | 		delete this.location.collection }], | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Config and interface stuff...
 | 
					
						
							|  |  |  | 	//
 | 
					
						
							|  |  |  | 	toggleCollectionCropRetention: ['Interface/Collection crop save mode', | 
					
						
							|  |  |  | 		{mode: 'advancedBrowseModeAction'}, | 
					
						
							|  |  |  | 		core.makeConfigToggler( | 
					
						
							|  |  |  | 			'collection-save-crop-state',  | 
					
						
							|  |  |  | 			[ | 
					
						
							|  |  |  | 				'all', | 
					
						
							|  |  |  | 				'main',  | 
					
						
							|  |  |  | 				'none', | 
					
						
							|  |  |  | 			])], | 
					
						
							|  |  |  | 	// XXX can we combine a toggler with list editor???
 | 
					
						
							|  |  |  | 	toggleCollections: ['- Collections/Collections', | 
					
						
							|  |  |  | 		toggler.Toggler(null, | 
					
						
							|  |  |  | 			function(_, state){ | 
					
						
							|  |  |  | 				return state == null ? | 
					
						
							|  |  |  | 					// cur state...
 | 
					
						
							|  |  |  | 					(this.collection  | 
					
						
							|  |  |  | 						|| MAIN_COLLECTION_TITLE) | 
					
						
							|  |  |  | 					// new state...
 | 
					
						
							|  |  |  | 					: (this.loadCollection(state)  | 
					
						
							|  |  |  | 						&& state) }, | 
					
						
							|  |  |  | 			function(){  | 
					
						
							|  |  |  | 				return [MAIN_COLLECTION_TITLE].concat(this.collection_order || []) })], | 
					
						
							|  |  |  | }) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | var Collection =  | 
					
						
							|  |  |  | module.Collection = core.ImageGridFeatures.Feature({ | 
					
						
							|  |  |  | 	title: '', | 
					
						
							|  |  |  | 	doc: '', | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	tag: 'collections', | 
					
						
							|  |  |  | 	depends: [ | 
					
						
							|  |  |  | 		'cache', | 
					
						
							|  |  |  | 		'base', | 
					
						
							|  |  |  | 		'location', | 
					
						
							|  |  |  | 		'crop', | 
					
						
							|  |  |  | 	], | 
					
						
							|  |  |  | 	suggested: [ | 
					
						
							|  |  |  | 		'collections-local-config', | 
					
						
							|  |  |  | 		'collection-tags', | 
					
						
							|  |  |  | 		'collection-marks', | 
					
						
							|  |  |  | 		'auto-collections', | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		'ui-collections', | 
					
						
							|  |  |  | 		'ui-collection-marks', | 
					
						
							|  |  |  | 		'fs-collections', | 
					
						
							|  |  |  | 	], | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	actions: CollectionActions,  | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	handlers: [ | 
					
						
							|  |  |  | 		// save before we serialize...
 | 
					
						
							|  |  |  | 		['json.pre', | 
					
						
							|  |  |  | 			function(){ this.saveCollection() }], | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Handle changes...
 | 
					
						
							|  |  |  | 		//
 | 
					
						
							|  |  |  | 		// 	Global tags:
 | 
					
						
							|  |  |  | 		// 		'collections'	- mark collection list as changed
 | 
					
						
							|  |  |  | 		// 		'collection: <gid>'	
 | 
					
						
							|  |  |  | 		// 						- collection-specific changes
 | 
					
						
							|  |  |  | 		//
 | 
					
						
							|  |  |  | 		// 	Collection-specific tags:
 | 
					
						
							|  |  |  | 		// 		'metadata'		- marks metadata as changed
 | 
					
						
							|  |  |  | 		// 							NOTE: this is ignored for the base 
 | 
					
						
							|  |  |  | 		// 								collection...
 | 
					
						
							|  |  |  | 		//
 | 
					
						
							|  |  |  | 		// 	Collection-local tags (see: .config['collection-transfer-changes']):
 | 
					
						
							|  |  |  | 		// 		'metadata'
 | 
					
						
							|  |  |  | 		// 		'data'
 | 
					
						
							|  |  |  | 		//
 | 
					
						
							|  |  |  | 		//
 | 
					
						
							|  |  |  | 		//	Mark collection list as changed...
 | 
					
						
							|  |  |  | 		// 	.markChanged('collections')
 | 
					
						
							|  |  |  | 		// 		NOTE: this will not affect collections...
 | 
					
						
							|  |  |  | 		// 		NOTE: this is useful alone when removing collections...
 | 
					
						
							|  |  |  | 		//
 | 
					
						
							|  |  |  | 		// 	Mark tag as changed when collection is loaded...
 | 
					
						
							|  |  |  | 		// 	.markChanged(<tag>)
 | 
					
						
							|  |  |  | 		// 		NOTE: this only applies to collection-local or specific tags...
 | 
					
						
							|  |  |  | 		//
 | 
					
						
							|  |  |  | 		// 	Mark tag as changed when collection is not loaded...
 | 
					
						
							|  |  |  | 		// 	.markChanged('collection: '+JSON.stringify(<gid>), [<tag>, ..])
 | 
					
						
							|  |  |  | 		//
 | 
					
						
							|  |  |  | 		//
 | 
					
						
							|  |  |  | 		// collection add/remove...
 | 
					
						
							|  |  |  | 		[[ | 
					
						
							|  |  |  | 			'collectionCreated', | 
					
						
							|  |  |  | 			'collectionRemoved', | 
					
						
							|  |  |  | 		], | 
					
						
							|  |  |  | 			function(_, collection){ | 
					
						
							|  |  |  | 				// collection list changed...
 | 
					
						
							|  |  |  | 				this.markChanged('collections') | 
					
						
							|  |  |  | 				// collection changed...
 | 
					
						
							|  |  |  | 				collection in this.collections | 
					
						
							|  |  |  | 					&& this.markChanged( | 
					
						
							|  |  |  | 						'collection: ' | 
					
						
							|  |  |  | 							+JSON.stringify(this.collections[collection].gid || collection)) | 
					
						
							|  |  |  | 			}], | 
					
						
							|  |  |  | 		// collection list sort...
 | 
					
						
							|  |  |  | 		['sortCollections.pre', | 
					
						
							|  |  |  | 			function(){ | 
					
						
							|  |  |  | 				var o = (this.collection_order || []).slice() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				return function(){ | 
					
						
							|  |  |  | 					;(this.collection_order || []) | 
					
						
							|  |  |  | 							.filter(function(e, i){ return e != o[i] }).length > 0 | 
					
						
							|  |  |  | 						&& this.markChanged('collections') } }], | 
					
						
							|  |  |  | 		// collection title/list...
 | 
					
						
							|  |  |  | 		['renameCollection', | 
					
						
							|  |  |  | 			function(_, from, to){ | 
					
						
							|  |  |  | 				this | 
					
						
							|  |  |  | 					.markChanged('collections') | 
					
						
							|  |  |  | 					.markChanged('collection: ' | 
					
						
							|  |  |  | 						+ JSON.stringify(this.collections[to].gid), ['metadata']) }], | 
					
						
							|  |  |  | 		// basic collection edits...
 | 
					
						
							|  |  |  | 		[[ | 
					
						
							|  |  |  | 			// NOTE: no need to handle .collect(..) here as it calls .joinCollect(..)
 | 
					
						
							|  |  |  | 			'joinCollect.pre', | 
					
						
							|  |  |  | 			'uncollect.pre', | 
					
						
							|  |  |  | 		],  | 
					
						
							|  |  |  | 			function(){ | 
					
						
							|  |  |  | 				var that = this | 
					
						
							|  |  |  | 				var args = [...arguments] | 
					
						
							|  |  |  | 				var title = (args.length == 1 ? args[0] : args[1]) || this.collection | 
					
						
							|  |  |  | 				var collection = (this.collections || {})[title] || {} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				var count = collection.data ?  | 
					
						
							|  |  |  | 					collection.data.length  | 
					
						
							|  |  |  | 					: collection.count | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				return function(){ | 
					
						
							|  |  |  | 					// NOTE: if a collection does not exist by this point 
 | 
					
						
							|  |  |  | 					// 		it will be handled by collection .collectionCreated(..)
 | 
					
						
							|  |  |  | 					// 		...this means we are either creating a new collection
 | 
					
						
							|  |  |  | 					// 		or removing from a non-existing collection.
 | 
					
						
							|  |  |  | 					title in this.collections | 
					
						
							|  |  |  | 						&& this.ensureCollection(title) | 
					
						
							|  |  |  | 							.then(function(){ | 
					
						
							|  |  |  | 								var new_count = collection.data ?  | 
					
						
							|  |  |  | 									collection.data.length  | 
					
						
							|  |  |  | 									: collection.count | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 								new_count != count | 
					
						
							|  |  |  | 									&& that.markChanged('collections') | 
					
						
							|  |  |  | 									&& that.markChanged( | 
					
						
							|  |  |  | 										'collection: ' | 
					
						
							|  |  |  | 											+JSON.stringify(collection.gid || title), | 
					
						
							|  |  |  | 										['data']) }) } }], | 
					
						
							|  |  |  | 		['joinCollect', | 
					
						
							|  |  |  | 			function(_, align, collection, data){ | 
					
						
							|  |  |  | 				var args = [...arguments] | 
					
						
							|  |  |  | 				var title = (args.length == 1 ? args[0] : args[1]) || this.collection | 
					
						
							|  |  |  | 				var collection = (this.collections || {})[title] || {} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				data = data || this.data | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				;(!data || data.ribbon_order.length > 1) | 
					
						
							|  |  |  | 					&& this.markChanged( | 
					
						
							|  |  |  | 						'collection: ' | 
					
						
							|  |  |  | 							+JSON.stringify(collection.gid || title), | 
					
						
							|  |  |  | 						['data']) }], | 
					
						
							|  |  |  | 		// transfer changes on load/unload collection...
 | 
					
						
							|  |  |  | 		['collectionLoading.pre', | 
					
						
							|  |  |  | 			function(to){ | 
					
						
							|  |  |  | 				var that = this | 
					
						
							|  |  |  | 				var from = this.collection || MAIN_COLLECTION_TITLE | 
					
						
							|  |  |  | 				if(from == to || this.changes === undefined || this.changes === true){ | 
					
						
							|  |  |  | 					return } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				var change_tags = this.config['collection-transfer-changes']  | 
					
						
							|  |  |  | 					|| COLLECTION_TRANSFER_CHANGES | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				var from_changes = change_tags | 
					
						
							|  |  |  | 					.filter(function(item){ | 
					
						
							|  |  |  | 						return that.changes === true || (that.changes || {})[item] }) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				return function(){ | 
					
						
							|  |  |  | 					if(to == from){ | 
					
						
							|  |  |  | 						return } | 
					
						
							|  |  |  | 					var gid = (this.collections[to] || {}).gid || to | 
					
						
							|  |  |  | 					var changes = this.changes !== false ?  | 
					
						
							|  |  |  | 						this.changes['collection: '+JSON.stringify(gid)]  | 
					
						
							|  |  |  | 						: [] | 
					
						
							|  |  |  | 					var from_id = 'collection: ' | 
					
						
							|  |  |  | 						+JSON.stringify(from == MAIN_COLLECTION_TITLE ? | 
					
						
							|  |  |  | 							MAIN_COLLECTION_GID | 
					
						
							|  |  |  | 							: this.collections[from].gid || from) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 					// everything has changed, no need to bother with details...
 | 
					
						
							|  |  |  | 					if(changes === true){ | 
					
						
							|  |  |  | 						return } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 					// save changes to 'from'...
 | 
					
						
							|  |  |  | 					from_changes.length > 0 | 
					
						
							|  |  |  | 						&& this.markChanged(from_id, from_changes) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 					// load changes from 'to'..
 | 
					
						
							|  |  |  | 					change_tags.forEach(function(item){ | 
					
						
							|  |  |  | 						if(changes && changes.indexOf(item) >= 0){ | 
					
						
							|  |  |  | 							that.markChanged(item) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 						} else if(that.changes && that.changes[item]){ | 
					
						
							|  |  |  | 							delete that.changes[item] } }) } }], | 
					
						
							|  |  |  | 		// update current collection changes...
 | 
					
						
							|  |  |  | 		//
 | 
					
						
							|  |  |  | 		// This will:
 | 
					
						
							|  |  |  | 		// 	1) update .changes['collection: <gid>'] with the current
 | 
					
						
							|  |  |  | 		// 		loaded .changes state...
 | 
					
						
							|  |  |  | 		// 	2) in 'base' mode, update the res.changes with base data 
 | 
					
						
							|  |  |  | 		// 		changes...
 | 
					
						
							|  |  |  | 		//
 | 
					
						
							|  |  |  | 		// NOTE: we do not need to do anything on the .load(..) side...
 | 
					
						
							|  |  |  | 		['json.pre',  | 
					
						
							|  |  |  | 			function(mode){ | 
					
						
							|  |  |  | 				var cur = this.collection || MAIN_COLLECTION_TITLE | 
					
						
							|  |  |  | 				if(cur == null || cur == MAIN_COLLECTION_TITLE){ | 
					
						
							|  |  |  | 					return } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				var changes = this.changes | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				// everything/nothing changed -- nothing to do...
 | 
					
						
							|  |  |  | 				if(!changes || changes === true || changes[cur] === true){ | 
					
						
							|  |  |  | 					return } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				var gid = this.collectionGID | 
					
						
							|  |  |  | 				var id = 'collection: '+ JSON.stringify(gid) | 
					
						
							|  |  |  | 				var change_tags = this.config['collection-transfer-changes'] | 
					
						
							|  |  |  | 					|| COLLECTION_TRANSFER_CHANGES | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				var changed = change_tags  | 
					
						
							|  |  |  | 					.filter(function(tag){ | 
					
						
							|  |  |  | 						return changes[tag] === true }) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				if(changed.length > 0 && this.changes[id] !== true){ | 
					
						
							|  |  |  | 					this.changes[id] = (this.changes[id] || []) | 
					
						
							|  |  |  | 						.concat(changed) | 
					
						
							|  |  |  | 						.unique() } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				// reset the base change tags to the base data (from collection data)...
 | 
					
						
							|  |  |  | 				if(mode == 'base'){ | 
					
						
							|  |  |  | 					return function(res){ | 
					
						
							|  |  |  | 						var base_id = 'collection: '+ JSON.stringify(MAIN_COLLECTION_GID) | 
					
						
							|  |  |  | 						var base = this.changes[base_id] || [] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 						// no need to save the base collection changes...
 | 
					
						
							|  |  |  | 						delete res.changes[base_id] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 						// clear...
 | 
					
						
							|  |  |  | 						change_tags.forEach(function(tag){ | 
					
						
							|  |  |  | 							delete res.changes[tag] }) | 
					
						
							|  |  |  | 						// set...
 | 
					
						
							|  |  |  | 						base.forEach(function(tag){ | 
					
						
							|  |  |  | 							res.changes[tag] = true }) } } }], | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Handle collection serialization format...
 | 
					
						
							|  |  |  | 		//
 | 
					
						
							|  |  |  | 		// Return format:
 | 
					
						
							|  |  |  | 		// 	{
 | 
					
						
							|  |  |  | 		// 		...
 | 
					
						
							|  |  |  | 		//
 | 
					
						
							|  |  |  | 		// 		// Collection gid-title index...
 | 
					
						
							|  |  |  | 		//		//
 | 
					
						
							|  |  |  | 		// 		// NOTE: this is sorted via .collection_order in .json(..)...
 | 
					
						
							|  |  |  | 		// 		// 
 | 
					
						
							|  |  |  | 		// 		// NOTE: if .collections is undefined this is not returned...
 | 
					
						
							|  |  |  | 		// 		// XXX this may cause issues if after removing the 
 | 
					
						
							|  |  |  | 		// 		//		last collection and .collections is deleted,
 | 
					
						
							|  |  |  | 		// 		//		then the last saved collection state will 
 | 
					
						
							|  |  |  | 		// 		//		get loaded instead of an empty collection list
 | 
					
						
							|  |  |  | 		// 		//		...currently this is not a problem as .collections
 | 
					
						
							|  |  |  | 		// 		//		is never explicitly set to undefined, but is a 
 | 
					
						
							|  |  |  | 		// 		//		potential pitfall...
 | 
					
						
							|  |  |  | 		// 		//		Q: should this return {} when .collections is undefined?
 | 
					
						
							|  |  |  | 		// 		collections: {
 | 
					
						
							|  |  |  | 		// 			// normal collection...
 | 
					
						
							|  |  |  | 		// 			<gid>: {
 | 
					
						
							|  |  |  | 		// 				title: <title>,
 | 
					
						
							|  |  |  | 		// 				count: <count>,
 | 
					
						
							|  |  |  | 		// 				...
 | 
					
						
							|  |  |  | 		// 			},
 | 
					
						
							|  |  |  | 		//
 | 
					
						
							|  |  |  | 		// 			// un-initialise default collection...
 | 
					
						
							|  |  |  | 		// 			//
 | 
					
						
							|  |  |  | 		// 			// i.e. a collection that is included in 
 | 
					
						
							|  |  |  | 		// 			// .config['default-collections'] and thus present in
 | 
					
						
							|  |  |  | 		// 			// .collection_order but not present in .collections
 | 
					
						
							|  |  |  | 		// 			<gid>: false,
 | 
					
						
							|  |  |  | 		//
 | 
					
						
							|  |  |  | 		// 			...
 | 
					
						
							|  |  |  | 		// 		}
 | 
					
						
							|  |  |  | 		//
 | 
					
						
							|  |  |  | 		// 		// Collection metadata...
 | 
					
						
							|  |  |  | 		// 		'collections/<gid>/metadata': {
 | 
					
						
							|  |  |  | 		// 			gid: <gid>,
 | 
					
						
							|  |  |  | 		// 			title: <title>,
 | 
					
						
							|  |  |  | 		// 			...
 | 
					
						
							|  |  |  | 		// 		},
 | 
					
						
							|  |  |  | 		//
 | 
					
						
							|  |  |  | 		// 		// Collection index data...
 | 
					
						
							|  |  |  | 		//		//
 | 
					
						
							|  |  |  | 		// 		// NOTE: this can contain the same tags as the root index
 | 
					
						
							|  |  |  | 		// 		//		this the collection format is the same as the 
 | 
					
						
							|  |  |  | 		// 		//		containing index format...
 | 
					
						
							|  |  |  | 		// 		// 		This is built by: 
 | 
					
						
							|  |  |  | 		// 		//			.prepareIndexForWrite(
 | 
					
						
							|  |  |  | 		// 		//				<collection-data>, 
 | 
					
						
							|  |  |  | 		// 		//				<collection-changes>)
 | 
					
						
							|  |  |  | 		// 		//		Where:
 | 
					
						
							|  |  |  | 		// 		//			<collection-data>
 | 
					
						
							|  |  |  | 		// 		//				taken as-is from .collections[gid] as 
 | 
					
						
							|  |  |  | 		// 		//				returned by .json(..)
 | 
					
						
							|  |  |  | 		// 		//			<collection-changes>
 | 
					
						
							|  |  |  | 		// 		//				built from .changes['collection: <gid>']
 | 
					
						
							|  |  |  | 		// 		//		And placed under path:
 | 
					
						
							|  |  |  | 		// 		//			collections/<gid>
 | 
					
						
							|  |  |  | 		// 		// NOTE: as the collection index is recursive, care must
 | 
					
						
							|  |  |  | 		// 		//		be taken when/if nested collections are needed
 | 
					
						
							|  |  |  | 		// 		//		to avoid self referencing...
 | 
					
						
							|  |  |  | 		// 		'collections/<gid>/<tag>': <tag-data>,
 | 
					
						
							|  |  |  | 		//
 | 
					
						
							|  |  |  | 		// 		...
 | 
					
						
							|  |  |  | 		// 	}
 | 
					
						
							|  |  |  | 		//
 | 
					
						
							|  |  |  | 		//
 | 
					
						
							|  |  |  | 		// NOTE: the base collection (MAIN_COLLECTION_TITLE) is not saved 
 | 
					
						
							|  |  |  | 		// 		in collections, it is stored in the root index...
 | 
					
						
							|  |  |  | 		//
 | 
					
						
							|  |  |  | 		// XXX we do not need .count in collection metadata as it is 
 | 
					
						
							|  |  |  | 		// 		stored in collections...
 | 
					
						
							|  |  |  | 		['prepareIndexForWrite',  | 
					
						
							|  |  |  | 			function(res){ | 
					
						
							|  |  |  | 				if(!res.changes){ | 
					
						
							|  |  |  | 					return } | 
					
						
							|  |  |  | 				var that = this | 
					
						
							|  |  |  | 				var changes = res.changes | 
					
						
							|  |  |  | 				var collections = this.collections | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				// collections fully/partially changed...
 | 
					
						
							|  |  |  | 				var full = changes === true ?  | 
					
						
							|  |  |  | 					Object.keys(collections || {}) | 
					
						
							|  |  |  | 					: Object.keys(collections || {}) | 
					
						
							|  |  |  | 						.filter(function(t){  | 
					
						
							|  |  |  | 							return res.changes['collection: ' | 
					
						
							|  |  |  | 								+ JSON.stringify(collections[t].gid)] === true }) | 
					
						
							|  |  |  | 				var partial = changes === true ? [] | 
					
						
							|  |  |  | 					: Object.keys(collections || {}) | 
					
						
							|  |  |  | 						.filter(function(t){  | 
					
						
							|  |  |  | 							return full.indexOf(t) < 0 | 
					
						
							|  |  |  | 								&& res.changes['collection: ' | 
					
						
							|  |  |  | 									+ JSON.stringify(collections[t].gid)] }) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				// collection index...
 | 
					
						
							|  |  |  | 				// NOTE: we are placing this in the index root to 
 | 
					
						
							|  |  |  | 				// 		simplify lazy-loading of the collection 
 | 
					
						
							|  |  |  | 				// 		index...
 | 
					
						
							|  |  |  | 				// NOTE: if there are no collections defined this section
 | 
					
						
							|  |  |  | 				// 		is skipped...
 | 
					
						
							|  |  |  | 				if(collections  | 
					
						
							|  |  |  | 						&& changes  | 
					
						
							|  |  |  | 						&& (changes === true  | 
					
						
							|  |  |  | 							|| changes.collections)){ | 
					
						
							|  |  |  | 					//var index = res.index['collection-index'] = {}
 | 
					
						
							|  |  |  | 					var index = res.index['collections'] = {} | 
					
						
							|  |  |  | 					// NOTE: we do not need to use .collection_order here
 | 
					
						
							|  |  |  | 					// 		as .json(..) returns the collections in the 
 | 
					
						
							|  |  |  | 					// 		correct order...
 | 
					
						
							|  |  |  | 					Object.keys(res.raw.collections || {}) | 
					
						
							|  |  |  | 						.forEach(function(title){  | 
					
						
							|  |  |  | 							if(title in collections){ | 
					
						
							|  |  |  | 								var gid = (collections[title] || {}).gid || title | 
					
						
							|  |  |  | 								var m = index[gid] = { title: title } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 								if(res.raw.collections[title].count){ | 
					
						
							|  |  |  | 									m['count'] = res.raw.collections[title].count } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 							// empty / default collections (placeholders)...
 | 
					
						
							|  |  |  | 							} else { | 
					
						
							|  |  |  | 								index[title] = false } }) } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				// collections...
 | 
					
						
							|  |  |  | 				if((full.length > 0 || partial.length > 0) | 
					
						
							|  |  |  | 						&& res.raw.collections){ | 
					
						
							|  |  |  | 					// select the actual changed collection list...
 | 
					
						
							|  |  |  | 					changed = changes === true ?  | 
					
						
							|  |  |  | 						Object.keys(res.raw.collections) | 
					
						
							|  |  |  | 						: (full).concat(partial) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 					var change_tags = this.config['collection-transfer-changes'] | 
					
						
							|  |  |  | 						|| COLLECTION_TRANSFER_CHANGES | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 					changed | 
					
						
							|  |  |  | 						// skip the raw field...
 | 
					
						
							|  |  |  | 						.filter(function(k){  | 
					
						
							|  |  |  | 							return res.raw.collections[k]  | 
					
						
							|  |  |  | 								&& changed.indexOf(k) >= 0 }) | 
					
						
							|  |  |  | 						.forEach(function(k){ | 
					
						
							|  |  |  | 							var gid = res.raw.collections[k].gid || k | 
					
						
							|  |  |  | 							var id = 'collection: '+ JSON.stringify(gid) | 
					
						
							|  |  |  | 							var path = 'collections/'+ gid | 
					
						
							|  |  |  | 							var raw = res.raw.collections[k] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 							// local collection changes...
 | 
					
						
							|  |  |  | 							var local_changes = partial.indexOf(k) < 0 || {} | 
					
						
							|  |  |  | 							if(local_changes !== true && res.changes[id] !== true){ | 
					
						
							|  |  |  | 								(res.changes[id] || []) | 
					
						
							|  |  |  | 									.forEach(function(c){  | 
					
						
							|  |  |  | 										local_changes[c] = true }) } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 							// collections/<gid>/metadata
 | 
					
						
							|  |  |  | 							var metadata = {} | 
					
						
							|  |  |  | 							if(full.indexOf(k) >= 0  | 
					
						
							|  |  |  | 									|| res.changes[id].indexOf('metadata') >= 0){ | 
					
						
							|  |  |  | 								res.index[path +'/metadata'] = metadata } | 
					
						
							|  |  |  | 							Object.keys(raw) | 
					
						
							|  |  |  | 								.forEach(function(key){ metadata[key] = raw[key] }) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 							raw.date = res.date | 
					
						
							|  |  |  | 							var prepared = that.prepareIndexForWrite(raw, local_changes).index | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 							// move the collection data to collection path...
 | 
					
						
							|  |  |  | 							Object.keys(prepared) | 
					
						
							|  |  |  | 								.filter(function(key){ return key != 'collections' }) | 
					
						
							|  |  |  | 								.forEach(function(key){ | 
					
						
							|  |  |  | 									res.index[path +'/'+ key] = prepared[key] | 
					
						
							|  |  |  | 									delete metadata[key] }) | 
					
						
							|  |  |  | 							// cleanup metadata...
 | 
					
						
							|  |  |  | 							// XXX do we need this???
 | 
					
						
							|  |  |  | 							change_tags.forEach(function(key){ | 
					
						
							|  |  |  | 								delete metadata[key] }) }) } }], | 
					
						
							|  |  |  | 		// XXX merge multiple collections...
 | 
					
						
							|  |  |  | 		// 		...this can be called multiple times pre single load, once
 | 
					
						
							|  |  |  | 		// 		per merged index...
 | 
					
						
							|  |  |  | 		['prepareIndexForLoad', | 
					
						
							|  |  |  | 			function(res, json, base_path){ | 
					
						
							|  |  |  | 				// collection index...
 | 
					
						
							|  |  |  | 				var collections = {} | 
					
						
							|  |  |  | 				var collections_index = {} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				var index = json['collections'] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				if(index){ | 
					
						
							|  |  |  | 					// get collection order...
 | 
					
						
							|  |  |  | 					var order = Object.keys(index) | 
					
						
							|  |  |  | 						.map(function(k){  | 
					
						
							|  |  |  | 							return index[k] ? index[k].gid || index[k].title || k : k }) | 
					
						
							|  |  |  | 					if(order.length > 0){ | 
					
						
							|  |  |  | 						res.collection_order = order } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 					// collection data...
 | 
					
						
							|  |  |  | 					Object.keys(index).forEach(function(gid){ | 
					
						
							|  |  |  | 						if(index[gid] === false){ | 
					
						
							|  |  |  | 							return } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 						//var title = index[gid]
 | 
					
						
							|  |  |  | 						var title = index[gid].title || index[gid] | 
					
						
							|  |  |  | 						var path = 'collections/'+ gid | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 						var m = collections_index[gid] = collections[title] = { | 
					
						
							|  |  |  | 							gid: gid, | 
					
						
							|  |  |  | 							title: title, | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 							// XXX
 | 
					
						
							|  |  |  | 							path: path, | 
					
						
							|  |  |  | 						} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 						if(index[gid].count){ | 
					
						
							|  |  |  | 							m.count = index[gid].count } }) } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				if(Object.keys(collections).length > 0){ | 
					
						
							|  |  |  | 					res.collections = collections } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				// group collection data...
 | 
					
						
							|  |  |  | 				//
 | 
					
						
							|  |  |  | 				// NOTE: this will load collections/* stuff if present...
 | 
					
						
							|  |  |  | 				//
 | 
					
						
							|  |  |  | 				// XXX would be nice to have a mechanism to pass info to 
 | 
					
						
							|  |  |  | 				// 		the loader on what paths to load without actually 
 | 
					
						
							|  |  |  | 				// 		loading them manually...
 | 
					
						
							|  |  |  | 				// 		...without this mechanism the data used here would
 | 
					
						
							|  |  |  | 				// 		not exist...
 | 
					
						
							|  |  |  | 				var collection_data = {} | 
					
						
							|  |  |  | 				Object.keys(json) | 
					
						
							|  |  |  | 					.filter(function(k){ return k.startsWith('collections/') }) | 
					
						
							|  |  |  | 					.forEach(function(k){ | 
					
						
							|  |  |  | 						var s = k.split(/[\\\/]+/g).slice(1) | 
					
						
							|  |  |  | 						var gid = s.shift() | 
					
						
							|  |  |  | 						var key = s.shift() | 
					
						
							|  |  |  | 						var title = collections_index[gid].title | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 						// load only collections in index...
 | 
					
						
							|  |  |  | 						if(title){ | 
					
						
							|  |  |  | 							var data = collection_data[gid] = collection_data[gid] || {} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 							// overwrite metadata...
 | 
					
						
							|  |  |  | 							if(key == 'metadata'){ | 
					
						
							|  |  |  | 								collections_index[gid] = collections[title] = json[k] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 							// other stuff -> collection data...
 | 
					
						
							|  |  |  | 							} else { | 
					
						
							|  |  |  | 								data[key] = json[k] } } }) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				// XXX prepare collection data for loading...
 | 
					
						
							|  |  |  | 				Object.keys(collection_data) | 
					
						
							|  |  |  | 					.forEach(function(gid){ | 
					
						
							|  |  |  | 						// XXX would be nice to be able to use .prepareIndexForLoad(..) 
 | 
					
						
							|  |  |  | 						// 		to handle collection internals produced by
 | 
					
						
							|  |  |  | 						// 		.prepareIndexForLoad(..)...
 | 
					
						
							|  |  |  | 						// 		...would need to pass it the local data...
 | 
					
						
							|  |  |  | 						// XXX
 | 
					
						
							|  |  |  | 					}) | 
					
						
							|  |  |  | 			}], | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// invalidate caches...
 | 
					
						
							|  |  |  | 		[[ | 
					
						
							|  |  |  | 			'loadCollection', | 
					
						
							|  |  |  | 			'uncollect', | 
					
						
							|  |  |  | 		], | 
					
						
							|  |  |  | 			'clearCache: "view(-.*)?" "*" -- Clear view cache'], | 
					
						
							|  |  |  | 	], | 
					
						
							|  |  |  | }) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | //---------------------------------------------------------------------
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | var CollectionLocalConfig = actions.Actions({ | 
					
						
							|  |  |  | 	config: { | 
					
						
							|  |  |  | 		// XXX should this be user editable???
 | 
					
						
							|  |  |  | 		// XXX should/can this be local to collection???
 | 
					
						
							|  |  |  | 		'collection-local-config': [ | 
					
						
							|  |  |  | 		], | 
					
						
							|  |  |  | 	}, | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// handle collection .config
 | 
					
						
							|  |  |  | 	// XXX problems:
 | 
					
						
							|  |  |  | 	// 		- config leaks -- when moving crom collection to collection 
 | 
					
						
							|  |  |  | 	// 			with individual option sets some options may not get 
 | 
					
						
							|  |  |  | 	// 			restored if handled incorrectly...
 | 
					
						
							|  |  |  | 	// 			...one way to deal with this is to restore the base config
 | 
					
						
							|  |  |  | 	// 			on every load before loading the new config...
 | 
					
						
							|  |  |  | 	collectionConfigLoader: ['- Collections/', | 
					
						
							|  |  |  | 		{collectionFormat: 'config'}, | 
					
						
							|  |  |  | 		function(title, state, logger){  | 
					
						
							|  |  |  | 			// XXX save old config -- in their respective collection...
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// XXX load MAIN_COLLECTION_TITLE config...
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// XXX load new config -- from target collection... 
 | 
					
						
							|  |  |  | 		}], | 
					
						
							|  |  |  | }) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | var CollectionLocalConfig =  | 
					
						
							|  |  |  | module.CollectionLocalConfig = core.ImageGridFeatures.Feature({ | 
					
						
							|  |  |  | 	title: '', | 
					
						
							|  |  |  | 	doc: '', | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	tag: 'collections-local-config', | 
					
						
							|  |  |  | 	depends: [ | 
					
						
							|  |  |  | 		'collections', | 
					
						
							|  |  |  | 	], | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	handlers: [ | 
					
						
							|  |  |  | 		/* XXX | 
					
						
							|  |  |  | 		['collectionLoading.pre', | 
					
						
							|  |  |  | 			function(){ | 
					
						
							|  |  |  | 				var that = this | 
					
						
							|  |  |  | 				var state = {} | 
					
						
							|  |  |  | 				var opts = this.config['collection-local-config'] || [] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				// save outgoing collection state...
 | 
					
						
							|  |  |  | 				var cfg = {} | 
					
						
							|  |  |  | 				opts.forEach(function(n){ | 
					
						
							|  |  |  | 					cfg[n] = JSON.parse(JSON.stringify(that.config[n]))  | 
					
						
							|  |  |  | 				}) | 
					
						
							|  |  |  | 			}], | 
					
						
							|  |  |  | 		//*/
 | 
					
						
							|  |  |  | 	], | 
					
						
							|  |  |  | }) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | //---------------------------------------------------------------------
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | var CollectionTagsActions = actions.Actions({ | 
					
						
							|  |  |  | 	config: { | 
					
						
							|  |  |  | 		// List of tags to be stored in a collection, unique to it...
 | 
					
						
							|  |  |  | 		//
 | 
					
						
							|  |  |  | 		// NOTE: the rest of the tags are shared between all collections
 | 
					
						
							|  |  |  | 		// NOTE: to disable local tags either delete this, set it to null
 | 
					
						
							|  |  |  | 		// 		or to an empty list.
 | 
					
						
							|  |  |  | 		'collection-local-tags': [], | 
					
						
							|  |  |  | 		 | 
					
						
							|  |  |  | 		/* | 
					
						
							|  |  |  | 		'collection-transfer-changes':  | 
					
						
							|  |  |  | 			// XXX need a way to exrtend config values in order of merge
 | 
					
						
							|  |  |  | 			// 		and not manually...
 | 
					
						
							|  |  |  | 			CollectionActions.config['collection-transfer-changes'] | 
					
						
							|  |  |  | 				.concat([ | 
					
						
							|  |  |  | 				]), | 
					
						
							|  |  |  | 		//*/
 | 
					
						
							|  |  |  | 	}, | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	collectTagged: ['- Collections|Tag/', | 
					
						
							|  |  |  | 		function(query, collection){ | 
					
						
							|  |  |  | 			return this.collect(this.data.tagQuery(query), collection) }], | 
					
						
							|  |  |  | 	uncollectTagged: ['- Collections|Tag/', | 
					
						
							|  |  |  | 		function(query, collection){ | 
					
						
							|  |  |  | 			return this.uncollect(this.data.tagQuery(query), collection) }], | 
					
						
							|  |  |  | }) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // XXX need to either extend this to use the tag API to support the 
 | 
					
						
							|  |  |  | // 		compound tags or explicitly restrict this to specific tags...
 | 
					
						
							|  |  |  | // 		...currently this in places uses the API and in other places
 | 
					
						
							|  |  |  | // 		directly accesses .__index -- this may lead to odd cases where
 | 
					
						
							|  |  |  | // 		not all tags get loaded/unloaded in spite of correctly conforming 
 | 
					
						
							|  |  |  | // 		to the API specs...
 | 
					
						
							|  |  |  | var CollectionTags =  | 
					
						
							|  |  |  | module.CollectionTags = core.ImageGridFeatures.Feature({ | 
					
						
							|  |  |  | 	title: 'Collection tag handling', | 
					
						
							|  |  |  | 	doc: core.doc`
 | 
					
						
							|  |  |  | 	What this does: | 
					
						
							|  |  |  | 	- Makes tags global through all collections | 
					
						
							|  |  |  | 	- Handles local tags per collection | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	Global tags: | 
					
						
							|  |  |  | 	------------ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	Global tags are shared through all the collections, this helps keep | 
					
						
							|  |  |  | 	image-specific tags, keywords and meta-information stored in tags  | 
					
						
							|  |  |  | 	global, i.e. connected to specific image and not collection.  | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	Global tags are stored in .data.tags and cleared out of from the  | 
					
						
							|  |  |  | 	collection's:  | 
					
						
							|  |  |  | 		.collections[<title>].data | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	Collection local tags: | 
					
						
							|  |  |  | 	---------------------- | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	Local tags are listed in .config['collection-local-tags'], this makes | 
					
						
							|  |  |  | 	selection, bookmarking and other process related tags local to each  | 
					
						
							|  |  |  | 	collection. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	Collection-local tags are stored in .collections[<title>].local_tags | 
					
						
							|  |  |  | 	and overwrite the corresponding tags in .data.tags on collection load. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	`,
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	tag: 'collection-tags', | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	depends: [ | 
					
						
							|  |  |  | 		'collections', | 
					
						
							|  |  |  | 		'tags', | 
					
						
							|  |  |  | 	], | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	actions: CollectionTagsActions, | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	handlers: [ | 
					
						
							|  |  |  | 		// move tags between collections...
 | 
					
						
							|  |  |  | 		['collectionLoading.pre', | 
					
						
							|  |  |  | 			function(title){ | 
					
						
							|  |  |  | 				var that = this | 
					
						
							|  |  |  | 				var local_tag_names = this.config['collection-local-tags'] || [] | 
					
						
							|  |  |  | 				var tags = this.data.tags | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				// NOTE: this is done at the .pre stage as we need to grab 
 | 
					
						
							|  |  |  | 				// 		the tags BEFORE the data gets cleared (in the case 
 | 
					
						
							|  |  |  | 				// 		of MAIN_COLLECTION_TITLE)...
 | 
					
						
							|  |  |  | 				var local_tags = (this.collections[title] || {}).local_tags || {} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				return function(){ | 
					
						
							|  |  |  | 					tags.__index = tags.__index || {} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 					// load local_tags...
 | 
					
						
							|  |  |  | 					local_tag_names | 
					
						
							|  |  |  | 						.forEach(function(tag){  | 
					
						
							|  |  |  | 							/* XXX for some reason this does not work... | 
					
						
							|  |  |  | 							tags.tag(tag, [...(local_tags[tag]  | 
					
						
							|  |  |  | 									|| that.data.tags.values(tag))]) | 
					
						
							|  |  |  | 							/*/ | 
					
						
							|  |  |  | 							// XXX this is not correct as we can have mixed tags...
 | 
					
						
							|  |  |  | 							// 		...use actual tag API...
 | 
					
						
							|  |  |  | 							tags.__index[tag] = new Set(local_tags[tag]  | 
					
						
							|  |  |  | 								|| (that.data.tags.__index || {})[tag]  | 
					
						
							|  |  |  | 								|| []) | 
					
						
							|  |  |  | 							//*/
 | 
					
						
							|  |  |  | 						}) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 					;(this.crop_stack || []) | 
					
						
							|  |  |  | 						.forEach(function(d){ d.tags = tags }) | 
					
						
							|  |  |  | 					this.data.tags = tags } }], | 
					
						
							|  |  |  | 		// remove tags from unloaded collections...
 | 
					
						
							|  |  |  | 		['collectionUnloaded', | 
					
						
							|  |  |  | 			function(_, title){ | 
					
						
							|  |  |  | 				if(title in this.collections  | 
					
						
							|  |  |  | 						&& 'data' in this.collections[title]){ | 
					
						
							|  |  |  | 					delete this.collections[title].data.tags } }], | 
					
						
							|  |  |  | 		// remove tags when saving...
 | 
					
						
							|  |  |  | 		['saveCollection.pre', | 
					
						
							|  |  |  | 			function(title, mode, force){ | 
					
						
							|  |  |  | 				var that = this | 
					
						
							|  |  |  | 				title = title || this.collection || MAIN_COLLECTION_TITLE | 
					
						
							|  |  |  | 				var local_tag_names = this.config['collection-local-tags'] || [] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				// do not do anything for main collection unless force is true...
 | 
					
						
							|  |  |  | 				if(title == MAIN_COLLECTION_TITLE && !force){ | 
					
						
							|  |  |  | 					return } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				// we need this to prevent copy of tags on first save...
 | 
					
						
							|  |  |  | 				var new_set = !(title in (this.collections || {})) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				return function(){ | 
					
						
							|  |  |  | 					// save local tags...
 | 
					
						
							|  |  |  | 					var local_tags = this.collections[title].local_tags = {} | 
					
						
							|  |  |  | 					local_tag_names | 
					
						
							|  |  |  | 						.forEach(function(tag){  | 
					
						
							|  |  |  | 							/* XXX not sure which approach is better,  | 
					
						
							|  |  |  | 							//		API vs. direct .__index edit...
 | 
					
						
							|  |  |  | 							local_tags[tag] = (!new_set || title == MAIN_COLLECTION_TITLE) ?  | 
					
						
							|  |  |  | 								// XXX this might yield a slightly wider set of values...
 | 
					
						
							|  |  |  | 								new Set(that.data.tags.values(tag)) | 
					
						
							|  |  |  | 								: new Set() | 
					
						
							|  |  |  | 							/*/ | 
					
						
							|  |  |  | 							// XXX this is not correct as we can have mixed tags...
 | 
					
						
							|  |  |  | 							// 		...use actual tag API...
 | 
					
						
							|  |  |  | 							local_tags[tag] = (!new_set || title == MAIN_COLLECTION_TITLE) ?  | 
					
						
							|  |  |  | 								[...(that.data.tags.__index || {})[tag] || []] | 
					
						
							|  |  |  | 								: [] | 
					
						
							|  |  |  | 							//*/
 | 
					
						
							|  |  |  | 						}) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 					// delete the .data.tags of the collections...
 | 
					
						
							|  |  |  | 					delete (this.collections[title].data || {}).__tags || {} } | 
					
						
							|  |  |  | 			}], | 
					
						
							|  |  |  | 		// prevent .uncollect(..) from removing global tags...
 | 
					
						
							|  |  |  | 		// XXX this seems a bit hacky (???)
 | 
					
						
							|  |  |  | 		['uncollect.pre', | 
					
						
							|  |  |  | 			function(_, gids, title){ | 
					
						
							|  |  |  | 				var that = this | 
					
						
							|  |  |  | 				var local_tag_names = this.config['collection-local-tags'] || [] | 
					
						
							|  |  |  | 				gids = gids || this.current | 
					
						
							|  |  |  | 				gids = gids instanceof Array ? gids : [gids] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				// prevent global tag removal...
 | 
					
						
							|  |  |  | 				var tags = this.data.tags | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				return function(){ | 
					
						
							|  |  |  | 					// update local tags...
 | 
					
						
							|  |  |  | 					tags.untag(local_tag_names, gids) } }], | 
					
						
							|  |  |  | 		// save .local_tags to json...
 | 
					
						
							|  |  |  | 		// NOTE: we do not need to explicitly load anything as .load() 
 | 
					
						
							|  |  |  | 		// 		will load everything we need...
 | 
					
						
							|  |  |  | 		['json', | 
					
						
							|  |  |  | 			function(res, mode){ | 
					
						
							|  |  |  | 				var c = this.collections | 
					
						
							|  |  |  | 				var rc = res.collections | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				// NOTE: at this point .crop_stack is handled, so we 
 | 
					
						
							|  |  |  | 				// 		do not need to care about it...
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				// in 'base' mode set .data.tags and .local_tags to 
 | 
					
						
							|  |  |  | 				// the base collection data...
 | 
					
						
							|  |  |  | 				if(mode == 'base'  | 
					
						
							|  |  |  | 						&& this.collection != null | 
					
						
							|  |  |  | 						&& this.collection != MAIN_COLLECTION_TITLE){ | 
					
						
							|  |  |  | 					var tags = this.data.tags.json() | 
					
						
							|  |  |  | 					var ltags = c[MAIN_COLLECTION_TITLE].local_tags || {} | 
					
						
							|  |  |  | 					var rtags = res.data.tags.tags = {} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 					// move all the tags...
 | 
					
						
							|  |  |  | 					Object.keys(tags.tags) | 
					
						
							|  |  |  | 						.filter(function(tag){ return ltags[tag] == null }) | 
					
						
							|  |  |  | 						.forEach(function(tag){ rtags[tag] = tags.tags[tag] }) | 
					
						
							|  |  |  | 					// overwrite the local tags for the base...
 | 
					
						
							|  |  |  | 					Object.keys(ltags) | 
					
						
							|  |  |  | 						.forEach(function(tag){ rtags[tag] = ltags[tag] }) } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				// clear tags for all collections...
 | 
					
						
							|  |  |  | 				rc | 
					
						
							|  |  |  | 					&& Object.keys(rc || {}) | 
					
						
							|  |  |  | 						// XXX skip unloaded collections...
 | 
					
						
							|  |  |  | 						.filter(function(title){ return !!rc[title].data }) | 
					
						
							|  |  |  | 						.forEach(function(title){ | 
					
						
							|  |  |  | 							rc[title].data.tags.tags = c[title].local_tags }) }], | 
					
						
							|  |  |  | 		// load collection local tags from .data.tags to .local_tags...
 | 
					
						
							|  |  |  | 		// ...this is needed if the collections are fully loaded as part 
 | 
					
						
							|  |  |  | 		// of the index...
 | 
					
						
							|  |  |  | 		// XXX do we actually need this???
 | 
					
						
							|  |  |  | 		['load', | 
					
						
							|  |  |  | 			function(_, json){ | 
					
						
							|  |  |  | 				var that = this | 
					
						
							|  |  |  | 				this.collections | 
					
						
							|  |  |  | 					&& Object.keys(json.collections || {}) | 
					
						
							|  |  |  | 						// skip loaded collections that are already Data objects...
 | 
					
						
							|  |  |  | 						// XXX not sure about this...
 | 
					
						
							|  |  |  | 						.filter(function(title){ | 
					
						
							|  |  |  | 							return !(json.collections[title].data instanceof data.Data) }) | 
					
						
							|  |  |  | 						// do the loading...
 | 
					
						
							|  |  |  | 						.forEach(function(title){ | 
					
						
							|  |  |  | 							var c = that.collections[title] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 							if(!c || !c.data){ | 
					
						
							|  |  |  | 								return } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 							var t = (c.data.tags || {}).tags || {} | 
					
						
							|  |  |  | 							var lt = c.local_tags = c.local_tags || {} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 							;(that.config['collection-local-tags'] || []) | 
					
						
							|  |  |  | 								.forEach(function(tag){ | 
					
						
							|  |  |  | 									lt[tag] = new Set(lt[tag] || t[tag] || []) }) }) }], | 
					
						
							|  |  |  | 	], | 
					
						
							|  |  |  | }) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | //---------------------------------------------------------------------
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // XXX add UI...
 | 
					
						
							|  |  |  | // XXX removing items from auto-collection has no effect as it will be 
 | 
					
						
							|  |  |  | // 		reconstructed on next load -- is this the right way to go???
 | 
					
						
							|  |  |  | var AutoCollectionsActions = actions.Actions({ | 
					
						
							|  |  |  | 	collectionAutoLevelLoader: ['- Collections/', | 
					
						
							|  |  |  | 		core.doc`
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		`,
 | 
					
						
							|  |  |  | 		{collectionFormat: 'level_query'}, | 
					
						
							|  |  |  | 		function(title, state){  | 
					
						
							|  |  |  | 			return new Promise((function(resolve){ | 
					
						
							|  |  |  | 				var source = state.source || MAIN_COLLECTION_TITLE | 
					
						
							|  |  |  | 				source = source == MAIN_COLLECTION_TITLE ?  | 
					
						
							|  |  |  | 					((this.crop_stack || [])[0]  | 
					
						
							|  |  |  | 						|| this.data) | 
					
						
							|  |  |  | 					// XXX need a way to preload collection data...
 | 
					
						
							|  |  |  | 					: ((this.collection[source].crop_stack || [])[0]  | 
					
						
							|  |  |  | 						|| this.collections[source].data) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				var query = state.level_query | 
					
						
							|  |  |  | 				query = query == 'top' ?  | 
					
						
							|  |  |  | 						[0, 1] | 
					
						
							|  |  |  | 					: query == 'bottom' ? | 
					
						
							|  |  |  | 						[-1] | 
					
						
							|  |  |  | 					: query instanceof Array ?  | 
					
						
							|  |  |  | 						query | 
					
						
							|  |  |  | 					: typeof(query) == typeof('str') ?  | 
					
						
							|  |  |  | 						query.split('+').map(function(e){ return e.trim() }) | 
					
						
							|  |  |  | 					: query > 0 ?  | 
					
						
							|  |  |  | 						[0, query] | 
					
						
							|  |  |  | 					: [query] | 
					
						
							|  |  |  | 				query = query[0] == 'top' ? | 
					
						
							|  |  |  | 						[0, parseInt(query[1])+1] | 
					
						
							|  |  |  | 					: query[0] == 'bottom' ? | 
					
						
							|  |  |  | 						[-parseInt(query[1])-1] | 
					
						
							|  |  |  | 					: query | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				var levels = source.ribbon_order.slice.apply(source.ribbon_order, query) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				var gids = [] | 
					
						
							|  |  |  | 				levels.forEach(function(gid){ | 
					
						
							|  |  |  | 					source.makeSparseImages(source.ribbons[gid], gids) }) | 
					
						
							|  |  |  | 				gids = gids.compact() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				// get items that topped matching the query...
 | 
					
						
							|  |  |  | 				var remove = state.data ? | 
					
						
							|  |  |  | 					state.data.order | 
					
						
							|  |  |  | 						.filter(function(gid){ return gids.indexOf(gid) < 0 }) | 
					
						
							|  |  |  | 					: [] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				// build data...
 | 
					
						
							|  |  |  | 				state.data = data.Data.fromArray(gids) | 
					
						
							|  |  |  | 					// join with saved state...
 | 
					
						
							|  |  |  | 					.join(state.data || data.Data()) | 
					
						
							|  |  |  | 					// remove unmatching...
 | 
					
						
							|  |  |  | 					.clear(remove) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				resolve() | 
					
						
							|  |  |  | 			}).bind(this)) }], | 
					
						
							|  |  |  | 	makeAutoLevelCollection: ['- Collections/', | 
					
						
							|  |  |  | 		core.doc`Make level auto-collection...
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		`,
 | 
					
						
							|  |  |  | 		function(title, source, a, b){ | 
					
						
							|  |  |  | 			// XXX query 
 | 
					
						
							|  |  |  | 			var query = b != null ? [a, b] : a | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			this.saveCollection(title, 'empty') | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			this.collections[title].level_query = query | 
					
						
							|  |  |  | 			this.collections[title].source = source }], | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// XXX do we need real tag queries???
 | 
					
						
							|  |  |  | 	collectionAutoTagsLoader: ['- Collections/', | 
					
						
							|  |  |  | 		core.doc`
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		NOTE: this will ignore local tags. | 
					
						
							|  |  |  | 		NOTE: this will prepend new matching items to the saved state. | 
					
						
							|  |  |  | 		`,
 | 
					
						
							|  |  |  | 		{collectionFormat: 'tag_query'}, | 
					
						
							|  |  |  | 		function(title, state){  | 
					
						
							|  |  |  | 			return new Promise((function(resolve){ | 
					
						
							|  |  |  | 				var local_tag_names = this.config['collection-local-tags'] || [] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				var tags = (state.tag_query || []) | 
					
						
							|  |  |  | 					.filter(function(tag){  | 
					
						
							|  |  |  | 						return local_tag_names.indexOf(tag) < 0 }) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				var gids = this.data.tagQuery(tags) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				// get items that topped matching the query...
 | 
					
						
							|  |  |  | 				var remove = state.data ? | 
					
						
							|  |  |  | 					state.data.order | 
					
						
							|  |  |  | 						.filter(function(gid){ return gids.indexOf(gid) < 0 }) | 
					
						
							|  |  |  | 					: [] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				// build data...
 | 
					
						
							|  |  |  | 				state.data = data.Data.fromArray(gids) | 
					
						
							|  |  |  | 					// join with saved state...
 | 
					
						
							|  |  |  | 					.join(state.data || data.Data()) | 
					
						
							|  |  |  | 					// remove unmatching...
 | 
					
						
							|  |  |  | 					.clear(remove) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				resolve() | 
					
						
							|  |  |  | 			}).bind(this)) }], | 
					
						
							|  |  |  | 	makeAutoTagCollection: ['- Collections/', | 
					
						
							|  |  |  | 		core.doc`Make tag auto-collection...
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			Make a tag auto-collection... | 
					
						
							|  |  |  | 			.makeAutoTagCollection(title, tag) | 
					
						
							|  |  |  | 			.makeAutoTagCollection(title, tag, tag, ..) | 
					
						
							|  |  |  | 			.makeAutoTagCollection(title, [tag, tag, ..]) | 
					
						
							|  |  |  | 				-> this | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		NOTE: at least one tag must be supplied... | 
					
						
							|  |  |  | 		`,
 | 
					
						
							|  |  |  | 		function(title, tags){ | 
					
						
							|  |  |  | 			tags = arguments.length > 2 ? [...arguments].slice(1) : tags | 
					
						
							|  |  |  | 			tags = tags instanceof Array ? tags : [tags] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			if(tags.length == 0){ | 
					
						
							|  |  |  | 				return } | 
					
						
							|  |  |  | 			this.saveCollection(title, 'empty') | 
					
						
							|  |  |  | 			this.collections[title].tag_query = tags }], | 
					
						
							|  |  |  | }) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | var AutoCollections = | 
					
						
							|  |  |  | module.AutoCollections = core.ImageGridFeatures.Feature({ | 
					
						
							|  |  |  | 	title: 'Auto collections', | 
					
						
							|  |  |  | 	doc: core.doc`
 | 
					
						
							|  |  |  | 	A collection is different from a crop in that it: | 
					
						
							|  |  |  | 		- preserves ribbon state | 
					
						
							|  |  |  | 		- preserves order | 
					
						
							|  |  |  | 		- preserves local tags | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	Tag changes are handled by removing images that were untagged (no  | 
					
						
							|  |  |  | 	longer matching) from the collection and adding newly tagged/matching  | 
					
						
							|  |  |  | 	images to collection. | 
					
						
							|  |  |  | 	`,
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	tag: 'auto-collections', | 
					
						
							|  |  |  | 	depends: [ | 
					
						
							|  |  |  | 		'collections', | 
					
						
							|  |  |  | 	], | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	actions: AutoCollectionsActions, | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	handlers: [ | 
					
						
							|  |  |  | 		['json', | 
					
						
							|  |  |  | 			function(res){ | 
					
						
							|  |  |  | 				var c = this.collections || {} | 
					
						
							|  |  |  | 				var rc = res.collections || {} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				Object.keys(rc) | 
					
						
							|  |  |  | 					.forEach(function(title){ | 
					
						
							|  |  |  | 						var cur = c[title] | 
					
						
							|  |  |  | 						var r = rc[title] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 						if(!cur){ | 
					
						
							|  |  |  | 							return } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 						// XXX is this the right way to go???
 | 
					
						
							|  |  |  | 						if('tag_query' in cur){ | 
					
						
							|  |  |  | 							r.tag_query = cur.tag_query | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 						} else if('level_query' in cur){ | 
					
						
							|  |  |  | 							r.level_query = cur.level_query | 
					
						
							|  |  |  | 							if(cur.source){ | 
					
						
							|  |  |  | 								r.source = cur.source } } }) }], | 
					
						
							|  |  |  | 	], | 
					
						
							|  |  |  | }) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | //---------------------------------------------------------------------
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Make an action that when called without enough arguments show a 
 | 
					
						
							|  |  |  | // collection selector dialog and just call the given function when 
 | 
					
						
							|  |  |  | // enough arguments are given.
 | 
					
						
							|  |  |  | //
 | 
					
						
							|  |  |  | // NOTE: if n > 1 and <n args are given then the given args will get 
 | 
					
						
							|  |  |  | // 		passed to func with an appended title...
 | 
					
						
							|  |  |  | // XXX should we use options object here a-la .browseCollections(..)???
 | 
					
						
							|  |  |  | var mixedModeCollectionAction = function(func, n, options){ | 
					
						
							|  |  |  | 	return widgets.uiDialog(function(){ | 
					
						
							|  |  |  | 		var args = [...arguments] | 
					
						
							|  |  |  | 		// check if minimum number of arguments is reached...
 | 
					
						
							|  |  |  | 		return args.length < (n || 1) ?  | 
					
						
							|  |  |  | 			// show the dialog...
 | 
					
						
							|  |  |  | 			this.browseCollections(function(title){  | 
					
						
							|  |  |  | 					return func.call(this, ...args.concat([title])) }, options)  | 
					
						
							|  |  |  | 			: func.apply(this, args) }) } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // Like mixedModeCollectionAction(..) but will do nothing if enough args 
 | 
					
						
							|  |  |  | // are given...
 | 
					
						
							|  |  |  | var collectionGetterWrapper = function(func, n, options){ | 
					
						
							|  |  |  | 	return widgets.uiDialog(function(){ | 
					
						
							|  |  |  | 		var args = [...arguments] | 
					
						
							|  |  |  | 		// check if minimum number of arguments is reached...
 | 
					
						
							|  |  |  | 		return args.length < (n || 1) | 
					
						
							|  |  |  | 			// show the dialog...
 | 
					
						
							|  |  |  | 			&& this.browseCollections(function(title){  | 
					
						
							|  |  |  | 					return func.call(this, ...args.concat([title])) },  | 
					
						
							|  |  |  | 				options) }) } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | // XXX show collections in image metadata... (???)
 | 
					
						
							|  |  |  | // XXX might be nice to indicate if a collection is loaded -- has .data???
 | 
					
						
							|  |  |  | // XXX might be nice to add collection previews to the collection list...
 | 
					
						
							|  |  |  | // 		...show the base ribbon from collection as background
 | 
					
						
							|  |  |  | var UICollectionActions = actions.Actions({ | 
					
						
							|  |  |  | 	config: { | 
					
						
							|  |  |  | 		// Last used collection (for adding merging)...
 | 
					
						
							|  |  |  | 		//
 | 
					
						
							|  |  |  | 		// This will be auto-selected in .browseCollections(..) on next 
 | 
					
						
							|  |  |  | 		// add/edit operation...
 | 
					
						
							|  |  |  | 		//'collection-last-used': null,
 | 
					
						
							|  |  |  | 		 | 
					
						
							|  |  |  | 		// can be:
 | 
					
						
							|  |  |  | 		// 		'end' | null	- place new items at end of list
 | 
					
						
							|  |  |  | 		// 		'start'			- place new items at start of list
 | 
					
						
							|  |  |  | 		//
 | 
					
						
							|  |  |  | 		// XXX edit this with a toggler???
 | 
					
						
							|  |  |  | 		'collection-ui-place-new': 'start', | 
					
						
							|  |  |  | 	}, | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// UI...
 | 
					
						
							|  |  |  | 	//
 | 
					
						
							|  |  |  | 	// XXX would be nice to make this nested (i.e. path list) -- collection grouping... (???)
 | 
					
						
							|  |  |  | 	// XXX should we use options object instead of arguments???
 | 
					
						
							|  |  |  | 	// XXX might need to check (in a standard way) that nothing is loaded...
 | 
					
						
							|  |  |  | 	browseCollections: ['Collections/$Collections...', | 
					
						
							|  |  |  | 		core.doc`Collection list...
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			.browseCollections(action, options) | 
					
						
							|  |  |  | 				-> dialog | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		options format: | 
					
						
							|  |  |  | 			{ | 
					
						
							|  |  |  | 				new_message: null | false | <str>, | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				last_used: <bool> | <title>, | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				show_main: null | <bool> | <func>, | 
					
						
							|  |  |  | 			} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		All arguments are optional. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		If action is given options.last_used defaults to true. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		If options.last_used is true then .config['collection-last-used'] | 
					
						
							|  |  |  | 		is used to select the last used collection and set when selecting | 
					
						
							|  |  |  | 		an item. | 
					
						
							|  |  |  | 		It options.last_used is a string, then .config[options.last_used] | 
					
						
							|  |  |  | 		will be used to store the last used collection title. | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		NOTE: collections are added live and not on dialog close... | 
					
						
							|  |  |  | 		`,
 | 
					
						
							|  |  |  | 		//widgets.makeUIDialog(function(action, new_message, last_used_collection){
 | 
					
						
							|  |  |  | 		widgets.makeUIDialog(function(action, options={}){ | 
					
						
							|  |  |  | 			var that = this | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			var {new_message, last_used, show_main} = options | 
					
						
							|  |  |  | 			last_used = options.last_used == null ?  | 
					
						
							|  |  |  | 					(action && 'collection-last-used') | 
					
						
							|  |  |  | 				: options.last_used === true ?  | 
					
						
							|  |  |  | 					'collection-last-used'  | 
					
						
							|  |  |  | 				: options.last_used | 
					
						
							|  |  |  | 			var to_remove = [] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			var collections = that.collection_order =  | 
					
						
							|  |  |  | 				//(that.collection_order || []).slice()
 | 
					
						
							|  |  |  | 				that.collection_order.slice() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			//var defaults = that.config['default-collections'] || []
 | 
					
						
							|  |  |  | 			//collections = collections.concat(defaults).unique()
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			return browse.makeLister(null,  | 
					
						
							|  |  |  | 				function(path, make){ | 
					
						
							|  |  |  | 					var dialog = this | 
					
						
							|  |  |  | 						.on('update', function(){ | 
					
						
							|  |  |  | 							dialog.filter(JSON.stringify((that.collection || MAIN_COLLECTION_TITLE) | 
					
						
							|  |  |  | 									.replace(/\$/g, ''))) | 
					
						
							|  |  |  | 								.addClass('highlighted') }) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 					// nothing loaded...
 | 
					
						
							|  |  |  | 					// NOTE: we have to check both .data and .collection as
 | 
					
						
							|  |  |  | 					// 		we can have an empty collection loaded -- empty
 | 
					
						
							|  |  |  | 					// 		.data but a set .collection...
 | 
					
						
							|  |  |  | 					if(that.data.length == 0 && that.collection == null){ | 
					
						
							|  |  |  | 						make.Empty('No collections...') | 
					
						
							|  |  |  | 						return } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 					var openHandler = function(_, title){ | 
					
						
							|  |  |  | 						var title = $(this).find('.text').attr('text') || title | 
					
						
							|  |  |  | 						// create collection if it does not exist...
 | 
					
						
							|  |  |  | 						if(!that.collections  | 
					
						
							|  |  |  | 								|| !(title in that.collections)){ | 
					
						
							|  |  |  | 							that.newCollection(title) } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 						var gid = that.current | 
					
						
							|  |  |  | 						action ? | 
					
						
							|  |  |  | 							action.call(that, title) | 
					
						
							|  |  |  | 							: that.loadCollection(title) | 
					
						
							|  |  |  | 						that.focusImage(gid) | 
					
						
							|  |  |  | 						dialog.close() } | 
					
						
							|  |  |  | 					var setItemState = function(title){ | 
					
						
							|  |  |  | 						var gid = ((that.collections || {})[title] || {}).gid || title | 
					
						
							|  |  |  | 						// handle main collection changes...
 | 
					
						
							|  |  |  | 						gid = title == MAIN_COLLECTION_TITLE ?  | 
					
						
							|  |  |  | 							MAIN_COLLECTION_GID  | 
					
						
							|  |  |  | 							: gid | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 						var text = this.find('.text').last() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 						// saved state...
 | 
					
						
							|  |  |  | 						var unsaved = that.changes === true  | 
					
						
							|  |  |  | 							|| (that.changes || {})['collection: '+ JSON.stringify(gid)] | 
					
						
							|  |  |  | 							|| (that.collectionGID == gid  | 
					
						
							|  |  |  | 								&& (that.config['collection-transfer-changes'] | 
					
						
							|  |  |  | 										|| COLLECTION_TRANSFER_CHANGES) | 
					
						
							|  |  |  | 									.filter(function(a){  | 
					
						
							|  |  |  | 										return !!(that.changes || {})[a] }).length > 0) | 
					
						
							|  |  |  | 						unsaved | 
					
						
							|  |  |  | 							&& text.attr('unsaved', true) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 						// collection crop...
 | 
					
						
							|  |  |  | 						var cs =  | 
					
						
							|  |  |  | 							title == (that.collection || MAIN_COLLECTION_TITLE) ?  | 
					
						
							|  |  |  | 								that.crop_stack | 
					
						
							|  |  |  | 							: (that.collections || {})[title] ? | 
					
						
							|  |  |  | 								that.collections[title].crop_stack | 
					
						
							|  |  |  | 							: null | 
					
						
							|  |  |  | 						cs | 
					
						
							|  |  |  | 							&& text.attr('cropped', cs.length) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 						// collection size...
 | 
					
						
							|  |  |  | 						var c = (that.collections || {})[title] || {} | 
					
						
							|  |  |  | 						var i = (c.data && c.data.length) | 
					
						
							|  |  |  | 								|| c.count | 
					
						
							|  |  |  | 								|| false  | 
					
						
							|  |  |  | 						// main collection loaded...
 | 
					
						
							|  |  |  | 						i = (!i && title == MAIN_COLLECTION_TITLE && !that.collection) ? | 
					
						
							|  |  |  | 							that.data.length  | 
					
						
							|  |  |  | 							: i | 
					
						
							|  |  |  | 						i && $(this).attr('count', i) } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 					// update collection list if changed externally...
 | 
					
						
							|  |  |  | 					/* XXX | 
					
						
							|  |  |  | 					collections.splice.apply(collections,  | 
					
						
							|  |  |  | 						// NOTE: if the length calculation here looks a "bit"
 | 
					
						
							|  |  |  | 						// 		convoluted, that's because it is, this fixes
 | 
					
						
							|  |  |  | 						// 		a really odd bug in old Chrome versions where
 | 
					
						
							|  |  |  | 						// 			L.splice(0, L.length, ...) 
 | 
					
						
							|  |  |  | 						// 		in some odd conditions leaves an element 
 | 
					
						
							|  |  |  | 						// 		in the original array...
 | 
					
						
							|  |  |  | 						// 		(is a jit error???)
 | 
					
						
							|  |  |  | 						[0, (that.collection_order || []).length + collections.length] | 
					
						
							|  |  |  | 							.concat(collections | 
					
						
							|  |  |  | 								.concat(that.collection_order || []) | 
					
						
							|  |  |  | 								.unique())) | 
					
						
							|  |  |  | 					/*/ | 
					
						
							|  |  |  | 					console.log('>>>>', that.collection_order) | 
					
						
							|  |  |  | 					collections | 
					
						
							|  |  |  | 						.splice(0, collections.length, | 
					
						
							|  |  |  | 							...[ | 
					
						
							|  |  |  | 								...collections, | 
					
						
							|  |  |  | 								...(that.collection_order || []), | 
					
						
							|  |  |  | 							].tailUnique()) | 
					
						
							|  |  |  | 					//*/
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 					// main collection...
 | 
					
						
							|  |  |  | 					var main = typeof(show_main) == 'function' ? | 
					
						
							|  |  |  | 						show_main.call(that) | 
					
						
							|  |  |  | 						: show_main | 
					
						
							|  |  |  | 					;(main === true | 
					
						
							|  |  |  | 							|| (main == null && !action)) | 
					
						
							|  |  |  | 						&& collections.indexOf(MAIN_COLLECTION_TITLE) < 0 | 
					
						
							|  |  |  | 						&& make([MAIN_COLLECTION_TITLE],  | 
					
						
							|  |  |  | 							{  | 
					
						
							|  |  |  | 								events: { | 
					
						
							|  |  |  | 									update: function(_, title){ | 
					
						
							|  |  |  | 										// make this look almost like a list element...
 | 
					
						
							|  |  |  | 										// XXX hack???
 | 
					
						
							|  |  |  | 										$(this).find('.text:first-child') | 
					
						
							|  |  |  | 											.before($('<span>') | 
					
						
							|  |  |  | 												.css('color', 'transparent') | 
					
						
							|  |  |  | 												.addClass('sort-handle') | 
					
						
							|  |  |  | 												.html('☰')) | 
					
						
							|  |  |  | 										setItemState | 
					
						
							|  |  |  | 											//.call($(this), title)
 | 
					
						
							|  |  |  | 											.call($(this), $(this).find('.text').attr('text')) | 
					
						
							|  |  |  | 									}, | 
					
						
							|  |  |  | 									open: openHandler, | 
					
						
							|  |  |  | 								}, | 
					
						
							|  |  |  | 								// NOTE: we are adding a blank button here 
 | 
					
						
							|  |  |  | 								// 		to align item counts...
 | 
					
						
							|  |  |  | 								// XXX HACK: can we automate this -- html5 grid layout???
 | 
					
						
							|  |  |  | 								buttons: [['×']], | 
					
						
							|  |  |  | 							}) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 					// collection list...
 | 
					
						
							|  |  |  | 					make.EditableList(collections,  | 
					
						
							|  |  |  | 						{ | 
					
						
							|  |  |  | 							new_item: new_message ?  | 
					
						
							|  |  |  | 									new_message  | 
					
						
							|  |  |  | 								// explicitly disabled new item...
 | 
					
						
							|  |  |  | 								: (new_message === false || new_message === null) ? | 
					
						
							|  |  |  | 									false | 
					
						
							|  |  |  | 								: action ?  | 
					
						
							|  |  |  | 									'$New...'  | 
					
						
							|  |  |  | 								: '$New from current state...', | 
					
						
							|  |  |  | 							place_new_item: that.config['collection-ui-place-new'], | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 							unique: true, | 
					
						
							|  |  |  | 							sortable: 'y', | 
					
						
							|  |  |  | 							to_remove: to_remove, | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 							itemopen: openHandler, | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 							normalize: function(title){  | 
					
						
							|  |  |  | 								return title.trim() }, | 
					
						
							|  |  |  | 							check: function(title){  | 
					
						
							|  |  |  | 								return title.length > 0 }, | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 							each: setItemState,  | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 							itemadded: function(title){ | 
					
						
							|  |  |  | 								action ? | 
					
						
							|  |  |  | 									that.newCollection(title) | 
					
						
							|  |  |  | 									: that.saveCollection(title) }, | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 							disabled: (main === false  | 
					
						
							|  |  |  | 									|| (main == null && action)) ?  | 
					
						
							|  |  |  | 								[MAIN_COLLECTION_TITLE]  | 
					
						
							|  |  |  | 								: false, | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 							update_merge: 'merge', | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 							// XXX REVISE...
 | 
					
						
							|  |  |  | 							itemedit: function(_, from, to){ | 
					
						
							|  |  |  | 								that.renameCollection(from, to) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 								// rename was successful...
 | 
					
						
							|  |  |  | 								if(to in that.collections){ | 
					
						
							|  |  |  | 									collections[collections.indexOf(from)] = to } }, }) },  | 
					
						
							|  |  |  | 				{ | 
					
						
							|  |  |  | 					cls: 'collection-list', | 
					
						
							|  |  |  | 					// focus current collection...
 | 
					
						
							|  |  |  | 					selected: (last_used  | 
					
						
							|  |  |  | 							&& that.config[last_used]) ? | 
					
						
							|  |  |  | 						that.config[last_used] | 
					
						
							|  |  |  | 						: JSON.stringify( | 
					
						
							|  |  |  | 							(that.collection || MAIN_COLLECTION_TITLE) | 
					
						
							|  |  |  | 								// XXX not sure it is good that we have to do this...
 | 
					
						
							|  |  |  | 								.replace(/\$/g, '')), | 
					
						
							|  |  |  | 				}) | 
					
						
							|  |  |  | 				.open(function(_, title){ | 
					
						
							|  |  |  | 					last_used | 
					
						
							|  |  |  | 						&& (that.config[last_used] = title) }) | 
					
						
							|  |  |  | 				.close(function(){ | 
					
						
							|  |  |  | 					that.collection_order = collections | 
					
						
							|  |  |  | 					to_remove | 
					
						
							|  |  |  | 						.forEach(function(title){  | 
					
						
							|  |  |  | 							that.removeCollection(title) }) }) })], | 
					
						
							|  |  |  | 	// XXX should this be able to add new collections???
 | 
					
						
							|  |  |  | 	browseImageCollections: ['Collections|Image/Image $collections...', | 
					
						
							|  |  |  | 		widgets.makeUIDialog(function(gid){ | 
					
						
							|  |  |  | 			var that = this | 
					
						
							|  |  |  | 			gid = this.data.getImage(gid) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			var defaults = this.config['default-collections'] | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			var all | 
					
						
							|  |  |  | 			var collections | 
					
						
							|  |  |  | 			var to_remove | 
					
						
							|  |  |  | 			var t | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			return browse.makeLister(null,  | 
					
						
							|  |  |  | 				function(path, make){ | 
					
						
							|  |  |  | 					var dialog = this | 
					
						
							|  |  |  | 						.on('update', function(){ | 
					
						
							|  |  |  | 							dialog.filter(JSON.stringify((that.collection || MAIN_COLLECTION_TITLE) | 
					
						
							|  |  |  | 									.replace(/\$/g, ''))) | 
					
						
							|  |  |  | 								.addClass('highlighted') }) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 					all = all || that.collection_order || [] | 
					
						
							|  |  |  | 					if(defaults){ | 
					
						
							|  |  |  | 						all.splice.apply(all,  | 
					
						
							|  |  |  | 							[0, all.length] | 
					
						
							|  |  |  | 								.concat(all.concat(defaults).unique())) } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 					// load collections...
 | 
					
						
							|  |  |  | 					var loading = all | 
					
						
							|  |  |  | 						.filter(function(c){ | 
					
						
							|  |  |  | 							return (that.collections || {})[c]  | 
					
						
							|  |  |  | 								&& !that.collections[c].data }) | 
					
						
							|  |  |  | 						.map(function(c){  | 
					
						
							|  |  |  | 							that | 
					
						
							|  |  |  | 								.ensureCollection(c)  | 
					
						
							|  |  |  | 								.then(function(collection){ | 
					
						
							|  |  |  | 									collection.data.getImage(gid || that.current) ? | 
					
						
							|  |  |  | 										collections.push(c) | 
					
						
							|  |  |  | 										: to_remove.push(c.replace(/\$/g, '')) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 									// NOTE: we'll avoid calling update 
 | 
					
						
							|  |  |  | 									// 		too often...
 | 
					
						
							|  |  |  | 									clearTimeout(t) | 
					
						
							|  |  |  | 									t = setTimeout(function(){ | 
					
						
							|  |  |  | 										dialog.update() }, 100) })  | 
					
						
							|  |  |  | 							return c }) | 
					
						
							|  |  |  | 					 | 
					
						
							|  |  |  | 					// containing collections...
 | 
					
						
							|  |  |  | 					collections = collections | 
					
						
							|  |  |  | 						|| that.inCollections(gid || null) | 
					
						
							|  |  |  | 							.filter(function(title){  | 
					
						
							|  |  |  | 								return loading.indexOf(title) < 0 }) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 					// build the disabled list...
 | 
					
						
							|  |  |  | 					if(!to_remove){ | 
					
						
							|  |  |  | 						to_remove = [] | 
					
						
							|  |  |  | 						all.forEach(function(title){ | 
					
						
							|  |  |  | 							collections.indexOf(title) < 0 | 
					
						
							|  |  |  | 								&& loading.indexOf(title) < 0 | 
					
						
							|  |  |  | 								&& to_remove.push(title.replace(/\$/g, '')) }) } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 					all.length > 0 ? | 
					
						
							|  |  |  | 						make.EditableList(all,  | 
					
						
							|  |  |  | 							{ | 
					
						
							|  |  |  | 								new_item: false, | 
					
						
							|  |  |  | 								sortable: 'y', | 
					
						
							|  |  |  | 								disabled: loading, | 
					
						
							|  |  |  | 								to_remove: to_remove, | 
					
						
							|  |  |  | 								itemopen: function(_, title){ | 
					
						
							|  |  |  | 									var i = to_remove.indexOf(title) | 
					
						
							|  |  |  | 									i >= 0 ?  | 
					
						
							|  |  |  | 										to_remove.splice(i, 1)  | 
					
						
							|  |  |  | 										: to_remove.push(title) | 
					
						
							|  |  |  | 									dialog.update() }, | 
					
						
							|  |  |  | 								itemedit: function(_, from, to){ | 
					
						
							|  |  |  | 									that.renameCollection(from, to) | 
					
						
							|  |  |  | 									all[all.indexOf(from)] = to | 
					
						
							|  |  |  | 									that.collection_order = all }, | 
					
						
							|  |  |  | 							}) | 
					
						
							|  |  |  | 						: make.Empty('No collections...') }) | 
					
						
							|  |  |  | 				.close(function(){ | 
					
						
							|  |  |  | 					that.collection_order = all | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 					all.forEach(function(title){ | 
					
						
							|  |  |  | 						collections.indexOf(title) < 0 | 
					
						
							|  |  |  | 							&& to_remove.indexOf(title.replace(/\$/g, '')) < 0 | 
					
						
							|  |  |  | 							&& that.collect(gid, title) }) | 
					
						
							|  |  |  | 					to_remove.forEach(function(title){  | 
					
						
							|  |  |  | 						that.uncollect(gid, title) }) }) })], | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Collection actions with collection selection...
 | 
					
						
							|  |  |  | 	//
 | 
					
						
							|  |  |  | 	// XXX need to add "ALL" -- might need to rework .browseCollections(..) for this...
 | 
					
						
							|  |  |  | 	// XXX also do:
 | 
					
						
							|  |  |  | 	// 		.saveCollection(..)
 | 
					
						
							|  |  |  | 	// XXX EXPERIMENTAL...
 | 
					
						
							|  |  |  | 	// 		...we might not actually need this as this essentially will
 | 
					
						
							|  |  |  | 	// 		do the same thing as .browseCollections(..)
 | 
					
						
							|  |  |  | 	// 		...combining this with .browseCollections(..) might complicate
 | 
					
						
							|  |  |  | 	// 		things as we still need to reuse the later for other things...
 | 
					
						
							|  |  |  | 	loadCollection: [ | 
					
						
							|  |  |  | 		collectionGetterWrapper(function(title){ this.loadCollection(title) })], | 
					
						
							|  |  |  | 	loadMainCollection: ['Collections/Exit collection view', | 
					
						
							|  |  |  | 		{ | 
					
						
							|  |  |  | 			mode: 'uncollect',  | 
					
						
							|  |  |  | 			// prevent this from showing up in .uiDialogs list...
 | 
					
						
							|  |  |  | 			__dialog__: false, | 
					
						
							|  |  |  | 		}, | 
					
						
							|  |  |  | 		`loadCollection: "${MAIN_COLLECTION_TITLE}"`], | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// XXX extend .saveCollection(..) and remove this...
 | 
					
						
							|  |  |  | 	// 		...see .loadCollection(..) notes above...
 | 
					
						
							|  |  |  | 	// XXX should we warn the user when overwriting???
 | 
					
						
							|  |  |  | 	saveAsCollection: ['Collections/$Save as collection...', | 
					
						
							|  |  |  | 		mixedModeCollectionAction(function(title){ | 
					
						
							|  |  |  | 			this.saveCollection(title, 'current')  | 
					
						
							|  |  |  | 			// XXX should we be doing this manually here or in .saveCollection(..)
 | 
					
						
							|  |  |  | 			title == this.collection | 
					
						
							|  |  |  | 				&& this.loadCollection('!') })], | 
					
						
							|  |  |  | 	collect: [ | 
					
						
							|  |  |  | 		collectionGetterWrapper(function(gids, title){  | 
					
						
							|  |  |  | 			if(title == null){ | 
					
						
							|  |  |  | 				title = gids | 
					
						
							|  |  |  | 				gids = null } | 
					
						
							|  |  |  | 			this.collect(gids || 'current', title) }, 2)], | 
					
						
							|  |  |  | 	collectRibbon: ['Collections|Ribbon/Add $ribbon to collection...', | 
					
						
							|  |  |  | 		'collect: "ribbon"'], | 
					
						
							|  |  |  | 	collectLoaded: ['Collections/$Add loaded images to collection...', | 
					
						
							|  |  |  | 		'collect: "loaded"'], | 
					
						
							|  |  |  | 	joinCollect: [ | 
					
						
							|  |  |  | 		collectionGetterWrapper(function(title){ this.joinCollect(title) })], | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// XXX do we need this???
 | 
					
						
							|  |  |  | 	cropImagesInCollection: ['Collections|Crop/Crop images in collection...', | 
					
						
							|  |  |  | 		{mode: function(){  | 
					
						
							|  |  |  | 			return (!this.collections  | 
					
						
							|  |  |  | 					|| Object.keys(this.collections).length == 0)  | 
					
						
							|  |  |  | 				&& 'disabled' }}, | 
					
						
							|  |  |  | 		mixedModeCollectionAction(function(title){ | 
					
						
							|  |  |  | 				var that = this | 
					
						
							|  |  |  | 				this.ensureCollection(title) | 
					
						
							|  |  |  | 					.then(function(collection){ | 
					
						
							|  |  |  | 						var images = collection.data.getImages('all') | 
					
						
							|  |  |  | 						that.crop(images, false) }) },  | 
					
						
							|  |  |  | 			null, | 
					
						
							|  |  |  | 			{ last_used: false })], | 
					
						
							|  |  |  | 	cropOutImagesInCollection: ['Collections|Crop/Remove collection images from crop...', | 
					
						
							|  |  |  | 		{mode: 'cropImagesInCollection'}, | 
					
						
							|  |  |  | 		mixedModeCollectionAction(function(title){ | 
					
						
							|  |  |  | 				var that = this | 
					
						
							|  |  |  | 				this.ensureCollection(title) | 
					
						
							|  |  |  | 					.then(function(collection){ | 
					
						
							|  |  |  | 						var to_remove = collection.data.getImages('all') | 
					
						
							|  |  |  | 						var images = that.data.getImages('loaded') | 
					
						
							|  |  |  | 							.filter(function(gid){ return to_remove.indexOf(gid) < 0 }) | 
					
						
							|  |  |  | 						that.crop(images, false) }) },  | 
					
						
							|  |  |  | 			null, | 
					
						
							|  |  |  | 			{ last_used: false })], | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// XXX should this be in Collections/ ???
 | 
					
						
							|  |  |  | 	editDefaultCollections: ['Interface|Collections/Edit default collections...', | 
					
						
							|  |  |  | 		widgets.makeConfigListEditorDialog( | 
					
						
							|  |  |  | 			'default-collections',  | 
					
						
							|  |  |  | 			{ | 
					
						
							|  |  |  | 				cls: 'collection-list', | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				unique: true, | 
					
						
							|  |  |  | 				sortable: 'y', | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 				normalize: function(title){  | 
					
						
							|  |  |  | 					return title.trim() }, | 
					
						
							|  |  |  | 				check: function(title){  | 
					
						
							|  |  |  | 					return title.length > 0  | 
					
						
							|  |  |  | 						&& title != MAIN_COLLECTION_TITLE }, | 
					
						
							|  |  |  | 			})], | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	/*/ XXX this is not used by metadata yet... | 
					
						
							|  |  |  | 	metadataSection: ['- Image/', | 
					
						
							|  |  |  | 		function(gid, make){ | 
					
						
							|  |  |  | 			// XXX
 | 
					
						
							|  |  |  | 		}], | 
					
						
							|  |  |  | 	//*/
 | 
					
						
							|  |  |  | 	 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	/*/ XXX experementing... | 
					
						
							|  |  |  | 	//		would be nice to:
 | 
					
						
							|  |  |  | 	//			- have an action accessible within the action menu and standalone
 | 
					
						
							|  |  |  | 	//			- topology:
 | 
					
						
							|  |  |  | 	//				<collection>/
 | 
					
						
							|  |  |  | 	//					<collection-option>: <value>
 | 
					
						
							|  |  |  | 	//					...
 | 
					
						
							|  |  |  | 	//				...
 | 
					
						
							|  |  |  | 	//			- creating a collection should open its options...
 | 
					
						
							|  |  |  | 	//
 | 
					
						
							|  |  |  | 	collectionsList: ['Collections/Collections list/*', | 
					
						
							|  |  |  | 		function(path, make){ | 
					
						
							|  |  |  | 			make.EditableList(this.collection_order) | 
					
						
							|  |  |  | 		}], | 
					
						
							|  |  |  | 	//*/
 | 
					
						
							|  |  |  | 	 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// XXX doc...
 | 
					
						
							|  |  |  | 	collectionSort: ['- Collections/', | 
					
						
							|  |  |  | 		core.doc`Sort collection A (sorted) as collection B (sort_as)...
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			.collectionSort(sorted, sort_as) | 
					
						
							|  |  |  | 			.collectionSort(sorted, sort_as, mode) | 
					
						
							|  |  |  | 				-> promise(A, B) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		NOTE: in spite of the name this can also sort the main data/collection. | 
					
						
							|  |  |  | 		NOTE: if either sorted or sort_as are not given or are null the main  | 
					
						
							|  |  |  | 			collection is assumed by default. | 
					
						
							|  |  |  | 		NOTE: if sorted and sort_as are the same collection this will do nothing. | 
					
						
							|  |  |  | 		`,
 | 
					
						
							|  |  |  | 		function(sorted, sort_as, mode='in-place'){ | 
					
						
							|  |  |  | 			var that = this | 
					
						
							|  |  |  | 			var sort = mode == 'in-place' ? | 
					
						
							|  |  |  | 				'inplaceSortAs' | 
					
						
							|  |  |  | 				: 'sortAs' | 
					
						
							|  |  |  | 			if(sorted == sort_as){ | 
					
						
							|  |  |  | 				return Promise.resolve() } | 
					
						
							|  |  |  | 			// NOTE: need to update view if the sorted collection is loaded...
 | 
					
						
							|  |  |  | 			var loaded = sorted == this.collection | 
					
						
							|  |  |  | 			return Promise.all([ | 
					
						
							|  |  |  | 				this.ensureCollection(sorted), | 
					
						
							|  |  |  | 				this.ensureCollection(sort_as), | 
					
						
							|  |  |  | 			]).then(function([sorted, sort_as]){ | 
					
						
							|  |  |  | 				sorted.data.order[sort](sort_as.data.order) | 
					
						
							|  |  |  | 				sorted.data.updateImagePositions()  | 
					
						
							|  |  |  | 				loaded | 
					
						
							|  |  |  | 					 && that.sortImages('update') }) }], | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// XXX revise naming...
 | 
					
						
							|  |  |  | 	sortAsCollection: ['Sort|Collections/Sort as collection...', | 
					
						
							|  |  |  | 		core.doc`Sort current collection as selected.`, | 
					
						
							|  |  |  | 		{sortMethod: true, | 
					
						
							|  |  |  | 		mode: function(){ | 
					
						
							|  |  |  | 			return this.collections_length > 0 || 'disabled' }, }, | 
					
						
							|  |  |  | 		mixedModeCollectionAction( | 
					
						
							|  |  |  | 			function(sort_as){ | 
					
						
							|  |  |  | 				return this.collectionSort(this.collection, sort_as) }, | 
					
						
							|  |  |  | 			null, | 
					
						
							|  |  |  | 			{ show_main: function(){  | 
					
						
							|  |  |  | 				return !!this.collection } })], | 
					
						
							|  |  |  | 	sortCollectionAsThis: ['Sort|Collections/Sort collection as current...', | 
					
						
							|  |  |  | 		core.doc`Sort selected collection as current.`, | 
					
						
							|  |  |  | 		{sortMethod: true, | 
					
						
							|  |  |  | 		mode: 'sortAsCollection', }, | 
					
						
							|  |  |  | 		mixedModeCollectionAction( | 
					
						
							|  |  |  | 			function(sorted){ | 
					
						
							|  |  |  | 				return this.collectionSort(sorted, this.collection) }, | 
					
						
							|  |  |  | 			null, | 
					
						
							|  |  |  | 			{ show_main: function(){  | 
					
						
							|  |  |  | 				return !!this.collection } })], | 
					
						
							|  |  |  | }) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | var UICollection =  | 
					
						
							|  |  |  | module.UICollection = core.ImageGridFeatures.Feature({ | 
					
						
							|  |  |  | 	title: '', | 
					
						
							|  |  |  | 	doc: '', | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	tag: 'ui-collections', | 
					
						
							|  |  |  | 	depends: [ | 
					
						
							|  |  |  | 		'ui', | 
					
						
							|  |  |  | 		'collections', | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// XXX needed only for .addMarkedToCollection(..)
 | 
					
						
							|  |  |  | 		'collection-tags', | 
					
						
							|  |  |  | 	], | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	actions: UICollectionActions,  | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	handlers: [ | 
					
						
							|  |  |  | 		// we need to do this as we transfer tags after everything is 
 | 
					
						
							|  |  |  | 		// loaded...
 | 
					
						
							|  |  |  | 		['collectionLoading', | 
					
						
							|  |  |  | 			function(){ | 
					
						
							|  |  |  | 				this.reload() }], | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// update view when editing current collection...
 | 
					
						
							|  |  |  | 		[[ | 
					
						
							|  |  |  | 			'uncollect',  | 
					
						
							|  |  |  | 			'joinCollect', | 
					
						
							|  |  |  | 		], | 
					
						
							|  |  |  | 			function(_, gids, collection){ | 
					
						
							|  |  |  | 				(collection == null || this.collection == collection) | 
					
						
							|  |  |  | 					&& this.reload(true) }], | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// maintain crop viewer state when loading/unloading collections...
 | 
					
						
							|  |  |  | 		['load clear reload collectionLoading collectionUnloaded', | 
					
						
							|  |  |  | 			function(){ | 
					
						
							|  |  |  | 				if(!this.dom){ | 
					
						
							|  |  |  | 					return } | 
					
						
							|  |  |  | 				this.dom[this.collection ?  | 
					
						
							|  |  |  | 					'addClass'  | 
					
						
							|  |  |  | 					: 'removeClass']('collection-mode') | 
					
						
							|  |  |  | 				this.dom[this.cropped ?  | 
					
						
							|  |  |  | 					'addClass'  | 
					
						
							|  |  |  | 					: 'removeClass']('crop-mode') }], | 
					
						
							|  |  |  | 	], | 
					
						
							|  |  |  | }) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | //---------------------------------------------------------------------
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | var CollectionMarksActions = actions.Actions({ | 
					
						
							|  |  |  | 	config: { | 
					
						
							|  |  |  | 		'collection-local-tags':  | 
					
						
							|  |  |  | 			// XXX need a way to exrtend config values in order of merge
 | 
					
						
							|  |  |  | 			// 		and not manually...
 | 
					
						
							|  |  |  | 			CollectionTagsActions.config['collection-local-tags'] | 
					
						
							|  |  |  | 				.concat([ | 
					
						
							|  |  |  | 					'bookmark', | 
					
						
							|  |  |  | 					'marked', | 
					
						
							|  |  |  | 				]), | 
					
						
							|  |  |  | 		 | 
					
						
							|  |  |  | 		'collection-transfer-changes':  | 
					
						
							|  |  |  | 			// XXX need a way to exrtend config values in order of merge
 | 
					
						
							|  |  |  | 			// 		and not manually...
 | 
					
						
							|  |  |  | 			//CollectionTagsActions.config['collection-transfer-changes']
 | 
					
						
							|  |  |  | 			CollectionActions.config['collection-transfer-changes'] | 
					
						
							|  |  |  | 				.concat([ | 
					
						
							|  |  |  | 					'bookmarked',  | 
					
						
							|  |  |  | 					'marked', | 
					
						
							|  |  |  | 				]), | 
					
						
							|  |  |  | 	}, | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// marked...
 | 
					
						
							|  |  |  | 	collectMarked: ['- Collections|Mark/', | 
					
						
							|  |  |  | 		function(collection){ | 
					
						
							|  |  |  | 			return this.collect(this.marked, collection) }], | 
					
						
							|  |  |  | 	uncollectMarked: ['Collections|Mark/Remove marked from collection', | 
					
						
							|  |  |  | 		{mode: function(){  | 
					
						
							|  |  |  | 			return (!this.collection  | 
					
						
							|  |  |  | 					|| this.marked.length == 0)  | 
					
						
							|  |  |  | 				&& 'disabled' }}, | 
					
						
							|  |  |  | 		function(collection){ | 
					
						
							|  |  |  | 			return this.uncollect(this.marked, collection) }], | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// bookmarked...
 | 
					
						
							|  |  |  | 	collectBookmarked: ['- Collections|Bookmark/', | 
					
						
							|  |  |  | 		function(collection){ | 
					
						
							|  |  |  | 			return this.collectTagged('bookmark', collection) }], | 
					
						
							|  |  |  | 	uncollectBookmarked: ['Collections|Bookmark/Remove bookmarked from collection', | 
					
						
							|  |  |  | 		{mode: function(){  | 
					
						
							|  |  |  | 			return (!this.collection || this.bookmarked.length == 0) && 'disabled' }}, | 
					
						
							|  |  |  | 		function(collection){ | 
					
						
							|  |  |  | 			return this.uncollectTagged('bookmark', collection) }], | 
					
						
							|  |  |  | }) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | var CollectionMarks =  | 
					
						
							|  |  |  | module.CollectionMarks = core.ImageGridFeatures.Feature({ | 
					
						
							|  |  |  | 	title: '', | 
					
						
							|  |  |  | 	doc: '', | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	tag: 'collection-marks', | 
					
						
							|  |  |  | 	depends: [ | 
					
						
							|  |  |  | 		'marks', | 
					
						
							|  |  |  | 		'collection-tags', | 
					
						
							|  |  |  | 		'ui-collections', | 
					
						
							|  |  |  | 	], | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	actions: CollectionMarksActions, | 
					
						
							|  |  |  | }) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | //---------------------------------------------------------------------
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | var UICollectionMarksActions = actions.Actions({ | 
					
						
							|  |  |  | 	// UI...
 | 
					
						
							|  |  |  | 	// XXX should these be a separate feature???
 | 
					
						
							|  |  |  | 	markImagesInCollection: ['Collections|Mark/$Mark images in collection...', | 
					
						
							|  |  |  | 		{mode: 'cropImagesInCollection'}, | 
					
						
							|  |  |  | 		mixedModeCollectionAction(function(title){ | 
					
						
							|  |  |  | 			var that = this | 
					
						
							|  |  |  | 			this.ensureCollection(title) | 
					
						
							|  |  |  | 				.then(function(collection){ | 
					
						
							|  |  |  | 					var images = collection.data.getImages('all') | 
					
						
							|  |  |  | 					that.toggleMark(images, 'on') }) })], | 
					
						
							|  |  |  | 	addMarkedToCollection: ['Collections|Mark/Add marked to $collection...', | 
					
						
							|  |  |  | 		{mode: function(){  | 
					
						
							|  |  |  | 			return this.marked.length == 0  | 
					
						
							|  |  |  | 				&& 'disabled' }}, | 
					
						
							|  |  |  | 		mixedModeCollectionAction(function(title){  | 
					
						
							|  |  |  | 			this.collectMarked(title) })], | 
					
						
							|  |  |  | }) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | var UICollectionMarks =  | 
					
						
							|  |  |  | module.UICollectionMarks = core.ImageGridFeatures.Feature({ | 
					
						
							|  |  |  | 	title: '', | 
					
						
							|  |  |  | 	doc: '', | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	tag: 'ui-collection-marks', | 
					
						
							|  |  |  | 	depends: [ | 
					
						
							|  |  |  | 		'collection-marks', | 
					
						
							|  |  |  | 		'ui-collections', | 
					
						
							|  |  |  | 	], | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	actions: UICollectionMarksActions, | 
					
						
							|  |  |  | }) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | //---------------------------------------------------------------------
 | 
					
						
							|  |  |  | // XXX Things to try/do:
 | 
					
						
							|  |  |  | // 		- load directories as collections (auto?)...
 | 
					
						
							|  |  |  | // 		- export collections to directories...
 | 
					
						
							|  |  |  | // 		- auto-export collections (on save)...
 | 
					
						
							|  |  |  | // 			- add new images
 | 
					
						
							|  |  |  | // 			- remove old images...
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | var FileSystemCollectionActions = actions.Actions({ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Format:
 | 
					
						
							|  |  |  | 	// 	{
 | 
					
						
							|  |  |  | 	// 		path: <string>,
 | 
					
						
							|  |  |  | 	// 		...
 | 
					
						
							|  |  |  | 	// 	}
 | 
					
						
							|  |  |  | 	collections: null, | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// XXX this does not work for merged indexes as each index has 
 | 
					
						
							|  |  |  | 	// 		different gids and paths for same collection title...
 | 
					
						
							|  |  |  | 	// 		...need to merge these correctly...
 | 
					
						
							|  |  |  | 	// 			- merge collections by title
 | 
					
						
							|  |  |  | 	// 			- multiple gids
 | 
					
						
							|  |  |  | 	// 			- multiple paths
 | 
					
						
							|  |  |  | 	collectionPathLoader: ['- Collections/', | 
					
						
							|  |  |  | 		{collectionFormat: 'path'}, | 
					
						
							|  |  |  | 		function(title, state, logger){  | 
					
						
							|  |  |  | 			var that = this | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// if data is present, do not reload...
 | 
					
						
							|  |  |  | 			if(state.data){ | 
					
						
							|  |  |  | 				return } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// XXX get a logger...
 | 
					
						
							|  |  |  | 			logger = logger || this.logger | 
					
						
							|  |  |  | 			logger = logger && logger.push('Load') | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			return Promise.all((this.location.loaded || [this.location.path]) | 
					
						
							|  |  |  | 				.map(function(path){ | 
					
						
							|  |  |  | 					path = util.normalizePath([ | 
					
						
							|  |  |  | 						path, | 
					
						
							|  |  |  | 						that.config['index-dir'],  | 
					
						
							|  |  |  | 						// XXX use index-specific path...
 | 
					
						
							|  |  |  | 						state.path, | 
					
						
							|  |  |  | 					].join('/')) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 					return file.loadIndex(path, false, logger) | 
					
						
							|  |  |  | 						.then(function(res){ | 
					
						
							|  |  |  | 							// load the collection data...
 | 
					
						
							|  |  |  | 							that.collections[title].data =  | 
					
						
							|  |  |  | 								that.prepareIndexForLoad(res[path]).data }) })) }], | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// XXX revise...
 | 
					
						
							|  |  |  | 	// XXX this should be generic... (???)
 | 
					
						
							|  |  |  | 	// 		...I think the action itself should be generic, but what this
 | 
					
						
							|  |  |  | 	// 		specific action does is very specific to file collections...
 | 
					
						
							|  |  |  | 	// 		...think of a protocol))))
 | 
					
						
							|  |  |  | 	unloadUnchangedCollections: ['Collections|File/Unload saved collections', | 
					
						
							|  |  |  | 		function(logger){ | 
					
						
							|  |  |  | 			var that = this | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			if(this.changes === true || this.changes === undefined){ | 
					
						
							|  |  |  | 				return } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			// XXX get a logger...
 | 
					
						
							|  |  |  | 			logger = logger || this.logger | 
					
						
							|  |  |  | 			logger = logger && logger.push('Unload') | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			Object.keys(this.collections) | 
					
						
							|  |  |  | 				.forEach(function(title){ | 
					
						
							|  |  |  | 					var c = that.collections[title]  | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 					var key = 'collection: '+JSON.stringify(c.gid || title) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 					if(title != MAIN_COLLECTION_TITLE | 
					
						
							|  |  |  | 							&& title != that.collection | 
					
						
							|  |  |  | 							&& c.path  | 
					
						
							|  |  |  | 							&& c.data | 
					
						
							|  |  |  | 							&& (that.changes === false  | 
					
						
							|  |  |  | 								|| !(key in that.changes))){ | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 						logger && logger.emit('title', title) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 						c.count = c.data.length | 
					
						
							|  |  |  | 						delete c.data } }) }], | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	importCollectionsFromPath: ['- Collections|File/Import collections from path', | 
					
						
							|  |  |  | 		function(path){ | 
					
						
							|  |  |  | 			// XXX
 | 
					
						
							|  |  |  | 		}], | 
					
						
							|  |  |  | }) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | var FileSystemCollection =  | 
					
						
							|  |  |  | module.FileSystemCollection = core.ImageGridFeatures.Feature({ | 
					
						
							|  |  |  | 	title: '', | 
					
						
							|  |  |  | 	doc: '', | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	tag: 'fs-collections', | 
					
						
							|  |  |  | 	depends: [ | 
					
						
							|  |  |  | 		'index-format', | 
					
						
							|  |  |  | 		'fs', | 
					
						
							|  |  |  | 		'collections', | 
					
						
							|  |  |  | 	], | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	actions: FileSystemCollectionActions, | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	handlers: [ | 
					
						
							|  |  |  | 	], | 
					
						
							|  |  |  | }) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | //---------------------------------------------------------------------
 | 
					
						
							|  |  |  | // XXX localstorage-collections (???)
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | /********************************************************************** | 
					
						
							|  |  |  | * vim:set ts=4 sw=4 :                               */ return module }) |