mirror of
				https://github.com/flynx/ImageGrid.git
				synced 2025-10-29 18:30:09 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			1245 lines
		
	
	
		
			31 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			1245 lines
		
	
	
		
			31 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
| /**********************************************************************
 | |
| * 
 | |
| *
 | |
| *
 | |
| **********************************************************************/
 | |
| 
 | |
| //var DEBUG = DEBUG != null ? DEBUG : true
 | |
| 
 | |
| // Flag indicating a new image file was constructed...
 | |
| var IMAGES_CREATED = false
 | |
| 
 | |
| var IMAGE_PATTERN = /.*\.(jpg|jpeg|png|gif)$/i
 | |
| 
 | |
| var FILE_LOADERS = []
 | |
| var FILE_SAVERS = {}
 | |
| var FILES_UPDATED = []
 | |
| 
 | |
| 
 | |
| 
 | |
| /**********************************************************************
 | |
| * File storage (Extension API -- CEF/PhoneGap/...)
 | |
| *
 | |
| * XXX need to cleanup this section...
 | |
| */
 | |
| 
 | |
| 
 | |
| /********************************************************* Helpers ***/
 | |
| 
 | |
| function makeBaseFilename(base, ext){
 | |
| 	ext = ext == null ? CONFIG.json_ext : ext
 | |
| 
 | |
| 	return CONFIG.base_file_pattern
 | |
| 		.replace('${BASE}', base)
 | |
| 		.replace('${EXT}', ext)
 | |
| }
 | |
| function makeFilename(base, date, ext){
 | |
| 	date = date == null ? Date.timeStamp() : date
 | |
| 	ext = ext == null ? CONFIG.json_ext : ext
 | |
| 
 | |
| 	return CONFIG.file_pattern
 | |
| 			.replace('${DATE}', date)
 | |
| 			.replace('${BASE}', base)
 | |
| 			.replace('${EXT}', ext)
 | |
| }
 | |
| function makeFilenamePattern(base, ext){
 | |
| 	return RegExp(makeFilename(base, '^[0-9]*', ext)+'$')
 | |
| }
 | |
| 
 | |
| function makeDiffFilename(base, date, diff, ext){
 | |
| 	date = date == null ? Date.timeStamp() : date
 | |
| 	diff = diff == null ? CONFIG.diff_suffix : diff
 | |
| 	ext = ext == null ? CONFIG.json_ext : ext
 | |
| 
 | |
| 	return CONFIG.diff_file_pattern
 | |
| 			.replace('${DATE}', date)
 | |
| 			.replace('${BASE}', base)
 | |
| 			.replace('${DIFF_SUFIX}', diff)
 | |
| 			.replace('${EXT}', ext)
 | |
| }
 | |
| function makeDiffFilePattern(base, diff, ext){
 | |
| 	return RegExp(makeDiffFilename(base, '^[0-9]*', ext)+'$')
 | |
| }
 | |
| 
 | |
| 
 | |
| // Report deferred progress
 | |
| //
 | |
| // This uses showStatus(...) and showErrorStatus(...) to report actions.
 | |
| //
 | |
| // Will use showErrorStatus(...) iff "Error" is the last argument of the
 | |
| // progress/notify action, removing it (string 'Error') from the arguments.
 | |
| //
 | |
| // Will return the original deferred.
 | |
| function statusNotify(prefix, loader, not_queued){
 | |
| 	not_queued = not_queued == null ? true : not_queued
 | |
| 	var report = not_queued == true ? showStatus : showStatusQ
 | |
| 	if(loader == null){
 | |
| 		loader = prefix
 | |
| 		prefix = null
 | |
| 	}
 | |
| 	return loader
 | |
| 		.progress(function(){
 | |
| 			var args = $.makeArray(arguments)
 | |
| 
 | |
| 			var getter = args[args.length-1]
 | |
| 			if(getter != null && getter.isResolved != null){
 | |
| 				args.pop()
 | |
| 					.done(function(){
 | |
| 						report(args.join(': '))
 | |
| 					})
 | |
| 					.fail(function(){
 | |
| 						showErrorStatus(args.join(': '))
 | |
| 					})
 | |
| 				return
 | |
| 			}
 | |
| 
 | |
| 			if(prefix != null && prefix != ''){
 | |
| 				args.splice(0, 0, prefix)
 | |
| 			}
 | |
| 			if(args.indexOf('Error') >= 0){
 | |
| 				args.pop()
 | |
| 				return showErrorStatus(args.join(': '))
 | |
| 			}
 | |
| 			return report(args.join(': '))
 | |
| 		})
 | |
| }
 | |
| 
 | |
| 
 | |
| // Report progress status via a progress bar...
 | |
| //
 | |
| // 		<msg> (5 of 500)					x
 | |
| // 		|============>----------------------|
 | |
| //
 | |
| //
 | |
| // The tracker is optional, if not given a generic deferred object will
 | |
| // be created and used.
 | |
| //
 | |
| // If close_button is false, no close button will be dosplayed, hidden 
 | |
| // by default.
 | |
| //
 | |
| // Returns the tracker object.
 | |
| //
 | |
| // The progress is read of the tracker (Deferred) and reported based 
 | |
| // on progress notifications on it.
 | |
| //
 | |
| // Two types of notification are supported:
 | |
| // 	- deferred
 | |
| // 		the .progress(..) handler will receive a deferred as a last 
 | |
| // 		argument, this will in turn do two things:
 | |
| // 			1) increment the max value of the progress bar in place
 | |
| // 			2) when the defered is done, increment the value of the 
 | |
| // 				progress bar
 | |
| // 	- simple
 | |
| // 		increment both max and value of the progress bar
 | |
| //
 | |
| // The progress bar will go into a "done" state and close if the 
 | |
| // tracker deferred is explicitly resolved.
 | |
| //
 | |
| //
 | |
| // See: progressBar(..) for more details on the actual widget.
 | |
| //
 | |
| // NOTE: closing the progress bar will not do anything...
 | |
| function statusProgress(msg, tracker, close_button){
 | |
| 	tracker = tracker == null ? $.Deferred() : null
 | |
| 	close_button = close_button == null ? false : close_button
 | |
| 
 | |
| 	var progress = progressBar(msg, null, close_button)
 | |
| 	var total = 0
 | |
| 	var done = 0
 | |
| 
 | |
| 	// XXX do we need a fail handler here???
 | |
| 	return tracker
 | |
| 		.done(function(){
 | |
| 			// XXX for some reason this does not allways display "done"
 | |
| 			closeProgressBar(progress)
 | |
| 		})
 | |
| 		.fail(function(){
 | |
| 			closeProgressBar(progress, 'error')
 | |
| 		})
 | |
| 		.progress(function(){
 | |
| 			var args = $.makeArray(arguments)
 | |
| 			var getter = args[args.length-1]
 | |
| 			total += 1
 | |
| 
 | |
| 			// the getter is a deferred...
 | |
| 			if(getter != null && getter.isResolved != null){
 | |
| 				args.pop()
 | |
| 					.always(function(){
 | |
| 						done += 1
 | |
| 						updateProgressBar(progress, done, total)
 | |
| 					})
 | |
| 
 | |
| 			// no getter...
 | |
| 			} else {
 | |
| 				done += 1
 | |
| 			}
 | |
| 
 | |
| 			updateProgressBar(progress, done, total)
 | |
| 		})
 | |
| }
 | |
| 
 | |
| 
 | |
| // Bubble up actions in the deferred chain
 | |
| //
 | |
| // i.e. proxy them form one deferred (from) to the next (to)...
 | |
| //
 | |
| // Will chain progress/notify and if only_progress is not set, also
 | |
| // done/resolve and fail/reject from "from" to "to" deferred objects.
 | |
| //
 | |
| // Will add prefix to the list of arguments of progress/notify and
 | |
| // fail/reject (if not disabled), unless it is set to null.
 | |
| //
 | |
| // Will return "from" object.
 | |
| function bubbleProgress(prefix, from, to, only_progress){
 | |
| 	only_progress = only_progress == null ? false : only_progress
 | |
| 
 | |
| 	from
 | |
| 		.progress(function(){ 
 | |
| 			var args = $.makeArray(arguments)
 | |
| 			prefix != null && args.splice(0, 0, prefix)
 | |
| 			to.notify.apply(to, args) 
 | |
| 		})
 | |
| 
 | |
| 	if(!only_progress){
 | |
| 		from
 | |
| 			.done(function(){
 | |
| 				var args = $.makeArray(arguments)
 | |
| 				to.resolve.apply(to, args) 
 | |
| 			})
 | |
| 			.fail(function(){
 | |
| 				var args = $.makeArray(arguments)
 | |
| 				prefix != null && args.splice(0, 0, prefix)
 | |
| 				to.reject.apply(to, args) 
 | |
| 			})
 | |
| 	}
 | |
| 
 | |
| 	return from
 | |
| }
 | |
| 
 | |
| 
 | |
| // Semi-generic deferred JSON file loader
 | |
| //
 | |
| // If pattern is given, then search for the latest (ordered last) file 
 | |
| // and load that.
 | |
| // else load the dfl file.
 | |
| //
 | |
| // If diff_pattern is given, then merge all matching files in order 
 | |
| // (first to last) with the loaded "main" file
 | |
| //
 | |
| // If default_data is then not finding a file will not fail, instead the
 | |
| // default_data will be the resolved data.
 | |
| //
 | |
| // NOTE: if diffs are available this expects the file to contain an object,
 | |
| // 		and will extend ($.extend(..)) that object.
 | |
| // NOTE: if neither of dfl, pattern or diff_pattern are given, then this
 | |
| // 		is essentially the same as $.getJSON(...)
 | |
| // NOTE: this needs listDir(...) to search for latest versions of files.
 | |
| function loadLatestJSONFile(path, dfl, pattern, diff_pattern, default_data, tracker){
 | |
| 	var pparts = path.split(/[\/\\]/)
 | |
| 	dfl = dfl == null ? pparts.pop() : dfl
 | |
| 	//path = path == dfl ? '.' : path
 | |
| 	path = pparts.join('/')
 | |
| 
 | |
| 	var res = $.Deferred()
 | |
| 	
 | |
| 	if(dfl == ''){
 | |
| 		return res.reject()
 | |
| 	}
 | |
| 
 | |
| 	// can't find diffs if can't list dirs...
 | |
| 	if(window.listDir == null && (pattern != null || diff_pattern != null)){
 | |
| 		return res.reject('listDir unsupported.')
 | |
| 	}
 | |
| 
 | |
| 	var file_list = null
 | |
| 
 | |
| 	// find the latest...
 | |
| 	if(pattern != null){
 | |
| 		file_list = listDir(path)
 | |
| 		if(file_list == null){
 | |
| 			return res.reject()
 | |
| 		}
 | |
| 		pattern = RegExp(pattern)
 | |
| 		var file = $.map(file_list, function(e){ 
 | |
| 			return pattern.test(e) ? e : null
 | |
| 		}).sort().reverse()[0]
 | |
| 	}
 | |
| 	var file = file == null ? dfl : file
 | |
| 	var base_date = file.split('-')[0]
 | |
| 	base_date = base_date == file ? '' : base_date
 | |
| 	
 | |
| 	var diff_data = {}
 | |
| 	var diff = true
 | |
| 
 | |
| 	// collect and merge diffs...
 | |
| 	if(diff_pattern != null){
 | |
| 		file_list = file_list == null ? listDir(path) : file_list
 | |
| 		if(file_list == null){
 | |
| 			return res.reject()
 | |
| 		}
 | |
| 		diff_pattern = RegExp(diff_pattern)
 | |
| 		var diff_data = [diff_data]
 | |
| 		var diffs_names = $.map(file_list, function(e){ 
 | |
| 			return diff_pattern.test(e) && e.split('-')[0] >= base_date ? e : null
 | |
| 		}).sort()
 | |
| 		diff = $.when.apply(null, $.map(diffs_names, function(e, i){
 | |
| 					var getter = $.getJSON(path +'/'+ e)
 | |
| 						.done(function(data){
 | |
| 							// NOTE: we are not using push here so as to
 | |
| 							// 		keep the sort order...
 | |
| 							diff_data[i+1] = data
 | |
| 						})
 | |
| 						.always(function(){
 | |
| 							res.notify(e, getter)
 | |
| 						})
 | |
| 
 | |
| 					tracker != null && tracker.notify(e, getter)
 | |
| 
 | |
| 					return getter
 | |
| 				}))
 | |
| 			// merge the diffs...
 | |
| 			// NOTE: .then(...) handlers get different signature args 
 | |
| 			// 		depending on the number of arguments to .when(...)...
 | |
| 			.then(function(){
 | |
| 				$.extend.apply(null, diff_data)
 | |
| 				diff_data = diff_data[0]
 | |
| 			})
 | |
| 	} 
 | |
| 
 | |
| 	// load the main file and merge the diff with it...
 | |
| 	var getter = $.getJSON(path +'/'+ file)
 | |
| 		.always(function(){
 | |
| 			res.notify(file, getter)
 | |
| 			tracker != null && tracker.notify(file, getter)
 | |
| 		})
 | |
| 	$.when(diff, getter)
 | |
| 		.done(function(_, json){
 | |
| 			json = json[0]
 | |
| 
 | |
| 			// merge diffs...
 | |
| 			if(Object.keys(diff_data).length != 0){
 | |
| 				$.extend(json, diff_data)
 | |
| 			}
 | |
| 
 | |
| 			res.resolve(json)
 | |
| 		})
 | |
| 		.fail(function(){
 | |
| 			if(default_data != null){
 | |
| 				res.resolve(default_data)
 | |
| 			} else {
 | |
| 				res.reject()
 | |
| 			}
 | |
| 		})
 | |
| 
 | |
| 	return res
 | |
| }
 | |
| 
 | |
| 
 | |
| 
 | |
| /**********************************************************************
 | |
| * File serialization framework...
 | |
| *
 | |
| * This helps us define:
 | |
| * 	- named data type loader -- makeFileLoader(..)
 | |
| * 		- support different file versions -- load latest
 | |
| * 		- support a specific directory structure and centralized 
 | |
| * 			configuration
 | |
| * 	- named data type saver -- makeFileSaver(..)
 | |
| * 		- save a version
 | |
| * 		- support a specific directory structure and centralized 
 | |
| * 			configuration
 | |
| * 	- a way to mark named data type as modified -- fileUpdated(..)
 | |
| * 	- a way to load all registered data types -- runFileLoaders(..)
 | |
| * 	- a way to save modified data types -- runFileSavers(..)
 | |
| *
 | |
| * NOTE: saving/loading a diff is done manually, see:
 | |
| * 		- saveFileImages(..)
 | |
| * 		- saveFileImagesDiff(..)
 | |
| * 		- loadFileImages(..)
 | |
| */
 | |
| 
 | |
| // Construct a JSON file loader...
 | |
| //
 | |
| // This will:
 | |
| // 	- create configured file loader
 | |
| // 	- register the loader in the FILE_LOADERS collection, unless 
 | |
| // 		skip_reg is true
 | |
| //
 | |
| // The loader will:
 | |
| // 	- get all the config data from CONFIG,
 | |
| // 	- load the file name from cache dir (see: loadLatestJSONFile(..)),
 | |
| // 	- if file does not exist use default_data
 | |
| // 	- call set_data to setup read data,
 | |
| // 	- trigger the evt_name event on the viewer when done,
 | |
| // 	- on load error call error function if given,
 | |
| // 	- report progress (see: bubbleProgress(..))
 | |
| //
 | |
| //
 | |
| // Also see:
 | |
| // 	makeFileSaver(..)
 | |
| // 	runFileLoaders(..)
 | |
| // 	runFileSavers(..)
 | |
| // 	fileUpdated(..)
 | |
| //
 | |
| // NOTE: config change to name will not affect this...
 | |
| function makeFileLoader(title, name, default_data, set_data, error, evt_name, skip_reg){
 | |
| 	var _loader = function(path, tracker){
 | |
| 		var res = $.Deferred()
 | |
| 
 | |
| 		// NOTE: these are static!!
 | |
| 		var file_dfl = makeBaseFilename(name)
 | |
| 		var file_pattern = makeFilenamePattern(name)
 | |
| 
 | |
| 		// default locations...
 | |
| 		if(path == null){
 | |
| 			var base = normalizePath(CONFIG.cache_dir_var)
 | |
| 			var loader = loadLatestJSONFile(base, 
 | |
| 					file_dfl, 
 | |
| 					file_pattern,
 | |
| 					null,
 | |
| 					default_data,
 | |
| 					tracker)
 | |
| 		
 | |
| 		// explicit path...
 | |
| 		} else {
 | |
| 			path = normalizePath(path)
 | |
| 			var base = path.split(CONFIG.cache_dir)[0]
 | |
| 			base = base +'/'+ CONFIG.cache_dir
 | |
| 
 | |
| 			var loader = loadLatestJSONFile(base, 
 | |
| 					file_dfl, 
 | |
| 					file_pattern,
 | |
| 					null,
 | |
| 					default_data,
 | |
| 					tracker)
 | |
| 		}
 | |
| 
 | |
| 		res.done(set_data)
 | |
| 
 | |
| 		bubbleProgress(title, loader, res)
 | |
| 
 | |
| 		if(error != null){
 | |
| 			res.fail(error)
 | |
| 		}
 | |
| 		if(evt_name != null){
 | |
| 			res.done(function(){ $('.viewer').trigger(evt_name) })
 | |
| 		}
 | |
| 		return res
 | |
| 	}
 | |
| 	if(!skip_reg){ 
 | |
| 		FILE_LOADERS.push(_loader)
 | |
| 	}
 | |
| 	return _loader
 | |
| }
 | |
| 
 | |
| 
 | |
| // XXX make this check for updates -- no need to re-save if nothing 
 | |
| // 		changed...
 | |
| function makeFileSaver(title, file_dfl, get_data, skip_reg){
 | |
| 	var _saver = function(path, date){
 | |
| 		path = path == null 
 | |
| 			? normalizePath(CONFIG.cache_dir_var +'/'+ makeFilename(file_dfl, date)) 
 | |
| 			: path
 | |
| 
 | |
| 		dumpJSON(path, get_data())
 | |
| 	}
 | |
| 	if(!skip_reg){
 | |
| 		FILE_SAVERS[title] = _saver
 | |
| 	}
 | |
| 	return _saver
 | |
| }
 | |
| 
 | |
| 
 | |
| // Mark file type as updated...
 | |
| //
 | |
| function fileUpdated(name){
 | |
| 	if(FILES_UPDATED.indexOf(name) < 0 && name in FILE_SAVERS){
 | |
| 		FILES_UPDATED.push(name)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| 
 | |
| // NOTE: this will bubble progress to next automatically...
 | |
| function runFileLoaders(prefix, next, tracker){
 | |
| 	FILES_UPDATED = []
 | |
| 	return $.when.apply(null, FILE_LOADERS.map(function(load){
 | |
| 		return bubbleProgress(prefix, load(null, tracker), next, true)
 | |
| 	}))
 | |
| }
 | |
| 
 | |
| 
 | |
| // NOTE: if all is set, this will force save everything...
 | |
| // XXX do we need bubbleProgress(..) here???
 | |
| function runFileSavers(path, date, all){
 | |
| 	var updated = FILES_UPDATED
 | |
| 	FILES_UPDATED = []
 | |
| 	for(var n in FILE_SAVERS){
 | |
| 		if(all || updated.indexOf(n) >= 0){
 | |
| 			showStatusQ('Saving: File:', n)
 | |
| 			FILE_SAVERS[n](path, date)
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| 
 | |
| 
 | |
| /************************************************************ Data ***/
 | |
| // XXX should this be here or in data.js???
 | |
| 
 | |
| // NOTE: this is not registered in file loaders as it needs to be called
 | |
| // 		explicitly before anything else is loaded...
 | |
| // NOTE: after v2.2 this will not load the current position as it's stored
 | |
| // 		in a separate file (current.json), see: loadFileState(..) for 
 | |
| // 		details...
 | |
| var loadFileData = makeFileLoader(
 | |
| 		'Data', 
 | |
| 		CONFIG.data_file, 
 | |
| 		null,
 | |
| 		function(data){ 
 | |
| 			// legacy format...
 | |
| 			if(data.version == null){
 | |
| 				json = convertDataGen1(data)
 | |
| 				DATA = data.data
 | |
| 				IMAGES = data.images
 | |
| 				MARKED = []
 | |
| 
 | |
| 			// version 2.*
 | |
| 			} else if(/2\.[0-9]*/.test(data.version)) {
 | |
| 				DATA = data
 | |
| 			}
 | |
| 		},
 | |
| 		null,
 | |
| 		null,
 | |
| 		// stop from registering in FILE_LOADERS...
 | |
| 		true)
 | |
| 
 | |
| 
 | |
| var saveFileData = makeFileSaver(
 | |
| 		'Data',
 | |
| 		CONFIG.data_file, 
 | |
| 		function(){ 
 | |
| 			var data = getAllData()
 | |
| 			data.current = DATA.current
 | |
| 			return data
 | |
| 		})
 | |
| 
 | |
| 
 | |
| // NOTE: this will set the updated flag ONLY of out of cropped mode...
 | |
| function dataUpdated(){
 | |
| 	if(!isViewCropped()){
 | |
| 		fileUpdated('Data')
 | |
| 		$('.viewer').trigger('dataUpdated')
 | |
| 	}
 | |
| }
 | |
| 
 | |
| 
 | |
| 
 | |
| /********************************************************** Images ***/
 | |
| 
 | |
| // Load images from file
 | |
| //
 | |
| // This will also merge all diff files.
 | |
| function loadFileImages(path, tracker, no_load_diffs){
 | |
| 	no_load_diffs = window.listDir == null ? true : no_load_diffs 
 | |
| 
 | |
| 	var res = $.Deferred()
 | |
| 
 | |
| 	// default locations...
 | |
| 	if(path == null){
 | |
| 		var base = normalizePath(CONFIG.cache_dir_var) 
 | |
| 		var loader = loadLatestJSONFile(base, 
 | |
| 				makeBaseFilename(CONFIG.images_file), 
 | |
| 				makeFilenamePattern(CONFIG.images_file), 
 | |
| 				makeDiffFilePattern(CONFIG.images_file),
 | |
| 				null,
 | |
| 				tracker)
 | |
| 	
 | |
| 	// explicit base dir...
 | |
| 	} else if(!/\.json$/i.test(path)) {
 | |
| 		var base = normalizePath(path +'/'+ CONFIG.cache_dir_var) 
 | |
| 		var loader = loadLatestJSONFile(base, 
 | |
| 				makeBaseFilename(CONFIG.images_file), 
 | |
| 				makeFilenamePattern(CONFIG.images_file), 
 | |
| 				makeDiffFilePattern(CONFIG.images_file),
 | |
| 				null,
 | |
| 				tracker)
 | |
| 
 | |
| 	// explicit path...
 | |
| 	} else {
 | |
| 		var loader = loadLatestJSONFile(normalizePath(path), null, null, null, null, tracker)
 | |
| 	}
 | |
| 
 | |
| 	bubbleProgress('Images', loader, res)
 | |
| 
 | |
| 	res.done(function(images){
 | |
| 		IMAGES = images
 | |
| 		IMAGES_UPDATED = []
 | |
| 		IMAGES_CREATED = false
 | |
| 
 | |
| 		// XXX is this the correct spot to do this???
 | |
| 		$('.viewer').trigger('imagesLoaded')
 | |
| 	})
 | |
| 
 | |
| 	return res
 | |
| }
 | |
| 
 | |
| 
 | |
| // Save current images list to file
 | |
| //
 | |
| var saveFileImages = makeFileSaver(
 | |
| 		'Images',
 | |
| 		CONFIG.images_file, 
 | |
| 		function(){ 
 | |
| 			IMAGES_UPDATED = []
 | |
| 			IMAGES_CREATED = false
 | |
| 			return IMAGES
 | |
| 		},
 | |
| 		true)
 | |
| 
 | |
| 
 | |
| function saveFileImagesAndRemoveDiffs(){
 | |
| 	// save...
 | |
| 	saveFileImages()
 | |
| 
 | |
| 	// remove...
 | |
| 	var diff_pattern = makeDiffFilePattern(CONFIG.images_file) 
 | |
| 	$.each($.map(listDir(normalizePath(CONFIG.cache_dir_var)), function(e){ 
 | |
| 			return diff_pattern.test(e) ? e : null
 | |
| 		}), function(i, e){
 | |
| 			showStatusQ('Removeing:', e)
 | |
| 			removeFile(normalizePath(CONFIG.cache_dir_var +'/'+ e))
 | |
| 		})
 | |
| }
 | |
| 
 | |
| 
 | |
| function saveFileImagesDiff(name, date){
 | |
| 	var path = normalizePath(CONFIG.cache_dir_var)
 | |
| 	name = name == null 
 | |
| 		? normalizePath(path +'/'+ makeDiffFilename(CONFIG.images_file, date)) 
 | |
| 		: name
 | |
| 
 | |
| 	var updated = {}
 | |
| 	$.each(IMAGES_UPDATED, function(i, e){
 | |
| 		updated[e] = IMAGES[e]
 | |
| 	})
 | |
| 
 | |
| 	dumpJSON(name, updated)
 | |
| 
 | |
| 	IMAGES_UPDATED = []
 | |
| }
 | |
| 
 | |
| 
 | |
| 
 | |
| /**********************************************************************
 | |
| * Global loaders/savers
 | |
| */
 | |
| 
 | |
| // Load images, ribbons and run registered load callbacks...
 | |
| //
 | |
| // XXX add support for explicit filenames...
 | |
| function loadFileState(path, prefix, tracker){
 | |
| 	prefix = prefix == null ? 'Data' : prefix
 | |
| 	prefix = prefix === false ? null : prefix
 | |
| 
 | |
| 	// XXX explicit data file path...
 | |
| 	if(/\.json$/i.test(path)){
 | |
| 		// XXX at this 
 | |
| 		var base = path.split(CONFIG.cache_dir)[0]
 | |
| 		base = base == path ? '.' : base
 | |
| 	} else {
 | |
| 		var base = path.split(CONFIG.cache_dir)[0]
 | |
| 		base = base == path ? '.' : base
 | |
| 	}
 | |
| 
 | |
| 	var res = $.Deferred()
 | |
| 
 | |
| 	// XXX load config...
 | |
| 
 | |
| 	bubbleProgress(prefix,
 | |
| 			loadFileData(path, tracker),
 | |
| 			res, 
 | |
| 			true)
 | |
| 		.done(function(json){
 | |
| 			setBaseURL(base)
 | |
| 
 | |
| 			// load the rest of the files...
 | |
| 			$.when(
 | |
| 					// load current position...
 | |
| 					// added on 2.2
 | |
| 					bubbleProgress(prefix,
 | |
| 							loadLatestJSONFile(path, 
 | |
| 								makeBaseFilename(CONFIG.current_file),
 | |
| 								null,
 | |
| 								null,
 | |
| 								DATA.current,
 | |
| 								tracker), 
 | |
| 							res, 
 | |
| 							true)
 | |
| 						.done(function(cur){
 | |
| 							DATA.current = cur
 | |
| 						}),
 | |
| 
 | |
| 					// load images...
 | |
| 					bubbleProgress(prefix,
 | |
| 						loadFileImages(base, tracker), res, true),
 | |
| 
 | |
| 					// run registered loaders...
 | |
| 					// added on 2.1
 | |
| 					runFileLoaders(prefix, res, tracker))
 | |
| 				.done(function(){
 | |
| 					$('.viewer').trigger('fileStateLoaded')
 | |
| 					reloadViewer()
 | |
| 					res.resolve()
 | |
| 				})
 | |
| 				// XXX fail???
 | |
| 		})
 | |
| 		.fail(function(){
 | |
| 			res.reject('Loading', path, 'Error')
 | |
| 		})
 | |
| 
 | |
| 	return res
 | |
| }
 | |
| 
 | |
| 
 | |
| // Save, ribbons and run registered save callbacks...
 | |
| //
 | |
| // NOTE: this will NOT save images, that operation must be explicitly 
 | |
| // 		performed by saveFileImages(...)
 | |
| //
 | |
| // XXX do propper reporting...
 | |
| // XXX check if name is needed and how it works...
 | |
| function saveFileState(name, no_normalize_path){
 | |
| 	var date = Date.timeStamp()
 | |
| 
 | |
| 	if(!no_normalize_path){
 | |
| 		var path = normalizePath(CONFIG.cache_dir_var)
 | |
| 	}
 | |
| 
 | |
| 	var cur_file = makeBaseFilename(CONFIG.current_file)
 | |
| 	showStatusQ('Saving: File:', cur_file)
 | |
| 	// allways update the current position...
 | |
| 	dumpJSON(path +'/'+ cur_file, DATA.current)
 | |
| 
 | |
| 	// save created images...
 | |
| 	if(IMAGES_CREATED){
 | |
| 		showStatusQ('Saving: File: Images.')
 | |
| 		saveFileImages(name, date)
 | |
| 
 | |
| 	// save the updated images...
 | |
| 	} else if(IMAGES_UPDATED.length > 0){
 | |
| 		showStatusQ('Saving: File: Images diff.')
 | |
| 		saveFileImagesDiff(name, date)
 | |
| 	}
 | |
| 
 | |
| 	// save the rest of the data...
 | |
| 	runFileSavers(name, date)
 | |
| }
 | |
| 
 | |
| 
 | |
| // Load a directory as-is
 | |
| //
 | |
| // XXX check if we need to pass down sorting settings to the generators...
 | |
| function loadRawDir(path, no_preview_processing, prefix){
 | |
| 	prefix = prefix == null ? 'Data' : prefix
 | |
| 	prefix = prefix === false ? null : prefix
 | |
| 
 | |
| 	var files = listDir(path)
 | |
| 
 | |
| 	var res = $.Deferred()
 | |
| 
 | |
| 	// filter images...
 | |
| 	var image_paths = $.map(files, function(e){
 | |
| 		return IMAGE_PATTERN.test(e) ? e : null
 | |
| 	})
 | |
| 
 | |
| 	if(image_paths.length == 0){
 | |
| 		// no images in path...
 | |
| 		res.notify(prefix, 'Load', path, 'Error')
 | |
| 		return res.reject()
 | |
| 	}
 | |
| 
 | |
| 	setBaseURL(path)
 | |
| 
 | |
| 	IMAGES = imagesFromUrls(image_paths)
 | |
| 	IMAGES_CREATED = true
 | |
| 	res.notify(prefix, 'Loaded', 'Images.')
 | |
| 
 | |
| 	DATA = dataFromImages(IMAGES)
 | |
| 	dataUpdated()
 | |
| 	res.notify(prefix, 'Loaded', 'Data.')
 | |
| 
 | |
| 	// XXX this will reload viewer...
 | |
| 	//updateRibbonsFromFavDirs()
 | |
| 	DATA.ribbons = ribbonsFromFavDirs(null, null, imageOrderCmp)
 | |
| 	res.notify(prefix, 'Loaded', 'Fav dirs.')
 | |
| 
 | |
| 	MARKED = []
 | |
| 
 | |
| 	reloadViewer()
 | |
| 
 | |
| 	var _run = function(func){
 | |
| 		var res = $.Deferred()
 | |
| 		func()
 | |
| 			//.done(function(){ 
 | |
| 			.depleted(function(){ 
 | |
| 				res.resolve() 
 | |
| 			})
 | |
| 		return res
 | |
| 	}
 | |
| 
 | |
| 	// load/generate previews...
 | |
| 	if(!no_preview_processing){
 | |
| 		var p = _run(makeImagesPreviewsQ)
 | |
| 	} else {
 | |
| 		var p = $.Deferred().resolve()
 | |
| 	}
 | |
| 
 | |
| 	// read orientation form files...
 | |
| 	res.notify(prefix, 'Loading', 'Images metadata.')
 | |
| 	var o = $.when(
 | |
| 			p,
 | |
| 			_run(readImagesOrientationQ),
 | |
| 			_run(readImagesDatesQ),
 | |
| 			// XXX this is still experimental...
 | |
| 			// 		...not sure if this will not cause race conditions with
 | |
| 			// 		the other readers...
 | |
| 			_run(updateImagesGIDsQ)
 | |
| 		)
 | |
| 		.done(function(){
 | |
| 			res.notify(prefix, 'Loaded', 'Images metadata.')
 | |
| 			//updateImagesGIDsQ()
 | |
| 		})
 | |
| 
 | |
| 	// NOTE: we are not waiting for previews and orientation...
 | |
| 	return res.resolve()
 | |
| }
 | |
| 
 | |
| 
 | |
| // Load a path
 | |
| //
 | |
| // This will try and do one of the following in order:
 | |
| // 	1) look for a cache and load it,
 | |
| // 	2) load data from within the directory
 | |
| // 	3) load a directory as-is
 | |
| // 		load fav dirs
 | |
| //
 | |
| // NOTE: this will create an images.json file in cache on opening an 
 | |
| // 		un-cached dir (XXX is this correct???)
 | |
| // NOTE: if tracker === false no tracker will get created...
 | |
| function loadDir(path, no_preview_processing, prefix, tracker){
 | |
| 	prefix = prefix == null ? 'Data' : prefix
 | |
| 	prefix = prefix === false ? null : prefix
 | |
| 
 | |
| 	var res = $.Deferred()
 | |
| 
 | |
| 	if(tracker == null){
 | |
| 		var tracker = statusProgress('Loading')
 | |
| 		// XXX is this the right way to go???
 | |
| 		res
 | |
| 			.done(function(){
 | |
| 				tracker.resolve()
 | |
| 			})
 | |
| 			.fail(function(){
 | |
| 				tracker.reject()
 | |
| 			})
 | |
| 	}
 | |
| 	if(tracker == false){
 | |
| 		tracker == null
 | |
| 	}
 | |
| 
 | |
| 	// stop all workers running on current image set before we 
 | |
| 	// move to the next...
 | |
| 	// XXX is this the correct spot for this???
 | |
| 	killAllWorkers()
 | |
| 
 | |
| 	IMAGES_CREATED = false
 | |
| 
 | |
| 	path = normalizePath(path)
 | |
| 	var orig_path = path
 | |
| 	var data
 | |
| 
 | |
| 	res.notify(prefix, 'Loading', path)
 | |
| 
 | |
| 	var files = listDir(path)
 | |
| 
 | |
| 	if(files == null){
 | |
| 		//showErrorStatus('No files in path: ' + path)
 | |
| 		res.notify('load_error', path)
 | |
| 		return res.reject()
 | |
| 	}
 | |
| 
 | |
| 	// see if there is a cache...
 | |
| 	if(files.indexOf(CONFIG.cache_dir) >= 0){
 | |
| 		path = path +'/'+ CONFIG.cache_dir
 | |
| 	}
 | |
| 
 | |
| 	bubbleProgress(prefix, 
 | |
| 			loadFileState(path, false, tracker), res, true)
 | |
| 		.done(function(){
 | |
| 			res.resolve()
 | |
| 		})
 | |
| 		.fail(function(){
 | |
| 			bubbleProgress('Raw directory', 
 | |
| 				loadRawDir(orig_path, no_preview_processing), res)
 | |
| 		})
 | |
| 
 | |
| 	return res
 | |
| }
 | |
| 
 | |
| 
 | |
| // Construct a ribbons hierarchy from the fav dirs structure
 | |
| //
 | |
| // NOTE: this depends on listDir(...)
 | |
| // NOTE: this assumes that images contain ALL the images...
 | |
| // NOTE: this assumes that all file names are unique...
 | |
| function ribbonsFromFavDirs(path, images, cmp, dir_name){
 | |
| 	path = path == null ? getBaseURL() : path
 | |
| 	images = images == null ? IMAGES : images
 | |
| 	dir_name = dir_name == null ? 'fav' : dir_name
 | |
| 
 | |
| 	// build a reverse name-gid index for fast access...
 | |
| 	var index = {}
 | |
| 	var name
 | |
| 	for(var gid in images){
 | |
| 		name = getImageFileName(gid)
 | |
| 		// XXX we assume that names are unique...
 | |
| 		index[name] = gid
 | |
| 	}
 | |
| 
 | |
| 	var ribbons = []
 | |
| 	// add the base row...
 | |
| 	var base = Object.keys(images)
 | |
| 	ribbons.push(base)
 | |
| 
 | |
| 	var files = listDir(path)	
 | |
| 	var cur_path = path
 | |
| 	while(files.indexOf(dir_name) >= 0){
 | |
| 		cur_path += '/' + dir_name
 | |
| 		files = listDir(cur_path)
 | |
| 		ribbon = []
 | |
| 		// collect the images...
 | |
| 		$.each(files, function(i, e){
 | |
| 			var _gid = index[e]
 | |
| 			// skip files not in index...
 | |
| 			// NOTE: we do not need to filter the files by name as we 
 | |
| 			// 		trust the index...
 | |
| 			if(_gid == null){
 | |
| 				return 
 | |
| 			}
 | |
| 			// remove the found item from each of the below ribbons...
 | |
| 			$.each(ribbons, function(i ,e){
 | |
| 				if(e.indexOf(_gid) != -1){
 | |
| 					e.splice(e.indexOf(_gid), 1)
 | |
| 				}
 | |
| 			})
 | |
| 
 | |
| 			ribbon.push(_gid)
 | |
| 		})
 | |
| 		ribbons.push(ribbon)
 | |
| 	}
 | |
| 
 | |
| 	// remove empty ribbons and sort the rest...
 | |
| 	ribbons = $.map(ribbons, function(e){ 
 | |
| 		return e.length > 0 ? [cmp == null ? e : e.sort(cmp)] : null 
 | |
| 	})
 | |
| 
 | |
| 	return ribbons.reverse()
 | |
| }
 | |
| 
 | |
| 
 | |
| // Load ribbon structure from fav directory tree
 | |
| //
 | |
| // XXX loads duplicate images....
 | |
| function updateRibbonsFromFavDirs(){
 | |
| 	DATA.ribbons = ribbonsFromFavDirs(null, null, imageOrderCmp)
 | |
| 	sortImagesByDate()
 | |
| 	reloadViewer()
 | |
| 	dataUpdated()
 | |
| }
 | |
| 
 | |
| 
 | |
| function exportImageTo(gid, path, im_name, size){
 | |
| 	path = path == null ? BASE_URL : path
 | |
| 	im_name = im_name == null ? '%f' : im_name
 | |
| 	size = size == null ? 1000 : size
 | |
| 
 | |
| 	// get correct preview...
 | |
| 	var src = getBestPreview(gid, size).url
 | |
| 	var orig = getImageFileName(gid)
 | |
| 
 | |
| 	// XXX might be a good idea to combine this with docs as a 
 | |
| 	// 		single mechanism...
 | |
| 	// form image name...
 | |
| 	var dest = im_name
 | |
| 	// full filename...
 | |
| 	dest = dest.replace('%f', orig)
 | |
| 	// file name w.o. ext...
 | |
| 	dest = dest.replace('%n', orig.split('.')[0])
 | |
| 	// ext...
 | |
| 	dest = dest.replace('%e', '.'+src.split('.').pop())
 | |
| 	// marked status...
 | |
| 	dest = dest.replace(/%\(([^)]*)\)m/, MARKED.indexOf(gid) >= 0 ? '$1' : '')
 | |
| 	// bookmarked status...
 | |
| 	dest = dest.replace(/%\(([^)]*)\)b/, BOOKMARKS.indexOf(gid) >= 0 ? '$1' : '')
 | |
| 	// gid...
 | |
| 	dest = dest.replace('%gid', gid)
 | |
| 	dest = dest.replace('%g', gid.slice(34))
 | |
| 
 | |
| 	dest = path +'/'+ dest
 | |
| 
 | |
| 	// copy... 
 | |
| 	return (function(src, dest){
 | |
| 		return copyFile(src, dest)
 | |
| 			.done(function(){
 | |
| 				// XXX do we actually need this???
 | |
| 				console.log(src, 'done.')
 | |
| 			})
 | |
| 			.fail(function(err){
 | |
| 				// XXX do we actually need this???
 | |
| 				console.warn(src, 'err:', err)
 | |
| 			})
 | |
| 	})(src, dest)
 | |
| }
 | |
| 
 | |
| 
 | |
| // Export current state to directory...
 | |
| //
 | |
| // XXX this copies the files in parallel, make it sync and sequential...
 | |
| // 		...reason is simple, if we stop the copy we need to end up with 
 | |
| // 		part of the files copied full rather than all partially...
 | |
| function exportImagesTo(path, im_name, dir_name, size){
 | |
| 	path = path == null ? BASE_URL : path
 | |
| 	im_name = im_name == null ? '%f' : im_name
 | |
| 	dir_name = dir_name == null ? 'fav' : dir_name
 | |
| 	size = size == null ? 1000 : size
 | |
| 
 | |
| 	var res = $.Deferred()
 | |
| 
 | |
| 	var base_path = path
 | |
| 	path = normalizePath(path)
 | |
| 
 | |
| 	var order = DATA.order
 | |
| 	var Z = (('10e' + (order.length + '').length) * 1 + '').slice(2)
 | |
| 
 | |
| 	// mainly used for file naming, gives us ability to number images 
 | |
| 	// in the current selection...
 | |
| 	var selection = []
 | |
| 	$.each(DATA.ribbons, function(_, e){
 | |
| 		selection = selection.concat(e)
 | |
| 	})
 | |
| 	selection.sort(imageOrderCmp)
 | |
| 	var z = (('10e' + (selection.length + '').length) * 1 + '').slice(2)
 | |
| 
 | |
| 	// use an external pool...
 | |
| 	//var pool = makeDeferredPool()
 | |
| 	var pool = getWorkerQueue('Export previews', 64)
 | |
| 		.depleted(function(){
 | |
| 			showStatusQ('Export: done.')
 | |
| 			res.resolve()
 | |
| 		})
 | |
| 		.filling()
 | |
| 
 | |
| 	// go through ribbons...
 | |
| 	for(var i=DATA.ribbons.length-1; i >= 0; i--){
 | |
| 		var ribbon = DATA.ribbons[i]
 | |
| 		// go through images...
 | |
| 		for(var j=0; j < ribbon.length; j++){
 | |
| 			var gid = ribbon[j]
 | |
| 
 | |
| 			// do global naming...
 | |
| 			var dest = im_name
 | |
| 			// global order...
 | |
| 			var o = order.indexOf(gid) + ''
 | |
| 			dest = dest.replace('%I', (Z + o).slice(o.length))
 | |
| 			// current order...
 | |
| 			var o = selection.indexOf(gid) + ''
 | |
| 			dest = dest.replace('%i', (z + o).slice(o.length))
 | |
| 
 | |
| 			pool.enqueue(exportImageTo, gid, path, dest, size)
 | |
| 		}
 | |
| 
 | |
| 		path = normalizePath(path +'/'+ dir_name)
 | |
| 	}
 | |
| 
 | |
| 	pool.doneFilling()
 | |
| 	return res
 | |
| }
 | |
| 
 | |
| 
 | |
| 
 | |
| /**********************************************************************
 | |
| * Metadata readers...
 | |
| */
 | |
| 
 | |
| // NOTE: this will overwrite current image orientation...
 | |
| //
 | |
| // XXX this depends on getImageOrientation(...)
 | |
| function readImageOrientation(gid, no_update_loaded){
 | |
| 	gid = gid == null ? getImageGID() : gid
 | |
| 	var img = IMAGES[gid]
 | |
| 
 | |
| 	if(img == null){
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	return getImageOrientation(normalizePath(img.path))
 | |
| 		.done(function(o){
 | |
| 			var o_o = img.orientation
 | |
| 			var o_f = img.flipped
 | |
| 
 | |
| 			img.orientation = o.orientation
 | |
| 			img.flipped = o.flipped
 | |
| 
 | |
| 			// mark image dirty...
 | |
| 			if(o_o != o.orientation || o_f != o.flipped){
 | |
| 				imageUpdated(gid)
 | |
| 			}
 | |
| 
 | |
| 			// update image if loaded...
 | |
| 			if(!no_update_loaded){
 | |
| 				var o = getImage(gid)
 | |
| 				if(o.length > 0){
 | |
| 					updateImage(o)
 | |
| 				}
 | |
| 			}
 | |
| 		})
 | |
| }
 | |
| function readImagesOrientation(gids, no_update_loaded){
 | |
| 	gids = gids == null ? getClosestGIDs() : gids
 | |
| 	var res = []
 | |
| 
 | |
| 	$.each(gids, function(_, gid){
 | |
| 		res.push(readImageOrientation(gid, no_update_loaded))
 | |
| 	})
 | |
| 
 | |
| 	return $.when.apply(null, res)
 | |
| }
 | |
| // queued version of readImagesOrientation(...)
 | |
| //
 | |
| function readImagesOrientationQ(gids, no_update_loaded){
 | |
| 	gids = gids == null ? getClosestGIDs() : gids
 | |
| 
 | |
| 	var queue = getWorkerQueue('Read images orientation', 4)
 | |
| 		.filling()
 | |
| 
 | |
| 	var last = null
 | |
| 
 | |
| 	// attach workers to queue...
 | |
| 	$.each(gids, function(_, gid){
 | |
| 		last = queue.enqueue(readImageOrientation, gid, no_update_loaded)
 | |
| 	})
 | |
| 
 | |
| 	return queue.doneFilling()
 | |
| }
 | |
| 
 | |
| 
 | |
| function readImageDate(gid, images){
 | |
| 	images = images == null ? IMAGES : images
 | |
| 	var img = images[gid]
 | |
| 	return getEXIFDate(normalizePath(img.path))
 | |
| 		.done(function(date){
 | |
| 			img.ctime = Date.fromTimeStamp(date).getTime()/1000
 | |
| 			imageUpdated(gid)
 | |
| 		})
 | |
| }
 | |
| function readImagesDates(images){
 | |
| 	images = images == null ? IMAGES : images
 | |
| 
 | |
| 	return $.when.apply(null, $.map(images, function(_, gid){
 | |
| 		return readImageDate(gid, images)
 | |
| 	}))
 | |
| }
 | |
| function readImagesDatesQ(images){
 | |
| 	images = images == null ? IMAGES : images
 | |
| 
 | |
| 	var queue = getWorkerQueue('Read images dates', 4)
 | |
| 		.filling()
 | |
| 
 | |
| 	$.each(images, function(gid, img){
 | |
| 		queue.enqueue(readImageDate, gid, images)
 | |
| 	})
 | |
| 
 | |
| 	return queue.doneFilling()
 | |
| }
 | |
| 
 | |
| 
 | |
| function updateImageGID(gid, images, data){
 | |
| 	gid = gid == null ? getImageGID() : gid
 | |
| 	images = images == null ? IMAGES : images
 | |
| 	data = data == null ? DATA : data
 | |
| 	var img = images[gid]
 | |
| 	var key = gid
 | |
| 
 | |
| 	return getEXIFGID(normalizePath(img.path))
 | |
| 		.done(function(gid){
 | |
| 			if(gid == img.id){
 | |
| 				return
 | |
| 			}
 | |
| 
 | |
| 			img.id = gid
 | |
| 
 | |
| 			// images...
 | |
| 			images[gid] = images[key]
 | |
| 			delete images[key]
 | |
| 			imageUpdated(gid)
 | |
| 
 | |
| 			// data...
 | |
| 			if(data != null){
 | |
| 				// replace current...
 | |
| 				if(data.current == key){
 | |
| 					data.current = gid
 | |
| 				}
 | |
| 				// replace in order...
 | |
| 				data.order[data.order.indexOf(key)] = gid
 | |
| 				// replace in ribbons...
 | |
| 				for(var i=0; i < data.ribbons.length; i++){
 | |
| 					var r = data.ribbons[i]
 | |
| 					var k = r.indexOf(key)
 | |
| 					if(k >= 0){
 | |
| 						r[k] = gid
 | |
| 					}
 | |
| 				}
 | |
| 
 | |
| 				dataUpdated()
 | |
| 			}
 | |
| 
 | |
| 			// update the actual image...
 | |
| 			var i = getImage(key)
 | |
| 			if(i.length > 0){
 | |
| 				updateImage(i, gid)
 | |
| 			}
 | |
| 
 | |
| 			$('.viewer').trigger('updatedImageGID', [key, gid])
 | |
| 		})
 | |
| }
 | |
| function updateImagesGIDs(images, data){
 | |
| 	images = images == null ? IMAGES : images
 | |
| 
 | |
| 	var res = $.when.apply(null, $.map(images, function(_, key){
 | |
| 		return updateImageGID(key, images, data)
 | |
| 	}))
 | |
| 
 | |
| 	IMAGES_CREATED = true
 | |
| 
 | |
| 	return res
 | |
| }
 | |
| function updateImagesGIDsQ(images, data){
 | |
| 	images = images == null ? IMAGES : images
 | |
| 
 | |
| 	var queue = getWorkerQueue('Update GIDs', 4)
 | |
| 		.filling()
 | |
| 
 | |
| 	$.each(images, function(key){
 | |
| 		queue.enqueue(updateImageGID, key, images, data)
 | |
| 	})
 | |
| 
 | |
| 	IMAGES_CREATED = true
 | |
| 
 | |
| 	return queue.doneFilling()
 | |
| }
 | |
| 
 | |
| 
 | |
| 
 | |
| /**********************************************************************
 | |
| * vim:set ts=4 sw=4 :                                                */
 |