workaround progression pipe bug
[booh] / bin / booh
old mode 100755 (executable)
new mode 100644 (file)
index 00ff918..5766217
--- a/bin/booh
+++ b/bin/booh
@@ -10,7 +10,7 @@
 # called Boo, so this one will be it "Booh". Or whatever.
 #
 #
-# Copyright (c) 2004-2008 Guillaume Cottenceau <http://zarb.org/~gc/resource/gc_mail.png>
+# Copyright (c) 2004-2013 Guillaume Cottenceau <http://zarb.org/~gc/resource/gc_mail.png>
 #
 # This software may be freely redistributed under the terms of the GNU
 # public license version 2.
 # along with this program; if not, write to the Free Software
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 
+begin
+    require 'rubygems'
+rescue LoadError
+end
+
 require 'getoptlong'
 require 'tempfile'
 require 'thread'
@@ -31,20 +36,19 @@ require 'gettext'
 include GetText
 bindtextdomain("booh")
 
-require 'booh/rexml/document'
+require 'rexml/document'
 include REXML
 
 require 'booh/booh-lib'
 include Booh
 require 'booh/UndoHandler'
-require 'booh/Synchronizator'
 
 
 #- options
 $options = [
-    [ '--help',          '-h', GetoptLong::NO_ARGUMENT,       _("Get help message") ],
-
+    [ '--help',          '-h', GetoptLong::NO_ARGUMENT, _("Get help message") ],
     [ '--verbose-level', '-v', GetoptLong::REQUIRED_ARGUMENT, _("Set max verbosity level (0: errors, 1: warnings, 2: important messages, 3: other messages)") ],
+    [ '--version',       '-V', GetoptLong::NO_ARGUMENT, _("Print version and exit") ],
 ]
 
 #- default values for some globals 
@@ -72,6 +76,15 @@ def handle_options
                 usage
                 exit(0)
 
+            when '--version'
+                puts _("Booh version %s
+
+Copyright (c) 2005-2013 Guillaume Cottenceau.
+This is free software; see the source for copying conditions.  There is NO
+warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.") % $VERSION
+
+                exit(0)
+
             when '--verbose-level'
                 $verbose_level = arg.to_i
 
@@ -84,42 +97,56 @@ def handle_options
     end
 end
 
+def count_cpus
+    cpus = 0
+    for line in IO.readlines('/proc/cpuinfo') do
+        line =~ /^processor/ and cpus += 1
+    end
+    return cpus
+end
+
 def read_config
     $config = {}
     $config_file = File.expand_path('~/.booh-gui-rc')
     if File.readable?($config_file)
-        $xmldoc = REXML::Document.new(File.new($config_file))
-        $xmldoc.root.elements.each { |element|
-            txt = element.get_text
-            if txt
-                if txt.value =~ /~~~/ || element.name == 'last-opens'
-                    $config[element.name] = txt.value.split(/~~~/)
+        begin
+            xmldoc = REXML::Document.new(File.new($config_file))
+        rescue
+            #- encoding unsupported anymore? file edited manually? ignore then
+            msg 1, "Ignoring #{$config_file}, failed to parse it: #{$!}"
+        end
+        if xmldoc
+            xmldoc.root.elements.each { |element|
+                txt = element.get_text
+                if txt
+                    if txt.value =~ /~~~/ || element.name == 'last-opens'
+                        $config[element.name] = txt.value.split(/~~~/)
+                    else
+                        $config[element.name] = txt.value
+                    end
+                elsif element.elements.size == 0
+                    $config[element.name] = ''
                 else
-                    $config[element.name] = txt.value
+                    $config[element.name] = {}
+                    element.each { |chld|
+                        txt = chld.get_text
+                        $config[element.name][chld.name] = txt ? txt.value : nil
+                    }
                 end
-            elsif element.elements.size == 0
-                $config[element.name] = ''
-            else
-                $config[element.name] = {}
-                element.each { |chld|
-                    txt = chld.get_text
-                    $config[element.name][chld.name] = txt ? txt.value : nil
-                }
-            end
-        }
+            }
+        end
     end
-    $config['video-viewer'] ||= '/usr/bin/mplayer %f'
-    $config['image-editor'] ||= '/usr/bin/gimp-remote %f'
-    $config['browser'] ||= "/usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f"
+    $config['video-viewer'] ||= '/usr/bin/mplayer %f || /usr/bin/vlc %f'
+    $config['image-editor'] ||= '/usr/bin/gimp-remote %f || /usr/bin/gimp %f'
+    $config['browser'] ||= "/usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f || /usr/bin/firefox -remote 'openURL(%f,new-window)' || /usr/bin/firefox %f"
+    $config['use-mp4'] ||= "true"
+    $config['mp4-generator'] ||= "/usr/bin/ffmpeg -i %f -b 800k -ar 22050 -ab 32k %o"
     $config['comments-format'] ||= '%t'
     if !FileTest.directory?(File.expand_path('~/.booh'))
         system("mkdir ~/.booh")
     end
     if $config['mproc'].nil?
-        cpus = 0
-        for line in IO.readlines('/proc/cpuinfo') do
-            line =~ /^processor/ and cpus += 1
-        end
+        cpus = count_cpus
         if cpus > 1
             $config['mproc'] = cpus
         end
@@ -129,6 +156,29 @@ def read_config
     $todelete = []
 end
 
+def check_config_preferences_dep
+    viewer_binary = $config['video-viewer'].split.first
+    if viewer_binary && !File.executable?(viewer_binary)
+        show_popup($main_window, utf8(_("The configured video viewer seems to be unavailable.
+You should fix this in Edit/Preferences so that you can view videos.
+
+Problem was: '%s' is not an executable file.
+Hint: don't forget to specify the full path to the executable,
+e.g. '/usr/bin/mplayer' is correct but 'mplayer' only is not.") % viewer_binary), { :pos_centered => true, :not_transient => true })
+    end
+
+    mp4_generator_binary = $config['use-mp4'] == 'true' && $config['mp4-generator'].split.first
+    if mp4_generator_binary && !File.executable?(mp4_generator_binary)
+        show_popup($main_window, utf8(_("The configured .mp4 generator seems to be unavailable.
+You should fix this in Edit/Preferences so that you can have working
+embedded flash videos.
+
+Problem was: '%s' is not an executable file.
+Hint: don't forget to specify the full path to the executable,
+e.g. '/usr/bin/ffmpeg' is correct but 'ffmpeg' only is not.") % mp4_generator_binary), { :pos_centered => true, :not_transient => true })
+    end
+end
+
 def check_config
     if !system("which convert >/dev/null 2>/dev/null")
         show_popup($main_window, utf8(_("The program 'convert' is needed. Please install it.
@@ -147,30 +197,29 @@ It is generally available with the 'ImageMagick' software package.")), { :pos_ce
         show_popup($main_window, utf8(_("The following program(s) are needed to handle videos: '%s'. Videos will be ignored.") % missing.join(', ')), { :pos_centered => true })
     end
 
-    viewer_binary = $config['video-viewer'].split.first
-    if viewer_binary && !File.executable?(viewer_binary)
-        show_popup($main_window, utf8(_("The configured video viewer seems to be unavailable.
-You should fix this in Edit/Preferences so that you can view videos.
+    check_config_preferences_dep
 
-Problem was: '%s' is not an executable file.
-Hint: don't forget to specify the full path to the executable,
-e.g. '/usr/bin/mplayer' is correct but 'mplayer' only is not.") % viewer_binary), { :pos_centered => true, :not_transient => true })
+    cpus = count_cpus
+
+    if $config['cpus'] && cpus > $config['cpus'].to_i
+        show_popup($main_window, utf8(_("It seems you now have more CPUs available than last time booh was run.
+You should probably increase the amount of CPUs configured in Edit/Preferences,
+so that web-albums are generated as fast as possible on this computer.")), { :pos_centered => true, :not_transient => true })
     end
-    image_editor_binary = $config['image-editor'].split.first
-    if image_editor_binary && !File.executable?(image_editor_binary)
+    $config['cpus'] = cpus
+end
+
+def check_image_editor
+    if last_failed_binary = check_multi_binaries($config['image-editor'])
         show_popup($main_window, utf8(_("The configured image editor seems to be unavailable.
 You should fix this in Edit/Preferences so that you can edit photos externally.
 
 Problem was: '%s' is not an executable file.
 Hint: don't forget to specify the full path to the executable,
-e.g. '/usr/bin/gimp-remote' is correct but 'gimp-remote' only is not.") % image_editor_binary), { :pos_centered => true, :not_transient => true })
-    end
-    browser_binary = $config['browser'].split.first
-    if browser_binary && !File.executable?(browser_binary)
-        show_popup($main_window, utf8(_("The configured browser seems to be unavailable.
-You should fix this in Edit/Preferences so that you can open URLs.
-
-Problem was: '%s' is not an executable file.") % browser_binary), { :pos_centered => true, :not_transient => true })
+e.g. '/usr/bin/gimp-remote' is correct but 'gimp-remote' only is not.") % last_failed_binary), { :pos_centered => true, :not_transient => true })
+        return false
+    else
+        return true
     end
 end
 
@@ -179,11 +228,10 @@ def write_config
         $config['last-opens'] = $config['last-opens'][-10, 10]
     end
 
-    ios = File.open($config_file, "w")
-    $xmldoc = Document.new "<booh-gui-rc version='#{$VERSION}'/>"
-    $xmldoc << XMLDecl.new(XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET)
+    xmldoc = Document.new("<booh-gui-rc version='#{$VERSION}'/>")
+    xmldoc << XMLDecl.new(XMLDecl::DEFAULT_VERSION, $CURRENT_CHARSET)
     $config.each_pair { |key, value|
-        elem = $xmldoc.root.add_element key
+        elem = xmldoc.root.add_element key
         if value.is_a? Hash
             $config[key].each_pair { |subkey, subvalue|
                 subelem = elem.add_element subkey
@@ -199,7 +247,8 @@ def write_config
             end
         end
     }
-    $xmldoc.write(ios, 0)
+    ios = File.open($config_file, "w")
+    xmldoc.write(ios)
     ios.close
 
     $tempfiles.each { |f|
@@ -287,7 +336,13 @@ def view_element(filename, closures)
                 end
         end
     end
-    evt = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::Frame.new.add(Gtk::Image.new(dest_img)).set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
+    aspect = utf8(_("Aspect: unknown"))
+    size = get_image_size(from_utf8("#{$current_path}/#{filename}"))
+    if size
+        aspect = utf8(_("Aspect: %s") % sprintf("%1.3f", size[:x].to_f/size[:y]))
+    end
+    vbox = Gtk::VBox.new.add(Gtk::Image.new(dest_img)).add(Gtk::Label.new.set_markup("<i>#{aspect}</i>"))
+    evt = Gtk::EventBox.new.add(Gtk::Alignment.new(0.5, 0.5, 0, 0).add(Gtk::Frame.new.add(vbox).set_shadow_type(Gtk::SHADOW_ETCHED_OUT)))
     evt.signal_connect('button-press-event') { |this, event|
         if event.event_type == Gdk::Event::BUTTON_PRESS && event.button == 1
             $config['nogestures'] or $gesture_press = { :x => event.x, :y => event.y }
@@ -446,9 +501,9 @@ def update_shown_pixbuf(thumbnail_img, img, desired_x, desired_y)
         pixbuf = rotate_pixbuf(pixbuf, $modified_pixbufs[thumbnail_img][:angle_to_orig])
         msg 3, "sizes: #{pixbuf.width} #{pixbuf.height} - desired #{desired_x}x#{desired_x}"
         if pixbuf.height > desired_y
-            pixbuf = pixbuf.scale(pixbuf.width * (desired_y.to_f/pixbuf.height), desired_y, Gdk::Pixbuf::INTERP_BILINEAR)
+            pixbuf = pixbuf.scale(pixbuf.width * (desired_y.to_f/pixbuf.height), desired_y, :bilinear)
         elsif pixbuf.width < desired_x && pixbuf.height < desired_y
-            pixbuf = pixbuf.scale(desired_x, pixbuf.height * (desired_x.to_f/pixbuf.width), Gdk::Pixbuf::INTERP_BILINEAR)
+            pixbuf = pixbuf.scale(desired_x, pixbuf.height * (desired_x.to_f/pixbuf.width), :bilinear)
         end
     end
 
@@ -472,7 +527,8 @@ def rotate_real(angle, thumbnail_img, img, xmlelem, attributes_prefix, desired_x
     new_angle = (xmlelem.attributes["#{attributes_prefix}rotate"].to_i + angle) % 360
     xmlelem.add_attribute("#{attributes_prefix}rotate", new_angle.to_s)
 
-    if $config['rotate-set-exif'] == 'true'
+    #- change exif orientation if configured so (but forget in case of thumbnails caption)
+    if $config['rotate-set-exif'] == 'true' && xmlelem.attributes['filename']
         Exif.set_orientation(from_utf8($current_path + '/' + xmlelem.attributes['filename']), angle_to_exif_orientation(new_angle))
     end
 
@@ -502,33 +558,41 @@ end
 
 def color_swap(xmldir, attributes_prefix)
     $modified = true
-    if xmldir.attributes["#{attributes_prefix}color-swap"]
-        xmldir.delete_attribute("#{attributes_prefix}color-swap")
-    else
-        xmldir.add_attribute("#{attributes_prefix}color-swap", '1')
-    end
+    rexml_thread_protect {
+        if xmldir.attributes["#{attributes_prefix}color-swap"]
+            xmldir.delete_attribute("#{attributes_prefix}color-swap")
+        else
+            xmldir.add_attribute("#{attributes_prefix}color-swap", '1')
+        end
+    }
 end
 
 def enhance(xmldir, attributes_prefix)
     $modified = true
-    if xmldir.attributes["#{attributes_prefix}enhance"]
-        xmldir.delete_attribute("#{attributes_prefix}enhance")
-    else
-        xmldir.add_attribute("#{attributes_prefix}enhance", '1')
-    end
+    rexml_thread_protect {
+        if xmldir.attributes["#{attributes_prefix}enhance"]
+            xmldir.delete_attribute("#{attributes_prefix}enhance")
+        else
+            xmldir.add_attribute("#{attributes_prefix}enhance", '1')
+        end
+    }
 end
 
 def change_seektime(xmldir, attributes_prefix, value)
     $modified = true
-    xmldir.add_attribute("#{attributes_prefix}seektime", value)
+    rexml_thread_protect {
+        xmldir.add_attribute("#{attributes_prefix}seektime", value)
+    }
 end
 
 def ask_new_seektime(xmldir, attributes_prefix)
-    if xmldir
-        value = xmldir.attributes["#{attributes_prefix}seektime"]
-    else
-        value = ''
-    end
+    value = rexml_thread_protect {
+        if xmldir
+            xmldir.attributes["#{attributes_prefix}seektime"]
+        else
+            ''
+        end
+    }
 
     dialog = Gtk::Dialog.new(utf8(_("Change seek time")),
                              $main_window,
@@ -542,7 +606,7 @@ _("Please specify the <b>seek time</b> of the video, to take the thumbnail
 from, in seconds.
 "))
     dialog.vbox.add(lbl)
-    dialog.vbox.add(entry = Gtk::Entry.new.set_text(value))
+    dialog.vbox.add(entry = Gtk::Entry.new.set_text(value || ''))
     entry.signal_connect('key-press-event') { |w, event|
         if event.keyval == Gdk::Keyval::GDK_Return
             dialog.response(Gtk::Dialog::RESPONSE_OK)
@@ -573,19 +637,23 @@ end
 
 def change_pano_amount(xmldir, attributes_prefix, value)
     $modified = true
-    if value.nil?
-        xmldir.delete_attribute("#{attributes_prefix}pano-amount")
-    else
-        xmldir.add_attribute("#{attributes_prefix}pano-amount", value.to_s)
-    end
+    rexml_thread_protect {
+        if value.nil?
+            xmldir.delete_attribute("#{attributes_prefix}pano-amount")
+        else
+            xmldir.add_attribute("#{attributes_prefix}pano-amount", value.to_s)
+        end
+    }
 end
 
 def ask_new_pano_amount(xmldir, attributes_prefix)
-    if xmldir
-        value = xmldir.attributes["#{attributes_prefix}pano-amount"]
-    else
-        value = nil
-    end
+    value = rexml_thread_protect {
+        if xmldir
+            xmldir.attributes["#{attributes_prefix}pano-amount"]
+        else
+            nil
+        end
+    }
 
     dialog = Gtk::Dialog.new(utf8(_("Specify panorama amount")),
                              $main_window,
@@ -830,8 +898,15 @@ def gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
     end
 end
 
+$max_gen_thumbnail_threads = nil
+$current_gen_thumbnail_threads = 0
+$gen_thumbnail_monitor = Monitor.new
+
 def gen_real_thumbnail(type, origfile, destfile, xmldir, size, img, infotype)
-    Thread.new {
+    if $max_gen_thumbnail_threads.nil?
+        $max_gen_thumbnail_threads = 1 + $config['mproc'].to_i || 1
+    end
+    genproc = Proc.new { 
         push_mousecursor_wait
         gen_real_thumbnail_core(type, origfile, destfile, xmldir, size, infotype)
         gtk_thread_protect {
@@ -840,6 +915,25 @@ def gen_real_thumbnail(type, origfile, destfile, xmldir, size, img, infotype)
         }
         pop_mousecursor
     }
+    usethread = false
+    $gen_thumbnail_monitor.synchronize {
+        if $current_gen_thumbnail_threads < $max_gen_thumbnail_threads
+            $current_gen_thumbnail_threads += 1
+            usethread = true
+        end
+    }
+    if usethread
+        msg 3, "generate thumbnail from new thread"
+        Thread.new {
+            genproc.call
+            $gen_thumbnail_monitor.synchronize {
+                $current_gen_thumbnail_threads -= 1
+            }
+        }
+    else
+        msg 3, "generate thumbnail from current thread"
+        genproc.call
+    end
 end
 
 def popup_thumbnail_menu(event, optionals, fullpath, type, xmldir, attributes_prefix, possible_actions, closures)
@@ -984,8 +1078,8 @@ def popup_thumbnail_menu(event, optionals, fullpath, type, xmldir, attributes_pr
         end
     }
     if !possible_actions[:can_multiple] || $selected_elements.length == 0
-        menu.append(enhance = Gtk::ImageMenuItem.new(utf8(xmldir.attributes["#{attributes_prefix}enhance"] ? _("Original contrast") :
-                                                                                                             _("Enhance constrast"))))
+        menu.append(enhance = Gtk::ImageMenuItem.new(utf8(rexml_thread_protect { xmldir.attributes["#{attributes_prefix}enhance"] } ? _("Original contrast") :
+                                                                                                                                      _("Enhance constrast"))))
     else
         menu.append(enhance = Gtk::ImageMenuItem.new(utf8(_("Toggle contrast enhancement"))))
     end
@@ -1023,9 +1117,11 @@ def popup_thumbnail_menu(event, optionals, fullpath, type, xmldir, attributes_pr
         menu.append(editexternally = Gtk::ImageMenuItem.new(utf8(_("Edit image"))))
         editexternally.image = Gtk::Image.new("#{$FPATH}/images/stock-tool-ink-16.png")
         editexternally.signal_connect('activate') {
-            cmd = from_utf8($config['image-editor']).gsub('%f', "'#{fullpath}'")
-            msg 2, cmd
-            system(cmd)
+            if check_image_editor
+                cmd = from_utf8($config['image-editor']).gsub('%f', "'#{fullpath}'")
+                msg 2, cmd
+                system(cmd)
+            end
         }
     end
     menu.append(refresh_item = Gtk::ImageMenuItem.new(Gtk::Stock::REFRESH))
@@ -1097,7 +1193,7 @@ def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
     }
 
     if type == 'video'
-        pxb = Gdk::Pixbuf.new("#{$FPATH}/images/video_border.png")
+        pxb = GdkPixbuf::Pixbuf.new(:file => "#{$FPATH}/images/video_border.png")
         frame1.add(Gtk::HBox.new.pack_start(da1 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false).
                                  pack_start(img = Gtk::Image.new).
                                  pack_start(da2 = Gtk::DrawingArea.new.set_size_request(pxb.width, -1), false, false))
@@ -1152,13 +1248,17 @@ def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
         cleanup_all_thumbnails.call
         #- refresh is not undoable and doesn't change the album, however we must regenerate all thumbnails when generating the album
         $modified = true
-        $xmldir.delete_attribute('already-generated')
+        rexml_thread_protect {
+            $xmldir.delete_attribute('already-generated')
+        }
         my_gen_real_thumbnail.call
     }
  
     rotate_and_cleanup = proc { |angle|
         cleanup_all_thumbnails.call
-        rotate(angle, thumbnail_img, img, $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
+        rexml_thread_protect {
+            rotate(angle, thumbnail_img, img, $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y])
+        }
     }
 
     move = proc { |direction|
@@ -1189,7 +1289,9 @@ def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
     color_swap_and_cleanup = proc {
         perform_color_swap_and_cleanup = proc {
             cleanup_all_thumbnails.call
-            color_swap($xmldir.elements["*[@filename='#{filename}']"], '')
+            rexml_thread_protect {
+                color_swap($xmldir.elements["*[@filename='#{filename}']"], '')
+            }
             my_gen_real_thumbnail.call
         }
 
@@ -1213,7 +1315,9 @@ def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
     change_seektime_and_cleanup_real = proc { |values|
         perform_change_seektime_and_cleanup = proc { |val|
             cleanup_all_thumbnails.call
-            change_seektime($xmldir.elements["*[@filename='#{filename}']"], '', val)
+            rexml_thread_protect {
+                change_seektime($xmldir.elements["*[@filename='#{filename}']"], '', val)
+            }
             my_gen_real_thumbnail.call
         }
         perform_change_seektime_and_cleanup.call(values[:new])
@@ -1234,15 +1338,19 @@ def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
     }
 
     change_seektime_and_cleanup = proc {
-        if values = ask_new_seektime($xmldir.elements["*[@filename='#{filename}']"], '')
-            change_seektime_and_cleanup_real.call(values)
-        end
+        rexml_thread_protect {
+            if values = ask_new_seektime($xmldir.elements["*[@filename='#{filename}']"], '')
+                change_seektime_and_cleanup_real.call(values)
+            end
+        }
     }
 
     change_pano_amount_and_cleanup_real = proc { |values|
         perform_change_pano_amount_and_cleanup = proc { |val|
             cleanup_all_thumbnails.call
-            change_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '', val)
+            rexml_thread_protect {
+                change_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '', val)
+            }
         }
         perform_change_pano_amount_and_cleanup.call(values[:new])
         
@@ -1262,17 +1370,21 @@ def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
     }
 
     change_pano_amount_and_cleanup = proc {
-        if values = ask_new_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '')
-            change_pano_amount_and_cleanup_real.call(values)
-        end
+        rexml_thread_protect {
+            if values = ask_new_pano_amount($xmldir.elements["*[@filename='#{filename}']"], '')
+                change_pano_amount_and_cleanup_real.call(values)
+            end
+        }
     }
 
     whitebalance_and_cleanup_real = proc { |values|
         perform_change_whitebalance_and_cleanup = proc { |val|
             cleanup_all_thumbnails.call
-            change_whitebalance($xmldir.elements["*[@filename='#{filename}']"], '', val)
-            recalc_whitebalance(val, fullpath, thumbnail_img, img,
-                                $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
+            rexml_thread_protect {
+                change_whitebalance($xmldir.elements["*[@filename='#{filename}']"], '', val)
+                recalc_whitebalance(val, fullpath, thumbnail_img, img,
+                                    $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
+            }
         }
         perform_change_whitebalance_and_cleanup.call(values[:new])
 
@@ -1292,18 +1404,22 @@ def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
     }
 
     whitebalance_and_cleanup = proc {
-        if values = ask_whitebalance(fullpath, thumbnail_img, img,
-                                     $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
-            whitebalance_and_cleanup_real.call(values)
-        end
+        rexml_thread_protect {
+            if values = ask_whitebalance(fullpath, thumbnail_img, img,
+                                         $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
+                whitebalance_and_cleanup_real.call(values)
+            end
+        }
     }
 
     gammacorrect_and_cleanup_real = proc { |values|
         perform_change_gammacorrect_and_cleanup = Proc.new { |val|
             cleanup_all_thumbnails.call
-            change_gammacorrect($xmldir.elements["*[@filename='#{filename}']"], '', val)
-            recalc_gammacorrect(val, fullpath, thumbnail_img, img,
-                                $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
+            rexml_thread_protect {
+                change_gammacorrect($xmldir.elements["*[@filename='#{filename}']"], '', val)
+                recalc_gammacorrect(val, fullpath, thumbnail_img, img,
+                                    $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
+            }
         }
         perform_change_gammacorrect_and_cleanup.call(values[:new])
         
@@ -1323,16 +1439,20 @@ def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
     }
     
     gammacorrect_and_cleanup = Proc.new {
-        if values = ask_gammacorrect(fullpath, thumbnail_img, img,
-                                     $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
-            gammacorrect_and_cleanup_real.call(values)
-        end
+        rexml_thread_protect {
+            if values = ask_gammacorrect(fullpath, thumbnail_img, img,
+                                         $xmldir.elements["*[@filename='#{filename}']"], '', $default_thumbnails[:x], $default_thumbnails[:y], '')
+                gammacorrect_and_cleanup_real.call(values)
+            end
+        }
     }
     
     enhance_and_cleanup = proc {
         perform_enhance_and_cleanup = proc {
             cleanup_all_thumbnails.call
-            enhance($xmldir.elements["*[@filename='#{filename}']"], '')
+            rexml_thread_protect {
+                enhance($xmldir.elements["*[@filename='#{filename}']"], '')
+            }
             my_gen_real_thumbnail.call
         }
         
@@ -1544,7 +1664,7 @@ def add_thumbnail(autotable, filename, type, thumbnail_img, caption)
             if !$ignore_next_release
                 x, y = autotable.get_current_pos(vbox)
                 next_ = autotable.get_next_widget(vbox)
-                popup_thumbnail_menu(event, ['delete'], fullpath, type, $xmldir.elements["*[@filename='#{filename}']"], '',
+                popup_thumbnail_menu(event, ['delete'], fullpath, type, rexml_thread_protect { $xmldir.elements["*[@filename='#{filename}']"] }, '',
                                      { :can_left => x > 0, :can_right => next_ && autotable.get_current_pos(next_)[0] > x,
                                        :can_up => y > 0, :can_down => y < autotable.get_max_y, :can_multiple => true, :can_panorama => true },
                                      { :rotate => rotate_and_cleanup, :move => move, :color_swap => color_swap_and_cleanup, :enhance => enhance_and_cleanup,
@@ -1652,7 +1772,9 @@ def create_auto_table
             if ym < press_y && ym > pos_y || ym < pos_y && ym > press_y
                 if (xm < press_x && xm > pos_x || xm < pos_x && xm > press_x) && ! $selected_elements[path]
                     $selected_elements[path] = { :pixbuf => $name2widgets[path][:img].pixbuf }
-                    $name2widgets[path][:img].pixbuf = $name2widgets[path][:img].pixbuf.saturate_and_pixelate(1, true)
+                    if $name2widgets[path][:img].pixbuf
+                        $name2widgets[path][:img].pixbuf = $name2widgets[path][:img].pixbuf.saturate_and_pixelate(1, true)
+                    end
                 end
             end
             if $selected_elements[path] && ! $selected_elements[path][:keep]
@@ -1771,7 +1893,7 @@ def save_current_file
         begin
             begin
                 ios = File.open($filename, "w")
-                $xmldoc.write(ios, 0)
+                $xmldoc.write(ios)
                 ios.close
             rescue Iconv::IllegalSequence
                 #- user might have entered text which cannot be encoded in his default encoding. retry in UTF-8.
@@ -1780,7 +1902,7 @@ def save_current_file
                 end
                 $xmldoc.xml_decl.encoding = 'UTF-8'
                 ios = File.open($filename, "w")
-                $xmldoc.write(ios, 0)
+                $xmldoc.write(ios)
                 ios.close
             end
             return true
@@ -1805,8 +1927,13 @@ def save_current_file_user
 
     msg 3, "performing actual deletion of: " + $todelete.join(', ')
     $todelete.each { |f|
-        File.delete(f)
+        begin
+            File.delete(f)
+        rescue
+            puts "Failed to delete #{f}: #{$!}"
+        end
     }
+    $todelete = []
 end
 
 def mark_document_as_dirty
@@ -1850,10 +1977,10 @@ def ask_save_modifications(msg1, msg2, *options)
                 #- already-generated markers in original file
                 if $generated_outofline
                     begin
-                        $xmldoc = REXML::Document.new File.new($orig_filename)
+                        $xmldoc = REXML::Document.new(File.new($orig_filename))
                         mark_document_as_dirty
                         ios = File.open($orig_filename, "w")
-                        $xmldoc.write(ios, 0)
+                        $xmldoc.write(ios)
                         ios.close
                     rescue Exception
                         puts "exception: #{$!}"
@@ -1863,7 +1990,6 @@ def ask_save_modifications(msg1, msg2, *options)
             if response == Gtk::Dialog::RESPONSE_CANCEL
                 ret = false
             end
-            $todelete = []  #- unconditionally clear the list of images/videos to delete
         }
     end
     return ret
@@ -1896,9 +2022,6 @@ def show_popup(parent, msg, *options)
     if options[0] && options[0][:selectable]
         lbl.selectable = true
     end
-    if options[0] && options[0][:topwidget]
-        dialog.vbox.add(options[0][:topwidget])
-    end
     if options[0] && options[0][:scrolled]
         sw = Gtk::ScrolledWindow.new(nil, nil)
         sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
@@ -2003,7 +2126,10 @@ def backend_wait_message(parent, msg, infopipe_path, mode)
     infopipe = File.open(infopipe_path, File::RDONLY | File::NONBLOCK)
     refresh_thread = Thread.new {
         directories_counter = 0
+        #- immediately stops if trying to read before file is written from backend.. simple dirty solution for the moment
+        sleep 1 
         while line = infopipe.gets
+            msg 3, "infopipe got data: #{line}"
             if line =~ /^directories: (\d+), sizes: (\d+)/
                 directories = $1.to_f + 1
                 sizes = $2.to_f
@@ -2076,10 +2202,11 @@ end
 
 def call_backend(cmd, waitmsg, mode, params)
     pipe = Tempfile.new("boohpipe")
+    path = pipe.path
     pipe.close!
-    system("mkfifo #{pipe.path}")
-    cmd += " --info-pipe #{pipe.path}"
-    button, w8 = backend_wait_message($main_window, waitmsg, pipe.path, mode)
+    system("mkfifo #{path}")
+    cmd += " --info-pipe #{path}"
+    button, w8 = backend_wait_message($main_window, waitmsg, path, mode)
     pid = nil
     Thread.new {
         msg 2, cmd
@@ -2097,7 +2224,9 @@ def call_backend(cmd, waitmsg, mode, params)
                 #- say nothing, user aborted
             else
                 gtk_thread_protect { show_popup($main_window,
-                                                utf8(_("There was something wrong, sorry:\n\n%s") % $diemsg)) }
+                                                utf8($diemsg ? _("Unexpected internal error, sorry:\n\n%s") % $diemsg :
+                                                               _("Unexpected internal error, sorry.\nCheck console for error message."))) }
+                $diemsg = nil
             end
         else
             exec(cmd)
@@ -2194,10 +2323,12 @@ def sort_by_exif_date
     $modified = true
     save_changes
     current_order = []
-    $xmldir.elements.each { |element|
-        if element.name == 'image' || element.name == 'video'
-            current_order << element.attributes['filename']
-        end
+    rexml_thread_protect {
+        $xmldir.elements.each { |element|
+            if element.name == 'image' || element.name == 'video'
+                current_order << element.attributes['filename']
+            end
+        }
     }
 
     #- look for EXIF dates
@@ -2230,6 +2361,8 @@ def sort_by_exif_date
                 date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
                 if ! date_time.nil?
                     dates[f] = date_time
+                elsif f =~ /(20\d{2}).?(\d{2}).?(\d{2}).(\d{2}).?(\d{2}).?(\d{2})/
+                    dates[f] = "#$1:#$2:#$3 #$4:#$5:#$6"
                 end
             end
             if aborted
@@ -2246,21 +2379,27 @@ def sort_by_exif_date
             date_time = Exif.datetimeoriginal(from_utf8($current_path + "/" + f))
             if ! date_time.nil?
                 dates[f] = date_time
+            elsif f =~ /(20\d{2}).?(\d{2}).?(\d{2}).(\d{2}).?(\d{2}).?(\d{2})/
+                dates[f] = "#$1:#$2:#$3 #$4:#$5:#$6"
             end
         }
     end
 
     saves = {}
-    $xmldir.elements.each { |element|
-        if element.name == 'image' || element.name == 'video'
-            saves[element.attributes['filename']] = element.remove
-        end
+    rexml_thread_protect {
+        $xmldir.elements.each { |element|
+            if element.name == 'image' || element.name == 'video'
+                saves[element.attributes['filename']] = element.remove
+            end
+        }
     }
 
     neworder = smartsort(current_order, dates)
 
-    neworder.each { |f|
-        $xmldir.add_element(saves[f].name, saves[f].attributes)
+    rexml_thread_protect {
+        neworder.each { |f|
+            $xmldir.add_element(saves[f].name, saves[f].attributes)
+        }
     }
 
     #- let the auto-table reflect new ordering
@@ -2314,7 +2453,7 @@ def change_dir
     $subalbums = Gtk::Table.new(0, 0, true)
     current_y_sub_albums = 0
 
-    $xmldir = Synchronizator.new($xmldoc.elements["//dir[@path='#{$current_path}']"])
+    $xmldir = $xmldoc.elements["//dir[@path='#{$current_path}']"]
     $subalbums_edits = {}
     subalbums_counter = 0
     subalbums_edits_bypos = {}
@@ -2370,13 +2509,36 @@ def change_dir
             f.add(preview_img = Gtk::Image.new)
             preview.show_all
             fc.signal_connect('update-preview') { |w|
-                begin
-                    if fc.preview_filename
-                        preview_img.pixbuf = rotate_pixbuf(Gdk::Pixbuf.new(fc.preview_filename, 240, 180), guess_rotate(fc.preview_filename))
-                        fc.preview_widget_active = true
+                if fc.preview_filename
+                    if entry2type(fc.preview_filename) == 'video'
+                        image_path = nil
+                        tmpdir = nil
+                        begin
+                            tmpdir = gen_video_thumbnail(fc.preview_filename, false, 0)
+                            if tmpdir.nil?
+                                fc.preview_widget_active = false
+                            else
+                                tmpimage = "#{tmpdir}/00000001.jpg"
+                                begin
+                                    preview_img.pixbuf = GdkPixbuf::Pixbuf.new(:file => tmpimage, :width => 240,
+                                                                               :height => 180)
+                                    fc.preview_widget_active = true
+                                rescue Gdk::PixbufError
+                                    fc.preview_widget_active = false
+                                ensure
+                                    File.delete(tmpimage)
+                                    Dir.rmdir(tmpdir)
+                                end
+                            end
+                        end
+                    else
+                        begin
+                            preview_img.pixbuf = rotate_pixbuf(GdkPixbuf::Pixbuf.new(:file => fc.preview_filename, :width => 240, :height => 180), guess_rotate(fc.preview_filename))
+                            fc.preview_widget_active = true
+                        rescue Gdk::PixbufError
+                            fc.preview_widget_active = false
+                        end
                     end
-                rescue Gdk::PixbufError
-                    fc.preview_widget_active = false
                 end
             }
             if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
@@ -2633,7 +2795,7 @@ def change_dir
     if $xmldir.child_byname_notattr('dir', 'deleted')
         #- title edition
         frame, $subalbums_title = create_editzone($subalbums_sw, 0, nil)
-        $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption']
+        $subalbums_title.buffer.text = $xmldir.attributes['subdirs-caption'] || ''
         $subalbums_title.set_justification(Gtk::Justification::CENTER)
         $subalbums_vb.pack_start(Gtk::Alignment.new(0.5, 0.5, 0.5, 0).add(frame), false, false)
         #- this album image/caption
@@ -2680,7 +2842,7 @@ end
 
 def pixbuf_or_nil(filename)
     begin
-        return Gdk::Pixbuf.new(filename)
+        return GdkPixbuf::Pixbuf.new(:file => filename)
     rescue
         return nil
     end
@@ -2706,9 +2868,9 @@ def theme_choose(current)
         end
     }
 
-    dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC))
+    dialog.vbox.add(sw = Gtk::ScrolledWindow.new(nil, nil).add(treeview).set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC))
 
-    `find '#{$FPATH}/themes' -mindepth 1 -maxdepth 1 -type d`.each { |dir|
+    ([ $FPATH + '/themes/gradient' ] + (`find '#{$FPATH}/themes' ~/.booh-themes -mindepth 1 -maxdepth 1 -type d 2>/dev/null`.split("\n").find_all { |e| e !~ /\bgradient\b/ }.sort)).each { |dir|
         dir.chomp!
         iter = model.append
         iter[0] = File.basename(dir)
@@ -2719,9 +2881,9 @@ def theme_choose(current)
             treeview.selection.select_iter(iter)
         end
     }
-
-    dialog.set_default_size(700, 400)
+    dialog.set_default_size(-1, 500)
     dialog.vbox.show_all
+
     dialog.run { |response|
         iter = treeview.selection.selected
         dialog.destroy
@@ -2818,7 +2980,7 @@ def open_file(filename)
     end
 
     begin
-        $xmldoc = REXML::Document.new File.new(filename)
+        $xmldoc = REXML::Document.new(File.new(filename))
     rescue Exception
         $xmldoc = nil
     end
@@ -2843,12 +3005,12 @@ def open_file(filename)
         return utf8(_("Corrupted booh file..."))
     end
 
-    if $xmldoc.root.attributes['version'] < '0.8.99.2'
+    if $xmldoc.root.attributes['version'] < $VERSION
         msg 2, _("File's version %s, booh version now %s, marking dirty") % [ $xmldoc.root.attributes['version'], $VERSION ]
         mark_document_as_dirty
         if $xmldoc.root.attributes['version'] < '0.8.4'
             msg 1, _("File's version prior to 0.8.4, migrating directories and filenames in destination directory if needed")
-            `find '#{source}' -type d -follow`.sort.collect { |v| v.chomp }.each { |dir|
+            `find '#{source}' -type d -follow`.split("\n").sort.collect { |v| v.chomp }.each { |dir|
                 old_dest_dir = make_dest_filename_old(dir.sub(/^#{Regexp.quote(source)}/, dest))
                 new_dest_dir = make_dest_filename(dir.sub(/^#{Regexp.quote(source)}/, dest))
                 if old_dest_dir != new_dest_dir
@@ -2889,7 +3051,7 @@ def open_file(filename)
 
     populate_subalbums_treeview(true)
 
-    $save.sensitive = $save_as.sensitive = $merge_current.sensitive = $merge_newsubs.sensitive = $merge.sensitive = $generate.sensitive = $view_wa.sensitive = $properties.sensitive = $remove_all_captions.sensitive = $sort_by_exif_date.sensitive = true
+    $save.sensitive = $save_as.sensitive = $merge_current.sensitive = $merge_newsubs.sensitive = $merge.sensitive = $extend.sensitive = $generate.sensitive = $view_wa.sensitive = $upload.sensitive = $properties.sensitive = $remove_all_captions.sensitive = $sort_by_exif_date.sensitive = true
     return nil
 end
 
@@ -2901,10 +3063,12 @@ def open_file_user(filename)
             $config['last-opens'] << utf8(filename)
         end
         $orig_filename = $filename
+        $main_window.title = 'booh - ' + File.basename($orig_filename)
         tmp = Tempfile.new("boohtemp")
+        $filename = tmp.path
         tmp.close!
         #- for security
-        ios = File.open($filename = tmp.path, File::RDWR|File::CREAT|File::EXCL)
+        ios = File.open($filename, File::RDWR|File::CREAT|File::EXCL)
         ios.close
         $tempfiles << $filename << "#{$filename}.backup"
     else
@@ -2927,9 +3091,40 @@ def open_file_popup
     fc.add_shortcut_folder(File.expand_path("~/.booh"))
     fc.set_current_folder(File.expand_path("~/.booh"))
     fc.transient_for = $main_window
+    fc.preview_widget = previewlabel = Gtk::Label.new.show
+    fc.signal_connect('update-preview') { |w|
+        if fc.preview_filename
+            begin
+                push_mousecursor_wait(fc)
+                xmldoc = REXML::Document.new(File.new(fc.preview_filename))
+                subalbums = 0
+                images = 0
+                videos = 0
+                xmldoc.elements.each('//*') { |elem|
+                    if elem.name == 'dir'
+                        subalbums += 1
+                    elsif elem.name == 'image'
+                        images += 1
+                    elsif elem.name == 'video'
+                        videos += 1
+                    end
+                }
+            rescue Exception
+            ensure
+                pop_mousecursor(fc)
+            end
+            if !xmldoc || !xmldoc.root || xmldoc.root.name != 'booh'
+                fc.preview_widget_active = false
+            else
+                previewlabel.markup = utf8(_("<i>Source:</i> %s\n<i>Destination:</i> %s\n<i>Subalbums:</i> %s\n<i>Images:</i> %s\n<i>Videos:</i> %s") %
+                                           [ xmldoc.root.attributes['source'], xmldoc.root.attributes['destination'], subalbums, images, videos ])
+                fc.preview_widget_active = true
+            end
+        end
+    }
     ok = false
     while !ok
-        if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
+        if fc.run == Gtk::Dialog::RESPONSE_ACCEPT && fc.filename
             push_mousecursor_wait(fc)
             msg = open_file_user(fc.filename)
             pop_mousecursor(fc)
@@ -2955,6 +3150,9 @@ def additional_booh_options
     if $config['transcode-videos']
         options += "--transcode-videos '#{$config['transcode-videos']}' "
     end
+    if $config['use-mp4'] == 'true'
+        options += "--mp4-generator '#{$config['mp4-generator']}' "
+    end
     return options
 end
 
@@ -3030,7 +3228,7 @@ navigation in their language (if language is available).
                 value = []
                 value[0] = cbs.find_all { |e| e[1].active? }.collect { |e| e[0] }
                 value[1] = fallback_language
-                languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| utf8(langname(v)) }.join(', '), utf8(langname(value[1])) ])
+                languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| langname(v) }.join(', '), langname(value[1]) ])
             end
             dialog2.destroy
         }
@@ -3045,7 +3243,7 @@ navigation in their language (if language is available).
         rb_no.active = true
     else
         rb_yes.active = true
-        languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| utf8(langname(v)) }.join(', '), utf8(langname(value[1])) ])
+        languages.label = utf8(_("Languages: %s. Fallback: %s.") % [ value[0].collect { |v| langname(v) }.join(', '), langname(value[1]) ])
     end
     rb_no.signal_connect('clicked') {
         if rb_no.active?
@@ -3120,7 +3318,7 @@ def new_album
     tooltips = Gtk::Tooltips.new
     frame2 = Gtk::Frame.new(utf8(_("Configuration"))).add(vb = Gtk::VBox.new)
     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Theme: "))), false, false, 0).
-                         pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'simple'), false, false, 0))
+                         pack_start(theme_button = Gtk::Button.new($config['default-theme'] || 'gradient'), false, false, 0))
     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("Sizes of images to generate: "))), false, false, 0).
                                    pack_start(sizes = Gtk::HBox.new, false, false, 0))
     vb.add(optimize432 = Gtk::CheckButton.new(utf8(_("Optimize for 3/2 aspect ratio"))).set_active($config['default-optimize32'].to_b))
@@ -3169,39 +3367,69 @@ def new_album
     vb.add(Gtk::HBox.new(false, 3).pack_start(Gtk::Label.new(utf8(_("'Made with' markup on pages bottom: "))), false, false, 0).
                                    pack_start(madewithentry = Gtk::Entry.new.set_text(utf8(_("made with <a href=%booh>booh</a>!"))), true, true, 0))
     tooltips.set_tip(madewithentry, utf8(_("Optional HTML markup to use on pages bottom for a small 'made with' label; %booh is replaced by the website of booh;\nfor example: made with <a href=%booh>booh</a>!")), nil)
+    vb.add(addthis = Gtk::CheckButton.new(utf8(_("Include the 'addthis' bookmarking and sharing button"))).set_active($config['default-addthis'].to_b))
+    vb.add(quotehtml = Gtk::CheckButton.new(utf8(_("Quote HTML markup in captions"))).set_active($config['default-quotehtml'].to_b))
+    tooltips.set_tip(quotehtml, utf8(_("If checked, text using markup special characters such as '<grin>' will be shown properly; if unchecked, markup such as '<a href..' links will be interpreted by the browser properly")), nil)
 
     src_nb_calculated_for = ''
-    src_nb_thread = nil
+    src_nb_process = nil
     process_src_nb = proc {
         if src.text != src_nb_calculated_for
             src_nb_calculated_for = src.text
-            if src_nb_thread
-                Thread.kill(src_nb_thread)
-                src_nb_thread = nil
+            if src_nb_process
+                begin
+                    Process.kill(9, src_nb_process)
+                rescue Errno::ESRCH
+                    #- process doesn't exist anymore - race condition
+                end
             end
             if src_nb_calculated_for != '' && from_utf8_safe(src_nb_calculated_for) == ''
                 src_nb.set_markup(utf8(_("<span size='small'><i>invalid source directory</i></span>")))
             else
                 if File.directory?(from_utf8_safe(src_nb_calculated_for)) && src_nb_calculated_for != '/'
                     if File.readable?(from_utf8_safe(src_nb_calculated_for))
-                        src_nb_thread = Thread.new {
-                            gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>"))) }
-                            total = { 'image' => 0, 'video' => 0, nil => 0 }
-                            `find '#{from_utf8_safe(src_nb_calculated_for)}' -type d -follow`.each { |dir|
-                                if File.basename(dir) =~ /^\./
-                                    next
-                                else
-                                    begin
-                                        Dir.entries(dir.chomp).each { |file|
-                                            total[entry2type(file)] += 1
-                                        }
-                                    rescue Errno::EACCES, Errno::ENOENT
+                        rd, wr = IO.pipe
+                        if src_nb_process
+                            while src_nb_process
+                                msg 3, "sleeping for completion of previous process"
+                                sleep 0.05
+                            end
+                            gtk_thread_flush  #- flush to avoid race condition in src_nb markup update
+                        end
+                        src_nb.set_markup(utf8(_("<span size='small'><i>processing...</i></span>")))
+                        total = { 'image' => 0, 'video' => 0, nil => 0 }
+                        if src_nb_process = fork
+                            msg 3, "spawned #{src_nb_process} for #{src_nb_calculated_for}"
+                            #- parent
+                            wr.close
+                            Thread.new {
+                                rd.readlines.each { |dir|
+                                    if File.basename(dir) =~ /^\./
+                                        next
+                                    else
+                                        begin
+                                            Dir.entries(dir.chomp).each { |file|
+                                                total[entry2type(file)] += 1
+                                            }
+                                        rescue Errno::EACCES, Errno::ENOENT
+                                        end
                                     end
+                                }
+                                rd.close
+                                msg 3, "ripping #{src_nb_process}"
+                                dummy, exitstatus = Process.waitpid2(src_nb_process)
+                                if exitstatus == 0
+                                    gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s photos and %s videos</i></span>") % [ total['image'], total['video'] ])) }
                                 end
+                                src_nb_process = nil
                             }
-                            gtk_thread_protect { src_nb.set_markup(utf8(_("<span size='small'><i>%s photos and %s videos</i></span>") % [ total['image'], total['video'] ])) }
-                            src_nb_thread = nil
-                        }
+                            
+                        else
+                            #- child
+                            rd.close
+                            wr.write(`find '#{from_utf8_safe(src_nb_calculated_for)}' -type d -follow`)
+                            Process.exit!(0)  #- _exit
+                        end                       
                     else
                         src_nb.set_markup(utf8(_("<span size='small'><i>permission denied</i></span>")))
                     end
@@ -3223,7 +3451,7 @@ def new_album
                                         nil,
                                         [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
         fc.transient_for = $main_window
-        if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
+        if fc.run == Gtk::Dialog::RESPONSE_ACCEPT && fc.filename
             src.text = utf8(fc.filename)
             process_src_nb.call
             conf.text = File.expand_path("~/.booh/#{File.basename(src.text)}")
@@ -3238,7 +3466,7 @@ def new_album
                                         nil,
                                         [Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT], [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
         fc.transient_for = $main_window
-        if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
+        if fc.run == Gtk::Dialog::RESPONSE_ACCEPT && fc.filename
             dest.text = utf8(fc.filename)
         end
         fc.destroy
@@ -3253,7 +3481,7 @@ def new_album
         fc.transient_for = $main_window
         fc.add_shortcut_folder(File.expand_path("~/.booh"))
         fc.set_current_folder(File.expand_path("~/.booh"))
-        if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
+        if fc.run == Gtk::Dialog::RESPONSE_ACCEPT && fc.filename
             conf.text = utf8(fc.filename)
         end
         fc.destroy
@@ -3371,15 +3599,27 @@ Are you sure you want to continue?")), { :okcancel => true })
         $config['default-theme'] = theme
         $config['default-multi-languages'] = multilanguages_value
         $config['default-optimize32'] = optimize432.active?.to_s
+        $config['default-addthis'] = addthis.active?.to_s
+        $config['default-quotehtml'] = quotehtml.active?.to_s
         sizes = theme_sizes.find_all { |e| e[:widget].active? }.collect { |e| e[:value] }.join(',')
         nperrow = nperrows.find { |e| e[:widget].active? }[:value]
         nperpage = nperpage_model.get_value(nperpagecombo.active_iter, 1)
         opt432 = optimize432.active?
         madewith = madewithentry.text.gsub('\'', '&#39;')  #- because the parameters to booh-backend are between apostrophes
         indexlink = indexlinkentry.text.gsub('\'', '&#39;')
+        athis = addthis.active?
+        qhtml = quotehtml.active?
     end
-    if src_nb_thread
-        Thread.kill(src_nb_thread)
+    if src_nb_process
+        begin
+            Process.kill(9, src_nb_process)
+            while src_nb_process
+                msg 3, "sleeping for completion of previous process"
+                sleep 0.05
+            end
+        rescue Errno::ESRCH
+            #- process doesn't exist
+        end
         gtk_thread_flush  #- needed because we're about to destroy widgets in dialog, for which they may be some pending gtk calls
     end
     dialog.destroy
@@ -3390,10 +3630,14 @@ Are you sure you want to continue?")), { :okcancel => true })
                      "--verbose-level #{$verbose_level} --theme #{theme} --sizes #{sizes} --thumbnails-per-row #{nperrow} " +
                      (nperpage ? "--thumbnails-per-page #{nperpage} " : '') +
                      (multilanguages_value ? "--multi-languages #{multilanguages_value} " : '') +
-                     "#{opt432 ? '--optimize-for-32' : ''} --made-with '#{madewith}' --index-link '#{indexlink}' #{additional_booh_options}",
+                     "#{opt432 ? '--optimize-for-32' : ''} --made-with '#{madewith}' --index-link '#{indexlink}' " +
+                     "#{athis ? '--addthis' : ''} #{qhtml ? '--quote-html' : ''} #{additional_booh_options}",
                      utf8(_("Please wait while scanning source directory...")),
                      'full scan',
-                     { :closure_after => proc { open_file_user(configskel) } })
+                     { :closure_after => proc {
+                             open_file_user(configskel)
+                             $main_window.urgency_hint = true
+                         } })
     end
 end
 
@@ -3416,6 +3660,8 @@ def properties
     end
     madewith = ($xmldoc.root.attributes['made-with'] || '').gsub('&#39;', '\'')
     indexlink = ($xmldoc.root.attributes['index-link'] || '').gsub('&#39;', '\'')
+    athis = !$xmldoc.root.attributes['addthis'].nil?
+    qhtml = !$xmldoc.root.attributes['quote-html'].nil?
     save_multilanguages_value = multilanguages_value = $xmldoc.root.attributes['multi-languages']
 
     tooltips = Gtk::Tooltips.new
@@ -3492,6 +3738,9 @@ def properties
         madewithentry.text = madewith
     end
     tooltips.set_tip(madewithentry, utf8(_('Optional HTML markup to use on pages bottom for a small \'made with\' label; %booh is replaced by the website of booh;\nfor example: made with <a href=%booh>booh</a>!')), nil)
+    vb.add(addthis = Gtk::CheckButton.new(utf8(_("Include the 'addthis' bookmarking and sharing button"))).set_active(athis))
+    vb.add(quotehtml = Gtk::CheckButton.new(utf8(_("Quote HTML markup in captions"))).set_active(qhtml))
+    tooltips.set_tip(quotehtml, utf8(_("If checked, text using markup special characters such as '<grin>' will be shown properly; if unchecked, markup such as '<a href..' links will be interpreted by the browser properly")), nil)
 
     theme_sizes = []
     nperrows = []
@@ -3578,9 +3827,11 @@ def properties
     save_nperpage = nperpage_model.get_value(nperpagecombo.active_iter, 1)
     save_madewith = madewithentry.text.gsub('\'', '&#39;')  #- because the parameters to booh-backend are between apostrophes
     save_indexlink = indexlinkentry.text.gsub('\'', '&#39;')
+    save_addthis = addthis.active?
+    save_quotehtml = quotehtml.active?
     dialog.destroy
     
-    if ok && (save_theme != theme || save_limit_sizes != limit_sizes || save_opt432 != opt432 || save_nperrow != nperrow || save_nperpage != nperpage || save_madewith != madewith || save_indexlink != indexlinkentry || save_multilanguages_value != multilanguages_value)
+    if ok && (save_theme != theme || save_limit_sizes != limit_sizes || save_opt432 != opt432 || save_nperrow != nperrow || save_nperpage != nperpage || save_madewith != madewith || save_indexlink != indexlink || save_multilanguages_value != multilanguages_value || save_quotehtml != qhtml || save_addthis != athis)
         #- some sort of automatic preferences
         if save_theme != theme
             $config['default-theme'] = save_theme
@@ -3591,18 +3842,26 @@ def properties
         if save_opt432 != opt432
             $config['default-optimize32'] = save_opt432.to_s
         end
+        if save_addthis != athis
+            $config['default-addthis'] = save_addthis.to_s
+        end
+        if save_quotehtml != qhtml
+            $config['default-quotehtml'] = save_quotehtml.to_s
+        end
         mark_document_as_dirty
         save_current_file
         call_backend("booh-backend --use-config '#{$filename}' --for-gui --verbose-level #{$verbose_level} " +
                      "--thumbnails-per-row #{save_nperrow} --theme #{save_theme} --sizes #{save_limit_sizes.join(',')} " +
                      (save_nperpage ? "--thumbnails-per-page #{save_nperpage} " : '') +
                      (save_multilanguages_value ? "--multi-languages #{save_multilanguages_value} " : '') +
-                     "#{save_opt432 ? '--optimize-for-32' : ''} --made-with '#{save_madewith}' --index-link '#{save_indexlink}' #{additional_booh_options}",
+                     "#{save_opt432 ? '--optimize-for-32' : ''} --made-with '#{save_madewith}' --index-link '#{save_indexlink}' " +
+                     "#{save_addthis ? '--addthis' : ''} #{save_quotehtml ? '--quote-html' : ''} #{additional_booh_options}",
                      utf8(_("Please wait while scanning source directory...")),
                      'full scan',
                      { :closure_after => proc {
                              open_file($filename)
                              $modified = true
+                             $main_window.urgency_hint = true
                          } })
     else
         #- select_theme merges global variables, need to return to current choices
@@ -3623,6 +3882,7 @@ def merge_current
                          open_file($filename)
                          $albums_tv.selection.select_path(sel[0])
                          $modified = true
+                         $main_window.urgency_hint = true
                      } })
 end
 
@@ -3639,6 +3899,7 @@ def merge_newsubs
                          open_file($filename)
                          $albums_tv.selection.select_path(sel[0])
                          $modified = true
+                         $main_window.urgency_hint = true
                      } })
 end
 
@@ -3657,6 +3918,7 @@ def merge
                  { :closure_after => proc {
                          open_file($filename)
                          $modified = true
+                         $main_window.urgency_hint = true
                      } })
 end
 
@@ -3670,7 +3932,7 @@ def save_as_do
     fc.add_shortcut_folder(File.expand_path("~/.booh"))
     fc.set_current_folder(File.expand_path("~/.booh"))
     fc.filename = $orig_filename
-    if fc.run == Gtk::Dialog::RESPONSE_ACCEPT
+    if fc.run == Gtk::Dialog::RESPONSE_ACCEPT && fc.filename
         $orig_filename = fc.filename
         if ! save_current_file_user
             fc.destroy
@@ -3689,39 +3951,64 @@ def preferences
                              [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
                              [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
 
+    table_counter = 0
     dialog.vbox.add(notebook = Gtk::Notebook.new)
     notebook.append_page(tbl = Gtk::Table.new(0, 0, false), Gtk::Label.new(utf8(_("Options"))))
     tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for watching videos: ")))),
-               0, 1, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
+               0, 1, table_counter, table_counter + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
     tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(video_viewer_entry = Gtk::Entry.new.set_text($config['video-viewer']).set_size_request(250, -1)),
-               1, 2, 0, 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
+               1, 2, table_counter, table_counter + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
     tooltips = Gtk::Tooltips.new
     tooltips.set_tip(video_viewer_entry, utf8(_("Use %f to specify the filename;
 for example: /usr/bin/mplayer %f")), nil)
+
+    table_counter += 1
     tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Command for editing images: ")))),
-               0, 1, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
+               0, 1, table_counter, table_counter + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
     tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(image_editor_entry = Gtk::Entry.new.set_text($config['image-editor'])),
-               1, 2, 1, 2, Gtk::FILL, Gtk::SHRINK, 2, 2)
+               1, 2, table_counter, table_counter + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
     tooltips.set_tip(image_editor_entry, utf8(_("Use %f to specify the filename;
 for example: /usr/bin/gimp-remote %f")), nil)
+
+    table_counter += 1
     tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(Gtk::Label.new.set_markup(utf8(_("Browser's command: ")))),
-               0, 1, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
+               0, 1, table_counter, table_counter + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
     tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(browser_entry = Gtk::Entry.new.set_text($config['browser'])),
-               1, 2, 2, 3, Gtk::FILL, Gtk::SHRINK, 2, 2)
+               1, 2, table_counter, table_counter + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
     tooltips.set_tip(browser_entry, utf8(_("Use %f to specify the filename;
 for example: /usr/bin/mozilla-firefox -remote 'openURL(%f,new-window)' || /usr/bin/mozilla-firefox %f")), nil)
+
+    table_counter += 1
+    tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(mp4_check = Gtk::CheckButton.new(utf8(_("Use this .mp4 generator for videos:")))),
+               0, 1, table_counter, table_counter + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
+    tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(mp4_generator_entry = Gtk::Entry.new.set_text($config['mp4-generator']).set_sensitive(false)),
+               1, 2, table_counter, table_counter + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
+    tooltips.set_tip(mp4_generator_entry, utf8(_("Use %f to specify the input filename, %o the output filename;
+for example: /usr/bin/ffmpeg -i %f -b ${i}k -ar 22050 -ab 32k %o")), nil)
+
+    table_counter += 1
     tbl.attach(Gtk::Alignment.new(1, 0.5, 0, 0).add(smp_check = Gtk::CheckButton.new(utf8(_("Use symmetric multi-processing")))),
-               0, 1, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
+               0, 1, table_counter, table_counter + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
     tbl.attach(Gtk::Alignment.new(0, 0.5, 1, 0).add(smp_hbox = Gtk::HBox.new.add(smp_spin = Gtk::SpinButton.new(2, 16, 1)).add(Gtk::Label.new(utf8(_("processors")))).set_sensitive(false)),
-               1, 2, 3, 4, Gtk::FILL, Gtk::SHRINK, 2, 2)
+               1, 2, table_counter, table_counter + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
     tooltips.set_tip(smp_check, utf8(_("When activated, this option allows the thumbnails creation to run faster. However, if you don't have a multi-processor machine, this will only slow down processing!")), nil)
+
+    table_counter += 1
     tbl.attach(nogestures_check = Gtk::CheckButton.new(utf8(_("Disable mouse gestures"))),
-               0, 2, 4, 5, Gtk::FILL, Gtk::SHRINK, 2, 2)
+               0, 2, table_counter, table_counter + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
     tooltips.set_tip(nogestures_check, utf8(_("Mouse gestures are 'unusual' mouse movements triggering special actions, and are great for speeding up your editions. Get details on available mouse gestures from the Help menu.")), nil)
+
+    table_counter += 1
     tbl.attach(deleteondisk_check = Gtk::CheckButton.new(utf8(_("Delete original photos/videos as well"))),
-               0, 2, 6, 7, Gtk::FILL, Gtk::SHRINK, 2, 2)
+               0, 2, table_counter, table_counter + 1, Gtk::FILL, Gtk::SHRINK, 2, 2)
     tooltips.set_tip(deleteondisk_check, utf8(_("Normally, deleting a photo or video in booh only removes it from the web-album. If you check this option, the original file in source directory will be removed as well. Undo is possible, since actual deletion is performed only when web-album is saved.")), nil)
 
+    mp4_check.signal_connect('toggled') {
+        mp4_generator_entry.sensitive = mp4_check.active?
+    }
+    if $config['use-mp4'] == 'true'
+        mp4_check.active = true
+    end
     smp_check.signal_connect('toggled') {
         smp_hbox.sensitive = smp_check.active?
     }
@@ -3908,6 +4195,12 @@ for example: avi:mencoder -nosound -ovc xvid -xvidencopts bitrate=800:me_quality
             $config['video-viewer'] = from_utf8(video_viewer_entry.text)
             $config['image-editor'] = from_utf8(image_editor_entry.text)
             $config['browser'] = from_utf8(browser_entry.text)
+            if mp4_check.active?
+                $config['use-mp4'] = 'true'
+                $config['mp4-generator'] = from_utf8(mp4_generator_entry.text)
+            else
+                $config['use-mp4'] = 'false'
+            end
             if smp_check.active?
                 $config['mproc'] = smp_spin.value.to_i
             else
@@ -3927,6 +4220,8 @@ for example: avi:mencoder -nosound -ovc xvid -xvidencopts bitrate=800:me_quality
         end
     }
     dialog.destroy
+
+    check_config_preferences_dep
 end
 
 def perform_undo
@@ -3958,6 +4253,173 @@ Click the <span foreground='darkblue'>None</span> icon when you're finished with
 ") % intro), { :pos_centered => true })
 end
 
+def perform_remote_synchronization(url, detail_label, progressbar_window, dialog, read, write)
+    begin
+        gtk_thread_protect {
+            detail_label.set_markup("<i>" + utf8(_("Logging into remote site...")) + "</i>")
+        }
+        lftp_additionals = $config['lftp-additionals']
+        if lftp_additionals
+            msg 3, "Adding lftp additionals:\n" + lftp_additionals
+            write.puts(lftp_additionals)
+        else
+            msg 3, "No lftp additionals"
+        end
+        write.puts("set net:max-retries 1")
+        write.puts("set cmd:fail-exit true")
+        write.puts("open " + url)
+        write.puts("lcd " + File.dirname($xmldoc.root.attributes['destination']))
+        write.puts("ls")  #- force connection and fail on exit, in order to detect problems with host or path
+        write.puts("echo __ls_EOF_1234567890abcdefghijk")  #- detect end
+
+        ok_to_mirror = false
+        while line = read.gets
+            msg 3, "received from lftp (login stage): #{line}"
+            if line == "__ls_EOF_1234567890abcdefghijk\n"
+                ok_to_mirror = true
+                break
+            end
+        end
+        if ! ok_to_mirror
+            gtk_thread_protect {
+                progressbar_window.destroy
+                show_popup(dialog, utf8(_("Failed to connect to specified URL, please check your input.")), { :pos_centered => true })
+            }
+            return
+        end
+
+        msg 3, "lftp login and ls ok, mirroring..."
+        gtk_thread_protect {
+            detail_label.set_markup("<i>" + utf8(_("Mirroring data...")) + "</i>")
+        }
+        mirrored_successfully = false
+        write.puts("set net:max-retries 5")
+        write.puts("mirror -R " + File.basename($xmldoc.root.attributes['destination']))
+        write.puts("echo __finished___ls_EOF_1234567890abcdefghijk")  #- detect end
+        while line = read.gets
+            msg 3, "received from lftp (mirror stage): #{line}"
+            if line == "__finished___ls_EOF_1234567890abcdefghijk\n"
+                mirrored_successfully = true
+                break
+            end
+        end
+
+        write.close
+        read.close
+
+        gtk_thread_protect {
+            progressbar_window.destroy
+            $main_window.urgency_hint = true
+            if mirrored_successfully
+                show_popup(dialog, utf8(_("Successfully mirrored into remote repository.")), { :pos_centered => true })
+            else
+                show_popup(dialog, utf8(_("Failed to mirror into remote repository.")), { :pos_centered => true })
+            end
+        }
+
+    rescue
+        msg 3, "failed lftp dialog: #{$!}"
+    end
+end
+
+def remote_synchronization
+
+    remote_synchro = Gtk::Dialog.new(utf8(_("Upload web-album")),
+                                     $main_window,
+                                     Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
+                                     [Gtk::Stock::CLOSE, Gtk::Dialog::RESPONSE_OK])
+    remote_synchro.vbox.add(Gtk::Label.new.set_markup(utf8(_("<b>Upload web-album.</b>
+
+Mirror web-album into remote repository (lftp URL):
+<i>The destination directory '%s' will be created/updated there.</i>") % File.basename($xmldoc.root.attributes['destination']))).set_alignment(0, 0))
+    remote_synchro.vbox.add(Gtk::HBox.new(false, 0).pack_start(repo = Gtk::Entry.new.set_text($xmldoc.root.attributes['remote_synchronization_url'] || ''), true, true).
+                                                    pack_start(mirror = Gtk::Button.new(utf8(_("Upload"))).set_image(Gtk::Image.new("#{$FPATH}/images/stock-upload-16.png")), false, false))
+
+    mirror.signal_connect('clicked') {
+        if $xmldoc.root.attributes['remote_synchronization_url'] != repo.text
+            $modified = true
+            $xmldoc.root.add_attribute('remote_synchronization_url', repo.text)
+        end
+        w = create_window
+        w.set_transient_for(remote_synchro)
+        w.modal = true
+        vb = Gtk::VBox.new(false, 5).set_border_width(5)
+        vb.pack_start(Gtk::Label.new(utf8(_("Please wait, mirroring..."))), false, false)
+        vb.pack_start(detail = Gtk::Label.new.set_markup("<i>" + utf8(_("Initialization...")) + "</i>"), false, false)
+        vb.pack_start(pb = Gtk::ProgressBar.new, false, false)
+        bottom = Gtk::Alignment.new(0.5, 0.5, 0, 0).add(b = Gtk::Button.new(utf8(_("_Abort"))))
+        b.image = Gtk::Image.new("#{$FPATH}/images/stock-close-24.png")
+        vb.pack_end(bottom, false, false)
+        refresh_thread = Thread.new {
+            while true
+                gtk_thread_protect { pb.pulse }
+                sleep 0.5
+            end
+        }
+        w.add(vb)
+        w.signal_connect('delete-event') { w.destroy }
+        w.signal_connect('destroy') {
+            Thread.kill(refresh_thread)
+            gtk_thread_flush  #- needed because we're about to destroy widgets in w, for which they may be some pending gtk calls
+        }
+        w.window_position = Gtk::Window::POS_CENTER
+
+        pid = nil
+        cmd = 'lftp'
+        rd1, wr1 = IO.pipe
+        rd2, wr2 = IO.pipe
+
+        if ! pid = fork
+            rd2.close
+            wr1.close
+            $stdin.reopen(rd1)
+            $stdout.reopen(wr2)
+            $stderr.reopen(wr2)
+            begin
+                exec(cmd)
+            rescue
+                Process.exit!(66)  #- _exit
+            end
+        end
+
+        rd1.close
+        wr2.close
+
+        remote_synchronization_thread = Thread.new {
+            perform_remote_synchronization(repo.text, detail, w, remote_synchro, rd2, wr1)
+            begin
+                Process.kill('SIGTERM', pid)
+            rescue
+            end
+        }
+
+        b.signal_connect('clicked') {
+            Thread.kill(remote_synchronization_thread)
+            begin
+                Process.kill('SIGTERM', pid)
+            rescue
+                #- race condition (process just died)
+            end
+            w.destroy
+        }
+        w.show_all
+
+        Thread.new {
+            id, exitstatus = Process.waitpid2(pid)
+            if exitstatus >> 8 == 66
+                gtk_thread_protect {
+                    w.destroy
+                    show_popup(remote_synchro, utf8(_("Failed to execute 'lftp' program.")), { :pos_centered => true })
+                }
+            end
+        }
+    }
+
+    remote_synchro.window_position = Gtk::Window::POS_CENTER
+    remote_synchro.show_all
+    remote_synchro.run { remote_synchro.destroy }
+end
+
 def create_menu_and_toolbar
     
     #- menu
@@ -3982,11 +4444,16 @@ def create_menu_and_toolbar
     $merge.image = Gtk::Image.new("#{$FPATH}/images/stock-reset-16.png")
     tooltips.set_tip($merge, utf8(_("Take into account new/removed subalbums (subdirectories) and new/removed photos/videos in existing subalbums (anywhere)")), nil)
     filesubmenu.append(            Gtk::SeparatorMenuItem.new)
+    filesubmenu.append($extend   = Gtk::ImageMenuItem.new(utf8(_("Extend album..."))).set_sensitive(false))
+    $extend.image = Gtk::Image.new("#{$FPATH}/images/stock-scale-16.png")
+    filesubmenu.append(            Gtk::SeparatorMenuItem.new)
     filesubmenu.append($generate = Gtk::ImageMenuItem.new(utf8(_("Generate web-album"))).set_sensitive(false))
     $generate.image = Gtk::Image.new("#{$FPATH}/images/stock-web-16.png")
     tooltips.set_tip($generate, utf8(_("(Re)generate web-album from latest changes into the destination directory")), nil)
     filesubmenu.append($view_wa = Gtk::ImageMenuItem.new(utf8(_("View web-album with browser"))).set_sensitive(false))
     $view_wa.image = Gtk::Image.new("#{$FPATH}/images/stock-view-webalbum-16.png")
+    filesubmenu.append($upload = Gtk::ImageMenuItem.new(utf8(_("Upload web-album"))).set_sensitive(false))
+    $upload.image = Gtk::Image.new("#{$FPATH}/images/stock-upload-16.png")
     filesubmenu.append(            Gtk::SeparatorMenuItem.new)
     filesubmenu.append($properties = Gtk::ImageMenuItem.new(Gtk::Stock::PROPERTIES).set_sensitive(false))
     tooltips.set_tip($properties, utf8(_("View and modify properties of the web-album")), nil)
@@ -4002,6 +4469,7 @@ def create_menu_and_toolbar
     $merge_current.signal_connect('activate') { merge_current }
     $merge_newsubs.signal_connect('activate') { merge_newsubs }
     $merge.signal_connect('activate') { merge }
+    $extend.signal_connect('activate') { extend_ }
     $generate.signal_connect('activate') {
         save_current_file
         call_backend("booh-backend --config '#{$filename}' --verbose-level #{$verbose_level} #{additional_booh_options}",
@@ -4010,9 +4478,9 @@ def create_menu_and_toolbar
                      { :successmsg => $xmldoc.root.attributes['multi-languages'] ?
                          utf8(_("Your web-album is now ready in directory '%s'.
 As multi-languages is activated, you will not be able to view it
-comfortably in your browser though.") % $xmldoc.root.attributes['destination']) :
+locally in your browser though.") % $xmldoc.root.attributes['destination']) :
                          utf8(_("Your web-album is now ready in directory '%s'.
-Click to view it in your browser:") % $xmldoc.root.attributes['destination']),
+Click to view it in your browser:") % [ $xmldoc.root.attributes['destination'] ]),
                        :successmsg_linkurl => $xmldoc.root.attributes['multi-languages'] ? $xmldoc.root.attributes['destination'] :
                                                                                            $xmldoc.root.attributes['destination'] + '/index.html',
                        :closure_after => proc {
@@ -4025,6 +4493,7 @@ Click to view it in your browser:") % $xmldoc.root.attributes['destination']),
                              $redo_tb.sensitive = $redo_mb.sensitive = false
                              save_current_file
                              $generated_outofline = true
+                             $main_window.urgency_hint = true
                          }})
     }
     $view_wa.signal_connect('activate') {
@@ -4035,6 +4504,18 @@ Click to view it in your browser:") % $xmldoc.root.attributes['destination']),
             show_popup($main_window, utf8(_("Seems like you should generate the web-album first.")))
         end
     }
+    $upload.signal_connect('activate') {
+        indexhtml = $xmldoc.root.attributes['destination'] + '/index.html'
+        if File.exists?(indexhtml)
+            if !system("which lftp >/dev/null 2>/dev/null")
+                show_popup($main_window, utf8(_("The program 'lftp' is needed to upload web-albums. Please install it.")), { :pos_centered => true })
+            else
+                remote_synchronization
+            end
+        else
+            show_popup($main_window, utf8(_("Seems like you should generate the web-album first.")))
+        end
+    }
     $properties.signal_connect('activate') { properties }
 
     quit.signal_connect('activate') { try_quit }
@@ -4257,23 +4738,24 @@ def gtk_thread_protect(&proc)
     if Thread.current == Thread.main
         proc.call
     else
-        $protect_gtk_pending_calls.synchronize {
+        $gtk_pending_calls.synchronize {
             $gtk_pending_calls << proc
         }
     end
 end
 
 def gtk_thread_flush
-    #- try to lock. we cannot synchronize blindly because this might be called from
-    #- within the timeout flushing procs. if this is the case, not doing anything
-    #- should be ok since the timeout is already flushing them all.
-    if $protect_gtk_pending_calls.try_lock
-        for closure in $gtk_pending_calls
+    closure = nil
+    continue = true
+    begin
+        $gtk_pending_calls.synchronize {
+            closure = $gtk_pending_calls.shift
+            continue = $gtk_pending_calls.size > 0
+        }
+        if closure
             closure.call
         end
-        $gtk_pending_calls = []
-        $protect_gtk_pending_calls.unlock
-    end
+    end while continue
 end
 
 def ask_password_protect
@@ -4397,6 +4879,140 @@ below the Document Root), and specify this location in the password protect dial
     }
 end
 
+def extend_
+    if ! ask_save_modifications(utf8(_("Save modifications?")),
+                                utf8(_("You need to save or discard your changes before extending the album.")),
+                                { :no => Gtk::Stock::DISCARD })
+        return
+    end
+
+    #- handle discard
+    $xmldoc = REXML::Document.new(File.new($orig_filename))
+    $modified = false
+
+    dialog = Gtk::Dialog.new(utf8(_("Extend the album")),
+                             $main_window,
+                             Gtk::Dialog::MODAL | Gtk::Dialog::DESTROY_WITH_PARENT,
+                             [Gtk::Stock::OK, Gtk::Dialog::RESPONSE_OK],
+                             [Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL])
+
+    lbl = Gtk::Label.new
+    lbl.markup = utf8(
+_("If you want this album to be part of a larger album, you can choose to
+<b>extend</b> it. This album will become a sub-album of the larger album.
+A new directory will be created in source and destination directories with
+the specified name, and everything currently in there will be moved down.
+A typical use case is a ski album, with <span foreground='darkblue'>Tignes</span> and <span foreground='darkblue'>Courchevel</span> subalbums;
+if you want to extend it to a vacations album, then you may input the
+name <span foreground='darkblue'>Ski</span>; after the album is extended, then you may create other
+directories at the same level as <span foreground='darkblue'>Ski</span> in the source directory, such as
+<span foreground='darkblue'>Summers</span> and <span foreground='darkblue'>Christmas</span> and arrange related photos/videos in there."))
+    dialog.vbox.add(lbl)
+    dialog.vbox.add(Gtk::Label.new)
+
+    new_tv = Gtk::TreeView.new.set_size_request(-1, 150)
+    new_tv.append_column(Gtk::TreeViewColumn.new('', Gtk::CellRendererText.new, { :text => 0 }))
+    new_tv.set_headers_visible(false)
+    new_tv.selection.mode = Gtk::SELECTION_NONE
+    new_tv.set_model(new_ts = Gtk::TreeStore.new(String))
+    topdir = $xmldoc.elements['//dir']
+    append_dir_elem = proc { |parent_iter, xmldir|
+        child_iter = new_ts.append(parent_iter)
+        child_iter[0] = File.basename(xmldir.attributes['path'])
+        xmldir.elements.each('dir') { |elem|
+            if !elem.attributes['deleted']
+                append_dir_elem.call(child_iter, elem)
+            end
+        }
+        child_iter
+    }
+    top = new_ts.append(nil)
+    top[0] = File.basename(topdir.attributes['path'])
+    new_top = append_dir_elem.call(top, topdir)
+    new_tv.expand_all
+    new_sw = Gtk::ScrolledWindow.new(nil, nil)
+    new_sw.set_policy(Gtk::POLICY_NEVER, Gtk::POLICY_AUTOMATIC)
+    new_sw.add(new_tv)
+    dialog.vbox.add(Gtk::HBox.new.pack_start(Gtk::Label.new(utf8(_("New layout: "))), false, false, 0).pack_start(new_sw, true, true, 0))
+    dialog.vbox.add(Gtk::Label.new)
+
+    dialog.vbox.add(Gtk::HBox.new.pack_start(Gtk::Label.new(utf8(_("Directory name:"))), false, false, 0).pack_start(e = Gtk::Entry.new.set_text(File.basename($xmldoc.root.attributes['source'])), true, true, 0))
+#    dialog.window_position = Gtk::Window::POS_MOUSE
+    new_top[0] = e.text
+    e.signal_connect('changed') {
+        new_top[0] = e.text
+    }
+    dialog.show_all
+    e.grab_focus
+
+    keepon = true
+    while keepon
+        dialog.run { |response|
+            perform = proc {
+                name = e.text
+                if name =~ /'/
+                    show_popup(dialog, utf8(_("Source directory or sub-directories can't contain a single-quote character, sorry: %s") % name))
+                    return true
+                end
+                name = from_utf8(name)
+                source = from_utf8($xmldoc.root.attributes['source'])
+                dest = from_utf8($xmldoc.root.attributes['destination'])
+                if File.exists?("#{source}/#{name}")
+                    show_popup(dialog, utf8(_("%s already exists.") % "#{source}/#{name}"))
+                    return true
+                end
+                if ! FileTest.writable?(source)
+                    show_popup(dialog, utf8(_("No write access to '%s'.") % source))
+                    return true
+                end
+                if File.exists?("#{dest}/#{name}")
+                    show_popup(dialog, utf8(_("%s already exists.") % "#{dest}/#{name}"))
+                    return true
+                end
+                tomove1 = Dir.entries(source) - [ '.', '..' ]
+                tomove2 = Dir.entries(dest) - [ '.', '..' ]
+                begin
+                    Dir.mkdir("#{source}/#{name}")
+                    Dir.mkdir("#{dest}/#{name}")
+                rescue Exception
+                    show_popup(dialog, utf8(_("Erroneous name.")))
+                    puts $!
+                    return true
+                end
+                tomove1.each { |file|
+                    sys("mv '#{source}/#{file}' '#{source}/#{name}'")
+                }
+                tomove2.each { |file|
+                    sys("mv '#{dest}/#{file}' '#{dest}/#{name}'")
+                }
+
+                substmatch = /^#{Regexp.quote($xmldoc.root.attributes['source'])}/
+                substrepl = $xmldoc.root.attributes['source'] + '/' + utf8(name)
+                substelems = [ 'path', 'subdirs-captionfile', 'thumbnails-captionfile' ]
+                $xmldoc.elements.each('//dir') { |elem|
+                    substelems.each { |elname|
+                        if el = elem.attributes[elname]
+                            elem.add_attribute(elname, el.sub(substmatch, substrepl))
+                        end
+                    }
+                }
+
+                $xmldoc.root.add_element('dir', { 'path' => utf8(source) }).add_element($xmldoc.root.elements[1].remove)
+                save_current_file_user
+                open_file($orig_filename)
+                return false
+            }
+
+            if response == Gtk::Dialog::RESPONSE_OK
+                keepon = perform.call
+            else
+                keepon = false
+            end
+        }
+    end
+    dialog.destroy
+end
+
 def create_main_window
 
     mb, tb = create_menu_and_toolbar
@@ -4420,22 +5036,6 @@ def create_main_window
         pop_mousecursor
     }
 
-#    offset = "0:0"
-#    Gtk.timeout_add(1000) {
-#        puts "trying offset #{offset}"
-#        iter = $albums_ts.get_iter(offset)
-#        if iter
-#            puts "...ok at offset #{offset}"
-#            $current_path = $albums_ts.get_value(iter, 1)
-#            change_dir
-#        end
-#        if offset == "0:0"
-#           offset = "0:1"
-#        else   
-#           offset = "0:0"
-#        end
-#    }
-
     $albums_tv.signal_connect('button-release-event') { |w, event|
         if event.event_type == Gdk::Event::BUTTON_RELEASE && event.button == 3 && !$current_path.nil?
             menu = Gtk::Menu.new
@@ -4457,7 +5057,7 @@ def create_main_window
         end
     }
 
-    $albums_ts = Gtk::TreeStore.new(String, String, Gdk::Pixbuf)
+    $albums_ts = Gtk::TreeStore.new(String, String, GdkPixbuf::Pixbuf)
     $albums_tv.set_model($albums_ts)
     $albums_tv.signal_connect('realize') { $albums_tv.grab_focus }
 
@@ -4504,6 +5104,9 @@ def create_main_window
     $main_window.signal_connect('delete-event') {
         try_quit({ :disallow_cancel => true })
     }
+    $main_window.signal_connect('focus-in-event') {
+        $main_window.urgency_hint = false
+    }
 
     #- read/save size and position of window
     if $config['pos-x'] && $config['pos-y']
@@ -4512,7 +5115,7 @@ def create_main_window
         $main_window.window_position = Gtk::Window::POS_CENTER
     end
     msg 3, "size: #{$config['width']}x#{$config['height']}"
-    $main_window.set_default_size(($config['width'] || 600).to_i, ($config['height'] || 400).to_i)
+    $main_window.set_default_size(($config['width'] || 800).to_i, ($config['height'] || 600).to_i)
     $main_window.signal_connect('configure-event') {
         msg 3, "configure: pos: #{$main_window.window.root_origin.inspect} size: #{$main_window.window.size.inspect}"
         x, y = $main_window.window.root_origin
@@ -4524,15 +5127,10 @@ def create_main_window
         false
     }
 
-    $protect_gtk_pending_calls = Mutex.new
     $gtk_pending_calls = []
+    $gtk_pending_calls.extend(MonitorMixin)
     Gtk.timeout_add(100) {
-        $protect_gtk_pending_calls.synchronize {
-            for closure in $gtk_pending_calls
-                closure.call
-            end
-            $gtk_pending_calls = []
-        }
+        gtk_thread_flush
         true
     }
 
@@ -4541,20 +5139,46 @@ def create_main_window
 end
 
 
+if str = Gtk.check_version(2, 8, 0)
+    puts "This program requires GTK+ 2.8.0 or later"
+    puts str
+    exit
+end
+
 handle_options
 
+binding_version = Gtk::BINDING_VERSION
+msg 3, "binding version: " + binding_version.join('.')
+if binding_version == [ 0, 15, 0 ]
+    puts "It seems that we're running ruby-gtk2 0.15.0; this version is known to crash; please upgrade or downgrade."
+    exit
+end
+if binding_version == [ 0, 17, 0 ]
+    puts "It seems that we're running ruby-gtk2 0.17.0; this version is known to have a serious memory leak; please upgrade or downgrade."
+    exit
+end
+if binding_version == [ 0, 18, 0 ]
+    puts "It seems that we're running ruby-gtk2 0.18.0; this version will crash due to missing Gdk::GC; please upgrade or downgrade."
+    exit
+end
+ruby_version = RUBY_VERSION.split('.').collect { |v| v.to_i }
+if binding_version[0] <= 0 && binding_version[1] <= 16 && ruby_version[0] >= 1 && ruby_version[1] >= 8 && ruby_version[2] >= 7
+    puts "It seems that we're running ruby-gtk2 <= 0.16.0 with ruby >= 1.8.7; this combination is known to crash; please upgrade or downgrade some."
+    exit
+end
 
 Thread.abort_on_exception = true
-
 read_config
 
 Gtk.init
 create_main_window
+
 check_config
 
 if ARGV[0]
     open_file_user(ARGV[0])
 end
+
 Gtk.main
 
 write_config