mirror of
https://github.com/flynx/ImageGrid.git
synced 2025-10-29 02:10:08 +00:00
748 lines
20 KiB
JavaScript
Executable File
748 lines
20 KiB
JavaScript
Executable File
/**********************************************************************
|
|
*
|
|
*
|
|
*
|
|
**********************************************************************/
|
|
((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define)
|
|
(function(require){ var module={} // make module AMD/node compatible...
|
|
/*********************************************************************/
|
|
|
|
var data = require('imagegrid/data')
|
|
|
|
var toggler = require('lib/toggler')
|
|
var actions = require('lib/actions')
|
|
var features = require('lib/features')
|
|
|
|
var browse = require('lib/widget/browse')
|
|
|
|
var core = require('features/core')
|
|
var widgets = require('features/ui-widgets')
|
|
|
|
|
|
|
|
/*********************************************************************/
|
|
// XXX should collections be in the Crop menu????
|
|
// ...essentially a collection is a saved crop, so this would be
|
|
// logical, would simplify control, etc.
|
|
//
|
|
|
|
// XXX things we need to do to collections:
|
|
// - auto-collections
|
|
// - tags -- adding/removing images adds/removes tags
|
|
// - ribbons -- top / bottom / n-m / top+2 / ..
|
|
// XXX might be a good idea to make collection loading part of the
|
|
// .load(..) protocol...
|
|
// ...this could be done via a url suffix, as a shorthand.
|
|
// something like:
|
|
// /path/to/index:collection
|
|
// -> /path/to/index/sub/path/.ImageGrid/collections/collection
|
|
// XXX loading collections by direct path would require us to look
|
|
// in the containing index for missing parts (*images.json, ...)
|
|
// XXX saving a local collection would require us to save to two
|
|
// locations:
|
|
// - collection specific stuff (data) to collection path
|
|
// - global stuff (images, tags, ...) to base index...
|
|
// XXX handle tags here???
|
|
// ...keep them global or local to collection???
|
|
// global sounds better...
|
|
// XXX undo...
|
|
var CollectionActions = actions.Actions({
|
|
config: {
|
|
// XXX add default collection list to config...
|
|
'default-collections': [
|
|
],
|
|
},
|
|
|
|
// Format:
|
|
// {
|
|
// <title>: {
|
|
// title: <title>,
|
|
// gid: <gid>,
|
|
//
|
|
// // base collection format -- raw data...
|
|
// data: <data>,
|
|
//
|
|
// ...
|
|
// },
|
|
// ...
|
|
// }
|
|
collections: null,
|
|
|
|
get collection(){
|
|
return this.location.collection },
|
|
set collection(value){
|
|
this.loadCollection(value) },
|
|
|
|
// Format:
|
|
// {
|
|
// // NOTE: this is always the first handler...
|
|
// 'data': <action-name>,
|
|
//
|
|
// <format>: <action-name>,
|
|
// ...
|
|
// }
|
|
get collection_handlers(){
|
|
var handlers = this.__collection_handlers || {}
|
|
|
|
if(Object.keys(handlers).length == 0){
|
|
var that = this
|
|
handlers['data'] = null
|
|
this.actions.forEach(function(action){
|
|
var fmt = that.getActionAttr(action, 'collectionFormat')
|
|
if(fmt){
|
|
handlers[fmt] = action
|
|
}
|
|
})
|
|
}
|
|
|
|
return handlers
|
|
},
|
|
|
|
collectionDataLoader: ['- Collections/',
|
|
core.doc`Collection data loader
|
|
|
|
.collectionDataLoader(title, data)
|
|
-> promise
|
|
|
|
The resulting promise will resolve to a Data object that will get
|
|
loaded as the collection.
|
|
|
|
data is of the .collections item format.
|
|
`,
|
|
{collectionFormat: 'data'},
|
|
function(title, data){
|
|
return new Promise(function(resolve){ resolve(data.data) }) }],
|
|
|
|
loadCollection: ['- Collections/',
|
|
core.doc`Load collection...
|
|
|
|
This will get collection data and crop into it.
|
|
|
|
If .data for a collection is not available this will do nothing,
|
|
this enables extending actions to handle the collection in
|
|
different ways.
|
|
|
|
Protocol:
|
|
- collection format handlers: .collection_handlers
|
|
- built from actions that define .collectionFormat attr to
|
|
contain the target format string.
|
|
- format is determined by matching it to a key in .collections[collection]
|
|
e.g. 'data' is applicable if .collections[collection].data is not null
|
|
- the format key's value is passed to the handler action
|
|
- the handler is expected to return a promise
|
|
- only the first matching handler is called
|
|
- the data handler is always first to get checked
|
|
|
|
For an example handler see:
|
|
.collectionDataLoader(..)
|
|
|
|
|
|
The .data handler is always first to enable caching, i.e. once some
|
|
non-data handler is done, it can set the .data which will be loaded
|
|
directly the next time.
|
|
To invalidate such a cache .data should simply be deleted.
|
|
`,
|
|
function(collection){
|
|
var that = this
|
|
if(collection == null
|
|
|| this.collections == null
|
|
|| !(collection in this.collections)){
|
|
return
|
|
}
|
|
|
|
var data = this.collections[collection]
|
|
var handlers = this.collection_handlers
|
|
|
|
// XXX might be good to sort handlers...
|
|
// XXX
|
|
|
|
for(var format in handlers){
|
|
if(data[format]){
|
|
return this[handlers[format]](collection, data)
|
|
.then(function(data){
|
|
data
|
|
&& that.crop.chainCall(that, function(){
|
|
// NOTE: the collection and .data may have different
|
|
// orders and/or sets of elements, this we need
|
|
// to sync, and do it BEFORE all the rendering
|
|
// happens...
|
|
that.data.updateImagePositions()
|
|
}, data)
|
|
// NOTE: we need this to sync the possible different
|
|
// states (order, ...) of the collection and .data...
|
|
&& that.collectionLoaded(collection)
|
|
})
|
|
}
|
|
}
|
|
}],
|
|
|
|
// events...
|
|
collectionLoaded: ['- Collections/',
|
|
core.doc`This is called by .loadCollection(..) or one of the
|
|
overloading actions when collection load is done...
|
|
|
|
`,
|
|
core.notUserCallable(function(collection){
|
|
// This is the window resize event...
|
|
//
|
|
// Not for direct use.
|
|
this.data.collection = this.location.collection = collection
|
|
})],
|
|
collectionUnloaded: ['- Collections/',
|
|
core.doc`This is called when unloading a collection.
|
|
`,
|
|
core.notUserCallable(function(collection){
|
|
// This is the window resize event...
|
|
//
|
|
// Not for direct use.
|
|
})],
|
|
|
|
saveCollection: ['- Collections/',
|
|
core.doc`Save current state to collection
|
|
|
|
Save Current state as collection
|
|
.saveCollection(collection)
|
|
-> this
|
|
|
|
Save new empty collection
|
|
.saveCollection(collection, true)
|
|
-> this
|
|
`,
|
|
function(collection, empty){
|
|
var that = this
|
|
collection = collection || this.collection
|
|
|
|
if(collection == null){
|
|
return
|
|
}
|
|
|
|
var collections = this.collections = this.collections || {}
|
|
|
|
collections[collection] = {
|
|
title: collection,
|
|
|
|
// NOTE: we do not need to care about tags here as they
|
|
// will get overwritten on load...
|
|
data: (empty ?
|
|
(new this.data.constructor())
|
|
: this.data
|
|
.clone()
|
|
.removeUnloadedGIDs())
|
|
.run(function(){
|
|
this.collection = collection
|
|
// NOTE: we are doing this manually after .removeUnloadedGIDs(..)
|
|
// as the later will mess-up the structures
|
|
// inherited from the main .data, namely tags...
|
|
this.tags = that.data.tags
|
|
}),
|
|
}
|
|
}],
|
|
newCollection: ['- Collections/',
|
|
function(collection){ return this.saveCollection(collection, true) }],
|
|
// XXX should we do anything special if collection is loaded???
|
|
removeCollection: ['- Collections/',
|
|
function(collection){
|
|
delete this.collections[collection]
|
|
}],
|
|
|
|
inCollections: ['- Image/',
|
|
core.doc`Get list of collections containing item`,
|
|
function(gid){
|
|
var that = this
|
|
gid = this.data.getImage(gid)
|
|
return Object.keys(this.collections || {})
|
|
.filter(function(c){
|
|
return !gid
|
|
|| that.collections[c].data.getImage(gid) })
|
|
}],
|
|
|
|
collect: ['- Collections/',
|
|
core.doc`Add items to collection
|
|
|
|
NOTE: this will not account for item topology.`,
|
|
function(gids, collection){
|
|
var that = this
|
|
|
|
gids = gids == 'loaded' ? this.data.getImages('loaded')
|
|
: gids instanceof Array ? gids
|
|
: [gids]
|
|
gids = gids
|
|
.map(function(gid){
|
|
return gid in that.data.ribbons ?
|
|
// when adding a ribbon gid expand to images...
|
|
that.data.ribbons[gid].compact()
|
|
: [that.data.getImage(gid)] })
|
|
.reduce(function(a, b){ return a.concat(b) }, [])
|
|
|
|
collection = collection || this.collection
|
|
|
|
if(collection == null){
|
|
return
|
|
}
|
|
|
|
// add to collection...
|
|
var data = this.data.constructor.fromArray(gids)
|
|
|
|
return this.joinCollect(null, collection, data)
|
|
}],
|
|
joinCollect: ['- Collections/Merge to collection',
|
|
core.doc`Merge/Join current state to collection
|
|
|
|
Join current state into collection
|
|
.joinCollect(collection)
|
|
-> this
|
|
|
|
Join current state with specific alignment into collection
|
|
.joinCollect(align, collection)
|
|
-> this
|
|
|
|
Join data to collection with specific alignment
|
|
.joinCollect(align, collection, data)
|
|
-> this
|
|
|
|
This is like .collect(..) but will preserve topology.
|
|
|
|
NOTE: for align docs see Data.join(..)
|
|
NOTE: if align is set to null or not given then it will be set
|
|
to default value.
|
|
NOTE: this will join to the left (prepend) of the collections, this is
|
|
different from how basic .join(..) works (appends)
|
|
`,
|
|
function(align, collection, data){
|
|
collection = arguments.length == 1 ? align : collection
|
|
if(collection == null){
|
|
return
|
|
}
|
|
// if only collection is given, reset align to null...
|
|
align = align === collection ? null : align
|
|
|
|
if(this.collections && this.collections[collection]){
|
|
//this.collections[collection].data.join(align, data || this.data.clone())
|
|
this.collections[collection].data = (data || this.data)
|
|
.clone()
|
|
.join(align, this.collections[collection].data)
|
|
|
|
} else {
|
|
this.saveCollection(collection)
|
|
}
|
|
}],
|
|
uncollect: ['Collections|Image/$Uncollect image',
|
|
{browseMode: function(){ return !this.collection && 'disabled' }},
|
|
function(gids, collection){
|
|
var that = this
|
|
|
|
gids = gids == 'loaded' ? this.data.getImages('loaded')
|
|
: gids instanceof Array ? gids
|
|
: [gids]
|
|
gids = gids
|
|
.map(function(gid){
|
|
return gid in that.data.ribbons ?
|
|
// when adding a ribbon gid expand to images...
|
|
that.data.ribbons[gid].compact()
|
|
: [that.data.getImage(gid)] })
|
|
.reduce(function(a, b){ return a.concat(b) }, [])
|
|
|
|
collection = collection || this.collection
|
|
|
|
if(collection == null){
|
|
return
|
|
}
|
|
|
|
/*/ NOTE: we are not using .data.updateImagePositions(gids, 'hide')
|
|
// here because it will remove the gids from everything
|
|
// while we need them removed only from ribbons...
|
|
var hideGIDs = function(){
|
|
var d = this
|
|
gids.forEach(function(gid){
|
|
var i = d.order.indexOf(gid)
|
|
Object.keys(d.ribbons).forEach(function(r){
|
|
delete d.ribbons[r][i]
|
|
})
|
|
})
|
|
}
|
|
//*/
|
|
|
|
if(this.collection == collection){
|
|
this.data
|
|
//.run(hideGIDs)
|
|
.removeGIDs(gids)
|
|
.removeEmptyRibbons()
|
|
}
|
|
|
|
this.collections[collection].data
|
|
//.run(hideGIDs)
|
|
.removeGIDs(gids)
|
|
.removeEmptyRibbons()
|
|
}],
|
|
|
|
// manage serialization and loading...
|
|
// XXX make this reflect the format automatically...
|
|
load: [function(json){
|
|
var that = this
|
|
var collections = {}
|
|
var c = json.collections || {}
|
|
|
|
Object.keys(c).forEach(function(title){
|
|
var data = data.Data
|
|
.fromJSON(c[title].data)
|
|
|
|
// XXX make this reflect the format automatically...
|
|
collections[title] = {
|
|
title: title,
|
|
|
|
data: data,
|
|
}
|
|
})
|
|
|
|
if(Object.keys(collections).length > 0){
|
|
this.collections = collections
|
|
}
|
|
}],
|
|
json: [function(){ return function(res){
|
|
var collections = this.collections
|
|
if(collections){
|
|
res.collections = {}
|
|
Object.keys(this.collections).forEach(function(title){
|
|
var data = collections[title].data.dumpJSON()
|
|
delete data.tags
|
|
|
|
// XXX make this reflect the format automatically...
|
|
res.collections[title] = {
|
|
title: title,
|
|
|
|
data: data,
|
|
}
|
|
})
|
|
}
|
|
} }],
|
|
clear: [function(){
|
|
this.collection
|
|
&& this.collectionUnloaded('*')
|
|
delete this.collections
|
|
delete this.location.collection
|
|
}],
|
|
})
|
|
|
|
|
|
var Collection =
|
|
module.Collection = core.ImageGridFeatures.Feature({
|
|
title: '',
|
|
doc: '',
|
|
|
|
tag: 'collections',
|
|
depends: [
|
|
'base',
|
|
'crop',
|
|
],
|
|
suggested: [
|
|
'ui-collections',
|
|
'fs-collections',
|
|
],
|
|
|
|
actions: CollectionActions,
|
|
|
|
handlers: [
|
|
// maintain the .collection state...
|
|
['uncrop.pre',
|
|
function(){
|
|
var collection = this.collection
|
|
return function(){
|
|
collection != null
|
|
&& collection != this.data.collection
|
|
&& this.collectionUnloaded(collection) }
|
|
}],
|
|
['collectionLoaded',
|
|
function(){
|
|
console.log('COLLECTION: LOADED')
|
|
}],
|
|
['collectionUnloaded',
|
|
function(_, collection){
|
|
var collection = this.location.collection = this.data.collection
|
|
|
|
// cleanup...
|
|
if(collection == null){
|
|
delete this.location.collection
|
|
}
|
|
|
|
console.log('COLLECTION: UNLOADED')
|
|
|
|
this.data.updateImagePositions()
|
|
}],
|
|
],
|
|
})
|
|
|
|
|
|
|
|
//---------------------------------------------------------------------
|
|
|
|
// XXX make collections sortable...
|
|
// XXX do we need a collection button (like crop button?) ???
|
|
// XXX show collections in image metadata...
|
|
var UICollectionActions = actions.Actions({
|
|
browseCollections: ['Collections|Crop/$Collec$tions...',
|
|
widgets.makeUIDialog(function(action){
|
|
var that = this
|
|
var to_remove = []
|
|
|
|
return browse.makeLister(null,
|
|
function(path, make){
|
|
var dialog = this
|
|
.on('update', function(){
|
|
that.collection
|
|
&& dialog.filter(JSON.stringify(that.collection))
|
|
.addClass('highlighted')
|
|
})
|
|
|
|
var collections = Object.keys(that.collections || {})
|
|
|
|
make.EditableList(collections,
|
|
{
|
|
unique: true,
|
|
to_remove: to_remove,
|
|
itemopen: function(title){
|
|
var gid = that.current
|
|
action ?
|
|
action.call(that, title)
|
|
: that.loadCollection(title)
|
|
that.focusImage(gid)
|
|
dialog.close()
|
|
},
|
|
normalize: function(title){
|
|
return title.trim() },
|
|
check: function(title){
|
|
return title.length > 0 },
|
|
|
|
itemadded: function(title){
|
|
action ?
|
|
that.newCollection(title)
|
|
: that.saveCollection(title) },
|
|
})
|
|
}, {
|
|
// focus current collection...
|
|
selected: that.collection,
|
|
})
|
|
.close(function(){
|
|
to_remove.forEach(function(title){
|
|
that.removeCollection(title)
|
|
})
|
|
})
|
|
})],
|
|
browseImageCollections: ['Image/$Collections...',
|
|
{dialogTitle: 'Image Collections...'},
|
|
widgets.makeUIDialog(function(gid){
|
|
var that = this
|
|
gid = this.data.getImage(gid)
|
|
|
|
var all
|
|
var collections
|
|
|
|
var to_remove
|
|
|
|
return browse.makeLister(null,
|
|
function(path, make){
|
|
var dialog = this
|
|
.on('update', function(){
|
|
that.collection
|
|
&& dialog.filter(JSON.stringify(that.collection))
|
|
.addClass('highlighted')
|
|
})
|
|
|
|
all = Object.keys(that.collections || {})
|
|
|
|
collections = collections
|
|
|| that.inCollections(gid || null)
|
|
|
|
// build the disabled list...
|
|
if(!to_remove){
|
|
to_remove = []
|
|
all.forEach(function(title){
|
|
collections.indexOf(title) < 0
|
|
&& to_remove.push(title)
|
|
})
|
|
}
|
|
|
|
all.length > 0 ?
|
|
make.EditableList(all,
|
|
{
|
|
new_item: false,
|
|
to_remove: to_remove,
|
|
itemopen: function(title){
|
|
var i = to_remove.indexOf(title)
|
|
|
|
i >= 0 ?
|
|
to_remove.splice(i, 1)
|
|
: to_remove.push(title)
|
|
|
|
dialog.update()
|
|
},
|
|
})
|
|
: make.Empty()
|
|
})
|
|
.close(function(){
|
|
all.forEach(function(title){
|
|
collections.indexOf(title) < 0
|
|
&& to_remove.indexOf(title) < 0
|
|
&& that.collect(gid, title)
|
|
})
|
|
to_remove.forEach(function(title){
|
|
that.uncollect(gid, title)
|
|
})
|
|
})
|
|
})],
|
|
|
|
// Collections actions with collection selection...
|
|
// XXX should we warn the user when overwriting???
|
|
saveAsCollection: ['Collections|Crop/$Save as collection...',
|
|
widgets.uiDialog(function(){
|
|
return this.browseCollections(function(title){
|
|
this.saveCollection(title) }) })],
|
|
addToCollection: ['Collections|Crop|Image/Add $image to collection...',
|
|
widgets.uiDialog(function(gids){
|
|
return this.browseCollections(function(title){
|
|
this.collect(gids || this.current, title) }) })],
|
|
addLoadedToCollection: ['Collections|Crop/$Add loaded images to collection...',
|
|
widgets.uiDialog(function(){ return this.addToCollection('loaded') })],
|
|
joinToCollection: ['Collections|Crop/$Merge view to collection...',
|
|
widgets.uiDialog(function(){
|
|
return this.browseCollections(function(title){
|
|
this.joinCollect(title) }) })],
|
|
|
|
/*/ XXX this is not used by metadata yet...
|
|
metadataSection: ['- Image/',
|
|
function(gid, make){
|
|
}],
|
|
//*/
|
|
})
|
|
|
|
|
|
var UICollection =
|
|
module.UICollection = core.ImageGridFeatures.Feature({
|
|
title: '',
|
|
doc: '',
|
|
|
|
tag: 'ui-collections',
|
|
depends: [
|
|
'ui',
|
|
'collections',
|
|
],
|
|
|
|
actions: UICollectionActions,
|
|
|
|
handlers: [
|
|
['uncollect',
|
|
function(_, gids, collection){
|
|
(collection == null || this.collection == collection)
|
|
&& this.reload(true)
|
|
}],
|
|
],
|
|
})
|
|
|
|
|
|
|
|
//---------------------------------------------------------------------
|
|
// XXX Things to try/do:
|
|
// - save collection on exit/write (?)
|
|
// - lazy load collections (load list, lazy-load data)
|
|
// - load directories as collections...
|
|
// - export collections to directories...
|
|
// - collection history...
|
|
|
|
var FileSystemCollectionActions = actions.Actions({
|
|
|
|
// Format:
|
|
// {
|
|
// path: <string>,
|
|
// ...
|
|
// }
|
|
collections: null,
|
|
|
|
collectionPathLoader: ['- Collections/',
|
|
{collectionFormat: 'path'},
|
|
function(data, loader){
|
|
// XXX
|
|
}],
|
|
|
|
importCollectionsFromPath: ['- Collections|File/Import collections from path',
|
|
function(path){
|
|
// XXX
|
|
}],
|
|
})
|
|
|
|
|
|
// XXX manage format...
|
|
// XXX manage changes...
|
|
var FileSystemCollection =
|
|
module.FileSystemCollection = core.ImageGridFeatures.Feature({
|
|
title: '',
|
|
doc: '',
|
|
|
|
tag: 'fs-collections',
|
|
depends: [
|
|
'index-format',
|
|
'fs',
|
|
'collections',
|
|
],
|
|
|
|
actions: FileSystemCollectionActions,
|
|
|
|
handlers: [
|
|
// XXX maintain changes...
|
|
// XXX
|
|
[[
|
|
'collect',
|
|
'joinCollect',
|
|
'uncollect',
|
|
|
|
'saveCollection',
|
|
|
|
'removeCollection',
|
|
],
|
|
function(){
|
|
// XXX mark changed collections...
|
|
// XXX added/removed collection -> mark collection index as changed...
|
|
}],
|
|
|
|
// XXX handle removed collections -- move to trash (???)
|
|
// ...might be a good idea to add something like index gc API...
|
|
['prepareIndexForWrite',
|
|
function(res, _, full){
|
|
var changed = full == true
|
|
|| res.changes === true
|
|
|| res.changes.collections
|
|
|
|
if(changed && res.raw.collections){
|
|
// select the actual changed collection list...
|
|
changed = changed === true ?
|
|
Object.keys(res.raw.collections)
|
|
: changed
|
|
|
|
// collection index...
|
|
res.index['collection-index'] = Object.keys(this.collections)
|
|
|
|
Object.keys(changed)
|
|
// skip the raw field...
|
|
.filter(function(k){ return changed.indexOf(k) >= 0 })
|
|
.forEach(function(k){
|
|
// XXX use collection gid...
|
|
res.index['collections/' + k] = res.raw.collections[k]
|
|
})
|
|
}
|
|
}],
|
|
['prepareJSONForLoad',
|
|
function(res, json){
|
|
// XXX
|
|
}],
|
|
],
|
|
})
|
|
|
|
|
|
|
|
//---------------------------------------------------------------------
|
|
// XXX localstorage-collections (???)
|
|
|
|
|
|
|
|
|
|
/**********************************************************************
|
|
* vim:set ts=4 sw=4 : */ return module })
|