function Gallery(_o, _parent, _fnBack) {
  
  // ================================================================
  // == PRIVATE =====================================================
  // ================================================================
  
  var currIdx = -1;
  var images = [];
  var info = {
    dir: "",
    title: "",
    subtitle: ""
  };
  var ui = {
    back: null,       // zooming out element
    cont: null,
    imgTitle: null,   // the title image
    list: null,       // list of images
    parent: _parent,
    zoom: null,       // zoomed version of images
    zoomImg: null     // element holding currently zoomed-in image
  };
  var zooming = false;
  
  var that = this;
  
  
  // ----------------------------------------------------------------
  function construct() {
    info.dir = _o.dir;
    info.title = _o.title;
    info.subtitle = _o.subtitle;
    
    ui.cont = $$("div", null, null, "gallery");
    
    // (1) Back link, title, and subtitle:
    var spanBack = $$("span", ui.cont, null, "gallery-back", "Galleries");
    spanBack.onclick = _fnBack;
    $$("span", ui.cont, null, "gallery-title-dot", "&nbsp;&bull;&nbsp;");
    $$("span", ui.cont, null, "gallery-title", _o.title);
    if (_o.subtitle) $$("div", ui.cont, null, "gallery-subtitle", _o.subtitle);
    
    // (2) List of images:
    ui.list = $$("table", ui.cont);
    ui.list.setAttribute("cellpadding", "0");
    ui.list.setAttribute("cellspacing", "0");
    var tbody = $$("tbody", ui.list);
    
    for (var i=0, ni=_o.images.length; i < ni; i++) {
      var image = _o.images[i];
      var tr = $$("tr", tbody);
      var td01 = $$("td", tr, null, "gallery-item-01");
      var td02 = $$("td", tr, null, "gallery-item-02");
      
      ui.imgTitle = $$("img", td01, null, "gallery-item-img");
      ui.imgTitle.setAttribute("src", _o.dir + "/mini/" + image.filename + "-off." + image.ext);
      ui.imgTitle.setAttribute("alt", "");
      ui.imgTitle.onclick = function(gallery, idx) {
        return function (e) {
          gallery.zoomIn(idx);
        };
      }(pub,i);
      ui.imgTitle.onmouseover = function(img,src) {
        return function (e) {
          img.setAttribute("src", src);
        };
      }(ui.imgTitle, _o.dir + "/mini/" + image.filename + "-on." + image.ext);
      ui.imgTitle.onmouseout = function(img,src) {
        return function (e) {
          img.setAttribute("src", src);
        };
      }(ui.imgTitle, _o.dir + "/mini/" + image.filename + "-off." + image.ext);
      
      var span = $$("span", td02, null, "gallery-item-title", image.title);
      if (image.subtitle) $$("span", td02, null, "gallery-item-subtitle", "<br />" + image.subtitle);
      
      /*
      var imgBig = $$("img", null, null, "gallery-zoom-img");
      // TODO: imgBig.setAttribute("src", _o.dir + "/" + image.filename + "." + image.ext);
      imgBig.setAttribute("alt", "");
      imgBig.oncontextmenu = function (e) { return false; };
      if (image.w) imgBig.setAttribute("width", image.w);
      if (image.h) imgBig.setAttribute("height", image.h);
      */
      
      images.push(image);
    }
    
    // (3) Zoomed images:
    /*
    ui.back = $$("div", ui.cont, null, "gallery-back", "back to images list");
    ui.back.onclick = pub.zoomOut;
    
    ui.zoom = $$("table", ui.cont);
    ui.zoom.setAttribute("cellpadding", "0");
    ui.zoom.setAttribute("cellspacing", "0");
    ui.zoom.style.display = "none";
    var tr = $$("tr", $$("tbody", ui.zoom));
    $$("td", tr, null, "gallery-zoom-nav", "prev").onclick = function (gallery) { return function (e) { gallery.gotoPrev(); } }(pub);
    ui.zoomImg = $$("td", tr, null, "gallery-zoom-img");
    $$("td", tr, null, "gallery-zoom-nav", "next").onclick = function (gallery) { return function (e) { gallery.gotoNext(); } }(pub)
    */
    
    ui.parent.appendChild(ui.cont);
  }
  
  
  // ================================================================
  // == PUBLIC ======================================================
  // ================================================================
  
  var pub = {
    
    // ----------------------------------------------------------------
    gotoFirst: function () {
      if (!zooming) return;
      currIdx = 0;
    },
    
    
    // ----------------------------------------------------------------
    gotoLast: function () {
      if (!zooming) return;
      currIdx = items.length-1;
    },
    
    
    // ----------------------------------------------------------------
    gotoNext: function () {
      if (!zooming || currIdx === images.length-1) return;
      ui.zoomImg.removeChild(images[currIdx++]);
      ui.zoomImg.appendChild(images[currIdx]);
    },
    
    
    // ----------------------------------------------------------------
    gotoPrev: function () {
      if (!zooming || currIdx === 0) return;
      ui.zoomImg.removeChild(images[currIdx--]);
      ui.zoomImg.appendChild(images[currIdx]);
    },
    
    
    // ----------------------------------------------------------------
    hide: function () { ui.cont.style.display = "none"; },
    show: function () { ui.cont.style.display = "block"; },
    
    
    // ----------------------------------------------------------------
    zoomIn: function (idx) {
      /*
      ui.zoomImg.appendChild(images[idx]);
      ui.list.style.display = "none";
      ui.back.style.display = "block";
      ui.zoom.style.display = "block";
      */
      currIdx = idx;
      zooming = true;
      
      var img = images[idx];
      var args = 
        "?s=" + encodeURIComponent(info.dir + "/" + img.filename + "." + img.ext) +
        "&w=" + img.w +
        "&h=" + img.h +
        "&t=" + encodeURIComponent(img.title) +
        "&m=0" +
        (img.date ? "&date=" + img.date.toDateString() : "") +
        "&siCamera=" + img.shotInfo.camera +
        "&siLens=" + img.shotInfo.lens +
        "&siISO=" + img.shotInfo.iso +
        "&siT=" + img.shotInfo.t +
        "&siF=" + img.shotInfo.f +
        "&siTripod=" + (img.shotInfo.tripod ? 1 : 0) +
        "&siOther=" + img.shotInfo.other;
      window.open("img.html" + args, "", "title=1,toolbar=0,location=0,directories=0,status=0,menubar=0,scrollbars=0,resizable=0");
    },
    
    
    // ----------------------------------------------------------------
    zoomOut: function () {
      ui.zoom.style.display = "none";
      ui.back.style.display = "none";
      ui.list.style.display = "block";
      ui.zoomImg.removeChild(images[currIdx]);
      currIdx = -1;
      zooming = false;
    }
    
  };
  
  
  // ================================================================
  // == CONSTRUCT ===================================================
  // ================================================================
  
  construct();
  return pub;
};
