mirror of
https://github.com/flynx/ImageGrid.git
synced 2025-10-28 18:00:09 +00:00
959 lines
24 KiB
JavaScript
Executable File
959 lines
24 KiB
JavaScript
Executable File
/**********************************************************************
|
|
*
|
|
*
|
|
*
|
|
**********************************************************************/
|
|
|
|
//var DEBUG = DEBUG != null ? DEBUG : true
|
|
|
|
var CURSOR_SHOW_THRESHOLD = 20
|
|
var CURSOR_HIDE_TIMEOUT = 1000
|
|
|
|
var STATUS_QUEUE = []
|
|
var STATUS_QUEUE_TIME = 200
|
|
|
|
var CONTEXT_INDICATOR_UPDATERS = []
|
|
|
|
|
|
// this can be:
|
|
// - 'floating'
|
|
// - 'panel'
|
|
var PROGRESS_WIDGET_CONTAINER = 'floating'
|
|
// can be between 0 and 3000
|
|
var PROGRESS_HIDE_TIMEOUT = 1500
|
|
|
|
|
|
|
|
/*********************************************************************/
|
|
|
|
// XXX revise...
|
|
// NOTE: to catch the click event correctly while the cursor is hidden
|
|
// this must be the first to get the event...
|
|
// NOTE: this uses element.data to store the timer and cursor position...
|
|
function autoHideCursor(elem){
|
|
elem = $(elem)
|
|
var data = elem.data()
|
|
elem
|
|
.on('mousemove', function(evt){
|
|
var cursor = elem.css('cursor')
|
|
|
|
data._cursor_pos = data._cursor_pos == null || cursor != 'none' ?
|
|
[evt.clientX, evt.clientY]
|
|
: data._cursor_pos
|
|
|
|
// cursor visible -- extend visibility...
|
|
if(cursor != 'none'){
|
|
|
|
if(data._cursor_timeout != null){
|
|
clearTimeout(data._cursor_timeout)
|
|
}
|
|
data._cursor_timeout = setTimeout(function(){
|
|
if(Math.abs(evt.clientX - data._cursor_pos[0]) < CURSOR_SHOW_THRESHOLD
|
|
|| Math.abs(evt.clientY - data._cursor_pos[1]) < CURSOR_SHOW_THRESHOLD){
|
|
|
|
elem.css('cursor', 'none')
|
|
}
|
|
}, CURSOR_HIDE_TIMEOUT)
|
|
|
|
|
|
// cursor hidden -- if outside the threshold, show...
|
|
} else if(Math.abs(evt.clientX - data._cursor_pos[0]) > CURSOR_SHOW_THRESHOLD
|
|
|| Math.abs(evt.clientY - data._cursor_pos[1]) > CURSOR_SHOW_THRESHOLD){
|
|
|
|
elem.css('cursor', '')
|
|
}
|
|
})
|
|
.click(function(evt){
|
|
if(elem.css('cursor') == 'none'){
|
|
//event.stopImmediatePropagation()
|
|
//event.preventDefault()
|
|
|
|
if(data._cursor_timeout != null){
|
|
clearTimeout(data._cursor_timeout)
|
|
data._cursor_timeout = null
|
|
}
|
|
|
|
elem.css('cursor', '')
|
|
//return false
|
|
}
|
|
})
|
|
return elem
|
|
}
|
|
|
|
|
|
/*
|
|
// XXX does not work...
|
|
// ...does not show the cursor without moving it...
|
|
function showCursor(elem){
|
|
elem = $(elem)
|
|
var data = elem.data()
|
|
if(data._cursor_timeout != null){
|
|
clearTimeout(data._cursor_timeout)
|
|
}
|
|
elem.css('cursor', '')
|
|
}
|
|
*/
|
|
|
|
|
|
function setupIndicators(){
|
|
showGlobalIndicator(
|
|
'cropped-view',
|
|
'Cropped view (shift-F2/F3/C/F)')
|
|
.css('cursor', 'hand')
|
|
.click(function(){ uncropData() })
|
|
}
|
|
|
|
|
|
function makeContextIndicatorUpdater(image_class){
|
|
var _updater = function(image){
|
|
var indicator = $('.context-mode-indicators .current-image-'+image_class)
|
|
if(image.hasClass(image_class)){
|
|
indicator.addClass('shown')
|
|
} else {
|
|
indicator.removeClass('shown')
|
|
}
|
|
}
|
|
CONTEXT_INDICATOR_UPDATERS.push(_updater)
|
|
return _updater
|
|
}
|
|
|
|
|
|
function updateContextIndicators(image){
|
|
image = image == null ? getImage() : $(image)
|
|
|
|
CONTEXT_INDICATOR_UPDATERS.map(function(update){
|
|
update(image)
|
|
})
|
|
}
|
|
|
|
|
|
function showCurrentMarker(){
|
|
return $('<div/>')
|
|
.addClass('current-marker')
|
|
.css({
|
|
opacity: '0',
|
|
top: '0px',
|
|
left: '0px',
|
|
})
|
|
.appendTo($('.ribbon-set'))
|
|
.animate({
|
|
'opacity': 1
|
|
}, 500)
|
|
.mouseover(function(){
|
|
$('.current.image')
|
|
})
|
|
}
|
|
|
|
function updateCurrentMarker(){
|
|
var scale = getElementScale($('.ribbon-set'))
|
|
var marker = $('.current-marker')
|
|
var cur = $('.current.image')
|
|
var w = cur.outerWidth(true)
|
|
var h = cur.outerHeight(true)
|
|
marker = marker.length == 0 ? showCurrentMarker() : marker
|
|
var d = getRelativeVisualPosition(marker, cur)
|
|
return marker.css({
|
|
top: parseFloat(marker.css('top')) + d.top/scale,
|
|
left: parseFloat(marker.css('left')) + d.left/scale,
|
|
// keep size same as the image...
|
|
width: w,
|
|
height: h,
|
|
})
|
|
}
|
|
|
|
|
|
function flashIndicator(direction){
|
|
var cls = {
|
|
// shift up/down...
|
|
prev: '.up-indicator',
|
|
next: '.down-indicator',
|
|
// hit start/end/top/bottom of view...
|
|
start: '.start-indicator',
|
|
end: '.end-indicator',
|
|
top: '.top-indicator',
|
|
bottom: '.bottom-indicator',
|
|
}[direction]
|
|
|
|
var indicator = $(cls)
|
|
|
|
if(indicator.length == 0){
|
|
indicator = $('<div>')
|
|
.addClass(cls.replace('.', ''))
|
|
.appendTo($('.viewer'))
|
|
}
|
|
|
|
return indicator
|
|
// NOTE: this needs to be visible in all cases and key press
|
|
// rhythms...
|
|
.show()
|
|
.delay(100)
|
|
.fadeOut(300)
|
|
}
|
|
|
|
|
|
function showRibbonIndicator(){
|
|
var cls = '.ribbon-indicator'
|
|
var indicator = $(cls)
|
|
|
|
if(indicator.length == 0){
|
|
indicator = $('<div>')
|
|
.addClass(cls.replace('.', ''))
|
|
.appendTo($('.viewer'))
|
|
}
|
|
|
|
var r = getRibbonIndex()
|
|
|
|
// get the base ribbon...
|
|
var base = getBaseRibbonIndex()
|
|
|
|
var r = r == base ? r+'*' : r
|
|
return indicator.text(r)
|
|
}
|
|
|
|
|
|
function flashRibbonIndicator(){
|
|
var indicator = showRibbonIndicator()
|
|
var cls = '.flashing-ribbon-indicator'
|
|
|
|
var flashing_indicator = $(cls)
|
|
|
|
if(flashing_indicator.length == 0){
|
|
flashing_indicator = indicator
|
|
.clone()
|
|
.addClass(cls.replace('.', ''))
|
|
.appendTo($('.viewer'))
|
|
}
|
|
|
|
return flashing_indicator
|
|
// .stop()
|
|
// .show()
|
|
// .delay(200)
|
|
// .fadeOut(500)
|
|
.show()
|
|
.delay(100)
|
|
.fadeOut(300)
|
|
}
|
|
|
|
|
|
// Update an info element
|
|
//
|
|
// align can be:
|
|
// - top
|
|
// - bottom
|
|
//
|
|
// If target is an existing info container (class: overlay-info) then
|
|
// just fill that.
|
|
function updateInfo(elem, data, target){
|
|
var viewer = $('.viewer')
|
|
target = target == null ? viewer : $(target)
|
|
elem = elem == null ? $('.overlay-info') : $(elem)
|
|
|
|
if(elem.length == 0){
|
|
elem = $('<div/>')
|
|
}
|
|
|
|
elem
|
|
.addClass('overlay-info')
|
|
.html('')
|
|
.off()
|
|
|
|
if(typeof(data) == typeof('abc')){
|
|
elem.html(data)
|
|
} else {
|
|
elem.append(data)
|
|
}
|
|
|
|
elem
|
|
.appendTo(target)
|
|
|
|
return elem
|
|
}
|
|
|
|
|
|
function showInfo(elem, data, target){
|
|
elem = elem == null ? $('.overlay-info') : elem
|
|
elem = data == null ? elem : updateInfo(elem, data, traget)
|
|
return elem.fadeIn()
|
|
}
|
|
|
|
|
|
function hideInfo(elem){
|
|
elem = elem == null ? $('.overlay-info') : elem
|
|
return elem.fadeOut()
|
|
}
|
|
|
|
|
|
// Update status message
|
|
//
|
|
// NOTE: this will update message content and return it as-is, things
|
|
// like showing the message are to be done manually...
|
|
// see: showStatus(...) and showErrorStatus(...) for a higher level
|
|
// API...
|
|
// NOTE: in addition to showing user status, this will also log the
|
|
// satus to browser console...
|
|
// NOTE: the message will be logged to console via either console.log(...)
|
|
// or console.error(...), if the message starts with "Error".
|
|
// NOTE: if message is null, then just return the status element...
|
|
//
|
|
// XXX add abbility to append and clear status...
|
|
function updateStatus(message){
|
|
|
|
var elem = $('.global-status')
|
|
if(elem.length == 0){
|
|
elem = $('<div class="global-status"/>')
|
|
}
|
|
if(message == null){
|
|
return elem
|
|
}
|
|
|
|
if(typeof(message) == typeof('s') && /^error.*/i.test(message)){
|
|
console.error.apply(console, arguments)
|
|
} else {
|
|
console.log.apply(console, arguments)
|
|
}
|
|
|
|
if(arguments.length > 1){
|
|
message = Array.apply(Array, arguments).join(' ')
|
|
}
|
|
|
|
return updateInfo(elem, message)
|
|
}
|
|
|
|
|
|
// Same as updateInfo(...) but will aslo show and animate-close the message
|
|
//
|
|
// XXX the next call will not reset the animation of the previous, rather
|
|
// it will pause it and rezume...
|
|
// ...not sure if this is correct.
|
|
function showStatus(message){
|
|
return updateStatus.apply(null, arguments)
|
|
//.stop()
|
|
.stop(true, false)
|
|
//.finish()
|
|
.show()
|
|
.delay(500)
|
|
.fadeOut(800)
|
|
}
|
|
|
|
|
|
// Same as showStatus(...) but queue the message so as to display it for
|
|
// a meaningful amount of time...
|
|
//
|
|
// - This will print the first message right away.
|
|
// - Each consecutive message if STATUS_QUEUE_TIME has not passed yet
|
|
// will get queued.
|
|
// - Once the STATUS_QUEUE_TIME has passed the next message is reported
|
|
// and so on until the queue is empty.
|
|
//
|
|
// NOTE: for very a fast and large sequence of messages the reporting
|
|
// may (will) take longer (significantly) than the actual "job"...
|
|
// NOTE: this will delay the logging also...
|
|
function showStatusQ(message){
|
|
if(STATUS_QUEUE.length == 0){
|
|
|
|
// end marker...
|
|
STATUS_QUEUE.push(0)
|
|
|
|
showStatus.apply(null, arguments)
|
|
|
|
function _printer(){
|
|
// if queue is empty we have nothing to do...
|
|
if(STATUS_QUEUE.length == 1){
|
|
STATUS_QUEUE.pop()
|
|
return
|
|
}
|
|
// if not empty show a status and repeat...
|
|
showStatus.apply(null, STATUS_QUEUE.pop())
|
|
setTimeout(_printer, STATUS_QUEUE_TIME)
|
|
}
|
|
|
|
setTimeout(_printer, STATUS_QUEUE_TIME)
|
|
|
|
// queue not empty...
|
|
} else {
|
|
STATUS_QUEUE.splice(1, 0, Array.apply(Array, arguments))
|
|
}
|
|
}
|
|
|
|
|
|
// Same as showStatus(...) but will always add 'Error: ' to the start
|
|
// of the message
|
|
//
|
|
// NOTE: this will show the message but will not hide it.
|
|
function showErrorStatus(message){
|
|
message = Array.apply(Array, arguments)
|
|
message.splice(0, 0, 'Error:')
|
|
//return showStatusQ.apply(null, message)
|
|
return updateStatus.apply(null, message)
|
|
.one('click', function(){ $(this).fadeOut() })
|
|
//.stop()
|
|
.stop(true, false)
|
|
//.finish()
|
|
.show()
|
|
}
|
|
|
|
|
|
// shorthand methods...
|
|
function hideStatus(){
|
|
// yes, this indeed looks funny -- to hide a status you need to show
|
|
// it without any arguments... ;)
|
|
return showStatus()
|
|
}
|
|
function getStatus(){
|
|
return updateStatus()
|
|
}
|
|
|
|
|
|
function makeIndicator(text){
|
|
return $('<span class="indicator expanding-text">'+
|
|
'<span class="hidden">'+ text +'</span>'+
|
|
'<span class="shown">'+ text[0] +'</span>'+
|
|
'</span>')
|
|
}
|
|
|
|
function showGlobalIndicator(cls, text){
|
|
var c = $('.global-mode-indicators')
|
|
if(c.length == 0){
|
|
c = $('<div>')
|
|
.addClass('global-mode-indicators')
|
|
.append('<span class="mode-tip">Global status</span>')
|
|
.appendTo($('.viewer'))
|
|
}
|
|
return makeIndicator(text)
|
|
.addClass(cls)
|
|
.appendTo(c)
|
|
}
|
|
function showContextIndicator(cls, text){
|
|
var c = $('.context-mode-indicators')
|
|
if(c.length == 0){
|
|
c = $('<div>')
|
|
.addClass('context-mode-indicators')
|
|
.append('<span class="mode-tip">Context status</span>')
|
|
.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 = $('<div class="progress-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 = $('<span class="progress-details"/>')
|
|
var bar = $('<progress/>')
|
|
|
|
// the progress bar widget...
|
|
var widget = $('<div class="progress-bar" name="'+name+'">'+name+'</div>')
|
|
// progress state...
|
|
.append(state)
|
|
// the close button...
|
|
if(close !== false){
|
|
widget
|
|
.append($('<span class="close">×</span>')
|
|
.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: $('<details/>')
|
|
.append($('<summary/>')
|
|
.html(text))
|
|
.append($('<span/>')
|
|
.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: <b>'+ state +'</b>.', 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, '<br>')
|
|
var tags = data.tags
|
|
tags = tags == null ? '' : tags.join(', ')
|
|
|
|
return formDialog(null,
|
|
('<div>'+
|
|
'<h2>"'+ name +'"</h2>'+
|
|
|
|
'<table>'+
|
|
// basic info...
|
|
// XXX BUG: something here breaks when self-generated data is
|
|
// currently open -- .length of undefined...
|
|
'<tr><td colspan="2"><hr></td></tr>'+
|
|
'<tr><td>GID: </td><td>'+ gid +'</td></tr>'+
|
|
'<tr><td>Date: </td><td>'+ date +'</td></tr>'+
|
|
'<tr><td>Path: </td><td>"'+ unescape(data.path) +'"</td></tr>'+
|
|
'<tr><td>Orientation: </td><td>'+ orientation +'°'+flipped+'</td></tr>'+
|
|
'<tr><td>Order: </td><td>'+ order +'</td></tr>'+
|
|
'<tr><td>Position (ribbon): </td><td>'+ (DATA.ribbons[r].indexOf(gid)+1) +
|
|
'/'+ DATA.ribbons[r].length +'</td></tr>'+
|
|
'<tr><td>Position (global): </td><td>'+ (order+1) +'/'+ DATA.order.length +'</td></tr>'+
|
|
'<tr><td>Sorted: </td><td>'+
|
|
//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+'') +
|
|
'%</td></tr>'+
|
|
|
|
// editable fields...
|
|
'<tr><td colspan="2"><hr></td></tr>'+
|
|
// XXX this expanding to a too big size will mess up the screen...
|
|
// add per editable and global dialog max-height and overflow
|
|
'<tr><td>Comment: </td><td class="comment" contenteditable>'+ comment +'</td></tr>'+
|
|
'<tr><td>Tags: </td><td class="tags" contenteditable>'+ tags +'</td></tr>'+
|
|
'</table>'+
|
|
'<br>'+
|
|
'</div>'),
|
|
// 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(/<br>/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: <div class="comment" contenteditable/>'),
|
|
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 : */
|