summaryrefslogtreecommitdiff
path: root/src/publishing
diff options
context:
space:
mode:
authorJörg Frings-Fürst <debian@jff-webhosting.net>2014-07-23 09:06:59 +0200
committerJörg Frings-Fürst <debian@jff-webhosting.net>2014-07-23 09:06:59 +0200
commit4ea2cc3bd4a7d9b1c54a9d33e6a1cf82e7c8c21d (patch)
treed2e54377d14d604356c86862a326f64ae64dadd6 /src/publishing
Imported Upstream version 0.18.1upstream/0.18.1
Diffstat (limited to 'src/publishing')
-rw-r--r--src/publishing/APIGlue.vala133
-rw-r--r--src/publishing/Publishing.vala24
-rw-r--r--src/publishing/PublishingPluginHost.vala238
-rw-r--r--src/publishing/PublishingUI.vala552
-rw-r--r--src/publishing/mk/publishing.mk31
5 files changed, 978 insertions, 0 deletions
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<string, string> param_string = new Gee.HashMap<string, string>();
+
+ 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<Tag>? 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<i>%s</i>\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<MediaSource> 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<Spit.Pluggable> 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<MediaSource> 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<LibraryPhoto> photos = new Gee.ArrayList<LibraryPhoto>();
+ Gee.ArrayList<Video> videos = new Gee.ArrayList<Video>();
+ MediaSourceCollection.filter_media(to_publish, photos, videos);
+
+ Spit.Publishing.Service[] avail_services =
+ load_services((photos.size > 0), (videos.size > 0));
+
+ if (avail_services.length == 0) {
+ // There are no enabled publishing services that accept this media type,
+ // warn the user.
+ AppWindow.error_message_with_title(_("Unable to publish"),
+ _("Shotwell cannot publish the selected items because you do not have a compatible publishing plugin enabled. To correct this, choose <b>Edit %s Preferences</b> and enable one or more of the publishing plugins on the <b>Plugins</b> tab.").printf("▸"),
+ null, false);
+
+ return;
+ }
+
+ // If we get down here, it means that at least one publishing service
+ // was found that could accept this type of media, so continue normally.
+
+ debug("PublishingDialog.go( )");
+
+ active_instance = new PublishingDialog(to_publish);
+
+ active_instance.run();
+
+ active_instance = null;
+
+ // start timing just before we return
+ since_last_start.start();
+ elapsed_is_valid = true;
+ }
+
+ private bool on_window_close(Gdk.EventAny evt) {
+ host.stop_publishing();
+ host = null;
+ hide();
+ destroy();
+
+ return true;
+ }
+
+ private void on_service_changed() {
+ Gtk.TreeIter iter;
+ bool have_active_iter = false;
+ have_active_iter = service_selector_box.get_active_iter(out iter);
+
+ // this occurs when the user removes the last active publisher
+ if (!have_active_iter) {
+ // default to the first in the list (as good as any)
+ service_selector_box.set_active(0);
+
+ // and get active again
+ service_selector_box.get_active_iter(out iter);
+ }
+
+ Value service_name_val;
+ service_selector_box_model.get_value(iter, 1, out service_name_val);
+
+ string service_name = (string) service_name_val;
+
+ Spit.Publishing.Service? selected_service = null;
+ Spit.Publishing.Service[] services = load_all_services();
+ foreach (Spit.Publishing.Service service in services) {
+ if (service.get_pluggable_name() == service_name) {
+ selected_service = service;
+ break;
+ }
+ }
+ assert(selected_service != null);
+
+ Config.Facade.get_instance().set_last_used_service(selected_service.get_id());
+
+ host = new Spit.Publishing.ConcretePublishingHost(selected_service, this, publishables);
+ host.start_publishing();
+ }
+
+ private void on_close_cancel_clicked() {
+ debug("PublishingDialog: on_close_cancel_clicked( ): invoked.");
+
+ host.stop_publishing();
+ host = null;
+ hide();
+ destroy();
+ }
+
+ private void set_large_window_mode() {
+ set_size_request(LARGE_WINDOW_WIDTH, LARGE_WINDOW_HEIGHT);
+ central_area_layouter.set_size_request(LARGE_WINDOW_WIDTH - BORDER_REGION_WIDTH,
+ LARGE_WINDOW_HEIGHT - BORDER_REGION_HEIGHT);
+ resizable = false;
+ }
+
+ private void set_colossal_window_mode() {
+ set_size_request(COLOSSAL_WINDOW_WIDTH, COLOSSAL_WINDOW_HEIGHT);
+ central_area_layouter.set_size_request(COLOSSAL_WINDOW_WIDTH - BORDER_REGION_WIDTH,
+ COLOSSAL_WINDOW_HEIGHT - BORDER_REGION_HEIGHT);
+ resizable = false;
+ }
+
+ private void set_standard_window_mode() {
+ set_size_request(STANDARD_WINDOW_WIDTH, STANDARD_WINDOW_HEIGHT);
+ central_area_layouter.set_size_request(STANDARD_WINDOW_WIDTH - BORDER_REGION_WIDTH,
+ STANDARD_WINDOW_HEIGHT - BORDER_REGION_HEIGHT);
+ resizable = false;
+ }
+
+ private void set_free_sizable_window_mode() {
+ resizable = true;
+ }
+
+ private void clear_free_sizable_window_mode() {
+ resizable = false;
+ }
+
+ public Spit.Publishing.DialogPane get_active_pane() {
+ return active_pane;
+ }
+
+ public void set_close_button_mode() {
+ close_cancel_button.set_label(_("_Close"));
+ set_default(close_cancel_button);
+ }
+
+ public void set_cancel_button_mode() {
+ close_cancel_button.set_label(_("_Cancel"));
+ }
+
+ public void lock_service() {
+ service_selector_box.set_sensitive(false);
+ }
+
+ public void unlock_service() {
+ service_selector_box.set_sensitive(true);
+ }
+
+ public void install_pane(Spit.Publishing.DialogPane pane) {
+ debug("PublishingDialog: install_pane( ): invoked.");
+
+ if (active_pane != null) {
+ debug("PublishingDialog: install_pane( ): a pane is already installed; removing it.");
+
+ active_pane.on_pane_uninstalled();
+ central_area_layouter.remove(active_pane.get_widget());
+ }
+
+ central_area_layouter.pack_start(pane.get_widget(), true, true, 0);
+ show_all();
+
+ Spit.Publishing.DialogPane.GeometryOptions geometry_options =
+ pane.get_preferred_geometry();
+ if ((geometry_options & Spit.Publishing.DialogPane.GeometryOptions.EXTENDED_SIZE) != 0)
+ set_large_window_mode();
+ else if ((geometry_options & Spit.Publishing.DialogPane.GeometryOptions.COLOSSAL_SIZE) != 0)
+ set_colossal_window_mode();
+ else
+ set_standard_window_mode();
+
+ if ((geometry_options & Spit.Publishing.DialogPane.GeometryOptions.RESIZABLE) != 0)
+ set_free_sizable_window_mode();
+ else
+ clear_free_sizable_window_mode();
+
+ active_pane = pane;
+ pane.on_pane_installed();
+ }
+
+ public new int run() {
+ on_service_changed();
+
+ int result = base.run();
+
+ host = null;
+
+ return result;
+ }
+}
+
+}
+
diff --git a/src/publishing/mk/publishing.mk b/src/publishing/mk/publishing.mk
new file mode 100644
index 0000000..fd31b81
--- /dev/null
+++ b/src/publishing/mk/publishing.mk
@@ -0,0 +1,31 @@
+
+# UNIT_NAME is the Vala namespace. A file named UNIT_NAME.vala must be in this directory with
+# a init() and terminate() function declared in the namespace.
+UNIT_NAME := Publishing
+
+# UNIT_DIR should match the subdirectory the files are located in. Generally UNIT_NAME in all
+# lowercase. The name of this file should be UNIT_DIR.mk.
+UNIT_DIR := publishing
+
+# All Vala files in the unit should be listed here with no subdirectory prefix.
+#
+# NOTE: Do *not* include the unit's master file, i.e. UNIT_NAME.vala.
+UNIT_FILES := \
+ PublishingUI.vala \
+ PublishingPluginHost.vala \
+ APIGlue.vala
+
+# Any unit this unit relies upon (and should be initialized before it's initialized) should
+# be listed here using its Vala namespace.
+#
+# NOTE: All units are assumed to rely upon the unit-unit. Do not include that here.
+UNIT_USES := \
+ Plugins
+
+# List any additional files that are used in the build process as a part of this unit that should
+# be packaged in the tarball. File names should be relative to the unit's home directory.
+UNIT_RC :=
+
+# unitize.mk must be called at the end of each UNIT_DIR.mk file.
+include unitize.mk
+