From 566dc060676b41e1e58a446b7dcc4159e242fee6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=B6rg=20Frings-F=C3=BCrst?= <debian@jff-webhosting.net>
Date: Tue, 23 Sep 2014 09:36:45 +0200
Subject: Imported Upstream version 0.20.0

---
 plugins/common/Resources.vala                      |    2 +-
 plugins/plugins.mk                                 |    8 +-
 .../GalleryConnector.vala                          | 2034 ++++++++++++++++++++
 plugins/shotwell-publishing-extras/Makefile        |   11 +-
 .../RajcePublishing.vala                           | 1554 +++++++++++++++
 plugins/shotwell-publishing-extras/gallery3.png    |  Bin 0 -> 802 bytes
 .../gallery3_authentication_pane.glade             |  245 +++
 .../gallery3_publishing_options_pane.glade         |  282 +++
 plugins/shotwell-publishing-extras/rajce.png       |  Bin 0 -> 1650 bytes
 .../rajce_authentication_pane.glade                |  150 ++
 .../rajce_publishing_options_pane.glade            |  275 +++
 .../shotwell-publishing-extras.vala                |    2 +
 .../shotwell-publishing/FacebookPublishing.vala    |    2 +-
 plugins/shotwell-publishing/FlickrPublishing.vala  |    4 +-
 14 files changed, 4563 insertions(+), 6 deletions(-)
 create mode 100644 plugins/shotwell-publishing-extras/GalleryConnector.vala
 create mode 100644 plugins/shotwell-publishing-extras/RajcePublishing.vala
 create mode 100644 plugins/shotwell-publishing-extras/gallery3.png
 create mode 100644 plugins/shotwell-publishing-extras/gallery3_authentication_pane.glade
 create mode 100644 plugins/shotwell-publishing-extras/gallery3_publishing_options_pane.glade
 create mode 100644 plugins/shotwell-publishing-extras/rajce.png
 create mode 100644 plugins/shotwell-publishing-extras/rajce_authentication_pane.glade
 create mode 100644 plugins/shotwell-publishing-extras/rajce_publishing_options_pane.glade

(limited to 'plugins')

diff --git a/plugins/common/Resources.vala b/plugins/common/Resources.vala
index bcdc590..79d4818 100644
--- a/plugins/common/Resources.vala
+++ b/plugins/common/Resources.vala
@@ -6,7 +6,7 @@
 
 namespace Resources {
 
-public const string WEBSITE_NAME = _("Visit the Yorba web site");
+public const string WEBSITE_NAME = _("Visit the Shotwell home page");
 public const string WEBSITE_URL = "https://wiki.gnome.org/Apps/Shotwell";
 
 public const string LICENSE = """
diff --git a/plugins/plugins.mk b/plugins/plugins.mk
index 35bb80c..2f28608 100644
--- a/plugins/plugins.mk
+++ b/plugins/plugins.mk
@@ -25,9 +25,15 @@ EXTRA_PLUGINS := \
 EXTRA_PLUGINS_RC := \
 	plugins/shotwell-publishing-extras/yandex_publish_model.glade \
 	plugins/shotwell-data-imports/f-spot-24.png \
+	plugins/shotwell-publishing-extras/gallery3.png \
+	plugins/shotwell-publishing-extras/gallery3_authentication_pane.glade \
+	plugins/shotwell-publishing-extras/gallery3_publishing_options_pane.glade \
 	plugins/shotwell-publishing-extras/tumblr.png \
 	plugins/shotwell-publishing-extras/tumblr_authentication_pane.glade \
-	plugins/shotwell-publishing-extras/tumblr_publishing_options_pane.glade
+	plugins/shotwell-publishing-extras/tumblr_publishing_options_pane.glade \
+	plugins/shotwell-publishing-extras/rajce.png \
+	plugins/shotwell-publishing-extras/rajce_authentication_pane.glade \
+	plugins/shotwell-publishing-extras/rajce_publishing_options_pane.glade
 
 ALL_PLUGINS := $(PLUGINS) $(EXTRA_PLUGINS)
 
diff --git a/plugins/shotwell-publishing-extras/GalleryConnector.vala b/plugins/shotwell-publishing-extras/GalleryConnector.vala
new file mode 100644
index 0000000..682aff0
--- /dev/null
+++ b/plugins/shotwell-publishing-extras/GalleryConnector.vala
@@ -0,0 +1,2034 @@
+/* Copyright 2012-2013 Joe Sapp nixphoeni@gentoo.org
+ *
+ * This software is licensed under the GNU LGPL (version 2.1 or later).
+ * See the COPYING file in this distribution.
+ */
+
+
+static const string G3_VERSION = "0.1";
+
+static const string G3_LICENSE = """
+The Gallery3Publishing module is free software; you can redistribute it
+and/or modify it under the terms of the GNU Lesser General Public
+License as published by the Free Software Foundation; either version 2.1
+of the License, or (at your option) any later version.
+
+The Gallery3Publishing module is distributed in the hope that it will be
+useful, but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser
+General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with The Gallery3Publishing module; if not, write to the Free
+Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
+02110-1301 USA
+""";
+
+static const string WEBSITE_URL =
+    "https://github.com/sappjw/shotwell-gallery3";
+
+// This module's Spit.Module
+private class ShotwellPublishingGallery3 : Object, Spit.Module {
+    private Spit.Pluggable[] pluggables = new Spit.Pluggable[0];
+
+    public ShotwellPublishingGallery3(GLib.File module_file) {
+        GLib.File resource_directory = module_file.get_parent();
+
+        pluggables += new Gallery3Service(resource_directory);
+    }
+
+    public unowned string get_module_name() {
+        return _("Gallery3 publishing module");
+    }
+
+    public unowned string get_version() {
+        return G3_VERSION;
+    }
+
+    public unowned string get_id() {
+        return "org.yorba.shotwell.sharing.gallery3";
+    }
+
+    public unowned Spit.Pluggable[]? get_pluggables() {
+        return pluggables;
+    }
+}
+
+// The Pluggable
+public class Gallery3Service : Object, Spit.Pluggable,
+        Spit.Publishing.Service {
+    private const string ICON_FILENAME = "gallery3.png";
+
+    private static Gdk.Pixbuf[] icon_pixbuf_set = null;
+
+    public Gallery3Service(GLib.File resource_directory) {
+        if (icon_pixbuf_set == null)
+            icon_pixbuf_set = Resources.load_icon_set(
+                resource_directory.get_child(ICON_FILENAME));
+    }
+
+    public int get_pluggable_interface(int min_host_interface,
+            int max_host_interface) {
+        return Spit.negotiate_interfaces(min_host_interface,
+            max_host_interface,
+            Spit.Publishing.CURRENT_INTERFACE);
+    }
+
+    public unowned string get_id() {
+        return "publishing-gallery3";
+    }
+
+    public unowned string get_pluggable_name() {
+        return "Gallery3";
+    }
+
+    public void get_info(ref Spit.PluggableInfo info) {
+        info.authors = "Joe Sapp";
+        info.copyright = "2012-2013 Joe Sapp";
+        info.translators = Resources.TRANSLATORS;
+        info.version = G3_VERSION;
+        info.website_url = WEBSITE_URL;
+        info.is_license_wordwrapped = false;
+        info.license = G3_LICENSE;
+        info.icons = icon_pixbuf_set;
+    }
+
+    public void activation(bool enabled) {
+    }
+
+    public Spit.Publishing.Publisher create_publisher(
+            Spit.Publishing.PluginHost host) {
+        return new Publishing.Gallery3.GalleryPublisher(this, host);
+    }
+
+    public Spit.Publishing.Publisher.MediaType get_supported_media() {
+        return (Spit.Publishing.Publisher.MediaType.PHOTO |
+            Spit.Publishing.Publisher.MediaType.VIDEO);
+    }
+}
+
+
+namespace Publishing.Gallery3 {
+private const string SERVICE_NAME = "Gallery3";
+private const string SERVICE_WELCOME_MESSAGE =
+    _("You are not currently logged into your Gallery.\n\nYou must have already signed up for a Gallery3 account to complete the login process.");
+private const string DEFAULT_ALBUM_DIR = _("Shotwell");
+private const string DEFAULT_ALBUM_TITLE =
+    _("Shotwell default directory");
+private const string REST_PATH = "/index.php/rest";
+
+private class Album {
+
+    // Properties
+    public string name { get; private set; default = ""; }
+    public string title { get; private set; default = ""; }
+    public string summary { get; private set; default = ""; }
+    public string parentname { get; private set; default = ""; }
+    public string url { get; private set; default = ""; }
+    public string path { get; private set; default = ""; }
+    public bool editable { get; private set; default = false; }
+
+    // Each element is a collection
+    public Album(Json.Object collection) {
+
+        unowned Json.Object entity =
+            collection.get_object_member("entity");
+
+        title = entity.get_string_member("title");
+        name = entity.get_string_member("name");
+        parentname = entity.get_string_member("parent");
+        url = collection.get_string_member("url");
+        editable = entity.get_boolean_member("can_edit");
+
+        // Get the path from the last two elements of the URL.
+        // This should always be "/item/#" where "#" is a number.
+        path = strip_session_url(url);
+
+    }
+
+}
+
+private class BaseGalleryTransaction :
+        Publishing.RESTSupport.Transaction {
+
+    protected Json.Parser parser;
+
+    // BaseGalleryTransaction constructor
+    public BaseGalleryTransaction(Session session, string endpoint_url,
+            string item_path = "",
+            Publishing.RESTSupport.HttpMethod method =
+            Publishing.RESTSupport.HttpMethod.POST) {
+
+        // TODO: eventually we can remove this
+        if ((item_path != "") && (item_path[0] != '/')) {
+            warning("Bad item path, this is a bug!");
+            error(item_path);
+        }
+
+        base.with_endpoint_url(session,
+            endpoint_url + REST_PATH + item_path,
+            method);
+
+        this.parser = new Json.Parser();
+
+    }
+
+    protected unowned Json.Node get_root_node()
+            throws Spit.Publishing.PublishingError {
+
+        string json_object;
+        unowned Json.Node root_node;
+
+        json_object = get_response();
+
+        if ((null == json_object) || (0 == json_object.length))
+            throw new Spit.Publishing.PublishingError.MALFORMED_RESPONSE(
+                "No response data from %s", get_endpoint_url());
+
+        try {
+            this.parser.load_from_data(json_object);
+        }
+        catch (GLib.Error e) {
+            // If this didn't work, reset the "executed" state
+            warning("ERROR: didn't load JSON data");
+            set_is_executed(false);
+            throw new Spit.Publishing.PublishingError.PROTOCOL_ERROR(e.message);
+        }
+
+        root_node = this.parser.get_root();
+        if (root_node.is_null())
+            throw new Spit.Publishing.PublishingError.MALFORMED_RESPONSE(
+                "Root node is null, doesn't appear to be JSON data");
+
+        return root_node;
+
+    }
+
+}
+
+private class KeyFetchTransaction : BaseGalleryTransaction {
+
+    private string key = "";
+
+    // KeyFetchTransaction constructor
+    //
+    // url: Base gallery URL
+    public KeyFetchTransaction(Session session, string url,
+            string username, string password) {
+        base(session, url);
+        add_argument("user", username);
+        add_argument("password", password);
+    }
+
+    public string get_key() {
+
+        if (key != "")
+            return key;
+
+        key = get_response();
+
+        // The returned data isn't actually a JSON object...
+        if (null == key || "" == key || 0 == key.length) {
+            warning("No response data from \"%s\"", get_endpoint_url());
+            return "";
+        }
+
+        // Eliminate quotes surrounding key
+        key = key[1:-1];
+
+        return key;
+    }
+
+}
+
+private class GalleryRequestTransaction : BaseGalleryTransaction {
+
+    // GalleryRequestTransaction constructor
+    //
+    // item: Item URL component
+    public GalleryRequestTransaction(Session session, string item,
+            Publishing.RESTSupport.HttpMethod method =
+            Publishing.RESTSupport.HttpMethod.GET) {
+
+        if (!session.is_authenticated()) {
+            error("Not authenticated");
+        }
+        else {
+            base(session, session.url, item, method);
+            add_header("X-Gallery-Request-Key", session.key);
+            add_header("X-Gallery-Request-Method", "GET");
+        }
+
+    }
+
+}
+
+private class GetAlbumURLsTransaction : GalleryRequestTransaction {
+
+    public GetAlbumURLsTransaction(Session session) {
+
+        base(session, "/item/1");
+        add_argument("type", "album");
+        add_argument("scope", "all");
+
+    }
+
+    public string [] get_album_urls() {
+
+        unowned Json.Node root_node;
+        unowned Json.Array all_members;
+
+        try {
+            root_node = get_root_node();
+        }
+        catch (Spit.Publishing.PublishingError e) {
+            error("Could not get root node");
+        }
+
+        all_members =
+            root_node.get_object().get_array_member("members");
+
+        string [] member_urls = null;
+
+        for (uint i = 0; i <= all_members.get_length() - 1; i++)
+            member_urls += all_members.get_string_element(i);
+
+        return member_urls;
+
+    }
+
+}
+
+private class GetAlbumsTransaction : GalleryRequestTransaction {
+
+    // Properties
+    // Original list of album URLs
+    public string [] album_urls { get; private set; default = null; }
+    // How many URLs have been sent?
+    public uint urls_sent { get; private set; default = 0; }
+    // Are there (possibly) more URLs to send?
+    public bool more_urls { get; private set; default = false; }
+
+    public GetAlbumsTransaction(Session session, string [] _album_urls,
+                                uint start = 0) {
+
+        base(session, "/items");
+        add_argument("scope", "all");
+
+        // Save original list of URLs
+        album_urls = _album_urls;
+
+        // Wrap each URL in double quotes and separate by a comma, but
+        // we should try to keep the length of the URL under 255
+        // characters.  We need to do this to avoid problems with URLs
+        // that are too long on some web servers (and, really, if there
+        // are alot of albums, this can get large quickly).
+        // The Gallery3 API should probably allow this in a POST
+        // transaction...
+        string url_list = "[";
+        string [] my_album_urls = null;
+        string? endpoint_url = session.get_endpoint_url();
+        int url_length = (null != endpoint_url) ?
+            endpoint_url.length : 0;
+        url_length += 18; // for: ?scope=all&urls=[]
+
+        // We have to allow at least one URL at a time
+        if (start <= album_urls.length - 1) {
+
+            urls_sent = start;
+            do {
+                my_album_urls += "\"" + album_urls[urls_sent] + "\"";
+                // Add 3 for: "",
+                url_length += album_urls[urls_sent].length + 3;
+                urls_sent++;
+            } while ((urls_sent <= album_urls.length - 1) &&
+                     (url_length +
+                      album_urls[urls_sent].length + 3 <= 255));
+            url_list += string.joinv(",", my_album_urls);
+
+            more_urls = (urls_sent <= (album_urls.length - 1));
+
+        }
+        url_list += "]";
+
+        add_argument("urls", url_list);
+
+    }
+
+    public Album [] get_albums()
+            throws Spit.Publishing.PublishingError {
+
+        Album [] albums = null;
+        Album tmp_album;
+        unowned Json.Node root_node = get_root_node();
+        unowned Json.Array members = root_node.get_array();
+
+        // Only add editable items
+        for (uint i = 0; i <= members.get_length() - 1; i++) {
+            tmp_album = new Album(members.get_object_element(i));
+
+            if (tmp_album.editable)
+                albums += tmp_album;
+            else
+                warning(@"Album \"$(tmp_album.title)\" is not editable");
+        }
+
+        return albums;
+    }
+
+}
+
+// Class to create or get a tag URL.
+// Tag URLs are placed in the "item_tags" object and relate an item and
+// its tags.
+private class GalleryGetTagTransaction : BaseGalleryTransaction {
+
+    public GalleryGetTagTransaction(Session session, string tag_name) {
+
+        if (!session.is_authenticated()) {
+            error("Not authenticated");
+        }
+        else {
+            Json.Generator entity = new Json.Generator();
+            Json.Node root_node = new Json.Node(Json.NodeType.OBJECT);
+            Json.Object obj = new Json.Object();
+
+            base(session, session.url,
+                "/tags",
+                Publishing.RESTSupport.HttpMethod.POST);
+            add_header("X-Gallery-Request-Key", session.key);
+            add_header("X-Gallery-Request-Method", "POST");
+
+            obj.set_string_member("name", tag_name);
+            root_node.set_object(obj);
+            entity.set_root(root_node);
+
+            size_t entity_length;
+            string entity_value = entity.to_data(out entity_length);
+
+            debug("created entity: %s", entity_value);
+
+            add_argument("entity", entity_value);
+        }
+
+    }
+
+    public string tag_url() {
+
+        unowned Json.Node root_node;
+        string url;
+
+        try {
+            root_node = get_root_node();
+        }
+        catch (Spit.Publishing.PublishingError e) {
+            error("Could not get root node");
+        }
+
+        url =
+            root_node.get_object().get_string_member("url");
+
+        return url;
+
+    }
+
+}
+
+// Get the item_tags URL for a given item
+private class GalleryGetItemTagsURLsTransaction :
+        GalleryRequestTransaction {
+
+    private string item_tags_path = "";
+
+    public GalleryGetItemTagsURLsTransaction(Session session,
+            string item_url) {
+
+        base(session, item_url);
+
+    }
+
+    public string get_item_tags_path() {
+
+        unowned Json.Node root_node;
+        unowned Json.Object relationships, tags;
+
+        if ("" == item_tags_path) {
+
+            try {
+                root_node = get_root_node();
+            }
+            catch (Spit.Publishing.PublishingError e) {
+                error("Could not get root node");
+            }
+
+            relationships =
+                root_node.get_object().get_object_member("relationships");
+            tags = relationships.get_object_member("tags");
+
+            item_tags_path = tags.get_string_member("url");
+
+            // Remove the session URL from the beginning of this URL
+            item_tags_path = strip_session_url(item_tags_path);
+
+        }
+
+        return item_tags_path;
+
+    }
+
+}
+
+// Set a tag relationship with an item
+private class GallerySetTagRelationshipTransaction :
+        BaseGalleryTransaction {
+
+    public GallerySetTagRelationshipTransaction(Session session,
+            string item_tags_path, string tag_url, string item_url) {
+
+        if (!session.is_authenticated()) {
+            error("Not authenticated");
+        }
+        else {
+            Json.Generator entity = new Json.Generator();
+            Json.Node root_node = new Json.Node(Json.NodeType.OBJECT);
+            Json.Object obj = new Json.Object();
+
+            base(session, session.url,
+                item_tags_path,
+                Publishing.RESTSupport.HttpMethod.POST);
+            add_header("X-Gallery-Request-Key", session.key);
+            add_header("X-Gallery-Request-Method", "POST");
+
+            obj.set_string_member("tag", tag_url);
+            obj.set_string_member("item", item_url);
+            root_node.set_object(obj);
+            entity.set_root(root_node);
+
+            size_t entity_length;
+            string entity_value = entity.to_data(out entity_length);
+
+            debug("created entity: %s", entity_value);
+
+            add_argument("entity", entity_value);
+        }
+
+    }
+
+}
+
+private class GalleryAlbumCreateTransaction : BaseGalleryTransaction {
+
+    // Properties
+    public PublishingParameters parameters { get; private set; }
+    // Private variables
+    private string? session_url;
+
+    // GalleryAlbumCreateTransaction constructor
+    //
+    // parameters: New album parameters
+    public GalleryAlbumCreateTransaction(Session session,
+            PublishingParameters parameters) {
+
+        if (!session.is_authenticated()) {
+            error("Not authenticated");
+        }
+        else {
+            Json.Generator entity = new Json.Generator();
+            Json.Node root_node = new Json.Node(Json.NodeType.OBJECT);
+            Json.Object obj = new Json.Object();
+
+            base(session, session.url, "/item/1",
+                Publishing.RESTSupport.HttpMethod.POST);
+            add_header("X-Gallery-Request-Key", session.key);
+            add_header("X-Gallery-Request-Method", "POST");
+
+            this.session_url = session.url;
+            this.parameters = parameters;
+
+            obj.set_string_member("name", parameters.album_name);
+            obj.set_string_member("type", "album");
+            obj.set_string_member("title", parameters.album_title);
+            root_node.set_object(obj);
+            entity.set_root(root_node);
+
+            string entity_value = entity.to_data(null);
+
+            debug("created entity: %s", entity_value);
+
+            add_argument("entity", entity_value);
+        }
+
+    }
+
+    public string get_new_album_path() {
+
+        unowned Json.Node root_node;
+        string new_path;
+
+        try {
+            root_node = get_root_node();
+        }
+        catch (Spit.Publishing.PublishingError e) {
+            error("Could not get root node");
+        }
+
+        new_path =
+            root_node.get_object().get_string_member("url");
+        new_path = strip_session_url(new_path);
+
+        return new_path;
+
+    }
+
+}
+
+private class GalleryUploadTransaction :
+        Publishing.RESTSupport.UploadTransaction {
+
+    private Session session;
+    private Json.Generator generator;
+    private PublishingParameters parameters;
+    private string item_url;
+    private string item_path;
+    private string item_tags_path;
+
+    public GalleryUploadTransaction(Session session,
+            PublishingParameters parameters,
+            Spit.Publishing.Publishable publishable) {
+
+        // TODO: eventually we can remove this
+        if (parameters.album_path[0] != '/') {
+            warning("Bad upload item path, this is a bug!");
+            error(parameters.album_path);
+        }
+
+        base.with_endpoint_url(session, publishable,
+            session.url + REST_PATH + parameters.album_path);
+
+        this.parameters = parameters;
+        this.session = session;
+
+        add_header("X-Gallery-Request-Key", session.key);
+        add_header("X-Gallery-Request-Method", "POST");
+
+        GLib.HashTable<string, string> disposition_table =
+            new GLib.HashTable<string, string>(GLib.str_hash,
+                                               GLib.str_equal);
+        string? title = publishable.get_publishing_name();
+        string filename = publishable.get_param_string(
+            Spit.Publishing.Publishable.PARAM_STRING_BASENAME);
+        if (title == null || title == "")
+            //TODO: remove extension?
+            title = filename;
+
+        disposition_table.insert("filename", @"$(filename)");
+        disposition_table.insert("name", "file");
+
+        set_binary_disposition_table(disposition_table);
+
+        // Do the JSON stuff
+        generator = new Json.Generator();
+        string desc = publishable.get_param_string(
+            Spit.Publishing.Publishable.PARAM_STRING_COMMENT);
+        string type = (publishable.get_media_type() ==
+            Spit.Publishing.Publisher.MediaType.VIDEO) ?
+                "movie" : "photo";
+
+        Json.Node root_node = new Json.Node(Json.NodeType.OBJECT);
+        Json.Object obj = new Json.Object();
+        obj.set_string_member("name", filename);
+        obj.set_string_member("type", type);
+        obj.set_string_member("title", title);
+        obj.set_string_member("description", desc);
+
+        root_node.set_object(obj);
+        generator.set_root(root_node);
+
+        add_argument("entity", generator.to_data(null));
+    }
+
+    private string get_new_item_url() {
+
+        string json_object;
+        string new_url;
+        unowned Json.Node root_node;
+        Json.Parser parser = new Json.Parser();
+
+        json_object = get_response();
+
+        if ((null == json_object) || (0 == json_object.length)) {
+            warning("No response data from %s", get_endpoint_url());
+            return "";
+        }
+
+        debug("json_object: %s", json_object);
+
+        try {
+            parser.load_from_data(json_object);
+        }
+        catch (GLib.Error e) {
+            // If this didn't work, reset the "executed" state
+            // TODO: can we recover from this?
+            warning("ERROR: didn't load JSON data");
+            set_is_executed(false);
+            error(e.message);
+        }
+
+        root_node = parser.get_root();
+        if (root_node.is_null()) {
+            warning("Root node is null, doesn't appear to be JSON data");
+            return "";
+        }
+
+        new_url =
+            root_node.get_object().get_string_member("url");
+
+        return new_url;
+
+    }
+
+    private void do_set_tag_relationship(string tag_url)
+            throws Spit.Publishing.PublishingError {
+        GallerySetTagRelationshipTransaction tag_txn =
+            new GallerySetTagRelationshipTransaction(
+                (Session) get_parent_session(), item_tags_path,
+                tag_url, item_url);
+
+        tag_txn.execute();
+
+        debug("Response from setting tag relationship: %s",
+            tag_txn.get_response());
+    }
+
+    private string get_new_item_tags_path() {
+        GalleryGetItemTagsURLsTransaction tag_urls_txn =
+            new GalleryGetItemTagsURLsTransaction(
+                (Session) get_parent_session(), item_path);
+
+        try {
+            tag_urls_txn.execute();
+        } catch (Spit.Publishing.PublishingError err) {
+            debug("Problem getting the item_tags URL: %s",
+                err.message);
+            return "";
+        }
+
+        return tag_urls_txn.get_item_tags_path();
+    }
+
+    private string get_tag_url(string tag) {
+
+        GalleryGetTagTransaction tag_txn =
+            new GalleryGetTagTransaction(
+                (Session) get_parent_session(), tag);
+
+        try {
+            tag_txn.execute();
+        } catch (Spit.Publishing.PublishingError err) {
+            debug("Problem getting the tags URL: %s",
+                err.message);
+            return "";
+        }
+
+        return tag_txn.tag_url();
+
+    }
+
+    private void on_upload_completed()
+            throws Spit.Publishing.PublishingError {
+
+        debug("EVENT: upload completed");
+
+        if (!parameters.strip_metadata) {
+
+            string[] keywords;
+
+            debug("EVENT: evaluating tags");
+
+            keywords = base.publishable.get_publishing_keywords();
+
+            // If this publishable has no tags, continue
+            if (null == keywords) {
+                debug("No tags");
+                return;
+            }
+
+            // Get URLs from the file we just finished uploading
+            item_url = get_new_item_url();
+            item_path = strip_session_url(item_url);
+            item_tags_path = get_new_item_tags_path();
+            debug("new item path is %s", item_path);
+            debug("item_tags path is %s", item_tags_path);
+
+            // Verify these aren't empty
+            if (("" == item_path) || ("" == item_tags_path)) {
+                throw new
+                    Spit.Publishing.PublishingError.COMMUNICATION_FAILED(
+                        "Could not obtain URL of uploaded item or its " +
+                        "\"item_tags\" relationship URL");
+            }
+
+            // Do the tagging here
+            foreach (string tag in keywords) {
+                debug(@"Found tag: $(tag)");
+                string new_tag_url = get_tag_url(tag);
+
+                try {
+                    do_set_tag_relationship(new_tag_url);
+                } catch (Spit.Publishing.PublishingError err) {
+                    debug("Problem setting the relationship between tag " +
+                        "and item: %s", err.message);
+                    throw err;
+                }
+            }
+
+        }
+
+    }
+
+    public override void execute()
+            throws Spit.Publishing.PublishingError {
+        base.execute();
+
+        // Run tagging operations here
+        on_upload_completed();
+    }
+
+}
+
+
+public class GalleryPublisher : Spit.Publishing.Publisher, GLib.Object {
+    private const string BAD_FILE_MSG = _("\n\nThe file \"%s\" may not be supported by or may be too large for this instance of Gallery3.");
+    private const string BAD_MOVIE_MSG = _("\nNote that Gallery3 only supports the video types that Flowplayer does.");
+
+    private weak Spit.Publishing.PluginHost host = null;
+    private Spit.Publishing.ProgressCallback progress_reporter = null;
+    private weak Spit.Publishing.Service service = null;
+    private Session session = null;
+    private bool running = false;
+    private Album[] albums = null;
+    private string key = null;
+
+    private PublishingOptionsPane publishing_options_pane = null;
+
+    public GalleryPublisher(Spit.Publishing.Service service,
+            Spit.Publishing.PluginHost host) {
+        this.service = service;
+        this.host = host;
+        this.session = new Session();
+    }
+
+    public bool is_running() {
+        return running;
+    }
+
+    public Spit.Publishing.Service get_service() {
+        return service;
+    }
+
+    public void start() {
+        if (is_running())
+            return;
+
+        if (host == null)
+            error("GalleryPublisher: start( ): can't start; this " +
+              "publisher is not restartable.");
+
+        debug("GalleryPublisher: starting interaction.");
+
+        running = true;
+
+        key = get_api_key();
+
+        if ((null == key) || ("" == key))
+            do_show_service_welcome_pane();
+        else {
+            string url = get_gallery_url();
+            string username = get_gallery_username();
+
+            if ((null == username) || (null == key) || (null == url))
+                do_show_service_welcome_pane();
+            else {
+                debug("ACTION: attempting network login for user " +
+                    "'%s' at URL '%s' from saved credentials.",
+                    username, url);
+
+                host.install_account_fetch_wait_pane();
+
+                session.authenticate(url, username, key);
+
+                // Initiate an album transaction
+                do_fetch_album_urls();
+            }
+        }
+    }
+
+    public void stop() {
+        debug("GalleryPublisher: stop( ) invoked.");
+
+        running = false;
+    }
+
+    // Config getters/setters
+    // API key
+    internal string? get_api_key() {
+        return host.get_config_string("api-key", null);
+    }
+
+    internal void set_api_key(string key) {
+        host.set_config_string("api-key", key);
+    }
+
+    // URL
+    internal string? get_gallery_url() {
+        return host.get_config_string("url", null);
+    }
+
+    internal void set_gallery_url(string url) {
+        host.set_config_string("url", url);
+    }
+
+    // Username
+    internal string? get_gallery_username() {
+        return host.get_config_string("username", null);
+    }
+
+    internal void set_gallery_username(string username) {
+        host.set_config_string("username", username);
+    }
+
+    internal bool? get_persistent_strip_metadata() {
+        return host.get_config_bool("strip-metadata", false);
+    }
+
+    internal void set_persistent_strip_metadata(bool strip_metadata) {
+        host.set_config_bool("strip-metadata", strip_metadata);
+    }
+
+    internal int? get_scaling_constraint_id() {
+        return host.get_config_int("scaling-constraint-id", 0);
+    }
+
+    internal void set_scaling_constraint_id(int constraint) {
+        host.set_config_int("scaling-constraint-id", constraint);
+    }
+
+    internal int? get_scaling_pixels() {
+        return host.get_config_int("scaling-pixels", 1024);
+    }
+
+    internal void set_scaling_pixels(int pixels) {
+        host.set_config_int("scaling-pixels", pixels);
+    }
+
+    // Pane installation functions
+    private void do_show_service_welcome_pane() {
+        debug("ACTION: showing service welcome pane.");
+
+        host.install_welcome_pane(SERVICE_WELCOME_MESSAGE,
+          on_service_welcome_login);
+    }
+
+    private void do_show_credentials_pane(CredentialsPane.Mode mode) {
+        debug("ACTION: showing credentials capture pane in %s mode.",
+          mode.to_string());
+
+        session.deauthenticate();
+
+        CredentialsPane creds_pane =
+            new CredentialsPane(host, mode, get_gallery_url(),
+                get_gallery_username(), get_api_key());
+        creds_pane.go_back.connect(on_credentials_go_back);
+        creds_pane.login.connect(on_credentials_login);
+
+        host.install_dialog_pane(creds_pane);
+    }
+
+    private void do_network_login(string url, string username,
+            string password) {
+        debug("ACTION: attempting network login for user '%s' at URL " +
+            "'%s'.", username, url);
+
+        host.install_login_wait_pane();
+
+        KeyFetchTransaction fetch_trans =
+            new KeyFetchTransaction(session, url, username, password);
+        fetch_trans.network_error.connect(on_key_fetch_error);
+        fetch_trans.completed.connect(on_key_fetch_complete);
+
+        try {
+            fetch_trans.execute();
+        } catch (Spit.Publishing.PublishingError err) {
+            debug("Caught an error attempting to login");
+            // 403 errors may be recoverable, so don't post the error to
+            // our host immediately; instead, try to recover from it
+            on_key_fetch_error(fetch_trans, err);
+        }
+    }
+
+    private void do_fetch_album_urls() {
+
+        host.install_account_fetch_wait_pane();
+
+        GetAlbumURLsTransaction album_trans =
+            new GetAlbumURLsTransaction(session);
+        album_trans.network_error.connect(on_album_urls_fetch_error);
+        album_trans.completed.connect(on_album_urls_fetch_complete);
+
+        try {
+            album_trans.execute();
+        } catch (Spit.Publishing.PublishingError err) {
+            debug("Caught an error attempting to fetch albums");
+            // 403 errors may be recoverable, so don't post the error to
+            // our host immediately; instead, try to recover from it
+            on_album_urls_fetch_error(album_trans, err);
+        }
+
+    }
+
+    private void do_fetch_albums(string [] album_urls, uint start = 0) {
+
+        GetAlbumsTransaction album_trans =
+            new GetAlbumsTransaction(session, album_urls, start);
+        album_trans.network_error.connect(on_album_fetch_error);
+        album_trans.completed.connect(on_album_fetch_complete);
+
+        try {
+            album_trans.execute();
+        } catch (Spit.Publishing.PublishingError err) {
+            // 403 errors may be recoverable, so don't post the error to
+            // our host immediately; instead, try to recover from it
+            on_album_fetch_error(album_trans, err);
+        }
+
+    }
+
+    private void do_show_publishing_options_pane(string url,
+            string username) {
+
+        debug("ACTION: showing publishing options pane");
+
+        Gtk.Builder builder = new Gtk.Builder();
+
+        try {
+            builder.add_from_file(
+                host.get_module_file().get_parent().get_child(
+                    "gallery3_publishing_options_pane.glade").get_path());
+        }
+        catch (Error e) {
+            warning("Could not parse UI file! Error: %s.", e.message);
+            host.post_error(
+                new Spit.Publishing.PublishingError.LOCAL_FILE_ERROR(
+                    _("A file required for publishing is " +
+                        "unavailable. Publishing to " + SERVICE_NAME +
+                        " can't continue.")));
+            return;
+        }
+
+        publishing_options_pane =
+            new PublishingOptionsPane(host, url, username, albums,
+                builder, get_persistent_strip_metadata(),
+                get_scaling_constraint_id(), get_scaling_pixels());
+        publishing_options_pane.publish.connect(
+            on_publishing_options_pane_publish);
+        publishing_options_pane.logout.connect(
+            on_publishing_options_pane_logout);
+        host.install_dialog_pane(publishing_options_pane);
+
+    }
+
+    private void do_create_album(PublishingParameters parameters) {
+
+        debug("ACTION: creating album");
+
+        GalleryAlbumCreateTransaction album_trans =
+            new GalleryAlbumCreateTransaction(session, parameters);
+        album_trans.network_error.connect(on_album_create_error);
+        album_trans.completed.connect(on_album_create_complete);
+
+        try {
+            album_trans.execute();
+        } catch (Spit.Publishing.PublishingError err) {
+            // 403 errors may be recoverable, so don't post the error to
+            // our host immediately; instead, try to recover from it
+            on_album_create_error(album_trans, err);
+        }
+
+    }
+
+    private void do_publish(PublishingParameters parameters) {
+
+        debug("ACTION: publishing items");
+
+        set_persistent_strip_metadata(parameters.strip_metadata);
+        set_scaling_constraint_id(
+            (parameters.photo_major_axis_size <= 0) ? 0 : 1);
+        set_scaling_pixels(parameters.photo_major_axis_size);
+        host.set_service_locked(true);
+        progress_reporter =
+            host.serialize_publishables(parameters.photo_major_axis_size,
+                parameters.strip_metadata);
+
+        // Serialization is a long and potentially cancellable
+        // operation, so before we use the publishables, make sure that
+        // the publishing interaction is still running. If it isn't, the
+        // publishing environment may be partially torn down so do a
+        // short-circuit return.
+        if (!is_running())
+            return;
+
+        Uploader uploader =
+            new Uploader(session, host.get_publishables(),
+                parameters);
+        uploader.upload_complete.connect(on_publish_complete);
+        uploader.upload_error.connect(on_publish_error);
+        uploader.upload(on_upload_status_updated);
+
+    }
+
+    private void do_show_success_pane() {
+        debug("ACTION: showing success pane.");
+
+        host.set_service_locked(false);
+        host.install_success_pane();
+    }
+
+    // Callbacks
+    private void on_service_welcome_login() {
+        if (!is_running())
+            return;
+
+        debug("EVENT: user clicked 'Login' in welcome pane.");
+
+        do_show_credentials_pane(CredentialsPane.Mode.INTRO);
+    }
+
+    private void on_credentials_login(string url, string username,
+            string password) {
+        if (!is_running())
+            return;
+
+        debug("EVENT: user '%s' clicked 'Login' in credentials pane.",
+          username);
+
+        set_gallery_url(url);
+        set_gallery_username(username);
+        do_network_login(url, username, password);
+    }
+
+    private void on_credentials_go_back() {
+        if (!is_running())
+            return;
+
+        debug("EVENT: user is attempting to go back.");
+
+        do_show_service_welcome_pane();
+    }
+
+    private void on_key_fetch_error(
+            Publishing.RESTSupport.Transaction bad_txn,
+            Spit.Publishing.PublishingError err) {
+        bad_txn.completed.disconnect(on_key_fetch_complete);
+        bad_txn.network_error.disconnect(on_key_fetch_error);
+
+        if (!is_running())
+            return;
+
+        // ignore these events if the session is already auth'd
+        if (session.is_authenticated())
+            return;
+
+        debug("EVENT: network transaction to fetch key for login " +
+            "failed; response = '%s'.",
+            bad_txn.get_response());
+
+        // HTTP error 403 is invalid authentication -- if we get this
+        // error during key fetch then we can just show the login screen
+        // again with a retry message; if we get any error other than
+        // 403 though, we can't recover from it, so just post the error
+        // to the user
+        if (bad_txn.get_status_code() == 403) {
+            // TODO: can we give more detail on the problem?
+            do_show_credentials_pane(CredentialsPane.Mode.FAILED_RETRY);
+        }
+        else if (bad_txn.get_status_code() == 400) {
+            // This might not be a Gallery URL
+            // TODO: can we give more detail on the problem?
+            do_show_credentials_pane(CredentialsPane.Mode.NOT_GALLERY_URL);
+        }
+        else {
+            host.post_error(err);
+        }
+    }
+
+    private void on_key_fetch_complete(
+            Publishing.RESTSupport.Transaction txn) {
+        txn.completed.disconnect(on_key_fetch_complete);
+        txn.network_error.disconnect(on_key_fetch_error);
+
+        if (!is_running())
+            return;
+
+        // ignore these events if the session is already auth'd
+        if (session.is_authenticated())
+            return;
+
+        key = (txn as KeyFetchTransaction).get_key();
+
+        if (key == null) error("key doesn\'t exist");
+        else {
+            string url = get_gallery_url();
+            string username = get_gallery_username();
+
+            debug("EVENT: network transaction to fetch key completed " +
+                  "successfully.");
+
+            set_api_key(key);
+            session.authenticate(url, username, key);
+
+            // Initiate an album transaction
+            do_fetch_album_urls();
+        }
+    }
+
+    private void on_album_urls_fetch_error(
+            Publishing.RESTSupport.Transaction bad_txn,
+            Spit.Publishing.PublishingError err) {
+        bad_txn.completed.disconnect(on_album_urls_fetch_complete);
+        bad_txn.network_error.disconnect(on_album_urls_fetch_error);
+
+        if (!is_running())
+            return;
+
+        // ignore these events if the session is not auth'd
+        if (!session.is_authenticated())
+            return;
+
+        debug("EVENT: network transaction to fetch album URLs " +
+            "failed; response = \'%s\'.",
+            bad_txn.get_response());
+
+        // HTTP error 403 is invalid authentication -- if we get this
+        // error during key fetch then we can just show the login screen
+        // again with a retry message; if we get any error other than
+        // 403 though, we can't recover from it, so just post the error
+        // to the user
+        if (bad_txn.get_status_code() == 403) {
+            // TODO: can we give more detail on the problem?
+            do_show_credentials_pane(CredentialsPane.Mode.FAILED_RETRY);
+        }
+        else if (bad_txn.get_status_code() == 400) {
+            // This might not be a Gallery URL
+            // TODO: can we give more detail on the problem?
+            do_show_credentials_pane(CredentialsPane.Mode.NOT_GALLERY_URL);
+        }
+        else {
+            host.post_error(err);
+        }
+    }
+
+    private void on_album_urls_fetch_complete(
+            Publishing.RESTSupport.Transaction txn) {
+        txn.completed.disconnect(on_album_urls_fetch_complete);
+        txn.network_error.disconnect(on_album_urls_fetch_error);
+
+        if (!is_running())
+            return;
+
+        // ignore these events if the session is not auth'd
+        if (!session.is_authenticated())
+            return;
+
+        debug("EVENT: retrieving all album URLs.");
+
+        string [] album_urls =
+            (txn as GetAlbumURLsTransaction).get_album_urls();
+
+        if (null == album_urls) {
+
+            string url = session.url;
+            string username = session.username;
+
+            do_show_publishing_options_pane(url, username);
+
+        }
+        else
+            do_fetch_albums(album_urls);
+    }
+
+    private void on_album_fetch_error(
+            Publishing.RESTSupport.Transaction bad_txn,
+            Spit.Publishing.PublishingError err) {
+        bad_txn.completed.disconnect(on_album_fetch_complete);
+        bad_txn.network_error.disconnect(on_album_fetch_error);
+
+        if (!is_running())
+            return;
+
+        // ignore these events if the session is not auth'd
+        if (!session.is_authenticated())
+            return;
+
+        debug("EVENT: network transaction to fetch albums " +
+            "failed; response = \'%s\'.",
+            bad_txn.get_response());
+
+        // HTTP error 403 is invalid authentication -- if we get this
+        // error during key fetch then we can just show the login screen
+        // again with a retry message; if we get any error other than
+        // 403 though, we can't recover from it, so just post the error
+        // to the user
+        if (bad_txn.get_status_code() == 403) {
+            // TODO: can we give more detail on the problem?
+            do_show_credentials_pane(CredentialsPane.Mode.FAILED_RETRY);
+        }
+        else if (bad_txn.get_status_code() == 400) {
+            // This might not be a Gallery URL
+            // TODO: can we give more detail on the problem?
+            do_show_credentials_pane(CredentialsPane.Mode.NOT_GALLERY_URL);
+        }
+        else {
+            host.post_error(err);
+        }
+    }
+
+    private void on_album_fetch_complete(
+            Publishing.RESTSupport.Transaction txn) {
+        txn.completed.disconnect(on_album_fetch_complete);
+        txn.network_error.disconnect(on_album_fetch_error);
+
+        Album[] new_albums = null;
+
+        if (!is_running())
+            return;
+
+        // ignore these events if the session is not auth'd
+        if (!session.is_authenticated())
+            return;
+
+        debug("EVENT: user is attempting to populate the album list.");
+
+        try {
+            new_albums =
+                (txn as GetAlbumsTransaction).get_albums();
+        } catch (Spit.Publishing.PublishingError err) {
+            on_album_fetch_error(txn, err);
+        }
+
+        // Append new albums to existing
+        for (int i = 0; i <= new_albums.length - 1; i++)
+            albums += new_albums[i];
+
+        if ((txn as GetAlbumsTransaction).more_urls) {
+
+            do_fetch_albums((txn as GetAlbumsTransaction).album_urls,
+                (txn as GetAlbumsTransaction).urls_sent);
+
+        }
+        else {
+
+            string url = session.url;
+            string username = session.username;
+
+            do_show_publishing_options_pane(url, username);
+
+        }
+    }
+
+    private void on_album_create_error(
+            Publishing.RESTSupport.Transaction bad_txn,
+            Spit.Publishing.PublishingError err) {
+        bad_txn.completed.disconnect(on_album_create_complete);
+        bad_txn.network_error.disconnect(on_album_create_error);
+
+        if (!is_running())
+            return;
+
+        // ignore these events if the session is not auth'd
+        if (!session.is_authenticated())
+            return;
+
+        debug("EVENT: network transaction to create an album " +
+            "failed; response = \'%s\'.",
+            bad_txn.get_response());
+
+        // HTTP error 403 is invalid authentication -- if we get this
+        // error during key fetch then we can just show the login screen
+        // again with a retry message; if we get any error other than
+        // 403 though, we can't recover from it, so just post the error
+        // to the user
+        if (bad_txn.get_status_code() == 403) {
+            // TODO: can we give more detail on the problem?
+            do_show_credentials_pane(CredentialsPane.Mode.FAILED_RETRY);
+        }
+        else if (bad_txn.get_status_code() == 400) {
+            // This might not be a Gallery URL
+            // TODO: can we give more detail on the problem?
+            do_show_credentials_pane(CredentialsPane.Mode.NOT_GALLERY_URL);
+        }
+        else {
+            host.post_error(err);
+        }
+    }
+
+    private void on_album_create_complete(
+            Publishing.RESTSupport.Transaction txn) {
+        txn.completed.disconnect(on_album_create_complete);
+        txn.network_error.disconnect(on_album_create_error);
+
+        if (!is_running())
+            return;
+
+        // ignore these events if the session is not auth'd
+        if (!session.is_authenticated())
+            return;
+
+        PublishingParameters new_params =
+            (txn as GalleryAlbumCreateTransaction).parameters;
+        new_params.album_path =
+            (txn as GalleryAlbumCreateTransaction).get_new_album_path();
+
+        debug("EVENT: user has created an album at \"%s\".",
+            new_params.album_path);
+
+        do_publish(new_params);
+    }
+
+    private void on_publish_error(
+            Publishing.RESTSupport.BatchUploader _uploader,
+            Spit.Publishing.PublishingError err) {
+        if (!is_running())
+            return;
+
+        Uploader uploader = _uploader as Uploader;
+        GLib.Error g3_err = err.copy();
+
+        debug("EVENT: uploader reports upload error = '%s' " +
+            "for file '%s' (code %d)", err.message,
+                uploader.current_publishable_name, uploader.status_code);
+
+        uploader.upload_complete.disconnect(on_publish_complete);
+        uploader.upload_error.disconnect(on_publish_error);
+
+        // Is this a 400 error? Then it may be a bad file.
+        if (uploader.status_code == 400) {
+            g3_err.message +=
+                BAD_FILE_MSG.printf(uploader.current_publishable_name);
+            // Add an additional message if this appears to be a video
+            // file.
+            if (uploader.current_publishable_type ==
+                    Spit.Publishing.Publisher.MediaType.VIDEO)
+                g3_err.message += BAD_MOVIE_MSG;
+        }
+        host.post_error(g3_err);
+    }
+
+    private void on_upload_status_updated(int file_number,
+        double completed_fraction) {
+
+        if (!is_running())
+            return;
+
+        debug("EVENT: uploader reports upload %.2f percent complete.",
+            100.0 * completed_fraction);
+
+        assert(progress_reporter != null);
+
+        progress_reporter(file_number, completed_fraction);
+
+    }
+
+    private void on_publish_complete(
+            Publishing.RESTSupport.BatchUploader uploader,
+            int num_published) {
+        uploader.upload_complete.disconnect(on_publish_complete);
+        uploader.upload_error.disconnect(on_publish_error);
+
+        if (!is_running())
+            return;
+
+        // ignore these events if the session is not auth'd
+        if (!session.is_authenticated())
+            return;
+
+        debug("EVENT: publishing complete; %d items published",
+            num_published);
+
+        do_show_success_pane();
+
+    }
+
+    private void on_publishing_options_pane_logout() {
+        publishing_options_pane.publish.disconnect(
+            on_publishing_options_pane_publish);
+        publishing_options_pane.logout.disconnect(
+            on_publishing_options_pane_logout);
+
+        if (!is_running())
+            return;
+
+        debug("EVENT: user is attempting to log out.");
+
+        session.deauthenticate();
+        do_show_service_welcome_pane();
+    }
+
+    private void on_publishing_options_pane_publish(PublishingParameters parameters) {
+        publishing_options_pane.publish.disconnect(
+            on_publishing_options_pane_publish);
+        publishing_options_pane.logout.disconnect(
+            on_publishing_options_pane_logout);
+
+        if (!is_running())
+            return;
+
+        debug("EVENT: user is attempting to publish something.");
+
+        if (parameters.is_to_new_album()) {
+            debug("EVENT: must create new album \"%s\" first.",
+                parameters.album_name);
+            do_create_album(parameters);
+        }
+        else {
+            do_publish(parameters);
+        }
+    }
+
+}
+
+internal class PublishingOptionsPane : Spit.Publishing.DialogPane, GLib.Object {
+    private const string DEFAULT_ALBUM_NAME = "";
+    private const string LAST_ALBUM_CONFIG_KEY = "last-album";
+
+    private Gtk.Builder builder = null;
+
+    private Gtk.Grid pane_widget = null;
+    private Gtk.Label title_label = null;
+    private Gtk.RadioButton use_existing_radio = null;
+    private Gtk.ComboBoxText existing_albums_combo = null;
+    private Gtk.RadioButton create_new_radio = null;
+    private Gtk.Entry new_album_entry = null;
+    private Gtk.ComboBoxText scaling_combo = null;
+    private Gtk.Entry pixels = null;
+    private Gtk.CheckButton strip_metadata_check = null;
+    private Gtk.Button publish_button = null;
+    private Gtk.Button logout_button = null;
+
+    private Album[] albums;
+    private weak Spit.Publishing.PluginHost host;
+
+    public signal void publish(PublishingParameters parameters);
+    public signal void logout();
+
+    public PublishingOptionsPane(Spit.Publishing.PluginHost host,
+            string url, string username, Album[] albums,
+            Gtk.Builder builder, bool strip_metadata,
+            int scaling_id, int scaling_pixels) {
+        this.albums = albums;
+        this.host = host;
+
+        this.builder = builder;
+        assert(null != builder);
+        assert(builder.get_objects().length() > 0);
+
+        // pull in all widgets from builder
+        pane_widget = builder.get_object("pane_widget") as Gtk.Grid;
+        title_label = builder.get_object("title_label") as Gtk.Label;
+        use_existing_radio = builder.get_object("publish_to_existing_radio") as Gtk.RadioButton;
+        existing_albums_combo = builder.get_object("existing_albums_combo") as Gtk.ComboBoxText;
+        scaling_combo = builder.get_object("scaling_constraint_combo") as Gtk.ComboBoxText;
+        pixels = builder.get_object("major_axis_pixels") as Gtk.Entry;
+        create_new_radio = builder.get_object("publish_new_radio") as Gtk.RadioButton;
+        new_album_entry = builder.get_object("new_album_name") as Gtk.Entry;
+        strip_metadata_check = this.builder.get_object("strip_metadata_check") as Gtk.CheckButton;
+        publish_button = builder.get_object("publish_button") as Gtk.Button;
+        logout_button = builder.get_object("logout_button") as Gtk.Button;
+
+        // populate any widgets whose contents are
+        // programmatically-generated
+        title_label.set_label(
+            _("Publishing to %s as %s.").printf(url, username));
+        strip_metadata_check.set_active(strip_metadata);
+        scaling_combo.set_active(scaling_id);
+        pixels.set_text(@"$(scaling_pixels)");
+
+        // connect all signals
+        use_existing_radio.clicked.connect(on_use_existing_radio_clicked);
+        create_new_radio.clicked.connect(on_create_new_radio_clicked);
+        new_album_entry.changed.connect(on_new_album_entry_changed);
+        scaling_combo.changed.connect(on_scaling_constraint_changed);
+        pixels.changed.connect(on_pixels_changed);
+        logout_button.clicked.connect(on_logout_clicked);
+        publish_button.clicked.connect(on_publish_clicked);
+    }
+
+    private void on_publish_clicked() {
+        string album_name;
+        int photo_major_axis_size =
+            (scaling_combo.get_active() == 1) ?
+                int.parse(pixels.get_text()) : -1;
+        PublishingParameters param;
+
+        if (create_new_radio.get_active()) {
+            album_name = new_album_entry.get_text();
+            host.set_config_string(LAST_ALBUM_CONFIG_KEY, album_name);
+            param =
+                new PublishingParameters.to_new_album(album_name);
+            debug("Trying to publish to \"%s\"", album_name);
+        } else {
+            album_name =
+                albums[existing_albums_combo.get_active()].title;
+            host.set_config_string(LAST_ALBUM_CONFIG_KEY, album_name);
+            string album_path =
+                albums[existing_albums_combo.get_active()].path;
+            param =
+                new PublishingParameters.to_existing_album(album_path);
+        }
+
+        param.photo_major_axis_size = photo_major_axis_size;
+        param.strip_metadata = strip_metadata_check.get_active();
+
+        publish(param);
+    }
+
+    private void on_use_existing_radio_clicked() {
+        existing_albums_combo.set_sensitive(true);
+        new_album_entry.set_sensitive(false);
+        existing_albums_combo.grab_focus();
+        update_publish_button_sensitivity();
+    }
+
+    private void on_create_new_radio_clicked() {
+        new_album_entry.set_sensitive(true);
+        existing_albums_combo.set_sensitive(false);
+        new_album_entry.grab_focus();
+        update_publish_button_sensitivity();
+    }
+
+    private void on_logout_clicked() {
+        logout();
+    }
+
+    private void update_publish_button_sensitivity() {
+        string album_name = new_album_entry.get_text();
+        publish_button.set_sensitive(!(album_name.strip() == "" &&
+            create_new_radio.get_active()));
+    }
+
+    private void on_new_album_entry_changed() {
+        update_publish_button_sensitivity();
+    }
+
+    private void update_pixel_entry_sensitivity() {
+        pixels.set_sensitive(scaling_combo.get_active() == 1);
+    }
+
+    private void on_scaling_constraint_changed() {
+        update_pixel_entry_sensitivity();
+    }
+
+    private void on_pixels_changed() {
+        string orig_text = pixels.get_text();
+        char last_char = orig_text[orig_text.length - 1];
+
+        if (orig_text.length > 0) {
+            if (!last_char.isdigit())
+                pixels.set_text(orig_text.substring(0,
+                    orig_text.length - 1));
+        }
+    }
+
+    public void installed() {
+        int default_album_id = -1;
+        string last_album =
+            host.get_config_string(LAST_ALBUM_CONFIG_KEY, "");
+        for (int i = 0; i <= albums.length - 1; i++) {
+            existing_albums_combo.append_text(albums[i].title);
+            if ((albums[i].title == last_album) ||
+                ((DEFAULT_ALBUM_NAME == albums[i].title) &&
+                    (-1 == default_album_id)))
+                default_album_id = i;
+        }
+
+        if (albums.length == 0) {
+            existing_albums_combo.set_sensitive(false);
+            use_existing_radio.set_sensitive(false);
+            create_new_radio.set_active(true);
+            new_album_entry.grab_focus();
+            new_album_entry.set_text(DEFAULT_ALBUM_NAME);
+        } else {
+            if (default_album_id >= 0) {
+                use_existing_radio.set_active(true);
+                existing_albums_combo.set_active(default_album_id);
+                new_album_entry.set_sensitive(false);
+            } else {
+                create_new_radio.set_active(true);
+                existing_albums_combo.set_active(0);
+                new_album_entry.set_text(DEFAULT_ALBUM_NAME);
+                new_album_entry.grab_focus();
+            }
+        }
+        update_publish_button_sensitivity();
+        update_pixel_entry_sensitivity();
+    }
+
+    public Gtk.Widget get_widget() {
+        return pane_widget;
+    }
+
+    public Spit.Publishing.DialogPane.GeometryOptions get_preferred_geometry() {
+        return Spit.Publishing.DialogPane.GeometryOptions.NONE;
+    }
+
+    public void on_pane_installed() {
+        installed();
+    }
+
+    public void on_pane_uninstalled() {
+    }
+}
+
+internal class PublishingParameters {
+
+    // Private variables for properties
+    private string _album_title = "";
+
+    // Properties
+    public string album_title {
+        get {
+            assert(is_to_new_album());
+            return _album_title;
+        }
+        private set { _album_title = value; }
+    }
+    public string album_name { get; private set; default = ""; }
+    public string album_path { get; set; default = ""; }
+    public string entity_title { get; private set; default = ""; }
+    public int photo_major_axis_size { get; set; default = -1; }
+    public bool strip_metadata { get; set; default = false; }
+
+    private PublishingParameters() {
+    }
+
+    public PublishingParameters.to_new_album(string album_title) {
+        this.album_name = album_title.delimit(" ", '-');
+        //this.album_name = this.album_name.delimit("\"\'", '');
+        this.album_title = album_title;
+    }
+
+    public PublishingParameters.to_existing_album(string album_path) {
+        this.album_path = album_path;
+    }
+
+    public bool is_to_new_album() {
+        return (album_name != "");
+    }
+}
+
+internal class CredentialsPane : Spit.Publishing.DialogPane, GLib.Object {
+    public enum Mode {
+        INTRO,
+        FAILED_RETRY,
+        NOT_GALLERY_URL;
+
+        public string to_string() {
+            switch (this) {
+                case Mode.INTRO:
+                    return "INTRO";
+
+                case Mode.FAILED_RETRY:
+                    return "FAILED_RETRY";
+
+                case Mode.NOT_GALLERY_URL:
+                    return "NOT_GALLERY_URL";
+
+                default:
+                    error("unrecognized CredentialsPane.Mode enumeration value");
+            }
+        }
+    }
+
+    private CredentialsGrid frame = null;
+    private Gtk.Widget grid_widget = null;
+
+    public signal void go_back();
+    public signal void login(string url, string uname, string password,
+        string key);
+
+    public CredentialsPane(Spit.Publishing.PluginHost host,
+            Mode mode = Mode.INTRO,
+            string? url = null, string? username = null,
+            string? key = null) {
+
+        Gtk.Builder builder = new Gtk.Builder();
+
+        try {
+            builder.add_from_file(
+                host.get_module_file().get_parent().get_child(
+                    "gallery3_authentication_pane.glade").get_path());
+        }
+        catch (Error e) {
+            warning("Could not parse UI file! Error: %s.", e.message);
+            host.post_error(
+                new Spit.Publishing.PublishingError.LOCAL_FILE_ERROR(
+                    _("A file required for publishing is " +
+                        "unavailable. Publishing to " + SERVICE_NAME +
+                        " can't continue.")));
+            return;
+        }
+
+        frame = new CredentialsGrid(host, mode, url, username, key, builder);
+        grid_widget = frame.pane_widget as Gtk.Widget;
+    }
+
+    protected void notify_go_back() {
+        go_back();
+    }
+
+    protected void notify_login(string url, string uname,
+            string password, string key) {
+        login(url, uname, password, key);
+    }
+
+    public Gtk.Widget get_widget() {
+        assert(null != grid_widget);
+        return grid_widget;
+    }
+
+    public Spit.Publishing.DialogPane.GeometryOptions get_preferred_geometry() {
+        return Spit.Publishing.DialogPane.GeometryOptions.NONE;
+    }
+
+    public void on_pane_installed() {
+        frame.go_back.connect(notify_go_back);
+        frame.login.connect(notify_login);
+
+        frame.installed();
+    }
+
+    public void on_pane_uninstalled() {
+        frame.go_back.disconnect(notify_go_back);
+        frame.login.disconnect(notify_login);
+    }
+}
+
+internal class CredentialsGrid : GLib.Object {
+    private const string INTRO_MESSAGE = _("Enter the URL for your Gallery3 site and the username and password (or API key) for your Gallery3 account.");
+    private const string FAILED_RETRY_MESSAGE = _("The username and password or API key were incorrect. To try again, re-enter your username and password below.");
+    private const string NOT_GALLERY_URL_MESSAGE = _("The URL entered does not appear to be the main directory of a Gallery3 instance. Please make sure you typed it correctly and it does not have any trailing components (e.g., index.php).");
+
+    public Gtk.Grid pane_widget { get; private set; default = null; }
+
+    private weak Spit.Publishing.PluginHost host = null;
+    private Gtk.Builder builder = null;
+    private Gtk.Label intro_message_label = null;
+    private Gtk.Entry url_entry = null;
+    private Gtk.Entry username_entry = null;
+    private Gtk.Entry password_entry = null;
+    private Gtk.Entry key_entry = null;
+    private Gtk.Button login_button = null;
+    private Gtk.Button go_back_button = null;
+    private string? url = null;
+    private string? username = null;
+    private string? key = null;
+
+    public signal void go_back();
+    public signal void login(string url, string username,
+        string password, string key);
+
+    public CredentialsGrid(Spit.Publishing.PluginHost host,
+            CredentialsPane.Mode mode = CredentialsPane.Mode.INTRO,
+            string? url = null, string? username = null,
+            string? key = null,
+            Gtk.Builder builder) {
+        this.host = host;
+        this.url = url;
+        this.key = key;
+        this.username = username;
+
+        this.builder = builder;
+        assert(builder != null);
+        assert(builder.get_objects().length() > 0);
+
+        // pull in all widgets from builder
+        pane_widget = builder.get_object("gallery3_auth_pane_widget") as Gtk.Grid;
+        intro_message_label = builder.get_object("intro_message_label") as Gtk.Label;
+        url_entry = builder.get_object("url_entry") as Gtk.Entry;
+        username_entry = builder.get_object("username_entry") as Gtk.Entry;
+        key_entry = builder.get_object("key_entry") as Gtk.Entry;
+        password_entry = builder.get_object("password_entry") as Gtk.Entry;
+        go_back_button = builder.get_object("go_back_button") as Gtk.Button;
+        login_button = builder.get_object("login_button") as Gtk.Button;
+
+        // Intro message
+        switch (mode) {
+            case CredentialsPane.Mode.INTRO:
+                intro_message_label.set_markup(INTRO_MESSAGE);
+            break;
+
+            case CredentialsPane.Mode.FAILED_RETRY:
+                intro_message_label.set_markup("<b>%s</b>\n\n%s".printf(_(
+                    "Unrecognized User"), FAILED_RETRY_MESSAGE));
+            break;
+
+            case CredentialsPane.Mode.NOT_GALLERY_URL:
+                intro_message_label.set_markup("<b>%s</b>\n\n%s".printf(
+                    _(SERVICE_NAME + " Site Not Found"),
+                    NOT_GALLERY_URL_MESSAGE));
+            break;
+
+            default:
+                error("Invalid CredentialsPane mode");
+        }
+
+        // Gallery URL
+        if (url != null) {
+            url_entry.set_text(url);
+            username_entry.grab_focus();
+        }
+        url_entry.changed.connect(on_url_or_username_changed);
+        // User name
+        if (username != null) {
+            username_entry.set_text(username);
+            password_entry.grab_focus();
+        }
+        username_entry.changed.connect(on_url_or_username_changed);
+
+        // Key
+        if (key != null) {
+            key_entry.set_text(key);
+            key_entry.grab_focus();
+        }
+        key_entry.changed.connect(on_url_or_username_changed);
+
+        // Buttons
+        go_back_button.clicked.connect(on_go_back_button_clicked);
+        login_button.clicked.connect(on_login_button_clicked);
+        login_button.set_sensitive((url != null) && (username != null));
+    }
+
+    private void on_login_button_clicked() {
+        login(url_entry.get_text(), username_entry.get_text(),
+            password_entry.get_text(), key_entry.get_text());
+    }
+
+    private void on_go_back_button_clicked() {
+        go_back();
+    }
+
+    private void on_url_or_username_changed() {
+        login_button.set_sensitive(
+            ((url_entry.get_text() != "") &&
+             (username_entry.get_text() != "")) ||
+            (key_entry.get_text() != ""));
+    }
+
+    public void installed() {
+        host.set_service_locked(false);
+
+        // TODO: following line necessary?
+        host.set_dialog_default_widget(login_button);
+    }
+}
+
+internal class Session : Publishing.RESTSupport.Session {
+
+    // Properties
+    public string? url { get; private set; default = null; }
+    public string? username { get; private set; default = null; }
+    public string? key { get; private set; default = null; }
+
+    public Session() {
+    }
+
+    public override bool is_authenticated() {
+        return (null != key);
+    }
+
+    public void authenticate(string gallery_url, string username, string key) {
+        this.url = gallery_url;
+        this.username = username;
+        this.key = key;
+
+        notify_authenticated();
+    }
+
+    public void deauthenticate() {
+        url = null;
+        username = null;
+        key = null;
+    }
+
+}
+
+internal class Uploader : Publishing.RESTSupport.BatchUploader {
+
+    private PublishingParameters parameters;
+    private string _current_publishable_name;
+    private Spit.Publishing.Publisher.MediaType _current_media_type;
+    private Publishing.RESTSupport.Transaction? _current_transaction;
+
+    /* Properties */
+    public string current_publishable_name {
+        get {
+            return _current_publishable_name;
+        }
+    }
+    public uint status_code {
+        get {
+            return _current_transaction.get_status_code();
+        }
+    }
+    public Spit.Publishing.Publisher.MediaType
+            current_publishable_type {
+        get {
+            return _current_media_type;
+        }
+    }
+
+    public Uploader(Session session,
+            Spit.Publishing.Publishable[] publishables,
+            PublishingParameters parameters) {
+
+        base(session, publishables);
+
+        this.parameters = parameters;
+
+    }
+
+    protected override Publishing.RESTSupport.Transaction
+            create_transaction(Spit.Publishing.Publishable publishable) {
+
+        Spit.Publishing.Publishable p = get_current_publishable();
+        _current_publishable_name =
+            p.get_param_string(Spit.Publishing.Publishable.PARAM_STRING_BASENAME);
+        _current_media_type = p.get_media_type();
+
+        _current_transaction =
+            new GalleryUploadTransaction((Session) get_session(),
+                parameters, p);
+        return _current_transaction;
+
+    }
+
+}
+
+private string strip_session_url(string url) {
+
+    // Remove the session URL from the beginning of this URL
+    debug("Searching for \"%s\" in \"%s\"",
+        REST_PATH, url);
+    int item_loc =
+        url.last_index_of(REST_PATH);
+
+    if (-1 == item_loc)
+        error("Did not find \"%s\" in the base of the new item " +
+            "URL \"%s\"", REST_PATH, url);
+
+    return url.substring(item_loc + REST_PATH.length);
+
+}
+
+}
+
+// vi:ts=4:sw=4:et
diff --git a/plugins/shotwell-publishing-extras/Makefile b/plugins/shotwell-publishing-extras/Makefile
index 51f649f..9259fbb 100644
--- a/plugins/shotwell-publishing-extras/Makefile
+++ b/plugins/shotwell-publishing-extras/Makefile
@@ -11,17 +11,26 @@ PLUGIN_PKGS := \
 	json-glib-1.0
 
 SRC_FILES := \
+	GalleryConnector.vala \
 	shotwell-publishing-extras.vala \
 	YandexPublishing.vala \
 	TumblrPublishing.vala \
+	RajcePublishing.vala \
 	../../src/util/string.vala \
 	../common/RESTSupport.vala
 
+# RC_FILES must also be added to ../plugins.mk to ensure they're installed properly
 RC_FILES := \
+	gallery3.png \
+	gallery3_authentication_pane.glade \
+	gallery3_publishing_options_pane.glade \
 	yandex_publish_model.glade \
 	tumblr.png \
 	tumblr_authentication_pane.glade \
-	tumblr_publishing_options_pane.glade
+	tumblr_publishing_options_pane.glade \
+	rajce.png \
+	rajce_authentication_pane.glade \
+	rajce_publishing_options_pane.glade
 
 include ../Makefile.plugin.mk
 
diff --git a/plugins/shotwell-publishing-extras/RajcePublishing.vala b/plugins/shotwell-publishing-extras/RajcePublishing.vala
new file mode 100644
index 0000000..8ae05c6
--- /dev/null
+++ b/plugins/shotwell-publishing-extras/RajcePublishing.vala
@@ -0,0 +1,1554 @@
+/* Copyright 2014 rajce.net
+ *
+ * This software is licensed under the GNU Lesser General Public License
+ * (version 2.1 or later).  See the COPYING file in this distribution.
+ */
+
+public class RajceService : Object, Spit.Pluggable, Spit.Publishing.Service
+{
+    private const string ICON_FILENAME = "rajce.png";
+
+    private static Gdk.Pixbuf[] icon_pixbuf_set = null;
+    
+    public RajceService(GLib.File resource_directory)
+	{
+        if (icon_pixbuf_set == null)
+            icon_pixbuf_set = Resources.load_icon_set(resource_directory.get_child(ICON_FILENAME));
+    }
+
+    public int get_pluggable_interface(int min_host_interface, int max_host_interface)
+	{
+        return Spit.negotiate_interfaces(min_host_interface, max_host_interface,
+            Spit.Publishing.CURRENT_INTERFACE);
+    }
+    
+    public unowned string get_id()
+	{
+        return "org.yorba.shotwell.publishing.rajce";
+    }
+    
+    public unowned string get_pluggable_name()
+	{
+        return "Rajce";
+    }
+    
+    public void get_info(ref Spit.PluggableInfo info)
+	{
+        info.authors = "rajce.net developers";
+        info.copyright = _("Copyright (C) 2013 rajce.net");
+        info.translators = Resources.TRANSLATORS;
+        info.version = _VERSION;
+        info.website_name = Resources.WEBSITE_NAME;
+        info.website_url = Resources.WEBSITE_URL;
+        info.is_license_wordwrapped = false;
+        info.license = Resources.LICENSE;
+        info.icons = icon_pixbuf_set;
+    }
+    
+    public Spit.Publishing.Publisher create_publisher(Spit.Publishing.PluginHost host)
+	{
+        return new Publishing.Rajce.RajcePublisher(this, host);
+    }
+
+    public Spit.Publishing.Publisher.MediaType get_supported_media()
+	{
+        return( Spit.Publishing.Publisher.MediaType.PHOTO /*| Spit.Publishing.Publisher.MediaType.VIDEO*/ );
+    }
+    
+    public void activation(bool enabled) {}
+}
+
+namespace Publishing.Rajce
+{
+
+public class RajcePublisher : Spit.Publishing.Publisher, GLib.Object
+{
+    private Spit.Publishing.PluginHost host = null;
+    private Spit.Publishing.ProgressCallback progress_reporter = null;
+    private Spit.Publishing.Service service = null;
+    private bool running = false;
+    private Session session;
+//    private string username = "";
+//    private string token = "";
+//    private int last_photo_size = -1;
+//    private bool hide_album = false;
+//    private bool show_album = true;
+//    private bool remember = false;
+//    private bool strip_metadata = false;
+    private Album[] albums = null;
+    private PublishingParameters parameters = null;
+    private Spit.Publishing.Publisher.MediaType media_type = Spit.Publishing.Publisher.MediaType.NONE;
+
+    public RajcePublisher(Spit.Publishing.Service service, Spit.Publishing.PluginHost host)
+	{
+        debug("RajcePublisher created.");
+        this.service = service;
+        this.host = host;
+        this.session = new Session();
+        
+        foreach(Spit.Publishing.Publishable p in host.get_publishables())
+            media_type |= p.get_media_type();
+    }
+    
+    private string get_rajce_url()
+	{
+        return "http://www.rajce.idnes.cz/liveAPI/index.php";
+    }
+
+	// Publisher interface implementation
+	
+    public Spit.Publishing.Service get_service() { return service; }
+    public Spit.Publishing.PluginHost get_host() { return host; }
+    public bool is_running() { return running; }
+    
+    public void start()
+	{
+        if (is_running())
+            return;
+        
+        debug("RajcePublisher: start");
+        running = true;
+        
+        if (session.is_authenticated())
+		{
+            debug("RajcePublisher: session is authenticated.");
+            do_fetch_albums();
+        }
+		else
+		{
+            debug("RajcePublisher: session is not authenticated.");
+            string? persistent_username = get_username();
+            string? persistent_token = get_token();
+            bool? persistent_remember = get_remember();
+            if (persistent_username != null && persistent_token != null)
+                do_network_login(persistent_username, persistent_token, persistent_remember );
+            else
+                do_show_authentication_pane();
+        }
+    }
+    
+    public void stop()
+	{
+        debug("RajcePublisher: stop");
+        running = false;
+    }
+
+	// persistent data
+
+    public string? get_url() { return get_rajce_url(); }
+    public string? get_username() { return host.get_config_string("username", null); }
+    private void set_username(string username) { host.set_config_string("username", username); }
+    public string? get_token() { return host.get_config_string("token", null); }
+    private void set_token(string? token) { host.set_config_string("token", token); }
+//    public int get_last_photo_size() { return host.get_config_int("last-photo-size", -1); }
+//    private void set_last_photo_size(int last_photo_size) { host.set_config_int("last-photo-size", last_photo_size); }
+    public bool get_remember() { return host.get_config_bool("remember", false); }
+    private void set_remember(bool remember) { host.set_config_bool("remember", remember); }
+    public bool get_hide_album() { return host.get_config_bool("hide-album", false); }
+    public void set_hide_album(bool hide_album) { host.set_config_bool("hide-album", hide_album); }
+    public bool get_show_album() { return host.get_config_bool("show-album", true); }
+    public void set_show_album(bool show_album) { host.set_config_bool("show-album", show_album); }
+//    public bool get_strip_metadata() { return host.get_config_bool("strip-metadata", false); }
+//    private void set_strip_metadata(bool strip_metadata) { host.set_config_bool("strip-metadata", strip_metadata); }
+
+    // Actions and events
+    
+    /**
+     * Action that shows the authentication pane.
+     */
+    private void do_show_authentication_pane(AuthenticationPane.Mode mode = AuthenticationPane.Mode.INTRO)
+	{
+        debug("ACTION: installing authentication pane");
+
+        host.set_service_locked(false);
+        AuthenticationPane authentication_pane = new AuthenticationPane(this, mode);
+        authentication_pane.login.connect(on_authentication_pane_login_clicked);
+        host.install_dialog_pane(authentication_pane, Spit.Publishing.PluginHost.ButtonMode.CLOSE);
+        host.set_dialog_default_widget(authentication_pane.get_default_widget());
+    }
+
+    /**
+     * Event triggered when the login button in the authentication panel is clicked.
+     */
+    private void on_authentication_pane_login_clicked( string username, string token, bool remember )
+	{
+        debug("EVENT: on_authentication_pane_login_clicked");
+        if (!running)
+            return;
+        do_network_login(username, token, remember);
+    }
+    
+    /**
+     * Action to perform a network login to a Rajce service.
+     */
+    private void do_network_login(string username, string token, bool remember)
+	{
+        debug("ACTION: logging in");
+        host.set_service_locked(true);
+        host.install_login_wait_pane();
+        set_remember( remember );
+        set_username( username );
+        set_token( remember ? token : null );
+        SessionLoginTransaction login_trans = new SessionLoginTransaction(session, get_url(), username, token);
+        login_trans.network_error.connect(on_login_network_error);
+        login_trans.completed.connect(on_login_network_complete);
+        try
+		{
+            login_trans.execute();
+        }
+		catch (Spit.Publishing.PublishingError err)
+		{
+            debug("ERROR: do_network_login");
+            do_show_error(err);
+        }
+    }
+    
+    /**
+     * Event triggered when the network login action is complete and successful.
+     */
+    private void on_login_network_complete(Publishing.RESTSupport.Transaction txn)
+	{
+        debug("EVENT: on_login_network_complete");
+        txn.completed.disconnect(on_login_network_complete);
+        txn.network_error.disconnect(on_login_network_error);
+        
+        try
+		{
+            Publishing.RESTSupport.XmlDocument doc = Publishing.RESTSupport.XmlDocument.parse_string( txn.get_response(), Transaction.validate_xml);
+            Xml.Node* response = doc.get_root_node();
+            Xml.Node* sessionToken = doc.get_named_child( response, "sessionToken" );
+            Xml.Node* maxWidth = doc.get_named_child( response, "maxWidth" );
+            Xml.Node* maxHeight = doc.get_named_child( response, "maxHeight" );
+            Xml.Node* quality = doc.get_named_child( response, "quality" );
+            Xml.Node* nick = doc.get_named_child( response, "nick" );
+			int maxW = int.parse( maxWidth->get_content() );
+			int maxH = int.parse( maxHeight->get_content() );
+			if( maxW > maxH )
+			{
+				maxH = maxW;
+			}
+			session.authenticate( sessionToken->get_content(), nick->get_content(), 0, maxH, int.parse( quality->get_content() ) ); 
+        }
+		catch (Spit.Publishing.PublishingError err)
+		{
+			int code_int = int.parse(err.message);
+			if (code_int == 999)
+			{
+                debug("ERROR: on_login_network_complete, code 999");
+                do_show_authentication_pane(AuthenticationPane.Mode.FAILED_RETRY_USER);
+            }
+			else
+			{
+                debug("ERROR: on_login_network_complete");
+                do_show_error(err);
+            }
+            return;
+        }
+        do_fetch_albums();
+    }
+    
+    /**
+     * Event triggered when a network login action fails due to a network error.
+     */
+    private void on_login_network_error( Publishing.RESTSupport.Transaction bad_txn, Spit.Publishing.PublishingError err )
+	{
+        debug("EVENT: on_login_network_error");
+        bad_txn.completed.disconnect(on_login_network_complete);
+        bad_txn.network_error.disconnect(on_login_network_error);
+        do_show_authentication_pane(AuthenticationPane.Mode.FAILED_RETRY_USER);
+    }
+
+    /**
+     * Action that fetches all user albums from the Rajce.
+     */
+    private void do_fetch_albums()
+	{
+        debug("ACTION: fetching albums");
+        host.set_service_locked(true);
+        host.install_account_fetch_wait_pane();
+
+        GetAlbumsTransaction get_albums_trans = new GetAlbumsTransaction(session, get_url() );
+        get_albums_trans.network_error.connect(on_albums_fetch_error);
+        get_albums_trans.completed.connect(on_albums_fetch_complete);
+        
+        try
+		{
+            get_albums_trans.execute();
+        }
+		catch (Spit.Publishing.PublishingError err)
+		{
+            debug("ERROR: do_fetch_albums");
+            do_show_error(err);
+        }
+    }
+
+    /**
+     * Event triggered when the fetch albums action completes successfully.
+     */
+    private void on_albums_fetch_complete(Publishing.RESTSupport.Transaction txn)
+	{
+        debug("EVENT: on_albums_fetch_complete");
+        txn.completed.disconnect(on_albums_fetch_complete);
+        txn.network_error.disconnect(on_albums_fetch_error);
+        debug("RajcePlugin: list of albums: %s", txn.get_response());
+        if (albums != null)
+		{
+            albums = null;
+        }
+		Gee.ArrayList<Album> list = new Gee.ArrayList<Album>();
+        try
+		{
+            Publishing.RESTSupport.XmlDocument doc = Publishing.RESTSupport.XmlDocument.parse_string( txn.get_response(), Transaction.validate_xml);
+            Xml.Node* response = doc.get_root_node();
+            Xml.Node* sessionToken = doc.get_named_child( response, "sessionToken" );
+            Xml.Node* nodealbums = doc.get_named_child( response, "albums" );
+			for( Xml.Node* album = nodealbums->children; album != null; album = album->next )
+			{
+				int id = int.parse( album->get_prop("id") );
+		        string albumName = doc.get_named_child( album, "albumName" )->get_content();
+		        string url = doc.get_named_child( album, "url" )->get_content();
+		        string thumbUrl = doc.get_named_child( album, "thumbUrl" )->get_content();
+		        string createDate = doc.get_named_child( album, "createDate" )->get_content();
+		        string updateDate = doc.get_named_child( album, "updateDate" )->get_content();
+		        bool hidden = ( int.parse( doc.get_named_child( album, "hidden" )->get_content() ) > 0 ? true : false );
+		        bool secure = ( int.parse( doc.get_named_child( album, "secure" )->get_content() ) > 0 ? true : false );
+		        int photoCount = int.parse( doc.get_named_child( album, "photoCount" )->get_content() );
+				list.insert( 0, new Album( id, albumName, url, thumbUrl, createDate, updateDate, hidden, secure, photoCount ) ); 
+			}
+			list.sort( Album.compare_albums );
+			albums = list.to_array();
+			session.set_usertoken( sessionToken->get_content() );
+        }
+		catch (Spit.Publishing.PublishingError err)
+		{
+            debug("ERROR: on_albums_fetch_complete");
+            do_show_error(err);
+            return;
+        }
+        do_show_publishing_options_pane();
+    }
+    
+    /**
+     * Event triggered when the fetch albums transaction fails due to a network error.
+     */
+    private void on_albums_fetch_error( Publishing.RESTSupport.Transaction bad_txn, Spit.Publishing.PublishingError err )
+	{
+        debug("EVENT: on_albums_fetch_error");
+        bad_txn.completed.disconnect(on_albums_fetch_complete);
+        bad_txn.network_error.disconnect(on_albums_fetch_error);
+        on_network_error(bad_txn, err);
+    }
+    
+    /**
+     * Action that shows the publishing options pane.
+     */
+    private void do_show_publishing_options_pane()
+	{
+        debug("ACTION: installing publishing options pane");
+        host.set_service_locked(false);
+        PublishingOptionsPane opts_pane = new PublishingOptionsPane( this, session.get_username(), albums );
+        opts_pane.logout.connect(on_publishing_options_pane_logout_clicked);
+        opts_pane.publish.connect(on_publishing_options_pane_publish_clicked);
+        host.install_dialog_pane(opts_pane, Spit.Publishing.PluginHost.ButtonMode.CLOSE);
+        host.set_dialog_default_widget(opts_pane.get_default_widget());
+    }
+    
+    /**
+     * Event triggered when the user clicks logout in the publishing options pane.
+     */
+    private void on_publishing_options_pane_logout_clicked()
+	{
+        debug("EVENT: on_publishing_options_pane_logout_clicked");
+        session.deauthenticate();
+        do_show_authentication_pane( AuthenticationPane.Mode.INTRO );
+    }
+  
+    /**
+     * Event triggered when the user clicks publish in the publishing options pane.
+     *
+     * @param parameters the publishing parameters
+     */
+    private void on_publishing_options_pane_publish_clicked( PublishingParameters parameters )
+	{
+        debug("EVENT: on_publishing_options_pane_publish_clicked");
+        this.parameters = parameters;
+        do_begin_upload();
+    }
+  
+    /**
+     * Begin upload action: open existing album or create a new one
+     */
+    private void do_begin_upload()
+	{
+		host.set_service_locked(true);
+		if( parameters.album_id == 0 )
+		{
+			// new album
+		    debug("ACTION: closing album");
+			CreateAlbumTransaction create_album_trans = new CreateAlbumTransaction(session, get_url(), parameters.album_name, this.parameters.album_hidden );
+		    create_album_trans.network_error.connect(on_create_album_error);
+		    create_album_trans.completed.connect(on_create_album_complete);
+		    try
+			{
+		        create_album_trans.execute();
+		    }
+			catch (Spit.Publishing.PublishingError err)
+			{
+		        debug("ERROR: create album");
+		        do_show_error(err);
+		    }
+		}
+		else
+		{
+			// existing album
+		    debug("ACTION: opening album");
+			OpenAlbumTransaction open_album_trans = new OpenAlbumTransaction(session, get_url(), parameters.album_id );
+		    open_album_trans.network_error.connect(on_open_album_error);
+		    open_album_trans.completed.connect(on_open_album_complete);
+		    try
+			{
+		        open_album_trans.execute();
+		    }
+			catch (Spit.Publishing.PublishingError err)
+			{
+		        debug("ERROR: open album");
+		        do_show_error(err);
+		    }
+		}
+	}
+
+    /**
+     * Event triggered when the create album completes successfully.
+     */
+    private void on_create_album_complete( Publishing.RESTSupport.Transaction txn)
+	{
+        debug("EVENT: on_create_album_complete");
+        txn.completed.disconnect(on_create_album_complete);
+        txn.network_error.disconnect(on_create_album_error);
+        debug("RajcePlugin: create album: %s", txn.get_response());
+        try
+		{
+            Publishing.RESTSupport.XmlDocument doc = Publishing.RESTSupport.XmlDocument.parse_string( txn.get_response(), Transaction.validate_xml);
+            Xml.Node* response = doc.get_root_node();
+            string sessionToken = doc.get_named_child( response, "sessionToken" )->get_content();
+            string albumToken = doc.get_named_child( response, "albumToken" )->get_content();
+	        parameters.album_id = int.parse( doc.get_named_child( response, "albumID" )->get_content() );
+			session.set_usertoken( sessionToken );
+			session.set_albumtoken( albumToken );
+        }
+		catch (Spit.Publishing.PublishingError err)
+		{
+            debug("ERROR: on_create_album_complete");
+            do_show_error(err);
+            return;
+        }
+        do_upload_photos();
+    }
+    
+    /**
+     * Event triggered when the create album transaction fails due to a network error.
+     */
+    private void on_create_album_error( Publishing.RESTSupport.Transaction bad_txn, Spit.Publishing.PublishingError err )
+	{
+        debug("EVENT: on_create_album_error");
+        bad_txn.completed.disconnect(on_create_album_complete);
+        bad_txn.network_error.disconnect(on_create_album_error);
+        on_network_error(bad_txn, err);
+    }
+
+    /**
+     * Event triggered when the open album completes successfully.
+     */
+    private void on_open_album_complete(Publishing.RESTSupport.Transaction txn)
+	{
+        debug("EVENT: on_open_album_complete");
+        txn.completed.disconnect(on_open_album_complete);
+        txn.network_error.disconnect(on_open_album_error);
+        debug("RajcePlugin: open album: %s", txn.get_response());
+        try
+		{
+            Publishing.RESTSupport.XmlDocument doc = Publishing.RESTSupport.XmlDocument.parse_string( txn.get_response(), Transaction.validate_xml);
+            Xml.Node* response = doc.get_root_node();
+            string sessionToken = doc.get_named_child( response, "sessionToken" )->get_content();
+            string albumToken = doc.get_named_child( response, "albumToken" )->get_content();
+			session.set_usertoken( sessionToken );
+			session.set_albumtoken( albumToken );
+        }
+		catch (Spit.Publishing.PublishingError err)
+		{
+            debug("ERROR: on_open_album_complete");
+            do_show_error(err);
+            return;
+        }
+        do_upload_photos();
+    }
+    
+    /**
+     * Event triggered when the open album transaction fails due to a network error.
+     */
+    private void on_open_album_error( Publishing.RESTSupport.Transaction bad_txn, Spit.Publishing.PublishingError err )
+	{
+        debug("EVENT: on_open_album_error");
+        bad_txn.completed.disconnect(on_open_album_complete);
+        bad_txn.network_error.disconnect(on_open_album_error);
+        on_network_error(bad_txn, err);
+    }
+
+    /**
+     * Upload photos: the key part of the plugin
+     */
+    private void do_upload_photos()
+	{
+        debug("ACTION: uploading photos");
+        progress_reporter = host.serialize_publishables( session.get_maxsize() );
+        Spit.Publishing.Publishable[] publishables = host.get_publishables();
+        
+        Uploader uploader = new Uploader( session, get_url(), publishables, parameters );
+        uploader.upload_complete.connect( on_upload_photos_complete );
+        uploader.upload_error.connect( on_upload_photos_error );
+        uploader.upload( on_upload_photos_status_updated );
+    }
+    
+    /**
+     * Event triggered when the batch uploader reports that at least one of the
+     * network transactions encapsulating uploads has completed successfully
+     */
+    private void on_upload_photos_complete(Publishing.RESTSupport.BatchUploader uploader, int num_published)
+	{
+        debug("EVENT: on_upload_photos_complete");
+        uploader.upload_complete.disconnect(on_upload_photos_complete);
+        uploader.upload_error.disconnect(on_upload_photos_error);
+        
+        // TODO: should a message be displayed to the user if num_published is zero?
+		do_end_upload();
+    }
+    
+    /**
+     * Event triggered when the batch uploader reports that at least one of the
+     * network transactions encapsulating uploads has caused a network error
+     */
+    private void on_upload_photos_error( Publishing.RESTSupport.BatchUploader uploader, Spit.Publishing.PublishingError err)
+	{
+        debug("EVENT: on_upload_photos_error");
+        uploader.upload_complete.disconnect(on_upload_photos_complete);
+        uploader.upload_error.disconnect(on_upload_photos_error);
+        do_show_error(err);
+    }
+    
+    /**
+     * Event triggered when upload progresses and the status needs to be updated.
+     */
+    private void on_upload_photos_status_updated(int file_number, double completed_fraction)
+	{
+        if( is_running() )
+		{
+		    debug("EVENT: uploader reports upload %.2f percent complete.", 100.0 * completed_fraction);
+		    assert(progress_reporter != null);
+		    progress_reporter(file_number, completed_fraction);
+		}
+    }
+
+    private void do_end_upload()
+	{
+		if( get_show_album() )
+		{
+			do_get_album_url();
+		}
+		else
+		{
+			do_close_album();
+		}
+	}
+	
+	/**
+     * End upload action: get album url
+     */
+    private void do_get_album_url()
+	{
+        debug("ACTION: getting album URL");
+        host.set_service_locked(true);
+		GetAlbumUrlTransaction get_album_url_trans = new GetAlbumUrlTransaction(session, get_url() );
+	    get_album_url_trans.network_error.connect(on_get_album_url_error);
+	    get_album_url_trans.completed.connect(on_get_album_url_complete);
+	    try
+		{
+	        get_album_url_trans.execute();
+	    }
+		catch (Spit.Publishing.PublishingError err)
+		{
+	        debug("ERROR: close album");
+	        do_show_error(err);
+	    }
+	}
+
+    /**
+     * Event triggered when the get album url completes successfully.
+     */
+    private void on_get_album_url_complete(Publishing.RESTSupport.Transaction txn)
+	{
+        debug("EVENT: on_get_album_url_complete");
+        txn.completed.disconnect(on_get_album_url_complete);
+        txn.network_error.disconnect(on_get_album_url_error);
+        debug("RajcePlugin: get album url: %s", txn.get_response());
+        try
+		{
+            Publishing.RESTSupport.XmlDocument doc = Publishing.RESTSupport.XmlDocument.parse_string( txn.get_response(), Transaction.validate_xml);
+            Xml.Node* response = doc.get_root_node();
+            string sessionToken = doc.get_named_child( response, "sessionToken" )->get_content();
+            string url = doc.get_named_child( response, "url" )->get_content();
+			session.set_usertoken( sessionToken );
+			session.set_albumticket( url );
+        }
+		catch (Spit.Publishing.PublishingError err)
+		{
+            debug("ERROR: on_get_album_url_complete");
+		// ignore this error
+//            do_show_error(err);
+//            return;
+        }
+        do_close_album();
+    }
+    
+    /**
+     * Event triggered when the get album url transaction fails due to a network error.
+     */
+    private void on_get_album_url_error( Publishing.RESTSupport.Transaction bad_txn, Spit.Publishing.PublishingError err )
+	{
+        debug("EVENT: on_get_album_url_error");
+        bad_txn.completed.disconnect(on_get_album_url_complete);
+        bad_txn.network_error.disconnect(on_get_album_url_error);
+		// ignore this error
+//        on_network_error(bad_txn, err);
+        do_close_album();
+    }
+
+
+    /**
+     * End upload action: close album
+     */
+    private void do_close_album()
+	{
+        debug("ACTION: closing album");
+        host.set_service_locked(true);
+		CloseAlbumTransaction close_album_trans = new CloseAlbumTransaction(session, get_url() );
+	    close_album_trans.network_error.connect(on_close_album_error);
+	    close_album_trans.completed.connect(on_close_album_complete);
+	    try
+		{
+	        close_album_trans.execute();
+	    }
+		catch (Spit.Publishing.PublishingError err)
+		{
+	        debug("ERROR: close album");
+	        do_show_error(err);
+	    }
+	}
+
+    /**
+     * Event triggered when the close album completes successfully.
+     */
+    private void on_close_album_complete(Publishing.RESTSupport.Transaction txn)
+	{
+        debug("EVENT: on_close_album_complete");
+        txn.completed.disconnect(on_close_album_complete);
+        txn.network_error.disconnect(on_close_album_error);
+        debug("RajcePlugin: close album: %s", txn.get_response());
+        try
+		{
+            Publishing.RESTSupport.XmlDocument doc = Publishing.RESTSupport.XmlDocument.parse_string( txn.get_response(), Transaction.validate_xml);
+            Xml.Node* response = doc.get_root_node();
+            string sessionToken = doc.get_named_child( response, "sessionToken" )->get_content();
+			session.set_usertoken( sessionToken );
+			session.set_albumtoken( null );
+        }
+		catch (Spit.Publishing.PublishingError err)
+		{
+            debug("ERROR: on_close_album_complete");
+            do_show_error(err);
+            return;
+        }
+        do_show_success_pane();
+    }
+    
+    /**
+     * Event triggered when the close album transaction fails due to a network error.
+     */
+    private void on_close_album_error( Publishing.RESTSupport.Transaction bad_txn, Spit.Publishing.PublishingError err )
+	{
+        debug("EVENT: on_close_album_error");
+        bad_txn.completed.disconnect(on_close_album_complete);
+        bad_txn.network_error.disconnect(on_close_album_error);
+		// ignore this error
+//        on_network_error(bad_txn, err);
+        do_show_success_pane();
+    }
+
+		
+    /**
+     * Action to display the success pane in the publishing dialog.
+     */
+    private void do_show_success_pane()
+	{
+        debug("ACTION: installing success pane");
+		if( get_show_album() && session.get_albumticket() != null )
+		{
+			try
+			{
+				GLib.Process.spawn_command_line_async( "xdg-open " + session.get_albumticket() );
+			}
+			catch( GLib.SpawnError e )
+			{
+			}
+		}
+        host.set_service_locked(false);
+        host.install_success_pane();
+    }
+    
+    /**
+     * Helper event to handle network errors.
+     */
+    private void on_network_error( Publishing.RESTSupport.Transaction bad_txn, Spit.Publishing.PublishingError err )
+	{
+        debug("EVENT: on_network_error");
+        do_show_error(err);
+    }
+    
+    /**
+     * Action to display an error to the user.
+     */
+    private void do_show_error(Spit.Publishing.PublishingError e)
+	{
+        debug("ACTION: do_show_error");
+        string error_type = "UNKNOWN";
+        if (e is Spit.Publishing.PublishingError.NO_ANSWER)
+		{
+            do_show_authentication_pane(AuthenticationPane.Mode.FAILED_RETRY_USER);
+            return;
+        } else if(e is Spit.Publishing.PublishingError.COMMUNICATION_FAILED) {
+            error_type = "COMMUNICATION_FAILED";
+        } else if(e is Spit.Publishing.PublishingError.PROTOCOL_ERROR) {
+            error_type = "PROTOCOL_ERROR";
+        } else if(e is Spit.Publishing.PublishingError.SERVICE_ERROR) {
+            error_type = "SERVICE_ERROR";
+        } else if(e is Spit.Publishing.PublishingError.MALFORMED_RESPONSE) {
+            error_type = "MALFORMED_RESPONSE";
+        } else if(e is Spit.Publishing.PublishingError.LOCAL_FILE_ERROR) {
+            error_type = "LOCAL_FILE_ERROR";
+        } else if(e is Spit.Publishing.PublishingError.EXPIRED_SESSION) {
+            error_type = "EXPIRED_SESSION";
+        }
+        
+        debug("Unhandled error: type=%s; message='%s'".printf(error_type, e.message));
+        do_show_error_message(_("An error message occurred when publishing to Rajce. Please try again."));
+    }
+    
+    /**
+     * Action to display an error message to the user.
+     */
+    private void do_show_error_message(string message)
+	{
+        debug("ACTION: do_show_error_message");
+        host.install_static_message_pane(message, Spit.Publishing.PluginHost.ButtonMode.CLOSE);
+    }
+    
+}
+
+// Rajce Album
+internal class Album
+{
+    public int id;
+    public string albumName;
+    public string url;
+    public string thumbUrl;
+    public string createDate;
+    public string updateDate;
+    public bool hidden;
+    public bool secure;
+	public int photoCount;
+
+    public Album( int id, string albumName, string url, string thumbUrl, string createDate, string updateDate, bool hidden, bool secure, int photoCount )
+	{
+        this.id = id;
+        this.albumName = albumName;
+        this.url = url;
+        this.thumbUrl = thumbUrl;
+        this.createDate = createDate;
+        this.updateDate = updateDate;
+        this.hidden = hidden;
+        this.secure = secure;
+        this.photoCount = photoCount;
+    }
+	public static int compare_albums(Album? a, Album? b)
+	{
+		if( a == null && b == null )
+		{
+			return 0;
+		}
+		else if( a == null && b != null )
+		{
+			return 1;
+		}
+		else if( a != null && b == null )
+		{
+			return -1;
+		}
+		return( b.updateDate.ascii_casecmp( a.updateDate ) );
+	}
+}
+
+// Uploader
+internal class Uploader : Publishing.RESTSupport.BatchUploader
+{
+    private PublishingParameters parameters;
+	private string url;
+
+    public Uploader(Session session, string url, Spit.Publishing.Publishable[] publishables, PublishingParameters parameters)
+	{
+        base(session, publishables);
+        this.parameters = parameters;
+		this.url = url;
+    }
+
+    protected override Publishing.RESTSupport.Transaction create_transaction( Spit.Publishing.Publishable publishable )
+	{
+        return new AddPhotoTransaction((Session) get_session(), url, parameters, publishable);
+    }
+}
+
+// UI elements
+
+/**
+ * The authentication pane used when asking service URL, user name and password
+ * from the user.
+ */
+internal class AuthenticationPane : Spit.Publishing.DialogPane, Object
+{
+    public enum Mode
+	{
+        INTRO,
+        FAILED_RETRY_USER
+    }
+    private static string INTRO_MESSAGE = _("Enter email and password associated with your Rajce account.");
+    private static string FAILED_RETRY_USER_MESSAGE = _("Invalid email and/or password. Please try again");
+
+    private Gtk.Box pane_widget = null;
+    private Gtk.Builder builder;
+    private Gtk.Entry username_entry;
+    private Gtk.Entry password_entry;
+    private Gtk.CheckButton remember_checkbutton;
+    private Gtk.Button login_button;
+	private bool crypt = true;
+
+    public signal void login( string user, string token, bool remember );
+
+    public AuthenticationPane( RajcePublisher publisher, Mode mode = Mode.INTRO )
+	{
+        this.pane_widget = new Gtk.Box(Gtk.Orientation.VERTICAL, 0);
+        File ui_file = publisher.get_host().get_module_file().get_parent().get_child("rajce_authentication_pane.glade");
+        try
+		{
+            builder = new Gtk.Builder();
+            builder.add_from_file(ui_file.get_path());
+            builder.connect_signals(null);
+            Gtk.Alignment align = builder.get_object("alignment") as Gtk.Alignment;
+            Gtk.Label message_label = builder.get_object("message_label") as Gtk.Label;
+            switch (mode)
+			{
+                case Mode.INTRO:
+                    message_label.set_text(INTRO_MESSAGE);
+                    break;
+
+                case Mode.FAILED_RETRY_USER:
+                    message_label.set_markup("<b>%s</b>\n\n%s".printf(_(
+                        "Invalid User Email or Password"), FAILED_RETRY_USER_MESSAGE));
+                    break;
+            }
+            username_entry = builder.get_object ("username_entry") as Gtk.Entry;
+            string? persistent_username = publisher.get_username();
+            if (persistent_username != null)
+			{
+                username_entry.set_text(persistent_username);
+            }
+            password_entry = builder.get_object ("password_entry") as Gtk.Entry;
+            string? persistent_token = publisher.get_token();
+            if (persistent_token != null)
+			{
+                password_entry.set_text(persistent_token);
+				this.crypt = false;
+            }
+			else
+			{
+				this.crypt = true;
+			}
+            remember_checkbutton = builder.get_object ("remember_checkbutton") as Gtk.CheckButton;
+            remember_checkbutton.set_active(publisher.get_remember());
+            login_button = builder.get_object("login_button") as Gtk.Button;
+
+			Gtk.Label label2 = builder.get_object("label2") as Gtk.Label;
+			Gtk.Label label3 = builder.get_object("label3") as Gtk.Label;
+
+			label2.set_label(_("_Email address") );
+			label3.set_label(_("_Password") );
+			remember_checkbutton.set_label(_("_Remember") );
+			login_button.set_label(_("Login") );
+			
+            username_entry.changed.connect(on_user_changed);
+            password_entry.changed.connect(on_password_changed);
+            login_button.clicked.connect(on_login_button_clicked);
+            align.reparent(pane_widget);
+            publisher.get_host().set_dialog_default_widget(login_button);
+        }
+		catch (Error e)
+		{
+            warning("Could not load UI: %s", e.message);
+        }
+    }
+    
+    public Gtk.Widget get_default_widget()
+	{
+        return login_button;
+    }
+
+    private void on_login_button_clicked()
+	{
+		string token = password_entry.get_text();
+		if( this.crypt )
+		{
+			token = GLib.Checksum.compute_for_string( GLib.ChecksumType.MD5, token );
+		}
+        login(username_entry.get_text(), token, remember_checkbutton.get_active());
+    }
+
+    private void on_user_changed()
+	{
+        update_login_button_sensitivity();
+    }
+
+    private void on_password_changed()
+	{
+		this.crypt = true;
+        update_login_button_sensitivity();
+    }
+    
+    private void update_login_button_sensitivity()
+	{
+        login_button.set_sensitive(
+            !is_string_empty(username_entry.get_text()) &&
+            !is_string_empty(password_entry.get_text())
+        );
+    }
+    
+    public Gtk.Widget get_widget()
+	{
+        return pane_widget;
+    }
+    
+    public Spit.Publishing.DialogPane.GeometryOptions get_preferred_geometry()
+	{
+        return Spit.Publishing.DialogPane.GeometryOptions.NONE;
+    }
+    
+    public void on_pane_installed()
+	{
+        username_entry.grab_focus();
+        password_entry.set_activates_default(true);
+        login_button.can_default = true;
+        update_login_button_sensitivity();
+    }
+    public void on_pane_uninstalled() {}
+  
+}
+
+internal class PublishingOptionsPane : Spit.Publishing.DialogPane, GLib.Object
+{
+	RajcePublisher publisher;
+    private Album[] albums;
+    private string username;
+	
+	private Gtk.Builder builder = null;
+    private Gtk.Box pane_widget = null;
+    private Gtk.Label login_identity_label = null;
+    private Gtk.Label publish_to_label = null;
+    private Gtk.RadioButton use_existing_radio = null;
+    private Gtk.ComboBoxText existing_albums_combo = null;
+    private Gtk.RadioButton create_new_radio = null;
+    private Gtk.Entry new_album_entry = null;
+    private Gtk.CheckButton hide_check = null;
+    private Gtk.CheckButton show_check = null;
+    private Gtk.Button publish_button = null;
+    private Gtk.Button logout_button = null;
+
+    public signal void publish( PublishingParameters parameters );
+    public signal void logout();
+
+    public PublishingOptionsPane( RajcePublisher publisher, string username, Album[] albums )
+	{
+        this.username = username;
+        this.albums = albums;
+        this.publisher = publisher;
+        this.pane_widget = new Gtk.Box(Gtk.Orientation.VERTICAL, 0);
+		
+        File ui_file = publisher.get_host().get_module_file().get_parent().get_child("rajce_publishing_options_pane.glade");
+        try
+		{
+		    this.builder = new Gtk.Builder();
+			builder.add_from_file(ui_file.get_path());
+            builder.connect_signals(null);
+			
+		    pane_widget = (Gtk.Box) builder.get_object("rajce_pane_widget");
+		    login_identity_label = (Gtk.Label) builder.get_object("login_identity_label");
+		    publish_to_label = (Gtk.Label) builder.get_object("publish_to_label");
+		    use_existing_radio = (Gtk.RadioButton) builder.get_object("use_existing_radio");
+		    existing_albums_combo = (Gtk.ComboBoxText) builder.get_object("existing_albums_combo");
+		    create_new_radio = (Gtk.RadioButton) builder.get_object("create_new_radio");
+		    new_album_entry = (Gtk.Entry) builder.get_object("new_album_entry");
+		    hide_check = (Gtk.CheckButton) builder.get_object("hide_check");
+			hide_check.set_label(_("_Hide album") );
+		    show_check = (Gtk.CheckButton) builder.get_object("show_check");
+		    publish_button = (Gtk.Button) builder.get_object("publish_button");
+		    logout_button = (Gtk.Button) builder.get_object("logout_button");
+
+		    hide_check.set_active( publisher.get_hide_album() );
+		    show_check.set_active( publisher.get_show_album() );
+		    login_identity_label.set_label(_("You are logged into Rajce as %s.").printf(username));
+		    publish_to_label.set_label(_("Photos will appear in:"));
+			use_existing_radio.set_label(_("An _existing album:") );
+			create_new_radio.set_label(_("A _new album named:") );
+			show_check.set_label(_("Open target _album in browser") );
+			publish_button.set_label(_("_Publish") );
+			logout_button.set_label(_("_Logout") );
+			
+		    use_existing_radio.clicked.connect(on_use_existing_radio_clicked);
+		    create_new_radio.clicked.connect(on_create_new_radio_clicked);
+		    new_album_entry.changed.connect(on_new_album_entry_changed);
+		    logout_button.clicked.connect(on_logout_clicked);
+		    publish_button.clicked.connect(on_publish_clicked);
+        }
+		catch (Error e)
+		{
+            warning("Could not load UI: %s", e.message);
+        }
+		
+    }
+
+    private void on_publish_clicked()
+	{
+        bool show_album = show_check.get_active();
+		publisher.set_show_album( show_album );
+        if (create_new_radio.get_active())
+		{
+            string album_name = new_album_entry.get_text();
+            bool hide_album = hide_check.get_active();
+			publisher.set_hide_album( hide_album );
+            publish( new PublishingParameters.to_new_album( album_name, hide_album ) );
+        }
+		else
+		{
+            int id = albums[existing_albums_combo.get_active()].id;
+			string album_name = albums[existing_albums_combo.get_active()].albumName;
+            publish( new PublishingParameters.to_existing_album( album_name, id ) );
+        }
+    }
+
+    private void on_use_existing_radio_clicked()
+	{
+        existing_albums_combo.set_sensitive(true);
+        new_album_entry.set_sensitive(false);
+        existing_albums_combo.grab_focus();
+        update_publish_button_sensitivity();
+        hide_check.set_sensitive(false);
+    }
+
+    private void on_create_new_radio_clicked()
+	{
+        new_album_entry.set_sensitive(true);
+        existing_albums_combo.set_sensitive(false);
+        new_album_entry.grab_focus();
+        update_publish_button_sensitivity();
+        hide_check.set_sensitive(true);
+    }
+
+    private void on_logout_clicked()
+	{
+        logout();
+    }
+    private void update_publish_button_sensitivity()
+	{
+        string album_name = new_album_entry.get_text();
+        publish_button.set_sensitive( album_name.strip() != "" || !create_new_radio.get_active());
+    }
+    private void on_new_album_entry_changed()
+	{
+        update_publish_button_sensitivity();
+    }
+    public void installed()
+	{
+        for (int i = 0; i < albums.length; i++)
+		{
+			// TODO: sort albums according to their updateDate property
+            existing_albums_combo.append_text( albums[i].albumName );
+        }
+        if (albums.length == 0)
+		{
+            existing_albums_combo.set_sensitive(false);
+            use_existing_radio.set_sensitive(false);
+        }
+		else
+		{
+            existing_albums_combo.set_active(0);
+            existing_albums_combo.set_sensitive(true);
+            use_existing_radio.set_sensitive(true);
+        }
+        create_new_radio.set_active(true);
+		on_create_new_radio_clicked();
+    }
+    
+    protected void notify_publish(PublishingParameters parameters)
+	{
+        publish( parameters );
+    }
+    
+    protected void notify_logout()
+	{
+        logout();
+    }
+
+    public Gtk.Widget get_default_widget()
+	{
+        return logout_button;
+    }
+	public Gtk.Widget get_widget()
+	{
+        return pane_widget;
+    }
+    
+    public Spit.Publishing.DialogPane.GeometryOptions get_preferred_geometry()
+	{
+        return Spit.Publishing.DialogPane.GeometryOptions.NONE;
+    }
+    
+    public void on_pane_installed()
+	{
+        installed();
+        publish.connect(notify_publish);
+        logout.connect(notify_logout);
+    }
+    
+    public void on_pane_uninstalled()
+	{
+        publish.disconnect(notify_publish);
+        logout.disconnect(notify_logout);
+    }
+}
+
+internal class PublishingParameters
+{
+    public string? album_name;
+    public bool? album_hidden;
+    public int? album_id;
+    
+    private PublishingParameters()
+	{
+    }
+    public PublishingParameters.to_new_album( string album_name, bool album_hidden )
+	{
+        this.album_name = album_name;
+        this.album_hidden = album_hidden;
+		this.album_id = 0;
+    }
+    public PublishingParameters.to_existing_album( string album_name, int album_id )
+	{
+        this.album_name = album_name;
+        this.album_hidden = null;
+		this.album_id = album_id;
+    }
+}
+
+// REST support classes
+/**
+ * Session class that keeps track of the credentials
+ */
+internal class Session : Publishing.RESTSupport.Session {
+    private string? usertoken = null;
+    private string? albumtoken = null;
+    private string? albumticket = null;
+    private string? username = null;
+    private int? userid = null;
+    private int? maxsize = null;
+    private int? quality = null;
+
+    public Session()
+	{
+        base("");
+    }
+
+    public override bool is_authenticated()
+	{
+        return (userid != null && usertoken != null && username != null);
+    }
+
+    public void authenticate(string token, string name, int id, int maxsize, int quality )
+	{
+        this.usertoken = token;
+        this.username = name;
+        this.userid = id;
+        this.maxsize = maxsize;
+        this.quality = quality;
+    }
+
+    public void deauthenticate()
+	{
+        usertoken = null;
+    	albumtoken = null;
+    	albumticket = null;
+        username = null;
+        userid = null;
+	    maxsize = null;
+	    quality = null;
+    }
+	
+    public void set_usertoken( string? usertoken ){ this.usertoken = usertoken; }
+    public void set_albumtoken( string? albumtoken ){ this.albumtoken = albumtoken; }
+    public void set_albumticket( string? albumticket ){ this.albumticket = albumticket; }
+	
+    public string get_usertoken() { return usertoken; }
+    public string get_albumtoken() { return albumtoken; }
+    public string get_albumticket() { return albumticket; }
+    public string get_username() { return username; }
+//    public int get_userid() { return userid; }
+    public int get_maxsize() { return maxsize; }
+//    public int get_quality() { return quality; }
+}
+
+internal class ArgItem
+{
+    public string? key;
+    public string? val;
+    public ArgItem[] children;
+	
+    public ArgItem( string? k, string? v )
+	{
+		key = k;
+		val = v;
+		children = new ArgItem[0];
+	}
+    public void AddChild( ArgItem child )
+	{
+		children += child;
+	}
+    public void AddChildren( ArgItem[] newchildren )
+	{
+		foreach( ArgItem child in newchildren )
+		{
+			AddChild( child );
+		}
+	}
+    ~ArgItem()
+	{
+		foreach( ArgItem child in children )
+		{
+			child = null;			
+		}
+	}
+}
+
+/// <summary>
+/// implementation of Rajce Live API
+/// </summary>
+internal class LiveApiRequest
+{
+    private ArgItem[] _params;
+    private string _cmd;
+    public LiveApiRequest( string cmd )
+    {
+        _params = new ArgItem[0];
+        _cmd = cmd;
+    }
+    /// <summary>
+    /// add string parameter
+    /// </summary>
+    public void AddParam( string name, string val )
+    {
+        _params += new ArgItem( name, val );
+    }
+    /// <summary>
+    /// add boolean parameter
+    /// </summary>
+    public void AddParamBool( string name, bool val )
+    {
+        AddParam( name, val ? "1" : "0" );
+    }
+    /// <summary>
+    /// add integer parameter
+    /// </summary>
+    public void AddParamInt( string name, int val )
+    {
+        AddParam( name, val.to_string() );
+    }
+/*    /// <summary>
+    /// add double parameter
+    /// </summary>
+    public void AddParamDouble( string name, double val )
+    {
+        AddParam( name, val.to_string() );
+    }
+*/    /// <summary>
+    /// add compound parameter
+    /// </summary>
+    public void AddParamNode( string name, ArgItem[] val )
+    {
+		ArgItem newItem = new ArgItem( name, null );
+		newItem.AddChildren( val );
+        _params += newItem; 
+    }
+    /// <summary>
+    /// create XML fragment containing all parameters
+    /// </summary>
+    public string Params2XmlString( bool urlencode = true )
+    {
+        Xml.Doc* doc = new Xml.Doc( "1.0" );
+        Xml.Node* root = new Xml.Node( null, "request" );
+        doc->set_root_element( root );
+        root->new_text_child( null, "command", _cmd );
+        Xml.Node* par = root->new_text_child( null, "parameters", "" );
+		foreach( ArgItem arg in _params )
+		{
+        	WriteParam( par, arg );
+		}
+        string xmlstr;
+        doc->dump_memory_enc( out xmlstr );
+        delete doc;
+		if( urlencode )
+		{
+        	return Soup.URI.encode( xmlstr, "&;" );
+		}
+		return xmlstr;
+    }
+    /// <summary>
+    /// write single or compound (recursively) parameter into XML
+    /// </summary>
+    private static void WriteParam( Xml.Node* node, ArgItem arg )
+    {
+		if( arg.children.length == 0 )
+		{
+	        node->new_text_child( null, arg.key, arg.val );
+		}
+		else
+		{
+	        Xml.Node* subnode = node->new_text_child( null, arg.key, "" );
+			foreach( ArgItem child in arg.children )
+			{
+		    	WriteParam( subnode, child );
+			}
+		}
+    }
+}
+
+
+/**
+ * Generic REST transaction class.
+ *
+ * This class implements the generic logic for all REST transactions used
+ * by the Rajce publishing plugin.
+ */
+internal class Transaction : Publishing.RESTSupport.Transaction
+{
+    public Transaction(Session session)
+	{
+        base(session);
+    }
+
+    public static string? validate_xml(Publishing.RESTSupport.XmlDocument doc)
+	{
+        Xml.Node* root = doc.get_root_node();
+		if( root == null )
+		{
+            return "No XML returned from server";
+		}
+        string name = root->name;
+        
+        // treat malformed root as an error condition
+        if( name == null || name != "response" )
+		{
+            return "No response from Rajce in XML";
+		}
+        Xml.Node* errcode;
+        Xml.Node* result;
+        try
+		{
+            errcode = doc.get_named_child(root, "errorCode");
+            result = doc.get_named_child(root, "result");
+        }
+		catch (Spit.Publishing.PublishingError err)
+		{
+            return null;
+        }
+        return "999 Rajce Error [%d]: %s".printf( int.parse( errcode->get_content() ), result->get_content() );
+    }
+}
+
+/**
+ * Transaction used to implement the network login interaction.
+ */
+internal class SessionLoginTransaction : Transaction
+{
+    public SessionLoginTransaction(Session session, string url, string username, string token)
+	{
+		debug("SessionLoginTransaction: URL: %s", url);
+        base.with_endpoint_url(session, url);
+		LiveApiRequest req = new LiveApiRequest( "login" );
+		req.AddParam( "clientID", "RajceShotwellPlugin" ); 
+		req.AddParam( "currentVersion", "1.1.1.1" ); 
+		req.AddParam( "login", username ); 
+		req.AddParam( "password", token ); 
+		string xml = req.Params2XmlString();
+        add_argument("data", xml);
+    }
+}
+
+/**
+ * Transaction used to implement the get albums interaction.
+ */
+internal class GetAlbumsTransaction : Transaction
+{
+    public GetAlbumsTransaction(Session session, string url)
+	{
+        base.with_endpoint_url(session, url);
+		LiveApiRequest req = new LiveApiRequest( "getAlbumList" );
+		req.AddParam( "token", session.get_usertoken() );
+		ArgItem[] columns = new ArgItem[0];
+		columns += new ArgItem( "column", "viewCount" );
+		columns += new ArgItem( "column", "isFavourite" );
+		columns += new ArgItem( "column", "descriptionHtml" );
+		columns += new ArgItem( "column", "coverPhotoID" );
+		columns += new ArgItem( "column", "localPath" );
+		req.AddParamNode( "columns", columns );
+		string xml = req.Params2XmlString();
+        add_argument("data", xml );
+    }
+}
+
+/**
+ * Transaction used to implement the create album interaction.
+ */
+internal class CreateAlbumTransaction : Transaction
+{
+    public CreateAlbumTransaction( Session session, string url, string albumName, bool hidden )
+	{
+        base.with_endpoint_url(session, url);
+		LiveApiRequest req = new LiveApiRequest( "createAlbum" );
+		req.AddParam( "token", session.get_usertoken() ); 
+		req.AddParam( "albumName", albumName ); 
+		req.AddParam( "albumDescription", "" ); 
+		req.AddParamBool( "albumVisible", !hidden ); 
+		string xml = req.Params2XmlString();
+        add_argument("data", xml);
+    }
+}
+
+/**
+ * Transaction used to implement the open album interaction.
+ */
+internal class OpenAlbumTransaction : Transaction
+{
+    public OpenAlbumTransaction( Session session, string url, int albumID )
+	{
+        base.with_endpoint_url(session, url);
+		LiveApiRequest req = new LiveApiRequest( "openAlbum" );
+		req.AddParam( "token", session.get_usertoken() ); 
+		req.AddParamInt( "albumID", albumID ); 
+		string xml = req.Params2XmlString();
+        add_argument("data", xml);
+    }
+}
+
+/**
+ * Transaction used to implement the close album interaction.
+ */
+internal class GetAlbumUrlTransaction : Transaction
+{
+    public GetAlbumUrlTransaction( Session session, string url )
+	{
+        base.with_endpoint_url(session, url);
+		LiveApiRequest req = new LiveApiRequest( "getAlbumUrl" );
+		req.AddParam( "token", session.get_usertoken() ); 
+		req.AddParam( "albumToken", session.get_albumtoken() ); 
+		string xml = req.Params2XmlString();
+        add_argument("data", xml);
+    }
+}
+
+/**
+ * Transaction used to implement the close album interaction.
+ */
+internal class CloseAlbumTransaction : Transaction
+{
+    public CloseAlbumTransaction( Session session, string url )
+	{
+        base.with_endpoint_url(session, url);
+		LiveApiRequest req = new LiveApiRequest( "closeAlbum" );
+		req.AddParam( "token", session.get_usertoken() ); 
+		req.AddParam( "albumToken", session.get_albumtoken() ); 
+		string xml = req.Params2XmlString();
+        add_argument("data", xml);
+    }
+}
+
+/**
+ * Transaction used to implement the get categories interaction.
+ */
+internal class GetCategoriesTransaction : Transaction
+{
+    public GetCategoriesTransaction( Session session, string url )
+	{
+        base.with_endpoint_url(session, url);
+		LiveApiRequest req = new LiveApiRequest( "getCategories" );
+		req.AddParam( "token", session.get_usertoken() ); 
+		string xml = req.Params2XmlString();
+        add_argument("data", xml);
+    }
+}
+
+/**
+ * Transaction used to implement the upload photo.
+ */
+private class AddPhotoTransaction : Publishing.RESTSupport.UploadTransaction
+{
+    private PublishingParameters parameters = null;
+
+    public AddPhotoTransaction(Session session, string url, PublishingParameters parameters, Spit.Publishing.Publishable publishable)
+	{
+        base.with_endpoint_url( session, publishable, url );
+        this.parameters = parameters;
+        
+        debug("RajcePlugin: Uploading photo %s to%s album %s", publishable.get_serialized_file().get_basename(), ( parameters.album_id > 0 ? "" : " new" ), parameters.album_name );
+
+		string basename = publishable.get_param_string( Spit.Publishing.Publishable.PARAM_STRING_BASENAME );
+		string comment = publishable.get_param_string( Spit.Publishing.Publishable.PARAM_STRING_COMMENT );
+		string pubname = publishable.get_publishing_name();
+		
+		int width = session.get_maxsize();
+		int height = session.get_maxsize();
+		
+		LiveApiRequest req = new LiveApiRequest( "addPhoto" );
+		req.AddParam( "token", session.get_usertoken() ); 
+		req.AddParamInt( "width", width ); 
+		req.AddParamInt( "height", height ); 
+		req.AddParam( "albumToken", session.get_albumtoken() ); 
+		req.AddParam( "photoName", pubname ); 
+		req.AddParam( "fullFileName", basename ); 
+		req.AddParam( "description", ( comment != null ? comment : "" ) ); 
+		string xml = req.Params2XmlString( false );
+        add_argument( "data", xml );
+		
+        GLib.HashTable<string, string> disposition_table = new GLib.HashTable<string, string>(GLib.str_hash, GLib.str_equal);
+        disposition_table.insert("name", "photo");
+        disposition_table.insert("filename", Soup.URI.encode( basename, null ) );
+        set_binary_disposition_table( disposition_table );
+    }
+
+}
+
+
+}
+
diff --git a/plugins/shotwell-publishing-extras/gallery3.png b/plugins/shotwell-publishing-extras/gallery3.png
new file mode 100644
index 0000000..9e3c5cc
Binary files /dev/null and b/plugins/shotwell-publishing-extras/gallery3.png differ
diff --git a/plugins/shotwell-publishing-extras/gallery3_authentication_pane.glade b/plugins/shotwell-publishing-extras/gallery3_authentication_pane.glade
new file mode 100644
index 0000000..43eb422
--- /dev/null
+++ b/plugins/shotwell-publishing-extras/gallery3_authentication_pane.glade
@@ -0,0 +1,245 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <!-- interface-requires gtk+ 3.0 -->
+  <object class="GtkAction" id="go_back_action">
+    <property name="label" translatable="yes">Go _Back</property>
+  </object>
+  <object class="GtkAction" id="login_action">
+    <property name="label" translatable="yes">_Login</property>
+  </object>
+  <object class="GtkGrid" id="gallery3_auth_pane_widget">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <child>
+      <object class="GtkLabel" id="intro_message_label">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="ypad">15</property>
+        <property name="label" translatable="yes">Intro message replaced at runtime</property>
+        <property name="use_markup">True</property>
+        <property name="wrap">True</property>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">0</property>
+        <property name="width">5</property>
+        <property name="height">1</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="url_entry_label">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="margin_bottom">30</property>
+        <property name="label" translatable="yes">_Gallery3 URL:</property>
+        <property name="use_underline">True</property>
+        <property name="mnemonic_widget">url_entry</property>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">1</property>
+        <property name="width">1</property>
+        <property name="height">1</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkEntry" id="url_entry">
+        <property name="visible">True</property>
+        <property name="can_focus">True</property>
+        <property name="has_focus">True</property>
+        <property name="margin_bottom">30</property>
+        <property name="invisible_char">●</property>
+      </object>
+      <packing>
+        <property name="left_attach">1</property>
+        <property name="top_attach">1</property>
+        <property name="width">4</property>
+        <property name="height">1</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="username_entry_label">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="label" translatable="yes">_User name:</property>
+        <property name="use_underline">True</property>
+        <property name="mnemonic_widget">username_entry</property>
+      </object>
+      <packing>
+        <property name="left_attach">2</property>
+        <property name="top_attach">2</property>
+        <property name="width">1</property>
+        <property name="height">1</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="password_entry_label">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="label" translatable="yes">_Password:</property>
+        <property name="use_underline">True</property>
+        <property name="mnemonic_widget">password_entry</property>
+      </object>
+      <packing>
+        <property name="left_attach">2</property>
+        <property name="top_attach">3</property>
+        <property name="width">1</property>
+        <property name="height">1</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkEntry" id="username_entry">
+        <property name="visible">True</property>
+        <property name="can_focus">True</property>
+        <property name="invisible_char">●</property>
+      </object>
+      <packing>
+        <property name="left_attach">3</property>
+        <property name="top_attach">2</property>
+        <property name="width">1</property>
+        <property name="height">1</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkEntry" id="password_entry">
+        <property name="visible">True</property>
+        <property name="can_focus">True</property>
+        <property name="visibility">False</property>
+        <property name="invisible_char">●</property>
+        <property name="activates_default">True</property>
+      </object>
+      <packing>
+        <property name="left_attach">3</property>
+        <property name="top_attach">3</property>
+        <property name="width">1</property>
+        <property name="height">1</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkGrid" id="buttons_grid">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="margin_top">30</property>
+        <child>
+          <object class="GtkButton" id="go_back_button">
+            <property name="label" translatable="yes">Go _Back</property>
+            <property name="related_action">go_back_action</property>
+            <property name="width_request">102</property>
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">True</property>
+            <property name="halign">center</property>
+            <property name="valign">center</property>
+            <property name="hexpand">True</property>
+            <property name="use_underline">True</property>
+          </object>
+          <packing>
+            <property name="left_attach">0</property>
+            <property name="top_attach">0</property>
+            <property name="width">1</property>
+            <property name="height">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkButton" id="login_button">
+            <property name="label" translatable="yes">_Login</property>
+            <property name="related_action">login_action</property>
+            <property name="width_request">102</property>
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="is_focus">True</property>
+            <property name="can_default">True</property>
+            <property name="has_default">True</property>
+            <property name="receives_default">True</property>
+            <property name="halign">center</property>
+            <property name="valign">center</property>
+            <property name="hexpand">True</property>
+            <property name="use_underline">True</property>
+          </object>
+          <packing>
+            <property name="left_attach">1</property>
+            <property name="top_attach">0</property>
+            <property name="width">1</property>
+            <property name="height">1</property>
+          </packing>
+        </child>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">6</property>
+        <property name="width">5</property>
+        <property name="height">1</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="key_entry_label">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="label" translatable="yes">API _Key:</property>
+        <property name="use_underline">True</property>
+        <property name="mnemonic_widget">key_entry</property>
+      </object>
+      <packing>
+        <property name="left_attach">2</property>
+        <property name="top_attach">5</property>
+        <property name="width">1</property>
+        <property name="height">1</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkEntry" id="key_entry">
+        <property name="visible">True</property>
+        <property name="can_focus">True</property>
+        <property name="invisible_char">●</property>
+        <property name="activates_default">True</property>
+        <property name="width_chars">33</property>
+      </object>
+      <packing>
+        <property name="left_attach">3</property>
+        <property name="top_attach">5</property>
+        <property name="width">1</property>
+        <property name="height">1</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkLabel" id="or_label">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="label" translatable="yes">or</property>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">4</property>
+        <property name="width">5</property>
+        <property name="height">1</property>
+      </packing>
+    </child>
+    <child>
+      <placeholder/>
+    </child>
+    <child>
+      <placeholder/>
+    </child>
+    <child>
+      <placeholder/>
+    </child>
+    <child>
+      <placeholder/>
+    </child>
+    <child>
+      <placeholder/>
+    </child>
+    <child>
+      <placeholder/>
+    </child>
+    <child>
+      <placeholder/>
+    </child>
+    <child>
+      <placeholder/>
+    </child>
+    <child>
+      <placeholder/>
+    </child>
+  </object>
+</interface>
diff --git a/plugins/shotwell-publishing-extras/gallery3_publishing_options_pane.glade b/plugins/shotwell-publishing-extras/gallery3_publishing_options_pane.glade
new file mode 100644
index 0000000..17e3569
--- /dev/null
+++ b/plugins/shotwell-publishing-extras/gallery3_publishing_options_pane.glade
@@ -0,0 +1,282 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <!-- interface-requires gtk+ 3.0 -->
+  <object class="GtkAction" id="logout_action">
+    <property name="label" translatable="yes">_Logout</property>
+  </object>
+  <object class="GtkAction" id="publish_action">
+    <property name="label" translatable="yes">_Publish</property>
+  </object>
+  <object class="GtkRadioAction" id="publish_new_radioaction">
+    <property name="label" translatable="yes">A _new album</property>
+    <property name="draw_as_radio">True</property>
+    <property name="value">1</property>
+    <property name="current_value">1</property>
+  </object>
+  <object class="GtkRadioAction" id="publish_to_existing_radioaction">
+    <property name="label" translatable="yes">An _existing album</property>
+    <property name="draw_as_radio">True</property>
+    <property name="group">publish_new_radioaction</property>
+  </object>
+  <object class="GtkToggleAction" id="strip_metadata_toggleaction">
+    <property name="label" translatable="yes">_Remove location, tag and camera-identifying data before uploading</property>
+  </object>
+  <object class="GtkGrid" id="pane_widget">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <child>
+      <object class="GtkLabel" id="title_label">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="xalign">0.30000001192092896</property>
+        <property name="ypad">16</property>
+        <property name="label" translatable="yes">'Publishing to $url as $username' (populated in application code)</property>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">0</property>
+        <property name="width">2</property>
+        <property name="height">1</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkGrid" id="options_grid">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="margin_bottom">16</property>
+        <property name="hexpand">True</property>
+        <property name="row_spacing">8</property>
+        <property name="column_spacing">32</property>
+        <property name="column_homogeneous">True</property>
+        <child>
+          <object class="GtkRadioButton" id="publish_to_existing_radio">
+            <property name="related_action">publish_to_existing_radioaction</property>
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">False</property>
+            <property name="xalign">0</property>
+            <property name="draw_indicator">True</property>
+            <property name="group">publish_new_radio</property>
+          </object>
+          <packing>
+            <property name="left_attach">0</property>
+            <property name="top_attach">0</property>
+            <property name="width">1</property>
+            <property name="height">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkComboBoxText" id="existing_albums_combo">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="entry_text_column">0</property>
+            <property name="id_column">1</property>
+          </object>
+          <packing>
+            <property name="left_attach">1</property>
+            <property name="top_attach">0</property>
+            <property name="width">1</property>
+            <property name="height">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkRadioButton" id="publish_new_radio">
+            <property name="related_action">publish_new_radioaction</property>
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">False</property>
+            <property name="xalign">0</property>
+            <property name="draw_indicator">True</property>
+          </object>
+          <packing>
+            <property name="left_attach">0</property>
+            <property name="top_attach">1</property>
+            <property name="width">1</property>
+            <property name="height">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkEntry" id="new_album_name">
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="invisible_char">●</property>
+          </object>
+          <packing>
+            <property name="left_attach">1</property>
+            <property name="top_attach">1</property>
+            <property name="width">1</property>
+            <property name="height">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkCheckButton" id="strip_metadata_check">
+            <property name="related_action">strip_metadata_toggleaction</property>
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">False</property>
+            <property name="valign">center</property>
+            <property name="margin_top">16</property>
+            <property name="hexpand">True</property>
+            <property name="xalign">0</property>
+            <property name="draw_indicator">True</property>
+          </object>
+          <packing>
+            <property name="left_attach">0</property>
+            <property name="top_attach">5</property>
+            <property name="width">2</property>
+            <property name="height">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkLabel" id="major_axis_label">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="halign">start</property>
+            <property name="label" translatable="yes">Scaling constraint:</property>
+          </object>
+          <packing>
+            <property name="left_attach">0</property>
+            <property name="top_attach">3</property>
+            <property name="width">1</property>
+            <property name="height">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkGrid" id="pixels_grid">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="column_spacing">5</property>
+            <child>
+              <object class="GtkLabel" id="pixels_label">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="label" translatable="yes">pixels</property>
+              </object>
+              <packing>
+                <property name="left_attach">1</property>
+                <property name="top_attach">0</property>
+                <property name="width">1</property>
+                <property name="height">1</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkEntry" id="major_axis_pixels">
+                <property name="visible">True</property>
+                <property name="sensitive">False</property>
+                <property name="can_focus">True</property>
+                <property name="hexpand">True</property>
+                <property name="invisible_char">●</property>
+                <property name="truncate_multiline">True</property>
+                <property name="caps_lock_warning">False</property>
+                <property name="input_purpose">number</property>
+              </object>
+              <packing>
+                <property name="left_attach">0</property>
+                <property name="top_attach">0</property>
+                <property name="width">1</property>
+                <property name="height">1</property>
+              </packing>
+            </child>
+          </object>
+          <packing>
+            <property name="left_attach">1</property>
+            <property name="top_attach">4</property>
+            <property name="width">1</property>
+            <property name="height">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkComboBoxText" id="scaling_constraint_combo">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="entry_text_column">0</property>
+            <property name="id_column">1</property>
+            <items>
+              <item translatable="yes">Original size</item>
+              <item translatable="yes">Width or height</item>
+            </items>
+          </object>
+          <packing>
+            <property name="left_attach">1</property>
+            <property name="top_attach">3</property>
+            <property name="width">1</property>
+            <property name="height">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkSeparator" id="album_separator">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="margin_left">5</property>
+            <property name="margin_right">5</property>
+          </object>
+          <packing>
+            <property name="left_attach">0</property>
+            <property name="top_attach">2</property>
+            <property name="width">2</property>
+            <property name="height">1</property>
+          </packing>
+        </child>
+        <child>
+          <placeholder/>
+        </child>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">1</property>
+        <property name="width">2</property>
+        <property name="height">1</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkGrid" id="buttons_grid">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="margin_left">112</property>
+        <property name="margin_right">112</property>
+        <property name="margin_top">48</property>
+        <property name="margin_bottom">24</property>
+        <property name="hexpand">True</property>
+        <property name="column_spacing">128</property>
+        <property name="column_homogeneous">True</property>
+        <child>
+          <object class="GtkButton" id="logout_button">
+            <property name="related_action">logout_action</property>
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">True</property>
+          </object>
+          <packing>
+            <property name="left_attach">0</property>
+            <property name="top_attach">0</property>
+            <property name="width">1</property>
+            <property name="height">1</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkButton" id="publish_button">
+            <property name="related_action">publish_action</property>
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="is_focus">True</property>
+            <property name="can_default">True</property>
+            <property name="has_default">True</property>
+            <property name="receives_default">True</property>
+          </object>
+          <packing>
+            <property name="left_attach">1</property>
+            <property name="top_attach">0</property>
+            <property name="width">1</property>
+            <property name="height">1</property>
+          </packing>
+        </child>
+      </object>
+      <packing>
+        <property name="left_attach">0</property>
+        <property name="top_attach">2</property>
+        <property name="width">2</property>
+        <property name="height">1</property>
+      </packing>
+    </child>
+  </object>
+</interface>
diff --git a/plugins/shotwell-publishing-extras/rajce.png b/plugins/shotwell-publishing-extras/rajce.png
new file mode 100644
index 0000000..8ab0995
Binary files /dev/null and b/plugins/shotwell-publishing-extras/rajce.png differ
diff --git a/plugins/shotwell-publishing-extras/rajce_authentication_pane.glade b/plugins/shotwell-publishing-extras/rajce_authentication_pane.glade
new file mode 100644
index 0000000..61f6c69
--- /dev/null
+++ b/plugins/shotwell-publishing-extras/rajce_authentication_pane.glade
@@ -0,0 +1,150 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <!-- interface-requires gtk+ 3.0 -->
+  <object class="GtkWindow" id="authentication_pane">
+    <property name="can_focus">False</property>
+    <child>
+      <object class="GtkAlignment" id="alignment">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="xscale">0.5</property>
+        <property name="yscale">0.5</property>
+        <child>
+          <object class="GtkVBox" id="vbox1">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="margin_left">30</property>
+            <property name="margin_right">30</property>
+            <property name="hexpand">True</property>
+            <property name="spacing">8</property>
+            <child>
+              <object class="GtkLabel" id="message_label">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="hexpand">True</property>
+                <property name="vexpand">True</property>
+                <property name="xalign">0</property>
+                <property name="label" translatable="yes">label</property>
+                <property name="wrap">True</property>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">True</property>
+                <property name="position">0</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkTable" id="field_table">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="n_rows">2</property>
+                <property name="n_columns">2</property>
+                <property name="column_spacing">8</property>
+                <property name="row_spacing">2</property>
+                <child>
+                  <object class="GtkLabel" id="label2">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="xalign">0</property>
+                    <property name="label" translatable="yes">_Email address</property>
+                    <property name="use_underline">True</property>
+                    <property name="mnemonic_widget">username_entry</property>
+                  </object>
+                </child>
+                <child>
+                  <object class="GtkLabel" id="label3">
+                    <property name="visible">True</property>
+                    <property name="can_focus">False</property>
+                    <property name="xalign">0</property>
+                    <property name="label" translatable="yes">_Password</property>
+                    <property name="use_underline">True</property>
+                    <property name="mnemonic_widget">password_entry</property>
+                  </object>
+                  <packing>
+                    <property name="top_attach">1</property>
+                    <property name="bottom_attach">2</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkEntry" id="username_entry">
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                    <property name="invisible_char">●</property>
+                  </object>
+                  <packing>
+                    <property name="left_attach">1</property>
+                    <property name="right_attach">2</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkEntry" id="password_entry">
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                    <property name="visibility">False</property>
+                    <property name="invisible_char">●</property>
+                    <property name="invisible_char_set">True</property>
+                  </object>
+                  <packing>
+                    <property name="left_attach">1</property>
+                    <property name="right_attach">2</property>
+                    <property name="top_attach">1</property>
+                    <property name="bottom_attach">2</property>
+                  </packing>
+                </child>
+              </object>
+              <packing>
+                <property name="expand">True</property>
+                <property name="fill">True</property>
+                <property name="position">1</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkCheckButton" id="remember_checkbutton">
+                <property name="label" translatable="yes">_Remember</property>
+                <property name="use_action_appearance">False</property>
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="receives_default">False</property>
+                <property name="use_action_appearance">False</property>
+                <property name="use_underline">True</property>
+                <property name="xalign">0</property>
+                <property name="draw_indicator">True</property>
+              </object>
+              <packing>
+                <property name="expand">True</property>
+                <property name="fill">True</property>
+                <property name="position">2</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkHButtonBox" id="hbuttonbox1">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <child>
+                  <object class="GtkButton" id="login_button">
+                    <property name="label" translatable="yes">Login</property>
+                    <property name="use_action_appearance">False</property>
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                    <property name="receives_default">True</property>
+                    <property name="use_action_appearance">False</property>
+                  </object>
+                  <packing>
+                    <property name="expand">False</property>
+                    <property name="fill">False</property>
+                    <property name="position">0</property>
+                  </packing>
+                </child>
+              </object>
+              <packing>
+                <property name="expand">True</property>
+                <property name="fill">True</property>
+                <property name="position">3</property>
+              </packing>
+            </child>
+          </object>
+        </child>
+      </object>
+    </child>
+  </object>
+</interface>
diff --git a/plugins/shotwell-publishing-extras/rajce_publishing_options_pane.glade b/plugins/shotwell-publishing-extras/rajce_publishing_options_pane.glade
new file mode 100644
index 0000000..c6b992c
--- /dev/null
+++ b/plugins/shotwell-publishing-extras/rajce_publishing_options_pane.glade
@@ -0,0 +1,275 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interface>
+  <!-- interface-requires gtk+ 3.0 -->
+  <object class="GtkBox" id="rajce_pane_widget">
+    <property name="visible">True</property>
+    <property name="can_focus">False</property>
+    <property name="orientation">vertical</property>
+    <property name="spacing">1</property>
+    <child>
+      <placeholder/>
+    </child>
+    <child>
+      <object class="GtkBox" id="user_area_box">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="halign">center</property>
+        <property name="margin_left">36</property>
+        <property name="margin_right">36</property>
+        <property name="margin_top">24</property>
+        <property name="margin_bottom">24</property>
+        <property name="spacing">12</property>
+        <child>
+          <object class="GtkLabel" id="login_identity_label">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="valign">center</property>
+            <property name="label" translatable="yes">you are logged in rajce as $name</property>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="padding">4</property>
+            <property name="position">0</property>
+          </packing>
+        </child>
+        <child>
+          <object class="GtkButton" id="logout_button">
+            <property name="label" translatable="yes">_Logout</property>
+            <property name="use_action_appearance">False</property>
+            <property name="width_request">64</property>
+            <property name="height_request">24</property>
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">True</property>
+            <property name="valign">center</property>
+            <property name="hexpand">True</property>
+            <property name="vexpand">True</property>
+            <property name="use_action_appearance">False</property>
+            <property name="use_underline">True</property>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+      </object>
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">True</property>
+        <property name="position">1</property>
+      </packing>
+    </child>
+    <child>
+      <object class="GtkBox" id="album_gallery_layout_box">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="margin_left">16</property>
+        <property name="margin_right">16</property>
+        <property name="orientation">vertical</property>
+        <child>
+          <placeholder/>
+        </child>
+        <child>
+          <object class="GtkGrid" id="album_choice_area_grid">
+            <property name="visible">True</property>
+            <property name="can_focus">False</property>
+            <property name="margin_left">1</property>
+            <property name="row_spacing">8</property>
+            <child>
+              <object class="GtkComboBoxText" id="existing_albums_combo">
+                <property name="width_request">320</property>
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="entry_text_column">0</property>
+                <property name="id_column">1</property>
+              </object>
+              <packing>
+                <property name="left_attach">1</property>
+                <property name="top_attach">1</property>
+                <property name="width">1</property>
+                <property name="height">1</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkEntry" id="new_album_entry">
+                <property name="width_request">320</property>
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="invisible_char">•</property>
+              </object>
+              <packing>
+                <property name="left_attach">1</property>
+                <property name="top_attach">2</property>
+                <property name="width">1</property>
+                <property name="height">1</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkRadioButton" id="use_existing_radio">
+                <property name="label" translatable="yes">An _existing album:</property>
+                <property name="use_action_appearance">False</property>
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="receives_default">False</property>
+                <property name="halign">start</property>
+                <property name="margin_left">4</property>
+                <property name="margin_right">4</property>
+                <property name="use_action_appearance">False</property>
+                <property name="use_underline">True</property>
+                <property name="xalign">0</property>
+                <property name="active">True</property>
+                <property name="draw_indicator">True</property>
+                <property name="group">create_new_radio</property>
+              </object>
+              <packing>
+                <property name="left_attach">0</property>
+                <property name="top_attach">1</property>
+                <property name="width">1</property>
+                <property name="height">1</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkRadioButton" id="create_new_radio">
+                <property name="label" translatable="yes">A _new album named:</property>
+                <property name="use_action_appearance">False</property>
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="receives_default">False</property>
+                <property name="halign">start</property>
+                <property name="margin_left">4</property>
+                <property name="margin_right">4</property>
+                <property name="use_action_appearance">False</property>
+                <property name="use_underline">True</property>
+                <property name="xalign">0</property>
+                <property name="yalign">0.47999998927116394</property>
+                <property name="active">True</property>
+                <property name="draw_indicator">True</property>
+              </object>
+              <packing>
+                <property name="left_attach">0</property>
+                <property name="top_attach">2</property>
+                <property name="width">1</property>
+                <property name="height">1</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkCheckButton" id="hide_check">
+                <property name="label" translatable="yes">_Hide album</property>
+                <property name="use_action_appearance">False</property>
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="receives_default">False</property>
+                <property name="use_action_appearance">False</property>
+                <property name="use_underline">True</property>
+                <property name="xalign">0</property>
+                <property name="draw_indicator">True</property>
+              </object>
+              <packing>
+                <property name="left_attach">1</property>
+                <property name="top_attach">3</property>
+                <property name="width">1</property>
+                <property name="height">1</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkCheckButton" id="show_check">
+                <property name="label" translatable="yes">Open target _album in browser</property>
+                <property name="use_action_appearance">False</property>
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="receives_default">False</property>
+                <property name="use_action_appearance">False</property>
+                <property name="use_underline">True</property>
+                <property name="xalign">0</property>
+                <property name="draw_indicator">True</property>
+              </object>
+              <packing>
+                <property name="left_attach">1</property>
+                <property name="top_attach">4</property>
+                <property name="width">1</property>
+                <property name="height">1</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkLabel" id="publish_to_label">
+                <property name="visible">True</property>
+                <property name="can_focus">False</property>
+                <property name="margin_top">4</property>
+                <property name="margin_bottom">8</property>
+                <property name="xalign">0</property>
+                <property name="label" translatable="yes">$mediatype will appear in</property>
+              </object>
+              <packing>
+                <property name="left_attach">0</property>
+                <property name="top_attach">0</property>
+                <property name="width">2</property>
+                <property name="height">1</property>
+              </packing>
+            </child>
+            <child>
+              <placeholder/>
+            </child>
+            <child>
+              <placeholder/>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="padding">4</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+      </object>
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">True</property>
+        <property name="position">2</property>
+      </packing>
+    </child>
+    <child>
+      <placeholder/>
+    </child>
+    <child>
+      <object class="GtkBox" id="button_area_box">
+        <property name="visible">True</property>
+        <property name="can_focus">False</property>
+        <property name="margin_left">196</property>
+        <property name="margin_right">196</property>
+        <property name="margin_top">24</property>
+        <property name="margin_bottom">24</property>
+        <property name="spacing">128</property>
+        <property name="homogeneous">True</property>
+        <child>
+          <object class="GtkButton" id="publish_button">
+            <property name="label" translatable="yes">_Publish</property>
+            <property name="use_action_appearance">False</property>
+            <property name="width_request">96</property>
+            <property name="height_request">30</property>
+            <property name="visible">True</property>
+            <property name="can_focus">True</property>
+            <property name="receives_default">True</property>
+            <property name="halign">center</property>
+            <property name="valign">center</property>
+            <property name="hexpand">True</property>
+            <property name="vexpand">True</property>
+            <property name="use_action_appearance">False</property>
+            <property name="use_underline">True</property>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">True</property>
+            <property name="position">0</property>
+          </packing>
+        </child>
+      </object>
+      <packing>
+        <property name="expand">False</property>
+        <property name="fill">True</property>
+        <property name="position">4</property>
+      </packing>
+    </child>
+  </object>
+</interface>
diff --git a/plugins/shotwell-publishing-extras/shotwell-publishing-extras.vala b/plugins/shotwell-publishing-extras/shotwell-publishing-extras.vala
index c83acf1..c5e32ee 100644
--- a/plugins/shotwell-publishing-extras/shotwell-publishing-extras.vala
+++ b/plugins/shotwell-publishing-extras/shotwell-publishing-extras.vala
@@ -12,6 +12,8 @@ private class ShotwellPublishingExtraServices : Object, Spit.Module {
     public ShotwellPublishingExtraServices(GLib.File module_file) {
         pluggables += new YandexService();
         pluggables += new TumblrService(module_file.get_parent());
+        pluggables += new RajceService(module_file.get_parent());
+        pluggables += new Gallery3Service(module_file.get_parent());
     }
     
     public unowned string get_module_name() {
diff --git a/plugins/shotwell-publishing/FacebookPublishing.vala b/plugins/shotwell-publishing/FacebookPublishing.vala
index d3f5a0f..79b7a0a 100644
--- a/plugins/shotwell-publishing/FacebookPublishing.vala
+++ b/plugins/shotwell-publishing/FacebookPublishing.vala
@@ -1494,7 +1494,7 @@ internal class GraphSession {
             
             case EXPIRED_SESSION_STATUS_CODE:
                 error = new Spit.Publishing.PublishingError.EXPIRED_SESSION(
-                    "OAuth Access Token has Expired. Logout user.", real_message.get_uri(), msg.status_code);
+                    "OAuth Access Token has Expired. Logout user.");
             break;
             
             case Soup.KnownStatusCode.CANT_RESOLVE:
diff --git a/plugins/shotwell-publishing/FlickrPublishing.vala b/plugins/shotwell-publishing/FlickrPublishing.vala
index dc483cc..dcf7971 100644
--- a/plugins/shotwell-publishing/FlickrPublishing.vala
+++ b/plugins/shotwell-publishing/FlickrPublishing.vala
@@ -397,7 +397,7 @@ public class FlickrPublisher : Spit.Publishing.Publisher, GLib.Object {
             
             if (split_pair.length != 2)
                 host.post_error(new Spit.Publishing.PublishingError.MALFORMED_RESPONSE(
-                    "'%s' isn't a valid response to an OAuth authentication request"));
+                    "'%s' isn't a valid response to an OAuth authentication request", response));
 
             if (split_pair[0] == "oauth_token")
                 oauth_token = split_pair[1];
@@ -407,7 +407,7 @@ public class FlickrPublisher : Spit.Publishing.Publisher, GLib.Object {
         
         if (oauth_token == null || oauth_token_secret == null)
             host.post_error(new Spit.Publishing.PublishingError.MALFORMED_RESPONSE(
-                "'%s' isn't a valid response to an OAuth authentication request"));
+                "'%s' isn't a valid response to an OAuth authentication request", response));
         
         
         on_authentication_token_available(oauth_token, oauth_token_secret);
-- 
cgit v1.2.3