diff options
author | Jörg Frings-Fürst <debian@jff-webhosting.net> | 2014-09-23 09:36:56 +0200 |
---|---|---|
committer | Jörg Frings-Fürst <debian@jff-webhosting.net> | 2014-09-23 09:36:56 +0200 |
commit | 8004f1a7ad3cc6a3659517f3374bdf1b63416b24 (patch) | |
tree | b7b82cc6e01cd47d22f660e25508a7d8d555dc89 /plugins | |
parent | 28c68a75acac7ac3ce7fb169bc6aa65e16551c53 (diff) | |
parent | 566dc060676b41e1e58a446b7dcc4159e242fee6 (diff) |
Merge tag 'upstream/0.20.0'
Upstream version 0.20.0
Diffstat (limited to 'plugins')
-rw-r--r-- | plugins/common/Resources.vala | 2 | ||||
-rw-r--r-- | plugins/plugins.mk | 8 | ||||
-rw-r--r-- | plugins/shotwell-publishing-extras/GalleryConnector.vala | 2034 | ||||
-rw-r--r-- | plugins/shotwell-publishing-extras/Makefile | 11 | ||||
-rw-r--r-- | plugins/shotwell-publishing-extras/RajcePublishing.vala | 1554 | ||||
-rw-r--r-- | plugins/shotwell-publishing-extras/gallery3.png | bin | 0 -> 802 bytes | |||
-rw-r--r-- | plugins/shotwell-publishing-extras/gallery3_authentication_pane.glade | 245 | ||||
-rw-r--r-- | plugins/shotwell-publishing-extras/gallery3_publishing_options_pane.glade | 282 | ||||
-rw-r--r-- | plugins/shotwell-publishing-extras/rajce.png | bin | 0 -> 1650 bytes | |||
-rw-r--r-- | plugins/shotwell-publishing-extras/rajce_authentication_pane.glade | 150 | ||||
-rw-r--r-- | plugins/shotwell-publishing-extras/rajce_publishing_options_pane.glade | 275 | ||||
-rw-r--r-- | plugins/shotwell-publishing-extras/shotwell-publishing-extras.vala | 2 | ||||
-rw-r--r-- | plugins/shotwell-publishing/FacebookPublishing.vala | 2 | ||||
-rw-r--r-- | plugins/shotwell-publishing/FlickrPublishing.vala | 4 |
14 files changed, 4563 insertions, 6 deletions
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 Binary files differnew file mode 100644 index 0000000..9e3c5cc --- /dev/null +++ b/plugins/shotwell-publishing-extras/gallery3.png 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 Binary files differnew file mode 100644 index 0000000..8ab0995 --- /dev/null +++ b/plugins/shotwell-publishing-extras/rajce.png 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); |