From 4ea2cc3bd4a7d9b1c54a9d33e6a1cf82e7c8c21d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=B6rg=20Frings-F=C3=BCrst?= Date: Wed, 23 Jul 2014 09:06:59 +0200 Subject: Imported Upstream version 0.18.1 --- src/publishing/APIGlue.vala | 133 ++++++++ src/publishing/Publishing.vala | 24 ++ src/publishing/PublishingPluginHost.vala | 238 +++++++++++++ src/publishing/PublishingUI.vala | 552 +++++++++++++++++++++++++++++++ src/publishing/mk/publishing.mk | 31 ++ 5 files changed, 978 insertions(+) create mode 100644 src/publishing/APIGlue.vala create mode 100644 src/publishing/Publishing.vala create mode 100644 src/publishing/PublishingPluginHost.vala create mode 100644 src/publishing/PublishingUI.vala create mode 100644 src/publishing/mk/publishing.mk (limited to 'src/publishing') diff --git a/src/publishing/APIGlue.vala b/src/publishing/APIGlue.vala new file mode 100644 index 0000000..f282c1f --- /dev/null +++ b/src/publishing/APIGlue.vala @@ -0,0 +1,133 @@ +/* Copyright 2011-2014 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ +namespace Publishing.Glue { + +public class MediaSourcePublishableWrapper : Spit.Publishing.Publishable, GLib.Object { + private static int name_ticker = 0; + + private MediaSource wrapped; + private GLib.File? serialized_file = null; + private Gee.Map param_string = new Gee.HashMap(); + + public MediaSourcePublishableWrapper(MediaSource to_wrap) { + wrapped = to_wrap; + setup_parameters(); + } + + public void clean_up() { + if (serialized_file == null) + return; + + debug("cleaning up temporary publishing file '%s'.", serialized_file.get_path()); + + try { + serialized_file.delete(null); + } catch (Error err) { + warning("couldn't delete temporary publishing file '%s'.", serialized_file.get_path()); + } + + serialized_file = null; + } + + private void setup_parameters() { + param_string.set(PARAM_STRING_BASENAME, wrapped.get_basename()); + param_string.set(PARAM_STRING_TITLE, wrapped.get_title()); + param_string.set(PARAM_STRING_COMMENT, wrapped.get_comment()); + + if (wrapped.get_event() != null) + param_string.set(PARAM_STRING_EVENTCOMMENT, wrapped.get_event().get_comment()); + else + param_string.set(PARAM_STRING_EVENTCOMMENT, ""); + } + + public GLib.File serialize_for_publishing(int content_major_axis, + bool strip_metadata = false) throws Spit.Publishing.PublishingError { + + if (wrapped is LibraryPhoto) { + LibraryPhoto photo = (LibraryPhoto) wrapped; + + GLib.File to_file = + AppDirs.get_temp_dir().get_child("publishing-%d.jpg".printf(name_ticker++)); + + debug("writing photo '%s' to temporary file '%s' for publishing.", + photo.get_source_id(), to_file.get_path()); + try { + Scaling scaling = (content_major_axis > 0) ? + Scaling.for_best_fit(content_major_axis, false) : Scaling.for_original(); + photo.export(to_file, scaling, Jpeg.Quality.HIGH, PhotoFileFormat.JFIF, false, !strip_metadata); + } catch (Error err) { + throw new Spit.Publishing.PublishingError.LOCAL_FILE_ERROR( + "unable to serialize photo '%s' for publishing.", photo.get_name()); + } + + serialized_file = to_file; + } else if (wrapped is Video) { + Video video = (Video) wrapped; + + string basename; + string extension; + disassemble_filename(video.get_file().get_basename(), out basename, out extension); + + GLib.File to_file = + GLib.File.new_for_path("publishing-%d.%s".printf(name_ticker++, extension)); + + debug("writing video '%s' to temporary file '%s' for publishing.", + video.get_source_id(), to_file.get_path()); + try { + video.export(to_file); + } catch (Error err) { + throw new Spit.Publishing.PublishingError.LOCAL_FILE_ERROR( + "unable to serialize video '%s' for publishing.", video.get_name()); + } + + serialized_file = to_file; + } else { + error("MediaSourcePublishableWrapper.serialize_for_publishing( ): unknown media type."); + } + + return serialized_file; + } + + public string get_publishing_name() { + return wrapped.get_title() != null ? wrapped.get_title() : ""; + } + + public string? get_param_string(string name) { + return param_string.get(name); + } + + public string[] get_publishing_keywords() { + string[] result = new string[0]; + + Gee.Collection? tagset = Tag.global.fetch_sorted_for_source(wrapped); + if (tagset != null) { + foreach (Tag tag in tagset) { + result += tag.get_name(); + } + } + + return (result.length > 0) ? result : null; + } + + public Spit.Publishing.Publisher.MediaType get_media_type() { + if (wrapped is LibraryPhoto) + return Spit.Publishing.Publisher.MediaType.PHOTO; + else if (wrapped is Video) + return Spit.Publishing.Publisher.MediaType.VIDEO; + else + return Spit.Publishing.Publisher.MediaType.NONE; + } + + public GLib.File? get_serialized_file() { + return serialized_file; + } + + public GLib.DateTime get_exposure_date_time() { + return new GLib.DateTime.from_unix_local(wrapped.get_exposure_time()); + } +} + +} diff --git a/src/publishing/Publishing.vala b/src/publishing/Publishing.vala new file mode 100644 index 0000000..fb04eab --- /dev/null +++ b/src/publishing/Publishing.vala @@ -0,0 +1,24 @@ +/* Copyright 2011-2014 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +namespace Publishing { + +public void init() throws Error { + string[] core_ids = new string[0]; + core_ids += "org.yorba.shotwell.publishing.facebook"; + core_ids += "org.yorba.shotwell.publishing.picasa"; + core_ids += "org.yorba.shotwell.publishing.flickr"; + core_ids += "org.yorba.shotwell.publishing.youtube"; + + Plugins.register_extension_point(typeof(Spit.Publishing.Service), _("Publishing"), + Resources.PUBLISH, core_ids); +} + +public void terminate() { +} + +} + diff --git a/src/publishing/PublishingPluginHost.vala b/src/publishing/PublishingPluginHost.vala new file mode 100644 index 0000000..1a5ed86 --- /dev/null +++ b/src/publishing/PublishingPluginHost.vala @@ -0,0 +1,238 @@ +/* Copyright 2011-2014 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +namespace Spit.Publishing { + +public class ConcretePublishingHost : Plugins.StandardHostInterface, + Spit.Publishing.PluginHost { + private const string PREPARE_STATUS_DESCRIPTION = _("Preparing for upload"); + private const string UPLOAD_STATUS_DESCRIPTION = _("Uploading %d of %d"); + private const double STATUS_PREPARATION_FRACTION = 0.3; + private const double STATUS_UPLOAD_FRACTION = 0.7; + + private weak PublishingUI.PublishingDialog dialog = null; + private Spit.Publishing.Publisher active_publisher = null; + private Publishable[] publishables = null; + private unowned LoginCallback current_login_callback = null; + private bool publishing_halted = false; + private Spit.Publishing.Publisher.MediaType media_type = + Spit.Publishing.Publisher.MediaType.NONE; + + public ConcretePublishingHost(Service service, PublishingUI.PublishingDialog dialog, + Publishable[] publishables) { + base(service, "sharing"); + this.dialog = dialog; + this.publishables = publishables; + + foreach (Publishable curr_publishable in publishables) + this.media_type |= curr_publishable.get_media_type(); + + this.active_publisher = service.create_publisher(this); + } + + private void on_login_clicked() { + if (current_login_callback != null) + current_login_callback(); + } + + private void clean_up() { + foreach (Publishable publishable in publishables) + ((global::Publishing.Glue.MediaSourcePublishableWrapper) publishable).clean_up(); + } + + private void report_plugin_upload_progress(int file_number, double fraction_complete) { + // if the currently installed pane isn't the progress pane, do nothing + if (!(dialog.get_active_pane() is PublishingUI.ProgressPane)) + return; + + PublishingUI.ProgressPane pane = (PublishingUI.ProgressPane) dialog.get_active_pane(); + + string status_string = UPLOAD_STATUS_DESCRIPTION.printf(file_number, + publishables.length); + double status_fraction = STATUS_PREPARATION_FRACTION + (STATUS_UPLOAD_FRACTION * + fraction_complete); + + pane.set_status(status_string, status_fraction); + } + + private void install_progress_pane() { + PublishingUI.ProgressPane progress_pane = new PublishingUI.ProgressPane(); + + dialog.install_pane(progress_pane); + set_button_mode(Spit.Publishing.PluginHost.ButtonMode.CANCEL); + } + + public void install_dialog_pane(Spit.Publishing.DialogPane pane, + Spit.Publishing.PluginHost.ButtonMode button_mode) { + debug("Publishing.PluginHost: install_dialog_pane( ): invoked."); + + if (active_publisher == null || (!active_publisher.is_running())) + return; + + dialog.install_pane(pane); + + set_button_mode(button_mode); + } + + public void post_error(Error err) { + string msg = _("Publishing to %s can't continue because an error occurred:").printf( + active_publisher.get_service().get_pluggable_name()); + msg += GLib.Markup.printf_escaped("\n\n%s\n\n", err.message); + msg += _("To try publishing to another service, select one from the above menu."); + + dialog.install_pane(new PublishingUI.StaticMessagePane(msg, true)); + dialog.set_close_button_mode(); + dialog.unlock_service(); + + active_publisher.stop(); + + // post_error( ) tells the active_publisher to stop publishing and displays a + // non-removable error pane that effectively ends the publishing interaction, + // so no problem calling clean_up( ) here. + clean_up(); + } + + public void stop_publishing() { + debug("ConcretePublishingHost.stop_publishing( ): invoked."); + + if (active_publisher.is_running()) + active_publisher.stop(); + + clean_up(); + + publishing_halted = true; + } + + public void start_publishing() { + if (active_publisher.is_running()) + return; + + debug("ConcretePublishingHost.start_publishing( ): invoked."); + + active_publisher.start(); + } + + public Publisher get_publisher() { + return active_publisher; + } + + public void install_static_message_pane(string message, + Spit.Publishing.PluginHost.ButtonMode button_mode) { + + set_button_mode(button_mode); + + dialog.install_pane(new PublishingUI.StaticMessagePane(message)); + } + + public void install_pango_message_pane(string markup, + Spit.Publishing.PluginHost.ButtonMode button_mode) { + set_button_mode(button_mode); + + dialog.install_pane(new PublishingUI.StaticMessagePane(markup, true)); + } + + public void install_success_pane() { + dialog.install_pane(new PublishingUI.SuccessPane(get_publishable_media_type(), + publishables.length)); + dialog.set_close_button_mode(); + + // the success pane is a terminal pane; once it's installed, the publishing + // interaction is considered over, so clean up + clean_up(); + } + + public void install_account_fetch_wait_pane() { + dialog.install_pane(new PublishingUI.AccountFetchWaitPane()); + set_button_mode(Spit.Publishing.PluginHost.ButtonMode.CANCEL); + } + + public void install_login_wait_pane() { + dialog.install_pane(new PublishingUI.LoginWaitPane()); + } + + public void install_welcome_pane(string welcome_message, LoginCallback login_clicked_callback) { + PublishingUI.LoginWelcomePane login_pane = + new PublishingUI.LoginWelcomePane(welcome_message); + current_login_callback = login_clicked_callback; + login_pane.login_requested.connect(on_login_clicked); + + set_button_mode(Spit.Publishing.PluginHost.ButtonMode.CLOSE); + + dialog.install_pane(login_pane); + } + + public void set_service_locked(bool locked) { + if (locked) + dialog.lock_service(); + else + dialog.unlock_service(); + } + + public void set_button_mode(Spit.Publishing.PluginHost.ButtonMode mode) { + if (mode == Spit.Publishing.PluginHost.ButtonMode.CLOSE) + dialog.set_close_button_mode(); + else if (mode == Spit.Publishing.PluginHost.ButtonMode.CANCEL) + dialog.set_cancel_button_mode(); + else + error("unrecognized button mode enumeration value"); + } + + public void set_dialog_default_widget(Gtk.Widget widget) { + widget.can_default = true; + dialog.set_default(widget); + } + + public Spit.Publishing.Publisher.MediaType get_publishable_media_type() { + return media_type; + } + + public Publishable[] get_publishables() { + return publishables; + } + + public Spit.Publishing.ProgressCallback? serialize_publishables(int content_major_axis, + bool strip_metadata = false) { + install_progress_pane(); + PublishingUI.ProgressPane progress_pane = + (PublishingUI.ProgressPane) dialog.get_active_pane(); + + // spin the event loop right after installing the progress_pane so that the progress_pane + // will appear and let the user know that something is going on while file serialization + // takes place + spin_event_loop(); + + int i = 0; + foreach (Spit.Publishing.Publishable publishable in publishables) { + if (publishing_halted || !active_publisher.is_running()) + return null; + + try { + global::Publishing.Glue.MediaSourcePublishableWrapper wrapper = + (global::Publishing.Glue.MediaSourcePublishableWrapper) publishable; + wrapper.serialize_for_publishing(content_major_axis, strip_metadata); + } catch (Spit.Publishing.PublishingError err) { + post_error(err); + return null; + } + + double phase_fraction_complete = ((double) (i + 1)) / ((double) publishables.length); + double fraction_complete = phase_fraction_complete * STATUS_PREPARATION_FRACTION; + + debug("serialize_publishables( ): fraction_complete = %f.", fraction_complete); + + progress_pane.set_status(PREPARE_STATUS_DESCRIPTION, fraction_complete); + + spin_event_loop(); + + i++; + } + + return report_plugin_upload_progress; + } +} + +} + diff --git a/src/publishing/PublishingUI.vala b/src/publishing/PublishingUI.vala new file mode 100644 index 0000000..4f63f55 --- /dev/null +++ b/src/publishing/PublishingUI.vala @@ -0,0 +1,552 @@ +/* Copyright 2011-2014 Yorba Foundation + * + * This software is licensed under the GNU Lesser General Public License + * (version 2.1 or later). See the COPYING file in this distribution. + */ + +namespace PublishingUI { + +public class ConcreteDialogPane : Spit.Publishing.DialogPane, GLib.Object { + protected Gtk.Box pane_widget = null; + protected Gtk.Builder builder = null; + + public ConcreteDialogPane() { + builder = AppWindow.create_builder(); + } + + 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() { + } + + public void on_pane_uninstalled() { + } +} + +public class StaticMessagePane : ConcreteDialogPane { + private Gtk.Label msg_label = null; + + public StaticMessagePane(string message_string, bool enable_markup = false) { + base(); + msg_label = builder.get_object("static_msg_label") as Gtk.Label; + pane_widget = builder.get_object("static_msg_pane_widget") as Gtk.Box; + + if (enable_markup) { + msg_label.set_markup(message_string); + msg_label.set_line_wrap(true); + msg_label.set_use_markup(true); + } else { + msg_label.set_label(message_string); + } + } +} + +public class LoginWelcomePane : ConcreteDialogPane { + private Gtk.Button login_button = null; + private Gtk.Label not_logged_in_label = null; + + public signal void login_requested(); + + public LoginWelcomePane(string service_welcome_message) { + base(); + pane_widget = builder.get_object("welcome_pane_widget") as Gtk.Box; + login_button = builder.get_object("login_button") as Gtk.Button; + not_logged_in_label = builder.get_object("not_logged_in_label") as Gtk.Label; + + login_button.clicked.connect(on_login_clicked); + not_logged_in_label.set_use_markup(true); + not_logged_in_label.set_markup(service_welcome_message); + } + + private void on_login_clicked() { + login_requested(); + } +} + +public class ProgressPane : ConcreteDialogPane { + private Gtk.ProgressBar progress_bar = null; + + public ProgressPane() { + base(); + pane_widget = (Gtk.Box) builder.get_object("progress_pane_widget"); + progress_bar = (Gtk.ProgressBar) builder.get_object("publishing_progress_bar"); + } + + public void set_text(string text) { + progress_bar.set_text(text); + } + + public void set_progress(double progress) { + progress_bar.set_fraction(progress); + } + + public void set_status(string status_text, double progress) { + if (status_text != progress_bar.get_text()) + progress_bar.set_text(status_text); + + set_progress(progress); + } +} + +public class SuccessPane : StaticMessagePane { + public SuccessPane(Spit.Publishing.Publisher.MediaType published_media, int num_uploaded = 1) { + string? message_string = null; + + // Here, we check whether more than one item is being uploaded, and if so, display + // an alternate message. + if(num_uploaded > 1) { + if (published_media == (Spit.Publishing.Publisher.MediaType.PHOTO | Spit.Publishing.Publisher.MediaType.VIDEO)) + message_string = _("The selected photos/videos were successfully published."); + else if (published_media == Spit.Publishing.Publisher.MediaType.VIDEO) + message_string = _("The selected videos were successfully published."); + else + message_string = _("The selected photos were successfully published."); + } else { + if (published_media == Spit.Publishing.Publisher.MediaType.VIDEO) + message_string = _("The selected video was successfully published."); + else + message_string = _("The selected photo was successfully published."); + } + base(message_string); + } +} + +public class AccountFetchWaitPane : StaticMessagePane { + public AccountFetchWaitPane() { + base(_("Fetching account information...")); + } +} + +public class LoginWaitPane : StaticMessagePane { + public LoginWaitPane() { + base(_("Logging in...")); + } +} + +public class PublishingDialog : Gtk.Dialog { + private const int LARGE_WINDOW_WIDTH = 860; + private const int LARGE_WINDOW_HEIGHT = 688; + private const int COLOSSAL_WINDOW_WIDTH = 1024; + private const int COLOSSAL_WINDOW_HEIGHT = 688; + private const int STANDARD_WINDOW_WIDTH = 632; + private const int STANDARD_WINDOW_HEIGHT = 540; + private const int BORDER_REGION_WIDTH = 16; + private const int BORDER_REGION_HEIGHT = 100; + + public const int STANDARD_CONTENT_LABEL_WIDTH = 500; + public const int STANDARD_ACTION_BUTTON_WIDTH = 128; + + private static PublishingDialog active_instance = null; + + private Gtk.ListStore service_selector_box_model; + private Gtk.ComboBox service_selector_box; + private Gtk.Label service_selector_box_label; + private Gtk.Box central_area_layouter; + private Gtk.Button close_cancel_button; + private Spit.Publishing.DialogPane active_pane; + private Spit.Publishing.Publishable[] publishables; + private Spit.Publishing.ConcretePublishingHost host; + private Spit.PluggableInfo info; + + protected PublishingDialog(Gee.Collection to_publish) { + assert(to_publish.size > 0); + + resizable = false; + delete_event.connect(on_window_close); + + publishables = new Spit.Publishing.Publishable[0]; + bool has_photos = false; + bool has_videos = false; + foreach (MediaSource media in to_publish) { + Spit.Publishing.Publishable publishable = + new Publishing.Glue.MediaSourcePublishableWrapper(media); + if (publishable.get_media_type() == Spit.Publishing.Publisher.MediaType.PHOTO) + has_photos = true; + else if (publishable.get_media_type() == Spit.Publishing.Publisher.MediaType.VIDEO) + has_videos = true; + else + assert_not_reached(); + + publishables += publishable; + } + + string title = null; + string label = null; + + if (has_photos && !has_videos) { + title = null;_("Publish Photos"); + label = _("Publish photos _to:"); + } else if (!has_photos && has_videos) { + title = _("Publish Videos"); + label = _("Publish videos _to"); + } else { + title = _("Publish Photos and Videos"); + label = _("Publish photos and videos _to"); + } + set_title(title); + + service_selector_box_model = new Gtk.ListStore(2, typeof(Gdk.Pixbuf), typeof(string)); + service_selector_box = new Gtk.ComboBox.with_model(service_selector_box_model); + + Gtk.CellRendererPixbuf renderer_pix = new Gtk.CellRendererPixbuf(); + service_selector_box.pack_start(renderer_pix,true); + service_selector_box.add_attribute(renderer_pix, "pixbuf", 0); + + Gtk.CellRendererText renderer_text = new Gtk.CellRendererText(); + service_selector_box.pack_start(renderer_text,true); + service_selector_box.add_attribute(renderer_text, "text", 1); + + service_selector_box.set_active(0); + + service_selector_box_label = new Gtk.Label.with_mnemonic(label); + service_selector_box_label.set_mnemonic_widget(service_selector_box); + service_selector_box_label.set_alignment(0.0f, 0.5f); + + // get the name of the service the user last used + string? last_used_service = Config.Facade.get_instance().get_last_used_service(); + + Spit.Publishing.Service[] loaded_services = load_services(has_photos, has_videos); + + Gtk.TreeIter iter; + + foreach (Spit.Publishing.Service service in loaded_services) { + service_selector_box_model.append(out iter); + + string curr_service_id = service.get_id(); + + service.get_info(ref info); + + if (null != info.icons && 0 < info.icons.length) { + // check if the icons object is set -- if set use that icon + service_selector_box_model.set(iter, 0, info.icons[0], 1, + service.get_pluggable_name()); + + // in case the icons object is not set on the next iteration + info.icons[0] = Resources.get_icon(Resources.ICON_GENERIC_PLUGIN); + } else { + // if icons object is null or zero length use a generic icon + service_selector_box_model.set(iter, 0, Resources.get_icon( + Resources.ICON_GENERIC_PLUGIN), 1, service.get_pluggable_name()); + } + + if (last_used_service == null) { + service_selector_box.set_active_iter(iter); + last_used_service = service.get_id(); + } else if (last_used_service == curr_service_id) { + service_selector_box.set_active_iter(iter); + } + } + + service_selector_box.changed.connect(on_service_changed); + + /* the wrapper is not an extraneous widget -- it's necessary to prevent the service + selection box from growing and shrinking whenever its parent's size changes. + When wrapped inside a Gtk.Alignment, the Alignment grows and shrinks instead of + the service selection box. */ + Gtk.Alignment service_selector_box_wrapper = new Gtk.Alignment(1.0f, 0.5f, 0.0f, 0.0f); + service_selector_box_wrapper.add(service_selector_box); + + Gtk.Box service_selector_layouter = new Gtk.Box(Gtk.Orientation.HORIZONTAL, 8); + service_selector_layouter.set_border_width(12); + service_selector_layouter.add(service_selector_box_label); + service_selector_layouter.pack_start(service_selector_box_wrapper, true, true, 0); + + /* 'service area' is the selector assembly plus the horizontal rule dividing it from the + rest of the dialog */ + Gtk.Box service_area_layouter = new Gtk.Box(Gtk.Orientation.VERTICAL, 0); + service_area_layouter.add(service_selector_layouter); + service_area_layouter.add(new Gtk.Separator(Gtk.Orientation.HORIZONTAL)); + + Gtk.Alignment service_area_wrapper = new Gtk.Alignment(0.0f, 0.0f, 1.0f, 0.0f); + service_area_wrapper.add(service_area_layouter); + + central_area_layouter = new Gtk.Box(Gtk.Orientation.VERTICAL, 0); + + get_content_area().pack_start(service_area_wrapper, false, false, 0); + get_content_area().pack_start(central_area_layouter, true, true, 0); + + close_cancel_button = new Gtk.Button.with_mnemonic("_Cancel"); + close_cancel_button.set_can_default(true); + close_cancel_button.clicked.connect(on_close_cancel_clicked); + ((Gtk.Container) get_action_area()).add(close_cancel_button); + + set_standard_window_mode(); + + show_all(); + } + + private static Spit.Publishing.Service[] load_all_services() { + Spit.Publishing.Service[] loaded_services = new Spit.Publishing.Service[0]; + + // load publishing services from plug-ins + Gee.Collection pluggables = Plugins.get_pluggables_for_type( + typeof(Spit.Publishing.Service)); + + debug("PublisingDialog: discovered %d pluggable publishing services.", pluggables.size); + + foreach (Spit.Pluggable pluggable in pluggables) { + int pluggable_interface = pluggable.get_pluggable_interface( + Spit.Publishing.CURRENT_INTERFACE, Spit.Publishing.CURRENT_INTERFACE); + if (pluggable_interface != Spit.Publishing.CURRENT_INTERFACE) { + warning("Unable to load publisher %s: reported interface %d.", + Plugins.get_pluggable_module_id(pluggable), pluggable_interface); + + continue; + } + + Spit.Publishing.Service service = + (Spit.Publishing.Service) pluggable; + + debug("PublishingDialog: discovered pluggable publishing service '%s'.", + service.get_pluggable_name()); + + loaded_services += service; + } + + // Sort publishing services by name. + Posix.qsort(loaded_services, loaded_services.length, sizeof(Spit.Publishing.Service), + (a, b) => {return utf8_cs_compare((*((Spit.Publishing.Service**) a))->get_pluggable_name(), + (*((Spit.Publishing.Service**) b))->get_pluggable_name()); + }); + + return loaded_services; + } + + private static Spit.Publishing.Service[] load_services(bool has_photos, bool has_videos) { + assert (has_photos || has_videos); + + Spit.Publishing.Service[] filtered_services = new Spit.Publishing.Service[0]; + Spit.Publishing.Service[] all_services = load_all_services(); + + foreach (Spit.Publishing.Service service in all_services) { + + if (has_photos && !has_videos) { + if ((service.get_supported_media() & Spit.Publishing.Publisher.MediaType.PHOTO) != 0) + filtered_services += service; + } else if (!has_photos && has_videos) { + if ((service.get_supported_media() & Spit.Publishing.Publisher.MediaType.VIDEO) != 0) + filtered_services += service; + } else { + if (((service.get_supported_media() & Spit.Publishing.Publisher.MediaType.PHOTO) != 0) && + ((service.get_supported_media() & Spit.Publishing.Publisher.MediaType.VIDEO) != 0)) + filtered_services += service; + } + } + + return filtered_services; + } + + // Because of this bug: http://trac.yorba.org/ticket/3623, we use some extreme measures. The + // bug occurs because, in some cases, when publishing is started asynchronous network + // transactions are performed. The mechanism inside libsoup that we use to perform asynchronous + // network transactions isn't based on threads but is instead based on the GLib event loop. So + // whenever we run a network transaction, the GLib event loop gets spun. One consequence of + // this is that PublishingDialog.go( ) can be called multiple times. Note that since events + // are processed sequentially, PublishingDialog.go( ) is never called re-entrantly. It just + // gets called twice back-to-back in quick succession. So use a timer to do a short circuit + // return if this call to go( ) follows immediately on the heels of another call to go( ). + private static Timer since_last_start = null; + private static bool elapsed_is_valid = false; + public static void go(Gee.Collection to_publish) { + if (active_instance != null) + return; + + if (since_last_start == null) { + // GLib.Timers start themselves automatically when they're created, so stop our + // new timer and reset it to zero 'til were ready to start timing. + since_last_start = new Timer(); + since_last_start.stop(); + since_last_start.reset(); + elapsed_is_valid = false; + } else { + double elapsed = since_last_start.elapsed(); + if ((elapsed < 0.05) && (elapsed_is_valid)) + return; + } + + Gee.ArrayList photos = new Gee.ArrayList(); + Gee.ArrayList