')
.appendTo($('.viewer'))
}
return makeIndicator(text)
.addClass(cls)
.appendTo(c)
}
function showContextIndicator(cls, text){
var c = $('.context-mode-indicators')
if(c.length == 0){
c = $('
')
.addClass('context-mode-indicators')
.append('
Context status')
.appendTo($('.viewer'))
}
return makeIndicator(text)
.addClass(cls)
.appendTo(c)
}
/**********************************************************************
* Progress bar...
*/
// Make or get progress bar container...
//
// mode can be:
// - null - default
// - 'floating'
// - 'panel'
function getProgressContainer(mode, parent){
parent = parent == null ? $('.viewer') : parent
mode = mode == null ? PROGRESS_WIDGET_CONTAINER : mode
if(mode == 'floating'){
// widget container...
var container = parent.find('.progress-container')
if(container.length == 0){
container = $('
')
.appendTo(parent)
}
} else {
var container = getPanel('Progress')
if(container.length == 0){
container = makeSubPanel('Progress')
.addClass('.progress-container')
}
container = container.find('.content')
}
return container
}
// Make or get progress bar by name...
//
// Setting close to false will disable the close button...
//
// Events:
// - progressUpdate (done, total)
// Triggered by user to update progress bar state.
//
// takes two arguments:
// done - the number of done tasks
// total - the total number of tasks
//
// Usage:
// widget.trigger('progressUpdate', [done, total])
//
// Shorthand:
// updateProgressBar(name, done[, total])
//
// - progressClose
// Triggered by the close button.
// By default triggers the progressDone event.
//
// Shorthand:
// closeProgressBar(name[, msg])
//
// - progressDone
// Triggered by user or progressClose handler.
// Set the progress bar to done state and hide after hide_timeout.
//
// - progressReset
// Triggered by user or progressBar(..) if the progress bar already
// exists and is hidden (display: none).
// Reset the progress bar to it's initial (indeterminite) state
// and show it.
//
// Shorthand:
// resetProgressBar(name)
//
function progressBar(name, container, close, hide_timeout, auto_remove){
container = container == null
? getProgressContainer()
: container
close = close === undefined
? function(){
$(this).trigger('progressDone') }
: close
hide_timeout = hide_timeout == null ? PROGRESS_HIDE_TIMEOUT
: hide_timeout < 0 ? 0
: hide_timeout > 3000 ? 3000
: hide_timeout
auto_remove = auto_remove == null ? true : auto_remove
var widget = getProgressBar(name)
// a progress bar already exists, reset it and return...
// XXX should we re-bind the event handlers here???
if(widget.length > 0 && widget.css('display') == 'none'){
return widget.trigger('progressReset')
}
// fields we'll need to update...
var state = $('
')
var bar = $('
')
// the progress bar widget...
var widget = $('
'+name+'
')
// progress state...
.append(state)
// the close button...
if(close !== false){
widget
.append($('
×')
.click(function(){
$(this).trigger('progressClose')
}))
}
widget
.append(bar)
.appendTo(container)
.on('progressUpdate', function(evt, done, total){
done = done == null ? bar.attr('value') : done
total = total == null ? bar.attr('max') : total
bar.attr({
value: done,
max: total
})
state.text(' ('+done+' of '+total+')')
})
.on('progressDone', function(evt, done, msg){
done = done == null ? bar.attr('value') : done
msg = msg == null ? 'done' : msg
bar.attr('value', done)
state.text(' ('+msg+')')
widget.find('.close').hide()
setTimeout(function(){
widget.hide()
// XXX this is not a good way to go...
// need a clean way to reset...
if(auto_remove){
widget.remove()
}
}, hide_timeout)
})
.on('progressReset', function(){
widget
.css('display', '')
.find('.close')
.css('display', '')
state.text('')
bar.attr({
value: '',
max: '',
})
})
if(close === false){
widget.on('progressClose', function(evt, msg){
if(msg != null){
widget.trigger('progressDone', [null, msg])
} else {
widget.trigger('progressDone')
}
})
} else if(close != null){
widget.on('progressClose', close)
}
bar = $(bar[0])
state = $(state[0])
widget = $(widget[0])
return widget
}
function getProgressBar(name){
return $('.progress-bar[name="'+name+'"]')
}
/******************************************* Event trigger helpers ***/
function triggerProgressBarEvent(name, evt, args){
var widget = typeof(name) == typeof('str')
? getProgressBar(name)
: name
return widget.trigger(evt, args)
}
function resetProgressBar(name){
return triggerProgressBarEvent(name, 'progressReset')
}
function updateProgressBar(name, done, total){
return triggerProgressBarEvent(name, 'progressUpdate', [done, total])
}
function closeProgressBar(name, msg){
if(msg != null){
return triggerProgressBarEvent(name, 'progressClose', [msg])
}
return triggerProgressBarEvent(name, 'progressClose')
}
/**********************************************************************
* Dialogs...
*/
function detailedAlert(text, description, button){
return formDialog(null, '', {'': {
html: $('
')
.append($('
')
.html(text))
.append($('
')
.html(description))
}}, button == null ? false : button, 'detailed-alert')
}
// NOTE: this will not work without node-webkit...
function getDir(message, dfl, btn){
btn = btn == null ? 'OK' : btn
dfl = dfl == null ? '' : dfl
var res = $.Deferred()
formDialog(null, message, {'': {ndir: dfl}}, btn, 'getDir')
.done(function(data){ res.resolve(data['']) })
.fail(function(){ res.reject() })
return res
}
/***************************************** Domain-specific dialogs ***/
// XXX do reporting...
// XXX would be nice to save settings...
// ...might be good to use datalist...
function exportPreviewsDialog(state, dfl){
dfl = dfl == null ? BASE_URL : dfl
// XXX make this more generic...
// tell the user what state are we exporting...
if(state == null){
var imgs = 0
// NOTE: we are not using order or image count as these sets may
// be larger that the current crop...
DATA.ribbons.map(function(e){
imgs += e.length
})
state = toggleSingleImageMode('?') == 'on' ? 'current image' : state
state = state == null && isViewCropped() ?
'cropped view: '+
imgs+' images in '+
DATA.ribbons.length+' ribbons'
: state
state = state == null ?
'all: '+
imgs+' images in '+
DATA.ribbons.length+' ribbons'
: state
}
var res = $.Deferred()
updateStatus('Export...').show()
// NOTE: we are not defining the object in-place here because some
// keys become unreadable with JS syntax preventing us from
// splitting the key into several lines...
var cfg = {}
var img_pattern = 'Image name pattern | '+
'%f - full filename (same as %n%e)\n'+
'%n - filename\n'+
'%e - extension (with leading dot)\n'+
'%(abc)m - if marked insert "abc"\n'+
'%(abc)b - if bookmarked insert "abc"\n'+
'%gid - long gid\n'+
'%g - short gid\n'
// multiple images...
if(state != 'current image'){
cfg[img_pattern +
'%I - global order\n'+
'%i - current selection order'] = '%f'
cfg['Level directory name'] = 'fav'
// single image...
} else {
cfg[img_pattern +
'\n'+
'NOTE: %i and %I are not supported for single\n'+
'image exporting.'] = '%f'
}
cfg['Size | '+
'The selected size is aproximate, the actual\n'+
'preview will be copied from cache.\n'+
'\n'+
'NOTE: if not all previews are yet generated,\n'+
'this will save the available previews, not all\n'+
'of which may be of the right size, if this\n'+
'happens wait till all the previews are done\n'+
'and export again.'] = {
select: ['Original image'].concat(PREVIEW_SIZES.slice().sort()),
default: 1
}
cfg['Destination | '+
'Relative paths are supported.\n\n'+
'NOTE: All paths are relative to the curent\n'+
'directory.'] = {ndir: dfl}
var keys = Object.keys(cfg)
formDialog(null, 'Export:
'+ state +'.', cfg, 'OK', 'exportPreviewsDialog')
.done(function(data){
// get the form data...
var name = data[keys[0]]
if(state != 'current image'){
var size = data[keys[2]]
var path = normalizePath(data[keys[3]])
var dir = data[keys[1]]
} else {
var size = data[keys[1]]
var path = normalizePath(data[keys[2]])
}
size = size == 'Original image' ? Math.max.apply(null, PREVIEW_SIZES)*2 : parseInt(size)-5
// do the actual exporting...
// full state...
if(state != 'current image'){
exportImagesTo(path, name, dir, size)
// single image...
} else {
exportImageTo(getImageGID(), path, name, size)
}
// XXX do real reporting...
showStatusQ('Copying data...')
res.resolve(data[''])
})
.fail(function(){
showStatusQ('Export: canceled.')
res.reject()
})
return res
}
function loadDirectoryDialog(dfl){
dfl = dfl == null ? BASE_URL : dfl
updateStatus('Open...').show()
formDialog(null, 'Path to open | To see list of previously loaded urls press ctrl-H.', {
'': {ndir: dfl},
'Precess previews': true,
}, 'OK', 'loadDirectoryDialog')
.done(function(data){
var path = normalizePath(data[''].trim())
var process_previews = data['Precess previews']
// reset the modes...
toggleSingleImageMode('off')
toggleSingleRibbonMode('off')
toggleMarkedOnlyView('off')
// do the loading...
statusNotify(loadDir(path, !process_previews))
/*
.done(function(){
if(process_previews){
showStatusQ('Previews: processing started...')
// generate/attach previews...
makeImagesPreviewsQ(DATA.order)
.done(function(){
showStatusQ('Previews: processing done.')
})
}
})
*/
.done(function(){
// XXX is this the right place for this???
pushURLHistory(BASE_URL)
})
})
.fail(function(){
showStatusQ('Open: canceled.')
})
}
// XXX get EXIF, IPTC...
function showImageInfo(){
var gid = getImageGID(getImage())
var r = getRibbonIndex(getRibbon())
var data = IMAGES[gid]
var orientation = data.orientation
orientation = orientation == null ? 0 : orientation
var flipped = data.flipped
flipped = flipped == null ? '' : ', flipped '+flipped+'ly'
var order = DATA.order.indexOf(gid)
var name = getImageFileName(gid)
var date = new Date(data.ctime * 1000)
var comment = data.comment
comment = comment == null ? '' : comment
comment = comment.replace(/\n/g, '
')
var tags = data.tags
tags = tags == null ? '' : tags.join(', ')
return formDialog(null,
('
'+
'
"'+ name +'"
'+
'
'+
// basic info...
// XXX BUG: something here breaks when self-generated data is
// currently open -- .length of undefined...
'
|
'+
'| GID: | '+ gid +' |
'+
'| Date: | '+ date +' |
'+
'| Path: | "'+ unescape(data.path) +'" |
'+
'| Orientation: | '+ orientation +'°'+flipped+' |
'+
'| Order: | '+ order +' |
'+
'| Position (ribbon): | '+ (DATA.ribbons[r].indexOf(gid)+1) +
'/'+ DATA.ribbons[r].length +' |
'+
'| Position (global): | '+ (order+1) +'/'+ DATA.order.length +' |
'+
'| Sorted: | '+
//Math.round(((DATA.order.length-tagSelectAND('unsorted', DATA.order).length)/DATA.order.length)*100+'') +
//Math.round(((DATA.order.length-tagSelectAND('unsorted').length)/DATA.order.length)*100+'') +
Math.round(((DATA.order.length-TAGS['unsorted'].length)/DATA.order.length)*100+'') +
'% |
'+
// editable fields...
'
|
'+
// XXX this expanding to a too big size will mess up the screen...
// add per editable and global dialog max-height and overflow
'| Comment: |
'+
'| Tags: | '+ tags +' |
'+
'
'+
'
'+
'
'),
// NOTE: without a save button, there will be no way to accept the
// form on a touch-only device...
{}, 'OK', 'showImageInfoDialog')
// save the form data...
.done(function(_, form){
// comment...
var ncomment = form.find('.comment').html()
if(ncomment != comment){
ncomment = ncomment.replace(/
/ig, '\n')
if(ncomment.trim() == ''){
delete data.comment
} else {
data.comment = ncomment
}
imageUpdated(gid)
}
// tags...
var ntags = form.find('.tags').text().trim()
if(ntags != tags){
ntags = ntags.split(/\s*,\s*/)
updateTags(ntags, gid)
}
})
}
/*********************************************************************/
// XXX need a propper:
// - update mechanics...
// - save mechanics
function makeCommentPanel(panel){
return makeSubPanel(
'Info: Comment',
$('Comment: '),
panel,
true,
true)
}
/*********************************************************************/
function setupUI(viewer){
console.log('UI: setup...')
setupIndicators()
return viewer
.click(function(){
if($('.ribbon').length == 0){
loadDirectoryDialog()
}
})
.on([
'focusingImage',
'fittingImages',
//'updatingImageProportions',
'horizontalShiftedImage',
].join(' '),
function(){
updateCurrentMarker()
})
}
SETUP_BINDINGS.push(setupUI)
/**********************************************************************
* vim:set ts=4 sw=4 nowrap : */