mirror of
https://github.com/flynx/ImageGrid.git
synced 2025-10-28 18:00:09 +00:00
1837 lines
48 KiB
JavaScript
Executable File
1837 lines
48 KiB
JavaScript
Executable File
/**********************************************************************
|
|
*
|
|
*
|
|
*
|
|
**********************************************************************/
|
|
|
|
define(function(require){ var module = {}
|
|
|
|
//var DEBUG = DEBUG != null ? DEBUG : true
|
|
|
|
// XXX this should not be imported!!!
|
|
// ...something wrong with requirejs(..)
|
|
if(typeof(process) != 'undefined'){
|
|
var fse = requirejs('fs-extra')
|
|
var pathlib = requirejs('path')
|
|
var glob = requirejs('glob')
|
|
var file = requirejs('./file')
|
|
}
|
|
|
|
var data = require('data')
|
|
var images = require('images')
|
|
|
|
var util = require('lib/util')
|
|
|
|
var actions = require('lib/actions')
|
|
var features = require('lib/features')
|
|
var keyboard = require('lib/keyboard')
|
|
|
|
var core = require('features/core')
|
|
|
|
var widgets = require('features/ui-widgets')
|
|
|
|
var overlay = require('lib/widget/overlay')
|
|
var browse = require('lib/widget/browse')
|
|
var browseWalk = require('lib/widget/browse-walk')
|
|
|
|
|
|
|
|
/*********************************************************************/
|
|
|
|
if(typeof(process) != 'undefined'){
|
|
var copy = file.denodeify(fse.copy)
|
|
var ensureDir = file.denodeify(fse.ensureDir)
|
|
}
|
|
|
|
|
|
|
|
/*********************************************************************/
|
|
// fs reader/loader...
|
|
|
|
|
|
// XXX revise base path mechanics...
|
|
// .loaded_paths
|
|
var FileSystemLoaderActions = actions.Actions({
|
|
config: {
|
|
'index-dir': '.ImageGrid',
|
|
|
|
'image-file-pattern': '*+(jpg|jpeg|png|JPG|JPEG|PNG)',
|
|
|
|
'image-file-read-stat': true,
|
|
|
|
// XXX if true and multiple indexes found, load only the first
|
|
// without merging...
|
|
'load-first-index-only': false,
|
|
},
|
|
|
|
clone: [function(full){
|
|
return function(res){
|
|
if(this.location){
|
|
res.location.path = this.location.path
|
|
res.location.method = this.location.method
|
|
}
|
|
if(this.loaded_paths){
|
|
res.loaded_paths = JSON.parse(JSON.stringify(this.loaded_paths))
|
|
}
|
|
}
|
|
}],
|
|
|
|
loaded_paths: null,
|
|
|
|
|
|
// XXX should this be more general???
|
|
reloadState: ['File/Reload viewer state...',
|
|
function(){
|
|
if(this.location
|
|
&& this.location.method
|
|
&& this.location.path){
|
|
return this[this.location.method](this.location.path)
|
|
}
|
|
}],
|
|
|
|
|
|
// XXX is this a hack???
|
|
// XXX need a more generic form...
|
|
checkPath: ['- File/',
|
|
function(path){ return fse.existsSync(path) }],
|
|
|
|
|
|
loadSaveHistoryList: ['- File/',
|
|
function(path){
|
|
path = path || this.location.path
|
|
|
|
return file.loadSaveHistoryList(path)
|
|
}],
|
|
|
|
// NOTE: when passed no path this will not do anything...
|
|
// NOTE: this will add a .from field to .location, this will indicate
|
|
// the date starting from which saves are loaded.
|
|
//
|
|
// XXX how should .location be handled when merging indexes or
|
|
// viewing multiple/clustered indexes???
|
|
// XXX add a symmetric equivalent to .prepareIndexForWrite(..) so as
|
|
// to enable features to load their data...
|
|
// XXX should this return a promise??? ...a clean promise???
|
|
// XXX look inside...
|
|
loadIndex: ['- File/Load index',
|
|
function(path, from_date, logger){
|
|
var that = this
|
|
|
|
if(path == null){
|
|
return
|
|
}
|
|
if(from_date && from_date.emit != null){
|
|
logger = from_date
|
|
from_date = null
|
|
}
|
|
|
|
// XXX get a logger...
|
|
logger = logger || this.logger
|
|
|
|
// XXX make this load incrementally (i.e. and EventEmitter
|
|
// a-la glob)....
|
|
//file.loadIndex(path, this.config['index-dir'], logger)
|
|
return file.loadIndex(path, this.config['index-dir'], from_date, logger)
|
|
.catch(function(err){
|
|
// XXX
|
|
console.error(err)
|
|
})
|
|
.then(function(res){
|
|
// XXX if res is empty load raw...
|
|
|
|
// XXX use the logger...
|
|
//console.log('FOUND INDEXES:', Object.keys(res).length)
|
|
|
|
// skip nested paths...
|
|
// XXX make this optional...
|
|
// XXX this is best done BEFORE we load all the
|
|
// indexes, e.g. in .loadIndex(..)
|
|
var paths = Object.keys(res)
|
|
var skipped = []
|
|
paths
|
|
.sort()
|
|
.forEach(function(p){
|
|
// already removed...
|
|
if(skipped.indexOf(p) >= 0){
|
|
return
|
|
}
|
|
|
|
paths
|
|
// get all paths that fully contain p...
|
|
.filter(function(o){
|
|
return o != p && o.indexOf(p) == 0
|
|
})
|
|
// drop all longer paths...
|
|
.forEach(function(e){
|
|
skipped.push(e)
|
|
delete res[e]
|
|
})
|
|
})
|
|
//console.log('SKIPPING NESTED:', skipped.length)
|
|
|
|
var index
|
|
var base_path
|
|
var loaded = []
|
|
|
|
// NOTE: res may contain multiple indexes...
|
|
//for(var k in res){
|
|
for(var i=0; i < paths.length; i++){
|
|
var k = paths[i]
|
|
|
|
// skip empty indexes...
|
|
// XXX should we rebuild or list here???
|
|
if(res[k].data == null || res[k].images == null){
|
|
continue
|
|
}
|
|
|
|
var part = file.buildIndex(res[k], k)
|
|
|
|
// load the first index...
|
|
if(index == null){
|
|
// XXX use the logger...
|
|
//console.log('LOADING:', k, res)
|
|
logger && logger.emit('base index', k, res)
|
|
|
|
index = part
|
|
|
|
// merge indexes...
|
|
// XXX need to skip sub-indexes in the same sub-tree...
|
|
// ...skip any path that fully contains an
|
|
// already loaded path..
|
|
// XXX load data in chunks rather than merge...
|
|
} else {
|
|
//console.log('MERGING:', k, part)
|
|
logger && logger.emit('merge index', k, res)
|
|
|
|
// merge...
|
|
index.data.join(part.data)
|
|
index.images.join(part.images)
|
|
}
|
|
|
|
loaded.push(k)
|
|
|
|
// XXX do a better merge and remove this...
|
|
// ...we either need to lazy-load clustered indexes
|
|
// or merge, in both cases base_path should reflet
|
|
// the fact that we have multiple indexes...
|
|
if(that.config['load-first-index-only']){
|
|
break
|
|
}
|
|
}
|
|
|
|
logger && logger.emit('load index', index)
|
|
|
|
that.load(index)
|
|
|
|
that.loaded_paths = loaded
|
|
that.__location = {
|
|
path: loaded.length == 1 ? loaded[0] : path,
|
|
method: 'loadIndex',
|
|
}
|
|
|
|
if(from_date){
|
|
that.__location.from = from_date
|
|
}
|
|
})
|
|
}],
|
|
// XXX use the logger...
|
|
// XXX add a recursive option...
|
|
// ...might also be nice to add sub-dirs to ribbons...
|
|
// XXX make image pattern more generic...
|
|
// XXX should this return a promise??? ...a clean promise???
|
|
loadImages: ['- File/Load images',
|
|
function(path, logger){
|
|
if(path == null){
|
|
return
|
|
}
|
|
|
|
var that = this
|
|
|
|
// NOTE: we set this before we start the load so as to let
|
|
// clients know what we are loading and not force them
|
|
// to wait to find out...
|
|
// XXX not sure if this is the way to go...
|
|
this.__location = {
|
|
path: path,
|
|
method: 'loadImages',
|
|
}
|
|
|
|
glob(path + '/'+ this.config['image-file-pattern'],
|
|
{stat: !!this.config['image-file-read-stat']})
|
|
.on('error', function(err){
|
|
console.log('!!!!', err)
|
|
})
|
|
/*
|
|
.on('match', function(img){
|
|
// XXX stat stuff...
|
|
fse.statSync(img)
|
|
})
|
|
*/
|
|
.on('end', function(lst){
|
|
that.loadURLs(lst, path)
|
|
// XXX do we need to normalize paths after we get them from glob??
|
|
//that.loadURLs(lst.map(pathlib.posix.normalize), path)
|
|
//that.loadURLs(lst
|
|
// .map(function(p){ return util.normalizePath(p) }), path)
|
|
|
|
if(!!that.config['image-file-read-stat']){
|
|
var stats = this.statCache
|
|
var p = pathlib.posix
|
|
|
|
that.images.forEach(function(gid, img){
|
|
var stat = stats[p.join(img.base_path, img.path)]
|
|
|
|
img.atime = stat.atime
|
|
img.mtime = stat.mtime
|
|
img.ctime = stat.ctime
|
|
img.birthtime = stat.birthtime
|
|
|
|
img.size = stat.size
|
|
|
|
// XXX do we need anything else???
|
|
})
|
|
}
|
|
|
|
// NOTE: we set it again because .loadURLs() does a clear
|
|
// before it starts loading...
|
|
// XXX is this a bug???
|
|
that.__location = {
|
|
path: path,
|
|
method: 'loadImages',
|
|
}
|
|
})
|
|
}],
|
|
|
|
// XXX auto-detect format or let the user chose...
|
|
// XXX should this return a promise??? ...a clean promise???
|
|
// XXX should the added section be marked or sorted???
|
|
loadPath: ['- File/Load path (STUB)',
|
|
function(path, logger){
|
|
// XXX check if this.config['index-dir'] exists, if yes then
|
|
// .loadIndex(..) else .loadImages(..)
|
|
|
|
//this.location.method = 'loadImages'
|
|
}],
|
|
|
|
// XXX should this return a promise??? ...a clean promise???
|
|
// XXX revise logger...
|
|
loadNewImages: ['File/Load new images',
|
|
function(path, logger){
|
|
path = path || this.location.path
|
|
logger = logger || this.logger
|
|
|
|
if(path == null){
|
|
return
|
|
}
|
|
|
|
var that = this
|
|
|
|
// cache the loaded images...
|
|
var loaded = this.images.map(function(gid, img){ return img.path })
|
|
var base_pattern = RegExp('^'+path)
|
|
|
|
// find images...
|
|
glob(path + '/'+ this.config['image-file-pattern'],
|
|
{stat: !!this.config['image-file-read-stat']})
|
|
.on('end', function(lst){
|
|
var stats = this.statCache
|
|
|
|
// create a new images chunk...
|
|
lst = lst
|
|
// filter out loaded images...
|
|
.filter(function(p){
|
|
return loaded.indexOf(
|
|
util.normalizePath(p)
|
|
// remove the base path if it exists...
|
|
.replace(base_pattern, '')
|
|
// normalize the leading './'
|
|
.replace(/^[\/\\]+/, './')) < 0
|
|
})
|
|
|
|
|
|
// nothing new...
|
|
if(lst.length == 0){
|
|
// XXX
|
|
logger && logger.emit('loaded', [])
|
|
return
|
|
}
|
|
|
|
// XXX
|
|
logger && logger.emit('queued', lst)
|
|
|
|
var new_images = images.Images.fromArray(lst, path)
|
|
var gids = new_images.keys()
|
|
var new_data = that.data.constructor.fromArray(gids)
|
|
|
|
new_images.forEach(function(gid, img){
|
|
var stat = stats[p.join(img.base_path, img.path)]
|
|
|
|
img.atime = stat.atime
|
|
img.mtime = stat.mtime
|
|
img.ctime = stat.ctime
|
|
img.birthtime = stat.birthtime
|
|
|
|
img.size = stat.size
|
|
|
|
// XXX do we need anything else???
|
|
})
|
|
|
|
// merge with index...
|
|
// NOTE: we are prepending new images to the start...
|
|
// NOTE: all ribbon gids will change here...
|
|
var cur = that.data.current
|
|
// XXX this does not seem to work...
|
|
//that.data = new_data.join(that.data)
|
|
that.data = new_data.join('top', that.data)
|
|
that.data.current = cur
|
|
|
|
that.images.join(new_images)
|
|
|
|
that.reload()
|
|
|
|
// XXX report that we are done...
|
|
logger && logger.emit('loaded', lst)
|
|
})
|
|
}],
|
|
|
|
clear: [function(){
|
|
delete this.__location
|
|
delete this.loaded_paths
|
|
}],
|
|
})
|
|
|
|
|
|
var FileSystemLoader =
|
|
module.FileSystemLoader = core.ImageGridFeatures.Feature({
|
|
title: '',
|
|
doc: '',
|
|
|
|
tag: 'fs-loader',
|
|
depends: [
|
|
'location',
|
|
'tasks',
|
|
],
|
|
suggested: [
|
|
'ui-fs-loader',
|
|
'fs-url-history',
|
|
],
|
|
|
|
actions: FileSystemLoaderActions,
|
|
|
|
isApplicable: function(){
|
|
return this.runtime == 'node' || this.runtime == 'nw' },
|
|
})
|
|
|
|
|
|
|
|
//---------------------------------------------------------------------
|
|
|
|
// XXX would need to delay the original action while the user is
|
|
// browsing...
|
|
var makeBrowseProxy = function(action, callback){
|
|
return widgets.uiDialog(function(path, logger){
|
|
var that = this
|
|
path = path || this.location.path
|
|
// XXX should we set a start path here to current???
|
|
return this.browsePath(path,
|
|
function(path){
|
|
var res = that[action](path, logger)
|
|
callback && callback.call(that, path)
|
|
return res
|
|
})
|
|
})
|
|
}
|
|
|
|
|
|
// XXX show list of indexes when more than one are found....
|
|
// Ex:
|
|
// - <index-1> x - 'x' will strike out the element...
|
|
// - <index-2> x
|
|
// - ...
|
|
// - load all - load all non striked out elements
|
|
// ...would be nice to add either ability to sort manually or some
|
|
// modes of auto-sorting, or both...
|
|
// ...might be a good idea to add root images with an option to
|
|
// load them...
|
|
// ...do not think that recursively searching for images is a
|
|
// good idea...
|
|
var FileSystemLoaderUIActions = actions.Actions({
|
|
config: {
|
|
// list of loaders to complete .browsePath(..) action
|
|
//
|
|
// NOTE: these will be displayed in the same order as they appear
|
|
// in the list.
|
|
// NOTE: the first one is auto-selected.
|
|
'path-loaders': [
|
|
'loadIndex',
|
|
'loadImages',
|
|
//'loadPath',
|
|
],
|
|
|
|
'file-browser-settings': {
|
|
disableFiles: true,
|
|
showNonTraversable: true,
|
|
showDisabled: true,
|
|
},
|
|
|
|
// if set true, if unsaved changes present when opening a save
|
|
// history state, save the changes...
|
|
'auto-save-on-save-history-open': true,
|
|
},
|
|
|
|
// Save comments...
|
|
//
|
|
// Format:
|
|
// {
|
|
// // comment staged for next .saveIndex(..)...
|
|
// 'current': <comment>,
|
|
//
|
|
// <timestamp>: <comment>,
|
|
// ...
|
|
// }
|
|
savecomments: null,
|
|
|
|
|
|
// Comment a save...
|
|
//
|
|
// Comment current save...
|
|
// .setSaveComment(comment)
|
|
// -> actions
|
|
//
|
|
// Reset current save comment...
|
|
// .setSaveComment(null)
|
|
// -> actions
|
|
//
|
|
// Comment specific save...
|
|
// .setSaveComment(save, comment)
|
|
// -> actions
|
|
//
|
|
// Reset specific save comment...
|
|
// .setSaveComment(save, null)
|
|
// -> actions
|
|
//
|
|
// NOTE: "save" is the save format as returned by file.groupByDate(..),
|
|
// or .loadSaveHistoryList(..)
|
|
// ...normally it is Date.timeStamp() compatible string.
|
|
setSaveComment: ['- File/Comment a save',
|
|
function(save, comment){
|
|
var comments = this.savecomments = this.savecomments || {}
|
|
|
|
// no explicit save given -- stage a comment for next save...
|
|
if(comment === undefined){
|
|
comment = save
|
|
save = 'current'
|
|
}
|
|
|
|
if(comment === undefined){
|
|
return
|
|
|
|
} else if(comment == null){
|
|
delete comments[save]
|
|
|
|
} else {
|
|
comments[save] = comment
|
|
}
|
|
|
|
this.markChanged('savecomments')
|
|
}],
|
|
|
|
|
|
// FS browser...
|
|
//
|
|
// XXX should the loader list be nested or open in overlay (as-is now)???
|
|
browsePath: ['File/Browse file system...',
|
|
widgets.makeUIDialog(function(base, callback){
|
|
var that = this
|
|
base = base || this.location.path || '/'
|
|
|
|
var o = browseWalk.makeWalk(
|
|
null, base, this.config['image-file-pattern'],
|
|
this.config['file-browser-settings'])
|
|
// path selected...
|
|
.open(function(evt, path){
|
|
var item = o.selected
|
|
|
|
// single loader...
|
|
if(callback && callback.constructor === Function){
|
|
// close self and parent...
|
|
o.parent.close()
|
|
|
|
callback(path)
|
|
|
|
// list of loaders...
|
|
} else {
|
|
// user-provided list...
|
|
if(callback){
|
|
var loaders = callback
|
|
|
|
// build the loaders list from .config...
|
|
} else {
|
|
var loaders = {}
|
|
that.config['path-loaders'].forEach(function(m){
|
|
loaders[that.getDoc(m)[m][0].split('/').pop()] = function(){
|
|
return that[m](path)
|
|
}
|
|
})
|
|
}
|
|
|
|
// show user the list...
|
|
var so = that.Overlay(browse.makeList(null, loaders)
|
|
// close self and parent...
|
|
.open(function(){
|
|
so.close()
|
|
o.parent.close()
|
|
}))
|
|
// closed menu...
|
|
.close(function(){
|
|
//o.parent.focus()
|
|
o.select(item)
|
|
})
|
|
// select top element...
|
|
so.client.select(0)
|
|
|
|
return so
|
|
}
|
|
})
|
|
// we closed the browser -- save settings to .config...
|
|
.on('close', function(){
|
|
|
|
var config = that.config['file-browser-settings']
|
|
|
|
config.disableFiles = o.options.disableFiles
|
|
config.showDisabled = o.options.showDisabled
|
|
config.showNonTraversable = o.options.showNonTraversable
|
|
})
|
|
|
|
return o
|
|
})],
|
|
|
|
// Browse indexes/images...
|
|
//
|
|
// NOTE: if no path is passed (null) these behave just like .browsePath(..)
|
|
// with the appropriate callback otherwise it will just load
|
|
// the given path (no UI) while .browsePath(..) will load the
|
|
// UI in all cases but will treat the given path as a base path
|
|
// to start from.
|
|
browseIndex: ['File/Load index...', makeBrowseProxy('loadIndex')],
|
|
browseImages: ['File/Load images...', makeBrowseProxy('loadImages')],
|
|
|
|
browseSubIndexes: ['File/List sub-indexes...',
|
|
widgets.makeUIDialog(function(){
|
|
var that = this
|
|
var index_dir = this.config['index-dir']
|
|
|
|
var o = browse.makeLister(null, function(path, make){
|
|
var dialog = this
|
|
var path = that.location.path
|
|
|
|
if(that.location.method != 'loadIndex'){
|
|
make('No indexes loaded...', null, true)
|
|
return
|
|
}
|
|
|
|
// indicate that we are working...
|
|
var spinner = make($('<center><div class="loader"/></center>'))
|
|
|
|
// XXX we do not need to actually read anything....
|
|
//file.loadIndex(path, that.config['index-dir'], this.logger)
|
|
// XXX we need to prune the indexes -- avoid loading nested indexes...
|
|
file.listIndexes(path, index_dir)
|
|
.on('error', function(err){
|
|
console.error(err)
|
|
})
|
|
.on('end', function(res){
|
|
|
|
// we got the data, we can now remove the spinner...
|
|
spinner.remove()
|
|
|
|
res.forEach(function(p){
|
|
// trim local paths and keep external paths as-is...
|
|
p = p.split(index_dir)[0]
|
|
var txt = p.split(path).pop()
|
|
txt = txt != p ? './'+pathlib.join('.', txt) : txt
|
|
|
|
make(txt)
|
|
.on('open', function(){
|
|
that.loadIndex(p)
|
|
})
|
|
})
|
|
})
|
|
})
|
|
.on('open', function(){
|
|
o.parent.close()
|
|
})
|
|
|
|
return o
|
|
})],
|
|
|
|
// List save history dialog...
|
|
//
|
|
// NOTE: for multiple indexes this will show the combined history
|
|
// and selecting a postion will load all the participating
|
|
// indexes to that specific date or closest earlier state.
|
|
// NOTE: this will show no history if .location.method is not loadIndex..
|
|
// NOTE: this will drop all unsaved changes when loading a state (XXX)
|
|
// NOTE: this will set changes to all when loading a different state
|
|
// that the latest and to non otherwise....
|
|
//
|
|
// XXX add comment editing...
|
|
// XXX need to handle saves (saveIndex(..) and friends) when loaded
|
|
// a specific history position...
|
|
// ...in theory saving and old index will create an incremental
|
|
// save which should not damage the history and can be fixed
|
|
// either by removing the actual .json files or simply loading
|
|
// from a previous position and re-saving... (XXX test)
|
|
// XXX should this also list journal stuff or have the ability for
|
|
// extending???
|
|
// XXX should this save the unsaved changes when switching to a version
|
|
// or discard them (current behavior)
|
|
// ...saving might be logical...
|
|
listSaveHistory: ['File/History...',
|
|
widgets.makeUIDialog(function(){
|
|
var that = this
|
|
|
|
var o = browse.makeLister(null, function(path, make){
|
|
var dialog = this
|
|
|
|
var from = that.location.from
|
|
from = from && Date.fromTimeStamp(from).toShortDate()
|
|
|
|
if(that.changes !== false && !that.location.historic){
|
|
var title = ['Unsaved state']
|
|
|
|
var comment = that.savecomments && that.savecomments['current']
|
|
//title.push(comment || '')
|
|
comment && title.push(comment)
|
|
|
|
// XXX is this the best format???
|
|
title = title.join(' - ')
|
|
|
|
make(title)
|
|
|
|
make('---')
|
|
}
|
|
|
|
// only search for history if we have an index loaded...
|
|
if(that.location.method != 'loadIndex'){
|
|
make('No history...', null, true)
|
|
|
|
// select the 'Unsaved' item...
|
|
dialog.select()
|
|
.addClass('highlighted')
|
|
|
|
return
|
|
}
|
|
|
|
// indicate that we are working...
|
|
var spinner = make($('<center><div class="loader"/></center>'))
|
|
|
|
that.loadSaveHistoryList()
|
|
.catch(function(err){
|
|
// XXX
|
|
console.error(err)
|
|
})
|
|
.then(function(data){
|
|
var list = []
|
|
|
|
// got the data, remove the spinner...
|
|
spinner.remove()
|
|
|
|
Object.keys(data).forEach(function(path){
|
|
Object.keys(data[path]).forEach(function(d){
|
|
list.push(d)
|
|
})
|
|
})
|
|
|
|
list
|
|
.sort()
|
|
.reverse()
|
|
|
|
// Special case: unsaved state...
|
|
if(that.unsaved_index){
|
|
var title = ['Unsaved state']
|
|
|
|
var comment = that.savecomments && that.savecomments['current']
|
|
//title.push(comment || '')
|
|
comment && title.push(comment)
|
|
|
|
// XXX is this the best format???
|
|
title = title.join(' - ')
|
|
|
|
make(title)
|
|
.on('open', function(){
|
|
var location = that.location
|
|
|
|
that.load(that.unsaved_index)
|
|
|
|
delete that.unsaved_index
|
|
|
|
delete location.historic
|
|
delete location.from
|
|
|
|
that.__location = location
|
|
})
|
|
|
|
make('---')
|
|
|
|
// Special case: top save state is the default...
|
|
// NOTE: no need to mark anything for change...
|
|
} else {
|
|
var first = list.shift()
|
|
first && make(Date.fromTimeStamp(first).toShortDate())
|
|
.on('open', function(){
|
|
that.loadIndex(that.location.path, first)
|
|
})
|
|
}
|
|
|
|
list
|
|
.forEach(function(d){
|
|
var txt = Date.fromTimeStamp(d).toShortDate()
|
|
|
|
// get the save name...
|
|
var title = [txt]
|
|
var comment = that.savecomments && that.savecomments[d]
|
|
//title.push(comment || '')
|
|
comment && title.push(comment)
|
|
|
|
// XXX is this the best format???
|
|
title = title.join(' - ')
|
|
|
|
make(title)
|
|
.attr('timestamp', d)
|
|
.on('open', function(){
|
|
// auto save...
|
|
if(that.config['auto-save-on-save-history-open' ]
|
|
&& that.changes !== false
|
|
&& !that.location.historic){
|
|
// XXX should we use a crop for this???
|
|
that.unsaved_index = that.json()
|
|
}
|
|
|
|
// NOTE: this will drop all unsaved changes...
|
|
that.loadIndex(that.location.path, d)
|
|
.then(function(){
|
|
that.markChanged('all')
|
|
that.location.historic = true
|
|
})
|
|
})
|
|
// mark the current loaded position...
|
|
.addClass(txt == from ? 'selected highlighted' : '')
|
|
})
|
|
|
|
make.done()
|
|
|
|
// NOTE: here we will select 'Latest' if nothing
|
|
// was selected...
|
|
dialog.select()
|
|
.addClass('highlighted')
|
|
})
|
|
})
|
|
.on('open', function(){
|
|
o.parent.close()
|
|
})
|
|
|
|
return o
|
|
})],
|
|
})
|
|
|
|
|
|
// XXX is this a good name???
|
|
var FileSystemLoaderUI =
|
|
module.FileSystemLoaderUI = core.ImageGridFeatures.Feature({
|
|
title: '',
|
|
doc: '',
|
|
|
|
tag: 'ui-fs-loader',
|
|
depends: [
|
|
'ui',
|
|
'fs-loader'
|
|
],
|
|
|
|
actions: FileSystemLoaderUIActions,
|
|
|
|
handlers: [
|
|
// save/resore .savecomments
|
|
//
|
|
['json',
|
|
function(res){
|
|
if(this.savecomments != null){
|
|
res.savecomments = JSON.parse(JSON.stringify(this.savecomments))
|
|
}
|
|
}],
|
|
['load',
|
|
function(_, data){
|
|
if(data.savecomments != null){
|
|
this.savecomments = data.savecomments
|
|
}
|
|
}],
|
|
// Prepare comments for writing...
|
|
//
|
|
// NOTE: defining this here enables us to actually post-bind to
|
|
// an action that is defined later or may not even be
|
|
// available.
|
|
['prepareIndexForWrite',
|
|
function(res){
|
|
var changed = this.changes == null
|
|
|| this.changes.savecomments
|
|
|
|
if(changed){
|
|
var comments = res.raw.savecomments || {}
|
|
|
|
// set the 'current' comment to the correct date...
|
|
if(comments.current){
|
|
comments[res.date] = comments.current
|
|
delete comments.current
|
|
}
|
|
|
|
res.prepared.savecomments = comments
|
|
}
|
|
}],
|
|
// replace .savecomments['current'] with .location.from...
|
|
['saveIndex',
|
|
function(){
|
|
var comments = this.savecomments
|
|
|
|
if(comments && comments.current){
|
|
comments[this.location.from] = comments.current
|
|
delete comments.current
|
|
}
|
|
}],
|
|
]
|
|
})
|
|
|
|
|
|
|
|
//---------------------------------------------------------------------
|
|
|
|
var pushToHistory = function(action, to_top, checker){
|
|
return [action,
|
|
function(_, path){
|
|
path = util.normalizePath(path)
|
|
if(path){
|
|
this.pushURLToHistory(
|
|
util.normalizePath(path),
|
|
action,
|
|
checker || 'checkPath')
|
|
}
|
|
if(to_top){
|
|
this.setTopURLHistory(path)
|
|
}
|
|
}]
|
|
}
|
|
|
|
var FileSystemURLHistory =
|
|
module.FileSystemLoaderURLHistory = core.ImageGridFeatures.Feature({
|
|
title: '',
|
|
doc: '',
|
|
|
|
tag: 'fs-url-history',
|
|
depends: [
|
|
'fs-loader',
|
|
'url-history',
|
|
],
|
|
suggested: [
|
|
'ui-fs-url-history',
|
|
],
|
|
|
|
handlers: [
|
|
pushToHistory('loadImages'),
|
|
pushToHistory('loadIndex'),
|
|
pushToHistory('loadPath'),
|
|
//pushToHistory('loadNewImages'),
|
|
],
|
|
})
|
|
|
|
|
|
|
|
//---------------------------------------------------------------------
|
|
|
|
// Opening the url via .browsePath(..) if url is in history will move
|
|
// it to top of list...
|
|
var FileSystemURLHistoryUI =
|
|
module.FileSystemLoaderURLHistoryUI = core.ImageGridFeatures.Feature({
|
|
title: '',
|
|
doc: '',
|
|
|
|
tag: 'ui-fs-url-history',
|
|
depends: [
|
|
'ui-fs-loader',
|
|
'fs-url-history',
|
|
],
|
|
|
|
handlers: [
|
|
['browsePath',
|
|
function(res){
|
|
var that = this
|
|
res.open(function(_, path){
|
|
that.setTopURLHistory(path)
|
|
})
|
|
}],
|
|
],
|
|
})
|
|
|
|
|
|
|
|
//---------------------------------------------------------------------
|
|
// fs writer...
|
|
|
|
var FileSystemWriterActions = actions.Actions({
|
|
config: {
|
|
//'index-filename-template': '${DATE}-${KEYWORD}.${EXT}',
|
|
|
|
'export-path': null,
|
|
'export-paths': [],
|
|
|
|
'export-preview-name-pattern': '%f',
|
|
'export-preview-name-patterns': [
|
|
'%f',
|
|
'%n%(-bookmarked)b%e',
|
|
'%n%(-marked)m%e',
|
|
],
|
|
|
|
'export-level-directory-name': 'fav',
|
|
'export-level-directory-names': [
|
|
'fav',
|
|
'select',
|
|
],
|
|
|
|
'export-preview-size': 1000,
|
|
// XXX add options to indicate:
|
|
// - long side
|
|
// - short side
|
|
// - vertical
|
|
// - horizontal
|
|
// - ...
|
|
'export-preview-sizes': [
|
|
'500',
|
|
'900',
|
|
'1000',
|
|
'1280',
|
|
'1920',
|
|
],
|
|
},
|
|
|
|
// This can be:
|
|
// - null/undefined - write all
|
|
// - true - write all
|
|
// - false - write nothing
|
|
// - {
|
|
// // write/skip data...
|
|
// data: <bool>,
|
|
//
|
|
// // write/skip images or write a diff including the given
|
|
// // <gid>s only...
|
|
// images: <bool> | [ <gid>, ... ],
|
|
//
|
|
// // write/skip tags...
|
|
// tags: <bool>,
|
|
//
|
|
// // write/skip bookmarks...
|
|
// bookmarked: <bool>,
|
|
//
|
|
// // write/skip selected...
|
|
// selected: <bool>,
|
|
// }
|
|
//
|
|
// NOTE: in the complex format all fields ar optional; if a field
|
|
// is not included it is not written (same as when set to false)
|
|
// NOTE: .current is written always.
|
|
chages: null,
|
|
|
|
clone: [function(full){
|
|
return function(res){
|
|
res.changes = null
|
|
if(full && this.hasOwnProperty('changes') && this.changes){
|
|
res.changes = JSON.parse(JSON.stringify(this.changes))
|
|
}
|
|
}
|
|
}],
|
|
|
|
// Mark data sections as changed...
|
|
//
|
|
// Mark everything changed...
|
|
// .markChanged('all')
|
|
//
|
|
// Mark nothing changed...
|
|
// .markChanged('none')
|
|
//
|
|
// Mark a section changed...
|
|
// .markChanged('data')
|
|
// .markChanged('tags')
|
|
// .markChanged('selected')
|
|
// .markChanged('bookmarked')
|
|
//
|
|
// Mark image changed...
|
|
// .markChanged(<gid>, ...)
|
|
//
|
|
//
|
|
// NOTE: when .changes is null (i.e. everything changed, marked via
|
|
// .markChanged('all')) then calling this with anything other
|
|
// than 'none' will have no effect.
|
|
markChanged: ['- System/',
|
|
function(section){
|
|
var that = this
|
|
var args = util.args2array(arguments)
|
|
//var changes = this.changes =
|
|
var changes =
|
|
this.hasOwnProperty('changes') ?
|
|
this.changes || {}
|
|
: {}
|
|
|
|
//console.log('CHANGED:', args)
|
|
|
|
// all...
|
|
if(args.length == 1 && args[0] == 'all'){
|
|
// NOTE: this is better than delete as it will shadow
|
|
// the parent's changes in case we got cloned from
|
|
// a live instance...
|
|
//delete this.changes
|
|
this.changes = null
|
|
|
|
// none...
|
|
} else if(args.length == 1 && args[0] == 'none'){
|
|
this.changes = false
|
|
|
|
// everything is marked changed, everything will be saved
|
|
// anyway...
|
|
// NOTE: to reset this use .markChanged('none') and then
|
|
// manually add the desired changes...
|
|
} else if(this.changes == null){
|
|
return
|
|
|
|
} else {
|
|
var images = (changes.images || [])
|
|
|
|
args.forEach(function(arg){
|
|
var gid = that.data.getImage(arg)
|
|
|
|
// special case: image gid...
|
|
if(gid != -1 && gid != null){
|
|
images.push(gid)
|
|
images = images.unique()
|
|
|
|
changes.images = images
|
|
that.changes = changes
|
|
|
|
// all other keywords...
|
|
} else {
|
|
changes[arg] = true
|
|
that.changes = changes
|
|
}
|
|
})
|
|
}
|
|
}],
|
|
|
|
// Convert json index to a format compatible with file.writeIndex(..)
|
|
//
|
|
// This is here so as other features can participate in index
|
|
// preparation...
|
|
// There are several stages features can control the output format:
|
|
// 1) .json() action
|
|
// - use this for global high level serialization format
|
|
// - the output of this is .load(..) compatible
|
|
// 2) .prepareIndexForWrite(..) action
|
|
// - use this for file system write preparation
|
|
// - this directly affects the index structure
|
|
//
|
|
// This will get the base index, ignoring the cropped state.
|
|
//
|
|
// Returns:
|
|
// {
|
|
// // Timestamp...
|
|
// // NOTE: this is the timestamp used to write the index.
|
|
// date: <timestamp>,
|
|
//
|
|
// // This is the original json object, either the one passed as
|
|
// // an argument or the one returned by .json('base')
|
|
// raw: <original-json>,
|
|
//
|
|
// // this is the prepared object, the one that is going to be
|
|
// // saved.
|
|
// prepared: <prepared-json>,
|
|
// }
|
|
//
|
|
//
|
|
// The format for the <prapared-json> is as follows:
|
|
// {
|
|
// <keyword>: <data>,
|
|
// ...
|
|
// }
|
|
//
|
|
// The <prepared-json> is written out to a fs index in the following
|
|
// way:
|
|
// <index-dir>/<timestamp>-<keyword>.json
|
|
//
|
|
// <index-dir> - taken from .config['index-dir'] (default: '.ImageGrid')
|
|
// <timestamp> - as returned by Date.timeStamp() (see: jli)
|
|
//
|
|
// For more info see file.writeIndex(..) and file.loadIndex(..).
|
|
//
|
|
prepareIndexForWrite: ['- File/Prepare index for writing',
|
|
function(json, full){
|
|
json = json || this.json('base')
|
|
var changes = full ? null
|
|
: this.hasOwnProperty('changes') ? this.changes
|
|
: null
|
|
return {
|
|
date: window.Date.timeStamp(),
|
|
raw: json,
|
|
prepared: file.prepareIndex(json, changes),
|
|
}
|
|
}],
|
|
|
|
// NOTE: with no arguments this will save index to .location.path
|
|
// XXX should this return a promise??? ...a clean promise???
|
|
saveIndex: ['- File/',
|
|
function(path, logger){
|
|
var that = this
|
|
path = path || this.location.path
|
|
|
|
// resolve relative paths...
|
|
if(/^(\.\.?[\\\/]|[^\\\/])/.test(path)
|
|
// and skip windows drives...
|
|
&& !/^[a-z]:[\\\/]/i.test(path)){
|
|
// XXX do we need to normalize???
|
|
path = this.location.path +'/'+ path
|
|
}
|
|
|
|
// XXX get a logger...
|
|
logger = logger || this.logger
|
|
|
|
// XXX get real base path...
|
|
//path = path || this.location.path +'/'+ this.config['index-dir']
|
|
|
|
var indes = this.prepareIndexForWrite()
|
|
|
|
return file.writeIndex(
|
|
index.prepared,
|
|
// XXX should we check if index dir is present in path???
|
|
//path,
|
|
path +'/'+ this.config['index-dir'],
|
|
inex.date,
|
|
this.config['index-filename-template'],
|
|
logger || this.logger)
|
|
.then(function(){
|
|
that.location.method = 'loadIndex'
|
|
that.location.from = date
|
|
})
|
|
}],
|
|
|
|
// XXX ways to treat a collection:
|
|
// - crop data
|
|
// - independent index
|
|
// XXX save to: .ImageGrid/collections/<title>/
|
|
// XXX move to a feature???
|
|
// XXX API: save/load/list/remove
|
|
// ...need to track save location (not the save as the index)...
|
|
// XXX
|
|
saveCollection: ['- File/Save collection',
|
|
function(title){
|
|
// XXX
|
|
}],
|
|
|
|
// Export current state as a full loadable index
|
|
//
|
|
// XXX resolve env variables in path...
|
|
// XXX what sould happen if no path is given???
|
|
// XXX should this return a promise??? ...a clean promise???
|
|
// XXX add preview selection...
|
|
// XXX handle .image.path and other stack files...
|
|
// XXX local collections???
|
|
exportIndex: ['- File/Export/Export index',
|
|
function(path, logger){
|
|
logger = logger || this.logger
|
|
|
|
// XXX is this correct???
|
|
path = path || './exported'
|
|
|
|
// XXX resolve env variables in path...
|
|
// XXX
|
|
|
|
// resolve relative paths...
|
|
if(/^(\.\.?[\\\/]|[^\\\/])/.test(path)
|
|
// and skip windows drives...
|
|
&& !/^[a-z]:[\\\/]/i.test(path)){
|
|
// XXX do we need to normalize???
|
|
path = this.location.path +'/'+ path
|
|
}
|
|
|
|
var json = this.json()
|
|
|
|
// get all loaded gids...
|
|
var gids = []
|
|
for(var r in json.data.ribbons){
|
|
this.data.makeSparseImages(json.data.ribbons[r], gids)
|
|
}
|
|
gids = gids.compact()
|
|
|
|
// build .images with loaded images...
|
|
// XXX list of previews should be configurable (max size)
|
|
var images = {}
|
|
gids.forEach(function(gid){
|
|
var img = json.images[gid]
|
|
if(img){
|
|
images[gid] = json.images[gid]
|
|
|
|
// remove un-needed previews...
|
|
// XXX
|
|
}
|
|
})
|
|
|
|
// prepare and save index to target path...
|
|
json.data.order = gids
|
|
json.images = images
|
|
// XXX should we check if index dir is present in path???
|
|
var index_path = path +'/'+ this.config['index-dir']
|
|
|
|
// copy previews for the loaded images...
|
|
// XXX should also optionally populate the base dir and nested favs...
|
|
var base_dir = this.location.path
|
|
|
|
gids.forEach(function(gid){
|
|
var img = json.images[gid]
|
|
var img_base = img.base_path
|
|
var previews = img.preview
|
|
|
|
// NOTE: we are copying everything to one place so no
|
|
// need for a base path...
|
|
delete img.base_path
|
|
|
|
// XXX copy img.path -- the main image, especially when no previews present....
|
|
// XXX
|
|
|
|
if(previews || img.path){
|
|
Object.keys(previews || {})
|
|
.map(function(res){ return decodeURI(previews[res]) })
|
|
// XXX should we copy this, especially if it's a hi-res???
|
|
.concat([img.path || null])
|
|
.forEach(function(preview_path){
|
|
if(preview_path == null){
|
|
return
|
|
}
|
|
|
|
var from = (img_base || base_dir) +'/'+ preview_path
|
|
var to = path +'/'+ preview_path
|
|
|
|
// XXX do we queue these or let the OS handle it???
|
|
// ...needs testing, if node's fs queues the io
|
|
// internally then we do not need to bother...
|
|
// XXX
|
|
ensureDir(pathlib.dirname(to))
|
|
.catch(function(err){
|
|
logger && logger.emit('error', err) })
|
|
.then(function(){
|
|
return copy(from, to)
|
|
// XXX do we need to have both of this
|
|
// and the above .catch(..) or can
|
|
// we just use the one above (after
|
|
// .then(..))
|
|
.then(function(){
|
|
logger && logger.emit('done', to) })
|
|
.catch(function(err){
|
|
logger && logger.emit('error', err) })
|
|
})
|
|
})
|
|
}
|
|
})
|
|
|
|
// NOTE: if we are to use .saveIndex(..) here, do not forget
|
|
// to reset .changes
|
|
file.writeIndex(
|
|
this.prepareIndexForWrite(json, true).prepared,
|
|
index_path,
|
|
this.config['index-filename-template'],
|
|
logger || this.logger)
|
|
|
|
}],
|
|
|
|
// XXX might also be good to save/load the export options to .ImageGrid-export.json
|
|
// XXX resolve env variables in path... (???)
|
|
// XXX make custom previews (option)...
|
|
// ...should this be a function of .images.getBestPreview(..)???
|
|
// XXX report errors...
|
|
// XXX stop the process on errors...
|
|
// XXX use tasks...
|
|
exportDirs: ['- File/Export/Export ribbons as directories',
|
|
function(path, pattern, level_dir, size, logger){
|
|
logger = logger || this.logger
|
|
var that = this
|
|
var base_dir = this.location.path
|
|
|
|
// XXX resolve env variables in path...
|
|
// XXX
|
|
|
|
// resolve relative paths...
|
|
if(/^(\.\.?[\\\/]|[^\\\/])/.test(path)
|
|
// and skip windows drives...
|
|
&& !/^[a-z]:[\\\/]/i.test(path)){
|
|
// XXX do we need to normalize???
|
|
path = this.location.path +'/'+ path
|
|
}
|
|
|
|
var to_dir = path
|
|
|
|
// get/set the config data...
|
|
// XXX should this store the last set???
|
|
level_dir = level_dir || this.config['export-level-directory-name'] || 'fav'
|
|
size = size || this.config['export-preview-size'] || 1000
|
|
pattern = pattern || this.config['export-preview-name-pattern'] || '%f'
|
|
|
|
|
|
// XXX need to abort on fatal errors...
|
|
this.data.ribbon_order
|
|
.slice()
|
|
.reverse()
|
|
.forEach(function(ribbon){
|
|
// NOTE: this is here to keep the specific path local to
|
|
// this scope...
|
|
var img_dir = to_dir
|
|
|
|
ensureDir(pathlib.dirname(img_dir))
|
|
.catch(function(err){
|
|
logger && logger.emit('error', err) })
|
|
.then(function(){
|
|
that.data.ribbons[ribbon].forEach(function(gid){
|
|
var img = that.images[gid]
|
|
var img_name = pathlib.basename(img.path || (img.name + img.ext))
|
|
|
|
|
|
// get best preview...
|
|
var from = decodeURI(
|
|
(img.base_path || base_dir)
|
|
+'/'
|
|
+ that.images.getBestPreview(gid, size).url)
|
|
|
|
// XXX see if we need to make a preview (sharp)
|
|
// XXX
|
|
|
|
// XXX get/form image name...
|
|
// XXX might be a good idea to connect this to the info framework...
|
|
var ext = pathlib.extname(img_name)
|
|
var tags = that.data.getTags(gid)
|
|
|
|
var name = pattern
|
|
// file name...
|
|
.replace(/%f/, img_name)
|
|
.replace(/%n/, img_name.replace(ext, ''))
|
|
.replace(/%e/, ext)
|
|
|
|
// gid...
|
|
.replace(/%gid/, gid)
|
|
// XXX get the correct short gid length...
|
|
.replace(/%g/, gid.slice(-7, -1))
|
|
|
|
// order...
|
|
.replace(/%i/, that.data.getImageOrder(gid))
|
|
.replace(/%I/, that.data.getImageOrder(gid, 'global'))
|
|
|
|
// tags...
|
|
// XXX test: %n%(b)b%(m)m%e
|
|
.replace(
|
|
/%\((.*)\)m/, tags.indexOf('selected') >= 0 ? '$1' : '')
|
|
.replace(
|
|
/%\((.*)\)b/, tags.indexOf('bookmark') >= 0 ? '$1' : '')
|
|
|
|
// metadata...
|
|
// XXX
|
|
|
|
var to = img_dir +'/'+ name
|
|
|
|
return copy(from, to)
|
|
.then(function(){
|
|
logger && logger.emit('done', to) })
|
|
.catch(function(err){
|
|
logger && logger.emit('error', err) })
|
|
})
|
|
})
|
|
|
|
to_dir += '/'+level_dir
|
|
})
|
|
}]
|
|
})
|
|
|
|
|
|
var FileSystemWriter =
|
|
module.FileSystemWriter = core.ImageGridFeatures.Feature({
|
|
title: '',
|
|
doc: '',
|
|
|
|
tag: 'fs-writer',
|
|
// NOTE: this is mostly because of the base path handling...
|
|
depends: [
|
|
'fs-loader'
|
|
],
|
|
suggested: [
|
|
'ui-fs-writer',
|
|
],
|
|
|
|
actions: FileSystemWriterActions,
|
|
|
|
isApplicable: function(){
|
|
return this.runtime == 'node' || this.runtime == 'nw' },
|
|
|
|
// monitor changes...
|
|
// XXX should we use .load(..) to trigger changes instead of .loadURLs(..)???
|
|
// ...the motivation is that .crop(..) may also trigger loads...
|
|
// ....needs more thought...
|
|
handlers: [
|
|
// clear changes...
|
|
// XXX currently if no args are passed then nothing is
|
|
// done here, this might change...
|
|
['loadIndex',
|
|
function(res, path){
|
|
if(path){
|
|
//this.markChanged('none')
|
|
var that = this
|
|
res.then(function(){
|
|
that.markChanged('none')
|
|
})
|
|
}
|
|
}],
|
|
['saveIndex',
|
|
function(res, path){
|
|
// NOTE: if saving to a different path than loaded do not
|
|
// drop the .changes flags...
|
|
if(path && path == this.location.path){
|
|
//this.markChanged('none')
|
|
var that = this
|
|
res.then(function(){
|
|
this.markChanged('none')
|
|
})
|
|
}
|
|
}],
|
|
|
|
// everything changed...
|
|
[[
|
|
'loadURLs',
|
|
'clear',
|
|
],
|
|
function(){
|
|
this.markChanged('all')
|
|
}],
|
|
|
|
// data...
|
|
[[
|
|
//'clear',
|
|
//'load',
|
|
|
|
'setBaseRibbon',
|
|
|
|
'shiftImageTo',
|
|
'shiftImageUp',
|
|
'shiftImageDown',
|
|
'shiftImageLeft',
|
|
'shiftImageRight',
|
|
'shiftRibbonUp',
|
|
'shiftRibbonDown',
|
|
|
|
'sortImages',
|
|
'reverseImages',
|
|
'reverseRibbons',
|
|
|
|
'group',
|
|
'ungroup',
|
|
'expandGroup',
|
|
'collapseGroup',
|
|
],
|
|
function(_, target){ this.markChanged('data') }],
|
|
|
|
// image specific...
|
|
[[
|
|
'rotateCW',
|
|
'rotateCCW',
|
|
'flipHorizontal',
|
|
'flipVertical',
|
|
],
|
|
function(_, target){ this.markChanged(target) }],
|
|
|
|
// tags and images...
|
|
// NOTE: tags are also stored in images...
|
|
['tag untag',
|
|
function(_, tags, gids){
|
|
var changes = []
|
|
|
|
gids = gids || [this.data.getImage()]
|
|
gids = gids.constructor !== Array ? [this.data.getImage(gids)] : gids
|
|
|
|
tags = tags || []
|
|
tags = tags.constructor !== Array ? [tags] : tags
|
|
|
|
// images...
|
|
changes = changes.concat(gids)
|
|
|
|
// tags...
|
|
if(tags.length > 0){
|
|
changes.push('tags')
|
|
|
|
// selected...
|
|
if(tags.indexOf('selected') >= 0){
|
|
changes.push('selected')
|
|
}
|
|
|
|
// bookmark...
|
|
if(tags.indexOf('bookmark') >= 0){
|
|
changes.push('bookmarked')
|
|
}
|
|
}
|
|
|
|
this.markChanged.apply(this, changes)
|
|
}],
|
|
]
|
|
})
|
|
|
|
|
|
//---------------------------------------------------------------------
|
|
// XXX add writer UI feature...
|
|
// - save as.. (browser)
|
|
// - save if not base path present (browser)
|
|
var FileSystemWriterUIActions = actions.Actions({
|
|
config: {
|
|
'export-dialog-mode': 'Directories',
|
|
|
|
'export-dialog-modes': {
|
|
'Images only': {
|
|
action: 'exportDirs',
|
|
data: [
|
|
'pattern',
|
|
'size',
|
|
'level_dir',
|
|
'target_dir',
|
|
],
|
|
},
|
|
'Full index': {
|
|
action: 'exportIndex',
|
|
data: [
|
|
//'size',
|
|
'target_dir',
|
|
],
|
|
},
|
|
},
|
|
},
|
|
|
|
// XXX this needs feedback...
|
|
// XXX should this return a promise???
|
|
saveIndexHere: ['File/Save',
|
|
function(){
|
|
if(this.location.path){
|
|
this.saveIndex()
|
|
|
|
} else {
|
|
this.browseSaveIndex()
|
|
}
|
|
}],
|
|
// XXX should this be a UI action???
|
|
// ...at this point this depends on .saveIndexHere(..), thus
|
|
// it is here...
|
|
// XXX should this return a promise???
|
|
saveFullIndex: ['File/Save (full)',
|
|
function(){
|
|
return this
|
|
.markChanged('all')
|
|
.saveIndexHere()}],
|
|
|
|
// XXX need to be able to make dirs...
|
|
browseExportIndex: ['File/Export/Export Index to...',
|
|
makeBrowseProxy('exportIndex')],
|
|
// XXX need to be able to make dirs...
|
|
browseExportDirs: ['File/Export/Export Images to...',
|
|
makeBrowseProxy('exportDirs')],
|
|
|
|
|
|
// Export dialog...
|
|
//
|
|
// Export <mode> is set by:
|
|
// .config['export-mode']
|
|
//
|
|
// The fields used and their order is determined by:
|
|
// .config['export-modes'][<mode>].data (list)
|
|
//
|
|
// The action used to export is determined by:
|
|
// .config['export-modes'][<mode>].action
|
|
//
|
|
//
|
|
// Dialog fields...
|
|
//
|
|
// Format:
|
|
// {
|
|
// // Arguments:
|
|
// // actions - the actions object
|
|
// // make - browse item constructor
|
|
// // (see: browse.Browser.update(..) for more info)
|
|
// // parent - the parent dialog
|
|
// <key>: function(actions, make, overlay){ ... },
|
|
// ...
|
|
// }
|
|
//
|
|
// NOTE: .__export_dialog_fields__ can be defined both in the feature
|
|
// as well as in the instance.
|
|
__export_dialog_fields__: {
|
|
'pattern': function(actions, make, parent){
|
|
return make(['Filename pattern: ',
|
|
function(){
|
|
return actions.config['export-preview-name-pattern'] || '%f' }])
|
|
.on('open',
|
|
widgets.makeNestedConfigListEditor(actions, parent,
|
|
'export-preview-name-patterns',
|
|
'export-preview-name-pattern'))
|
|
},
|
|
'level_dir': function(actions, make, parent){
|
|
return make(['Level directory: ',
|
|
function(){
|
|
return actions.config['export-level-directory-name'] || 'fav' }])
|
|
.on('open',
|
|
widgets.makeNestedConfigListEditor(actions, parent,
|
|
'export-level-directory-names',
|
|
'export-level-directory-name'))
|
|
},
|
|
'size': function(actions, make, parent){
|
|
return make(['Image size: ',
|
|
function(){
|
|
return actions.config['export-preview-size'] || 1000 }])
|
|
// XXX add validation???
|
|
.on('open',
|
|
widgets.makeNestedConfigListEditor(actions, parent,
|
|
'export-preview-sizes',
|
|
'export-preview-size',
|
|
{
|
|
sort: function(a, b){ return parseInt(a) - parseInt(b) },
|
|
}))
|
|
|
|
},
|
|
// XXX BUG: history closing errors -- non-critical...
|
|
'target_dir': function(actions, make, parent){
|
|
var elem = make(['To: ',
|
|
function(){ return actions.config['export-path'] || './' }],
|
|
{ buttons: [
|
|
['browse', function(p){
|
|
var e = this.filter('"'+p+'"', false)
|
|
var path = e.find('.text').last().text()
|
|
var txt = e.find('.text').first().text()
|
|
|
|
// XXX add new dir global button...
|
|
return actions.browsePath(path,
|
|
function(path){
|
|
actions.config['export-path'] = path
|
|
actions.config['export-paths'].splice(0, 0, path)
|
|
|
|
parent.update()
|
|
parent.select(txt)
|
|
})
|
|
}],
|
|
// XXX BUG: closing this breaks on parant.focus()...
|
|
['histroy', widgets.makeNestedConfigListEditor(actions, parent,
|
|
'export-paths',
|
|
'export-path',
|
|
{
|
|
new_button: false,
|
|
})],
|
|
]})
|
|
// XXX make this editable???
|
|
.on('open', function(){
|
|
event.preventDefault()
|
|
|
|
var path = elem.find('.text').last()
|
|
.makeEditable({
|
|
clear_on_edit: false,
|
|
abort_keys: [
|
|
'Esc',
|
|
],
|
|
})
|
|
.on('edit-done', function(_, path){
|
|
actions.config['export-path'] = path
|
|
actions.config['export-paths'].indexOf(path) < 0
|
|
&& actions.config['export-paths'].splice(0, 0, path)
|
|
|
|
})
|
|
.on('edit-aborted edit-done', function(evt, path){
|
|
parent.update()
|
|
.then(function(){
|
|
parent.select(path)
|
|
})
|
|
})
|
|
})
|
|
}
|
|
},
|
|
// XXX indicate export state: index, crop, image...
|
|
exportDialog: ['File/Export/Export optioons...',
|
|
widgets.makeUIDialog(function(){
|
|
var that = this
|
|
|
|
var o = browse.makeLister(null, function(path, make){
|
|
var dialog = this
|
|
var mode = that.config['export-dialog-mode'] || 'Images only'
|
|
var data = that.config['export-dialog-modes'][mode].data
|
|
|
|
// mode selector...
|
|
make(['Export mode: ',
|
|
function(){ return that.config['export-dialog-mode'] || 'Directories' }])
|
|
.on('open',
|
|
widgets.makeNestedConfigListEditor(that, o,
|
|
'export-dialog-modes',
|
|
'export-dialog-mode',
|
|
{
|
|
new_button: false,
|
|
itemButtons: [],
|
|
}))
|
|
|
|
// get the root and user fields...
|
|
var fields = that.__export_dialog_fields__ || {}
|
|
var base_fields = FileSystemWriterUIActions.__export_dialog_fields__ || {}
|
|
// build the fields...
|
|
data.forEach(function(k){
|
|
(fields[k]
|
|
&& fields[k].call(that, that, make, dialog))
|
|
|| (base_fields[k]
|
|
&& base_fields[k].call(that, that, make, dialog))
|
|
})
|
|
|
|
// Start/stop action...
|
|
make([function(){
|
|
// XXX indicate export state: index, crop, image...
|
|
return 'Export'}])
|
|
.on('open', function(){
|
|
var mode = that.config['export-dialog-modes'][that.config['export-dialog-mode']]
|
|
that[mode.action](
|
|
that.config['export-path'] || that.location.path)
|
|
dialog.parent.close()
|
|
})
|
|
.addClass('selected')
|
|
|
|
make.done()
|
|
})
|
|
|
|
o.dom.addClass('metadata-view tail-action')
|
|
|
|
return o
|
|
})],
|
|
})
|
|
|
|
|
|
var FileSystemWriterUI =
|
|
module.FileSystemWriterUI = core.ImageGridFeatures.Feature({
|
|
title: '',
|
|
doc: '',
|
|
|
|
tag: 'ui-fs-writer',
|
|
depends: [
|
|
'fs-writer',
|
|
'ui-fs-loader',
|
|
],
|
|
|
|
actions: FileSystemWriterUIActions,
|
|
})
|
|
|
|
|
|
|
|
//---------------------------------------------------------------------
|
|
|
|
core.ImageGridFeatures.Feature('fs', [
|
|
'fs-loader',
|
|
'fs-writer',
|
|
])
|
|
|
|
|
|
/**********************************************************************
|
|
* vim:set ts=4 sw=4 : */
|
|
return module })
|