/********************************************************************** * * * **********************************************************************/ 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 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... // // 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) .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] // XXX save dates... res[k].__dates && console.log('DATES:', res[k].__dates) // 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', } }) }], // 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: // - x - 'x' will strike out the element... // - 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, }, }, // XXX for some reason the path list blinks (.update()???) when sub // menu is shown... // 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 })], // 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. // XXX should passing no path to this start browsing from the current // path or from the root? // XXX should these be dialog objects??? browseIndex: ['File/Load index...', makeBrowseProxy('loadIndex')], browseImages: ['File/Load images...', makeBrowseProxy('loadImages')], }) // 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, }) //--------------------------------------------------------------------- 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: , // // // write/skip images or write a diff including the given // // s only... // images: | [ , ... ], // // // write/skip tags... // tags: , // // // write/skip bookmarks... // bookmarked: , // // // write/skip selected... // selected: , // } // // 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(, ...) // // // 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) .prepareIndex(..) 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: // { // // This is the original json object, either the one passed as // // an argument or the one returned by .json('base') // raw: , // // // this is the prepared object, the one that is going to be // // saved. // prepared: , // } // // // The format for the is as follows: // { // : , // ... // } // // The is written out to a fs index in the following // way: // /-.json // // - taken from .config['index-dir'] (default: '.ImageGrid') // - 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 { 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'] return file.writeIndex( this.prepareIndexForWrite().prepared, // XXX should we check if index dir is present in path??? //path, path +'/'+ this.config['index-dir'], this.config['index-filename-template'], logger || this.logger) .then(function(){ that.location.method = 'loadIndex' }) }], // XXX ways to treat a collection: // - crop data // - independent index // XXX save to: .ImageGrid/collections// // 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() }) }) o.dom.addClass('metadata-view tail-action') setTimeout(function(){ o.select(-1) }, 0) 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 })