mirror of
				https://github.com/flynx/ImageGrid.git
				synced 2025-10-31 11:20:09 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			3001 lines
		
	
	
		
			85 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			3001 lines
		
	
	
		
			85 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
| /**********************************************************************
 | |
| * 
 | |
| *
 | |
| *
 | |
| **********************************************************************/
 | |
| ((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 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
 | |
| 			.concat(order)
 | |
| 			.reverse()
 | |
| 			.unique()
 | |
| 			.reverse()
 | |
| 
 | |
| 		// XXX 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...
 | |
| 	get collection_handlers(){
 | |
| 		var handlers = this.__collection_handlers = this.__collection_handlers || {}
 | |
| 
 | |
| 		if(Object.keys(handlers).length == 0){
 | |
| 			var that = this
 | |
| 			handlers['data'] = null
 | |
| 			this.actions.forEach(function(action){
 | |
| 				var fmt = that.getActionAttr(action, 'collectionFormat')
 | |
| 				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.notUserCallable(function(collection){
 | |
| 			// This is the window resize event...
 | |
| 			//
 | |
| 			// Not for direct use.
 | |
| 		})],
 | |
| 	collectionUnloaded: ['- Collections/',
 | |
| 		core.doc`This is called when unloading a collection.
 | |
| 		`,
 | |
| 		core.notUserCallable(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.notUserCallable(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.notUserCallable(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
 | |
| 
 | |
| 			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...
 | |
| 			p = 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) })
 | |
| 			})
 | |
| 
 | |
| 			return p
 | |
| 		}],
 | |
| 
 | |
| 
 | |
| 	// 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...
 | |
| 			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
 | |
| 		`,
 | |
| 		{browseMode: '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) ] })
 | |
| 				.reduce(function(a, b){ return a.concat(b) }, [])
 | |
| 
 | |
| 			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.
 | |
| 		`,
 | |
| 		{
 | |
| 			browseMode: 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)] })
 | |
| 				.reduce(function(a, b){ return a.concat(b) }, [])
 | |
| 
 | |
| 			// 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.
 | |
| 		`,
 | |
| 		{browseMode: '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)
 | |
| 					.dumpJSON()
 | |
| 
 | |
| 				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.dumpJSON()
 | |
| 					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.dumpJSON() })
 | |
| 				}
 | |
| 			})
 | |
| 		}
 | |
| 	} }],
 | |
| 	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',
 | |
| 		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: [
 | |
| 		'base',
 | |
| 		'location',
 | |
| 		'crop',
 | |
| 	],
 | |
| 	suggested: [
 | |
| 		'collections-local-config',
 | |
| 		'collection-tags',
 | |
| 		'auto-collections',
 | |
| 
 | |
| 		'ui-collections',
 | |
| 		'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 = [].slice.call(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 = [].slice.call(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...
 | |
| 		['prepareJSONForLoad',
 | |
| 			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 .prepareJSONForLoad(..) 
 | |
| 						// 		to handle collection internals produced by
 | |
| 						// 		.prepareJSONForLoad(..)...
 | |
| 						// 		...would need to pass it the local data...
 | |
| 						// XXX
 | |
| 					})
 | |
| 			}],
 | |
| 	],
 | |
| })
 | |
| 
 | |
| 
 | |
| 
 | |
| //---------------------------------------------------------------------
 | |
| 
 | |
| 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': [
 | |
| 			'bookmark',
 | |
| 			'selected',
 | |
| 		],
 | |
| 		
 | |
| 		'collection-transfer-changes': 
 | |
| 			CollectionActions.config['collection-transfer-changes']
 | |
| 				.concat([
 | |
| 					'bookmarked', 
 | |
| 					'selected',
 | |
| 				]),
 | |
| 	},
 | |
| 
 | |
| 	collectTagged: ['- Collections|Tag/',
 | |
| 		function(tags, collection){
 | |
| 			return this.collect(this.data.getTaggedByAll(tags), collection) }],
 | |
| 	uncollectTagged: ['- Collections|Tag/',
 | |
| 		function(tags, collection){
 | |
| 			return this.uncollect(this.data.getTaggedByAll(tags), collection) }],
 | |
| 
 | |
| 	// marked...
 | |
| 	collectMarked: ['- Collections|Mark/',
 | |
| 		function(collection){
 | |
| 			return this.collectTagged('selected', collection) }],
 | |
| 	uncollectMarked: ['Collections|Mark/Remove marked from collection',
 | |
| 		{browseMode: function(){ 
 | |
| 			return (!this.collection || this.marked.length == 0) && 'disabled' }},
 | |
| 		function(collection){
 | |
| 			return this.uncollectTagged('selected', collection) }],
 | |
| 
 | |
| 	// bookmarked...
 | |
| 	collectBookmarked: ['- Collections|Bookmark/',
 | |
| 		function(collection){
 | |
| 			return this.collectTagged('bookmark', collection) }],
 | |
| 	uncollectBookmarked: ['Collections|Bookmark/Remove bookmarked from collection',
 | |
| 		{browseMode: function(){ 
 | |
| 			return (!this.collection || this.bookmarked.length == 0) && 'disabled' }},
 | |
| 		function(collection){
 | |
| 			return this.uncollectTagged('bookmark', collection) }],
 | |
| })
 | |
| 
 | |
| 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',
 | |
| 
 | |
| 		// XXX
 | |
| 		'image-marks',
 | |
| 		'image-bookmarks',
 | |
| 	],
 | |
| 
 | |
| 	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(){
 | |
| 					// load local_tags...
 | |
| 					local_tag_names
 | |
| 						.forEach(function(tag){ 
 | |
| 							tags[tag] = local_tags[tag] || [] 
 | |
| 						})
 | |
| 
 | |
| 					;(this.crop_stack || [])
 | |
| 						.forEach(function(d){ d.tags = tags })
 | |
| 					this.data.tags = tags
 | |
| 
 | |
| 					this.data.sortTags()
 | |
| 				}
 | |
| 			}],
 | |
| 		// 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){ 
 | |
| 							local_tags[tag] = (!new_set || title == MAIN_COLLECTION_TITLE) ? 
 | |
| 								((that.data.tags || {})[tag] || []) 
 | |
| 								: []
 | |
| 						})
 | |
| 
 | |
| 					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'] || []
 | |
| 
 | |
| 				// prevent global tag removal...
 | |
| 				var tags = this.data.tags
 | |
| 				delete this.data.tags
 | |
| 
 | |
| 				return function(){
 | |
| 					// update local tags...
 | |
| 					local_tag_names.forEach(function(tag){
 | |
| 						tags[tag] = that.data.makeSparseImages(tags[tag], true) })
 | |
| 
 | |
| 					this.data.tags = tags
 | |
| 					this.data.sortTags()
 | |
| 				}
 | |
| 			}],
 | |
| 		// save .local_tags to json...
 | |
| 		// NOTE: we do not need to explicitly load anything as .load() 
 | |
| 		// 		will load everything we need and .collectionLoading(..)
 | |
| 		// 		will .sortTags() for us...
 | |
| 		['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 || {}
 | |
| 					var ltags = c[MAIN_COLLECTION_TITLE].local_tags || {}
 | |
| 					var rtags = res.data.tags = {}
 | |
| 
 | |
| 					// move all the tags...
 | |
| 					Object.keys(tags)
 | |
| 						.filter(function(tag){ return ltags[tag] == null })
 | |
| 						.forEach(function(tag){ rtags[tag] = tags[tag].compact() })
 | |
| 					// overwrite the local tags for the base...
 | |
| 					Object.keys(ltags)
 | |
| 						.forEach(function(tag){ rtags[tag] = ltags[tag].compact() })
 | |
| 				}
 | |
| 
 | |
| 				// clear and compact tags for all collections...
 | |
| 				rc
 | |
| 					&& Object.keys(rc || {})
 | |
| 						// XXX skip unloaded collections...
 | |
| 						.filter(function(title){ return !!rc[title].data })
 | |
| 						.forEach(function(title){
 | |
| 							var tags = c[title].local_tags || {}
 | |
| 							var rtags = {}
 | |
| 
 | |
| 							// compact the local tags...
 | |
| 							Object.keys(tags)
 | |
| 								.forEach(function(tag){
 | |
| 									rtags[tag] = tags[tag].compact() })
 | |
| 
 | |
| 							// move .local_tags to .data.tags
 | |
| 							rc[title].data.tags = rtags
 | |
| 						})
 | |
| 			}],
 | |
| 		// load collection local tags from .data.tags to .local_tags...
 | |
| 		['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){
 | |
| 								return
 | |
| 							}
 | |
| 							var t = (c.data || {}).tags || {}
 | |
| 
 | |
| 							c.local_tags = c.local_tags || {}
 | |
| 							;(that.config['collection-local-tags'] || [])
 | |
| 								.forEach(function(tag){
 | |
| 									c.local_tags[tag] = c.local_tags[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 })
 | |
| 
 | |
| 				// XXX should this be a real tag query???
 | |
| 				var gids = this.data.getTaggedByAll(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 ? [].slice.call(arguments, 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...
 | |
| var mixedModeCollectionAction = function(func, n, last_used_collection){
 | |
| 	return widgets.uiDialog(function(){
 | |
| 		var args = [].slice.call(arguments)
 | |
| 		// check if minimum number of arguments is reached...
 | |
| 		return args.length < (n || 1) ? 
 | |
| 			// show the dialog...
 | |
| 			//this.browseCollections(func) 
 | |
| 			this.browseCollections(function(title){ 
 | |
| 					return func.call(this, ...args.concat([title])) },
 | |
| 				null,
 | |
| 				last_used_collection) 
 | |
| 			: func.apply(this, args) }) }
 | |
| 
 | |
| // Like mixedModeCollectionAction(..) but will do nothing if enough args 
 | |
| // are given...
 | |
| var collectionGetterWrapper = function(func, n, last_used_collection){
 | |
| 	return widgets.uiDialog(function(){
 | |
| 		var args = [].slice.call(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])) },
 | |
| 				null,
 | |
| 				last_used_collection) }) }
 | |
| 
 | |
| 
 | |
| // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 
 | |
| 
 | |
| // 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,
 | |
| 	},
 | |
| 
 | |
| 	// UI...
 | |
| 	//
 | |
| 	// XXX would be nice to make this nested (i.e. path list) -- collection grouping... (???)
 | |
| 	// XXX should we use options object???
 | |
| 	browseCollections: ['Collections/$Collections...',
 | |
| 		core.doc`Collection list...
 | |
| 
 | |
| 			.browseCollections(action, new_message, last_used_collection)
 | |
| 				-> dialog
 | |
| 
 | |
| 		All arguments are optional.
 | |
| 
 | |
| 		If action is given last_used_collection defaults to true.
 | |
| 
 | |
| 		If last_used_collection is true then .config['collection-last-used']
 | |
| 		is used to select the last used collection and set when selecting
 | |
| 		an item.
 | |
| 		It last_used_collection is a string, then .config[last_used_collection]
 | |
| 		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){
 | |
| 			var that = this
 | |
| 			last_used_collection = last_used_collection == null ? 
 | |
| 					(action && 'collection-last-used')
 | |
| 				: last_used_collection === true ? 
 | |
| 					'collection-last-used' 
 | |
| 				: last_used_collection
 | |
| 			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')
 | |
| 						})
 | |
| 
 | |
| 					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...
 | |
| 					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()))
 | |
| 
 | |
| 					// main collection...
 | |
| 					!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 
 | |
| 								: action ? 
 | |
| 									'$New...' 
 | |
| 								: '$New from current state...',
 | |
| 
 | |
| 							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: 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_collection 
 | |
| 							&& that.config[last_used_collection]) ?
 | |
| 						that.config[last_used_collection]
 | |
| 						: 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_collection
 | |
| 						&& (that.config[last_used_collection] = 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...
 | |
| 	loadCollection: [
 | |
| 		collectionGetterWrapper(function(title){ this.loadCollection(title) })],
 | |
| 	loadMainCollection: ['Collections/Exit collection view',
 | |
| 		{
 | |
| 			browseMode: '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...',
 | |
| 		{browseMode: 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, false)],
 | |
| 	cropOutImagesInCollection: ['Collections|Crop/Crop $out images in collec$tion...',
 | |
| 		{browseMode: '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, false)],
 | |
| 
 | |
| 	// XXX should these be here or in marks-specific feature???
 | |
| 	markImagesInCollection: ['Collections|Mark/$Mark images in collection...',
 | |
| 		{browseMode: '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...',
 | |
| 		{browseMode: function(){ 
 | |
| 			return this.marked.length == 0 && 'disabled' }},
 | |
| 		mixedModeCollectionAction(function(title){ this.collectMarked(title) })],
 | |
| 
 | |
| 	// 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)
 | |
| 		}],
 | |
| 	//*/
 | |
| })
 | |
| 
 | |
| 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')
 | |
| 			}],
 | |
| 	],
 | |
| })
 | |
| 
 | |
| 
 | |
| 
 | |
| //---------------------------------------------------------------------
 | |
| // 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.prepareJSONForLoad(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 })
 |