/********************************************************************** * * * **********************************************************************/ define(function(require){ var module = {} //var DEBUG = DEBUG != null ? DEBUG : true // XXX this should not be imported!!! // ...something wrong with requirejs(..) if(window.nodejs != null){ var fse = requirejs('fs-extra') var pathlib = requirejs('path') var glob = requirejs('glob') var file = requirejs('./file') // XXX this for some reason does not load in nw while require(..) // for some reason works in browser... //var browseWalk = requirejs('./lib/widget/browse-walk') var browseWalk = require('lib/widget/browse-walk') } var util = require('lib/util') var actions = require('lib/actions') var features = require('lib/features') var core = require('features/core') var overlay = require('lib/widget/overlay') var browse = require('lib/widget/browse') /*********************************************************************/ // 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)', }, 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) }], // NOTE: when passed no path this will not do anything... // XXX should this set something like .path??? // ...and how should this 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 look inside... loadIndex: ['- File/Load index', function(path, logger){ var that = this if(path == null){ return } // 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) .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.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){ // 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... // XXX this appears to lose bookmarks and other tags... 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... break } logger && logger.emit('load index', index) that.loaded_paths = loaded // XXX should we get the requested path or the base path currently loaded that.__location ={ path: loaded.length == 1 ? loaded[0] : path, method: 'loadIndex', } that.load(index) }) }], // 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... 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']) .on('error', function(err){ console.log('!!!!', err) }) .on('end', function(lst){ that.loadURLs(lst .map(function(p){ return util.normalizePath(p) }), path) // 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... 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 merging does not work (something wrong with .data.join(..)) // 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']) .on('end', function(lst){ // 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) // 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', ], actions: FileSystemLoaderActions, isApplicable: function(){ return window.nodejs != null }, }) //--------------------------------------------------------------------- // XXX would need to delay the original action while the user is // browsing... var makeBrowseProxy = function(action, callback){ return 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 }) } } 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...', function(base, callback){ var that = this base = base || this.location.path || '/' var o = overlay.Overlay(this.ribbons.viewer, browseWalk.makeWalk( null, base, this.config['image-file-pattern'], this.config['file-browser-settings']) // path selected... .open(function(evt, path){ var item = o.client.selected // single loader... if(callback && callback.constructor === Function){ // close self and parent... o.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 = overlay.Overlay(that.ribbons.viewer, browse.makeList(null, loaders) // close self and parent... .open(function(){ so.close() o.close() })) // closed menu... .close(function(){ o.focus() o.client.select(item) }) // select top element... so.client.select(0) return so } })) // we closed the browser -- save settings to .config... .close(function(){ var config = that.config['file-browser-settings'] config.disableFiles = o.client.options.disableFiles config.showDisabled = o.client.options.showDisabled config.showNonTraversable = o.client.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? 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: ['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', ], 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.client.open(function(_, path){ that.setTopURLHistory(path) }) }], ], }) //--------------------------------------------------------------------- // fs writer... var FileSystemWriterActions = actions.Actions({ config: { //'index-filename-template': '${DATE}-${KEYWORD}.${EXT}', }, // 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)) } } }], // 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 saveIndex: ['- File/Save index', function(path, logger){ var that = this // XXX this is a stub to make this compatible with makeBrowseProxy(..) // ...we do need a default here... /* if(path == null){ return } */ path = path || this.location.path // XXX get a logger... logger = logger || this.logger // XXX get real base path... //path = path || this.location.path +'/'+ this.config['index-dir'] 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 same as ctrl-shif-s in gen3 exportView: ['File/Export current view', function(){ }], // XXX not done yet... // needs: // ensureDir(..) // copy(..) // ...both denodeify(..)'ed // XXX export current state as a full loadable index // XXX might be interesting to unify this and .exportView(..) // XXX local collections??? exportCollection: ['File/Export as collection', function(path, logger){ 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??? path = path +'/'+ this.config['index-dir'] // NOTE: if we are to use .saveIndex(..) here, do not forget // to reset .changes file.writeIndex( this.prepareIndexForWrite(json).prepared, path, this.config['index-filename-template'], logger || this.logger) // copy previews for the loaded images... // XXX should also optionally populate the base dir and nested favs... var base_dir = this.base_dir gids.forEach(function(gid){ var img = json.images[gid] var img_base = img.base_path img.base_path = path var previews = img.preview for(var res in previews){ 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){ // XXX }) .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(..)) .catch(function(err){ // XXX }) }) } }) }], }) 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'], actions: FileSystemWriterActions, isApplicable: function(){ return window.nodejs != null }, // 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(_, path){ if(path){ this.changes = false } }], ['saveIndex', function(_, path){ // NOTE: if saving to a different path than loaded do not // drop the .changes flags... if(path && path == this.location.path){ this.changes = false } }], // everything changed... [[ 'loadURLs', 'clear', ], function(){ // 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 }], // data... [[ //'clear', //'load', 'setBaseRibbon', 'shiftImageTo', 'shiftImageUp', 'shiftImageDown', 'shiftImageLeft', 'shiftImageRight', 'shiftRibbonUp', 'shiftRibbonDown', 'sortImages', 'reverseImages', 'reverseRibbons', 'group', 'ungroup', 'expandGroup', 'collapseGroup', ], function(_, target){ var changes = this.changes = this.hasOwnProperty('changes') ? this.changes || {} : {} changes.data = true }], // image specific... [[ 'rotateCW', 'rotateCCW', 'flipHorizontal', 'flipVertical', ], function(_, target){ var changes = this.changes = this.hasOwnProperty('changes') ? this.changes || {} : {} var images = changes.images = changes.images || [] target = this.data.getImage(target) images.push(target) }], // tags and images... // NOTE: tags are also stored in images... ['tag untag', function(_, tags, gids){ var changes = this.changes = this.hasOwnProperty('changes') ? this.changes || {} : {} var images = changes.images = changes.images || [] gids = gids || [this.data.getImage()] gids = gids.constructor !== Array ? [this.data.getImage(gids)] : gids tags = tags || [] tags = tags.constructor !== Array ? [tags] : tags // images... changes.images = images.concat(gids).unique() // tags... if(tags.length > 0){ changes.tags = true // selected... if(tags.indexOf('selected') >= 0){ changes.selected = true } // bookmark... if(tags.indexOf('bookmark') >= 0){ changes.bookmarked = true } } }], ] }) //--------------------------------------------------------------------- // XXX add writer UI feature... // - save as.. (browser) // - save if not base path present (browser) var FileSystemWriterUIActions = actions.Actions({ // XXX should this ask the user for a path??? // XXX this for some reason works differently than browseSaveIndex(..) // and saves images-diff instead of images... saveIndexHere: ['File/Save', function(){ if(this.location.path){ this.saveIndex(this.location.path) } }], // XXX add ability to create dirs... browseSaveIndex: ['File/Save index to...', makeBrowseProxy('saveIndex', function(){ this.loaction.method = 'loadIndex' })], }) var FileSystemWriterUI = module.FileSystemWriterUI = core.ImageGridFeatures.Feature({ title: '', doc: '', tag: 'ui-fs-writer', depends: [ 'fs-writer', 'ui-fs-loader', ], actions: FileSystemWriterUIActions, }) /********************************************************************** * vim:set ts=4 sw=4 : */ return module })