[eog-plugins] Add Postasa plugin allowing uploads to PicasaWeb



commit 6fb64e0ae158c3a580376044610e4e4312e488fe
Author: Richard Schwarting <aquarichy gmail com>
Date:   Wed Feb 10 19:16:14 2010 +0100

    Add Postasa plugin allowing uploads to PicasaWeb
    
    This new plugin supports uploading images to PicasaWeb using libgdata.
    It requires the yet unreleased libgdata-0.6.0, but it should be out in
    time. See bug #600190.

 configure.ac                                  |   26 +-
 plugins/postasa/Makefile.am                   |   43 ++
 plugins/postasa/eog-postasa-plugin.c          |  824 +++++++++++++++++++++++++
 plugins/postasa/eog-postasa-plugin.h          |   78 +++
 plugins/postasa/postasa-config.xml            |  155 +++++
 plugins/postasa/postasa-uploads.xml           |  171 +++++
 plugins/postasa/postasa.eog-plugin.desktop.in |    9 +
 7 files changed, 1302 insertions(+), 4 deletions(-)
---
diff --git a/configure.ac b/configure.ac
index 146937e..a1b7415 100644
--- a/configure.ac
+++ b/configure.ac
@@ -60,9 +60,9 @@ EOG_HAS_PYTHON=1
 
 AC_MSG_CHECKING([which plugins to build])
 
-ALL_PLUGINS="postr map fit-to-width exif-display send-by-mail"
-USEFUL_PLUGINS="postr map fit-to-width exif-display send-by-mail"
-DEFAULT_PLUGINS="postr map fit-to-width exif-display send-by-mail"
+ALL_PLUGINS="postr map fit-to-width exif-display send-by-mail postasa"
+USEFUL_PLUGINS="postr map fit-to-width exif-display send-by-mail postasa"
+DEFAULT_PLUGINS="postr map fit-to-width exif-display send-by-mail postasa"
 
 PYTHON_ALL_PLUGINS="slideshowshuffle pythonconsole"
 PYTHON_USEFUL_PLUGINS="slideshowshuffle pythonconsole"
@@ -82,7 +82,7 @@ AC_ARG_WITH([plugins],
 	    [AS_HELP_STRING([--with-plugins=LIST],
 			    [Comma-separated list specifying the plugins to build.
 			    Available: map, exif-display, fit-to-width,
-			    postr, pythonconsole, slideshowshuffle,
+			    postr, postasa, pythonconsole, slideshowshuffle,
 			    as well as the aliases default,
 			    all, and really-all])],
 	    [plugins=$with_plugins],
@@ -289,6 +289,23 @@ then
 	fi
 fi
 
+# *******
+# Postasa
+# *******
+plugin_defined postasa
+if test "$?" = 1
+then
+    PKG_CHECK_MODULES(POSTASA,
+        [ libgdata >= 0.6.0 ],
+        [],[have_postasa=no])
+    AC_SUBST(POSTASA_LIBS)
+    AC_SUBST(POSTASA_CFLAGS)
+    if test "x$have_postasa" = "xno"
+    then
+        undef_plugin postasa "libgdata was not found"
+    fi
+fi
+
 # ***
 # Map
 # ***
@@ -356,6 +373,7 @@ plugins/exif-display/Makefile
 plugins/send-by-mail/Makefile
 plugins/slideshowshuffle/Makefile
 plugins/postr/Makefile
+plugins/postasa/Makefile
 plugins/pythonconsole/Makefile
 po/Makefile.in])
 
diff --git a/plugins/postasa/Makefile.am b/plugins/postasa/Makefile.am
new file mode 100644
index 0000000..657a75a
--- /dev/null
+++ b/plugins/postasa/Makefile.am
@@ -0,0 +1,43 @@
+# Postasa plugin
+plugindir = $(libdir)/eog/plugins
+
+INCLUDES = \
+	-I$(top_srcdir) 				\
+	$(EOG_CFLAGS) 					\
+	$(WARN_CFLAGS)					\
+	$(POSTASA_CFLAGS) 				\
+	-DEOG_LOCALEDIR=\""$(prefix)/$(DATADIRNAME)/locale"\" \
+	-DEOG_PLUGINDIR=\""$(plugindir)"\"
+
+plugin_LTLIBRARIES = libpostasa.la
+
+libpostasa_la_SOURCES = \
+	eog-postasa-plugin.h				\
+	eog-postasa-plugin.c
+
+libpostasa_la_LDFLAGS = \
+	$(PLUGIN_LIBTOOL_FLAGS)				\
+	$(EOG_LIBS)
+
+libpostasa_la_LIBADD = $(EOG_LIBS)			\
+	$(POSTASA_LIBS)
+
+
+# Plugin Info
+
+plugin_in_files = postasa.eog-plugin.desktop.in
+
+UI_FILES = postasa-config.xml postasa-uploads.xml default.png
+ui_DATA = $(UI_FILES)
+uidir = $(plugindir)/postasa
+
+%.eog-plugin: %.eog-plugin.desktop.in $(INTLTOOL_MERGE) $(wildcard $(top_srcdir)/po/*po) ; $(AM_V_GEN)$(INTLTOOL_MERGE) $(top_srcdir)/po $< $@ -d -u -c $(top_builddir)/po/.intltool-merge-cache
+
+plugin_DATA = $(plugin_in_files:.eog-plugin.desktop.in=.eog-plugin)
+
+EXTRA_DIST = $(plugin_in_files) $(UI_FILES)
+
+CLEANFILES = $(plugin_DATA)
+DISTCLEANFILES = $(plugin_DATA)
+
+-include $(top_srcdir)/git.mk
diff --git a/plugins/postasa/eog-postasa-plugin.c b/plugins/postasa/eog-postasa-plugin.c
new file mode 100644
index 0000000..f7b646e
--- /dev/null
+++ b/plugins/postasa/eog-postasa-plugin.c
@@ -0,0 +1,824 @@
+/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
+
+/* Postasa -- PicasaWeb Uploader plugin
+ *
+ * Copyright (C) 2009 The Free Software Foundation
+ *
+ * Author: Richard Schwarting <aquarichy gmail com>
+ * Initially based on Postr code by: Lucas Rocha
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ **/
+
+#ifdef HAVE_CONFIG_H
+#include <config.h>
+#endif
+
+#include "eog-postasa-plugin.h"
+
+#include <gmodule.h>
+#include <glib/gi18n-lib.h>
+
+#include <eog/eog-debug.h>
+#include <eog/eog-thumb-view.h>
+#include <eog/eog-image.h>
+
+#include <gdata/gdata.h>
+
+#define WINDOW_DATA_KEY "EogPostasaWindowData"
+#define MENU_PATH "/MainMenu/ToolsMenu/ToolsOps_2"
+
+#define GTKBUILDER_CONFIG_FILE EOG_PLUGINDIR"/postasa/postasa-config.xml"
+#define GTKBUILDER_UPLOAD_FILE EOG_PLUGINDIR"/postasa/postasa-uploads.xml"
+#define DEFAULT_THUMBNAIL EOG_PLUGINDIR"/postasa/default.png"
+
+EOG_PLUGIN_REGISTER_TYPE(EogPostasaPlugin, eog_postasa_plugin)
+
+/**
+ * _EogPostasaPluginPrivate:
+ *
+ * Private data for the Postasa EoG plugin
+ **/
+struct _EogPostasaPluginPrivate
+{
+	EogWindow    *eog_window;
+	GDataPicasaWebService *service;
+	GCancellable *login_cancellable;
+
+	/* TODO: consider using GConf to store the username in; perhaps not the password */
+	GtkDialog    *login_dialog;
+	GtkEntry     *username_entry;
+	GtkEntry     *password_entry;
+	GtkLabel     *login_message;
+	GtkButton    *login_button;
+	GtkButton    *cancel_button;
+	gboolean      uploads_pending;
+
+	GtkWindow    *uploads_window;
+	GtkTreeView  *uploads_view;
+	GtkListStore *uploads_store;
+};
+
+/**
+ * WindowData:
+ *
+ * Data we'll associate with the window, describing the action group
+ * as well as the plugin, so we'll be able to access it even from the
+ * #GtkActionEntry's callback.
+ **/
+typedef struct
+{
+	GtkActionGroup *ui_action_group;
+	guint ui_id;
+	EogPostasaPlugin *plugin;
+} WindowData;
+
+/**
+ * PicasaWebUploadFileAsyncData:
+ *
+ * Small chunk of data for use by our asynchronous PicasaWeb upload
+ * API.  It describes the position in the upload window's tree view, for
+ * cancellation purposes, and the the image file that we want to
+ * upload.
+ *
+ * TODO: remove this and the async API when we get a proper upload
+ * async method into libgdata.
+ **/
+typedef struct
+{
+	GtkTreeIter *iter;
+	GFile *imgfile;
+} PicasaWebUploadFileAsyncData;
+
+static void picasaweb_upload_cb (GtkAction *action, EogWindow *window);
+static GtkWidget *login_get_dialog (EogPostasaPlugin *plugin);
+static gboolean login_dialog_close (EogPostasaPlugin *plugin);
+
+/**
+ * action_entries:
+ *
+ * Describes the #GtkActionEntry representing the Postasa upload menu
+ * item and it's callback.
+ **/
+static const GtkActionEntry action_entries[] =
+{
+	{ "RunPostasa",
+	  "postasa",
+	  N_("Upload to PicasaWeb"),
+	  NULL,
+	  N_("Upload your pictures to PicasaWeb"),
+	  G_CALLBACK (picasaweb_upload_cb) }
+};
+
+/*** Uploads Dialog ***/
+
+/**
+ * uploads_cancel_row:
+ *
+ * Function on the upload list store to cancel a specified upload.
+ * This is intended to be called by foreach functions.
+ *
+ * There's a small chance that the #GCancellable cancellation might not
+ * occur before the upload is completed, in which case we do not
+ * change the row's status to "Cancelled".
+ **/
+static gboolean
+uploads_cancel_row (GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter, EogPostasaPlugin *plugin)
+{
+	GCancellable *cancellable;
+	gint status;
+
+	gtk_tree_model_get (model, iter, 4, &cancellable, -1);
+	g_cancellable_cancel (cancellable);
+	/* we let picasaweb_upload_async_cb() handle setting a Cancelled message */
+
+	return FALSE;
+}
+
+/**
+ * uploads_cancel_cb:
+ *
+ * Obtains the current selection and calls an API to cancel each one.
+ **/
+static void
+uploads_cancel_cb (GtkWidget *cancel_button, EogPostasaPlugin *plugin)
+{
+	GtkTreeSelection *selection;
+	selection = gtk_tree_view_get_selection (plugin->priv->uploads_view); /* don't unref */
+
+	gtk_tree_selection_selected_foreach (selection, (GtkTreeSelectionForeachFunc)uploads_cancel_row, plugin);
+}
+
+/**
+ * uploads_cancel_all_cb:
+ *
+ * Calls an API on every row to cancel its upload.
+ **/
+static void
+uploads_cancel_all_cb (GtkWidget *cancel_all_button, EogPostasaPlugin *plugin)
+{
+	gtk_tree_model_foreach (GTK_TREE_MODEL (plugin->priv->uploads_store), (GtkTreeModelForeachFunc)uploads_cancel_row, plugin);
+}
+
+/**
+ * uploads_get_dialog:
+ *
+ * Returns the a #GtkWindow representing the Uploads window.  If it
+ * has not already been created, it creates it.  The Uploads window is
+ * set to be hidden instead of destroyed when closed, to avoid having
+ * to recreate it and re-parse the UI file, etc.
+ **/
+static GtkWindow *
+uploads_get_dialog (EogPostasaPlugin *plugin)
+{
+	GtkBuilder *builder;
+	GError *error = NULL;
+	GtkButton *cancel_button;
+	GtkButton *cancel_all_button;
+
+	if (plugin->priv->uploads_window == NULL) {
+		builder = gtk_builder_new ();
+		gtk_builder_add_from_file (builder, GTKBUILDER_UPLOAD_FILE, &error);
+		if (error != NULL) {
+			g_warning ("Couldn't load Postasa uploads UI file:%d:%s", error->code, error->message);
+			g_error_free (error);
+			return NULL;
+		}
+
+		/* note: do not unref gtk_builder_get_object() returns */
+		plugin->priv->uploads_window = GTK_WINDOW     (gtk_builder_get_object (builder, "uploads_window"));
+		plugin->priv->uploads_view   = GTK_TREE_VIEW  (gtk_builder_get_object (builder, "uploads_view"));
+		plugin->priv->uploads_store  = GTK_LIST_STORE (gtk_builder_get_object (builder, "uploads_store"));
+
+		cancel_button     = GTK_BUTTON (gtk_builder_get_object (builder, "cancel_button"));
+		cancel_all_button = GTK_BUTTON (gtk_builder_get_object (builder, "cancel_all_button"));
+
+		/* TODO: can't set expand = TRUE when packing cells into columns via glade-3/GtkBuilder apparently?
+		   bgo #602152  So for now, we take them, clear them out, and remap them.  Ugh.  Better solutions welcome.  */
+		GtkTreeViewColumn *file_col       = GTK_TREE_VIEW_COLUMN (gtk_builder_get_object (builder, "file_col"));
+		GtkCellRenderer   *thumbnail_cell = GTK_CELL_RENDERER    (gtk_builder_get_object (builder, "thumbnail_cell"));
+		GtkCellRenderer   *filepath_cell  = GTK_CELL_RENDERER    (gtk_builder_get_object (builder, "filepath_cell"));
+		gtk_tree_view_column_clear (file_col);
+		gtk_tree_view_column_pack_start (file_col, thumbnail_cell, FALSE);
+		gtk_tree_view_column_pack_end (file_col, filepath_cell, TRUE);
+		gtk_tree_view_column_add_attribute (file_col, thumbnail_cell, "pixbuf", 0);
+		gtk_tree_view_column_add_attribute (file_col, filepath_cell, "text", 1);
+		GtkTreeViewColumn *progress_col   = GTK_TREE_VIEW_COLUMN (gtk_builder_get_object (builder, "progress_col"));
+		GtkCellRenderer   *progress_cell  = GTK_CELL_RENDERER    (gtk_builder_get_object (builder, "progress_cell"));
+		gtk_tree_view_column_clear (progress_col);
+		gtk_tree_view_column_pack_end (progress_col, progress_cell, TRUE);
+		gtk_tree_view_column_add_attribute (progress_col, progress_cell, "pulse", 3);
+		gtk_tree_view_column_add_attribute (progress_col, progress_cell, "text", 5);
+
+		g_object_unref (builder);
+
+		g_signal_connect (G_OBJECT (cancel_button),     "clicked", G_CALLBACK (uploads_cancel_cb), plugin);
+		g_signal_connect (G_OBJECT (cancel_all_button), "clicked", G_CALLBACK (uploads_cancel_all_cb), plugin);
+		g_signal_connect (G_OBJECT (plugin->priv->uploads_window), "delete-event", G_CALLBACK (gtk_widget_hide_on_delete), plugin);
+	}
+
+	return plugin->priv->uploads_window;
+}
+
+typedef struct {
+	EogPostasaPlugin *plugin;
+	GtkTreeIter iter;
+} PulseData;
+
+static gboolean
+pulse (PulseData *data)
+{
+	gint status;
+	GCancellable *cancellable;
+
+	gtk_tree_model_get (GTK_TREE_MODEL (data->plugin->priv->uploads_store), &(data->iter), 3, &status, 4, &cancellable, -1);
+
+	if (0 <= status && status < G_MAXINT && g_cancellable_is_cancelled (cancellable) == FALSE) {
+		/* TODO: consider potential for races and how g_timeout_add works wrt threading; none seen in testing, though */
+		gtk_list_store_set (data->plugin->priv->uploads_store, &(data->iter), 3, status+1, -1);
+		return TRUE;
+	} else {
+		/* either we've failed, <0, or we're done, G_MAX_INT */
+		g_slice_free (PulseData, data);
+		return FALSE;
+	}
+}
+
+/**
+ * uploads_add_entry:
+ *
+ * Adds a new row to the Uploads tree view for an #EogImage to upload.
+ * The row stores the upload's #GCancellable and returns.
+ *
+ * Returns: a #GtkTreeIter that should be freed with g_slice_free()
+ **/
+static GtkTreeIter *
+uploads_add_entry (EogPostasaPlugin *plugin, EogImage *image, GCancellable *cancellable)
+{
+	GtkWindow *uploads_window;
+	GdkPixbuf *thumbnail_pixbuf;
+	GdkPixbuf *scaled_pixbuf;
+	gchar *size, *uri;
+	GtkTreeIter *iter;
+	GError *error = NULL;
+
+	/* display the Uploads window got from the plugin */
+	uploads_window = uploads_get_dialog (plugin);
+	gtk_widget_show_all (GTK_WIDGET (uploads_window));
+
+	/* obtain the data describing the upload */
+	/* TODO: submit patch with documentaiton for eog_image_get_*,
+	   particularly what needs unrefing */
+	uri = eog_image_get_uri_for_display (image);
+	thumbnail_pixbuf = eog_image_get_thumbnail (image);
+	if (GDK_IS_PIXBUF (thumbnail_pixbuf)) {
+		scaled_pixbuf = gdk_pixbuf_scale_simple (thumbnail_pixbuf, 32, 32, GDK_INTERP_BILINEAR);
+		g_object_unref (thumbnail_pixbuf);
+	} else {
+		scaled_pixbuf = gdk_pixbuf_new_from_file (DEFAULT_THUMBNAIL, &error);
+	}
+	size = g_strdup_printf ("%luKB", eog_image_get_bytes (image) / 1024);
+	iter = g_slice_new0 (GtkTreeIter);
+
+	/* insert the data into the upload's list store */
+	gtk_list_store_insert_with_values (plugin->priv->uploads_store, iter, 0,
+					   0, scaled_pixbuf,
+					   1, uri,
+					   2, size,
+					   3, 50, /* upload status: set to G_MAXINT when done, to 0 to not start */
+					   4, cancellable,
+					   5, N_("Uploading..."),
+					   -1); /* TODO: where should cancellabe, scaled_pixbuf be unref'd? don't worry about it since
+						   they'll exist until EoG exits anyway? or in eog_postasa_plugin_finalize()? */
+	g_free (uri);
+	g_free (size);
+	g_object_unref (scaled_pixbuf);
+
+	/* Set the progress bar to pulse every 50ms; freed in pulse() when upload is no longer in progress */
+	PulseData *data; /* just needs to be freed with g_slice_free() */
+	data = g_slice_new0 (PulseData);
+	data->plugin = plugin;
+	data->iter = *iter;
+	g_timeout_add (50, (GSourceFunc)pulse, data);
+
+	return iter;
+}
+
+static void
+free_picasaweb_upload_file_async_data (PicasaWebUploadFileAsyncData *data)
+{
+	g_object_unref (data->imgfile);
+	g_slice_free (GtkTreeIter, data->iter);
+	g_slice_free (PicasaWebUploadFileAsyncData, data);
+}
+
+/**
+ * picasaweb_upload_async_cb:
+ *
+ * Handles completion of the image's asynchronous upload to PicasaWeb.
+ *
+ * If the #GAsyncResults indicates success, we'll update the
+ * treeview's row for the given upload indicating this.  Elsewise, if
+ * it wasn't cancelled, then we report an error.
+ *
+ * NOTE: we also clean up the #PicasaWebUploadFileAsyncData here.
+ *
+ * TODO: we don't yet make the progress bar throb, how do we do that?
+ *
+ **/
+static void
+picasaweb_upload_async_cb (EogPostasaPlugin *plugin, GAsyncResult *res, PicasaWebUploadFileAsyncData *data)
+{
+	GCancellable* cancellable;
+	gchar *filename;
+	GError *error = NULL; /* TODO: make sure to clear all set errors */
+
+	if (g_simple_async_result_get_op_res_gboolean (G_SIMPLE_ASYNC_RESULT (res)) == TRUE) {
+		gtk_list_store_set (plugin->priv->uploads_store, data->iter, 3, G_MAXINT, 5, N_("Uploaded"), -1);
+	} else {
+		gtk_tree_model_get (GTK_TREE_MODEL (plugin->priv->uploads_store), data->iter, 4, &cancellable, -1);
+		if (g_cancellable_is_cancelled (cancellable) == TRUE) {
+			gtk_list_store_set (plugin->priv->uploads_store, data->iter, 3, -1, 5, N_("Cancelled"), -1);
+		} else {
+			g_simple_async_result_propagate_error (G_SIMPLE_ASYNC_RESULT (res), &error);
+			gtk_list_store_set (plugin->priv->uploads_store, data->iter, 3, -1, 5, error ? error->message : N_("Failed"), -1);
+			g_clear_error (&error);
+		}
+	}
+
+	free_picasaweb_upload_file_async_data (data);
+}
+
+/*** PicasaWeb ***/
+
+/**
+ * tmp_picasaweb_upload_async:
+ *
+ * Temporary solution to provide asynchronous uploading and stop
+ * blocking the UI until gdata_picasaweb_service_upload_file_async()
+ * becomes available (bgo #600262).  This method does the synchronous
+ * file upload, but is run asynchronously via
+ * g_simple_async_result_run_in_thread().
+ *
+ * This sets up a minimal #GDataPicasaWebFile entry, using the
+ * basename of the filepath for the file's title (which is not the
+ * caption, but might be something we would consider doing).  The
+ * image file and the minimal entry are then uploaded to PicasaWeb's
+ * default album of "Drop Box".  In the future, we might consider
+ * adding an Album Chooser to the Preferences/login window, but only
+ * if there's demand.
+ **/
+static void
+tmp_picasaweb_upload_async (GSimpleAsyncResult *result, GObject *source_object, GCancellable *cancellable)
+{
+	GDataPicasaWebFile *new_file = NULL;
+	EogPostasaPlugin *plugin = EOG_POSTASA_PLUGIN (source_object);
+	GDataPicasaWebFile *file_entry;
+	PicasaWebUploadFileAsyncData *data;
+	gchar *filepath;
+	gchar *filename;
+	GError *error = NULL;
+
+	data = (PicasaWebUploadFileAsyncData*)g_async_result_get_user_data (G_ASYNC_RESULT (result));
+
+	/* get filename to set image title */
+	file_entry = gdata_picasaweb_file_new (NULL);
+	filename = g_file_get_basename (data->imgfile);
+	gdata_entry_set_title (GDATA_ENTRY (file_entry), filename);
+	g_free (filename);
+
+	new_file = gdata_picasaweb_service_upload_file (plugin->priv->service, NULL /* Uploading to Drop Box */, file_entry,
+							data->imgfile, cancellable, &error);
+	g_object_unref (file_entry);
+
+	if (new_file == NULL || error) {
+		if (g_cancellable_is_cancelled (cancellable) == FALSE) {
+			g_simple_async_result_set_from_error (result, error);
+			g_clear_error (&error); /* we can clear this, because set_from_error() does a g_error_copy() */
+		}
+	} else {
+		g_simple_async_result_set_op_res_gboolean (result, TRUE);
+	}
+
+	if (new_file != NULL)
+		g_object_unref (new_file);
+}
+
+
+/**
+ * picasaweb_upload_files:
+ *
+ * This obtains the list of selected images in EoG (selected in the
+ * thumbview), sets up asynchronous uploads through
+ * tmp_picasaweb_upload_async() in their own thread and instigates
+ * them.
+
+ * This attempts to upload the selected files.  It provides a message
+ * near the end indicating the number successfully uploaded, and any
+ * error messages encountered along the way.
+ *
+ * TODO: once gdata_picasaweb_service_upload_file_async() is available
+ * from libgdata, simplify this as possible.
+ **/
+static void
+picasaweb_upload_files (EogPostasaPlugin *plugin)
+{
+	EogWindow *window;
+	GtkWidget *thumbview;
+	GList *images, *node;
+	EogImage *image;
+	GFile *imgfile;
+	GCancellable *cancellable;
+	GSimpleAsyncResult *result;
+	PicasaWebUploadFileAsyncData *data;
+
+	if (gdata_service_is_authenticated (GDATA_SERVICE (plugin->priv->service)) == FALSE) {
+		g_warning ("PicasaWeb could not be authenticated.  Aborting upload.");
+		return;
+	}
+
+	window = plugin->priv->eog_window;
+	thumbview = eog_window_get_thumb_view (window); /* do not unref */
+	images = eog_thumb_view_get_selected_images (EOG_THUMB_VIEW (thumbview)); /* need to use g_list_free() */
+
+	for (node = g_list_first (images); node != NULL; node = node->next) {
+		image = (EogImage *) node->data;
+		cancellable = g_cancellable_new (); /* TODO: this gets attached to the image's list row; free with row */
+
+		imgfile = eog_image_get_file (image); /* unref this */
+
+		data = g_slice_new0(PicasaWebUploadFileAsyncData); /* freed by picasaweb_upload_async_cb() or below */
+		data->imgfile = g_file_dup (imgfile); /* unref'd in free_picasaweb_upload_file_async_data() */
+		data->iter = uploads_add_entry (plugin, image, cancellable); /* freed with data */
+
+		if (g_file_query_exists (imgfile, cancellable)) {
+			/* TODO: want to replace much of this with gdata_picasaweb_service_upload_file_async when that's avail */
+			result = g_simple_async_result_new (G_OBJECT (plugin), (GAsyncReadyCallback)picasaweb_upload_async_cb,
+							    data, tmp_picasaweb_upload_async); /* TODO: should this be freed? where? */
+			g_simple_async_result_run_in_thread (result, tmp_picasaweb_upload_async, 0, cancellable);
+		} else {
+			/* TODO: consider setting a proper error and passing it in the data through GSimpleAsyncResult's thread */
+			gtk_list_store_set (plugin->priv->uploads_store, data->iter, 3, -1, 5, "File not found", -1);
+			free_picasaweb_upload_file_async_data (data);
+		}
+		g_object_unref (imgfile);
+	}
+	g_list_free (images);
+}
+
+/**
+ * picasaweb_login_async_cb:
+ *
+ * Handles the result of the asynchronous
+ * gdata_service_authenticate_async() operation, called by our
+ * picasaweb_login_cb().  Upon success, it switches "Cancel" to
+ * "Close".  Regardless of the response, it re-enables the Login
+ * button (which is desensitised during the login attempt).
+ **/
+static void
+picasaweb_login_async_cb (GDataPicasaWebService *service, GAsyncResult *result, EogPostasaPlugin *plugin)
+{
+	GError *error = NULL;
+	gchar *message;
+	gboolean success = FALSE;
+
+	success = gdata_service_authenticate_finish (GDATA_SERVICE (service), result, &error);
+
+	gtk_widget_set_sensitive (GTK_WIDGET (plugin->priv->login_button), TRUE);
+	gtk_widget_set_sensitive (GTK_WIDGET (plugin->priv->username_entry), TRUE);
+	gtk_widget_set_sensitive (GTK_WIDGET (plugin->priv->password_entry), TRUE);
+
+	if (success == FALSE || error != NULL) {
+		message = g_strdup_printf (N_("Login failed. %s"), error->message);
+		gtk_label_set_text (plugin->priv->login_message, message);
+		g_free (message);
+	} else {
+		gtk_label_set_text (plugin->priv->login_message, N_("Logged in successully."));
+		gtk_button_set_label (plugin->priv->cancel_button, N_("Close"));
+		login_dialog_close (plugin);
+	}
+}
+
+/**
+ * picasaweb_login_cb:
+ *
+ * Handles "clicked" for the Login button.  Attempts to use input from
+ * the username and password entries to authenticate.  It does this
+ * asynchronously, leaving it to @picasaweb_login_async_cb to handle
+ * the result.  It disables the Login button while authenticating
+ * (re-enabled when done) and ensures the Close/Cancel button says
+ * Cancel (which, incidentally, will call g_cancellable_cancel on the
+ * provided #GCancellable.
+ **/
+static void
+picasaweb_login_cb (GtkWidget *login_button, gpointer _plugin)
+{
+	EogPostasaPlugin *plugin = EOG_POSTASA_PLUGIN (_plugin);
+
+	gtk_button_set_label (plugin->priv->cancel_button, N_("Cancel"));
+	gtk_widget_set_sensitive (login_button, FALSE);
+	gtk_widget_set_sensitive (GTK_WIDGET (plugin->priv->username_entry), FALSE);
+	gtk_widget_set_sensitive (GTK_WIDGET (plugin->priv->password_entry), FALSE);
+
+	/* TODO: want to handle passwords more securely */
+	gtk_label_set_text (plugin->priv->login_message, N_("Logging in..."));
+	g_cancellable_reset (plugin->priv->login_cancellable);
+	gdata_service_authenticate_async (GDATA_SERVICE (plugin->priv->service),
+					  gtk_entry_get_text (plugin->priv->username_entry),
+					  gtk_entry_get_text (plugin->priv->password_entry),
+					  plugin->priv->login_cancellable, (GAsyncReadyCallback)picasaweb_login_async_cb, plugin);
+}
+
+/**
+ * picasaweb_upload_cb:
+ *
+ * This checks that we are authenticated (popping up the login window
+ * if we're not) and, if we are, moves on to upload the files.
+ **/
+static void
+picasaweb_upload_cb (GtkAction	*action,
+		     EogWindow *window)
+{
+	WindowData *data = g_object_get_data (G_OBJECT (window), WINDOW_DATA_KEY);
+	EogPostasaPlugin *plugin = data->plugin;
+
+	if (gdata_service_is_authenticated (GDATA_SERVICE (plugin->priv->service)) == TRUE) {
+		picasaweb_upload_files (plugin);
+	} else {
+		/* when the dialog closes, it checks if this is set to see if it should upload anything */
+		plugin->priv->uploads_pending = TRUE;
+
+		login_get_dialog (plugin);
+		gtk_label_set_text (plugin->priv->login_message, N_("Please log in to continue upload."));
+		gtk_window_present (GTK_WINDOW (plugin->priv->login_dialog));
+	}
+}
+
+
+/*** Login Dialog ***/
+
+/**
+ * login_dialog_close:
+ *
+ * Closes the login dialog, used for closing the window or cancelling
+ * login.  This will also cancel any authentication in progress.  If
+ * the login dialog was prompted by an upload attempt, it will resume
+ * the upload attempt.
+ **/
+static gboolean
+login_dialog_close (EogPostasaPlugin *plugin)
+{
+	/* abort the authentication attempt if in progress and we're cancelling */
+	g_cancellable_cancel (plugin->priv->login_cancellable);
+	gtk_widget_hide (GTK_WIDGET (plugin->priv->login_dialog));
+
+	if (plugin->priv->uploads_pending == TRUE) {
+		plugin->priv->uploads_pending = FALSE;
+		picasaweb_upload_files (plugin);
+	}
+
+	return TRUE;
+}
+
+/**
+ * login_dialog_cancel_button_cb:
+ *
+ * Handles clicks on the Cancel/Close button.
+ **/
+static gboolean
+login_dialog_cancel_button_cb (GtkWidget *cancel_button, EogPostasaPlugin *plugin)
+{
+	return login_dialog_close (plugin);
+}
+
+/**
+ * login_dialog_delete_event_cb:
+ *
+ * Handles other attempts to close the dialog. (e.g. window manager)
+ **/
+static gboolean
+login_dialog_delete_event_cb (GtkWidget *widget, GdkEvent *event, gpointer *_plugin)
+{
+	return login_dialog_close (EOG_POSTASA_PLUGIN (_plugin));
+}
+
+/**
+ * login_get_dialog:
+ *
+ * Retrieves the login dialog.  If it has not yet been constructed, it
+ * does so.  If the user is already authenticated, it populates the
+ * username and password boxes with the relevant values.
+ **/
+static GtkWidget *
+login_get_dialog (EogPostasaPlugin *plugin)
+{
+	GtkBuilder *builder;
+	GError *error = NULL;
+
+	if (plugin->priv->login_dialog == NULL) {
+		builder = gtk_builder_new ();
+		gtk_builder_add_from_file (builder, GTKBUILDER_CONFIG_FILE, &error);
+		if (error != NULL) {
+			g_warning ("Couldn't load Postasa configuration UI file:%d:%s", error->code, error->message);
+			g_error_free (error);
+		}
+
+		/* do not unref gtk_builder_get_object() returns */
+		plugin->priv->username_entry = GTK_ENTRY  (gtk_builder_get_object (builder, "username_entry"));
+		plugin->priv->password_entry = GTK_ENTRY  (gtk_builder_get_object (builder, "password_entry"));
+		plugin->priv->login_dialog   = GTK_DIALOG (gtk_builder_get_object (builder, "postasa_login_dialog"));
+		plugin->priv->cancel_button  = GTK_BUTTON (gtk_builder_get_object (builder, "cancel_button"));
+		plugin->priv->login_button   = GTK_BUTTON (gtk_builder_get_object (builder, "login_button"));
+		plugin->priv->login_message  = GTK_LABEL  (gtk_builder_get_object (builder, "login_message"));
+
+		g_object_unref (builder);
+
+		g_signal_connect (G_OBJECT (plugin->priv->login_button),  "clicked", G_CALLBACK (picasaweb_login_cb),     plugin);
+		g_signal_connect (G_OBJECT (plugin->priv->cancel_button), "clicked", G_CALLBACK (login_dialog_cancel_button_cb), plugin);
+		g_signal_connect (G_OBJECT (plugin->priv->login_dialog), "delete-event", G_CALLBACK (login_dialog_delete_event_cb), plugin);
+
+		if (gdata_service_is_authenticated (GDATA_SERVICE (plugin->priv->service))) {
+			gtk_entry_set_text (plugin->priv->username_entry, gdata_service_get_username (GDATA_SERVICE (plugin->priv->service)));
+			gtk_entry_set_text (plugin->priv->password_entry, gdata_service_get_password (GDATA_SERVICE (plugin->priv->service)));
+		}
+	}
+
+	return GTK_WIDGET (plugin->priv->login_dialog);
+}
+
+
+/*** EogPlugin Functions ***/
+
+/**
+ * free_window_data:
+ *
+ * This handles destruction of the #WindowData we define for this plugin.
+ **/
+static void
+free_window_data (WindowData *data)
+{
+	eog_debug (DEBUG_PLUGINS);
+
+	g_return_if_fail (data != NULL);
+	g_object_unref (data->ui_action_group);
+	g_free (data);
+}
+
+/**
+ * impl_create_config_dialog:
+ *
+ * Plugin hook for obtaining the configure/preferences dialog.  In
+ * this case, it's just our Login dialog.  In the future, it might
+ * include an album chooser, if there's demand.
+ **/
+static GtkWidget *
+impl_create_config_dialog (EogPlugin *_plugin)
+{
+	EogPostasaPlugin *plugin = EOG_POSTASA_PLUGIN (_plugin);
+	return GTK_WIDGET (login_get_dialog (plugin));
+}
+
+/**
+ * impl_activate:
+ *
+ * Plugin hook for plugin activation.  Creates #WindowData for the
+ * #EogPostasaPlugin that gets associated with the window and defines
+ * some UI.
+ **/
+static void
+impl_activate (EogPlugin *_plugin,
+	       EogWindow *window)
+{
+	GtkUIManager *manager;
+	WindowData *data;
+	EogPostasaPlugin *plugin = EOG_POSTASA_PLUGIN (_plugin);
+
+	eog_debug (DEBUG_PLUGINS);
+
+	plugin->priv->eog_window = window;
+
+	data = g_new (WindowData, 1); /* free'd by free_window_data() when window object is destroyed */
+	data->plugin = EOG_POSTASA_PLUGIN (plugin); /* circular references, fun */
+	data->ui_action_group = gtk_action_group_new ("EogPostasaPluginActions"); /* freed with WindowData in free_window_data() */
+	gtk_action_group_set_translation_domain (data->ui_action_group, GETTEXT_PACKAGE);
+	gtk_action_group_add_actions (data->ui_action_group, action_entries, G_N_ELEMENTS (action_entries), window);
+
+	manager = eog_window_get_ui_manager (window); /* do not unref */
+	gtk_ui_manager_insert_action_group (manager, data->ui_action_group, -1);
+	data->ui_id = gtk_ui_manager_new_merge_id (manager);
+	gtk_ui_manager_add_ui (manager, data->ui_id, MENU_PATH, "RunPostasa", "RunPostasa", GTK_UI_MANAGER_MENUITEM, FALSE);
+
+	g_object_set_data_full (G_OBJECT (window), WINDOW_DATA_KEY, data, (GDestroyNotify) free_window_data);
+}
+
+/**
+ * impl_deactivate:
+ *
+ * Plugin hook for plugin deactivation. Removes UI and #WindowData
+ **/
+static void
+impl_deactivate	(EogPlugin *plugin,
+		 EogWindow *window)
+{
+	GtkUIManager *manager;
+	WindowData *data;
+
+	eog_debug (DEBUG_PLUGINS);
+
+	manager = eog_window_get_ui_manager (window);
+
+	data = (WindowData *) g_object_get_data (G_OBJECT (window), WINDOW_DATA_KEY);
+	g_return_if_fail (data != NULL);
+
+	gtk_ui_manager_remove_ui (manager, data->ui_id);
+	gtk_ui_manager_remove_action_group (manager, data->ui_action_group);
+
+	g_object_set_data (G_OBJECT (window), WINDOW_DATA_KEY, NULL); /* TODO: does doing this interfere with the GDestroyNotify
+									 set in impl_activate()?  It's how it was done in Postr... */
+}
+
+/**
+ * impl_update_ui:
+ *
+ * Plugin hook for updating the UI.  I don't think we do anything of use in here.
+ **/
+static void
+impl_update_ui (EogPlugin *plugin,
+		EogWindow *window)
+{
+}
+
+
+/*** GObject Functions ***/
+
+/**
+ * eog_postasa_plugin_init:
+ *
+ * Object initialisation method.  Sets up the (unauthenticated)
+ * PicasaWeb service, a #GCancellable for login, and sets the
+ * uploads_pending flag to %FALSE.
+ **/
+static void
+eog_postasa_plugin_init (EogPostasaPlugin *plugin)
+{
+	eog_debug_message (DEBUG_PLUGINS, "EogPostasaPlugin initializing");
+
+	plugin->priv = G_TYPE_INSTANCE_GET_PRIVATE (plugin, EOG_TYPE_POSTASA_PLUGIN, EogPostasaPluginPrivate);
+	plugin->priv->service = gdata_picasaweb_service_new ("EogPostasa"); /* unref'd in eog_postasa_plugin_finalize() */
+	plugin->priv->login_cancellable = g_cancellable_new (); /* unref'd in eog_postasa_plugin_finalize() */
+	plugin->priv->uploads_pending = FALSE;
+}
+
+/**
+ * eog_postasa_plugin_finalize:
+ *
+ * Cleans up the #EogPostasaPlugin object, unref'ing its #GDataPicasaWebService and #GCancellable.
+ **/
+static void
+eog_postasa_plugin_finalize (GObject *_plugin)
+{
+	EogPostasaPlugin *plugin = EOG_POSTASA_PLUGIN (_plugin);
+
+	eog_debug_message (DEBUG_PLUGINS, "EogPostasaPlugin finalizing");
+
+	g_object_unref (plugin->priv->service);
+	g_object_unref (plugin->priv->login_cancellable);
+	if (G_IS_OBJECT (plugin->priv->uploads_store))
+		/* we check in case the upload window was never created */
+		g_object_unref (plugin->priv->uploads_store);
+
+	G_OBJECT_CLASS (eog_postasa_plugin_parent_class)->finalize (_plugin);
+}
+
+/**
+ * eog_postasa_plugin_class_init:
+ *
+ * Plugin class initialisation.  Binds class hooks to actual implementations.
+ **/
+static void
+eog_postasa_plugin_class_init (EogPostasaPluginClass *klass)
+{
+	GObjectClass *object_class = G_OBJECT_CLASS (klass);
+	EogPluginClass *plugin_class = EOG_PLUGIN_CLASS (klass);
+
+	g_type_class_add_private (klass, sizeof (EogPostasaPluginPrivate));
+
+	object_class->finalize = eog_postasa_plugin_finalize;
+
+	plugin_class->activate = impl_activate;
+	plugin_class->deactivate = impl_deactivate;
+	plugin_class->update_ui = impl_update_ui;
+	plugin_class->create_configure_dialog = impl_create_config_dialog;
+}
diff --git a/plugins/postasa/eog-postasa-plugin.h b/plugins/postasa/eog-postasa-plugin.h
new file mode 100644
index 0000000..06e81e5
--- /dev/null
+++ b/plugins/postasa/eog-postasa-plugin.h
@@ -0,0 +1,78 @@
+/* -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 8; tab-width: 8 -*- */
+
+/* Postasa -- PicasaWeb Uploader plugin
+ *
+ * Copyright (C) 2009 The Free Software Foundation
+ *
+ * Author: Richard Schwarting <aquarichy gmail com>
+ * Initially based on Postr code by: Lucas Rocha
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
+ */
+
+#ifndef __EOG_POSTASA_PLUGIN_H__
+#define __EOG_POSTASA_PLUGIN_H__
+
+#include <glib.h>
+#include <glib-object.h>
+#include <eog/eog-plugin.h>
+
+G_BEGIN_DECLS
+
+/*
+ * Type checking and casting macros
+ */
+#define EOG_TYPE_POSTASA_PLUGIN		(eog_postasa_plugin_get_type ())
+#define EOG_POSTASA_PLUGIN(o)		(G_TYPE_CHECK_INSTANCE_CAST ((o), EOG_TYPE_POSTASA_PLUGIN, EogPostasaPlugin))
+#define EOG_POSTASA_PLUGIN_CLASS(k)	(G_TYPE_CHECK_CLASS_CAST((k),     EOG_TYPE_POSTASA_PLUGIN, EogPostasaPluginClass))
+#define EOG_IS_POSTASA_PLUGIN(o)	        (G_TYPE_CHECK_INSTANCE_TYPE ((o), EOG_TYPE_POSTASA_PLUGIN))
+#define EOG_IS_POSTASA_PLUGIN_CLASS(k)	(G_TYPE_CHECK_CLASS_TYPE ((k),    EOG_TYPE_POSTASA_PLUGIN))
+#define EOG_POSTASA_PLUGIN_GET_CLASS(o)	(G_TYPE_INSTANCE_GET_CLASS ((o),  EOG_TYPE_POSTASA_PLUGIN, EogPostasaPluginClass))
+
+/* Private structure type */
+typedef struct _EogPostasaPluginPrivate	EogPostasaPluginPrivate;
+
+/*
+ * Main object structure
+ */
+typedef struct _EogPostasaPlugin		EogPostasaPlugin;
+
+struct _EogPostasaPlugin
+{
+	EogPlugin parent_instance;
+	EogPostasaPluginPrivate *priv;
+};
+
+/*
+ * Class definition
+ */
+typedef struct _EogPostasaPluginClass	EogPostasaPluginClass;
+
+struct _EogPostasaPluginClass
+{
+	EogPluginClass parent_class;
+};
+
+/*
+ * Public methods
+ */
+GType	eog_postasa_plugin_get_type		(void) G_GNUC_CONST;
+
+/* All the plugins must implement this function */
+G_MODULE_EXPORT GType register_eog_plugin (GTypeModule *module);
+
+G_END_DECLS
+
+#endif /* __EOG_POSTASA_PLUGIN_H__ */
diff --git a/plugins/postasa/postasa-config.xml b/plugins/postasa/postasa-config.xml
new file mode 100644
index 0000000..4eee067
--- /dev/null
+++ b/plugins/postasa/postasa-config.xml
@@ -0,0 +1,155 @@
+<?xml version="1.0"?>
+<interface>
+  <requires lib="gtk+" version="2.16"/>
+  <!-- interface-naming-policy project-wide -->
+  <object class="GtkDialog" id="postasa_login_dialog">
+    <property name="height_request">150</property>
+    <property name="border_width">5</property>
+    <property name="type_hint">normal</property>
+    <property name="has_separator">False</property>
+    <child internal-child="vbox">
+      <object class="GtkVBox" id="dialog_vbox">
+        <property name="visible">True</property>
+        <property name="orientation">vertical</property>
+        <property name="spacing">2</property>
+        <child>
+          <object class="GtkVBox" id="login_box">
+            <property name="visible">True</property>
+            <property name="orientation">vertical</property>
+            <child>
+              <object class="GtkLabel" id="title">
+                <property name="visible">True</property>
+                <property name="ypad">6</property>
+                <property name="label" translatable="yes">PicasaWeb Login</property>
+              </object>
+              <packing>
+                <property name="position">0</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkHBox" id="username_hbox">
+                <property name="width_request">300</property>
+                <property name="visible">True</property>
+                <child>
+                  <object class="GtkLabel" id="username_label">
+                    <property name="width_request">60</property>
+                    <property name="visible">True</property>
+                    <property name="label" translatable="yes">Username</property>
+                  </object>
+                  <packing>
+                    <property name="position">0</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkEntry" id="username_entry">
+                    <property name="width_request">240</property>
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                    <property name="invisible_char">&#x25CF;</property>
+                  </object>
+                  <packing>
+                    <property name="position">1</property>
+                  </packing>
+                </child>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">False</property>
+                <property name="position">1</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkHBox" id="password_hbox">
+                <property name="width_request">300</property>
+                <property name="visible">True</property>
+                <child>
+                  <object class="GtkLabel" id="password_label">
+                    <property name="width_request">60</property>
+                    <property name="visible">True</property>
+                    <property name="label" translatable="yes">Password</property>
+                  </object>
+                  <packing>
+                    <property name="position">0</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkEntry" id="password_entry">
+                    <property name="width_request">240</property>
+                    <property name="visible">True</property>
+                    <property name="can_focus">True</property>
+                    <property name="visibility">False</property>
+                    <property name="invisible_char">&#x25CF;</property>
+                    <property name="invisible_char_set">True</property>
+                  </object>
+                  <packing>
+                    <property name="position">1</property>
+                  </packing>
+                </child>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">False</property>
+                <property name="position">2</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkLabel" id="login_message">
+                <property name="visible">True</property>
+                <property name="ypad">7</property>
+              </object>
+              <packing>
+                <property name="position">3</property>
+              </packing>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="fill">False</property>
+            <property name="position">1</property>
+          </packing>
+        </child>
+        <child internal-child="action_area">
+          <object class="GtkHButtonBox" id="button_box">
+            <property name="visible">True</property>
+            <property name="layout_style">end</property>
+            <child>
+              <object class="GtkButton" id="cancel_button">
+                <property name="label" translatable="yes">Cancel</property>
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="receives_default">True</property>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">False</property>
+                <property name="position">0</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkButton" id="login_button">
+                <property name="label" translatable="yes">Login</property>
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="receives_default">True</property>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">False</property>
+                <property name="position">1</property>
+              </packing>
+            </child>
+          </object>
+          <packing>
+            <property name="expand">False</property>
+            <property name="pack_type">end</property>
+            <property name="position">0</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+    <action-widgets>
+      <action-widget response="0">cancel_button</action-widget>
+      <action-widget response="-1">login_button</action-widget>
+    </action-widgets>
+  </object>
+</interface>
diff --git a/plugins/postasa/postasa-uploads.xml b/plugins/postasa/postasa-uploads.xml
new file mode 100644
index 0000000..0c9db90
--- /dev/null
+++ b/plugins/postasa/postasa-uploads.xml
@@ -0,0 +1,171 @@
+<?xml version="1.0"?>
+<interface>
+  <requires lib="gtk+" version="2.16"/>
+  <!-- interface-naming-policy project-wide -->
+  <object class="GtkListStore" id="uploads_store">
+    <columns>
+      <!-- column-name thumbnail -->
+      <column type="GdkPixbuf"/>
+      <!-- column-name filepath -->
+      <column type="gchararray"/>
+      <!-- column-name size -->
+      <column type="gchararray"/>
+      <!-- column-name upload_status -->
+      <column type="guint"/>
+      <!-- column-name cancellable -->
+      <column type="GObject"/>
+      <!-- column-name upload_message -->
+      <column type="gchararray"/>
+    </columns>
+  </object>
+  <object class="GtkWindow" id="uploads_window">
+    <property name="width_request">550</property>
+    <property name="height_request">250</property>
+    <child>
+      <object class="GtkHBox" id="hbox1">
+        <property name="visible">True</property>
+        <child>
+          <object class="GtkVBox" id="vbox2">
+            <property name="visible">True</property>
+            <property name="orientation">vertical</property>
+            <child>
+              <object class="GtkLabel" id="uploads_label">
+                <property name="height_request">0</property>
+                <property name="visible">True</property>
+                <property name="xalign">0</property>
+                <property name="ypad">10</property>
+                <property name="label" translatable="yes">Uploads:</property>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">False</property>
+                <property name="position">0</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkScrolledWindow" id="scrolledwindow1">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="hscrollbar_policy">never</property>
+                <property name="vscrollbar_policy">automatic</property>
+                <child>
+                  <object class="GtkViewport" id="viewport1">
+                    <property name="visible">True</property>
+                    <property name="resize_mode">queue</property>
+                    <child>
+                      <object class="GtkTreeView" id="uploads_view">
+                        <property name="visible">True</property>
+                        <property name="can_focus">True</property>
+                        <property name="model">uploads_store</property>
+                        <child>
+                          <object class="GtkTreeViewColumn" id="file_col">
+                            <property name="title">File</property>
+                            <property name="expand">True</property>
+                            <child>
+                              <object class="GtkCellRendererPixbuf" id="thumbnail_cell"/>
+                              <attributes>
+                                <attribute name="pixbuf">0</attribute>
+                              </attributes>
+                            </child>
+                            <child>
+                              <object class="GtkCellRendererText" id="filepath_cell">
+                                <property name="ellipsize">start</property>
+                              </object>
+                              <attributes>
+                                <attribute name="text">1</attribute>
+                              </attributes>
+                            </child>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkTreeViewColumn" id="size_col">
+                            <property name="sizing">fixed</property>
+                            <property name="fixed_width">60</property>
+                            <property name="title">Size</property>
+                            <child>
+                              <object class="GtkCellRendererText" id="size_cell"/>
+                              <attributes>
+                                <attribute name="text">2</attribute>
+                              </attributes>
+                            </child>
+                          </object>
+                        </child>
+                        <child>
+                          <object class="GtkTreeViewColumn" id="progress_col">
+                            <property name="min_width">160</property>
+                            <property name="title">Status</property>
+                            <child>
+                              <object class="GtkCellRendererProgress" id="progress_cell"/>
+                              <attributes>
+                                <attribute name="pulse">3</attribute>
+                                <attribute name="text">5</attribute>
+                              </attributes>
+                            </child>
+                          </object>
+                        </child>
+                      </object>
+                    </child>
+                  </object>
+                </child>
+              </object>
+              <packing>
+                <property name="position">1</property>
+              </packing>
+            </child>
+            <child>
+              <object class="GtkHBox" id="hbox2">
+                <property name="visible">True</property>
+                <child>
+                  <object class="GtkHButtonBox" id="hbuttonbox1">
+                    <property name="visible">True</property>
+                    <property name="spacing">10</property>
+                    <property name="layout_style">end</property>
+                    <child>
+                      <object class="GtkButton" id="cancel_button">
+                        <property name="label" translatable="yes">Cancel Selected</property>
+                        <property name="visible">True</property>
+                        <property name="can_focus">True</property>
+                        <property name="receives_default">True</property>
+                      </object>
+                      <packing>
+                        <property name="expand">False</property>
+                        <property name="fill">False</property>
+                        <property name="position">0</property>
+                      </packing>
+                    </child>
+                    <child>
+                      <object class="GtkButton" id="cancel_all_button">
+                        <property name="label" translatable="yes">Cancel All</property>
+                        <property name="visible">True</property>
+                        <property name="can_focus">True</property>
+                        <property name="receives_default">True</property>
+                      </object>
+                      <packing>
+                        <property name="expand">False</property>
+                        <property name="fill">False</property>
+                        <property name="position">1</property>
+                      </packing>
+                    </child>
+                  </object>
+                  <packing>
+                    <property name="position">0</property>
+                  </packing>
+                </child>
+              </object>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">False</property>
+                <property name="padding">10</property>
+                <property name="position">2</property>
+              </packing>
+            </child>
+          </object>
+          <packing>
+            <property name="padding">10</property>
+            <property name="position">0</property>
+          </packing>
+        </child>
+      </object>
+    </child>
+  </object>
+</interface>
diff --git a/plugins/postasa/postasa.eog-plugin.desktop.in b/plugins/postasa/postasa.eog-plugin.desktop.in
new file mode 100644
index 0000000..51f1080
--- /dev/null
+++ b/plugins/postasa/postasa.eog-plugin.desktop.in
@@ -0,0 +1,9 @@
+[Eog Plugin]
+Module=postasa
+IAge=2
+_Name=PicasaWeb Uploader
+Icon=postasa
+_Description=Upload your pictures to PicasaWeb
+Authors=Richard Schwarting <aquarichy gmail com>
+Copyright=Copyright © 2009 Richard Schwarting
+Website=http://www.gnome.org/projects/eog



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]