diff options
author | Jörg Frings-Fürst <debian@jff.email> | 2023-06-28 21:35:52 +0200 |
---|---|---|
committer | Jörg Frings-Fürst <debian@jff.email> | 2023-06-28 21:35:52 +0200 |
commit | b86540b743f1a87a163ffb811c8fe22a01fefa38 (patch) | |
tree | b47cb3bb83c2377234226fb3987ab3320a987dd9 /plugins/shotwell-publishing | |
parent | ac6e0b731b9f0b2efd392e3309a5c07e2a66adad (diff) | |
parent | e905d8e16eec152d19797937f13ba3cf4b8f8aca (diff) |
Merge branch 'release/debian/0.32.1-1'debian/0.32.1-1
Diffstat (limited to 'plugins/shotwell-publishing')
20 files changed, 960 insertions, 2534 deletions
diff --git a/plugins/shotwell-publishing/FacebookPublishing.vala b/plugins/shotwell-publishing/FacebookPublishing.vala deleted file mode 100644 index 1633269..0000000 --- a/plugins/shotwell-publishing/FacebookPublishing.vala +++ /dev/null @@ -1,1392 +0,0 @@ -/* Copyright 2016 Software Freedom Conservancy Inc. - * - * 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 FacebookService : Object, Spit.Pluggable, Spit.Publishing.Service { - private const string ICON_FILENAME = "facebook.png"; - - private static Gdk.Pixbuf[] icon_pixbuf_set = null; - - public FacebookService(GLib.File resource_directory) { - if (icon_pixbuf_set == null) - icon_pixbuf_set = Resources.load_from_resource - (Resources.RESOURCE_PATH + "/" + 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.facebook"; - } - - public unowned string get_pluggable_name() { - return "Facebook"; - } - - public void get_info(ref Spit.PluggableInfo info) { - info.authors = "Lucas Beeler"; - info.copyright = _("Copyright 2016 Software Freedom Conservancy Inc."); - 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 void activation(bool enabled) { - } - - public Spit.Publishing.Publisher create_publisher(Spit.Publishing.PluginHost host) { - return new Publishing.Facebook.FacebookPublisher(this, host); - } - - public Spit.Publishing.Publisher.MediaType get_supported_media() { - return (Spit.Publishing.Publisher.MediaType.PHOTO | - Spit.Publishing.Publisher.MediaType.VIDEO); - } -} - -namespace Publishing.Facebook { -// global parameters for the Facebook publishing plugin -- don't touch these (unless you really, -// truly, deep-down know what you're doing) -public const string SERVICE_NAME = "facebook"; -internal const string USER_VISIBLE_NAME = "Facebook"; -internal const string DEFAULT_ALBUM_NAME = _("Shotwell Connect"); -internal const int EXPIRED_SESSION_STATUS_CODE = 400; - -internal class Album { - public string name; - public string id; - - public Album(string name, string id) { - this.name = name; - this.id = id; - } -} - -internal enum Resolution { - STANDARD, - HIGH; - - public string get_name() { - switch (this) { - case STANDARD: - return _("Standard (720 pixels)"); - - case HIGH: - return _("Large (2048 pixels)"); - - default: - error("Unknown resolution %s", this.to_string()); - } - } - - public int get_pixels() { - switch (this) { - case STANDARD: - return 720; - - case HIGH: - return 2048; - - default: - error("Unknown resolution %s", this.to_string()); - } - } -} - -internal class PublishingParameters { - public const int UNKNOWN_ALBUM = -1; - - public bool strip_metadata; - public Album[] albums; - public int target_album; - public string? new_album_name; // the name of the new album being created during this - // publishing interaction or null if publishing to an existing - // album - - public string? privacy_object; // a serialized JSON object encoding the privacy settings of the - // published resources - public Resolution resolution; - - public PublishingParameters() { - this.albums = null; - this.privacy_object = null; - this.target_album = UNKNOWN_ALBUM; - this.new_album_name = null; - this.strip_metadata = false; - this.resolution = Resolution.HIGH; - } - - public void add_album(string name, string id) { - if (albums == null) - albums = new Album[0]; - - Album new_album = new Album(name, id); - albums += new_album; - } - - public void set_target_album_by_name(string? name) { - if (name == null) { - target_album = UNKNOWN_ALBUM; - return; - } - - for (int i = 0; i < albums.length; i++) { - - if (albums[i].name == name) { - target_album = i; - return; - } - } - - target_album = UNKNOWN_ALBUM; - } - - public string? get_target_album_name() { - if (albums == null || target_album == UNKNOWN_ALBUM) - return null; - - return albums[target_album].name; - } - - public string? get_target_album_id() { - if (albums == null || target_album == UNKNOWN_ALBUM) - return null; - - return albums[target_album].id; - } -} - -public class FacebookPublisher : Spit.Publishing.Publisher, GLib.Object { - private PublishingParameters publishing_params; - private weak Spit.Publishing.PluginHost host = null; - private Spit.Publishing.ProgressCallback progress_reporter = null; - private weak Spit.Publishing.Service service = null; - private Spit.Publishing.Authenticator authenticator = null; - private bool running = false; - private GraphSession graph_session; - private PublishingOptionsPane? publishing_options_pane = null; - private Uploader? uploader = null; - private string? uid = null; - private string? username = null; - - public FacebookPublisher(Spit.Publishing.Service service, - Spit.Publishing.PluginHost host) { - debug("FacebookPublisher instantiated."); - - this.service = service; - this.host = host; - - this.publishing_params = new PublishingParameters(); - this.authenticator = - Publishing.Authenticator.Factory.get_instance().create("facebook", - host); - - this.graph_session = new GraphSession(); - graph_session.authenticated.connect(on_session_authenticated); - } - - private bool get_persistent_strip_metadata() { - return host.get_config_bool("strip_metadata", false); - } - - private void set_persistent_strip_metadata(bool strip_metadata) { - host.set_config_bool("strip_metadata", strip_metadata); - } - - // Part of the fix for #3232. These have to be - // public so the legacy options pane may use them. - public int get_persistent_default_size() { - return host.get_config_int("default_size", 0); - } - - public void set_persistent_default_size(int size) { - host.set_config_int("default_size", size); - } - - /* - private void do_test_connection_to_endpoint() { - debug("ACTION: testing connection to Facebook endpoint."); - host.set_service_locked(true); - - host.install_static_message_pane(_("Testing connection to Facebook…")); - - GraphMessage endpoint_test_message = graph_session.new_endpoint_test(); - endpoint_test_message.completed.connect(on_endpoint_test_completed); - endpoint_test_message.failed.connect(on_endpoint_test_error); - - graph_session.send_message(endpoint_test_message); - } - */ - - private void do_fetch_user_info() { - debug("ACTION: fetching user information."); - - host.set_service_locked(true); - host.install_account_fetch_wait_pane(); - - GraphMessage user_info_message = graph_session.new_query("/me"); - - user_info_message.completed.connect(on_fetch_user_info_completed); - user_info_message.failed.connect(on_fetch_user_info_error); - - graph_session.send_message(user_info_message); - } - - private void do_fetch_album_descriptions() { - debug("ACTION: fetching album list."); - - host.set_service_locked(true); - host.install_account_fetch_wait_pane(); - - GraphMessage albums_message = graph_session.new_query("/%s/albums".printf(uid)); - - albums_message.completed.connect(on_fetch_albums_completed); - albums_message.failed.connect(on_fetch_albums_error); - - graph_session.send_message(albums_message); - } - - private void do_extract_user_info_from_json(string json) { - debug("ACTION: extracting user info from JSON response."); - - try { - Json.Parser parser = new Json.Parser(); - parser.load_from_data(json); - - Json.Node root = parser.get_root(); - Json.Object response_object = root.get_object(); - uid = response_object.get_string_member("id"); - username = response_object.get_string_member("name"); - } catch (Error error) { - host.post_error(new Spit.Publishing.PublishingError.MALFORMED_RESPONSE(error.message)); - return; - } - - on_user_info_extracted(); - } - - private void do_extract_albums_from_json(string json) { - debug("ACTION: extracting album info from JSON response."); - - try { - Json.Parser parser = new Json.Parser(); - parser.load_from_data(json); - - Json.Node root = parser.get_root(); - Json.Object response_object = root.get_object(); - Json.Array album_list = response_object.get_array_member("data"); - - publishing_params.albums = new Album[0]; - - for (int i = 0; i < album_list.get_length(); i++) { - Json.Object current_album = album_list.get_object_element(i); - string album_id = current_album.get_string_member("id"); - string album_name = current_album.get_string_member("name"); - - // Note that we are completely ignoring the "can_upload" flag in the list of albums - // that we pulled from facebook eariler -- effectively, we add every album to the - // publishing_params album list regardless of the value of its can_upload flag. In - // the future we may wish to make adding to the publishing_params album list - // conditional on the value of the can_upload flag being true - publishing_params.add_album(album_name, album_id); - } - } catch (Error error) { - host.post_error(new Spit.Publishing.PublishingError.MALFORMED_RESPONSE(error.message)); - return; - } - - on_albums_extracted(); - } - - private void do_create_new_album() { - debug("ACTION: creating a new album named \"%s\".\n", publishing_params.new_album_name); - - host.set_service_locked(true); - host.install_static_message_pane(_("Creating album…")); - - GraphMessage create_album_message = graph_session.new_create_album( - publishing_params.new_album_name, publishing_params.privacy_object); - - create_album_message.completed.connect(on_create_album_completed); - create_album_message.failed.connect(on_create_album_error); - - graph_session.send_message(create_album_message); - } - - private void do_show_publishing_options_pane() { - debug("ACTION: showing publishing options pane."); - - host.set_service_locked(false); - Gtk.Builder builder = new Gtk.Builder(); - - try { - // the trailing get_path() is required, since add_from_file can't cope - // with File objects directly and expects a pathname instead. - builder.add_from_resource (Resources.RESOURCE_PATH + "/" + - "facebook_publishing_options_pane.ui"); - } 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 Facebook can’t continue."))); - return; - } - - publishing_options_pane = new PublishingOptionsPane(username, publishing_params.albums, - host.get_publishable_media_type(), this, builder, get_persistent_strip_metadata(), - authenticator.can_logout()); - publishing_options_pane.logout.connect(on_publishing_options_pane_logout); - publishing_options_pane.publish.connect(on_publishing_options_pane_publish); - host.install_dialog_pane(publishing_options_pane, - Spit.Publishing.PluginHost.ButtonMode.CANCEL); - } - - private void do_logout() { - debug("ACTION: clearing persistent session information and restaring interaction."); - this.authenticator.logout(); - - running = false; - start(); - } - - private void do_add_new_local_album_from_json(string album_name, string json) { - try { - Json.Parser parser = new Json.Parser(); - parser.load_from_data(json); - - Json.Node root = parser.get_root(); - Json.Object response_object = root.get_object(); - string album_id = response_object.get_string_member("id"); - - publishing_params.add_album(album_name, album_id); - } catch (Error error) { - host.post_error(new Spit.Publishing.PublishingError.MALFORMED_RESPONSE(error.message)); - return; - } - - publishing_params.set_target_album_by_name(album_name); - do_upload(); - } - - - private void on_authenticator_succeeded() { - debug("EVENT: Authenticator login succeeded."); - - do_authenticate_session(); - } - - private void on_authenticator_failed() { - } - - private void do_authenticate_session() { - var parameter = this.authenticator.get_authentication_parameter(); - Variant access_token; - if (!parameter.lookup_extended("AccessToken", null, out access_token)) { - critical("Authenticator signalled success, but does not provide access token"); - assert_not_reached(); - } - graph_session.authenticated.connect(on_session_authenticated); - graph_session.authenticate(access_token.get_string()); - } - - private void do_upload() { - debug("ACTION: uploading photos to album '%s'", - publishing_params.target_album == PublishingParameters.UNKNOWN_ALBUM ? "(none)" : - publishing_params.get_target_album_name()); - - host.set_service_locked(true); - - progress_reporter = host.serialize_publishables(publishing_params.resolution.get_pixels(), - publishing_params.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; - - Spit.Publishing.Publishable[] publishables = host.get_publishables(); - uploader = new Uploader(graph_session, publishing_params, publishables); - - uploader.upload_complete.connect(on_upload_complete); - uploader.upload_error.connect(on_upload_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(); - } - - private void on_generic_error(Spit.Publishing.PublishingError error) { - if (error is Spit.Publishing.PublishingError.EXPIRED_SESSION) - do_logout(); - else - host.post_error(error); - } - -#if 0 - private void on_endpoint_test_completed(GraphMessage message) { - message.completed.disconnect(on_endpoint_test_completed); - message.failed.disconnect(on_endpoint_test_error); - - if (!is_running()) - return; - - debug("EVENT: endpoint test transaction detected that the Facebook endpoint is alive."); - - do_hosted_web_authentication(); - } - - private void on_endpoint_test_error(GraphMessage message, - Spit.Publishing.PublishingError error) { - message.completed.disconnect(on_endpoint_test_completed); - message.failed.disconnect(on_endpoint_test_error); - - if (!is_running()) - return; - - debug("EVENT: endpoint test transaction failed to detect a connection to the Facebook " + - "endpoint" + error.message); - - on_generic_error(error); - } -#endif - - private void on_session_authenticated() { - graph_session.authenticated.disconnect(on_session_authenticated); - - if (!is_running()) - return; - - assert(graph_session.is_authenticated()); - debug("EVENT: an authenticated session has become available."); - - do_fetch_user_info(); - } - - private void on_fetch_user_info_completed(GraphMessage message) { - message.completed.disconnect(on_fetch_user_info_completed); - message.failed.disconnect(on_fetch_user_info_error); - - if (!is_running()) - return; - - debug("EVENT: user info fetch completed; response = '%s'.", message.get_response_body()); - - do_extract_user_info_from_json(message.get_response_body()); - } - - private void on_fetch_user_info_error(GraphMessage message, - Spit.Publishing.PublishingError error) { - message.completed.disconnect(on_fetch_user_info_completed); - message.failed.disconnect(on_fetch_user_info_error); - - if (!is_running()) - return; - - debug("EVENT: fetching user info generated and error."); - - on_generic_error(error); - } - - private void on_user_info_extracted() { - if (!is_running()) - return; - - debug("EVENT: user info extracted from JSON response: uid = %s; name = %s.", uid, username); - - do_fetch_album_descriptions(); - } - - private void on_fetch_albums_completed(GraphMessage message) { - message.completed.disconnect(on_fetch_albums_completed); - message.failed.disconnect(on_fetch_albums_error); - - if (!is_running()) - return; - - debug("EVENT: album descriptions fetch transaction completed; response = '%s'.", - message.get_response_body()); - - do_extract_albums_from_json(message.get_response_body()); - } - - private void on_fetch_albums_error(GraphMessage message, - Spit.Publishing.PublishingError err) { - message.completed.disconnect(on_fetch_albums_completed); - message.failed.disconnect(on_fetch_albums_error); - - if (!is_running()) - return; - - debug("EVENT: album description fetch attempt generated an error."); - - on_generic_error(err); - } - - private void on_albums_extracted() { - if (!is_running()) - return; - - debug("EVENT: successfully extracted %d albums from JSON response", - publishing_params.albums.length); - - do_show_publishing_options_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 clicked 'Logout' in publishing options pane."); - - do_logout(); - } - - private void on_publishing_options_pane_publish(string? target_album, string privacy_setting, - Resolution resolution, bool strip_metadata) { - 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 clicked 'Publish' in publishing options pane."); - - publishing_params.strip_metadata = strip_metadata; - set_persistent_strip_metadata(strip_metadata); - publishing_params.resolution = resolution; - set_persistent_default_size(resolution); - publishing_params.privacy_object = privacy_setting; - - if (target_album != null) { - // we are publishing at least one photo so we need the name of an album to which - // we'll upload the photo(s) - publishing_params.set_target_album_by_name(target_album); - if (publishing_params.target_album != PublishingParameters.UNKNOWN_ALBUM) { - do_upload(); - } else { - publishing_params.new_album_name = target_album; - do_create_new_album(); - } - } else { - // we're publishing only videos and we don't need an album name - do_upload(); - } - } - - private void on_create_album_completed(GraphMessage message) { - message.completed.disconnect(on_create_album_completed); - message.failed.disconnect(on_create_album_error); - - assert(publishing_params.new_album_name != null); - - if (!is_running()) - return; - - debug("EVENT: created new album resource on remote host; response body = %s.\n", - message.get_response_body()); - - do_add_new_local_album_from_json(publishing_params.new_album_name, - message.get_response_body()); - } - - private void on_create_album_error(GraphMessage message, Spit.Publishing.PublishingError err) { - message.completed.disconnect(on_create_album_completed); - message.failed.disconnect(on_create_album_error); - - if (!is_running()) - return; - - debug("EVENT: attempt to create new album generated an error."); - - on_generic_error(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_upload_complete(Uploader uploader, int num_published) { - uploader.upload_complete.disconnect(on_upload_complete); - uploader.upload_error.disconnect(on_upload_error); - - if (!is_running()) - return; - - debug("EVENT: uploader reports upload complete; %d items published.", num_published); - - do_show_success_pane(); - } - - private void on_upload_error(Uploader uploader, Spit.Publishing.PublishingError err) { - uploader.upload_complete.disconnect(on_upload_complete); - uploader.upload_error.disconnect(on_upload_error); - - if (!is_running()) - return; - - debug("EVENT: uploader reports upload error = '%s'.", err.message); - - host.post_error(err); - } - - public Spit.Publishing.Service get_service() { - return service; - } - - public string get_service_name() { - return SERVICE_NAME; - } - - public string get_user_visible_name() { - return USER_VISIBLE_NAME; - } - - public void start() { - if (is_running()) - return; - - debug("FacebookPublisher: starting interaction."); - - running = true; - - // reset all publishing parameters to their default values -- in case this start is - // actually a restart - publishing_params = new PublishingParameters(); - - this.authenticator.authenticated.connect(on_authenticator_succeeded); - this.authenticator.authentication_failed.connect(on_authenticator_failed); - this.authenticator.authenticate(); - } - - public void stop() { - debug("FacebookPublisher: stop( ) invoked."); - - if (graph_session != null) - graph_session.stop_transactions(); - - host = null; - running = false; - } - - public bool is_running() { - return running; - } -} - -internal class PublishingOptionsPane : Spit.Publishing.DialogPane, GLib.Object { - private Gtk.Builder builder; - private Gtk.Box pane_widget = null; - private Gtk.RadioButton use_existing_radio = null; - private Gtk.RadioButton create_new_radio = null; - private Gtk.ComboBoxText existing_albums_combo = null; - private Gtk.ComboBoxText visibility_combo = null; - private Gtk.Entry new_album_entry = null; - private Gtk.CheckButton strip_metadata_check = null; - private Gtk.Button publish_button = null; - private Gtk.Button logout_button = null; - private Gtk.Label how_to_label = null; - private Album[] albums = null; - private FacebookPublisher publisher = null; - private PrivacyDescription[] privacy_descriptions; - - private Resolution[] possible_resolutions; - private Gtk.ComboBoxText resolution_combo = null; - - private Spit.Publishing.Publisher.MediaType media_type; - - private const string HEADER_LABEL_TEXT = _("You are logged into Facebook as %s.\n\n"); - private const string PHOTOS_LABEL_TEXT = _("Where would you like to publish the selected photos?"); - private const string RESOLUTION_LABEL_TEXT = _("Upload _size:"); - private const int CONTENT_GROUP_SPACING = 32; - private const int STANDARD_ACTION_BUTTON_WIDTH = 128; - - public signal void logout(); - public signal void publish(string? target_album, string privacy_setting, - Resolution target_resolution, bool strip_metadata); - - private class PrivacyDescription { - public string description; - public string privacy_setting; - - public PrivacyDescription(string description, string privacy_setting) { - this.description = description; - this.privacy_setting = privacy_setting; - } - } - - public PublishingOptionsPane(string username, Album[] albums, - Spit.Publishing.Publisher.MediaType media_type, FacebookPublisher publisher, - Gtk.Builder builder, bool strip_metadata, bool can_logout) { - - this.builder = builder; - assert(builder != null); - assert(builder.get_objects().length() > 0); - - this.albums = albums; - this.privacy_descriptions = create_privacy_descriptions(); - - this.possible_resolutions = create_resolution_list(); - this.publisher = publisher; - - // we'll need to know if the user is importing video or not when sorting out visibility. - this.media_type = media_type; - - pane_widget = (Gtk.Box) builder.get_object("facebook_pane_box"); - pane_widget.set_border_width(16); - - use_existing_radio = (Gtk.RadioButton) this.builder.get_object("use_existing_radio"); - create_new_radio = (Gtk.RadioButton) this.builder.get_object("create_new_radio"); - existing_albums_combo = (Gtk.ComboBoxText) this.builder.get_object("existing_albums_combo"); - visibility_combo = (Gtk.ComboBoxText) this.builder.get_object("visibility_combo"); - publish_button = (Gtk.Button) this.builder.get_object("publish_button"); - logout_button = (Gtk.Button) this.builder.get_object("logout_button"); - if (!can_logout) { - logout_button.parent.remove (logout_button); - } - new_album_entry = (Gtk.Entry) this.builder.get_object("new_album_entry"); - resolution_combo = (Gtk.ComboBoxText) this.builder.get_object("resolution_combo"); - how_to_label = (Gtk.Label) this.builder.get_object("how_to_label"); - strip_metadata_check = (Gtk.CheckButton) this.builder.get_object("strip_metadata_check"); - - create_new_radio.clicked.connect(on_create_new_toggled); - use_existing_radio.clicked.connect(on_use_existing_toggled); - - string label_text = HEADER_LABEL_TEXT.printf(username); - if ((media_type & Spit.Publishing.Publisher.MediaType.PHOTO) != 0) - label_text += PHOTOS_LABEL_TEXT; - how_to_label.set_label(label_text); - strip_metadata_check.set_active(strip_metadata); - - setup_visibility_combo(); - visibility_combo.set_active(0); - - publish_button.clicked.connect(on_publish_button_clicked); - logout_button.clicked.connect(on_logout_button_clicked); - - setup_resolution_combo(); - resolution_combo.set_active(publisher.get_persistent_default_size()); - resolution_combo.changed.connect(on_size_changed); - - // Ticket #3175, part 2: make sure this widget starts out sensitive - // if it needs to by checking whether we're starting with a video - // or a new gallery. - visibility_combo.set_sensitive( - (create_new_radio != null && create_new_radio.active) || - ((media_type & Spit.Publishing.Publisher.MediaType.VIDEO) != 0)); - - // if publishing only videos, disable all photo-specific controls - if (media_type == Spit.Publishing.Publisher.MediaType.VIDEO) { - strip_metadata_check.set_active(false); - strip_metadata_check.set_sensitive(false); - resolution_combo.set_sensitive(false); - use_existing_radio.set_sensitive(false); - create_new_radio.set_sensitive(false); - existing_albums_combo.set_sensitive(false); - new_album_entry.set_sensitive(false); - } - } - - private bool publishing_photos() { - return (media_type & Spit.Publishing.Publisher.MediaType.PHOTO) != 0; - } - - private void setup_visibility_combo() { - foreach (PrivacyDescription p in privacy_descriptions) - visibility_combo.append_text(p.description); - } - - private void setup_resolution_combo() { - foreach (Resolution res in possible_resolutions) - resolution_combo.append_text(res.get_name()); - } - - private void on_use_existing_toggled() { - if (use_existing_radio.active) { - existing_albums_combo.set_sensitive(true); - new_album_entry.set_sensitive(false); - - // Ticket #3175 - if we're not adding a new gallery - // or a video, then we shouldn't be allowed tof - // choose visibility, since it has no effect. - visibility_combo.set_sensitive((media_type & Spit.Publishing.Publisher.MediaType.VIDEO) != 0); - - existing_albums_combo.grab_focus(); - } - } - - private void on_create_new_toggled() { - if (create_new_radio.active) { - existing_albums_combo.set_sensitive(false); - new_album_entry.set_sensitive(true); - new_album_entry.grab_focus(); - - // Ticket #3175 - if we're creating a new gallery, make sure this is - // active, since it may have possibly been set inactive. - visibility_combo.set_sensitive(true); - } - } - - private void on_size_changed() { - publisher.set_persistent_default_size(resolution_combo.get_active()); - } - - private void on_logout_button_clicked() { - logout(); - } - - private void on_publish_button_clicked() { - string album_name; - string privacy_setting = privacy_descriptions[visibility_combo.get_active()].privacy_setting; - - Resolution resolution_setting; - - if (publishing_photos()) { - resolution_setting = possible_resolutions[resolution_combo.get_active()]; - if (use_existing_radio.active) { - album_name = existing_albums_combo.get_active_text(); - } else { - album_name = new_album_entry.get_text(); - } - } else { - resolution_setting = Resolution.STANDARD; - album_name = null; - } - - publish(album_name, privacy_setting, resolution_setting, strip_metadata_check.get_active()); - } - - private PrivacyDescription[] create_privacy_descriptions() { - PrivacyDescription[] result = new PrivacyDescription[0]; - - result += new PrivacyDescription(_("Just me"), "{ 'value' : 'SELF' }"); - result += new PrivacyDescription(_("Friends"), "{ 'value' : 'ALL_FRIENDS' }"); - result += new PrivacyDescription(_("Everyone"), "{ 'value' : 'EVERYONE' }"); - - return result; - } - - private Resolution[] create_resolution_list() { - Resolution[] result = new Resolution[0]; - - result += Resolution.STANDARD; - result += Resolution.HIGH; - - return result; - } - - public void installed() { - if (publishing_photos()) { - if (albums.length == 0) { - create_new_radio.set_active(true); - new_album_entry.set_text(DEFAULT_ALBUM_NAME); - existing_albums_combo.set_sensitive(false); - use_existing_radio.set_sensitive(false); - } else { - int default_album_seq_num = -1; - int ticker = 0; - foreach (Album album in albums) { - existing_albums_combo.append_text(album.name); - if (album.name == DEFAULT_ALBUM_NAME) - default_album_seq_num = ticker; - ticker++; - } - if (default_album_seq_num != -1) { - existing_albums_combo.set_active(default_album_seq_num); - use_existing_radio.set_active(true); - new_album_entry.set_sensitive(false); - } - else { - create_new_radio.set_active(true); - existing_albums_combo.set_active(0); - existing_albums_combo.set_sensitive(false); - new_album_entry.set_text(DEFAULT_ALBUM_NAME); - } - } - } - - publish_button.grab_focus(); - } - - private void notify_logout() { - logout(); - } - - private void notify_publish(string? target_album, string privacy_setting, Resolution target_resolution) { - publish(target_album, privacy_setting, target_resolution, strip_metadata_check.get_active()); - } - - 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() { - logout.connect(notify_logout); - publish.connect(notify_publish); - - installed(); - } - - public void on_pane_uninstalled() { - logout.disconnect(notify_logout); - publish.disconnect(notify_publish); - } -} - -internal enum Endpoint { - DEFAULT, - VIDEO, - TEST_CONNECTION; - - public string to_uri() { - switch (this) { - case DEFAULT: - return "https://graph.facebook.com/"; - - case VIDEO: - return "https://graph-video.facebook.com/"; - - case TEST_CONNECTION: - return "https://www.facebook.com/"; - - default: - assert_not_reached(); - } - } -} - -internal abstract class GraphMessage { - public signal void completed(); - public signal void failed(Spit.Publishing.PublishingError err); - public signal void data_transmitted(int bytes_sent_so_far, int total_bytes); - - public abstract string get_uri(); - public abstract string get_response_body(); -} - -internal class GraphSession { - private abstract class GraphMessageImpl : GraphMessage { - public Publishing.RESTSupport.HttpMethod method; - public string uri; - public string access_token; - public Soup.Message soup_message; - public weak GraphSession host_session; - public int bytes_so_far; - - protected GraphMessageImpl(GraphSession host_session, Publishing.RESTSupport.HttpMethod method, - string relative_uri, string access_token, Endpoint endpoint = Endpoint.DEFAULT) { - this.method = method; - this.access_token = access_token; - this.host_session = host_session; - this.bytes_so_far = 0; - - string endpoint_uri = endpoint.to_uri(); - try { - Regex starting_slashes = new Regex("^/+"); - this.uri = endpoint_uri + starting_slashes.replace(relative_uri, -1, 0, ""); - } catch (RegexError err) { - assert_not_reached(); - } - } - - public virtual bool prepare_for_transmission() { - return true; - } - - public override string get_uri() { - return uri; - } - - public override string get_response_body() { - return (string) soup_message.response_body.data; - } - - public void on_wrote_body_data(Soup.Buffer chunk) { - bytes_so_far += (int) chunk.length; - - data_transmitted(bytes_so_far, (int) soup_message.request_body.length); - } - } - - private class GraphQueryMessage : GraphMessageImpl { - public GraphQueryMessage(GraphSession host_session, string relative_uri, - string access_token) { - base(host_session, Publishing.RESTSupport.HttpMethod.GET, relative_uri, access_token); - - Soup.URI destination_uri = new Soup.URI(uri + "?access_token=" + access_token); - soup_message = new Soup.Message.from_uri(method.to_string(), destination_uri); - soup_message.wrote_body_data.connect(on_wrote_body_data); - } - } - - private class GraphEndpointProbeMessage : GraphMessageImpl { - public GraphEndpointProbeMessage(GraphSession host_session) { - base(host_session, Publishing.RESTSupport.HttpMethod.GET, "/", "", - Endpoint.TEST_CONNECTION); - - soup_message = new Soup.Message.from_uri(method.to_string(), new Soup.URI(uri)); - soup_message.wrote_body_data.connect(on_wrote_body_data); - } - } - - private class GraphUploadMessage : GraphMessageImpl { - private MappedFile mapped_file = null; - private Spit.Publishing.Publishable publishable; - - public GraphUploadMessage(GraphSession host_session, string access_token, - string relative_uri, Spit.Publishing.Publishable publishable, - bool suppress_titling, string? resource_privacy = null) { - base(host_session, Publishing.RESTSupport.HttpMethod.POST, relative_uri, access_token, - (publishable.get_media_type() == Spit.Publishing.Publisher.MediaType.VIDEO) ? - Endpoint.VIDEO : Endpoint.DEFAULT); - - // Video uploads require a privacy string at the per-resource level. Since they aren't - // placed in albums, they can't inherit their privacy settings from their containing - // album like photos do - assert(publishable.get_media_type() != Spit.Publishing.Publisher.MediaType.VIDEO || - resource_privacy != null); - - this.publishable = publishable; - - // attempt to map the binary payload from disk into memory - try { - this.mapped_file = new MappedFile(publishable.get_serialized_file().get_path(), - false); - } catch (FileError e) { - return; - } - - this.soup_message = new Soup.Message.from_uri(method.to_string(), new Soup.URI(uri)); - soup_message.wrote_body_data.connect(on_wrote_body_data); - - unowned uint8[] payload = (uint8[]) mapped_file.get_contents(); - payload.length = (int) mapped_file.get_length(); - - Soup.Buffer image_data = new Soup.Buffer(Soup.MemoryUse.TEMPORARY, payload); - - Soup.Multipart mp_envelope = new Soup.Multipart("multipart/form-data"); - - mp_envelope.append_form_string("access_token", access_token); - - if (publishable.get_media_type() == Spit.Publishing.Publisher.MediaType.VIDEO) - mp_envelope.append_form_string("privacy", resource_privacy); - - //Get photo title and post it as message on FB API - string publishable_title = publishable.get_param_string("title"); - if (!suppress_titling && publishable_title != null) - mp_envelope.append_form_string("name", publishable_title); - - //Set 'message' data field with EXIF comment field. Title has precedence. - string publishable_comment = publishable.get_param_string("comment"); - if (!suppress_titling && publishable_comment != null) - mp_envelope.append_form_string("message", publishable_comment); - - //Sets correct date of the picture - if (!suppress_titling) - mp_envelope.append_form_string("backdated_time", publishable.get_exposure_date_time().to_string()); - - string source_file_mime_type = - (publishable.get_media_type() == Spit.Publishing.Publisher.MediaType.VIDEO) ? - "video" : "image/jpeg"; - mp_envelope.append_form_file("source", publishable.get_serialized_file().get_basename(), - source_file_mime_type, image_data); - - mp_envelope.to_message(soup_message.request_headers, soup_message.request_body); - } - - public override bool prepare_for_transmission() { - if (mapped_file == null) { - failed(new Spit.Publishing.PublishingError.LOCAL_FILE_ERROR( - "File %s is unavailable.".printf(publishable.get_serialized_file().get_path()))); - return false; - } else { - return true; - } - } - } - - private class GraphCreateAlbumMessage : GraphMessageImpl { - public GraphCreateAlbumMessage(GraphSession host_session, string access_token, - string album_name, string album_privacy) { - base(host_session, Publishing.RESTSupport.HttpMethod.POST, "/me/albums", access_token); - - assert(album_privacy != null && album_privacy != ""); - - this.soup_message = new Soup.Message.from_uri(method.to_string(), new Soup.URI(uri)); - - Soup.Multipart mp_envelope = new Soup.Multipart("multipart/form-data"); - - mp_envelope.append_form_string("access_token", access_token); - mp_envelope.append_form_string("name", album_name); - mp_envelope.append_form_string("privacy", album_privacy); - - mp_envelope.to_message(soup_message.request_headers, soup_message.request_body); - } - } - - public signal void authenticated(); - - private Soup.Session soup_session; - private string? access_token; - private GraphMessage? current_message; - - public GraphSession() { - this.soup_session = new Soup.Session (); - this.soup_session.request_unqueued.connect (on_request_unqueued); - this.soup_session.timeout = 15; - this.access_token = null; - this.current_message = null; - this.soup_session.ssl_use_system_ca_file = true; - } - - ~GraphSession() { - soup_session.request_unqueued.disconnect (on_request_unqueued); - } - - private void manage_message(GraphMessage msg) { - assert(current_message == null); - - current_message = msg; - } - - private void unmanage_message(GraphMessage msg) { - assert(current_message != null); - - current_message = null; - } - - private void on_request_unqueued(Soup.Message msg) { - assert(current_message != null); - GraphMessageImpl real_message = (GraphMessageImpl) current_message; - assert(real_message.soup_message == msg); - - // these error types are always recoverable given the unique behavior of the Facebook - // endpoint, so try again - if (msg.status_code == Soup.KnownStatusCode.IO_ERROR || - msg.status_code == Soup.KnownStatusCode.MALFORMED || - msg.status_code == Soup.KnownStatusCode.TRY_AGAIN) { - real_message.bytes_so_far = 0; - soup_session.queue_message(msg, null); - return; - } - - unmanage_message(real_message); - msg.wrote_body_data.disconnect(real_message.on_wrote_body_data); - - Spit.Publishing.PublishingError? error = null; - switch (msg.status_code) { - case Soup.KnownStatusCode.OK: - case Soup.KnownStatusCode.CREATED: // HTTP code 201 (CREATED) signals that a new - // resource was created in response to a PUT - // or POST - break; - - case EXPIRED_SESSION_STATUS_CODE: - error = new Spit.Publishing.PublishingError.EXPIRED_SESSION( - "OAuth Access Token has Expired. Logout user."); - break; - - case Soup.KnownStatusCode.CANT_RESOLVE: - case Soup.KnownStatusCode.CANT_RESOLVE_PROXY: - error = new Spit.Publishing.PublishingError.NO_ANSWER( - "Unable to resolve %s (error code %u)", real_message.get_uri(), msg.status_code); - break; - - case Soup.KnownStatusCode.CANT_CONNECT: - case Soup.KnownStatusCode.CANT_CONNECT_PROXY: - error = new Spit.Publishing.PublishingError.NO_ANSWER( - "Unable to connect to %s (error code %u)", real_message.get_uri(), msg.status_code); - break; - - default: - // status codes below 100 are used by Soup, 100 and above are defined HTTP - // codes - if (msg.status_code >= 100) { - error = new Spit.Publishing.PublishingError.NO_ANSWER( - "Service %s returned HTTP status code %u %s", real_message.get_uri(), - msg.status_code, msg.reason_phrase); - } else { - debug(msg.reason_phrase); - error = new Spit.Publishing.PublishingError.NO_ANSWER( - "Failure communicating with %s (error code %u)", real_message.get_uri(), - msg.status_code); - } - break; - } - - // All valid communication with Facebook involves body data in the response - if (error == null) - if (msg.response_body.data == null || msg.response_body.data.length == 0) - error = new Spit.Publishing.PublishingError.MALFORMED_RESPONSE( - "No response data from %s", real_message.get_uri()); - - if (error == null) - real_message.completed(); - else - real_message.failed(error); - } - - public void authenticate(string access_token) { - this.access_token = access_token; - authenticated(); - } - - public bool is_authenticated() { - return access_token != null; - } - -#if 0 - public GraphMessage new_endpoint_test() { - return new GraphEndpointProbeMessage(this); - } -#endif - - public GraphMessage new_query(string resource_path) { - return new GraphQueryMessage(this, resource_path, access_token); - } - - public GraphMessage new_upload(string resource_path, Spit.Publishing.Publishable publishable, - bool suppress_titling, string? resource_privacy = null) { - return new GraphUploadMessage(this, access_token, resource_path, publishable, - suppress_titling, resource_privacy); - } - - public GraphMessage new_create_album(string album_name, string privacy) { - return new GraphSession.GraphCreateAlbumMessage(this, access_token, album_name, privacy); - } - - public void send_message(GraphMessage message) { - GraphMessageImpl real_message = (GraphMessageImpl) message; - - debug("making HTTP request to URI: " + real_message.soup_message.uri.to_string(false)); - - if (real_message.prepare_for_transmission()) { - manage_message(message); - soup_session.queue_message(real_message.soup_message, null); - } - } - - public void stop_transactions() { - soup_session.abort(); - } -} - -internal class Uploader { - private int current_file; - private Spit.Publishing.Publishable[] publishables; - private GraphSession session; - private PublishingParameters publishing_params; - private unowned Spit.Publishing.ProgressCallback? status_updated = null; - - public signal void upload_complete(int num_photos_published); - public signal void upload_error(Spit.Publishing.PublishingError err); - - public Uploader(GraphSession session, PublishingParameters publishing_params, - Spit.Publishing.Publishable[] publishables) { - this.current_file = 0; - this.publishables = publishables; - this.session = session; - this.publishing_params = publishing_params; - } - - private void send_current_file() { - Spit.Publishing.Publishable publishable = publishables[current_file]; - GLib.File? file = publishable.get_serialized_file(); - - // if the current publishable hasn't been serialized, then skip it - if (file == null) { - current_file++; - return; - } - - string resource_uri = - (publishable.get_media_type() == Spit.Publishing.Publisher.MediaType.PHOTO) ? - "/%s/photos".printf(publishing_params.get_target_album_id()) : "/me/videos"; - string? resource_privacy = - (publishable.get_media_type() == Spit.Publishing.Publisher.MediaType.VIDEO) ? - publishing_params.privacy_object : null; - GraphMessage upload_message = session.new_upload(resource_uri, publishable, - publishing_params.strip_metadata, resource_privacy); - - upload_message.data_transmitted.connect(on_chunk_transmitted); - upload_message.completed.connect(on_message_completed); - upload_message.failed.connect(on_message_failed); - - session.send_message(upload_message); - } - - private void send_files() { - current_file = 0; - send_current_file(); - } - - private void on_chunk_transmitted(int bytes_written_so_far, int total_bytes) { - double file_span = 1.0 / publishables.length; - double this_file_fraction_complete = ((double) bytes_written_so_far) / total_bytes; - double fraction_complete = (current_file * file_span) + (this_file_fraction_complete * - file_span); - - if (status_updated != null) - status_updated(current_file + 1, fraction_complete); - } - - private void on_message_completed(GraphMessage message) { - message.data_transmitted.disconnect(on_chunk_transmitted); - message.completed.disconnect(on_message_completed); - message.failed.disconnect(on_message_failed); - - current_file++; - if (current_file < publishables.length) { - send_current_file(); - } else { - upload_complete(current_file); - } - } - - private void on_message_failed(GraphMessage message, Spit.Publishing.PublishingError error) { - message.data_transmitted.disconnect(on_chunk_transmitted); - message.completed.disconnect(on_message_completed); - message.failed.disconnect(on_message_failed); - - upload_error(error); - } - - public void upload(Spit.Publishing.ProgressCallback? status_updated = null) { - this.status_updated = status_updated; - - if (publishables.length > 0) - send_files(); - } -} - -} - diff --git a/plugins/shotwell-publishing/FlickrPublishing.vala b/plugins/shotwell-publishing/FlickrPublishing.vala index 5a80284..8c7e9a1 100644 --- a/plugins/shotwell-publishing/FlickrPublishing.vala +++ b/plugins/shotwell-publishing/FlickrPublishing.vala @@ -5,15 +5,8 @@ */ public class FlickrService : Object, Spit.Pluggable, Spit.Publishing.Service { - private const string ICON_FILENAME = "flickr.png"; - private static Gdk.Pixbuf[] icon_pixbuf_set = null; - - public FlickrService(GLib.File resource_directory) { - if (icon_pixbuf_set == null) - icon_pixbuf_set = Resources.load_from_resource - (Resources.RESOURCE_PATH + "/" + ICON_FILENAME); - } + public FlickrService() {} public int get_pluggable_interface(int min_host_interface, int max_host_interface) { return Spit.negotiate_interfaces(min_host_interface, max_host_interface, @@ -21,23 +14,19 @@ public class FlickrService : Object, Spit.Pluggable, Spit.Publishing.Service { } public unowned string get_id() { - return "org.yorba.shotwell.publishing.flickr"; + return "org.gnome.shotwell.publishing.flickr"; } public unowned string get_pluggable_name() { return "Flickr"; } - public void get_info(ref Spit.PluggableInfo info) { + public Spit.PluggableInfo get_info() { + var info = new Spit.PluggableInfo(); info.authors = "Lucas Beeler"; info.copyright = _("Copyright 2016 Software Freedom Conservancy Inc."); - 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; + + return info; } public void activation(bool enabled) { @@ -80,10 +69,12 @@ internal class VisibilitySpecification { // not a struct because we want reference semantics internal class PublishingParameters { public UserKind user_kind; - public int64 quota_free_bytes; + public int64 max_images_count; + public uint64 uploaded_images_count; public int photo_major_axis_size; public string username; public VisibilitySpecification visibility_specification; + public bool strip_metadata; public PublishingParameters() { } @@ -154,30 +145,7 @@ public class FlickrPublisher : Spit.Publishing.Publisher, GLib.Object { parameters.username = session.get_username(); - do_fetch_account_info(); - } - - private void on_account_fetch_txn_completed(Publishing.RESTSupport.Transaction txn) { - txn.completed.disconnect(on_account_fetch_txn_completed); - txn.network_error.disconnect(on_account_fetch_txn_error); - - if (!is_running()) - return; - - debug("EVENT: account fetch transaction response received over the network"); - do_parse_account_info_from_xml(txn.get_response()); - } - - private void on_account_fetch_txn_error(Publishing.RESTSupport.Transaction txn, - Spit.Publishing.PublishingError err) { - txn.completed.disconnect(on_account_fetch_txn_completed); - txn.network_error.disconnect(on_account_fetch_txn_error); - - if (!is_running()) - return; - - debug("EVENT: account fetch transaction caused a network error"); - host.post_error(err); + do_fetch_account_info.begin(); } private void on_account_info_available() { @@ -196,7 +164,7 @@ public class FlickrPublisher : Spit.Publishing.Publisher, GLib.Object { return; debug("EVENT: user clicked the 'Publish' button in the publishing options pane"); - do_publish(strip_metadata); + do_publish.begin(strip_metadata); } private void on_publishing_options_pane_logout() { @@ -222,45 +190,19 @@ public class FlickrPublisher : Spit.Publishing.Publisher, GLib.Object { progress_reporter(file_number, completed_fraction); } - private void on_upload_complete(Publishing.RESTSupport.BatchUploader uploader, - int num_published) { - if (!is_running()) - return; - - debug("EVENT: uploader reports upload complete; %d items published.", num_published); - - uploader.upload_complete.disconnect(on_upload_complete); - uploader.upload_error.disconnect(on_upload_error); - - do_show_success_pane(); - } - - private void on_upload_error(Publishing.RESTSupport.BatchUploader uploader, - Spit.Publishing.PublishingError err) { - if (!is_running()) - return; - - debug("EVENT: uploader reports upload error = '%s'.", err.message); - - uploader.upload_complete.disconnect(on_upload_complete); - uploader.upload_error.disconnect(on_upload_error); - - host.post_error(err); - } - - private void do_fetch_account_info() { + private async void do_fetch_account_info() { debug("ACTION: running network transaction to fetch account information"); host.set_service_locked(true); host.install_account_fetch_wait_pane(); AccountInfoFetchTransaction txn = new AccountInfoFetchTransaction(session); - txn.completed.connect(on_account_fetch_txn_completed); - txn.network_error.connect(on_account_fetch_txn_error); - try { - txn.execute(); - } catch (Spit.Publishing.PublishingError err) { + yield txn.execute_async(); + debug("EVENT: account fetch transaction response received over the network"); + do_parse_account_info_from_xml(txn.get_response()); + } catch (Error err) { + debug("EVENT: account fetch transaction caused a network error"); host.post_error(err); } } @@ -275,9 +217,8 @@ public class FlickrPublisher : Spit.Publishing.Publisher, GLib.Object { string is_pro_str = response_doc.get_property_value(user_node, "ispro"); - Xml.Node* bandwidth_node = response_doc.get_named_child(user_node, "bandwidth"); - - string remaining_kb_str = response_doc.get_property_value(bandwidth_node, "remainingkb"); + string max_images_str = response_doc.get_property_value(user_node, "upload_limit"); + string uploaded_images_str = response_doc.get_property_value(user_node, "upload_count"); UserKind user_kind; if (is_pro_str == "0") @@ -287,10 +228,9 @@ public class FlickrPublisher : Spit.Publishing.Publisher, GLib.Object { else throw new Spit.Publishing.PublishingError.MALFORMED_RESPONSE( "Unable to determine if user has free or pro account"); - - var quota_bytes_left = int64.parse(remaining_kb_str) * 1024; - parameters.quota_free_bytes = quota_bytes_left; + parameters.max_images_count = int64.parse(max_images_str); + parameters.uploaded_images_count = int64.parse(uploaded_images_str); parameters.user_kind = user_kind; } catch (Spit.Publishing.PublishingError err) { @@ -353,7 +293,7 @@ public class FlickrPublisher : Spit.Publishing.Publisher, GLib.Object { return a.get_exposure_date_time().compare(b.get_exposure_date_time()); } - private void do_publish(bool strip_metadata) { + private async void do_publish(bool strip_metadata) { set_persistent_strip_metadata(strip_metadata); debug("ACTION: uploading media items to remote server."); @@ -377,9 +317,14 @@ public class FlickrPublisher : Spit.Publishing.Publisher, GLib.Object { sorted_list.sort(flickr_date_time_compare_func); Uploader uploader = new Uploader(session, sorted_list.to_array(), parameters, strip_metadata); - uploader.upload_complete.connect(on_upload_complete); - uploader.upload_error.connect(on_upload_error); - uploader.upload(on_upload_status_updated); + try { + var num_published = yield uploader.upload_async(on_upload_status_updated); + debug("EVENT: uploader reports upload complete; %d items published.", num_published); + do_show_success_pane(); + } catch (Error err) { + debug("EVENT: uploader reports upload error = '%s'.", err.message); + host.post_error(err); + } } private void do_show_success_pane() { @@ -506,7 +451,7 @@ private class UploadTransaction : Publishing.RESTSupport.OAuth1.UploadTransactio public UploadTransaction(Publishing.RESTSupport.OAuth1.Session session, PublishingParameters parameters, Spit.Publishing.Publishable publishable) { - base(session, publishable, "https://api.flickr.com/services/upload"); + base(session, publishable, "https://up.flickr.com/services/upload"); this.parameters = parameters; @@ -514,6 +459,18 @@ private class UploadTransaction : Publishing.RESTSupport.OAuth1.UploadTransactio add_argument("is_friend", ("%d".printf(parameters.visibility_specification.friends_level))); add_argument("is_family", ("%d".printf(parameters.visibility_specification.family_level))); + if (!parameters.strip_metadata) { + var title = publishable.get_param_string(Spit.Publishing.Publishable.PARAM_STRING_TITLE); + if (title != null && title != "") { + add_argument("title", title); + } + + var comment = publishable.get_param_string(Spit.Publishing.Publishable.PARAM_STRING_COMMENT); + if (comment != null && comment != "") { + add_argument("description", comment); + } + } + GLib.HashTable<string, string> disposition_table = new GLib.HashTable<string, string>(GLib.str_hash, GLib.str_equal); string? filename = publishable.get_publishing_name(); @@ -522,7 +479,7 @@ private class UploadTransaction : Publishing.RESTSupport.OAuth1.UploadTransactio /// TODO: This may need to be revisited to send the title separately; please see /// http://www.flickr.com/services/api/upload.api.html for more details. - disposition_table.insert("filename", Soup.URI.encode( + disposition_table.insert("filename", GLib.Uri.escape_string( publishable.get_param_string(Spit.Publishing.Publishable.PARAM_STRING_BASENAME), null)); disposition_table.insert("name", "photo"); @@ -530,9 +487,9 @@ private class UploadTransaction : Publishing.RESTSupport.OAuth1.UploadTransactio set_binary_disposition_table(disposition_table); } - public override void execute() throws Spit.Publishing.PublishingError { + public override async void execute_async() throws Spit.Publishing.PublishingError { this.authorize(); - base.execute(); + yield base.execute_async(); } } @@ -606,19 +563,19 @@ internal class PublishingOptionsPane : Spit.Publishing.DialogPane, GLib.Object { string upload_label_text = _("You are logged into Flickr as %s.\n\n").printf(parameters.username); if (parameters.user_kind == UserKind.FREE) { - upload_label_text += _("Your free Flickr account limits how much data you can upload per month.\nThis month you have %s remaining in your upload quota.").printf(GLib.format_size(parameters.quota_free_bytes, FormatSizeFlags.LONG_FORMAT | FormatSizeFlags.IEC_UNITS)); + upload_label_text += _("Your free Flickr account limits how many photos you can upload to the service.\nYou have uploaded %llu out of your %lld file limit.").printf(parameters.uploaded_images_count, parameters.max_images_count); } else { - upload_label_text += _("Your Flickr Pro account entitles you to unlimited uploads."); + upload_label_text += ngettext("Your Flickr Pro account entitles you to unlimited uploads. You have currently uploaded a file", "Your Flickr Pro account entitles you to unlimited uploads. You have currently uploaded %d files", (int) parameters.uploaded_images_count).printf((int) parameters.uploaded_images_count); } upload_info_label.set_label(upload_label_text); - string visibility_label_text = _("Photos _visible to:"); + string visibility_label_text = _("Photos _visible to"); if ((media_type == Spit.Publishing.Publisher.MediaType.VIDEO)) { - visibility_label_text = _("Videos _visible to:"); + visibility_label_text = _("Videos _visible to"); } else if ((media_type == (Spit.Publishing.Publisher.MediaType.PHOTO | Spit.Publishing.Publisher.MediaType.VIDEO))) { - visibility_label_text = _("Photos and videos _visible to:"); + visibility_label_text = _("Photos and videos _visible to"); } visibility_label.set_label(visibility_label_text); @@ -646,6 +603,7 @@ internal class PublishingOptionsPane : Spit.Publishing.DialogPane, GLib.Object { } private void on_publish_clicked() { + parameters.strip_metadata = strip_metadata_check.get_active(); parameters.visibility_specification = visibilities[visibility_combo.get_active()].specification; @@ -765,52 +723,62 @@ internal class Uploader : Publishing.RESTSupport.BatchUploader { if (!publishable_metadata.has_iptc()) return; - if (publishable_metadata.has_tag("Iptc.Application2.Caption")) - publishable_metadata.set_tag_string("Iptc.Application2.Caption", - Publishing.RESTSupport.asciify_string(publishable_metadata.get_tag_string( - "Iptc.Application2.Caption"))); - - if (publishable_metadata.has_tag("Iptc.Application2.Headline")) - publishable_metadata.set_tag_string("Iptc.Application2.Headline", - Publishing.RESTSupport.asciify_string(publishable_metadata.get_tag_string( - "Iptc.Application2.Headline"))); - - if (publishable_metadata.has_tag("Iptc.Application2.Keywords")) { - Gee.Set<string> keyword_set = new Gee.HashSet<string>(); - string[] iptc_keywords = publishable_metadata.get_tag_multiple("Iptc.Application2.Keywords"); - if (iptc_keywords != null) - foreach (string keyword in iptc_keywords) - keyword_set.add(keyword); - - string[] xmp_keywords = publishable_metadata.get_tag_multiple("Xmp.dc.subject"); - if (xmp_keywords != null) - foreach (string keyword in xmp_keywords) - keyword_set.add(keyword); - - string[] all_keywords = keyword_set.to_array(); - // append a null pointer to the end of all_keywords -- this is a necessary workaround - // for http://trac.yorba.org/ticket/3264. See also http://trac.yorba.org/ticket/3257, - // which describes the user-visible behavior seen in the Flickr Connector as a result - // of the former bug. - all_keywords += null; - - string[] no_keywords = new string[1]; - // append a null pointer to the end of no_keywords -- this is a necessary workaround - // for http://trac.yorba.org/ticket/3264. See also http://trac.yorba.org/ticket/3257, - // which describes the user-visible behavior seen in the Flickr Connector as a result - // of the former bug. - no_keywords[0] = null; - - publishable_metadata.set_tag_multiple("Xmp.dc.subject", all_keywords); - publishable_metadata.set_tag_multiple("Iptc.Application2.Keywords", no_keywords); - - try { - publishable_metadata.save_file(publishable.get_serialized_file().get_path()); - } catch (GLib.Error err) { - warning("couldn't write metadata to file '%s' for upload preprocessing.", - publishable.get_serialized_file().get_path()); + try { + if (publishable_metadata.try_has_tag("Iptc.Application2.Caption")) + publishable_metadata.try_set_tag_string("Iptc.Application2.Caption", + Publishing.RESTSupport.asciify_string(publishable_metadata.try_get_tag_string( + "Iptc.Application2.Caption"))); + } catch (Error err) {} + + try { + if (publishable_metadata.try_has_tag("Iptc.Application2.Headline")) + publishable_metadata.try_set_tag_string("Iptc.Application2.Headline", + Publishing.RESTSupport.asciify_string(publishable_metadata.try_get_tag_string( + "Iptc.Application2.Headline"))); + } catch (Error error) {} + + try { + if (publishable_metadata.try_has_tag("Iptc.Application2.Keywords")) { + Gee.Set<string> keyword_set = new Gee.HashSet<string>(); + string[] iptc_keywords = publishable_metadata.try_get_tag_multiple("Iptc.Application2.Keywords"); + if (iptc_keywords != null) + foreach (string keyword in iptc_keywords) + keyword_set.add(keyword); + + string[] xmp_keywords = publishable_metadata.try_get_tag_multiple("Xmp.dc.subject"); + if (xmp_keywords != null) + foreach (string keyword in xmp_keywords) + keyword_set.add(keyword); + + string[] all_keywords = keyword_set.to_array(); + // append a null pointer to the end of all_keywords -- this is a necessary workaround + // https://bugzilla.gnome.org/show_bug.cgi?id=712479. See also + // https://bugzilla.gnome.org/show_bug.cgi?id=717438 which describes the user-visible + // behavior seen in the Flickr Connector as a result of the former bug. + all_keywords += null; + + string[] no_keywords = new string[1]; + // append a null pointer to the end of no_keywords -- this is a necessary workaround + // for similar reasons as above. + no_keywords[0] = null; + + try { + publishable_metadata.try_set_tag_multiple("Xmp.dc.subject", all_keywords); + } catch (Error error) { + } + try { + publishable_metadata.try_set_tag_multiple("Iptc.Application2.Keywords", no_keywords); + } catch (Error error) { + } + + try { + publishable_metadata.save_file(publishable.get_serialized_file().get_path()); + } catch (GLib.Error err) { + warning("couldn't write metadata to file '%s' for upload preprocessing.", + publishable.get_serialized_file().get_path()); + } } - } + } catch (Error error) {} } protected override Publishing.RESTSupport.Transaction create_transaction( diff --git a/plugins/shotwell-publishing/PhotosPublisher.vala b/plugins/shotwell-publishing/PhotosPublisher.vala index 879f5fc..b592317 100644 --- a/plugins/shotwell-publishing/PhotosPublisher.vala +++ b/plugins/shotwell-publishing/PhotosPublisher.vala @@ -127,7 +127,7 @@ private class MediaCreationTransaction : Publishing.RESTSupport.GooglePublisher. this.titles = titles; } - public override void execute () throws Spit.Publishing.PublishingError { + public override async void execute_async () throws Spit.Publishing.PublishingError { var builder = new Json.Builder(); builder.begin_object(); builder.set_member_name("albumId"); @@ -149,7 +149,7 @@ private class MediaCreationTransaction : Publishing.RESTSupport.GooglePublisher. builder.end_object(); set_custom_payload(Json.to_string (builder.get_root (), false), "application/json"); - base.execute(); + yield base.execute_async(); } } @@ -163,7 +163,7 @@ private class AlbumCreationTransaction : Publishing.RESTSupport.GooglePublisher. this.title = title; } - public override void execute () throws Spit.Publishing.PublishingError { + public override async void execute_async() throws Spit.Publishing.PublishingError { var builder = new Json.Builder(); builder.begin_object(); builder.set_member_name("album"); @@ -174,58 +174,18 @@ private class AlbumCreationTransaction : Publishing.RESTSupport.GooglePublisher. builder.end_object(); set_custom_payload(Json.to_string (builder.get_root (), false), "application/json"); - base.execute(); + yield base.execute_async(); } } private class AlbumDirectoryTransaction : Publishing.RESTSupport.GooglePublisher.AuthenticatedTransaction { private const string ENDPOINT_URL = "https://photoslibrary.googleapis.com/v1/albums"; - private Album[] albums = new Album[0]; - public AlbumDirectoryTransaction(Publishing.RESTSupport.GoogleSession session) { + public AlbumDirectoryTransaction(Publishing.RESTSupport.GoogleSession session, string? token) { base(session, ENDPOINT_URL, Publishing.RESTSupport.HttpMethod.GET); - this.completed.connect(on_internal_continue_pagination); - } - - public Album[] get_albums() { - return this.albums; - } - - private void on_internal_continue_pagination() { - try { - debug(this.get_response()); - var json = Json.from_string (this.get_response()); - var object = json.get_object (); - if (!object.has_member ("albums")) { - return; - } - var pagination_token_node = object.get_member ("nextPageToken"); - var response_albums = object.get_member ("albums").get_array(); - response_albums.foreach_element( (a, b, element) => { - var album = element.get_object(); - var title = album.get_member("title"); - var is_writable = album.get_member("isWriteable"); - if (title != null && is_writable != null && is_writable.get_boolean()) - albums += new Album(title.get_string(), album.get_string_member("id")); - }); - - if (pagination_token_node != null) { - this.set_argument ("pageToken", pagination_token_node.get_string ()); - Signal.stop_emission_by_name (this, "completed"); - Idle.add(() => { - try { - this.execute(); - } catch (Spit.Publishing.PublishingError error) { - this.network_error(error); - } - - return false; - }); - } - } catch (Error error) { - critical ("Got error %s while trying to parse response, delegating", error.message); - this.network_error(new Spit.Publishing.PublishingError.MALFORMED_RESPONSE(error.message)); + if (token != null) { + add_argument("pageToken", token); } } } @@ -264,61 +224,70 @@ public class Publisher : Publishing.RESTSupport.GooglePublisher { } protected override void on_login_flow_complete() { + do_publishing_process.begin(); + } + + private async void do_publishing_process() { debug("EVENT: OAuth login flow complete."); this.publishing_parameters.set_user_name (this.authenticator.get_authentication_parameter()["UserName"].get_string()); get_host().install_account_fetch_wait_pane(); get_host().set_service_locked(true); + var albums = new Album[0]; - AlbumDirectoryTransaction txn = new AlbumDirectoryTransaction(get_session()); - txn.completed.connect(on_initial_album_fetch_complete); - txn.network_error.connect(on_initial_album_fetch_error); - + AlbumDirectoryTransaction txn = new AlbumDirectoryTransaction(get_session(), null); try { - txn.execute(); - } catch (Spit.Publishing.PublishingError error) { - on_initial_album_fetch_error(txn, error); - } - } - - private void on_initial_album_fetch_complete(Publishing.RESTSupport.Transaction txn) { - txn.completed.disconnect(on_initial_album_fetch_complete); - txn.network_error.disconnect(on_initial_album_fetch_error); - - if (!is_running()) - return; - - debug("EVENT: finished fetching album information."); - - display_account_information((AlbumDirectoryTransaction)txn); - } - - private void on_initial_album_fetch_error(Publishing.RESTSupport.Transaction txn, - Spit.Publishing.PublishingError error) { - txn.completed.disconnect(on_initial_album_fetch_complete); - txn.network_error.disconnect(on_initial_album_fetch_error); - - if (!is_running()) - return; - - debug("EVENT: fetching album information failed; response = '%s'.", - txn.get_response()); - - if (txn.get_status_code() == 403 || txn.get_status_code() == 404) { - do_logout(); - } else { - // If we get any other kind of error, we can't recover, so just post it to the user - get_host().post_error(error); + string? pagination_token = null; + do { + yield txn.execute_async(); + + if (!is_running()) + return; + + var json = Json.from_string (txn.get_response()); + var object = json.get_object (); + if (!object.has_member ("albums")) { + throw new Spit.Publishing.PublishingError.MALFORMED_RESPONSE("Album fetch did not contain expected data"); + } + + if (object.has_member("nextPageToken")) { + pagination_token = object.get_member ("nextPageToken").get_string(); + } else { + pagination_token = null; + } + + var response_albums = object.get_member ("albums").get_array(); + response_albums.foreach_element( (a, b, element) => { + var album = element.get_object(); + var title = album.get_member("title"); + var is_writable = album.get_member("isWriteable"); + if (title != null && is_writable != null && is_writable.get_boolean()) + albums += new Album(title.get_string(), album.get_string_member("id")); + }); + + if (pagination_token != null) { + debug("Not finished fetching all albums, getting more... %s", pagination_token); + txn = new AlbumDirectoryTransaction(get_session(), pagination_token); + } + } while (pagination_token != null); + + debug("EVENT: finished fetching album information."); + this.publishing_parameters.set_albums(albums); + + show_publishing_options_pane(); + } catch (Error err) { + debug("EVENT: fetching album information failed; response = '%s'.", + txn.get_response()); + + if (txn.get_status_code() == 403 || txn.get_status_code() == 404) { + do_logout(); + } else { + // If we get any other kind of error, we can't recover, so just post it to the user + get_host().post_error(err); + } } } - private void display_account_information(AlbumDirectoryTransaction txn) { - debug("ACTION: parsing album information"); - this.publishing_parameters.set_albums(txn.get_albums()); - - show_publishing_options_pane(); - } - private void show_publishing_options_pane() { debug("ACTION: showing publishing options pane."); @@ -348,65 +317,43 @@ public class Publisher : Publishing.RESTSupport.GooglePublisher { save_parameters_to_configuration_system(publishing_parameters); if (publishing_parameters.get_target_album_entry_id () != null) { - do_upload(); + do_upload.begin(); } else { - do_create_album(); + do_create_album.begin(); } } - private void do_create_album() { + private async void do_create_album() { debug("ACTION: Creating album"); assert(publishing_parameters.get_target_album_entry_id () == null); get_host().set_service_locked(true); var txn = new AlbumCreationTransaction(get_session(), publishing_parameters.get_target_album_name()); - txn.completed.connect(on_album_create_complete); - txn.network_error.connect(on_album_create_error); try { - txn.execute(); - } catch (Spit.Publishing.PublishingError error) { - on_album_create_error(txn, error); - } - } - - 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); + yield txn.execute_async(); - if (!is_running()) - return; + if (!is_running()) + return; - debug("EVENT: finished creating album information: %s", txn.get_response()); + debug("EVENT: finished creating album information: %s", txn.get_response()); - try { var node = Json.from_string(txn.get_response()); var object = node.get_object(); publishing_parameters.set_target_album_entry_id (object.get_string_member ("id")); - do_upload(); - } catch (Error error) { - on_album_create_error(txn, new Spit.Publishing.PublishingError.MALFORMED_RESPONSE (error.message)); - } - } - - private void on_album_create_error(Publishing.RESTSupport.Transaction txn, - Spit.Publishing.PublishingError error) { - txn.completed.disconnect(on_initial_album_fetch_complete); - txn.network_error.disconnect(on_initial_album_fetch_error); + yield do_upload(); + } catch (Error err) { + debug("EVENT: creating album failed; response = '%s'.", + txn.get_response()); - if (!is_running()) - return; - - debug("EVENT: creating album failed; response = '%s'.", - txn.get_response()); - - if (txn.get_status_code() == 403 || txn.get_status_code() == 404) { - do_logout(); - } else { - // If we get any other kind of error, we can't recover, so just post it to the user - get_host().post_error(error); + if (txn.get_status_code() == 403 || txn.get_status_code() == 404) { + do_logout(); + } else { + // If we get any other kind of error, we can't recover, so just post it to the user + get_host().post_error(err); + } } } @@ -420,7 +367,7 @@ public class Publisher : Publishing.RESTSupport.GooglePublisher { } } - private void do_upload() { + private async void do_upload() { debug("ACTION: uploading media items to remote server."); get_host().set_service_locked(true); @@ -439,10 +386,17 @@ public class Publisher : Publishing.RESTSupport.GooglePublisher { Spit.Publishing.Publishable[] publishables = get_host().get_publishables(); Uploader uploader = new Uploader(get_session(), publishables, publishing_parameters); - uploader.upload_complete.connect(on_upload_complete); - uploader.upload_error.connect(on_upload_error); + try { + yield uploader.upload_async(on_upload_status_updated); + yield do_media_creation_batch(uploader); + } catch (Error err) { + if (!is_running()) + return; + + debug("EVENT: uploader reports upload error = '%s'.", err.message); - uploader.upload(on_upload_status_updated); + get_host().post_error(err); + } } private void on_upload_status_updated(int file_number, double completed_fraction) { @@ -456,85 +410,39 @@ public class Publisher : Publishing.RESTSupport.GooglePublisher { progress_reporter(file_number, completed_fraction); } - private void on_upload_complete(Publishing.RESTSupport.BatchUploader uploader, - int num_published) { - if (!is_running()) - return; - - debug("EVENT: uploader reports upload complete; %d items published.", num_published); - - uploader.upload_complete.disconnect(on_upload_complete); - uploader.upload_error.disconnect(on_upload_error); - - do_media_creation_batch(uploader); - } - - private void do_media_creation_batch(Publishing.RESTSupport.BatchUploader uploader) { + private async void do_media_creation_batch(Publishing.RESTSupport.BatchUploader uploader) { var u = (Uploader) uploader; - if (creation_offset >= u.upload_tokens.length) { - on_media_creation_complete(); - return; - } + while (creation_offset < u.upload_tokens.length) { + var end = creation_offset + MAX_BATCH_SIZE < u.upload_tokens.length ? + creation_offset + MAX_BATCH_SIZE : u.upload_tokens.length; + + var txn = new MediaCreationTransaction(get_session(), + u.upload_tokens[creation_offset:end], + u.titles[creation_offset:end], + publishing_parameters.get_target_album_entry_id()); - var end = creation_offset + MAX_BATCH_SIZE < u.upload_tokens.length ? - creation_offset + MAX_BATCH_SIZE : u.upload_tokens.length; - - var txn = new MediaCreationTransaction(get_session(), - u.upload_tokens[creation_offset:end], - u.titles[creation_offset:end], - publishing_parameters.get_target_album_entry_id()); - - txn.completed.connect(() => { - do_media_creation_batch(uploader); - }); - - txn.network_error.connect(on_media_creation_error); - - try { creation_offset = end; - txn.execute(); - } catch (Spit.Publishing.PublishingError error) { - on_media_creation_error(txn, error); + try { + yield txn.execute_async(); + if (!is_running()) + return; + + debug("EVENT: Media creation reports success."); + + get_host().set_service_locked(false); + get_host().install_success_pane(); + } catch (Spit.Publishing.PublishingError err) { + if (!is_running()) + return; + + debug("EVENT: Media creation reports error: %s", err.message); + + get_host().post_error(err); + } } } - private void on_upload_error(Publishing.RESTSupport.BatchUploader uploader, - Spit.Publishing.PublishingError err) { - if (!is_running()) - return; - - debug("EVENT: uploader reports upload error = '%s'.", err.message); - - uploader.upload_complete.disconnect(on_upload_complete); - uploader.upload_error.disconnect(on_upload_error); - - get_host().post_error(err); - } - - private void on_media_creation_complete() { - if (!is_running()) - return; - - debug("EVENT: Media creation reports success."); - - get_host().set_service_locked(false); - get_host().install_success_pane(); - } - - private void on_media_creation_error(Publishing.RESTSupport.Transaction txn, - Spit.Publishing.PublishingError err) { - txn.completed.disconnect(on_media_creation_complete); - txn.network_error.disconnect(on_media_creation_error); - - if (!is_running()) - return; - - debug("EVENT: Media creation reports error: %s", err.message); - - get_host().post_error(err); - } - public override bool is_running() { return running; } diff --git a/plugins/shotwell-publishing/PhotosPublishingPane.vala b/plugins/shotwell-publishing/PhotosPublishingPane.vala index d1b00d6..fe2dfbd 100644 --- a/plugins/shotwell-publishing/PhotosPublishingPane.vala +++ b/plugins/shotwell-publishing/PhotosPublishingPane.vala @@ -22,25 +22,25 @@ internal class PublishingOptionsPane : Gtk.Box, Spit.Publishing.DialogPane { }; [GtkChild] - private Gtk.Button logout_button; + private unowned Gtk.Button logout_button; [GtkChild] - private Gtk.Button publish_button; + private unowned Gtk.Button publish_button; [GtkChild] - private Gtk.RadioButton existing_album_radio; + private unowned Gtk.RadioButton existing_album_radio; [GtkChild] - private Gtk.ComboBoxText existing_albums_combo; + private unowned Gtk.ComboBoxText existing_albums_combo; [GtkChild] - private Gtk.ComboBoxText size_combo; + private unowned Gtk.ComboBoxText size_combo; [GtkChild] - private Gtk.Label publish_to_label; + private unowned Gtk.Label publish_to_label; [GtkChild] - private Gtk.Label login_identity_label; + private unowned Gtk.Label login_identity_label; [GtkChild] - private Gtk.CheckButton strip_metadata_check; + private unowned Gtk.CheckButton strip_metadata_check; [GtkChild] - private Gtk.RadioButton new_album_radio; + private unowned Gtk.RadioButton new_album_radio; [GtkChild] - private Gtk.Entry new_album_entry; + private unowned Gtk.Entry new_album_entry; public signal void publish(); public signal void logout(); diff --git a/plugins/shotwell-publishing/PhotosService.vala b/plugins/shotwell-publishing/PhotosService.vala index 8e328f4..c68510b 100644 --- a/plugins/shotwell-publishing/PhotosService.vala +++ b/plugins/shotwell-publishing/PhotosService.vala @@ -8,15 +8,7 @@ namespace Publishing.GooglePhotos { public class Service : Object, Spit.Pluggable, Spit.Publishing.Service { - private const string ICON_FILENAME = "google-photos.svg"; - - private static Gdk.Pixbuf[] icon_pixbuf_set = null; - - static construct { - icon_pixbuf_set = Resources.load_from_resource(Resources.RESOURCE_PATH + "/" + ICON_FILENAME); - } - - public Service(GLib.File resource_directory) {} + public Service() {} public int get_pluggable_interface(int min_host_interface, int max_host_interface) { return Spit.negotiate_interfaces(min_host_interface, max_host_interface, @@ -31,16 +23,14 @@ public class Service : Object, Spit.Pluggable, Spit.Publishing.Service { return "Google Photos"; } - public void get_info(ref Spit.PluggableInfo info) { + public Spit.PluggableInfo get_info() { + var info = new Spit.PluggableInfo(); + info.authors = "Jens Georg"; info.copyright = _("Copyright 2019 Jens Georg <mail@jensge.org>"); - 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; + info.icon_name = "google-photos"; + + return info; } public Spit.Publishing.Publisher create_publisher(Spit.Publishing.PluginHost host) { diff --git a/plugins/shotwell-publishing/PhotosUploader.vala b/plugins/shotwell-publishing/PhotosUploader.vala index 83137ee..68b8e41 100644 --- a/plugins/shotwell-publishing/PhotosUploader.vala +++ b/plugins/shotwell-publishing/PhotosUploader.vala @@ -11,7 +11,7 @@ internal class UploadTransaction : Publishing.RESTSupport.GooglePublisher.Authen private PublishingParameters parameters; private Publishing.RESTSupport.GoogleSession session; private Spit.Publishing.Publishable publishable; - private MappedFile mapped_file; + private InputStream mapped_file; public UploadTransaction(Publishing.RESTSupport.GoogleSession session, PublishingParameters parameters, Spit.Publishing.Publishable publishable) { @@ -28,13 +28,16 @@ internal class UploadTransaction : Publishing.RESTSupport.GooglePublisher.Authen return this.publishable; } - public override void execute() throws Spit.Publishing.PublishingError { + public override async void execute_async() throws Spit.Publishing.PublishingError { var basename = publishable.get_param_string(Spit.Publishing.Publishable.PARAM_STRING_BASENAME); + int64 mapped_file_size = -1; // attempt to map the binary image data from disk into memory try { - mapped_file = new MappedFile(publishable.get_serialized_file().get_path(), false); - } catch (FileError e) { + mapped_file = publishable.get_serialized_file().read(null); + var info = ((FileInputStream)mapped_file).query_info("standard::size", null); + mapped_file_size = info.get_size(); + } catch (Error e) { string msg = "Google Photos: couldn't read data from %s: %s".printf( publishable.get_serialized_file().get_path(), e.message); warning("%s", msg); @@ -42,14 +45,6 @@ internal class UploadTransaction : Publishing.RESTSupport.GooglePublisher.Authen throw new Spit.Publishing.PublishingError.LOCAL_FILE_ERROR(msg); } - unowned uint8[] photo_data = (uint8[]) mapped_file.get_contents(); - photo_data.length = (int) mapped_file.get_length(); - - // bind the binary image data read from disk into a Soup.Buffer object so that we - // can attach it to the multipart request, then actaully append the buffer - // to the multipart request. Then, set the MIME type for this part. - Soup.Buffer bindable_data = new Soup.Buffer(Soup.MemoryUse.TEMPORARY, photo_data); - // create a message that can be sent over the wire whose payload is the multipart container // that we've been building up var outbound_message = new Soup.Message ("POST", get_endpoint_url()); @@ -58,12 +53,12 @@ internal class UploadTransaction : Publishing.RESTSupport.GooglePublisher.Authen outbound_message.request_headers.append("X-Goog-Upload-File-Name", basename); outbound_message.request_headers.append("X-Goog-Upload-Protocol", "raw"); outbound_message.request_headers.set_content_type("application/octet-stream", null); - outbound_message.request_body.append_buffer (bindable_data); - set_message(outbound_message); + outbound_message.set_request_body(null, mapped_file, (ssize_t)mapped_file_size); + set_message(outbound_message, (ulong)mapped_file_size); // send the message and get its response set_is_executed(true); - send(); + yield send_async(); } } diff --git a/plugins/shotwell-publishing/PiwigoPublishing.vala b/plugins/shotwell-publishing/PiwigoPublishing.vala index d311ac5..9bf0013 100644 --- a/plugins/shotwell-publishing/PiwigoPublishing.vala +++ b/plugins/shotwell-publishing/PiwigoPublishing.vala @@ -1,18 +1,28 @@ -/* Copyright 2016 Software Freedom Conservancy Inc. - * - * This software is licensed under the GNU Lesser General Public License - * (version 2.1 or later). See the COPYING file in this distribution. - */ +// SPDX-License-Identifier: LGPL-2.1-or-later +// SPDX-FileCopyrightText: 2016 Software Freedom Conservancy Inc. -public class PiwigoService : Object, Spit.Pluggable, Spit.Publishing.Service { - private const string ICON_FILENAME = "piwigo.svg"; +internal class Publishing.Piwigo.Account : Spit.Publishing.Account, Object { + public string server_uri; + public string user; - private static Gdk.Pixbuf[] icon_pixbuf_set = null; - - public PiwigoService(GLib.File resource_directory) { - if (icon_pixbuf_set == null) - icon_pixbuf_set = Resources.load_from_resource - (Resources.RESOURCE_PATH + "/" + ICON_FILENAME); + public Account(string server_uri, string user) { + this.server_uri = server_uri; + this.user = user; + } + + public string display_name() { + try { + var uri = Uri.parse(server_uri, UriFlags.NONE); + return user + "@" + uri.get_host(); + } catch (Error err) { + debug("Failed to parse uri in Piwigo account. %s", err.message); + return user + "@" + server_uri; + } + } +} + +public class PiwigoService : Object, Spit.Pluggable, Spit.Publishing.Service { + public PiwigoService() { } public int get_pluggable_interface(int min_host_interface, int max_host_interface) { @@ -21,35 +31,68 @@ public class PiwigoService : Object, Spit.Pluggable, Spit.Publishing.Service { } public unowned string get_id() { - return "org.yorba.shotwell.publishing.piwigo"; + return "org.gnome.shotwell.publishing.piwigo"; } public unowned string get_pluggable_name() { return "Piwigo"; } - public void get_info(ref Spit.PluggableInfo info) { + public Spit.PluggableInfo get_info() { + var info = new Spit.PluggableInfo(); + info.authors = "Bruno Girin"; info.copyright = _("Copyright 2016 Software Freedom Conservancy Inc."); 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; + info.icon_name = "piwigo"; + + return info; } public void activation(bool enabled) { } public Spit.Publishing.Publisher create_publisher(Spit.Publishing.PluginHost host) { - return new Publishing.Piwigo.PiwigoPublisher(this, host); + return new Publishing.Piwigo.PiwigoPublisher(this, host, null); + } + + public Spit.Publishing.Publisher create_publisher_with_account(Spit.Publishing.PluginHost host, + Spit.Publishing.Account? account) { + return new Publishing.Piwigo.PiwigoPublisher(this, host, account); } public Spit.Publishing.Publisher.MediaType get_supported_media() { return (Spit.Publishing.Publisher.MediaType.PHOTO); } + + public Gee.List<Spit.Publishing.Account>? get_accounts(string profile_id) { + var list = new Gee.ArrayList<Spit.Publishing.Account>(); + + // Always add the empty default account to allow new logins + list.add(new Spit.Publishing.DefaultAccount()); + + // Collect information from saved logins + var schema = new Secret.Schema (Publishing.Piwigo.PiwigoPublisher.PASSWORD_SCHEME, Secret.SchemaFlags.NONE, + Publishing.Piwigo.PiwigoPublisher.SCHEMA_KEY_PROFILE_ID, Secret.SchemaAttributeType.STRING, + "url", Secret.SchemaAttributeType.STRING, + "user", Secret.SchemaAttributeType.STRING); + + var attributes = new HashTable<string, string>(str_hash, str_equal); + attributes[Publishing.Piwigo.PiwigoPublisher.SCHEMA_KEY_PROFILE_ID] = profile_id; + try { + var entries = Secret.password_searchv_sync(schema, attributes, Secret.SearchFlags.ALL, null); + + foreach (var entry in entries) { + var found_attributes = entry.get_attributes(); + list.add(new Publishing.Piwigo.Account(found_attributes["url"], found_attributes["user"])); + } + } catch (Error err) { + warning("Failed to look up accounts for Piwigo: %s", err.message); + } + + return list; + } } namespace Publishing.Piwigo { @@ -117,12 +160,16 @@ internal class PublishingParameters { public SizeEntry photo_size = null; public bool title_as_comment = false; public bool no_upload_tags = false; + public bool no_upload_ratings = false; public PublishingParameters() { } } public class PiwigoPublisher : Spit.Publishing.Publisher, GLib.Object { + internal const string PASSWORD_SCHEME = "org.gnome.Shotwell.Piwigo"; + internal const string SCHEMA_KEY_PROFILE_ID = "shotwell-profile-id"; + private Spit.Publishing.Service service; private Spit.Publishing.PluginHost host; private bool running = false; @@ -131,13 +178,26 @@ public class PiwigoPublisher : Spit.Publishing.Publisher, GLib.Object { private Category[] categories = null; private PublishingParameters parameters = null; private Spit.Publishing.ProgressCallback progress_reporter = null; + private Secret.Schema? schema = null; + private Publishing.Piwigo.Account? account = null; public PiwigoPublisher(Spit.Publishing.Service service, - Spit.Publishing.PluginHost host) { + Spit.Publishing.PluginHost host, + Spit.Publishing.Account? account) { debug("PiwigoPublisher instantiated."); this.service = service; this.host = host; session = new Session(); + + // This should only ever be the default account which we don't care about + if (account is Publishing.Piwigo.Account) { + this.account = (Publishing.Piwigo.Account)account; + } + + this.schema = new Secret.Schema (PASSWORD_SCHEME, Secret.SchemaFlags.NONE, + SCHEMA_KEY_PROFILE_ID, Secret.SchemaAttributeType.STRING, + "url", Secret.SchemaAttributeType.STRING, + "user", Secret.SchemaAttributeType.STRING); } // Publisher interface implementation @@ -164,14 +224,16 @@ public class PiwigoPublisher : Spit.Publishing.Publisher, GLib.Object { if (session.is_authenticated()) { debug("PiwigoPublisher: session is authenticated."); - do_fetch_categories(); + do_fetch_categories.begin(); } else { debug("PiwigoPublisher: session is not authenticated."); string? persistent_url = get_persistent_url(); string? persistent_username = get_persistent_username(); - string? persistent_password = get_persistent_password(); + string? persistent_password = get_persistent_password(persistent_url, persistent_username); + + // This will only be null if either of the other two was null or the password did not exist if (persistent_url != null && persistent_username != null && persistent_password != null) - do_network_login(persistent_url, persistent_username, + do_network_login.begin(persistent_url, persistent_username, persistent_password, get_remember_password()); else do_show_authentication_pane(); @@ -185,27 +247,65 @@ public class PiwigoPublisher : Spit.Publishing.Publisher, GLib.Object { // Session and persistent data public string? get_persistent_url() { - return host.get_config_string("url", null); + if (account != null) { + return account.server_uri; + } + + return null; } private void set_persistent_url(string url) { - host.set_config_string("url", url); + // Do nothing } public string? get_persistent_username() { - return host.get_config_string("username", null); + if (account != null) { + return account.user; + } + + return null; } private void set_persistent_username(string username) { - host.set_config_string("username", username); + // Do nothing } - public string? get_persistent_password() { - return host.get_config_string("password", null); + public string? get_persistent_password(string? url, string? user) { + if (url != null && user != null) { + try { + var pw = Secret.password_lookup_sync(this.schema, null, + SCHEMA_KEY_PROFILE_ID, host.get_current_profile_id(), + "url", url, "user", user); + + return pw; + } catch (Error err) { + critical("Failed to lookup the password for url %s and user %s: %s", url, user, err.message); + + return null; + } + } + + return null; } - private void set_persistent_password(string? password) { - host.set_config_string("password", password); + private void set_persistent_password(string? url, string? user, string? password) { + try { + if (password == null) { + // remove + Secret.password_clear_sync(this.schema, null, + SCHEMA_KEY_PROFILE_ID, host.get_current_profile_id(), + "url", url, "user", user); + } else { + Secret.password_store_sync(this.schema, Secret.COLLECTION_DEFAULT, + "Shotwell publishing (Piwigo account %s@%s)".printf(user, url), + password, + null, + SCHEMA_KEY_PROFILE_ID, host.get_current_profile_id(), + "url", url, "user", user); + } + } catch (Error err) { + critical("Failed to store password for %s@%s: %s", user, url, err.message); + } } public bool get_remember_password() { @@ -256,6 +356,14 @@ public class PiwigoPublisher : Spit.Publishing.Publisher, GLib.Object { host.set_config_bool("last-no-upload-tags", no_upload_tags); } + private bool get_last_no_upload_ratings() { + return host.get_config_bool("last-no-upload-ratings", false); + } + + private void set_last_no_upload_ratings(bool no_upload_ratings) { + host.set_config_bool("last-no-upload-ratings", no_upload_ratings); + } + private bool get_metadata_removal_choice() { return host.get_config_bool("strip_metadata", false); } @@ -290,9 +398,14 @@ public class PiwigoPublisher : Spit.Publishing.Publisher, GLib.Object { private void do_show_ssl_downgrade_pane (SessionLoginTransaction trans, string url) { - var uri = new Soup.URI (url); + string host_name = ""; + try { + host_name = GLib.Uri.parse (url, GLib.UriFlags.NONE).get_host(); + } catch (Error err) { + debug("Failed to parse URL: %s", err.message); + } host.set_service_locked (false); - var ssl_pane = new SSLErrorPane (trans, uri.get_host ()); + var ssl_pane = new SSLErrorPane (trans, host_name); ssl_pane.proceed.connect (() => { debug ("SSL: User wants us to retry with broken certificate"); this.session = new Session (); @@ -300,9 +413,9 @@ public class PiwigoPublisher : Spit.Publishing.Publisher, GLib.Object { string? persistent_url = get_persistent_url(); string? persistent_username = get_persistent_username(); - string? persistent_password = get_persistent_password(); + string? persistent_password = get_persistent_password(persistent_url, persistent_username); if (persistent_url != null && persistent_username != null && persistent_password != null) - do_network_login(persistent_url, persistent_username, + do_network_login.begin(persistent_url, persistent_username, persistent_password, get_remember_password()); else do_show_authentication_pane(); @@ -330,7 +443,7 @@ public class PiwigoPublisher : Spit.Publishing.Publisher, GLib.Object { if (!running) return; - do_network_login(url, username, password, remember_password); + do_network_login.begin(url, username, password, remember_password); } /** @@ -344,24 +457,24 @@ public class PiwigoPublisher : Spit.Publishing.Publisher, GLib.Object { * @param username the name of the Piwigo user used to login * @param password the password of the Piwigo user used to login */ - private void do_network_login(string url, string username, string password, bool remember_password) { + private async void do_network_login(string url, string username, string password, bool remember_password) { debug("ACTION: logging in"); host.set_service_locked(true); host.install_login_wait_pane(); set_remember_password(remember_password); - if (remember_password) - set_persistent_password(password); - else - set_persistent_password(null); + if (remember_password) { + set_persistent_password(url, username, password); + } else { + set_persistent_password(url, username, null); + } SessionLoginTransaction login_trans = new SessionLoginTransaction( session, normalise_url(url), username, password); - login_trans.network_error.connect(on_login_network_error); - login_trans.completed.connect(on_login_network_complete); try { - login_trans.execute(); + yield login_trans.execute_async(); + on_login_network_complete(login_trans); } catch (Spit.Publishing.PublishingError err) { if (err is Spit.Publishing.PublishingError.SSL_FAILED) { debug ("ERROR: SSL connection problems"); @@ -405,8 +518,6 @@ public class PiwigoPublisher : Spit.Publishing.Publisher, GLib.Object { */ 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.parse_string( @@ -435,34 +546,9 @@ public class PiwigoPublisher : Spit.Publishing.Publisher, GLib.Object { debug("Setting session pwg_id to %s", pwg_id); session.set_pwg_id(pwg_id); - do_fetch_session_status(endpoint_url, pwg_id); - } - - /** - * Event triggered when a network login action fails due to a network error. - * - * This event triggered as a result of a network error during the login - * transaction. As a result, it assumes that the service URL entered in the - * authentication dialog is incorrect and re-presents the authentication - * dialog with FAILED_RETRY_URL mode. - * - * @param bad_txn the received REST transaction - * @param err the received 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); - - if (session.is_authenticated()) // ignore these events if the session is already auth'd - return; - - do_show_authentication_pane(AuthenticationPane.Mode.FAILED_RETRY_URL); + do_fetch_session_status.begin(endpoint_url, pwg_id); } - + /** * Action to fetch the session status for a known Piwigo user. * @@ -473,29 +559,27 @@ public class PiwigoPublisher : Spit.Publishing.Publisher, GLib.Object { * persisted. In this case, it will log the user in and confirm the * identity. */ - private void do_fetch_session_status(string url = "", string pwg_id = "") { + private async void do_fetch_session_status(string url = "", string pwg_id = "") { debug("ACTION: fetching session status"); host.set_service_locked(true); host.install_account_fetch_wait_pane(); if (!session.is_authenticated()) { SessionGetStatusTransaction status_txn = new SessionGetStatusTransaction.unauthenticated(session, url, pwg_id); - status_txn.network_error.connect(on_session_get_status_error); - status_txn.completed.connect(on_session_get_status_complete); try { - status_txn.execute(); + yield status_txn.execute_async(); + on_session_get_status_complete(status_txn); } catch (Spit.Publishing.PublishingError err) { debug("ERROR: do_fetch_session_status, not authenticated"); do_show_error(err); } } else { SessionGetStatusTransaction status_txn = new SessionGetStatusTransaction(session); - status_txn.network_error.connect(on_session_get_status_error); - status_txn.completed.connect(on_session_get_status_complete); try { - status_txn.execute(); + yield status_txn.execute_async(); + on_session_get_status_complete(status_txn); } catch (Spit.Publishing.PublishingError err) { debug("ERROR: do_fetch_session_status, authenticated"); do_show_error(err); @@ -512,8 +596,6 @@ public class PiwigoPublisher : Spit.Publishing.Publisher, GLib.Object { */ private void on_session_get_status_complete(Publishing.RESTSupport.Transaction txn) { debug("EVENT: on_session_get_status_complete"); - txn.completed.disconnect(on_session_get_status_complete); - txn.network_error.disconnect(on_session_get_status_error); if (!session.is_authenticated()) { string endpoint_url = txn.get_endpoint_url(); @@ -533,7 +615,7 @@ public class PiwigoPublisher : Spit.Publishing.Publisher, GLib.Object { session.authenticate(endpoint_url, username, pwg_id); set_persistent_url(session.get_pwg_url()); set_persistent_username(session.get_username()); - do_fetch_categories(); + do_fetch_categories.begin(); } catch (Spit.Publishing.PublishingError err2) { debug("ERROR: on_session_get_status_complete, inner"); do_show_error(err2); @@ -548,43 +630,30 @@ public class PiwigoPublisher : Spit.Publishing.Publisher, GLib.Object { // This should never happen as the session should not be // authenticated at that point so this call is a safeguard // against the interaction not happening properly. - do_fetch_categories(); + do_fetch_categories.begin(); } } /** - * Event triggered when the get session status fails due to a network error. - */ - private void on_session_get_status_error( - Publishing.RESTSupport.Transaction bad_txn, - Spit.Publishing.PublishingError err - ) { - debug("EVENT: on_session_get_status_error"); - bad_txn.completed.disconnect(on_session_get_status_complete); - bad_txn.network_error.disconnect(on_session_get_status_error); - on_network_error(bad_txn, err); - } - - /** * Action that fetches all available categories from the Piwigo service. * * This action fetches all categories from the Piwigo service in order * to populate the publishing pane presented to the user. */ - private void do_fetch_categories() { + private async void do_fetch_categories() { debug("ACTION: fetching categories"); host.set_service_locked(true); host.install_account_fetch_wait_pane(); CategoriesGetListTransaction cat_trans = new CategoriesGetListTransaction(session); - cat_trans.network_error.connect(on_category_fetch_error); - cat_trans.completed.connect(on_category_fetch_complete); try { - cat_trans.execute(); + yield cat_trans.execute_async(); + on_category_fetch_complete(cat_trans); } catch (Spit.Publishing.PublishingError err) { debug("ERROR: do_fetch_categories"); do_show_error(err); + return; } } @@ -597,8 +666,6 @@ public class PiwigoPublisher : Spit.Publishing.Publisher, GLib.Object { */ private void on_category_fetch_complete(Publishing.RESTSupport.Transaction txn) { debug("EVENT: on_category_fetch_complete"); - txn.completed.disconnect(on_category_fetch_complete); - txn.network_error.disconnect(on_category_fetch_error); debug("PiwigoConnector: list of categories: %s", txn.get_response()); // Empty the categories if (categories != null) { @@ -654,20 +721,6 @@ public class PiwigoPublisher : Spit.Publishing.Publisher, GLib.Object { } /** - * Event triggered when the fetch categories transaction fails due to a - * network error. - */ - private void on_category_fetch_error( - Publishing.RESTSupport.Transaction bad_txn, - Spit.Publishing.PublishingError err - ) { - debug("EVENT: on_category_fetch_error"); - bad_txn.completed.disconnect(on_category_fetch_complete); - bad_txn.network_error.disconnect(on_category_fetch_error); - on_network_error(bad_txn, err); - } - - /** * Action that shows the publishing options pane. * * This action method shows the publishing options pane. @@ -678,8 +731,8 @@ public class PiwigoPublisher : Spit.Publishing.Publisher, GLib.Object { host.set_service_locked(false); PublishingOptionsPane opts_pane = new PublishingOptionsPane( this, categories, get_last_category(), get_last_permission_level(), get_last_photo_size(), - get_last_title_as_comment(), get_last_no_upload_tags(), get_metadata_removal_choice()); - opts_pane.logout.connect(on_publishing_options_pane_logout_clicked); + get_last_title_as_comment(), get_last_no_upload_tags(), get_last_no_upload_ratings(), get_metadata_removal_choice()); + opts_pane.logout.connect(() => { on_publishing_options_pane_logout_clicked.begin(); }); 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()); @@ -688,14 +741,12 @@ public class PiwigoPublisher : Spit.Publishing.Publisher, GLib.Object { /** * Event triggered when the user clicks logout in the publishing options pane. */ - private void on_publishing_options_pane_logout_clicked() { + private async void on_publishing_options_pane_logout_clicked() { debug("EVENT: on_publishing_options_pane_logout_clicked"); - SessionLogoutTransaction logout_trans = new SessionLogoutTransaction(session); - logout_trans.network_error.connect(on_logout_network_error); - logout_trans.completed.connect(on_logout_network_complete); try { - logout_trans.execute(); + yield new SessionLogoutTransaction(session).execute_async(); + on_logout_network_complete(); } catch (Spit.Publishing.PublishingError err) { debug("ERROR: on_publishing_options_pane_logout_clicked"); do_show_error(err); @@ -708,29 +759,14 @@ public class PiwigoPublisher : Spit.Publishing.Publisher, GLib.Object { * This event de-authenticates the session and shows the authentication * pane again. */ - private void on_logout_network_complete(Publishing.RESTSupport.Transaction txn) { + private void on_logout_network_complete() { debug("EVENT: on_logout_network_complete"); - txn.completed.disconnect(on_logout_network_complete); - txn.network_error.disconnect(on_logout_network_error); session.deauthenticate(); do_show_authentication_pane(AuthenticationPane.Mode.INTRO); } - - /** - * Event triggered when the logout action fails due to a network error. - */ - private void on_logout_network_error( - Publishing.RESTSupport.Transaction bad_txn, - Spit.Publishing.PublishingError err - ) { - debug("EVENT: on_logout_network_error"); - bad_txn.completed.disconnect(on_logout_network_complete); - bad_txn.network_error.disconnect(on_logout_network_error); - on_network_error(bad_txn, err); - } - + /** * Event triggered when the user clicks publish in the publishing options pane. * @@ -748,9 +784,9 @@ public class PiwigoPublisher : Spit.Publishing.Publisher, GLib.Object { this.strip_metadata = strip_metadata; if (parameters.category.is_local()) { - do_create_category(parameters.category); + do_create_category.begin(parameters.category); } else { - do_upload(this.strip_metadata); + do_upload.begin(this.strip_metadata); } } @@ -760,12 +796,12 @@ public class PiwigoPublisher : Spit.Publishing.Publisher, GLib.Object { * This actions runs a REST transaction to create a new category in the * Piwigo library. It displays a wait pane with an information message * while the transaction is running. This action should only be called with - * a local cateogory, i.e. one that does not exist on the server and does + * a local category, i.e. one that does not exist on the server and does * not yet have an ID. * * @param category the new category to create on the server */ - private void do_create_category(Category category) { + private async void do_create_category(Category category) { debug("ACTION: creating a new category: %s".printf(category.name)); assert(category.is_local()); @@ -774,11 +810,10 @@ public class PiwigoPublisher : Spit.Publishing.Publisher, GLib.Object { CategoriesAddTransaction creation_trans = new CategoriesAddTransaction( session, category.name.strip(), int.parse(category.uppercats), category.comment); - creation_trans.network_error.connect(on_category_add_error); - creation_trans.completed.connect(on_category_add_complete); try { - creation_trans.execute(); + yield creation_trans.execute_async(); + on_category_add_complete(creation_trans); } catch (Spit.Publishing.PublishingError err) { debug("ERROR: do_create_category"); do_show_error(err); @@ -794,8 +829,6 @@ public class PiwigoPublisher : Spit.Publishing.Publisher, GLib.Object { */ private void on_category_add_complete(Publishing.RESTSupport.Transaction txn) { debug("EVENT: on_category_add_complete"); - txn.completed.disconnect(on_category_add_complete); - txn.network_error.disconnect(on_category_add_error); // Parse the response try { @@ -808,30 +841,17 @@ public class PiwigoPublisher : Spit.Publishing.Publisher, GLib.Object { string id_string = id_node->get_content(); int id = int.parse(id_string); parameters.category.id = id; - do_upload(strip_metadata); + do_upload.begin(strip_metadata); } catch (Spit.Publishing.PublishingError err) { debug("ERROR: on_category_add_complete"); do_show_error(err); } } - - /** - * Event triggered when the add category action fails due to a network error. - */ - private void on_category_add_error( - Publishing.RESTSupport.Transaction bad_txn, - Spit.Publishing.PublishingError err - ) { - debug("EVENT: on_category_add_error"); - bad_txn.completed.disconnect(on_category_add_complete); - bad_txn.network_error.disconnect(on_category_add_error); - on_network_error(bad_txn, err); - } - + /** * Upload action: the big one, the one we've been waiting for! */ - private void do_upload(bool strip_metadata) { + private async void do_upload(bool strip_metadata) { this.strip_metadata = strip_metadata; debug("ACTION: uploading pictures"); @@ -842,25 +862,27 @@ public class PiwigoPublisher : Spit.Publishing.Publisher, GLib.Object { set_last_photo_size(parameters.photo_size.id); set_last_title_as_comment(parameters.title_as_comment); set_last_no_upload_tags(parameters.no_upload_tags); + set_last_no_upload_ratings(parameters.no_upload_ratings); set_metadata_removal_choice(strip_metadata); progress_reporter = host.serialize_publishables(parameters.photo_size.id, this.strip_metadata); Spit.Publishing.Publishable[] publishables = host.get_publishables(); Uploader uploader = new Uploader(session, publishables, parameters); - uploader.upload_complete.connect(on_upload_complete); - uploader.upload_error.connect(on_upload_error); - uploader.upload(on_upload_status_updated); + try { + var num_published = yield uploader.upload_async(on_upload_status_updated); + on_upload_complete(num_published); + } catch (Spit.Publishing.PublishingError err) { + do_show_error(err); + } } /** * Event triggered when the batch uploader reports that at least one of the * network transactions encapsulating uploads has completed successfully */ - private void on_upload_complete(Publishing.RESTSupport.BatchUploader uploader, int num_published) { + private void on_upload_complete(int num_published) { debug("EVENT: on_upload_complete"); - uploader.upload_complete.disconnect(on_upload_complete); - uploader.upload_error.disconnect(on_upload_error); // TODO: should a message be displayed to the user if num_published is zero? @@ -869,22 +891,7 @@ public class PiwigoPublisher : Spit.Publishing.Publisher, GLib.Object { do_show_success_pane(); } - - /** - * 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_error( - Publishing.RESTSupport.BatchUploader uploader, - Spit.Publishing.PublishingError err - ) { - debug("EVENT: on_upload_error"); - uploader.upload_complete.disconnect(on_upload_complete); - uploader.upload_error.disconnect(on_upload_error); - - do_show_error(err); - } - + /** * Event triggered when upload progresses and the status needs to be updated. */ @@ -910,17 +917,6 @@ public class PiwigoPublisher : Spit.Publishing.Publisher, GLib.Object { } /** - * 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) { @@ -1029,8 +1025,9 @@ internal class SSLErrorPane : Shotwell.Plugins.Common.BuilderPane { base.constructed (); var label = this.get_builder ().get_object ("main_text") as Gtk.Label; + var bold_host = "<b>%s</b>".printf(host); // %s is the host name that we tried to connect to - label.set_text (_("This does not look like the real <b>%s</b>. Attackers might be trying to steal or alter information going to or from this site (for example, private messages, credit card information, or passwords).").printf (host)); + label.set_text (_("This does not look like the real %s. Attackers might be trying to steal or alter information going to or from this site (for example, private messages, credit card information, or passwords).").printf(bold_host)); label.use_markup = true; label = this.get_builder ().get_object ("ssl_errors") as Gtk.Label; @@ -1136,7 +1133,7 @@ internal class AuthenticationPane : Shotwell.Plugins.Common.BuilderPane { username_entry.set_text(persistent_username); } password_entry = builder.get_object ("password_entry") as Gtk.Entry; - string? persistent_password = publisher.get_persistent_password(); + string? persistent_password = publisher.get_persistent_password(persistent_url, persistent_username); if (persistent_password != null) { password_entry.set_text(persistent_password); } @@ -1205,6 +1202,7 @@ internal class PublishingOptionsPane : Shotwell.Plugins.Common.BuilderPane { private Gtk.CheckButton strip_metadata_check = null; private Gtk.CheckButton title_as_comment_check = null; private Gtk.CheckButton no_upload_tags_check = null; + private Gtk.CheckButton no_upload_ratings_check = null; private Gtk.Button logout_button; private Gtk.Button publish_button; private Gtk.TextView album_comment; @@ -1218,6 +1216,7 @@ internal class PublishingOptionsPane : Shotwell.Plugins.Common.BuilderPane { public int last_photo_size { private get; construct; } public bool last_title_as_comment { private get; construct; } public bool last_no_upload_tags { private get; construct; } + public bool last_no_upload_ratings { private get; construct; } public bool strip_metadata_enabled { private get; construct; } public Gee.List<Category> existing_categories { private get; construct; } public string default_comment { private get; construct; } @@ -1232,6 +1231,7 @@ internal class PublishingOptionsPane : Shotwell.Plugins.Common.BuilderPane { int last_photo_size, bool last_title_as_comment, bool last_no_upload_tags, + bool last_no_upload_ratings, bool strip_metadata_enabled) { Object (resource_path : Resources.RESOURCE_PATH + "/piwigo_publishing_options_pane.ui", @@ -1242,6 +1242,7 @@ internal class PublishingOptionsPane : Shotwell.Plugins.Common.BuilderPane { last_photo_size : last_photo_size, last_title_as_comment : last_title_as_comment, last_no_upload_tags : last_no_upload_tags, + last_no_upload_ratings : last_no_upload_ratings, strip_metadata_enabled : strip_metadata_enabled, existing_categories : new Gee.ArrayList<Category>.wrap (categories, Category.equal), @@ -1275,6 +1276,9 @@ internal class PublishingOptionsPane : Shotwell.Plugins.Common.BuilderPane { no_upload_tags_check = builder.get_object("no_upload_tags_check") as Gtk.CheckButton; no_upload_tags_check.set_active(last_no_upload_tags); + no_upload_ratings_check = builder.get_object("no_upload_ratings_check") as Gtk.CheckButton; + no_upload_ratings_check.set_active(last_no_upload_ratings); + logout_button = builder.get_object("logout_button") as Gtk.Button; logout_button.clicked.connect(on_logout_button_clicked); @@ -1325,6 +1329,7 @@ internal class PublishingOptionsPane : Shotwell.Plugins.Common.BuilderPane { params.photo_size = photo_sizes[size_combo.get_active()]; params.title_as_comment = title_as_comment_check.get_active(); params.no_upload_tags = no_upload_tags_check.get_active(); + params.no_upload_ratings = no_upload_ratings_check.get_active(); if (create_new_radio.get_active()) { string uploadcomment = album_comment.buffer.text.strip(); int a = within_existing_combo.get_active(); @@ -1720,12 +1725,14 @@ private class CategoriesAddTransaction : Transaction { private class ImagesAddTransaction : Publishing.RESTSupport.UploadTransaction { private PublishingParameters parameters = null; + private Session session = null; public ImagesAddTransaction(Session session, PublishingParameters parameters, Spit.Publishing.Publishable publishable) { base.with_endpoint_url(session, publishable, session.get_pwg_url()); if (session.is_authenticated()) { add_header("Cookie", "pwg_id=".concat(session.get_pwg_id())); } + this.session = session; this.parameters = parameters; string[] keywords = publishable.get_publishing_keywords(); @@ -1785,12 +1792,40 @@ private class ImagesAddTransaction : Publishing.RESTSupport.UploadTransaction { !basename.down().has_suffix(".jpg")) { basename += ".jpg"; } - disposition_table.insert("filename", Soup.URI.encode(basename, null)); + disposition_table.insert("filename", GLib.Uri.escape_string(basename, null)); disposition_table.insert("name", "image"); set_binary_disposition_table(disposition_table); + base.completed.connect(on_completed); + } + + private void on_completed() { + try{ + Publishing.RESTSupport.XmlDocument resp_doc = Publishing.RESTSupport.XmlDocument.parse_string( + base.get_response(), Transaction.validate_xml); + Xml.Node* image_node = resp_doc.get_named_child(resp_doc.get_root_node(), "image_id"); + string image_id = image_node->get_content(); + + if (!parameters.no_upload_ratings) + new ImagesAddRating(session, publishable, image_id); + } catch(Spit.Publishing.PublishingError err) { + debug("Response parse error"); + } } } -} // namespace +private class ImagesAddRating : Publishing.RESTSupport.UploadTransaction { + public ImagesAddRating(Session session, Spit.Publishing.Publishable publishable, string image_id) { + base.with_endpoint_url(session, publishable, session.get_pwg_url()); + if (session.is_authenticated()) { + add_header("Cookie", "pwg_id=".concat(session.get_pwg_id())); + } + add_argument("method", "pwg.images.rate"); + add_argument("image_id", image_id); + add_argument("rate", publishable.get_rating().to_string()); + base.execute_async.begin(); + } +} + +} // namespace diff --git a/plugins/shotwell-publishing/TumblrPublishing.vala b/plugins/shotwell-publishing/TumblrPublishing.vala index 7061d6d..c9fed3f 100644 --- a/plugins/shotwell-publishing/TumblrPublishing.vala +++ b/plugins/shotwell-publishing/TumblrPublishing.vala @@ -6,16 +6,7 @@ */ public class TumblrService : Object, Spit.Pluggable, Spit.Publishing.Service { - private const string ICON_FILENAME = "tumblr.png"; - - private static Gdk.Pixbuf[] icon_pixbuf_set = null; - - public TumblrService(GLib.File resource_directory) { - if (icon_pixbuf_set == null) - icon_pixbuf_set = - Resources.load_from_resource(Resources.RESOURCE_PATH + "/" + - ICON_FILENAME); - } + public TumblrService() {} public int get_pluggable_interface(int min_host_interface, int max_host_interface) { return Spit.negotiate_interfaces(min_host_interface, max_host_interface, @@ -23,23 +14,21 @@ public class TumblrService : Object, Spit.Pluggable, Spit.Publishing.Service { } public unowned string get_id() { - return "org.yorba.shotwell.publishing.tumblr"; + return "org.gnome.shotwell.publishing.tumblr"; } public unowned string get_pluggable_name() { return "Tumblr"; } - public void get_info(ref Spit.PluggableInfo info) { + public Spit.PluggableInfo get_info() { + var info = new Spit.PluggableInfo(); + info.authors = "Jeroen Arnoldus"; info.copyright = _("Copyright 2012 BJA Electronics"); - 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; + info.icon_name = "tumblr"; + + return info; } public void activation(bool enabled) { @@ -189,72 +178,47 @@ namespace Publishing.Tumblr { auth_token_secret.get_string(), ""); - do_get_blogs(); + do_get_blogs.begin(); } - private void do_get_blogs() { + private async void do_get_blogs() { debug("ACTION: obtain all blogs of the tumblr user"); UserInfoFetchTransaction txn = new UserInfoFetchTransaction(session); - txn.completed.connect(on_info_request_txn_completed); - txn.network_error.connect(on_info_request_txn_error); try { - txn.execute(); - } catch (Spit.Publishing.PublishingError err) { + yield txn.execute_async(); + if (!is_running()) + return; + + debug("EVENT: user info request transaction completed; response = '%s'", + txn.get_response()); + do_parse_token_info_from_user_request(txn.get_response()); + do_show_publishing_options_pane(); + } catch (Error err) { + session.deauthenticate(); + //invalidate_persistent_session(); + debug("EVENT: user info request transaction caused a network error"); host.post_error(err); } - - } - - private void on_info_request_txn_completed(Publishing.RESTSupport.Transaction txn) { - txn.completed.disconnect(on_info_request_txn_completed); - txn.network_error.disconnect(on_info_request_txn_error); - - if (!is_running()) - return; - - debug("EVENT: user info request transaction completed; response = '%s'", - txn.get_response()); - do_parse_token_info_from_user_request(txn.get_response()); - do_show_publishing_options_pane(); - } - - - private void do_parse_token_info_from_user_request(string response) { + private void do_parse_token_info_from_user_request(string response) throws Error { debug("ACTION: parsing info request response '%s' into list of available blogs", response); - try { - var parser = new Json.Parser(); - parser.load_from_data (response, -1); - var root_object = parser.get_root().get_object(); - this.username = root_object.get_object_member("response").get_object_member("user").get_string_member ("name"); - debug("Got user name: %s",username); - foreach (var blognode in root_object.get_object_member("response").get_object_member("user").get_array_member("blogs").get_elements ()) { - var blog = blognode.get_object (); - string name = blog.get_string_member ("name"); - string url = blog.get_string_member ("url").replace("http://","").replace("https://", "").replace("/",""); - debug("Got blog name: %s and url: %s", name, url); - this.blogs += new BlogEntry(name,url); - } - } catch (Error err) { - host.post_error(err); + + var parser = new Json.Parser(); + parser.load_from_data (response, -1); + var root_object = parser.get_root().get_object(); + this.username = root_object.get_object_member("response").get_object_member("user").get_string_member ("name"); + debug("Got user name: %s",username); + foreach (var blognode in root_object.get_object_member("response").get_object_member("user").get_array_member("blogs").get_elements ()) { + var blog = blognode.get_object (); + string name = blog.get_string_member ("name"); + string url = blog.get_string_member ("url").replace("http://","").replace("https://", "").replace("/",""); + debug("Got blog name: %s and url: %s", name, url); + this.blogs += new BlogEntry(name,url); } } - private void on_info_request_txn_error(Publishing.RESTSupport.Transaction txn, - Spit.Publishing.PublishingError err) { - txn.completed.disconnect(on_info_request_txn_completed); - txn.network_error.disconnect(on_info_request_txn_error); - - if (!is_running()) - return; - - session.deauthenticate(); - //invalidate_persistent_session(); - debug("EVENT: user info request transaction caused a network error"); - host.post_error(err); - } private void do_show_publishing_options_pane() { debug("ACTION: displaying publishing options pane"); @@ -328,7 +292,7 @@ namespace Publishing.Tumblr { Uploader uploader = new Uploader(session, sorted_list.to_array(),blog_url); uploader.upload_complete.connect(on_upload_complete); uploader.upload_error.connect(on_upload_error); - uploader.upload(on_upload_status_updated); + uploader.upload_async.begin(on_upload_status_updated); } private void do_show_success_pane() { @@ -575,22 +539,6 @@ namespace Publishing.Tumblr { internal class UploadTransaction : Publishing.RESTSupport.OAuth1.UploadTransaction { //Workaround for Soup.URI.encode() to support binary data (i.e. string with \0) - private string encode( uint8[] data ){ - var s = new StringBuilder(); - char[] bytes = new char[2]; - bytes[1] = 0; - foreach( var byte in data ) - { - if(byte == 0) { - s.append( "%00" ); - } else { - bytes[0] = (char)byte; - s.append( Soup.URI.encode((string) bytes, ENCODE_RFC_3986_EXTRA) ); - } - } - return s.str; - } - public UploadTransaction(Publishing.RESTSupport.OAuth1.Session session,Spit.Publishing.Publishable publishable, string blog_url) { debug("Init upload transaction"); @@ -598,25 +546,21 @@ namespace Publishing.Tumblr { } - public override void execute() throws Spit.Publishing.PublishingError { + public override async void execute_async() throws Spit.Publishing.PublishingError { string payload; size_t payload_length; try { FileUtils.get_contents(base.publishable.get_serialized_file().get_path(), out payload, out payload_length); - string reqdata = this.encode(payload.data[0:payload_length]); - - - - add_argument("data[0]", reqdata); + add_argument("data64", Base64.encode(payload.data[0:payload_length])); add_argument("type", "photo"); string[] keywords = base.publishable.get_publishing_keywords(); string tags = ""; if (keywords != null) { tags = string.joinv (",", keywords); } - add_argument("tags", Soup.URI.encode(tags, ENCODE_RFC_3986_EXTRA)); + add_argument("tags", tags); } catch (FileError e) { throw new Spit.Publishing.PublishingError.LOCAL_FILE_ERROR( @@ -626,13 +570,15 @@ namespace Publishing.Tumblr { this.authorize(); - Publishing.RESTSupport.Argument[] request_arguments = get_arguments(); - assert(request_arguments.length > 0); - - var request_data = Publishing.RESTSupport.Argument.serialize_list(request_arguments); + var form = new GLib.HashTable<string, string>(GLib.str_hash, GLib.str_equal); + foreach (var arg in get_arguments()) { + form.insert(arg.key, arg.value); + } + assert(form.size() > 0); - Soup.Message outbound_message = new Soup.Message( "POST", get_endpoint_url()); - outbound_message.set_request("application/x-www-form-urlencoded", Soup.MemoryUse.COPY, request_data.data); + var outbound_message = new Soup.Message ("POST", get_endpoint_url()); + var body = new Bytes(Soup.Form.encode_hash(form).data); + outbound_message.set_request_body_from_bytes(Soup.FORM_MIME_TYPE_URLENCODED, body); // TODO: there must be a better way to iterate over a map Gee.MapIterator<string, string> i = base.message_headers.map_iterator(); @@ -641,11 +587,11 @@ namespace Publishing.Tumblr { outbound_message.request_headers.append(i.get_key(), i.get_value()); cont = i.next(); } - set_message(outbound_message); + set_message(outbound_message, body.length); set_is_executed(true); - send(); + yield send_async(); } } diff --git a/plugins/shotwell-publishing/YouTubePublishing.vala b/plugins/shotwell-publishing/YouTubePublishing.vala index e50d17a..f0a9866 100644 --- a/plugins/shotwell-publishing/YouTubePublishing.vala +++ b/plugins/shotwell-publishing/YouTubePublishing.vala @@ -5,14 +5,7 @@ */ public class YouTubeService : Object, Spit.Pluggable, Spit.Publishing.Service { - private const string ICON_FILENAME = "youtube.png"; - - private static Gdk.Pixbuf[] icon_pixbuf_set = null; - - public YouTubeService(GLib.File resource_directory) { - if (icon_pixbuf_set == null) - icon_pixbuf_set = Resources.load_from_resource - (Resources.RESOURCE_PATH + "/" + ICON_FILENAME); + public YouTubeService() { } public int get_pluggable_interface(int min_host_interface, int max_host_interface) { @@ -21,23 +14,20 @@ public class YouTubeService : Object, Spit.Pluggable, Spit.Publishing.Service { } public unowned string get_id() { - return "org.yorba.shotwell.publishing.youtube"; + return "org.gnome.shotwell.publishing.youtube"; } public unowned string get_pluggable_name() { return "YouTube"; } - public void get_info(ref Spit.PluggableInfo info) { + public Spit.PluggableInfo get_info() { + var info = new Spit.PluggableInfo(); info.authors = "Jani Monoses, Lucas Beeler"; info.copyright = _("Copyright 2016 Software Freedom Conservancy Inc."); - 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; + info.icon_name = "youtube"; + + return info; } public Spit.Publishing.Publisher create_publisher(Spit.Publishing.PluginHost host) { @@ -60,10 +50,23 @@ private const string DEVELOPER_KEY = private enum PrivacySetting { PUBLIC, UNLISTED, - PRIVATE + PRIVATE; + + public string to_string() { + switch (this) { + case PUBLIC: + return "public"; + case UNLISTED: + return "unlisted"; + case PRIVATE: + return "private"; + default: + assert_not_reached(); + } + } } -private class PublishingParameters { +internal class PublishingParameters { private PrivacySetting privacy; private string? user_name; @@ -89,44 +92,14 @@ private class PublishingParameters { } } -internal class YouTubeAuthorizer : GData.Authorizer, Object { - private RESTSupport.GoogleSession session; - private Spit.Publishing.Authenticator authenticator; - - public YouTubeAuthorizer(RESTSupport.GoogleSession session, Spit.Publishing.Authenticator authenticator) { - this.session = session; - this.authenticator = authenticator; - } - - public bool is_authorized_for_domain(GData.AuthorizationDomain domain) { - return domain.scope.has_suffix ("auth/youtube"); - } - - public void process_request(GData.AuthorizationDomain? domain, - Soup.Message message) { - if (domain == null) { - return; - } - - var header = "Bearer %s".printf(session.get_access_token()); - message.request_headers.replace("Authorization", header); - } - - public bool refresh_authorization (GLib.Cancellable? cancellable = null) throws GLib.Error { - this.authenticator.refresh(); - return true; - } -} - public class YouTubePublisher : Publishing.RESTSupport.GooglePublisher { private bool running; private PublishingParameters publishing_parameters; private Spit.Publishing.ProgressCallback? progress_reporter; private Spit.Publishing.Authenticator authenticator; - private GData.YouTubeService youtube_service; public YouTubePublisher(Spit.Publishing.Service service, Spit.Publishing.PluginHost host) { - base(service, host, "https://gdata.youtube.com/"); + base(service, host, "https://www.googleapis.com/upload/youtube/v3/videos"); this.running = false; this.publishing_parameters = new PublishingParameters(); @@ -161,8 +134,6 @@ public class YouTubePublisher : Publishing.RESTSupport.GooglePublisher { publishing_parameters.set_user_name(get_session().get_user_name()); - this.youtube_service = new GData.YouTubeService(DEVELOPER_KEY, - new YouTubeAuthorizer(get_session(), this.authenticator)); do_show_publishing_options_pane(); } @@ -181,7 +152,7 @@ public class YouTubePublisher : Publishing.RESTSupport.GooglePublisher { if (!is_running()) return; - do_upload(); + do_upload.begin(); } private void on_upload_status_updated(int file_number, double completed_fraction) { @@ -195,32 +166,6 @@ public class YouTubePublisher : Publishing.RESTSupport.GooglePublisher { progress_reporter(file_number, completed_fraction); } - private void on_upload_complete(Publishing.RESTSupport.BatchUploader uploader, - int num_published) { - uploader.upload_complete.disconnect(on_upload_complete); - uploader.upload_error.disconnect(on_upload_error); - - debug("EVENT: uploader reports upload complete; %d items published.", num_published); - - if (!is_running()) - return; - - do_show_success_pane(); - } - - private void on_upload_error(Publishing.RESTSupport.BatchUploader uploader, - Spit.Publishing.PublishingError err) { - uploader.upload_complete.disconnect(on_upload_complete); - uploader.upload_error.disconnect(on_upload_error); - - if (!is_running()) - return; - - debug("EVENT: uploader reports upload error = '%s'.", err.message); - - get_host().post_error(err); - } - private void do_show_publishing_options_pane() { debug("ACTION: showing publishing options pane."); @@ -245,7 +190,7 @@ public class YouTubePublisher : Publishing.RESTSupport.GooglePublisher { get_host().set_service_locked(false); } - private void do_upload() { + private async void do_upload() { debug("ACTION: uploading media items to remote server."); get_host().set_service_locked(true); @@ -261,12 +206,24 @@ public class YouTubePublisher : Publishing.RESTSupport.GooglePublisher { return; Spit.Publishing.Publishable[] publishables = get_host().get_publishables(); - Uploader uploader = new Uploader(this.youtube_service, get_session(), publishables, publishing_parameters); + Uploader uploader = new Uploader(get_session(), publishables, publishing_parameters); + + try { + var num_published = yield uploader.upload_async(on_upload_status_updated); + debug("EVENT: uploader reports upload complete; %d items published.", num_published); + + if (!is_running()) + return; + + do_show_success_pane(); + } catch (Error err) { + if (!is_running()) + return; - uploader.upload_complete.connect(on_upload_complete); - uploader.upload_error.connect(on_upload_error); + debug("EVENT: uploader reports upload error = '%s'.", err.message); - uploader.upload(on_upload_status_updated); + get_host().post_error(err); + } } private void do_show_success_pane() { @@ -397,104 +354,20 @@ internal class PublishingOptionsPane : Spit.Publishing.DialogPane, GLib.Object { } } -internal class UploadTransaction : Publishing.RESTSupport.GooglePublisher.AuthenticatedTransaction { - private const string ENDPOINT_URL = "https://uploads.gdata.youtube.com/feeds/api/users/default/uploads"; - private PublishingParameters parameters; - private Publishing.RESTSupport.GoogleSession session; - private Spit.Publishing.Publishable publishable; - private GData.YouTubeService youtube_service; - - public UploadTransaction(GData.YouTubeService youtube_service, Publishing.RESTSupport.GoogleSession session, - PublishingParameters parameters, Spit.Publishing.Publishable publishable) { - base(session, ENDPOINT_URL, Publishing.RESTSupport.HttpMethod.POST); - assert(session.is_authenticated()); - this.session = session; - this.parameters = parameters; - this.publishable = publishable; - this.youtube_service = youtube_service; - } - - public override void execute() throws Spit.Publishing.PublishingError { - var video = new GData.YouTubeVideo(null); - - var slug = publishable.get_param_string(Spit.Publishing.Publishable.PARAM_STRING_BASENAME); - // Set title to publishing name, but if that's empty default to filename. - string title = publishable.get_publishing_name(); - if (title == "") { - title = publishable.get_param_string(Spit.Publishing.Publishable.PARAM_STRING_BASENAME); - } - video.title = title; - - video.is_private = (parameters.get_privacy() == PrivacySetting.PRIVATE); - - if (parameters.get_privacy() == PrivacySetting.UNLISTED) { - video.set_access_control("list", GData.YouTubePermission.DENIED); - } else if (!video.is_private) { - video.set_access_control("list", GData.YouTubePermission.ALLOWED); - } - - var file = publishable.get_serialized_file(); - - try { - var info = file.query_info(FileAttribute.STANDARD_CONTENT_TYPE + "," + - FileAttribute.STANDARD_SIZE, FileQueryInfoFlags.NONE); - var upload_stream = this.youtube_service.upload_video(video, slug, - info.get_content_type()); - var input_stream = file.read(); - - // Yuck... - var loop = new MainLoop(null, false); - this.splice_with_progress.begin(info, input_stream, upload_stream, (obj, res) => { - try { - this.splice_with_progress.end(res); - } catch (Error error) { - critical("Failed to upload: %s", error.message); - } - loop.quit(); - }); - loop.run(); - video = this.youtube_service.finish_video_upload(upload_stream); - } catch (Error error) { - critical("Upload failed: %s", error.message); - } - } - - private async void splice_with_progress(GLib.FileInfo info, GLib.InputStream input, GLib.OutputStream output) throws Error { - var total_bytes = info.get_size(); - var bytes_to_write = total_bytes; - uint8 buffer[8192]; - - while (bytes_to_write > 0) { - var bytes_read = yield input.read_async(buffer); - if (bytes_read == 0) - break; - - var bytes_written = yield output.write_async(buffer[0:bytes_read]); - bytes_to_write -= bytes_written; - chunk_transmitted((int)(total_bytes - bytes_to_write), (int) total_bytes); - } - - yield output.close_async(); - yield input.close_async(); - } -} - internal class Uploader : Publishing.RESTSupport.BatchUploader { private PublishingParameters parameters; - private GData.YouTubeService youtube_service; - public Uploader(GData.YouTubeService youtube_service, Publishing.RESTSupport.GoogleSession session, + public Uploader(Publishing.RESTSupport.GoogleSession session, Spit.Publishing.Publishable[] publishables, PublishingParameters parameters) { base(session, publishables); this.parameters = parameters; - this.youtube_service = youtube_service; } protected override Publishing.RESTSupport.Transaction create_transaction( Spit.Publishing.Publishable publishable) { - return new UploadTransaction(this.youtube_service, (Publishing.RESTSupport.GoogleSession) get_session(), - parameters, get_current_publishable()); + return new UploadTransaction((Publishing.RESTSupport.GoogleSession) get_session(), + parameters, get_current_publishable()); } } diff --git a/plugins/shotwell-publishing/YoutubeUploader.vala b/plugins/shotwell-publishing/YoutubeUploader.vala new file mode 100644 index 0000000..47c6051 --- /dev/null +++ b/plugins/shotwell-publishing/YoutubeUploader.vala @@ -0,0 +1,76 @@ +using Spit; + +internal class Publishing.YouTube.UploadTransaction : Publishing.RESTSupport.GooglePublisher.AuthenticatedTransaction { + private PublishingParameters parameters; + private Publishing.RESTSupport.GoogleSession session; + private Spit.Publishing.Publishable publishable; + + public UploadTransaction(Publishing.RESTSupport.GoogleSession session, + PublishingParameters parameters, Spit.Publishing.Publishable publishable) { + base(session, "https://www.googleapis.com/upload/youtube/v3/videos", + Publishing.RESTSupport.HttpMethod.POST); + assert(session.is_authenticated()); + + this.session = session; + this.parameters = parameters; + this.publishable = publishable; + } + public override async void execute_async() throws Spit.Publishing.PublishingError { + // Collect parameters + + var slug = publishable.get_param_string(Spit.Publishing.Publishable.PARAM_STRING_BASENAME); + // Set title to publishing name, but if that's empty default to filename. + string title = publishable.get_publishing_name(); + if (title == "") { + title = publishable.get_param_string(Spit.Publishing.Publishable.PARAM_STRING_BASENAME); + } + + var builder = new Json.Builder(); + builder.begin_object(); + builder.set_member_name("snippet"); + builder.begin_object(); + builder.set_member_name("description"); + builder.add_string_value(slug); + builder.set_member_name("title"); + builder.add_string_value(title); + builder.end_object(); + builder.set_member_name("status"); + builder.begin_object(); + builder.set_member_name("privacyStatus"); + builder.add_string_value(parameters.get_privacy().to_string()); + builder.end_object(); + builder.end_object(); + + var meta_data = Json.to_string (builder.get_root(), false); + debug ("Parameters: %s", meta_data); + var message_parts = new Soup.Multipart("multipart/related"); + var headers = new Soup.MessageHeaders(Soup.MessageHeadersType.MULTIPART); + var encoding = new GLib.HashTable<string, string>(str_hash, str_equal); + encoding.insert("encoding", "UTF-8"); + headers.set_content_type ("application/json", encoding); + + message_parts.append_part (headers, new Bytes (meta_data.data)); + headers = new Soup.MessageHeaders(Soup.MessageHeadersType.MULTIPART); + headers.set_content_type ("application/octet-stream", null); + headers.append("Content-Transfer-Encoding", "binary"); + + MappedFile? mapped_file = null; + try { + mapped_file = new MappedFile(publishable.get_serialized_file().get_path(), false); + } catch (Error e) { + throw new Spit.Publishing.PublishingError.LOCAL_FILE_ERROR( + _("A temporary file needed for publishing is unavailable")); + } + + + message_parts.append_part (headers, mapped_file.get_bytes()); + + var outbound_message = new Soup.Message.from_multipart (get_endpoint_url() + "?part=" + GLib.Uri.escape_string ("snippet,status"), message_parts); + outbound_message.get_request_headers().append("Authorization", "Bearer " + + session.get_access_token()); + + set_message(outbound_message, mapped_file.get_length() + meta_data.length); + set_is_executed(true); + yield send_async(); + } +} diff --git a/plugins/shotwell-publishing/facebook.png b/plugins/shotwell-publishing/facebook.png Binary files differdeleted file mode 100644 index 384609f..0000000 --- a/plugins/shotwell-publishing/facebook.png +++ /dev/null diff --git a/plugins/shotwell-publishing/facebook_publishing_options_pane.ui b/plugins/shotwell-publishing/facebook_publishing_options_pane.ui deleted file mode 100644 index 4d6b021..0000000 --- a/plugins/shotwell-publishing/facebook_publishing_options_pane.ui +++ /dev/null @@ -1,223 +0,0 @@ -<?xml version="1.0" encoding="UTF-8"?> -<!-- Generated with glade 3.20.0 --> -<interface domain="shotwell"> - <requires lib="gtk+" version="3.14"/> - <object class="GtkBox" id="facebook_pane_box"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="homogeneous">True</property> - <child> - <object class="GtkBox" id="facebook_pane_inner_box"> - <property name="width_request">1</property> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="orientation">vertical</property> - <property name="spacing">16</property> - <child> - <object class="GtkLabel" id="how_to_label"> - <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="margin_top">16</property> - <property name="margin_bottom">16</property> - <property name="label"> (text depends on fb username and is modified in the app - -anything put into this field won't display)</property> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">True</property> - <property name="position">0</property> - </packing> - </child> - <child> - <object class="GtkGrid" id="grid1"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="row_spacing">8</property> - <property name="column_spacing">32</property> - <property name="column_homogeneous">True</property> - <child> - <object class="GtkRadioButton" id="use_existing_radio"> - <property name="label" translatable="yes">Publish to an e_xisting album:</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="use_underline">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">0</property> - </packing> - </child> - <child> - <object class="GtkRadioButton" id="create_new_radio"> - <property name="label" translatable="yes">Create a _new album named:</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_bottom">8</property> - <property name="use_underline">True</property> - <property name="active">True</property> - <property name="draw_indicator">True</property> - </object> - <packing> - <property name="left_attach">0</property> - <property name="top_attach">1</property> - </packing> - </child> - <child> - <object class="GtkEntry" id="new_album_entry"> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="margin_bottom">8</property> - <property name="invisible_char">●</property> - </object> - <packing> - <property name="left_attach">1</property> - <property name="top_attach">1</property> - </packing> - </child> - <child> - <object class="GtkComboBoxText" id="existing_albums_combo"> - <property name="visible">True</property> - <property name="can_focus">False</property> - </object> - <packing> - <property name="left_attach">1</property> - <property name="top_attach">0</property> - </packing> - </child> - <child> - <object class="GtkLabel" id="size_label"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="halign">start</property> - <property name="label" translatable="yes">Upload _size:</property> - <property name="use_underline">True</property> - <property name="mnemonic_widget">resolution_combo</property> - </object> - <packing> - <property name="left_attach">0</property> - <property name="top_attach">3</property> - </packing> - </child> - <child> - <object class="GtkComboBoxText" id="resolution_combo"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="halign">start</property> - </object> - <packing> - <property name="left_attach">1</property> - <property name="top_attach">3</property> - </packing> - </child> - <child> - <object class="GtkLabel" id="visibility_label"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="halign">start</property> - <property name="label" translatable="yes">Videos and new photo albums _visible to:</property> - <property name="use_underline">True</property> - <property name="mnemonic_widget">visibility_combo</property> - </object> - <packing> - <property name="left_attach">0</property> - <property name="top_attach">2</property> - </packing> - </child> - <child> - <object class="GtkComboBoxText" id="visibility_combo"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="halign">start</property> - </object> - <packing> - <property name="left_attach">1</property> - <property name="top_attach">2</property> - </packing> - </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> - <child> - <object class="GtkCheckButton" id="strip_metadata_check"> - <property name="label" translatable="yes">_Remove location, camera, and other identifying information before uploading</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="use_underline">True</property> - <property name="draw_indicator">True</property> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">True</property> - <property name="position">2</property> - </packing> - </child> - <child> - <object class="GtkBox" id="box2"> - <property name="visible">True</property> - <property name="can_focus">False</property> - <property name="spacing">32</property> - <property name="homogeneous">True</property> - <child> - <object class="GtkButton" id="logout_button"> - <property name="label" translatable="yes">_Logout</property> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="receives_default">True</property> - <property name="use_underline">True</property> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">True</property> - <property name="padding">80</property> - <property name="position">0</property> - </packing> - </child> - <child> - <object class="GtkButton" id="publish_button"> - <property name="label" translatable="yes">_Publish</property> - <property name="visible">True</property> - <property name="can_focus">True</property> - <property name="receives_default">True</property> - <property name="use_underline">True</property> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">True</property> - <property name="padding">80</property> - <property name="position">1</property> - </packing> - </child> - </object> - <packing> - <property name="expand">False</property> - <property name="fill">True</property> - <property name="padding">2</property> - <property name="position">3</property> - </packing> - </child> - </object> - <packing> - <property name="expand">True</property> - <property name="fill">True</property> - <property name="padding">8</property> - <property name="pack_type">end</property> - <property name="position">0</property> - </packing> - </child> - </object> -</interface> diff --git a/plugins/shotwell-publishing/flickr_publishing_options_pane.ui b/plugins/shotwell-publishing/flickr_publishing_options_pane.ui index 77d32cf..e5e61aa 100644 --- a/plugins/shotwell-publishing/flickr_publishing_options_pane.ui +++ b/plugins/shotwell-publishing/flickr_publishing_options_pane.ui @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="UTF-8"?> -<!-- Generated with glade 3.20.0 --> +<!-- Generated with glade 3.22.0 --> <interface domain="shotwell"> <requires lib="gtk+" version="3.14"/> <object class="GtkBox" id="flickr_pane"> @@ -36,8 +36,8 @@ so changes made here will not display)</property> <property name="halign">center</property> <property name="valign">center</property> <property name="margin_bottom">8</property> - <property name="row_spacing">8</property> - <property name="column_spacing">24</property> + <property name="row_spacing">6</property> + <property name="column_spacing">12</property> <child> <object class="GtkLabel" id="visibility_label"> <property name="visible">True</property> @@ -58,7 +58,7 @@ so changes made here will not display)</property> <property name="visible">True</property> <property name="can_focus">False</property> <property name="halign">end</property> - <property name="label" translatable="yes">Photo _size:</property> + <property name="label" translatable="yes">Photo _size</property> <property name="use_underline">True</property> <property name="mnemonic_widget">size_combo</property> </object> diff --git a/plugins/shotwell-publishing/meson.build b/plugins/shotwell-publishing/meson.build index eeac177..a93726b 100644 --- a/plugins/shotwell-publishing/meson.build +++ b/plugins/shotwell-publishing/meson.build @@ -1,9 +1,9 @@ shotwell_publishing_sources = [ 'shotwell-publishing.vala', - 'FacebookPublishing.vala', 'FlickrPublishing.vala', 'TumblrPublishing.vala', 'YouTubePublishing.vala', + 'YoutubeUploader.vala', 'PiwigoPublishing.vala', 'PhotosPublisher.vala', 'PhotosService.vala', @@ -13,13 +13,13 @@ shotwell_publishing_sources = [ shotwell_publishing_resources = gnome.compile_resources('publishing-resource', 'org.gnome.Shotwell.Publishing.gresource.xml', - source_dir : meson.source_root()) + source_dir : meson.project_source_root()) shared_module('shotwell-publishing', shotwell_publishing_sources + shotwell_publishing_resources, dependencies : [gtk, soup, gexiv2, gee, sw_plugin, json_glib, - webkit, sw_plugin_common_dep, xml, gdata, gcr, - gcr_ui, authenticator_dep], + webkit, sw_plugin_common_dep, xml, gcr, + gcr_ui, authenticator_dep, secret], c_args : ['-DPLUGIN_RESOURCE_PATH="/org/gnome/Shotwell/Publishing"', '-DGCR_API_SUBJECT_TO_CHANGE'], install: true, diff --git a/plugins/shotwell-publishing/org.gnome.Shotwell.Publishing.gresource.xml b/plugins/shotwell-publishing/org.gnome.Shotwell.Publishing.gresource.xml index e04ab02..5272df6 100644 --- a/plugins/shotwell-publishing/org.gnome.Shotwell.Publishing.gresource.xml +++ b/plugins/shotwell-publishing/org.gnome.Shotwell.Publishing.gresource.xml @@ -1,12 +1,12 @@ <?xml version="1.0" encoding="UTF-8"?> <gresources> <gresource prefix="/org/gnome/Shotwell/Publishing"> - <file>facebook.png</file> - <file>piwigo.svg</file> - <file>youtube.png</file> - <file>tumblr.png</file> - <file>google-photos.svg</file> - <file>facebook_publishing_options_pane.ui</file> + <!-- icons, with additional aliases into the icon namespace --> + <file alias="icons/hicolor/scalable/apps/piwigo.svg">piwigo.svg</file> + <file alias="icons/hicolor/24x24/apps/youtube.png">youtube.png</file> + <file alias="icons/hicolor/scalable/apps/google-photos.svg">google-photos.svg</file> + <file alias="icons/hicolor/scalable/apps/tumblr.svg">tumblr.svg</file> + <file>flickr_publishing_options_pane.ui</file> <file>google_photos_publishing_options_pane.ui</file> <file>piwigo_authentication_pane.ui</file> diff --git a/plugins/shotwell-publishing/piwigo_publishing_options_pane.ui b/plugins/shotwell-publishing/piwigo_publishing_options_pane.ui index 397d2ec..c17d616 100644 --- a/plugins/shotwell-publishing/piwigo_publishing_options_pane.ui +++ b/plugins/shotwell-publishing/piwigo_publishing_options_pane.ui @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="UTF-8"?> -<!-- Generated with glade 3.20.0 --> +<!-- Generated with glade 3.22.0 --> <interface domain="shotwell"> <requires lib="gtk+" version="3.14"/> <object class="GtkWindow" id="publishing_options_pane"> @@ -8,8 +8,8 @@ <object class="GtkBox" id="content"> <property name="visible">True</property> <property name="can_focus">False</property> - <property name="margin_left">10</property> - <property name="margin_right">10</property> + <property name="margin_start">10</property> + <property name="margin_end">10</property> <property name="orientation">vertical</property> <property name="spacing">12</property> <child> @@ -247,6 +247,22 @@ </packing> </child> <child> + <object class="GtkCheckButton" id="no_upload_ratings_check"> + <property name="label" translatable="yes">_Do not upload ratings</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="use_underline">True</property> + <property name="draw_indicator">True</property> + </object> + <packing> + <property name="expand">False</property> + <property name="fill">True</property> + <property name="position">5</property> + </packing> + </child> + <child> <object class="GtkButtonBox" id="hbuttonbox1"> <property name="visible">True</property> <property name="can_focus">False</property> @@ -280,10 +296,13 @@ <packing> <property name="expand">True</property> <property name="fill">True</property> - <property name="position">5</property> + <property name="position">6</property> </packing> </child> </object> </child> + <child type="titlebar"> + <placeholder/> + </child> </object> </interface> diff --git a/plugins/shotwell-publishing/shotwell-publishing.vala b/plugins/shotwell-publishing/shotwell-publishing.vala index 52c82ec..f5251f4 100644 --- a/plugins/shotwell-publishing/shotwell-publishing.vala +++ b/plugins/shotwell-publishing/shotwell-publishing.vala @@ -13,45 +13,37 @@ private class ShotwellPublishingCoreServices : Object, Spit.Module { // we need to get a module file handle because our pluggables have to load resources from the // module file directory public ShotwellPublishingCoreServices(GLib.File module_file) { - GLib.File resource_directory = module_file.get_parent(); var factory = Publishing.Authenticator.Factory.get_instance(); var authenicators = factory.get_available_authenticators(); // Prevent vala complaining when all authenticators from this plugin // are disabled - debug("Looking for resources in %s", resource_directory.get_path()); debug("Found %d authenicators", authenicators.size); -#if HAVE_FACEBOOK - if (authenicators.contains("facebook")) { - pluggables += new FacebookService(resource_directory); - } -#endif - #if HAVE_GOOGLEPHOTOS if (authenicators.contains("google-photos")) { - pluggables += new Publishing.GooglePhotos.Service(resource_directory); + pluggables += new Publishing.GooglePhotos.Service(); } #endif #if HAVE_FLICKR if (authenicators.contains("flickr")) { - pluggables += new FlickrService(resource_directory); + pluggables += new FlickrService(); } #endif #if HAVE_YOUTUBE if (authenicators.contains("youtube")) { - pluggables += new YouTubeService(resource_directory); + pluggables += new YouTubeService(); } #endif #if HAVE_PIWIGO - pluggables += new PiwigoService(resource_directory); + pluggables += new PiwigoService(); #endif #if HAVE_TUMBLR - pluggables += new TumblrService(module_file.get_parent()); + pluggables += new TumblrService(); #endif } @@ -64,7 +56,7 @@ private class ShotwellPublishingCoreServices : Object, Spit.Module { } public unowned string get_id() { - return "org.yorba.shotwell.publishing.core_services"; + return "org.gnome.shotwell.publishing.core_services"; } public unowned Spit.Pluggable[]? get_pluggables() { diff --git a/plugins/shotwell-publishing/tumblr.svg b/plugins/shotwell-publishing/tumblr.svg new file mode 100644 index 0000000..d245dd1 --- /dev/null +++ b/plugins/shotwell-publishing/tumblr.svg @@ -0,0 +1,245 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> + +<svg + version="1.1" + id="Layer_1" + x="0px" + y="0px" + viewBox="0 0 159.30001 159.3" + xml:space="preserve" + sodipodi:docname="tumblr.svg" + width="159.3" + height="159.3" + inkscape:version="1.2.1 (9c6d41e410, 2022-07-14)" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"><defs + id="defs341" /><sodipodi:namedview + id="namedview339" + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1.0" + inkscape:showpageshadow="2" + inkscape:pageopacity="0.0" + inkscape:pagecheckerboard="0" + inkscape:deskcolor="#d1d1d1" + showgrid="false" + inkscape:zoom="4.9089767" + inkscape:cx="12.120652" + inkscape:cy="79.650002" + inkscape:window-width="1920" + inkscape:window-height="1011" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="1" + inkscape:current-layer="Layer_1" /> +<style + type="text/css" + id="style248"> + .st0{fill:#FFFFFF;} + .st1{fill:#001935;} + .st2{fill:#231F20;} +</style> +<text + class="st0" + style="font-size:45.8156px;font-family:MyriadPro-Regular" + id="text250" + x="-696.69513" + y="-356.28101">Black #000000</text> +<text + id="text260" + x="-1587.3539" + y="-356.28101"><tspan + x="-1587.3539" + y="-356.28101" + class="st0" + style="font-size:45.8156px;font-family:MyriadPro-Regular" + id="tspan252">Notes:</tspan><tspan + x="-1587.3539" + y="-246.28101" + class="st0" + style="font-size:45.8156px;font-family:MyriadPro-Regular" + id="tspan254">Always use the full wordmark</tspan><tspan + x="-1587.3539" + y="-191.38101" + class="st0" + style="font-size:45.8156px;font-family:MyriadPro-Regular" + id="tspan256">unless you’re explicitly speaking to </tspan><tspan + x="-1587.3539" + y="-136.38101" + class="st0" + style="font-size:45.8156px;font-family:MyriadPro-Regular" + id="tspan258">or marketing the iOS or Android app </tspan></text> +<text + class="st0" + style="font-size:21.8043px;font-family:MyriadPro-Regular" + id="text262" + x="-691.97113" + y="189.9763">iOS icon</text> +<text + class="st0" + style="font-size:21.8043px;font-family:MyriadPro-Regular" + id="text264" + x="-696.69513" + y="-111.0251">Wordmark (always use full word)</text> +<text + class="st0" + style="font-size:21.8043px;font-family:MyriadPro-Regular" + id="text266" + x="-369.3826" + y="189.9763">Android Icon</text> +<text + class="st0" + style="font-size:21.8043px;font-family:MyriadPro-Regular" + id="text268" + x="-0.00025388421" + y="189.9763">t icon</text> +<text + class="st0" + style="font-size:45.8156px;font-family:MyriadPro-Regular" + id="text270" + x="-696.69513" + y="363.68301">White #000000</text> +<text + class="st0" + style="font-size:21.8043px;font-family:MyriadPro-Regular" + id="text272" + x="-691.97113" + y="909.94043">iOS icon</text> +<text + class="st0" + style="font-size:21.8043px;font-family:MyriadPro-Regular" + id="text274" + x="-696.69513" + y="608.9389">Wordmark (always use full word)</text> +<text + class="st0" + style="font-size:21.8043px;font-family:MyriadPro-Regular" + id="text276" + x="-369.3826" + y="909.94043">Android Icon</text> +<text + class="st0" + style="font-size:21.8043px;font-family:MyriadPro-Regular" + id="text278" + x="-0.00025388421" + y="909.94043">t icon</text> +<text + class="st0" + style="font-size:45.8156px;font-family:MyriadPro-Regular" + id="text280" + x="239.25819" + y="-356.28101">Blue #001935</text> +<text + class="st0" + style="font-size:21.8043px;font-family:MyriadPro-Regular" + id="text282" + x="243.98219" + y="189.9763">iOS icon</text> +<text + class="st0" + style="font-size:21.8043px;font-family:MyriadPro-Regular" + id="text284" + x="239.25819" + y="-111.0251">Wordmark (always use full word)</text> +<text + class="st0" + style="font-size:21.8043px;font-family:MyriadPro-Regular" + id="text286" + x="566.57068" + y="189.9763">Android Icon</text> +<text + class="st0" + style="font-size:21.8043px;font-family:MyriadPro-Regular" + id="text288" + x="935.953" + y="189.9763">t icon</text> +<path + class="st1" + d="m 302.9,-147.5 c -24,0 -41.8,-12.3 -41.8,-41.8 v -47.2 h -21.8 v -25.6 c 24,-6.2 34,-26.8 35.1,-44.7 h 24.9 v 40.6 h 29 v 29.7 h -29 v 41.1 c 0,12.3 6.2,16.6 16.1,16.6 h 14.1 v 31.3 z m 89,1.4 c 13.6,0 29.3,-5.9 37.8,-16.4 v 15 h 44.8 V -173 h -11.4 v -93.3 H 409 v 25.9 h 15.4 v 44.9 c 0,3.6 -3,18.7 -23.3,18.7 -13,0 -15.4,-7.8 -15.4,-17.7 v -71.8 h -39.9 v 73.4 c 0,18.3 8.3,46.8 46.1,46.8 z m 96.9,-1.4 h 65.6 V -173 H 541 v -44.4 c 0,-6.5 5.5,-19.7 22.4,-19.7 13.6,0 16.4,8.3 16.4,18.1 v 46 h -13.2 v 25.5 h 65.1 V -173 H 618 v -44.4 c 0,-6.5 5,-19.7 21.9,-19.7 13.7,0 17.1,8.3 17.1,18.1 v 46 h -13 v 25.5 h 64.8 V -173 h -13.3 v -41.5 c 0,-24.4 -9.2,-53.2 -45.6,-53.2 -18,0 -32.7,8.5 -38.9,19 -7.8,-11.9 -19.7,-19 -37.4,-19 -14.1,0 -29.5,6 -37.8,17.4 v -16 h -49.2 v 25.9 h 15.9 V -173 H 489 v 25.5 z m 303,1.4 c 33.2,0 51,-24.9 51,-62.2 0,-35.8 -16.6,-59.3 -50.3,-59.3 -11.9,0 -23.7,4.5 -30.9,10 v -49.2 h -54.8 v 25.7 h 15.9 v 133.6 h 36.5 V -158 c 9.4,7.8 20.5,11.9 32.6,11.9 z m -8.3,-29 c -11.6,0 -23,-7.4 -23,-30.6 0,-28 13.5,-33 22.8,-33 10.8,0 20.4,8.8 20.4,30.5 0,31.4 -14.1,32.4 -20.2,33.1 z m 66.5,27.6 h 66 V -173 h -14 v -133.9 h -54.4 v 25.7 h 15.9 V -173 H 850 Z m 80.4,0 h 64.9 V -173 H 982 v -33.2 c 0,-22.6 15.9,-27.5 28,-27.5 h 16.1 v -34 h -13 c -14.7,0 -28.3,7.1 -34.4,18.5 v -17.2 h -49.4 v 25.9 h 14.3 v 67.4 h -13.1 v 25.6 z" + id="path290" /> +<path + class="st1" + d="m 999.6,159.3 c -24,0 -41.8,-12.3 -41.8,-41.8 V 70.3 H 936 V 44.7 C 960,38.5 970,17.9 971.1,0 H 996 v 40.6 h 29 v 29.7 h -29 v 41.1 c 0,12.3 6.2,16.6 16.1,16.6 h 14.1 v 31.3 z" + id="path292" /> +<g + id="g298"> + <path + class="st1" + d="m 645.1,1.3 c -43.4,0 -78.6,35.1 -78.6,78.3 0,43.2 35.1,78.5 78.6,78.5 43.2,0 78.3,-35.3 78.3,-78.5 C 723.5,36.2 688.3,1.3 645.1,1.3 Z" + id="path294" /> + <path + class="st0" + d="m 651.2,97.3 c 0,7.1 3.6,9.5 9.2,9.5 h 8.1 v 18 h -15.3 c -13.7,0 -24,-7.1 -24,-24 V 73.7 H 616.7 V 59 c 13.7,-3.6 19.5,-15.4 20.2,-25.6 h 14.3 v 23.3 h 16.6 v 17 h -16.6 v 23.6" + id="path296" /> +</g> +<g + id="g304"> + <path + class="st1" + d="m 359.9,7.3 h -87 C 257,7.3 244,20.3 244,36.2 v 87 c 0,15.9 13,28.9 28.9,28.9 h 87 c 15.9,0 28.9,-13 28.9,-28.9 v -87 c 0,-15.9 -13,-28.9 -28.9,-28.9 z" + id="path300" /> + <path + class="st0" + d="M 341.3,124.8 H 326 c -13.7,0 -24,-7.1 -24,-24 V 73.7 H 289.5 V 59 C 303.2,55.4 309,43.6 309.7,33.4 H 324 v 23.3 h 16.6 v 17 H 324 v 23.6 c 0,7.1 3.6,9.5 9.2,9.5 h 8.1 z" + id="path302" /> +</g> +<path + class="st0" + d="m -633.1,573.9 c -24,0 -41.8,-12.3 -41.8,-41.8 v -47.2 h -21.8 v -25.6 c 24,-6.2 34,-26.8 35.1,-44.7 h 24.9 v 40.6 h 29 v 29.7 h -29 V 526 c 0,12.3 6.2,16.6 16.1,16.6 h 14.1 v 31.3 z m 89.1,1.4 c 13.6,0 29.3,-5.9 37.8,-16.4 v 15 h 44.8 v -25.5 h -11.4 V 455.1 H -527 V 481 h 15.4 v 45 c 0,3.6 -3,18.7 -23.3,18.7 -13,0 -15.4,-7.8 -15.4,-17.7 v -71.8 h -39.9 v 73.4 c 0.1,18.3 8.4,46.7 46.2,46.7 z m 96.9,-1.4 h 65.6 v -25.5 h -13.4 V 504 c 0,-6.5 5.5,-19.7 22.4,-19.7 13.6,0 16.4,8.3 16.4,18.1 v 46 h -13.2 v 25.5 h 65.1 V 548.4 H -318 V 504 c 0,-6.5 5,-19.7 21.9,-19.7 13.7,0 17.1,8.3 17.1,18.1 v 46 h -13 v 25.5 h 64.8 v -25.5 h -13.3 V 507 c 0,-24.4 -9.2,-53.2 -45.6,-53.2 -18,0 -32.7,8.5 -38.9,19 -7.8,-11.9 -19.7,-19 -37.4,-19 -14.1,0 -29.5,6 -37.8,17.4 v -16 h -49.2 v 25.9 h 15.9 v 67.4 H -447 v 25.4 z m 303,1.4 c 33.2,0 51,-24.9 51,-62.2 0,-35.8 -16.6,-59.3 -50.3,-59.3 -11.9,0 -23.7,4.5 -30.9,10 v -49.2 h -54.8 v 25.7 h 15.9 v 133.6 h 36.5 v -10.5 c 9.3,7.9 20.5,11.9 32.6,11.9 z m -8.3,-28.9 c -11.6,0 -23,-7.4 -23,-30.6 0,-28 13.5,-33 22.8,-33 10.8,0 20.4,8.8 20.4,30.5 -0.1,31.4 -14.1,32.4 -20.2,33.1 z m 66.5,27.5 h 66 V 548.4 H -34 V 414.6 h -54.4 v 25.7 h 15.9 V 548.5 H -86 v 25.4 z m 80.4,0 H 59.4 V 548.4 H 46.1 v -33.2 c 0,-22.6 15.9,-27.5 28,-27.5 h 16.1 v -34 h -13 c -14.7,0 -28.3,7.1 -34.4,18.5 V 455 H -6.7 v 25.9 H 7.6 v 67.4 H -5.5 Z" + id="path306" /> +<path + class="st0" + d="m 63.6,880.8 c -24,0 -41.8,-12.3 -41.8,-41.8 V 791.8 H 0 V 766.2 C 24,760 34,739.4 35.1,721.5 H 60 V 762 h 29 v 29.7 H 60 v 41.1 c 0,12.3 6.2,16.6 16.1,16.6 h 14.1 v 31.3 H 63.6 Z" + id="path308" /> +<g + id="g314"> + <path + class="st0" + d="m -290.8,722.7 c -43.4,0 -78.6,35.1 -78.6,78.3 0,43.2 35.1,78.5 78.6,78.5 43.2,0 78.3,-35.3 78.3,-78.5 0,-43.4 -35.1,-78.3 -78.3,-78.3 z" + id="path310" /> + <path + class="st2" + d="m -284.7,818.8 c 0,7.1 3.6,9.5 9.2,9.5 h 8.1 v 18 h -15.3 c -13.7,0 -24,-7.1 -24,-24 v -27.1 h -12.5 v -14.7 c 13.7,-3.6 19.5,-15.4 20.2,-25.6 h 14.3 v 23.3 h 16.6 v 17 h -16.6 v 23.6" + id="path312" /> +</g> +<g + id="g320"> + <path + class="st0" + d="m -576.1,728.7 h -87 c -15.9,0 -28.9,13 -28.9,28.9 v 87 c 0,15.9 13,28.9 28.9,28.9 h 87 c 15.9,0 28.9,-13 28.9,-28.9 v -87 c 0.1,-15.9 -13,-28.9 -28.9,-28.9 z" + id="path316" /> + <path + class="st2" + d="m -594.6,846.2 h -15.3 c -13.7,0 -24,-7.1 -24,-24 v -27.1 h -12.5 v -14.7 c 13.7,-3.6 19.5,-15.4 20.2,-25.6 h 14.3 v 23.3 h 16.6 v 17 h -16.6 v 23.6 c 0,7.1 3.6,9.5 9.2,9.5 h 8.1 z" + id="path318" /> +</g> +<path + d="m -633.1,-147.5 c -24,0 -41.8,-12.3 -41.8,-41.8 v -47.2 h -21.8 v -25.6 c 24,-6.2 34,-26.8 35.1,-44.7 h 24.9 v 40.6 h 29 v 29.7 h -29 v 41.1 c 0,12.3 6.2,16.6 16.1,16.6 h 14.1 v 31.3 z m 89.1,1.4 c 13.6,0 29.3,-5.9 37.8,-16.4 v 15 h 44.8 V -173 h -11.4 v -93.3 H -527 v 25.9 h 15.4 v 44.9 c 0,3.6 -3,18.7 -23.3,18.7 -13,0 -15.4,-7.8 -15.4,-17.7 v -71.8 h -39.9 v 73.4 c 0.1,18.3 8.4,46.8 46.2,46.8 z m 96.9,-1.4 h 65.6 V -173 h -13.4 v -44.4 c 0,-6.5 5.5,-19.7 22.4,-19.7 13.6,0 16.4,8.3 16.4,18.1 v 46 h -13.2 v 25.5 h 65.1 V -173 H -318 v -44.4 c 0,-6.5 5,-19.7 21.9,-19.7 13.7,0 17.1,8.3 17.1,18.1 v 46 h -13 v 25.5 h 64.8 V -173 h -13.3 v -41.5 c 0,-24.4 -9.2,-53.2 -45.6,-53.2 -18,0 -32.7,8.5 -38.9,19 -7.8,-11.9 -19.7,-19 -37.4,-19 -14.1,0 -29.5,6 -37.8,17.4 v -16 h -49.2 v 25.9 h 15.9 V -173 H -447 v 25.5 z m 303,1.4 c 33.2,0 51,-24.9 51,-62.2 0,-35.8 -16.6,-59.3 -50.3,-59.3 -11.9,0 -23.7,4.5 -30.9,10 v -49.2 h -54.8 v 25.7 h 15.9 v 133.6 h 36.5 V -158 c 9.3,7.8 20.5,11.9 32.6,11.9 z m -8.3,-29 c -11.6,0 -23,-7.4 -23,-30.6 0,-28 13.5,-33 22.8,-33 10.8,0 20.4,8.8 20.4,30.5 -0.1,31.4 -14.1,32.4 -20.2,33.1 z m 66.5,27.6 h 66 V -173 H -34 v -133.9 h -54.4 v 25.7 h 15.9 V -173 H -86 v 25.5 z m 80.4,0 H 59.4 V -173 H 46.1 v -33.2 c 0,-22.6 15.9,-27.5 28,-27.5 h 16.1 v -34 h -13 c -14.7,0 -28.3,7.1 -34.4,18.5 v -17.2 H -6.7 v 25.9 H 7.6 v 67.4 H -5.5 Z" + id="path322" /> +<path + d="m 98.150002,159.3 c -24,0 -41.8,-12.3 -41.8,-41.8 V 70.3 h -21.8 V 44.7 c 24,-6.2 34,-26.8 35.1,-44.7 h 24.9 V 40.6 H 123.55 V 70.3 H 94.550002 v 41.1 c 0,12.3 6.199998,16.6 16.099998,16.6 h 14.1 v 31.3 z" + id="path324" /> +<g + id="g330"> + <path + d="m -290.8,1.3 c -43.4,0 -78.6,35.1 -78.6,78.3 0,43.2 35.1,78.5 78.6,78.5 43.2,0 78.3,-35.3 78.3,-78.5 0,-43.4 -35.1,-78.3 -78.3,-78.3 z" + id="path326" /> + <path + class="st0" + d="m -284.7,97.3 c 0,7.1 3.6,9.5 9.2,9.5 h 8.1 v 18 h -15.3 c -13.7,0 -24,-7.1 -24,-24 V 73.7 h -12.5 V 59 c 13.7,-3.6 19.5,-15.4 20.2,-25.6 h 14.3 v 23.3 h 16.6 v 17 h -16.6 v 23.6" + id="path328" /> +</g> +<g + id="g336"> + <path + d="m -576.1,7.3 h -87 c -15.9,0 -28.9,13 -28.9,28.9 v 87 c 0,15.9 13,28.9 28.9,28.9 h 87 c 15.9,0 28.9,-13 28.9,-28.9 v -87 c 0.1,-15.9 -13,-28.9 -28.9,-28.9 z" + id="path332" /> + <path + class="st0" + d="m -594.6,124.8 h -15.3 c -13.7,0 -24,-7.1 -24,-24 V 73.7 h -12.5 V 59 c 13.7,-3.6 19.5,-15.4 20.2,-25.6 h 14.3 v 23.3 h 16.6 v 17 h -16.6 v 23.6 c 0,7.1 3.6,9.5 9.2,9.5 h 8.1 z" + id="path334" /> +</g> +</svg> diff --git a/plugins/shotwell-publishing/tumblr_publishing_options_pane.ui b/plugins/shotwell-publishing/tumblr_publishing_options_pane.ui index acc5459..982dc26 100644 --- a/plugins/shotwell-publishing/tumblr_publishing_options_pane.ui +++ b/plugins/shotwell-publishing/tumblr_publishing_options_pane.ui @@ -1,5 +1,5 @@ <?xml version="1.0" encoding="UTF-8"?> -<!-- Generated with glade 3.20.0 --> +<!-- Generated with glade 3.22.0 --> <interface domain="shotwell"> <requires lib="gtk+" version="3.14"/> <object class="GtkBox" id="tumblr_pane"> @@ -35,15 +35,16 @@ so changes made here will not display)</property> <property name="can_focus">False</property> <property name="halign">center</property> <property name="valign">center</property> - <property name="row_spacing">8</property> - <property name="column_spacing">24</property> + <property name="row_spacing">6</property> + <property name="column_spacing">12</property> <child> <object class="GtkLabel" id="blog_label"> <property name="visible">True</property> <property name="can_focus">False</property> - <property name="label" translatable="yes">Blogs:</property> + <property name="halign">end</property> + <property name="label" translatable="yes">Blogs</property> <property name="use_underline">True</property> - <property name="justify">right</property> + <property name="justify">center</property> <property name="mnemonic_widget">blog_combo</property> </object> <packing> @@ -56,7 +57,7 @@ so changes made here will not display)</property> <property name="visible">True</property> <property name="can_focus">False</property> <property name="halign">end</property> - <property name="label" translatable="yes">Photo _size:</property> + <property name="label" translatable="yes">Photo _size</property> <property name="use_underline">True</property> <property name="mnemonic_widget">size_combo</property> </object> diff --git a/plugins/shotwell-publishing/youtube_publishing_options_pane.ui b/plugins/shotwell-publishing/youtube_publishing_options_pane.ui index 47f75a1..777632e 100644 --- a/plugins/shotwell-publishing/youtube_publishing_options_pane.ui +++ b/plugins/shotwell-publishing/youtube_publishing_options_pane.ui @@ -27,18 +27,17 @@ <object class="GtkBox" id="box1"> <property name="visible">True</property> <property name="can_focus">False</property> - <property name="margin_left">40</property> - <property name="margin_right">40</property> + <property name="margin_start">40</property> + <property name="margin_end">40</property> <property name="margin_top">16</property> + <property name="spacing">6</property> <property name="homogeneous">True</property> <child> <object class="GtkLabel" id="privacy_label"> <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="xpad">10</property> - <property name="label" translatable="yes">Video privacy _setting:</property> + <property name="halign">end</property> + <property name="label" translatable="yes">Video privacy _setting</property> <property name="use_underline">True</property> </object> <packing> @@ -51,10 +50,6 @@ <object class="GtkComboBoxText" id="privacy_combo"> <property name="visible">True</property> <property name="can_focus">False</property> - <property name="margin_left">16</property> - <property name="margin_right">32</property> - <property name="entry_text_column">0</property> - <property name="id_column">1</property> </object> <packing> <property name="expand">False</property> @@ -74,8 +69,8 @@ <object class="GtkBox" id="button_area_box"> <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_start">112</property> + <property name="margin_end">112</property> <property name="margin_top">48</property> <property name="margin_bottom">24</property> <property name="spacing">128</property> @@ -86,7 +81,6 @@ <property name="visible">True</property> <property name="can_focus">True</property> <property name="receives_default">True</property> - <property name="use_action_appearance">False</property> <property name="use_underline">True</property> </object> <packing> @@ -101,7 +95,6 @@ <property name="visible">True</property> <property name="can_focus">True</property> <property name="receives_default">True</property> - <property name="use_action_appearance">False</property> <property name="use_underline">True</property> </object> <packing> |