f-spot r4369 - in trunk: . extensions extensions/BeagleService extensions/CDExport extensions/ChangePhotoPath extensions/DBusService extensions/DefaultExporters extensions/DevelopInUFraw extensions/Editors extensions/Editors/FlipEditor extensions/Editors/ResizeEditor extensions/Exporters extensions/Exporters/CDExport extensions/Exporters/DefaultExporters extensions/Exporters/FacebookExport extensions/Exporters/FacebookExport/Mono.Facebook extensions/Exporters/FlickrExport extensions/Exporters/FlickrExport/FlickrNet extensions/Exporters/FolderExport extensions/Exporters/GalleryExport extensions/Exporters/PicasaWebExport extensions/Exporters/PicasaWebExport/google-sharp extensions/Exporters/SmugMugExport extensions/Exporters/SmugMugExport/SmugMugNet extensions/Exporters/TabbloExport extensions/Exporters/TabbloExport/Tabblo extensions/FacebookExport extensions/FacebookExport/Mono.Facebook extensions/FlickrExport extensions/FlickrExport/FlickrNet extensions/FlipEditor ex tensions/FolderExport extensions/GalleryExport extensions/LightTable extensions/MergeDb extensions/MetaPixel extensions/Misc extensions/Misc/LightTable extensions/PicasaWebExport extensions/PicasaWebExport/google-sharp extensions/PictureTile extensions/RawPlusJpeg extensions/ResizeEditor extensions/RetroactiveRoll extensions/Services extensions/Services/BeagleService extensions/Services/DBusService extensions/SmugMugExport extensions/SmugMugExport/SmugMugNet extensions/SyncCatalog extensions/TabbloExport extensions/TabbloExport/Tabblo extensions/Tools extensions/Tools/ChangePhotoPath extensions/Tools/DevelopInUFraw extensions/Tools/MergeDb extensions/Tools/MetaPixel extensions/Tools/PictureTile extensions/Tools/RawPlusJpeg extensions/Tools/RetroactiveRoll extensions/Tools/SyncCatalog extensions/Tools/ZipExport extensions/ZipExport



Author: sdelcroix
Date: Tue Sep 16 13:11:27 2008
New Revision: 4369
URL: http://svn.gnome.org/viewvc/f-spot?rev=4369&view=rev

Log:
2008-09-16  Stephane Delcroix  <sdelcroix*novell.com>

	Split the addins in Editors, Exporters, Misc, Services, Tools.
	From now on, use the subfolder ChangeLog.


Added:
   trunk/extensions/Editors/
   trunk/extensions/Editors/ChangeLog
   trunk/extensions/Editors/FlipEditor/
   trunk/extensions/Editors/FlipEditor/.gitignore
      - copied, changed from r4368, /trunk/extensions/FlipEditor/.gitignore
   trunk/extensions/Editors/FlipEditor/FlipEditor.addin.xml
   trunk/extensions/Editors/FlipEditor/FlipEditor.cs
      - copied, changed from r4368, /trunk/extensions/FlipEditor/FlipEditor.cs
   trunk/extensions/Editors/FlipEditor/Makefile
      - copied, changed from r4368, /trunk/extensions/FlipEditor/Makefile
   trunk/extensions/Editors/ResizeEditor/
   trunk/extensions/Editors/ResizeEditor/.gitignore
      - copied, changed from r4368, /trunk/extensions/ResizeEditor/.gitignore
   trunk/extensions/Editors/ResizeEditor/Makefile
      - copied, changed from r4368, /trunk/extensions/ResizeEditor/Makefile
   trunk/extensions/Editors/ResizeEditor/ResizeEditor.addin.xml
      - copied, changed from r4368, /trunk/extensions/ResizeEditor/ResizeEditor.addin.xml
   trunk/extensions/Editors/ResizeEditor/ResizeEditor.cs
      - copied, changed from r4368, /trunk/extensions/ResizeEditor/ResizeEditor.cs
   trunk/extensions/Exporters/
   trunk/extensions/Exporters/.gitignore
   trunk/extensions/Exporters/CDExport/
   trunk/extensions/Exporters/CDExport/.gitignore
      - copied, changed from r4368, /trunk/extensions/BeagleService/.gitignore
   trunk/extensions/Exporters/CDExport/CDExport.addin.xml
      - copied, changed from r4368, /trunk/extensions/CDExport/CDExport.addin.xml
   trunk/extensions/Exporters/CDExport/CDExport.cs
   trunk/extensions/Exporters/CDExport/CDExport.glade
      - copied, changed from r4368, /trunk/extensions/CDExport/CDExport.glade
   trunk/extensions/Exporters/CDExport/Makefile.am
      - copied, changed from r4368, /trunk/extensions/CDExport/Makefile.am
   trunk/extensions/Exporters/ChangeLog
   trunk/extensions/Exporters/DefaultExporters/
   trunk/extensions/Exporters/DefaultExporters/.gitignore
      - copied, changed from r4368, /trunk/extensions/CDExport/.gitignore
   trunk/extensions/Exporters/DefaultExporters/DefaultExporters.addin.xml
      - copied, changed from r4368, /trunk/extensions/DefaultExporters/DefaultExporters.addin.xml
   trunk/extensions/Exporters/DefaultExporters/Makefile.am
      - copied, changed from r4368, /trunk/extensions/DefaultExporters/Makefile.am
   trunk/extensions/Exporters/FacebookExport/
   trunk/extensions/Exporters/FacebookExport/.gitignore
      - copied, changed from r4368, /trunk/extensions/FacebookExport/.gitignore
   trunk/extensions/Exporters/FacebookExport/FacebookExport.addin.xml
      - copied, changed from r4368, /trunk/extensions/FacebookExport/FacebookExport.addin.xml
   trunk/extensions/Exporters/FacebookExport/FacebookExport.cs
   trunk/extensions/Exporters/FacebookExport/FacebookExport.glade
      - copied, changed from r4368, /trunk/extensions/FacebookExport/FacebookExport.glade
   trunk/extensions/Exporters/FacebookExport/Makefile
   trunk/extensions/Exporters/FacebookExport/Mono.Facebook/
   trunk/extensions/Exporters/FacebookExport/Mono.Facebook/Album.cs
   trunk/extensions/Exporters/FacebookExport/Mono.Facebook/AssemblyInfo.cs
   trunk/extensions/Exporters/FacebookExport/Mono.Facebook/Error.cs
   trunk/extensions/Exporters/FacebookExport/Mono.Facebook/Event.cs
   trunk/extensions/Exporters/FacebookExport/Mono.Facebook/FacebookException.cs
   trunk/extensions/Exporters/FacebookExport/Mono.Facebook/FacebookParam.cs
   trunk/extensions/Exporters/FacebookExport/Mono.Facebook/FacebookSession.cs
   trunk/extensions/Exporters/FacebookExport/Mono.Facebook/Friend.cs
   trunk/extensions/Exporters/FacebookExport/Mono.Facebook/FriendInfo.cs
   trunk/extensions/Exporters/FacebookExport/Mono.Facebook/Group.cs
   trunk/extensions/Exporters/FacebookExport/Mono.Facebook/Location.cs
   trunk/extensions/Exporters/FacebookExport/Mono.Facebook/Notification.cs
   trunk/extensions/Exporters/FacebookExport/Mono.Facebook/PeopleList.cs
   trunk/extensions/Exporters/FacebookExport/Mono.Facebook/Photo.cs
   trunk/extensions/Exporters/FacebookExport/Mono.Facebook/Responses.cs
   trunk/extensions/Exporters/FacebookExport/Mono.Facebook/SessionWrapper.cs
   trunk/extensions/Exporters/FacebookExport/Mono.Facebook/Tag.cs
   trunk/extensions/Exporters/FacebookExport/Mono.Facebook/User.cs
   trunk/extensions/Exporters/FacebookExport/Mono.Facebook/Util.cs
   trunk/extensions/Exporters/FlickrExport/
   trunk/extensions/Exporters/FlickrExport/.gitignore
      - copied, changed from r4368, /trunk/extensions/DBusService/.gitignore
   trunk/extensions/Exporters/FlickrExport/FlickrExport.addin.xml
      - copied, changed from r4368, /trunk/extensions/FlickrExport/FlickrExport.addin.xml
   trunk/extensions/Exporters/FlickrExport/FlickrExport.cs
   trunk/extensions/Exporters/FlickrExport/FlickrExport.glade
      - copied, changed from r4368, /trunk/extensions/FlickrExport/FlickrExport.glade
   trunk/extensions/Exporters/FlickrExport/FlickrNet/
   trunk/extensions/Exporters/FlickrExport/FlickrNet/.gitignore
      - copied, changed from r4368, /trunk/extensions/DefaultExporters/.gitignore
   trunk/extensions/Exporters/FlickrExport/FlickrNet/ActivityEvent.cs
   trunk/extensions/Exporters/FlickrExport/FlickrNet/ActivityItem.cs
   trunk/extensions/Exporters/FlickrExport/FlickrNet/ApiKeyRequiredException.cs
   trunk/extensions/Exporters/FlickrExport/FlickrNet/AssemblyInfo.cs
   trunk/extensions/Exporters/FlickrExport/FlickrNet/Auth.cs
   trunk/extensions/Exporters/FlickrExport/FlickrNet/AuthenticationRequiredException.cs
   trunk/extensions/Exporters/FlickrExport/FlickrNet/Blogs.cs
   trunk/extensions/Exporters/FlickrExport/FlickrNet/BoundaryBox.cs
   trunk/extensions/Exporters/FlickrExport/FlickrNet/Cache.cs
   trunk/extensions/Exporters/FlickrExport/FlickrNet/Categories.cs
   trunk/extensions/Exporters/FlickrExport/FlickrNet/Comments.cs
   trunk/extensions/Exporters/FlickrExport/FlickrNet/Contacts.cs
   trunk/extensions/Exporters/FlickrExport/FlickrNet/Context.cs
   trunk/extensions/Exporters/FlickrExport/FlickrNet/DateGranularity.cs
   trunk/extensions/Exporters/FlickrExport/FlickrNet/Enums.cs
   trunk/extensions/Exporters/FlickrExport/FlickrNet/ExifPhoto.cs
   trunk/extensions/Exporters/FlickrExport/FlickrNet/Flickr.cs
   trunk/extensions/Exporters/FlickrExport/FlickrNet/FlickrApiException.cs
   trunk/extensions/Exporters/FlickrExport/FlickrNet/FlickrConfigurationManager.cs
   trunk/extensions/Exporters/FlickrExport/FlickrNet/FlickrConfigurationSettings.cs
   trunk/extensions/Exporters/FlickrExport/FlickrNet/FlickrException.cs
   trunk/extensions/Exporters/FlickrExport/FlickrNet/FlickrWebException.cs
   trunk/extensions/Exporters/FlickrExport/FlickrNet/GeoAccuracy.cs
   trunk/extensions/Exporters/FlickrExport/FlickrNet/GeoPermissions.cs
   trunk/extensions/Exporters/FlickrExport/FlickrNet/GroupSearchResults.cs
   trunk/extensions/Exporters/FlickrExport/FlickrNet/Groups.cs
   trunk/extensions/Exporters/FlickrExport/FlickrNet/License.txt
   trunk/extensions/Exporters/FlickrExport/FlickrNet/Licenses.cs
   trunk/extensions/Exporters/FlickrExport/FlickrNet/LockFile.cs
   trunk/extensions/Exporters/FlickrExport/FlickrNet/Makefile.am
   trunk/extensions/Exporters/FlickrExport/FlickrNet/Methods.cs
   trunk/extensions/Exporters/FlickrExport/FlickrNet/PartialSearchOptions.cs
   trunk/extensions/Exporters/FlickrExport/FlickrNet/PersistentCache.cs
   trunk/extensions/Exporters/FlickrExport/FlickrNet/Person.cs
   trunk/extensions/Exporters/FlickrExport/FlickrNet/Photo.cs
   trunk/extensions/Exporters/FlickrExport/FlickrNet/PhotoCounts.cs
   trunk/extensions/Exporters/FlickrExport/FlickrNet/PhotoDates.cs
   trunk/extensions/Exporters/FlickrExport/FlickrNet/PhotoInfo.cs
   trunk/extensions/Exporters/FlickrExport/FlickrNet/PhotoLocation.cs
   trunk/extensions/Exporters/FlickrExport/FlickrNet/PhotoPermissions.cs
   trunk/extensions/Exporters/FlickrExport/FlickrNet/PhotoSearchExtras.cs
   trunk/extensions/Exporters/FlickrExport/FlickrNet/PhotoSearchOptions.cs
   trunk/extensions/Exporters/FlickrExport/FlickrNet/PhotoSearchOrder.cs
   trunk/extensions/Exporters/FlickrExport/FlickrNet/PhotoSets.cs
   trunk/extensions/Exporters/FlickrExport/FlickrNet/Photos.cs
   trunk/extensions/Exporters/FlickrExport/FlickrNet/Response.cs
   trunk/extensions/Exporters/FlickrExport/FlickrNet/ResponseXmlException.cs
   trunk/extensions/Exporters/FlickrExport/FlickrNet/SafeNativeMethods.cs
   trunk/extensions/Exporters/FlickrExport/FlickrNet/SignatureRequiredException.cs
   trunk/extensions/Exporters/FlickrExport/FlickrNet/Sizes.cs
   trunk/extensions/Exporters/FlickrExport/FlickrNet/Tags.cs
   trunk/extensions/Exporters/FlickrExport/FlickrNet/TestAuthFlickr.cs
   trunk/extensions/Exporters/FlickrExport/FlickrNet/UploadProgressEvent.cs
   trunk/extensions/Exporters/FlickrExport/FlickrNet/Uploader.cs
   trunk/extensions/Exporters/FlickrExport/FlickrNet/User.cs
   trunk/extensions/Exporters/FlickrExport/FlickrNet/Utils.cs
   trunk/extensions/Exporters/FlickrExport/FlickrRemote.cs
   trunk/extensions/Exporters/FlickrExport/Makefile.am
      - copied, changed from r4368, /trunk/extensions/FlickrExport/Makefile.am
   trunk/extensions/Exporters/FolderExport/
   trunk/extensions/Exporters/FolderExport/.gitignore
      - copied, changed from r4368, /trunk/extensions/FlickrExport/.gitignore
   trunk/extensions/Exporters/FolderExport/FolderExport.addin.xml
      - copied, changed from r4368, /trunk/extensions/FolderExport/FolderExport.addin.xml
   trunk/extensions/Exporters/FolderExport/FolderExport.cs
   trunk/extensions/Exporters/FolderExport/FolderExport.glade
      - copied, changed from r4368, /trunk/extensions/FolderExport/FolderExport.glade
   trunk/extensions/Exporters/FolderExport/Makefile.am
      - copied, changed from r4368, /trunk/extensions/FolderExport/Makefile.am
   trunk/extensions/Exporters/FolderExport/f-spot-simple-white.css
   trunk/extensions/Exporters/FolderExport/f-spot-simple.css
   trunk/extensions/Exporters/FolderExport/f-spot.js
   trunk/extensions/Exporters/GalleryExport/
   trunk/extensions/Exporters/GalleryExport/.gitignore
      - copied, changed from r4368, /trunk/extensions/FlickrExport/FlickrNet/.gitignore
   trunk/extensions/Exporters/GalleryExport/GalleryExport.addin.xml
   trunk/extensions/Exporters/GalleryExport/GalleryExport.cs
   trunk/extensions/Exporters/GalleryExport/GalleryExport.glade
      - copied, changed from r4368, /trunk/extensions/GalleryExport/GalleryExport.glade
   trunk/extensions/Exporters/GalleryExport/GalleryRemote.cs
   trunk/extensions/Exporters/GalleryExport/Makefile.am
      - copied, changed from r4368, /trunk/extensions/GalleryExport/Makefile.am
   trunk/extensions/Exporters/Makefile.am
   trunk/extensions/Exporters/PicasaWebExport/
   trunk/extensions/Exporters/PicasaWebExport/.gitignore
      - copied, changed from r4368, /trunk/extensions/FolderExport/.gitignore
   trunk/extensions/Exporters/PicasaWebExport/Makefile.am
      - copied, changed from r4368, /trunk/extensions/PicasaWebExport/Makefile.am
   trunk/extensions/Exporters/PicasaWebExport/PicasaWebExport.addin.xml
      - copied, changed from r4368, /trunk/extensions/PicasaWebExport/PicasaWebExport.addin.xml
   trunk/extensions/Exporters/PicasaWebExport/PicasaWebExport.cs
   trunk/extensions/Exporters/PicasaWebExport/PicasaWebExport.glade
      - copied, changed from r4368, /trunk/extensions/PicasaWebExport/PicasaWebExport.glade
   trunk/extensions/Exporters/PicasaWebExport/google-sharp/
   trunk/extensions/Exporters/PicasaWebExport/google-sharp/.gitignore
      - copied, changed from r4368, /trunk/extensions/GalleryExport/.gitignore
   trunk/extensions/Exporters/PicasaWebExport/google-sharp/AlbumAccess.cs
   trunk/extensions/Exporters/PicasaWebExport/google-sharp/AssemblyInfo.cs
   trunk/extensions/Exporters/PicasaWebExport/google-sharp/Authentication.cs
   trunk/extensions/Exporters/PicasaWebExport/google-sharp/CaptchaException.cs
   trunk/extensions/Exporters/PicasaWebExport/google-sharp/CreateAlbumException.cs
   trunk/extensions/Exporters/PicasaWebExport/google-sharp/DeleteAlbumException.cs
   trunk/extensions/Exporters/PicasaWebExport/google-sharp/GDataApi.cs
   trunk/extensions/Exporters/PicasaWebExport/google-sharp/GoogleConnection.cs
   trunk/extensions/Exporters/PicasaWebExport/google-sharp/GoogleService.cs
   trunk/extensions/Exporters/PicasaWebExport/google-sharp/Makefile.am
      - copied, changed from r4368, /trunk/extensions/PicasaWebExport/google-sharp/Makefile.am
   trunk/extensions/Exporters/PicasaWebExport/google-sharp/MultipartRequest.cs
   trunk/extensions/Exporters/PicasaWebExport/google-sharp/NoCheckCertificatePolicy.cs
   trunk/extensions/Exporters/PicasaWebExport/google-sharp/PicasaAlbum.cs
   trunk/extensions/Exporters/PicasaWebExport/google-sharp/PicasaAlbumCollection.cs
   trunk/extensions/Exporters/PicasaWebExport/google-sharp/PicasaPicture.cs
   trunk/extensions/Exporters/PicasaWebExport/google-sharp/PicasaPictureCollection.cs
   trunk/extensions/Exporters/PicasaWebExport/google-sharp/PicasaWeb.cs
   trunk/extensions/Exporters/PicasaWebExport/google-sharp/UploadPictureException.cs
   trunk/extensions/Exporters/PicasaWebExport/google-sharp/UploadProgressEventArgs.cs
   trunk/extensions/Exporters/PicasaWebExport/google-sharp/UploadProgressEventHandler.cs
   trunk/extensions/Exporters/PicasaWebExport/google-sharp/XmlUtil.cs
   trunk/extensions/Exporters/SmugMugExport/
   trunk/extensions/Exporters/SmugMugExport/.gitignore
      - copied, changed from r4368, /trunk/extensions/PicasaWebExport/.gitignore
   trunk/extensions/Exporters/SmugMugExport/Makefile.am
      - copied, changed from r4368, /trunk/extensions/SmugMugExport/Makefile.am
   trunk/extensions/Exporters/SmugMugExport/SmugMugExport.addin.xml
      - copied, changed from r4368, /trunk/extensions/SmugMugExport/SmugMugExport.addin.xml
   trunk/extensions/Exporters/SmugMugExport/SmugMugExport.cs
   trunk/extensions/Exporters/SmugMugExport/SmugMugExport.glade
      - copied, changed from r4368, /trunk/extensions/SmugMugExport/SmugMugExport.glade
   trunk/extensions/Exporters/SmugMugExport/SmugMugNet/
   trunk/extensions/Exporters/SmugMugExport/SmugMugNet/.gitignore
      - copied, changed from r4368, /trunk/extensions/SmugMugExport/SmugMugNet/.gitignore
   trunk/extensions/Exporters/SmugMugExport/SmugMugNet/Makefile.am
   trunk/extensions/Exporters/SmugMugExport/SmugMugNet/NoCheckCertificatePolicy.cs
   trunk/extensions/Exporters/SmugMugExport/SmugMugNet/SmugMugApi.cs
   trunk/extensions/Exporters/TabbloExport/
   trunk/extensions/Exporters/TabbloExport/.gitignore
      - copied, changed from r4368, /trunk/extensions/TabbloExport/.gitignore
   trunk/extensions/Exporters/TabbloExport/ApplicationCentricCertificatePolicy.cs
   trunk/extensions/Exporters/TabbloExport/AssemblyInfo.cs
   trunk/extensions/Exporters/TabbloExport/BlindTrustCertificatePolicy.cs
   trunk/extensions/Exporters/TabbloExport/FSpotUploadProgress.cs
   trunk/extensions/Exporters/TabbloExport/Makefile.am
      - copied, changed from r4368, /trunk/extensions/TabbloExport/Makefile.am
   trunk/extensions/Exporters/TabbloExport/Preferences.cs
   trunk/extensions/Exporters/TabbloExport/Tabblo/
   trunk/extensions/Exporters/TabbloExport/Tabblo/AssemblyInfo.cs
   trunk/extensions/Exporters/TabbloExport/Tabblo/Connection.cs
   trunk/extensions/Exporters/TabbloExport/Tabblo/IPreferences.cs
   trunk/extensions/Exporters/TabbloExport/Tabblo/Makefile.am
   trunk/extensions/Exporters/TabbloExport/Tabblo/MultipartRequest.cs
   trunk/extensions/Exporters/TabbloExport/Tabblo/Picture.cs
   trunk/extensions/Exporters/TabbloExport/Tabblo/TabbloException.cs
   trunk/extensions/Exporters/TabbloExport/Tabblo/TotalUploadProgress.cs
   trunk/extensions/Exporters/TabbloExport/Tabblo/UploadProgressEventArgs.cs
   trunk/extensions/Exporters/TabbloExport/Tabblo/UploadProgressEventHandler.cs
   trunk/extensions/Exporters/TabbloExport/TabbloExport.addin.xml
      - copied, changed from r4368, /trunk/extensions/TabbloExport/TabbloExport.addin.xml
   trunk/extensions/Exporters/TabbloExport/TabbloExport.cs
   trunk/extensions/Exporters/TabbloExport/TabbloExport.glade
      - copied, changed from r4368, /trunk/extensions/TabbloExport/TabbloExport.glade
   trunk/extensions/Exporters/TabbloExport/TrustError.glade
      - copied, changed from r4368, /trunk/extensions/TabbloExport/TrustError.glade
   trunk/extensions/Exporters/TabbloExport/UserDecisionCertificatePolicy.cs
   trunk/extensions/Misc/
   trunk/extensions/Misc/ChangeLog
   trunk/extensions/Misc/LightTable/
   trunk/extensions/Misc/LightTable/LightTable.addin.xml
   trunk/extensions/Misc/LightTable/LightTable.cs
   trunk/extensions/Misc/LightTable/Makefile
      - copied, changed from r4368, /trunk/extensions/LightTable/Makefile
   trunk/extensions/Misc/LightTable/Photo.xaml
   trunk/extensions/Services/
   trunk/extensions/Services/.gitignore
   trunk/extensions/Services/BeagleService/
   trunk/extensions/Services/BeagleService/.gitignore
      - copied, changed from r4368, /trunk/extensions/PicasaWebExport/google-sharp/.gitignore
   trunk/extensions/Services/BeagleService/BeagleNotifier.cs
   trunk/extensions/Services/BeagleService/BeagleService.addin.xml
   trunk/extensions/Services/BeagleService/BeagleService.cs
   trunk/extensions/Services/BeagleService/Makefile.am
      - copied, changed from r4368, /trunk/extensions/BeagleService/Makefile.am
   trunk/extensions/Services/ChangeLog
   trunk/extensions/Services/DBusService/
   trunk/extensions/Services/DBusService/.gitignore
      - copied, changed from r4368, /trunk/extensions/SmugMugExport/.gitignore
   trunk/extensions/Services/DBusService/DBusProxy.cs
   trunk/extensions/Services/DBusService/DBusService.addin.xml
   trunk/extensions/Services/DBusService/DBusService.cs
   trunk/extensions/Services/DBusService/Makefile.am
      - copied, changed from r4368, /trunk/extensions/DBusService/Makefile.am
   trunk/extensions/Services/Makefile.am
   trunk/extensions/Tools/
   trunk/extensions/Tools/.gitignore
   trunk/extensions/Tools/ChangeLog
   trunk/extensions/Tools/ChangePhotoPath/
   trunk/extensions/Tools/ChangePhotoPath/.gitignore
      - copied, changed from r4368, /trunk/extensions/ChangePhotoPath/.gitignore
   trunk/extensions/Tools/ChangePhotoPath/ChangeLog
   trunk/extensions/Tools/ChangePhotoPath/ChangePhotoPath.addin.xml
   trunk/extensions/Tools/ChangePhotoPath/ChangePhotoPath.glade
      - copied, changed from r4368, /trunk/extensions/ChangePhotoPath/ChangePhotoPath.glade
   trunk/extensions/Tools/ChangePhotoPath/ChangePhotoPathController.cs
   trunk/extensions/Tools/ChangePhotoPath/ChangePhotoPathGui.cs
   trunk/extensions/Tools/ChangePhotoPath/IChangePhotoPathGui.cs
   trunk/extensions/Tools/ChangePhotoPath/Makefile   (contents, props changed)
   trunk/extensions/Tools/DevelopInUFraw/
   trunk/extensions/Tools/DevelopInUFraw/.gitignore
      - copied, changed from r4368, /trunk/extensions/DevelopInUFraw/.gitignore
   trunk/extensions/Tools/DevelopInUFraw/DevelopInUFRaw.addin.xml
   trunk/extensions/Tools/DevelopInUFraw/DevelopInUFRaw.cs
   trunk/extensions/Tools/DevelopInUFraw/Makefile
      - copied, changed from r4368, /trunk/extensions/DevelopInUFraw/Makefile
   trunk/extensions/Tools/Makefile.am
   trunk/extensions/Tools/MergeDb/
   trunk/extensions/Tools/MergeDb/.gitignore
      - copied, changed from r4368, /trunk/extensions/MergeDb/.gitignore
   trunk/extensions/Tools/MergeDb/Makefile.am
      - copied, changed from r4368, /trunk/extensions/MergeDb/Makefile.am
   trunk/extensions/Tools/MergeDb/MergeDb.addin.xml
      - copied, changed from r4368, /trunk/extensions/MergeDb/MergeDb.addin.xml
   trunk/extensions/Tools/MergeDb/MergeDb.cs
   trunk/extensions/Tools/MergeDb/MergeDb.glade
   trunk/extensions/Tools/MergeDb/MergeDbDialog.cs
   trunk/extensions/Tools/MergeDb/PickFolderDialog.cs
   trunk/extensions/Tools/MetaPixel/
   trunk/extensions/Tools/MetaPixel/.gitignore
      - copied, changed from r4368, /trunk/extensions/MetaPixel/.gitignore
   trunk/extensions/Tools/MetaPixel/Makefile
   trunk/extensions/Tools/MetaPixel/MetaPixel.addin.xml
   trunk/extensions/Tools/MetaPixel/MetaPixel.cs
   trunk/extensions/Tools/MetaPixel/MetaPixel.glade
      - copied, changed from r4368, /trunk/extensions/MetaPixel/MetaPixel.glade
   trunk/extensions/Tools/PictureTile/
   trunk/extensions/Tools/PictureTile/.gitignore
      - copied, changed from r4368, /trunk/extensions/PictureTile/.gitignore
   trunk/extensions/Tools/PictureTile/Makefile
   trunk/extensions/Tools/PictureTile/PictureTile.addin.xml
   trunk/extensions/Tools/PictureTile/PictureTile.cs
   trunk/extensions/Tools/PictureTile/PictureTile.glade
      - copied, changed from r4368, /trunk/extensions/PictureTile/PictureTile.glade
   trunk/extensions/Tools/RawPlusJpeg/
   trunk/extensions/Tools/RawPlusJpeg/.gitignore
      - copied, changed from r4368, /trunk/extensions/RawPlusJpeg/.gitignore
   trunk/extensions/Tools/RawPlusJpeg/Makefile
      - copied, changed from r4368, /trunk/extensions/RawPlusJpeg/Makefile
   trunk/extensions/Tools/RawPlusJpeg/RawPlusJpeg.addin.xml
      - copied, changed from r4368, /trunk/extensions/RawPlusJpeg/RawPlusJpeg.addin.xml
   trunk/extensions/Tools/RawPlusJpeg/RawPlusJpeg.cs
   trunk/extensions/Tools/RetroactiveRoll/
   trunk/extensions/Tools/RetroactiveRoll/.gitignore
      - copied, changed from r4368, /trunk/extensions/RetroactiveRoll/.gitignore
   trunk/extensions/Tools/RetroactiveRoll/Makefile
      - copied, changed from r4368, /trunk/extensions/RetroactiveRoll/Makefile
   trunk/extensions/Tools/RetroactiveRoll/RetroactiveRoll.addin.xml
   trunk/extensions/Tools/RetroactiveRoll/RetroactiveRoll.cs
   trunk/extensions/Tools/SyncCatalog/
   trunk/extensions/Tools/SyncCatalog/.gitignore
      - copied, changed from r4368, /trunk/extensions/SyncCatalog/.gitignore
   trunk/extensions/Tools/SyncCatalog/Makefile
      - copied, changed from r4368, /trunk/extensions/SyncCatalog/Makefile
   trunk/extensions/Tools/SyncCatalog/SyncCatalog.addin.xml
      - copied, changed from r4368, /trunk/extensions/SyncCatalog/SyncCatalog.addin.xml
   trunk/extensions/Tools/SyncCatalog/SyncCatalog.cs
   trunk/extensions/Tools/ZipExport/
   trunk/extensions/Tools/ZipExport/.gitignore
      - copied, changed from r4368, /trunk/extensions/ZipExport/.gitignore
   trunk/extensions/Tools/ZipExport/Makefile
   trunk/extensions/Tools/ZipExport/ZipExport.addin.xml
      - copied, changed from r4368, /trunk/extensions/ZipExport/ZipExport.addin.xml
   trunk/extensions/Tools/ZipExport/ZipExport.cs
   trunk/extensions/Tools/ZipExport/ZipExport.glade
      - copied, changed from r4368, /trunk/extensions/ZipExport/ZipExport.glade
Removed:
   trunk/extensions/BeagleService/.gitignore
   trunk/extensions/BeagleService/BeagleNotifier.cs
   trunk/extensions/BeagleService/BeagleService.addin.xml
   trunk/extensions/BeagleService/BeagleService.cs
   trunk/extensions/BeagleService/Makefile.am
   trunk/extensions/CDExport/.gitignore
   trunk/extensions/CDExport/CDExport.addin.xml
   trunk/extensions/CDExport/CDExport.cs
   trunk/extensions/CDExport/CDExport.glade
   trunk/extensions/CDExport/Makefile.am
   trunk/extensions/ChangePhotoPath/.gitignore
   trunk/extensions/ChangePhotoPath/ChangeLog
   trunk/extensions/ChangePhotoPath/ChangePhotoPath.addin.xml
   trunk/extensions/ChangePhotoPath/ChangePhotoPath.glade
   trunk/extensions/ChangePhotoPath/ChangePhotoPathController.cs
   trunk/extensions/ChangePhotoPath/ChangePhotoPathGui.cs
   trunk/extensions/ChangePhotoPath/IChangePhotoPathGui.cs
   trunk/extensions/ChangePhotoPath/Makefile
   trunk/extensions/DBusService/.gitignore
   trunk/extensions/DBusService/DBusProxy.cs
   trunk/extensions/DBusService/DBusService.addin.xml
   trunk/extensions/DBusService/DBusService.cs
   trunk/extensions/DBusService/Makefile.am
   trunk/extensions/DefaultExporters/.gitignore
   trunk/extensions/DefaultExporters/DefaultExporters.addin.xml
   trunk/extensions/DefaultExporters/Makefile.am
   trunk/extensions/DevelopInUFraw/.gitignore
   trunk/extensions/DevelopInUFraw/DevelopInUFRaw.addin.xml
   trunk/extensions/DevelopInUFraw/DevelopInUFRaw.cs
   trunk/extensions/DevelopInUFraw/Makefile
   trunk/extensions/FacebookExport/.gitignore
   trunk/extensions/FacebookExport/FacebookExport.addin.xml
   trunk/extensions/FacebookExport/FacebookExport.cs
   trunk/extensions/FacebookExport/FacebookExport.glade
   trunk/extensions/FacebookExport/Makefile
   trunk/extensions/FacebookExport/Mono.Facebook/Album.cs
   trunk/extensions/FacebookExport/Mono.Facebook/AssemblyInfo.cs
   trunk/extensions/FacebookExport/Mono.Facebook/Error.cs
   trunk/extensions/FacebookExport/Mono.Facebook/Event.cs
   trunk/extensions/FacebookExport/Mono.Facebook/FacebookException.cs
   trunk/extensions/FacebookExport/Mono.Facebook/FacebookParam.cs
   trunk/extensions/FacebookExport/Mono.Facebook/FacebookSession.cs
   trunk/extensions/FacebookExport/Mono.Facebook/Friend.cs
   trunk/extensions/FacebookExport/Mono.Facebook/FriendInfo.cs
   trunk/extensions/FacebookExport/Mono.Facebook/Group.cs
   trunk/extensions/FacebookExport/Mono.Facebook/Location.cs
   trunk/extensions/FacebookExport/Mono.Facebook/Notification.cs
   trunk/extensions/FacebookExport/Mono.Facebook/PeopleList.cs
   trunk/extensions/FacebookExport/Mono.Facebook/Photo.cs
   trunk/extensions/FacebookExport/Mono.Facebook/Responses.cs
   trunk/extensions/FacebookExport/Mono.Facebook/SessionWrapper.cs
   trunk/extensions/FacebookExport/Mono.Facebook/Tag.cs
   trunk/extensions/FacebookExport/Mono.Facebook/User.cs
   trunk/extensions/FacebookExport/Mono.Facebook/Util.cs
   trunk/extensions/FlickrExport/.gitignore
   trunk/extensions/FlickrExport/FlickrExport.addin.xml
   trunk/extensions/FlickrExport/FlickrExport.cs
   trunk/extensions/FlickrExport/FlickrExport.glade
   trunk/extensions/FlickrExport/FlickrNet/.gitignore
   trunk/extensions/FlickrExport/FlickrNet/ActivityEvent.cs
   trunk/extensions/FlickrExport/FlickrNet/ActivityItem.cs
   trunk/extensions/FlickrExport/FlickrNet/ApiKeyRequiredException.cs
   trunk/extensions/FlickrExport/FlickrNet/AssemblyInfo.cs
   trunk/extensions/FlickrExport/FlickrNet/Auth.cs
   trunk/extensions/FlickrExport/FlickrNet/AuthenticationRequiredException.cs
   trunk/extensions/FlickrExport/FlickrNet/Blogs.cs
   trunk/extensions/FlickrExport/FlickrNet/BoundaryBox.cs
   trunk/extensions/FlickrExport/FlickrNet/Cache.cs
   trunk/extensions/FlickrExport/FlickrNet/Categories.cs
   trunk/extensions/FlickrExport/FlickrNet/Comments.cs
   trunk/extensions/FlickrExport/FlickrNet/Contacts.cs
   trunk/extensions/FlickrExport/FlickrNet/Context.cs
   trunk/extensions/FlickrExport/FlickrNet/DateGranularity.cs
   trunk/extensions/FlickrExport/FlickrNet/Enums.cs
   trunk/extensions/FlickrExport/FlickrNet/ExifPhoto.cs
   trunk/extensions/FlickrExport/FlickrNet/Flickr.cs
   trunk/extensions/FlickrExport/FlickrNet/FlickrApiException.cs
   trunk/extensions/FlickrExport/FlickrNet/FlickrConfigurationManager.cs
   trunk/extensions/FlickrExport/FlickrNet/FlickrConfigurationSettings.cs
   trunk/extensions/FlickrExport/FlickrNet/FlickrException.cs
   trunk/extensions/FlickrExport/FlickrNet/FlickrWebException.cs
   trunk/extensions/FlickrExport/FlickrNet/GeoAccuracy.cs
   trunk/extensions/FlickrExport/FlickrNet/GeoPermissions.cs
   trunk/extensions/FlickrExport/FlickrNet/GroupSearchResults.cs
   trunk/extensions/FlickrExport/FlickrNet/Groups.cs
   trunk/extensions/FlickrExport/FlickrNet/License.txt
   trunk/extensions/FlickrExport/FlickrNet/Licenses.cs
   trunk/extensions/FlickrExport/FlickrNet/LockFile.cs
   trunk/extensions/FlickrExport/FlickrNet/Makefile.am
   trunk/extensions/FlickrExport/FlickrNet/Methods.cs
   trunk/extensions/FlickrExport/FlickrNet/PartialSearchOptions.cs
   trunk/extensions/FlickrExport/FlickrNet/PersistentCache.cs
   trunk/extensions/FlickrExport/FlickrNet/Person.cs
   trunk/extensions/FlickrExport/FlickrNet/Photo.cs
   trunk/extensions/FlickrExport/FlickrNet/PhotoCounts.cs
   trunk/extensions/FlickrExport/FlickrNet/PhotoDates.cs
   trunk/extensions/FlickrExport/FlickrNet/PhotoInfo.cs
   trunk/extensions/FlickrExport/FlickrNet/PhotoLocation.cs
   trunk/extensions/FlickrExport/FlickrNet/PhotoPermissions.cs
   trunk/extensions/FlickrExport/FlickrNet/PhotoSearchExtras.cs
   trunk/extensions/FlickrExport/FlickrNet/PhotoSearchOptions.cs
   trunk/extensions/FlickrExport/FlickrNet/PhotoSearchOrder.cs
   trunk/extensions/FlickrExport/FlickrNet/PhotoSets.cs
   trunk/extensions/FlickrExport/FlickrNet/Photos.cs
   trunk/extensions/FlickrExport/FlickrNet/Response.cs
   trunk/extensions/FlickrExport/FlickrNet/ResponseXmlException.cs
   trunk/extensions/FlickrExport/FlickrNet/SafeNativeMethods.cs
   trunk/extensions/FlickrExport/FlickrNet/SignatureRequiredException.cs
   trunk/extensions/FlickrExport/FlickrNet/Sizes.cs
   trunk/extensions/FlickrExport/FlickrNet/Tags.cs
   trunk/extensions/FlickrExport/FlickrNet/TestAuthFlickr.cs
   trunk/extensions/FlickrExport/FlickrNet/UploadProgressEvent.cs
   trunk/extensions/FlickrExport/FlickrNet/Uploader.cs
   trunk/extensions/FlickrExport/FlickrNet/User.cs
   trunk/extensions/FlickrExport/FlickrNet/Utils.cs
   trunk/extensions/FlickrExport/FlickrRemote.cs
   trunk/extensions/FlickrExport/Makefile.am
   trunk/extensions/FlipEditor/.gitignore
   trunk/extensions/FlipEditor/FlipEditor.addin.xml
   trunk/extensions/FlipEditor/FlipEditor.cs
   trunk/extensions/FlipEditor/Makefile
   trunk/extensions/FolderExport/.gitignore
   trunk/extensions/FolderExport/FolderExport.addin.xml
   trunk/extensions/FolderExport/FolderExport.cs
   trunk/extensions/FolderExport/FolderExport.glade
   trunk/extensions/FolderExport/Makefile.am
   trunk/extensions/FolderExport/f-spot-simple-white.css
   trunk/extensions/FolderExport/f-spot-simple.css
   trunk/extensions/FolderExport/f-spot.js
   trunk/extensions/GalleryExport/.gitignore
   trunk/extensions/GalleryExport/GalleryExport.addin.xml
   trunk/extensions/GalleryExport/GalleryExport.cs
   trunk/extensions/GalleryExport/GalleryExport.glade
   trunk/extensions/GalleryExport/GalleryRemote.cs
   trunk/extensions/GalleryExport/Makefile.am
   trunk/extensions/LightTable/LightTable.addin.xml
   trunk/extensions/LightTable/LightTable.cs
   trunk/extensions/LightTable/Makefile
   trunk/extensions/LightTable/Photo.xaml
   trunk/extensions/MergeDb/.gitignore
   trunk/extensions/MergeDb/Makefile.am
   trunk/extensions/MergeDb/MergeDb.addin.xml
   trunk/extensions/MergeDb/MergeDb.cs
   trunk/extensions/MergeDb/MergeDb.glade
   trunk/extensions/MergeDb/MergeDbDialog.cs
   trunk/extensions/MergeDb/PickFolderDialog.cs
   trunk/extensions/MetaPixel/.gitignore
   trunk/extensions/MetaPixel/Makefile
   trunk/extensions/MetaPixel/MetaPixel.addin.xml
   trunk/extensions/MetaPixel/MetaPixel.cs
   trunk/extensions/MetaPixel/MetaPixel.glade
   trunk/extensions/PicasaWebExport/.gitignore
   trunk/extensions/PicasaWebExport/Makefile.am
   trunk/extensions/PicasaWebExport/PicasaWebExport.addin.xml
   trunk/extensions/PicasaWebExport/PicasaWebExport.cs
   trunk/extensions/PicasaWebExport/PicasaWebExport.glade
   trunk/extensions/PicasaWebExport/google-sharp/.gitignore
   trunk/extensions/PicasaWebExport/google-sharp/AlbumAccess.cs
   trunk/extensions/PicasaWebExport/google-sharp/AssemblyInfo.cs
   trunk/extensions/PicasaWebExport/google-sharp/Authentication.cs
   trunk/extensions/PicasaWebExport/google-sharp/CaptchaException.cs
   trunk/extensions/PicasaWebExport/google-sharp/CreateAlbumException.cs
   trunk/extensions/PicasaWebExport/google-sharp/DeleteAlbumException.cs
   trunk/extensions/PicasaWebExport/google-sharp/GDataApi.cs
   trunk/extensions/PicasaWebExport/google-sharp/GoogleConnection.cs
   trunk/extensions/PicasaWebExport/google-sharp/GoogleService.cs
   trunk/extensions/PicasaWebExport/google-sharp/Makefile.am
   trunk/extensions/PicasaWebExport/google-sharp/MultipartRequest.cs
   trunk/extensions/PicasaWebExport/google-sharp/NoCheckCertificatePolicy.cs
   trunk/extensions/PicasaWebExport/google-sharp/PicasaAlbum.cs
   trunk/extensions/PicasaWebExport/google-sharp/PicasaAlbumCollection.cs
   trunk/extensions/PicasaWebExport/google-sharp/PicasaPicture.cs
   trunk/extensions/PicasaWebExport/google-sharp/PicasaPictureCollection.cs
   trunk/extensions/PicasaWebExport/google-sharp/PicasaWeb.cs
   trunk/extensions/PicasaWebExport/google-sharp/UploadPictureException.cs
   trunk/extensions/PicasaWebExport/google-sharp/UploadProgressEventArgs.cs
   trunk/extensions/PicasaWebExport/google-sharp/UploadProgressEventHandler.cs
   trunk/extensions/PicasaWebExport/google-sharp/XmlUtil.cs
   trunk/extensions/PictureTile/.gitignore
   trunk/extensions/PictureTile/Makefile
   trunk/extensions/PictureTile/PictureTile.addin.xml
   trunk/extensions/PictureTile/PictureTile.cs
   trunk/extensions/PictureTile/PictureTile.glade
   trunk/extensions/RawPlusJpeg/.gitignore
   trunk/extensions/RawPlusJpeg/Makefile
   trunk/extensions/RawPlusJpeg/RawPlusJpeg.addin.xml
   trunk/extensions/RawPlusJpeg/RawPlusJpeg.cs
   trunk/extensions/ResizeEditor/.gitignore
   trunk/extensions/ResizeEditor/Makefile
   trunk/extensions/ResizeEditor/ResizeEditor.addin.xml
   trunk/extensions/ResizeEditor/ResizeEditor.cs
   trunk/extensions/RetroactiveRoll/.gitignore
   trunk/extensions/RetroactiveRoll/Makefile
   trunk/extensions/RetroactiveRoll/RetroactiveRoll.addin.xml
   trunk/extensions/RetroactiveRoll/RetroactiveRoll.cs
   trunk/extensions/SmugMugExport/.gitignore
   trunk/extensions/SmugMugExport/Makefile.am
   trunk/extensions/SmugMugExport/SmugMugExport.addin.xml
   trunk/extensions/SmugMugExport/SmugMugExport.cs
   trunk/extensions/SmugMugExport/SmugMugExport.glade
   trunk/extensions/SmugMugExport/SmugMugNet/.gitignore
   trunk/extensions/SmugMugExport/SmugMugNet/Makefile.am
   trunk/extensions/SmugMugExport/SmugMugNet/NoCheckCertificatePolicy.cs
   trunk/extensions/SmugMugExport/SmugMugNet/SmugMugApi.cs
   trunk/extensions/SyncCatalog/.gitignore
   trunk/extensions/SyncCatalog/Makefile
   trunk/extensions/SyncCatalog/SyncCatalog.addin.xml
   trunk/extensions/SyncCatalog/SyncCatalog.cs
   trunk/extensions/TabbloExport/.gitignore
   trunk/extensions/TabbloExport/ApplicationCentricCertificatePolicy.cs
   trunk/extensions/TabbloExport/AssemblyInfo.cs
   trunk/extensions/TabbloExport/BlindTrustCertificatePolicy.cs
   trunk/extensions/TabbloExport/FSpotUploadProgress.cs
   trunk/extensions/TabbloExport/Makefile.am
   trunk/extensions/TabbloExport/Preferences.cs
   trunk/extensions/TabbloExport/Tabblo/AssemblyInfo.cs
   trunk/extensions/TabbloExport/Tabblo/Connection.cs
   trunk/extensions/TabbloExport/Tabblo/IPreferences.cs
   trunk/extensions/TabbloExport/Tabblo/Makefile.am
   trunk/extensions/TabbloExport/Tabblo/MultipartRequest.cs
   trunk/extensions/TabbloExport/Tabblo/Picture.cs
   trunk/extensions/TabbloExport/Tabblo/TabbloException.cs
   trunk/extensions/TabbloExport/Tabblo/TotalUploadProgress.cs
   trunk/extensions/TabbloExport/Tabblo/UploadProgressEventArgs.cs
   trunk/extensions/TabbloExport/Tabblo/UploadProgressEventHandler.cs
   trunk/extensions/TabbloExport/TabbloExport.addin.xml
   trunk/extensions/TabbloExport/TabbloExport.cs
   trunk/extensions/TabbloExport/TabbloExport.glade
   trunk/extensions/TabbloExport/TrustError.glade
   trunk/extensions/TabbloExport/UserDecisionCertificatePolicy.cs
   trunk/extensions/ZipExport/.gitignore
   trunk/extensions/ZipExport/Makefile
   trunk/extensions/ZipExport/ZipExport.addin.xml
   trunk/extensions/ZipExport/ZipExport.cs
   trunk/extensions/ZipExport/ZipExport.glade
Modified:
   trunk/configure.in
   trunk/extensions/ChangeLog
   trunk/extensions/Makefile.am

Modified: trunk/configure.in
==============================================================================
--- trunk/configure.in	(original)
+++ trunk/configure.in	Tue Sep 16 13:11:27 2008
@@ -357,21 +357,24 @@
 Tao/Tao.GlPostProcess/Makefile
 Tao/Tao.OpenGl.ExtensionLoader/Makefile
 extensions/Makefile
-extensions/BeagleService/Makefile
-extensions/CDExport/Makefile
-extensions/DBusService/Makefile
-extensions/DefaultExporters/Makefile
-extensions/FlickrExport/Makefile
-extensions/FlickrExport/FlickrNet/Makefile
-extensions/GalleryExport/Makefile
-extensions/FolderExport/Makefile
-extensions/SmugMugExport/SmugMugNet/Makefile
-extensions/SmugMugExport/Makefile
-extensions/MergeDb/Makefile
-extensions/TabbloExport/Makefile
-extensions/TabbloExport/Tabblo/Makefile
-extensions/PicasaWebExport/Makefile
-extensions/PicasaWebExport/google-sharp/Makefile
+extensions/Exporters/Makefile
+extensions/Exporters/CDExport/Makefile
+extensions/Exporters/DefaultExporters/Makefile
+extensions/Exporters/FlickrExport/Makefile
+extensions/Exporters/FlickrExport/FlickrNet/Makefile
+extensions/Exporters/GalleryExport/Makefile
+extensions/Exporters/FolderExport/Makefile
+extensions/Exporters/SmugMugExport/SmugMugNet/Makefile
+extensions/Exporters/SmugMugExport/Makefile
+extensions/Exporters/TabbloExport/Makefile
+extensions/Exporters/TabbloExport/Tabblo/Makefile
+extensions/Exporters/PicasaWebExport/Makefile
+extensions/Exporters/PicasaWebExport/google-sharp/Makefile
+extensions/Services/Makefile
+extensions/Services/BeagleService/Makefile
+extensions/Services/DBusService/Makefile
+extensions/Tools/Makefile
+extensions/Tools/MergeDb/Makefile
 f-spot.pc
 f-spot.spec
 f-spot.desktop.in

Copied: trunk/extensions/Editors/FlipEditor/.gitignore (from r4368, /trunk/extensions/FlipEditor/.gitignore)
==============================================================================

Added: trunk/extensions/Editors/FlipEditor/FlipEditor.addin.xml
==============================================================================
--- (empty file)
+++ trunk/extensions/Editors/FlipEditor/FlipEditor.addin.xml	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,15 @@
+<Addin namespace="FSpot"
+	id="FlipEditor"
+	version="0.4.5.0"
+	name="FlipEditor"
+	description="Flips the image horizontally."
+	author="Ruben Vermeersch"
+	url="http://f-spot.org/Extensions";
+	category="Editors">
+	<Dependencies>
+		<Addin id="Core" version="0.4.4.102"/>
+	</Dependencies>
+	<Extension path = "/FSpot/Editors">
+		<Editor editor_type = "FSpot.Addins.Editors.FlipEditor"/>
+	</Extension>
+</Addin>

Copied: trunk/extensions/Editors/FlipEditor/FlipEditor.cs (from r4368, /trunk/extensions/FlipEditor/FlipEditor.cs)
==============================================================================

Copied: trunk/extensions/Editors/FlipEditor/Makefile (from r4368, /trunk/extensions/FlipEditor/Makefile)
==============================================================================

Copied: trunk/extensions/Editors/ResizeEditor/.gitignore (from r4368, /trunk/extensions/ResizeEditor/.gitignore)
==============================================================================

Copied: trunk/extensions/Editors/ResizeEditor/Makefile (from r4368, /trunk/extensions/ResizeEditor/Makefile)
==============================================================================

Copied: trunk/extensions/Editors/ResizeEditor/ResizeEditor.addin.xml (from r4368, /trunk/extensions/ResizeEditor/ResizeEditor.addin.xml)
==============================================================================

Copied: trunk/extensions/Editors/ResizeEditor/ResizeEditor.cs (from r4368, /trunk/extensions/ResizeEditor/ResizeEditor.cs)
==============================================================================

Added: trunk/extensions/Exporters/.gitignore
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/.gitignore	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,2 @@
+/Makefile
+/Makefile.in

Copied: trunk/extensions/Exporters/CDExport/.gitignore (from r4368, /trunk/extensions/BeagleService/.gitignore)
==============================================================================

Copied: trunk/extensions/Exporters/CDExport/CDExport.addin.xml (from r4368, /trunk/extensions/CDExport/CDExport.addin.xml)
==============================================================================

Added: trunk/extensions/Exporters/CDExport/CDExport.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/CDExport/CDExport.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,357 @@
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+using Mono.Unix;
+using FSpot;
+using FSpot.Filters;
+using FSpot.Widgets;
+using FSpot.Utils;
+#if GIO_2_16
+using GLib;
+#endif
+
+namespace FSpotCDExport {
+	public class CDExport : FSpot.Extensions.IExporter {
+		IBrowsableCollection selection;
+
+		[Glade.Widget] Gtk.Dialog dialog;
+		[Glade.Widget] Gtk.ScrolledWindow thumb_scrolledwindow;
+		[Glade.Widget] Gtk.CheckButton remove_check;
+		[Glade.Widget] Gtk.CheckButton rotate_check;
+		[Glade.Widget] Gtk.Label size_label;
+		[Glade.Widget] Gtk.Frame previous_frame;
+
+#if GIO_2_16
+		Gtk.Window listwindow;
+		System.Uri dest = new System.Uri ("burn:///");
+#else
+		Gnome.Vfs.Uri dest = new Gnome.Vfs.Uri ("burn:///");
+#endif
+
+		int photo_index;
+		bool clean;
+		bool rotate;
+
+		FSpot.ThreadProgressDialog progress_dialog;
+		System.Threading.Thread command_thread;
+
+		private Glade.XML xml;
+		private string dialog_name = "cd_export_dialog";
+
+		public CDExport ()
+		{
+		}
+
+		public void Run (IBrowsableCollection selection)
+		{
+
+			xml = new Glade.XML (null, "CDExport.glade", dialog_name, "f-spot");
+			xml.Autoconnect (this);
+
+			this.selection = selection;
+
+			// Calculate the total size
+			long total_size = 0;
+			string path;
+			System.IO.FileInfo file_info;
+
+			foreach (IBrowsableItem item in selection.Items) {
+				path = item.DefaultVersionUri.LocalPath;
+				if (System.IO.File.Exists (path)) {
+					file_info = new System.IO.FileInfo (path);
+					total_size += file_info.Length;
+				}
+			}
+
+			IconView view = new IconView (selection);
+			view.DisplayDates = false;
+			view.DisplayTags = false;
+
+			Dialog.Modal = false;
+			Dialog.TransientFor = null;
+
+			size_label.Text = SizeUtil.ToHumanReadable (total_size);
+
+			thumb_scrolledwindow.Add (view);
+			Dialog.ShowAll ();
+
+			previous_frame.Visible = IsEmpty (dest);
+			//LoadHistory ();
+
+			Dialog.Response += HandleResponse;
+		}
+
+		void HandleBrowseExisting (object sender, System.EventArgs args)
+		{
+#if GIO_2_16
+			if (listwindow == null) {
+				listwindow = new Gtk.Window ("Pending files to write");
+				listwindow.SetDefaultSize (400, 200);
+				listwindow.DeleteEvent += delegate (object o, Gtk.DeleteEventArgs e) {(o as Gtk.Window).Destroy (); listwindow = null;};
+				Gtk.TextView view = new Gtk.TextView ();
+				Gtk.TextBuffer buffer = view.Buffer;
+				Gtk.ScrolledWindow sw = new Gtk.ScrolledWindow ();
+				sw.Add (view);
+				listwindow.Add (sw);
+			} else {
+				((listwindow.Child as Gtk.ScrolledWindow).Child as Gtk.TextView).Buffer.Text = "";
+			}
+			ListAll (((listwindow.Child as Gtk.ScrolledWindow).Child as Gtk.TextView).Buffer, dest);
+			listwindow.ShowAll ();
+#else
+			GnomeUtil.UrlShow (dest.ToString ());
+#endif
+		}
+
+#if GIO_2_16
+		void ListAll (Gtk.TextBuffer t, System.Uri path)
+		{
+			GLib.File f = FileFactory.NewForUri (path);
+			foreach (GLib.FileInfo info in f.EnumerateChildren ("*", FileQueryInfoFlags.None, null)) {
+				t.Text += new System.Uri (path, info.Name).ToString () + Environment.NewLine;
+				if (info.FileType == FileType.Directory)
+					ListAll (t, new System.Uri (path, info.Name + "/"));
+			}
+		}
+#endif
+		[DllImport ("libc")]
+		extern static int system (string program);
+
+//		//FIXME: rewrite this as a Filter
+#if GIO_2_16
+	        public static GLib.File UniqueName (System.Uri path, string shortname)
+#else
+	        public static Gnome.Vfs.Uri UniqueName (Gnome.Vfs.Uri path, string shortname)
+#endif
+	        {
+	                int i = 1;
+#if GIO_2_16
+			GLib.File dest = FileFactory.NewForUri (new System.Uri (path, shortname));
+#else
+			Gnome.Vfs.Uri target = path.Clone();
+	                Gnome.Vfs.Uri dest = target.AppendFileName(shortname);
+#endif
+	                while (dest.Exists) {
+	                        string numbered_name = System.String.Format ("{0}-{1}{2}",
+	                                                              System.IO.Path.GetFileNameWithoutExtension (shortname),
+	                                                              i++,
+	                                                              System.IO.Path.GetExtension (shortname));
+
+#if GIO_2_16
+				dest = FileFactory.NewForUri (new System.Uri (path, numbered_name));
+#else
+				dest = target.AppendFileName(numbered_name);
+#endif
+	                }
+
+	                return dest;
+	        }
+
+#if GIO_2_16
+		void Clean (System.Uri path)
+		{
+			GLib.File source = FileFactory.NewForUri (path);
+			foreach (GLib.FileInfo info in source.EnumerateChildren ("*", FileQueryInfoFlags.None, null)) {
+				if (info.FileType == FileType.Directory)
+					Clean (new System.Uri(path, info.Name + "/"));
+				FileFactory.NewForUri (new System.Uri (path, info.Name)).Delete ();
+			}
+		}
+#else
+		void Clean ()
+		{
+			Gnome.Vfs.Uri target = dest.Clone ();
+			Gnome.Vfs.XferProgressCallback cb = new Gnome.Vfs.XferProgressCallback (Progress);
+			Gnome.Vfs.Xfer.XferDeleteList (new Gnome.Vfs.Uri [] {target}, Gnome.Vfs.XferErrorMode.Query, Gnome.Vfs.XferOptions.Recursive, cb);
+		}
+#endif
+
+#if GIO_2_16
+		bool IsEmpty (System.Uri path)
+		{
+			foreach (GLib.FileInfo fi in FileFactory.NewForUri (path).EnumerateChildren ("*", FileQueryInfoFlags.None, null))
+				return true;
+			return false;
+		}
+#else
+		bool IsEmpty (Gnome.Vfs.Uri path)
+		{
+			return Gnome.Vfs.Directory.GetEntries (path).Length != 0;
+		}
+#endif
+
+		public void Transfer () {
+			try {
+#if GIO_2_16
+				bool result = true;
+#else
+				Gnome.Vfs.Result result = Gnome.Vfs.Result.Ok;
+#endif
+
+				if (clean)
+#if GIO_2_16
+					Clean (dest);
+#else
+					Clean ();
+#endif
+
+				foreach (IBrowsableItem photo in selection.Items) {
+
+				//FIXME need to implement the uniquename as a filter
+					using (FilterRequest request = new FilterRequest (photo.DefaultVersionUri)) {
+						if (rotate)
+							new OrientationFilter ().Convert (request);
+
+#if GIO_2_16
+						GLib.File source = FileFactory.NewForUri (request.Current.ToString ());
+#else
+						Gnome.Vfs.Uri source = new Gnome.Vfs.Uri (request.Current.ToString ());
+						Gnome.Vfs.Uri target = dest.Clone ();
+#endif
+#if GIO_2_16
+						GLib.File target = UniqueName (dest, photo.Name);
+						FileProgressCallback cb = Progress;
+#else
+						target = UniqueName (target, photo.Name);
+						Gnome.Vfs.XferProgressCallback cb = new Gnome.Vfs.XferProgressCallback (Progress);
+#endif
+
+						progress_dialog.Message = System.String.Format (Catalog.GetString ("Transferring picture \"{0}\" To CD"), photo.Name);
+						progress_dialog.Fraction = photo_index / (double) selection.Count;
+						progress_dialog.ProgressText = System.String.Format (Catalog.GetString ("{0} of {1}"),
+											     photo_index, selection.Count);
+
+#if GIO_2_16
+						result &= source.Copy (target,
+									FileCopyFlags.None,
+									null,
+									cb);
+#else
+						result = Gnome.Vfs.Xfer.XferUri (source, target,
+										 Gnome.Vfs.XferOptions.Default,
+										 Gnome.Vfs.XferErrorMode.Abort,
+										 Gnome.Vfs.XferOverwriteMode.Replace,
+										 cb);
+#endif
+					}
+					photo_index++;
+				}
+
+				// FIXME the error dialog here is ugly and needs improvement when strings are not frozen.
+#if GIO_2_16
+				if (result) {
+#else
+				if (result == Gnome.Vfs.Result.Ok) {
+#endif
+					progress_dialog.Message = Catalog.GetString ("Done Sending Photos");
+					progress_dialog.Fraction = 1.0;
+					progress_dialog.ProgressText = Catalog.GetString ("Transfer Complete");
+					progress_dialog.ButtonLabel = Gtk.Stock.Ok;
+					progress_dialog.Hide ();
+					system ("nautilus-cd-burner");
+				} else {
+					throw new System.Exception (System.String.Format ("{0}{3}{1}{3}{2}",
+											  progress_dialog.Message,
+											  Catalog.GetString ("Error While Transferring"),
+											  result.ToString (),
+											  System.Environment.NewLine));
+				}
+
+			} catch (System.Exception e) {
+				progress_dialog.Message = e.ToString ();
+				progress_dialog.ProgressText = Catalog.GetString ("Error Transferring");
+				return;
+			}
+			Gtk.Application.Invoke (this.Destroy);
+		}
+
+		private void Destroy (object sender, System.EventArgs args)
+		{
+			progress_dialog.Destroy ();
+		}
+
+#if GIO_2_16
+		private void Progress (long current_num_bytes, long total_num_bytes)
+#else
+		private int Progress (Gnome.Vfs.XferProgressInfo info)
+#endif
+		{
+#if GIO_2_16
+			progress_dialog.ProgressText = Catalog.GetString ("copying...");
+#else
+			progress_dialog.ProgressText = info.Phase.ToString ();
+#endif
+
+#if GIO_2_16
+			if (total_num_bytes > 0)
+				progress_dialog.Fraction = current_num_bytes / (double)total_num_bytes;
+#else
+			if (info.BytesTotal > 0)
+				progress_dialog.Fraction = info.BytesCopied / (double)info.BytesTotal;
+#endif
+
+#if !GIO_2_16
+			switch (info.Status) {
+			case Gnome.Vfs.XferProgressStatus.Vfserror:
+				progress_dialog.Message = Catalog.GetString ("Error: Error while transferring; Aborting");
+				return (int)Gnome.Vfs.XferErrorAction.Abort;
+			case Gnome.Vfs.XferProgressStatus.Overwrite:
+				progress_dialog.ProgressText = Catalog.GetString ("Error: File Already Exists; Aborting");
+				return (int)Gnome.Vfs.XferOverwriteAction.Abort;
+			default:
+				return 1;
+			}
+#endif
+		}
+
+		private void HandleMsg (Gnome.Vfs.ModuleCallback cb)
+		{
+			Gnome.Vfs.ModuleCallbackStatusMessage msg = cb as Gnome.Vfs.ModuleCallbackStatusMessage;
+			System.Console.WriteLine ("{0}", msg.Message);
+		}
+
+		private void HandleAuth (Gnome.Vfs.ModuleCallback cb)
+		{
+			Gnome.Vfs.ModuleCallbackFullAuthentication fcb = cb as Gnome.Vfs.ModuleCallbackFullAuthentication;
+			System.Console.Write ("Enter your username ({0}): ", fcb.Username);
+			string username = System.Console.ReadLine ();
+			System.Console.Write ("Enter your password : ");
+			string passwd = System.Console.ReadLine ();
+
+			if (username.Length > 0)
+				fcb.Username = username;
+			fcb.Password = passwd;
+		}
+
+		private void HandleResponse (object sender, Gtk.ResponseArgs args)
+		{
+#if GIO_2_16
+			if (listwindow != null)
+				listwindow.Destroy ();
+#endif
+			if (args.ResponseId != Gtk.ResponseType.Ok) {
+				Dialog.Destroy ();
+				return;
+			}
+
+			clean = remove_check.Active;
+			rotate = rotate_check.Active;
+			Dialog.Destroy ();
+
+			command_thread = new System.Threading.Thread (new System.Threading.ThreadStart (Transfer));
+			command_thread.Name = Catalog.GetString ("Transferring Pictures");
+
+			progress_dialog = new FSpot.ThreadProgressDialog (command_thread, selection.Count);
+			progress_dialog.Start ();
+		}
+
+		private Gtk.Dialog Dialog {
+			get {
+				if (dialog == null)
+					dialog = (Gtk.Dialog) xml.GetWidget (dialog_name);
+
+				return dialog;
+			}
+		}
+	}
+}

Copied: trunk/extensions/Exporters/CDExport/CDExport.glade (from r4368, /trunk/extensions/CDExport/CDExport.glade)
==============================================================================

Copied: trunk/extensions/Exporters/CDExport/Makefile.am (from r4368, /trunk/extensions/CDExport/Makefile.am)
==============================================================================

Copied: trunk/extensions/Exporters/DefaultExporters/.gitignore (from r4368, /trunk/extensions/CDExport/.gitignore)
==============================================================================

Copied: trunk/extensions/Exporters/DefaultExporters/DefaultExporters.addin.xml (from r4368, /trunk/extensions/DefaultExporters/DefaultExporters.addin.xml)
==============================================================================

Copied: trunk/extensions/Exporters/DefaultExporters/Makefile.am (from r4368, /trunk/extensions/DefaultExporters/Makefile.am)
==============================================================================

Copied: trunk/extensions/Exporters/FacebookExport/.gitignore (from r4368, /trunk/extensions/FacebookExport/.gitignore)
==============================================================================

Copied: trunk/extensions/Exporters/FacebookExport/FacebookExport.addin.xml (from r4368, /trunk/extensions/FacebookExport/FacebookExport.addin.xml)
==============================================================================

Added: trunk/extensions/Exporters/FacebookExport/FacebookExport.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FacebookExport/FacebookExport.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,559 @@
+/*
+ * FacebookExport.cs
+ *
+ * Authors:
+ *   George Talusan <george convolve ca>
+ *   Stephane Delcroix <stephane delcroix org>
+ *
+ * Copyright (C) 2007 George Talusan
+ */
+
+using System;
+using System.Net;
+using System.IO;
+using System.Text;
+using System.Threading;
+using System.Collections;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Web;
+using Mono.Unix;
+using Gtk;
+
+using FSpot;
+using FSpot.Utils;
+using FSpot.UI.Dialog;
+using FSpot.Extensions;
+using FSpot.Filters;
+
+using Mono.Facebook;
+
+namespace FSpot.Exporter.Facebook
+{
+	internal class FacebookAccount
+	{
+		private static string keyring_item_name = "Facebook Account";
+
+		private static string api_key = "c23d1683e87313fa046954ea253a240e";
+		private static string secret = "743e9a2e6a1c35ce961321bceea7b514";
+
+		private FacebookSession facebookSession;
+		private bool connected;
+
+		public FacebookAccount ()
+		{ }
+
+		public Uri CreateToken ()
+		{
+			FacebookSession session = new FacebookSession (api_key, secret);
+			Uri token = session.CreateToken();
+			facebookSession = session;
+			connected = false;
+			return token;
+		}
+
+		public FacebookSession Facebook
+		{
+			get { return facebookSession; }
+		}
+
+		public bool Authenticated
+		{
+			get {
+				try {
+					if (connected)
+						return true;
+					facebookSession.GetSession();
+					connected = true;
+					return true;
+				}
+				catch (FacebookException fe) {
+					Console.WriteLine (fe);
+					return false;
+				}
+			}
+		}
+
+		public bool HasInfiniteSession
+		{
+			get {
+				if (Authenticated)
+					return true;
+
+				return true;
+			}
+		}
+	}
+
+	internal class AlbumStore : ListStore
+	{
+		private Album[] _albums;
+
+		public AlbumStore (Album[] albums) : base (typeof (string))
+		{
+			_albums = albums;
+
+			foreach (Album album in Albums)
+				AppendValues (album.Name);
+		}
+
+		public Album[] Albums
+		{
+			get { return _albums; }
+		}
+	}
+
+	internal class TagStore : ListStore
+	{
+		private List<Mono.Facebook.Tag> _tags;
+
+		private Dictionary<long, User> _friends;
+
+		public TagStore (FacebookSession session, List<Mono.Facebook.Tag> tags, Dictionary<long, User> friends) : base (typeof (string))
+		{
+			_tags = tags;
+
+			foreach (Mono.Facebook.Tag tag in Tags) {
+				long subject = tag.Subject;
+				User info = _friends [subject];
+				if (info == null ) {
+					try {
+						info = session.GetUserInfo (new long[] { subject }, new string[] { "first_name", "last_name" }) [0];
+					}
+					catch (FacebookException) {
+						continue;
+					}
+				}
+				AppendValues (String.Format ("{0} {1}", info.FirstName ?? "", info.LastName ?? ""));
+			}
+		}
+
+		public List<Mono.Facebook.Tag> Tags
+		{
+			get { return _tags ?? new List<Mono.Facebook.Tag> (); }
+		}
+	}
+
+	internal class FacebookTagPopup
+	{
+		private Dictionary<long, User> _friends;
+
+		private Gtk.Window _popup;
+
+		public FacebookTagPopup (Dictionary<long, User> friends)
+		{
+			Friends = friends;
+
+			Glade.XML xml = new Glade.XML (null, "FacebookExport.glade", "facebook_tag_popup", "f-spot");
+			xml.Autoconnect (this);
+
+			Popup = xml.GetWidget ("facebook_tag_popup") as Gtk.Window;
+			Popup.Show ();
+		}
+
+		public Dictionary<long, User> Friends
+		{
+			get { return _friends; }
+			set { _friends = value; }
+		}
+
+		protected Gtk.Window Popup
+		{
+			get { return _popup; }
+			set { _popup = value; }
+		}
+	}
+
+	public class FacebookExport : IExporter
+	{
+		private int size = 604;
+
+		private FacebookAccount account;
+		private Dictionary<long, User> friends;
+
+		/* parallel arrays */
+		private int current_item;
+		private IBrowsableItem[] items;
+		private string[] captions;
+		private List<Mono.Facebook.Tag>[] tags;
+
+		private Glade.XML xml;
+		private string dialog_name = "facebook_export_dialog";
+		private Gtk.Dialog dialog;
+
+		private FSpot.Widgets.IconView thumbnail_iconview;
+
+		FSpot.ThreadProgressDialog progress_dialog;
+
+		[Glade.WidgetAttribute]
+		Gtk.VBox album_info_vbox;
+
+		[Glade.WidgetAttribute]
+		Gtk.VBox picture_info_vbox;
+
+		[Glade.WidgetAttribute]
+		Gtk.Button login_button;
+
+		[Glade.WidgetAttribute]
+		Gtk.Button logout_button;
+
+		[Glade.WidgetAttribute]
+		Gtk.Label whoami_label;
+
+		[Glade.WidgetAttribute]
+		Gtk.RadioButton existing_album_radiobutton;
+
+		[Glade.WidgetAttribute]
+		Gtk.RadioButton create_album_radiobutton;
+
+		[Glade.WidgetAttribute]
+		Gtk.ComboBox existing_album_combobox;
+
+		[Glade.WidgetAttribute]
+		Gtk.HBox new_album_info1_hbox;
+
+		[Glade.WidgetAttribute]
+		Gtk.HBox new_album_info2_hbox;
+
+		[Glade.WidgetAttribute]
+		Gtk.Entry album_name_entry;
+
+		[Glade.WidgetAttribute]
+		Gtk.Entry album_location_entry;
+
+		[Glade.WidgetAttribute]
+		Gtk.Entry album_description_entry;
+
+		[Glade.WidgetAttribute]
+		Gtk.ScrolledWindow thumbnails_scrolled_window;
+
+		[Glade.WidgetAttribute]
+		Gtk.TextView caption_textview;
+
+		[Glade.WidgetAttribute]
+		Gtk.TreeView tag_treeview;
+
+		[Glade.WidgetAttribute]
+		Gtk.EventBox tag_image_eventbox;
+
+		private Gtk.Image tag_image;
+		private int tag_image_height;
+		private int tag_image_width;
+
+		System.Threading.Thread command_thread;
+
+		public FacebookExport ()
+		{
+		}
+
+		public void Run (IBrowsableCollection selection)
+		{
+			CreateDialog ();
+
+			items = selection.Items;
+
+			if (items.Length > 60) {
+				HigMessageDialog mbox = new HigMessageDialog (Dialog, Gtk.DialogFlags.DestroyWithParent | Gtk.DialogFlags.Modal, Gtk.MessageType.Error, Gtk.ButtonsType.Ok, Catalog.GetString ("Too many images to export"), Catalog.GetString ("Facebook only permits 60 photographs per album.  Please refine your selection and try again."));
+				mbox.Run ();
+				mbox.Destroy ();
+				return;
+			}
+
+			captions = new string [items.Length];
+			tags = new List<Mono.Facebook.Tag> [items.Length];
+
+			thumbnail_iconview = new FSpot.Widgets.IconView (selection);
+			thumbnail_iconview.DisplayDates = false;
+			thumbnail_iconview.DisplayTags = false;
+			thumbnail_iconview.ButtonPressEvent += HandleThumbnailIconViewButtonPressEvent;
+			thumbnail_iconview.KeyPressEvent += HandleThumbnailIconViewKeyPressEvent;
+			thumbnail_iconview.Show ();
+			thumbnails_scrolled_window.Add (thumbnail_iconview);
+
+			login_button.Visible = true;
+			login_button.Clicked += HandleLoginClicked;
+
+			logout_button.Visible = false;
+			logout_button.Clicked += HandleLogoutClicked;
+
+			whoami_label.Text = Catalog.GetString ("You are not logged in.");
+
+			album_info_vbox.Sensitive = false;
+			picture_info_vbox.Sensitive = false;
+
+			create_album_radiobutton.Toggled += HandleCreateAlbumToggled;
+			create_album_radiobutton.Active = true;
+
+			existing_album_radiobutton.Toggled += HandleExistingAlbumToggled;
+
+			CellRendererText cell = new CellRendererText ();
+			existing_album_combobox.PackStart (cell, false);
+
+			tag_image_eventbox.ButtonPressEvent += HandleTagImageButtonPressEvent;
+
+			Dialog.Response += HandleResponse;
+			Dialog.Show ();
+		}
+
+		public void CreateDialog ()
+		{
+			xml = new Glade.XML (null, "FacebookExport.glade", dialog_name, "f-spot");
+			xml.Autoconnect (this);
+		}
+
+		private Gtk.Dialog Dialog {
+			get {
+				if (dialog == null)
+					dialog = (Gtk.Dialog) xml.GetWidget (dialog_name);
+
+				return dialog;
+			}
+		}
+
+		public void HandleLoginClicked (object sender, EventArgs args)
+		{
+			account = new FacebookAccount();
+
+			Uri token = account.CreateToken ();
+			GnomeUtil.UrlShow (token.ToString ());
+
+			HigMessageDialog mbox = new HigMessageDialog (Dialog, Gtk.DialogFlags.DestroyWithParent | Gtk.DialogFlags.Modal, Gtk.MessageType.Info, Gtk.ButtonsType.Ok, Catalog.GetString ("Waiting for authentication"), Catalog.GetString ("F-Spot will now launch your browser so that you can log into Facebook.  Turn on the \"Save my login information\" checkbox on Facebook and F-Spot will log into Facebook automatically from now on."));
+
+			mbox.Run ();
+			mbox.Destroy ();
+
+			if (account.Authenticated == false) {
+				HigMessageDialog error = new HigMessageDialog (Dialog, Gtk.DialogFlags.DestroyWithParent | Gtk.DialogFlags.Modal, Gtk.MessageType.Error, Gtk.ButtonsType.Ok, Catalog.GetString ("Error logging into Facebook"), Catalog.GetString ("There was a problem logging into Facebook.  Check your credentials and try again."));
+				error.Run ();
+				error.Destroy ();
+			}
+			else {
+				login_button.Visible = false;
+				logout_button.Visible = true;
+
+				album_info_vbox.Sensitive = true;
+				picture_info_vbox.Sensitive = true;
+
+				User me = account.Facebook.GetLoggedInUser ().GetUserInfo ();
+				whoami_label.Text = String.Format (Catalog.GetString ("{0} {1} is logged into Facebook"), me.FirstName, me.LastName);
+
+				Friend[] friend_list = account.Facebook.GetFriends ();
+				long[] uids = new long [friend_list.Length];
+
+				for (int i = 0; i < friend_list.Length; i++)
+					uids [i] = friend_list [i].UId;
+
+				User[] infos = account.Facebook.GetUserInfo (uids, new string[] { "first_name", "last_name" });
+				friends = new Dictionary<long, User> ();
+
+				foreach (User user in infos)
+					friends.Add (user.UId, user);
+
+				Album[] albums = account.Facebook.GetAlbums ();
+				AlbumStore store = new AlbumStore (albums);
+				existing_album_combobox.Model = store;
+				existing_album_combobox.Active = 0;
+			}
+		}
+
+		private void HandleLogoutClicked (object sender, EventArgs args)
+		{
+			login_button.Visible = true;
+			logout_button.Visible = false;
+
+			whoami_label.Text = Catalog.GetString ("You are not logged in.");
+
+			album_info_vbox.Sensitive = false;
+			picture_info_vbox.Sensitive = false;
+		}
+
+		private void HandleCreateAlbumToggled (object sender, EventArgs args)
+		{
+			if (create_album_radiobutton.Active == false)
+				return;
+
+			new_album_info1_hbox.Sensitive = true;
+			new_album_info2_hbox.Sensitive = true;
+
+			existing_album_combobox.Sensitive = false;
+		}
+
+		private void HandleExistingAlbumToggled (object sender, EventArgs args)
+		{
+			if (existing_album_radiobutton.Active == false)
+				return;
+
+			new_album_info1_hbox.Sensitive = false;
+			new_album_info2_hbox.Sensitive = false;
+
+			existing_album_combobox.Sensitive = true;
+		}
+
+		private void HandleThumbnailIconViewButtonPressEvent (object sender, Gtk.ButtonPressEventArgs args)
+		{
+			int old_item = current_item;
+			current_item = thumbnail_iconview.CellAtPosition ((int) args.Event.X, (int) args.Event.Y, false);
+
+			if (current_item < 0 || current_item >=  items.Length) {                                current_item = old_item;
+				return;
+                        }
+
+			captions [old_item] = caption_textview.Buffer.Text;
+
+			string caption = captions [current_item];
+			if (caption == null)
+				captions [current_item] = caption = "";
+			caption_textview.Buffer.Text = caption;
+
+			tag_treeview.Model = new TagStore (account.Facebook, tags [current_item], friends);
+
+			IBrowsableItem item = items [current_item];
+			string thumbnail_path = ThumbnailGenerator.ThumbnailPath (item.DefaultVersionUri);
+
+			if (tag_image_eventbox.Children.Length > 0) {
+				tag_image_eventbox.Remove (tag_image);
+				tag_image.Destroy ();
+			}
+
+			using (ImageFile image = new ImageFile (thumbnail_path)) {
+				Gdk.Pixbuf data = image.Load ();
+				data = PixbufUtils.ScaleToMaxSize (data, 400, 400);
+				tag_image_height = data.Height;
+				tag_image_width = data.Width;
+				tag_image = new Gtk.Image (data);
+				tag_image_eventbox.Add (tag_image);
+				tag_image_eventbox.ShowAll ();
+			}
+		}
+
+		private void HandleTagImageButtonPressEvent (object sender, Gtk.ButtonPressEventArgs args)
+		{
+			double x = args.Event.X;
+			double y = args.Event.Y;
+
+			// translate the centered image to top left corner
+			double tag_image_center_x = tag_image_width / 2;
+			double tag_image_center_y = tag_image_height / 2;
+
+			double allocation_center_x = tag_image_eventbox.Allocation.Width / 2;
+			double allocation_center_y = tag_image_eventbox.Allocation.Height / 2;
+
+			double dx = allocation_center_x - tag_image_center_x;
+			double dy = allocation_center_y - tag_image_center_y;
+
+			if (dx < 0)
+				dx = 0;
+			if (dy < 0)
+				dy = 0;
+
+			x -= dx;
+			y -= dy;
+
+			// bail if we're in the eventbox but not the image
+			if (x < 0 || x > tag_image_width)
+				return;
+			if (y < 0 || y > tag_image_height)
+				return;
+
+			FacebookTagPopup popup = new FacebookTagPopup (friends);
+		}
+
+		private void HandleThumbnailIconViewKeyPressEvent (object sender, Gtk.KeyPressEventArgs args)
+		{
+			thumbnail_iconview.Selection.Clear ();
+		}
+
+		private void HandleResponse (object sender, Gtk.ResponseArgs args)
+		{
+			if (args.ResponseId != Gtk.ResponseType.Ok) {
+				Dialog.Destroy ();
+				return;
+			}
+
+			if (account != null) {
+				Dialog.Hide ();
+
+				command_thread = new System.Threading.Thread (new System.Threading.ThreadStart (Upload));
+				command_thread.Name = Mono.Unix.Catalog.GetString ("Uploading Pictures");
+
+				progress_dialog = new FSpot.ThreadProgressDialog (command_thread, items.Length);
+				progress_dialog.Start ();
+			}
+		}
+
+		private void Upload ()
+		{
+			Album album = null;
+
+			if (create_album_radiobutton.Active) {
+				string name = album_name_entry.Text;
+				if (name.Length == 0) {
+					HigMessageDialog mbox = new HigMessageDialog (Dialog, Gtk.DialogFlags.DestroyWithParent | Gtk.DialogFlags.Modal, Gtk.MessageType.Error, Gtk.ButtonsType.Ok, Catalog.GetString ("Album must have a name"), Catalog.GetString ("Please name your album or choose an existing album."));
+					mbox.Run ();
+					mbox.Destroy ();
+					return;
+				}
+
+				string description = album_description_entry.Text;
+				string location = album_location_entry.Text;
+
+				try {
+					album = account.Facebook.CreateAlbum (name, description, location);
+				}
+				catch (FacebookException fe) {
+					HigMessageDialog mbox = new HigMessageDialog (Dialog, Gtk.DialogFlags.DestroyWithParent | Gtk.DialogFlags.Modal, Gtk.MessageType.Error, Gtk.ButtonsType.Ok, Catalog.GetString ("Creating a new album failed"), String.Format (Catalog.GetString ("An error occurred creating a new album.\n\n{0}"), fe.Message));
+					mbox.Run ();
+					mbox.Destroy ();
+					return;
+				}
+			}
+			else {
+				AlbumStore store = (AlbumStore) existing_album_combobox.Model;
+				album = store.Albums [existing_album_combobox.Active];
+			}
+
+			long sent_bytes = 0;
+
+			FilterSet filters = new FilterSet ();
+			filters.Add (new JpegFilter ());
+			filters.Add (new ResizeFilter ((uint) size));
+
+			for (int i = 0; i < items.Length; i++) {
+				try {
+					IBrowsableItem item = items [i];
+
+					FileInfo file_info;
+					Console.WriteLine ("uploading {0}", i);
+
+					progress_dialog.Message = String.Format (Catalog.GetString ("Uploading picture \"{0}\" ({1} of {2})"), item.Name, i + 1, items.Length);
+					progress_dialog.ProgressText = string.Empty;
+					progress_dialog.Fraction = i / (double) items.Length;
+
+					FilterRequest request = new FilterRequest (item.DefaultVersionUri);
+					filters.Convert (request);
+
+					file_info = new FileInfo (request.Current.LocalPath);
+
+					Mono.Facebook.Photo photo = album.Upload (captions [i] ?? "", request.Current.LocalPath);
+
+					sent_bytes += file_info.Length;
+				}
+				catch (Exception e) {
+					progress_dialog.Message = String.Format (Catalog.GetString ("Error Uploading To Facebook: {0}"), e.Message);
+					progress_dialog.ProgressText = Catalog.GetString ("Error");
+					Console.WriteLine (e);
+
+					if (progress_dialog.PerformRetrySkip ())
+						i--;
+				}
+			}
+
+			progress_dialog.Message = Catalog.GetString ("Done Sending Photos");
+			progress_dialog.Fraction = 1.0;
+			progress_dialog.ProgressText = Catalog.GetString ("Upload Complete");
+			progress_dialog.ButtonLabel = Gtk.Stock.Ok;
+
+			Dialog.Destroy ();
+		}
+	}
+}

Copied: trunk/extensions/Exporters/FacebookExport/FacebookExport.glade (from r4368, /trunk/extensions/FacebookExport/FacebookExport.glade)
==============================================================================

Added: trunk/extensions/Exporters/FacebookExport/Makefile
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FacebookExport/Makefile	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,57 @@
+NAME = FacebookExport
+
+SOURCES = \
+	FacebookExport.cs
+
+PACKAGES = \
+	-pkg:glade-sharp-2.0 \
+	-pkg:gtk-sharp-2.0 \
+	-pkg:f-spot
+
+ASSEMBLIES = \
+	-r:Mono.Posix			\
+	-r:Mono.Facebook.dll
+
+RESOURCES = \
+	-resource:$(NAME).glade \
+	-resource:$(NAME).addin.xml
+
+ASSEMBLY = $(NAME).dll
+
+FACEBOOK_SOURCES =				\
+	Mono.Facebook/Album.cs			\
+	Mono.Facebook/AssemblyInfo.cs		\
+	Mono.Facebook/Error.cs			\
+	Mono.Facebook/Event.cs			\
+	Mono.Facebook/FacebookException.cs	\
+	Mono.Facebook/FacebookParam.cs		\
+	Mono.Facebook/FacebookSession.cs	\
+	Mono.Facebook/Friend.cs			\
+	Mono.Facebook/FriendInfo.cs		\
+	Mono.Facebook/Group.cs			\
+	Mono.Facebook/Location.cs		\
+	Mono.Facebook/Notification.cs		\
+	Mono.Facebook/PeopleList.cs		\
+	Mono.Facebook/Photo.cs			\
+	Mono.Facebook/Responses.cs		\
+	Mono.Facebook/SessionWrapper.cs		\
+	Mono.Facebook/Tag.cs			\
+	Mono.Facebook/User.cs			\
+	Mono.Facebook/Util.cs
+
+all: $(ASSEMBLY)
+
+Mono.Facebook.dll: $(FACEBOOK_SOURCES)
+	gmcs -target:library -out:$@ $(FACEBOOK_SOURCES)
+
+install: all
+	cp *.dll ~/.gnome2/f-spot/addins/
+
+mpack: $(ASSEMBLY)
+	mautil p $(ASSEMBLY)
+
+$(ASSEMBLY): $(SOURCES) $(NAME).addin.xml Mono.Facebook.dll
+	gmcs -target:library -out:$@ $(SOURCES) $(PACKAGES) $(ASSEMBLIES) $(RESOURCES)
+
+clean:
+	rm -f $(ASSEMBLY) *~ *.bak *.mpack *.gladep *.dll

Added: trunk/extensions/Exporters/FacebookExport/Mono.Facebook/Album.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FacebookExport/Mono.Facebook/Album.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,118 @@
+//
+// Mono.Facebook.Album.cs:
+//
+// Authors:
+//	Thomas Van Machelen (thomas vanmachelen gmail com)
+//
+// (C) Copyright 2007 Novell, Inc. (http://www.novell.com)
+//
+
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Xml.Serialization;
+using System.Text;
+using System.Net;
+
+namespace Mono.Facebook
+{
+	[XmlRoot ("photos_createAlbum_response", Namespace="http://api.facebook.com/1.0/";)]
+	public class Album : SessionWrapper
+	{
+		[XmlElement ("aid")]
+		public long AId;
+
+		[XmlElement ("cover_pid")]
+		public long ConverPId;
+
+		[XmlElement ("owner")]
+		public int Owner;
+
+		[XmlElement ("name")]
+		public string Name;
+
+		[XmlElement ("created")]
+		public int Created;
+
+		[XmlElement ("modified")]
+		public int Modified;
+
+		[XmlElement ("description")]
+		public string Description;
+
+		[XmlElement ("location")]
+		public string Location;
+
+		[XmlElement ("link")]
+		public string Link;
+
+		[XmlIgnore ()]
+		public Uri Uri
+		{
+			get { return new Uri (Link); }
+		}
+
+		public Photo[] GetPhotos ()
+		{
+			PhotosResponse rsp = Session.Util.GetResponse<PhotosResponse> ("facebook.photos.get",
+				FacebookParam.Create ("aid", AId),
+				FacebookParam.Create ("session_key", Session.SessionKey),
+				FacebookParam.Create ("call_id", DateTime.Now.Ticks));
+
+			foreach (Photo p in rsp.Photos)
+				p.Session = Session;
+
+			return rsp.Photos;
+		}
+
+		public Tag[] GetTags ()
+		{
+			StringBuilder pids = new StringBuilder ();
+
+			foreach (Photo p in GetPhotos ()) {
+				if (pids.Length > 0)
+					pids.Append (",");
+
+				pids.Append (p.PId);
+			}
+
+			PhotoTagsResponse rsp = Session.Util.GetResponse<PhotoTagsResponse> ("facebook.photos.getTags",
+				FacebookParam.Create ("pids", pids),
+				FacebookParam.Create ("session_key", Session.SessionKey),
+				FacebookParam.Create ("call_id", System.DateTime.Now.Ticks));
+
+			foreach (Tag t in rsp.Tags)
+				t.Session = Session;
+
+			return rsp.Tags;
+		}
+
+
+		public Photo Upload (string caption, string path)
+		{
+			Photo uploaded = Session.Util.Upload (AId, caption, path, Session.SessionKey);
+			uploaded.Session = this.Session;
+
+			return uploaded;
+		}
+	}
+}

Added: trunk/extensions/Exporters/FacebookExport/Mono.Facebook/AssemblyInfo.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FacebookExport/Mono.Facebook/AssemblyInfo.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,35 @@
+// AssemblyInfo.cs for Mono.Facebook.dll
+//
+// Authors:
+//	Thomas Van Machelen (thomas vanmachelen gmail com)
+//
+// Copyright (c) 2007 Novell, Inc. (http://www.novell.com)
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using System.Reflection;
+using System.Runtime.CompilerServices;
+
+[assembly: AssemblyVersion("0.1.0")]
+[assembly: AssemblyTitle ("Mono.Facebook")]
+[assembly: AssemblyDescription ("")]
+[assembly: AssemblyCopyright ("(c) 2007 Novell, Inc.")]
+[assembly: AssemblyCompany ("Novell, Inc.")]

Added: trunk/extensions/Exporters/FacebookExport/Mono.Facebook/Error.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FacebookExport/Mono.Facebook/Error.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,43 @@
+//
+// Mono.Facebook.FacebookException.cs:
+//
+// Authors:
+//	George Talusan (george convolve ca)
+//
+// (C) Copyright 2007 Novell, Inc. (http://www.novell.com)
+//
+
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+using System;
+using System.Xml.Serialization;
+
+namespace Mono.Facebook
+{
+	[XmlRoot ("error_response", Namespace = "http://api.facebook.com/1.0/";, IsNullable = false)]
+	public class Error
+	{
+		[XmlElement ("error_code")]
+		public int ErrorCode;
+
+		[XmlElement ("error_msg")]
+		public string ErrorMsg;
+	}
+}

Added: trunk/extensions/Exporters/FacebookExport/Mono.Facebook/Event.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FacebookExport/Mono.Facebook/Event.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,145 @@
+//
+// Mono.Facebook.Event.cs:
+// // Authors:
+//	George Talusan (george convolve ca)
+//
+
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Xml.Serialization;
+using System.Text;
+
+namespace Mono.Facebook
+{
+	public class EventMemberList
+	{
+		public Friend[] Attending;
+
+		public Friend[] Unsure;
+
+		public Friend[] Declined;
+
+		public Friend[] NotReplied;
+	}
+
+	public class Event : SessionWrapper
+	{
+		[XmlElement ("eid")]
+		public long EId;
+
+		[XmlElement ("name")]
+		public string Name;
+
+		[XmlElement ("tagline")]
+		public string Tagline;
+
+		[XmlElement ("nid")]
+		public long NId;
+
+		[XmlElement ("pic")]
+		public string Pic;
+
+		[XmlIgnore ()]
+		public Uri PicUri
+		{
+			get { return new Uri (Pic); }
+		}
+
+		[XmlElement ("pic_big")]
+		public string PicBig;
+
+		[XmlIgnore ()]
+		public Uri PicBigUri
+		{
+			get { return new Uri (PicBig); }
+		}
+
+		[XmlElement ("pic_small")]
+		public string PicSmall;
+
+		[XmlIgnore ()]
+		public Uri PicSmallUri
+		{
+			get { return new Uri (PicSmall); }
+		}
+
+		[XmlElement ("host")]
+		public string Host;
+
+		[XmlElement ("description")]
+		public string Description;
+
+		[XmlElement ("event_type")]
+		public string EventType;
+
+		[XmlElement ("event_subtype")]
+		public string EventSubType;
+
+		[XmlElement ("start_time")]
+		public long StartTime;
+
+		[XmlElement ("end_time")]
+		public long EndTime;
+
+		[XmlElement ("creator")]
+		public long? Creator;
+
+		[XmlElement ("update_time")]
+		public long UpdateTime;
+
+		[XmlElement ("location")]
+		public string Location;
+
+		[XmlElement ("venue")]
+		public Location Venue;
+
+		[XmlIgnore ()]
+		public EventMemberList MemberList {
+			get {
+				EventMembersResponse rsp = Session.Util.GetResponse<EventMembersResponse> ("facebook.events.getMembers",
+					FacebookParam.Create ("session_key", Session.SessionKey),
+					FacebookParam.Create ("call_id", DateTime.Now.Ticks),
+					FacebookParam.Create ("eid", EId));
+
+				EventMemberList list = new EventMemberList ();
+
+				list.Attending = new Friend [rsp.Attending.UIds.Length];
+				for (int i = 0; i < list.Attending.Length; i++)
+					list.Attending[i] = new Friend (rsp.Attending.UIds [i], this.Session);
+
+				list.Unsure = new Friend [rsp.Unsure.UIds.Length];
+				for (int i = 0; i < list.Unsure.Length; i++)
+					list.Unsure [i] = new Friend (rsp.Unsure.UIds [i], this.Session);
+
+				list.Declined = new Friend [rsp.Declined.UIds.Length];
+				for (int i = 0; i < list.Declined.Length; i ++)
+					list.Declined [i] = new Friend (rsp.Declined.UIds [i], this.Session);
+
+				list.NotReplied = new Friend [rsp.NotReplied.UIds.Length];
+
+				return list;
+			}
+
+		}
+	}
+}

Added: trunk/extensions/Exporters/FacebookExport/Mono.Facebook/FacebookException.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FacebookExport/Mono.Facebook/FacebookException.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,55 @@
+//
+// Mono.Facebook.FacebookException.cs:
+//
+// Authors:
+//	Thomas Van Machelen (thomas vanmachelen gmail com)
+//
+// (C) Copyright 2007 Novell, Inc. (http://www.novell.com)
+//
+
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+using System;
+
+public class FacebookException : Exception
+{
+	private int error_code;
+	private string error_message;
+
+	public int ErrorCode {
+		get { return error_code; }
+	}
+
+	public string ErrorMessage {
+		get { return error_message; }
+	}
+
+	public FacebookException (int error_code, string error_message)
+		: base (CreateMessage (error_code, error_message))
+	{
+		this.error_code = error_code;
+		this.error_message = error_message;
+	}
+
+	private static string CreateMessage (int error_code, string error_message)
+	{
+		return string.Format ("Code: {0}, Message: {1}", error_code, error_message);
+	}
+}

Added: trunk/extensions/Exporters/FacebookExport/Mono.Facebook/FacebookParam.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FacebookExport/Mono.Facebook/FacebookParam.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,91 @@
+//
+// Mono.Facebook.FacebookParam.cs:
+//
+// Authors:
+//	Thomas Van Machelen (thomas vanmachelen gmail com)
+//	George Talusan (george convolvce ca)
+//
+// (C) Copyright 2007 Novell, Inc. (http://www.novell.com)
+//
+
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+using System;
+using System.Text;
+
+namespace Mono.Facebook
+{
+	public class FacebookParam : IComparable
+	{
+		private string name;
+		private object value;
+
+		public string Name {
+			get{ return name; }
+		}
+
+		public string Value {
+			get {
+				if (value is Array)
+					return ConvertArrayToString (value as Array);
+				else
+					return value.ToString ();
+			}
+		}
+
+		protected FacebookParam (string name, object value)
+		{
+			this.name = name;
+			this.value = value;
+		}
+
+		public override string ToString ()
+		{
+			return string.Format ("{0}={1}", Name, Value);
+		}
+
+		public static FacebookParam Create (string name, object value)
+		{
+			return new FacebookParam (name, value);
+		}
+
+		public int CompareTo (object obj)
+		{
+			if (!(obj is FacebookParam))
+				return -1;
+
+			return this.name.CompareTo ((obj as FacebookParam).name);
+		}
+
+		private static string ConvertArrayToString (Array a)
+		{
+			StringBuilder builder = new StringBuilder ();
+
+			for (int i = 0; i < a.Length; i++) {
+				if (i > 0)
+					builder.Append (",");
+
+				builder.Append (a.GetValue (i).ToString ());
+			}
+
+			return builder.ToString ();
+		}
+	}
+}

Added: trunk/extensions/Exporters/FacebookExport/Mono.Facebook/FacebookSession.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FacebookExport/Mono.Facebook/FacebookSession.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,288 @@
+//
+// Mono.Facebook.FacebookSession.cs:
+//
+// Authors:
+//	Thomas Van Machelen (thomas vanmachelen gmail com)
+//	George Talusan (george convolve ca)
+//
+// (C) Copyright 2007 Novell, Inc. (http://www.novell.com)
+//
+
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+using System;
+using System.Xml;
+using System.Collections.Generic;
+using System.Text;
+using System.IO;
+using System.Net;
+
+namespace Mono.Facebook
+{
+	public class FacebookSession
+	{
+		Util util;
+		SessionInfo session_info;
+		string auth_token;
+
+		internal Util Util
+		{
+			get { return util; }
+		}
+
+		internal string SessionKey
+		{
+			get { return session_info.SessionKey; }
+		}
+
+		// use this for plain sessions
+		public FacebookSession (string api_key, string shared_secret)
+		{
+			util = new Util (api_key, shared_secret);
+		}
+
+		// use this if you want to re-start an infinite session
+		public FacebookSession (string api_key, SessionInfo session_info)
+			: this (api_key, session_info.Secret)
+		{
+			this.session_info = session_info;
+		}
+
+		public Uri CreateToken ()
+		{
+			XmlDocument doc = util.GetResponse ("facebook.auth.createToken");
+			auth_token = doc.InnerText;
+
+			return new Uri (string.Format ("http://www.facebook.com/login.php?api_key={0}&v=1.0&auth_token={1}";, util.ApiKey, auth_token));
+		}
+
+		public Uri GetUriForInfiniteToken()
+		{
+			return new Uri(string.Format("http://www.facebook.com/code_gen.php?v=1.0&api_key={0}";, util.ApiKey));
+		}
+
+		public SessionInfo GetSession ()
+		{
+			return GetSessionFromToken(auth_token);
+		}
+
+		public SessionInfo GetSessionFromToken(string auth_token)
+		{
+			this.session_info = util.GetResponse<SessionInfo>("facebook.auth.getSession",
+					FacebookParam.Create("auth_token", auth_token));
+			this.util.SharedSecret = session_info.Secret;
+
+			this.auth_token = string.Empty;
+
+			return session_info;
+		}
+
+		public Album[] GetAlbums ()
+		{
+			AlbumsResponse rsp = util.GetResponse<AlbumsResponse> ("facebook.photos.getAlbums",
+				FacebookParam.Create ("uid", session_info.UId),
+				FacebookParam.Create ("session_key", session_info.SessionKey),
+				FacebookParam.Create ("call_id", DateTime.Now.Ticks));
+
+			foreach (Album album in rsp.Albums)
+				album.Session = this;
+
+			return rsp.Albums;
+		}
+
+		public Album CreateAlbum (string name, string description, string location)
+		{
+			// create parameter list
+			List<FacebookParam> param_list = new List<FacebookParam> ();
+			param_list.Add (FacebookParam.Create ("session_key", session_info.SessionKey));
+			param_list.Add (FacebookParam.Create ("call_id", DateTime.Now.Ticks));
+			param_list.Add (FacebookParam.Create ("name", name));
+
+			if (description != null && description != string.Empty)
+				param_list.Add (FacebookParam.Create ("description", description));
+
+			if (location != null && location != string.Empty)
+				param_list.Add (FacebookParam.Create ("location", location));
+
+			// create the albums
+			Album new_album = util.GetResponse<Album> ("facebook.photos.createAlbum", param_list.ToArray ());
+			new_album.Session = this;
+
+			// return
+			return new_album;
+		}
+
+		public Group[] GetGroups ()
+		{
+			return this.GetGroups (session_info.UId, null);
+		}
+
+		public Group[] GetGroups (long? uid, long[] gids)
+		{
+			List<FacebookParam> param_list = new List<FacebookParam>();
+			param_list.Add (FacebookParam.Create ("session_key", session_info.SessionKey));
+			param_list.Add (FacebookParam.Create ("call_id", DateTime.Now.Ticks));
+			if (uid != null)
+				param_list.Add (FacebookParam.Create ("uid", uid));
+
+			if (gids != null)
+				param_list.Add (FacebookParam.Create ("gids", gids));
+
+			GroupsResponse rsp = util.GetResponse<GroupsResponse>("facebook.groups.get", param_list.ToArray ());
+
+			foreach (Group gr in rsp.Groups)
+				gr.Session = this;
+
+			return rsp.Groups;
+		}
+
+		public Event[] GetEvents ()
+		{
+			return GetEvents (session_info.UId, null, 0, 0, null);
+		}
+
+		public Event[] GetEvents (long? uid, long[] eids, long start_time, long end_time, string rsvp_status)
+		{
+			List<FacebookParam> param_list = new List<FacebookParam>();
+			param_list.Add (FacebookParam.Create ("session_key", session_info.SessionKey));
+			param_list.Add (FacebookParam.Create ("call_id", DateTime.Now.Ticks));
+			if (uid != null)
+				param_list.Add (FacebookParam.Create ("uid", uid));
+
+			if (eids != null)
+				param_list.Add (FacebookParam.Create ("eids", eids));
+
+			param_list.Add (FacebookParam.Create ("start_time", start_time));
+			param_list.Add (FacebookParam.Create ("end_time", start_time));
+			if (rsvp_status != null)
+				param_list.Add (FacebookParam.Create ("rsvp_status", rsvp_status));
+
+			EventsResponse rsp = util.GetResponse<EventsResponse>("facebook.events.get", param_list.ToArray ());
+
+			foreach (Event evt in rsp.Events)
+				evt.Session = this;
+
+			return rsp.Events;
+		}
+
+		public User GetUserInfo (long uid)
+		{
+			User[] users = this.GetUserInfo (new long[1] { uid }, User.FIELDS);
+
+			if (users.Length < 1)
+				return null;
+
+			return users[0];
+		}
+
+		public User[] GetUserInfo (long[] uids, string[] fields)
+		{
+			List<FacebookParam> param_list = new List<FacebookParam>();
+			param_list.Add (FacebookParam.Create ("session_key", session_info.SessionKey));
+			param_list.Add (FacebookParam.Create ("call_id", DateTime.Now.Ticks));
+
+			if (uids == null || uids.Length == 0)
+				throw new Exception ("uid not provided");
+
+			param_list.Add (FacebookParam.Create ("uids", uids));
+			param_list.Add (FacebookParam.Create ("fields", fields));
+
+			UserInfoResponse rsp = util.GetResponse<UserInfoResponse>("facebook.users.getInfo", param_list.ToArray ());
+			return rsp.Users;
+		}
+
+		public Me GetLoggedInUser ()
+		{
+			return new Me (session_info.UId, this);
+		}
+
+		public Notifications GetNotifications ()
+		{
+			Notifications notifications = util.GetResponse<Notifications>("facebook.notifications.get",
+				FacebookParam.Create ("uid", session_info.UId),
+				FacebookParam.Create ("session_key", session_info.SessionKey),
+				FacebookParam.Create ("call_id", DateTime.Now.Ticks));
+
+			foreach (Friend f in notifications.FriendRequests)
+				f.Session = this;
+
+			return notifications;
+		}
+
+		public Friend[] GetFriends ()
+		{
+			FriendsResponse response = Util.GetResponse<FriendsResponse>("facebook.friends.get",
+					FacebookParam.Create ("session_key", SessionKey),
+					FacebookParam.Create ("call_id", DateTime.Now.Ticks));
+
+			Friend[] friends = new Friend[response.UIds.Length];
+
+			for (int i = 0; i < friends.Length; i++)
+				friends[i] = new Friend (response.UIds[i], this);
+
+			return friends;
+		}
+
+		public bool AreFriends (Friend friend1, Friend friend2)
+		{
+			return AreFriends (friend1.UId, friend2.UId);
+		}
+
+		public bool AreFriends (long uid1, long uid2)
+		{
+			AreFriendsResponse response = Util.GetResponse<AreFriendsResponse>("facebook.friends.areFriends",
+				   FacebookParam.Create ("session_key", SessionKey),
+				   FacebookParam.Create ("call_id", DateTime.Now.Ticks),
+				   FacebookParam.Create ("uids1", uid1),
+				   FacebookParam.Create ("uids2", uid2));
+
+			return response.friend_infos[0].AreFriends;
+		}
+
+		public FriendInfo[] AreFriends (long[] uids1, long[] uids2)
+		{
+			List<FacebookParam> param_list = new List<FacebookParam> ();
+			param_list.Add (FacebookParam.Create ("session_key", session_info.SessionKey));
+			param_list.Add (FacebookParam.Create ("call_id", DateTime.Now.Ticks));
+
+			if (uids1 == null || uids1.Length == 0)
+				throw new Exception ("uids1 not provided");
+
+			if (uids2 == null || uids2.Length == 0)
+				throw new Exception ("uids2 not provided");
+
+			param_list.Add (FacebookParam.Create ("uids1", uids1));
+			param_list.Add (FacebookParam.Create ("uids2", uids2));
+
+			AreFriendsResponse rsp = util.GetResponse<AreFriendsResponse> ("facebook.friends.areFriends", param_list.ToArray ());
+			return rsp.friend_infos;
+		}
+
+		public XmlDocument Query (string sql_query)
+		{
+			XmlDocument doc = Util.GetResponse ("facebook.fql.query",
+				FacebookParam.Create ("session_key", SessionKey),
+				FacebookParam.Create ("call_id", DateTime.Now.Ticks),
+				FacebookParam.Create ("query", sql_query));
+
+			return doc;
+		}
+	}
+}

Added: trunk/extensions/Exporters/FacebookExport/Mono.Facebook/Friend.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FacebookExport/Mono.Facebook/Friend.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,99 @@
+//
+// Mono.Facebook.Friend.cs:
+//
+// Authors:
+//	George Talusan (george convolve ca)
+//
+
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Xml.Serialization;
+using System.Text;
+
+namespace Mono.Facebook
+{
+	public class Friend : SessionWrapper
+	{
+		[XmlElement ("uid")]
+		public long UId;
+
+		public Friend (long UId, FacebookSession session)
+		{
+			this.UId = UId;
+			this.Session = session;
+		}
+
+		public Friend ()
+		{ }
+
+		public User GetUserInfo ()
+		{
+			User[] users = Session.GetUserInfo (new long[] { UId }, User.FIELDS);
+
+			return users[0];
+		}
+
+		public bool IsFriendsWith (Friend friend)
+		{
+			FriendInfo[] info = IsFriendsWith (new Friend[] { friend });
+			return info[0].AreFriends;
+		}
+
+		public Group[] Groups
+		{
+			get { return Session.GetGroups (UId, null); }
+		}
+
+		public Event[] Events
+		{
+			get { return Session.GetEvents (UId, null, 0, 0, null); }
+		}
+
+		public FriendInfo[] IsFriendsWith (Friend[] friends)
+		{
+			long[] me = new long[friends.Length];
+			for (int i = 0; i < me.Length; i++)
+				me[i] = UId;
+
+			long[] them = new long[friends.Length];
+			for (int i = 0; i < friends.Length; i++)
+			{
+				them[i] = friends[i].UId;
+			}
+
+			return Session.AreFriends (me, them);
+		}
+	}
+
+	public class Me : Friend
+	{
+		public Me (long uid, FacebookSession session)
+			: base (uid, session)
+		{ }
+
+		public Notifications Notifications
+		{
+			get { return Session.GetNotifications (); }
+		}
+	}
+}

Added: trunk/extensions/Exporters/FacebookExport/Mono.Facebook/FriendInfo.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FacebookExport/Mono.Facebook/FriendInfo.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,43 @@
+//
+// Mono.Facebook.FriendInfo.cs:
+//
+// Authors:
+//	Thomas Van Machelen (thomas vanmachelen gmail com)
+//
+
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+using System;
+using System.Xml.Serialization;
+
+namespace Mono.Facebook
+{
+	public class FriendInfo
+	{
+		[XmlElement ("uid1")]
+		public int UId1;
+
+		[XmlElement ("uid2")]
+		public int UId2;
+
+		[XmlElement ("are_friends")]
+		public bool AreFriends;
+	}
+}

Added: trunk/extensions/Exporters/FacebookExport/Mono.Facebook/Group.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FacebookExport/Mono.Facebook/Group.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,151 @@
+//
+// Mono.Facebook.Group.cs:
+//
+// Authors:
+//	Thomas Van Machelen (thomas vanmachelen gmail com)
+//	George Talusan (george convolve ca)
+//
+// (C) Copyright 2007 Novell, Inc. (http://www.novell.com)
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+using System;
+using System.Xml.Serialization;
+
+namespace Mono.Facebook
+{
+	public class Group : SessionWrapper
+	{
+		[XmlElement ("gid")]
+		public long GId;
+
+		[XmlElement ("name")]
+		public string Name;
+
+		[XmlElement ("nid")]
+		public int NId;
+
+		[XmlElement ("description")]
+		public string Description;
+
+		[XmlElement ("group_type")]
+		public string GroupType;
+
+		[XmlElement ("group_subtype")]
+		public string GroupSubType;
+
+		[XmlElement ("recent_news")]
+		public string RecentNews;
+
+		[XmlElement ("pic")]
+		public string pic;
+
+		[XmlIgnore ()]
+		public Uri Picture
+		{
+			get { return new Uri (pic); }
+		}
+
+		[XmlElement ("pic_big")]
+		public string pic_big;
+
+		[XmlIgnore ()]
+		public Uri PictureBig
+		{
+			get { return new Uri (pic_big); }
+		}
+
+		[XmlElement ("pic_small")]
+		public string pic_small;
+
+		[XmlIgnore ()]
+		public Uri PictureSmall
+		{
+			get { return new Uri (pic_small); }
+		}
+
+		[XmlElement ("creator")]
+		public System.Nullable<int> Creator;
+
+		[XmlElement ("update_time")]
+		public int UpdateTime;
+
+		[XmlElement ("office")]
+		public string Office;
+
+		[XmlElement ("website")]
+		public string website;
+
+		[XmlIgnore ()]
+		public Uri WebSite
+		{
+			get
+			{
+				if (website == string.Empty)
+					return null;
+
+				return new Uri (website);
+			}
+		}
+
+		[XmlElement ("venue")]
+		public Location Venue;
+
+
+		public GroupMemberList GetMembers ()
+		{
+			GroupMembersResponse rsp = Session.Util.GetResponse<GroupMembersResponse>("facebook.groups.getMembers",
+				FacebookParam.Create ("gid", GId),
+				FacebookParam.Create ("call_id", DateTime.Now.Ticks),
+				FacebookParam.Create ("session_key", Session.SessionKey));
+
+			GroupMemberList members = new GroupMemberList ();
+
+			members.Members = new Friend [rsp.Members.UIds.Length];
+			for (int i = 0; i < members.Members.Length; i++)
+				members.Members [i] = new Friend (rsp.Members.UIds [i], this.Session);
+
+			members.Admins = new Friend [rsp.Admins.UIds.Length];
+			for (int i = 0; i < members.Admins.Length; i++)
+				members.Admins [i] = new Friend (rsp.Admins.UIds [i], this.Session);
+
+			members.NotReplied = new Friend [rsp.NotReplied.UIds.Length];
+			for (int i = 0; i < members.NotReplied.Length; i++)
+				members.NotReplied [i] = new Friend (rsp.NotReplied.UIds [i], this.Session);
+
+			members.Officers = new Friend [rsp.Officers.UIds.Length];
+			for (int i = 0; i < members.Officers.Length; i++)
+				members.Officers [i] = new Friend (rsp.Officers.UIds [i], this.Session);
+
+			return members;
+		}
+	}
+
+	public class GroupMemberList
+	{
+		public Friend[] Members;
+
+		public Friend[] Admins;
+
+		public Friend[] Officers;
+
+		public Friend[] NotReplied;
+	}
+}

Added: trunk/extensions/Exporters/FacebookExport/Mono.Facebook/Location.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FacebookExport/Mono.Facebook/Location.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,48 @@
+//
+// Mono.Facebook.Location.cs:
+//
+// Authors:
+//	Thomas Van Machelen (thomas vanmachelen gmail com)
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+using System;
+using System.Xml.Serialization;
+
+namespace Mono.Facebook
+{
+	public class Location
+	{
+		[XmlElement ("street")]
+		public string Street;
+
+		[XmlElement ("city")]
+		public string City;
+
+		[XmlElement ("state")]
+		public string State;
+
+		[XmlElement ("country")]
+		public string Country;
+
+		[XmlElement ("zip")]
+		public string Zip;
+	}
+}

Added: trunk/extensions/Exporters/FacebookExport/Mono.Facebook/Notification.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FacebookExport/Mono.Facebook/Notification.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,59 @@
+//
+// Mono.Facebook.Notification.cs:
+//
+// Authors:
+//	George Talusan (george convolve ca)
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+using System;
+using System.IO;
+using System.Xml.Serialization;
+using System.Text;
+
+namespace Mono.Facebook
+{
+	public class Notification
+	{
+		[XmlElement ("unread")]
+		public int Unread;
+
+		[XmlElement ("most_recent")]
+		public long MostRecent;
+	}
+
+	[XmlRoot ("notifications_get_response", Namespace = "http://api.facebook.com/1.0/";, IsNullable = false)]
+	public class Notifications
+	{
+		[XmlElement ("messages")]
+		public Notification Messages;
+
+		[XmlElement ("pokes")]
+		public Notification Pokes;
+
+		[XmlElement ("shares")]
+		public Notification Shares;
+
+		[XmlElement ("friend_requests")]
+		public Friend[] FriendRequests;
+
+
+	}
+}

Added: trunk/extensions/Exporters/FacebookExport/Mono.Facebook/PeopleList.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FacebookExport/Mono.Facebook/PeopleList.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,42 @@
+//
+// Mono.Facebook.PeopleList.cs:
+//
+// Authors:
+//	George Talusan (george convolve ca)
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+using System;
+using System.Xml.Serialization;
+using System.Collections;
+
+namespace Mono.Facebook
+{
+	public class PeopleList
+	{
+		[XmlElement ("uid")]
+		public int[] uid_array;
+
+		public int[] UIds
+		{
+			get { return uid_array ?? new int[0]; }
+		}
+	}
+}

Added: trunk/extensions/Exporters/FacebookExport/Mono.Facebook/Photo.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FacebookExport/Mono.Facebook/Photo.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,106 @@
+//
+// Mono.Facebook.Photo.cs:
+//
+// Authors:
+//	Thomas Van Machelen (thomas vanmachelen gmail com)
+//
+// (C) Copyright 2007 Novell, Inc. (http://www.novell.com)
+//
+
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+using System.Xml.Serialization;
+
+namespace Mono.Facebook
+{
+	[XmlRoot ("photos_upload_response", Namespace="http://api.facebook.com/1.0/";)]
+	public class Photo : SessionWrapper
+	{
+		[XmlElement ("pid")]
+		public long PId;
+
+		[XmlElement ("aid")]
+		public long AId;
+
+		[XmlElement ("owner")]
+		public int Owner;
+
+		[XmlElement ("src")]
+		public string Source;
+
+		[XmlElement ("src_big")]
+		public string SourceBig;
+
+		[XmlElement ("src_small")]
+		public string SourceSmall;
+
+		[XmlElement ("link")]
+		public string Link;
+
+		[XmlElement ("caption")]
+		public string Caption;
+
+		[XmlElement ("created")]
+		public int Created;
+
+		public Tag[] GetTags ()
+		{
+			PhotoTagsResponse rsp = Session.Util.GetResponse<PhotoTagsResponse> ("facebook.photos.getTags",
+				FacebookParam.Create ("pids", PId),
+				FacebookParam.Create ("session_key", Session.SessionKey),
+				FacebookParam.Create ("call_id", System.DateTime.Now.Ticks));
+
+			foreach (Tag t in rsp.Tags)
+				t.Session = Session;
+
+			return rsp.Tags;
+		}
+
+		public Album GetAlbum ()
+		{
+			AlbumsResponse rsp = Session.Util.GetResponse<AlbumsResponse> ("facebook.photos.getAlbums",
+				FacebookParam.Create ("aids", AId),
+				FacebookParam.Create ("session_key", Session.SessionKey),
+				FacebookParam.Create ("call_id", System.DateTime.Now.Ticks));
+
+			if (rsp.Albums.Length < 1)
+				return null;
+
+			rsp.Albums[0].Session = Session;
+			return rsp.Albums[0];
+		}
+
+		// does not work right now: cannot tag photo already visible on facebook
+		public Tag AddTag (string tag_text, float x, float y)
+		{
+			Tag new_tag = Session.Util.GetResponse<Tag> ("facebook.photos.addTag",
+				FacebookParam.Create ("pid", PId),
+				FacebookParam.Create ("tag_text", tag_text),
+				FacebookParam.Create ("x", x),
+				FacebookParam.Create ("y", y),
+				FacebookParam.Create ("session_key", Session.SessionKey),
+				FacebookParam.Create ("call_id", System.DateTime.Now.Ticks));
+
+			new_tag.Session = Session;
+
+			return new_tag;
+		}
+	}
+}

Added: trunk/extensions/Exporters/FacebookExport/Mono.Facebook/Responses.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FacebookExport/Mono.Facebook/Responses.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,211 @@
+//
+// Mono.Facebook.Responses.cs:
+//
+// Authors:
+//	Thomas Van Machelen (thomas vanmachelen gmail com)
+//	George Talusan (george convolve ca)
+//
+// (C) Copyright 2007 Novell, Inc. (http://www.novell.com)
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+using System;
+using System.Xml.Serialization;
+
+namespace Mono.Facebook
+{
+	[XmlRoot ("auth_getSession_response", Namespace = "http://api.facebook.com/1.0/";, IsNullable = false)]
+	public class SessionInfo
+	{
+		[XmlElement ("session_key")]
+		public string SessionKey;
+
+		[XmlElement ("uid")]
+		public long UId;
+
+		[XmlElement ("secret")]
+		public string Secret;
+
+		[XmlElement ("expires")]
+		public long Expires;
+
+		[XmlIgnore ()]
+		public bool IsInfinite
+		{
+			get { return Expires == 0; }
+		}
+
+		public SessionInfo ()
+		{}
+
+		// use this if you want to create a session based on infinite session
+		// credentials
+		public SessionInfo (string session_key, long uid, string secret)
+		{
+			this.SessionKey = session_key;
+			this.UId = uid;
+			this.Secret = secret;
+			this.Expires = 0;
+		}
+	}
+
+	[XmlRoot ("photos_getAlbums_response", Namespace = "http://api.facebook.com/1.0/";, IsNullable = false)]
+	public class AlbumsResponse
+	{
+		[XmlElement ("album")]
+		public Album[] album_array;
+
+		[XmlIgnore ()]
+		public Album[] Albums
+		{
+			get { return album_array ?? new Album[0]; }
+		}
+
+		[XmlAttribute ("list")]
+		public bool List;
+	}
+
+	[XmlRoot ("photos_get_response", Namespace = "http://api.facebook.com/1.0/";, IsNullable = false)]
+	public class PhotosResponse
+	{
+		[XmlElement ("photo")]
+		public Photo[] photo_array;
+
+		[XmlIgnore ()]
+		public Photo[] Photos
+		{
+			get { return photo_array ?? new Photo[0]; }
+		}
+
+		[XmlAttribute ("list")]
+		public bool List;
+	}
+
+	[XmlRoot ("photos_getTags_response", Namespace = "http://api.facebook.com/1.0/";, IsNullable = false)]
+	public class PhotoTagsResponse
+	{
+		[XmlElement ("photo_tag")]
+		public Tag[] tag_array;
+
+		public Tag[] Tags
+		{
+			get { return tag_array ?? new Tag[0]; }
+		}
+
+		[XmlAttribute ("list")]
+		public bool List;
+	}
+	[XmlRoot ("groups_get_response", Namespace = "http://api.facebook.com/1.0/";)]
+	public class GroupsResponse
+	{
+		[XmlElement ("group")]
+		public Group[] group_array;
+
+		public Group[] Groups
+		{
+			get { return group_array ?? new Group[0]; }
+		}
+
+		[XmlAttribute ("list")]
+		public bool List;
+	}
+
+	[XmlRoot ("groups_getMembers_response", Namespace = "http://api.facebook.com/1.0/";, IsNullable = false)]
+	public class GroupMembersResponse
+	{
+		[XmlElement ("members")]
+		public PeopleList Members;
+
+		[XmlElement ("admins")]
+		public PeopleList Admins;
+
+		[XmlElement ("officers")]
+		public PeopleList Officers;
+
+		[XmlElement ("not_replied")]
+		public PeopleList NotReplied;
+	}
+
+	[XmlRoot ("users_getInfo_response", Namespace = "http://api.facebook.com/1.0/";, IsNullable = false)]
+	public class UserInfoResponse
+	{
+		[XmlElement ("user")]
+		public User[] user_array;
+
+		public User[] Users
+		{
+			get { return user_array ?? new User[0]; }
+		}
+
+		[XmlAttribute ("list")]
+		public bool List;
+	}
+
+	[XmlRoot ("events_get_response", Namespace="http://api.facebook.com/1.0/";, IsNullable=false)]
+	public class EventsResponse
+	{
+		[XmlElement ("event")]
+		public Event[] event_array;
+
+		public Event[] Events
+		{
+			get { return event_array ?? new Event[0]; }
+		}
+
+		[XmlAttribute ("list")]
+		public bool List;
+	}
+
+	[XmlRoot ("events_getMembers_response", Namespace = "http://api.facebook.com/1.0/";, IsNullable = false)]
+	public class EventMembersResponse
+	{
+		[XmlElement ("attending")]
+		public PeopleList Attending;
+
+		[XmlElement ("unsure")]
+		public PeopleList Unsure;
+
+		[XmlElement ("declined")]
+		public PeopleList Declined;
+
+		[XmlElement ("not_replied")]
+		public PeopleList NotReplied;
+	}
+
+	[XmlRoot ("friends_get_response", Namespace = "http://api.facebook.com/1.0/";, IsNullable = false)]
+	public class FriendsResponse
+	{
+		[XmlElement ("uid")]
+		public int[] uids;
+
+		[XmlIgnore ()]
+		public int[] UIds
+		{
+			get { return uids ?? new int[0]; }
+		}
+	}
+
+	[XmlRoot ("friends_areFriends_response", Namespace = "http://api.facebook.com/1.0/";, IsNullable = false)]
+	public class AreFriendsResponse
+	{
+		[XmlElement ("friend_info")]
+		public FriendInfo[] friend_infos;
+	}
+}

Added: trunk/extensions/Exporters/FacebookExport/Mono.Facebook/SessionWrapper.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FacebookExport/Mono.Facebook/SessionWrapper.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,38 @@
+//
+// Mono.Facebook.SessionWrapper.cs:
+//
+// Authors:
+//	Thomas Van Machelen (thomas vanmachelen gmail com)
+//
+// (C) Copyright 2007 Novell, Inc. (http://www.novell.com)
+//
+
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+using System.Xml.Serialization;
+
+namespace Mono.Facebook
+{
+	public class SessionWrapper
+	{
+		[XmlIgnore ()]
+		internal FacebookSession Session;
+	}
+}

Added: trunk/extensions/Exporters/FacebookExport/Mono.Facebook/Tag.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FacebookExport/Mono.Facebook/Tag.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,62 @@
+//
+// Mono.Facebook.Tag.cs:
+//
+// Authors:
+//	Thomas Van Machelen (thomas vanmachelen gmail com)
+//
+// (C) Copyright 2007 Novell, Inc. (http://www.novell.com)
+//
+
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using System.Xml.Serialization;
+
+namespace Mono.Facebook
+{
+	[XmlRoot ("photos_addTag_response", Namespace="http://api.facebook.com/v1.0";)]
+	public class Tag : SessionWrapper
+	{
+		[XmlElement ("pid")]
+		public long PId;
+
+		[XmlElement ("subject")]
+		public int Subject;
+
+		[XmlElement ("xcoord")]
+		public decimal XCoordinate;
+
+		[XmlElement ("ycoord")]
+		public decimal YCoordinate;
+
+		public Photo GetPhoto ()
+		{
+			PhotosResponse rsp = Session.Util.GetResponse<PhotosResponse> ("facebook.photos.get", FacebookParam.Create ("pids", PId),
+					FacebookParam.Create ("session_key", Session.SessionKey),
+					FacebookParam.Create ("call_id", System.DateTime.Now.Ticks));
+
+			if (rsp.Photos.Length < 1)
+				return null;
+
+			rsp.Photos[0].Session = Session;
+			return rsp.Photos[0];
+		}
+	}
+}

Added: trunk/extensions/Exporters/FacebookExport/Mono.Facebook/User.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FacebookExport/Mono.Facebook/User.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,370 @@
+//
+// Mono.Facebook.User.cs:
+//
+// Authors:
+//	Thomas Van Machelen (thomas vanmachelen gmail com)
+//	George Talusan (george convolve ca)
+//
+// (C) Copyright 2007 Novell, Inc. (http://www.novell.com)
+//
+
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+using System;
+using System.Xml.Serialization;
+
+namespace Mono.Facebook
+{
+	public class Affiliation
+	{
+		[XmlElement ("nid")]
+		public long NId;
+
+		[XmlElement ("name")]
+		public string Name;
+
+		[XmlElement ("type")]
+		public string Type;
+
+		[XmlElement ("status")]
+		public string Status;
+
+		[XmlElement ("year")]
+		public string Year;
+	}
+
+	public class Affiliations
+	{
+		[XmlElement ("affiliation")]
+		public Affiliation[] affiliations_array;
+
+		[XmlIgnore ()]
+		public Affiliation[] AffiliationCollection
+		{
+			get { return affiliations_array ?? new Affiliation[0]; }
+		}
+	}
+
+	public class Concentrations
+	{
+		[XmlElement ("concentration")]
+		public string[] concentration_array;
+
+		[XmlIgnore ()]
+		public string[] ConcentrationCollection
+		{
+			get { return concentration_array ?? new string[0]; }
+		}
+	}
+
+	public class EducationHistory
+	{
+		[XmlElement ("education_info")]
+		public EducationInfo[] educations_array;
+
+		[XmlIgnore ()]
+		public EducationInfo[] EducationInfo
+		{
+			get { return educations_array ?? new EducationInfo[0]; }
+		}
+	}
+
+	public class EducationInfo
+	{
+		[XmlElement ("name")]
+		public string Name;
+
+		[XmlElement ("year")]
+		public int Year;
+
+		[XmlElement ("concentrations")]
+		public Concentrations concentrations;
+
+		[XmlIgnore ()]
+		public string[] Concentrations
+		{
+			get { return concentrations.ConcentrationCollection; }
+		}
+	}
+
+	public class HighSchoolInfo
+	{
+		[XmlElement ("hs1_info")]
+		public string HighSchoolOneName;
+
+		[XmlElement ("hs2_info")]
+		public string HighSchoolTwoName;
+
+		[XmlElement ("grad_year")]
+		public int GraduationYear;
+
+		[XmlElement ("hs1_id")]
+		public int HighSchoolOneId;
+
+		[XmlElement ("hs2_id")]
+		public int HighSchoolTwoId;
+	}
+
+	public class MeetingFor
+	{
+		[XmlElement ("seeking")]
+		public string[] seeking;
+
+		[XmlIgnore ()]
+		public string[] Seeking
+		{
+			get { return seeking ?? new string[0]; }
+		}
+	}
+
+	public class MeetingSex
+	{
+		[XmlElement ("sex")]
+		public string[] sex;
+
+		[XmlIgnore ()]
+		public string[] Sex
+		{
+			get { return sex ?? new string[0]; }
+		}
+	}
+
+	public class Status
+	{
+		[XmlElement ("message")]
+		public string Message;
+
+		[XmlElement ("time")]
+		public long Time;
+	}
+
+	public class WorkHistory
+	{
+		[XmlElement ("work_info")]
+		public WorkInfo[] workinfo_array;
+
+		[XmlIgnore ()]
+		public WorkInfo[] WorkInfo
+		{
+			get { return workinfo_array ?? new WorkInfo[0]; }
+		}
+	}
+
+	public class WorkInfo
+	{
+		[XmlElement ("location")]
+		public Location Location;
+
+		[XmlElement ("company_name")]
+		public string CompanyName;
+
+		[XmlElement ("position")]
+		public string Position;
+
+		[XmlElement ("description")]
+		public string Description;
+
+		[XmlElement ("start_date")]
+		public string StartDate;
+
+		[XmlElement ("end_date")]
+		public string EndDate;
+	}
+
+	public class User : Friend
+	{
+		public static readonly string[] FIELDS = { "about_me", "activities", "affiliations", "birthday", "books",
+			"current_location", "education_history", "first_name", "hometown_location", "interests", "last_name",
+			"movies", "music", "name", "notes_count", "pic", "pic_big", "pic_small", "political", "profile_update_time",
+			"quotes", "relationship_status", "religion", "sex", "significant_other_id",
+			"status", "timezone", "tv", "uid", "wall_count" };
+
+		[XmlElement ("about_me")]
+		public string AboutMe;
+
+		[XmlElement ("activities")]
+		public string Activities;
+
+		[XmlElement ("affiliations")]
+		public Affiliations affiliations;
+
+		[XmlIgnore ()]
+		public Affiliation[] Affiliations
+		{
+			get
+			{
+				if (affiliations == null)
+				{
+					return new Affiliation[0];
+				}
+				else
+				{
+					return affiliations.AffiliationCollection ?? new Affiliation[0];
+				}
+			}
+		}
+
+		[XmlElement ("birthday")]
+		public string Birthday;
+
+		[XmlElement ("books")]
+		public string Books;
+
+		[XmlElement ("current_location")]
+		public Location CurrentLocation;
+
+		[XmlElement ("education_history")]
+		public EducationHistory EducationHistory;
+
+		[XmlElement ("first_name")]
+		public string FirstName;
+
+		[XmlElement ("hometown_location")]
+		public Location HomeTownLocation;
+
+		[XmlElement ("hs_info")]
+		public HighSchoolInfo HighSchoolInfo;
+
+		[XmlElement ("interests")]
+		public string Interests;
+
+		[XmlElement ("is_app_user")]
+		public string is_app_user;
+
+		public bool IsAppUser {
+			get {
+				return Util.GetBoolFromString(is_app_user);
+			}
+		}
+
+		[XmlElement ("last_name")]
+		public string LastName;
+
+		[XmlElement ("meeting_for")]
+		public MeetingFor MeetingFor;
+
+		[XmlElement ("meeting_sex")]
+		public MeetingSex MeetingSex;
+
+		[XmlElement ("movies")]
+		public string Movies;
+
+		[XmlElement ("music")]
+		public string Music;
+
+		[XmlElement ("name")]
+		public string Name;
+
+		[XmlElement("notes_count")]
+		public string notes_count;
+
+		[XmlIgnore()]
+		public int NotesCount {
+			get {
+				return Util.GetIntFromString(notes_count);
+			}
+		}
+
+		[XmlElement ("pic")]
+		public string Pic;
+
+		[XmlIgnore ()]
+		public Uri PicUri
+		{
+			get { return new Uri (Pic); }
+		}
+
+		[XmlElement ("pic_big")]
+		public string PicBig;
+
+		[XmlIgnore ()]
+		public Uri PicBigUri
+		{
+			get { return new Uri (PicBig); }
+		}
+
+		[XmlElement ("pic_small")]
+		public string PicSmall;
+
+		[XmlIgnore ()]
+		public Uri PicSmallUri
+		{
+			get { return new Uri (PicSmall); }
+		}
+
+		[XmlElement ("political")]
+		public string Political;
+
+		[XmlElement ("profile_update_time")]
+		public long ProfileUpdateTime;
+
+		[XmlElement ("quotes")]
+		public string Quotes;
+
+		[XmlElement ("relationship_status")]
+		public string RelationshipStatus;
+
+		[XmlElement ("religion")]
+		public string Religion;
+
+		[XmlElement ("sex")]
+		public string Sex;
+
+		[XmlElement ("significant_other_id")]
+		public string significant_other_id;
+
+		[XmlIgnore ()]
+		public long SignificantOtherId
+		{
+			get {
+				return Util.GetIntFromString(significant_other_id);
+			}
+		}
+
+		[XmlElement ("status")]
+		public Status Status;
+
+		[XmlElement ("timezone")]
+		public string timezone;
+
+		public int TimeZone {
+			get {
+				return Util.GetIntFromString(timezone);
+			}
+		}
+
+		[XmlElement ("tv")]
+		public string Tv;
+
+		[XmlElement ("wall_count")]
+		public string wall_count;
+
+		[XmlIgnore()]
+		public int WallCount
+		{
+			get {
+				return Util.GetIntFromString(wall_count);
+			}
+		}
+
+		[XmlElement ("work_history")]
+		public WorkHistory WorkHistory;
+	}
+}

Added: trunk/extensions/Exporters/FacebookExport/Mono.Facebook/Util.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FacebookExport/Mono.Facebook/Util.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,270 @@
+//
+// Mono.Facebook.Util.cs:
+//
+// Authors:
+//	Thomas Van Machelen (thomas vanmachelen gmail com)
+//
+// (C) Copyright 2007 Novell, Inc. (http://www.novell.com)
+//
+
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Net;
+using System.Security.Cryptography;
+using System.Text;
+using System.Xml;
+using System.Xml.Serialization;
+
+namespace Mono.Facebook
+{
+	public class Util
+	{
+		private const string URL = "http://api.facebook.com/restserver.php?";;
+		private const string BOUNDARY = "SoMeTeXtWeWiLlNeVeRsEe";
+		private const string LINE = "\r\n";
+
+		private static Dictionary<int, XmlSerializer> serializer_dict = new Dictionary<int, XmlSerializer>();
+
+		private FacebookParam VersionParam = FacebookParam.Create ("v", "1.0");
+		private string api_key;
+		private string secret;
+		private bool use_json;
+
+		private static XmlSerializer ErrorSerializer {
+			get {
+				return GetSerializer (typeof (Error));
+			}
+		}
+
+		public Util (string api_key, string secret)
+		{
+			this.api_key = api_key;
+			this.secret = secret;
+		}
+
+		public bool UseJson
+		{
+			get { return use_json; }
+			set { use_json = value; }
+		}
+
+		internal string SharedSecret
+		{
+			get { return secret; }
+			set { secret = value; }
+		}
+
+		internal string ApiKey
+		{
+			get { return api_key; }
+		}
+
+		public T GetResponse<T>(string method_name, params FacebookParam[] parameters)
+		{
+			string url = FormatGetUrl (method_name, parameters);
+			byte[] response_bytes = GetResponseBytes (url);
+
+			XmlSerializer response_serializer = GetSerializer (typeof (T));
+			try {
+				T response = (T)response_serializer.Deserialize(new MemoryStream(response_bytes));
+				return response;
+			}
+			catch {
+				Error error = (Error) ErrorSerializer.Deserialize (new MemoryStream (response_bytes));
+				throw new FacebookException (error.ErrorCode, error.ErrorMsg);
+			}
+		}
+
+		public XmlDocument GetResponse (string method_name, params FacebookParam[] parameters)
+		{
+			string url = FormatGetUrl (method_name, parameters);
+			byte[] response_bytes = GetResponseBytes (url);
+
+			XmlDocument doc = new XmlDocument ();
+			doc.LoadXml (Encoding.Default.GetString (response_bytes));
+
+			return doc;
+		}
+
+		internal Photo Upload (long aid, string caption, string path, string session_key)
+		{
+			// check for file
+			FileInfo file = new FileInfo (path);
+
+			if (!file.Exists)
+				throw new FileNotFoundException ("Upload file not found", path);
+
+			// create parameter string
+			List<FacebookParam> parameter_list = new List<FacebookParam>();
+			parameter_list.Add (FacebookParam.Create ("call_id", DateTime.Now.Ticks));
+			parameter_list.Add (FacebookParam.Create ("session_key", session_key));
+			parameter_list.Add (FacebookParam.Create ("aid", aid));
+
+			if (caption != null && caption != string.Empty)
+				parameter_list.Add (FacebookParam.Create ("caption", caption));
+
+			FacebookParam[] param_array = Sign ("facebook.photos.upload", parameter_list.ToArray ());
+			string param_string = string.Empty;
+
+			foreach (FacebookParam param in param_array) {
+				param_string = param_string + "--" + BOUNDARY + LINE;
+				param_string = param_string + "Content-Disposition: form-data; name=\"" + param.Name + "\"" + LINE + LINE + param.Value + LINE;
+			}
+
+			param_string = param_string + "--" + BOUNDARY + LINE
+				+ "Content-Disposition: form-data; name=\"filename\"; filename=\"" + file.Name + "\"" + LINE
+				+ "Content-Type: image/jpeg" + LINE + LINE;
+
+			string closing_string = LINE + "--" + BOUNDARY + "--";
+
+			// get bytes
+			byte[] param_bytes = System.Text.Encoding.Default.GetBytes (param_string);
+			byte[] closing_bytes = System.Text.Encoding.Default.GetBytes (closing_string);
+
+			byte[] file_bytes = System.IO.File.ReadAllBytes (path);
+
+			// compose
+			List<byte> byte_list = new List<byte>(param_bytes.Length + file_bytes.Length + closing_bytes.Length);
+			byte_list.AddRange (param_bytes);
+			byte_list.AddRange (file_bytes);
+			byte_list.AddRange (closing_bytes);
+
+			byte[] final_bytes = new byte[byte_list.Count];
+			byte_list.CopyTo (final_bytes);
+
+			// create request
+			WebClient wc = new WebClient ();
+			wc.Headers.Set ("Content-Type", "multipart/form-data; boundary=" + BOUNDARY);
+			wc.Headers.Add ("MIME-version", "1.0");
+
+			// upload
+			byte[] response = wc.UploadData ("http://api.facebook.com/restserver.php?";, "POST", final_bytes);
+
+			// deserialize
+			XmlSerializer photo_serializer = new XmlSerializer (typeof (Photo));
+
+			try {
+				return (Photo)photo_serializer.Deserialize (new MemoryStream (response));
+			}
+			catch {
+				Error error = (Error) ErrorSerializer.Deserialize (new MemoryStream (response));
+				throw new FacebookException (error.ErrorCode, error.ErrorMsg);
+			}
+		}
+
+		private static byte[] GetResponseBytes (string url)
+		{
+			WebRequest request = HttpWebRequest.Create (url);
+			WebResponse response = null;
+
+			try
+			{
+				response = request.GetResponse ();
+				using (StreamReader reader = new StreamReader(response.GetResponseStream(), Encoding.UTF8))
+				{
+					return Encoding.UTF8.GetBytes(reader.ReadToEnd());
+				}
+			}
+			finally
+			{
+				if (response != null)
+					response.Close ();
+			}
+		}
+
+		private string FormatGetUrl (string method_name, params FacebookParam[] parameters)
+		{
+			FacebookParam[] signed = Sign (method_name, parameters);
+
+			StringBuilder builder = new StringBuilder (URL);
+
+			for (int i = 0; i < signed.Length; i++)
+			{
+				if (i > 0)
+					builder.Append ("&");
+
+				builder.Append (signed[i].ToString ());
+			}
+
+			return builder.ToString ();
+		}
+
+		private static XmlSerializer GetSerializer (Type t)
+		{
+			int type_hash = t.GetHashCode ();
+
+			if (!serializer_dict.ContainsKey (type_hash))
+				serializer_dict.Add (type_hash, new XmlSerializer (t));
+
+			return serializer_dict[type_hash];
+		}
+
+		internal FacebookParam[] Sign (string method_name, FacebookParam[] parameters)
+		{
+			List<FacebookParam> list = new List<FacebookParam> (parameters);
+			list.Add (FacebookParam.Create ("method", method_name));
+			list.Add (FacebookParam.Create ("api_key", api_key));
+			list.Add (VersionParam);
+			list.Sort ();
+
+			StringBuilder values = new StringBuilder ();
+
+			foreach (FacebookParam param in list)
+				values.Append (param.ToString ());
+
+			values.Append (secret);
+
+			byte[] md5_result = MD5.Create ().ComputeHash (Encoding.ASCII.GetBytes (values.ToString ()));
+
+			StringBuilder sig_builder = new StringBuilder ();
+
+			foreach (byte b in md5_result)
+				sig_builder.Append (b.ToString ("x2"));
+
+			list.Add (FacebookParam.Create ("sig", sig_builder.ToString ()));
+
+			return list.ToArray ();
+		}
+
+		internal static int GetIntFromString(string input)
+		{
+			try {
+				return int.Parse(input);
+			}
+			catch {
+				return 0;
+			}
+		}
+
+		internal static bool GetBoolFromString(string input)
+		{
+			try {
+				return bool.Parse(input);
+			}
+			catch {
+				return false;
+			}
+
+		}
+	}
+}

Copied: trunk/extensions/Exporters/FlickrExport/.gitignore (from r4368, /trunk/extensions/DBusService/.gitignore)
==============================================================================

Copied: trunk/extensions/Exporters/FlickrExport/FlickrExport.addin.xml (from r4368, /trunk/extensions/FlickrExport/FlickrExport.addin.xml)
==============================================================================

Added: trunk/extensions/Exporters/FlickrExport/FlickrExport.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FlickrExport/FlickrExport.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,573 @@
+using FlickrNet;
+using System;
+using System.Collections;
+using System.IO;
+using System.Threading;
+using Mono.Unix;
+
+using FSpot;
+using FSpot.Filters;
+using FSpot.Widgets;
+using FSpot.Utils;
+using FSpot.UI.Dialog;
+
+namespace FSpotFlickrExport {
+	public class TwentyThreeHQExport : FlickrExport
+	{
+		public override void Run (IBrowsableCollection selection)
+		{
+			Run (SupportedService.TwentyThreeHQ, selection, false);
+		}
+	}
+
+	public class ZooomrExport : FlickrExport
+	{
+		public override void Run (IBrowsableCollection selection)
+		{
+			Run (SupportedService.Zooomr, selection, false);
+		}
+	}
+
+	public class FlickrExport : FSpot.Extensions.IExporter {
+		IBrowsableCollection selection;
+
+		[Glade.Widget] Gtk.Dialog	  dialog;
+		[Glade.Widget] Gtk.CheckButton    scale_check;
+		[Glade.Widget] Gtk.CheckButton    meta_check;
+		[Glade.Widget] Gtk.CheckButton    tag_check;
+		[Glade.Widget] Gtk.CheckButton    hierarchy_check;
+		[Glade.Widget] Gtk.CheckButton    ignore_top_level_check;
+		[Glade.Widget] Gtk.CheckButton    open_check;
+		[Glade.Widget] Gtk.SpinButton     size_spin;
+		[Glade.Widget] Gtk.ScrolledWindow thumb_scrolledwindow;
+		[Glade.Widget] Gtk.Button         auth_flickr;
+		[Glade.Widget] Gtk.Button         auth_done_flickr;
+		[Glade.Widget] Gtk.ProgressBar    used_bandwidth;
+		[Glade.Widget] Gtk.Button         do_export_flickr;
+		[Glade.Widget] Gtk.Label          auth_label;
+		[Glade.Widget] Gtk.RadioButton    public_radio;
+		[Glade.Widget] Gtk.CheckButton    family_check;
+		[Glade.Widget] Gtk.CheckButton    friend_check;
+
+		private Glade.XML xml;
+		private string dialog_name = "flickr_export_dialog";
+		System.Threading.Thread command_thread;
+		ThreadProgressDialog progress_dialog;
+		ProgressItem progress_item;
+
+		public const string EXPORT_SERVICE = "flickr/";
+		public const string SCALE_KEY = Preferences.APP_FSPOT_EXPORT + EXPORT_SERVICE + "scale";
+		public const string SIZE_KEY = Preferences.APP_FSPOT_EXPORT + EXPORT_SERVICE + "size";
+		public const string BROWSER_KEY = Preferences.APP_FSPOT_EXPORT + EXPORT_SERVICE + "browser";
+		public const string TAGS_KEY = Preferences.APP_FSPOT_EXPORT + EXPORT_SERVICE + "tags";
+		public const string STRIP_META_KEY = Preferences.APP_FSPOT_EXPORT + EXPORT_SERVICE + "strip_meta";
+		public const string PUBLIC_KEY = Preferences.APP_FSPOT_EXPORT + EXPORT_SERVICE + "public";
+		public const string FAMILY_KEY = Preferences.APP_FSPOT_EXPORT + EXPORT_SERVICE + "family";
+		public const string FRIENDS_KEY = Preferences.APP_FSPOT_EXPORT + EXPORT_SERVICE + "friends";
+		public const string TAG_HIERARCHY_KEY = Preferences.APP_FSPOT_EXPORT + EXPORT_SERVICE + "tag_hierarchy";
+		public const string IGNORE_TOP_LEVEL_KEY = Preferences.APP_FSPOT_EXPORT + EXPORT_SERVICE + "ignore_top_level";
+
+		bool open;
+		bool scale;
+		bool copy_metadata;
+
+		bool is_public;
+		bool is_friend;
+		bool is_family;
+
+		string token;
+
+		int photo_index;
+		int size;
+		Auth auth;
+
+		FlickrRemote fr;
+		private FlickrRemote.Service current_service;
+
+		string auth_text;
+		private State state;
+
+		private enum State {
+			Disconnected,
+			Connected,
+			InAuth,
+			Authorized
+		}
+
+		private State CurrentState {
+			get { return state; }
+			set {
+				switch (value) {
+				case State.Disconnected:
+					auth_label.Text = auth_text;
+					auth_flickr.Sensitive = true;
+					do_export_flickr.Sensitive = false;
+					auth_flickr.Label = Catalog.GetString ("Authorize");
+					used_bandwidth.Visible = false;
+					break;
+				case State.Connected:
+					auth_flickr.Sensitive = true;
+					do_export_flickr.Sensitive = false;
+					auth_label.Text = string.Format (Catalog.GetString ("Return to this window after you have finished the authorization process on {0} and click the \"Complete Authorization\" button below"), current_service.Name);
+					auth_flickr.Label = Catalog.GetString ("Complete Authorization");
+					used_bandwidth.Visible = false;
+					break;
+				case State.InAuth:
+					auth_flickr.Sensitive = false;
+					auth_label.Text = string.Format (Catalog.GetString ("Logging into {0}"), current_service.Name);
+					auth_flickr.Label = Catalog.GetString ("Checking credentials...");
+					do_export_flickr.Sensitive = false;
+					used_bandwidth.Visible = false;
+					break;
+				case State.Authorized:
+					do_export_flickr.Sensitive = true;
+					auth_flickr.Sensitive = true;
+					auth_label.Text = System.String.Format (Catalog.GetString ("Welcome {0} you are connected to {1}"),
+										auth.User.Username,
+										current_service.Name);
+					auth_flickr.Label = String.Format (Catalog.GetString ("Sign in as a different user"), auth.User.Username);
+					used_bandwidth.Visible = !fr.Connection.PeopleGetUploadStatus().IsPro &&
+									fr.Connection.PeopleGetUploadStatus().BandwidthMax > 0;
+					if (used_bandwidth.Visible) {
+						used_bandwidth.Fraction = fr.Connection.PeopleGetUploadStatus().PercentageUsed;
+						used_bandwidth.Text = string.Format (Catalog.GetString("Used {0} of your allowed {1} monthly quota"),
+									SizeUtil.ToHumanReadable(fr.Connection.PeopleGetUploadStatus().BandwidthUsed),
+									SizeUtil.ToHumanReadable(fr.Connection.PeopleGetUploadStatus().BandwidthMax));
+					}
+					break;
+				}
+				state = value;
+			}
+		}
+
+		public FlickrExport (IBrowsableCollection selection, bool display_tags) :
+			this (SupportedService.Flickr, selection, display_tags)
+		{ }
+
+
+		public FlickrExport (SupportedService service, IBrowsableCollection selection, bool display_tags) : this ()
+		{
+			Run (service, selection, display_tags);
+		}
+
+		public FlickrExport ()
+		{
+
+		}
+
+		public virtual void Run (IBrowsableCollection selection)
+		{
+			Run (SupportedService.Flickr, selection, false);
+		}
+
+		public void Run (SupportedService service, IBrowsableCollection selection, bool display_tags)
+		{
+			this.selection = selection;
+			this.current_service = FlickrRemote.Service.FromSupported (service);
+
+			IconView view = new IconView (selection);
+			view.DisplayTags = display_tags;
+			view.DisplayDates = false;
+
+			xml = new Glade.XML (null, "FlickrExport.glade", dialog_name, "f-spot");
+			xml.Autoconnect (this);
+
+			Dialog.Modal = false;
+			Dialog.TransientFor = null;
+
+			thumb_scrolledwindow.Add (view);
+			HandleSizeActive (null, null);
+
+			public_radio.Toggled += HandlePublicChanged;
+			tag_check.Toggled += HandleTagChanged;
+			hierarchy_check.Toggled += HandleHierarchyChanged;
+			HandleTagChanged (null, null);
+			HandleHierarchyChanged (null, null);
+
+			Dialog.ShowAll ();
+			Dialog.Response += HandleResponse;
+			auth_flickr.Clicked += HandleClicked;
+			auth_text = string.Format (auth_label.Text, current_service.Name);
+			auth_label.Text = auth_text;
+			used_bandwidth.Visible = false;
+
+			LoadPreference (SCALE_KEY);
+			LoadPreference (SIZE_KEY);
+			LoadPreference (BROWSER_KEY);
+			LoadPreference (TAGS_KEY);
+			LoadPreference (TAG_HIERARCHY_KEY);
+			LoadPreference (IGNORE_TOP_LEVEL_KEY);
+			LoadPreference (STRIP_META_KEY);
+			LoadPreference (PUBLIC_KEY);
+			LoadPreference (FAMILY_KEY);
+			LoadPreference (FRIENDS_KEY);
+			LoadPreference (current_service.PreferencePath);
+
+			do_export_flickr.Sensitive = false;
+			fr = new FlickrRemote (token, current_service);
+			if (token != null && token.Length > 0) {
+				StartAuth ();
+			}
+		}
+
+		public bool StartAuth ()
+		{
+			CurrentState = State.InAuth;
+			if (command_thread == null || ! command_thread.IsAlive) {
+				command_thread = new Thread (new ThreadStart (CheckAuthorization));
+				command_thread.Start ();
+			}
+			return true;
+		}
+
+		public void CheckAuthorization ()
+		{
+			AuthorizationEventArgs args = new AuthorizationEventArgs ();
+
+			try {
+				args.Auth = fr.CheckLogin ();
+			} catch (FlickrException e) {
+				args.Exception = e;
+			}
+
+			Gtk.Application.Invoke (this, args, delegate (object sender, EventArgs sargs) {
+				AuthorizationEventArgs wargs = (AuthorizationEventArgs) sargs;
+
+				do_export_flickr.Sensitive = wargs.Auth != null;
+				if (wargs.Auth != null) {
+					token = wargs.Auth.Token;
+					auth = wargs.Auth;
+					CurrentState = State.Authorized;
+					Preferences.Set (current_service.PreferencePath, token);
+				} else {
+					CurrentState = State.Disconnected;
+				}
+			});
+		}
+
+		private class AuthorizationEventArgs : System.EventArgs {
+			Exception e;
+			Auth auth;
+
+			public Exception Exception {
+				get { return e; }
+				set { e = value; }
+			}
+
+			public Auth Auth {
+				get { return auth; }
+				set { auth = value; }
+			}
+
+			public AuthorizationEventArgs ()
+			{
+			}
+		}
+
+		public void HandleSizeActive (object sender, System.EventArgs args)
+		{
+			size_spin.Sensitive = scale_check.Active;
+		}
+
+		private void Logout ()
+		{
+			token = null;
+			auth = null;
+			fr = new FlickrRemote (token, current_service);
+			Preferences.Set (current_service.PreferencePath, String.Empty);
+			CurrentState = State.Disconnected;
+		}
+
+		private void Login ()
+		{
+			try {
+				fr = new FlickrRemote (token, current_service);
+				fr.TryWebLogin();
+				CurrentState = State.Connected;
+			} catch (FlickrApiException e) {
+				if (e.Code == 98) {
+					Logout ();
+					Login ();
+				} else {
+					HigMessageDialog md =
+						new HigMessageDialog (Dialog,
+								      Gtk.DialogFlags.Modal |
+								      Gtk.DialogFlags.DestroyWithParent,
+								      Gtk.MessageType.Error, Gtk.ButtonsType.Ok,
+								      Catalog.GetString ("Unable to log on"), e.Message);
+
+					md.Run ();
+					md.Destroy ();
+					CurrentState = State.Disconnected;
+				}
+			}
+		}
+
+		private void HandleProgressChanged (ProgressItem item)
+		{
+			//System.Console.WriteLine ("Changed value = {0}", item.Value);
+			progress_dialog.Fraction = (photo_index - 1.0 + item.Value) / (double) selection.Count;
+		}
+
+		FileInfo info;
+		private void HandleFlickrProgress (object sender, UploadProgressEventArgs args)
+		{
+			if (args.UploadComplete) {
+				progress_dialog.Fraction = photo_index / (double) selection.Count;
+				progress_dialog.ProgressText = String.Format (Catalog.GetString ("Waiting for response {0} of {1}"),
+									      photo_index, selection.Count);
+			}
+			progress_dialog.Fraction = (photo_index - 1.0 + (args.Bytes / (double) info.Length)) / (double) selection.Count;
+		}
+
+		private class DateComparer : IComparer
+		{
+			public int Compare (object left, object right)
+			{
+				return DateTime.Compare ((left as IBrowsableItem).Time, (right as IBrowsableItem).Time);
+			}
+		}
+
+		private void Upload () {
+			progress_item = new ProgressItem ();
+			progress_item.Changed += HandleProgressChanged;
+			fr.Connection.OnUploadProgress += HandleFlickrProgress;
+
+			System.Collections.ArrayList ids = new System.Collections.ArrayList ();
+			IBrowsableItem [] photos = selection.Items;
+			Array.Sort (photos, new DateComparer ());
+
+			for (int index = 0; index < photos.Length; index++) {
+				try {
+					IBrowsableItem photo = photos [index];
+					progress_dialog.Message = System.String.Format (
+                                                Catalog.GetString ("Uploading picture \"{0}\""), photo.Name);
+
+					progress_dialog.Fraction = photo_index / (double)selection.Count;
+					photo_index++;
+					progress_dialog.ProgressText = System.String.Format (
+						Catalog.GetString ("{0} of {1}"), photo_index,
+						selection.Count);
+
+					info = new FileInfo (photo.DefaultVersionUri.LocalPath);
+					FilterSet stack = new FilterSet ();
+					if (scale)
+						stack.Add (new ResizeFilter ((uint)size));
+
+					string id = fr.Upload (photo, stack, is_public, is_family, is_friend);
+					ids.Add (id);
+
+					if (Core.Database != null && photo is FSpot.Photo)
+						Core.Database.Exports.Create ((photo as FSpot.Photo).Id,
+									      (photo as FSpot.Photo).DefaultVersionId,
+									      ExportStore.FlickrExportType,
+									      auth.User.UserId + ":" + auth.User.Username + ":" + current_service.Name + ":" + id);
+
+				} catch (System.Exception e) {
+					progress_dialog.Message = String.Format (Catalog.GetString ("Error Uploading To {0}: {1}"),
+										 current_service.Name,
+										 e.Message);
+					progress_dialog.ProgressText = Catalog.GetString ("Error");
+					System.Console.WriteLine (e);
+
+					if (progress_dialog.PerformRetrySkip ()) {
+						index--;
+						photo_index--;
+					}
+				}
+			}
+			progress_dialog.Message = Catalog.GetString ("Done Sending Photos");
+			progress_dialog.Fraction = 1.0;
+			progress_dialog.ProgressText = Catalog.GetString ("Upload Complete");
+			progress_dialog.ButtonLabel = Gtk.Stock.Ok;
+
+			if (open && ids.Count != 0) {
+				string view_url;
+				if (current_service.Name == "Zooomr.com")
+					view_url = string.Format ("http://www.{0}/photos/{1}/";, current_service.Name, auth.User.Username);
+				else {
+					view_url = string.Format ("http://www.{0}/tools/uploader_edit.gne?ids";, current_service.Name);
+					bool first = true;
+
+					foreach (string id in ids) {
+						view_url = view_url + (first ? "=" : ",") + id;
+						first = false;
+					}
+				}
+
+				GnomeUtil.UrlShow (view_url);
+			}
+		}
+
+		private void HandleClicked (object sender, System.EventArgs args)
+		{
+			switch (CurrentState) {
+			case State.Disconnected:
+				Login ();
+				break;
+			case State.Connected:
+				StartAuth ();
+				break;
+			case State.InAuth:
+				break;
+			case State.Authorized:
+				Logout ();
+				Login ();
+				break;
+			}
+		}
+
+		private void HandlePublicChanged (object sender, EventArgs args)
+		{
+			bool sensitive = ! public_radio.Active;
+			friend_check.Sensitive = sensitive;
+			family_check.Sensitive = sensitive;
+		}
+
+		private void HandleTagChanged (object sender, EventArgs args)
+		{
+			hierarchy_check.Sensitive = tag_check.Active;
+		}
+
+		private void HandleHierarchyChanged (object sender, EventArgs args)
+		{
+			ignore_top_level_check.Sensitive = hierarchy_check.Active;
+		}
+
+		private void HandleResponse (object sender, Gtk.ResponseArgs args)
+		{
+			if (args.ResponseId != Gtk.ResponseType.Ok) {
+				if (command_thread != null && command_thread.IsAlive)
+					command_thread.Abort ();
+
+				Dialog.Destroy ();
+				return;
+			}
+
+			if (fr.CheckLogin() == null) {
+				do_export_flickr.Sensitive = false;
+				HigMessageDialog md =
+					new HigMessageDialog (Dialog,
+							      Gtk.DialogFlags.Modal |
+							      Gtk.DialogFlags.DestroyWithParent,
+							      Gtk.MessageType.Error, Gtk.ButtonsType.Ok,
+							      Catalog.GetString ("Unable to log on."),
+							      string.Format (Catalog.GetString ("F-Spot was unable to log on to {0}.  Make sure you have given the authentication using {0} web browser interface."),
+									     current_service.Name));
+				md.Run ();
+				md.Destroy ();
+				return;
+			}
+
+			fr.ExportTags = tag_check.Active;
+			fr.ExportTagHierarchy = hierarchy_check.Active;
+			fr.ExportIgnoreTopLevel = ignore_top_level_check.Active;
+			open = open_check.Active;
+			scale = scale_check.Active;
+			copy_metadata = !meta_check.Active;
+			is_public = public_radio.Active;
+			is_family = family_check.Active;
+			is_friend = friend_check.Active;
+			if (scale)
+				size = size_spin.ValueAsInt;
+
+			command_thread = new Thread (new ThreadStart (Upload));
+			command_thread.Name = Catalog.GetString ("Uploading Pictures");
+
+			Dialog.Destroy ();
+			progress_dialog = new FSpot.ThreadProgressDialog (command_thread, selection.Count);
+			progress_dialog.Start ();
+
+			// Save these settings for next time
+			Preferences.Set (SCALE_KEY, scale);
+			Preferences.Set (SIZE_KEY, size);
+			Preferences.Set (BROWSER_KEY, open);
+			Preferences.Set (TAGS_KEY, tag_check.Active);
+			Preferences.Set (STRIP_META_KEY, meta_check.Active);
+			Preferences.Set (PUBLIC_KEY, public_radio.Active);
+			Preferences.Set (FAMILY_KEY, family_check.Active);
+			Preferences.Set (FRIENDS_KEY, friend_check.Active);
+			Preferences.Set (TAG_HIERARCHY_KEY, hierarchy_check.Active);
+			Preferences.Set (IGNORE_TOP_LEVEL_KEY, ignore_top_level_check.Active);
+			Preferences.Set (current_service.PreferencePath, fr.Token);
+		}
+
+		void LoadPreference (string key)
+		{
+			object val = Preferences.Get (key);
+
+			if (val == null)
+				return;
+
+			//System.Console.WriteLine("Setting {0} to {1}", key, val);
+			bool active;
+
+			switch (key) {
+			case SCALE_KEY:
+				if (scale_check.Active != (bool) val)
+					scale_check.Active = (bool) val;
+				break;
+			case SIZE_KEY:
+				size_spin.Value = (double) (int) val;
+				break;
+			case BROWSER_KEY:
+				if (open_check.Active != (bool) val)
+					open_check.Active = (bool) val;
+				break;
+			case TAGS_KEY:
+				if (tag_check.Active != (bool) val)
+					tag_check.Active = (bool) val;
+				break;
+			case TAG_HIERARCHY_KEY:
+				if (hierarchy_check.Active != (bool) val)
+					hierarchy_check.Active = (bool) val;
+				break;
+			case IGNORE_TOP_LEVEL_KEY:
+				if (ignore_top_level_check.Active != (bool) val)
+					ignore_top_level_check.Active = (bool) val;
+				break;
+			case STRIP_META_KEY:
+				if (meta_check.Active != (bool) val)
+					meta_check.Active = (bool) val;
+				break;
+			case FlickrRemote.TOKEN_FLICKR:
+			case FlickrRemote.TOKEN_23HQ:
+			case FlickrRemote.TOKEN_ZOOOMR:
+				token = (string) val;
+			        break;
+			case PUBLIC_KEY:
+				active = (bool) val;
+				if (public_radio.Active != active)
+					public_radio.Active = active;
+				break;
+			case FAMILY_KEY:
+				active = (bool) val;
+				if (family_check.Active != active)
+					family_check.Active = active;
+				break;
+			case FRIENDS_KEY:
+				active = (bool) val;
+				if (friend_check.Active != active)
+					friend_check.Active = active;
+				break;
+				/*
+			case Preferences.EXPORT_FLICKR_EMAIL:
+
+				/*
+			case Preferences.EXPORT_FLICKR_EMAIL:
+				email_entry.Text = (string) val;
+				break;
+				*/
+			}
+		}
+
+		private Gtk.Dialog Dialog {
+			get {
+				if (dialog == null)
+					dialog = (Gtk.Dialog) xml.GetWidget (dialog_name);
+
+				return dialog;
+			}
+		}
+	}
+}

Copied: trunk/extensions/Exporters/FlickrExport/FlickrExport.glade (from r4368, /trunk/extensions/FlickrExport/FlickrExport.glade)
==============================================================================

Copied: trunk/extensions/Exporters/FlickrExport/FlickrNet/.gitignore (from r4368, /trunk/extensions/DefaultExporters/.gitignore)
==============================================================================

Added: trunk/extensions/Exporters/FlickrExport/FlickrNet/ActivityEvent.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FlickrExport/FlickrNet/ActivityEvent.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,110 @@
+using System;
+using System.Xml;
+
+namespace FlickrNet
+{
+	/// <summary>
+	/// Summary description for ActivityEvent.
+	/// </summary>
+	public class ActivityEvent
+	{
+		private ActivityEventType _type;
+		private string _userId;
+		private string _userName;
+		private DateTime _dateAdded;
+		private string _content;
+
+		/// <summary>
+		/// THe <see cref="ActivityEventType"/> of the event, either Comment or Note.
+		/// </summary>
+		public ActivityEventType EventType
+		{
+			get { return _type; }
+		}
+
+		/// <summary>
+		/// The user id of the user who made the comment or note.
+		/// </summary>
+		public string UserId
+		{
+			get { return _userId; }
+		}
+
+		/// <summary>
+		/// The screen name of the user who made the comment or note.
+		/// </summary>
+		public string UserName
+		{
+			get { return _userName; }
+		}
+
+		/// <summary>
+		/// The date the note or comment was added.
+		/// </summary>
+		public DateTime DateAdded
+		{
+			get { return _dateAdded; }
+		}
+
+		/// <summary>
+		/// The text of the note or comment.
+		/// </summary>
+		public string Value
+		{
+			get { return _content; }
+		}
+
+		internal ActivityEvent(XmlNode eventNode)
+		{
+			XmlNode node;
+
+			node = eventNode.Attributes.GetNamedItem("type");
+			if( node == null )
+				_type = ActivityEventType.Unknown;
+			else if( node.Value == "comment" )
+				_type = ActivityEventType.Comment;
+			else if( node.Value == "note" )
+				_type = ActivityEventType.Note;
+			else if( node.Value == "fave" )
+				_type = ActivityEventType.Favourite;
+			else
+				_type = ActivityEventType.Unknown;
+
+			node = eventNode.Attributes.GetNamedItem("user");
+			if( node != null ) _userId = node.Value;
+
+			node = eventNode.Attributes.GetNamedItem("username");
+			if( node != null ) _userName = node.Value;
+
+			node = eventNode.Attributes.GetNamedItem("dateadded");
+			if( node != null ) _dateAdded = Utils.UnixTimestampToDate(node.Value);
+
+			node = eventNode.FirstChild;
+			if( node != null && node.NodeType == XmlNodeType.Text ) _content = node.Value;
+
+		}
+	}
+
+	/// <summary>
+	/// The type of the <see cref="ActivityEvent"/>.
+	/// </summary>
+	public enum ActivityEventType
+	{
+		/// <summary>
+		/// The event type was not specified, or was a new unsupported type.
+		/// </summary>
+		Unknown,
+		/// <summary>
+		/// The event was a comment.
+		/// </summary>
+		Comment,
+		/// <summary>
+		/// The event was a note.
+		/// </summary>
+		Note,
+		/// <summary>
+		/// The event is a favourite.
+		/// </summary>
+		Favourite
+	}
+}

Added: trunk/extensions/Exporters/FlickrExport/FlickrNet/ActivityItem.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FlickrExport/FlickrNet/ActivityItem.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,312 @@
+using System;
+using System.Xml;
+
+namespace FlickrNet
+{
+	/// <summary>
+	/// Activity class used for <see cref="Flickr.ActivityUserPhotos()"/>
+	/// and <see cref="Flickr.ActivityUserComments"/>.
+	/// </summary>
+	public class ActivityItem
+	{
+		private ActivityItemType _activityType;
+		private string _id;
+		private string _secret;
+		private string _server;
+		private string _farm;
+		private int _commentsNew;
+		private int _commentsOld;
+		private int _comments;
+		private string _ownerId;
+		private int _more;
+		private int _views;
+
+		// Photoset specific
+		private int _photos = -1;
+		private string _primaryId = null;
+
+		// Photo specific
+		private int _notesNew = -1;
+		private int _notesOld = -1;
+		private int _notes = -1;
+		private int _favs = -1;
+
+		private string _title;
+		private ActivityEvent[] _events;
+
+		/// <summary>
+		/// The <see cref="ActivityItemType"/> of the item.
+		/// </summary>
+		public ActivityItemType ItemType
+		{
+			get { return _activityType; }
+		}
+
+		/// <summary>
+		/// The ID of either the photoset or the photo.
+		/// </summary>
+		public string Id
+		{
+			get { return _id; }
+		}
+
+		/// <summary>
+		/// The secret for either the photo, or the primary photo for the photoset.
+		/// </summary>
+		public string Secret
+		{
+			get { return _secret; }
+		}
+
+		/// <summary>
+		/// The server for either the photo, or the primary photo for the photoset.
+		/// </summary>
+		public string Server
+		{
+			get { return _server; }
+		}
+
+		/// <summary>
+		/// The server farm for either the photo, or the primary photo for the photoset.
+		/// </summary>
+		public string Farm
+		{
+			get { return _farm; }
+		}
+
+		/// <summary>
+		/// The title of the photoset or photo.
+		/// </summary>
+		public string Title
+		{
+			get { return _title; }
+		}
+
+		/// <summary>
+		/// The number of new comments within the given time frame.
+		/// </summary>
+		/// <remarks>
+		/// Only applicable for <see cref="Flickr.ActivityUserPhotos()"/>.
+		/// </remarks>
+		public int CommentsNew
+		{
+			get { return _commentsNew; }
+		}
+
+		/// <summary>
+		/// The number of old comments within the given time frame.
+		/// </summary>
+		/// <remarks>
+		/// Only applicable for <see cref="Flickr.ActivityUserPhotos()"/>.
+		/// </remarks>
+		public int CommentsOld
+		{
+			get { return _commentsOld; }
+		}
+
+		/// <summary>
+		/// The number of comments on the item.
+		/// </summary>
+		/// <remarks>
+		/// Only applicable for <see cref="Flickr.ActivityUserComments"/>.
+		/// </remarks>
+		public int Comments
+		{
+			get { return _comments; }
+		}
+
+		/// <summary>
+		/// Gets the number of views for this photo or photoset.
+		/// </summary>
+		public int Views
+		{
+			get { return _views; }
+		}
+
+		/// <summary>
+		/// You want more! You got it!
+		/// </summary>
+		/// <remarks>
+		/// Actually, not sure what this it for!
+		/// </remarks>
+		public int More
+		{
+			get { return _more; }
+		}
+
+		/// <summary>
+		/// The user id of the owner of this item.
+		/// </summary>
+		public string OwnerId
+		{
+			get { return _ownerId; }
+		}
+
+		/// <summary>
+		/// If the type is a photoset then this contains the number of photos in the set. Otherwise returns -1.
+		/// </summary>
+		public int Photos
+		{
+			get { return _photos; }
+		}
+
+		/// <summary>
+		/// If this is a photoset then returns the primary photo id, otherwise will be null (<code>Nothing</code> in VB.Net).
+		/// </summary>
+		public string PrimaryPhotoId
+		{
+			get { return _primaryId; }
+		}
+
+		/// <summary>
+		/// The number of new notes within the given time frame.
+		/// </summary>
+		/// <remarks>
+		/// Only applicable for photos and when calling <see cref="Flickr.ActivityUserPhotos()"/>.
+		/// </remarks>
+		public int NotesNew
+		{
+			get { return _notesNew; }
+		}
+
+		/// <summary>
+		/// The number of old notes within the given time frame.
+		/// </summary>
+		/// <remarks>
+		/// Only applicable for photos and when calling <see cref="Flickr.ActivityUserPhotos()"/>.
+		/// </remarks>
+		public int NotesOld
+		{
+			get { return _notesOld; }
+		}
+
+		/// <summary>
+		/// The number of comments on the photo.
+		/// </summary>
+		/// <remarks>
+		/// Only applicable for photos and when calling <see cref="Flickr.ActivityUserComments"/>.
+		/// </remarks>
+		public int Notes
+		{
+			get { return _notes; }
+		}
+
+		/// <summary>
+		/// If the type is a photo then this contains the number of favourites in the set. Otherwise returns -1.
+		/// </summary>
+		public int Favourites
+		{
+			get { return _favs; }
+		}
+
+		/// <summary>
+		/// The events that comprise this activity item.
+		/// </summary>
+		public ActivityEvent[] Events
+		{
+			get { return _events; }
+		}
+
+		internal ActivityItem(XmlNode itemNode)
+		{
+			XmlNode node;
+
+			node = itemNode.Attributes.GetNamedItem("type");
+
+			if( node == null )
+				_activityType = ActivityItemType.Unknown;
+			else if( node.Value == "photoset" )
+				_activityType = ActivityItemType.Photoset;
+			else if( node.Value == "photo" )
+				_activityType = ActivityItemType.Photo;
+			else
+				_activityType = ActivityItemType.Unknown;
+
+			node = itemNode.Attributes.GetNamedItem("owner");
+			if( node != null ) _ownerId = node.Value;
+
+			node = itemNode.Attributes.GetNamedItem("id");
+			if( node != null ) _id = node.Value;
+
+			node = itemNode.Attributes.GetNamedItem("secret");
+			if( node != null ) _secret = node.Value;
+
+			node = itemNode.Attributes.GetNamedItem("server");
+			if( node != null ) _server = node.Value;
+
+			node = itemNode.Attributes.GetNamedItem("farm");
+			if( node != null ) _farm = node.Value;
+
+			node = itemNode.Attributes.GetNamedItem("commentsnew");
+			if( node != null ) _commentsNew = Convert.ToInt32(node.Value);
+
+			node = itemNode.Attributes.GetNamedItem("commentsold");
+			if( node != null ) _commentsOld = Convert.ToInt32(node.Value);
+
+			node = itemNode.Attributes.GetNamedItem("comments");
+			if( node != null ) _comments = Convert.ToInt32(node.Value);
+
+			node = itemNode.Attributes.GetNamedItem("more");
+			if( node != null ) _more = Convert.ToInt32(node.Value);
+
+			node = itemNode.Attributes.GetNamedItem("views");
+			if( node != null ) _views = Convert.ToInt32(node.Value);
+
+			node = itemNode.SelectSingleNode("title");
+			if( node != null ) _title = node.InnerText;
+
+			XmlNodeList list = itemNode.SelectNodes("activity/event");
+
+			_events = new ActivityEvent[list.Count];
+
+			for(int i = 0; i < _events.Length; i++)
+			{
+				node = list[i];
+				_events[i] = new ActivityEvent(node);
+			}
+
+			// Photoset specific
+			// Photos, PrimaryPhotoId
+
+			node = itemNode.Attributes.GetNamedItem("photos");
+			if( node != null ) _photos = Convert.ToInt32(node.Value);
+
+			node = itemNode.Attributes.GetNamedItem("primary");
+			if( node != null ) _primaryId = node.Value;
+
+			// Photo specific
+			// NodesNew and NodesOld, Favourites
+
+			node = itemNode.Attributes.GetNamedItem("notesnew");
+			if( node != null ) _notesNew = Convert.ToInt32(node.Value);
+
+			node = itemNode.Attributes.GetNamedItem("notesold");
+			if( node != null ) _notesOld = Convert.ToInt32(node.Value);
+
+			node = itemNode.Attributes.GetNamedItem("notes");
+			if( node != null ) _notes = Convert.ToInt32(node.Value);
+
+			node = itemNode.Attributes.GetNamedItem("faves");
+			if( node != null ) _favs = Convert.ToInt32(node.Value);
+		}
+	}
+
+	/// <summary>
+	/// The type of the <see cref="ActivityItem"/>.
+	/// </summary>
+	public enum ActivityItemType
+	{
+		/// <summary>
+		/// The type is unknown, either not set of a new unsupported type.
+		/// </summary>
+		Unknown,
+		/// <summary>
+		/// The activity item is on a photoset.
+		/// </summary>
+		Photoset,
+		/// <summary>
+		/// The activitiy item is on a photo.
+		/// </summary>
+		Photo
+	}
+}

Added: trunk/extensions/Exporters/FlickrExport/FlickrNet/ApiKeyRequiredException.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FlickrExport/FlickrNet/ApiKeyRequiredException.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,17 @@
+using System;
+
+namespace FlickrNet
+{
+	/// <summary>
+	/// Exception thrown is no API key is supplied.
+	/// </summary>
+	public class ApiKeyRequiredException : FlickrException
+	{
+		/// <summary>
+		/// Default constructor.
+		/// </summary>
+		public ApiKeyRequiredException() : base("API Key is required for all method calls")
+		{
+		}
+	}
+}

Added: trunk/extensions/Exporters/FlickrExport/FlickrNet/AssemblyInfo.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FlickrExport/FlickrNet/AssemblyInfo.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,67 @@
+using System;
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Security;
+using System.Security.Permissions;
+using System.Runtime.InteropServices;
+
+//
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+//
+[assembly: AssemblyTitle("Flickr .Net Api Library")]
+[assembly: AssemblyDescription(".Net library for accessing Flickr.com Api functionality")]
+[assembly: AssemblyConfiguration("")]
+[assembly: AssemblyCompany("Sam Judson")]
+[assembly: AssemblyProduct("Flickr .Net Api Library")]
+[assembly: AssemblyCopyright("See website http://www.wackylabs.net/flickr";)]
+[assembly: AssemblyTrademark("")]
+[assembly: AssemblyCulture("")]
+
+//
+// Version information for an assembly consists of the following four values:
+//
+//      Major Version
+//      Minor Version
+//      Build Number
+//      Revision
+//
+// You can specify all the values or you can default the Revision and Build Numbers
+// by using the '*' as shown below:
+
+[assembly: AssemblyVersion("2.1.5.*")]
+
+//
+// In order to sign your assembly you must specify a key to use. Refer to the
+// Microsoft .NET Framework documentation for more information on assembly signing.
+//
+// Use the attributes below to control which key is used for signing.
+//
+// Notes:
+//   (*) If no key is specified, the assembly is not signed.
+//   (*) KeyName refers to a key that has been installed in the Crypto Service
+//       Provider (CSP) on your machine. KeyFile refers to a file which contains
+//       a key.
+//   (*) If the KeyFile and the KeyName values are both specified, the
+//       following processing occurs:
+//       (1) If the KeyName can be found in the CSP, that key is used.
+//       (2) If the KeyName does not exist and the KeyFile does exist, the key
+//           in the KeyFile is installed into the CSP and used.
+//   (*) In order to create a KeyFile, you can use the sn.exe (Strong Name) utility.
+//       When specifying the KeyFile, the location of the KeyFile should be
+//       relative to the project output directory which is
+//       %Project Directory%\obj\<configuration>. For example, if your KeyFile is
+//       located in the project directory, you would specify the AssemblyKeyFile
+//       attribute as [assembly: AssemblyKeyFile("..\\..\\mykey.snk")]
+//   (*) Delay Signing is an advanced option - see the Microsoft .NET Framework
+//       documentation for more information on this.
+//
+[assembly: AssemblyDelaySign(false)]
+[assembly: AssemblyKeyName("")]
+
+[assembly: AllowPartiallyTrustedCallers()]
+[assembly: SecurityPermission(SecurityAction.RequestMinimum, Execution = true)]
+
+[assembly: CLSCompliantAttribute(true)]
+[assembly: ComVisible(false)]

Added: trunk/extensions/Exporters/FlickrExport/FlickrNet/Auth.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FlickrExport/FlickrNet/Auth.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,79 @@
+using System;
+
+namespace FlickrNet
+{
+	/// <summary>
+	/// Used to specify the authentication levels needed for the Auth methods.
+	/// </summary>
+	public enum AuthLevel
+	{
+		/// <summary>
+		/// No access required - do not use this value!
+		/// </summary>
+		None,
+		/// <summary>
+		/// Read only access is required by your application.
+		/// </summary>
+		Read,
+		/// <summary>
+		/// Read and write access is required by your application.
+		/// </summary>
+		Write,
+		/// <summary>
+		/// Read, write and delete access is required by your application.
+		/// Deleting does not mean deleting photos, just meta data such as tags.
+		/// </summary>
+		Delete
+	}
+
+	/// <summary>
+	/// Successful authentication returns a <see cref="Auth"/> object.
+	/// </summary>
+	public class Auth
+	{
+		private string _token;
+		private AuthLevel _permissions;
+		private FoundUser _user;
+
+		/// <summary>
+		/// The authentication token returned by the <see cref="Flickr.AuthGetToken"/> or <see cref="Flickr.AuthCheckToken"/> methods.
+		/// </summary>
+		public string Token
+		{
+			get { return _token; }
+			set { _token = value; }
+		}
+
+		/// <summary>
+		/// The permissions the current token allows the application to perform.
+		/// </summary>
+		public AuthLevel Permissions
+		{
+			get { return _permissions; }
+			set { _permissions = value; }
+		}
+
+		/// <summary>
+		/// The <see cref="User"/> object associated with the token. Readonly.
+		/// </summary>
+		public FoundUser User
+		{
+			get { return _user; }
+		}
+
+		/// <summary>
+		/// Creates a new instance of the <see cref="Auth"/> class.
+		/// </summary>
+		public Auth()
+		{
+		}
+
+		internal Auth(System.Xml.XmlElement element)
+		{
+			Token = element.SelectSingleNode("token").InnerText;
+			Permissions = (AuthLevel)Enum.Parse(typeof(AuthLevel), element.SelectSingleNode("perms").InnerText, true);
+			System.Xml.XmlNode node = element.SelectSingleNode("user");
+			_user = new FoundUser(node);
+		}
+	}
+}

Added: trunk/extensions/Exporters/FlickrExport/FlickrNet/AuthenticationRequiredException.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FlickrExport/FlickrNet/AuthenticationRequiredException.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,14 @@
+using System;
+
+namespace FlickrNet
+{
+	/// <summary>
+	/// Exception thrown when method requires authentication but no authentication token is supplied.
+	/// </summary>
+	public class AuthenticationRequiredException : FlickrException
+	{
+		internal AuthenticationRequiredException() : base("Method requires authentication but no token supplied.")
+		{
+		}
+	}
+}

Added: trunk/extensions/Exporters/FlickrExport/FlickrNet/Blogs.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FlickrExport/FlickrNet/Blogs.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,55 @@
+ïusing System.Xml.Serialization;
+using System.Xml.Schema;
+
+namespace FlickrNet
+{
+	/// <summary>
+	/// Contains a list of <see cref="Blog"/> items for the user.
+	/// </summary>
+	/// <remarks>
+	/// <see cref="Blogs.BlogCollection"/> may be null if no blogs are specified.
+	/// </remarks>
+	[System.Serializable]
+	public class Blogs
+	{
+		/// <summary>
+		/// An array of <see cref="Blog"/> items for the user.
+		/// </summary>
+		[XmlElement("blog", Form=XmlSchemaForm.Unqualified)]
+		public Blog[] BlogCollection;
+	}
+
+	/// <summary>
+	/// Provides details of a specific blog, as configured by the user.
+	/// </summary>
+	[System.Serializable]
+	public class Blog
+	{
+		/// <summary>
+		/// The ID Flickr has assigned to the blog. Use this to post to the blog using
+        /// <see cref="Flickr.BlogPostPhoto(string, string, string, string)"/> or
+        /// <see cref="Flickr.BlogPostPhoto(string, string, string, string, string)"/>.
+		/// </summary>
+		[XmlAttribute("id", Form=XmlSchemaForm.Unqualified)]
+		public string BlogId;
+
+		/// <summary>
+		/// The name you have assigned to the blog in Flickr.
+		/// </summary>
+		[XmlAttribute("name", Form=XmlSchemaForm.Unqualified)]
+		public string BlogName;
+
+		/// <summary>
+		/// The URL of the blog website.
+		/// </summary>
+		[XmlAttribute("url", Form=XmlSchemaForm.Unqualified)]
+		public string BlogUrl;
+
+		/// <summary>
+		/// If Flickr stores the password for this then this will be 0, meaning you do not need to pass in the
+		/// password when posting.
+		/// </summary>
+		[XmlAttribute("needspassword")]
+		public int NeedsPassword;
+	}
+}
\ No newline at end of file

Added: trunk/extensions/Exporters/FlickrExport/FlickrNet/BoundaryBox.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FlickrExport/FlickrNet/BoundaryBox.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,222 @@
+using System;
+
+namespace FlickrNet
+{
+
+	/// <summary>
+	/// Summary description for BoundaryBox.
+	/// </summary>
+	[Serializable]
+	public class BoundaryBox
+	{
+		private GeoAccuracy _accuracy = GeoAccuracy.Street;
+		private bool _isSet = false;
+
+		private double _minimumLat = -90;
+		private double _minimumLon = -180;
+		private double _maximumLat = 90;
+		private double _maximumLon = 180;
+
+		/// <summary>
+		/// Gets weither the boundary box has been set or not.
+		/// </summary>
+		internal bool IsSet
+		{
+			get { return _isSet; }
+		}
+
+		/// <summary>
+		/// The search accuracy - optional. Defaults to <see cref="GeoAccuracy.Street"/>.
+		/// </summary>
+		public GeoAccuracy Accuracy
+		{
+			get { return _accuracy; }
+			set { _accuracy = value; }
+		}
+
+		/// <summary>
+		/// The minimum latitude of the boundary box, i.e. bottom left hand corner.
+		/// </summary>
+		public double MinimumLatitude
+		{
+			get { return _minimumLat; }
+			set
+			{
+				if( value < -90 || value > 90 )
+					throw new ArgumentOutOfRangeException("MinimumLatitude", "Must be between -90 and 90");
+				_isSet = true; _minimumLat = value;
+			}
+		}
+
+		/// <summary>
+		/// The minimum longitude of the boundary box, i.e. bottom left hand corner. Range of -180 to 180 allowed.
+		/// </summary>
+		public double MinimumLongitude
+		{
+			get { return _minimumLon; }
+			set {
+				if( value < -180 || value > 180 )
+					throw new ArgumentOutOfRangeException("MinimumLongitude", "Must be between -180 and 180");
+				_isSet = true; _minimumLon = value;
+			}
+		}
+
+		/// <summary>
+		/// The maximum latitude of the boundary box, i.e. top right hand corner. Range of -90 to 90 allowed.
+		/// </summary>
+		public double MaximumLatitude
+		{
+			get { return _maximumLat; }
+			set
+			{
+				if( value < -90 || value > 90 )
+					throw new ArgumentOutOfRangeException("MaximumLatitude", "Must be between -90 and 90");
+				_isSet = true; _maximumLat = value;
+			}
+		}
+
+		/// <summary>
+		/// The maximum longitude of the boundary box, i.e. top right hand corner. Range of -180 to 180 allowed.
+		/// </summary>
+		public double MaximumLongitude
+		{
+			get { return _maximumLon; }
+			set
+			{
+				if( value < -180 || value > 180 )
+					throw new ArgumentOutOfRangeException("MaximumLongitude", "Must be between -180 and 180");
+				_isSet = true; _maximumLon = value;
+			}
+		}
+
+		/// <summary>
+		/// Default constructor
+		/// </summary>
+		public BoundaryBox()
+		{
+		}
+
+		/// <summary>
+		/// Default constructor, specifying only the accuracy level.
+		/// </summary>
+		/// <param name="accuracy">The <see cref="GeoAccuracy"/> of the search parameter.</param>
+		public BoundaryBox(GeoAccuracy accuracy)
+		{
+			_accuracy = accuracy;
+		}
+
+		/// <summary>
+		/// Constructor for the <see cref="BoundaryBox"/>
+		/// </summary>
+		/// <param name="points">A comma seperated list of co-ordinates defining the boundary box.</param>
+		/// <param name="accuracy">The <see cref="GeoAccuracy"/> of the search parameter.</param>
+		public BoundaryBox(string points, GeoAccuracy accuracy) : this(points)
+		{
+			_accuracy = accuracy;
+		}
+
+		/// <summary>
+		/// Constructor for the <see cref="BoundaryBox"/>
+		/// </summary>
+		/// <param name="points">A comma seperated list of co-ordinates defining the boundary box.</param>
+		public BoundaryBox(string points)
+		{
+			if( points == null ) throw new ArgumentNullException("points");
+
+			string[] splits = points.Split(',');
+
+			if( splits.Length != 4 )
+				throw new ArgumentException("Parameter must contain 4 values, seperated by commas.", "points");
+
+			try
+			{
+				MinimumLongitude = double.Parse(splits[0]);
+				MinimumLatitude = double.Parse(splits[1]);
+				MaximumLongitude = double.Parse(splits[2]);
+				MaximumLatitude = double.Parse(splits[3]);
+			}
+			catch(FormatException)
+			{
+				throw new ArgumentException("Unable to parse points as integer values", "points");
+			}
+		}
+
+		/// <summary>
+		/// Constructor for the <see cref="BoundaryBox"/>.
+		/// </summary>
+		/// <param name="minimumLongitude">The minimum longitude of the boundary box. Range of -180 to 180 allowed.</param>
+		/// <param name="minimumLatitude">The minimum latitude of the boundary box. Range of -90 to 90 allowed.</param>
+		/// <param name="maximumLongitude">The maximum longitude of the boundary box. Range of -180 to 180 allowed.</param>
+		/// <param name="maximumLatitude">The maximum latitude of the boundary box. Range of -90 to 90 allowed.</param>
+		/// <param name="accuracy">The <see cref="GeoAccuracy"/> of the search parameter.</param>
+		public BoundaryBox(double minimumLongitude, double minimumLatitude, double maximumLongitude, double maximumLatitude, GeoAccuracy accuracy) : this(minimumLongitude, minimumLatitude, maximumLongitude, maximumLatitude)
+		{
+			_accuracy = accuracy;
+		}
+
+		/// <summary>
+		/// Constructor for the <see cref="BoundaryBox"/>.
+		/// </summary>
+		/// <param name="minimumLongitude">The minimum longitude of the boundary box. Range of -180 to 180 allowed.</param>
+		/// <param name="minimumLatitude">The minimum latitude of the boundary box. Range of -90 to 90 allowed.</param>
+		/// <param name="maximumLongitude">The maximum longitude of the boundary box. Range of -180 to 180 allowed.</param>
+		/// <param name="maximumLatitude">The maximum latitude of the boundary box. Range of -90 to 90 allowed.</param>
+		public BoundaryBox(double minimumLongitude, double minimumLatitude, double maximumLongitude, double maximumLatitude)
+		{
+			MinimumLatitude = minimumLatitude;
+			MinimumLongitude = minimumLongitude;
+			MaximumLatitude = maximumLatitude;
+			MaximumLongitude = maximumLongitude;
+		}
+
+		/// <summary>
+		/// Overrides the ToString method.
+		/// </summary>
+		/// <returns>A comma seperated list of co-ordinates defining the boundary box.</returns>
+		public override string ToString()
+		{
+			return String.Format("{0},{1},{2},{3}", MinimumLongitude, MinimumLatitude, MaximumLongitude, MaximumLatitude);
+		}
+
+		/// <summary>
+		/// Example boundary box for the UK.
+		/// </summary>
+		public static BoundaryBox UK
+		{
+			get { return new BoundaryBox(-11.118164, 49.809632, 1.625977, 62.613562); }
+		}
+
+		/// <summary>
+		/// Example boundary box for Newcastle upon Tyne, England.
+		/// </summary>
+		public static BoundaryBox UKNewcastle
+		{
+			get { return new BoundaryBox(-1.71936, 54.935821, -1.389771, 55.145919); }
+		}
+
+		/// <summary>
+		/// Example boundary box for the USA (excludes Hawaii and Alaska).
+		/// </summary>
+		public static BoundaryBox USA
+		{
+			get { return new BoundaryBox(-130.429687, 22.43134, -58.535156, 49.382373); }
+		}
+
+		/// <summary>
+		/// Example boundary box for Canada.
+		/// </summary>
+		public static BoundaryBox Canada
+		{
+			get { return new BoundaryBox(-143.085937, 41.640078, -58.535156, 73.578167); }
+		}
+
+		/// <summary>
+		/// Example boundary box for the whole world.
+		/// </summary>
+		public static BoundaryBox World
+		{
+			get { return new BoundaryBox(-180, -90, 180, 90); }
+		}
+
+	}
+}

Added: trunk/extensions/Exporters/FlickrExport/FlickrNet/Cache.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FlickrExport/FlickrNet/Cache.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,387 @@
+using System;
+using System.Collections;
+using System.IO;
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace FlickrNet
+{
+	/// <summary>
+	/// Internal Cache class
+	/// </summary>
+	internal sealed class Cache
+	{
+		private class CacheException : Exception
+		{
+			public CacheException(string message) : base(message)
+			{}
+
+		}
+
+		private static PersistentCache _downloads;
+
+
+		/// <summary>
+		/// A static object containing the list of cached downloaded files.
+		/// </summary>
+		public static PersistentCache Downloads
+		{
+			get
+			{
+				lock(lockObject)
+				{
+					if( _downloads == null )
+						_downloads = new PersistentCache(Path.Combine(CacheLocation, "downloadCache.dat"), new PictureCacheItemPersister(), CacheSizeLimit);
+					return _downloads;
+				}
+			}
+		}
+
+		private static PersistentCache _responses;
+
+		/// <summary>
+		/// A static object containing the list of cached responses from Flickr.
+		/// </summary>
+		public static PersistentCache Responses
+		{
+			get
+			{
+				lock(lockObject)
+				{
+					if( _responses == null )
+						_responses = new PersistentCache(Path.Combine(CacheLocation, "responseCache.dat"), new ResponseCacheItemPersister(), CacheSizeLimit);
+					return _responses;
+				}
+			}
+		}
+
+
+		private Cache()
+		{
+		}
+
+		private static object lockObject = new object();
+
+		private enum Tristate
+		{
+			Null, True, False
+		}
+		private static Tristate _cacheDisabled;
+
+		internal static bool CacheDisabled
+		{
+			get
+			{
+#if !WindowsCE
+				if( _cacheDisabled == Tristate.Null && FlickrConfigurationManager.Settings != null )
+					_cacheDisabled = (FlickrConfigurationManager.Settings.CacheDisabled?Tristate.True:Tristate.False);
+#endif
+
+				if( _cacheDisabled == Tristate.Null ) _cacheDisabled = Tristate.False;
+
+				return (_cacheDisabled==Tristate.True);
+			}
+			set
+			{
+				_cacheDisabled = value?Tristate.True:Tristate.False;
+			}
+		}
+
+		private static string _cacheLocation;
+
+		internal static string CacheLocation
+		{
+			get
+			{
+#if !WindowsCE
+				if( _cacheLocation == null && FlickrConfigurationManager.Settings != null )
+					_cacheLocation = FlickrConfigurationManager.Settings.CacheLocation;
+#endif
+				if( _cacheLocation == null )
+				{
+					try
+					{
+#if !WindowsCE
+						_cacheLocation = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "FlickrNet");
+#else
+                        _cacheLocation = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "FlickrNetCache");
+#endif
+					}
+					catch(System.Security.SecurityException)
+					{
+						// Permission to read application directory not provided.
+						throw new CacheException("Unable to read default cache location. Please cacheLocation in configuration file or set manually in code");
+					}
+				}
+
+				if( _cacheLocation == null )
+					throw new CacheException("Unable to determine cache location. Please set cacheLocation in configuration file or set manually in code");
+
+				return _cacheLocation;
+			}
+			set
+			{
+				_cacheLocation = value;
+			}
+		}
+
+		// Default cache size is set to 50MB
+        private static long _cacheSizeLimit = 52428800;
+        private static long _cacheSize;
+
+		internal static long CacheSizeLimit
+		{
+			get
+			{
+                return _cacheSizeLimit;
+			}
+			set
+			{
+                _cacheSizeLimit = value;
+			}
+		}
+
+		internal static long CacheSize
+		{
+			get
+			{
+                return _cacheSize;
+			}
+			set
+			{
+                _cacheSize = value;
+			}
+		}
+
+
+		// Default cache timeout is 1 hour
+		private static TimeSpan _cachetimeout = new TimeSpan(0, 1, 0, 0, 0);
+
+		/// <summary>
+		/// The default timeout for cachable objects within the cache.
+		/// </summary>
+		public static TimeSpan CacheTimeout
+		{
+			get { return _cachetimeout; }
+			set { _cachetimeout = value; }
+		}
+
+		internal static void FlushCache(string url)
+		{
+			Responses[url] = null;
+			Downloads[url] = null;
+		}
+
+		internal static void FlushCache()
+		{
+			Responses.Flush();
+			Downloads.Flush();
+		}
+
+	}
+
+	/// <summary>
+	/// A cache item containing details of a REST response from Flickr.
+	/// </summary>
+	[Serializable]
+	public class ResponseCacheItem : ICacheItem
+	{
+		private string url;
+		private string response;
+		private DateTime creationTime;
+
+		/// <summary>
+		/// Gets or sets the original URL of the request.
+		/// </summary>
+		public string Url { get { return url; } set { url = value; } }
+
+		/// <summary>
+		/// Gets or sets the XML response.
+		/// </summary>
+		public string Response { get { return response; } set { response = value; } }
+
+		/// <summary>
+		/// Gets or sets the time the cache item was created.
+		/// </summary>
+		public DateTime CreationTime { get { return creationTime; } set { creationTime = value; } }
+
+		/// <summary>
+		/// Gets the filesize of the request.
+		/// </summary>
+		public long FileSize { get { return (response==null?0:response.Length); } }
+
+		void ICacheItem.OnItemFlushed()
+		{
+		}
+
+	}
+
+	internal class ResponseCacheItemPersister : CacheItemPersister
+	{
+		public override ICacheItem Read(Stream inputStream)
+		{
+			string s = Utils.ReadString(inputStream);
+			string response = Utils.ReadString(inputStream);
+
+			string[] chunks = s.Split('\n');
+
+			// Corrupted cache record, so throw IOException which is then handled and returns partial cache.
+			if( chunks.Length != 2 )
+				throw new IOException("Unexpected number of chunks found");
+
+			string url = chunks[0];
+			DateTime creationTime = new DateTime(long.Parse(chunks[1]));
+			ResponseCacheItem item = new ResponseCacheItem();
+			item.Url = url;
+			item.CreationTime = creationTime;
+			item.Response = response;
+			return item;
+		}
+
+		public override void Write(Stream outputStream, ICacheItem cacheItem)
+		{
+			ResponseCacheItem item = (ResponseCacheItem) cacheItem;
+			StringBuilder result = new StringBuilder();
+			result.Append(item.Url + "\n");
+			result.Append(item.CreationTime.Ticks.ToString("0"));
+			Utils.WriteString(outputStream, result.ToString());
+			Utils.WriteString(outputStream, item.Response);
+		}
+	}
+
+	/// <summary>
+	/// An item that can be stored in a cache.
+	/// </summary>
+	public interface ICacheItem
+	{
+		/// <summary>
+		/// The time this cache item was created.
+		/// </summary>
+		DateTime CreationTime { get; }
+
+		/// <summary>
+		/// Gets called back when the item gets flushed
+		/// from the cache.
+		/// </summary>
+		void OnItemFlushed();
+
+		/// <summary>
+		/// The size of this item, in bytes. Return 0
+		/// if size management is not important.
+		/// </summary>
+		long FileSize { get; }
+	}
+
+	/// <summary>
+	/// An interface that knows how to read/write subclasses
+	/// of ICacheItem.  Obviously there will be a tight
+	/// coupling between concrete implementations of ICacheItem
+	/// and concrete implementations of ICacheItemPersister.
+	/// </summary>
+	public abstract class CacheItemPersister
+	{
+		/// <summary>
+		/// Read a single cache item from the input stream.
+		/// </summary>
+		public abstract ICacheItem Read(Stream inputStream);
+
+		/// <summary>
+		/// Write a single cache item to the output stream.
+		/// </summary>
+		public abstract void Write(Stream outputStream, ICacheItem cacheItem);
+	}
+
+	/// <summary>
+	/// Contains details of image held with the Flickr.Net cache.
+	/// </summary>
+	[Serializable]
+	public class PictureCacheItem : ICacheItem
+	{
+		#region [ Internal Variables ]
+		internal string url;
+		internal DateTime creationTime;
+		internal string filename;
+		internal long fileSize;
+		#endregion
+
+		#region [ Public Properties ]
+		/// <summary>
+		/// The URL of the original image on Flickr.
+		/// </summary>
+		public string Url { get { return url; } }
+		/// <summary>
+		/// The <see cref="DateTime"/> that the cache item was created.
+		/// </summary>
+		public DateTime CreationTime { get { return creationTime; } }
+
+		/// <summary>
+		/// The filesize in bytes of the image.
+		/// </summary>
+		public long FileSize { get { return fileSize; } }
+		/// <summary>
+		/// The Flickr photo id of the image.
+		/// </summary>
+		public string PhotoId
+		{
+			get
+			{
+				if( url == null )
+					return null;
+				else
+				{
+					int begin = url.LastIndexOf("/");
+					int end = url.IndexOf("_");
+
+					return url.Substring(begin + 1, (end - begin) - 1);
+				}
+			}
+		}
+		#endregion
+
+		#region [ Public Methods ]
+
+		void ICacheItem.OnItemFlushed()
+		{
+			File.Delete(filename);
+		}
+
+		#endregion
+	}
+
+	/// <summary>
+	/// Persists PictureCacheItem objects.
+	/// </summary>
+	internal class PictureCacheItemPersister : CacheItemPersister
+	{
+		public override ICacheItem Read(Stream inputStream)
+		{
+			string s = Utils.ReadString(inputStream);
+
+			string[] chunks = s.Split('\n');
+			string url = chunks[0];
+			DateTime creationTime = new DateTime(long.Parse(chunks[1]));
+			string filename = chunks[2];
+			long fileSize = long.Parse(chunks[3]);
+
+			PictureCacheItem pci = new PictureCacheItem();
+			pci.url = url;
+			pci.creationTime = creationTime;
+			pci.filename = filename;
+			pci.fileSize = fileSize;
+			return pci;
+		}
+
+		public override void Write(Stream outputStream, ICacheItem cacheItem)
+		{
+			PictureCacheItem pci = (PictureCacheItem) cacheItem;
+			StringBuilder output = new StringBuilder();
+
+			output.Append(pci.url + "\n");
+			output.Append(pci.creationTime.Ticks + "\n");
+			output.Append(pci.filename + "\n");
+			output.Append(pci.fileSize + "\n");
+
+			Utils.WriteString(outputStream, output.ToString());
+		}
+	}
+}

Added: trunk/extensions/Exporters/FlickrExport/FlickrNet/Categories.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FlickrExport/FlickrNet/Categories.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,80 @@
+ïusing System.Xml.Serialization;
+using System.Xml.Schema;
+
+namespace FlickrNet
+{
+	/// <summary>
+	/// Contains details of a category, including groups belonging to the category and sub categories.
+	/// </summary>
+	[System.Serializable]
+	public class Category
+	{
+		/// <summary>
+		/// The name for the category.
+		/// </summary>
+		[XmlAttribute("name", Form=XmlSchemaForm.Unqualified)]
+		public string CategoryName;
+
+		/// <summary>
+		/// A forward slash delimited list of the parents of the current group.
+		/// </summary>
+		/// <remarks>
+		/// Can be matched against the list of PathIds to jump directly to a parent group.
+		/// </remarks>
+		/// <example>
+		/// Group Id 91, Romance will return "/Life/Romance" as the Path and "/90/91" as its PathIds
+		/// </example>
+		[XmlAttribute("path", Form=XmlSchemaForm.Unqualified)]
+		public string Path;
+
+		/// <summary>
+		/// A forward slash delimited list of the ids of the parents of the current group.
+		/// </summary>
+		/// <remarks>
+		/// Can be matched against the Path to jump directly to a parent group.
+		/// </remarks>
+		/// <example>
+		/// Group Id 91, Romance will return "/Life/Romance" as the Path and "/90/91" as its PathIds
+		/// </example>
+		[XmlAttribute("pathids", Form=XmlSchemaForm.Unqualified)]
+		public string PathIds;
+
+		/// <summary>
+		/// An array of <see cref="SubCategory"/> items.
+		/// </summary>
+		[XmlElement("subcat", Form=XmlSchemaForm.Unqualified)]
+		public SubCategory[] SubCategories;
+
+		/// <summary>
+		/// An array of <see cref="Group"/> items, listing the groups within this category.
+		/// </summary>
+		[XmlElement("group", Form=XmlSchemaForm.Unqualified)]
+		public Group[] Groups;
+	}
+
+	/// <summary>
+	/// Holds details of a sub category, including its id, name and the number of groups in it.
+	/// </summary>
+	[System.Serializable]
+	public class SubCategory
+	{
+		/// <summary>
+		/// The id of the category.
+		/// </summary>
+		[XmlAttribute("id", Form=XmlSchemaForm.Unqualified)]
+		public long SubCategoryId;
+
+		/// <summary>
+		/// The name of the category.
+		/// </summary>
+		[XmlAttribute("name", Form=XmlSchemaForm.Unqualified)]
+		public string SubCategoryName;
+
+		/// <summary>
+		/// The number of groups found within the category.
+		/// </summary>
+		[XmlAttribute("count", Form=XmlSchemaForm.Unqualified)]
+		public long GroupCount;
+	}
+
+}
\ No newline at end of file

Added: trunk/extensions/Exporters/FlickrExport/FlickrNet/Comments.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FlickrExport/FlickrNet/Comments.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,119 @@
+using System;
+using System.Xml;
+
+namespace FlickrNet
+{
+	/// <summary>
+	/// Parent object containing details of a photos comments.
+	/// </summary>
+	internal class PhotoComments
+	{
+		static private string _photoId;
+
+		static private Comment[] _comments;
+
+		static internal Comment[] GetComments(XmlNode node)
+		{
+			if( node.Attributes["photo_id"] != null )
+				_photoId = node.Attributes["photo_id"].Value;
+			XmlNodeList nodes = node.SelectNodes("comment");
+			_comments = new Comment[nodes.Count];
+
+			for(int i = 0; i < _comments.Length; i++)
+			{
+				_comments[i] = new Comment(_photoId, nodes[i]);
+			}
+
+			return _comments;
+		}
+	}
+
+	/// <summary>
+	/// Contains the details of a comment made on a photo.
+	/// returned by the <see cref="Flickr.PhotosCommentsGetList"/> method.
+	/// </summary>
+	public class Comment
+	{
+		private string _photoId;
+		private string _authorUserId;
+		private string _authorUserName;
+		private string _commentId;
+		private Uri _permaLink;
+		private DateTime _dateCreated;
+		private string _comment;
+
+		/// <summary>
+		/// The photo id associated with this comment.
+		/// </summary>
+		public string PhotoId
+		{
+			get { return _photoId; }
+		}
+
+		/// <summary>
+		/// The comment id of this comment.
+		/// </summary>
+		public string CommentId
+		{
+			get { return _commentId; }
+		}
+
+		/// <summary>
+		/// The user id of the author of the comment.
+		/// </summary>
+		public string AuthorUserId
+		{
+			get { return _authorUserId; }
+		}
+
+		/// <summary>
+		/// The username (screen name) of the author of the comment.
+		/// </summary>
+		public string AuthorUserName
+		{
+			get { return _authorUserName; }
+		}
+
+		/// <summary>
+		/// The permalink to the comment on the photos web page.
+		/// </summary>
+		public Uri Permalink
+		{
+			get { return _permaLink; }
+		}
+
+		/// <summary>
+		/// The date and time that the comment was created.
+		/// </summary>
+		public DateTime DateCreated
+		{
+			get { return _dateCreated; }
+		}
+
+		/// <summary>
+		/// The comment text (can contain HTML).
+		/// </summary>
+		public string CommentHtml
+		{
+			get { return _comment; }
+		}
+
+		internal Comment(string photoId, XmlNode node)
+		{
+			_photoId = photoId;
+
+			if( node.Attributes["id"] != null )
+				_commentId = node.Attributes["id"].Value;
+			if( node.Attributes["author"] != null )
+				_authorUserId = node.Attributes["author"].Value;
+			if( node.Attributes["authorname"] != null )
+				_authorUserName = node.Attributes["authorname"].Value;
+			if( node.Attributes["permalink"] != null )
+				_permaLink = new Uri(node.Attributes["permalink"].Value);
+			if( node.Attributes["datecreate"] != null )
+				_dateCreated = Utils.UnixTimestampToDate(node.Attributes["datecreate"].Value);
+			if( node.InnerXml.Length > 0 )
+				_comment = node.InnerText;
+		}
+	}
+}

Added: trunk/extensions/Exporters/FlickrExport/FlickrNet/Contacts.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FlickrExport/FlickrNet/Contacts.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,67 @@
+ïusing System.Xml.Serialization;
+using System.Xml.Schema;
+
+namespace FlickrNet
+{
+	/// <summary>
+	/// Contains a list of <see cref="Contact"/> items for a given user.
+	/// </summary>
+	[System.Serializable]
+	public class Contacts
+	{
+		/// <summary>
+		/// An array of <see cref="Contact"/> items for the user.
+		/// </summary>
+		[XmlElement("contact", Form=XmlSchemaForm.Unqualified)]
+		public Contact[] ContactCollection = new Contact[0];
+	}
+
+	/// <summary>
+	/// Contains details of a contact for a particular user.
+	/// </summary>
+	[System.Serializable]
+	public class Contact
+	{
+		/// <summary>
+		/// The user id of the contact.
+		/// </summary>
+		[XmlAttribute("nsid", Form=XmlSchemaForm.Unqualified)]
+		public string UserId;
+
+		/// <summary>
+		/// The username (or screen name) of the contact.
+		/// </summary>
+		[XmlAttribute("username", Form=XmlSchemaForm.Unqualified)]
+		public string UserName;
+
+		/// <summary>
+		/// Is this contact marked as a friend contact?
+		/// </summary>
+		[XmlAttribute("friend", Form=XmlSchemaForm.Unqualified)]
+		public int IsFriend;
+
+		/// <summary>
+		/// Is this user marked a family contact?
+		/// </summary>
+		[XmlAttribute("family", Form=XmlSchemaForm.Unqualified)]
+		public int IsFamily;
+
+		/// <summary>
+		/// Unsure how to even set this!
+		/// </summary>
+		[XmlAttribute("ignored", Form=XmlSchemaForm.Unqualified)]
+		public int IsIgnored;
+
+		/// <summary>
+		/// Is the user online at the moment (FlickrLive)
+		/// </summary>
+		[XmlAttribute("online", Form=XmlSchemaForm.Unqualified)]
+		public int IsOnline;
+
+		/// <summary>
+		/// If the user is online, but marked as away, then this will contains their away message.
+		/// </summary>
+		[XmlText()]
+		public string AwayDescription;
+	}
+}
\ No newline at end of file

Added: trunk/extensions/Exporters/FlickrExport/FlickrNet/Context.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FlickrExport/FlickrNet/Context.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,173 @@
+using System;
+using System.Collections;
+using System.Xml.Serialization;
+using System.Xml.Schema;
+using System.Xml;
+
+namespace FlickrNet
+{
+	/// <summary>
+	/// The context of the current photo, as returned by
+	/// <see cref="Flickr.PhotosGetContext"/>,
+	/// <see cref="Flickr.PhotosetsGetContext"/>
+	///  and <see cref="Flickr.GroupPoolGetContext"/> methods.
+	/// </summary>
+	public class Context
+	{
+		/// <summary>
+		/// The number of photos in the current context, e.g. Group, Set or photostream.
+		/// </summary>
+		public long Count;
+		/// <summary>
+		/// The next photo in the context.
+		/// </summary>
+		public ContextPhoto NextPhoto;
+		/// <summary>
+		/// The previous photo in the context.
+		/// </summary>
+		public ContextPhoto PreviousPhoto;
+	}
+
+	/// <summary>
+	/// Temporary class used to excapsulate the context count property.
+	/// </summary>
+	[System.Serializable]
+	public class ContextCount
+	{
+		/// <summary>
+		/// Default constructor.
+		/// </summary>
+		public ContextCount()
+		{
+		}
+
+		/// <summary>
+		/// The number of photos in the context.
+		/// </summary>
+		[XmlText()]
+		public long Count;
+	}
+
+	/// <summary>
+	/// The next (or previous) photo in the current context.
+	/// </summary>
+	[System.Serializable]
+	public class ContextPhoto
+	{
+		/// <summary>
+		/// The id of the next photo. Will be "0" if this photo is the last.
+		/// </summary>
+		[XmlAttribute("id", Form=XmlSchemaForm.Unqualified)]
+		public string PhotoId;
+
+		/// <summary>
+		/// The secret for the photo.
+		/// </summary>
+		[XmlAttribute("secret", Form=XmlSchemaForm.Unqualified)]
+		public string Secret;
+
+		/// <summary>
+		/// The title of the next photo in context.
+		/// </summary>
+		[XmlAttribute("title", Form=XmlSchemaForm.Unqualified)]
+		public string Title;
+
+		/// <summary>
+		/// The URL, in the given context, for the next or previous photo.
+		/// </summary>
+		[XmlAttribute("url", Form=XmlSchemaForm.Unqualified)]
+		public string Url;
+
+		/// <summary>
+		/// The URL for the thumbnail of the photo.
+		/// </summary>
+		[XmlAttribute("thumb", Form=XmlSchemaForm.Unqualified)]
+		public string Thumbnail;
+	}
+
+	/// <summary>
+	/// All contexts that a photo is in.
+	/// </summary>
+	public class AllContexts
+	{
+		private ContextSet[] _sets;
+		private ContextGroup[] _groups;
+
+		/// <summary>
+		/// An array of <see cref="ContextSet"/> objects for the current photo.
+		/// </summary>
+		public ContextSet[] Sets
+		{
+			get { return _sets; }
+		}
+
+		/// <summary>
+		/// An array of <see cref="ContextGroup"/> objects for the current photo.
+		/// </summary>
+		public ContextGroup[] Groups
+		{
+			get { return _groups; }
+		}
+
+		internal AllContexts(XmlElement[] elements)
+		{
+			ArrayList sets = new ArrayList();
+			ArrayList groups = new ArrayList();
+
+			foreach(XmlElement element in elements)
+			{
+				if( element.Name == "set" )
+				{
+					ContextSet aset = new ContextSet();
+					aset.PhotosetId = element.Attributes["id"].Value;
+					aset.Title = element.Attributes["title"].Value;
+					sets.Add(aset);
+				}
+
+				if( element.Name == "pool" )
+				{
+					ContextGroup agroup = new ContextGroup();
+					agroup.GroupId = element.Attributes["id"].Value;
+					agroup.Title = element.Attributes["title"].Value;
+					groups.Add(agroup);
+				}
+			}
+
+			_sets = new ContextSet[sets.Count];
+			sets.CopyTo(_sets);
+
+			_groups = new ContextGroup[groups.Count];
+			groups.CopyTo(_groups);
+		}
+	}
+
+	/// <summary>
+	/// A set context for a photo.
+	/// </summary>
+	public class ContextSet
+	{
+		/// <summary>
+		/// The Photoset ID of the set the selected photo is in.
+		/// </summary>
+		public string PhotosetId;
+		/// <summary>
+		/// The title of the set the selected photo is in.
+		/// </summary>
+		public string Title;
+	}
+
+	/// <summary>
+	/// A group context got a photo.
+	/// </summary>
+	public class ContextGroup
+	{
+		/// <summary>
+		/// The Group ID for the group that the selected photo is in.
+		/// </summary>
+		public string GroupId;
+		/// <summary>
+		/// The title of the group that then selected photo is in.
+		/// </summary>
+		public string Title;
+	}
+}

Added: trunk/extensions/Exporters/FlickrExport/FlickrNet/DateGranularity.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FlickrExport/FlickrNet/DateGranularity.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,24 @@
+using System;
+
+namespace FlickrNet
+{
+	/// <summary>
+	/// DateGranularity, used for setting taken date in <see cref="Flickr.PhotosSetDates(string, DateTime, DateGranularity)"/>
+    /// or <see cref="Flickr.PhotosSetDates(string, DateTime, DateTime, DateGranularity)"/>.
+	/// </summary>
+	public enum DateGranularity
+	{
+		/// <summary>
+		/// The date specified is the exact date the photograph was taken.
+		/// </summary>
+		FullDate = 0,
+		/// <summary>
+		/// The date specified is the year and month the photograph was taken.
+		/// </summary>
+		YearMonthOnly = 4,
+		/// <summary>
+		/// The date specified is the year the photograph was taken.
+		/// </summary>
+		YearOnly = 6
+	}
+}

Added: trunk/extensions/Exporters/FlickrExport/FlickrNet/Enums.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FlickrExport/FlickrNet/Enums.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,230 @@
+using System;
+using System.Xml.Serialization;
+
+namespace FlickrNet
+{
+	/// <summary>
+	/// A list of service the Flickr.Net API Supports.
+	/// </summary>
+	/// <remarks>
+	/// Not all methods are supported by all service. Behaviour of the library may be unpredictable if not using Flickr
+	/// as your service.
+	/// </remarks>
+	public enum SupportedService
+	{
+		/// <summary>
+		/// Flickr - http://www.flickr.com/services/api
+		/// </summary>
+		Flickr = 0,
+		/// <summary>
+		/// Zooomr - http://blog.zooomr.com/2006/03/27/attention-developers/
+		/// </summary>
+		Zooomr = 1,
+		/// <summary>
+		/// 23HQ = http://www.23hq.com/doc/api/
+		/// </summary>
+		TwentyThreeHQ = 2
+	}
+
+	/// <summary>
+	/// Used to specify where all tags must be matched or any tag to be matched.
+	/// </summary>
+	[Serializable]
+	public enum TagMode
+	{
+		/// <summary>
+		/// No tag mode specified.
+		/// </summary>
+		None,
+		/// <summary>
+		/// Any tag must match, but not all.
+		/// </summary>
+		AnyTag,
+		/// <summary>
+		/// All tags must be found.
+		/// </summary>
+		AllTags,
+		/// <summary>
+		/// Uncodumented and unsupported tag mode where boolean operators are supported.
+		/// </summary>
+		Boolean
+	}
+
+	/// <summary>
+	/// What type of content is the upload representing.
+	/// </summary>
+	[Serializable]
+	public enum ContentType
+	{
+		/// <summary>
+		/// No content type specified.
+		/// </summary>
+		None = 0,
+		/// <summary>
+		/// For normal photographs.
+		/// </summary>
+		Photo = 1,
+		/// <summary>
+		/// For screenshots.
+		/// </summary>
+		Screenshot = 2,
+		/// <summary>
+		/// For other uploads, such as artwork.
+		/// </summary>
+		Other = 3
+	}
+
+	/// <summary>
+	/// Safety level of the photographic image.
+	/// </summary>
+	[Serializable]
+	public enum SafetyLevel
+	{
+		/// <summary>
+		/// No safety level specified.
+		/// </summary>
+		None = 0,
+		/// <summary>
+		/// Very safe (suitable for a global family audience).
+		/// </summary>
+		Safe = 1,
+		/// <summary>
+		/// Moderate (the odd articstic nude is ok, but thats the limit).
+		/// </summary>
+		Moderate = 2,
+		/// <summary>
+		/// Restricted (suitable for over 18s only).
+		/// </summary>
+		Restricted = 3
+	}
+
+	/// <summary>
+	/// Determines weither the photo is visible in public searches. The default is 1, Visible.
+	/// </summary>
+	[Serializable]
+	public enum HiddenFromSearch
+	{
+		/// <summary>
+		/// No preference specified, defaults to your preferences on Flickr.
+		/// </summary>
+		None = 0,
+		/// <summary>
+		/// Photo is visible to public searches.
+		/// </summary>
+		Visible = 1,
+		/// <summary>
+		/// photo is hidden from public searches.
+		/// </summary>
+		Hidden = 2
+	}
+
+
+	/// <summary>
+	/// Used to specify where all tags must be matched or any tag to be matched.
+	/// </summary>
+	[Serializable]
+	public enum MachineTagMode
+	{
+		/// <summary>
+		/// No tag mode specified.
+		/// </summary>
+		None,
+		/// <summary>
+		/// Any tag must match, but not all.
+		/// </summary>
+		AnyTag,
+		/// <summary>
+		/// All tags must be found.
+		/// </summary>
+		AllTags
+	}
+
+	/// <summary>
+	/// When searching for photos you can filter on the privacy of the photos.
+	/// </summary>
+	[Serializable]
+	public enum PrivacyFilter
+	{
+		/// <summary>
+		/// Do not filter.
+		/// </summary>
+		None = 0,
+		/// <summary>
+		/// Show only public photos.
+		/// </summary>
+		PublicPhotos = 1,
+		/// <summary>
+		/// Show photos which are marked as private but viewable by friends.
+		/// </summary>
+		PrivateVisibleToFriends = 2,
+		/// <summary>
+		/// Show photos which are marked as private but viewable by family contacts.
+		/// </summary>
+		PrivateVisibleToFamily = 3,
+		/// <summary>
+		/// Show photos which are marked as private but viewable by friends and family contacts.
+		/// </summary>
+		PrivateVisibleToFriendsFamily = 4,
+		/// <summary>
+		/// Show photos which are marked as completely private.
+		/// </summary>
+		CompletelyPrivate = 5
+	}
+
+	/// <summary>
+	/// An enumeration defining who can add comments.
+	/// </summary>
+	[Serializable]
+	public enum PermissionComment
+	{
+		/// <summary>
+		/// Nobody.
+		/// </summary>
+		[XmlEnum("0")]
+		Nobody = 0,
+		/// <summary>
+		/// Friends and family only.
+		/// </summary>
+		[XmlEnum("1")]
+		FriendsAndFamily = 1,
+		/// <summary>
+		/// Contacts only.
+		/// </summary>
+		[XmlEnum("2")]
+		ContactsOnly = 2,
+		/// <summary>
+		/// All Flickr users.
+		/// </summary>
+		[XmlEnum("3")]
+		Everybody = 3
+	}
+
+	/// <summary>
+	/// An enumeration defining who can add meta data (tags and notes).
+	/// </summary>
+	public enum PermissionAddMeta
+	{
+		/// <summary>
+		/// The owner of the photo only.
+		/// </summary>
+		[XmlEnum("0")]
+		Owner = 0,
+		/// <summary>
+		/// Friends and family only.
+		/// </summary>
+		[XmlEnum("1")]
+		FriendsAndFamily = 1,
+		/// <summary>
+		/// All contacts.
+		/// </summary>
+		[XmlEnum("2")]
+		Contacts = 2,
+		/// <summary>
+		/// All Flickr users.
+		/// </summary>
+		[XmlEnum("3")]
+		Everybody = 3
+	}
+
+
+}

Added: trunk/extensions/Exporters/FlickrExport/FlickrNet/ExifPhoto.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FlickrExport/FlickrNet/ExifPhoto.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,134 @@
+using System;
+using System.Xml.Schema;
+using System.Xml.Serialization;
+
+namespace FlickrNet
+{
+	/// <summary>
+	/// EXIF data for the selected photo.
+	/// </summary>
+	[Serializable]
+	public class ExifPhoto
+	{
+		internal ExifPhoto(string photoId, string secret, string server, ExifTag[] array)
+		{
+			_photoId = photoId;
+			_secret = secret;
+			_server = server;
+			_tagCollection = array;
+		}
+
+		private string _photoId;
+		private string _secret;
+		private string _server;
+		private ExifTag[] _tagCollection;
+
+		/// <summary>
+		/// The Photo ID for the photo whose EXIF data this is.
+		/// </summary>
+		public string PhotoId
+		{
+			get { return _photoId; }
+		}
+
+		/// <summary>
+		/// The Secret of the photo.
+		/// </summary>
+		public string Secret
+		{
+			get { return _secret; }
+		}
+
+		/// <summary>
+		/// The server number for the photo.
+		/// </summary>
+		public string Server
+		{
+			get { return _server; }
+		}
+
+		/// <summary>
+		/// An array of EXIF tags. See <see cref="ExifTag"/> for more details.
+		/// </summary>
+		public ExifTag[] ExifTagCollection
+		{
+			get { return _tagCollection; }
+		}
+	}
+
+	/// <summary>
+	/// Details of an individual EXIF tag.
+	/// </summary>
+	[Serializable]
+	public class ExifTag
+	{
+		private string _tagSpace;
+		private int _tagSpaceId;
+		private string _tag;
+		private string _label;
+		private string _raw;
+		private string _clean;
+
+		/// <summary>
+		/// The type of EXIF data, e.g. EXIF, TIFF, GPS etc.
+		/// </summary>
+		[XmlAttribute("tagspace")]
+		public string TagSpace
+		{
+			get { return _tagSpace; }
+			set { _tagSpace = value; }
+		}
+
+		/// <summary>
+		/// An id number for the type of tag space.
+		/// </summary>
+		[XmlAttribute("tagspaceid")]
+		public int TagSpaceId
+		{
+			get { return _tagSpaceId; }
+			set { _tagSpaceId = value; }
+		}
+
+		/// <summary>
+		/// The tag number.
+		/// </summary>
+		[XmlAttribute("tag")]
+		public string Tag
+		{
+			get { return _tag; }
+			set { _tag = value; }
+		}
+
+		/// <summary>
+		/// The label, or description for the tag, such as Aperture
+		/// or Manufacturer
+		/// </summary>
+		[XmlAttribute("label")]
+		public string Label
+		{
+			get { return _label; }
+			set { _label = value; }
+		}
+
+		/// <summary>
+		/// The raw EXIF data.
+		/// </summary>
+		[XmlElement("raw")]
+		public string Raw
+		{
+			get { return _raw; }
+			set { _raw = value; }
+		}
+
+		/// <summary>
+		/// An optional clean version of the <see cref="Raw"/> property.
+		/// May be null if the <c>Raw</c> property is in a suitable format already.
+		/// </summary>
+		[XmlElement("clean")]
+		public string Clean
+		{
+			get { return _clean; }
+			set { _clean = value; }
+		}
+	}
+}

Added: trunk/extensions/Exporters/FlickrExport/FlickrNet/Flickr.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FlickrExport/FlickrNet/Flickr.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,5225 @@
+using System;
+using System.Net;
+using System.IO;
+using System.Xml;
+using System.Xml.XPath;
+using System.Xml.Serialization;
+using System.Text;
+using System.Collections;
+using System.Collections.Specialized;
+
+namespace FlickrNet
+{
+	/// <summary>
+	/// The main Flickr class.
+	/// </summary>
+	/// <remarks>
+	/// Create an instance of this class and then call its methods to perform methods on Flickr.
+	/// </remarks>
+	/// <example>
+	/// <code>FlickrNet.Flickr flickr = new FlickrNet.Flickr();
+	/// User user = flickr.PeopleFindByEmail("cal iamcal com");
+	/// Console.WriteLine("User Id is " + u.UserId);</code>
+	/// </example>
+	//[System.Net.WebPermission(System.Security.Permissions.SecurityAction.Demand, ConnectPattern="http://www.flickr.com/.*";)]
+	public class Flickr
+	{
+
+		#region [ Upload Event and Delegate ]
+		/// <summary>
+		///
+		/// </summary>
+		public delegate void UploadProgressHandler(object sender, UploadProgressEventArgs e);
+
+		/// <summary>
+		/// UploadProgressHandler is fired during a synchronous upload process to signify that
+		/// a segment of uploading has been completed. This is approximately 50 bytes. The total
+		/// uploaded is recorded in the <see cref="UploadProgressEventArgs"/> class.
+		/// </summary>
+		public event UploadProgressHandler OnUploadProgress;
+		#endregion
+
+		#region [ Private Variables ]
+#if !WindowsCE
+		private static bool _isServiceSet = false;
+#endif
+		private static SupportedService _defaultService = SupportedService.Flickr;
+
+		private SupportedService _service = SupportedService.Flickr;
+
+		private string BaseUrl
+		{
+			get { return _baseUrl[(int)_service]; }
+		}
+
+		private string[] _baseUrl = new string[] {
+															"http://api.flickr.com/services/rest/";,
+															"http://www.zooomr.com/api/rest/";,
+															"http://www.23hq.com/services/rest/"};
+
+		private string UploadUrl
+		{
+			get { return _uploadUrl[(int)_service]; }
+		}
+		private static string[] _uploadUrl = new string[] {
+															  "http://api.flickr.com/services/upload/";,
+															  "http://www.zooomr.com/api/upload";,
+															  "http://www.23hq.com/services/upload/"};
+
+		private string ReplaceUrl
+		{
+			get { return _replaceUrl[(int)_service]; }
+		}
+		private static string[] _replaceUrl = new string[] {
+															   "http://api.flickr.com/services/replace/";,
+															   "http://www.zooomr.com/api/replace";,
+															   "http://www.23hq.com/services/replace/"};
+
+		private string AuthUrl
+		{
+			get { return _authUrl[(int)_service]; }
+		}
+		private static string[] _authUrl = new string[] {
+															   "http://www.flickr.com/services/auth/";,
+															   "http://www.zooomr.com/services/auth/";,
+															   "http://www.23hq.com/services/auth/"};
+
+		private string _apiKey;
+		private string _apiToken;
+		private string _sharedSecret;
+		private int _timeout = 30000;
+		private const string UserAgent = "Mozilla/4.0 FlickrNet API (compatible; MSIE 6.0; Windows NT 5.1)";
+		private string _lastRequest;
+		private string _lastResponse;
+
+		private WebProxy _proxy;// = WebProxy.GetDefaultProxy();
+
+		// Static serializers
+		private static XmlSerializer _responseSerializer = new XmlSerializer(typeof(FlickrNet.Response));
+		private static XmlSerializer _uploaderSerializer = new XmlSerializer(typeof(FlickrNet.Uploader));
+
+		#endregion
+
+		#region [ Public Properties ]
+		/// <summary>
+		/// Get or set the API Key to be used by all calls. API key is mandatory for all
+		/// calls to Flickr.
+		/// </summary>
+		public string ApiKey
+		{
+			get { return _apiKey; }
+			set { _apiKey = (value==null||value.Length==0?null:value); }
+		}
+
+		/// <summary>
+		/// API shared secret is required for all calls that require signing, which includes
+		/// all methods that require authentication, as well as the actual flickr.auth.* calls.
+		/// </summary>
+		public string ApiSecret
+		{
+			get { return _sharedSecret; }
+			set { _sharedSecret = (value==null||value.Length==0?null:value); }
+		}
+
+		/// <summary>
+		/// The API token is required for all calls that require authentication.
+		/// A <see cref="FlickrException"/> will be raised by Flickr if the API token is
+		/// not set when required.
+		/// </summary>
+		/// <remarks>
+		/// It should be noted that some methods will work without the API token, but
+		/// will return different results if used with them (such as group pool requests,
+		/// and results which include private pictures the authenticated user is allowed to see
+		/// (their own, or others).
+		/// </remarks>
+		[Obsolete("Renamed to AuthToken to be more consistent with the Flickr API")]
+		public string ApiToken
+		{
+			get { return _apiToken; }
+			set { _apiToken = (value==null||value.Length==0?null:value); }
+		}
+
+		/// <summary>
+		/// The authentication token is required for all calls that require authentication.
+		/// A <see cref="FlickrException"/> will be raised by Flickr if the authentication token is
+		/// not set when required.
+		/// </summary>
+		/// <remarks>
+		/// It should be noted that some methods will work without the authentication token, but
+		/// will return different results if used with them (such as group pool requests,
+		/// and results which include private pictures the authenticated user is allowed to see
+		/// (their own, or others).
+		/// </remarks>
+		public string AuthToken
+		{
+			get { return _apiToken; }
+			set { _apiToken = (value==null||value.Length==0?null:value); }
+		}
+
+		/// <summary>
+		/// Gets or sets whether the cache should be disabled. Use only in extreme cases where you are sure you
+		/// don't want any caching.
+		/// </summary>
+		public static bool CacheDisabled
+		{
+			get { return Cache.CacheDisabled; }
+			set { Cache.CacheDisabled = value; }
+		}
+
+		/// <summary>
+		/// All GET calls to Flickr are cached by the Flickr.Net API. Set the <see cref="CacheTimeout"/>
+		/// to determine how long these calls should be cached (make this as long as possible!)
+		/// </summary>
+		public static TimeSpan CacheTimeout
+		{
+			get { return Cache.CacheTimeout; }
+			set { Cache.CacheTimeout = value; }
+		}
+
+		/// <summary>
+		/// Sets or gets the location to store the Cache files.
+		/// </summary>
+		public static string CacheLocation
+		{
+			get { return Cache.CacheLocation; }
+			set { Cache.CacheLocation = value; }
+		}
+
+		/// <summary>
+		/// Gets the current size of the Cache.
+		/// </summary>
+		public static long CacheSize
+		{
+			get { return Cache.CacheSize; }
+		}
+
+		/// <summary>
+		/// <see cref="CacheSizeLimit"/> is the cache file size in bytes for downloaded
+		/// pictures. The default is 50MB (or 50 * 1024 * 1025 in bytes).
+		/// </summary>
+		public static long CacheSizeLimit
+		{
+			get { return Cache.CacheSizeLimit; }
+			set { Cache.CacheSizeLimit = value; }
+		}
+
+		/// <summary>
+		/// The default service to use for new Flickr instances
+		/// </summary>
+		public static SupportedService DefaultService
+		{
+			get
+			{
+#if !WindowsCE
+				if( !_isServiceSet && FlickrConfigurationManager.Settings != null )
+				{
+					_defaultService = FlickrConfigurationManager.Settings.Service;
+					_isServiceSet = true;
+				}
+#endif
+                return _defaultService;
+			}
+			set
+			{
+				_defaultService = value;
+#if !WindowsCE
+				_isServiceSet = true;
+#endif
+			}
+		}
+
+		/// <summary>
+		/// The current service that the Flickr API is using.
+		/// </summary>
+		public SupportedService CurrentService
+		{
+			get
+			{
+				return _service;
+			}
+			set
+			{
+				_service = value;
+#if !WindowsCE
+				if( _service == SupportedService.Zooomr ) ServicePointManager.Expect100Continue = false;
+#endif
+			}
+		}
+
+		/// <summary>
+		/// Internal timeout for all web requests in milliseconds. Defaults to 30 seconds.
+		/// </summary>
+		public int HttpTimeout
+		{
+			get { return _timeout; }
+			set { _timeout = value; }
+		}
+
+		/// <summary>
+		/// Checks to see if a shared secret and an api token are stored in the object.
+		/// Does not check if these values are valid values.
+		/// </summary>
+		public bool IsAuthenticated
+		{
+			get { return (_sharedSecret != null && _apiToken != null); }
+		}
+
+		/// <summary>
+		/// Returns the raw XML returned from the last response.
+		/// Only set it the response was not returned from cache.
+		/// </summary>
+		public string LastResponse
+		{
+			get { return _lastResponse; }
+		}
+
+		/// <summary>
+		/// Returns the last URL requested. Includes API signing.
+		/// </summary>
+		public string LastRequest
+		{
+			get { return _lastRequest; }
+		}
+
+		/// <summary>
+		/// You can set the <see cref="WebProxy"/> or alter its properties.
+		/// It defaults to your internet explorer proxy settings.
+		/// </summary>
+		public WebProxy Proxy { get { return _proxy; } set { _proxy = value; } }
+		#endregion
+
+		#region [ Cache Methods ]
+		/// <summary>
+		/// Clears the cache completely.
+		/// </summary>
+		public static void FlushCache()
+		{
+			Cache.FlushCache();
+		}
+
+		/// <summary>
+		/// Clears the cache for a particular URL.
+		/// </summary>
+		/// <param name="url">The URL to remove from the cache.</param>
+		/// <remarks>
+		/// The URL can either be an image URL for a downloaded picture, or
+		/// a request URL (see <see cref="LastRequest"/> for getting the last URL).
+		/// </remarks>
+		public static void FlushCache(string url)
+		{
+			Cache.FlushCache(url);
+		}
+
+		/// <summary>
+		/// Provides static access to the list of cached photos.
+		/// </summary>
+		/// <returns>An array of <see cref="PictureCacheItem"/> objects.</returns>
+		public static PictureCacheItem[] GetCachePictures()
+		{
+			return (PictureCacheItem[]) Cache.Downloads.ToArray(typeof(PictureCacheItem));
+		}
+		#endregion
+
+		#region [ Constructors ]
+
+		/// <summary>
+		/// Constructor loads configuration settings from app.config or web.config file if they exist.
+		/// </summary>
+		public Flickr()
+        {
+#if !WindowsCE
+            FlickrConfigurationSettings settings = FlickrConfigurationManager.Settings;
+			if( settings == null ) return;
+
+			if( settings.CacheSize != 0 ) CacheSizeLimit = settings.CacheSize;
+			if( settings.CacheTimeout != TimeSpan.MinValue ) CacheTimeout = settings.CacheTimeout;
+			ApiKey = settings.ApiKey;
+			AuthToken = settings.ApiToken;
+			ApiSecret = settings.SharedSecret;
+
+            if (settings.IsProxyDefined)
+			{
+				Proxy = new WebProxy();
+				Proxy.Address = new Uri("http://"; + settings.ProxyIPAddress + ":" + settings.ProxyPort);
+				if( settings.ProxyUsername != null && settings.ProxyUsername.Length > 0 )
+				{
+					NetworkCredential creds = new NetworkCredential();
+					creds.UserName = settings.ProxyUsername;
+					creds.Password = settings.ProxyPassword;
+					creds.Domain = settings.ProxyDomain;
+					Proxy.Credentials = creds;
+				}
+			}
+			else
+			{
+				// try and get default IE settings
+				try
+				{
+					Proxy = WebProxy.GetDefaultProxy();
+				}
+				catch(System.Security.SecurityException)
+				{
+					// Capture SecurityException for when running in a Medium Trust environment.
+				}
+			}
+
+#endif
+
+            CurrentService = DefaultService;
+		}
+
+		/// <summary>
+		/// Create a new instance of the <see cref="Flickr"/> class with no authentication.
+		/// </summary>
+		/// <param name="apiKey">Your Flickr API Key.</param>
+		public Flickr(string apiKey) : this(apiKey, "", "")
+		{
+		}
+
+		/// <summary>
+		/// Creates a new instance of the <see cref="Flickr"/> class with an API key and a Shared Secret.
+		/// This is only useful really useful for calling the Auth functions as all other
+		/// authenticationed methods also require the API Token.
+		/// </summary>
+		/// <param name="apiKey">Your Flickr API Key.</param>
+		/// <param name="sharedSecret">Your Flickr Shared Secret.</param>
+		public Flickr(string apiKey, string sharedSecret) : this(apiKey, sharedSecret, "")
+		{
+		}
+
+		/// <summary>
+		/// Create a new instance of the <see cref="Flickr"/> class with the email address and password given
+		/// </summary>
+		/// <param name="apiKey">Your Flickr API Key</param>
+		/// <param name="sharedSecret">Your FLickr Shared Secret.</param>
+		/// <param name="token">The token for the user who has been authenticated.</param>
+		public Flickr(string apiKey, string sharedSecret, string token) : this()
+		{
+			_apiKey = apiKey;
+			_sharedSecret = sharedSecret;
+			_apiToken = token;
+		}
+		#endregion
+
+		#region [ Private Methods ]
+		/// <summary>
+		/// A private method which performs the actual HTTP web request if
+		/// the details are not found within the cache.
+		/// </summary>
+		/// <param name="url">The URL to download.</param>
+		/// <param name="variables">The query string parameters to be added to the end of the URL.</param>
+		/// <returns>A <see cref="FlickrNet.Response"/> object.</returns>
+		/// <remarks>If the final length of the URL would be greater than 2000 characters
+		/// then they are sent as part of the body instead.</remarks>
+		private string DoGetResponse(string url, string variables)
+		{
+			HttpWebRequest req = null;
+			HttpWebResponse res = null;
+
+			if( variables.Length < 2000 )
+			{
+				url += "?" + variables;
+				variables = "";
+			}
+
+			// Initialise the web request
+			req = (HttpWebRequest)HttpWebRequest.Create(url);
+			req.Method = CurrentService==SupportedService.Zooomr?"GET":"POST";
+
+            if (req.Method == "POST") req.ContentLength = variables.Length;
+
+            req.UserAgent = UserAgent;
+			if( Proxy != null ) req.Proxy = Proxy;
+			req.Timeout = HttpTimeout;
+			req.KeepAlive = false;
+            if (variables.Length > 0)
+            {
+                req.ContentType = "application/x-www-form-urlencoded";
+                StreamWriter sw = new StreamWriter(req.GetRequestStream());
+                sw.Write(variables);
+                sw.Close();
+            }
+            else
+            {
+                // This is needed in the Compact Framework
+                // See for more details: http://msdn2.microsoft.com/en-us/library/1afx2b0f.aspx
+		if (req.Method=="POST")
+	                req.GetRequestStream().Close();
+            }
+
+			try
+			{
+				// Get response from the internet
+				res = (HttpWebResponse)req.GetResponse();
+			}
+			catch(WebException ex)
+			{
+				if( ex.Status == WebExceptionStatus.ProtocolError )
+				{
+					HttpWebResponse res2 = (HttpWebResponse)ex.Response;
+					if( res2 != null )
+					{
+						throw new FlickrWebException(String.Format("HTTP Error {0}, {1}", (int)res2.StatusCode, res2.StatusDescription), ex);
+					}
+				}
+				throw new FlickrWebException(ex.Message, ex);
+			}
+
+			string responseString = string.Empty;
+
+			using (StreamReader sr = new StreamReader(res.GetResponseStream()))
+			{
+				responseString = sr.ReadToEnd();
+			}
+
+			return responseString;
+		}
+
+		/// <summary>
+		/// Download a picture (or anything else actually).
+		/// </summary>
+		/// <param name="url"></param>
+		/// <returns></returns>
+		private Stream DoDownloadPicture(string url)
+		{
+			HttpWebRequest req = null;
+			HttpWebResponse res = null;
+
+			try
+			{
+				req = (HttpWebRequest)HttpWebRequest.Create(url);
+				req.UserAgent = UserAgent;
+				if( Proxy != null ) req.Proxy = Proxy;
+				req.Timeout = HttpTimeout;
+				req.KeepAlive = false;
+				res = (HttpWebResponse)req.GetResponse();
+			}
+			catch(WebException ex)
+			{
+				if( ex.Status == WebExceptionStatus.ProtocolError )
+				{
+					HttpWebResponse res2 = (HttpWebResponse)ex.Response;
+					if( res2 != null )
+					{
+						throw new FlickrWebException(String.Format("HTTP Error while downloading photo: {0}, {1}", (int)res2.StatusCode, res2.StatusDescription), ex);
+					}
+				}
+				else if( ex.Status == WebExceptionStatus.Timeout )
+				{
+					throw new FlickrWebException("The request timed-out", ex);
+				}
+				throw new FlickrWebException("Picture download failed (" + ex.Message + ")", ex);
+			}
+
+			return res.GetResponseStream();
+		}
+		#endregion
+
+		#region [ GetResponse methods ]
+		private Response GetResponseNoCache(Hashtable parameters)
+		{
+			return GetResponse(parameters, TimeSpan.MinValue);
+		}
+
+		private Response GetResponseAlwaysCache(Hashtable parameters)
+		{
+			return GetResponse(parameters, TimeSpan.MaxValue);
+		}
+
+		private Response GetResponseCache(Hashtable parameters)
+		{
+			return GetResponse(parameters, Cache.CacheTimeout);
+		}
+
+		private Response GetResponse(Hashtable parameters, TimeSpan cacheTimeout)
+		{
+			CheckApiKey();
+
+			// Calulate URL
+			string url = BaseUrl;
+
+            StringBuilder UrlStringBuilder = new StringBuilder("", 2 * 1024);
+            StringBuilder HashStringBuilder = new StringBuilder(_sharedSecret, 2 * 1024);
+
+			parameters["api_key"] = _apiKey;
+
+			if( _apiToken != null && _apiToken.Length > 0 )
+			{
+				parameters["auth_token"] = _apiToken;
+			}
+
+			string[] keys = new string[parameters.Keys.Count];
+			parameters.Keys.CopyTo(keys, 0);
+			Array.Sort(keys);
+
+			foreach(string key in keys)
+			{
+				if( UrlStringBuilder.Length > 0 ) UrlStringBuilder.Append("&");
+                UrlStringBuilder.Append(key);
+                UrlStringBuilder.Append("=");
+                UrlStringBuilder.Append(Utils.UrlEncode(Convert.ToString(parameters[key])));
+                HashStringBuilder.Append(key);
+                HashStringBuilder.Append(parameters[key]);
+			}
+
+            if (_sharedSecret != null && _sharedSecret.Length > 0)
+            {
+                if (UrlStringBuilder.Length > BaseUrl.Length + 1)
+                {
+                    UrlStringBuilder.Append("&");
+                }
+                UrlStringBuilder.Append("api_sig=");
+                UrlStringBuilder.Append(Md5Hash(HashStringBuilder.ToString()));
+            }
+
+			string variables = UrlStringBuilder.ToString();
+			_lastRequest = url;
+			_lastResponse = string.Empty;
+
+			if( CacheDisabled )
+			{
+				string responseXml = DoGetResponse(url, variables);
+				_lastResponse = responseXml;
+				return Utils.Deserialize(responseXml);
+			}
+			else
+			{
+				ResponseCacheItem cached = (ResponseCacheItem) Cache.Responses.Get(url + "?" + variables, cacheTimeout, true);
+				if (cached != null)
+				{
+					System.Diagnostics.Debug.WriteLine("Cache hit");
+					_lastResponse = cached.Response;
+					return Utils.Deserialize(cached.Response);
+				}
+				else
+				{
+					System.Diagnostics.Debug.WriteLine("Cache miss");
+					string responseXml = DoGetResponse(url, variables);
+					_lastResponse = responseXml;
+
+					ResponseCacheItem resCache = new ResponseCacheItem();
+					resCache.Response = responseXml;
+					resCache.Url = url;
+					resCache.CreationTime = DateTime.UtcNow;
+
+					FlickrNet.Response response = Utils.Deserialize(responseXml);
+
+					if( response.Status == ResponseStatus.OK )
+					{
+						Cache.Responses.Shrink(Math.Max(0, Cache.CacheSizeLimit - responseXml.Length));
+						Cache.Responses[url] = resCache;
+					}
+
+					return response;
+				}
+			}
+		}
+
+		#endregion
+
+		#region [ DownloadPicture ]
+		/// <summary>
+		/// Downloads the picture from a internet and transfers it to a stream object.
+		/// </summary>
+		/// <param name="url">The url of the image to download.</param>
+		/// <returns>A <see cref="Stream"/> object containing the downloaded picture.</returns>
+		/// <remarks>
+		/// The method checks the download cache first to see if the picture has already
+		/// been downloaded and if so returns the cached image. Otherwise it goes to the internet for the actual
+		/// image.
+		/// </remarks>
+		public System.IO.Stream DownloadPicture(string url)
+		{
+			if( CacheDisabled )
+			{
+				return DoDownloadPicture(url);
+			}
+
+			const int BUFFER_SIZE = 1024 * 10;
+
+			PictureCacheItem cacheItem = (PictureCacheItem) Cache.Downloads[url];
+			if (cacheItem != null)
+			{
+				return  new FileStream(cacheItem.filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+			}
+
+			PictureCacheItem picCache = new PictureCacheItem();
+			picCache.filename = Path.Combine(Cache.CacheLocation,Guid.NewGuid().ToString());
+			Stream read = DoDownloadPicture(url);
+			Stream write = new FileStream(picCache.filename, FileMode.Create, FileAccess.Write, FileShare.None);
+
+			byte[] buffer = new byte[BUFFER_SIZE];
+			int bytes = 0;
+			long fileSize = 0;
+
+			while( (bytes = read.Read(buffer, 0, BUFFER_SIZE)) > 0 )
+			{
+				fileSize += bytes;
+				write.Write(buffer, 0, bytes);
+			}
+
+			read.Close();
+			write.Close();
+
+			picCache.url = url;
+			picCache.creationTime = DateTime.UtcNow;
+			picCache.fileSize = fileSize;
+
+			Cache.Downloads.Shrink(Math.Max(0, Cache.CacheSizeLimit - fileSize));
+			Cache.Downloads[url] = picCache;
+
+			return new FileStream(picCache.filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+		}
+		#endregion
+
+		#region [ Auth ]
+		/// <summary>
+		/// Retrieve a temporary FROB from the Flickr service, to be used in redirecting the
+		/// user to the Flickr web site for authentication. Only required for desktop authentication.
+		/// </summary>
+		/// <remarks>
+		/// Pass the FROB to the <see cref="AuthCalcUrl"/> method to calculate the url.
+		/// </remarks>
+		/// <example>
+		/// <code>
+		/// string frob = flickr.AuthGetFrob();
+		/// string url = flickr.AuthCalcUrl(frob, AuthLevel.Read);
+		///
+		/// // redirect the user to the url above and then wait till they have authenticated and return to the app.
+		///
+		/// Auth auth = flickr.AuthGetToken(frob);
+		///
+		/// // then store the auth.Token for later use.
+		/// string token = auth.Token;
+		/// </code>
+		/// </example>
+		/// <returns>The FROB.</returns>
+		public string AuthGetFrob()
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.auth.getFrob");
+
+			FlickrNet.Response response = GetResponseNoCache(parameters);
+			if( response.Status == ResponseStatus.OK )
+			{
+				return response.AllElements[CurrentService==SupportedService.Zooomr?1:0].InnerText;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Calculates the URL to redirect the user to Flickr web site for
+		/// authentication. Used by desktop application.
+		/// See <see cref="AuthGetFrob"/> for example code.
+		/// </summary>
+		/// <param name="frob">The FROB to be used for authentication.</param>
+		/// <param name="authLevel">The <see cref="AuthLevel"/> stating the maximum authentication level your application requires.</param>
+		/// <returns>The url to redirect the user to.</returns>
+		public string AuthCalcUrl(string frob, AuthLevel authLevel)
+		{
+			if( _sharedSecret == null ) throw new SignatureRequiredException();
+
+			string hash = _sharedSecret + "api_key" + _apiKey + "frob" + frob + "perms" + authLevel.ToString().ToLower();
+			hash = Md5Hash(hash);
+			string url = AuthUrl + "?api_key=" + _apiKey + "&perms=" + authLevel.ToString().ToLower() + "&frob=" + frob;
+			url += "&api_sig=" + hash;
+
+			return url;
+		}
+
+		/// <summary>
+		/// Calculates the URL to redirect the user to Flickr web site for
+		/// auehtntication. Used by Web applications.
+		/// See <see cref="AuthGetFrob"/> for example code.
+		/// </summary>
+		/// <remarks>
+		/// The Flickr web site provides 'tiny urls' that can be used in place
+		/// of this URL when you specify your return url in the API key page.
+		/// It is recommended that you use these instead as they do not include
+		/// your API or shared secret.
+		/// </remarks>
+		/// <param name="authLevel">The <see cref="AuthLevel"/> stating the maximum authentication level your application requires.</param>
+		/// <returns>The url to redirect the user to.</returns>
+		public string AuthCalcWebUrl(AuthLevel authLevel)
+		{
+			if( _sharedSecret == null ) throw new SignatureRequiredException();
+
+			string hash = _sharedSecret + "api_key" + _apiKey + "perms" + authLevel.ToString().ToLower();
+			hash = Md5Hash(hash);
+			string url = AuthUrl + "?api_key=" + _apiKey + "&perms=" + authLevel.ToString().ToLower();
+			url += "&api_sig=" + hash;
+
+			return url;
+		}
+
+		/// <summary>
+		/// After the user has authenticated your application on the flickr web site call this
+		/// method with the FROB (either stored from <see cref="AuthGetFrob"/> or returned in the URL
+		/// from the Flickr web site) to get the users token.
+		/// </summary>
+		/// <param name="frob">The string containing the FROB.</param>
+		/// <returns>A <see cref="Auth"/> object containing user and token details.</returns>
+		public Auth AuthGetToken(string frob)
+		{
+			if( _sharedSecret == null ) throw new SignatureRequiredException();
+
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.auth.getToken");
+			parameters.Add("frob", frob);
+
+			FlickrNet.Response response = GetResponseNoCache(parameters);
+			if( response.Status == ResponseStatus.OK )
+			{
+				Auth auth = new Auth(response.AllElements[CurrentService==SupportedService.Zooomr?1:0]);
+				return auth;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Gets the full token details for a given mini token, entered by the user following a
+		/// web based authentication.
+		/// </summary>
+		/// <param name="miniToken">The mini token.</param>
+		/// <returns>An instance <see cref="Auth"/> class, detailing the user and their full token.</returns>
+		public Auth AuthGetFullToken(string miniToken)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.auth.getFullToken");
+			parameters.Add("mini_token", miniToken.Replace("-", ""));
+			FlickrNet.Response response = GetResponseNoCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				Auth auth = new Auth(response.AllElements[0]);
+				return auth;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Checks a authentication token with the flickr service to make
+		/// sure it is still valid.
+		/// </summary>
+		/// <param name="token">The authentication token to check.</param>
+		/// <returns>The <see cref="Auth"/> object detailing the user for the token.</returns>
+		public Auth AuthCheckToken(string token)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.auth.checkToken");
+			parameters.Add("auth_token", token);
+
+			FlickrNet.Response response = GetResponseNoCache(parameters);
+			if( response.Status == ResponseStatus.OK )
+			{
+				Auth auth = new Auth(response.AllElements[CurrentService==SupportedService.Zooomr?1:0]);
+				return auth;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+
+		}
+		#endregion
+
+		#region [ Activity ]
+		/// <summary>
+		/// Returns a list of recent activity on photos belonging to the calling user.
+		/// </summary>
+		/// <remarks>
+		/// <b>Do not poll this method more than once an hour.</b>
+		/// </remarks>
+		/// <returns>An array of <see cref="ActivityItem"/> instances.</returns>
+		public ActivityItem[] ActivityUserPhotos()
+		{
+			return ActivityUserPhotos(null);
+		}
+
+		/// <summary>
+		/// Returns a list of recent activity on photos belonging to the calling user.
+		/// </summary>
+		/// <remarks>
+		/// <b>Do not poll this method more than once an hour.</b>
+		/// </remarks>
+		/// <param name="timePeriod">The number of days or hours you want to get activity for.</param>
+		/// <param name="timeType">'d' for days, 'h' for hours.</param>
+		/// <returns>An array of <see cref="ActivityItem"/> instances.</returns>
+		public ActivityItem[] ActivityUserPhotos(int timePeriod, string timeType)
+		{
+			if( timePeriod == 0 )
+				throw new ArgumentOutOfRangeException("timePeriod", "Time Period should be greater than 0");
+
+			if( timeType == null )
+				throw new ArgumentNullException("timeType");
+
+			if( timeType != "d" && timeType != "h" )
+				throw new ArgumentOutOfRangeException("timeType", "Time type must be 'd' or 'h'");
+
+			return ActivityUserPhotos(timePeriod + timeType);
+		}
+
+		private ActivityItem[] ActivityUserPhotos(string timeframe)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.activity.userPhotos");
+			if( timeframe != null && timeframe.Length > 0 ) parameters.Add("timeframe", timeframe);
+
+			FlickrNet.Response response = GetResponseCache(parameters);
+			if( response.Status == ResponseStatus.OK )
+			{
+				XmlNodeList list = response.AllElements[0].SelectNodes("item");
+				ActivityItem[] items = new ActivityItem[list.Count];
+				for(int i = 0; i < items.Length; i++)
+				{
+					items[i] = new ActivityItem(list[i]);
+				}
+				return items;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Returns a list of recent activity on photos commented on by the calling user.
+		/// </summary>
+		/// <remarks>
+		/// <b>Do not poll this method more than once an hour.</b>
+		/// </remarks>
+		/// <returns></returns>
+		public ActivityItem[] ActivityUserComments(int page, int perPage)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.activity.userComments");
+			if( page > 0 ) parameters.Add("page", page);
+			if( perPage > 0 ) parameters.Add("per_page", perPage);
+
+			FlickrNet.Response response = GetResponseCache(parameters);
+			if( response.Status == ResponseStatus.OK )
+			{
+				XmlNodeList list = response.AllElements[0].SelectNodes("item");
+				ActivityItem[] items = new ActivityItem[list.Count];
+				for(int i = 0; i < items.Length; i++)
+				{
+					items[i] = new ActivityItem(list[i]);
+				}
+				return items;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+		#endregion
+
+		#region [ UploadPicture ]
+		/// <summary>
+		/// Uploads a file to Flickr.
+		/// </summary>
+		/// <param name="filename">The filename of the file to open.</param>
+		/// <returns>The id of the photo on a successful upload.</returns>
+		/// <exception cref="FlickrException">Thrown when Flickr returns an error. see http://www.flickr.com/services/api/upload.api.html for more details.</exception>
+		/// <remarks>Other exceptions may be thrown, see <see cref="FileStream"/> constructors for more details.</remarks>
+		public string UploadPicture(string filename)
+		{
+			return UploadPicture(filename, null, null, null, true, false, false);
+		}
+
+		/// <summary>
+		/// Uploads a file to Flickr.
+		/// </summary>
+		/// <param name="filename">The filename of the file to open.</param>
+		/// <param name="title">The title of the photograph.</param>
+		/// <returns>The id of the photo on a successful upload.</returns>
+		/// <exception cref="FlickrException">Thrown when Flickr returns an error. see http://www.flickr.com/services/api/upload.api.html for more details.</exception>
+		/// <remarks>Other exceptions may be thrown, see <see cref="FileStream"/> constructors for more details.</remarks>
+		public string UploadPicture(string filename, string title)
+		{
+			return UploadPicture(filename, title, null, null, true, false, false);
+		}
+
+		/// <summary>
+		/// Uploads a file to Flickr.
+		/// </summary>
+		/// <param name="filename">The filename of the file to open.</param>
+		/// <param name="title">The title of the photograph.</param>
+		/// <param name="description">The description of the photograph.</param>
+		/// <returns>The id of the photo on a successful upload.</returns>
+		/// <exception cref="FlickrException">Thrown when Flickr returns an error. see http://www.flickr.com/services/api/upload.api.html for more details.</exception>
+		/// <remarks>Other exceptions may be thrown, see <see cref="FileStream"/> constructors for more details.</remarks>
+		public string UploadPicture(string filename, string title, string description)
+		{
+			return UploadPicture(filename, title, description, null, true, false, false);
+		}
+
+		/// <summary>
+		/// Uploads a file to Flickr.
+		/// </summary>
+		/// <param name="filename">The filename of the file to open.</param>
+		/// <param name="title">The title of the photograph.</param>
+		/// <param name="description">The description of the photograph.</param>
+		/// <param name="tags">A comma seperated list of the tags to assign to the photograph.</param>
+		/// <returns>The id of the photo on a successful upload.</returns>
+		/// <exception cref="FlickrException">Thrown when Flickr returns an error. see http://www.flickr.com/services/api/upload.api.html for more details.</exception>
+		/// <remarks>Other exceptions may be thrown, see <see cref="FileStream"/> constructors for more details.</remarks>
+		public string UploadPicture(string filename, string title, string description, string tags)
+		{
+			Stream stream = new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+			return UploadPicture(stream, title, description, tags, -1, -1, -1, ContentType.None, SafetyLevel.None, HiddenFromSearch.None);
+		}
+
+		/// <summary>
+		/// Uploads a file to Flickr.
+		/// </summary>
+		/// <param name="filename">The filename of the file to open.</param>
+		/// <param name="title">The title of the photograph.</param>
+		/// <param name="description">The description of the photograph.</param>
+		/// <param name="tags">A comma seperated list of the tags to assign to the photograph.</param>
+		/// <param name="isPublic">True if the photograph should be public and false if it should be private.</param>
+		/// <param name="isFriend">True if the photograph should be marked as viewable by friends contacts.</param>
+		/// <param name="isFamily">True if the photograph should be marked as viewable by family contacts.</param>
+		/// <returns>The id of the photo on a successful upload.</returns>
+		/// <exception cref="FlickrException">Thrown when Flickr returns an error. see http://www.flickr.com/services/api/upload.api.html for more details.</exception>
+		/// <remarks>Other exceptions may be thrown, see <see cref="FileStream"/> constructors for more details.</remarks>
+		public string UploadPicture(string filename, string title, string description, string tags, bool isPublic, bool isFamily, bool isFriend)
+		{
+			Stream stream = new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
+			return UploadPicture(stream, title, description, tags, isPublic?1:0, isFamily?1:0, isFriend?1:0, ContentType.None, SafetyLevel.None, HiddenFromSearch.None);
+		}
+
+		/// <summary>
+		/// UploadPicture method that does all the uploading work.
+		/// </summary>
+		/// <param name="stream">The <see cref="Stream"/> object containing the pphoto to be uploaded.</param>
+		/// <param name="title">The title of the photo (optional).</param>
+		/// <param name="description">The description of the photograph (optional).</param>
+		/// <param name="tags">The tags for the photograph (optional).</param>
+		/// <param name="isPublic">0 for private, 1 for public.</param>
+		/// <param name="isFamily">1 if family, 0 is not.</param>
+		/// <param name="isFriend">1 if friend, 0 if not.</param>
+		/// <param name="contentType">The content type of the photo, i.e. Photo, Screenshot or Other.</param>
+		/// <param name="safetyLevel">The safety level of the photo, i.e. Safe, Moderate or Restricted.</param>
+		/// <param name="hiddenFromSearch">Is the photo hidden from public searches.</param>
+		/// <returns>The id of the photograph after successful uploading.</returns>
+		public string UploadPicture(Stream stream, string title, string description, string tags, int isPublic, int isFamily, int isFriend, ContentType contentType, SafetyLevel safetyLevel, HiddenFromSearch hiddenFromSearch)
+		{
+			/*
+			 *
+			 * Modified UploadPicture code taken from the Flickr.Net library
+			 * URL: http://workspaces.gotdotnet.com/flickrdotnet
+			 * It is used under the terms of the Common Public License 1.0
+			 * URL: http://www.opensource.org/licenses/cpl.php
+			 *
+			 * */
+
+			string boundary = "FLICKR_MIME_" + DateTime.Now.ToString("yyyyMMddhhmmss");
+
+			HttpWebRequest req = (HttpWebRequest)HttpWebRequest.Create(UploadUrl);
+			req.UserAgent = "Mozilla/4.0 FlickrNet API (compatible; MSIE 6.0; Windows NT 5.1)";
+			req.Method = "POST";
+			if( Proxy != null ) req.Proxy = Proxy;
+			//req.Referer = "http://www.flickr.com";;
+			req.KeepAlive = true;
+			req.Timeout = HttpTimeout * 1000;
+			req.ContentType = "multipart/form-data; boundary=" + boundary + "";
+			req.Expect = "";
+
+			StringBuilder sb = new StringBuilder();
+
+			Hashtable parameters = new Hashtable();
+
+			if( title != null && title.Length > 0 )
+			{
+				parameters.Add("title", title);
+			}
+			if( description != null && description.Length > 0 )
+			{
+				parameters.Add("description", description);
+			}
+			if( tags != null && tags.Length > 0 )
+			{
+				parameters.Add("tags", tags);
+			}
+			if( isPublic >= 0 )
+			{
+				parameters.Add("is_public", isPublic.ToString());
+			}
+			if( isFriend >= 0 )
+			{
+				parameters.Add("is_friend", isFriend.ToString());
+			}
+			if( isFamily >= 0 )
+			{
+				parameters.Add("is_family", isFamily.ToString());
+			}
+			if( safetyLevel != SafetyLevel.None )
+			{
+				parameters.Add("safety_level", (int)safetyLevel);
+			}
+			if( contentType != ContentType.None )
+			{
+				parameters.Add("content_type", (int)contentType);
+			}
+			if( hiddenFromSearch != HiddenFromSearch.None )
+			{
+				parameters.Add("hidden", (int)hiddenFromSearch);
+			}
+
+			parameters.Add("api_key", _apiKey);
+			parameters.Add("auth_token", _apiToken);
+
+			string[] keys = new string[parameters.Keys.Count];
+			parameters.Keys.CopyTo(keys, 0);
+			Array.Sort(keys);
+
+			StringBuilder HashStringBuilder = new StringBuilder(_sharedSecret, 2 * 1024);
+
+			foreach(string key in keys)
+			{
+                HashStringBuilder.Append(key);
+                HashStringBuilder.Append(parameters[key]);
+				sb.Append("--" + boundary + "\r\n");
+				sb.Append("Content-Disposition: form-data; name=\"" + key + "\"\r\n");
+				sb.Append("\r\n");
+				sb.Append(parameters[key] + "\r\n");
+			}
+
+			sb.Append("--" + boundary + "\r\n");
+			sb.Append("Content-Disposition: form-data; name=\"api_sig\"\r\n");
+			sb.Append("\r\n");
+            sb.Append(Md5Hash(HashStringBuilder.ToString()) + "\r\n");
+
+			// Photo
+			sb.Append("--" + boundary + "\r\n");
+			sb.Append("Content-Disposition: form-data; name=\"photo\"; filename=\"image.jpeg\"\r\n");
+			sb.Append("Content-Type: image/jpeg\r\n");
+			sb.Append("\r\n");
+
+			UTF8Encoding encoding = new UTF8Encoding();
+
+			byte[] postContents = encoding.GetBytes(sb.ToString());
+
+			byte[] photoContents = new byte[stream.Length];
+			stream.Read(photoContents, 0, photoContents.Length);
+			stream.Close();
+
+			byte[] postFooter = encoding.GetBytes("\r\n--" + boundary + "--\r\n");
+
+			byte[] dataBuffer = new byte[postContents.Length + photoContents.Length + postFooter.Length];
+			Buffer.BlockCopy(postContents, 0, dataBuffer, 0, postContents.Length);
+			Buffer.BlockCopy(photoContents, 0, dataBuffer, postContents.Length, photoContents.Length);
+			Buffer.BlockCopy(postFooter, 0, dataBuffer, postContents.Length + photoContents.Length, postFooter.Length);
+
+			req.ContentLength = dataBuffer.Length;
+
+			Stream resStream = req.GetRequestStream();
+
+			int j = 1;
+			int uploadBit = Math.Max(dataBuffer.Length / 100, 50*1024);
+			int uploadSoFar = 0;
+
+			for(int i = 0; i < dataBuffer.Length; i=i+uploadBit)
+			{
+				int toUpload = Math.Min(uploadBit, dataBuffer.Length - i);
+				uploadSoFar += toUpload;
+
+				resStream.Write(dataBuffer, i, toUpload);
+
+				if( (OnUploadProgress != null) && ((j++) % 5 == 0 || uploadSoFar == dataBuffer.Length) )
+				{
+					OnUploadProgress(this, new UploadProgressEventArgs(i+toUpload, uploadSoFar == dataBuffer.Length));
+				}
+			}
+			resStream.Close();
+
+			HttpWebResponse res = (HttpWebResponse)req.GetResponse();
+
+			XmlSerializer serializer = _uploaderSerializer;
+
+			StreamReader sr = new StreamReader(res.GetResponseStream());
+			string s= sr.ReadToEnd();
+			sr.Close();
+
+			StringReader str = new StringReader(s);
+
+			FlickrNet.Uploader uploader = (FlickrNet.Uploader)serializer.Deserialize(str);
+
+			if( uploader.Status == ResponseStatus.OK )
+			{
+				return uploader.PhotoId;
+			}
+			else
+			{
+				throw new FlickrApiException(uploader.Error);
+			}
+		}
+
+		/// <summary>
+		/// Replace an existing photo on Flickr.
+		/// </summary>
+		/// <param name="filename">The filename of the photo to upload.</param>
+		/// <param name="photoId">The ID of the photo to replace.</param>
+		/// <returns>The id of the photograph after successful uploading.</returns>
+		public string ReplacePicture(string filename, string photoId)
+		{
+			FileStream stream = null;
+			try
+			{
+				stream = new FileStream(filename, FileMode.Open, FileAccess.Read, FileShare.Read);
+				return ReplacePicture(stream, photoId);
+			}
+			finally
+			{
+				if( stream != null ) stream.Close();
+			}
+
+		}
+
+		/// <summary>
+		/// Replace an existing photo on Flickr.
+		/// </summary>
+		/// <param name="stream">The <see cref="Stream"/> object containing the photo to be uploaded.</param>
+		/// <param name="photoId">The ID of the photo to replace.</param>
+		/// <returns>The id of the photograph after successful uploading.</returns>
+		public string ReplacePicture(Stream stream, string photoId)
+		{
+			string boundary = "FLICKR_MIME_" + DateTime.Now.ToString("yyyyMMddhhmmss");
+
+			HttpWebRequest req = (HttpWebRequest)HttpWebRequest.Create(ReplaceUrl);
+			req.UserAgent = "Mozilla/4.0 FlickrNet API (compatible; MSIE 6.0; Windows NT 5.1)";
+			req.Method = "POST";
+			if( Proxy != null ) req.Proxy = Proxy;
+			req.Referer = "http://www.flickr.com";;
+			req.KeepAlive = false;
+			req.Timeout = HttpTimeout * 100;
+			req.ContentType = "multipart/form-data; boundary=" + boundary + "";
+
+			StringBuilder sb = new StringBuilder();
+
+			Hashtable parameters = new Hashtable();
+
+			parameters.Add("photo_id", photoId);
+			parameters.Add("api_key", _apiKey);
+			parameters.Add("auth_token", _apiToken);
+
+			string[] keys = new string[parameters.Keys.Count];
+			parameters.Keys.CopyTo(keys, 0);
+			Array.Sort(keys);
+
+			StringBuilder HashStringBuilder = new StringBuilder(_sharedSecret, 2 * 1024);
+
+			foreach(string key in keys)
+			{
+				HashStringBuilder.Append(key);
+				HashStringBuilder.Append(parameters[key]);
+				sb.Append("--" + boundary + "\r\n");
+				sb.Append("Content-Disposition: form-data; name=\"" + key + "\"\r\n");
+				sb.Append("\r\n");
+				sb.Append(parameters[key] + "\r\n");
+			}
+
+			sb.Append("--" + boundary + "\r\n");
+			sb.Append("Content-Disposition: form-data; name=\"api_sig\"\r\n");
+			sb.Append("\r\n");
+			sb.Append(Md5Hash(HashStringBuilder.ToString()) + "\r\n");
+
+			// Photo
+			sb.Append("--" + boundary + "\r\n");
+			sb.Append("Content-Disposition: form-data; name=\"photo\"; filename=\"image.jpeg\"\r\n");
+			sb.Append("Content-Type: image/jpeg\r\n");
+			sb.Append("\r\n");
+
+			UTF8Encoding encoding = new UTF8Encoding();
+
+			byte[] postContents = encoding.GetBytes(sb.ToString());
+
+			byte[] photoContents = new byte[stream.Length];
+			stream.Read(photoContents, 0, photoContents.Length);
+			stream.Close();
+
+			byte[] postFooter = encoding.GetBytes("\r\n--" + boundary + "--\r\n");
+
+			byte[] dataBuffer = new byte[postContents.Length + photoContents.Length + postFooter.Length];
+			Buffer.BlockCopy(postContents, 0, dataBuffer, 0, postContents.Length);
+			Buffer.BlockCopy(photoContents, 0, dataBuffer, postContents.Length, photoContents.Length);
+			Buffer.BlockCopy(postFooter, 0, dataBuffer, postContents.Length + photoContents.Length, postFooter.Length);
+
+			req.ContentLength = dataBuffer.Length;
+
+			Stream resStream = req.GetRequestStream();
+
+			int j = 1;
+			int uploadBit = Math.Max(dataBuffer.Length / 100, 50*1024);
+			int uploadSoFar = 0;
+
+			for(int i = 0; i < dataBuffer.Length; i=i+uploadBit)
+			{
+				int toUpload = Math.Min(uploadBit, dataBuffer.Length - i);
+				uploadSoFar += toUpload;
+
+				resStream.Write(dataBuffer, i, toUpload);
+
+				if( (OnUploadProgress != null) && ((j++) % 5 == 0 || uploadSoFar == dataBuffer.Length) )
+				{
+					OnUploadProgress(this, new UploadProgressEventArgs(i+toUpload, uploadSoFar == dataBuffer.Length));
+				}
+			}
+			resStream.Close();
+
+			HttpWebResponse res = (HttpWebResponse)req.GetResponse();
+
+			XmlSerializer serializer = _uploaderSerializer;
+
+			StreamReader sr = new StreamReader(res.GetResponseStream());
+			string s= sr.ReadToEnd();
+			sr.Close();
+
+			StringReader str = new StringReader(s);
+
+			FlickrNet.Uploader uploader = (FlickrNet.Uploader)serializer.Deserialize(str);
+
+			if( uploader.Status == ResponseStatus.OK )
+			{
+				return uploader.PhotoId;
+			}
+			else
+			{
+				throw new FlickrApiException(uploader.Error);
+			}
+		}
+		#endregion
+
+		#region [ Blogs ]
+		/// <summary>
+		/// Gets a list of blogs that have been set up by the user.
+		/// Requires authentication.
+		/// </summary>
+		/// <returns>A <see cref="Blogs"/> object containing the list of blogs.</returns>
+		/// <remarks></remarks>
+		public Blogs BlogGetList()
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.blogs.getList");
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return response.Blogs;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Posts a photo already uploaded to a blog.
+		/// Requires authentication.
+		/// </summary>
+		/// <param name="blogId">The Id of the blog to post the photo too.</param>
+		/// <param name="photoId">The Id of the photograph to post.</param>
+		/// <param name="title">The title of the blog post.</param>
+		/// <param name="description">The body of the blog post.</param>
+		/// <returns>True if the operation is successful.</returns>
+		public bool BlogPostPhoto(string blogId, string photoId, string title, string description)
+		{
+			return BlogPostPhoto(blogId, photoId, title, description, null);
+		}
+
+		/// <summary>
+		/// Posts a photo already uploaded to a blog.
+		/// Requires authentication.
+		/// </summary>
+		/// <param name="blogId">The Id of the blog to post the photo too.</param>
+		/// <param name="photoId">The Id of the photograph to post.</param>
+		/// <param name="title">The title of the blog post.</param>
+		/// <param name="description">The body of the blog post.</param>
+		/// <param name="blogPassword">The password of the blog if it is not already stored in flickr.</param>
+		/// <returns>True if the operation is successful.</returns>
+		public bool BlogPostPhoto(string blogId, string photoId, string title, string description, string blogPassword)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.blogs.postPhoto");
+			parameters.Add("blog_id", blogId);
+			parameters.Add("photo_id", photoId);
+			parameters.Add("title", title);
+			parameters.Add("description", description);
+			if( blogPassword != null ) parameters.Add("blog_password", blogPassword);
+
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return true;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+		#endregion
+
+		#region [ Contacts ]
+		/// <summary>
+		/// Gets a list of contacts for the logged in user.
+		/// Requires authentication.
+		/// </summary>
+		/// <returns>An instance of the <see cref="Contacts"/> class containing the list of contacts.</returns>
+		public Contacts ContactsGetList()
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.contacts.getList");
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return response.Contacts;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Gets a list of the given users contact, or those that are publically avaiable.
+		/// </summary>
+		/// <param name="userId">The Id of the user who's contacts you want to return.</param>
+		/// <returns>An instance of the <see cref="Contacts"/> class containing the list of contacts.</returns>
+		public Contacts ContactsGetPublicList(string userId)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.contacts.getPublicList");
+			parameters.Add("user_id", userId);
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return response.Contacts;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+		#endregion
+
+		#region [ Favorites ]
+		/// <summary>
+		/// Adds a photo to the logged in favourites.
+		/// Requires authentication.
+		/// </summary>
+		/// <param name="photoId">The id of the photograph to add.</param>
+		/// <returns>True if the operation is successful.</returns>
+		public bool FavoritesAdd(string photoId)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.favorites.add");
+			parameters.Add("photo_id", photoId);
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return true;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Removes a photograph from the logged in users favourites.
+		/// Requires authentication.
+		/// </summary>
+		/// <param name="photoId">The id of the photograph to remove.</param>
+		/// <returns>True if the operation is successful.</returns>
+		public bool FavoritesRemove(string photoId)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.favorites.remove");
+			parameters.Add("photo_id", photoId);
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return true;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Get a list of the currently logger in users favourites.
+		/// Requires authentication.
+		/// </summary>
+		/// <returns><see cref="Photos"/> instance containing a collection of <see cref="Photo"/> objects.</returns>
+		public Photos FavoritesGetList()
+		{
+			return FavoritesGetList(null, 0, 0);
+		}
+
+		/// <summary>
+		/// Get a list of the currently logger in users favourites.
+		/// Requires authentication.
+		/// </summary>
+		/// <param name="perPage">Number of photos to include per page.</param>
+		/// <param name="page">The page to download this time.</param>
+		/// <returns><see cref="Photos"/> instance containing a collection of <see cref="Photo"/> objects.</returns>
+		public Photos FavoritesGetList(int perPage, int page)
+		{
+			return FavoritesGetList(null, perPage, page);
+		}
+
+		/// <summary>
+		/// Get a list of favourites for the specified user.
+		/// </summary>
+		/// <param name="userId">The user id of the user whose favourites you wish to retrieve.</param>
+		/// <returns><see cref="Photos"/> instance containing a collection of <see cref="Photo"/> objects.</returns>
+		public Photos FavoritesGetList(string userId)
+		{
+			return FavoritesGetList(userId, 0, 0);
+		}
+
+		/// <summary>
+		/// Get a list of favourites for the specified user.
+		/// </summary>
+		/// <param name="userId">The user id of the user whose favourites you wish to retrieve.</param>
+		/// <param name="perPage">Number of photos to include per page.</param>
+		/// <param name="page">The page to download this time.</param>
+		/// <returns><see cref="Photos"/> instance containing a collection of <see cref="Photo"/> objects.</returns>
+		public Photos FavoritesGetList(string userId, int perPage, int page)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.favorites.getList");
+			if( userId != null ) parameters.Add("user_id", userId);
+			if( perPage > 0 ) parameters.Add("per_page", perPage.ToString());
+			if( page > 0 ) parameters.Add("page", page.ToString());
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return response.Photos;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Gets the public favourites for a specified user.
+		/// </summary>
+		/// <remarks>This function difers from <see cref="Flickr.FavoritesGetList(string)"/> in that the user id
+		/// is not optional.</remarks>
+		/// <param name="userId">The is of the user whose favourites you wish to return.</param>
+		/// <returns>A <see cref="Photos"/> object containing a collection of <see cref="Photo"/> objects.</returns>
+		public Photos FavoritesGetPublicList(string userId)
+		{
+			return FavoritesGetPublicList(userId, 0, 0);
+		}
+
+		/// <summary>
+		/// Gets the public favourites for a specified user.
+		/// </summary>
+		/// <remarks>This function difers from <see cref="Flickr.FavoritesGetList(string)"/> in that the user id
+		/// is not optional.</remarks>
+		/// <param name="userId">The is of the user whose favourites you wish to return.</param>
+		/// <param name="perPage">The number of photos to return per page.</param>
+		/// <param name="page">The specific page to return.</param>
+		/// <returns>A <see cref="Photos"/> object containing a collection of <see cref="Photo"/> objects.</returns>
+		public Photos FavoritesGetPublicList(string userId, int perPage, int page)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.favorites.getPublicList");
+			parameters.Add("user_id", userId);
+			if( perPage > 0 ) parameters.Add("per_page", perPage.ToString());
+			if( page > 0 ) parameters.Add("page", page.ToString());
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return response.Photos;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+		#endregion
+
+		#region [ Groups ]
+		/// <summary>
+		/// Returns the top <see cref="Category"/> with a list of sub-categories and groups.
+		/// (The top category does not have any groups in it but others may).
+		/// </summary>
+		/// <returns>A <see cref="Category"/> instance.</returns>
+		public Category GroupsBrowse()
+		{
+			return GroupsBrowse("0");
+		}
+
+		/// <summary>
+		/// Returns the <see cref="Category"/> specified by the category id with a list of sub-categories and groups.
+		/// </summary>
+		/// <param name="catId"></param>
+		/// <returns>A <see cref="Category"/> instance.</returns>
+		public Category GroupsBrowse(string catId)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.groups.browse");
+			parameters.Add("cat_id", catId);
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return response.Category;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Search the list of groups on Flickr for the text.
+		/// </summary>
+		/// <param name="text">The text to search for.</param>
+		/// <returns>A list of groups matching the search criteria.</returns>
+		public GroupSearchResults GroupsSearch(string text)
+		{
+			return GroupsSearch(text, 0, 0);
+		}
+
+		/// <summary>
+		/// Search the list of groups on Flickr for the text.
+		/// </summary>
+		/// <param name="text">The text to search for.</param>
+		/// <param name="page">The page of the results to return.</param>
+		/// <returns>A list of groups matching the search criteria.</returns>
+		public GroupSearchResults GroupsSearch(string text, int page)
+		{
+			return GroupsSearch(text, page, 0);
+		}
+
+		/// <summary>
+		/// Search the list of groups on Flickr for the text.
+		/// </summary>
+		/// <param name="text">The text to search for.</param>
+		/// <param name="page">The page of the results to return.</param>
+		/// <param name="perPage">The number of groups to list per page.</param>
+		/// <returns>A list of groups matching the search criteria.</returns>
+		public GroupSearchResults GroupsSearch(string text, int page, int perPage)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.groups.search");
+			parameters.Add("api_key", _apiKey);
+			parameters.Add("text", text);
+			if( page > 0 ) parameters.Add("page", page.ToString());
+			if( perPage > 0 ) parameters.Add("per_page", perPage.ToString());
+
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return new GroupSearchResults(response.AllElements[0]);
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Returns a <see cref="GroupFullInfo"/> object containing details about a group.
+		/// </summary>
+		/// <param name="groupId">The id of the group to return.</param>
+		/// <returns>The <see cref="GroupFullInfo"/> specified by the group id.</returns>
+		public GroupFullInfo GroupsGetInfo(string groupId)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.groups.getInfo");
+			parameters.Add("api_key", _apiKey);
+			parameters.Add("group_id", groupId);
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return new GroupFullInfo(response.AllElements[0]);
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+		#endregion
+
+		#region [ Group Pool ]
+		/// <summary>
+		/// Adds a photo to a pool you have permission to add photos to.
+		/// </summary>
+		/// <param name="photoId">The id of one of your photos to be added.</param>
+		/// <param name="groupId">The id of a group you are a member of.</param>
+		/// <returns>True on a successful addition.</returns>
+		public bool GroupPoolAdd(string photoId, string groupId)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.groups.pools.add");
+			parameters.Add("photo_id", photoId);
+			parameters.Add("group_id", groupId);
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return true;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Gets the context for a photo from within a group. This provides the
+		/// id and thumbnail url for the next and previous photos in the group.
+		/// </summary>
+		/// <param name="photoId">The Photo ID for the photo you want the context for.</param>
+		/// <param name="groupId">The group ID for the group you want the context to be relevant to.</param>
+		/// <returns>The <see cref="Context"/> of the photo in the group.</returns>
+		public Context GroupPoolGetContext(string photoId, string groupId)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.groups.pools.getContext");
+			parameters.Add("photo_id", photoId);
+			parameters.Add("group_id", groupId);
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				Context context = new Context();
+				context.Count = response.ContextCount.Count;
+				context.NextPhoto = response.ContextNextPhoto;
+				context.PreviousPhoto = response.ContextPrevPhoto;
+				return context;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Remove a picture from a group.
+		/// </summary>
+		/// <param name="photoId">The id of one of your pictures you wish to remove.</param>
+		/// <param name="groupId">The id of the group to remove the picture from.</param>
+		/// <returns>True if the photo is successfully removed.</returns>
+		public bool GroupPoolRemove(string photoId, string groupId)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.groups.pools.remove");
+			parameters.Add("photo_id", photoId);
+			parameters.Add("group_id", groupId);
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return true;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Gets a list of
+		/// </summary>
+		/// <returns></returns>
+		public MemberGroupInfo[] GroupPoolGetGroups()
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.groups.pools.getGroups");
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return MemberGroupInfo.GetMemberGroupInfo(response.AllElements[0]);
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Gets a list of photos for a given group.
+		/// </summary>
+		/// <param name="groupId">The group ID for the group.</param>
+		/// <returns>A <see cref="Photos"/> object containing the list of photos.</returns>
+		public Photos GroupPoolGetPhotos(string groupId)
+		{
+			return GroupPoolGetPhotos(groupId, null, null, PhotoSearchExtras.All, 0, 0);
+		}
+
+		/// <summary>
+		/// Gets a list of photos for a given group.
+		/// </summary>
+		/// <param name="groupId">The group ID for the group.</param>
+		/// <param name="tags">Space seperated list of tags that photos returned must have.</param>
+		/// <returns>A <see cref="Photos"/> object containing the list of photos.</returns>
+		public Photos GroupPoolGetPhotos(string groupId, string tags)
+		{
+			return GroupPoolGetPhotos(groupId, tags, null, PhotoSearchExtras.All, 0, 0);
+		}
+
+		/// <summary>
+		/// Gets a list of photos for a given group.
+		/// </summary>
+		/// <param name="groupId">The group ID for the group.</param>
+		/// <param name="perPage">The number of photos per page.</param>
+		/// <param name="page">The page to return.</param>
+		/// <returns>A <see cref="Photos"/> object containing the list of photos.</returns>
+		public Photos GroupPoolGetPhotos(string groupId, int perPage, int page)
+		{
+			return GroupPoolGetPhotos(groupId, null, null, PhotoSearchExtras.All, perPage, page);
+		}
+
+		/// <summary>
+		/// Gets a list of photos for a given group.
+		/// </summary>
+		/// <param name="groupId">The group ID for the group.</param>
+		/// <param name="tags">Space seperated list of tags that photos returned must have.</param>
+		/// <param name="perPage">The number of photos per page.</param>
+		/// <param name="page">The page to return.</param>
+		/// <returns>A <see cref="Photos"/> object containing the list of photos.</returns>
+		public Photos GroupPoolGetPhotos(string groupId, string tags, int perPage, int page)
+		{
+			return GroupPoolGetPhotos(groupId, tags, null, PhotoSearchExtras.All, perPage, page);
+		}
+
+		/// <summary>
+		/// Gets a list of photos for a given group.
+		/// </summary>
+		/// <param name="groupId">The group ID for the group.</param>
+		/// <param name="tags">Space seperated list of tags that photos returned must have.
+		/// Currently only supports 1 tag at a time.</param>
+		/// <param name="userId">The group member to return photos for.</param>
+		/// <param name="extras">The <see cref="PhotoSearchExtras"/> specifying which extras to return. All other overloads default to returning all extras.</param>
+		/// <param name="perPage">The number of photos per page.</param>
+		/// <param name="page">The page to return.</param>
+		/// <returns>A <see cref="Photos"/> object containing the list of photos.</returns>
+		public Photos GroupPoolGetPhotos(string groupId, string tags, string userId, PhotoSearchExtras extras, int perPage, int page)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.groups.pools.getPhotos");
+			parameters.Add("group_id", groupId);
+			if( tags != null && tags.Length > 0 )parameters.Add("tags", tags);
+			if( perPage > 0 ) parameters.Add("per_page", perPage.ToString());
+			if( page > 0 ) parameters.Add("page", page.ToString());
+			if( userId != null && userId.Length > 0 ) parameters.Add("user_id", userId);
+			if( extras != PhotoSearchExtras.None ) parameters.Add("extras", Utils.ExtrasToString(extras));
+
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return response.Photos;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+		#endregion
+
+		#region [ Interestingness ]
+		/// <summary>
+		/// Gets a list of photos from the most recent interstingness list.
+		/// </summary>
+		/// <param name="perPage">Number of photos per page.</param>
+		/// <param name="page">The page number to return.</param>
+		/// <param name="extras"><see cref="PhotoSearchExtras"/> enumeration.</param>
+		/// <returns><see cref="Photos"/> instance containing list of photos.</returns>
+		public Photos InterestingnessGetList(PhotoSearchExtras extras, int perPage, int page)
+		{
+			return InterestingnessGetList(DateTime.MinValue, extras, perPage, page);
+		}
+
+		/// <summary>
+		/// Gets a list of photos from the interstingness list for the specified date.
+		/// </summary>
+		/// <param name="date">The date to return the interestingness list for.</param>
+		/// <returns><see cref="Photos"/> instance containing list of photos.</returns>
+		public Photos InterestingnessGetList(DateTime date)
+		{
+			return InterestingnessGetList(date, PhotoSearchExtras.All, 0, 0);
+		}
+
+		/// <summary>
+		/// Gets a list of photos from the most recent interstingness list.
+		/// </summary>
+		/// <returns><see cref="Photos"/> instance containing list of photos.</returns>
+		public Photos InterestingnessGetList()
+		{
+			return InterestingnessGetList(DateTime.MinValue, PhotoSearchExtras.All, 0, 0);
+		}
+
+		/// <summary>
+		/// Gets a list of photos from the most recent interstingness list.
+		/// </summary>
+		/// <param name="date">The date to return the interestingness photos for.</param>
+		/// <param name="extras">The extra parameters to return along with the search results.
+		/// See <see cref="PhotoSearchOptions"/> for more details.</param>
+		/// <param name="perPage">The number of results to return per page.</param>
+		/// <param name="page">The page of the results to return.</param>
+		/// <returns></returns>
+		public Photos InterestingnessGetList(DateTime date, PhotoSearchExtras extras, int perPage, int page)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.interestingness.getList");
+
+			if( date > DateTime.MinValue ) parameters.Add("date", date.ToString("yyyy-MM-dd"));
+			if( perPage > 0 ) parameters.Add("per_page", perPage.ToString());
+			if( page > 0 ) parameters.Add("page", page.ToString());
+			if( extras != PhotoSearchExtras.None )
+				parameters.Add("extras", Utils.ExtrasToString(extras));
+
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return response.Photos;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+
+		#endregion
+
+		#region [ Notes ]
+		/// <summary>
+		/// Add a note to a picture.
+		/// </summary>
+		/// <param name="photoId">The photo id to add the note to.</param>
+		/// <param name="noteX">The X co-ordinate of the upper left corner of the note.</param>
+		/// <param name="noteY">The Y co-ordinate of the upper left corner of the note.</param>
+		/// <param name="noteWidth">The width of the note.</param>
+		/// <param name="noteHeight">The height of the note.</param>
+		/// <param name="noteText">The text in the note.</param>
+		/// <returns></returns>
+		public string NotesAdd(string photoId, int noteX, int noteY, int noteWidth, int noteHeight, string noteText)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.photos.notes.add");
+			parameters.Add("photo_id", photoId);
+			parameters.Add("note_x", noteX.ToString());
+			parameters.Add("note_y", noteY.ToString());
+			parameters.Add("note_w", noteWidth.ToString());
+			parameters.Add("note_h", noteHeight.ToString());
+			parameters.Add("note_text", noteText);
+
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				foreach(XmlElement element in response.AllElements)
+				{
+					return element.Attributes["id", ""].Value;
+				}
+				return string.Empty;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Edit and update a note.
+		/// </summary>
+		/// <param name="noteId">The ID of the note to update.</param>
+		/// <param name="noteX">The X co-ordinate of the upper left corner of the note.</param>
+		/// <param name="noteY">The Y co-ordinate of the upper left corner of the note.</param>
+		/// <param name="noteWidth">The width of the note.</param>
+		/// <param name="noteHeight">The height of the note.</param>
+		/// <param name="noteText">The new text in the note.</param>
+		public void NotesEdit(string noteId, int noteX, int noteY, int noteWidth, int noteHeight, string noteText)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.photos.notes.edit");
+			parameters.Add("note_id", noteId);
+			parameters.Add("note_x", noteX.ToString());
+			parameters.Add("note_y", noteY.ToString());
+			parameters.Add("note_w", noteWidth.ToString());
+			parameters.Add("note_h", noteHeight.ToString());
+			parameters.Add("note_text", noteText);
+
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Delete an existing note.
+		/// </summary>
+		/// <param name="noteId">The ID of the note.</param>
+		public void NotesDelete(string noteId)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.photos.notes.delete");
+			parameters.Add("note_id", noteId);
+
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+		#endregion
+
+		#region [ People ]
+		/// <summary>
+		/// Used to fid a flickr users details by specifying their email address.
+		/// </summary>
+		/// <param name="emailAddress">The email address to search on.</param>
+		/// <returns>The <see cref="FoundUser"/> object containing the matching details.</returns>
+		/// <exception cref="FlickrException">A FlickrException is raised if the email address is not found.</exception>
+		public FoundUser PeopleFindByEmail(string emailAddress)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.people.findByEmail");
+			parameters.Add("api_key", _apiKey);
+			parameters.Add("find_email", emailAddress);
+
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return new FoundUser(response.AllElements[0]);
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Returns a <see cref="FoundUser"/> object matching the screen name.
+		/// </summary>
+		/// <param name="username">The screen name or username of the user.</param>
+		/// <returns>A <see cref="FoundUser"/> class containing the userId and username of the user.</returns>
+		/// <exception cref="FlickrException">A FlickrException is raised if the email address is not found.</exception>
+		public FoundUser PeopleFindByUsername(string username)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.people.findByUsername");
+			parameters.Add("api_key", _apiKey);
+			parameters.Add("username", username);
+
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return new FoundUser(response.AllElements[0]);
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Gets the <see cref="Person"/> object for the given user id.
+		/// </summary>
+		/// <param name="userId">The user id to find.</param>
+		/// <returns>The <see cref="Person"/> object containing the users details.</returns>
+		public Person PeopleGetInfo(string userId)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.people.getInfo");
+			parameters.Add("api_key", _apiKey);
+			parameters.Add("user_id", userId);
+
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return Person.SerializePerson(response.AllElements[0]);
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Gets the upload status of the authenticated user.
+		/// </summary>
+		/// <returns>The <see cref="UserStatus"/> object containing the users details.</returns>
+		public UserStatus PeopleGetUploadStatus()
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.people.getUploadStatus");
+
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return new UserStatus(response.AllElements[0]);
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Get a list of public groups for a user.
+		/// </summary>
+		/// <param name="userId">The user id to get groups for.</param>
+		/// <returns>An array of <see cref="PublicGroupInfo"/> instances.</returns>
+		public PublicGroupInfo[] PeopleGetPublicGroups(string userId)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.people.getPublicGroups");
+			parameters.Add("api_key", _apiKey);
+			parameters.Add("user_id", userId);
+
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return PublicGroupInfo.GetPublicGroupInfo(response.AllElements[0]);
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Gets a users public photos. Excludes private photos.
+		/// </summary>
+		/// <param name="userId">The user id of the user.</param>
+		/// <returns>The collection of photos contained within a <see cref="Photo"/> object.</returns>
+		public Photos PeopleGetPublicPhotos(string userId)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.people.getPublicPhotos");
+			parameters.Add("api_key", _apiKey);
+			parameters.Add("user_id", userId);
+
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return response.Photos;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+		#endregion
+
+		#region [ Photos ]
+		/// <summary>
+		/// Add a selection of tags to a photo.
+		/// </summary>
+		/// <param name="photoId">The photo id of the photo.</param>
+		/// <param name="tags">An array of strings containing the tags.</param>
+		/// <returns>True if the tags are added successfully.</returns>
+		public void PhotosAddTags(string photoId, string[] tags)
+		{
+			string s = string.Join(",", tags);
+			PhotosAddTags(photoId, s);
+		}
+
+		/// <summary>
+		/// Add a selection of tags to a photo.
+		/// </summary>
+		/// <param name="photoId">The photo id of the photo.</param>
+		/// <param name="tags">An string of comma delimited tags.</param>
+		/// <returns>True if the tags are added successfully.</returns>
+		public void PhotosAddTags(string photoId, string tags)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.photos.addTags");
+			parameters.Add("photo_id", photoId);
+			parameters.Add("tags", tags);
+
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Delete a photo from Flickr.
+		/// </summary>
+		/// <remarks>
+		/// Requires Delete permissions. Also note, photos cannot be recovered once deleted.</remarks>
+		/// <param name="photoId">The ID of the photo to delete.</param>
+		public void PhotosDelete(string photoId)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.photos.delete");
+			parameters.Add("photo_id", photoId);
+
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Get all the contexts (group, set and photostream 'next' and 'previous'
+		/// pictures) for a photo.
+		/// </summary>
+		/// <param name="photoId">The photo id of the photo to get the contexts for.</param>
+		/// <returns>An instance of the <see cref="AllContexts"/> class.</returns>
+		public AllContexts PhotosGetAllContexts(string photoId)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.photos.getAllContexts");
+			parameters.Add("photo_id", photoId);
+
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				AllContexts contexts = new AllContexts(response.AllElements);
+				return contexts;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+
+		}
+
+		/// <summary>
+		/// Gets the most recent 10 photos from your contacts.
+		/// </summary>
+		/// <returns>An instance of the <see cref="Photo"/> class containing the photos.</returns>
+		public Photos PhotosGetContactsPhotos()
+		{
+			return PhotosGetContactsPhotos(0, false, false, false);
+		}
+
+		/// <summary>
+		/// Gets the most recent photos from your contacts.
+		/// </summary>
+		/// <remarks>Returns the most recent photos from all your contact, excluding yourself.</remarks>
+		/// <param name="count">The number of photos to return, from between 10 and 50.</param>
+		/// <returns>An instance of the <see cref="Photo"/> class containing the photos.</returns>
+		/// <exception cref="ArgumentOutOfRangeException">
+		/// Throws a <see cref="ArgumentOutOfRangeException"/> exception if the cound
+		/// is not between 10 and 50, or 0.</exception>
+		public Photos PhotosGetContactsPhotos(long count)
+		{
+			return PhotosGetContactsPhotos(count, false, false, false);
+		}
+
+		/// <summary>
+		/// Gets your contacts most recent photos.
+		/// </summary>
+		/// <param name="count">The number of photos to return, from between 10 and 50.</param>
+		/// <param name="justFriends">If true only returns photos from contacts marked as
+		/// 'friends'.</param>
+		/// <param name="singlePhoto">If true only returns a single photo for each of your contacts.
+		/// Ignores the count if this is true.</param>
+		/// <param name="includeSelf">If true includes yourself in the group of people to
+		/// return photos for.</param>
+		/// <returns>An instance of the <see cref="Photo"/> class containing the photos.</returns>
+		/// <exception cref="ArgumentOutOfRangeException">
+		/// Throws a <see cref="ArgumentOutOfRangeException"/> exception if the cound
+		/// is not between 10 and 50, or 0.</exception>
+		public Photos PhotosGetContactsPhotos(long count, bool justFriends, bool singlePhoto, bool includeSelf)
+		{
+			if( count != 0 && (count < 10 || count > 50) && !singlePhoto )
+			{
+				throw new ArgumentOutOfRangeException("count", String.Format("Count must be between 10 and 50. ({0})", count));
+			}
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.photos.getContactsPhotos");
+			if( count > 0 && !singlePhoto ) parameters.Add("count", count.ToString());
+			if( justFriends ) parameters.Add("just_friends", "1");
+			if( singlePhoto ) parameters.Add("single_photo", "1");
+			if( includeSelf ) parameters.Add("include_self", "1");
+
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return response.Photos;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Gets the public photos for given users ID's contacts.
+		/// </summary>
+		/// <param name="userId">The user ID whose contacts you wish to get photos for.</param>
+		/// <returns>A <see cref="Photos"/> object containing details of the photos returned.</returns>
+		public Photos PhotosGetContactsPublicPhotos(string userId)
+		{
+			return PhotosGetContactsPublicPhotos(userId, 0, false, false, false, PhotoSearchExtras.All);
+		}
+
+		/// <summary>
+		/// Gets the public photos for given users ID's contacts.
+		/// </summary>
+		/// <param name="userId">The user ID whose contacts you wish to get photos for.</param>
+		/// <param name="extras">A list of extra details to return for each photo.</param>
+		/// <returns>A <see cref="Photos"/> object containing details of the photos returned.</returns>
+		public Photos PhotosGetContactsPublicPhotos(string userId, PhotoSearchExtras extras)
+		{
+			return PhotosGetContactsPublicPhotos(userId, 0, false, false, false, extras);
+		}
+
+		/// <summary>
+		/// Gets the public photos for given users ID's contacts.
+		/// </summary>
+		/// <param name="userId">The user ID whose contacts you wish to get photos for.</param>
+		/// <param name="count">The number of photos to return. Defaults to 10, maximum is 50.</param>
+		/// <returns>A <see cref="Photos"/> object containing details of the photos returned.</returns>
+		public Photos PhotosGetContactsPublicPhotos(string userId, long count)
+		{
+			return PhotosGetContactsPublicPhotos(userId, count, false, false, false, PhotoSearchExtras.All);
+		}
+
+		/// <summary>
+		/// Gets the public photos for given users ID's contacts.
+		/// </summary>
+		/// <param name="userId">The user ID whose contacts you wish to get photos for.</param>
+		/// <param name="count">The number of photos to return. Defaults to 10, maximum is 50.</param>
+		/// <param name="extras">A list of extra details to return for each photo.</param>
+		/// <returns>A <see cref="Photos"/> object containing details of the photos returned.</returns>
+		public Photos PhotosGetContactsPublicPhotos(string userId, long count, PhotoSearchExtras extras)
+		{
+			return PhotosGetContactsPublicPhotos(userId, count, false, false, false, extras);
+		}
+
+		/// <summary>
+		/// Gets the public photos for given users ID's contacts.
+		/// </summary>
+		/// <param name="userId">The user ID whose contacts you wish to get photos for.</param>
+		/// <param name="count">The number of photos to return. Defaults to 10, maximum is 50.</param>
+		/// <param name="justFriends">True to just return photos from friends and family (excluding regular contacts).</param>
+		/// <param name="singlePhoto">True to return just a single photo for each contact.</param>
+		/// <param name="includeSelf">True to include photos from the user ID specified as well.</param>
+		/// <returns></returns>
+		public Photos PhotosGetContactsPublicPhotos(string userId, long count, bool justFriends, bool singlePhoto, bool includeSelf)
+		{
+			return PhotosGetContactsPublicPhotos(userId, count, justFriends, singlePhoto, includeSelf, PhotoSearchExtras.All);
+		}
+
+		/// <summary>
+		/// Gets the public photos for given users ID's contacts.
+		/// </summary>
+		/// <param name="userId">The user ID whose contacts you wish to get photos for.</param>
+		/// <param name="count">The number of photos to return. Defaults to 10, maximum is 50.</param>
+		/// <param name="justFriends">True to just return photos from friends and family (excluding regular contacts).</param>
+		/// <param name="singlePhoto">True to return just a single photo for each contact.</param>
+		/// <param name="includeSelf">True to include photos from the user ID specified as well.</param>
+		/// <param name="extras">A list of extra details to return for each photo.</param>
+		/// <returns></returns>
+		public Photos PhotosGetContactsPublicPhotos(string userId, long count, bool justFriends, bool singlePhoto, bool includeSelf, PhotoSearchExtras extras)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.photos.getContactsPublicPhotos");
+			parameters.Add("api_key", _apiKey);
+			parameters.Add("user_id", userId);
+			if( count > 0 ) parameters.Add("count", count.ToString());
+			if( justFriends ) parameters.Add("just_friends", "1");
+			if( singlePhoto ) parameters.Add("single_photo", "1");
+			if( includeSelf ) parameters.Add("include_self", "1");
+			if( extras != PhotoSearchExtras.None ) parameters.Add("extras", Utils.ExtrasToString(extras));
+
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return response.Photos;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Gets the context of the photo in the users photostream.
+		/// </summary>
+		/// <param name="photoId">The ID of the photo to return the context for.</param>
+		/// <returns></returns>
+		public Context PhotosGetContext(string photoId)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.photos.getContext");
+			parameters.Add("photo_id", photoId);
+
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				Context c = new Context();
+				c.Count = response.ContextCount.Count;
+				c.NextPhoto = response.ContextNextPhoto;
+				c.PreviousPhoto = response.ContextPrevPhoto;
+
+				return c;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Returns count of photos between each pair of dates in the list.
+		/// </summary>
+		/// <remarks>If you pass in DateA, DateB and DateC it returns
+		/// a list of the number of photos between DateA and DateB,
+		/// followed by the number between DateB and DateC.
+		/// More parameters means more sets.</remarks>
+		/// <param name="dates">Array of <see cref="DateTime"/> objects.</param>
+		/// <returns><see cref="PhotoCounts"/> class instance.</returns>
+		public PhotoCounts PhotosGetCounts(DateTime[] dates)
+		{
+			return PhotosGetCounts(dates, false);
+		}
+
+		/// <summary>
+		/// Returns count of photos between each pair of dates in the list.
+		/// </summary>
+		/// <remarks>If you pass in DateA, DateB and DateC it returns
+		/// a list of the number of photos between DateA and DateB,
+		/// followed by the number between DateB and DateC.
+		/// More parameters means more sets.</remarks>
+		/// <param name="dates">Array of <see cref="DateTime"/> objects.</param>
+		/// <param name="taken">Boolean parameter to specify if the dates are the taken date, or uploaded date.</param>
+		/// <returns><see cref="PhotoCounts"/> class instance.</returns>
+		public PhotoCounts PhotosGetCounts(DateTime[] dates, bool taken)
+		{
+			StringBuilder s = new StringBuilder(dates.Length * 20);
+			foreach(DateTime d in dates)
+			{
+				s.Append(Utils.DateToUnixTimestamp(d));
+				s.Append(",");
+			}
+			if( s.Length > 0 ) s.Remove(s.Length-2,1);
+
+			if( taken )
+                return PhotosGetCounts(null, s.ToString());
+			else
+				return PhotosGetCounts(s.ToString(), null);
+		}
+		/// <summary>
+		/// Returns count of photos between each pair of dates in the list.
+		/// </summary>
+		/// <remarks>If you pass in DateA, DateB and DateC it returns
+		/// a list of the number of photos between DateA and DateB,
+		/// followed by the number between DateB and DateC.
+		/// More parameters means more sets.</remarks>
+		/// <param name="dates">Comma-delimited list of dates in unix timestamp format. Optional.</param>
+		/// <returns><see cref="PhotoCounts"/> class instance.</returns>
+		public PhotoCounts PhotosGetCounts(string dates)
+		{
+			return PhotosGetCounts(dates, null);
+		}
+
+		/// <summary>
+		/// Returns count of photos between each pair of dates in the list.
+		/// </summary>
+		/// <remarks>If you pass in DateA, DateB and DateC it returns
+		/// a list of the number of photos between DateA and DateB,
+		/// followed by the number between DateB and DateC.
+		/// More parameters means more sets.</remarks>
+		/// <param name="dates">Comma-delimited list of dates in unix timestamp format. Optional.</param>
+		/// <param name="taken_dates">Comma-delimited list of dates in unix timestamp format. Optional.</param>
+		/// <returns><see cref="PhotoCounts"/> class instance.</returns>
+		public PhotoCounts PhotosGetCounts(string dates, string taken_dates)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.photos.getContactsPhotos");
+			if( dates != null && dates.Length > 0 ) parameters.Add("dates", dates);
+			if( taken_dates != null && taken_dates.Length > 0 ) parameters.Add("taken_dates", taken_dates);
+
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return response.PhotoCounts;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Gets the EXIF data for a given Photo ID.
+		/// </summary>
+		/// <param name="photoId">The Photo ID of the photo to return the EXIF data for.</param>
+		/// <returns>An instance of the <see cref="ExifPhoto"/> class containing the EXIF data.</returns>
+		public ExifPhoto PhotosGetExif(string photoId)
+		{
+			return PhotosGetExif(photoId, null);
+		}
+
+		/// <summary>
+		/// Gets the EXIF data for a given Photo ID.
+		/// </summary>
+		/// <param name="photoId">The Photo ID of the photo to return the EXIF data for.</param>
+		/// <param name="secret">The secret of the photo. If the secret is specified then
+		/// authentication checks are bypassed.</param>
+		/// <returns>An instance of the <see cref="ExifPhoto"/> class containing the EXIF data.</returns>
+		public ExifPhoto PhotosGetExif(string photoId, string secret)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.photos.getExif");
+			parameters.Add("photo_id", photoId);
+			if( secret != null ) parameters.Add("secret", secret);
+
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				ExifPhoto e = new ExifPhoto(response.PhotoInfo.PhotoId,
+					response.PhotoInfo.Secret,
+					response.PhotoInfo.Server,
+					response.PhotoInfo.ExifTagCollection);
+
+				return e;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Get information about a photo. The calling user must have permission to view the photo.
+		/// </summary>
+		/// <param name="photoId">The id of the photo to fetch information for.</param>
+		/// <returns>A <see cref="PhotoInfo"/> class detailing the properties of the photo.</returns>
+		public PhotoInfo PhotosGetInfo(string photoId)
+		{
+			return PhotosGetInfo(photoId, null);
+		}
+
+		/// <summary>
+		/// Get information about a photo. The calling user must have permission to view the photo.
+		/// </summary>
+		/// <param name="photoId">The id of the photo to fetch information for.</param>
+		/// <param name="secret">The secret for the photo. If the correct secret is passed then permissions checking is skipped. This enables the 'sharing' of individual photos by passing around the id and secret.</param>
+		/// <returns>A <see cref="PhotoInfo"/> class detailing the properties of the photo.</returns>
+		public PhotoInfo PhotosGetInfo(string photoId, string secret)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.photos.getInfo");
+			parameters.Add("photo_id", photoId);
+			if( secret != null ) parameters.Add("secret", secret);
+
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return response.PhotoInfo;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Get permissions for a photo.
+		/// </summary>
+		/// <param name="photoId">The id of the photo to get permissions for.</param>
+		/// <returns>An instance of the <see cref="PhotoPermissions"/> class containing the permissions of the specified photo.</returns>
+		public PhotoPermissions PhotosGetPerms(string photoId)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.photos.getPerms");
+			parameters.Add("photo_id", photoId);
+
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return new PhotoPermissions(response.AllElements[0]);
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Returns a list of the latest public photos uploaded to flickr.
+		/// </summary>
+		/// <returns>A <see cref="Photos"/> class containing the list of photos.</returns>
+		public Photos PhotosGetRecent()
+		{
+			return PhotosGetRecent(0, 0, PhotoSearchExtras.All);
+		}
+
+		/// <summary>
+		/// Returns a list of the latest public photos uploaded to flickr.
+		/// </summary>
+		/// <param name="extras">A comma-delimited list of extra information to fetch for each returned record.</param>
+		/// <returns>A <see cref="Photos"/> class containing the list of photos.</returns>
+		public Photos PhotosGetRecent(PhotoSearchExtras extras)
+		{
+			return PhotosGetRecent(0, 0, extras);
+		}
+
+		/// <summary>
+		/// Returns a list of the latest public photos uploaded to flickr.
+		/// </summary>
+		/// <param name="page">The page of results to return. If this argument is omitted, it defaults to 1.</param>
+		/// <param name="perPage">Number of photos to return per page. If this argument is omitted, it defaults to 100. The maximum allowed value is 500.</param>
+		/// <returns>A <see cref="Photos"/> class containing the list of photos.</returns>
+		public Photos PhotosGetRecent(long perPage, long page)
+		{
+			return PhotosGetRecent(perPage, page, PhotoSearchExtras.All);
+		}
+
+		/// <summary>
+		/// Returns a list of the latest public photos uploaded to flickr.
+		/// </summary>
+		/// <param name="extras">A comma-delimited list of extra information to fetch for each returned record.</param>
+		/// <param name="page">The page of results to return. If this argument is omitted, it defaults to 1.</param>
+		/// <param name="perPage">Number of photos to return per page. If this argument is omitted, it defaults to 100. The maximum allowed value is 500.</param>
+		/// <returns>A <see cref="Photos"/> class containing the list of photos.</returns>
+		public Photos PhotosGetRecent(long perPage, long page, PhotoSearchExtras extras)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.photos.getRecent");
+			parameters.Add("api_key", _apiKey);
+			if( perPage > 0 ) parameters.Add("per_page", perPage.ToString());
+			if( page > 0 ) parameters.Add("page", page.ToString());
+			if( extras != PhotoSearchExtras.None ) parameters.Add("extras", Utils.ExtrasToString(extras));
+
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return response.Photos;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Returns the available sizes for a photo. The calling user must have permission to view the photo.
+		/// </summary>
+		/// <param name="photoId">The id of the photo to fetch size information for.</param>
+		/// <returns>A <see cref="Sizes"/> class whose property <see cref="Sizes.SizeCollection"/> is an array of <see cref="Size"/> objects.</returns>
+		public Sizes PhotosGetSizes(string photoId)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.photos.getSizes");
+			parameters.Add("photo_id", photoId);
+
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return response.Sizes;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Returns a list of your photos with no tags.
+		/// </summary>
+		/// <returns>A <see cref="Photos"/> class containing the list of photos.</returns>
+		public Photos PhotosGetUntagged()
+		{
+			return PhotosGetUntagged(0, 0, PhotoSearchExtras.All);
+		}
+
+		/// <summary>
+		/// Returns a list of your photos with no tags.
+		/// </summary>
+		/// <param name="extras">A comma-delimited list of extra information to fetch for each returned record.</param>
+		/// <returns>A <see cref="Photos"/> class containing the list of photos.</returns>
+		public Photos PhotosGetUntagged(PhotoSearchExtras extras)
+		{
+			return PhotosGetUntagged(0, 0, extras);
+		}
+
+		/// <summary>
+		/// Returns a list of your photos with no tags.
+		/// </summary>
+		/// <param name="page">The page of results to return. If this argument is omitted, it defaults to 1.</param>
+		/// <param name="perPage">Number of photos to return per page. If this argument is omitted, it defaults to 100. The maximum allowed value is 500.</param>
+		/// <returns>A <see cref="Photos"/> class containing the list of photos.</returns>
+		public Photos PhotosGetUntagged(int perPage, int page)
+		{
+			return PhotosGetUntagged(perPage, page, PhotoSearchExtras.All);
+		}
+
+		/// <summary>
+		/// Returns a list of your photos with no tags.
+		/// </summary>
+		/// <param name="extras">A comma-delimited list of extra information to fetch for each returned record.</param>
+		/// <param name="page">The page of results to return. If this argument is omitted, it defaults to 1.</param>
+		/// <param name="perPage">Number of photos to return per page. If this argument is omitted, it defaults to 100. The maximum allowed value is 500.</param>
+		/// <returns>A <see cref="Photos"/> class containing the list of photos.</returns>
+		public Photos PhotosGetUntagged(int perPage, int page, PhotoSearchExtras extras)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.photos.getUntagged");
+			if( perPage > 0 ) parameters.Add("per_page", perPage.ToString());
+			if( page > 0 ) parameters.Add("page", page.ToString());
+			if( extras != PhotoSearchExtras.None ) parameters.Add("extras", Utils.ExtrasToString(extras));
+
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return response.Photos;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Gets a list of photos not in sets. Defaults to include all extra fields.
+		/// </summary>
+		/// <returns><see cref="Photos"/> instance containing list of photos.</returns>
+		public Photos PhotosGetNotInSet()
+		{
+			return PhotosGetNotInSet(new PartialSearchOptions());
+		}
+
+		/// <summary>
+		/// Gets a specific page of the list of photos which are not in sets.
+		/// Defaults to include all extra fields.
+		/// </summary>
+		/// <param name="page">The page number to return.</param>
+		/// <returns><see cref="Photos"/> instance containing list of photos.</returns>
+		public Photos PhotosGetNotInSet(int page)
+		{
+			return PhotosGetNotInSet(0, page, PhotoSearchExtras.None);
+		}
+
+		/// <summary>
+		/// Gets a specific page of the list of photos which are not in sets.
+		/// Defaults to include all extra fields.
+		/// </summary>
+		/// <param name="perPage">Number of photos per page.</param>
+		/// <param name="page">The page number to return.</param>
+		/// <returns><see cref="Photos"/> instance containing list of photos.</returns>
+		public Photos PhotosGetNotInSet(int perPage, int page)
+		{
+			return PhotosGetNotInSet(perPage, page, PhotoSearchExtras.None);
+		}
+
+		/// <summary>
+		/// Gets a list of a users photos which are not in a set.
+		/// </summary>
+		/// <param name="perPage">Number of photos per page.</param>
+		/// <param name="page">The page number to return.</param>
+		/// <param name="extras"><see cref="PhotoSearchExtras"/> enumeration.</param>
+		/// <returns><see cref="Photos"/> instance containing list of photos.</returns>
+		public Photos PhotosGetNotInSet(int perPage, int page, PhotoSearchExtras extras)
+		{
+			PartialSearchOptions options = new PartialSearchOptions();
+			options.PerPage = perPage;
+			options.Page = page;
+			options.Extras = extras;
+
+			return PhotosGetNotInSet(options);
+		}
+
+		/// <summary>
+		/// Gets a list of the authenticated users photos which are not in a set.
+		/// </summary>
+		/// <param name="options">A selection of options to filter/sort by.</param>
+		/// <returns>A collection of photos in the <see cref="Photos"/> class.</returns>
+		public Photos PhotosGetNotInSet(PartialSearchOptions options)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.photos.getNotInSet");
+			Utils.PartialOptionsIntoArray(options, parameters);
+
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return response.Photos;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Gets a list of all current licenses.
+		/// </summary>
+		/// <returns><see cref="Licenses"/> instance.</returns>
+		public Licenses PhotosLicensesGetInfo()
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.photos.licenses.getInfo");
+			parameters.Add("api_key", _apiKey);
+
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return response.Licenses;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Remove an existing tag.
+		/// </summary>
+		/// <param name="tagId">The id of the tag, as returned by <see cref="Flickr.PhotosGetInfo(string)"/> or similar method.</param>
+		/// <returns>True if the tag was removed.</returns>
+		public bool PhotosRemoveTag(string tagId)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.photos.removeTag");
+			parameters.Add("tag_id", tagId);
+
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return true;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Search for photos containing text, rather than tags.
+		/// </summary>
+		/// <param name="userId">The user whose photos you wish to search for.</param>
+		/// <param name="text">The text you want to search for in titles and descriptions.</param>
+		/// <returns>A <see cref="Photos"/> instance.</returns>
+		public Photos PhotosSearchText(string userId, string text)
+		{
+			return PhotosSearch(userId, "", 0, text, DateTime.MinValue, DateTime.MinValue, 0, 0, 0, PhotoSearchExtras.All);
+		}
+
+		/// <summary>
+		/// Search for photos containing text, rather than tags.
+		/// </summary>
+		/// <param name="userId">The user whose photos you wish to search for.</param>
+		/// <param name="text">The text you want to search for in titles and descriptions.</param>
+		/// <param name="extras">Optional extras to return.</param>
+		/// <returns>A <see cref="Photos"/> instance.</returns>
+		public Photos PhotosSearchText(string userId, string text, PhotoSearchExtras extras)
+		{
+			return PhotosSearch(userId, "", 0, text, DateTime.MinValue, DateTime.MinValue, 0, 0, 0, extras);
+		}
+
+		/// <summary>
+		/// Search for photos containing text, rather than tags.
+		/// </summary>
+		/// <param name="userId">The user whose photos you wish to search for.</param>
+		/// <param name="text">The text you want to search for in titles and descriptions.</param>
+		/// <param name="license">The license type to return.</param>
+		/// <returns>A <see cref="Photos"/> instance.</returns>
+		public Photos PhotosSearchText(string userId, string text, int license)
+		{
+			return PhotosSearch(userId, "", 0, text, DateTime.MinValue, DateTime.MinValue, license, 0, 0, PhotoSearchExtras.All);
+		}
+
+		/// <summary>
+		/// Search for photos containing text, rather than tags.
+		/// </summary>
+		/// <param name="userId">The user whose photos you wish to search for.</param>
+		/// <param name="text">The text you want to search for in titles and descriptions.</param>
+		/// <param name="license">The license type to return.</param>
+		/// <param name="extras">Optional extras to return.</param>
+		/// <returns>A <see cref="Photos"/> instance.</returns>
+		public Photos PhotosSearchText(string userId, string text, int license, PhotoSearchExtras extras)
+		{
+			return PhotosSearch(userId, "", 0, text, DateTime.MinValue, DateTime.MinValue, license, 0, 0, extras);
+		}
+
+		/// <summary>
+		/// Search for photos containing text, rather than tags.
+		/// </summary>
+		/// <param name="text">The text you want to search for in titles and descriptions.</param>
+		/// <param name="extras">Optional extras to return.</param>
+		/// <returns>A <see cref="Photos"/> instance.</returns>
+		public Photos PhotosSearchText(string text, PhotoSearchExtras extras)
+		{
+			return PhotosSearch(null, "", 0, text, DateTime.MinValue, DateTime.MinValue, 0, 0, 0, extras);
+		}
+
+		/// <summary>
+		/// Search for photos containing text, rather than tags.
+		/// </summary>
+		/// <param name="text">The text you want to search for in titles and descriptions.</param>
+		/// <returns>A <see cref="Photos"/> instance.</returns>
+		public Photos PhotosSearchText(string text)
+		{
+			return PhotosSearch(null, "", 0, text, DateTime.MinValue, DateTime.MinValue, 0, 0, 0, PhotoSearchExtras.All);
+		}
+
+		/// <summary>
+		/// Search for photos containing text, rather than tags.
+		/// </summary>
+		/// <param name="text">The text you want to search for in titles and descriptions.</param>
+		/// <param name="license">The license type to return.</param>
+		/// <returns>A <see cref="Photos"/> instance.</returns>
+		public Photos PhotosSearchText(string text, int license)
+		{
+			return PhotosSearch(null, "", 0, text, DateTime.MinValue, DateTime.MinValue, license, 0, 0, PhotoSearchExtras.All);
+		}
+
+		/// <summary>
+		/// Search for photos containing text, rather than tags.
+		/// </summary>
+		/// <param name="text">The text you want to search for in titles and descriptions.</param>
+		/// <param name="license">The license type to return.</param>
+		/// <param name="extras">Optional extras to return.</param>
+		/// <returns>A <see cref="Photos"/> instance.</returns>
+		public Photos PhotosSearchText(string text, int license, PhotoSearchExtras extras)
+		{
+			return PhotosSearch(null, "", 0, text, DateTime.MinValue, DateTime.MinValue, license, 0, 0, extras);
+		}
+
+		/// <summary>
+		/// Search for photos containing an array of tags.
+		/// </summary>
+		/// <param name="tags">An array of tags to search for.</param>
+		/// <param name="extras">Optional extras to return.</param>
+		/// <returns>A <see cref="Photos"/> instance.</returns>
+		public Photos PhotosSearch(string[] tags, PhotoSearchExtras extras)
+		{
+			return PhotosSearch(null, tags, 0, "", DateTime.MinValue, DateTime.MinValue, 0, 0, 0, extras);
+		}
+
+		/// <summary>
+		/// Search for photos containing an array of tags.
+		/// </summary>
+		/// <param name="tags">An array of tags to search for.</param>
+		/// <returns>A <see cref="Photos"/> instance.</returns>
+		public Photos PhotosSearch(string[] tags)
+		{
+			return PhotosSearch(null, tags, 0, "", DateTime.MinValue, DateTime.MinValue, 0, 0, 0, PhotoSearchExtras.All);
+		}
+
+		/// <summary>
+		/// Search for photos containing an array of tags.
+		/// </summary>
+		/// <param name="tags">An array of tags to search for.</param>
+		/// <param name="license">The license type to return.</param>
+		/// <param name="extras">Optional extras to return.</param>
+		/// <returns>A <see cref="Photos"/> instance.</returns>
+		public Photos PhotosSearch(string[] tags, int license, PhotoSearchExtras extras)
+		{
+			return PhotosSearch(null, tags, 0, "", DateTime.MinValue, DateTime.MinValue, license, 0, 0, extras);
+		}
+
+		/// <summary>
+		/// Search for photos containing an array of tags.
+		/// </summary>
+		/// <param name="tags">An array of tags to search for.</param>
+		/// <param name="license">The license type to return.</param>
+		/// <returns>A <see cref="Photos"/> instance.</returns>
+		public Photos PhotosSearch(string[] tags, int license)
+		{
+			return PhotosSearch(null, tags, 0, "", DateTime.MinValue, DateTime.MinValue, license, 0, 0, PhotoSearchExtras.All);
+		}
+
+		/// <summary>
+		/// Search for photos.
+		/// </summary>
+		/// <param name="tags">An array of tags to search for.</param>
+		/// <param name="tagMode">Match all tags, or any tag.</param>
+		/// <param name="text">Text to search for in photo title or description.</param>
+		/// <param name="perPage">Number of photos to return per page.</param>
+		/// <param name="page">The page number to return.</param>
+		/// <returns>A <see cref="Photos"/> instance.</returns>
+		public Photos PhotosSearch(string[] tags, TagMode tagMode, string text, int perPage, int page)
+		{
+			return PhotosSearch(null, tags, tagMode, text, DateTime.MinValue, DateTime.MinValue, 0, perPage, page, PhotoSearchExtras.All);
+		}
+
+		/// <summary>
+		/// Search for photos.
+		/// </summary>
+		/// <param name="tags">An array of tags to search for.</param>
+		/// <param name="tagMode">Match all tags, or any tag.</param>
+		/// <param name="text">Text to search for in photo title or description.</param>
+		/// <param name="perPage">Number of photos to return per page.</param>
+		/// <param name="page">The page number to return.</param>
+		/// <param name="extras">Optional extras to return.</param>
+		/// <returns>A <see cref="Photos"/> instance.</returns>
+		public Photos PhotosSearch(string[] tags, TagMode tagMode, string text, int perPage, int page, PhotoSearchExtras extras)
+		{
+			return PhotosSearch(null, tags, tagMode, text, DateTime.MinValue, DateTime.MinValue, 0, perPage, page, extras);
+		}
+
+		/// <summary>
+		/// Search for photos.
+		/// </summary>
+		/// <param name="tags">An array of tags to search for.</param>
+		/// <param name="tagMode">Match all tags, or any tag.</param>
+		/// <param name="text">Text to search for in photo title or description.</param>
+		/// <returns>A <see cref="Photos"/> instance.</returns>
+		public Photos PhotosSearch(string[] tags, TagMode tagMode, string text)
+		{
+			return PhotosSearch(null, tags, tagMode, text, DateTime.MinValue, DateTime.MinValue, 0, 0, 0, PhotoSearchExtras.All);
+		}
+
+		/// <summary>
+		/// Search for photos.
+		/// </summary>
+		/// <param name="tags">An array of tags to search for.</param>
+		/// <param name="tagMode">Match all tags, or any tag.</param>
+		/// <param name="text">Text to search for in photo title or description.</param>
+		/// <param name="extras">Optional extras to return.</param>
+		/// <returns>A <see cref="Photos"/> instance.</returns>
+		public Photos PhotosSearch(string[] tags, TagMode tagMode, string text, PhotoSearchExtras extras)
+		{
+			return PhotosSearch(null, tags, tagMode, text, DateTime.MinValue, DateTime.MinValue, 0, 0, 0, extras);
+		}
+
+		/// <summary>
+		/// Search for photos.
+		/// </summary>
+		/// <param name="userId">The ID of the user to search the photos of.</param>
+		/// <param name="tags">An array of tags to search for.</param>
+		/// <returns>A <see cref="Photos"/> instance.</returns>
+		public Photos PhotosSearch(string userId, string[] tags)
+		{
+			return PhotosSearch(userId, tags, 0, "", DateTime.MinValue, DateTime.MinValue, 0, 0, 0, PhotoSearchExtras.All);
+		}
+
+		/// <summary>
+		/// Search for photos.
+		/// </summary>
+		/// <param name="userId">The ID of the user to search the photos of.</param>
+		/// <param name="tags">An array of tags to search for.</param>
+		/// <param name="extras">Optional extras to return.</param>
+		/// <returns>A <see cref="Photos"/> instance.</returns>
+		public Photos PhotosSearch(string userId, string[] tags, PhotoSearchExtras extras)
+		{
+			return PhotosSearch(userId, tags, 0, "", DateTime.MinValue, DateTime.MinValue, 0, 0, 0, extras);
+		}
+
+		/// <summary>
+		/// Search for photos.
+		/// </summary>
+		/// <param name="userId">The ID of the user to search the photos of.</param>
+		/// <param name="tags">An array of tags to search for.</param>
+		/// <param name="license">The license type to return.</param>
+		/// <returns>A <see cref="Photos"/> instance.</returns>
+		public Photos PhotosSearch(string userId, string[] tags, int license)
+		{
+			return PhotosSearch(userId, tags, 0, "", DateTime.MinValue, DateTime.MinValue, license, 0, 0, PhotoSearchExtras.All);
+		}
+
+		/// <summary>
+		/// Search for photos.
+		/// </summary>
+		/// <param name="userId">The ID of the user to search the photos of.</param>
+		/// <param name="tags">An array of tags to search for.</param>
+		/// <param name="license">The license type to return.</param>
+		/// <param name="extras">Optional extras to return.</param>
+		/// <returns>A <see cref="Photos"/> instance.</returns>
+		public Photos PhotosSearch(string userId, string[] tags, int license, PhotoSearchExtras extras)
+		{
+			return PhotosSearch(userId, tags, 0, "", DateTime.MinValue, DateTime.MinValue, license, 0, 0, extras);
+		}
+
+		/// <summary>
+		/// Search for photos.
+		/// </summary>
+		/// <param name="userId">The ID of the user to search the photos of.</param>
+		/// <param name="tags">An array of tags to search for.</param>
+		/// <param name="tagMode">Match all tags, or any tag.</param>
+		/// <param name="text">Text to search for in photo title or description.</param>
+		/// <param name="perPage">Number of photos to return per page.</param>
+		/// <param name="page">The page number to return.</param>
+		/// <returns>A <see cref="Photos"/> instance.</returns>
+		public Photos PhotosSearch(string userId, string[] tags, TagMode tagMode, string text, int perPage, int page)
+		{
+			return PhotosSearch(userId, tags, tagMode, text, DateTime.MinValue, DateTime.MinValue, 0, perPage, page, PhotoSearchExtras.All);
+		}
+
+		/// <summary>
+		/// Search for photos.
+		/// </summary>
+		/// <param name="userId">The ID of the user to search the photos of.</param>
+		/// <param name="tags">An array of tags to search for.</param>
+		/// <param name="tagMode">Match all tags, or any tag.</param>
+		/// <param name="text">Text to search for in photo title or description.</param>
+		/// <param name="perPage">Number of photos to return per page.</param>
+		/// <param name="page">The page number to return.</param>
+		/// <param name="extras">Optional extras to return.</param>
+		/// <returns>A <see cref="Photos"/> instance.</returns>
+		public Photos PhotosSearch(string userId, string[] tags, TagMode tagMode, string text, int perPage, int page, PhotoSearchExtras extras)
+		{
+			return PhotosSearch(userId, tags, tagMode, text, DateTime.MinValue, DateTime.MinValue, 0, perPage, page, extras);
+		}
+
+		/// <summary>
+		/// Search for photos.
+		/// </summary>
+		/// <param name="userId">The ID of the user to search the photos of.</param>
+		/// <param name="tags">An array of tags to search for.</param>
+		/// <param name="tagMode">Match all tags, or any tag.</param>
+		/// <param name="text">Text to search for in photo title or description.</param>
+		/// <returns>A <see cref="Photos"/> instance.</returns>
+		public Photos PhotosSearch(string userId, string[] tags, TagMode tagMode, string text)
+		{
+			return PhotosSearch(userId, tags, tagMode, text, DateTime.MinValue, DateTime.MinValue, 0, 0, 0, PhotoSearchExtras.All);
+		}
+
+		/// <summary>
+		/// Search for photos.
+		/// </summary>
+		/// <param name="userId">The ID of the user to search the photos of.</param>
+		/// <param name="tags">An array of tags to search for.</param>
+		/// <param name="tagMode">Match all tags, or any tag.</param>
+		/// <param name="text">Text to search for in photo title or description.</param>
+		/// <param name="extras">Optional extras to return.</param>
+		/// <returns>A <see cref="Photos"/> instance.</returns>
+		public Photos PhotosSearch(string userId, string[] tags, TagMode tagMode, string text, PhotoSearchExtras extras)
+		{
+			return PhotosSearch(userId, tags, tagMode, text, DateTime.MinValue, DateTime.MinValue, 0, 0, 0, extras);
+		}
+
+		/// <summary>
+		/// Search for photos.
+		/// </summary>
+		/// <param name="userId">The ID of the user to search the photos of.</param>
+		/// <param name="tags">An array of tags to search for.</param>
+		/// <param name="tagMode">Match all tags, or any tag.</param>
+		/// <param name="text">Text to search for in photo title or description.</param>
+		/// <param name="minUploadDate">The minimum upload date.</param>
+		/// <param name="maxUploadDate">The maxmimum upload date.</param>
+		/// <param name="license">The license type to return.</param>
+		/// <param name="perPage">Number of photos to return per page.</param>
+		/// <param name="page">The page number to return.</param>
+		/// <param name="extras">Optional extras to return.</param>
+		/// <returns>A <see cref="Photos"/> instance.</returns>
+		public Photos PhotosSearch(string userId, string[] tags, TagMode tagMode, string text, DateTime minUploadDate, DateTime maxUploadDate, int license, int perPage, int page, PhotoSearchExtras extras)
+		{
+			return PhotosSearch(userId, String.Join(",", tags), tagMode, text, minUploadDate, maxUploadDate, license, perPage, page, extras);
+		}
+
+		/// <summary>
+		/// Search for photos.
+		/// </summary>
+		/// <param name="userId">The ID of the user to search the photos of.</param>
+		/// <param name="tags">An array of tags to search for.</param>
+		/// <param name="tagMode">Match all tags, or any tag.</param>
+		/// <param name="text">Text to search for in photo title or description.</param>
+		/// <param name="minUploadDate">The minimum upload date.</param>
+		/// <param name="maxUploadDate">The maxmimum upload date.</param>
+		/// <param name="license">The license type to return.</param>
+		/// <param name="perPage">Number of photos to return per page.</param>
+		/// <param name="page">The page number to return.</param>
+		/// <returns>A <see cref="Photos"/> instance.</returns>
+		public Photos PhotosSearch(string userId, string[] tags, TagMode tagMode, string text, DateTime minUploadDate, DateTime maxUploadDate, int license, int perPage, int page)
+		{
+			return PhotosSearch(userId, String.Join(",", tags), tagMode, text, minUploadDate, maxUploadDate, license, perPage, page, PhotoSearchExtras.All);
+		}
+
+		// PhotoSearch - tags versions
+
+		/// <summary>
+		/// Search for photos.
+		/// </summary>
+		/// <param name="tags">A comma seperated list of tags to search for.</param>
+		/// <param name="license">The license type to return.</param>
+		/// <param name="extras">Optional extras to return.</param>
+		/// <returns>A <see cref="Photos"/> instance.</returns>
+		public Photos PhotosSearch(string tags, int license, PhotoSearchExtras extras)
+		{
+			return PhotosSearch(null, tags, 0, "", DateTime.MinValue, DateTime.MinValue, license, 0, 0, extras);
+		}
+
+		/// <summary>
+		/// Search for photos.
+		/// </summary>
+		/// <param name="tags">A comma seperated list of tags to search for.</param>
+		/// <param name="license">The license type to return.</param>
+		/// <returns>A <see cref="Photos"/> instance.</returns>
+		public Photos PhotosSearch(string tags, int license)
+		{
+			return PhotosSearch(null, tags, 0, "", DateTime.MinValue, DateTime.MinValue, license, 0, 0, PhotoSearchExtras.All);
+		}
+
+		/// <summary>
+		/// Search for photos.
+		/// </summary>
+		/// <param name="tags">A comma seperated list of tags to search for.</param>
+		/// <param name="tagMode">Match all tags, or any tag.</param>
+		/// <param name="text">Text to search for in photo title or description.</param>
+		/// <param name="perPage">Number of photos to return per page.</param>
+		/// <param name="page">The page number to return.</param>
+		/// <returns>A <see cref="Photos"/> instance.</returns>
+		public Photos PhotosSearch(string tags, TagMode tagMode, string text, int perPage, int page)
+		{
+			return PhotosSearch(null, tags, tagMode, text, DateTime.MinValue, DateTime.MinValue, 0, perPage, page, PhotoSearchExtras.All);
+		}
+
+		/// <summary>
+		/// Search for photos.
+		/// </summary>
+		/// <param name="tags">A comma seperated list of tags to search for.</param>
+		/// <param name="tagMode">Match all tags, or any tag.</param>
+		/// <param name="text">Text to search for in photo title or description.</param>
+		/// <param name="perPage">Number of photos to return per page.</param>
+		/// <param name="page">The page number to return.</param>
+		/// <param name="extras">Optional extras to return.</param>
+		/// <returns>A <see cref="Photos"/> instance.</returns>
+		public Photos PhotosSearch(string tags, TagMode tagMode, string text, int perPage, int page, PhotoSearchExtras extras)
+		{
+			return PhotosSearch(null, tags, tagMode, text, DateTime.MinValue, DateTime.MinValue, 0, perPage, page, extras);
+		}
+
+		/// <summary>
+		/// Search for photos.
+		/// </summary>
+		/// <param name="tags">A comma seperated list of tags to search for.</param>
+		/// <param name="tagMode">Match all tags, or any tag.</param>
+		/// <param name="text">Text to search for in photo title or description.</param>
+		/// <returns>A <see cref="Photos"/> instance.</returns>
+		public Photos PhotosSearch(string tags, TagMode tagMode, string text)
+		{
+			return PhotosSearch(null, tags, tagMode, text, DateTime.MinValue, DateTime.MinValue, 0, 0, 0, PhotoSearchExtras.All);
+		}
+
+		/// <summary>
+		/// Search for photos.
+		/// </summary>
+		/// <param name="tags">A comma seperated list of tags to search for.</param>
+		/// <param name="tagMode">Match all tags, or any tag.</param>
+		/// <param name="text">Text to search for in photo title or description.</param>
+		/// <param name="extras">Optional extras to return.</param>
+		/// <returns>A <see cref="Photos"/> instance.</returns>
+		public Photos PhotosSearch(string tags, TagMode tagMode, string text, PhotoSearchExtras extras)
+		{
+			return PhotosSearch(null, tags, tagMode, text, DateTime.MinValue, DateTime.MinValue, 0, 0, 0, extras);
+		}
+
+		/// <summary>
+		/// Search for photos.
+		/// </summary>
+		/// <param name="userId">The ID of the user to search the photos of.</param>
+		/// <param name="tags">A comma seperated list of tags to search for.</param>
+		/// <returns>A <see cref="Photos"/> instance.</returns>
+		public Photos PhotosSearch(string userId, string tags)
+		{
+			return PhotosSearch(userId, tags, 0, "", DateTime.MinValue, DateTime.MinValue, 0, 0, 0, PhotoSearchExtras.All);
+		}
+
+		/// <summary>
+		/// Search for photos.
+		/// </summary>
+		/// <param name="userId">The ID of the user to search the photos of.</param>
+		/// <param name="tags">A comma seperated list of tags to search for.</param>
+		/// <param name="extras">Optional extras to return.</param>
+		/// <returns>A <see cref="Photos"/> instance.</returns>
+		public Photos PhotosSearch(string userId, string tags, PhotoSearchExtras extras)
+		{
+			return PhotosSearch(userId, tags, 0, "", DateTime.MinValue, DateTime.MinValue, 0, 0, 0, extras);
+		}
+
+		/// <summary>
+		/// Search for photos.
+		/// </summary>
+		/// <param name="userId">The ID of the user to search the photos of.</param>
+		/// <param name="tags">A comma seperated list of tags to search for.</param>
+		/// <param name="license">The license type to return.</param>
+		/// <returns>A <see cref="Photos"/> instance.</returns>
+		public Photos PhotosSearch(string userId, string tags, int license)
+		{
+			return PhotosSearch(userId, tags, 0, "", DateTime.MinValue, DateTime.MinValue, license, 0, 0, PhotoSearchExtras.All);
+		}
+
+		/// <summary>
+		/// Search for photos.
+		/// </summary>
+		/// <param name="userId">The ID of the user to search the photos of.</param>
+		/// <param name="tags">A comma seperated list of tags to search for.</param>
+		/// <param name="license">The license type to return.</param>
+		/// <param name="extras">Optional extras to return.</param>
+		/// <returns>A <see cref="Photos"/> instance.</returns>
+		public Photos PhotosSearch(string userId, string tags, int license, PhotoSearchExtras extras)
+		{
+			return PhotosSearch(userId, tags, 0, "", DateTime.MinValue, DateTime.MinValue, license, 0, 0, extras);
+		}
+
+		/// <summary>
+		/// Search for photos.
+		/// </summary>
+		/// <param name="userId">The ID of the user to search the photos of.</param>
+		/// <param name="tags">A comma seperated list of tags to search for.</param>
+		/// <param name="tagMode">Match all tags, or any tag.</param>
+		/// <param name="text">Text to search for in photo title or description.</param>
+		/// <param name="perPage">Number of photos to return per page.</param>
+		/// <param name="page">The page number to return.</param>
+		/// <returns>A <see cref="Photos"/> instance.</returns>
+		public Photos PhotosSearch(string userId, string tags, TagMode tagMode, string text, int perPage, int page)
+		{
+			return PhotosSearch(userId, tags, tagMode, text, DateTime.MinValue, DateTime.MinValue, 0, perPage, page, PhotoSearchExtras.All);
+		}
+
+		/// <summary>
+		/// Search for photos.
+		/// </summary>
+		/// <param name="userId">The ID of the user to search the photos of.</param>
+		/// <param name="tags">A comma seperated list of tags to search for.</param>
+		/// <param name="tagMode">Match all tags, or any tag.</param>
+		/// <param name="text">Text to search for in photo title or description.</param>
+		/// <param name="perPage">Number of photos to return per page.</param>
+		/// <param name="page">The page number to return.</param>
+		/// <param name="extras">Optional extras to return.</param>
+		/// <returns>A <see cref="Photos"/> instance.</returns>
+		public Photos PhotosSearch(string userId, string tags, TagMode tagMode, string text, int perPage, int page, PhotoSearchExtras extras)
+		{
+			return PhotosSearch(userId, tags, tagMode, text, DateTime.MinValue, DateTime.MinValue, 0, perPage, page, extras);
+		}
+
+		/// <summary>
+		/// Search for photos.
+		/// </summary>
+		/// <param name="userId">The ID of the user to search the photos of.</param>
+		/// <param name="tags">A comma seperated list of tags to search for.</param>
+		/// <param name="tagMode">Match all tags, or any tag.</param>
+		/// <param name="text">Text to search for in photo title or description.</param>
+		/// <returns>A <see cref="Photos"/> instance.</returns>
+		public Photos PhotosSearch(string userId, string tags, TagMode tagMode, string text)
+		{
+			return PhotosSearch(userId, tags, tagMode, text, DateTime.MinValue, DateTime.MinValue, 0, 0, 0, PhotoSearchExtras.All);
+		}
+
+		/// <summary>
+		/// Search for photos.
+		/// </summary>
+		/// <param name="userId">The ID of the user to search the photos of.</param>
+		/// <param name="tags">A comma seperated list of tags to search for.</param>
+		/// <param name="tagMode">Match all tags, or any tag.</param>
+		/// <param name="text">Text to search for in photo title or description.</param>
+		/// <param name="extras">Optional extras to return.</param>
+		/// <returns>A <see cref="Photos"/> instance.</returns>
+		public Photos PhotosSearch(string userId, string tags, TagMode tagMode, string text, PhotoSearchExtras extras)
+		{
+			return PhotosSearch(userId, tags, tagMode, text, DateTime.MinValue, DateTime.MinValue, 0, 0, 0, extras);
+		}
+
+		// Actual PhotoSearch function
+
+		/// <summary>
+		/// Search for photos.
+		/// </summary>
+		/// <param name="userId">The ID of the user to search the photos of.</param>
+		/// <param name="tags">A comma seperated list of tags to search for.</param>
+		/// <param name="tagMode">Match all tags, or any tag.</param>
+		/// <param name="text">Text to search for in photo title or description.</param>
+		/// <param name="perPage">Number of photos to return per page.</param>
+		/// <param name="page">The page number to return.</param>
+		/// <param name="extras">Optional extras to return.</param>
+		/// <param name="minUploadDate">The minimum upload date.</param>
+		/// <param name="maxUploadDate">The maxmimum upload date.</param>
+		/// <param name="license">The license type to return.</param>
+		/// <returns>A <see cref="Photos"/> instance.</returns>
+		public Photos PhotosSearch(string userId, string tags, TagMode tagMode, string text, DateTime minUploadDate, DateTime maxUploadDate, int license, int perPage, int page, PhotoSearchExtras extras)
+		{
+			PhotoSearchOptions options = new PhotoSearchOptions();
+			options.UserId = userId;
+			options.Tags = tags;
+			options.TagMode = tagMode;
+			options.Text = text;
+			options.MinUploadDate = minUploadDate;
+			options.MaxUploadDate = maxUploadDate;
+			if( license > 0 ) options.AddLicense(license);
+			options.PerPage = perPage;
+			options.Page = page;
+			options.Extras = extras;
+
+			return PhotosSearch(options);
+		}
+
+		/// <summary>
+		/// Search for a set of photos, based on the value of the <see cref="PhotoSearchOptions"/> parameters.
+		/// </summary>
+		/// <param name="options">The parameters to search for.</param>
+		/// <returns>A collection of photos contained within a <see cref="Photos"/> object.</returns>
+		public Photos PhotosSearch(PhotoSearchOptions options)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.photos.search");
+			if( options.UserId != null && options.UserId.Length > 0 ) parameters.Add("user_id", options.UserId);
+			if( options.Text != null && options.Text.Length > 0 ) parameters.Add("text", options.Text);
+			if( options.Tags != null && options.Tags.Length > 0 ) parameters.Add("tags", options.Tags);
+			if( options.TagMode != TagMode.None ) parameters.Add("tag_mode", options.TagModeString);
+			if( options.MachineTags != null && options.MachineTags.Length > 0 ) parameters.Add("machine_tags", options.MachineTags);
+			if( options.MachineTagMode != MachineTagMode.None ) parameters.Add("machine_tag_mode", options.MachineTagModeString);
+			if( options.MinUploadDate != DateTime.MinValue ) parameters.Add("min_upload_date", Utils.DateToUnixTimestamp(options.MinUploadDate).ToString());
+			if( options.MaxUploadDate != DateTime.MinValue ) parameters.Add("max_upload_date", Utils.DateToUnixTimestamp(options.MaxUploadDate).ToString());
+			if( options.MinTakenDate != DateTime.MinValue ) parameters.Add("min_taken_date", options.MinTakenDate.ToString("yyyy-MM-dd HH:mm:ss", System.Globalization.DateTimeFormatInfo.InvariantInfo));
+			if( options.MaxTakenDate != DateTime.MinValue ) parameters.Add("max_taken_date", options.MaxTakenDate.ToString("yyyy-MM-dd HH:mm:ss", System.Globalization.DateTimeFormatInfo.InvariantInfo));
+			if( options.Licenses.Length != 0 )
+			{
+				string lic = "";
+				for(int i = 0; i < options.Licenses.Length; i++)
+				{
+					if( i > 0 ) lic += ",";
+					lic += Convert.ToString(options.Licenses[i]);
+				}
+				parameters.Add("license", lic);
+			}
+			if( options.PerPage != 0 ) parameters.Add("per_page", options.PerPage.ToString());
+			if( options.Page != 0 ) parameters.Add("page", options.Page.ToString());
+			if( options.Extras != PhotoSearchExtras.None ) parameters.Add("extras", options.ExtrasString);
+			if( options.SortOrder != PhotoSearchSortOrder.None ) parameters.Add("sort", options.SortOrderString);
+			if( options.PrivacyFilter != PrivacyFilter.None ) parameters.Add("privacy_filter", options.PrivacyFilter.ToString("d"));
+			if( options.BoundaryBox.IsSet ) parameters.Add("bbox", options.BoundaryBox.ToString());
+			if( options.Accuracy != GeoAccuracy.None ) parameters.Add("accuracy", options.Accuracy.ToString("d"));
+
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return response.Photos;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Set the date taken for a photo.
+		/// </summary>
+		/// <remarks>
+		/// All dates are assumed to be GMT. It is the developers responsibility to change dates to the local users
+		/// timezone.
+		/// </remarks>
+		/// <param name="photoId">The id of the photo to set the date taken for.</param>
+		/// <param name="dateTaken">The date taken.</param>
+		/// <param name="granularity">The granularity of the date taken.</param>
+		/// <returns>True if the date was updated successfully.</returns>
+		public bool PhotosSetDates(string photoId, DateTime dateTaken, DateGranularity granularity)
+		{
+			return PhotosSetDates(photoId, DateTime.MinValue, dateTaken, granularity);
+		}
+
+		/// <summary>
+		/// Set the date the photo was posted (uploaded). This will affect the order in which photos
+		/// are seen in your photostream.
+		/// </summary>
+		/// <remarks>
+		/// All dates are assumed to be GMT. It is the developers responsibility to change dates to the local users
+		/// timezone.
+		/// </remarks>
+		/// <param name="photoId">The id of the photo to set the date posted.</param>
+		/// <param name="datePosted">The new date to set the date posted too.</param>
+		/// <returns>True if the date was updated successfully.</returns>
+		public bool PhotosSetDates(string photoId, DateTime datePosted)
+		{
+			return PhotosSetDates(photoId, datePosted, DateTime.MinValue, DateGranularity.FullDate);
+		}
+
+		/// <summary>
+		/// Set the date the photo was posted (uploaded) and the date the photo was taken.
+		/// Changing the date posted will affect the order in which photos are seen in your photostream.
+		/// </summary>
+		/// <remarks>
+		/// All dates are assumed to be GMT. It is the developers responsibility to change dates to the local users
+		/// timezone.
+		/// </remarks>
+		/// <param name="photoId">The id of the photo to set the dates.</param>
+		/// <param name="datePosted">The new date to set the date posted too.</param>
+		/// <param name="dateTaken">The new date to set the date taken too.</param>
+		/// <param name="granularity">The granularity of the date taken.</param>
+		/// <returns>True if the dates where updated successfully.</returns>
+		public bool PhotosSetDates(string photoId, DateTime datePosted, DateTime dateTaken, DateGranularity granularity)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.photos.setDates");
+			parameters.Add("photo_id", photoId);
+			if( datePosted != DateTime.MinValue ) parameters.Add("date_posted", Utils.DateToUnixTimestamp(datePosted).ToString());
+			if( dateTaken != DateTime.MinValue )
+			{
+				parameters.Add("date_taken", dateTaken.ToString("yyyy-MM-dd HH:mm:ss", System.Globalization.DateTimeFormatInfo.InvariantInfo));
+				parameters.Add("date_taken_granularity", granularity.ToString("d"));
+			}
+
+			FlickrNet.Response response = GetResponseNoCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return true;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+
+		}
+
+		/// <summary>
+		/// Sets the title and description of the photograph.
+		/// </summary>
+		/// <param name="photoId">The numerical photoId of the photograph.</param>
+		/// <param name="title">The new title of the photograph.</param>
+		/// <param name="description">The new description of the photograph.</param>
+		/// <returns>True when the operation is successful.</returns>
+		/// <exception cref="FlickrException">Thrown when the photo id cannot be found.</exception>
+		public bool PhotosSetMeta(string photoId, string title, string description)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.photos.setMeta");
+			parameters.Add("photo_id", photoId);
+			parameters.Add("title", title);
+			parameters.Add("description", description);
+
+			FlickrNet.Response response = GetResponseNoCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return true;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+
+		}
+
+		/// <summary>
+		/// Set the permissions on a photo.
+		/// </summary>
+		/// <param name="photoId">The id of the photo to update.</param>
+		/// <param name="isPublic">1 if the photo is public, 0 if it is not.</param>
+		/// <param name="isFriend">1 if the photo is viewable by friends, 0 if it is not.</param>
+		/// <param name="isFamily">1 if the photo is viewable by family, 0 if it is not.</param>
+		/// <param name="permComment">Who can add comments. See <see cref="PermissionComment"/> for more details.</param>
+		/// <param name="permAddMeta">Who can add metadata (notes and tags). See <see cref="PermissionAddMeta"/> for more details.</param>
+		public void PhotosSetPerms(string photoId, int isPublic, int isFriend, int isFamily, PermissionComment permComment, PermissionAddMeta permAddMeta)
+		{
+			PhotosSetPerms(photoId, (isPublic==1), (isFriend==1), (isFamily==1), permComment, permAddMeta);
+		}
+
+		/// <summary>
+		/// Set the permissions on a photo.
+		/// </summary>
+		/// <param name="photoId">The id of the photo to update.</param>
+		/// <param name="isPublic">True if the photo is public, False if it is not.</param>
+		/// <param name="isFriend">True if the photo is viewable by friends, False if it is not.</param>
+		/// <param name="isFamily">True if the photo is viewable by family, False if it is not.</param>
+		/// <param name="permComment">Who can add comments. See <see cref="PermissionComment"/> for more details.</param>
+		/// <param name="permAddMeta">Who can add metadata (notes and tags). See <see cref="PermissionAddMeta"/> for more details.</param>
+		public void PhotosSetPerms(string photoId, bool isPublic, bool isFriend, bool isFamily, PermissionComment permComment, PermissionAddMeta permAddMeta)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.photos.setPerms");
+			parameters.Add("photo_id", photoId);
+			parameters.Add("is_public", (isPublic?"1":"0"));
+			parameters.Add("is_friend", (isFriend?"1":"0"));
+			parameters.Add("is_family", (isFamily?"1":"0"));
+			parameters.Add("perm_comment", permComment.ToString("d"));
+			parameters.Add("perm_addmeta", permAddMeta.ToString("d"));
+
+			FlickrNet.Response response = GetResponseNoCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+
+		}
+
+		/// <summary>
+		/// Set the tags for a photo.
+		/// </summary>
+		/// <remarks>
+		/// This will remove all old tags and add these new ones specified. See <see cref="PhotosAddTags(string, string)"/>
+		/// to just add new tags without deleting old ones.
+		/// </remarks>
+		/// <param name="photoId">The id of the photo to update.</param>
+		/// <param name="tags">An array of tags.</param>
+		/// <returns>True if the photo was updated successfully.</returns>
+		public bool PhotosSetTags(string photoId, string[] tags)
+		{
+			string s = string.Join(",", tags);
+			return PhotosSetTags(photoId, s);
+		}
+
+		/// <summary>
+		/// Set the tags for a photo.
+		/// </summary>
+		/// <remarks>
+		/// This will remove all old tags and add these new ones specified. See <see cref="PhotosAddTags(string, string)"/>
+		/// to just add new tags without deleting old ones.
+		/// </remarks>
+		/// <param name="photoId">The id of the photo to update.</param>
+		/// <param name="tags">An comma-seperated list of tags.</param>
+		/// <returns>True if the photo was updated successfully.</returns>
+		public bool PhotosSetTags(string photoId, string tags)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.photos.setTags");
+			parameters.Add("photo_id", photoId);
+			parameters.Add("tags", tags);
+
+			FlickrNet.Response response = GetResponseNoCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return true;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+
+		}
+
+		/// <summary>
+		/// Sets the content type for a photo.
+		/// </summary>
+		/// <param name="photoId">The ID of the photos to set.</param>
+		/// <param name="contentType">The new content type.</param>
+		public void PhotosSetContentType(string photoId, ContentType contentType)
+		{
+			CheckRequiresAuthentication();
+
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.photos.setContentType");
+			parameters.Add("photo_id", photoId);
+			parameters.Add("content_type", (int)contentType);
+
+			FlickrNet.Response response = GetResponseNoCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Set the safety level for a photo, but only set the hidden aspect.
+		/// </summary>
+		/// <param name="photoId">The ID of the photo to set the hidden property for.</param>
+		/// <param name="hidden">The new value of the hidden value.</param>
+		public void PhotosSetSafetyLevel(string photoId, HiddenFromSearch hidden)
+		{
+			PhotosSetSafetyLevel(photoId, SafetyLevel.None, hidden);
+		}
+
+		/// <summary>
+		/// Set the safety level for a photo.
+		/// </summary>
+		/// <param name="photoId">The ID of the photo to set the safety level property for.</param>
+		/// <param name="safetyLevel">The new value of the safety level value.</param>
+		public void PhotosSetSafetyLevel(string photoId, SafetyLevel safetyLevel)
+		{
+			PhotosSetSafetyLevel(photoId, safetyLevel, HiddenFromSearch.None);
+		}
+
+		/// <summary>
+		/// Sets the safety level and hidden property of a photo.
+		/// </summary>
+		/// <param name="photoId">The ID of the photos to set.</param>
+		/// <param name="safetyLevel">The new content type.</param>
+		/// <param name="hidden">The new hidden value.</param>
+		public void PhotosSetSafetyLevel(string photoId, SafetyLevel safetyLevel, HiddenFromSearch hidden)
+		{
+			CheckRequiresAuthentication();
+
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.photos.setSafetyLevel");
+			parameters.Add("photo_id", photoId);
+			if( safetyLevel != SafetyLevel.None ) parameters.Add("safety_level", (int)safetyLevel);
+			switch(hidden)
+			{
+				case HiddenFromSearch.Visible:
+					parameters.Add("hidden", 0);
+					break;
+				case HiddenFromSearch.Hidden:
+					parameters.Add("hidden", 1);
+					break;
+			}
+
+			FlickrNet.Response response = GetResponseNoCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+		#endregion
+
+		#region [ Photos Comments ]
+		/// <summary>
+		/// Gets a list of comments for a photo.
+		/// </summary>
+		/// <param name="photoId">The id of the photo to return the comments for.</param>
+		/// <returns>An array of <see cref="Comment"/> objects.</returns>
+		public Comment[] PhotosCommentsGetList(string photoId)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.photos.comments.getList");
+			parameters.Add("photo_id", photoId);
+
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return PhotoComments.GetComments(response.AllElements[0]);
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Adds a new comment to a photo.
+		/// </summary>
+		/// <param name="photoId">The ID of the photo to add the comment to.</param>
+		/// <param name="commentText">The text of the comment. Can contain some HTML.</param>
+		/// <returns>The new ID of the created comment.</returns>
+		public string PhotosCommentsAddComment(string photoId, string commentText)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.photos.comments.addComment");
+			parameters.Add("photo_id", photoId);
+			parameters.Add("comment_text", commentText);
+
+			FlickrNet.Response response = GetResponseNoCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				XmlNode node = response.AllElements[0];
+				if( node.Attributes.GetNamedItem("id") != null )
+					return node.Attributes.GetNamedItem("id").Value;
+				else
+					throw new ResponseXmlException("Comment ID not found in response Xml.");
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Deletes a comment from a photo.
+		/// </summary>
+		/// <param name="commentId">The ID of the comment to delete.</param>
+		public void PhotosCommentsDeleteComment(string commentId)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.photos.comments.deleteComment");
+			parameters.Add("comment_id", commentId);
+
+			FlickrNet.Response response = GetResponseNoCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Edits a comment.
+		/// </summary>
+		/// <param name="commentId">The ID of the comment to edit.</param>
+		/// <param name="commentText">The new text for the comment.</param>
+		public void PhotosCommentsEditComment(string commentId, string commentText)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.photos.comments.editComment");
+			parameters.Add("comment_id", commentId);
+			parameters.Add("comment_text", commentText);
+
+			FlickrNet.Response response = GetResponseNoCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+		#endregion
+
+		#region [ Photosets ]
+		/// <summary>
+		/// Add a photo to a photoset.
+		/// </summary>
+		/// <param name="photosetId">The ID of the photoset to add the photo to.</param>
+		/// <param name="photoId">The ID of the photo to add.</param>
+		public void PhotosetsAddPhoto(string photosetId, string photoId)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.photosets.addPhoto");
+			parameters.Add("photoset_id", photosetId);
+			parameters.Add("photo_id", photoId);
+
+			FlickrNet.Response response = GetResponseNoCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Creates a blank photoset, with a title and a primary photo (minimum requirements).
+		/// </summary>
+		/// <param name="title">The title of the photoset.</param>
+		/// <param name="primaryPhotoId">The ID of the photo which will be the primary photo for the photoset. This photo will also be added to the set.</param>
+		/// <returns>The <see cref="Photoset"/> that is created.</returns>
+		public Photoset PhotosetsCreate(string title, string primaryPhotoId)
+		{
+			return PhotosetsCreate(title, null, primaryPhotoId);
+		}
+
+		/// <summary>
+		/// Creates a blank photoset, with a title, description and a primary photo.
+		/// </summary>
+		/// <param name="title">The title of the photoset.</param>
+		/// <param name="description">THe description of the photoset.</param>
+		/// <param name="primaryPhotoId">The ID of the photo which will be the primary photo for the photoset. This photo will also be added to the set.</param>
+		/// <returns>The <see cref="Photoset"/> that is created.</returns>
+		public Photoset PhotosetsCreate(string title, string description, string primaryPhotoId)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.photosets.create");
+			parameters.Add("title", title);
+			parameters.Add("primary_photo_id", primaryPhotoId);
+			parameters.Add("description", description);
+
+			FlickrNet.Response response = GetResponseNoCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return response.Photoset;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+
+		}
+
+		/// <summary>
+		/// Deletes the specified photoset.
+		/// </summary>
+		/// <param name="photosetId">The ID of the photoset to delete.</param>
+		/// <returns>Returns true when the photoset has been deleted.</returns>
+		public bool PhotosetsDelete(string photosetId)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.photosets.delete");
+			parameters.Add("photoset_id", photosetId);
+
+			FlickrNet.Response response = GetResponseNoCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return true;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+
+		}
+
+		/// <summary>
+		/// Updates the title and description for a photoset.
+		/// </summary>
+		/// <param name="photosetId">The ID of the photoset to update.</param>
+		/// <param name="title">The new title for the photoset.</param>
+		/// <param name="description">The new description for the photoset.</param>
+		/// <returns>Returns true when the photoset has been updated.</returns>
+		public bool PhotosetsEditMeta(string photosetId, string title, string description)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.photosets.editMeta");
+			parameters.Add("photoset_id", photosetId);
+			parameters.Add("title", title);
+			parameters.Add("description", description);
+
+			FlickrNet.Response response = GetResponseNoCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return true;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+
+		}
+
+		/// <summary>
+		/// Sets the photos for a photoset.
+		/// </summary>
+		/// <remarks>
+		/// Will remove any previous photos from the photoset.
+		/// The order in thich the photoids are given is the order they will appear in the
+		/// photoset page.
+		/// </remarks>
+		/// <param name="photosetId">The ID of the photoset to update.</param>
+		/// <param name="primaryPhotoId">The ID of the new primary photo for the photoset.</param>
+		/// <param name="photoIds">An array of photo IDs.</param>
+		/// <returns>Returns true when the photoset has been updated.</returns>
+		public bool PhotosetsEditPhotos(string photosetId, string primaryPhotoId, string[] photoIds)
+		{
+			return PhotosetsEditPhotos(photosetId, primaryPhotoId, string.Join(",", photoIds));
+		}
+
+
+		/// <summary>
+		/// Sets the photos for a photoset.
+		/// </summary>
+		/// <remarks>
+		/// Will remove any previous photos from the photoset.
+		/// The order in thich the photoids are given is the order they will appear in the
+		/// photoset page.
+		/// </remarks>
+		/// <param name="photosetId">The ID of the photoset to update.</param>
+		/// <param name="primaryPhotoId">The ID of the new primary photo for the photoset.</param>
+		/// <param name="photoIds">An comma seperated list of photo IDs.</param>
+		/// <returns>Returns true when the photoset has been updated.</returns>
+		public bool PhotosetsEditPhotos(string photosetId, string primaryPhotoId, string photoIds)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.photosets.editPhotos");
+			parameters.Add("photoset_id", photosetId);
+			parameters.Add("primary_photo_id", primaryPhotoId);
+			parameters.Add("photo_ids", photoIds);
+
+			FlickrNet.Response response = GetResponseNoCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return true;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+
+		}
+
+		/// <summary>
+		/// Gets the context of the specified photo within the photoset.
+		/// </summary>
+		/// <param name="photoId">The photo id of the photo in the set.</param>
+		/// <param name="photosetId">The id of the set.</param>
+		/// <returns><see cref="Context"/> of the specified photo.</returns>
+		public Context PhotosetsGetContext(string photoId, string photosetId)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.photosets.getContext");
+			parameters.Add("photo_id", photoId);
+			parameters.Add("photoset_id", photosetId);
+
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				Context c = new Context();
+				c.Count = response.ContextCount.Count;
+				c.NextPhoto = response.ContextNextPhoto;
+				c.PreviousPhoto = response.ContextPrevPhoto;
+
+				return c;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Gets the information about a photoset.
+		/// </summary>
+		/// <param name="photosetId">The ID of the photoset to return information for.</param>
+		/// <returns>A <see cref="Photoset"/> instance.</returns>
+		public Photoset PhotosetsGetInfo(string photosetId)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.photosets.getInfo");
+			parameters.Add("photoset_id", photosetId);
+
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return response.Photoset;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+
+		}
+
+		/// <summary>
+		/// Gets a list of the currently authenticated users photosets.
+		/// </summary>
+		/// <returns>A <see cref="Photosets"/> instance containing a collection of photosets.</returns>
+		public Photosets PhotosetsGetList()
+		{
+			return PhotosetsGetList(null);
+		}
+
+		/// <summary>
+		/// Gets a list of the specified users photosets.
+		/// </summary>
+		/// <param name="userId">The ID of the user to return the photosets of.</param>
+		/// <returns>A <see cref="Photosets"/> instance containing a collection of photosets.</returns>
+		public Photosets PhotosetsGetList(string userId)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.photosets.getList");
+			if( userId != null ) parameters.Add("user_id", userId);
+
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return response.Photosets;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Gets a collection of photos for a photoset.
+		/// </summary>
+		/// <param name="photosetId">The ID of the photoset to return photos for.</param>
+		/// <returns>A <see cref="Photoset"/> object containing the list of <see cref="Photo"/> instances.</returns>
+		public Photoset PhotosetsGetPhotos(string photosetId)
+		{
+			return PhotosetsGetPhotos(photosetId, PhotoSearchExtras.All, PrivacyFilter.None, 0, 0);
+		}
+
+		/// <summary>
+		/// Gets a collection of photos for a photoset.
+		/// </summary>
+		/// <param name="photosetId">The ID of the photoset to return photos for.</param>
+		/// <param name="page">The page to return, defaults to 1.</param>
+		/// <param name="perPage">The number of photos to return per page.</param>
+		/// <returns>A <see cref="Photoset"/> object containing the list of <see cref="Photo"/> instances.</returns>
+		public Photoset PhotosetsGetPhotos(string photosetId, int page, int perPage)
+		{
+			return PhotosetsGetPhotos(photosetId, PhotoSearchExtras.All, PrivacyFilter.None, page, perPage);
+		}
+
+		/// <summary>
+		/// Gets a collection of photos for a photoset.
+		/// </summary>
+		/// <param name="photosetId">The ID of the photoset to return photos for.</param>
+		/// <param name="privacyFilter">The privacy filter to search on.</param>
+		/// <returns>A <see cref="Photoset"/> object containing the list of <see cref="Photo"/> instances.</returns>
+		public Photoset PhotosetsGetPhotos(string photosetId, PrivacyFilter privacyFilter)
+		{
+			return PhotosetsGetPhotos(photosetId, PhotoSearchExtras.All, privacyFilter, 0, 0);
+		}
+
+		/// <summary>
+		/// Gets a collection of photos for a photoset.
+		/// </summary>
+		/// <param name="photosetId">The ID of the photoset to return photos for.</param>
+		/// <param name="privacyFilter">The privacy filter to search on.</param>
+		/// <param name="page">The page to return, defaults to 1.</param>
+		/// <param name="perPage">The number of photos to return per page.</param>
+		/// <returns>A <see cref="Photoset"/> object containing the list of <see cref="Photo"/> instances.</returns>
+		public Photoset PhotosetsGetPhotos(string photosetId, PrivacyFilter privacyFilter, int page, int perPage)
+		{
+			return PhotosetsGetPhotos(photosetId, PhotoSearchExtras.All, privacyFilter, page, perPage);
+		}
+
+		/// <summary>
+		/// Gets a collection of photos for a photoset.
+		/// </summary>
+		/// <param name="photosetId">The ID of the photoset to return photos for.</param>
+		/// <param name="extras">The extras to return for each photo.</param>
+		/// <returns>A <see cref="Photoset"/> object containing the list of <see cref="Photo"/> instances.</returns>
+		public Photoset PhotosetsGetPhotos(string photosetId, PhotoSearchExtras extras)
+		{
+			return PhotosetsGetPhotos(photosetId, extras, PrivacyFilter.None, 0, 0);
+		}
+
+		/// <summary>
+		/// Gets a collection of photos for a photoset.
+		/// </summary>
+		/// <param name="photosetId">The ID of the photoset to return photos for.</param>
+		/// <param name="extras">The extras to return for each photo.</param>
+		/// <param name="page">The page to return, defaults to 1.</param>
+		/// <param name="perPage">The number of photos to return per page.</param>
+		/// <returns>A <see cref="Photoset"/> object containing the list of <see cref="Photo"/> instances.</returns>
+		public Photoset PhotosetsGetPhotos(string photosetId, PhotoSearchExtras extras, int page, int perPage)
+		{
+			return PhotosetsGetPhotos(photosetId, extras, PrivacyFilter.None, page, perPage);
+		}
+
+		/// <summary>
+		/// Gets a collection of photos for a photoset.
+		/// </summary>
+		/// <param name="photosetId">The ID of the photoset to return photos for.</param>
+		/// <param name="extras">The extras to return for each photo.</param>
+		/// <param name="privacyFilter">The privacy filter to search on.</param>
+		/// <returns>A <see cref="Photoset"/> object containing the list of <see cref="Photo"/> instances.</returns>
+		public Photoset PhotosetsGetPhotos(string photosetId, PhotoSearchExtras extras, PrivacyFilter privacyFilter)
+		{
+			return PhotosetsGetPhotos(photosetId, extras, privacyFilter, 0, 0);
+		}
+
+		/// <summary>
+		/// Gets a collection of photos for a photoset.
+		/// </summary>
+		/// <param name="photosetId">The ID of the photoset to return photos for.</param>
+		/// <param name="extras">The extras to return for each photo.</param>
+		/// <param name="privacyFilter">The privacy filter to search on.</param>
+		/// <param name="page">The page to return, defaults to 1.</param>
+		/// <param name="perPage">The number of photos to return per page.</param>
+		/// <returns>An array of <see cref="Photo"/> instances.</returns>
+		public Photoset PhotosetsGetPhotos(string photosetId, PhotoSearchExtras extras, PrivacyFilter privacyFilter, int page, int perPage)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.photosets.getPhotos");
+			parameters.Add("photoset_id", photosetId);
+			if( extras != PhotoSearchExtras.None ) parameters.Add("extras", Utils.ExtrasToString(extras));
+			if( privacyFilter != PrivacyFilter.None ) parameters.Add("privacy_filter", privacyFilter.ToString("d"));
+			if( page > 0 ) parameters.Add("page", page);
+			if( perPage > 0 ) parameters.Add("per_page", perPage);
+
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				if( response.Photoset.OwnerId != null && response.Photoset.OwnerId.Length > 0 )
+				{
+					foreach(Photo p in response.Photoset.PhotoCollection)
+					{
+						p.UserId = response.Photoset.OwnerId;
+					}
+				}
+				return response.Photoset;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Changes the order of your photosets.
+		/// </summary>
+		/// <param name="photosetIds">An array of photoset IDs,
+		/// ordered with the set to show first, first in the list.
+		/// Any set IDs not given in the list will be set to appear at the end of the list, ordered by their IDs.</param>
+		public void PhotosetsOrderSets(string[] photosetIds)
+		{
+			PhotosetsOrderSets(string.Join(",", photosetIds));
+		}
+
+		/// <summary>
+		/// Changes the order of your photosets.
+		/// </summary>
+		/// <param name="photosetIds">A comma delimited list of photoset IDs,
+		/// ordered with the set to show first, first in the list.
+		/// Any set IDs not given in the list will be set to appear at the end of the list, ordered by their IDs.</param>
+		public void PhotosetsOrderSets(string photosetIds)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.photosets.orderSets");
+			parameters.Add("photosetIds", photosetIds);
+
+			FlickrNet.Response response = GetResponseNoCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Removes a photo from a photoset.
+		/// </summary>
+		/// <remarks>
+		/// An exception will be raised if the photo is not in the set.
+		/// </remarks>
+		/// <param name="photosetId">The ID of the photoset to remove the photo from.</param>
+		/// <param name="photoId">The ID of the photo to remove.</param>
+		public void PhotosetsRemovePhoto(string photosetId, string photoId)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.photosets.removePhoto");
+			parameters.Add("photoset_id", photosetId);
+			parameters.Add("photo_id", photoId);
+
+			FlickrNet.Response response = GetResponseNoCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+		#endregion
+
+		#region [ Photoset Comments ]
+		/// <summary>
+		/// Gets a list of comments for a photoset.
+		/// </summary>
+		/// <param name="photosetId">The id of the photoset to return the comments for.</param>
+		/// <returns>An array of <see cref="Comment"/> objects.</returns>
+		public Comment[] PhotosetsCommentsGetList(string photosetId)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.photosets.comments.getList");
+			parameters.Add("photoset_id", photosetId);
+
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return PhotoComments.GetComments(response.AllElements[0]);
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Adds a new comment to a photoset.
+		/// </summary>
+		/// <param name="photosetId">The ID of the photoset to add the comment to.</param>
+		/// <param name="commentText">The text of the comment. Can contain some HTML.</param>
+		/// <returns>The new ID of the created comment.</returns>
+		public string PhotosetsCommentsAddComment(string photosetId, string commentText)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.photosets.comments.addComment");
+			parameters.Add("photoset_id", photosetId);
+			parameters.Add("comment_text", commentText);
+
+			FlickrNet.Response response = GetResponseNoCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				XmlNode node = response.AllElements[0];
+				if( node.Attributes.GetNamedItem("id") != null )
+					return node.Attributes.GetNamedItem("id").Value;
+				else
+					throw new ResponseXmlException("Comment ID not found in response.");
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Deletes a comment from a photoset.
+		/// </summary>
+		/// <param name="commentId">The ID of the comment to delete.</param>
+		public void PhotosetsCommentsDeleteComment(string commentId)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.photosets.comments.deleteComment");
+			parameters.Add("comment_id", commentId);
+
+			FlickrNet.Response response = GetResponseNoCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Edits a comment.
+		/// </summary>
+		/// <param name="commentId">The ID of the comment to edit.</param>
+		/// <param name="commentText">The new text for the comment.</param>
+		public void PhotosetsCommentsEditComment(string commentId, string commentText)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.photosets.comments.editComment");
+			parameters.Add("comment_id", commentId);
+			parameters.Add("comment_text", commentText);
+
+			FlickrNet.Response response = GetResponseNoCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+		#endregion
+
+		#region [ Prefs ]
+		/// <summary>
+		/// Gets the currently authenticated users default safety level.
+		/// </summary>
+		/// <returns></returns>
+		public SafetyLevel PrefsGetSafetyLevel()
+		{
+			CheckRequiresAuthentication();
+
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.prefs.getSafetyLevel");
+
+			Response res = GetResponseCache(parameters);
+			if( res.Status == ResponseStatus.OK )
+			{
+				string s = res.AllElements[0].GetAttribute("safety_level");
+				return (SafetyLevel)int.Parse(s);
+			}
+			else
+			{
+				throw new FlickrApiException(res.Error);
+			}
+		}
+
+		/// <summary>
+		/// Gets the currently authenticated users default hidden from search setting.
+		/// </summary>
+		/// <returns></returns>
+		public HiddenFromSearch PrefsGetHidden()
+		{
+			CheckRequiresAuthentication();
+
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.prefs.getHidden");
+
+			Response res = GetResponseCache(parameters);
+			if( res.Status == ResponseStatus.OK )
+			{
+				string s = res.AllElements[0].GetAttribute("hidden");
+				return (HiddenFromSearch)int.Parse(s);
+			}
+			else
+			{
+				throw new FlickrApiException(res.Error);
+			}
+		}
+
+		/// <summary>
+		/// Gets the currently authenticated users default content type.
+		/// </summary>
+		/// <returns></returns>
+		public ContentType PrefsGetContentType()
+		{
+			CheckRequiresAuthentication();
+
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.prefs.getContentType");
+
+			Response res = GetResponseCache(parameters);
+			if( res.Status == ResponseStatus.OK )
+			{
+				string s = res.AllElements[0].GetAttribute("content_type");
+				return (ContentType)int.Parse(s);
+			}
+			else
+			{
+				throw new FlickrApiException(res.Error);
+			}
+		}
+		#endregion
+
+		#region [ Tags ]
+		/// <summary>
+		/// Get the tag list for a given photo.
+		/// </summary>
+		/// <param name="photoId">The id of the photo to return tags for.</param>
+		/// <returns>An instance of the <see cref="PhotoInfo"/> class containing only the <see cref="PhotoInfo.Tags"/> property.</returns>
+		public PhotoInfoTag[] TagsGetListPhoto(string photoId)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.tags.getListPhoto");
+			parameters.Add("api_key", _apiKey);
+			parameters.Add("photo_id", photoId);
+
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return response.PhotoInfo.Tags.TagCollection;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Get the tag list for a given user (or the currently logged in user).
+		/// </summary>
+		/// <returns>An array of <see cref="Tag"/> objects.</returns>
+		public Tag[] TagsGetListUser()
+		{
+			return TagsGetListUser(null);
+		}
+
+		/// <summary>
+		/// Get the tag list for a given user (or the currently logged in user).
+		/// </summary>
+		/// <param name="userId">The NSID of the user to fetch the tag list for. If this argument is not specified, the currently logged in user (if any) is assumed.</param>
+		/// <returns>An array of <see cref="Tag"/> objects.</returns>
+		public Tag[] TagsGetListUser(string userId)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.tags.getListUser");
+			if( userId != null && userId.Length > 0 ) parameters.Add("user_id", userId);
+
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				XmlNodeList nodes = response.AllElements[0].SelectNodes("//tag");
+				Tag[] tags = new Tag[nodes.Count];
+				for(int i = 0; i < tags.Length; i++)
+				{
+					tags[i] = new Tag(nodes[i]);
+				}
+				return tags;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Get the popular tags for a given user (or the currently logged in user).
+		/// </summary>
+		/// <returns>An array of <see cref="Tag"/> objects.</returns>
+		public Tag[] TagsGetListUserPopular()
+		{
+			return TagsGetListUserPopular(null, 0);
+		}
+
+		/// <summary>
+		/// Get the popular tags for a given user (or the currently logged in user).
+		/// </summary>
+		/// <param name="count">Number of popular tags to return. defaults to 10 when this argument is not present.</param>
+		/// <returns>An array of <see cref="Tag"/> objects.</returns>
+		public Tag[] TagsGetListUserPopular(int count)
+		{
+			return TagsGetListUserPopular(null, count);
+		}
+
+		/// <summary>
+		/// Get the popular tags for a given user (or the currently logged in user).
+		/// </summary>
+		/// <param name="userId">The NSID of the user to fetch the tag list for. If this argument is not specified, the currently logged in user (if any) is assumed.</param>
+		/// <returns>An array of <see cref="Tag"/> objects.</returns>
+		public Tag[] TagsGetListUserPopular(string userId)
+		{
+			return TagsGetListUserPopular(userId, 0);
+		}
+
+		/// <summary>
+		/// Get the popular tags for a given user (or the currently logged in user).
+		/// </summary>
+		/// <param name="userId">The NSID of the user to fetch the tag list for. If this argument is not specified, the currently logged in user (if any) is assumed.</param>
+		/// <param name="count">Number of popular tags to return. defaults to 10 when this argument is not present.</param>
+		/// <returns>An array of <see cref="Tag"/> objects.</returns>
+		public Tag[] TagsGetListUserPopular(string userId, long count)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.tags.getListUserPopular");
+			if( userId != null ) parameters.Add("user_id", userId);
+			if( count > 0 ) parameters.Add("count", count.ToString());
+
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				XmlNodeList nodes = response.AllElements[0].SelectNodes("//tag");
+				Tag[] tags = new Tag[nodes.Count];
+				for(int i = 0; i < tags.Length; i++)
+				{
+					tags[i] = new Tag(nodes[i]);
+				}
+				return tags;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Returns a list of tags 'related' to the given tag, based on clustered usage analysis.
+		/// </summary>
+		/// <param name="tag">The tag to fetch related tags for.</param>
+		/// <returns>An array of <see cref="Tag"/> objects.</returns>
+		public Tag[] TagsGetRelated(string tag)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.tags.getRelated");
+			parameters.Add("api_key", _apiKey);
+			parameters.Add("tag", tag);
+
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				XmlNodeList nodes = response.AllElements[0].SelectNodes("//tag");
+				Tag[] tags = new Tag[nodes.Count];
+				for(int i = 0; i < tags.Length; i++)
+				{
+					tags[i] = new Tag(nodes[i]);
+				}
+				return tags;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		#endregion
+
+		#region [ Transform ]
+
+		/// <summary>
+		/// Rotates a photo on Flickr.
+		/// </summary>
+		/// <remarks>
+		/// Does not rotate the original photo.
+		/// </remarks>
+		/// <param name="photoId">The ID of the photo.</param>
+		/// <param name="degrees">The number of degrees to rotate by. Valid values are 90, 180 and 270.</param>
+		public void TransformRotate(string photoId, int degrees)
+		{
+			if( photoId == null )
+				throw new ArgumentNullException("photoId");
+			if( degrees != 90 && degrees != 180 && degrees != 270 )
+				throw new ArgumentException("Must be 90, 180 or 270", "degrees");
+
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.photos.transform.rotate");
+			parameters.Add("photo_id", photoId);
+			parameters.Add("degrees", degrees.ToString("0"));
+
+			FlickrNet.Response response = GetResponseNoCache(parameters);
+			if( response.Status == ResponseStatus.OK )
+			{
+				return;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		#endregion
+
+		#region	[ Geo ]
+		/// <summary>
+		/// Returns the location data for a give photo.
+		/// </summary>
+		/// <param name="photoId">The ID of the photo to return the location information for.</param>
+		/// <returns>Returns null if the photo has no location information, otherwise returns the location information.</returns>
+		public PhotoLocation PhotosGeoGetLocation(string photoId)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.photos.geo.getLocation");
+			parameters.Add("photo_id", photoId);
+
+			FlickrNet.Response response = GetResponseCache(parameters);
+			if( response.Status == ResponseStatus.OK )
+			{
+				return response.PhotoInfo.Location;
+			}
+			else
+			{
+				if( response.Error.Code == 2 )
+					return null;
+				else
+					throw new FlickrApiException(response.Error);
+			}
+		}
+		/// <summary>
+		/// Sets the geo location for a photo.
+		/// </summary>
+		/// <param name="photoId">The ID of the photo to set to location for.</param>
+		/// <param name="latitude">The latitude of the geo location. A double number ranging from -180.00 to 180.00. Digits beyond 6 decimal places will be truncated.</param>
+		/// <param name="longitude">The longitude of the geo location. A double number ranging from -180.00 to 180.00. Digits beyond 6 decimal places will be truncated.</param>
+		public void PhotosGeoSetLocation(string photoId, double latitude, double longitude)
+		{
+			PhotosGeoSetLocation(photoId, latitude, longitude, GeoAccuracy.None);
+		}
+
+		/// <summary>
+		/// Sets the geo location for a photo.
+		/// </summary>
+		/// <param name="photoId">The ID of the photo to set to location for.</param>
+		/// <param name="latitude">The latitude of the geo location. A double number ranging from -180.00 to 180.00. Digits beyond 6 decimal places will be truncated.</param>
+		/// <param name="longitude">The longitude of the geo location. A double number ranging from -180.00 to 180.00. Digits beyond 6 decimal places will be truncated.</param>
+		/// <param name="accuracy">The accuracy of the photos geo location.</param>
+		public void PhotosGeoSetLocation(string photoId, double latitude, double longitude, GeoAccuracy accuracy)
+		{
+			System.Globalization.NumberFormatInfo nfi = System.Globalization.NumberFormatInfo.InvariantInfo;
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.photos.geo.setLocation");
+			parameters.Add("photo_id", photoId);
+			parameters.Add("lat", latitude.ToString(nfi));
+			parameters.Add("lon", longitude.ToString(nfi));
+			if( accuracy != GeoAccuracy.None )
+				parameters.Add("accuracy", ((int)accuracy).ToString());
+
+			FlickrNet.Response response = GetResponseNoCache(parameters);
+			if( response.Status == ResponseStatus.OK )
+			{
+				return;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Removes Location information.
+		/// </summary>
+		/// <param name="photoId">The photo ID of the photo to remove information from.</param>
+		/// <returns>Returns true if the location information as found and removed. Returns false if no photo information was found.</returns>
+		public bool PhotosGeoRemoveLocation(string photoId)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.photos.geo.removeLocation");
+			parameters.Add("photo_id", photoId);
+
+			FlickrNet.Response response = GetResponseNoCache(parameters);
+			if( response.Status == ResponseStatus.OK )
+			{
+				return true;
+			}
+			else
+			{
+				if( response.Error.Code == 2 )
+					return false;
+				else
+					throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Gets a list of photos that do not contain geo location information.
+		/// </summary>
+		/// <returns>A list of photos that do not contain location information.</returns>
+		public Photos PhotosGetWithoutGeoData()
+		{
+			PartialSearchOptions options = new PartialSearchOptions();
+			return PhotosGetWithoutGeoData(options);
+		}
+
+		/// <summary>
+		/// Gets a list of photos that do not contain geo location information.
+		/// </summary>
+		/// <param name="options">A limited set of options are supported.</param>
+		/// <returns>A list of photos that do not contain location information.</returns>
+		public Photos PhotosGetWithoutGeoData(PartialSearchOptions options)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.photos.getWithoutGeoData");
+			Utils.PartialOptionsIntoArray(options, parameters);
+
+			FlickrNet.Response response = GetResponseCache(parameters);
+			if( response.Status == ResponseStatus.OK )
+			{
+				return response.Photos;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Gets a list of photos that do not contain geo location information.
+		/// </summary>
+		/// <param name="options">A limited set of options are supported.
+		/// Unsupported arguments are ignored.
+		/// See http://www.flickr.com/services/api/flickr.photos.getWithGeoData.html for supported properties.</param>
+		/// <returns>A list of photos that do not contain location information.</returns>
+		[Obsolete("Use the PartialSearchOptions instead")]
+		public Photos PhotosGetWithoutGeoData(PhotoSearchOptions options)
+		{
+			PartialSearchOptions newOptions = new PartialSearchOptions(options);
+			return PhotosGetWithoutGeoData(newOptions);
+		}
+
+		/// <summary>
+		/// Gets a list of photos that contain geo location information.
+		/// </summary>
+		/// <remarks>
+		/// Note, this method doesn't actually return the location information with the photos,
+		/// unless you specify the <see cref="PhotoSearchExtras.Geo"/> option in the <c>extras</c> parameter.
+		/// </remarks>
+		/// <returns>A list of photos that contain Location information.</returns>
+		public Photos PhotosGetWithGeoData()
+		{
+			PartialSearchOptions options = new PartialSearchOptions();
+			return PhotosGetWithGeoData(options);
+		}
+
+		/// <summary>
+		/// Gets a list of photos that contain geo location information.
+		/// </summary>
+		/// <remarks>
+		/// Note, this method doesn't actually return the location information with the photos,
+		/// unless you specify the <see cref="PhotoSearchExtras.Geo"/> option in the <c>extras</c> parameter.
+		/// </remarks>
+		/// <param name="options">A limited set of options are supported.
+		/// Unsupported arguments are ignored.
+		/// See http://www.flickr.com/services/api/flickr.photos.getWithGeoData.html for supported properties.</param>
+		/// <returns>A list of photos that contain Location information.</returns>
+		[Obsolete("Use the new PartialSearchOptions instead")]
+		public Photos PhotosGetWithGeoData(PhotoSearchOptions options)
+		{
+			PartialSearchOptions newOptions = new PartialSearchOptions(options);
+			return PhotosGetWithGeoData(newOptions);
+		}
+
+		/// <summary>
+		/// Gets a list of photos that contain geo location information.
+		/// </summary>
+		/// <remarks>
+		/// Note, this method doesn't actually return the location information with the photos,
+		/// unless you specify the <see cref="PhotoSearchExtras.Geo"/> option in the <c>extras</c> parameter.
+		/// </remarks>
+		/// <param name="options">The options to filter/sort the results by.</param>
+		/// <returns>A list of photos that contain Location information.</returns>
+		public Photos PhotosGetWithGeoData(PartialSearchOptions options)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.photos.getWithGeoData");
+			Utils.PartialOptionsIntoArray(options, parameters);
+
+			FlickrNet.Response response = GetResponseCache(parameters);
+			if( response.Status == ResponseStatus.OK )
+			{
+				return response.Photos;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Get permissions for a photo.
+		/// </summary>
+		/// <param name="photoId">The id of the photo to get permissions for.</param>
+		/// <returns>An instance of the <see cref="PhotoPermissions"/> class containing the permissions of the specified photo.</returns>
+		public GeoPermissions PhotosGeoGetPerms(string photoId)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.photos.geo.getPerms");
+			parameters.Add("photo_id", photoId);
+
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return new GeoPermissions(response.AllElements[0]);
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Set the permission for who can see geotagged photos on Flickr.
+		/// </summary>
+		/// <param name="photoId">The ID of the photo permissions to update.</param>
+		/// <param name="IsPublic"></param>
+		/// <param name="IsContact"></param>
+		/// <param name="IsFamily"></param>
+		/// <param name="IsFriend"></param>
+		public void PhotosGeoSetPerms(string photoId, bool IsPublic, bool IsContact, bool IsFamily, bool IsFriend)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.photos.geo.setPerms");
+			parameters.Add("photo_id", photoId);
+			parameters.Add("is_public", IsPublic?"1":"0");
+			parameters.Add("is_contact", IsContact?"1":"0");
+			parameters.Add("is_friend", IsFriend?"1":"0");
+			parameters.Add("is_family", IsFamily?"1":"0");
+
+			FlickrNet.Response response = GetResponseNoCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+
+		#endregion
+
+		#region [ Tests ]
+		/// <summary>
+		/// Can be used to call unsupported methods in the Flickr API.
+		/// </summary>
+		/// <remarks>
+		/// Use of this method is not supported.
+		/// The way the FlickrNet API Library works may mean that some methods do not return an expected result
+		/// when using this method.
+		/// </remarks>
+		/// <param name="method">The method name, e.g. "flickr.test.null".</param>
+		/// <param name="parameters">A list of parameters. Note, api_key is added by default and is not included. Can be null.</param>
+		/// <returns>An array of <see cref="XmlElement"/> instances which is the expected response.</returns>
+		public XmlElement[] TestGeneric(string method, NameValueCollection parameters)
+		{
+			Hashtable _parameters = new Hashtable();
+			if( parameters != null )
+			{
+				foreach(string key in parameters.AllKeys)
+				{
+					_parameters.Add(key, parameters[key]);
+				}
+			}
+			_parameters.Add("method", method);
+
+			FlickrNet.Response response = GetResponseNoCache(_parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return response.AllElements;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+		/// <summary>
+		/// Runs the flickr.test.echo method and returned an array of <see cref="XmlElement"/> items.
+		/// </summary>
+		/// <param name="echoParameter">The parameter to pass to the method.</param>
+		/// <param name="echoValue">The value to pass to the method with the parameter.</param>
+		/// <returns>An array of <see cref="XmlElement"/> items.</returns>
+		/// <remarks>
+		/// The APi Key has been removed from the returned array and will not be shown.
+		/// </remarks>
+		/// <example>
+		/// <code>
+		/// XmlElement[] elements = flickr.TestEcho("&amp;param=value");
+		/// foreach(XmlElement element in elements)
+		/// {
+		///		if( element.Name = "method" )
+		///			Console.WriteLine("Method = " + element.InnerXml);
+		///		if( element.Name = "param" )
+		///			Console.WriteLine("Param = " + element.InnerXml);
+		/// }
+		/// </code>
+		/// </example>
+		public XmlElement[] TestEcho(string echoParameter, string echoValue)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.test.echo");
+			parameters.Add("api_key", _apiKey);
+			if( echoParameter != null && echoParameter.Length > 0 )
+			{
+				parameters.Add(echoParameter, echoValue);
+			}
+
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				// Remove the api_key element from the array.
+				XmlElement[] elements = new XmlElement[response.AllElements.Length - 1];
+				int c = 0;
+				foreach(XmlElement element in response.AllElements)
+				{
+					if(element.Name != "api_key" )
+						elements[c++] = element;
+				}
+				return elements;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Test the logged in state of the current Filckr object.
+		/// </summary>
+		/// <returns>The <see cref="FoundUser"/> object containing the username and userid of the current user.</returns>
+		public FoundUser TestLogin()
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.test.login");
+
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return new FoundUser(response.AllElements[0]);
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+		#endregion
+
+		#region [ Urls ]
+		/// <summary>
+		/// Returns the url to a group's page.
+		/// </summary>
+		/// <param name="groupId">The NSID of the group to fetch the url for.</param>
+		/// <returns>An instance of the <see cref="Uri"/> class containing the URL of the group page.</returns>
+		public Uri UrlsGetGroup(string groupId)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.urls.getGroup");
+			parameters.Add("api_key", _apiKey);
+			parameters.Add("group_id", groupId);
+
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				if( response.AllElements[0] != null && response.AllElements[0].Attributes["url"] != null )
+					return new Uri(response.AllElements[0].Attributes["url"].Value);
+				else
+					return null;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Returns the url to a user's photos.
+		/// </summary>
+		/// <returns>An instance of the <see cref="Uri"/> class containing the URL for the users photos.</returns>
+		public Uri UrlsGetUserPhotos()
+		{
+			return UrlsGetUserPhotos(null);
+		}
+
+		/// <summary>
+		/// Returns the url to a user's photos.
+		/// </summary>
+		/// <param name="userId">The NSID of the user to fetch the url for. If omitted, the calling user is assumed.</param>
+		/// <returns>The URL of the users photos.</returns>
+		public Uri UrlsGetUserPhotos(string userId)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.urls.getUserPhotos");
+			if( userId != null && userId.Length > 0 ) parameters.Add("user_id", userId);
+
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				if( response.AllElements[0] != null && response.AllElements[0].Attributes["url"] != null )
+					return new Uri(response.AllElements[0].Attributes["url"].Value);
+				else
+					return null;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Returns the url to a user's profile.
+		/// </summary>
+		/// <returns>An instance of the <see cref="Uri"/> class containing the URL for the users profile.</returns>
+		public Uri UrlsGetUserProfile()
+		{
+			return UrlsGetUserProfile(null);
+		}
+
+		/// <summary>
+		/// Returns the url to a user's profile.
+		/// </summary>
+		/// <param name="userId">The NSID of the user to fetch the url for. If omitted, the calling user is assumed.</param>
+		/// <returns>An instance of the <see cref="Uri"/> class containing the URL for the users profile.</returns>
+		public Uri UrlsGetUserProfile(string userId)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.urls.getUserProfile");
+			if( userId != null && userId.Length > 0 ) parameters.Add("user_id", userId);
+
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				if( response.AllElements[0] != null && response.AllElements[0].Attributes["url"] != null )
+					return new Uri(response.AllElements[0].Attributes["url"].Value);
+				else
+					return null;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Returns a group NSID, given the url to a group's page or photo pool.
+		/// </summary>
+		/// <param name="urlToFind">The url to the group's page or photo pool.</param>
+		/// <returns>The ID of the group at the specified URL on success, a null reference (Nothing in Visual Basic) if the group cannot be found.</returns>
+		public string UrlsLookupGroup(string urlToFind)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.urls.lookupGroup");
+			parameters.Add("api_key", _apiKey);
+			parameters.Add("url", urlToFind);
+
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				if( response.AllElements[0] != null && response.AllElements[0].Attributes["id"] != null )
+				{
+					return response.AllElements[0].Attributes["id"].Value;
+				}
+				else
+				{
+					return null;
+				}
+			}
+			else
+			{
+				if( response.Error.Code == 1 )
+					return null;
+				else
+					throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Returns a user NSID, given the url to a user's photos or profile.
+		/// </summary>
+		/// <param name="urlToFind">Thr url to the user's profile or photos page.</param>
+		/// <returns>An instance of the <see cref="FoundUser"/> class containing the users ID and username.</returns>
+		public FoundUser UrlsLookupUser(string urlToFind)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.urls.lookupUser");
+			parameters.Add("api_key", _apiKey);
+			parameters.Add("url", urlToFind);
+
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return new FoundUser(response.AllElements[0]);
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+		#endregion
+
+		#region [ Reflection ]
+		/// <summary>
+		/// Gets an array of supported method names for Flickr.
+		/// </summary>
+		/// <remarks>
+		/// Note: Not all methods might be supported by the FlickrNet Library.</remarks>
+		/// <returns></returns>
+		public string[] ReflectionGetMethods()
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.reflection.getMethods");
+			parameters.Add("api_key", _apiKey);
+
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return Methods.GetMethods(response.AllElements[0]);
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		/// <summary>
+		/// Gets the method details for a given method.
+		/// </summary>
+		/// <param name="methodName">The name of the method to retrieve.</param>
+		/// <returns>Returns a <see cref="Method"/> instance for the given method name.</returns>
+		public Method ReflectionGetMethodInfo(string methodName)
+		{
+			Hashtable parameters = new Hashtable();
+			parameters.Add("method", "flickr.reflection.getMethodInfo");
+			parameters.Add("api_key", _apiKey);
+			parameters.Add("method_name", methodName);
+
+			FlickrNet.Response response = GetResponseCache(parameters);
+
+			if( response.Status == ResponseStatus.OK )
+			{
+				return response.Method;
+			}
+			else
+			{
+				throw new FlickrApiException(response.Error);
+			}
+		}
+
+		#endregion
+
+		#region [ MD5 Hash ]
+		private static string Md5Hash(string unhashed)
+		{
+			System.Security.Cryptography.MD5CryptoServiceProvider csp = new System.Security.Cryptography.MD5CryptoServiceProvider();
+			byte[] bytes = System.Text.Encoding.UTF8.GetBytes(unhashed);
+			byte[] hashedBytes = csp.ComputeHash(bytes, 0, bytes.Length);
+			return BitConverter.ToString(hashedBytes).Replace("-", "").ToLower();
+		}
+		#endregion
+
+		private void CheckApiKey()
+		{
+			if( ApiKey == null || ApiKey.Length == 0 )
+				throw new ApiKeyRequiredException();
+		}
+		private void CheckRequiresAuthentication()
+		{
+			CheckApiKey();
+
+			if( ApiSecret == null || ApiSecret.Length == 0 )
+				throw new SignatureRequiredException();
+			if( AuthToken == null || AuthToken.Length == 0 )
+				throw new AuthenticationRequiredException();
+
+		}
+	}
+
+}

Added: trunk/extensions/Exporters/FlickrExport/FlickrNet/FlickrApiException.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FlickrExport/FlickrNet/FlickrApiException.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,46 @@
+using System;
+
+namespace FlickrNet
+{
+	/// <summary>
+	/// Exception thrown when the Flickr API returned a specifi error code.
+	/// </summary>
+	public class FlickrApiException : FlickrException
+	{
+		private int code;
+		private string msg = "";
+
+		internal FlickrApiException(ResponseError error)
+		{
+			code = error.Code;
+			msg = error.Message;
+		}
+
+		/// <summary>
+		/// Get the code of the Flickr error.
+		/// </summary>
+		public int Code
+		{
+			get { return code; }
+		}
+
+		/// <summary>
+		/// Gets the verbose message returned by Flickr.
+		/// </summary>
+		public string Verbose
+		{
+			get { return msg; }
+		}
+
+		/// <summary>
+		/// Overrides the message to return custom error message.
+		/// </summary>
+		public override string Message
+		{
+			get
+			{
+				return msg + " (" + code + ")";
+			}
+		}
+	}
+}

Added: trunk/extensions/Exporters/FlickrExport/FlickrNet/FlickrConfigurationManager.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FlickrExport/FlickrNet/FlickrConfigurationManager.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,40 @@
+using System;
+using System.Configuration;
+using System.Xml;
+
+#if !WindowsCE
+namespace FlickrNet
+{
+	/// <summary>
+	/// Summary description for FlickrConfigurationManager.
+	/// </summary>
+	internal class FlickrConfigurationManager : IConfigurationSectionHandler
+	{
+		private static string ConfigSection = "flickrNet";
+		private static FlickrConfigurationSettings settings;
+
+		public FlickrConfigurationManager()
+		{
+		}
+
+		public static FlickrConfigurationSettings Settings
+		{
+			get
+			{
+				if( settings == null )
+				{
+					settings = (FlickrConfigurationSettings)ConfigurationSettings.GetConfig( ConfigSection );
+				}
+
+				return settings;
+			}
+		}
+
+		public object Create(object parent, object configContext, XmlNode section)
+		{
+			ConfigSection = section.Name;
+			return new FlickrConfigurationSettings( section );
+		}
+	}
+}
+#endif
\ No newline at end of file

Added: trunk/extensions/Exporters/FlickrExport/FlickrNet/FlickrConfigurationSettings.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FlickrExport/FlickrNet/FlickrConfigurationSettings.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,306 @@
+#if !WindowsCE
+using System;
+using System.Collections.Specialized ;
+using System.Xml;
+
+namespace FlickrNet
+{
+	/// <summary>
+	/// Configuration settings for the Flickr.Net API Library.
+	/// </summary>
+	/// <remarks>
+	/// <p>First, register the configuration section in the configSections section:</p>
+	/// <p><code>&lt;configSections&gt;
+	/// &lt;section name="flickrNet" type="FlickrNet.FlickrConfigurationManager,FlickrNet"/&gt;
+	/// &lt;/configSections&gt;</code>
+	/// </p>
+	/// <p>
+	/// Next, include the following config section:
+	/// </p>
+	/// <p><code>
+	/// 	&lt;flickrNet
+	/// apiKey="1234567890abc"	// optional
+	/// secret="2134123"		// optional
+	/// token="234234"			// optional
+	/// cacheSize="1234"		// optional, in bytes (defaults to 50 * 1024 * 1024 = 50MB)
+	/// cacheTimeout="[d.]HH:mm:ss"	// optional, defaults to 1 day (1.00:00:00) - day component is optional
+	/// // e.g. 10 minutes = 00:10:00 or 1 hour = 01:00:00 or 2 days, 12 hours = 2.12:00:00
+	/// &gt;
+	/// &lt;proxy		// proxy element is optional, but if included the attributes below are mandatory as mentioned
+	/// ipaddress="127.0.0.1"	// mandatory
+	/// port="8000"				// mandatory
+	/// username="username"		// optional, but must have password if included
+	/// password="password"		// optional, see username
+	/// domain="domain"			// optional, used for Microsoft authenticated proxy servers
+	/// /&gt;
+	/// &lt;/flickrNet&gt;
+	/// </code></p>
+	/// </remarks>
+	internal class FlickrConfigurationSettings
+	{
+		#region Private Variables
+		private string _apiKey;
+		private string _apiSecret;
+		private string _apiToken;
+		private int _cacheSize;
+		private TimeSpan _cacheTimeout = TimeSpan.MinValue;
+		private string _proxyAddress;
+		private int _proxyPort;
+		private bool _proxyDefined;
+		private string _proxyUsername;
+		private string _proxyPassword;
+		private string _proxyDomain;
+		private string _cacheLocation;
+		private bool _cacheDisabled;
+		private SupportedService _service;
+		#endregion
+
+		#region FlickrConfigurationSettings Constructor
+		/// <summary>
+		/// Loads FlickrConfigurationSettings with the settings in the config file.
+		/// </summary>
+		/// <param name="configNode">XmlNode containing the configuration settings.</param>
+		public FlickrConfigurationSettings(XmlNode configNode)
+		{
+			if( configNode == null ) throw new ArgumentNullException("configNode");
+
+			foreach(XmlAttribute attribute in configNode.Attributes)
+			{
+				switch(attribute.Name)
+				{
+					case "apiKey":
+						_apiKey = attribute.Value;
+						break;
+					case "secret":
+						_apiSecret = attribute.Value;
+						break;
+					case "token":
+						_apiToken = attribute.Value;
+						break;
+					case "cacheDisabled":
+						try
+						{
+							_cacheDisabled = bool.Parse(attribute.Value);
+							break;
+						}
+						catch(FormatException ex)
+						{
+							throw new System.Configuration.ConfigurationException("cacheDisbled should be \"true\" or \"false\"", ex, configNode);
+						}
+					case "cacheSize":
+						try
+						{
+							_cacheSize = int.Parse(attribute.Value);
+							break;
+						}
+						catch(FormatException ex)
+						{
+							throw new System.Configuration.ConfigurationException("cacheSize should be integer value", ex, configNode);
+						}
+					case "cacheTimeout":
+						try
+						{
+							_cacheTimeout = TimeSpan.Parse(attribute.Value);
+							break;
+						}
+						catch(FormatException ex)
+						{
+							throw new System.Configuration.ConfigurationException("cacheTimeout should be TimeSpan value ([d:]HH:mm:ss)", ex, configNode);
+						}
+					case "cacheLocation":
+						_cacheLocation = attribute.Value;
+						break;
+
+					case "service":
+						try
+						{
+							_service = (SupportedService)Enum.Parse(typeof(SupportedService), attribute.Value, true);
+							break;
+						}
+						catch(ArgumentException ex)
+						{
+							throw new System.Configuration.ConfigurationException("service must be one of the supported services (See SupportedServices enum)", ex, configNode);
+						}
+
+					default:
+						throw new System.Configuration.ConfigurationException(String.Format("Unknown attribute '{0}' in flickrNet node", attribute.Name), configNode);
+				}
+			}
+
+			foreach(XmlNode node in configNode.ChildNodes)
+			{
+				switch(node.Name)
+				{
+					case "proxy":
+						ProcessProxyNode(node, configNode);
+						break;
+					default:
+						throw new System.Configuration.ConfigurationException(String.Format("Unknown node '{0}' in flickrNet node", node.Name), configNode);
+				}
+			}
+		}
+		#endregion
+
+		#region ProcessProxyNode - Constructor Helper Method
+		private void ProcessProxyNode(XmlNode proxy, XmlNode configNode)
+		{
+			if( proxy.ChildNodes.Count > 0 )
+				throw new System.Configuration.ConfigurationException("proxy element does not support child elements");
+
+			_proxyDefined = true;
+			foreach(XmlAttribute attribute in proxy.Attributes)
+			{
+
+				switch(attribute.Name)
+				{
+					case "ipaddress":
+						_proxyAddress = attribute.Value;
+						break;
+					case "port":
+						try
+						{
+							_proxyPort = int.Parse(attribute.Value);
+						}
+						catch(FormatException ex)
+						{
+							throw new System.Configuration.ConfigurationException("proxy port should be integer value", ex, configNode);
+						}
+						break;
+					case "username":
+						_proxyUsername = attribute.Value;
+						break;
+					case "password":
+						_proxyPassword = attribute.Value;
+						break;
+					case "domain":
+						_proxyDomain = attribute.Value;
+						break;
+					default:
+						throw new System.Configuration.ConfigurationException(String.Format("Unknown attribute '{0}' in flickrNet/proxy node", attribute.Name), configNode);
+				}
+			}
+
+			if( _proxyAddress == null )
+				throw new System.Configuration.ConfigurationException("proxy ipaddress is mandatory if you specify the proxy element");
+			if( _proxyPort == 0 )
+				throw new System.Configuration.ConfigurationException("proxy port is mandatory if you specify the proxy element");
+			if( _proxyUsername != null && _proxyPassword == null )
+				throw new System.Configuration.ConfigurationException("proxy password must be specified if proxy username is specified");
+			if( _proxyUsername == null && _proxyPassword != null )
+				throw new System.Configuration.ConfigurationException("proxy username must be specified if proxy password is specified");
+			if( _proxyDomain != null && _proxyUsername == null )
+				throw new System.Configuration.ConfigurationException("proxy username/password must be specified if proxy domain is specified");
+		}
+		#endregion
+
+		#region Public Properties
+		/// <summary>
+		/// API key. Null if not present. Optional.
+		/// </summary>
+		public string ApiKey
+		{
+			get { return _apiKey; }
+		}
+
+		/// <summary>
+		/// Shared Secret. Null if not present. Optional.
+		/// </summary>
+		public string SharedSecret
+		{
+			get { return _apiSecret; }
+		}
+
+		/// <summary>
+		/// API token. Null if not present. Optional.
+		/// </summary>
+		public string ApiToken
+		{
+			get { return _apiToken; }
+		}
+
+		/// <summary>
+		/// Cache size in bytes. 0 if not present. Optional.
+		/// </summary>
+		public bool CacheDisabled
+		{
+			get { return _cacheDisabled; }
+		}
+
+		/// <summary>
+		/// Cache size in bytes. 0 if not present. Optional.
+		/// </summary>
+		public int CacheSize
+		{
+			get { return _cacheSize; }
+		}
+
+		/// <summary>
+		/// Cache timeout. Equals TimeSpan.MinValue is not present. Optional.
+		/// </summary>
+		public TimeSpan CacheTimeout
+		{
+			get { return _cacheTimeout; }
+		}
+
+		public string CacheLocation
+		{
+			get { return _cacheLocation; }
+		}
+
+		public SupportedService Service
+		{
+			get { return _service; }
+		}
+
+		/// <summary>
+		/// If the proxy is defined in the configuration section.
+		/// </summary>
+		public bool IsProxyDefined
+		{
+			get { return _proxyDefined; }
+		}
+
+		/// <summary>
+		/// If <see cref="IsProxyDefined"/> is true then this is mandatory.
+		/// </summary>
+		public string ProxyIPAddress
+		{
+			get { return _proxyAddress; }
+		}
+
+		/// <summary>
+		/// If <see cref="IsProxyDefined"/> is true then this is mandatory.
+		/// </summary>
+		public int ProxyPort
+		{
+			get { return _proxyPort; }
+		}
+
+		/// <summary>
+		/// The username for the proxy. Optional.
+		/// </summary>
+		public string ProxyUsername
+		{
+			get { return _proxyUsername; }
+		}
+
+		/// <summary>
+		/// The password for the proxy. Optional.
+		/// </summary>
+		public string ProxyPassword
+		{
+			get { return _proxyPassword; }
+		}
+
+		/// <summary>
+		/// The domain for the proxy. Optional.
+		/// </summary>
+		public string ProxyDomain
+		{
+			get { return _proxyDomain; }
+		}
+		#endregion
+
+	}
+}
+#endif
\ No newline at end of file

Added: trunk/extensions/Exporters/FlickrExport/FlickrNet/FlickrException.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FlickrExport/FlickrNet/FlickrException.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,23 @@
+using System;
+
+namespace FlickrNet
+{
+	/// <summary>
+	/// Generic Flickr.Net Exception.
+	/// </summary>
+	[Serializable]
+	public class FlickrException : Exception
+	{
+		internal FlickrException()
+		{
+		}
+
+		internal FlickrException(string message) : base(message)
+		{
+		}
+
+		internal FlickrException(string message, Exception innerException) : base(message, innerException)
+		{
+		}
+	}
+}

Added: trunk/extensions/Exporters/FlickrExport/FlickrNet/FlickrWebException.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FlickrExport/FlickrNet/FlickrWebException.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,14 @@
+using System;
+
+namespace FlickrNet
+{
+	/// <summary>
+	/// Exception thrown when a communication error occurs with a web call.
+	/// </summary>
+	public class FlickrWebException : FlickrException
+	{
+		internal FlickrWebException(string message, Exception innerException) : base(message, innerException)
+		{
+		}
+	}
+}

Added: trunk/extensions/Exporters/FlickrExport/FlickrNet/GeoAccuracy.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FlickrExport/FlickrNet/GeoAccuracy.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,101 @@
+using System;
+using System.Xml.Serialization;
+
+namespace FlickrNet
+{
+	/// <summary>
+	/// Geo-taggin accuracy. Used in <see cref="PhotoSearchOptions.Accuracy"/> and <see cref="BoundaryBox.Accuracy"/>.
+	/// </summary>
+	/// <remarks>
+	/// Level descriptions are only approximate.
+	/// </remarks>
+	[Serializable]
+	public enum GeoAccuracy
+	{
+		/// <summary>
+		/// No accuracy level specified.
+		/// </summary>
+		[XmlEnum("0")]
+		None = 0,
+		/// <summary>
+		/// World level, level 1.
+		/// </summary>
+		[XmlEnum("1")]
+		World = 1,
+		/// <summary>
+		/// Level 2
+		/// </summary>
+		[XmlEnum("2")]
+		Level2 = 2,
+		/// <summary>
+		/// Level 3 - approximately Country level.
+		/// </summary>
+		[XmlEnum("3")]
+		Country = 3,
+		/// <summary>
+		/// Level 4
+		/// </summary>
+		[XmlEnum("4")]
+		Level4 = 4,
+		/// <summary>
+		/// Level 5
+		/// </summary>
+		[XmlEnum("5")]
+		Level5 = 5,
+		/// <summary>
+		/// Level 6 - approximately Region level
+		/// </summary>
+		[XmlEnum("6")]
+		Region = 6,
+		/// <summary>
+		/// Level 7
+		/// </summary>
+		[XmlEnum("7")]
+		Level7 = 7,
+		/// <summary>
+		/// Level 8
+		/// </summary>
+		[XmlEnum("8")]
+		Level8 = 8,
+		/// <summary>
+		/// Level 9
+		/// </summary>
+		[XmlEnum("9")]
+		Level9 = 9,
+		/// <summary>
+		/// Level 10
+		/// </summary>
+		[XmlEnum("10")]
+		Level10 = 10,
+		/// <summary>
+		/// Level 11 - approximately City level
+		/// </summary>
+		[XmlEnum("11")]
+		City = 11,
+		/// <summary>
+		/// Level 12
+		/// </summary>
+		[XmlEnum("12")]
+		Level12 = 12,
+		/// <summary>
+		/// Level 13
+		/// </summary>
+		[XmlEnum("13")]
+		Level13 = 13,
+		/// <summary>
+		/// Level 14
+		/// </summary>
+		[XmlEnum("14")]
+		Level14 = 14,
+		/// <summary>
+		/// Level 15
+		/// </summary>
+		[XmlEnum("15")]
+		Level15 = 15,
+		/// <summary>
+		/// Street level (16) - the most accurate level and the default.
+		/// </summary>
+		[XmlEnum("16")]
+		Street = 16
+	}
+}

Added: trunk/extensions/Exporters/FlickrExport/FlickrNet/GeoPermissions.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FlickrExport/FlickrNet/GeoPermissions.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,73 @@
+using System;
+using System.Xml;
+
+namespace FlickrNet
+{
+	/// <summary>
+	/// Permissions for the selected photo.
+	/// </summary>
+	[System.Serializable]
+	public class GeoPermissions
+	{
+		private string _photoId;
+		private bool _isPublic;
+		private bool _isContact;
+		private bool _isFriend;
+		private bool _isFamily;
+
+		internal GeoPermissions(XmlElement element)
+		{
+			if( element.Attributes.GetNamedItem("id") != null )
+				_photoId = element.Attributes.GetNamedItem("id").Value;
+			if( element.Attributes.GetNamedItem("ispublic") != null )
+				_isPublic = element.Attributes.GetNamedItem("ispublic").Value=="1";
+			if( element.Attributes.GetNamedItem("iscontact") != null )
+				_isContact = element.Attributes.GetNamedItem("iscontact").Value=="1";
+			if( element.Attributes.GetNamedItem("isfamily") != null )
+				_isFamily = element.Attributes.GetNamedItem("isfamily").Value=="1";
+			if( element.Attributes.GetNamedItem("isfriend") != null )
+				_isFriend = element.Attributes.GetNamedItem("isfriend").Value=="1";
+		}
+
+		/// <summary>
+		/// The ID for the photo whose permissions these are.
+		/// </summary>
+		public string PhotoId
+		{
+			get { return _photoId; }
+		}
+
+		/// <summary>
+		/// Are the general unwashed (public) allowed to see the Geo Location information for this photo.
+		/// </summary>
+		public bool IsPublic
+		{
+			get { return _isPublic; }
+		}
+
+		/// <summary>
+		/// Are contacts allowed to see the Geo Location information for this photo.
+		/// </summary>
+		public bool IsContact
+		{
+			get { return _isContact; }
+		}
+
+		/// <summary>
+		/// Are friends allowed to see the Geo Location information for this photo.
+		/// </summary>
+		public bool IsFriend
+		{
+			get { return _isFriend; }
+		}
+
+		/// <summary>
+		/// Are family allowed to see the Geo Location information for this photo.
+		/// </summary>
+		public bool IsFamily
+		{
+			get { return _isFamily; }
+		}
+
+	}
+}

Added: trunk/extensions/Exporters/FlickrExport/FlickrNet/GroupSearchResults.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FlickrExport/FlickrNet/GroupSearchResults.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,165 @@
+using System;
+using System.Xml;
+using System.Xml.XPath;
+
+namespace FlickrNet
+{
+	/// <summary>
+	/// Returned by <see cref="Flickr.GroupsSearch(string)"/> methods.
+	/// </summary>
+	public class GroupSearchResults
+	{
+		private int page;
+
+		/// <summary>
+		/// The current page that the group search results represents.
+		/// </summary>
+		public int Page { get { return page; } }
+
+		private int pages;
+
+		/// <summary>
+		/// The total number of pages this search would return.
+		/// </summary>
+		public int Pages { get { return pages; } }
+
+		private int perPage;
+
+		/// <summary>
+		/// The number of groups returned per photo.
+		/// </summary>
+		public int PerPage { get { return perPage; } }
+
+		private int total;
+		/// <summary>
+		/// The total number of groups that where returned for the search.
+		/// </summary>
+		public int Total { get { return total; } }
+
+		private GroupSearchResultCollection groups = new GroupSearchResultCollection();
+
+		/// <summary>
+		/// The collection of groups returned for this search.
+		/// </summary>
+		/// <example>
+		/// The following code iterates through the list of groups returned:
+		/// <code>
+		/// GroupSearchResults results = flickr.GroupsSearch("test");
+		/// foreach(GroupSearchResult result in results.Groups)
+		/// {
+		///		Console.WriteLine(result.GroupName);
+		/// }
+		/// </code>
+		/// </example>
+		public GroupSearchResultCollection Groups { get { return groups; } }
+
+		internal GroupSearchResults(XmlElement element)
+		{
+			page = Convert.ToInt32(element.GetAttribute("page"));
+			pages = Convert.ToInt32(element.GetAttribute("pages"));
+			perPage = Convert.ToInt32(element.GetAttribute("perpage"));
+			total = Convert.ToInt32(element.GetAttribute("total"));
+
+			XmlNodeList gs = element.SelectNodes("group");
+			groups.Clear();
+			for(int i = 0; i < gs.Count; i++)
+			{
+				groups.Add(new GroupSearchResult(gs[i]));
+			}
+		}
+	}
+
+	/// <summary>
+	/// Collection containing list of GroupSearchResult instances
+	/// </summary>
+	public class GroupSearchResultCollection : System.Collections.CollectionBase
+	{
+		/// <summary>
+		/// Method for adding a new <see cref="GroupSearchResult"/> to the collection.
+		/// </summary>
+		/// <param name="result"></param>
+		public void Add(GroupSearchResult result)
+		{
+			List.Add(result);
+		}
+
+		/// <summary>
+		/// Method for adding a collection of <see cref="GroupSearchResult"/> objects (contained within a
+		/// <see cref="GroupSearchResults"/> collection) to this collection.
+		/// </summary>
+		/// <param name="results"></param>
+		public void AddRange(GroupSearchResultCollection results)
+		{
+			foreach(GroupSearchResult result in results)
+				List.Add(result);
+		}
+
+		/// <summary>
+		/// Return a particular <see cref="GroupSearchResult"/> based on the index.
+		/// </summary>
+		public GroupSearchResult this[int index]
+		{
+			get { return (GroupSearchResult)List[index]; }
+			set { List[index] = value; }
+		}
+
+		/// <summary>
+		/// Removes the selected result from the collection.
+		/// </summary>
+		/// <param name="result">The result to remove.</param>
+		public void Remove(GroupSearchResult result)
+		{
+			List.Remove(result);
+		}
+
+		/// <summary>
+		/// Checks if the collection contains the result.
+		/// </summary>
+		/// <param name="result">The result to see if the collection contains.</param>
+		/// <returns>Returns true if the collecton contains the result, otherwise false.</returns>
+		public bool Contains(GroupSearchResult result)
+		{
+			return List.Contains(result);
+		}
+
+		/// <summary>
+		/// Copies the current collection to an array of <see cref="GroupSearchResult"/> objects.
+		/// </summary>
+		/// <param name="array"></param>
+		/// <param name="index"></param>
+		public void CopyTo(GroupSearchResult[] array, int index)
+		{
+			List.CopyTo(array, index);
+		}
+	}
+
+	/// <summary>
+	/// A class which encapsulates a single group search result.
+	/// </summary>
+	public class GroupSearchResult
+	{
+		private string _groupId;
+		private string _groupName;
+		private bool _eighteen;
+
+		/// <summary>
+		/// The group id for the result.
+		/// </summary>
+		public string GroupId { get { return _groupId; } }
+		/// <summary>
+		/// The group name for the result.
+		/// </summary>
+		public string GroupName { get { return _groupName; } }
+		/// <summary>
+		/// True if the group is an over eighteen (adult) group only.
+		/// </summary>
+		public bool EighteenPlus { get { return _eighteen; } }
+
+		internal GroupSearchResult(XmlNode node)
+		{
+			_groupId = node.Attributes["nsid"].Value;
+			_groupName = node.Attributes["name"].Value;
+			_eighteen = Convert.ToInt32(node.Attributes["eighteenplus"].Value)==1;
+		}
+	}
+}

Added: trunk/extensions/Exporters/FlickrExport/FlickrNet/Groups.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FlickrExport/FlickrNet/Groups.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,416 @@
+using System;
+using System.Xml.Serialization;
+using System.Xml.Schema;
+using System.Xml;
+
+namespace FlickrNet
+{
+	/// <summary>
+	/// Provides details of a particular group.
+	/// </summary>
+	/// <remarks>Used by <see cref="Flickr.GroupsBrowse()"/> and
+	/// <see cref="Flickr.GroupsBrowse(string)"/>.</remarks>
+	[System.Serializable]
+	public class Group
+	{
+		/// <summary>
+		/// The id of the group.
+		/// </summary>
+		[XmlAttribute("nsid", Form=XmlSchemaForm.Unqualified)]
+		public string GroupId;
+
+		/// <summary>
+		/// The name of the group
+		/// </summary>
+		[XmlAttribute("name", Form=XmlSchemaForm.Unqualified)]
+		public string GroupName;
+
+		/// <summary>
+		/// The number of memebers of the group.
+		/// </summary>
+		[XmlAttribute("members", Form=XmlSchemaForm.Unqualified)]
+		public long Members;
+	}
+
+	/// <summary>
+	/// Provides details of a particular group.
+	/// </summary>
+	/// <remarks>
+	/// Used by the Url methods and <see cref="Flickr.GroupsGetInfo"/> method.
+	/// The reason for a <see cref="Group"/> and <see cref="GroupFullInfo"/> are due to xml serialization
+	/// incompatabilities.
+	/// </remarks>
+	[System.Serializable]
+	public class GroupFullInfo
+	{
+		internal GroupFullInfo()
+		{
+		}
+
+		internal GroupFullInfo(XmlNode node)
+		{
+			if( node.Attributes.GetNamedItem("id") != null )
+				GroupId = node.Attributes.GetNamedItem("id").Value;
+			if( node.SelectSingleNode("name") != null )
+				GroupName = node.SelectSingleNode("name").InnerText;
+			if( node.SelectSingleNode("description") != null )
+				Description = node.SelectSingleNode("description").InnerXml;
+			if( node.SelectSingleNode("members") != null )
+				Members = int.Parse(node.SelectSingleNode("members").InnerText);
+			if( node.SelectSingleNode("privacy") != null )
+				Privacy = (PoolPrivacy)int.Parse(node.SelectSingleNode("privacy").InnerText);
+
+			if( node.SelectSingleNode("throttle") != null )
+			{
+				XmlNode throttle = node.SelectSingleNode("throttle");
+				ThrottleInfo = new GroupThrottleInfo();
+				if( throttle.Attributes.GetNamedItem("count") != null )
+					ThrottleInfo.Count = int.Parse(throttle.Attributes.GetNamedItem("count").Value);
+				if( throttle.Attributes.GetNamedItem("mode") != null )
+					ThrottleInfo.setMode(throttle.Attributes.GetNamedItem("mode").Value);
+				if( throttle.Attributes.GetNamedItem("remaining") != null )
+					ThrottleInfo.Remaining = int.Parse(throttle.Attributes.GetNamedItem("remaining").Value);
+			}
+		}
+
+		/// <remarks/>
+		public string GroupId;
+
+		/// <remarks/>
+		public string GroupName;
+
+		/// <remarks/>
+		public string Description;
+
+		/// <remarks/>
+		public long Members;
+
+		/// <remarks/>
+		public PoolPrivacy Privacy;
+
+		/// <remarks/>
+		public GroupThrottleInfo ThrottleInfo;
+
+		/// <summary>
+		/// Methods for automatically converting a <see cref="GroupFullInfo"/> object into
+		/// and instance of a <see cref="Group"/> object.
+		/// </summary>
+		/// <param name="groupInfo">The incoming object.</param>
+		/// <returns>The <see cref="Group"/> instance.</returns>
+		public static implicit operator Group( GroupFullInfo groupInfo )
+		{
+			Group g = new Group();
+			g.GroupId = groupInfo.GroupId;
+			g.GroupName = groupInfo.GroupName;
+			g.Members = groupInfo.Members;
+
+			return g;
+		}
+
+		/// <summary>
+		/// Converts the current <see cref="GroupFullInfo"/> into an instance of the
+		/// <see cref="Group"/> class.
+		/// </summary>
+		/// <returns>A <see cref="Group"/> instance.</returns>
+		public Group ToGroup()
+		{
+			return (Group)this;
+		}
+
+	}
+
+	/// <summary>
+	/// Throttle information about a group (i.e. posting limit)
+	/// </summary>
+	public class GroupThrottleInfo
+	{
+		/// <summary>
+		/// The number of posts in each period allowed to this group.
+		/// </summary>
+		public int Count;
+
+		/// <summary>
+		/// The posting limit mode for a group.
+		/// </summary>
+		public GroupThrottleMode Mode;
+
+		internal void setMode(string mode)
+		{
+			switch(mode)
+			{
+				case "day":
+					Mode = GroupThrottleMode.PerDay;
+					break;
+				case "week":
+					Mode = GroupThrottleMode.PerWeek;
+					break;
+				case "month":
+					Mode = GroupThrottleMode.PerMonth;
+					break;
+				case "ever":
+					Mode = GroupThrottleMode.Ever;
+					break;
+				case "none":
+					Mode = GroupThrottleMode.NoLimit;
+					break;
+				case "disabled":
+					Mode = GroupThrottleMode.Disabled;
+					break;
+				default:
+					throw new ArgumentException(string.Format("Unknown mode found {0}", mode), "mode");
+			}
+		}
+
+		/// <summary>
+		/// The number of remainging posts allowed by this user. If unauthenticated then this will be zero.
+		/// </summary>
+		public int Remaining;
+	}
+
+	/// <summary>
+	/// The posting limit most for a group.
+	/// </summary>
+	public enum GroupThrottleMode
+	{
+		/// <summary>
+		/// Per day posting limit.
+		/// </summary>
+		PerDay,
+		/// <summary>
+		/// Per week posting limit.
+		/// </summary>
+		PerWeek,
+		/// <summary>
+		/// Per month posting limit.
+		/// </summary>
+		PerMonth,
+		/// <summary>
+		/// No posting limit.
+		/// </summary>
+		NoLimit,
+		/// <summary>
+		/// Posting limit is total number of photos in the group.
+		/// </summary>
+		Ever,
+		/// <summary>
+		/// Posting is disabled to this group.
+		/// </summary>
+		Disabled
+
+	}
+
+	/// <summary>
+	/// Information about a group the authenticated user is a member of.
+	/// </summary>
+	public class MemberGroupInfo
+	{
+		internal static MemberGroupInfo[] GetMemberGroupInfo(XmlNode node)
+		{
+			XmlNodeList list = node.SelectNodes("//group");
+			MemberGroupInfo[] infos = new MemberGroupInfo[list.Count];
+			for(int i = 0; i < infos.Length; i++)
+			{
+				infos[i] = new MemberGroupInfo(list[i]);
+			}
+			return infos;
+		}
+
+		internal MemberGroupInfo(XmlNode node)
+		{
+			if( node.Attributes["nsid"] != null )
+				_groupId = node.Attributes["nsid"].Value;
+			if( node.Attributes["name"] != null )
+				_groupName = node.Attributes["name"].Value;
+			if( node.Attributes["admin"] != null )
+				_isAdmin = node.Attributes["admin"].Value=="1";
+			if( node.Attributes["privacy"] != null )
+				_privacy = (PoolPrivacy)Enum.Parse(typeof(PoolPrivacy),node.Attributes["privacy"].Value, true);
+			if( node.Attributes["photos"] != null )
+				_numberOfPhotos = Int32.Parse(node.Attributes["photos"].Value);
+			if( node.Attributes["iconserver"] != null )
+				_iconServer = node.Attributes["iconserver"].Value;
+		}
+
+		private string _groupId;
+
+		/// <summary>
+		/// Property which returns the group id for the group.
+		/// </summary>
+		public string GroupId
+		{
+			get { return _groupId; }
+		}
+
+		private string _groupName;
+
+		/// <summary>The group name.</summary>
+		public string GroupName
+		{
+			get { return _groupName; }
+		}
+
+		private bool _isAdmin;
+
+		/// <summary>
+		/// True if the user is the admin for the group, false if they are not.
+		/// </summary>
+		public bool IsAdmin
+		{
+			get { return _isAdmin; }
+		}
+
+		private long _numberOfPhotos;
+
+		/// <summary>
+		/// The number of photos currently in the group pool.
+		/// </summary>
+		public long NumberOfPhotos
+		{
+			get { return _numberOfPhotos; }
+		}
+
+		private PoolPrivacy _privacy;
+
+		/// <summary>
+		/// The privacy of the pool (see <see cref="PoolPrivacy"/>).
+		/// </summary>
+		public PoolPrivacy Privacy
+		{
+			get { return _privacy; }
+		}
+
+		private string _iconServer;
+
+		/// <summary>
+		/// The server number for the group icon.
+		/// </summary>
+		public string IconServer
+		{
+			get { return _iconServer; }
+		}
+
+		/// <summary>
+		/// The URL for the group icon.
+		/// </summary>
+		public Uri GroupIconUrl
+		{
+			get { return new Uri(String.Format("http://static.flickr.com/{0}/buddyicons/{1}.jpg";, IconServer, GroupId)); }
+		}
+
+		/// <summary>
+		/// The URL for the group web page.
+		/// </summary>
+		public Uri GroupUrl
+		{
+			get { return new Uri(String.Format("http://www.flickr.com/groups/{0}/";, GroupId)); }
+		}
+
+	}
+
+	/// <summary>
+	/// Information about public groups for a user.
+	/// </summary>
+	[System.Serializable]
+	public class PublicGroupInfo
+	{
+		internal static PublicGroupInfo[] GetPublicGroupInfo(XmlNode node)
+		{
+			XmlNodeList list = node.SelectNodes("//group");
+			PublicGroupInfo[] infos = new PublicGroupInfo[list.Count];
+			for(int i = 0; i < infos.Length; i++)
+			{
+				infos[i] = new PublicGroupInfo(list[i]);
+			}
+			return infos;
+		}
+
+		internal PublicGroupInfo(XmlNode node)
+		{
+			if( node.Attributes["nsid"] != null )
+				_groupId = node.Attributes["nsid"].Value;
+			if( node.Attributes["name"] != null )
+				_groupName = node.Attributes["name"].Value;
+			if( node.Attributes["admin"] != null )
+				_isAdmin = node.Attributes["admin"].Value=="1";
+			if( node.Attributes["eighteenplus"] != null )
+				_isEighteenPlus = node.Attributes["eighteenplus"].Value=="1";
+		}
+
+		private string _groupId;
+
+		/// <summary>
+		/// Property which returns the group id for the group.
+		/// </summary>
+		public string GroupId
+		{
+			get { return _groupId; }
+		}
+
+		private string _groupName;
+
+		/// <summary>The group name.</summary>
+		public string GroupName
+		{
+			get { return _groupName; }
+		}
+
+		private bool _isAdmin;
+
+		/// <summary>
+		/// True if the user is the admin for the group, false if they are not.
+		/// </summary>
+		public bool IsAdmin
+		{
+			get { return _isAdmin; }
+		}
+
+		private bool _isEighteenPlus;
+
+		/// <summary>
+		/// Will contain 1 if the group is restricted to people who are 18 years old or over, 0 if it is not.
+		/// </summary>
+		public bool EighteenPlus
+		{
+			get { return _isEighteenPlus; }
+		}
+
+		/// <summary>
+		/// The URL for the group web page.
+		/// </summary>
+		public Uri GroupUrl
+		{
+			get { return new Uri(String.Format("http://www.flickr.com/groups/{0}/";, GroupId)); }
+		}
+	}
+
+	/// <summary>
+	/// The various pricay settings for a group.
+	/// </summary>
+	[System.Serializable]
+	public enum PoolPrivacy
+	{
+		/// <summary>
+		/// No privacy setting specified.
+		/// </summary>
+		[XmlEnum("0")]
+		None = 0,
+
+		/// <summary>
+		/// The group is a private group. You cannot view pictures or posts until you are a
+		/// member. The group is also invite only.
+		/// </summary>
+		[XmlEnum("1")]
+		Private = 1,
+		/// <summary>
+		/// A public group where you can see posts and photos in the group. The group is however invite only.
+		/// </summary>
+		[XmlEnum("2")]
+		InviteOnlyPublic = 2,
+		/// <summary>
+		/// A public group.
+		/// </summary>
+		[XmlEnum("3")]
+		OpenPublic = 3
+	}
+
+}

Added: trunk/extensions/Exporters/FlickrExport/FlickrNet/License.txt
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FlickrExport/FlickrNet/License.txt	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,19 @@
+Flickr .Net API Library
+-----------------------
+All source code and information supplied as part of the Flick .Net API Library is copyright too its contributers.
+
+The source code has been released under a dual license - meaning you can use either licensed version of the library with your code.
+
+It is released under the Common Public License 1.0, a copy of which can be found at the link below.
+http://www.opensource.org/licenses/cpl.php
+
+It is released under the LGPL (GNU Lesser General Public License), a copy of which can be found at the link below.
+http://www.gnu.org/copyleft/lesser.html
+
+You are free to distribute copies of this Program in its compiled, unaltered form, including, but not limited to using it (in library form) in other .Net application, without use of this license.
+You are free to modify this code, however you must release the resulting Contributions under the CPL or the LGPL (or compatible license).
+
+Flickr API Key
+--------------
+I do not have permission to include a Flickr API Key in any distribution which includes source code.
+If I do by accident then it is not included in the above license and should not be used.

Added: trunk/extensions/Exporters/FlickrExport/FlickrNet/Licenses.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FlickrExport/FlickrNet/Licenses.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,46 @@
+using System;
+using System.Xml.Serialization;
+using System.Xml.Schema;
+
+namespace FlickrNet
+{
+	/// <summary>
+	/// A class which encapsulates a single property, an array of
+	/// <see cref="License"/> objects in its <see cref="LicenseCollection"/> property.
+	/// </summary>
+	[System.Serializable]
+	public class Licenses
+	{
+		/// <summary>A collection of available licenses.</summary>
+		/// <remarks/>
+		[XmlElement("license", Form=XmlSchemaForm.Unqualified)]
+		public License[] LicenseCollection;
+
+	}
+
+	/// <summary>
+	/// Details of a particular license available from Flickr.
+	/// </summary>
+	[System.Serializable]
+	public class License
+	{
+		/// <summary>
+        ///     The ID of the license. Used by <see cref="Flickr.PhotosGetInfo(string)"/> and
+        ///     <see cref="Flickr.PhotosGetInfo(string, string)"/>.
+        /// </summary>
+		/// <remarks/>
+		[XmlAttribute("id", Form=XmlSchemaForm.Unqualified)]
+		public int LicenseId;
+
+		/// <summary>The name of the license.</summary>
+		/// <remarks/>
+		[XmlAttribute("name", Form=XmlSchemaForm.Unqualified)]
+		public string LicenseName;
+
+		/// <summary>The URL for the license text.</summary>
+		/// <remarks/>
+		[XmlAttribute("url", Form=XmlSchemaForm.Unqualified)]
+		public string LicenseUrl;
+
+	}
+}

Added: trunk/extensions/Exporters/FlickrExport/FlickrNet/LockFile.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FlickrExport/FlickrNet/LockFile.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,120 @@
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Threading;
+
+namespace FlickrNet
+{
+	/// <summary>
+	/// A non-reentrant mutex that is implemented using
+	/// a lock file, and thus works across processes,
+	/// sessions, and machines (as long as the underlying
+	/// FS provides robust r/w locking).
+	///
+	/// To use:
+	///
+	/// FileLock fLock = new FileLock(@"c:\foo\my.lock");
+	///
+	/// using (fLock.Acquire())
+	/// {
+	///		// protected operations
+	/// }
+	/// </summary>
+	internal class LockFile
+	{
+		private readonly string filepath;
+		private readonly DisposeHelper disposeHelper;
+		private Stream stream;
+
+		public LockFile(string filepath)
+		{
+			this.filepath = filepath;
+			this.disposeHelper = new DisposeHelper(this);
+		}
+
+		public IDisposable Acquire()
+		{
+			string dir = Path.GetDirectoryName(filepath);
+
+			lock (this)
+			{
+#if !WindowsCE
+				while (stream != null)
+					Monitor.Wait(this);
+#endif
+
+				while (true)
+				{
+					if (!Directory.Exists(dir))
+						Directory.CreateDirectory(dir);
+					try
+					{
+						Debug.Assert(stream == null, "Stream was not null--programmer error");
+						stream = new FileStream(filepath, FileMode.OpenOrCreate, FileAccess.Read, FileShare.None, 8, false);
+						return disposeHelper;
+					}
+					catch (IOException ioe)
+					{
+						int errorCode = SafeNativeMethods.GetErrorCode(ioe);
+						switch (errorCode)
+						{
+							case 32:
+							case 33:
+							case 32 | 0x1620:
+							case 33 | 0x1620:
+								Thread.Sleep(50);
+								continue;
+							default:
+								throw;
+						}
+					}
+				}
+			}
+		}
+
+		internal void Release()
+		{
+			lock (this)
+			{
+#if !WindowsCE
+				// Doesn't hurt to pulse. Note that waiting threads will not actually
+				// continue to execute until this critical section is exited.
+				Monitor.PulseAll(this);
+#endif
+
+				if (stream == null)
+					throw new InvalidOperationException("Tried to dispose a FileLock that was not owned");
+				try
+				{
+					stream.Close();
+					try
+					{
+						File.Delete(filepath);
+					} catch(IOException) { /* could fail if already acquired elsewhere */ }
+				}
+				finally
+				{
+					stream = null;
+				}
+			}
+		}
+
+		private class DisposeHelper : IDisposable
+		{
+			private readonly LockFile lockFile;
+
+			public DisposeHelper(LockFile lockFile)
+			{
+				this.lockFile = lockFile;
+			}
+
+			public void Dispose()
+			{
+				lockFile.Release();
+			}
+		}
+
+
+	}
+}

Added: trunk/extensions/Exporters/FlickrExport/FlickrNet/Makefile.am
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FlickrExport/FlickrNet/Makefile.am	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,82 @@
+include $(top_srcdir)/Makefile.include
+
+ASSEMBLY_NAME = FlickrNet
+
+ASSEMBLY_SOURCES =				\
+	$(srcdir)/AssemblyInfo.cs 		\
+	$(srcdir)/ActivityEvent.cs		\
+	$(srcdir)/ActivityItem.cs		\
+	$(srcdir)/ApiKeyRequiredException.cs	\
+	$(srcdir)/Auth.cs 			\
+	$(srcdir)/AuthenticationRequiredException.cs		\
+	$(srcdir)/Blogs.cs 			\
+	$(srcdir)/BoundaryBox.cs 		\
+	$(srcdir)/Cache.cs 			\
+	$(srcdir)/Categories.cs 		\
+	$(srcdir)/Comments.cs 			\
+	$(srcdir)/Contacts.cs 			\
+	$(srcdir)/Context.cs 			\
+	$(srcdir)/DateGranularity.cs 		\
+	$(srcdir)/Enums.cs 			\
+	$(srcdir)/ExifPhoto.cs 			\
+	$(srcdir)/Flickr.cs 			\
+	$(srcdir)/FlickrApiException.cs		\
+	$(srcdir)/FlickrWebException.cs		\
+	$(srcdir)/FlickrConfigurationManager.cs	\
+	$(srcdir)/FlickrConfigurationSettings.cs\
+	$(srcdir)/FlickrException.cs 		\
+	$(srcdir)/GeoAccuracy.cs 		\
+	$(srcdir)/GeoPermissions.cs 		\
+	$(srcdir)/GroupSearchResults.cs 	\
+	$(srcdir)/Groups.cs 			\
+	$(srcdir)/Licenses.cs 			\
+	$(srcdir)/LockFile.cs 			\
+	$(srcdir)/Methods.cs 			\
+	$(srcdir)/PartialSearchOptions.cs 	\
+	$(srcdir)/PersistentCache.cs 		\
+	$(srcdir)/Person.cs 			\
+	$(srcdir)/Photo.cs 			\
+	$(srcdir)/PhotoCounts.cs 		\
+	$(srcdir)/PhotoDates.cs 		\
+	$(srcdir)/PhotoInfo.cs 			\
+	$(srcdir)/PhotoLocation.cs 		\
+	$(srcdir)/PhotoPermissions.cs 		\
+	$(srcdir)/PhotoSearchExtras.cs 		\
+	$(srcdir)/PhotoSearchOptions.cs 	\
+	$(srcdir)/PhotoSearchOrder.cs 		\
+	$(srcdir)/PhotoSets.cs 			\
+	$(srcdir)/Photos.cs 			\
+	$(srcdir)/Response.cs 			\
+	$(srcdir)/ResponseXmlException.cs	\
+	$(srcdir)/SafeNativeMethods.cs		\
+	$(srcdir)/SignatureRequiredException.cs	\
+	$(srcdir)/Sizes.cs 			\
+	$(srcdir)/Tags.cs 			\
+	$(srcdir)/Uploader.cs 			\
+	$(srcdir)/UploadProgressEvent.cs 	\
+	$(srcdir)/User.cs 			\
+	$(srcdir)/Utils.cs
+
+
+REFS =  -r:System.Web.dll	\
+	-r:System.Drawing.dll
+
+PKGS =
+
+ASSEMBLY = $(ASSEMBLY_NAME).dll
+
+all: $(ASSEMBLY)
+
+$(ASSEMBLY): $(ASSEMBLY_SOURCES)
+	$(CSC_LIB) -out:$@ $(PKGS) $(REFS) $(ASSEMBLY_SOURCES)
+
+assemblydir = $(pkglibdir)
+assembly_DATA =	$(ASSEMBLY)
+
+EXTRA_DIST =			\
+	$(ASSEMBLY_SOURCES)	\
+	License.txt
+
+CLEANFILES =			\
+	$(ASSEMBLY)		\
+	$(ASSEMBLY).mdb

Added: trunk/extensions/Exporters/FlickrExport/FlickrNet/Methods.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FlickrExport/FlickrNet/Methods.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,136 @@
+using System;
+using System.Xml;
+using System.Xml.Serialization;
+using System.Xml.Schema;
+
+namespace FlickrNet
+{
+	/// <summary>
+	/// Summary description for Methods.
+	/// </summary>
+	public class Methods
+	{
+		private Methods()
+		{
+		}
+
+		internal static string[] GetMethods(XmlElement element)
+		{
+			XmlNodeList nodes = element.SelectNodes("method");
+			string[] _methods = new string[nodes.Count];
+			for(int i = 0; i < nodes.Count; i++)
+			{
+				_methods[i] = nodes[i].Value;
+			}
+			return _methods;
+		}
+	}
+
+	/// <summary>
+	/// A method supported by the Flickr API.
+	/// </summary>
+	/// <remarks>
+	/// See <a href="http://www.flickr.com/services/api";>Flickr API Documentation</a> for a complete list
+	/// of methods.
+	/// </remarks>
+	[Serializable]
+	public class Method
+	{
+		/// <summary>
+		/// Default constructor.
+		/// </summary>
+		public Method()
+		{
+		}
+
+		/// <summary>
+		/// The name of the method.
+		/// </summary>
+		[XmlAttribute("name", Form=XmlSchemaForm.Unqualified)]
+		public string Name;
+
+		/// <summary>
+		/// The description of the method.
+		/// </summary>
+		[XmlElement("description", Form=XmlSchemaForm.Unqualified)]
+		public string Description;
+
+		/// <summary>
+		/// An example response for the method.
+		/// </summary>
+		[XmlElement("response", Form=XmlSchemaForm.Unqualified)]
+		public string Response;
+
+		/// <summary>
+		/// An explanation of the example response for the method.
+		/// </summary>
+		[XmlElement("explanation", Form=XmlSchemaForm.Unqualified)]
+		public string Explanation;
+
+		/// <summary>
+		/// The arguments of the method.
+		/// </summary>
+		[XmlElement("arguments", Form=XmlSchemaForm.Unqualified)]
+		public Arguments Arguments;
+
+		/// <summary>
+		/// The possible errors that could be returned by the method.
+		/// </summary>
+		[XmlArray()]
+		[XmlArrayItem("error", typeof(MethodError), Form=XmlSchemaForm.Unqualified)]
+		public MethodError[] Errors;
+
+	}
+
+	/// <summary>
+	/// An instance containing a collection of <see cref="Argument"/> instances.
+	/// </summary>
+	[Serializable]
+	public class Arguments
+	{
+		/// <summary>
+		/// A collection of <see cref="Argument"/> instances.
+		/// </summary>
+		[XmlElement("argument", Form=XmlSchemaForm.Unqualified)]
+		public Argument[] ArgumentCollection;
+	}
+
+	/// <summary>
+	/// An argument for a method.
+	/// </summary>
+	[Serializable]
+	public class Argument
+	{
+		/// <summary>
+		/// The name of the argument.
+		/// </summary>
+		[XmlElement("name")]
+		public string ArgumentName;
+
+		/// <summary>
+		/// Is the argument optional or not.
+		/// </summary>
+		[XmlElement("optional")]
+		public int Optional;
+
+		/// <summary>
+		/// The description of the argument.
+		/// </summary>
+		[XmlText()]
+		public string ArgumentDescription;
+	}
+
+	/// <summary>
+	/// A possible error that a method can return.
+	/// </summary>
+	[Serializable]
+	public class MethodError
+	{
+		/// <summary>
+		/// The code for the error.
+		/// </summary>
+		[XmlElement("code")]
+		public int Code;
+
+	}
+}

Added: trunk/extensions/Exporters/FlickrExport/FlickrNet/PartialSearchOptions.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FlickrExport/FlickrNet/PartialSearchOptions.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,179 @@
+using System;
+
+namespace FlickrNet
+{
+	/// <summary>
+	/// Summary description for PartialSearchOptions.
+	/// </summary>
+	public class PartialSearchOptions
+	{
+		#region Private Variables
+		private DateTime _minUploadDate = DateTime.MinValue;
+		private DateTime _maxUploadDate = DateTime.MinValue;
+		private DateTime _minTakenDate = DateTime.MinValue;
+		private DateTime _maxTakenDate = DateTime.MinValue;
+		private PhotoSearchExtras _extras = PhotoSearchExtras.None;
+		private PhotoSearchSortOrder _sort = PhotoSearchSortOrder.None;
+		private int _perPage = 0;
+		private int _page = 0;
+		private PrivacyFilter _privacyFilter = PrivacyFilter.None;
+		#endregion
+
+		#region Public Properties
+		/// <summary>
+		/// Minimum date uploaded. Defaults to <see cref="DateTime.MinValue"/> which
+		/// signifies that the value is not to be used.
+		/// </summary>
+		public DateTime MinUploadDate
+		{
+			get { return _minUploadDate; }
+			set { _minUploadDate = value; }
+		}
+
+		/// <summary>
+		/// Maximum date uploaded. Defaults to <see cref="DateTime.MinValue"/> which
+		/// signifies that the value is not to be used.
+		/// </summary>
+		public DateTime MaxUploadDate
+		{
+			get { return _maxUploadDate; }
+			set { _maxUploadDate = value; }
+		}
+
+		/// <summary>
+		/// Minimum date taken. Defaults to <see cref="DateTime.MinValue"/> which
+		/// signifies that the value is not to be used.
+		/// </summary>
+		public DateTime MinTakenDate
+		{
+			get { return _minTakenDate; }
+			set { _minTakenDate = value; }
+		}
+
+		/// <summary>
+		/// Maximum date taken. Defaults to <see cref="DateTime.MinValue"/> which
+		/// signifies that the value is not to be used.
+		/// </summary>
+		public DateTime MaxTakenDate
+		{
+			get { return _maxTakenDate; }
+			set { _maxTakenDate = value; }
+		}
+
+		/// <summary>
+		/// Optional extras to return, defaults to all. See <see cref="PhotoSearchExtras"/> for more details.
+		/// </summary>
+		public PhotoSearchExtras Extras
+		{
+			get { return _extras; }
+			set { _extras = value; }
+		}
+
+		/// <summary>
+		/// Number of photos to return per page. Defaults to 100.
+		/// </summary>
+		public int PerPage
+		{
+			get { return _perPage; }
+			set { _perPage = value; }
+		}
+
+		/// <summary>
+		/// The page to return. Defaults to page 1.
+		/// </summary>
+		public int Page
+		{
+			get { return _page; }
+			set
+			{
+				if( value < 0 ) throw new ArgumentOutOfRangeException("Page", "Must be greater than 0");
+				_page = value;
+			}
+		}
+
+		/// <summary>
+		/// The sort order of the returned list. Default is <see cref="PhotoSearchSortOrder.None"/>.
+		/// </summary>
+		public PhotoSearchSortOrder SortOrder
+		{
+			get { return _sort; }
+			set { _sort = value; }
+		}
+
+		/// <summary>
+		/// The privacy fitler to filter the search on.
+		/// </summary>
+		public PrivacyFilter PrivacyFilter
+		{
+			get { return _privacyFilter; }
+			set { _privacyFilter = value; }
+		}
+
+		#endregion
+
+		#region Constructors
+		/// <summary>
+		/// Default constructor.
+		/// </summary>
+		public PartialSearchOptions()
+		{
+		}
+
+		/// <summary>
+		/// Constructor taking a default <see cref="PhotoSearchExtras"/> parameter.
+		/// </summary>
+		/// <param name="extras">See <see cref="PhotoSearchExtras"/> for more details.</param>
+		public PartialSearchOptions(PhotoSearchExtras extras)
+		{
+			Extras = extras;
+		}
+
+		/// <summary>
+		/// Constructor taking a perPage and page parameter.
+		/// </summary>
+		/// <param name="perPage">The number of photos to return per page (maximum).</param>
+		/// <param name="page">The page number to return.</param>
+		public PartialSearchOptions(int perPage, int page)
+		{
+			PerPage = perPage;
+			Page = page;
+		}
+
+		/// <summary>
+		/// Constructor taking a perPage and page parameter and a default <see cref="PhotoSearchExtras"/> parameter.
+		/// </summary>
+		/// <param name="perPage">The number of photos to return per page (maximum).</param>
+		/// <param name="page">The page number to return.</param>
+		/// <param name="extras">See <see cref="PhotoSearchExtras"/> for more details.</param>
+		public PartialSearchOptions(int perPage, int page, PhotoSearchExtras extras)
+		{
+			PerPage = perPage;
+			Page = page;
+			Extras = extras;
+		}
+		#endregion
+
+		internal PartialSearchOptions(PhotoSearchOptions options)
+		{
+			this.Extras = options.Extras;
+			this.MaxTakenDate = options.MaxTakenDate;
+			this.MinTakenDate = options.MinTakenDate;
+			this.MaxUploadDate = options.MaxUploadDate;
+			this.MinUploadDate = options.MinUploadDate;
+			this.Page = options.Page;
+			this.PerPage = options.PerPage;
+			this.PrivacyFilter = options.PrivacyFilter;
+		}
+
+		internal string ExtrasString
+		{
+			get { return Utils.ExtrasToString(Extras); }
+		}
+
+		internal string SortOrderString
+		{
+			get	{ return Utils.SortOrderToString(SortOrder); }
+		}
+
+	}
+}

Added: trunk/extensions/Exporters/FlickrExport/FlickrNet/PersistentCache.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FlickrExport/FlickrNet/PersistentCache.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,340 @@
+using System;
+using System.Collections;
+using System.Diagnostics;
+using System.IO;
+//using System.Runtime.Serialization.Formatters.Binary;
+
+namespace FlickrNet
+{
+	/// <summary>
+	/// A threadsafe cache that is backed by disk storage.
+	///
+	/// All public methods that read or write state must be
+	/// protected by the lockFile.  Private methods should
+	/// not acquire the lockFile as it is not reentrant.
+	/// </summary>
+	internal sealed class PersistentCache
+	{
+		// The in-memory representation of the cache.
+		// Use SortedList instead of Hashtable only to maintain backward
+		// compatibility with previous serialization scheme.  If we
+		// abandon backward compatibility, we should switch to Hashtable.
+		private Hashtable dataTable = new Hashtable();
+
+		private readonly CacheItemPersister persister;
+
+		// true if dataTable contains changes vs. on-disk representation
+		private bool dirty;
+
+		// The persistent file representation of the cache.
+		private readonly FileInfo dataFile;
+		private DateTime timestamp;  // last-modified time of dataFile when cache data was last known to be in sync
+		private long length;         // length of dataFile when cache data was last known to be in sync
+		private long maxSize;
+
+		// The file-based mutex.  Named (dataFile.FullName + ".lock")
+		private readonly LockFile lockFile;
+
+		public PersistentCache(string filename, CacheItemPersister persister, long maxSize)
+		{
+			this.maxSize = maxSize;
+			this.persister = persister;
+			this.dataFile = new FileInfo(filename);
+			this.lockFile = new LockFile(filename + ".lock");
+		}
+
+		/// <summary>
+		/// Return all items in the cache.  Works similarly to
+		/// ArrayList.ToArray(Type).
+		/// </summary>
+		public ICacheItem[] ToArray(Type valueType)
+		{
+			using (lockFile.Acquire())
+			{
+				Refresh();
+				string[] keys;
+				Array values;
+				InternalGetAll(valueType, out keys, out values);
+				return (ICacheItem[]) values;
+			}
+		}
+
+		public int Count
+		{
+			get
+			{
+				using (lockFile.Acquire())
+				{
+					Refresh();
+					return dataTable.Count;
+				}
+			}
+		}
+
+		/// <summary>
+		/// Gets the maximum size for the persistent cache.
+		/// </summary>
+		public long MaxSize
+		{
+			get { return maxSize; }
+		}
+
+		/// <summary>
+		/// Gets or sets cache values.
+		/// </summary>
+		public ICacheItem this[string key]
+		{
+			get
+			{
+				if (key == null)
+					throw new ArgumentNullException("key");
+
+				using (lockFile.Acquire())
+				{
+					Refresh();
+					return InternalGet(key);
+				}
+			}
+			set
+			{
+				if (key == null)
+					throw new ArgumentNullException("key");
+
+				ICacheItem oldItem;
+
+				using (lockFile.Acquire())
+				{
+					Refresh();
+					oldItem = InternalSet(key, value);
+					Persist();
+				}
+				if (oldItem != null)
+					oldItem.OnItemFlushed();
+			}
+		}
+
+		public ICacheItem Get(string key, TimeSpan maxAge, bool removeIfExpired)
+		{
+			Debug.Assert(maxAge > TimeSpan.Zero || maxAge == TimeSpan.MinValue, "maxAge should be positive, not negative");
+
+			ICacheItem item;
+			bool expired;
+			using (lockFile.Acquire())
+			{
+				Refresh();
+
+				item = InternalGet(key);
+				expired = item != null && Expired(item.CreationTime, maxAge);
+				if (expired)
+				{
+					if (removeIfExpired)
+					{
+						item = RemoveKey(key);
+						Persist();
+					}
+					else
+						item = null;
+				}
+			}
+
+			if (expired && removeIfExpired)
+				item.OnItemFlushed();
+
+			return expired ? null : item;
+		}
+
+		public void Flush()
+		{
+			Shrink(0);
+		}
+
+		public void Shrink(long size)
+		{
+			if (size < 0)
+				throw new ArgumentException("Cannot shrink to a negative size", "size");
+
+			ArrayList flushed = new ArrayList();
+
+			using (lockFile.Acquire())
+			{
+				Refresh();
+
+				string[] keys;
+				Array values;
+				InternalGetAll(typeof(ICacheItem), out keys, out values);
+				long totalSize = 0;
+				foreach (ICacheItem cacheItem in values)
+					totalSize += cacheItem.FileSize;
+				for (int i = 0; i < keys.Length; i++)
+				{
+					if (totalSize <= size)
+						break;
+					ICacheItem cacheItem = (ICacheItem) values.GetValue(i);
+					totalSize -= cacheItem.FileSize;
+					flushed.Add(RemoveKey(keys[i]));
+				}
+
+				Persist();
+			}
+
+			foreach (ICacheItem flushedItem in flushed)
+			{
+				Debug.Assert(flushedItem != null, "Flushed item was null--programmer error");
+				if (flushedItem != null)
+					flushedItem.OnItemFlushed();
+			}
+		}
+
+		private bool Expired(DateTime test, TimeSpan age)
+		{
+			if (age == TimeSpan.MinValue)
+				return true;
+			else if (age == TimeSpan.MaxValue)
+				return false;
+			else
+				return test < DateTime.UtcNow - age;
+		}
+
+
+		private void InternalGetAll(Type valueType, out string[] keys, out Array values)
+		{
+			if (!typeof(ICacheItem).IsAssignableFrom(valueType))
+				throw new ArgumentException("Type " + valueType.FullName + " does not implement ICacheItem", "valueType");
+
+			keys = (string[]) new ArrayList(dataTable.Keys).ToArray(typeof(string));
+			values = Array.CreateInstance(valueType, keys.Length);
+			for (int i = 0; i < keys.Length; i++)
+				values.SetValue(dataTable[keys[i]], i);
+
+			Array.Sort(values, keys, new CreationTimeComparer());
+		}
+
+		private ICacheItem InternalGet(string key)
+		{
+			if (key == null)
+				throw new ArgumentNullException("key");
+
+			return (ICacheItem) dataTable[key];
+		}
+
+		/// <returns>The old value associated with <c>key</c>, if any.</returns>
+		private ICacheItem InternalSet(string key, ICacheItem value)
+		{
+			if (key == null)
+				throw new ArgumentNullException("key");
+
+			ICacheItem flushedItem;
+
+			flushedItem = RemoveKey(key);
+			if (value != null)  // don't ever let nulls get in
+				dataTable[key] = value;
+
+			dirty = dirty || !object.ReferenceEquals(flushedItem, value);
+
+			return flushedItem;
+		}
+
+		private ICacheItem RemoveKey(string key)
+		{
+			ICacheItem cacheItem = (ICacheItem) dataTable[key];
+			if (cacheItem != null)
+			{
+				dataTable.Remove(key);
+				dirty = true;
+			}
+			return cacheItem;
+		}
+
+		private void Refresh()
+		{
+			Debug.Assert(!dirty, "Refreshing even though cache is dirty");
+
+			DateTime newTimestamp = DateTime.MinValue;
+			long newLength = -1;
+			if (dataFile.Exists)
+			{
+				dataFile.Refresh();
+				newTimestamp = dataFile.LastWriteTime;
+				newLength = dataFile.Length;
+			}
+
+			if (timestamp != newTimestamp || length != newLength)
+			{
+				// file changed
+				if (!dataFile.Exists)
+					dataTable.Clear();
+				else
+				{
+					Debug.WriteLine("Loading cache from disk");
+					using (FileStream inStream = dataFile.Open(FileMode.Open, FileAccess.Read, FileShare.Read))
+					{
+						dataTable = Load(inStream);
+					}
+				}
+			}
+
+			timestamp = newTimestamp;
+			length = newLength;
+			dirty = false;
+		}
+
+		private void Persist()
+		{
+			if (!dirty)
+				return;
+
+			Debug.WriteLine("Saving cache to disk");
+			using (FileStream outStream = dataFile.Open(FileMode.OpenOrCreate, FileAccess.Write, FileShare.None))
+			{
+				Store(outStream, dataTable);
+			}
+
+			dataFile.Refresh();
+			timestamp = dataFile.LastWriteTime;
+			length = dataFile.Length;
+
+			dirty = false;
+		}
+
+		private Hashtable Load(Stream s)
+		{
+			Hashtable table = new Hashtable();
+			int itemCount = Utils.ReadInt32(s);
+			for (int i = 0; i < itemCount; i++)
+			{
+				try
+				{
+					string key = Utils.ReadString(s);
+					ICacheItem val = persister.Read(s);
+					if( val == null ) // corrupt cache file
+						return table;
+
+					table[key] = val;
+				}
+				catch(IOException)
+				{
+					return table;
+				}
+			}
+			return table;
+		}
+
+		private void Store(Stream s, Hashtable table)
+		{
+			Utils.WriteInt32(s, table.Count);
+			foreach (DictionaryEntry entry in table)
+			{
+				Utils.WriteString(s, (string) entry.Key);
+				persister.Write(s, (ICacheItem) entry.Value);
+			}
+		}
+
+		private class CreationTimeComparer : IComparer
+		{
+			public int Compare(object x, object y)
+			{
+				return ((ICacheItem)x).CreationTime.CompareTo(((ICacheItem)y).CreationTime);
+			}
+		}
+	}
+}

Added: trunk/extensions/Exporters/FlickrExport/FlickrNet/Person.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FlickrExport/FlickrNet/Person.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,165 @@
+using System;
+using System.Xml.Serialization;
+using System.Xml.Schema;
+
+namespace FlickrNet
+{
+
+	/// <summary>
+	/// The <see cref="Person"/> class contains details returned by the <see cref="Flickr.PeopleGetInfo"/>
+	/// method.
+	/// </summary>
+	[System.Serializable]
+	[XmlRoot("person")]
+	public class Person
+	{
+		internal static Person SerializePerson(System.Xml.XmlNode node)
+		{
+			Person p = (Person)Utils.Deserialize(node, typeof(Person));
+			return p;
+		}
+		private string _userId;
+		private int _isAdmin;
+		private int _isPro;
+		private int _iconServer;
+		private int _iconFarm;
+		private string _username;
+		private string _realname;
+		private string _location;
+		private PersonPhotosSummary _summary = new PersonPhotosSummary();
+		private string _photosUrl;
+		private string _profileUrl;
+		private string _mboxHash;
+
+		/// <summary>The user id of the user.</summary>
+		/// <remarks/>
+		[XmlAttribute("nsid", Form=XmlSchemaForm.Unqualified)]
+		public string UserId { get { return _userId; } set { _userId = value; } }
+
+		/// <summary>Is the user an administrator.
+		/// 1 = admin, 0 = normal user.</summary>
+		/// <remarks></remarks>
+		[XmlAttribute("isadmin", Form=XmlSchemaForm.Unqualified)]
+		public int IsAdmin { get { return _isAdmin; } set { _isAdmin = value; } }
+
+		/// <summary>Does the user posses a pro account.
+		/// 0 = free acouunt, 1 = pro account holder.</summary>
+		[XmlAttribute("ispro", Form=XmlSchemaForm.Unqualified)]
+		public int IsPro { get { return _isPro; } set { _isPro = value; } }
+
+		/// <summary>Does the user posses a pro account.
+		/// 0 = free acouunt, 1 = pro account holder.</summary>
+		[XmlAttribute("iconserver", Form=XmlSchemaForm.Unqualified)]
+		public int IconServer { get { return _iconServer; } set { _iconServer = value; } }
+
+		/// <summary>No idea what purpose this field serves.</summary>
+		[XmlAttribute("iconfarm", Form=XmlSchemaForm.Unqualified)]
+		public int IconFarm { get { return _iconFarm; } set { _iconFarm = value; } }
+
+		/// <summary>The users username, also known as their screenname.</summary>
+		[XmlElement("username", Form=XmlSchemaForm.Unqualified)]
+		public string UserName { get { return _username; } set { _username = value; } }
+
+		/// <summary>The users real name, as entered in their profile.</summary>
+		[XmlElement("realname", Form=XmlSchemaForm.Unqualified)]
+		public string RealName { get { return _realname; } set { _realname = value; } }
+
+		/// <summary>The SHA1 hash of the users email address - used for FOAF networking.</summary>
+		[XmlElement("mbox_sha1sum", Form=XmlSchemaForm.Unqualified)]
+		public string MailBoxSha1Hash { get { return _mboxHash; } set { _mboxHash = value; } }
+
+		/// <summary>Consists of your current location followed by country.</summary>
+		/// <example>e.g. Newcastle, UK.</example>
+		[XmlElement("location", Form=XmlSchemaForm.Unqualified)]
+		public string Location { get { return _location; } set { _location = value; } }
+
+		/// <summary>Sub element containing a summary of the users photo information.</summary>
+		/// <remarks/>
+		[XmlElement("photos", Form=XmlSchemaForm.Unqualified)]
+		public PersonPhotosSummary PhotosSummary { get { return _summary; } set { _summary = value; } }
+
+		/// <summary>
+		/// The users photo location on Flickr
+		/// http://www.flickr.com/photos/username/
+		/// </summary>
+		[XmlElement("photosurl",Form=XmlSchemaForm.Unqualified)]
+		public string PhotosUrl { get { return _photosUrl; } set { _photosUrl = value; } }
+
+		/// <summary>
+		/// The users profile location on Flickr
+		/// http://www.flickr.com/people/username/
+		/// </summary>
+		[XmlElement("profileurl",Form=XmlSchemaForm.Unqualified)]
+		public string ProfileUrl { get { return _profileUrl; } set { _profileUrl = value; } }
+
+		/// <summary>
+		/// Returns the <see cref="Uri"/> for the users Buddy Icon.
+		/// </summary>
+		[XmlIgnore()]
+		public Uri BuddyIconUrl
+		{
+			get
+			{
+				if( IconServer == 0 )
+					return new Uri("http://www.flickr.com/images/buddyicon.jpg";);
+				else
+					return new Uri(String.Format("http://static.flickr.com/{0}/buddyicons/{1}.jpg";, IconServer, UserId));
+			}
+		}
+	}
+
+	/// <summary>
+	/// A summary of a users photos.
+	/// </summary>
+	[System.Serializable]
+	public class PersonPhotosSummary
+	{
+		private int _photoCount;
+		private int _views;
+
+		/// <summary>The first date the user uploaded a picture, converted into <see cref="DateTime"/> format.</summary>
+		[XmlIgnore()]
+		public DateTime FirstDate
+		{
+			get { return Utils.UnixTimestampToDate(firstdate_raw); }
+		}
+
+		/// <summary>The first date the user took a picture, converted into <see cref="DateTime"/> format.</summary>
+		[XmlIgnore()]
+		public DateTime FirstTakenDate
+		{
+			get
+			{
+				if( firsttakendate_raw == null || firsttakendate_raw.Length == 0 ) return DateTime.MinValue;
+				return System.DateTime.Parse(firsttakendate_raw);
+			}
+		}
+
+		/// <summary>The total number of photos for the user.</summary>
+		/// <remarks/>
+		[XmlElement("count", Form=XmlSchemaForm.Unqualified)]
+		public int PhotoCount
+		{
+			get { return _photoCount; }
+			set { _photoCount = value; }
+		}
+
+		/// <summary>The total number of photos for the user.</summary>
+		/// <remarks/>
+		[XmlElement("views", Form=XmlSchemaForm.Unqualified)]
+		public int Views
+		{
+			get { return _views; }
+			set { _views = value; }
+		}
+
+		/// <remarks>The unix timestamp of the date the first photo was uploaded.</remarks>
+		[XmlElement("firstdate", Form=XmlSchemaForm.Unqualified)]
+		public string firstdate_raw;
+
+		/// <remarks>The date the first photo was taken.</remarks>
+		[XmlElement("firsttakendate", Form=XmlSchemaForm.Unqualified)]
+		public string firsttakendate_raw;
+
+	}
+}

Added: trunk/extensions/Exporters/FlickrExport/FlickrNet/Photo.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FlickrExport/FlickrNet/Photo.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,281 @@
+using System;
+using System.Xml.Serialization;
+using System.Xml.Schema;
+
+namespace FlickrNet
+{
+	/// <remarks/>
+	[System.Serializable]
+	public class Photo
+	{
+
+		private string _photoId;
+		private string _userId;
+		private string _secret;
+		private string _server;
+		private string _farm;
+		private string _title;
+		private int _isPublic;
+		private int _isFriend;
+		private int _isFamily;
+		private int _isPrimary;
+		private string _license;
+		private string _ownerName;
+		private string _iconServer;
+		private string _originalFormat;
+		private string _originalSecret;
+		private string _cleanTags;
+		private string _machineTags;
+		private decimal _latitude;
+		private decimal _longitude;
+		private GeoAccuracy _accuracy;
+
+		/// <remarks/>
+		[XmlAttribute("id", Form=XmlSchemaForm.Unqualified)]
+		public string PhotoId { get { return _photoId; } set { _photoId = value; } }
+
+		/// <remarks/>
+		[XmlAttribute("owner", Form=XmlSchemaForm.Unqualified)]
+		public string UserId { get { return _userId; } set { _userId = value; } }
+
+		/// <remarks/>
+		[XmlAttribute("secret", Form=XmlSchemaForm.Unqualified)]
+		public string Secret { get { return _secret; } set { _secret = value; } }
+
+		/// <remarks/>
+		[XmlAttribute("server", Form=XmlSchemaForm.Unqualified)]
+		public string Server { get { return _server; } set { _server = value; } }
+
+		/// <remarks/>
+		[XmlAttribute("farm", Form=XmlSchemaForm.Unqualified)]
+		public string Farm { get { return _farm; } set { _farm = value; } }
+
+		/// <remarks/>
+		[XmlAttribute("title", Form=XmlSchemaForm.Unqualified)]
+		public string Title { get { return _title; } set { _title = value; } }
+
+		/// <remarks/>
+		[XmlAttribute("ispublic", Form=XmlSchemaForm.Unqualified)]
+		public int IsPublic { get { return _isPublic; } set { _isPublic = value; } }
+
+		/// <remarks/>
+		[XmlAttribute("isfriend", Form=XmlSchemaForm.Unqualified)]
+		public int IsFriend { get { return _isFriend; } set { _isFriend = value; } }
+
+		/// <remarks/>
+		[XmlAttribute("isfamily", Form=XmlSchemaForm.Unqualified)]
+		public int IsFamily { get { return _isFamily; } set { _isFamily = value; } }
+
+		/// <remarks/>
+		[XmlAttribute("isprimary", Form=XmlSchemaForm.Unqualified)]
+		public int IsPrimary { get { return _isPrimary; } set { _isPrimary = value; } }
+
+		/// <remarks/>
+		[XmlAttribute("license", Form=XmlSchemaForm.Unqualified)]
+		public string License { get { return _license; } set { _license = value; } }
+
+		/// <remarks/>
+		[XmlAttribute("dateupload", Form=XmlSchemaForm.Unqualified)]
+		public string dateupload_raw;
+
+		/// <summary>
+		/// Converts the raw dateupload field to a <see cref="DateTime"/>.
+		/// </summary>
+		[XmlIgnore]
+		public DateTime DateUploaded
+		{
+			get { return Utils.UnixTimestampToDate(dateupload_raw); }
+		}
+
+		/// <summary>
+		/// Converts the raw lastupdate field to a <see cref="DateTime"/>.
+		/// Returns <see cref="DateTime.MinValue"/> if the raw value was not returned.
+		/// </summary>
+		[XmlIgnore]
+		public DateTime LastUpdated
+		{
+			get { return Utils.UnixTimestampToDate(lastupdate_raw); }
+		}
+
+		/// <remarks/>
+		[XmlAttribute("lastupdate", Form=XmlSchemaForm.Unqualified)]
+		public string lastupdate_raw;
+
+		/// <remarks/>
+		[XmlAttribute("dateadded", Form = XmlSchemaForm.Unqualified)]
+		public string dateadded_raw;
+
+		/// <summary>
+		/// Converts the raw DateAdded field to a <see cref="DateTime"/>.
+		/// Returns <see cref="DateTime.MinValue"/> if the raw value was not returned.
+		/// </summary>
+		[XmlIgnore]
+		public DateTime DateAdded
+		{
+			get { return Utils.UnixTimestampToDate(dateadded_raw); }
+		}
+
+		/// <remarks/>
+		[XmlAttribute("datetaken", Form=XmlSchemaForm.Unqualified)]
+		public string datetaken_raw;
+
+		/// <summary>
+		/// Converts the raw datetaken field to a <see cref="DateTime"/>.
+		/// Returns <see cref="DateTime.MinValue"/> if the raw value was not returned.
+		/// </summary>
+		[XmlIgnore]
+		public DateTime DateTaken
+		{
+			get
+			{
+				if( datetaken_raw == null || datetaken_raw.Length == 0 ) return DateTime.MinValue;
+				return System.DateTime.Parse(datetaken_raw);
+			}
+		}
+
+		/// <remarks/>
+		[XmlAttribute("ownername", Form=XmlSchemaForm.Unqualified)]
+		public string OwnerName { get { return _ownerName; } set { _ownerName = value; } }
+
+		/// <remarks/>
+		[XmlAttribute("iconserver", Form=XmlSchemaForm.Unqualified)]
+		public string IconServer { get { return _iconServer; } set { _iconServer = value; } }
+
+		/// <summary>
+		/// Optional extra field containing the original format (jpg, png etc) of the
+		/// photo.
+		/// </summary>
+		[XmlAttribute("originalformat", Form=XmlSchemaForm.Unqualified)]
+		public string OriginalFormat { get { return _originalFormat; } set { _originalFormat = value; } }
+
+		/// <summary>
+		/// Optional extra field containing the original 'secret' of the
+		/// photo used for forming the Url.
+		/// </summary>
+		[XmlAttribute("originalsecret", Form=XmlSchemaForm.Unqualified)]
+		public string OriginalSecret { get { return _originalSecret; } set { _originalSecret = value; } }
+
+		/// <summary>
+		/// Undocumented tags atrribute. Renamed to CleanTags.
+		/// </summary>
+		[Obsolete("Renamed to CleanTags, as the tags are clean, not raw")]
+		public string RawTags { get { return _cleanTags; } set { _cleanTags = value; } }
+
+		/// <summary>
+		/// Tags, in their clean format (exception is machine tags which retain their machine encoding).
+		/// </summary>
+		[XmlAttribute("tags", Form=XmlSchemaForm.Unqualified)]
+		public string CleanTags { get { return _cleanTags; } set { _cleanTags = value; } }
+
+		/// <summary>
+		/// Machine tags
+		/// </summary>
+		[XmlAttribute("machine_tags", Form=XmlSchemaForm.Unqualified)]
+		public string MachineTags { get { return _machineTags; } set { _machineTags = value; } }
+
+		/// <summary>
+		/// The url to the web page for this photo. Uses the users userId, not their web alias, but
+		/// will still work.
+		/// </summary>
+		[XmlIgnore()]
+		public string WebUrl
+		{
+			get { return string.Format("http://www.flickr.com/photos/{0}/{1}/";, UserId, PhotoId); }
+		}
+
+		/// <summary>
+		/// The URL for the square thumbnail of a photo.
+		/// </summary>
+		[XmlIgnore()]
+		public string SquareThumbnailUrl
+		{
+			get { return Utils.UrlFormat(this, "_s", "jpg"); }
+		}
+
+		/// <summary>
+		/// The URL for the thumbnail of a photo.
+		/// </summary>
+		[XmlIgnore()]
+		public string ThumbnailUrl
+		{
+			get { return Utils.UrlFormat(this, "_t", "jpg"); }
+		}
+
+		/// <summary>
+		/// The URL for the small copy of a photo.
+		/// </summary>
+		[XmlIgnore()]
+		public string SmallUrl
+		{
+			get { return Utils.UrlFormat(this, "_m", "jpg"); }
+		}
+
+		/// <summary>
+		/// The URL for the medium copy of a photo.
+		/// </summary>
+		/// <remarks>There is a chance that extremely small images will not have a medium copy.
+		/// Use <see cref="Flickr.PhotosGetSizes"/> to get the available URLs for a photo.</remarks>
+		[XmlIgnore()]
+		public string MediumUrl
+		{
+			get { return Utils.UrlFormat(this, "", "jpg"); }
+		}
+
+		/// <summary>
+		/// The URL for the large copy of a photo.
+		/// </summary>
+		/// <remarks>There is a chance that small images will not have a large copy.
+		/// Use <see cref="Flickr.PhotosGetSizes"/> to get the available URLs for a photo.</remarks>
+		[XmlIgnore()]
+		public string LargeUrl
+		{
+			get { return Utils.UrlFormat(this, "_b", "jpg"); }
+		}
+
+		/// <summary>
+		/// If <see cref="OriginalFormat"/> was returned then this will contain the url of the original file.
+		/// </summary>
+		[XmlIgnore()]
+		public string OriginalUrl
+		{
+			get
+			{
+				if( OriginalFormat == null || OriginalFormat.Length == 0 )
+					throw new InvalidOperationException("No original format information available.");
+
+				return Utils.UrlFormat(this, "_o", OriginalFormat);
+			}
+		}
+
+		/// <summary>
+		/// Latitude. Will be 0 if Geo extras not specified.
+		/// </summary>
+		[XmlAttribute("latitude", Form=XmlSchemaForm.Unqualified)]
+		public decimal Latitude
+		{
+			get { return _latitude; }
+			set { _latitude = value; }
+		}
+
+		/// <summary>
+		/// Longitude. Will be 0 if Geo extras not specified.
+		/// </summary>
+		[XmlAttribute("longitude", Form=XmlSchemaForm.Unqualified)]
+		public decimal Longitude
+		{
+			get { return _longitude; }
+			set { _longitude = value; }
+		}
+
+		/// <summary>
+		/// Geo-location accuracy. A value of None means that the information was not returned.
+		/// </summary>
+		[XmlAttribute("accuracy", Form=XmlSchemaForm.Unqualified)]
+		public GeoAccuracy Accuracy
+		{
+			get { return _accuracy; }
+			set { _accuracy = value; }
+		}
+	}
+
+}

Added: trunk/extensions/Exporters/FlickrExport/FlickrNet/PhotoCounts.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FlickrExport/FlickrNet/PhotoCounts.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,62 @@
+ïusing System;
+using System.Xml.Serialization;
+using System.Xml.Schema;
+
+namespace FlickrNet
+{
+	/// <summary>
+	/// The information about the number of photos a user has.
+	/// </summary>
+	[System.Serializable]
+	public class PhotoCounts
+	{
+		/// <summary>
+		/// An array of <see cref="PhotoCountInfo"/> instances. Null if no counts returned.
+		/// </summary>
+		[XmlElement("photocount", Form=XmlSchemaForm.Unqualified)]
+		public PhotoCountInfo[] PhotoCountInfoCollection = new PhotoCountInfo[0];
+	}
+
+	/// <summary>
+	/// The specifics of a particular count.
+	/// </summary>
+	[System.Serializable]
+	public class PhotoCountInfo
+	{
+		/// <summary>Total number of photos between the FromDate and the ToDate.</summary>
+		/// <remarks/>
+		[XmlAttribute("count", Form=XmlSchemaForm.Unqualified)]
+		public int PhotoCount;
+
+		/// <summary>The From date as a <see cref="DateTime"/> object.</summary>
+		[XmlIgnore()]
+		public DateTime FromDate
+		{
+			get
+			{
+				return Utils.UnixTimestampToDate(fromdate_raw);
+			}
+		}
+
+		/// <summary>The To date as a <see cref="DateTime"/> object.</summary>
+		[XmlIgnore()]
+		public DateTime ToDate
+		{
+			get
+			{
+				return Utils.UnixTimestampToDate(todate_raw);
+			}
+		}
+
+		/// <summary>The original from date in unix timestamp format.</summary>
+		/// <remarks/>
+		[XmlAttribute("fromdate", Form=XmlSchemaForm.Unqualified)]
+		public string fromdate_raw;
+
+		/// <summary>The original to date in unix timestamp format.</summary>
+		/// <remarks/>
+		[XmlAttribute("todate", Form=XmlSchemaForm.Unqualified)]
+		public string todate_raw;
+
+	}
+}
\ No newline at end of file

Added: trunk/extensions/Exporters/FlickrExport/FlickrNet/PhotoDates.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FlickrExport/FlickrNet/PhotoDates.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,68 @@
+using System;
+using System.Xml.Serialization;
+using System.Xml.Schema;
+
+namespace FlickrNet
+{
+	/// <summary>
+	/// The date information for a photo.
+	/// </summary>
+	[System.Serializable]
+	public class PhotoDates
+	{
+		/// <summary>
+		/// The date the photo was posted (or uploaded).
+		/// </summary>
+		[XmlIgnore]
+		public DateTime PostedDate
+		{
+			get { return Utils.UnixTimestampToDate(raw_posted); }
+		}
+
+		/// <summary>
+		/// The raw timestamp for the date the photo was posted.
+		/// </summary>
+		/// <remarks>Use <see cref="PhotoDates.PostedDate"/> instead.</remarks>
+		[XmlAttribute("posted", Form=XmlSchemaForm.Unqualified)]
+		public long raw_posted;
+
+		/// <summary>
+		/// The date the photo was taken.
+		/// </summary>
+		[XmlIgnore]
+		public DateTime TakenDate
+		{
+			get { return DateTime.Parse(raw_taken); }
+		}
+
+		/// <summary>
+		/// The raw timestamp for the date the photo was taken.
+		/// </summary>
+		/// <remarks>Use <see cref="PhotoDates.TakenDate"/> instead.</remarks>
+		[XmlAttribute("taken", Form=XmlSchemaForm.Unqualified)]
+		public string raw_taken;
+
+		/// <summary>
+		/// The granularity of the taken date.
+		/// </summary>
+		[XmlAttribute("takengranularity", Form=XmlSchemaForm.Unqualified)]
+		public int TakenGranularity;
+
+		/// <summary>
+		/// The raw timestamp for the date the photo was last updated.
+		/// </summary>
+		[XmlAttribute("lastupdate", Form=XmlSchemaForm.Unqualified)]
+		public long raw_lastupdate;
+
+		/// <summary>
+		/// The date the photo was last updated (includes comments, tags, title, description etc).
+		/// </summary>
+		[XmlIgnore()]
+		public DateTime LastUpdated
+		{
+			get{ return Utils.UnixTimestampToDate(raw_lastupdate); }
+		}
+
+	}
+
+}

Added: trunk/extensions/Exporters/FlickrExport/FlickrNet/PhotoInfo.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FlickrExport/FlickrNet/PhotoInfo.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,506 @@
+ïusing System;
+using System.Xml.Serialization;
+using System.Xml.Schema;
+
+namespace FlickrNet
+{
+	/// <summary>
+    /// Detailed information returned by <see cref="Flickr.PhotosGetInfo(string)"/> or <see cref="Flickr.PhotosGetInfo(string, string)"/> methods.
+	/// </summary>
+	[System.Serializable]
+	public class PhotoInfo
+	{
+		private string _photoId;
+		private string _secret;
+		private string _server;
+		private string _farm;
+		private string _originalFormat;
+		private string _originalSecret;
+		private int _views;
+		private int _comments;
+		private string _title;
+		private string _description;
+		private PhotoInfoTags _tags = new PhotoInfoTags();
+
+		/// <summary>
+		/// The id of the photo.
+		/// </summary>
+		[XmlAttribute("id", Form=XmlSchemaForm.Unqualified)]
+		public string PhotoId { get { return _photoId; } set { _photoId = value; } }
+
+		/// <summary>
+		/// The secret of the photo. Used to calculate the URL (amongst other things).
+		/// </summary>
+		[XmlAttribute("secret", Form=XmlSchemaForm.Unqualified)]
+		public string Secret { get { return _secret; } set { _secret = value; } }
+
+		/// <summary>
+		/// The server on which the photo resides.
+		/// </summary>
+		[XmlAttribute("server", Form=XmlSchemaForm.Unqualified)]
+		public string Server { get { return _server; } set { _server = value; } }
+
+		/// <summary>
+		/// The server farm on which the photo resides.
+		/// </summary>
+		[XmlAttribute("farm", Form=XmlSchemaForm.Unqualified)]
+		public string Farm { get { return _farm; } set { _farm = value; } }
+
+		/// <summary>
+		/// The original format of the image (e.g. jpg, png etc).
+		/// </summary>
+		[XmlAttribute("originalformat", Form=XmlSchemaForm.Unqualified)]
+		public string OriginalFormat { get { return _originalFormat; } set { _originalFormat = value; } }
+
+		/// <summary>
+		/// Optional extra field containing the original 'secret' of the
+		/// photo used for forming the Url.
+		/// </summary>
+		[XmlAttribute("originalsecret", Form=XmlSchemaForm.Unqualified)]
+		public string OriginalSecret { get { return _originalSecret; } set { _originalSecret = value; } }
+
+		/// <summary>
+		/// The date the photo was uploaded (or 'posted').
+		/// </summary>
+		[XmlIgnore()]
+		public DateTime DateUploaded
+		{
+			get { return Utils.UnixTimestampToDate(dateuploaded_raw); }
+		}
+
+		/// <summary>
+		/// The raw value for when the photo was uploaded.
+		/// </summary>
+		[XmlAttribute("dateuploaded", Form=XmlSchemaForm.Unqualified)]
+		public string dateuploaded_raw;
+
+		/// <summary>
+		/// Is the photo a favourite of the current authorised user.
+		/// Will be 0 if the user is not authorised.
+		/// </summary>
+		[XmlAttribute("isfavorite", Form=XmlSchemaForm.Unqualified)]
+		public int IsFavourite;
+
+		/// <summary>
+		/// The license of the photo.
+		/// </summary>
+		[XmlAttribute("license", Form=XmlSchemaForm.Unqualified)]
+		public int License;
+
+		/// <summary>
+		/// The owner of the photo.
+		/// </summary>
+		/// <remarks>
+		/// See <see cref="PhotoInfoOwner"/> for more details.
+		/// </remarks>
+		[XmlElement("owner", Form=XmlSchemaForm.Unqualified)]
+		public PhotoInfoOwner Owner;
+
+		/// <summary>
+		/// The title of the photo.
+		/// </summary>
+		[XmlElement("title", Form=XmlSchemaForm.Unqualified)]
+		public string Title { get { return _title; } set { _title = value; } }
+
+		/// <summary>
+		/// The description of the photo.
+		/// </summary>
+		[XmlElement("description", Form=XmlSchemaForm.Unqualified)]
+		public string Description { get { return _description; } set { _description = value; } }
+
+		/// <summary>
+		/// The visibility of the photo.
+		/// </summary>
+		/// <remarks>
+		/// See <see cref="PhotoInfoVisibility"/> for more details.
+		/// </remarks>
+		[XmlElement("visibility", Form=XmlSchemaForm.Unqualified)]
+		public PhotoInfoVisibility Visibility;
+
+		/// <summary>
+		/// The permissions of the photo.
+		/// </summary>
+		/// <remarks>
+		/// See <see cref="PhotoInfoPermissions"/> for more details.
+		/// </remarks>
+		[XmlElement("permissions", Form=XmlSchemaForm.Unqualified)]
+		public PhotoInfoPermissions Permissions;
+
+		/// <summary>
+		/// The editability of the photo.
+		/// </summary>
+		/// <remarks>
+		/// See <see cref="PhotoInfoEditability"/> for more details.
+		/// </remarks>
+		[XmlElement("editability", Form=XmlSchemaForm.Unqualified)]
+		public PhotoInfoEditability Editability;
+
+		/// <summary>
+		/// The number of comments the photo has.
+		/// </summary>
+		[XmlElement("comments", Form=XmlSchemaForm.Unqualified)]
+		public int CommentsCount
+		{
+			get { return _comments; } set { _comments = value; }
+		}
+
+		/// <summary>
+		/// The number of views the photo has.
+		/// </summary>
+		[XmlAttribute("views", Form=XmlSchemaForm.Unqualified)]
+		public int ViewCount
+		{
+			get { return _views; } set { _views = value; }
+		}
+
+		/// <summary>
+		/// The notes for the photo.
+		/// </summary>
+		[XmlElement("notes", Form=XmlSchemaForm.Unqualified)]
+		public PhotoInfoNotes Notes;
+
+		/// <summary>
+		/// The tags for the photo.
+		/// </summary>
+		[XmlElement("tags", Form=XmlSchemaForm.Unqualified)]
+		public PhotoInfoTags Tags
+		{
+			get { return _tags; }
+			set { _tags = (value==null?new PhotoInfoTags():value); }
+		}
+
+		/// <summary>
+		/// The EXIF tags for the photo.
+		/// </summary>
+		[XmlElement("exif", Form=XmlSchemaForm.Unqualified)]
+		public ExifTag[] ExifTagCollection;
+
+		/// <summary>
+		/// The dates (uploaded and taken dates) for the photo.
+		/// </summary>
+		[XmlElement("dates", Form=XmlSchemaForm.Unqualified)]
+		public PhotoDates Dates;
+
+		/// <summary>
+		/// The location information of this photo, if available.
+		/// </summary>
+		/// <remarks>
+		/// Will be null if the photo has no location information stored on Flickr.
+		/// </remarks>
+		[XmlElement("location", Form=XmlSchemaForm.Unqualified)]
+		public PhotoLocation Location;
+
+		/// <summary>
+		/// The Web url for flickr web page for this photo.
+		/// </summary>
+		[XmlIgnore()]
+		public string WebUrl
+		{
+			get { return string.Format("http://www.flickr.com/photos/{0}/{1}/";, Owner.UserId, PhotoId); }
+		}
+
+		/// <summary>
+		/// The URL for the square thumbnail for the photo.
+		/// </summary>
+		[XmlIgnore()]
+		public string SquareThumbnailUrl
+		{
+			get { return Utils.UrlFormat(this, "_s", "jpg"); }
+		}
+
+		/// <summary>
+		/// The URL for the thumbnail for the photo.
+		/// </summary>
+		[XmlIgnore()]
+		public string ThumbnailUrl
+		{
+			get { return Utils.UrlFormat(this, "_t", "jpg"); }
+		}
+
+		/// <summary>
+		/// The URL for the small version of this photo.
+		/// </summary>
+		[XmlIgnore()]
+		public string SmallUrl
+		{
+			get { return Utils.UrlFormat(this, "_m", "jpg"); }
+		}
+
+		/// <summary>
+		/// The URL for the medium version of this photo.
+		/// </summary>
+		/// <remarks>
+		/// There is no guarentee that this size of the image actually exists.
+		/// Use <see cref="Flickr.PhotosGetSizes"/> to get a list of existing photo URLs.
+		/// </remarks>
+		[XmlIgnore()]
+		public string MediumUrl
+		{
+			get { return Utils.UrlFormat(this, "", "jpg"); }
+		}
+
+		/// <summary>
+		/// The URL for the large version of this photo.
+		/// </summary>
+		/// <remarks>
+		/// There is no guarentee that this size of the image actually exists.
+		/// Use <see cref="Flickr.PhotosGetSizes"/> to get a list of existing photo URLs.
+		/// </remarks>
+		[XmlIgnore()]
+		public string LargeUrl
+		{
+			get { return Utils.UrlFormat(this, "_b", "jpg"); }
+		}
+
+		/// <summary>
+		/// If <see cref="OriginalFormat"/> was returned then this will contain the url of the original file.
+		/// </summary>
+		[XmlIgnore()]
+		public string OriginalUrl
+		{
+			get
+			{
+				if( OriginalFormat == null || OriginalFormat.Length == 0 )
+					throw new InvalidOperationException("No original format information available.");
+
+				return Utils.UrlFormat(this, "_o", OriginalFormat);
+			}
+		}
+	}
+
+	/// <summary>
+	/// The information about the owner of a photo.
+	/// </summary>
+	[System.Serializable]
+	public class PhotoInfoOwner
+	{
+		/// <summary>
+		/// The id of the own of the photo.
+		/// </summary>
+		[XmlAttribute("nsid", Form=XmlSchemaForm.Unqualified)]
+		public string UserId;
+
+		/// <summary>
+		/// The username of the owner of the photo.
+		/// </summary>
+		[XmlAttribute("username", Form=XmlSchemaForm.Unqualified)]
+		public string UserName;
+
+		/// <summary>
+		/// The real name (as stored on Flickr) of the owner of the photo.
+		/// </summary>
+		[XmlAttribute("realname", Form=XmlSchemaForm.Unqualified)]
+		public string RealName;
+
+		/// <summary>
+		/// The location (as stored on Flickr) of the owner of the photo.
+		/// </summary>
+		[XmlAttribute("location", Form=XmlSchemaForm.Unqualified)]
+		public string Location;
+	}
+
+	/// <summary>
+	/// The visibility of the photo.
+	/// </summary>
+	[System.Serializable]
+	public class PhotoInfoVisibility
+	{
+		/// <summary>
+		/// Is the photo visible to the public.
+		/// </summary>
+		[XmlAttribute("ispublic", Form=XmlSchemaForm.Unqualified)]
+		public int IsPublic;
+
+		/// <summary>
+		/// Is the photo visible to contacts marked as friends.
+		/// </summary>
+		[XmlAttribute("isfriend", Form=XmlSchemaForm.Unqualified)]
+		public int IsFriend;
+
+		/// <summary>
+		/// Is the photo visible to contacts marked as family.
+		/// </summary>
+		[XmlAttribute("isfamily", Form=XmlSchemaForm.Unqualified)]
+		public int IsFamily;
+	}
+
+	/// <summary>
+	/// Who has permissions to add information to this photo (comments, tag and notes).
+	/// </summary>
+	[System.Serializable]
+	public class PhotoInfoPermissions
+	{
+		/// <summary>
+		/// Who has permissions to add comments to this photo.
+		/// </summary>
+		[XmlAttribute("permcomment", Form=XmlSchemaForm.Unqualified)]
+		public PermissionComment PermissionComment;
+
+		/// <summary>
+		/// Who has permissions to add meta data (tags and notes) to this photo.
+		/// </summary>
+		[XmlAttribute("permaddmeta", Form=XmlSchemaForm.Unqualified)]
+		public PermissionAddMeta PermissionAddMeta;
+	}
+
+	/// <summary>
+	/// Information about who can edit the details of a photo.
+	/// </summary>
+	[System.Serializable]
+	public class PhotoInfoEditability
+	{
+		/// <summary>
+		/// Can the authorized user add new comments.
+		/// </summary>
+		/// <remarks>
+		/// "1" = true, "0" = false.
+		/// </remarks>
+		[XmlAttribute("cancomment", Form=XmlSchemaForm.Unqualified)]
+		public string CanComment;
+
+		/// <summary>
+		/// Can the authorized user add new meta data (tags and notes).
+		/// </summary>
+		/// <remarks>
+		/// "1" = true, "0" = false.
+		/// </remarks>
+		[XmlAttribute("canaddmeta", Form=XmlSchemaForm.Unqualified)]
+		public string CanAddMeta;
+	}
+
+	/// <summary>
+	/// A class containing information about the notes for a photo.
+	/// </summary>
+	[System.Serializable]
+	public class PhotoInfoNotes
+	{
+		/// <summary>
+		/// A collection of notes for this photo.
+		/// </summary>
+		[XmlElement("note", Form=XmlSchemaForm.Unqualified)]
+		public PhotoInfoNote[] NoteCollection;
+	}
+
+	/// <summary>
+	/// A class containing information about a note on a photo.
+	/// </summary>
+	[System.Serializable]
+	public class PhotoInfoNote
+	{
+		/// <summary>
+		/// The notes unique ID.
+		/// </summary>
+		[XmlAttribute("id", Form=XmlSchemaForm.Unqualified)]
+		public string NoteId;
+
+		/// <summary>
+		/// The User ID of the user who wrote the note.
+		/// </summary>
+		[XmlAttribute("author", Form=XmlSchemaForm.Unqualified)]
+		public string AuthorId;
+
+		/// <summary>
+		/// The name of the user who wrote the note.
+		/// </summary>
+		[XmlAttribute("authorname", Form=XmlSchemaForm.Unqualified)]
+		public string AuthorName;
+
+		/// <summary>
+		/// The x (left) position of the top left corner of the note.
+		/// </summary>
+		[XmlAttribute("x", Form=XmlSchemaForm.Unqualified)]
+		public int XPosition;
+
+		/// <summary>
+		/// The y (top) position of the top left corner of the note.
+		/// </summary>
+		[XmlAttribute("y", Form=XmlSchemaForm.Unqualified)]
+		public int YPosition;
+
+		/// <summary>
+		/// The width of the note.
+		/// </summary>
+		[XmlAttribute("w", Form=XmlSchemaForm.Unqualified)]
+		public int Width;
+
+		/// <summary>
+		/// The height of the note.
+		/// </summary>
+		[XmlAttribute("h", Form=XmlSchemaForm.Unqualified)]
+		public int Height;
+
+		/// <summary>
+		/// The text of the note.
+		/// </summary>
+		[XmlText()]
+		public string NoteText;
+	}
+
+	/// <summary>
+	/// A class containing a collection of tags for the photo.
+	/// </summary>
+	[System.Serializable]
+	public class PhotoInfoTags
+	{
+		private PhotoInfoTag[] _tags = new PhotoInfoTag[0];
+
+		/// <summary>
+		/// A collection of tags for the photo.
+		/// </summary>
+		[XmlElement("tag", Form=XmlSchemaForm.Unqualified)]
+		public PhotoInfoTag[] TagCollection
+		{
+			get { return _tags; }
+			set { _tags = (value==null?new PhotoInfoTag[0]:value); }
+		}
+	}
+
+	/// <summary>
+	/// The details of a tag of a photo.
+	/// </summary>
+	[System.Serializable]
+	public class PhotoInfoTag
+	{
+		private int _machineTag;
+		private string _tagId;
+		private string _authorId;
+		private string _authorName;
+
+		/// <summary>
+		/// The id of the tag.
+		/// </summary>
+		[XmlAttribute("id", Form=XmlSchemaForm.Unqualified)]
+		public string TagId { get { return _tagId; } set { _tagId = value; } }
+
+		/// <summary>
+		/// The author id of the tag.
+		/// </summary>
+		[XmlAttribute("author", Form=XmlSchemaForm.Unqualified)]
+		public string AuthorId { get { return _authorId; } set { _authorId = value; } }
+
+		/// <summary>
+		/// Author of the tag - only available if using <see cref="Flickr.TagsGetListPhoto"/>.
+		/// </summary>
+		[XmlAttribute("authorname", Form=XmlSchemaForm.Unqualified)]
+		public string AuthorName  { get { return _authorName; } set { _authorName = value; } }
+
+		/// <summary>
+		/// Raw copy of the tag, as the user entered it.
+		/// </summary>
+		[XmlAttribute("raw", Form=XmlSchemaForm.Unqualified)]
+		public string Raw;
+
+		/// <summary>
+		/// Raw copy of the tag, as the user entered it.
+		/// </summary>
+		[XmlAttribute("machine_tag", Form=XmlSchemaForm.Unqualified)]
+		public int IsMachineTag { get { return _machineTag; } set { _machineTag = value; } }
+
+		/// <summary>
+		/// The actually tag.
+		/// </summary>
+		[XmlText()]
+		public string TagText;
+	}
+
+}

Added: trunk/extensions/Exporters/FlickrExport/FlickrNet/PhotoLocation.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FlickrExport/FlickrNet/PhotoLocation.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,37 @@
+using System;
+using System.Xml.Serialization;
+using System.Xml.Schema;
+
+namespace FlickrNet
+{
+	/// <summary>
+	/// Summary description for PhotoLocation.
+	/// </summary>
+	public class PhotoLocation
+	{
+		/// <summary>
+		/// Default constructor
+		/// </summary>
+		public PhotoLocation()
+		{
+		}
+
+		/// <summary>
+		/// The latitude of the photo.
+		/// </summary>
+		[XmlAttribute("latitude", Form=XmlSchemaForm.Unqualified)]
+		public double Latitude;
+
+		/// <summary>
+		/// The longitude of the photo.
+		/// </summary>
+		[XmlAttribute("longitude", Form=XmlSchemaForm.Unqualified)]
+		public double Longitude;
+
+		/// <summary>
+		/// The accuracy of the location information. See <see cref="GeoAccuracy"/> for accuracy levels.
+		/// </summary>
+		[XmlAttribute("accuracy", Form=XmlSchemaForm.Unqualified)]
+		public GeoAccuracy Accuracy;
+	}
+}

Added: trunk/extensions/Exporters/FlickrExport/FlickrNet/PhotoPermissions.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FlickrExport/FlickrNet/PhotoPermissions.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,72 @@
+using System;
+using System.Xml;
+
+namespace FlickrNet
+{
+	/// <summary>
+	/// Permissions for the selected photo.
+	/// </summary>
+	[System.Serializable]
+	public class PhotoPermissions
+	{
+		private string _photoId;
+		private bool _isPublic;
+		private bool _isFriend;
+		private bool _isFamily;
+		private PermissionAddMeta _permAddMeta;
+		private PermissionComment _permComment;
+
+		internal PhotoPermissions(XmlElement element)
+		{
+			if( element.Attributes.GetNamedItem("id") != null )
+				_photoId = element.Attributes.GetNamedItem("id").Value;
+			if( element.Attributes.GetNamedItem("ispublic") != null )
+				_isPublic = element.Attributes.GetNamedItem("ispublic").Value=="1";
+			if( element.Attributes.GetNamedItem("isfamily") != null )
+				_isFamily = element.Attributes.GetNamedItem("isfamily").Value=="1";
+			if( element.Attributes.GetNamedItem("isfriend") != null )
+				_isFriend = element.Attributes.GetNamedItem("isfriend").Value=="1";
+			if( element.Attributes.GetNamedItem("permcomment") != null )
+				_permComment = (PermissionComment)Enum.Parse(typeof(PermissionComment), element.Attributes.GetNamedItem("permcomment").Value, true);
+			if( element.Attributes.GetNamedItem("permaddmeta") != null )
+				_permAddMeta = (PermissionAddMeta)Enum.Parse(typeof(PermissionAddMeta), element.Attributes.GetNamedItem("permaddmeta").Value, true);
+		}
+
+		/// <remarks/>
+		public string PhotoId
+		{
+			get { return _photoId; }
+		}
+
+		/// <remarks/>
+		public bool IsPublic
+		{
+			get { return _isPublic; }
+		}
+
+		/// <remarks/>
+		public bool IsFriend
+		{
+			get { return _isFriend; }
+		}
+
+		/// <remarks/>
+		public bool IsFamily
+		{
+			get { return _isFamily; }
+		}
+
+		/// <remarks/>
+		public PermissionComment PermissionComment
+		{
+			get { return _permComment; }
+		}
+
+		/// <remarks/>
+		public PermissionAddMeta PermissionAddMeta
+		{
+			get { return _permAddMeta; }
+		}
+	}
+
+}

Added: trunk/extensions/Exporters/FlickrExport/FlickrNet/PhotoSearchExtras.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FlickrExport/FlickrNet/PhotoSearchExtras.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,70 @@
+using System;
+
+namespace FlickrNet
+{
+	/// <summary>
+	/// Which photo search extras to be included. Can be combined to include more than one
+	/// value.
+	/// </summary>
+	/// <example>
+	/// The following code sets options to return both the license and owner name along with
+	/// the other search results.
+	/// <code>
+	/// PhotoSearchOptions options = new PhotoSearchOptions();
+	/// options.Extras = PhotoSearchExtras.License &amp; PhotoSearchExtras.OwnerName
+	/// </code>
+	/// </example>
+	[Flags]
+	[Serializable]
+	public enum PhotoSearchExtras
+	{
+		/// <summary>
+		/// No extras selected.
+		/// </summary>
+		None = 0,
+		/// <summary>
+		/// Returns a license.
+		/// </summary>
+		License = 1,
+		/// <summary>
+		/// Returned the date the photos was uploaded.
+		/// </summary>
+		DateUploaded = 2,
+		/// <summary>
+		/// Returned the date the photo was taken.
+		/// </summary>
+		DateTaken = 4,
+		/// <summary>
+		/// Returns the name of the owner of the photo.
+		/// </summary>
+		OwnerName = 8,
+		/// <summary>
+		/// Returns the server for the buddy icon for this user.
+		/// </summary>
+		IconServer = 16,
+		/// <summary>
+		/// Returns the extension for the original format of this photo.
+		/// </summary>
+		OriginalFormat = 32,
+		/// <summary>
+		/// Returns the date the photo was last updated.
+		/// </summary>
+		LastUpdated = 64,
+		/// <summary>
+		/// Returns Tags attribute
+		/// </summary>
+		Tags = 128,
+		/// <summary>
+		/// Geo-location information
+		/// </summary>
+		Geo = 256,
+		/// <summary>
+		/// Machine encoded tags
+		/// </summary>
+		MachineTags = 512,
+		/// <summary>
+		/// Returns all the above information.
+		/// </summary>
+		All = License | DateUploaded | DateTaken | OwnerName | IconServer | OriginalFormat | LastUpdated | Tags | Geo | MachineTags
+	}
+}

Added: trunk/extensions/Exporters/FlickrExport/FlickrNet/PhotoSearchOptions.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FlickrExport/FlickrNet/PhotoSearchOptions.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,363 @@
+using System;
+
+namespace FlickrNet
+{
+	/// <summary>
+	/// Summary description for PhotoSearchOptions.
+	/// </summary>
+	[Serializable]
+	public class PhotoSearchOptions
+	{
+		private string _userId;
+		private string _tags;
+		private TagMode _tagMode = TagMode.None;
+		private string _machineTags;
+		private MachineTagMode _machineTagMode = MachineTagMode.None;
+		private string _text;
+		private DateTime _minUploadDate = DateTime.MinValue;
+		private DateTime _maxUploadDate = DateTime.MinValue;
+		private DateTime _minTakenDate = DateTime.MinValue;
+		private DateTime _maxTakenDate = DateTime.MinValue;
+		private System.Collections.ArrayList _licenses = new System.Collections.ArrayList();
+		private PhotoSearchExtras _extras = PhotoSearchExtras.None;
+		private int _perPage = 0;
+		private int _page = 0;
+		private PhotoSearchSortOrder _sort = PhotoSearchSortOrder.None;
+		private PrivacyFilter _privacyFilter = PrivacyFilter.None;
+		private BoundaryBox _boundaryBox = new BoundaryBox();
+
+		/// <summary>
+		/// Creates a new instance of the search options.
+		/// </summary>
+		public PhotoSearchOptions()
+		{
+		}
+
+		/// <summary>
+		/// Creates a new instance of the search options, setting the UserId property to the parameter
+		/// passed in.
+		/// </summary>
+		/// <param name="userId">The ID of the User to search for.</param>
+		public PhotoSearchOptions(string userId) : this(userId, null, TagMode.AllTags, null)
+		{
+		}
+
+		/// <summary>
+		/// Create an instance of the <see cref="PhotoSearchOptions"/> for a given user ID and tag list.
+		/// </summary>
+		/// <param name="userId">The ID of the User to search for.</param>
+		/// <param name="tags">The tags (comma delimited) to search for. Will match all tags.</param>
+		public PhotoSearchOptions(string userId, string tags) : this( userId, tags, TagMode.AllTags, null)
+		{
+		}
+
+		/// <summary>
+		/// Create an instance of the <see cref="PhotoSearchOptions"/> for a given user ID and tag list,
+		/// with the selected tag mode.
+		/// </summary>
+		/// <param name="userId">The ID of the User to search for.</param>
+		/// <param name="tags">The tags (comma delimited) to search for.</param>
+		/// <param name="tagMode">The <see cref="TagMode"/> to use to search.</param>
+		public PhotoSearchOptions(string userId, string tags, TagMode tagMode) : this( userId, tags, tagMode, null)
+		{
+		}
+
+		/// <summary>
+		/// Create an instance of the <see cref="PhotoSearchOptions"/> for a given user ID and tag list,
+		/// with the selected tag mode, and containing the selected text.
+		/// </summary>
+		/// <param name="userId">The ID of the User to search for.</param>
+		/// <param name="tags">The tags (comma delimited) to search for.</param>
+		/// <param name="tagMode">The <see cref="TagMode"/> to use to search.</param>
+		/// <param name="text">The text to search for in photo title and descriptions.</param>
+		public PhotoSearchOptions(string userId, string tags, TagMode tagMode, string text)
+		{
+			this.UserId = userId;
+			this.Tags = tags;
+			this.TagMode = tagMode;
+			this.Text = text;
+		}
+
+		/// <summary>
+		/// The user Id of the user to search on. Defaults to null for no specific user.
+		/// </summary>
+		public string UserId
+		{
+			get { return _userId; }
+			set { _userId = value; }
+		}
+
+		/// <summary>
+		/// A comma delimited list of tags
+		/// </summary>
+		public string Tags
+		{
+			get { return _tags; }
+			set { _tags = value; }
+		}
+
+		/// <summary>
+		/// Tag mode can either be 'all', or 'any'. Defaults to <see cref="FlickrNet.TagMode.AllTags"/>
+		/// </summary>
+		public TagMode TagMode
+		{
+			get { return _tagMode; }
+			set { _tagMode = value; }
+		}
+
+		internal string TagModeString
+		{
+			get
+			{
+				switch(_tagMode)
+				{
+					case TagMode.None:
+						return "";
+					case TagMode.AllTags:
+						return "all";
+					case TagMode.AnyTag:
+						return "any";
+					case TagMode.Boolean:
+						return "bool";
+					default:
+						return "";
+				}
+			}
+		}
+
+		/// <summary>
+		/// Search for the given machine tags.
+		/// </summary>
+		/// <remarks>
+		/// See http://www.flickr.com/services/api/flickr.photos.search.html for details
+		/// on how to search for machine tags.
+		/// </remarks>
+		public string MachineTags
+		{
+			get { return _machineTags; } set { _machineTags = value; }
+		}
+
+		/// <summary>
+		/// The machine tag mode.
+		/// </summary>
+		/// <remarks>
+		/// Allowed values are any and all. It defaults to any if none specified.
+		/// </remarks>
+		public MachineTagMode MachineTagMode
+		{
+			get { return _machineTagMode; } set { _machineTagMode = value; }
+		}
+
+		internal string MachineTagModeString
+		{
+			get
+			{
+				switch(_machineTagMode)
+				{
+					case MachineTagMode.None:
+						return "";
+					case MachineTagMode.AllTags:
+						return "all";
+					case MachineTagMode.AnyTag:
+						return "any";
+					default:
+						return "";
+				}
+			}
+		}
+
+		/// <summary>
+		/// Search for the given text in photo titles and descriptions.
+		/// </summary>
+		public string Text
+		{
+			get { return _text; }
+			set { _text = value; }
+		}
+
+		/// <summary>
+		/// Minimum date uploaded. Defaults to <see cref="DateTime.MinValue"/> which
+		/// signifies that the value is not to be used.
+		/// </summary>
+		public DateTime MinUploadDate
+		{
+			get { return _minUploadDate; }
+			set { _minUploadDate = value; }
+		}
+
+		/// <summary>
+		/// Maximum date uploaded. Defaults to <see cref="DateTime.MinValue"/> which
+		/// signifies that the value is not to be used.
+		/// </summary>
+		public DateTime MaxUploadDate
+		{
+			get { return _maxUploadDate; }
+			set { _maxUploadDate = value; }
+		}
+
+		/// <summary>
+		/// Minimum date taken. Defaults to <see cref="DateTime.MinValue"/> which
+		/// signifies that the value is not to be used.
+		/// </summary>
+		public DateTime MinTakenDate
+		{
+			get { return _minTakenDate; }
+			set { _minTakenDate = value; }
+		}
+
+		/// <summary>
+		/// Maximum date taken. Defaults to <see cref="DateTime.MinValue"/> which
+		/// signifies that the value is not to be used.
+		/// </summary>
+		public DateTime MaxTakenDate
+		{
+			get { return _maxTakenDate; }
+			set { _maxTakenDate = value; }
+		}
+
+		/// <summary>
+		/// Only return licenses with the selected license number.
+		/// See http://www.flickr.com/services/api/flickr.photos.licenses.getInfo.html
+		/// for more details on the numbers to use.
+		/// </summary>
+		[Obsolete("Use AddLicense/RemoveLicense to add/remove licenses")]
+		public int License
+		{
+			get
+			{
+				if( _licenses.Count == 0 )
+					return 0;
+				else
+					return (int)_licenses[0];
+			}
+			set
+			{
+				if( _licenses.Count == 0 )
+					_licenses.Add(value);
+				else
+					_licenses[0] = value;
+			}
+		}
+
+		/// <summary>
+		/// Returns a copy of the licenses to be searched for.
+		/// </summary>
+		public int[] Licenses
+		{
+			get
+			{
+				return (int[])_licenses.ToArray(typeof(int));
+			}
+		}
+
+		/// <summary>
+		/// Adds a new license to the list of licenses to be searched for.
+		/// </summary>
+		/// <param name="license">The number of the license to search for.</param>
+		public void AddLicense(int license)
+		{
+			if( !_licenses.Contains(license) ) _licenses.Add(license);
+		}
+
+		/// <summary>
+		/// Removes a license from the list of licenses to be searched for.
+		/// </summary>
+		/// <param name="license">The number of the license to remove.</param>
+		public void RemoveLicense(int license)
+		{
+			if( _licenses.Contains(license) ) _licenses.Remove(license);
+		}
+
+		/// <summary>
+		/// Optional extras to return, defaults to all. See <see cref="PhotoSearchExtras"/> for more details.
+		/// </summary>
+		public PhotoSearchExtras Extras
+		{
+			get { return _extras; }
+			set { _extras = value; }
+		}
+
+		/// <summary>
+		/// Number of photos to return per page. Defaults to 100.
+		/// </summary>
+		public int PerPage
+		{
+			get { return _perPage; }
+			set { _perPage = value; }
+		}
+
+		/// <summary>
+		/// The page to return. Defaults to page 1.
+		/// </summary>
+		public int Page
+		{
+			get { return _page; }
+			set
+			{
+				if( value < 0 ) throw new ArgumentOutOfRangeException("Page", "Must be greater than 0");
+				_page = value;
+			}
+		}
+
+		/// <summary>
+		/// The sort order of the returned list. Default is <see cref="PhotoSearchSortOrder.None"/>.
+		/// </summary>
+		public PhotoSearchSortOrder SortOrder
+		{
+			get { return _sort; }
+			set { _sort = value; }
+		}
+
+		/// <summary>
+		/// The privacy fitler to filter the search on.
+		/// </summary>
+		public PrivacyFilter PrivacyFilter
+		{
+			get { return _privacyFilter; }
+			set { _privacyFilter = value; }
+		}
+
+		/// <summary>
+		/// The boundary box for which to search for geo location photos.
+		/// </summary>
+		public BoundaryBox BoundaryBox
+		{
+			get { return _boundaryBox; }
+			set
+			{
+				if( value == null )
+					  _boundaryBox = new BoundaryBox();
+				  else
+					  _boundaryBox = value;
+			}
+		}
+
+		/// <summary>
+		/// The accuracy of the search for geo location photos.
+		/// </summary>
+		/// <remarks>
+		/// Can also be set as a property of the <see cref="BoundaryBox"/> property.
+		/// </remarks>
+		public GeoAccuracy Accuracy
+		{
+			get { return _boundaryBox==null?GeoAccuracy.None:_boundaryBox.Accuracy; }
+			set
+			{
+				if (_boundaryBox==null) { _boundaryBox = new BoundaryBox(); }
+				_boundaryBox.Accuracy = value;
+			}
+
+		}
+
+		internal string ExtrasString
+		{
+			get { return Utils.ExtrasToString(Extras); }
+		}
+
+		internal string SortOrderString
+		{
+			get	{ return Utils.SortOrderToString(_sort); }
+		}
+	}
+
+}

Added: trunk/extensions/Exporters/FlickrExport/FlickrNet/PhotoSearchOrder.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FlickrExport/FlickrNet/PhotoSearchOrder.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,46 @@
+using System;
+
+namespace FlickrNet
+{
+	/// <summary>
+	/// The sort order for the <see cref="Flickr.PhotosSearch(PhotoSearchOptions)"/>,
+	/// <see cref="Flickr.PhotosGetWithGeoData()"/>, <see cref="Flickr.PhotosGetWithoutGeoData()"/> methods.
+	/// </summary>
+	[Serializable]
+	public enum PhotoSearchSortOrder
+	{
+		/// <summary>
+		/// No sort order.
+		/// </summary>
+		None,
+		/// <summary>
+		/// Sort by date uploaded (posted).
+		/// </summary>
+		DatePostedAsc,
+		/// <summary>
+		/// Sort by date uploaded (posted) in descending order.
+		/// </summary>
+		DatePostedDesc,
+		/// <summary>
+		/// Sort by date taken.
+		/// </summary>
+		DateTakenAsc,
+		/// <summary>
+		/// Sort by date taken in descending order.
+		/// </summary>
+		DateTakenDesc,
+		/// <summary>
+		/// Sort by interestingness.
+		/// </summary>
+		InterestingnessAsc,
+		/// <summary>
+		/// Sort by interestingness in descending order.
+		/// </summary>
+		InterestingnessDesc,
+		/// <summary>
+		/// Sort by relevance
+		/// </summary>
+		Relevance
+	}
+
+}

Added: trunk/extensions/Exporters/FlickrExport/FlickrNet/PhotoSets.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FlickrExport/FlickrNet/PhotoSets.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,200 @@
+ïusing System.Xml.Serialization;
+using System.Xml.Schema;
+
+namespace FlickrNet
+{
+	/// <summary>
+	/// Collection containing a users photosets.
+	/// </summary>
+	[System.Serializable]
+	public class Photosets
+	{
+		private int _canCreate;
+		private Photoset[] _photosetCollection = new Photoset[0];
+
+		/// <summary>
+		/// Can the user create more photosets.
+		/// </summary>
+		/// <remarks>
+		/// 1 meants yes, 0 means no.
+		/// </remarks>
+		[XmlAttribute("cancreate", Form=XmlSchemaForm.Unqualified)]
+		public int CanCreate
+		{
+			get { return _canCreate; }
+			set { _canCreate = value; }
+		}
+
+		/// <summary>
+		/// An array of <see cref="Photoset"/> objects.
+		/// </summary>
+		[XmlElement("photoset", Form=XmlSchemaForm.Unqualified)]
+		public Photoset[] PhotosetCollection
+		{
+			get { return _photosetCollection; }
+			set
+			{
+				if( value== null )
+					_photosetCollection = new Photoset[0];
+				else
+					_photosetCollection = value;
+			}
+		}
+	}
+
+	/// <summary>
+	/// A set of properties for the photoset.
+	/// </summary>
+	[System.Serializable]
+	public class Photoset
+	{
+		private string _photosetId;
+		private string _url;
+		private string _ownerId;
+		private string _primaryPhotoId;
+		private string _secret;
+		private string _server;
+		private string _farm;
+		private int _numberOfPhotos;
+		private string _title;
+		private string _description;
+		private Photo[] _photoCollection = new Photo[0];
+
+		/// <summary>
+		/// The ID of the photoset.
+		/// </summary>
+		[XmlAttribute("id", Form=XmlSchemaForm.Unqualified)]
+		public string PhotosetId
+		{
+			get { return _photosetId; } set { _photosetId = value; }
+		}
+
+		/// <summary>
+		/// The URL of the photoset.
+		/// </summary>
+		[XmlAttribute("url", Form=XmlSchemaForm.Unqualified)]
+		public string Url
+		{
+			get { return _url; } set { _url = value; }
+		}
+
+		/// <summary>
+		/// The ID of the owner of the photoset.
+		/// </summary>
+		[XmlAttribute("owner", Form=XmlSchemaForm.Unqualified)]
+		public string OwnerId
+		{
+			get { return _ownerId; } set { _ownerId = value; }
+		}
+
+		/// <summary>
+		/// The photo ID of the primary photo of the photoset.
+		/// </summary>
+		[XmlAttribute("primary", Form=XmlSchemaForm.Unqualified)]
+		public string PrimaryPhotoId
+		{
+			get { return _primaryPhotoId; } set { _primaryPhotoId = value; }
+		}
+
+		/// <summary>
+		/// The secret for the primary photo for the photoset.
+		/// </summary>
+		[XmlAttribute("secret", Form=XmlSchemaForm.Unqualified)]
+		public string Secret
+		{
+			get { return _secret; } set { _secret = value; }
+		}
+
+		/// <summary>
+		/// The server for the primary photo for the photoset.
+		/// </summary>
+		[XmlAttribute("server", Form=XmlSchemaForm.Unqualified)]
+		public string Server
+		{
+			get { return _server; } set { _server = value; }
+		}
+
+		/// <summary>
+		/// The server farm for the primary photo for the photoset.
+		/// </summary>
+		[XmlAttribute("farm", Form=XmlSchemaForm.Unqualified)]
+		public string Farm
+		{
+			get { return _farm; } set { _farm = value; }
+		}
+
+		/// <summary>
+		/// The number of photos in the photoset.
+		/// </summary>
+		[XmlAttribute("photos", Form=XmlSchemaForm.Unqualified)]
+		public int NumberOfPhotos
+		{
+			get { return _numberOfPhotos; } set { _numberOfPhotos = value; }
+		}
+
+		/// <summary>
+		/// The title of the photoset.
+		/// </summary>
+		[XmlElement("title", Form=XmlSchemaForm.Unqualified)]
+		public string Title
+		{
+			get { return _title; } set { _title = value; }
+		}
+
+		/// <summary>
+		/// The description of the photoset.
+		/// </summary>
+		[XmlElement("description", Form=XmlSchemaForm.Unqualified)]
+		public string Description
+		{
+			get { return _description; } set { _description = value; }
+		}
+
+		/// <summary>
+		/// An array of photo objects in the photoset.
+		/// </summary>
+		[XmlElement("photo", Form=XmlSchemaForm.Unqualified)]
+		public Photo[] PhotoCollection
+		{
+			get { return _photoCollection; }
+			set
+			{
+				if( value == null )
+					_photoCollection = new Photo[0];
+				else
+					_photoCollection = value;
+			}
+		}
+
+		/// <summary>
+		/// The URL for the thumbnail of a photo.
+		/// </summary>
+		[XmlIgnore()]
+		public string PhotosetThumbnailUrl
+		{
+			get { return Utils.UrlFormat(this, "_t", "jpg"); }
+		}
+
+		/// <summary>
+		/// The URL for the square thumbnail of a photo.
+		/// </summary>
+		[XmlIgnore()]
+		public string PhotosetSquareThumbnailUrl
+		{
+			get { return Utils.UrlFormat(this, "_s", "jpg"); }
+		}
+
+		/// <summary>
+		/// The URL for the small copy of a photo.
+		/// </summary>
+		[XmlIgnore()]
+		public string PhotosetSmallUrl
+		{
+			get { return Utils.UrlFormat(this, "_m", "jpg"); }
+		}
+
+
+
+
+	}
+}
\ No newline at end of file

Added: trunk/extensions/Exporters/FlickrExport/FlickrNet/Photos.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FlickrExport/FlickrNet/Photos.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,271 @@
+ïusing System;
+using System.Xml.Serialization;
+using System.Xml.Schema;
+using System.Collections;
+
+namespace FlickrNet
+{
+	/// <remarks/>
+	[Serializable]
+	public class Photos
+	{
+
+        private Photo[] _photos = new Photo[0];
+
+		/// <remarks/>
+        [XmlElement("photo", Form = XmlSchemaForm.Unqualified)]
+        public Photo[] PhotoCollection
+        {
+            get { return _photos; }
+			set
+			{
+				_photos = value==null?new Photo[0]:value;
+			}
+        }
+
+		/// <remarks/>
+		[XmlAttribute("page", Form=XmlSchemaForm.Unqualified)]
+		public long PageNumber;
+
+		/// <remarks/>
+		[XmlAttribute("pages", Form=XmlSchemaForm.Unqualified)]
+		public long TotalPages;
+
+		/// <remarks/>
+		[XmlAttribute("perpage", Form=XmlSchemaForm.Unqualified)]
+		public long PhotosPerPage;
+
+		/// <remarks/>
+		[XmlAttribute("total", Form=XmlSchemaForm.Unqualified)]
+		public long TotalPhotos;
+	}
+
+	/// <summary>
+	/// A collection of <see cref="Photo"/> instances.
+	/// </summary>
+	[System.Serializable]
+	public class PhotoCollection : CollectionBase
+	{
+
+		/// <summary>
+		/// Default constructor.
+		/// </summary>
+		public PhotoCollection()
+		{
+		}
+
+		/// <summary>
+		/// Creates an instance of the <see cref="PhotoCollection"/> from an array of <see cref="Photo"/>
+		/// instances.
+		/// </summary>
+		/// <param name="photos">An array of <see cref="Photo"/> instances.</param>
+		public PhotoCollection(Photo[] photos)
+		{
+			if (photos == null) return;
+
+			for (int i=0; i<photos.Length; i++)
+			{
+				List.Add(photos[i]);
+			}
+		}
+
+		/// <summary>
+		/// Gets number of photos in the current collection.
+		/// </summary>
+		public int Length
+		{
+			get { return List.Count; }
+		}
+
+		/// <summary>
+		/// Gets the index of a photo in this collection.
+		/// </summary>
+		/// <param name="photo">The photo to find.</param>
+		/// <returns>The index of the photo, -1 if it is not in the collection.</returns>
+		public int IndexOf(Photo photo)
+		{
+			return List.IndexOf(photo);
+		}
+
+		#region ICollection Members
+
+		/// <summary>
+		/// Gets a value indicating if the collection is synchronized (thread-safe).
+		/// </summary>
+		public bool IsSynchronized
+		{
+			get
+			{
+				return List.IsSynchronized;
+			}
+		}
+
+		/// <summary>
+		/// Copies the elements of the collection to an array of <see cref="Photo"/>, starting at a
+		/// particular index.
+		/// </summary>
+		/// <param name="array">The array to copy to.</param>
+		/// <param name="index">The index in the collection to start copying from.</param>
+		public void CopyTo(Photo[] array, int index)
+		{
+			List.CopyTo(array, index);
+		}
+
+		/// <summary>
+		/// Gets an object that can be used to synchronize the collection.
+		/// </summary>
+		public object SyncRoot
+		{
+			get
+			{
+				return List.SyncRoot;
+			}
+		}
+
+		#endregion
+
+		#region IList Members
+
+		/// <summary>
+		/// Gets a value indicating whether the collection is read-only.
+		/// </summary>
+		public bool IsReadOnly
+		{
+			get
+			{
+				return List.IsReadOnly;
+			}
+		}
+
+		/// <summary>
+		/// Gets or sets a photo based on the index in the collection.
+		/// </summary>
+		public Photo this[int index]
+		{
+			get
+			{
+				return (Photo)List[index];
+			}
+			set
+			{
+				List[index] = value;
+			}
+		}
+
+		/// <summary>
+		/// Inserts a <see cref="Photo"/> into the collection at the given index.
+		/// </summary>
+		/// <param name="index">The index to insert the <see cref="Photo"/> into.
+		/// Subsequent photos will be moved up.</param>
+		/// <param name="photo">The <see cref="Photo"/> to insert.</param>
+		public void Insert(int index, Photo photo)
+		{
+			List.Insert(index, photo);
+		}
+
+		/// <summary>
+		/// Removes a photo from the collection.
+		/// </summary>
+		/// <param name="photo">The <see cref="Photo"/> instance to remove from the collection.</param>
+		public void Remove(Photo photo)
+		{
+			List.Remove(photo);
+		}
+
+		/// <summary>
+		/// Returns true if the collection contains the photo.
+		/// </summary>
+		/// <param name="photo">The <see cref="Photo"/> instance to try to find.</param>
+		/// <returns>True of False, depending on if the <see cref="Photo"/> is found in the collection.</returns>
+		public bool Contains(Photo photo)
+		{
+			return List.Contains(photo);
+		}
+
+		/// <summary>
+		/// Adds a <see cref="Photo"/> to the collection.
+		/// </summary>
+		/// <param name="photo">The <see cref="Photo"/> instance to add to the collection.</param>
+		/// <returns>The index that the photo was added at.</returns>
+		public int Add(Photo photo)
+		{
+			return List.Add(photo);
+		}
+
+		/// <summary>
+		/// Adds an array of <see cref="Photo"/> instances to this collection.
+		/// </summary>
+		/// <param name="photos">An array of <see cref="Photo"/> instances.</param>
+		public void AddRange(Photo[] photos)
+		{
+			foreach(Photo photo in photos)
+				List.Add(photo);
+		}
+
+		/// <summary>
+		/// Adds all of the photos in another <see cref="PhotoCollection"/> to this collection.
+		/// </summary>
+		/// <param name="collection">The <see cref="PhotoCollection"/> containing the photos to add
+		/// to this collection.</param>
+		public void AddRange(PhotoCollection collection)
+		{
+			foreach(Photo photo in collection)
+				List.Add(photo);
+		}
+
+		/// <summary>
+		/// Gets an instance specifying whether the collection is a fixed size.
+		/// </summary>
+		public bool IsFixedSize
+		{
+			get
+			{
+				return List.IsFixedSize;
+			}
+		}
+
+		#endregion
+
+		/// <summary>
+		/// Converts a PhotoCollection instance to an array of Photo objects.
+		/// </summary>
+		/// <param name="collection">The collection to convert.</param>
+		/// <returns>An array of <see cref="Photo"/> objects.</returns>
+		public static implicit operator Photo[](PhotoCollection collection)
+		{
+			Photo[] photos = new Photo[collection.Count];
+			collection.CopyTo(photos, 0);
+			return photos;
+		}
+
+		/// <summary>
+		/// Converts the collection to an array of <see cref="Photo"/> objects.
+		/// </summary>
+		/// <returns>An array of <see cref="Photo"/> objects.</returns>
+		public Photo[] ToPhotoArray()
+		{
+			return (Photo[])this;
+		}
+
+		/// <summary>
+		/// Implicitly converts an array of <see cref="Photo"/> objects to a <see cref="PhotoCollection"/>.
+		/// </summary>
+		/// <param name="photos">The array of <see cref="Photo"/> objects to convert.</param>
+		/// <returns></returns>
+		public static implicit operator PhotoCollection(Photo[] photos)
+		{
+			return new PhotoCollection(photos);
+		}
+
+		/// <summary>
+		/// Creates a <see cref="PhotoCollection"/> from an array of <see cref="Photo"/> objects.
+		/// </summary>
+		/// <param name="photos">An array of <see cref="Photo"/> objects.</param>
+		/// <returns>A new <see cref="PhotoCollection"/> containing all the objects from the array.</returns>
+		public static PhotoCollection FromPhotoArray(Photo[] photos)
+		{
+			return (PhotoCollection)photos;
+		}
+	}
+
+}
\ No newline at end of file

Added: trunk/extensions/Exporters/FlickrExport/FlickrNet/Response.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FlickrExport/FlickrNet/Response.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,143 @@
+ïusing System;
+using System.Xml;
+using System.Xml.Serialization;
+using System.Xml.Schema;
+
+namespace FlickrNet
+{
+	/// <summary>
+	/// The root object returned by Flickr. Used with Xml Serialization to get the relevant object.
+	/// It is internal to the FlickrNet API Library and should not be used elsewhere.
+	/// </summary>
+	[XmlRoot("rsp", Namespace="", IsNullable=false)]
+	[Serializable]
+	public class Response
+	{
+
+		/// <remarks/>
+		[XmlElement("blogs", Form=XmlSchemaForm.Unqualified)]
+		public Blogs Blogs;
+
+		/// <remarks/>
+		[XmlElement("contacts", Form=XmlSchemaForm.Unqualified)]
+		public Contacts Contacts;
+
+		/// <remarks/>
+		[XmlElement("photos", Form=XmlSchemaForm.Unqualified)]
+		public Photos Photos;
+
+		/// <remarks/>
+		[XmlElement("category", Form=XmlSchemaForm.Unqualified)]
+		public Category Category;
+
+		/// <remarks/>
+		[XmlElement("photocounts", Form=XmlSchemaForm.Unqualified)]
+		public PhotoCounts PhotoCounts;
+
+		/// <remarks/>
+		[XmlElement("photo", Form=XmlSchemaForm.Unqualified)]
+		public PhotoInfo PhotoInfo;
+
+		/// <remarks/>
+		[XmlElement("photoset", Form=XmlSchemaForm.Unqualified)]
+		public Photoset Photoset;
+
+		/// <remarks/>
+		[XmlElement("photosets", Form=XmlSchemaForm.Unqualified)]
+		public Photosets Photosets;
+
+		/// <remarks/>
+		[XmlElement("sizes", Form=XmlSchemaForm.Unqualified)]
+		public Sizes Sizes;
+
+		/// <remarks/>
+		[XmlElement("licenses", Form=XmlSchemaForm.Unqualified)]
+		public Licenses Licenses;
+
+		/// <remarks/>
+		[XmlElement("count", Form=XmlSchemaForm.Unqualified)]
+		public ContextCount ContextCount;
+
+		/// <remarks/>
+		[XmlElement("nextphoto", Form=XmlSchemaForm.Unqualified)]
+		public ContextPhoto ContextNextPhoto;
+
+		/// <remarks/>
+		[XmlElement("prevphoto", Form=XmlSchemaForm.Unqualified)]
+		public ContextPhoto ContextPrevPhoto;
+
+		/// <remarks/>
+		[XmlAttribute("stat", Form=XmlSchemaForm.Unqualified)]
+		public ResponseStatus Status;
+
+		/// <summary>
+		/// If an error occurs the Error property is populated with
+		/// a <see cref="ResponseError"/> instance.
+		/// </summary>
+		[XmlElement("err", Form=XmlSchemaForm.Unqualified)]
+		public ResponseError Error;
+
+		/// <summary>
+		/// A <see cref="Method"/> instance.
+		/// </summary>
+		[XmlElement("method", Form=XmlSchemaForm.Unqualified)]
+		public Method Method;
+
+		/// <summary>
+		/// If using flickr.test.echo this contains all the other elements not covered above.
+		/// </summary>
+		/// <remarks>
+		/// t is an array of <see cref="XmlElement"/> objects. Use the XmlElement Name and InnerXml properties
+		/// to get the name and value of the returned property.
+		/// </remarks>
+		[XmlAnyElement(), NonSerialized()]
+		public XmlElement[] AllElements;
+	}
+
+	/// <summary>
+	/// If an error occurs then Flickr returns this object.
+	/// </summary>
+	[System.Serializable]
+	public class ResponseError
+	{
+		/// <summary>
+		/// The code or number of the error.
+		/// </summary>
+		/// <remarks>
+		/// 100 - Invalid Api Key.
+		/// 99  - User not logged in.
+		/// Other codes are specific to a method.
+		/// </remarks>
+		[XmlAttribute("code", Form=XmlSchemaForm.Unqualified)]
+		public int Code;
+
+		/// <summary>
+		/// The verbose message matching the error code.
+		/// </summary>
+		[XmlAttribute("msg", Form=XmlSchemaForm.Unqualified)]
+		public string Message;
+	}
+
+	/// <summary>
+	/// The status of the response, either ok or fail.
+	/// </summary>
+	public enum ResponseStatus
+	{
+		/// <summary>
+		/// An unknown status, and the default value if not set.
+		/// </summary>
+		[XmlEnum("unknown")]
+		Unknown,
+
+		/// <summary>
+		/// The response returns "ok" on a successful execution of the method.
+		/// </summary>
+		[XmlEnum("ok")]
+		OK,
+		/// <summary>
+		/// The response returns "fail" if there is an error, such as invalid API key or login failure.
+		/// </summary>
+		[XmlEnum("fail")]
+		Failed
+	}
+}
\ No newline at end of file

Added: trunk/extensions/Exporters/FlickrExport/FlickrNet/ResponseXmlException.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FlickrExport/FlickrNet/ResponseXmlException.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,18 @@
+using System;
+
+namespace FlickrNet
+{
+	/// <summary>
+	/// Exception thrown when an error parsing the returned XML.
+	/// </summary>
+	public class ResponseXmlException : FlickrException
+	{
+		internal ResponseXmlException(string message) : base(message)
+		{
+		}
+
+		internal ResponseXmlException(string message, Exception innerException) : base(message, innerException)
+		{
+		}
+	}
+}

Added: trunk/extensions/Exporters/FlickrExport/FlickrNet/SafeNativeMethods.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FlickrExport/FlickrNet/SafeNativeMethods.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,26 @@
+using System;
+
+namespace FlickrNet
+{
+	/// <summary>
+	/// Summary description for SafeNativeMethods.
+	/// </summary>
+#if !WindowsCE
+	[System.Security.SuppressUnmanagedCodeSecurity()]
+#endif
+    internal class SafeNativeMethods
+	{
+		private SafeNativeMethods()
+		{
+		}
+
+		internal static int GetErrorCode(System.IO.IOException ioe)
+		{
+#if !WindowsCE
+			return System.Runtime.InteropServices.Marshal.GetHRForException(ioe) & 0xFFFF;
+#else
+            return 0;
+#endif
+		}
+	}
+}

Added: trunk/extensions/Exporters/FlickrExport/FlickrNet/SignatureRequiredException.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FlickrExport/FlickrNet/SignatureRequiredException.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,14 @@
+using System;
+
+namespace FlickrNet
+{
+	/// <summary>
+	/// Thrown when a method requires a valid signature but no shared secret has been supplied.
+	/// </summary>
+	public class SignatureRequiredException : FlickrException
+	{
+		internal SignatureRequiredException() : base("Method requires signing but no shared secret supplied.")
+		{
+		}
+	}
+}

Added: trunk/extensions/Exporters/FlickrExport/FlickrNet/Sizes.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FlickrExport/FlickrNet/Sizes.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,87 @@
+ïusing System.Xml.Serialization;
+using System.Xml.Schema;
+
+namespace FlickrNet
+{
+	/// <summary>
+	/// Collection of <see cref="Size"/> items for a given photograph.
+	/// </summary>
+	[System.Serializable]
+	public class Sizes
+	{
+		private Size[] _sizeCollection = new Size[0];
+
+		/// <summary>
+		/// The size collection contains an array of <see cref="Size"/> items.
+		/// </summary>
+		[XmlElement("size", Form=XmlSchemaForm.Unqualified)]
+		public Size[] SizeCollection
+		{
+			get { return _sizeCollection; }
+			set { _sizeCollection = value; }
+		}
+	}
+
+	/// <summary>
+	/// Contains details about all the sizes available for a given photograph.
+	/// </summary>
+	[System.Serializable]
+	public class Size
+	{
+		private string _label;
+		private int _width;
+		private int _height;
+		private string _source;
+		private string _url;
+
+		/// <summary>
+		/// The label for the size, such as "Thumbnail", "Small", "Medium", "Large" and "Original".
+		/// </summary>
+		[XmlAttribute("label", Form=XmlSchemaForm.Unqualified)]
+		public string Label
+		{
+			get { return _label; }
+			set { _label = value; }
+		}
+
+        /// <summary>
+        /// The width of the resulting image, in pixels
+        /// </summary>
+		[XmlAttribute("width", Form=XmlSchemaForm.Unqualified)]
+		public int Width
+		{
+			get { return _width; }
+			set { _width = value; }
+		}
+
+		/// <summary>
+		/// The height of the resulting image, in pixels
+		/// </summary>
+		[XmlAttribute("height", Form=XmlSchemaForm.Unqualified)]
+		public int Height
+		{
+			get { return _height; }
+			set { _height = value; }
+		}
+
+		/// <summary>
+		/// The source url of the image.
+		/// </summary>
+		[XmlAttribute("source", Form=XmlSchemaForm.Unqualified)]
+		public string Source
+		{
+			get { return _source; }
+			set { _source = value; }
+		}
+
+		/// <summary>
+		/// The url to the photographs web page for this particular size.
+		/// </summary>
+		[XmlAttribute("url", Form=XmlSchemaForm.Unqualified)]
+		public string Url
+		{
+			get { return _url; }
+			set { _url = value; }
+		}
+	}
+}
\ No newline at end of file

Added: trunk/extensions/Exporters/FlickrExport/FlickrNet/Tags.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FlickrExport/FlickrNet/Tags.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,42 @@
+using System;
+using System.Xml;
+
+namespace FlickrNet
+{
+	/// <summary>
+	/// A simple tag class, containing a tag name and optional count (for <see cref="Flickr.TagsGetListUserPopular()"/>)
+	/// </summary>
+	public class Tag
+	{
+		private string _tagName;
+		private int _count;
+
+		/// <summary>
+		/// The name of the tag.
+		/// </summary>
+		public string TagName
+		{
+			get { return _tagName; }
+		}
+
+		/// <summary>
+		/// The poularity of the tag. Will be 0 where the popularity is not retreaved.
+		/// </summary>
+		public int Count
+		{
+			get { return _count; }
+		}
+
+		internal Tag(XmlNode node)
+		{
+			if( node.Attributes["count"] != null ) _count = Convert.ToInt32(node.Attributes["count"].Value);
+			_tagName = node.InnerText;
+		}
+
+		internal Tag(string tagName, int count)
+		{
+			_tagName = tagName;
+			_count = count;
+		}
+	}
+}

Added: trunk/extensions/Exporters/FlickrExport/FlickrNet/TestAuthFlickr.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FlickrExport/FlickrNet/TestAuthFlickr.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,134 @@
+using System;
+using System.Collections;
+using System.Collections.Specialized;
+using FlickrNet;
+
+// Testing the new authentication system
+
+public class TestAuthFlickr {
+    // The Glimmr API Key
+	private string _apikey_old = "2557e1ad5b7da6ffbe97387a7dad71b1";
+
+    // My key
+    private static string _apikey = "c6b39ee183385d9ce4ea188f85945016";
+    private static string _sharedsecret = "0a951ac44a423a04";
+    // private string _email = "acs barrapunto com";
+    // private string _password = "flickr";
+
+    private Flickr uploader;
+    private string photoURL;
+
+    public string TestAuth () {
+        // Flickr downloader = new Flickr (this._apikey, this._email, this._password );
+        uploader = new Flickr (_apikey, _sharedsecret);
+        return uploader.AuthGetFrob ();
+        // Looking to licenses
+        try {
+           Licenses licenses = uploader.PhotosLicensesGetInfo();
+           //foreach (License license in licenses.LicenseCollection) {
+           //    Console.WriteLine("License: {0} {1}", license.LicenseName, license.LicenseId);
+           //}
+        } catch( FlickrNet.FlickrException e ) {
+			Console.WriteLine( e.Code + ": " + e.Verbose );
+		}
+        // Searching some photos with a license
+        int licenseNumber = 5; // att-sa
+        string text = "infinity";
+        string[] tags = {"infinity", "love"};
+        photoURL = "http://www.flickr.com/photo_zoom.gne?id=";;
+
+        //for (int i=0; i < 2; i++) {
+        //    searchPags (text, 10, 1);
+        //    searchText (text);
+        //    searchTextLicense (text, licenseNumber);
+        //    searchTags (tags);
+        //}
+    }
+
+    private void timeUsed (long startTime) {
+        long endTime = DateTime.Now.Ticks;
+        TimeSpan timeTaken = new TimeSpan(endTime - startTime);
+        Console.WriteLine("--> Search: {0}", timeTaken.ToString());
+    }
+
+    private void searchText (string text) {
+        long startTime = DateTime.Now.Ticks;
+        Console.WriteLine ("Searching text " + text);
+        try {
+            Photos photos = uploader.PhotosSearchText (text);
+            showResults (photos);
+        } catch( FlickrNet.FlickrException e ) {
+			Console.WriteLine( e.Code + ": " + e.Verbose );
+		}
+        timeUsed (startTime);
+    }
+
+    private void searchTextLicense (string text, int license) {
+        long startTime = DateTime.Now.Ticks;
+        Console.WriteLine ("Searching text " + text + " license " + license);
+        try {
+            Photos photos = uploader.PhotosSearchText (text, license);
+            showResults (photos);
+        } catch( FlickrNet.FlickrException e ) {
+			Console.WriteLine( e.Code + ": " + e.Verbose );
+		}
+        timeUsed (startTime);
+    }
+
+    private void searchPags (string text, int results_per_page, int page) {
+        long startTime = DateTime.Now.Ticks;
+        Console.WriteLine ("Searching text with page " + text );
+        try {
+            //Photos photos = uploader.PhotosSearch(null, "", 0, text, DateTime.MinValue, DateTime.MinValue, 0, results_per_page, page);
+            //showResults (photos);
+        } catch( FlickrNet.FlickrException e ) {
+            Console.WriteLine( e.Code + ": " + e.Verbose );
+        }
+        timeUsed (startTime);
+    }
+
+    private void searchTags (string[] tags) {
+        long startTime = DateTime.Now.Ticks;
+        Console.WriteLine ("Searching tags " + tags);
+        try {
+            Photos photos = uploader.PhotosSearch(tags);
+            showResults (photos);
+        } catch( FlickrNet.FlickrException e ) {
+            Console.WriteLine( e.Code + ": " + e.Verbose );
+		}
+        timeUsed (startTime);
+    }
+
+    private void showResults (Photos photos) {
+        Console.WriteLine ("Total photos: {0}", photos.TotalPhotos);
+        foreach (Photo photo in photos.PhotoCollection) {
+            // Console.WriteLine ("Photo name: {0} {1} {2} {3}", photo.Title, photoURL+photo.PhotoId, photo.LargeUrl, photo.ThumbnailUrl);
+        }
+    }
+
+    public static void Main (string[] args) {
+        Console.WriteLine ("Testig the Flickr API");
+        TestAuthFlickr flickr = new TestAuthFlickr ();
+        string frob = flickr.TestAuth();
+        Console.WriteLine ("Frob: " + frob);
+        string login_url = flickr.uploader.AuthCalcUrl (frob, FlickrNet.AuthLevel.Write);
+
+        Console.WriteLine ("Please, login in Flickr using the next URL");
+        Console.WriteLine ("Login link: " + login_url);
+        Console.WriteLine ("Press any key when you have login in ...");
+        Console.ReadLine ();
+        Console.WriteLine ("Trying to get the token");
+        try {
+            Auth auth = flickr.uploader.AuthGetToken(frob);
+            Console.WriteLine ("We have the token!" + auth.Token);
+            flickr.uploader.ApiToken = auth.Token;
+            // Time to upload an image
+            flickr.uploader.UploadPicture ("/home/acs/fotos/dvd-sarge/sarge31.png");
+
+        } catch (FlickrNet.FlickrException ex) {
+            Console.WriteLine ("ERROR: Problems uploading photo to Flickr - "+ex.Verbose);
+        }
+    }
+}
+
+/* mcs TestAuthFlickr.cs -r:FlickrNet.dll */

Added: trunk/extensions/Exporters/FlickrExport/FlickrNet/UploadProgressEvent.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FlickrExport/FlickrNet/UploadProgressEvent.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,36 @@
+using System;
+
+namespace FlickrNet
+{
+	/// <summary>
+	/// Event arguments for a <see cref="Flickr.OnUploadProgress"/> event.
+	/// </summary>
+	public class UploadProgressEventArgs : EventArgs
+	{
+		private bool _uploadComplete;
+
+		private int _bytes;
+
+		/// <summary>
+		/// Number of bytes transfered so far.
+		/// </summary>
+		public int Bytes
+		{
+			get { return _bytes; }
+		}
+
+		/// <summary>
+		/// True if all bytes have been uploaded.
+		/// </summary>
+		public bool UploadComplete
+		{
+			get { return _uploadComplete; }
+		}
+
+		internal UploadProgressEventArgs(int bytes, bool complete)
+		{
+			_bytes = bytes;
+			_uploadComplete = complete;
+		}
+	}
+}

Added: trunk/extensions/Exporters/FlickrExport/FlickrNet/Uploader.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FlickrExport/FlickrNet/Uploader.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,59 @@
+using System;
+using System.Xml;
+using System.Xml.Schema;
+using System.Xml.Serialization;
+
+namespace FlickrNet
+{
+	/// <summary>
+	/// Information returned by the UploadPicture url.
+	/// </summary>
+	[XmlRoot("rsp")]
+	public class Uploader
+	{
+		private ResponseStatus _status;
+		private string _photoId;
+		private string _ticketId;
+		private ResponseError _error;
+
+		/// <summary>
+		/// The status of the upload, either "ok" or "fail".
+		/// </summary>
+		[XmlAttribute("stat", Form=XmlSchemaForm.Unqualified)]
+		public ResponseStatus Status
+		{
+			get { return _status; }
+			set { _status = value; }
+		}
+
+		/// <summary>
+		/// If the upload succeeded then this contains the id of the photo. Otherwise it will be zero.
+		/// </summary>
+		[XmlElement("photoid", Form=XmlSchemaForm.Unqualified)]
+		public string PhotoId
+		{
+			get { return _photoId; }
+			set { _photoId = value; }
+		}
+
+		/// <summary>
+		/// The ticket id, if using Asynchronous uploading.
+		/// </summary>
+		[XmlElement("ticketid", Form=XmlSchemaForm.Unqualified)]
+		public string TicketId
+		{
+			get { return _ticketId; }
+			set { _ticketId = value; }
+		}
+
+		/// <summary>
+		/// Contains the error returned if the upload is unsuccessful.
+		/// </summary>
+		[XmlElement("err", Form=XmlSchemaForm.Unqualified)]
+		public ResponseError Error
+		{
+			get { return _error; }
+			set { _error = value; }
+		}
+	}
+}

Added: trunk/extensions/Exporters/FlickrExport/FlickrNet/User.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FlickrExport/FlickrNet/User.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,144 @@
+ïusing System;
+using System.Xml;
+
+namespace FlickrNet
+{
+	/// <summary>
+	/// Contains details of a user
+	/// </summary>
+	[System.Serializable]
+	public class FoundUser
+	{
+		private string _userId;
+		private string _username;
+
+		/// <summary>
+		/// The ID of the found user.
+		/// </summary>
+		public string UserId
+		{
+			get { return _userId; }
+		}
+
+		/// <summary>
+		/// The username of the found user.
+		/// </summary>
+		public string Username
+		{
+			get { return _username; }
+		}
+
+		internal FoundUser(string userId, string username)
+		{
+			_userId = userId;
+			_username = username;
+		}
+
+		internal FoundUser(XmlNode node)
+		{
+			if( node.Attributes["nsid"] != null )
+				_userId = node.Attributes["nsid"].Value;
+			if( node.Attributes["id"] != null )
+				_userId = node.Attributes["id"].Value;
+			if( node.Attributes["username"] != null )
+				_username = node.Attributes["username"].Value;
+			if( node.SelectSingleNode("username") != null )
+				_username = node.SelectSingleNode("username").InnerText;
+		}
+	}
+
+	/// <summary>
+	/// The upload status of the user, as returned by <see cref="Flickr.PeopleGetUploadStatus"/>.
+	/// </summary>
+	[System.Serializable]
+	public class UserStatus
+	{
+		private bool _isPro;
+		private string _userId;
+		private string _username;
+		private long _bandwidthMax;
+		private long _bandwidthUsed;
+		private long _filesizeMax;
+
+		internal UserStatus(XmlNode node)
+		{
+			if( node == null )
+				throw new ArgumentNullException("node");
+
+			if( node.Attributes["id"] != null )
+				_userId = node.Attributes["id"].Value;
+			if( node.Attributes["nsid"] != null )
+				_userId = node.Attributes["nsid"].Value;
+			if( node.Attributes["ispro"] != null )
+				_isPro = node.Attributes["ispro"].Value=="1";
+			if( node.SelectSingleNode("username") != null )
+				_username = node.SelectSingleNode("username").InnerText;
+			XmlNode bandwidth = node.SelectSingleNode("bandwidth");
+			if( bandwidth != null )
+			{
+				_bandwidthMax = Convert.ToInt64(bandwidth.Attributes["max"].Value);
+				_bandwidthUsed = Convert.ToInt64(bandwidth.Attributes["used"].Value);
+			}
+			XmlNode filesize = node.SelectSingleNode("filesize");
+			if( filesize != null )
+			{
+				_filesizeMax = Convert.ToInt64(filesize.Attributes["max"].Value);
+			}
+		}
+		/// <summary>
+		/// The id of the user object.
+		/// </summary>
+		public string UserId
+		{
+			get { return _userId; }
+		}
+
+		/// <summary>
+		/// The Username of the selected user.
+		/// </summary>
+		public string UserName
+		{
+			get { return _username; }
+		}
+
+		/// <summary>
+		/// Is the current user a Pro account.
+		/// </summary>
+		public bool IsPro
+		{
+			get { return _isPro; }
+		}
+
+		/// <summary>
+		/// The maximum bandwidth (in bytes) that the user can use each month.
+		/// </summary>
+		public long BandwidthMax
+		{
+			get { return _bandwidthMax; }
+		}
+
+		/// <summary>
+		/// The number of bytes of the current months bandwidth that the user has used.
+		/// </summary>
+		public long BandwidthUsed
+		{
+			get { return _bandwidthUsed; }
+		}
+
+		/// <summary>
+		/// The maximum filesize (in bytes) that the user is allowed to upload.
+		/// </summary>
+		public long FilesizeMax
+		{
+			get { return _filesizeMax; }
+		}
+
+		/// <summary>
+		/// <see cref="Double"/> representing the percentage bandwidth used so far. Will range from 0 to 1.
+		/// </summary>
+		public Double PercentageUsed
+		{
+			get { return BandwidthUsed * 1.0 / BandwidthMax; }
+		}
+	}
+}
\ No newline at end of file

Added: trunk/extensions/Exporters/FlickrExport/FlickrNet/Utils.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FlickrExport/FlickrNet/Utils.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,351 @@
+using System;
+using System.IO;
+using System.Collections;
+using System.Text;
+using System.Text.RegularExpressions;
+using System.Xml.Serialization;
+
+namespace FlickrNet
+{
+	/// <summary>
+	/// Internal class providing certain utility functions to other classes.
+	/// </summary>
+	internal sealed class Utils
+	{
+		private static readonly DateTime unixStartDate = new DateTime(1970, 1, 1, 0, 0, 0);
+
+		private Utils()
+		{
+		}
+
+#if !WindowsCE
+		internal static string UrlEncode(string oldString)
+		{
+			if( oldString == null ) return null;
+
+			string a = System.Web.HttpUtility.UrlEncode(oldString);
+			a = a.Replace("&", "%26");
+			a = a.Replace("=", "%3D");
+			a = a.Replace(" ", "%20");
+			return a;
+		}
+#else
+        internal static string UrlEncode(string oldString)
+        {
+            if (oldString == null) return String.Empty;
+            StringBuilder sb = new StringBuilder(oldString.Length * 2);
+            Regex reg = new Regex("[a-zA-Z0-9$-_.+!*'(),]");
+
+            foreach (char c in oldString)
+            {
+                if (reg.IsMatch(c.ToString()))
+                {
+                    sb.Append(c);
+                }
+                else
+                {
+                    sb.Append(ToHex(c));
+                }
+            }
+            return sb.ToString();
+        }
+
+        private static string ToHex(char c)
+        {
+            return ((int)c).ToString("X");
+        }
+#endif
+
+		/// <summary>
+		/// Converts a <see cref="DateTime"/> object into a unix timestamp number.
+		/// </summary>
+		/// <param name="date">The date to convert.</param>
+		/// <returns>A long for the number of seconds since 1st January 1970, as per unix specification.</returns>
+		internal static long DateToUnixTimestamp(DateTime date)
+		{
+			TimeSpan ts = date - unixStartDate;
+			return (long)ts.TotalSeconds;
+		}
+
+		/// <summary>
+		/// Converts a string, representing a unix timestamp number into a <see cref="DateTime"/> object.
+		/// </summary>
+		/// <param name="timestamp">The timestamp, as a string.</param>
+		/// <returns>The <see cref="DateTime"/> object the time represents.</returns>
+		internal static DateTime UnixTimestampToDate(string timestamp)
+		{
+			if( timestamp == null || timestamp.Length == 0 ) return DateTime.MinValue;
+
+			return UnixTimestampToDate(long.Parse(timestamp));
+		}
+
+		/// <summary>
+		/// Converts a <see cref="long"/>, representing a unix timestamp number into a <see cref="DateTime"/> object.
+		/// </summary>
+		/// <param name="timestamp">The unix timestamp.</param>
+		/// <returns>The <see cref="DateTime"/> object the time represents.</returns>
+		internal static DateTime UnixTimestampToDate(long timestamp)
+		{
+			return unixStartDate.AddSeconds(timestamp);
+		}
+
+		/// <summary>
+		/// Utility method to convert the <see cref="PhotoSearchExtras"/> enum to a string.
+		/// </summary>
+		/// <example>
+		/// <code>
+		///     PhotoSearchExtras extras = PhotoSearchExtras.DateTaken &amp; PhotoSearchExtras.IconServer;
+		///     string val = Utils.ExtrasToString(extras);
+		///     Console.WriteLine(val);
+		/// </code>
+		/// outputs: "date_taken,icon_server";
+		/// </example>
+		/// <param name="extras"></param>
+		/// <returns></returns>
+		internal static string ExtrasToString(PhotoSearchExtras extras)
+		{
+			System.Text.StringBuilder sb = new System.Text.StringBuilder();
+			if( (extras & PhotoSearchExtras.DateTaken) == PhotoSearchExtras.DateTaken )
+				sb.Append("date_taken");
+			if( (extras & PhotoSearchExtras.DateUploaded) == PhotoSearchExtras.DateUploaded )
+			{
+				if( sb.Length>0 ) sb.Append(",");
+				sb.Append("date_upload");
+			}
+			if( (extras & PhotoSearchExtras.IconServer) == PhotoSearchExtras.IconServer )
+			{
+				if( sb.Length>0 ) sb.Append(",");
+				sb.Append("icon_server");
+			}
+			if( (extras & PhotoSearchExtras.License) == PhotoSearchExtras.License )
+			{
+				if( sb.Length>0 ) sb.Append(",");
+				sb.Append("license");
+			}
+			if( (extras & PhotoSearchExtras.OwnerName) == PhotoSearchExtras.OwnerName )
+			{
+				if( sb.Length>0 ) sb.Append(",");
+				sb.Append("owner_name");
+			}
+			if( (extras & PhotoSearchExtras.OriginalFormat) == PhotoSearchExtras.OriginalFormat )
+			{
+				if( sb.Length>0 ) sb.Append(",");
+				sb.Append("original_format");
+			}
+
+			if( (extras & PhotoSearchExtras.LastUpdated) == PhotoSearchExtras.LastUpdated )
+			{
+				if( sb.Length>0 ) sb.Append(",");
+				sb.Append("last_update");
+			}
+
+			if( (extras & PhotoSearchExtras.Tags) == PhotoSearchExtras.Tags )
+			{
+				if( sb.Length>0 ) sb.Append(",");
+				sb.Append("tags");
+			}
+
+			if( (extras & PhotoSearchExtras.Geo) == PhotoSearchExtras.Geo )
+			{
+				if( sb.Length>0 ) sb.Append(",");
+				sb.Append("geo");
+			}
+
+			return sb.ToString();
+		}
+
+		internal static string SortOrderToString(PhotoSearchSortOrder order)
+		{
+			switch(order)
+			{
+				case PhotoSearchSortOrder.DatePostedAsc:
+					return "date-posted-asc";
+				case PhotoSearchSortOrder.DatePostedDesc:
+					return "date-posted-desc";
+				case PhotoSearchSortOrder.DateTakenAsc:
+					return "date-taken-asc";
+				case PhotoSearchSortOrder.DateTakenDesc:
+					return "date-taken-desc";
+				case PhotoSearchSortOrder.InterestingnessAsc:
+					return "interestingness-asc";
+				case PhotoSearchSortOrder.InterestingnessDesc:
+					return "interestingness-desc";
+				case PhotoSearchSortOrder.Relevance:
+					return "relevance";
+				default:
+					return null;
+			}
+		}
+
+		internal static void PartialOptionsIntoArray(PartialSearchOptions options, Hashtable parameters)
+		{
+			if( options.MinUploadDate != DateTime.MinValue ) parameters.Add("min_uploaded_date", Utils.DateToUnixTimestamp(options.MinUploadDate).ToString());
+			if( options.MaxUploadDate != DateTime.MinValue ) parameters.Add("max_uploaded_date", Utils.DateToUnixTimestamp(options.MaxUploadDate).ToString());
+			if( options.MinTakenDate != DateTime.MinValue ) parameters.Add("min_taken_date", options.MinTakenDate.ToString("yyyy-MM-dd HH:mm:ss"));
+			if( options.MaxTakenDate != DateTime.MinValue ) parameters.Add("max_taken_date", options.MaxTakenDate.ToString("yyyy-MM-dd HH:mm:ss"));
+			if( options.Extras != PhotoSearchExtras.None ) parameters.Add("extras", options.ExtrasString);
+			if( options.SortOrder != PhotoSearchSortOrder.None ) parameters.Add("sort", options.SortOrderString);
+			if( options.PerPage > 0 ) parameters.Add("per_page", options.PerPage.ToString());
+			if( options.Page > 0 ) parameters.Add("page", options.Page.ToString());
+			if( options.PrivacyFilter != PrivacyFilter.None ) parameters.Add("privacy_filter", options.PrivacyFilter.ToString("d"));
+		}
+
+		internal static void WriteInt32(Stream s, int i)
+		{
+			s.WriteByte((byte) (i & 0xFF));
+			s.WriteByte((byte) ((i >> 8) & 0xFF));
+			s.WriteByte((byte) ((i >> 16) & 0xFF));
+			s.WriteByte((byte) ((i >> 24) & 0xFF));
+		}
+
+		internal static void WriteString(Stream s, string str)
+		{
+			WriteInt32(s, str.Length);
+			foreach (char c in str)
+			{
+				s.WriteByte((byte) (c & 0xFF));
+				s.WriteByte((byte) ((c >> 8) & 0xFF));
+			}
+		}
+
+		internal static void WriteAsciiString(Stream s, string str)
+		{
+			WriteInt32(s, str.Length);
+			foreach (char c in str)
+			{
+				s.WriteByte((byte) (c & 0x7F));
+			}
+		}
+
+		internal static int ReadInt32(Stream s)
+		{
+			int i = 0, b;
+			for (int j = 0; j < 4; j++)
+			{
+				b = s.ReadByte();
+				if (b == -1)
+					throw new IOException("Unexpected EOF encountered");
+				i |= (b << (j * 8));
+			}
+			return i;
+		}
+
+		internal static string ReadString(Stream s)
+		{
+			int len = ReadInt32(s);
+			char[] chars = new char[len];
+			for (int i = 0; i < len; i++)
+			{
+				int hi, lo;
+				lo = s.ReadByte();
+				hi = s.ReadByte();
+				if (lo == -1 || hi == -1)
+					throw new IOException("Unexpected EOF encountered");
+				chars[i] = (char) (lo | (hi << 8));
+			}
+			return new string(chars);
+		}
+
+		internal static string ReadAsciiString(Stream s)
+		{
+			int len = ReadInt32(s);
+			char[] chars = new char[len];
+			for (int i = 0; i < len; i++)
+			{
+				int c = s.ReadByte();
+				if (c == -1)
+					throw new IOException("Unexpected EOF encountered");
+				chars[i] = (char) (c & 0x7F);
+			}
+			return new string(chars);
+		}
+
+		private const string photoUrl = "http://farm{0}.static.flickr.com/{1}/{2}_{3}{4}.{5}";;
+
+		internal static string UrlFormat(Photo p, string size, string format)
+		{
+			if( size == "_o" )
+				return UrlFormat(photoUrl, p.Farm, p.Server, p.PhotoId, p.OriginalSecret, size, format);
+			else
+				return UrlFormat(photoUrl, p.Farm, p.Server, p.PhotoId, p.Secret, size, format);
+		}
+
+		internal static string UrlFormat(PhotoInfo p, string size, string format)
+		{
+			if( size == "_o" )
+				return UrlFormat(photoUrl, p.Farm, p.Server, p.PhotoId, p.OriginalSecret, size, format);
+			else
+				return UrlFormat(photoUrl, p.Farm, p.Server, p.PhotoId, p.Secret, size, format);
+		}
+
+		internal static string UrlFormat(Photoset p, string size, string format)
+		{
+			return UrlFormat(photoUrl, p.Farm, p.Server, p.PrimaryPhotoId, p.Secret, size, format);
+		}
+
+		private static string UrlFormat(string format, params object[] parameters)
+		{
+			return String.Format(format, parameters);
+		}
+
+		private static readonly Hashtable _serializers = new Hashtable();
+
+		private static XmlSerializer GetSerializer(Type type)
+		{
+			if( _serializers.ContainsKey(type.Name) )
+				return (XmlSerializer)_serializers[type.Name];
+			else
+			{
+				XmlSerializer s = new XmlSerializer(type);
+				_serializers.Add(type.Name, s);
+				return s;
+			}
+		}
+		/// <summary>
+		/// Converts the response string (in XML) into the <see cref="Response"/> object.
+		/// </summary>
+		/// <param name="responseString">The response from Flickr.</param>
+		/// <returns>A <see cref="Response"/> object containing the details of the </returns>
+		internal static Response Deserialize(string responseString)
+		{
+			XmlSerializer serializer = GetSerializer(typeof(FlickrNet.Response));
+			try
+			{
+				// Deserialise the web response into the Flickr response object
+				StringReader responseReader = new StringReader(responseString);
+				FlickrNet.Response response = (FlickrNet.Response)serializer.Deserialize(responseReader);
+				responseReader.Close();
+
+				return response;
+			}
+			catch(InvalidOperationException ex)
+			{
+				// Serialization error occurred!
+				throw new ResponseXmlException("Invalid response received from Flickr.", ex);
+			}
+		}
+
+		internal static object Deserialize(System.Xml.XmlNode node, Type type)
+		{
+			XmlSerializer serializer = GetSerializer(type);
+			try
+			{
+				// Deserialise the web response into the Flickr response object
+				System.Xml.XmlNodeReader reader = new System.Xml.XmlNodeReader(node);
+				object o = serializer.Deserialize(reader);
+				reader.Close();
+
+				return o;
+			}
+			catch(InvalidOperationException ex)
+			{
+				// Serialization error occurred!
+				throw new ResponseXmlException("Invalid response received from Flickr.", ex);
+			}
+		}
+
+
+
+	}
+
+}

Added: trunk/extensions/Exporters/FlickrExport/FlickrRemote.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FlickrExport/FlickrRemote.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,235 @@
+/*
+ * Simple upload based on the api at
+ * http://www.flickr.com/services/api/upload.api.html
+ *
+ * Modified by acs in order to use Flickr.Net
+ *
+ * Modified in order to use the new Auth API
+ *
+ * We use now the search API also
+ *
+ */
+using System;
+using System.IO;
+using System.Text;
+using System.Collections;
+using FlickrNet;
+using FSpot;
+using FSpot.Utils;
+using FSpot.Filters;
+
+public class FlickrRemote {
+	public static Licenses    licenses;
+	private string            frob;
+	private string            token;
+	private Auth              auth;
+	private Flickr            flickr;
+
+	public bool               ExportTags;
+	public bool               ExportTagHierarchy;
+	public bool               ExportIgnoreTopLevel;
+	public FSpot.ProgressItem Progress;
+
+	public const string TOKEN_FLICKR = Preferences.APP_FSPOT_EXPORT_TOKENS + "flickr";
+	public const string TOKEN_23HQ = Preferences.APP_FSPOT_EXPORT_TOKENS + "23hq";
+	public const string TOKEN_ZOOOMR = Preferences.APP_FSPOT_EXPORT_TOKENS + "zooomr";
+
+	public FlickrRemote (string token, Service service)
+	{
+		if (token == null || token.Length == 0) {
+			this.flickr = new Flickr (service.ApiKey, service.Secret);
+			this.token = null;
+		} else {
+			this.flickr = new Flickr (service.ApiKey, service.Secret, token);
+			this.token = token;
+		}
+
+		this.flickr.CurrentService = service.Id;
+	}
+
+	public string Token {
+		get { return token; }
+		set {
+			token = value;
+			flickr.AuthToken = value;
+		}
+	}
+
+	public Flickr Connection {
+		get { return flickr; }
+	}
+
+	public License[] GetLicenses ()
+	{
+		// Licenses won't change normally in a user session
+		if (licenses == null) {
+			try {
+				licenses = flickr.PhotosLicensesGetInfo();
+			} catch (FlickrNet.FlickrApiException e ) {
+				Console.WriteLine ( e.Code + ": " + e.Verbose );
+				return null;
+			}
+		}
+		return licenses.LicenseCollection;
+	}
+
+	public ArrayList Search (string[] tags, int licenseId)
+	{
+		ArrayList photos_url = new ArrayList ();
+		// Photos photos = flickr.PhotosSearchText (tags, licenseId);
+		Photos photos = flickr.PhotosSearch (tags);
+
+		if (photos != null) {
+			foreach (FlickrNet.Photo photo in photos.PhotoCollection) {
+				photos_url.Add (photo.ThumbnailUrl);
+			}
+		}
+
+		return photos_url;
+	}
+
+	public ArrayList Search (string tags, int licenseId)
+	{
+		ArrayList photos_url = new ArrayList ();
+		Photos photos = flickr.PhotosSearchText (tags, licenseId);
+
+		if (photos != null) {
+			foreach (FlickrNet.Photo photo in photos.PhotoCollection) {
+				photos_url.Add (photo.ThumbnailUrl);
+			}
+		}
+		return photos_url;
+	}
+
+	public Auth CheckLogin ()
+	{
+		if (frob == null) {
+			frob = flickr.AuthGetFrob ();
+			if (frob ==  null) {
+				Console.WriteLine ("ERROR: Problems login in Flickr. Don't have a frob");
+				return null;
+			}
+		}
+
+		if (token == null) {
+			try {
+				auth = flickr.AuthGetToken(frob);
+				token = auth.Token;
+				flickr.AuthToken = token;
+
+				return auth;
+			} catch (FlickrNet.FlickrApiException ex) {
+				Console.WriteLine ("ERROR: Problems login in Flickr - "+ex.Verbose);
+
+				return null;
+			}
+		}
+
+		auth = flickr.AuthCheckToken ("token");
+		return auth;
+	}
+
+	public string Upload (IBrowsableItem photo, IFilter filter, bool is_public, bool is_family, bool is_friend)
+	{
+		if (token == null) {
+			throw new Exception ("Must Login First");
+		}
+		// FIXME flickr needs rotation
+		string  error_verbose;
+
+		using (FilterRequest request = new FilterRequest (photo.DefaultVersionUri)) {
+
+			try {
+				string tags = null;
+
+				filter.Convert (request);
+				string path = request.Current.LocalPath;
+
+				if (ExportTags && photo.Tags != null) {
+					StringBuilder taglist = new StringBuilder ();
+					FSpot.Tag [] t = photo.Tags;
+					FSpot.Tag tag_iter = null;
+
+					for (int i = 0; i < t.Length; i++) {
+						if (i > 0)
+							taglist.Append (",");
+
+						taglist.Append (String.Format ("\"{0}\"", t[i].Name));
+
+						// Go through the tag parents
+						if (ExportTagHierarchy) {
+							tag_iter = t[i].Category;
+							while (tag_iter != Core.Database.Tags.RootCategory && tag_iter != null) {
+								// Skip top level tags because they have no meaning in a linear tag database
+								if (ExportIgnoreTopLevel && tag_iter.Category == Core.Database.Tags.RootCategory) {
+									break;
+								}
+
+								// FIXME Look if the tag is already there!
+								taglist.Append (",");
+								taglist.Append (String.Format ("\"{0}\"", tag_iter.Name));
+								tag_iter = tag_iter.Category;
+							}
+						}
+
+					}
+
+					tags = taglist.ToString ();
+				}
+				try {
+					string photoid =
+						flickr.UploadPicture (path, photo.Name, photo.Description, tags, is_public, is_family, is_friend);
+					return photoid;
+				} catch (FlickrNet.FlickrException ex) {
+					Console.WriteLine ("Problems uploading picture: " + ex.ToString());
+					error_verbose = ex.ToString();
+				}
+			} catch (Exception e) {
+				// FIXME we need to distinguish between file IO errors and xml errors here
+				throw new System.Exception ("Error while uploading", e);
+			}
+		}
+
+		throw new System.Exception (error_verbose);
+	}
+
+	public void TryWebLogin () {
+		frob = flickr.AuthGetFrob ();
+		string login_url = flickr.AuthCalcUrl (frob, FlickrNet.AuthLevel.Write);
+
+		GnomeUtil.UrlShow (login_url);
+	}
+
+	public class Service {
+		public string ApiKey;
+		public string Secret;
+		public SupportedService Id;
+		public string Name;
+		public string PreferencePath;
+
+		public static Service [] Supported = {
+			new Service (SupportedService.Flickr, "Flickr.com", "c6b39ee183385d9ce4ea188f85945016", "0a951ac44a423a04", TOKEN_FLICKR),
+			new Service (SupportedService.TwentyThreeHQ, "23hq.com", "c6b39ee183385d9ce4ea188f85945016", "0a951ac44a423a04", TOKEN_23HQ),
+			new Service (SupportedService.Zooomr, "Zooomr.com", "a2075d8ff1b7b059df761649835562e4", "6c66738681", TOKEN_ZOOOMR)
+		};
+
+		public Service (SupportedService id, string name, string api_key, string secret, string pref)
+		{
+			Id = id;
+			ApiKey = api_key;
+			Secret = secret;
+			Name = name;
+			PreferencePath = pref;
+		}
+
+		public static Service FromSupported (SupportedService id)
+		{
+			foreach (Service s in Supported) {
+				if (s.Id == id)
+					return s;
+			}
+
+			throw new System.ArgumentException ("Unknown service type");
+		}
+	}
+}

Copied: trunk/extensions/Exporters/FlickrExport/Makefile.am (from r4368, /trunk/extensions/FlickrExport/Makefile.am)
==============================================================================

Copied: trunk/extensions/Exporters/FolderExport/.gitignore (from r4368, /trunk/extensions/FlickrExport/.gitignore)
==============================================================================

Copied: trunk/extensions/Exporters/FolderExport/FolderExport.addin.xml (from r4368, /trunk/extensions/FolderExport/FolderExport.addin.xml)
==============================================================================

Added: trunk/extensions/Exporters/FolderExport/FolderExport.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FolderExport/FolderExport.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,1498 @@
+/*
+ * Copyright (C) 2005 Alessandro Gervaso <gervystar gervystar net>
+ *
+ * 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.
+ */
+
+//This should be used to export the selected pics to an original gallery
+//located on a VFS location.
+using System;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Collections;
+
+using Mono.Unix;
+
+using ICSharpCode.SharpZipLib.Checksums;
+using ICSharpCode.SharpZipLib.Zip;
+using ICSharpCode.SharpZipLib.GZip;
+
+using FSpot;
+using FSpot.Filters;
+using FSpot.Widgets;
+using FSpot.Utils;
+
+namespace FSpotFolderExport {
+	public class FolderExport : FSpot.Extensions.IExporter {
+		IBrowsableCollection selection;
+
+		[Glade.Widget] Gtk.Dialog dialog;
+		[Glade.Widget] Gtk.ScrolledWindow thumb_scrolledwindow;
+		[Glade.Widget] Gtk.Entry name_entry;
+		[Glade.Widget] Gtk.Entry description_entry;
+
+		//[Glade.Widget] Gtk.CheckButton meta_check;
+		[Glade.Widget] Gtk.CheckButton scale_check;
+		[Glade.Widget] Gtk.CheckButton rotate_check;
+		[Glade.Widget] Gtk.CheckButton export_tags_check;
+		[Glade.Widget] Gtk.CheckButton export_tag_icons_check;
+		[Glade.Widget] Gtk.CheckButton open_check;
+
+		[Glade.Widget] Gtk.RadioButton static_radio;
+		[Glade.Widget] Gtk.RadioButton original_radio;
+		[Glade.Widget] Gtk.RadioButton plain_radio;
+
+		[Glade.Widget] Gtk.SpinButton size_spin;
+
+		[Glade.Widget] Gtk.HBox chooser_hbox;
+
+		public const string EXPORT_SERVICE = "folder/";
+		public const string SCALE_KEY = Preferences.APP_FSPOT_EXPORT + EXPORT_SERVICE + "scale";
+		public const string SIZE_KEY = Preferences.APP_FSPOT_EXPORT + EXPORT_SERVICE + "size";
+		public const string OPEN_KEY = Preferences.APP_FSPOT_EXPORT + EXPORT_SERVICE + "browser";
+		public const string ROTATE_KEY = Preferences.APP_FSPOT_EXPORT + EXPORT_SERVICE + "rotate";
+		public const string EXPORT_TAGS_KEY = Preferences.APP_FSPOT_EXPORT + EXPORT_SERVICE + "export_tags";
+		public const string EXPORT_TAG_ICONS_KEY = Preferences.APP_FSPOT_EXPORT + EXPORT_SERVICE + "export_tag_icons";
+		public const string METHOD_KEY = Preferences.APP_FSPOT_EXPORT + EXPORT_SERVICE + "method";
+		public const string URI_KEY = Preferences.APP_FSPOT_EXPORT + EXPORT_SERVICE + "uri";
+		public const string SHARPEN_KEY = Preferences.APP_FSPOT_EXPORT + EXPORT_SERVICE + "sharpen";
+		public const string INCLUDE_TARBALLS_KEY = Preferences.APP_FSPOT_EXPORT + EXPORT_SERVICE + "include_tarballs";
+
+		private Glade.XML xml;
+		private string dialog_name = "folder_export_dialog";
+		Gnome.Vfs.Uri dest;
+		Gtk.FileChooserButton uri_chooser;
+
+		int photo_index;
+		bool open;
+		bool scale;
+		bool rotate;
+		bool exportTags;
+		bool exportTagIcons;
+		int size;
+
+		string description;
+		string gallery_name = "Gallery";
+		// FIME this needs to be a real temp directory
+		string gallery_path = Path.Combine (Path.GetTempPath (), "f-spot-original-" + System.DateTime.Now.Ticks.ToString ());
+
+		FSpot.ThreadProgressDialog progress_dialog;
+		System.Threading.Thread command_thread;
+
+		public FolderExport ()
+		{}
+		public void Run (IBrowsableCollection selection)
+		{
+			/*
+			Gnome.Vfs.ModuleCallbackFullAuthentication auth = new Gnome.Vfs.ModuleCallbackFullAuthentication ();
+			auth.Callback += new Gnome.Vfs.ModuleCallbackHandler (HandleAuth);
+			auth.SetDefault ();
+			auth.Push ();
+
+			Gnome.Vfs.ModuleCallbackAuthentication mauth = new Gnome.Vfs.ModuleCallbackAuthentication ();
+			mauth.Callback += new Gnome.Vfs.ModuleCallbackHandler (HandleAuth);
+			mauth.SetDefault ();
+			mauth.Push ();
+
+			Gnome.Vfs.ModuleCallbackSaveAuthentication sauth = new Gnome.Vfs.ModuleCallbackSaveAuthentication ();
+			sauth.Callback += new Gnome.Vfs.ModuleCallbackHandler (HandleAuth);
+			sauth.SetDefault ();
+			sauth.Push ();
+
+			Gnome.Vfs.ModuleCallbackStatusMessage msg = new Gnome.Vfs.ModuleCallbackStatusMessage ();
+			msg.Callback += new Gnome.Vfs.ModuleCallbackHandler (HandleMsg);
+			msg.SetDefault ();
+			msg.Push ();
+			*/
+			this.selection = selection;
+
+			IconView view = (IconView) new IconView (selection);
+			view.DisplayDates = false;
+			view.DisplayTags = false;
+
+			xml = new Glade.XML (null, "FolderExport.glade", dialog_name, "f-spot");
+			xml.Autoconnect (this);
+			Dialog.Modal = false;
+			Dialog.TransientFor = null;
+
+			thumb_scrolledwindow.Add (view);
+			HandleSizeActive (null, null);
+			name_entry.Text = gallery_name;
+
+			string uri_path = System.IO.Path.Combine (FSpot.Global.HomeDirectory, "Desktop");
+			if (!System.IO.Directory.Exists (uri_path))
+			        uri_path = FSpot.Global.HomeDirectory;
+
+			uri_chooser = new Gtk.FileChooserButton (Catalog.GetString ("Select Export Folder"),
+								 Gtk.FileChooserAction.SelectFolder);
+
+			uri_chooser.LocalOnly = false;
+
+			if (Preferences.Get (URI_KEY) != null && Preferences.Get (URI_KEY) as string != String.Empty)
+				uri_chooser.SetUri (Preferences.Get (URI_KEY) as string);
+			else
+				uri_chooser.SetFilename (uri_path);
+
+			chooser_hbox.PackStart (uri_chooser);
+
+			Dialog.ShowAll ();
+
+			//LoadHistory ();
+			Dialog.Response += HandleResponse;
+
+			LoadPreference (SCALE_KEY);
+			LoadPreference (SIZE_KEY);
+			LoadPreference (OPEN_KEY);
+			LoadPreference (ROTATE_KEY);
+			LoadPreference (EXPORT_TAGS_KEY);
+			LoadPreference (EXPORT_TAG_ICONS_KEY);
+			LoadPreference (METHOD_KEY);
+		}
+
+		public void HandleSizeActive (object sender, System.EventArgs args)
+		{
+			size_spin.Sensitive = scale_check.Active;
+		}
+
+		public void HandleStandaloneActive (object sender, System.EventArgs args)
+		{
+			export_tags_check.Sensitive = static_radio.Active;
+			HandleExportTagsActive (sender, args);
+		}
+
+		public void HandleExportTagsActive (object sender, System.EventArgs args)
+		{
+			export_tag_icons_check.Sensitive = export_tags_check.Active && static_radio.Active;
+		}
+
+		public void Upload ()
+		{
+			// FIXME use mkstemp
+
+			Gnome.Vfs.Result result = Gnome.Vfs.Result.Ok;
+
+			try {
+				Dialog.Hide ();
+
+				Gnome.Vfs.Uri source = new Gnome.Vfs.Uri (Path.Combine (gallery_path, gallery_name));
+				Gnome.Vfs.Uri target = dest.Clone();
+				target = target.AppendFileName(source.ExtractShortName ());
+
+				if (dest.IsLocal)
+					gallery_path = Gnome.Vfs.Uri.GetLocalPathFromUri (dest.ToString ());
+
+				progress_dialog.Message = Catalog.GetString ("Building Gallery");
+				progress_dialog.Fraction = 0.0;
+
+				FolderGallery gallery;
+				if (static_radio.Active) {
+					gallery = new HtmlGallery (selection, gallery_path, gallery_name);
+				} else if (original_radio.Active) {
+					gallery = new OriginalGallery (selection, gallery_path, gallery_name);
+				} else {
+					gallery = new FolderGallery (selection, gallery_path, gallery_name);
+				}
+
+				if (scale) {
+					System.Console.WriteLine ("setting scale to {0}", size);
+					gallery.SetScale (size);
+				} else {
+					System.Console.WriteLine ("Exporting full size image");
+				}
+
+				if (rotate) {
+					System.Console.WriteLine ("Exporting rotated image");
+					gallery.SetRotate();
+				}
+
+				if (exportTags)
+					gallery.SetExportTags ();
+
+				if (exportTagIcons)
+					gallery.SetExportTagIcons ();
+
+				gallery.Description = description;
+
+				gallery.GenerateLayout ();
+				FilterSet filter_set = new FilterSet ();
+				if (scale)
+					filter_set.Add (new ResizeFilter ((uint) size));
+				else if (rotate)
+					filter_set.Add (new OrientationFilter ());
+				filter_set.Add (new ChmodFilter ());
+				filter_set.Add (new UniqueNameFilter (gallery_path));
+
+				for (int photo_index = 0; photo_index < selection.Count; photo_index++)
+				{
+					try {
+						progress_dialog.Message = System.String.Format (Catalog.GetString ("Uploading picture \"{0}\""), selection[photo_index].Name);
+						progress_dialog.Fraction = photo_index / (double) selection.Count;
+						gallery.ProcessImage (photo_index, filter_set);
+						progress_dialog.ProgressText = System.String.Format (Catalog.GetString ("{0} of {1}"), (photo_index + 1), selection.Count);
+					}
+					catch (Exception e) {
+						progress_dialog.Message = String.Format (Catalog.GetString ("Error uploading picture \"{0}\" to Gallery:{2}{1}"),
+							selection[photo_index].Name, e.Message, Environment.NewLine);
+						progress_dialog.ProgressText = Catalog.GetString ("Error");
+
+						if (progress_dialog.PerformRetrySkip ())
+							photo_index--;
+					}
+
+				}
+
+				//create the zip tarballs for original
+				if (gallery is OriginalGallery) {
+					bool include_tarballs;
+					try {
+						include_tarballs = (bool)Preferences.Get (INCLUDE_TARBALLS_KEY);
+					} catch (NullReferenceException){
+						include_tarballs = true;
+						Preferences.Set (INCLUDE_TARBALLS_KEY, true);
+					}
+					if (include_tarballs)
+						(gallery as OriginalGallery).CreateZip ();
+				}
+
+				// we've created the structure, now if the destination was local we are done
+				// otherwise we xfer
+				if (!dest.IsLocal) {
+					Console.WriteLine(target);
+					System.Console.WriteLine ("Xfering {0} to {1}", source.ToString (), target.ToString ());
+					result = Gnome.Vfs.Xfer.XferUri (source, target,
+									 Gnome.Vfs.XferOptions.Default,
+									 Gnome.Vfs.XferErrorMode.Abort,
+									 Gnome.Vfs.XferOverwriteMode.Replace,
+									 Progress);
+				}
+
+				if (result == Gnome.Vfs.Result.Ok) {
+
+					progress_dialog.Message = Catalog.GetString ("Done Sending Photos");
+					progress_dialog.Fraction = 1.0;
+					progress_dialog.ProgressText = Catalog.GetString ("Transfer Complete");
+					progress_dialog.ButtonLabel = Gtk.Stock.Ok;
+
+				} else {
+					progress_dialog.ProgressText = result.ToString ();
+					progress_dialog.Message = Catalog.GetString ("Error While Transferring");
+				}
+
+				if (open) {
+					GnomeUtil.UrlShow (target.ToString ());
+				}
+
+				// Save these settings for next time
+				Preferences.Set (SCALE_KEY, scale);
+				Preferences.Set (SIZE_KEY, size);
+				Preferences.Set (OPEN_KEY, open);
+				Preferences.Set (ROTATE_KEY, rotate);
+				Preferences.Set (EXPORT_TAGS_KEY, exportTags);
+				Preferences.Set (EXPORT_TAG_ICONS_KEY, exportTagIcons);
+				Preferences.Set (METHOD_KEY, static_radio.Active ? "static" : original_radio.Active ? "original" : "folder" );
+				Preferences.Set (URI_KEY, uri_chooser.Uri);
+			} catch (System.Exception e) {
+				// Console.WriteLine (e);
+				progress_dialog.Message = e.ToString ();
+				progress_dialog.ProgressText = Catalog.GetString ("Error Transferring");
+			} finally {
+				// if the destination isn't local then we want to remove the temp directory we
+				// created.
+				if (!dest.IsLocal)
+					System.IO.Directory.Delete (gallery_path, true);
+
+				Gtk.Application.Invoke (delegate { Dialog.Destroy(); });
+
+			}
+		}
+
+		private int Progress (Gnome.Vfs.XferProgressInfo info)
+		{
+			progress_dialog.ProgressText = info.Phase.ToString ();
+
+			if (info.BytesTotal > 0) {
+				progress_dialog.Fraction = info.BytesCopied / (double)info.BytesTotal;
+			}
+
+			switch (info.Status) {
+			case Gnome.Vfs.XferProgressStatus.Vfserror:
+				progress_dialog.Message = Catalog.GetString ("Error: Error while transferring; Aborting");
+				return (int)Gnome.Vfs.XferErrorAction.Abort;
+			case Gnome.Vfs.XferProgressStatus.Overwrite:
+				progress_dialog.ProgressText = Catalog.GetString ("Error: File Already Exists; Aborting");
+				return (int)Gnome.Vfs.XferOverwriteAction.Abort;
+			default:
+				return 1;
+			}
+
+		}
+
+		private void HandleMsg (Gnome.Vfs.ModuleCallback cb)
+		{
+			Gnome.Vfs.ModuleCallbackStatusMessage msg = cb as Gnome.Vfs.ModuleCallbackStatusMessage;
+			System.Console.WriteLine ("{0}", msg.Message);
+		}
+
+		private void HandleAuth (Gnome.Vfs.ModuleCallback cb)
+		{
+			Gnome.Vfs.ModuleCallbackFullAuthentication fcb = cb as Gnome.Vfs.ModuleCallbackFullAuthentication;
+			System.Console.Write ("Enter your username ({0}): ", fcb.Username);
+			string username = System.Console.ReadLine ();
+			System.Console.Write ("Enter your password : ");
+			string passwd = System.Console.ReadLine ();
+
+			if (username.Length > 0)
+				fcb.Username = username;
+			fcb.Password = passwd;
+		}
+
+		private void HandleResponse (object sender, Gtk.ResponseArgs args)
+		{
+			if (args.ResponseId != Gtk.ResponseType.Ok) {
+				// FIXME this is to work around a bug in gtk+ where
+				// the filesystem events are still listened to when
+				// a FileChooserButton is destroyed but not finalized
+				// and an event comes in that wants to update the child widgets.
+				Dialog.Destroy ();
+				uri_chooser.Dispose ();
+				uri_chooser = null;
+				return;
+			}
+
+			dest = new Gnome.Vfs.Uri (uri_chooser.Uri);
+			open = open_check.Active;
+			scale = scale_check.Active;
+			rotate = rotate_check.Active;
+			exportTags = export_tags_check.Active;
+			exportTagIcons = export_tag_icons_check.Active;
+
+			gallery_name = name_entry.Text;
+
+			if (description_entry != null)
+				description = description_entry.Text;
+
+			if (scale)
+				size = size_spin.ValueAsInt;
+
+			command_thread = new System.Threading.Thread (new System.Threading.ThreadStart (Upload));
+			command_thread.Name = Catalog.GetString ("Transferring Pictures");
+
+			//FIXME: get the files/dirs count in a cleaner way than (* 5 + 2(zip) + 9)
+			// selection * 5 (original, mq, lq, thumbs, comments)
+			// 2: zipfiles
+			// 9: directories + info.txt + .htaccess
+			// this should actually be 1 anyway, because we transfer just one dir
+			progress_dialog = new FSpot.ThreadProgressDialog (command_thread, 1);
+			progress_dialog.Start ();
+		}
+
+		void LoadPreference (string key)
+		{
+			object val = Preferences.Get (key);
+
+			if (val == null)
+				return;
+
+			//System.Console.WriteLine ("Setting {0} to {1}", key, val);
+
+			switch (key) {
+			case SCALE_KEY:
+				if (scale_check.Active != (bool) val)
+					scale_check.Active = (bool) val;
+				break;
+
+			case SIZE_KEY:
+				size_spin.Value = (double) (int) val;
+				break;
+
+			case OPEN_KEY:
+				if (open_check.Active != (bool) val)
+					open_check.Active = (bool) val;
+				break;
+
+			case ROTATE_KEY:
+				if (rotate_check.Active != (bool) val)
+					rotate_check.Active = (bool) val;
+				break;
+
+			case EXPORT_TAGS_KEY:
+				if (export_tags_check.Active != (bool) val)
+					export_tags_check.Active = (bool) val;
+				break;
+
+			case EXPORT_TAG_ICONS_KEY:
+				if (export_tag_icons_check.Active != (bool) val)
+					export_tag_icons_check.Active = (bool) val;
+				break;
+
+			case METHOD_KEY:
+				static_radio.Active = (string) val == "static";
+				original_radio.Active = (string) val == "original";
+				plain_radio.Active = (string) val == "folder";
+				break;
+			}
+		}
+
+		private Gtk.Dialog Dialog {
+			get {
+				if (dialog == null)
+					dialog = (Gtk.Dialog) xml.GetWidget (dialog_name);
+
+				return dialog;
+			}
+		}
+	}
+
+	internal class FolderGallery
+	{
+		protected IBrowsableCollection collection;
+		protected string gallery_name;
+		protected string gallery_path;
+		protected bool scale;
+		protected int size;
+		protected bool rotate;
+		protected bool exportTags;
+		protected bool exportTagIcons;
+		protected string description;
+		protected string language;
+		protected System.Uri destination;
+
+		protected ScaleRequest [] requests;
+
+		protected string [] pixbuf_keys = { "quality", null };
+		protected string [] pixbuf_values = { "95", null };
+
+		protected struct ScaleRequest {
+			public string Name;
+			public int Width;
+			public int Height;
+			public bool Skip;
+			public bool CopyExif;
+
+			public ScaleRequest (string name, int width, int height, bool skip) : this (name, width, height, skip, false) {}
+
+			public ScaleRequest (string name, int width, int height, bool skip, bool exif)
+			{
+				this.Name = name != null ? name : String.Empty;
+				this.Width = width;
+				this.Height = height;
+				this.Skip = skip;
+				this.CopyExif = exif;
+			}
+
+			public static ScaleRequest Default = new ScaleRequest (String.Empty, 0, 0, false);
+
+			public bool AvoidScale (int size) {
+				return (size < this.Width && size < this.Height && this.Skip);
+			}
+		}
+
+		internal FolderGallery (IBrowsableCollection selection, string path, string gallery_name)
+		{
+			this.collection = selection;
+			this.gallery_name = gallery_name;
+			this.gallery_path = Path.Combine (path, gallery_name);
+			this.requests = new ScaleRequest [] { ScaleRequest.Default };
+		}
+
+		public virtual void GenerateLayout ()
+		{
+			MakeDir (gallery_path);
+
+		}
+
+		protected virtual string ImageName (int image_num)
+		{
+			return System.IO.Path.GetFileName(FileImportBackend.UniqueName(gallery_path, System.IO.Path.GetFileName (collection [image_num].DefaultVersionUri.LocalPath)));
+		}
+
+		public void ProcessImage (int image_num, FilterSet filter_set)
+		{
+			IBrowsableItem photo = collection [image_num];
+			string photo_path = photo.DefaultVersionUri.LocalPath;
+			string path;
+			ScaleRequest req;
+
+			req = requests [0];
+
+			MakeDir (SubdirPath (req.Name));
+			path = SubdirPath (req.Name, ImageName (image_num));
+
+			using (FilterRequest request = new FilterRequest (photo.DefaultVersionUri)) {
+				filter_set.Convert (request);
+				if (request.Current.LocalPath == path)
+					request.Preserve(request.Current);
+				else
+					File.Copy (request.Current.LocalPath, path, true);
+
+				if (photo != null && photo is Photo && Core.Database != null) {
+					Core.Database.Exports.Create ((photo as Photo).Id, (photo as Photo).DefaultVersionId,
+								      ExportStore.FolderExportType,
+								      // FIXME this is wrong, the final path is the one
+								      // after the Xfer.
+								      UriUtils.PathToFileUriEscaped (path).ToString ());
+				}
+
+				using (Exif.ExifData data = new Exif.ExifData (photo_path)) {
+					for (int i = 1; i < requests.Length; i++) {
+
+						req = requests [i];
+						if (scale && req.AvoidScale (size))
+							continue;
+
+						FilterSet req_set = new FilterSet ();
+						req_set.Add (new ResizeFilter ((uint)Math.Max (req.Width, req.Height)));
+
+						bool sharpen;
+						try {
+							sharpen = (bool)Preferences.Get (FolderExport.SHARPEN_KEY);
+						} catch (NullReferenceException) {
+							sharpen = true;
+							Preferences.Set (FolderExport.SHARPEN_KEY, true);
+						}
+
+						if (sharpen) {
+							if (req.Name == "lq")
+								req_set.Add (new SharpFilter (0.1, 2, 4));
+							if (req.Name == "thumbs")
+								req_set.Add (new SharpFilter (0.1, 2, 5));
+						}
+						using (FilterRequest tmp_req = new FilterRequest (photo.DefaultVersionUri)) {
+							req_set.Convert (tmp_req);
+							MakeDir (SubdirPath (req.Name));
+							path = SubdirPath (req.Name, ImageName (image_num));
+							System.IO.File.Copy (tmp_req.Current.LocalPath, path, true);
+						}
+
+					}
+				}
+			}
+		}
+
+		protected string MakeDir (string path)
+		{
+			try {
+				Directory.CreateDirectory (path);
+			} catch {
+				Console.WriteLine ("Error in creating directory " + path);
+			}
+			return path;
+		}
+
+		protected string SubdirPath (string subdir)
+		{
+			return SubdirPath (subdir, null);
+		}
+
+		protected string SubdirPath (string subdir, string file)
+		{
+			string path = Path.Combine (gallery_path, subdir);
+			if (file != null)
+				path = Path.Combine (path, file);
+
+			return path;
+		}
+
+		public string GalleryPath {
+			get {
+				return gallery_path;
+			}
+		}
+
+		public string Description {
+			get {
+				return description;
+			}
+			set {
+				description = value;
+			}
+		}
+
+		public string Language {
+			get {
+				if (language == null)
+					language=GetLanguage();
+				return language;
+			}
+		}
+
+		public Uri Destination {
+			get {
+				return destination;
+			}
+			set {
+				this.destination = value;
+			}
+		}
+
+		public void SetScale (int size) {
+			this.scale = true;
+			this.size = size;
+			requests [0].Width = size;
+			requests [0].Height = size;
+		}
+
+		public void SetRotate () {
+			this.rotate = true;
+		}
+
+		public void SetExportTags () {
+			this.exportTags = true;
+		}
+
+		public void SetExportTagIcons () {
+			this.exportTagIcons = true;
+		}
+
+		private string GetLanguage()
+		{
+			string language;
+
+			if ((language = Environment.GetEnvironmentVariable ("LC_ALL")) == null)
+				if ((language = Environment.GetEnvironmentVariable ("LC_MESSAGES")) == null)
+					if ((language = Environment.GetEnvironmentVariable ("LANG")) == null)
+						language = "en";
+
+			if (language.IndexOf('.') >= 0)
+				language = language.Substring(0,language.IndexOf('.'));
+			if (language.IndexOf('@') >= 0)
+				language = language.Substring(0,language.IndexOf('@'));
+			language = language.Replace('_','-');
+
+			return language;
+		}
+	}
+
+	class OriginalGallery : FolderGallery
+	{
+		public OriginalGallery (IBrowsableCollection selection, string path, string name) : base (selection, path, name)
+		{
+			requests = new ScaleRequest [] { new ScaleRequest ("hq", 0, 0, false),
+							 new ScaleRequest ("mq", 800, 600, true),
+							 new ScaleRequest ("lq", 640, 480, false, true),
+							 new ScaleRequest ("thumbs", 120, 120, false) };
+		}
+
+		public override void GenerateLayout ()
+		{
+			base.GenerateLayout ();
+			MakeDir (SubdirPath ("comments"));
+			CreateHtaccess();
+			CreateInfo();
+			SetTime ();
+		}
+
+		protected override string ImageName (int photo_index)
+		{
+			return String.Format ("img-{0}.jpg", photo_index + 1);
+		}
+
+		private string AlternateName (int photo_index, string extension)
+		{
+			return System.IO.Path.GetFileNameWithoutExtension (ImageName (photo_index)) + extension;
+		}
+
+		private void SetTime ()
+		{
+			try {
+				for (int i = 0; i < collection.Count; i++)
+					CreateComments (collection [i].DefaultVersionUri.LocalPath, i);
+
+				Directory.SetLastWriteTimeUtc(gallery_path, collection [0].Time);
+			} catch (System.Exception e) {
+				System.Console.WriteLine (e.ToString ());
+			}
+		}
+
+		internal void CreateZip ()
+		{
+			MakeDir (SubdirPath ("zip"));
+			try {
+				if (System.IO.Directory.Exists (SubdirPath ("mq")))
+				    CreateZipFile("mq");
+
+				if (System.IO.Directory.Exists (SubdirPath ("hq")))
+				    CreateZipFile("hq");
+
+			} catch (System.Exception e) {
+				System.Console.WriteLine (e.ToString ());
+			}
+		}
+
+		private void CreateComments(string photo_path, int photo_index)
+		{
+			StreamWriter comment = File.CreateText(SubdirPath  ("comments", photo_index + 1 + ".txt"));
+			comment.Write("<span>photo " + (photo_index + 1) + "</span> ");
+			comment.Write (collection [photo_index].Description + Environment.NewLine);
+			comment.Close();
+		}
+
+		private void CreateZipFile(string img_quality)
+		{
+			string[] filenames = Directory.GetFiles(SubdirPath (img_quality));
+			Crc32 crc = new Crc32();
+			ZipOutputStream s = new ZipOutputStream(File.Create(SubdirPath ("zip", img_quality + ".zip")));
+
+			s.SetLevel(0);
+			foreach (string file in filenames) {
+				FileStream fs = File.OpenRead(file);
+
+				byte[] buffer = new byte[fs.Length];
+				fs.Read(buffer, 0, buffer.Length);
+				ZipEntry entry = new ZipEntry(Path.GetFileName(file));
+
+				entry.DateTime = DateTime.Now;
+
+				// set Size and the crc, because the information
+				// about the size and crc should be stored in the header
+				// if it is not set it is automatically written in the footer.
+				// (in this case size == crc == -1 in the header)
+				// Some ZIP programs have problems with zip files that don't store
+				// the size and crc in the header.
+				entry.Size = fs.Length;
+				fs.Close();
+
+				crc.Reset();
+				crc.Update(buffer);
+
+				entry.Crc  = crc.Value;
+
+				s.PutNextEntry(entry);
+
+				s.Write(buffer, 0, buffer.Length);
+
+			}
+
+			s.Finish();
+			s.Close();
+		}
+
+		private void CreateHtaccess()
+		{
+			StreamWriter htaccess = File.CreateText(Path.Combine (gallery_path,".htaccess"));
+			htaccess.Write("<Files info.txt>" + Environment.NewLine + "\tdeny from all" + Environment.NewLine+ "</Files>" + Environment.NewLine);
+			htaccess.Close();
+		}
+
+		private void CreateInfo()
+		{
+			StreamWriter info = File.CreateText(Path.Combine (gallery_path, "info.txt"));
+			info.WriteLine("name|" + gallery_name);
+			info.WriteLine("date|" + collection [0].Time.Date.ToString ("dd.MM.yyyy"));
+			info.WriteLine("description|" + description);
+			info.Close();
+		}
+	}
+
+	class HtmlGallery : FolderGallery
+	{
+		int current;
+		int perpage = 16;
+		string stylesheet = "f-spot-simple.css";
+		string altstylesheet = "f-spot-simple-white.css";
+		string javascript = "f-spot.js";
+
+		//Note for translators: light as clear, opposite as dark
+		static string light = Catalog.GetString("Light");
+		static string dark = Catalog.GetString("Dark");
+
+		ArrayList allTagNames = new ArrayList ();
+		Hashtable allTags = new Hashtable ();
+		Hashtable tagSets = new Hashtable ();
+
+		public HtmlGallery (IBrowsableCollection selection, string path, string name) : base (selection, path, name)
+		{
+			requests = new ScaleRequest [] { new ScaleRequest ("hq", 0, 0, false),
+							 new ScaleRequest ("mq", 480, 320, false),
+							 new ScaleRequest ("thumbs", 120, 90, false) };
+		}
+
+		protected override string ImageName (int photo_index)
+		{
+			return String.Format ("img-{0}.jpg", photo_index + 1);
+		}
+
+		public override void GenerateLayout ()
+		{
+			if (collection.Count == 0)
+				return;
+
+			base.GenerateLayout ();
+
+			IBrowsableItem [] photos = collection.Items;
+
+			int i;
+			for (i = 0; i < photos.Length; i++)
+				SavePhotoHtmlIndex (i);
+
+			for (i = 0; i < PageCount; i++)
+				SaveHtmlIndex (i);
+
+			if (exportTags) {
+				// identify tags present in these photos
+				i = 0;
+				foreach (IBrowsableItem photo in photos) {
+					foreach (Tag tag in photo.Tags) {
+						if (!tagSets.ContainsKey (tag.Name)) {
+							tagSets.Add (tag.Name, new ArrayList ());
+							allTags.Add (tag.Name, tag);
+						}
+						((ArrayList) tagSets [tag.Name]).Add (i);
+					}
+					i++;
+				}
+				allTagNames = new ArrayList (tagSets.Keys);
+				allTagNames.Sort ();
+
+				// create tag pages
+				SaveTagsPage ();
+				foreach (string tag in allTagNames) {
+					for (i = 0; i < TagPageCount (tag); i++)
+						SaveTagIndex (tag, i);
+				}
+			}
+
+			if (exportTags && exportTagIcons) {
+				SaveTagIcons ();
+			}
+
+			MakeDir (SubdirPath ("style"));
+			System.Reflection.Assembly assembly = System.Reflection.Assembly.GetCallingAssembly ();
+			using (Stream s = assembly.GetManifestResourceStream (stylesheet)) {
+				using (Stream fs = System.IO.File.Open (SubdirPath ("style", stylesheet), System.IO.FileMode.Create)) {
+
+					byte [] buffer = new byte [8192];
+					int n;
+					while ((n = s.Read (buffer, 0, buffer.Length)) != 0)
+						fs.Write (buffer, 0,  n);
+
+				}
+			}
+			/* quick and stupid solution
+			   this should have been iterated over an array of stylesheets, really
+			*/
+			using (Stream s = assembly.GetManifestResourceStream (altstylesheet)) {
+				using (Stream fs = System.IO.File.Open (SubdirPath ("style", altstylesheet), System.IO.FileMode.Create)) {
+
+					byte [] buffer = new byte [8192];
+					int n = 0;
+					while ((n = s.Read (buffer, 0, buffer.Length)) != 0)
+						fs.Write (buffer, 0,  n);
+
+				}
+			}
+
+			/* Javascript for persistant style change */
+			MakeDir (SubdirPath ("script"));
+			using (Stream s = assembly.GetManifestResourceStream (javascript)) {
+				using (Stream fs = System.IO.File.Open (SubdirPath ("script", javascript), System.IO.FileMode.Create)) {
+
+					byte [] buffer = new byte [8192];
+					int n = 0;
+					while ((n = s.Read (buffer, 0, buffer.Length)) != 0)
+						fs.Write (buffer, 0,  n);
+
+				}
+			}
+		}
+
+		public int PageCount {
+			get {
+				return 	(int) System.Math.Ceiling (collection.Items.Length / (double)perpage);
+			}
+		}
+
+		public int TagPageCount (string tag)
+		{
+			return (int) System.Math.Ceiling (((ArrayList) tagSets [tag]).Count / (double)perpage);
+		}
+
+		public string PhotoThumbPath (int item)
+		{
+			return System.IO.Path.Combine (requests [2].Name, ImageName (item));
+		}
+
+		public string PhotoWebPath (int item)
+		{
+			return System.IO.Path.Combine (requests [1].Name, ImageName (item));
+		}
+
+		public string PhotoOriginalPath (int item)
+		{
+			return System.IO.Path.Combine (requests [0].Name, ImageName (item));
+		}
+
+		public string PhotoIndexPath (int item)
+		{
+			return (System.IO.Path.GetFileNameWithoutExtension (ImageName (item)) + ".html");
+		}
+
+		public static void WritePageNav (System.Web.UI.HtmlTextWriter writer, string id, string url, string name)
+		{
+			writer.AddAttribute ("id", id);
+			writer.RenderBeginTag ("div");
+
+			writer.AddAttribute ("href", url);
+			writer.RenderBeginTag ("a");
+			writer.Write (name);
+			writer.RenderEndTag ();
+
+			writer.RenderEndTag ();
+		}
+
+		public void SavePhotoHtmlIndex (int i)
+		{
+			System.IO.StreamWriter stream = System.IO.File.CreateText (SubdirPath (PhotoIndexPath (i)));
+			System.Web.UI.HtmlTextWriter writer = new System.Web.UI.HtmlTextWriter (stream);
+
+			//writer.Indent = 4;
+
+			//writer.Write ("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">");
+			writer.WriteLine ("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\";>");
+			writer.AddAttribute ("xmlns", "http://www.w3.org/1999/xhtml";);
+			writer.AddAttribute ("xml:lang", this.Language);
+			writer.RenderBeginTag ("html");
+
+			WriteHeader (writer);
+
+			writer.AddAttribute ("onload", "checkForTheme()");
+			writer.RenderBeginTag ("body");
+
+			writer.AddAttribute ("class", "container1");
+			writer.RenderBeginTag ("div");
+
+			writer.AddAttribute ("class", "header");
+			writer.RenderBeginTag ("div");
+
+			writer.AddAttribute ("id", "title");
+			writer.RenderBeginTag ("div");
+			writer.Write (gallery_name);
+			writer.RenderEndTag ();
+
+			writer.AddAttribute ("class", "navi");
+			writer.RenderBeginTag ("div");
+
+			if (i > 0)
+				// Abbreviation of previous
+				WritePageNav (writer, "prev", PhotoIndexPath (i - 1), Catalog.GetString("Prev"));
+
+			WritePageNav (writer, "index", IndexPath (i / perpage), Catalog.GetString("Index"));
+
+			if (exportTags)
+				WritePageNav (writer, "tagpage", TagsIndexPath (), Catalog.GetString ("Tags"));
+
+			if (i < collection.Count -1)
+				WritePageNav (writer, "next", PhotoIndexPath (i + 1), Catalog.GetString("Next"));
+
+			writer.RenderEndTag (); //navi
+
+			writer.RenderEndTag (); //header
+
+			writer.AddAttribute ("class", "photo");
+			writer.RenderBeginTag ("div");
+
+			writer.AddAttribute ("href", PhotoOriginalPath (i));
+			writer.RenderBeginTag ("a");
+
+			writer.AddAttribute ("src", PhotoWebPath (i));
+			writer.AddAttribute ("alt", "#");
+			writer.AddAttribute ("class", "picture");
+			writer.RenderBeginTag ("img");
+			writer.RenderEndTag (); //img
+			writer.RenderEndTag (); //a
+
+			writer.AddAttribute ("id", "description");
+			writer.RenderBeginTag ("div");
+			writer.Write (collection [i].Description);
+			writer.RenderEndTag (); //div#description
+
+			writer.RenderEndTag (); //div.photo
+
+			WriteTagsLinks (writer, collection [i].Tags);
+
+			WriteStyleSelectionBox (writer);
+
+			writer.RenderEndTag (); //container1
+
+			WriteFooter (writer);
+
+			writer.RenderEndTag (); //body
+			writer.RenderEndTag (); // html
+
+			writer.Close ();
+			stream.Close ();
+		}
+
+		public static string IndexPath (int page_num)
+		{
+			if (page_num == 0)
+				return "index.html";
+			else
+				return String.Format ("index{0}.html", page_num);
+		}
+
+		public static string TagsIndexPath ()
+		{
+			return "tags.html";
+		}
+
+		public static string TagIndexPath (string tag, int page_num)
+		{
+			string name = "tag_"+tag;
+			name = name.Replace ("/", "_").Replace (" ","_");
+			if (page_num == 0)
+				return name + ".html";
+			else
+				return name + String.Format ("_{0}.html", page_num);
+		}
+
+		static string IndexTitle (int page)
+		{
+			return String.Format ("{0}", page + 1);
+		}
+
+		public void WriteHeader (System.Web.UI.HtmlTextWriter writer)
+		{
+			WriteHeader (writer, "");
+		}
+
+		public void WriteHeader (System.Web.UI.HtmlTextWriter writer, string titleExtension)
+		{
+			writer.RenderBeginTag ("head");
+			/* It seems HtmlTextWriter always uses UTF-8, unless told otherwise */
+			writer.Write ("<meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" />");
+			writer.WriteLine ();
+			writer.RenderBeginTag ("title");
+			writer.Write (gallery_name + titleExtension);
+			writer.RenderEndTag ();
+
+			writer.Write ("<link type=\"text/css\" rel=\"stylesheet\" href=\"");
+			writer.Write (String.Format ("{0}", "style/" + stylesheet));
+			writer.Write ("\" title=\"" + dark + "\" media=\"screen\" />" + Environment.NewLine);
+
+			writer.Write ("<link type=\"text/css\" rel=\"prefetch ") ;
+			writer.Write ("alternate stylesheet\" href=\"");
+			writer.Write (String.Format ("{0}", "style/" + altstylesheet));
+			writer.Write ("\" title=\"" + light + "\" media=\"screen\" />" + Environment.NewLine);
+
+			writer.Write ("<script src=\"script/" + javascript + "\"");
+			writer.Write (" type=\"text/javascript\"></script>" + Environment.NewLine);
+
+			writer.RenderEndTag ();
+		}
+
+		public static void WriteFooter (System.Web.UI.HtmlTextWriter writer)
+		{
+			writer.AddAttribute ("class", "footer");
+			writer.RenderBeginTag ("div");
+
+			writer.Write (Catalog.GetString ("Gallery generated by") + " ");
+
+			writer.AddAttribute ("href", "http://www.gnome.org/projects/f-spot";);
+			writer.RenderBeginTag ("a");
+			writer.Write (String.Format ("{0} {1}", FSpot.Defines.PACKAGE, FSpot.Defines.VERSION));
+			writer.RenderEndTag ();
+
+			writer.RenderEndTag ();
+		}
+
+		public static void WriteStyleSelectionBox (System.Web.UI.HtmlTextWriter writer)
+		{
+			//Style Selection Box
+			writer.AddAttribute ("id", "styleboxcontainer");
+			writer.RenderBeginTag ("div");
+			writer.AddAttribute ("id", "stylebox");
+			writer.AddAttribute ("style", "display: none;");
+			writer.RenderBeginTag ("div");
+			writer.RenderBeginTag ("ul");
+			writer.RenderBeginTag ("li");
+			writer.AddAttribute ("href", "#");
+			writer.AddAttribute ("title", dark);
+			writer.AddAttribute ("onclick", "setActiveStyleSheet('" + dark + "')");
+			writer.RenderBeginTag ("a");
+			writer.Write (dark);
+			writer.RenderEndTag (); //a
+			writer.RenderEndTag (); //li
+			writer.RenderBeginTag ("li");
+			writer.AddAttribute ("href", "#");
+			writer.AddAttribute ("title", light);
+			writer.AddAttribute ("onclick", "setActiveStyleSheet('" + light + "')");
+			writer.RenderBeginTag ("a");
+			writer.Write (light);
+			writer.RenderEndTag (); //a
+			writer.RenderEndTag (); //li
+			writer.RenderEndTag (); //ul
+			writer.RenderEndTag (); //div stylebox
+			writer.RenderBeginTag ("div");
+			writer.Write ("<span class=\"style_toggle\">");
+			writer.Write ("<a href=\"javascript:toggle_stylebox()\">");
+			writer.Write ("<span id=\"showlink\">" + Catalog.GetString("Show Styles") + "</span><span id=\"hidelink\" ");
+			writer.Write ("style=\"display:none;\">" + Catalog.GetString("Hide Styles") + "</span></a></span>" + Environment.NewLine);
+			writer.RenderEndTag (); //div toggle
+			writer.RenderEndTag (); //div styleboxcontainer
+		}
+
+		public void WriteTagsLinks (System.Web.UI.HtmlTextWriter writer, Tag[] tags)
+		{
+			ArrayList tagsList = new ArrayList (tags.Length);
+			foreach (Tag tag in tags) {
+				tagsList.Add (tag);
+			}
+			WriteTagsLinks (writer, tagsList);
+		}
+
+		public void WriteTagsLinks (System.Web.UI.HtmlTextWriter writer, System.Collections.ICollection tags)
+		{
+
+			// check if we should write tags
+			if (!exportTags && tags.Count>0)
+				return;
+
+			writer.AddAttribute ("id", "tagbox");
+			writer.RenderBeginTag ("div");
+			writer.RenderBeginTag ("h1");
+			writer.Write (Catalog.GetString ("Tags"));
+			writer.RenderEndTag (); //h1
+			writer.AddAttribute ("id", "innertagbox");
+			writer.RenderBeginTag ("ul");
+			foreach (Tag tag in tags) {
+				writer.AddAttribute ("class", "tag");
+				writer.RenderBeginTag ("li");
+				writer.AddAttribute ("href", TagIndexPath (tag.Name, 0));
+				writer.RenderBeginTag ("a");
+				if (exportTagIcons) {
+					writer.AddAttribute ("alt", tag.Name);
+					writer.AddAttribute ("longdesc", Catalog.GetString ("Tags: ")+tag.Name);
+					writer.AddAttribute ("title", Catalog.GetString ("Tags: ")+tag.Name);
+					writer.AddAttribute ("src", TagPath (tag));
+					writer.RenderBeginTag ("img");
+					writer.RenderEndTag ();
+				}
+				writer.Write(" ");
+				if (exportTagIcons)
+					writer.AddAttribute ("class", "tagtext-icon");
+				else
+					writer.AddAttribute ("class", "tagtext-noicon");
+				writer.RenderBeginTag ("span");
+				writer.Write (tag.Name);
+				writer.RenderEndTag (); //span.tagtext
+				writer.RenderEndTag (); //a href
+				writer.RenderEndTag (); //div.tag
+			}
+			writer.RenderEndTag (); //div#tagbox
+		}
+
+		public void SaveTagsPage ()
+		{
+			System.IO.StreamWriter stream = System.IO.File.CreateText (SubdirPath (TagsIndexPath ()));
+			System.Web.UI.HtmlTextWriter writer = new System.Web.UI.HtmlTextWriter (stream);
+
+			writer.WriteLine ("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\";>");
+			writer.AddAttribute ("xmlns", "http://www.w3.org/1999/xhtml";);
+			writer.AddAttribute ("xml:lang", this.Language);
+			writer.RenderBeginTag ("html");
+			string titleExtension = " " + Catalog.GetString ("Tags");
+			WriteHeader (writer, titleExtension);
+
+			writer.AddAttribute ("onload", "checkForTheme()");
+			writer.AddAttribute ("id", "tagpage");
+			writer.RenderBeginTag ("body");
+
+			writer.AddAttribute ("class", "container1");
+			writer.RenderBeginTag ("div");
+
+			writer.AddAttribute ("class", "header");
+			writer.RenderBeginTag ("div");
+
+			writer.AddAttribute ("id", "title");
+			writer.RenderBeginTag ("div");
+			writer.Write (gallery_name + titleExtension);
+			writer.RenderEndTag (); //title div
+
+			writer.AddAttribute ("class", "navi");
+			writer.RenderBeginTag ("div");
+
+			writer.AddAttribute ("class", "navipage");
+			writer.RenderBeginTag ("div");
+
+			writer.AddAttribute ("href", IndexPath (0));
+			writer.RenderBeginTag ("a");
+			writer.Write (Catalog.GetString ("Index"));
+			writer.RenderEndTag (); //a
+
+			writer.RenderEndTag (); //navipage
+			writer.RenderEndTag (); //navi
+			writer.RenderEndTag (); //header
+
+			WriteTagsLinks (writer, allTags.Values);
+
+			WriteStyleSelectionBox (writer);
+
+			writer.RenderEndTag (); //container1
+
+			WriteFooter (writer);
+
+			writer.RenderEndTag (); //body
+			writer.RenderEndTag (); //html
+
+			writer.Close ();
+			stream.Close ();
+		}
+
+		public void SaveTagIndex (string tag, int page_num)
+		{
+			System.IO.StreamWriter stream = System.IO.File.CreateText (SubdirPath (TagIndexPath (tag, page_num)));
+			System.Web.UI.HtmlTextWriter writer = new System.Web.UI.HtmlTextWriter (stream);
+
+			writer.WriteLine ("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\";>");
+			writer.AddAttribute ("xmlns", "http://www.w3.org/1999/xhtml";);
+			writer.AddAttribute ("xml:lang", this.Language);
+			writer.RenderBeginTag ("html");
+			string titleExtension = ": " + tag;
+			WriteHeader (writer, titleExtension);
+
+			writer.AddAttribute ("onload", "checkForTheme()");
+			writer.RenderBeginTag ("body");
+
+			writer.AddAttribute ("class", "container1");
+			writer.RenderBeginTag ("div");
+
+			writer.AddAttribute ("class", "header");
+			writer.RenderBeginTag ("div");
+
+			writer.AddAttribute ("id", "title");
+			writer.RenderBeginTag ("div");
+			writer.Write (gallery_name + titleExtension);
+			writer.RenderEndTag (); //title div
+
+			writer.AddAttribute ("class", "navi");
+			writer.RenderBeginTag ("div");
+
+			// link to all photos
+			writer.AddAttribute ("class", "navipage");
+			writer.RenderBeginTag ("div");
+
+			writer.AddAttribute ("href", IndexPath (0));
+			writer.RenderBeginTag ("a");
+			writer.Write ("Index");
+			writer.RenderEndTag (); //a
+
+			writer.RenderEndTag (); //navipage
+			// end link to all photos
+
+			// link to all tags
+			writer.AddAttribute ("class", "navipage");
+			writer.RenderBeginTag ("div");
+
+			writer.AddAttribute ("href", TagsIndexPath ());
+			writer.RenderBeginTag ("a");
+			writer.Write ("Tags");
+			writer.RenderEndTag (); //a
+
+			writer.RenderEndTag (); //navipage
+			// end link to all tags
+
+			writer.AddAttribute ("class", "navilabel");
+			writer.RenderBeginTag ("div");
+			writer.Write (Catalog.GetString ("Page:"));
+			writer.RenderEndTag (); //pages div
+
+			int i;
+			for (i = 0; i < TagPageCount (tag); i++) {
+				writer.AddAttribute ("class", i == page_num ? "navipage-current" : "navipage");
+				writer.RenderBeginTag ("div");
+
+				writer.AddAttribute ("href", TagIndexPath (tag, i));
+				writer.RenderBeginTag ("a");
+				writer.Write (IndexTitle (i));
+				writer.RenderEndTag (); //a
+
+				writer.RenderEndTag (); //navipage
+			}
+			writer.RenderEndTag (); //navi
+			writer.RenderEndTag (); //header
+
+			writer.AddAttribute ("class", "thumbs");
+			writer.RenderBeginTag ("div");
+
+			int start = page_num * perpage;
+			ArrayList tagSet = (ArrayList) tagSets [tag];
+			int end = Math.Min (start + perpage, tagSet.Count);
+			for (i = start; i < end; i++) {
+				writer.AddAttribute ("href", PhotoIndexPath ((int) tagSet [i]));
+				writer.RenderBeginTag ("a");
+
+				writer.AddAttribute  ("src", PhotoThumbPath ((int) tagSet [i]));
+				writer.AddAttribute  ("alt", "#");
+				writer.RenderBeginTag ("img");
+				writer.RenderEndTag ();
+
+				writer.RenderEndTag (); //a
+			}
+
+			writer.RenderEndTag (); //thumbs
+
+			writer.AddAttribute ("id", "gallery_description");
+			writer.RenderBeginTag ("div");
+			writer.Write (description);
+			writer.RenderEndTag (); //description
+
+			WriteStyleSelectionBox (writer);
+
+			writer.RenderEndTag (); //container1
+
+			WriteFooter (writer);
+
+			writer.RenderEndTag (); //body
+			writer.RenderEndTag (); //html
+
+			writer.Close ();
+			stream.Close ();
+		}
+
+		public void SaveTagIcons ()
+		{
+			MakeDir (SubdirPath ("tags"));
+			foreach (Tag tag in allTags.Values)
+				SaveTagIcon (tag);
+		}
+
+		public void SaveTagIcon (Tag tag) {
+			Gdk.Pixbuf icon = tag.Icon;
+			Gdk.Pixbuf scaled = null;
+			if (icon.Height != 52 || icon.Width != 52) {
+				scaled=icon.ScaleSimple(52,52,Gdk.InterpType.Bilinear);
+			} else
+				scaled=icon;
+			scaled.Save (SubdirPath("tags",TagName(tag)), "png");
+		}
+
+		public string TagPath (Tag tag)
+		{
+			return System.IO.Path.Combine("tags",TagName(tag));
+		}
+
+		public string TagName (Tag tag)
+		{
+			return "tag_"+ ((DbItem)tag).Id+".png";
+		}
+
+		public void SaveHtmlIndex (int page_num)
+		{
+			System.IO.StreamWriter stream = System.IO.File.CreateText (SubdirPath (IndexPath (page_num)));
+			System.Web.UI.HtmlTextWriter writer = new System.Web.UI.HtmlTextWriter (stream);
+
+			//writer.Indent = 4;
+
+			//writer.Write ("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">");
+			writer.WriteLine ("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Strict//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd\";>");
+			writer.AddAttribute ("xmlns", "http://www.w3.org/1999/xhtml";);
+			writer.AddAttribute ("xml:lang", this.Language);
+			writer.RenderBeginTag ("html");
+			WriteHeader (writer);
+
+			writer.AddAttribute ("onload", "checkForTheme()");
+			writer.RenderBeginTag ("body");
+
+
+
+			writer.AddAttribute ("class", "container1");
+			writer.RenderBeginTag ("div");
+
+			writer.AddAttribute ("class", "header");
+			writer.RenderBeginTag ("div");
+
+			writer.AddAttribute ("id", "title");
+			writer.RenderBeginTag ("div");
+			writer.Write (gallery_name);
+			writer.RenderEndTag (); //title div
+
+			writer.AddAttribute ("class", "navi");
+			writer.RenderBeginTag ("div");
+
+			if (exportTags) {
+				// link to all tags
+				writer.AddAttribute ("class", "navipage");
+				writer.RenderBeginTag ("div");
+
+				writer.AddAttribute ("href", TagsIndexPath ());
+				writer.RenderBeginTag ("a");
+				writer.Write ("Tags");
+				writer.RenderEndTag (); //a
+
+				writer.RenderEndTag (); //navipage
+				// end link to all tags
+			}
+
+			writer.AddAttribute ("class", "navilabel");
+			writer.RenderBeginTag ("div");
+			writer.Write (Catalog.GetString ("Page:"));
+			writer.RenderEndTag (); //pages div
+
+			int i;
+			for (i = 0; i < PageCount; i++) {
+				writer.AddAttribute ("class", i == page_num ? "navipage-current" : "navipage");
+				writer.RenderBeginTag ("div");
+
+				writer.AddAttribute ("href", IndexPath (i));
+				writer.RenderBeginTag ("a");
+				writer.Write (IndexTitle (i));
+				writer.RenderEndTag (); //a
+
+				writer.RenderEndTag (); //navipage
+			}
+			writer.RenderEndTag (); //navi
+			writer.RenderEndTag (); //header
+
+			writer.AddAttribute ("class", "thumbs");
+			writer.RenderBeginTag ("div");
+
+			int start = page_num * perpage;
+			int end = Math.Min (start + perpage, collection.Count);
+			for (i = start; i < end; i++) {
+				writer.AddAttribute ("href", PhotoIndexPath (i));
+				writer.RenderBeginTag ("a");
+
+				writer.AddAttribute  ("src", PhotoThumbPath (i));
+				writer.AddAttribute  ("alt", "#");
+				writer.RenderBeginTag ("img");
+				writer.RenderEndTag ();
+
+				writer.RenderEndTag (); //a
+			}
+
+			writer.RenderEndTag (); //thumbs
+
+			writer.AddAttribute ("id", "gallery_description");
+			writer.RenderBeginTag ("div");
+			writer.Write (description);
+			writer.RenderEndTag (); //description
+
+			WriteStyleSelectionBox (writer);
+
+			writer.RenderEndTag (); //container1
+
+			WriteFooter (writer);
+
+			writer.RenderEndTag (); //body
+			writer.RenderEndTag (); //html
+
+			writer.Close ();
+			stream.Close ();
+		}
+
+	}
+}

Copied: trunk/extensions/Exporters/FolderExport/FolderExport.glade (from r4368, /trunk/extensions/FolderExport/FolderExport.glade)
==============================================================================

Copied: trunk/extensions/Exporters/FolderExport/Makefile.am (from r4368, /trunk/extensions/FolderExport/Makefile.am)
==============================================================================

Added: trunk/extensions/Exporters/FolderExport/f-spot-simple-white.css
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FolderExport/f-spot-simple-white.css	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,222 @@
+/*
+
+Light F-Spot HTML Gallery Stylesheet
+Giacomo Rizzo <alt free-os it>
+based on Jakub 'jimmac' Steiner <jimmac novell com> work.
+
+*/
+
+body {
+  font-family: luxi sans, trebuchet ms, sans-serif;
+  color: #888;
+  margin: 12px;
+}
+
+.header {
+/*	position: relative; */
+	width: 100%;
+	margin-bottom: 20px;
+	padding: 0px;
+  border-bottom: 1px dotted #888;
+  font-size: 12px;
+}
+
+#title {
+  color: #bbb;
+  font-weight: bold;
+  margin: 0;
+  padding: 0;
+  margin-left: 3px;
+	font-size: large;
+	letter-spacing: .5em;
+}
+
+div.navi {
+	float: right;
+	text-align: right;
+	margin-top: 3px;
+}
+
+div.navilabel {
+	padding: 3px 10px 0px 0px;
+	margin-right: 10px;
+}
+
+div.navi div {
+	float: left;
+	margin: 2px;
+}
+
+/* image pages navigation */
+div.navi a {
+	display: block;
+	width: 80px;
+	height: 17px;
+	-moz-border-radius: 3px;
+	border: 1px solid #444;
+	text-align: center;
+	padding-top: 3px;
+}
+
+div#index a:hover, div#prev a:hover, div#next a:hover {
+	text-decoration: none;
+  background-color: #c49200;
+	color: #333;
+}
+
+/* index page navigation */
+div.navi div.navipage a, div.navi div.navipage-current a {
+	display: block;
+	width: auto;
+	padding-left: 6px;
+	padding-right: 6px;
+	height: 17px;
+	-moz-border-radius: 3px;
+	border: 1px solid #444;
+	text-align: center;
+	padding-top: 3px;
+}
+
+.navipage a:hover, .navipage-current a:hover {
+  text-decoration: none;
+  background-color: #c49200;
+	color: #333;
+}
+
+.navipage-current a {
+	background-color: #666;
+}
+
+a {
+  text-decoration: none;
+  color: #c49200;
+}
+
+a:hover {
+  text-decoration: underline;
+  color: #da1;
+}
+
+div.container1 {
+	width: 630px;
+	margin: auto;
+}
+
+div.photo {
+  text-align: center;
+  vertical-align: middle;
+  margin-top: 5%;
+}
+
+div.thumbs  {
+	clear: both;
+  padding: 6px;
+  text-align: center;
+}
+
+div.thumbs a {
+	margin: 6px;
+	display: block;
+	float: left;
+	width: 140px;
+	height: 140px;
+	line-height: 140px;
+	border: 1px solid #727272;
+	-moz-border-radius: 5px;
+}
+
+div.thumbs img {
+  padding: 4px;
+  vertical-align: middle;
+  border: 0px;
+}
+
+div#gallery_description {
+	clear: both;
+	padding: 10px 12px 0px 12px;
+}
+
+div.photo img.picture {
+  padding: 16px;
+  border: 1px solid #555;
+	-moz-border-radius: 7px;
+	background-color: #333;
+	margin: 20px;
+}
+
+ul#innertagbox {
+  display: inline;
+  margin: 0px;
+  padding: 0px;
+}
+div#tagbox {
+  -moz-border-radius: 7px;
+  border: 1px solid #555;
+  background-color: #333;
+  padding: 5px;
+  margin-top: 2em;
+  display: inline-block;
+}
+
+div#tagbox h1 {
+  margin-left: 1em;
+}
+
+div#tagbox li {
+  display: block;
+  margin: 3px;
+}
+
+div#tagbox img.tag {
+  margin: 5px;
+}
+
+div#tagbox .tagtext-icon {
+  position:relative;
+  top: -22px;
+}
+
+div.thumbs a:hover {
+	background-color: #222;
+  border: 2px solid #c49200;
+	margin: 5px;
+}
+
+div.photo div {
+  text-align: center;
+  margin-top: 6px;
+  font-size: 18px;
+  font-family: Luxi Serif, Georgia, Helvetica;
+//  font-style: italic;
+}
+
+div.footer {
+	clear: both;
+	padding: 20px 10px 0px 10px;
+  font-size: 10px;
+	width: 640px;
+	margin: auto;
+ }
+
+div#styleboxcontainer {
+	position: fixed;
+	bottom: 0px;
+	right: 8px;
+	font-size: 10px;
+	margin: 4px;
+	padding: 4px;
+}
+
+#stylebox ul {
+	list-style-type: none;
+	padding-left: 0px;
+}
+
+/* tags list */
+#tagbox h1 {
+	font-size: large;
+}
+
+#tagbox .tag {
+	margin-left: 1em;
+}

Added: trunk/extensions/Exporters/FolderExport/f-spot-simple.css
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FolderExport/f-spot-simple.css	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,223 @@
+/*
+
+Default F-Spot HTML Gallery Stylesheet
+Jakub 'jimmac' Steiner <jimmac novell com>
+
+*/
+
+body {
+  font-family: luxi sans, trebuchet ms, sans-serif;
+  color: #888;
+  margin: 12px;
+	background-color: #3c3c3c;
+}
+
+.header {
+/*	position: relative; */
+	width: 100%;
+	margin-bottom: 20px;
+	padding: 0px;
+  border-bottom: 1px dotted #888;
+  font-size: 12px;
+}
+
+#title {
+  color: #bbb;
+  font-weight: bold;
+  margin: 0;
+  padding: 0;
+  margin-left: 3px;
+	font-size: large;
+	letter-spacing: .5em;
+}
+
+div.navi {
+	float: right;
+	text-align: right;
+	margin-top: 3px;
+}
+
+div.navilabel {
+	padding: 3px 10px 0px 0px;
+	margin-right: 10px;
+}
+
+div.navi div {
+	float: left;
+	margin: 2px;
+}
+
+/* image pages navigation */
+div.navi a {
+	display: block;
+	width: 80px;
+	height: 17px;
+	-moz-border-radius: 3px;
+	border: 1px solid #444;
+	text-align: center;
+	padding-top: 3px;
+}
+
+div#index a:hover, div#prev a:hover, div#next a:hover {
+	text-decoration: none;
+  background-color: #c49200;
+	color: #333;
+}
+
+/* index page navigation */
+div.navi div.navipage a, div.navi div.navipage-current a {
+	display: block;
+	width: auto;
+	padding-left: 6px;
+	padding-right: 6px;
+	height: 17px;
+	-moz-border-radius: 3px;
+	border: 1px solid #444;
+	text-align: center;
+	padding-top: 3px;
+}
+
+.navipage a:hover, .navipage-current a:hover {
+  text-decoration: none;
+  background-color: #c49200;
+	color: #333;
+}
+
+.navipage-current a {
+	background-color: #666;
+}
+
+a {
+  text-decoration: none;
+  color: #c49200;
+}
+
+a:hover {
+  text-decoration: underline;
+  color: #da1;
+}
+
+div.container1 {
+	width: 630px;
+	margin: auto;
+}
+
+div.photo {
+  text-align: center;
+  vertical-align: middle;
+  margin-top: 5%;
+}
+
+div.thumbs  {
+	clear: both;
+  padding: 6px;
+  text-align: center;
+}
+
+div.thumbs a {
+	margin: 6px;
+	display: block;
+	float: left;
+	width: 140px;
+	height: 140px;
+	line-height: 140px;
+	border: 1px solid #727272;
+	background-color: #282828;
+	-moz-border-radius: 5px;
+}
+
+div.thumbs img {
+  padding: 4px;
+  vertical-align: middle;
+  border: 0px;
+}
+
+div#gallery_description {
+	clear: both;
+	padding: 10px 12px 0px 12px;
+}
+
+div.photo img.picture {
+  padding: 16px;
+  border: 1px solid #555;
+	-moz-border-radius: 7px;
+	background-color: #333;
+	margin: 20px;
+}
+
+ul#innertagbox {
+  display: inline;
+  margin: 0px;
+  padding: 0px;
+}
+div#tagbox {
+  -moz-border-radius: 7px;
+  border: 1px solid #555;
+  background-color: #333;
+  padding: 5px;
+  margin-top: 2em;
+  display: inline-block;
+}
+
+div#tagbox h1 {
+  margin-left: 1em;
+}
+
+div#tagbox li {
+  display: block;
+  margin: 3px;
+}
+
+div#tagbox img.tag {
+  margin: 5px;
+}
+
+div#tagbox .tagtext-icon {
+  position:relative;
+  top: -22px;
+}
+
+div.thumbs a:hover {
+	background-color: #222;
+  border: 2px solid #c49200;
+	margin: 5px;
+}
+
+div.photo div {
+  text-align: center;
+  margin-top: 6px;
+  font-size: 18px;
+  font-family: Luxi Serif, Georgia, Helvetica;
+//  font-style: italic;
+}
+
+div.footer {
+	clear: both;
+	padding: 20px 10px 0px 10px;
+  font-size: 10px;
+	width: 640px;
+	margin: auto;
+ }
+
+div#styleboxcontainer {
+	position: fixed;
+	bottom: 0px;
+	right: 8px;
+	font-size: 10px;
+	margin: 4px;
+	padding: 4px;
+}
+
+#stylebox ul {
+	list-style-type: none;
+	padding-left: 0px;
+}
+
+/* tags list */
+#tagbox h1 {
+	font-size: large;
+}
+
+#tagbox .tag {
+	margin-left: 1em;
+}

Added: trunk/extensions/Exporters/FolderExport/f-spot.js
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/FolderExport/f-spot.js	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,92 @@
+function setActiveStyleSheet(title) {
+   var i, a, main;
+   for(i=0; (a = document.getElementsByTagName("link")[i]); i++) {
+     if(a.getAttribute("rel").indexOf("style") != -1
+        && a.getAttribute("title")) {
+       a.disabled = true;
+       if(a.getAttribute("title") == title) a.disabled = false;
+     }
+   }
+   if (title!="") {
+      setCookie("theme", title);
+   }
+}
+
+function getInactiveStyleSheet() {
+  var i, a;
+  for(i=0; (a = document.getElementsByTagName("link")[i]); i++) {
+    if(a.getAttribute("rel").indexOf("style") != -1 && a.getAttribute("title") && a.disabled) return a.getAttribute("title");
+  }
+  return null;
+}
+
+
+function setCookie(name, value, expires, path, domain, secure)
+{
+    document.cookie= name + "=" + escape(value) +
+        ((expires) ? "; expires=" + expires.toGMTString() : "") +
+        ((path) ? "; path=" + path : "") +
+        ((domain) ? "; domain=" + domain : "") +
+        ((secure) ? "; secure" : "");
+}
+
+function getCookie(name)
+{
+    var dc = document.cookie;
+    var prefix = name + "=";
+    var begin = dc.indexOf("; " + prefix);
+    if (begin == -1)
+    {
+        begin = dc.indexOf(prefix);
+        if (begin != 0) return null;
+    }
+    else
+    {
+        begin += 2;
+    }
+    var end = document.cookie.indexOf(";", begin);
+    if (end == -1)
+    {
+        end = dc.length;
+    }
+    return unescape(dc.substring(begin + prefix.length, end));
+}
+
+function deleteCookie(name, path, domain)
+{
+    if (getCookie(name))
+    {
+        document.cookie = name + "=" +
+            ((path) ? "; path=" + path : "") +
+            ((domain) ? "; domain=" + domain : "") +
+            "; expires=Thu, 01-Jan-70 00:00:01 GMT";
+    }
+}
+
+
+function checkForTheme() {
+   var theme = getCookie('theme');
+
+   //alert(theme);
+   if (theme) {
+      setActiveStyleSheet(theme);
+   }
+}
+
+// to hide and show the styles
+// inspired by www.wikipedia.org
+function toggle_stylebox() {
+    var stylebox = document.getElementById('stylebox');
+    var showlink=document.getElementById('showlink');
+    var hidelink=document.getElementById('hidelink');
+    if(stylebox.style.display == 'none') {
+	stylebox_was = stylebox.style.display;
+	stylebox.style.display = '';
+	hidelink.style.display='';
+	showlink.style.display='none';
+    } else {
+	stylebox.style.display = stylebox_was;
+	hidelink.style.display='none';
+	showlink.style.display='';
+    }
+}

Copied: trunk/extensions/Exporters/GalleryExport/.gitignore (from r4368, /trunk/extensions/FlickrExport/FlickrNet/.gitignore)
==============================================================================

Added: trunk/extensions/Exporters/GalleryExport/GalleryExport.addin.xml
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/GalleryExport/GalleryExport.addin.xml	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,17 @@
+<Addin namespace="FSpot"
+	version="0.4.4.102"
+	name="Gallery Export"
+	description="This extension allows you to export your photos to PHP Gallery (http://gallery.menalto.com)."
+	author="F-Spot team"
+	url="http://f-spot.org";
+	defaultEnabled="true"
+	category="Export">
+
+	<Dependencies>
+		<Addin id="Core" version="0.4.4.100"/>
+	</Dependencies>
+
+	<Extension path = "/FSpot/Menus/Exports">
+		<ExportMenuItem id="Gallery" _label = "Web _Gallery..." class = "G2Export.GalleryExport" />
+	</Extension>
+</Addin>

Added: trunk/extensions/Exporters/GalleryExport/GalleryExport.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/GalleryExport/GalleryExport.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,1011 @@
+using System;
+using System.Net;
+using System.IO;
+using System.Text;
+using System.Collections;
+using System.Collections.Specialized;
+using System.Web;
+using Mono.Unix;
+
+using FSpot;
+using FSpot.Filters;
+using FSpot.Widgets;
+using FSpot.Utils;
+using FSpot.UI.Dialog;
+using FSpot.Extensions;
+
+using GalleryRemote;
+
+namespace G2Export {
+	public class GalleryAccount {
+		public GalleryAccount (string name, string url, string username, string password) : this (name, url, username, password, GalleryVersion.VersionUnknown) {}
+		public GalleryAccount (string name, string url, string username, string password, GalleryVersion version)
+		{
+			this.name = name;
+			this.username = username;
+			this.password = password;
+			this.Url = url;
+
+			if (version != GalleryVersion.VersionUnknown) {
+				this.version = version;
+			} else {
+				this.version = Gallery.DetectGalleryVersion(Url);
+			}
+		}
+
+		public const string EXPORT_SERVICE = "gallery/";
+		public const string LIGHTTPD_WORKAROUND_KEY = Preferences.APP_FSPOT_EXPORT + EXPORT_SERVICE + "lighttpd_workaround";
+
+		public Gallery Connect ()
+		{
+			//System.Console.WriteLine ("GalleryAccount.Connect()");
+			Gallery gal = null;
+
+			if (version == GalleryVersion.VersionUnknown)
+				this.version = Gallery.DetectGalleryVersion(Url);
+
+			if (version == GalleryVersion.Version1) {
+				gal = new Gallery1 (url, url);
+			} else if (version == GalleryVersion.Version2) {
+				gal = new Gallery2 (url, url);
+			} else {
+				throw new GalleryException (Catalog.GetString("Cannot connect to a Gallery for which the version is unknown.\nPlease check that you have Remote plugin 1.0.8 or later"));
+			}
+
+			System.Console.WriteLine ("Gallery created: " + gal);
+
+			gal.Login (username, password);
+
+			gallery = gal;
+			connected = true;
+
+			object val = Preferences.Get (LIGHTTPD_WORKAROUND_KEY);
+			if (val != null)
+				gallery.expect_continue = !(bool)val;
+
+			return gallery;
+		}
+
+		GalleryVersion version;
+		public GalleryVersion Version{
+			get {
+				return version;
+			}
+		}
+
+		private bool connected;
+		public bool Connected {
+			get {
+				bool retVal = false;
+				if(gallery != null) {
+					retVal = gallery.IsConnected ();
+				}
+				if (connected != retVal) {
+					System.Console.WriteLine ("Connected and retVal for IsConnected() don't agree");
+				}
+				return retVal;
+			}
+		}
+
+		public void MarkChanged ()
+		{
+			connected = false;
+			gallery = null;
+		}
+
+		Gallery gallery;
+		public Gallery Gallery {
+			get {
+				return gallery;
+			}
+		}
+
+		string name;
+		public string Name {
+			get {
+				return name;
+			}
+			set {
+				name = value;
+			}
+		}
+
+		string url;
+		public string Url {
+			get {
+				return url;
+			}
+			set {
+				if (url != value) {
+					url = value;
+					MarkChanged ();
+				}
+			}
+		}
+
+		string username;
+		public string Username {
+			get {
+				return username;
+			}
+			set {
+				if (username != value) {
+					username = value;
+					MarkChanged ();
+				}
+			}
+		}
+
+		string password;
+		public string Password {
+			get {
+				return password;
+			}
+			set {
+				if (password != value) {
+					password = value;
+					MarkChanged ();
+				}
+			}
+		}
+	}
+
+
+	public class GalleryAccountManager
+	{
+		private static GalleryAccountManager instance;
+		string xml_path;
+		ArrayList accounts;
+
+		public delegate void AccountListChangedHandler (GalleryAccountManager manager, GalleryAccount changed_account);
+		public event AccountListChangedHandler AccountListChanged;
+
+		public static GalleryAccountManager GetInstance ()
+		{
+			if (instance == null) {
+				instance = new GalleryAccountManager ();
+			}
+
+			return instance;
+		}
+
+		private GalleryAccountManager ()
+		{
+			// FIXME this xml file path should be be retrieved from a central location not hard coded there
+			this.xml_path = System.IO.Path.Combine (FSpot.Global.BaseDirectory, "Accounts.xml");
+
+			accounts = new ArrayList ();
+			ReadAccounts ();
+		}
+
+		public void MarkChanged ()
+		{
+			MarkChanged (true, null);
+		}
+
+		public void MarkChanged (bool write, GalleryAccount changed_account)
+		{
+			if (write)
+				WriteAccounts ();
+
+			if (AccountListChanged != null)
+				AccountListChanged (this, changed_account);
+		}
+
+		public ArrayList GetAccounts ()
+		{
+			return accounts;
+		}
+
+		public void AddAccount (GalleryAccount account)
+		{
+			AddAccount (account, true);
+		}
+
+		public void AddAccount (GalleryAccount account, bool write)
+		{
+			accounts.Add (account);
+			MarkChanged (write, account);
+		}
+
+		public void RemoveAccount (GalleryAccount account)
+		{
+			accounts.Remove (account);
+			MarkChanged ();
+		}
+
+		public void WriteAccounts ()
+		{
+			System.Xml.XmlTextWriter writer = new System.Xml.XmlTextWriter (xml_path, System.Text.Encoding.Default);
+
+			writer.Formatting = System.Xml.Formatting.Indented;
+			writer.Indentation = 2;
+			writer.IndentChar = ' ';
+
+			writer.WriteStartDocument (true);
+
+			writer.WriteStartElement ("GalleryRemote");
+			foreach (GalleryAccount account in accounts) {
+				writer.WriteStartElement ("Account");
+				writer.WriteElementString ("Name", account.Name);
+
+				writer.WriteElementString ("Url", account.Url);
+				writer.WriteElementString ("Username", account.Username);
+				writer.WriteElementString ("Password", account.Password);
+				writer.WriteElementString ("Version", account.Version.ToString());
+				writer.WriteEndElement (); //Account
+			}
+			writer.WriteEndElement ();
+			writer.WriteEndDocument ();
+			writer.Close ();
+		}
+
+		private GalleryAccount ParseAccount (System.Xml.XmlNode node)
+		{
+			if (node.Name != "Account")
+
+				return null;
+
+			string name = null;
+			string url = null;
+			string username = null;
+			string password = null;
+			GalleryVersion version = GalleryVersion.VersionUnknown;
+
+			foreach (System.Xml.XmlNode child in node.ChildNodes) {
+				if (child.Name == "Name") {
+					name = child.ChildNodes [0].Value;
+
+				} else if (child.Name == "Url") {
+					url = child.ChildNodes [0].Value;
+				} else if (child.Name == "Password") {
+					password = child.ChildNodes [0].Value;
+				} else if (child.Name == "Username") {
+					username = child.ChildNodes [0].Value;
+				} else if (child.Name == "Version") {
+					string versionString = child.ChildNodes [0].Value;
+					if (versionString == "Version1")
+						version = GalleryVersion.Version1;
+					else if (versionString == "Version2")
+						version = GalleryVersion.Version2;
+					else
+						Console.WriteLine ("Unexpected versions string: " + versionString);
+				}
+			}
+			return new GalleryAccount (name, url, username, password, version);
+		}
+
+		private void ReadAccounts ()
+		{
+
+			if (! File.Exists (xml_path)) {
+				MarkChanged ();
+				return;
+			}
+
+			try {
+				string query = "//GalleryRemote/Account";
+				System.Xml.XmlDocument doc = new System.Xml.XmlDocument ();
+
+				//System.Console.WriteLine ("xml_path: " + xml_path);
+				doc.Load (xml_path);
+				System.Xml.XmlNodeList nodes = doc.SelectNodes (query);
+
+				//System.Console.WriteLine ("selected {0} nodes match {1}", nodes.Count, query);
+				foreach (System.Xml.XmlNode node in nodes) {
+					GalleryAccount account = ParseAccount (node);
+					if (account != null)
+						AddAccount (account, false);
+
+				}
+			} catch (System.Exception e) {
+				// FIXME do something
+				System.Console.WriteLine ("Exception loading gallery accounts");
+				System.Console.WriteLine (e);
+			}
+
+			MarkChanged ();
+		}
+	}
+
+	public class AccountDialog {
+		public AccountDialog (Gtk.Window parent) : this (parent, null, false) {
+			add_dialog.Response += HandleAddResponse;
+			add_button.Sensitive = false;
+		}
+
+		public AccountDialog (Gtk.Window parent, GalleryAccount account, bool show_error)
+		{
+			Glade.XML xml = new Glade.XML (null, "GalleryExport.glade", "gallery_add_dialog", "f-spot");
+			xml.Autoconnect (this);
+			add_dialog = (Gtk.Dialog) xml.GetWidget ("gallery_add_dialog");
+			add_dialog.Modal = false;
+			add_dialog.TransientFor = parent;
+			add_dialog.DefaultResponse = Gtk.ResponseType.Ok;
+
+			this.account = account;
+
+			status_area.Visible = show_error;
+
+			if (account != null) {
+				gallery_entry.Text = account.Name;
+				url_entry.Text = account.Url;
+				password_entry.Text = account.Password;
+				username_entry.Text = account.Username;
+				add_button.Label = Gtk.Stock.Ok;
+				add_dialog.Response += HandleEditResponse;
+			}
+
+			if (remove_button != null)
+				remove_button.Visible = account != null;
+
+			add_dialog.Show ();
+
+			gallery_entry.Changed += HandleChanged;
+			url_entry.Changed += HandleChanged;
+			password_entry.Changed += HandleChanged;
+			username_entry.Changed += HandleChanged;
+			HandleChanged (null, null);
+		}
+
+		private void HandleChanged (object sender, System.EventArgs args)
+		{
+			name = gallery_entry.Text;
+			url = url_entry.Text;
+			password = password_entry.Text;
+			username = username_entry.Text;
+
+			if (name == String.Empty || url == String.Empty || password == String.Empty || username == String.Empty)
+				add_button.Sensitive = false;
+			else
+				add_button.Sensitive = true;
+
+		}
+
+		[GLib.ConnectBefore]
+		protected void HandleAddResponse (object sender, Gtk.ResponseArgs args)
+		{
+			if (args.ResponseId == Gtk.ResponseType.Ok) {
+				try {
+					Uri uri = new Uri (url);
+					if (uri.Scheme != Uri.UriSchemeHttp &&
+					    uri.Scheme != Uri.UriSchemeHttps)
+						throw new System.UriFormatException ();
+
+					//Check for name uniqueness
+					foreach (GalleryAccount acc in GalleryAccountManager.GetInstance ().GetAccounts ())
+						if (acc.Name == name)
+							throw new ArgumentException ("name");
+					GalleryAccount created = new GalleryAccount (name,
+										     url,
+										     username,
+										     password);
+
+					created.Connect ();
+					GalleryAccountManager.GetInstance ().AddAccount (created);
+					account = created;
+				} catch (System.UriFormatException) {
+					HigMessageDialog md =
+						new HigMessageDialog (add_dialog,
+								      Gtk.DialogFlags.Modal |
+								      Gtk.DialogFlags.DestroyWithParent,
+								      Gtk.MessageType.Error, Gtk.ButtonsType.Ok,
+								      Catalog.GetString ("Invalid URL"),
+								      Catalog.GetString ("The gallery URL entry does not appear to be a valid URL"));
+					md.Run ();
+					md.Destroy ();
+					return;
+				} catch (GalleryRemote.GalleryException e) {
+					HigMessageDialog md =
+						new HigMessageDialog (add_dialog,
+								      Gtk.DialogFlags.Modal |
+								      Gtk.DialogFlags.DestroyWithParent,
+								      Gtk.MessageType.Error, Gtk.ButtonsType.Ok,
+								      Catalog.GetString ("Error while connecting to Gallery"),
+								      String.Format (Catalog.GetString ("The following error was encountered while attempting to log in: {0}"), e.Message));
+					if (e.ResponseText != null) {
+						System.Console.WriteLine (e.Message);
+						System.Console.WriteLine (e.ResponseText);
+					}
+					md.Run ();
+					md.Destroy ();
+					return;
+				} catch (ArgumentException ae) {
+					HigMessageDialog md =
+						new HigMessageDialog (add_dialog,
+								      Gtk.DialogFlags.Modal |
+								      Gtk.DialogFlags.DestroyWithParent,
+								      Gtk.MessageType.Error, Gtk.ButtonsType.Ok,
+								      Catalog.GetString ("A Gallery with this name already exists"),
+								      String.Format (Catalog.GetString ("There is already a Gallery with the same name in your registered Galleries. Please choose a unique name.")));
+					System.Console.WriteLine (ae);
+					md.Run ();
+					md.Destroy ();
+					return;
+				} catch (System.Net.WebException we) {
+					HigMessageDialog md =
+						new HigMessageDialog (add_dialog,
+								      Gtk.DialogFlags.Modal |
+								      Gtk.DialogFlags.DestroyWithParent,
+								      Gtk.MessageType.Error, Gtk.ButtonsType.Ok,
+								      Catalog.GetString ("Error while connecting to Gallery"),
+								      String.Format (Catalog.GetString ("The following error was encountered while attempting to log in: {0}"), we.Message));
+					md.Run ();
+					md.Destroy ();
+					return;
+				} catch (System.Exception se) {
+					HigMessageDialog md =
+						new HigMessageDialog (add_dialog,
+								      Gtk.DialogFlags.Modal |
+								      Gtk.DialogFlags.DestroyWithParent,
+								      Gtk.MessageType.Error, Gtk.ButtonsType.Ok,
+								      Catalog.GetString ("Error while connecting to Gallery"),
+								      String.Format (Catalog.GetString ("The following error was encountered while attempting to log in: {0}"), se.Message));
+					Console.WriteLine (se);
+					md.Run ();
+					md.Destroy ();
+					return;
+				}
+			}
+			add_dialog.Destroy ();
+		}
+
+		protected void HandleEditResponse (object sender, Gtk.ResponseArgs args)
+		{
+			if (args.ResponseId == Gtk.ResponseType.Ok) {
+				account.Name = name;
+				account.Url = url;
+				account.Username = username;
+				account.Password = password;
+				GalleryAccountManager.GetInstance ().MarkChanged (true, account);
+			} else if (args.ResponseId == Gtk.ResponseType.Reject) {
+				// NOTE we are using Reject to signal the remove action.
+				GalleryAccountManager.GetInstance ().RemoveAccount (account);
+			}
+			add_dialog.Destroy ();
+		}
+
+		private GalleryAccount account;
+		private string name;
+		private string url;
+		private string password;
+		private string username;
+
+		// widgets
+		[Glade.Widget] Gtk.Dialog add_dialog;
+
+		[Glade.Widget] Gtk.Entry url_entry;
+		[Glade.Widget] Gtk.Entry password_entry;
+		[Glade.Widget] Gtk.Entry gallery_entry;
+		[Glade.Widget] Gtk.Entry username_entry;
+
+		[Glade.Widget] Gtk.Button add_button;
+		[Glade.Widget] Gtk.Button remove_button;
+		[Glade.Widget] Gtk.Button cancel_button;
+
+		[Glade.Widget] Gtk.HBox status_area;
+	}
+
+	public class GalleryAddAlbum
+	{
+		[Glade.Widget] Gtk.Dialog add_album_dialog;
+		[Glade.Widget] Gtk.OptionMenu album_optionmenu;
+
+		[Glade.Widget] Gtk.Entry name_entry;
+		[Glade.Widget] Gtk.Entry description_entry;
+		[Glade.Widget] Gtk.Entry title_entry;
+
+		[Glade.Widget] Gtk.Button add_button;
+		[Glade.Widget] Gtk.Button cancel_button;
+
+		private GalleryExport export;
+		private Gallery gallery;
+		private string parent;
+		private string name;
+		private string description;
+		private string title;
+
+		public GalleryAddAlbum (GalleryExport export, Gallery gallery)
+		{
+			Glade.XML xml = new Glade.XML (null, "GalleryExport.glade", "gallery_add_album_dialog", "f-spot");
+			xml.Autoconnect (this);
+			add_album_dialog = (Gtk.Dialog) xml.GetWidget ("gallery_add_album_dialog");
+			add_album_dialog.Modal = true;
+			this.export = export;
+			this.gallery = gallery;
+			PopulateAlbums ();
+
+			add_album_dialog.Response += HandleAddResponse;
+
+			name_entry.Changed += HandleChanged;
+			description_entry.Changed += HandleChanged;
+			title_entry.Changed += HandleChanged;
+			HandleChanged (null, null);
+		}
+
+		private void PopulateAlbums ()
+		{
+			Gtk.Menu menu = new Gtk.Menu ();
+			if (gallery.Version == GalleryVersion.Version1) {
+				Gtk.MenuItem top_item = new Gtk.MenuItem (Catalog.GetString ("(TopLevel)"));
+				menu.Append (top_item);
+			}
+
+			foreach (Album album in gallery.Albums) {
+				System.Text.StringBuilder label_builder = new System.Text.StringBuilder ();
+
+				for (int i=0; i < album.Parents.Count; i++) {
+					label_builder.Append ("  ");
+				}
+				label_builder.Append (album.Title);
+
+				Gtk.MenuItem item = new Gtk.MenuItem (label_builder.ToString ());
+				((Gtk.Label)item.Child).UseUnderline = false;
+				menu.Append (item);
+
+				AlbumPermission create_sub = album.Perms & AlbumPermission.CreateSubAlbum;
+
+				if (create_sub == 0)
+					item.Sensitive = false;
+			}
+
+			album_optionmenu.Sensitive = true;
+			menu.ShowAll ();
+			album_optionmenu.Menu = menu;
+		}
+
+		private void HandleChanged (object sender, EventArgs args)
+		{
+			if (gallery.Version == GalleryVersion.Version1) {
+				if (gallery.Albums.Count == 0 || album_optionmenu.History <= 0) {
+					parent = String.Empty;
+				} else {
+					parent = ((Album) gallery.Albums [album_optionmenu.History-1]).Name;
+				}
+			} else {
+				if (gallery.Albums.Count == 0 || album_optionmenu.History < 0) {
+					parent = String.Empty;
+				} else {
+					parent = ((Album) gallery.Albums [album_optionmenu.History]).Name;
+				}
+			}
+			name = name_entry.Text;
+			description = description_entry.Text;
+			title = title_entry.Text;
+
+			if (name == String.Empty || title == String.Empty)
+				add_button.Sensitive = false;
+			else
+				add_button.Sensitive = true;
+		}
+
+		[GLib.ConnectBefore]
+		protected void HandleAddResponse (object sender, Gtk.ResponseArgs args)
+		{
+			if (args.ResponseId == Gtk.ResponseType.Ok) {
+				if (!System.Text.RegularExpressions.Regex.IsMatch (name, "^[A-Za-z0-9_-]+$")) {
+					HigMessageDialog md =
+						new HigMessageDialog (add_album_dialog,
+								      Gtk.DialogFlags.Modal |
+								      Gtk.DialogFlags.DestroyWithParent,
+								      Gtk.MessageType.Error, Gtk.ButtonsType.Ok,
+								      Catalog.GetString ("Invalid Gallery name"),
+								      Catalog.GetString ("The gallery name contains invalid characters.\nOnly letters, numbers, - and _ are allowed"));
+					md.Run ();
+					md.Destroy ();
+					return;
+				}
+				try {
+					gallery.NewAlbum (parent, name, title, description);
+					export.HandleAlbumAdded (title);
+				} catch (GalleryCommandException e) {
+					gallery.PopupException(e, add_album_dialog);
+					return;
+				}
+			}
+			add_album_dialog.Destroy ();
+		}
+	}
+
+
+	public class GalleryExport : IExporter {
+		public GalleryExport ()
+		{
+		}
+
+		public void Run (IBrowsableCollection selection)
+		{
+			Glade.XML xml = new Glade.XML (null, "GalleryExport.glade", "gallery_export_dialog", "f-spot");
+			xml.Autoconnect (this);
+			export_dialog = (Gtk.Dialog) xml.GetWidget ("gallery_export_dialog");
+
+			this.items = selection.Items;
+			Array.Sort<IBrowsableItem> (this.items as Photo[], new Photo.CompareDateName());
+			album_button.Sensitive = false;
+			IconView view = new IconView (selection);
+			view.DisplayDates = false;
+			view.DisplayTags = false;
+
+			export_dialog.Modal = false;
+			export_dialog.TransientFor = null;
+
+			thumb_scrolledwindow.Add (view);
+			view.Show ();
+			export_dialog.Show ();
+
+			GalleryAccountManager manager = GalleryAccountManager.GetInstance ();
+			manager.AccountListChanged += PopulateGalleryOptionMenu;
+			PopulateGalleryOptionMenu (manager, null);
+
+			if (edit_button != null)
+				edit_button.Clicked += HandleEditGallery;
+
+			export_dialog.Response += HandleResponse;
+			connect = true;
+			HandleSizeActive (null, null);
+			Connect ();
+
+			LoadPreference (SCALE_KEY);
+			LoadPreference (SIZE_KEY);
+			LoadPreference (BROWSER_KEY);
+			LoadPreference (META_KEY);
+			LoadPreference (ROTATE_KEY);
+		}
+
+		public const string EXPORT_SERVICE = "gallery/";
+		public const string SCALE_KEY = Preferences.APP_FSPOT_EXPORT + EXPORT_SERVICE + "scale";
+		public const string SIZE_KEY = Preferences.APP_FSPOT_EXPORT + EXPORT_SERVICE + "size";
+		public const string BROWSER_KEY = Preferences.APP_FSPOT_EXPORT + EXPORT_SERVICE + "browser";
+		public const string META_KEY = Preferences.APP_FSPOT_EXPORT + EXPORT_SERVICE + "meta";
+		public const string ROTATE_KEY = Preferences.APP_FSPOT_EXPORT + EXPORT_SERVICE + "rotate";
+		public const string LIGHTTPD_WORKAROUND_KEY = Preferences.APP_FSPOT_EXPORT + EXPORT_SERVICE + "lighttpd_workaround";
+
+		private bool scale;
+		private bool rotate;
+		private int size;
+		private bool browser;
+		private bool meta;
+		private bool connect = false;
+
+		IBrowsableItem[] items;
+		int photo_index;
+		FSpot.ThreadProgressDialog progress_dialog;
+
+		ArrayList accounts;
+		private GalleryAccount account;
+		private Album album;
+
+		private string xml_path;
+
+		// Widgets
+		[Glade.Widget] Gtk.Dialog export_dialog;
+		[Glade.Widget] Gtk.OptionMenu gallery_optionmenu;
+		[Glade.Widget] Gtk.OptionMenu album_optionmenu;
+
+		[Glade.Widget] Gtk.Entry width_entry;
+		[Glade.Widget] Gtk.Entry height_entry;
+
+		[Glade.Widget] Gtk.CheckButton browser_check;
+		[Glade.Widget] Gtk.CheckButton scale_check;
+		[Glade.Widget] Gtk.CheckButton meta_check;
+		[Glade.Widget] Gtk.CheckButton rotate_check;
+
+		[Glade.Widget] Gtk.SpinButton size_spin;
+
+		[Glade.Widget] Gtk.Button album_button;
+		[Glade.Widget] Gtk.Button add_button;
+		[Glade.Widget] Gtk.Button edit_button;
+
+		[Glade.Widget] Gtk.Button export_button;
+		[Glade.Widget] Gtk.Button cancel_button;
+
+		[Glade.Widget] Gtk.ScrolledWindow thumb_scrolledwindow;
+
+		System.Threading.Thread command_thread;
+
+
+		private void HandleResponse (object sender, Gtk.ResponseArgs args)
+		{
+			if (args.ResponseId != Gtk.ResponseType.Ok) {
+				export_dialog.Destroy ();
+				return;
+			}
+
+			if (scale_check != null) {
+				scale = scale_check.Active;
+				size = size_spin.ValueAsInt;
+			} else
+				scale = false;
+
+			browser = browser_check.Active;
+			meta = meta_check.Active;
+			rotate = rotate_check.Active;
+
+			if (account != null) {
+				//System.Console.WriteLine ("history = {0}", album_optionmenu.History);
+				album = (Album) account.Gallery.Albums [Math.Max (0, album_optionmenu.History)];
+				photo_index = 0;
+
+				export_dialog.Destroy ();
+
+				command_thread = new System.Threading.Thread (new System.Threading.ThreadStart (this.Upload));
+				command_thread.Name = Catalog.GetString ("Uploading Pictures");
+
+				progress_dialog = new FSpot.ThreadProgressDialog (command_thread, items.Length);
+				progress_dialog.Start ();
+
+				// Save these settings for next time
+				Preferences.Set (SCALE_KEY, scale);
+				Preferences.Set (SIZE_KEY, size);
+				Preferences.Set (BROWSER_KEY, browser);
+				Preferences.Set (META_KEY, meta);
+				Preferences.Set (ROTATE_KEY, rotate);
+			}
+		}
+
+		private void HandleProgressChanged (ProgressItem item)
+		{
+			//System.Console.WriteLine ("Changed value = {0}", item.Value);
+			progress_dialog.Fraction = (photo_index - 1.0 + item.Value) / (double) items.Length;
+		}
+
+		public void HandleSizeActive (object sender, EventArgs args)
+		{
+			size_spin.Sensitive = scale_check.Active;
+		}
+
+
+		private void Upload ()
+		{
+				account.Gallery.Progress = new ProgressItem ();
+				account.Gallery.Progress.Changed += HandleProgressChanged;
+
+				System.Console.WriteLine ("Starting upload");
+
+				FilterSet filters = new FilterSet ();
+				if (account.Version == GalleryVersion.Version1)
+					filters.Add (new WhiteListFilter (new string []{".jpg", ".jpeg", ".png", ".gif"}));
+				if (scale)
+					filters.Add (new ResizeFilter ((uint) size));
+				else if (rotate)
+					filters.Add (new OrientationFilter ());
+
+
+				while (photo_index < items.Length) {
+					IBrowsableItem item = items [photo_index];
+
+					System.Console.WriteLine ("uploading {0}", photo_index);
+
+					progress_dialog.Message = System.String.Format (Catalog.GetString ("Uploading picture \"{0}\""), item.Name);
+					progress_dialog.Fraction = photo_index / (double) items.Length;
+					photo_index++;
+
+					progress_dialog.ProgressText = System.String.Format (Catalog.GetString ("{0} of {1}"), photo_index, items.Length);
+
+
+					FilterRequest req = new FilterRequest (item.DefaultVersionUri);
+
+					filters.Convert (req);
+					try {
+						int id = album.Add (item, req.Current.LocalPath);
+
+						if (item != null && item is Photo && Core.Database != null && id != 0) {
+							Core.Database.Exports.Create ((item as Photo).Id, (item as Photo).DefaultVersionId,
+										      ExportStore.Gallery2ExportType,
+										      String.Format("{0}:{1}",album.Gallery.Uri.ToString (), id.ToString ()));
+						}
+					} catch (System.Exception e) {
+						progress_dialog.Message = String.Format (Catalog.GetString ("Error uploading picture \"{0}\" to Gallery: {1}"), item.Name, e.Message);
+						progress_dialog.ProgressText = Catalog.GetString ("Error");
+						Console.WriteLine (e);
+
+						if (progress_dialog.PerformRetrySkip ()) {
+							photo_index--;
+						}
+					}
+			}
+
+			progress_dialog.Message = Catalog.GetString ("Done Sending Photos");
+			progress_dialog.Fraction = 1.0;
+			progress_dialog.ProgressText = Catalog.GetString ("Upload Complete");
+			progress_dialog.ButtonLabel = Gtk.Stock.Ok;
+
+			if (browser) {
+				GnomeUtil.UrlShow (album.GetUrl());
+			}
+		}
+
+		private void PopulateGalleryOptionMenu (GalleryAccountManager manager, GalleryAccount changed_account)
+		{
+			Gtk.Menu menu = new Gtk.Menu ();
+			this.account = changed_account;
+			int pos = -1;
+
+			accounts = manager.GetAccounts ();
+			if (accounts == null || accounts.Count == 0) {
+				Gtk.MenuItem item = new Gtk.MenuItem (Catalog.GetString ("(No Gallery)"));
+				menu.Append (item);
+				gallery_optionmenu.Sensitive = false;
+				edit_button.Sensitive = false;
+			} else {
+				int i = 0;
+				foreach (GalleryAccount account in accounts) {
+					if (account == changed_account)
+						pos = i;
+
+					Gtk.MenuItem item = new Gtk.MenuItem (account.Name);
+					menu.Append (item);
+					i++;
+				}
+				gallery_optionmenu.Sensitive = true;
+				edit_button.Sensitive = true;
+			}
+
+			menu.ShowAll ();
+			gallery_optionmenu.Menu = menu;
+			gallery_optionmenu.SetHistory ((uint)pos);
+		}
+
+		private void Connect ()
+		{
+			Connect (null);
+		}
+
+		private void Connect (GalleryAccount selected)
+		{
+			try {
+				if (accounts.Count != 0 && connect) {
+					if (selected == null)
+						account = (GalleryAccount) accounts [gallery_optionmenu.History];
+					else
+						account = selected;
+
+					if (!account.Connected)
+						account.Connect ();
+
+					PopulateAlbumOptionMenu (account.Gallery);
+					album_button.Sensitive = true;
+				}
+			} catch (System.Exception ex) {
+				if (selected != null)
+					account = selected;
+
+				System.Console.WriteLine ("{0}",ex);
+				PopulateAlbumOptionMenu (account.Gallery);
+				album_button.Sensitive = false;
+
+				new AccountDialog (export_dialog, account, true);
+			}
+		}
+
+		private void HandleAccountSelected (object sender, System.EventArgs args)
+		{
+			Connect ();
+		}
+
+		public void HandleAlbumAdded (string title) {
+			GalleryAccount account = (GalleryAccount) accounts [gallery_optionmenu.History];
+			PopulateAlbumOptionMenu (account.Gallery);
+
+			// make the newly created album selected
+			ArrayList albums = account.Gallery.Albums;
+			for (int i=0; i < albums.Count; i++) {
+				if (((Album)albums[i]).Title == title) {
+					album_optionmenu.SetHistory((uint)i);
+				}
+			}
+		}
+
+		private void PopulateAlbumOptionMenu (Gallery gallery)
+		{
+			System.Collections.ArrayList albums = null;
+			if (gallery != null) {
+				//gallery.FetchAlbumsPrune ();
+				try {
+					gallery.FetchAlbums ();
+					albums = gallery.Albums;
+				} catch (GalleryCommandException e) {
+					gallery.PopupException (e, export_dialog);
+					return;
+				}
+			}
+
+			Gtk.Menu menu = new Gtk.Menu ();
+
+			bool disconnected = gallery == null || !account.Connected || albums == null;
+
+			if (disconnected || albums.Count == 0) {
+				string msg = disconnected ? Catalog.GetString ("(Not Connected)")
+					: Catalog.GetString ("(No Albums)");
+
+				Gtk.MenuItem item = new Gtk.MenuItem (msg);
+				menu.Append (item);
+
+				export_button.Sensitive = false;
+				album_optionmenu.Sensitive = false;
+				album_button.Sensitive = false;
+
+				if (disconnected)
+					album_button.Sensitive = false;
+			} else {
+				foreach (Album album in albums) {
+					System.Text.StringBuilder label_builder = new System.Text.StringBuilder ();
+
+					for (int i=0; i < album.Parents.Count; i++) {
+						label_builder.Append ("  ");
+					}
+					label_builder.Append (album.Title);
+
+					Gtk.MenuItem item = new Gtk.MenuItem (label_builder.ToString ());
+					((Gtk.Label)item.Child).UseUnderline = false;
+					menu.Append (item);
+
+				        AlbumPermission add_permission = album.Perms & AlbumPermission.Add;
+
+					if (add_permission == 0)
+						item.Sensitive = false;
+				}
+
+				export_button.Sensitive = items.Length > 0;
+				album_optionmenu.Sensitive = true;
+				album_button.Sensitive = true;
+			}
+
+			menu.ShowAll ();
+			album_optionmenu.Menu = menu;
+		}
+
+		public void HandleAddGallery (object sender, System.EventArgs args)
+		{
+			new AccountDialog (export_dialog);
+		}
+
+		public void HandleEditGallery (object sender, System.EventArgs args)
+		{
+			new AccountDialog (export_dialog, account, false);
+		}
+
+		public void HandleAddAlbum (object sender, System.EventArgs args)
+		{
+			if (account == null)
+				throw new GalleryException (Catalog.GetString ("No account selected"));
+
+			new GalleryAddAlbum (this, account.Gallery);
+		}
+
+		void LoadPreference (string key)
+		{
+			object val = Preferences.Get (key);
+
+			if (val == null)
+				return;
+
+			//System.Console.WriteLine ("Setting {0} to {1}", key, val);
+
+			switch (key) {
+			case SCALE_KEY:
+				if (scale_check.Active != (bool) val)
+					scale_check.Active = (bool) val;
+				break;
+
+			case SIZE_KEY:
+				size_spin.Value = (double) (int) val;
+				break;
+
+			case BROWSER_KEY:
+				if (browser_check.Active != (bool) val)
+					browser_check.Active = (bool) val;
+				break;
+
+			case META_KEY:
+				if (meta_check.Active != (bool) val)
+					meta_check.Active = (bool) val;
+				break;
+			case ROTATE_KEY:
+				if (rotate_check.Active != (bool) val)
+					rotate_check.Active = (bool) val;
+				break;
+			}
+		}
+	}
+}

Copied: trunk/extensions/Exporters/GalleryExport/GalleryExport.glade (from r4368, /trunk/extensions/GalleryExport/GalleryExport.glade)
==============================================================================

Added: trunk/extensions/Exporters/GalleryExport/GalleryRemote.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/GalleryExport/GalleryRemote.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,1170 @@
+using System;
+using System.Net;
+using System.IO;
+using System.Text;
+using System.Collections;
+using System.Collections.Specialized;
+using System.Web;
+using Mono.Unix;
+using FSpot;
+using FSpot.UI.Dialog;
+
+/* These classes are based off the documentation at
+ *
+ * http://codex.gallery2.org/index.php/Gallery_Remote:Protocol
+ */
+
+namespace GalleryRemote {
+	public enum AlbumPermission : byte
+	{
+		None = 0,
+		Add = 1,
+		Write = 2,
+		Delete = 4,
+		DeleteAlbum = 8,
+		CreateSubAlbum = 16
+	}
+
+	public class Album : IComparable {
+		public int RefNum;
+		public string Name = null;
+		public string Title = null;
+		public string Summary = null;
+		public int ParentRefNum;
+		public int ResizeSize;
+		public int ThumbSize;
+		public ArrayList Images = null;
+		public string BaseURL;
+
+		Gallery gallery;
+
+		public AlbumPermission Perms = AlbumPermission.None;
+
+		public Album Parent {
+			get {
+				if (ParentRefNum != 0)
+					return gallery.LookupAlbum (ParentRefNum);
+				else
+					return null;
+			}
+		}
+
+		protected ArrayList parents = null;
+		public ArrayList Parents {
+			get {
+				if (parents != null)
+					return parents;
+
+				if (Parent == null) {
+				       parents = new ArrayList ();
+				} else {
+					parents = Parent.Parents.Clone () as ArrayList;
+					parents.Add (Parent.RefNum);
+				}
+
+				return parents;
+			}
+		}
+
+		public Gallery Gallery {
+			get { return gallery; }
+		}
+
+		public Album (Gallery gallery, string name, int ref_num)
+		{
+			Name = name;
+			this.gallery = gallery;
+			this.RefNum = ref_num;
+			Images = new ArrayList ();
+		}
+
+		public void Rename (string name)
+		{
+			gallery.MoveAlbum (this, name);
+		}
+
+		public void Add (FSpot.IBrowsableItem item)
+		{
+			Add (item, item.DefaultVersionUri.LocalPath);
+		}
+
+		public int Add (FSpot.IBrowsableItem item, string path)
+		{
+			if (item == null)
+				Console.WriteLine ("NO PHOTO");
+
+			return gallery.AddItem (this,
+					 path,
+					 Path.GetFileName (item.DefaultVersionUri.LocalPath),
+					 item.Name,
+					 item.Description,
+					 true);
+		}
+
+		public string GetUrl ()
+		{
+			return gallery.GetAlbumUrl(this);
+		}
+
+		public int CompareTo (Object obj)
+		{
+			Album other = obj as Album;
+
+			int numThis = this.Parents.Count;
+			int numOther = other.Parents.Count;
+			int thisVal = -1, otherVal = -1;
+
+			//find where they first differ
+			int maxIters = Math.Min (numThis, numOther);
+			int i = 0;
+			while (i < maxIters) {
+				thisVal = (int)this.Parents[i];
+				otherVal = (int)other.Parents[i];
+				if (thisVal != otherVal) {
+					break;
+				}
+				i++;
+			}
+
+			int retVal;
+			if (i < numThis && i < numOther) {
+				//Parentage differed
+				retVal = thisVal.CompareTo (otherVal);
+
+			} else if (i < numThis) {
+				//other shorter
+				thisVal = (int)this.Parents[i];
+				retVal = thisVal.CompareTo (other.RefNum);
+
+				//if equal, we want to make the shorter one come first
+				if (retVal == 0)
+					retVal = 1;
+
+			} else if (i < numOther) {
+				//this shorter
+				otherVal = (int)other.Parents[i];
+				retVal = this.RefNum.CompareTo (otherVal);
+
+				//if equal, we want to make the shorter one come first
+				if (retVal == 0)
+					retVal = -1;
+
+			} else {
+				//children of the same parent
+				retVal = this.RefNum.CompareTo (other.RefNum);
+			}
+
+			return retVal;
+		}
+	}
+
+	public class Image {
+		public string Name;
+		public int RawWidth;
+		public int RawHeight;
+		public string ResizedName;
+		public int ResizedWidth;
+		public int ResizedHeight;
+		public string ThumbName;
+		public int ThumbWidth;
+		public int ThumbHeight;
+		public int RawFilesize;
+
+		public string Caption;
+		public string Description;
+		public int Clicks;
+
+		public Album Owner;
+
+		public string Url;
+
+		public Image (Album album, string name) {
+			Name = name;
+			Owner = album;
+		}
+	}
+
+	public enum ResultCode {
+		Success = 0,
+		MajorVersionInvalid = 101,
+		MajorMinorVersionInvalid = 102,
+		VersionFormatInvalid = 103,
+		VersionMissing = 104,
+		PasswordWrong = 201,
+		LoginMissing = 202,
+		UnknownComand = 301,
+		NoAddPermission = 401,
+		NoFilename = 402,
+		UploadPhotoFailed = 403,
+		NoWritePermission = 404,
+		NoCreateAlbumPermission = 501,
+		CreateAlbumFailed = 502,
+		// This result is specific to this implementation
+		UnknownResponse = 1000
+	}
+
+	public class GalleryException : System.Exception {
+		string response_text;
+
+		public string ResponseText {
+			get { return response_text; }
+		}
+
+		public GalleryException (string text) : base (text)
+		{
+		}
+
+		public GalleryException (string text, string full_response) : base (text)
+		{
+			response_text = full_response;
+		}
+	}
+
+	public class GalleryCommandException : GalleryException {
+		ResultCode status;
+
+		public GalleryCommandException (string status_text, ResultCode result) : base (status_text) {
+			status = result;
+		}
+
+		public ResultCode Status {
+			get {
+				return status;
+			}
+		}
+	}
+
+	public abstract class Gallery
+	{
+		protected Uri uri;
+		public Uri Uri{
+			get {
+				return uri;
+			}
+		}
+
+
+		protected string name;
+		public string Name {
+			get {
+				return name;
+			}
+			set {
+				name = value;
+			}
+		}
+
+		string auth_token;
+		public string AuthToken {
+			get {
+				return auth_token;
+			}
+			set {
+				auth_token = value;
+			}
+		}
+
+		protected GalleryVersion version;
+
+		public GalleryVersion Version {
+			get {
+				return version;
+			}
+		}
+
+		protected ArrayList albums;
+		public ArrayList Albums{
+			get {
+				return albums;
+			}
+		}
+
+		public bool expect_continue = true;
+
+		protected CookieContainer cookies = null;
+		public FSpot.ProgressItem Progress = null;
+
+		public abstract void Login (string username, string passwd);
+		public abstract ArrayList FetchAlbums ();
+		public abstract ArrayList FetchAlbumsPrune ();
+		public abstract bool MoveAlbum (Album album, string end_name);
+		public abstract int AddItem (Album album, string path, string filename, string caption, string description, bool autorotate);
+		//public abstract Album AlbumProperties (string album);
+		public abstract bool NewAlbum (string parent_name, string name, string title, string description);
+		public abstract ArrayList FetchAlbumImages (Album album, bool include_ablums);
+
+		public abstract string GetAlbumUrl (Album album);
+
+		public Gallery (string name)
+		{
+			this.name = name;
+			cookies = new CookieContainer ();
+			albums = new ArrayList ();
+		}
+
+		public static GalleryVersion DetectGalleryVersion (string url)
+		{
+			//Figure out if the url is for G1 or G2
+			Console.WriteLine ("Detecting Gallery version");
+
+			GalleryVersion version;
+
+			if (url.EndsWith (Gallery1.script_name)) {
+				version = GalleryVersion.Version1;
+			} else if (url.EndsWith (Gallery2.script_name)) {
+				version = GalleryVersion.Version2;
+			} else {
+				//check what script is available on the server
+
+				FormClient client = new FormClient ();
+
+				try {
+					client.Submit (new Uri (Gallery.FixUrl (url, Gallery1.script_name)));
+					version =  GalleryVersion.Version1;
+
+				} catch (System.Net.WebException) {
+					try {
+						client.Submit (new Uri (Gallery.FixUrl (url, Gallery2.script_name)));
+						version =  GalleryVersion.Version2;
+
+					} catch (System.Net.WebException) {
+						//Uh oh, neither version detected
+						version = GalleryVersion.VersionUnknown;
+					}
+				}
+			}
+
+			Console.WriteLine ("Detected: " + version.ToString());
+			return version;
+		}
+
+
+		public bool IsConnected ()
+		{
+
+			bool retVal = true;
+			//Console.WriteLine ("^^^^^^^Checking IsConnected");
+			foreach (Cookie cookie in cookies.GetCookies(Uri)) {
+				bool isExpired = cookie.Expired;
+				//Console.WriteLine (cookie.Name + " " + (isExpired ? "expired" : "valid"));
+				if (isExpired)
+					retVal = false;
+			}
+			//return cookies.GetCookies(Uri).Count > 0;
+			return retVal;
+		}
+		//Reads until it finds the start of the response
+		protected StreamReader findResponse (HttpWebResponse response)
+		{
+			StreamReader reader = new StreamReader (response.GetResponseStream (), Encoding.UTF8);
+			if (reader == null)
+				throw new GalleryException (Catalog.GetString ("Error reading server response"));
+
+			string line;
+			string full_response = null;
+			while ((line = reader.ReadLine ()) != null) {
+				full_response += line;
+				if (line.IndexOf ("#__GR2PROTO__", 0) > -1)
+					break;
+			}
+
+			if (line == null) {
+				// failed to find the response
+				throw new GalleryException (Catalog.GetString ("Server returned response without Gallery content"), full_response);
+			}
+
+			return reader;
+
+		}
+
+		protected string [] GetNextLine (StreamReader reader)
+		{
+			char [] value_split = new char [1] {'='};
+			bool haveLine = false;
+			string[] array = null;
+			while(!haveLine) {
+				string line = reader.ReadLine ();
+				//Console.WriteLine ("READING: " + line);
+				if (line != null) {
+					array = line.Split (value_split, 2);
+					haveLine = !LineIgnored (array);
+				}
+				else {
+					//end of input
+					return null;
+				}
+			}
+
+			return array;
+		}
+
+		private bool LineIgnored (string[] line)
+		{
+			if (line[0].StartsWith ("debug")) {
+				return true;
+			} else if (line[0].StartsWith ("can_create_root")) {
+				return true;
+			} else {
+				return false;
+			}
+		}
+
+		protected bool ParseLogin (HttpWebResponse response)
+		{
+			string [] data;
+			StreamReader reader = null;
+			ResultCode status = ResultCode.UnknownResponse;
+			string status_text = "Error: Unable to parse server response";
+			try {
+
+				reader = findResponse (response);
+				while ((data = GetNextLine (reader)) != null) {
+					if (data[0] == "status") {
+						status = (ResultCode) int.Parse (data [1]);
+					} else if (data[0].StartsWith ("status_text")) {
+						status_text = data[1];
+						Console.WriteLine ("StatusText : {0}", data[1]);
+					} else if (data[0].StartsWith ("server_version")) {
+						//FIXME we should use the to determine what capabilities the server has
+					} else if (data[0].StartsWith ("auth_token")) {
+						AuthToken = data[1];
+					} else {
+						Console.WriteLine ("Unparsed Line in ParseLogin(): {0}={1}", data[0], data[1]);
+					}
+				}
+				//Console.WriteLine ("Found: {0} cookies", response.Cookies.Count);
+				if (status != ResultCode.Success) {
+					Console.WriteLine (status_text);
+					throw new GalleryCommandException (status_text, status);
+				}
+
+				return true;
+			} finally {
+				if (reader != null)
+					reader.Close ();
+
+				response.Close ();
+			}
+		}
+
+		public ArrayList ParseFetchAlbums (HttpWebResponse response)
+		{
+			//Console.WriteLine ("in ParseFetchAlbums()");
+			string [] data;
+			StreamReader reader = null;
+			ResultCode status = ResultCode.UnknownResponse;
+			string status_text = "Error: Unable to parse server response";
+			albums = new ArrayList ();
+			try {
+
+				Album current_album = null;
+				reader = findResponse (response);
+				while ((data = GetNextLine (reader)) != null) {
+					//Console.WriteLine ("Parsing Line: {0}={1}", data[0], data[1]);
+					if (data[0] == "status") {
+						status = (ResultCode) int.Parse (data [1]);
+					} else if (data[0].StartsWith ("status_text")) {
+						status_text = data[1];
+						Console.WriteLine ("StatusText : {0}", data[1]);
+					} else if (data[0].StartsWith ("album.name")) {
+						//this is the URL name
+						int ref_num = -1;
+						if (this.Version == GalleryVersion.Version1) {
+							string [] segments = data[0].Split (new char[1]{'.'});
+							ref_num = int.Parse (segments[segments.Length -1]);
+						} else {
+							ref_num = int.Parse (data[1]);
+						}
+						current_album = new Album (this, data[1], ref_num);
+						albums.Add (current_album);
+						//Console.WriteLine ("current_album: " + data[1]);
+					} else if (data[0].StartsWith ("album.title")) {
+						//this is the display name
+						current_album.Title = data[1];
+					} else if (data[0].StartsWith ("album.summary")) {
+						current_album.Summary = data[1];
+					} else if (data[0].StartsWith ("album.parent")) {
+						//FetchAlbums and G2 FetchAlbumsPrune return ints
+						//G1 FetchAlbumsPrune returns album names (and 0 for root albums)
+						try {
+							current_album.ParentRefNum = int.Parse (data[1]);
+						} catch (System.FormatException) {
+							current_album.ParentRefNum = LookupAlbum (data[1]).RefNum;
+						}
+						//Console.WriteLine ("album.parent data[1]: " + data[1]);
+					} else if (data[0].StartsWith ("album.resize_size")) {
+						current_album.ResizeSize = int.Parse (data[1]);
+					} else if (data[0].StartsWith ("album.thumb_size")) {
+						current_album.ThumbSize = int.Parse (data[1]);
+					} else if (data[0].StartsWith ("album.info.extrafields")) {
+						//ignore, this is the album description
+					} else if (data[0].StartsWith ("album.perms.add")) {
+						if (data[1] == "true")
+							current_album.Perms |= AlbumPermission.Add;
+					} else if (data[0].StartsWith ("album.perms.write")) {
+						if (data[1] == "true")
+							current_album.Perms |= AlbumPermission.Write;
+					} else if (data[0].StartsWith ("album.perms.del_item")) {
+						if (data[1] == "true")
+							current_album.Perms |= AlbumPermission.Delete;
+					} else if (data[0].StartsWith ("album.perms.del_alb")) {
+						if (data[1] == "true")
+							current_album.Perms |= AlbumPermission.DeleteAlbum;
+					} else if (data[0].StartsWith ("album.perms.create_sub")) {
+						if (data[1] == "true")
+							current_album.Perms |= AlbumPermission.CreateSubAlbum;
+					} else if (data[0].StartsWith ("album_count")) {
+						if (Albums.Count != int.Parse (data[1]))
+							Console.WriteLine ("Parsed album count does not match album_count.  Something is amiss");
+					} else if (data[0].StartsWith ("auth_token")) {
+						AuthToken = data [1];
+					} else {
+						Console.WriteLine ("Unparsed Line in ParseFetchAlbums(): {0}={1}", data[0], data[1]);
+					}
+				}
+				//Console.WriteLine ("Found: {0} cookies", response.Cookies.Count);
+				if (status != ResultCode.Success) {
+					Console.WriteLine (status_text);
+					throw new GalleryCommandException (status_text, status);
+				}
+
+				//Console.WriteLine (After parse albums.Count + " albums parsed");
+				return albums;
+			} finally {
+				if (reader != null)
+					reader.Close ();
+
+				response.Close ();
+			}
+		}
+
+		public int ParseAddItem (HttpWebResponse response)
+		{
+			string [] data;
+			StreamReader reader = null;
+			ResultCode status = ResultCode.UnknownResponse;
+			string status_text = "Error: Unable to parse server response";
+			int item_id = 0;
+			try {
+
+				reader = findResponse (response);
+				while ((data = GetNextLine (reader)) != null) {
+					if (data[0] == "status") {
+						status = (ResultCode) int.Parse (data [1]);
+					} else if (data[0].StartsWith ("status_text")) {
+						status_text = data[1];
+						Console.WriteLine ("StatusText : {0}", data[1]);
+					} else if (data[0].StartsWith ("auth_token")) {
+						AuthToken = data[1];
+					} else if (data[0].StartsWith ("item_name")) {
+						item_id = int.Parse (data [1]);
+					} else {
+						Console.WriteLine ("Unparsed Line in ParseAddItem(): {0}={1}", data[0], data[1]);
+					}
+				}
+				//Console.WriteLine ("Found: {0} cookies", response.Cookies.Count);
+				if (status != ResultCode.Success) {
+					Console.WriteLine (status_text);
+					throw new GalleryCommandException (status_text, status);
+				}
+
+				return item_id;
+			} finally {
+				if (reader != null)
+					reader.Close ();
+
+				response.Close ();
+			}
+		}
+
+		public bool ParseNewAlbum (HttpWebResponse response)
+		{
+			return ParseBasic (response);
+		}
+
+		public bool ParseMoveAlbum (HttpWebResponse response)
+		{
+			return ParseBasic (response);
+		}
+		/*
+		public Album ParseAlbumProperties (HttpWebResponse response)
+		{
+			string [] data;
+			StreamReader reader = null;
+			ResultCode status = ResultCode.UnknownResponse;
+			string status_text = "Error: Unable to parse server response";
+			try {
+
+				reader = findResponse (response);
+				while ((data = GetNextLine (reader)) != null) {
+					if (data[0] == "status") {
+						status = (ResultCode) int.Parse (data [1]);
+					} else if (data[0].StartsWith ("status_text")) {
+						status_text = data[1];
+						Console.WriteLine ("StatusText : {0}", data[1]);
+					} else if (data[0].StartsWith ("auto-resize")) {
+						//ignore
+					} else {
+						Console.WriteLine ("Unparsed Line in ParseBasic(): {0}={1}", data[0], data[1]);
+					}
+				}
+				//Console.WriteLine ("Found: {0} cookies", response.Cookies.Count);
+				if (status != ResultCode.Success) {
+					Console.WriteLine (status_text);
+					throw new GalleryCommandException (status_text, status);
+				}
+
+				return true;
+			} finally {
+				if (reader != null)
+					reader.Close ();
+
+				response.Close ();
+			}
+		}
+		*/
+
+		private bool ParseBasic (HttpWebResponse response)
+		{
+			string [] data;
+			StreamReader reader = null;
+			ResultCode status = ResultCode.UnknownResponse;
+			string status_text = "Error: Unable to parse server response";
+			try {
+
+				reader = findResponse (response);
+				while ((data = GetNextLine (reader)) != null) {
+					if (data[0] == "status") {
+						status = (ResultCode) int.Parse (data [1]);
+					} else if (data[0].StartsWith ("status_text")) {
+						status_text = data[1];
+						Console.WriteLine ("StatusText : {0}", data[1]);
+					} else if (data[0].StartsWith ("auth_token")) {
+						AuthToken = data[1];
+					} else {
+						Console.WriteLine ("Unparsed Line in ParseBasic(): {0}={1}", data[0], data[1]);
+					}
+				}
+				//Console.WriteLine ("Found: {0} cookies", response.Cookies.Count);
+				if (status != ResultCode.Success) {
+					Console.WriteLine (status_text + " Status: " + status);
+					throw new GalleryCommandException (status_text, status);
+				}
+
+				return true;
+			} finally {
+				if (reader != null)
+					reader.Close ();
+
+				response.Close ();
+			}
+		}
+
+		public Album LookupAlbum (string name)
+		{
+			Album match = null;
+
+			foreach (Album album in albums) {
+				if (album.Name == name) {
+					match = album;
+					break;
+				}
+			}
+			return match;
+		}
+
+		public Album LookupAlbum (int ref_num)
+		{
+			// FIXME this is really not the best way to do this
+			Album match = null;
+
+			foreach (Album album in albums) {
+				if (album.RefNum == ref_num) {
+					match = album;
+					break;
+				}
+			}
+			return match;
+		}
+
+		public static string FixUrl(string url, string end)
+		{
+			string fixedUrl = url;
+			if (!url.EndsWith (end)) {
+				if (!url.EndsWith ("/"))
+					fixedUrl = url + "/";
+				fixedUrl = fixedUrl + end;
+			}
+			return fixedUrl;
+
+		}
+
+		public void PopupException (GalleryCommandException e, Gtk.Dialog d)
+		{
+			System.Console.WriteLine(String.Format ("{0} : {1} ({2})", e.Message, e.ResponseText, e.Status));
+			HigMessageDialog md =
+				new HigMessageDialog (d,
+						      Gtk.DialogFlags.Modal |
+						      Gtk.DialogFlags.DestroyWithParent,
+						      Gtk.MessageType.Error, Gtk.ButtonsType.Ok,
+						      Catalog.GetString ("Error while creating new album"),
+						      String.Format (Catalog.GetString ("The following error was encountered while attempting to perform the requested operation:\n{0} ({1})"), e.Message, e.Status));
+			md.Run ();
+			md.Destroy ();
+		}
+
+	}
+
+	public class Gallery1 : Gallery
+	{
+		public const string script_name = "gallery_remote2.php";
+		public Gallery1 (string url) : this (url, url) {}
+		public Gallery1 (string name, string url) : base (name)
+		{
+			this.uri = new Uri (FixUrl (url, script_name));
+			version = GalleryVersion.Version1;
+		}
+
+		public override void Login (string username, string passwd)
+		{
+			//Console.WriteLine ("Gallery1: Attempting to login");
+			FormClient client = new FormClient (cookies);
+
+			client.Add ("cmd", "login");
+			client.Add ("protocol_version", "2.3");
+			client.Add ("uname", username);
+			client.Add ("password", passwd);
+
+			ParseLogin (client.Submit (uri));
+		}
+
+		public override ArrayList FetchAlbums ()
+		{
+			FormClient client = new FormClient (cookies);
+
+			client.Add ("cmd", "fetch-albums");
+			client.Add ("protocol_version", "2.3");
+
+			return ParseFetchAlbums (client.Submit (uri));
+		}
+
+
+		public override bool MoveAlbum (Album album, string end_name)
+		{
+			FormClient client = new FormClient (cookies);
+
+			client.Add ("cmd", "move-album");
+			client.Add ("protocol_version", "2.7");
+			client.Add ("set_albumName", album.Name);
+			client.Add ("set_destalbumName", end_name);
+
+			return ParseMoveAlbum (client.Submit (uri));
+		}
+
+		public override int AddItem (Album album,
+				     string path,
+				     string filename,
+				     string caption,
+				     string description,
+				     bool autorotate)
+		{
+			FormClient client = new FormClient (cookies);
+
+			client.Add ("cmd", "add-item");
+			client.Add ("protocol_version", "2.9");
+			client.Add ("set_albumName", album.Name);
+			client.Add ("caption", caption);
+			client.Add ("userfile_name", filename);
+			client.Add ("force_filename", filename);
+			client.Add ("auto_rotate", autorotate ? "yes" : "no");
+			client.Add ("userfile", new FileInfo (path));
+			client.Add ("extrafield.Description", description);
+			client.expect_continue = expect_continue;
+
+			return ParseAddItem (client.Submit (uri, Progress));
+		}
+
+		/*
+		public override Album AlbumProperties (string album)
+		{
+			FormClient client = new FormClient (cookies);
+			client.Add ("cmd", "album-properties");
+			client.Add ("protocol_version", "2.3");
+			client.Add ("set_albumName", album);
+
+			return ParseAlbumProperties (client.Submit (uri));
+		}
+		*/
+
+		public override bool NewAlbum (string parent_name,
+				      string name,
+				      string title,
+				      string description)
+		{
+			FormClient client = new FormClient (cookies);
+			client.Multipart = true;
+			client.Add ("cmd", "new-album");
+			client.Add ("protocol_version", "2.8");
+			client.Add ("set_albumName", parent_name);
+			client.Add ("newAlbumName", name);
+			client.Add ("newAlbumTitle", title);
+			client.Add ("newAlbumDesc", description);
+
+			return ParseNewAlbum (client.Submit (uri));
+		}
+
+		public override ArrayList FetchAlbumImages (Album album, bool include_ablums)
+		{
+			FormClient client = new FormClient (cookies);
+			client.Add ("cmd", "fetch-album-images");
+			client.Add ("protocol_version","2.3");
+			client.Add ("set_albumName", album.Name);
+			client.Add ("albums_too", include_ablums ? "yes" : "no");
+
+			album.Images = ParseFetchAlbumImages (client.Submit (uri), album);
+			return album.Images;
+		}
+
+		public override ArrayList FetchAlbumsPrune ()
+		{
+			FormClient client = new FormClient (cookies);
+			client.Add ("cmd", "fetch-albums-prune");
+			client.Add ("protocol_version", "2.3");
+			client.Add ("check_writable", "no");
+			ArrayList a = ParseFetchAlbums (client.Submit (uri));
+			a.Sort();
+			return a;
+		}
+
+		public ArrayList ParseFetchAlbumImages (HttpWebResponse response, Album album)
+		{
+			string [] data;
+			StreamReader reader = null;
+			ResultCode status = ResultCode.UnknownResponse;
+			string status_text = "Error: Unable to parse server response";
+			try {
+				Image current_image = null;
+				reader = findResponse (response);
+				while ((data = GetNextLine (reader)) != null) {
+					if (data[0] == "status") {
+						status = (ResultCode) int.Parse (data [1]);
+					} else if (data[0].StartsWith ("status_text")) {
+						status_text = data[1];
+						Console.WriteLine ("StatusText : {0}", data[1]);
+					} else if (data[0].StartsWith ("image.name")) {
+						current_image = new Image (album, data[1]);
+						album.Images.Add (current_image);
+					} else if (data[0].StartsWith ("image.raw_width")) {
+						current_image.RawWidth = int.Parse (data[1]);
+					} else if (data[0].StartsWith ("image.raw_height")) {
+						current_image.RawHeight = int.Parse (data[1]);
+					} else if (data[0].StartsWith ("image.raw_height")) {
+						current_image.RawHeight = int.Parse (data[1]);
+					} else if (data[0].StartsWith ("image.raw_filesize")) {
+					} else if (data[0].StartsWith ("image.capturedate.year")) {
+					} else if (data[0].StartsWith ("image.capturedate.mon")) {
+					} else if (data[0].StartsWith ("image.capturedate.mday")) {
+					} else if (data[0].StartsWith ("image.capturedate.hours")) {
+					} else if (data[0].StartsWith ("image.capturedate.minutes")) {
+					} else if (data[0].StartsWith ("image.capturedate.seconds")) {
+					} else if (data[0].StartsWith ("image.hidden")) {
+					} else if (data[0].StartsWith ("image.resizedName")) {
+						current_image.ResizedName = data[1];
+					} else if (data[0].StartsWith ("image.resized_width")) {
+						current_image.ResizedWidth = int.Parse (data[1]);
+					} else if (data[0].StartsWith ("image.resized_height")) {
+						current_image.ResizedHeight = int.Parse (data[1]);
+					} else if (data[0].StartsWith ("image.thumbName")) {
+						current_image.ThumbName = data[1];
+					} else if (data[0].StartsWith ("image.thumb_width")) {
+						current_image.ThumbWidth = int.Parse (data[1]);
+					} else if (data[0].StartsWith ("image.thumb_height")) {
+						current_image.ThumbHeight = int.Parse (data[1]);
+					} else if (data[0].StartsWith ("image.caption")) {
+						current_image.Caption = data[1];
+					} else if (data[0].StartsWith ("image.extrafield.Description")) {
+						current_image.Description = data[1];
+					} else if (data[0].StartsWith ("image.clicks")) {
+						try {
+							current_image.Clicks = int.Parse (data[1]);
+						} catch (System.FormatException) {
+							current_image.Clicks = 0;
+						}
+					} else if (data[0].StartsWith ("baseurl")) {
+						album.BaseURL = data[1];
+					} else if (data[0].StartsWith ("image_count")) {
+						if (album.Images.Count != int.Parse (data[1]))
+							Console.WriteLine ("Parsed image count for " + album.Name + "(" + album.Images.Count + ") does not match image_count (" + data[1] + ").  Something is amiss");
+					} else {
+						Console.WriteLine ("Unparsed Line in ParseFetchAlbumImages(): {0}={1}", data[0], data[1]);
+					}
+				}
+				//Console.WriteLine ("Found: {0} cookies", response.Cookies.Count);
+				if (status != ResultCode.Success) {
+					Console.WriteLine (status_text);
+					throw new GalleryCommandException (status_text, status);
+				}
+
+
+				//Set the Urls for downloading the images.
+				string baseUrl = album.BaseURL + "/";
+				foreach (Image image in album.Images) {
+					image.Url = baseUrl + image.Name;
+				}
+
+				return album.Images;
+			} finally {
+				if (reader != null)
+					reader.Close ();
+
+				response.Close ();
+			}
+		}
+
+		public override string GetAlbumUrl (Album album)
+		{
+			string url = Uri.ToString();
+			url = url.Remove (url.Length - script_name.Length, script_name.Length);
+
+			string path = album.Name;
+
+			url = url + path;
+			url = url.Replace (" ", "+");
+			return url;
+		}
+
+
+	}
+	public class Gallery2 : Gallery
+	{
+		public const string script_name = "main.php";
+
+		public Gallery2 (string url) : this (url, url) {}
+		public Gallery2 (string name, string url) : base (name)
+		{
+			this.uri = new Uri (FixUrl (url, script_name));
+			version = GalleryVersion.Version2;
+		}
+
+		public override void Login (string username, string passwd)
+		{
+			Console.WriteLine ("Gallery2: Attempting to login");
+			FormClient client = new FormClient (cookies);
+
+			client.Add ("g2_form[cmd]", "login");
+			client.Add ("g2_form[protocol_version]", "2.10");
+			client.Add ("g2_form[uname]", username);
+			client.Add ("g2_form[password]", passwd);
+			AddG2Specific (client);
+
+			ParseLogin (client.Submit (uri));
+		}
+
+		public override ArrayList FetchAlbums ()
+		{
+			//FetchAlbums doesn't exist for G2, we have to use FetchAlbumsPrune()
+			return FetchAlbumsPrune ();
+		}
+
+
+		public override bool MoveAlbum (Album album, string end_name)
+		{
+			FormClient client = new FormClient (cookies);
+
+			client.Add ("g2_form[cmd]", "move-album");
+			client.Add ("g2_form[protocol_version]", "2.10");
+			client.Add ("g2_form[set_albumName]", album.Name);
+			client.Add ("g2_form[set_destalbumName]", end_name);
+			AddG2Specific (client);
+
+			return ParseMoveAlbum (client.Submit (uri));
+		}
+
+		public override int AddItem (Album album,
+				     string path,
+				     string filename,
+				     string caption,
+				     string description,
+				     bool autorotate)
+		{
+			FormClient client = new FormClient (cookies);
+
+			client.Add ("g2_form[cmd]", "add-item");
+			client.Add ("g2_form[protocol_version]", "2.10");
+			client.Add ("g2_form[set_albumName]", album.Name);
+			client.Add ("g2_form[caption]", caption);
+			client.Add ("g2_form[userfile_name]", filename);
+			client.Add ("g2_form[force_filename]", filename);
+			client.Add ("g2_form[auto_rotate]", autorotate ? "yes" : "no");
+			client.Add ("g2_form[extrafield.Description]", description);
+			client.Add ("g2_userfile", new FileInfo (path));
+			client.expect_continue = expect_continue;
+			AddG2Specific (client);
+
+			return ParseAddItem (client.Submit (uri, Progress));
+		}
+
+		/*
+		public override Album AlbumProperties (string album)
+		{
+			FormClient client = new FormClient (cookies);
+			client.Add ("cmd", "album-properties");
+			client.Add ("protocol_version", "2.3");
+			client.Add ("set_albumName", album);
+
+			return ParseAlbumProperties (client.Submit (uri));
+		}
+		*/
+
+		public override bool NewAlbum (string parent_name,
+				      string name,
+				      string title,
+				      string description)
+		{
+			FormClient client = new FormClient (cookies);
+			client.Multipart = true;
+			client.Add ("g2_form[cmd]", "new-album");
+			client.Add ("g2_form[protocol_version]", "2.10");
+			client.Add ("g2_form[set_albumName]", parent_name);
+			client.Add ("g2_form[newAlbumName]", name);
+			client.Add ("g2_form[newAlbumTitle]", title);
+			client.Add ("g2_form[newAlbumDesc]", description);
+			AddG2Specific (client);
+
+			return ParseNewAlbum (client.Submit (uri));
+		}
+
+		public override ArrayList FetchAlbumImages (Album album, bool include_ablums)
+		{
+			FormClient client = new FormClient (cookies);
+			client.Add ("g2_form[cmd]", "fetch-album-images");
+			client.Add ("g2_form[protocol_version]","2.10");
+			client.Add ("g2_form[set_albumName]", album.Name);
+			client.Add ("g2_form[albums_too]", include_ablums ? "yes" : "no");
+			AddG2Specific (client);
+
+			album.Images = ParseFetchAlbumImages (client.Submit (uri), album);
+			return album.Images;
+		}
+
+		public override ArrayList FetchAlbumsPrune ()
+		{
+			FormClient client = new FormClient (cookies);
+			client.Add ("g2_form[cmd]", "fetch-albums-prune");
+			client.Add ("g2_form[protocol_version]", "2.10");
+			client.Add ("g2_form[check_writable]", "no");
+			AddG2Specific (client);
+
+			ArrayList a = ParseFetchAlbums (client.Submit (uri));
+			a.Sort();
+			return a;
+		}
+
+		private void AddG2Specific (FormClient client)
+		{
+			if (AuthToken != null && AuthToken != String.Empty)
+				client.Add("g2_authToken", AuthToken);
+			client.Add("g2_controller", "remote.GalleryRemote");
+		}
+
+		public ArrayList ParseFetchAlbumImages (HttpWebResponse response, Album album)
+		{
+			string [] data;
+			StreamReader reader = null;
+			ResultCode status = ResultCode.UnknownResponse;
+			string status_text = "Error: Unable to parse server response";
+			try {
+				Image current_image = null;
+				string baseUrl = Uri.ToString() + "?g2_view=core.DownloadItem&g2_itemId=";
+				reader = findResponse (response);
+				while ((data = GetNextLine (reader)) != null) {
+					if (data[0] == "status") {
+						status = (ResultCode) int.Parse (data [1]);
+					} else if (data[0].StartsWith ("status_text")) {
+						status_text = data[1];
+						Console.WriteLine ("StatusText : {0}", data[1]);
+					} else if (data[0].StartsWith ("image.name")) {
+						//for G2 this is the number used to download the image.
+						current_image = new Image (album, "awaiting 'title'");
+						album.Images.Add (current_image);
+						current_image.Url = baseUrl + data[1];
+					} else if (data[0].StartsWith ("image.title")) {
+						//for G2 the "title" is the name"
+						current_image.Name = data[1];
+					} else if (data[0].StartsWith ("image.raw_width")) {
+						current_image.RawWidth = int.Parse (data[1]);
+					} else if (data[0].StartsWith ("image.raw_height")) {
+						current_image.RawHeight = int.Parse (data[1]);
+					} else if (data[0].StartsWith ("image.raw_height")) {
+						current_image.RawHeight = int.Parse (data[1]);
+					//ignore these for now
+					} else if (data[0].StartsWith ("image.raw_filesize")) {
+					} else if (data[0].StartsWith ("image.forceExtension")) {
+					} else if (data[0].StartsWith ("image.capturedate.year")) {
+					} else if (data[0].StartsWith ("image.capturedate.mon")) {
+					} else if (data[0].StartsWith ("image.capturedate.mday")) {
+					} else if (data[0].StartsWith ("image.capturedate.hours")) {
+					} else if (data[0].StartsWith ("image.capturedate.minutes")) {
+					} else if (data[0].StartsWith ("image.capturedate.seconds")) {
+					} else if (data[0].StartsWith ("image.hidden")) {
+					} else if (data[0].StartsWith ("image.resizedName")) {
+						current_image.ResizedName = data[1];
+					} else if (data[0].StartsWith ("image.resized_width")) {
+						current_image.ResizedWidth = int.Parse (data[1]);
+					} else if (data[0].StartsWith ("image.resized_height")) {
+						current_image.ResizedHeight = int.Parse (data[1]);
+					} else if (data[0].StartsWith ("image.thumbName")) {
+						current_image.ThumbName = data[1];
+					} else if (data[0].StartsWith ("image.thumb_width")) {
+						current_image.ThumbWidth = int.Parse (data[1]);
+					} else if (data[0].StartsWith ("image.thumb_height")) {
+						current_image.ThumbHeight = int.Parse (data[1]);
+					} else if (data[0].StartsWith ("image.caption")) {
+						current_image.Caption = data[1];
+					} else if (data[0].StartsWith ("image.extrafield.Description")) {
+						current_image.Description = data[1];
+					} else if (data[0].StartsWith ("image.clicks")) {
+						try {
+							current_image.Clicks = int.Parse (data[1]);
+						} catch (System.FormatException) {
+							current_image.Clicks = 0;
+						}
+					} else if (data[0].StartsWith ("baseurl")) {
+						album.BaseURL = data[1];
+					} else if (data[0].StartsWith ("image_count")) {
+						if (album.Images.Count != int.Parse (data[1]))
+							Console.WriteLine ("Parsed image count for " + album.Name + "(" + album.Images.Count + ") does not match image_count (" + data[1] + ").  Something is amiss");
+					} else {
+						Console.WriteLine ("Unparsed Line in ParseFetchAlbumImages(): {0}={1}", data[0], data[1]);
+					}
+				}
+				Console.WriteLine ("Found: {0} cookies", response.Cookies.Count);
+				if (status != ResultCode.Success) {
+					Console.WriteLine (status_text);
+					throw new GalleryCommandException (status_text, status);
+				}
+
+				return album.Images;
+
+			} finally {
+				if (reader != null)
+					reader.Close ();
+
+				response.Close ();
+			}
+		}
+
+		public override string GetAlbumUrl (Album album)
+		{
+			return Uri.ToString() + "?g2_view=core.ShowItem&g2_itemId=" + album.Name;
+		}
+
+	}
+
+	public enum GalleryVersion : byte
+	{
+		VersionUnknown = 0,
+		Version1 = 1,
+		Version2 = 2
+	}
+}

Copied: trunk/extensions/Exporters/GalleryExport/Makefile.am (from r4368, /trunk/extensions/GalleryExport/Makefile.am)
==============================================================================

Added: trunk/extensions/Exporters/Makefile.am
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/Makefile.am	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,9 @@
+SUBDIRS = 			\
+	CDExport		\
+	DefaultExporters	\
+	GalleryExport		\
+	FlickrExport		\
+	FolderExport		\
+	PicasaWebExport		\
+	TabbloExport		\
+	SmugMugExport

Copied: trunk/extensions/Exporters/PicasaWebExport/.gitignore (from r4368, /trunk/extensions/FolderExport/.gitignore)
==============================================================================

Copied: trunk/extensions/Exporters/PicasaWebExport/Makefile.am (from r4368, /trunk/extensions/PicasaWebExport/Makefile.am)
==============================================================================

Copied: trunk/extensions/Exporters/PicasaWebExport/PicasaWebExport.addin.xml (from r4368, /trunk/extensions/PicasaWebExport/PicasaWebExport.addin.xml)
==============================================================================

Added: trunk/extensions/Exporters/PicasaWebExport/PicasaWebExport.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/PicasaWebExport/PicasaWebExport.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,972 @@
+/*
+ * PicasaWebExport.cs
+ *
+ * Authors:
+ *   Stephane Delcroix <stephane delcroix org>
+ *
+ * Copyright (C) 2006 Stephane Delcroix
+ */
+
+using System;
+using System.Net;
+using System.IO;
+using System.Text;
+using System.Collections;
+using System.Collections.Specialized;
+using System.Web;
+using Mono.Unix;
+
+using FSpot;
+using FSpot.Filters;
+using FSpot.Widgets;
+using FSpot.Utils;
+using FSpot.UI.Dialog;
+
+using Gnome.Keyring;
+
+using Mono.Google;
+using Mono.Google.Picasa;
+
+namespace FSpotGoogleExport {
+	public class GoogleAccount {
+
+		private string username;
+		private string password;
+		private string token;
+		private string unlock_captcha;
+		private GoogleConnection connection;
+		private PicasaWeb picasa;
+
+		public GoogleAccount (string username, string password)
+		{
+			this.username = username;
+			this.password = password;
+		}
+
+		public GoogleAccount (string username, string password, string token, string unlock_captcha)
+		{
+			this.username = username;
+			this.password = password;
+			this.token = token;
+			this.unlock_captcha = unlock_captcha;
+		}
+
+		public PicasaWeb Connect ()
+		{
+			System.Console.WriteLine ("GoogleAccount.Connect()");
+			GoogleConnection conn = new GoogleConnection (GoogleService.Picasa);
+			ServicePointManager.CertificatePolicy = new NoCheckCertificatePolicy ();
+			if (unlock_captcha == null || token == null)
+				conn.Authenticate(username, password);
+			else {
+				conn.Authenticate(username, password, token, unlock_captcha);
+				token = null;
+				unlock_captcha = null;
+			}
+			connection = conn;
+			PicasaWeb picasa = new PicasaWeb(conn);
+			this.picasa = picasa;
+			return picasa;
+		}
+
+		private void MarkChanged()
+		{
+			connection = null;
+		}
+
+		public bool Connected {
+			get {
+				return (connection != null);
+			}
+		}
+
+		public string Username {
+			get {
+				return username;
+			}
+			set {
+				if (username != value) {
+					username = value;
+					MarkChanged ();
+				}
+			}
+		}
+
+		public string Password {
+			get {
+				return password;
+			}
+			set {
+				if (password != value) {
+					password = value;
+					MarkChanged ();
+				}
+			}
+		}
+
+		public string Token {
+			get {
+				return token;
+			}
+			set {
+				token = value;
+			}
+		}
+
+		public string UnlockCaptcha {
+			get {
+				return unlock_captcha;
+			}
+			set {
+				unlock_captcha = value;
+			}
+		}
+
+		public PicasaWeb Picasa {
+			get {
+				return picasa;
+			}
+		}
+	}
+
+
+	public class GoogleAccountManager
+	{
+		private static GoogleAccountManager instance;
+		private const string keyring_item_name = "Google Account";
+		ArrayList accounts;
+
+		public delegate void AccountListChangedHandler (GoogleAccountManager manager, GoogleAccount changed_account);
+		public event AccountListChangedHandler AccountListChanged;
+
+		public static GoogleAccountManager GetInstance ()
+		{
+			if (instance == null) {
+				instance = new GoogleAccountManager ();
+			}
+
+			return instance;
+		}
+
+		private GoogleAccountManager ()
+		{
+			accounts = new ArrayList ();
+			ReadAccounts ();
+		}
+
+		public void MarkChanged ()
+		{
+			MarkChanged (true, null);
+		}
+
+		public void MarkChanged (bool write, GoogleAccount changed_account)
+		{
+			if (write)
+				WriteAccounts ();
+
+			if (AccountListChanged != null)
+				AccountListChanged (this, changed_account);
+		}
+
+		public ArrayList GetAccounts ()
+		{
+			return accounts;
+		}
+
+		public void AddAccount (GoogleAccount account)
+		{
+			AddAccount (account, true);
+		}
+
+		public void AddAccount (GoogleAccount account, bool write)
+		{
+			accounts.Add (account);
+			MarkChanged (write, account);
+		}
+
+		public void RemoveAccount (GoogleAccount account)
+		{
+			string keyring;
+			try {
+				keyring = Ring.GetDefaultKeyring();
+			} catch {
+				return;
+			}
+			Hashtable request_attributes = new Hashtable();
+			request_attributes["name"] = keyring_item_name;
+			request_attributes["username"] = account.Username;
+			try {
+				foreach(ItemData result in Ring.Find(ItemType.GenericSecret, request_attributes)) {
+					Ring.DeleteItem(keyring, result.ItemID);
+				}
+			} catch (Exception e) {
+				Console.WriteLine(e);
+			}
+			accounts.Remove (account);
+			MarkChanged ();
+		}
+
+		public void WriteAccounts ()
+		{
+			string keyring;
+			try {
+				keyring = Ring.GetDefaultKeyring();
+			} catch {
+				return;
+			}
+			foreach (GoogleAccount account in accounts) {
+				Hashtable update_request_attributes = new Hashtable();
+				update_request_attributes["name"] = keyring_item_name;
+				update_request_attributes["username"] = account.Username;
+
+				try {
+					Ring.CreateItem(keyring, ItemType.GenericSecret, keyring_item_name, update_request_attributes, account.Password, true);
+				} catch {
+					continue;
+				}
+			}
+		}
+
+		private void ReadAccounts ()
+		{
+
+			Hashtable request_attributes = new Hashtable();
+			request_attributes["name"] = keyring_item_name;
+			try {
+				foreach(ItemData result in Ring.Find(ItemType.GenericSecret, request_attributes)) {
+					if(!result.Attributes.ContainsKey("name") || !result.Attributes.ContainsKey("username") ||
+						(result.Attributes["name"] as string) != keyring_item_name)
+						continue;
+
+					string username = (string)result.Attributes["username"];
+					string password = result.Secret;
+
+					if (username == null || username == String.Empty || password == null || password == String.Empty)
+						throw new ApplicationException ("Invalid username/password in keyring");
+
+					GoogleAccount account = new GoogleAccount(username, password);
+					if (account != null)
+						AddAccount (account, false);
+
+				}
+			} catch (Exception e) {
+				Console.Error.WriteLine(e);
+			}
+
+			MarkChanged ();
+		}
+	}
+
+	public class GoogleAccountDialog {
+		public GoogleAccountDialog (Gtk.Window parent) : this (parent, null, false, null) {
+			Dialog.Response += HandleAddResponse;
+			add_button.Sensitive = false;
+		}
+
+		public GoogleAccountDialog (Gtk.Window parent, GoogleAccount account, bool show_error, CaptchaException captcha_exception)
+		{
+			xml = new Glade.XML (null, "PicasaWebExport.glade", dialog_name, "f-spot");
+			xml.Autoconnect (this);
+			Dialog.Modal = false;
+			Dialog.TransientFor = parent;
+			Dialog.DefaultResponse = Gtk.ResponseType.Ok;
+
+			this.account = account;
+
+			bool show_captcha = (captcha_exception != null);
+			status_area.Visible = show_error;
+			locked_area.Visible = show_captcha;
+			captcha_label.Visible = show_captcha;
+			captcha_entry.Visible = show_captcha;
+			captcha_image.Visible = show_captcha;
+
+			password_entry.ActivatesDefault = true;
+			username_entry.ActivatesDefault = true;
+
+			if (show_captcha) {
+				try {
+					using  (ImageFile img = ImageFile.Create(new Uri(captcha_exception.CaptchaUrl))) {
+						captcha_image.Pixbuf = img.Load();
+						token = captcha_exception.Token;
+					}
+				} catch (Exception) {}
+			}
+
+			if (account != null) {
+				password_entry.Text = account.Password;
+				username_entry.Text = account.Username;
+				add_button.Label = Gtk.Stock.Ok;
+				Dialog.Response += HandleEditResponse;
+			}
+
+			if (remove_button != null)
+				remove_button.Visible = account != null;
+
+			this.Dialog.Show ();
+
+			password_entry.Changed += HandleChanged;
+			username_entry.Changed += HandleChanged;
+			HandleChanged (null, null);
+		}
+
+		private void HandleChanged (object sender, System.EventArgs args)
+		{
+			password = password_entry.Text;
+			username = username_entry.Text;
+
+			add_button.Sensitive = !(password == String.Empty || username == String.Empty);
+		}
+
+		[GLib.ConnectBefore]
+		protected void HandleAddResponse (object sender, Gtk.ResponseArgs args)
+		{
+			if (args.ResponseId == Gtk.ResponseType.Ok) {
+				GoogleAccount account = new GoogleAccount (username, password);
+				GoogleAccountManager.GetInstance ().AddAccount (account);
+			}
+			Dialog.Destroy ();
+		}
+
+		protected void HandleEditResponse (object sender, Gtk.ResponseArgs args)
+		{
+			if (args.ResponseId == Gtk.ResponseType.Ok) {
+				account.Username = username;
+				account.Password = password;
+				account.Token = token;
+				account.UnlockCaptcha = captcha_entry.Text;
+				GoogleAccountManager.GetInstance ().MarkChanged (true, account);
+			} else if (args.ResponseId == Gtk.ResponseType.Reject) {
+				// NOTE we are using Reject to signal the remove action.
+				GoogleAccountManager.GetInstance ().RemoveAccount (account);
+			}
+			Dialog.Destroy ();
+		}
+
+		private Gtk.Dialog Dialog {
+			get {
+				if (dialog == null)
+					dialog = (Gtk.Dialog) xml.GetWidget (dialog_name);
+
+				return dialog;
+			}
+		}
+
+		private GoogleAccount account;
+		private string password;
+		private string username;
+		private string token;
+
+		private Glade.XML xml;
+		private string dialog_name = "google_add_dialog";
+
+		// widgets
+		[Glade.Widget] Gtk.Dialog dialog;
+		[Glade.Widget] Gtk.Entry password_entry;
+		[Glade.Widget] Gtk.Entry username_entry;
+		[Glade.Widget] Gtk.Entry captcha_entry;
+
+		[Glade.Widget] Gtk.Button add_button;
+		[Glade.Widget] Gtk.Button remove_button;
+		[Glade.Widget] Gtk.Button cancel_button;
+
+		[Glade.Widget] Gtk.HBox status_area;
+		[Glade.Widget] Gtk.HBox locked_area;
+
+		[Glade.Widget] Gtk.Image captcha_image;
+		[Glade.Widget] Gtk.Label captcha_label;
+
+	}
+
+	public class GoogleAddAlbum {
+		[Glade.Widget] Gtk.Dialog dialog;
+		[Glade.Widget] Gtk.OptionMenu album_optionmenu;
+
+		[Glade.Widget] Gtk.Entry title_entry;
+		[Glade.Widget] Gtk.Entry description_entry;
+		[Glade.Widget] Gtk.CheckButton public_check;
+
+		[Glade.Widget] Gtk.Button add_button;
+		[Glade.Widget] Gtk.Button cancel_button;
+
+		private Glade.XML xml;
+		private string dialog_name = "google_add_album_dialog";
+
+		private GoogleExport export;
+		private PicasaWeb picasa;
+		private string description;
+		private string title;
+		private bool public_album;
+
+		public GoogleAddAlbum (GoogleExport export, PicasaWeb picasa)
+		{
+			xml = new Glade.XML (null, "PicasaWebExport.glade", dialog_name, "f-spot");
+			xml.Autoconnect (this);
+
+			this.export = export;
+			this.picasa = picasa;
+
+			Dialog.Response += HandleAddResponse;
+
+			description_entry.Changed += HandleChanged;
+			title_entry.Changed += HandleChanged;
+			HandleChanged (null, null);
+		}
+
+		private void HandleChanged (object sender, EventArgs args)
+		{
+			description = description_entry.Text;
+			title = title_entry.Text;
+			public_album = public_check.Active;
+
+			if (title == String.Empty)
+				add_button.Sensitive = false;
+			else
+				add_button.Sensitive = true;
+		}
+
+		[GLib.ConnectBefore]
+		protected void HandleAddResponse (object sender, Gtk.ResponseArgs args)
+		{
+			if (args.ResponseId == Gtk.ResponseType.Ok) {
+				public_album = public_check.Active;
+
+				try {
+					picasa.CreateAlbum (System.Web.HttpUtility.HtmlEncode (title), description, public_album ? AlbumAccess.Public : AlbumAccess.Private);
+				} catch (System.Exception e) {
+					HigMessageDialog md =
+					new HigMessageDialog (Dialog,
+							      Gtk.DialogFlags.Modal |
+							      Gtk.DialogFlags.DestroyWithParent,
+								      Gtk.MessageType.Error, Gtk.ButtonsType.Ok,
+							      Catalog.GetString ("Error while creating Album"),
+							      String.Format (Catalog.GetString ("The following error was encountered while attempting to create an album: {0}"), e.Message));
+					md.Run ();
+					md.Destroy ();
+					return;
+				}
+				export.HandleAlbumAdded (title);
+			}
+			Dialog.Destroy ();
+		}
+
+		private Gtk.Dialog Dialog {
+			get {
+				if (dialog == null)
+					dialog = (Gtk.Dialog) xml.GetWidget (dialog_name);
+
+				return dialog;
+			}
+		}
+	}
+
+
+	public class GoogleExport : FSpot.Extensions.IExporter {
+		public GoogleExport ()
+		{
+		}
+
+		public void Run (IBrowsableCollection selection)
+		{
+			xml = new Glade.XML (null, "PicasaWebExport.glade", dialog_name, "f-spot");
+			xml.Autoconnect (this);
+
+			this.items = selection.Items;
+			album_button.Sensitive = false;
+			IconView view = new IconView (selection);
+			view.DisplayDates = false;
+			view.DisplayTags = false;
+
+			Dialog.Modal = false;
+			Dialog.TransientFor = null;
+
+			thumb_scrolledwindow.Add (view);
+			view.Show ();
+			Dialog.Show ();
+
+
+			GoogleAccountManager manager = GoogleAccountManager.GetInstance ();
+			manager.AccountListChanged += PopulateGoogleOptionMenu;
+			PopulateGoogleOptionMenu (manager, null);
+			album_optionmenu.Changed += HandleAlbumOptionMenuChanged;
+
+			if (edit_button != null)
+				edit_button.Clicked += HandleEditGallery;
+
+			Dialog.Response += HandleResponse;
+			connect = true;
+			HandleSizeActive (null, null);
+			Connect ();
+
+			scale_check.Toggled += HandleScaleCheckToggled;
+
+			LoadPreference (SCALE_KEY);
+			LoadPreference (SIZE_KEY);
+			LoadPreference (ROTATE_KEY);
+			LoadPreference (BROWSER_KEY);
+//			LoadPreference (Preferences.EXPORT_PICASAWEB_META);
+			LoadPreference (TAG_KEY);
+		}
+
+		private bool scale;
+		private int size;
+		private bool browser;
+		private bool rotate;
+//		private bool meta;
+		private bool export_tag;
+		private bool connect = false;
+
+		private long approx_size = 0;
+		private long sent_bytes = 0;
+
+		IBrowsableItem [] items;
+		int photo_index;
+		FSpot.ThreadProgressDialog progress_dialog;
+
+		ArrayList accounts;
+		private GoogleAccount account;
+		private PicasaAlbum album;
+		private PicasaAlbumCollection albums = null;
+
+		private string xml_path;
+
+		private Glade.XML xml;
+		private string dialog_name = "google_export_dialog";
+
+		public const string EXPORT_SERVICE = "picasaweb/";
+		public const string SCALE_KEY = Preferences.APP_FSPOT_EXPORT + EXPORT_SERVICE + "scale";
+		public const string SIZE_KEY = Preferences.APP_FSPOT_EXPORT + EXPORT_SERVICE + "size";
+		public const string ROTATE_KEY = Preferences.APP_FSPOT_EXPORT + EXPORT_SERVICE + "rotate";
+		public const string BROWSER_KEY = Preferences.APP_FSPOT_EXPORT + EXPORT_SERVICE + "browser";
+		public const string TAG_KEY = Preferences.APP_FSPOT_EXPORT + EXPORT_SERVICE + "tag";
+
+		// widgets
+		[Glade.Widget] Gtk.Dialog dialog;
+		[Glade.Widget] Gtk.OptionMenu gallery_optionmenu;
+		[Glade.Widget] Gtk.OptionMenu album_optionmenu;
+
+		[Glade.Widget] Gtk.Entry width_entry;
+		[Glade.Widget] Gtk.Entry height_entry;
+
+		[Glade.Widget] Gtk.Label status_label;
+		[Glade.Widget] Gtk.Label album_status_label;
+
+		[Glade.Widget] Gtk.CheckButton browser_check;
+		[Glade.Widget] Gtk.CheckButton scale_check;
+		[Glade.Widget] Gtk.CheckButton rotate_check;
+//		[Glade.Widget] Gtk.CheckButton meta_check;
+		[Glade.Widget] Gtk.CheckButton tag_check;
+
+		[Glade.Widget] Gtk.SpinButton size_spin;
+
+		[Glade.Widget] Gtk.Button album_button;
+		[Glade.Widget] Gtk.Button add_button;
+		[Glade.Widget] Gtk.Button edit_button;
+
+		[Glade.Widget] Gtk.Button export_button;
+		[Glade.Widget] Gtk.Button cancel_button;
+
+		[Glade.Widget] Gtk.ScrolledWindow thumb_scrolledwindow;
+
+		System.Threading.Thread command_thread;
+
+		private void HandleResponse (object sender, Gtk.ResponseArgs args)
+		{
+			if (args.ResponseId != Gtk.ResponseType.Ok) {
+				Dialog.Destroy ();
+				return;
+			}
+
+			if (scale_check != null) {
+				scale = scale_check.Active;
+				size = size_spin.ValueAsInt;
+			} else
+				scale = false;
+
+			browser = browser_check.Active;
+			rotate = rotate_check.Active;
+//			meta = meta_check.Active;
+			export_tag = tag_check.Active;
+
+			if (account != null) {
+				//System.Console.WriteLine ("history = {0}", album_optionmenu.History);
+				album = (PicasaAlbum) account.Picasa.GetAlbums() [Math.Max (0, album_optionmenu.History)];
+				photo_index = 0;
+
+				Dialog.Destroy ();
+
+				command_thread = new System.Threading.Thread (new System.Threading.ThreadStart (this.Upload));
+				command_thread.Name = Catalog.GetString ("Uploading Pictures");
+
+				progress_dialog = new FSpot.ThreadProgressDialog (command_thread, items.Length);
+				progress_dialog.Start ();
+
+				// Save these settings for next time
+				Preferences.Set (SCALE_KEY, scale);
+				Preferences.Set (SIZE_KEY, size);
+				Preferences.Set (ROTATE_KEY, rotate);
+				Preferences.Set (BROWSER_KEY, browser);
+//				Preferences.Set (Preferences.EXPORT_GALLERY_META, meta);
+				Preferences.Set (TAG_KEY, export_tag);
+			}
+		}
+
+		public void HandleSizeActive (object sender, EventArgs args)
+		{
+			size_spin.Sensitive = scale_check.Active;
+		}
+
+		private void HandleUploadProgress(object o, UploadProgressEventArgs args)
+		{
+				if (approx_size == 0)
+					progress_dialog.ProgressText = System.String.Format (Catalog.GetString ("{0} Sent"), SizeUtil.ToHumanReadable(args.BytesSent));
+				else
+					progress_dialog.ProgressText = System.String.Format (Catalog.GetString ("{0} of approx. {1}"), SizeUtil.ToHumanReadable(sent_bytes + args.BytesSent), SizeUtil.ToHumanReadable(approx_size));
+				progress_dialog.Fraction = ((photo_index - 1) / (double) items.Length) + (args.BytesSent / (args.BytesTotal * (double) items.Length));
+		}
+
+		private class DateComparer : IComparer
+		{
+			public int Compare (object left, object right)
+			{
+				return DateTime.Compare ((left as IBrowsableItem).Time, (right as IBrowsableItem).Time);
+			}
+		}
+
+		private void Upload ()
+		{
+			album.UploadProgress += HandleUploadProgress;
+			sent_bytes = 0;
+			approx_size = 0;
+
+			System.Console.WriteLine ("Starting Upload to Picasa");
+
+			FilterSet filters = new FilterSet ();
+			filters.Add (new JpegFilter ());
+
+			if (scale)
+				filters.Add (new ResizeFilter ((uint)size));
+
+			if (rotate)
+				filters.Add (new OrientationFilter ());
+
+			Array.Sort (items, new DateComparer ());
+
+			while (photo_index < items.Length) {
+				try {
+					IBrowsableItem item = items[photo_index];
+
+					FileInfo file_info;
+					Console.WriteLine ("uploading {0}", photo_index);
+
+					progress_dialog.Message = String.Format (Catalog.GetString ("Uploading picture \"{0}\" ({1} of {2})"),
+										 item.Name, photo_index+1, items.Length);
+					photo_index++;
+
+					PicasaPicture picture;
+					using (FilterRequest request = new FilterRequest (item.DefaultVersionUri)) {
+						filters.Convert (request);
+						file_info = new FileInfo (request.Current.LocalPath);
+
+						if (approx_size == 0) //first image
+							approx_size = file_info.Length * items.Length;
+						else
+							approx_size = sent_bytes * items.Length / (photo_index - 1);
+
+						picture = album.UploadPicture (request.Current.LocalPath, Path.ChangeExtension (item.Name, "jpg"), item.Description);
+						sent_bytes += file_info.Length;
+					}
+					if (Core.Database != null && item is Photo)
+						Core.Database.Exports.Create ((item as Photo).Id,
+									      (item as Photo).DefaultVersionId,
+									      ExportStore.PicasaExportType,
+									      picture.Link);
+
+					//tagging
+					if (item.Tags != null && export_tag)
+						foreach (Tag tag in item.Tags)
+							picture.AddTag (tag.Name);
+				} catch (System.Threading.ThreadAbortException te) {
+					Log.Exception (te);
+					System.Threading.Thread.ResetAbort ();
+				} catch (System.Exception e) {
+					progress_dialog.Message = String.Format (Catalog.GetString ("Error Uploading To Gallery: {0}"),
+										 e.Message);
+					progress_dialog.ProgressText = Catalog.GetString ("Error");
+					System.Console.WriteLine (e);
+
+					if (progress_dialog.PerformRetrySkip ())
+						photo_index--;
+				}
+			}
+
+			progress_dialog.Message = Catalog.GetString ("Done Sending Photos");
+			progress_dialog.Fraction = 1.0;
+			progress_dialog.ProgressText = Catalog.GetString ("Upload Complete");
+			progress_dialog.ButtonLabel = Gtk.Stock.Ok;
+
+			if (browser) {
+				GnomeUtil.UrlShow (album.Link);
+			}
+		}
+
+		private void HandleScaleCheckToggled (object o, EventArgs e)
+		{
+			rotate_check.Sensitive = !scale_check.Active;
+		}
+
+		private void PopulateGoogleOptionMenu (GoogleAccountManager manager, GoogleAccount changed_account)
+		{
+			Gtk.Menu menu = new Gtk.Menu ();
+			this.account = changed_account;
+			int pos = -1;
+
+			accounts = manager.GetAccounts ();
+			if (accounts == null || accounts.Count == 0) {
+				Gtk.MenuItem item = new Gtk.MenuItem (Catalog.GetString ("(No Gallery)"));
+				menu.Append (item);
+				gallery_optionmenu.Sensitive = false;
+				edit_button.Sensitive = false;
+			} else {
+				int i = 0;
+				foreach (GoogleAccount account in accounts) {
+					if (account == changed_account)
+						pos = i;
+
+					Gtk.MenuItem item = new Gtk.MenuItem (account.Username);
+					menu.Append (item);
+					i++;
+				}
+				gallery_optionmenu.Sensitive = true;
+				edit_button.Sensitive = true;
+			}
+
+			menu.ShowAll ();
+			gallery_optionmenu.Menu = menu;
+			gallery_optionmenu.SetHistory ((uint)pos);
+		}
+
+		private void Connect ()
+		{
+			Connect (null);
+		}
+
+		private void Connect (GoogleAccount selected)
+		{
+			Connect (selected, null, null);
+		}
+
+		private void Connect (GoogleAccount selected, string token, string text)
+		{
+			try {
+				if (accounts.Count != 0 && connect) {
+					if (selected == null)
+						account = (GoogleAccount) accounts [gallery_optionmenu.History];
+					else
+						account = selected;
+
+					if (!account.Connected)
+						account.Connect ();
+
+					PopulateAlbumOptionMenu (account.Picasa);
+
+					long qu = account.Picasa.QuotaUsed;
+					long ql = account.Picasa.QuotaLimit;
+
+					StringBuilder sb = new StringBuilder("<small>");
+					sb.Append(Catalog.GetString("Available space:"));
+					sb.Append(SizeUtil.ToHumanReadable (ql - qu));
+					sb.Append(" (");
+					sb.Append(100 * qu / ql);
+					sb.Append("% used out of ");
+					sb.Append(SizeUtil.ToHumanReadable (ql));
+					sb.Append(")");
+					sb.Append("</small>");
+
+					status_label.Text = sb.ToString();
+					status_label.UseMarkup = true;
+
+					album_button.Sensitive = true;
+				}
+			} catch (CaptchaException exc){
+				System.Console.WriteLine("Your google account is locked");
+				if (selected != null)
+					account = selected;
+
+				PopulateAlbumOptionMenu (account.Picasa);
+				album_button.Sensitive = false;
+
+				new GoogleAccountDialog (this.Dialog, account, false, exc);
+
+				System.Console.WriteLine ("Your google account is locked, you can unlock it by visiting: {0}", CaptchaException.UnlockCaptchaURL);
+
+			} catch (System.Exception) {
+				System.Console.WriteLine ("Can not connect to Picasa. Bad username ? password ? network connection ?");
+				//System.Console.WriteLine ("{0}",ex);
+				if (selected != null)
+					account = selected;
+
+				PopulateAlbumOptionMenu (account.Picasa);
+
+				status_label.Text = String.Empty;
+				album_button.Sensitive = false;
+
+				new GoogleAccountDialog (this.Dialog, account, true, null);
+			}
+		}
+
+		private void HandleAccountSelected (object sender, System.EventArgs args)
+		{
+			Connect ();
+		}
+
+		public void HandleAlbumAdded (string title) {
+			GoogleAccount account = (GoogleAccount) accounts [gallery_optionmenu.History];
+			PopulateAlbumOptionMenu (account.Picasa);
+
+			// make the newly created album selected
+//			PicasaAlbumCollection albums = account.Picasa.GetAlbums();
+			for (int i=0; i < albums.Count; i++) {
+				if (((PicasaAlbum)albums[i]).Title == title) {
+					album_optionmenu.SetHistory((uint)i);
+				}
+			}
+		}
+
+		private void PopulateAlbumOptionMenu (PicasaWeb picasa)
+		{
+			if (picasa != null) {
+				try {
+					albums = picasa.GetAlbums();
+				} catch {
+					Console.WriteLine("Can't get the albums");
+					albums = null;
+					picasa = null;
+				}
+			}
+
+			Gtk.Menu menu = new Gtk.Menu ();
+
+			bool disconnected = picasa == null || !account.Connected || albums == null;
+
+			if (disconnected || albums.Count == 0) {
+				string msg = disconnected ? Catalog.GetString ("(Not Connected)")
+					: Catalog.GetString ("(No Albums)");
+
+				Gtk.MenuItem item = new Gtk.MenuItem (msg);
+				menu.Append (item);
+
+				export_button.Sensitive = false;
+				album_optionmenu.Sensitive = false;
+				album_button.Sensitive = false;
+
+				if (disconnected)
+					album_button.Sensitive = false;
+			} else {
+				foreach (PicasaAlbum album in albums.AllValues) {
+					System.Text.StringBuilder label_builder = new System.Text.StringBuilder ();
+
+					label_builder.Append (album.Title);
+					label_builder.Append (" (" + album.PicturesCount + ")");
+
+					Gtk.MenuItem item = new Gtk.MenuItem (label_builder.ToString ());
+					((Gtk.Label)item.Child).UseUnderline = false;
+					menu.Append (item);
+				}
+
+				export_button.Sensitive = items.Length > 0;
+				album_optionmenu.Sensitive = true;
+				album_button.Sensitive = true;
+			}
+
+			menu.ShowAll ();
+			album_optionmenu.Menu = menu;
+		}
+
+		public void HandleAlbumOptionMenuChanged (object sender, System.EventArgs args)
+		{
+			if (albums == null || albums.Count == 0)
+				return;
+
+			PicasaAlbum a = albums [album_optionmenu.History];
+			export_button.Sensitive = a.PicturesRemaining >= items.Length;
+			if (album_status_label.Visible = !export_button.Sensitive) {
+				album_status_label.Text = String.Format (Catalog.GetString ("<small>The selected album has a limit of {0} pictures,\n" +
+									   "which would be passed with the current selection of {1} images</small>"),
+									a.PicturesCount + a.PicturesRemaining, items.Length);
+				album_status_label.UseMarkup = true;
+			} else {
+				album_status_label.Text = String.Empty;
+			}
+		}
+
+		public void HandleAddGallery (object sender, System.EventArgs args)
+		{
+			new GoogleAccountDialog (this.Dialog);
+		}
+
+		public void HandleEditGallery (object sender, System.EventArgs args)
+		{
+			new GoogleAccountDialog (this.Dialog, account, false, null);
+		}
+
+		public void HandleAddAlbum (object sender, System.EventArgs args)
+		{
+			if (account == null)
+				throw new Exception (Catalog.GetString ("No account selected"));
+
+			new GoogleAddAlbum (this, account.Picasa);
+		}
+
+		void LoadPreference (string key)
+		{
+			object val = Preferences.Get (key);
+
+			if (val == null)
+				return;
+
+			//System.Console.WriteLine ("Setting {0} to {1}", key, val);
+
+			switch (key) {
+			case SCALE_KEY:
+				if (scale_check.Active != (bool) val) {
+					scale_check.Active = (bool) val;
+					rotate_check.Sensitive = !(bool) val;
+				}
+				break;
+
+			case SIZE_KEY:
+				size_spin.Value = (double) (int) val;
+				break;
+
+			case BROWSER_KEY:
+				if (browser_check.Active != (bool) val)
+					browser_check.Active = (bool) val;
+				break;
+
+			case ROTATE_KEY:
+				if (rotate_check.Active != (bool) val)
+					rotate_check.Active = (bool) val;
+				break;
+
+//			case Preferences.EXPORT_GALLERY_META:
+//				if (meta_check.Active != (bool) val)
+//					meta_check.Active = (bool) val;
+//				break;
+
+			case TAG_KEY:
+				if (tag_check.Active != (bool) val)
+					tag_check.Active = (bool) val;
+				break;
+			}
+		}
+
+		private Gtk.Dialog Dialog {
+			get {
+				if (dialog == null)
+					dialog = (Gtk.Dialog) xml.GetWidget (dialog_name);
+
+				return dialog;
+			}
+		}
+	}
+}

Copied: trunk/extensions/Exporters/PicasaWebExport/PicasaWebExport.glade (from r4368, /trunk/extensions/PicasaWebExport/PicasaWebExport.glade)
==============================================================================

Copied: trunk/extensions/Exporters/PicasaWebExport/google-sharp/.gitignore (from r4368, /trunk/extensions/GalleryExport/.gitignore)
==============================================================================

Added: trunk/extensions/Exporters/PicasaWebExport/google-sharp/AlbumAccess.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/PicasaWebExport/google-sharp/AlbumAccess.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,35 @@
+//
+// Mono.Google.Picasa.AlbumAccess.cs:
+//
+// Authors:
+//	Gonzalo Paniagua Javier (gonzalo ximian com)
+//
+// (C) Copyright 2006 Novell, Inc. (http://www.novell.com)
+//
+
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+namespace Mono.Google.Picasa {
+	public enum AlbumAccess {
+		Public,
+		Private
+	}
+}

Added: trunk/extensions/Exporters/PicasaWebExport/google-sharp/AssemblyInfo.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/PicasaWebExport/google-sharp/AssemblyInfo.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,35 @@
+// AssemblyInfo.cs for Mono.Google.dll
+//
+// Authors:
+//	Gonzalo Paniagua Javier (gonzalo ximian com)
+//
+// Copyright (c) 2006 Novell, Inc. (http://www.novell.com)
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using System.Reflection;
+using System.Runtime.CompilerServices;
+
+[assembly: AssemblyVersion("0.1.0")]
+[assembly: AssemblyTitle ("Mono.Google")]
+[assembly: AssemblyDescription ("")]
+[assembly: AssemblyCopyright ("(c) 2006 Novell, Inc.")]
+[assembly: AssemblyCompany ("Novell, Inc.")]

Added: trunk/extensions/Exporters/PicasaWebExport/google-sharp/Authentication.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/PicasaWebExport/google-sharp/Authentication.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,157 @@
+//
+// Mono.Google.Authentication.cs:
+//
+// Authors:
+//	Gonzalo Paniagua Javier (gonzalo ximian com)
+//	Stephane Delcroix (stephane delcroix org)
+//
+// (C) Copyright 2006 Novell, Inc. (http://www.novell.com)
+// (C) Copyright 2007 S. Delcroix
+//
+
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+// Check the Google Authentication Page at http://code.google.com/apis/accounts/AuthForInstalledApps.html
+//
+
+using System;
+using System.IO;
+using System.Net;
+using System.Text;
+using System.Web;
+
+namespace Mono.Google {
+	class Authentication {
+		static string client_login_url = "https://www.google.com/accounts/ClientLogin";;
+
+		public static string GetAuthorization (GoogleConnection conn, string email, string password,
+				GoogleService service, string token, string captcha)
+		{
+			if (email == null || email == String.Empty || password == null || password == String.Empty)
+				return null;
+
+			email = HttpUtility.UrlEncode (email);
+			password = HttpUtility.UrlEncode (password);
+			string appname = HttpUtility.UrlEncode (conn.ApplicationName);
+			string service_code = service.ServiceCode;
+
+			StringBuilder content = new StringBuilder ();
+			content.Append ("accountType=HOSTED_OR_GOOGLE");
+			content.AppendFormat ("&Email={0}", email);
+			content.AppendFormat ("&Passwd={0}", password);
+			content.AppendFormat ("&email={0}", email);
+			content.AppendFormat ("&service={0}", service_code);
+			content.AppendFormat ("&source={0}", appname);
+
+			if (token != null) {
+				content.AppendFormat ("&logintoken={0}", token);
+				content.AppendFormat ("&logincaptcha={0}", captcha);
+			}
+			byte [] bytes = Encoding.UTF8.GetBytes (content.ToString ());
+
+			HttpWebRequest request = (HttpWebRequest) WebRequest.Create (client_login_url);
+			request.Method = "POST";
+			request.ContentType = "application/x-www-form-urlencoded";
+			request.ContentLength = bytes.Length;
+
+			Stream output = request.GetRequestStream ();
+			output.Write (bytes, 0, bytes.Length);
+			output.Close ();
+
+			HttpWebResponse response = null;
+			try {
+				response = (HttpWebResponse) request.GetResponse ();
+			} catch (WebException wexc) {
+				response = wexc.Response as HttpWebResponse;
+				if (response == null)
+					throw;
+				ThrowOnError (response);
+				throw; // if the method above does not throw, we do
+			}
+
+			//string sid = null;
+			//string lsid = null;
+			string auth = null;
+
+			using (Stream stream = response.GetResponseStream ()) {
+				StreamReader sr = new StreamReader (stream, Encoding.UTF8);
+				string s;
+				while ((s = sr.ReadLine ()) != null) {
+					if (s.StartsWith ("Auth="))
+						auth = s.Substring (5);
+					//else if (s.StartsWith ("LSID="))
+					//	lsid = s.Substring (5);
+					//else if (s.StartsWith ("SID="))
+					//	sid = s.Substring (4);
+				}
+			}
+			response.Close ();
+
+			return auth;
+		}
+
+		static void ThrowOnError (HttpWebResponse response)
+		{
+			if (response.StatusCode != HttpStatusCode.Forbidden)
+				return;
+
+			string url = null;
+			string token = null;
+			string captcha_url = null;
+			string code = null;
+			using (StreamReader reader = new StreamReader (response.GetResponseStream ())) {
+				string str;
+				while ((str = reader.ReadLine ()) != null) {
+					if (str.StartsWith ("Url=")) {
+						url = str.Substring (4);
+					} else if (str.StartsWith ("Error=")) {
+						/* These are the values for Error
+							None,
+							BadAuthentication,
+							NotVerified,
+							TermsNotAgreed,
+							CaptchaRequired,
+							Unknown,
+							AccountDeleted,
+							AccountDisabled,
+							ServiceUnavailable
+						*/
+						code = str.Substring (6);
+					} else if (str.StartsWith ("CaptchaToken=")) {
+						token = str.Substring (13);
+					} else if (str.StartsWith ("CaptchaUrl=")) {
+						captcha_url = str.Substring (11);
+					}
+				}
+			}
+			if (code == "CaptchaRequired" && token != null && captcha_url != null) {
+				if (url != null) {
+					Uri uri = new Uri (url);
+					captcha_url = new Uri (uri, captcha_url).ToString ();
+				} else if (!captcha_url.StartsWith ("https://";)) {
+					captcha_url = "https://www.google.com/accounts/"; + captcha_url;
+				}
+				throw new CaptchaException (url, token, captcha_url);
+			}
+
+			throw new UnauthorizedAccessException (String.Format ("Access to '{0}' is denied ({1})", url, code));
+		}
+	}
+}

Added: trunk/extensions/Exporters/PicasaWebExport/google-sharp/CaptchaException.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/PicasaWebExport/google-sharp/CaptchaException.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,86 @@
+//
+// Mono.Google.CaptchaException
+//
+// Authors:
+// 	Gonzalo Paniagua Javier (gonzalo novell com)
+//
+// Copyright (c) 2006 Novell, Inc.  (http://www.novell.com)
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using System;
+using System.Runtime.Serialization;
+
+namespace Mono.Google {
+	[Serializable]
+	public class CaptchaException : UnauthorizedAccessException, ISerializable
+	{
+		public static string UnlockCaptchaURL = "https://www.google.com/accounts/DisplayUnlockCaptcha";;
+		string url;
+		string token;
+		string captcha_url;
+
+		public CaptchaException ()
+		{
+		}
+
+		public CaptchaException (string url, string token, string captcha_url)
+		{
+			this.url = url;
+			this.token = token;
+			this.captcha_url = captcha_url;
+		}
+
+		protected CaptchaException (SerializationInfo info, StreamingContext context)
+			: base (info, context)
+		{
+			if (info == null)
+				throw new ArgumentNullException ("info");
+
+			url = info.GetString ("url");
+			token = info.GetString ("token");
+			captcha_url = info.GetString ("captcha_url");
+		}
+
+		void ISerializable.GetObjectData (SerializationInfo info, StreamingContext context)
+		{
+			if (info == null)
+				throw new ArgumentNullException ("info");
+
+			base.GetObjectData (info, context);
+			info.AddValue ("url", url);
+			info.AddValue ("token", token);
+			info.AddValue ("captcha_url", captcha_url);
+		}
+
+		public string Url {
+			get { return url; }
+		}
+
+		public string Token {
+			get { return token; }
+		}
+
+		public string CaptchaUrl {
+			get { return captcha_url; }
+		}
+	}
+}

Added: trunk/extensions/Exporters/PicasaWebExport/google-sharp/CreateAlbumException.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/PicasaWebExport/google-sharp/CreateAlbumException.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,43 @@
+//
+// Mono.Google.Picasa.CreateAlbumException.cs:
+//
+// Authors:
+//	Gonzalo Paniagua Javier (gonzalo ximian com)
+//
+// (C) Copyright 2006 Novell, Inc. (http://www.novell.com)
+//
+
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using System;
+using System.Runtime.Serialization;
+namespace Mono.Google.Picasa {
+	public class CreateAlbumException : Exception {
+		public CreateAlbumException (string msg) : base (msg)
+		{
+		}
+
+		protected CreateAlbumException (SerializationInfo info, StreamingContext context)
+			: base (info, context)
+		{
+		}
+	}
+}

Added: trunk/extensions/Exporters/PicasaWebExport/google-sharp/DeleteAlbumException.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/PicasaWebExport/google-sharp/DeleteAlbumException.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,43 @@
+//
+// Mono.Google.Picasa.DeleteAlbumException.cs:
+//
+// Authors:
+//	Gonzalo Paniagua Javier (gonzalo ximian com)
+//
+// (C) Copyright 2006 Novell, Inc. (http://www.novell.com)
+//
+
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using System;
+using System.Runtime.Serialization;
+namespace Mono.Google.Picasa {
+	public class DeleteAlbumException : Exception {
+		public DeleteAlbumException (string msg) : base (msg)
+		{
+		}
+
+		protected DeleteAlbumException (SerializationInfo info, StreamingContext context)
+			: base (info, context)
+		{
+		}
+	}
+}

Added: trunk/extensions/Exporters/PicasaWebExport/google-sharp/GDataApi.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/PicasaWebExport/google-sharp/GDataApi.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,92 @@
+/*
+ * Mono.Google.Picasa.GDataApi.cs
+ *
+ * Author(s):
+ *   Stephane Delcroix  <stephane delcroix org>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining
+ * a copy of this software and associated documentation files (the
+ * "Software"), to deal in the Software without restriction, including
+ * without limitation the rights to use, copy, modify, merge, publish,
+ * distribute, sublicense, and/or sell copies of the Software, and to
+ * permit persons to whom the Software is furnished to do so, subject to
+ * the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be
+ * included in all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+ * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+ * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+ * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+ * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+ * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+ * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ *
+ * Check Picasa Web Albums Data Api at http://code.google.com/apis/picasaweb/gdata.html
+ *
+ */
+
+
+namespace Mono.Google.Picasa {
+	class GDataApi {
+		private const string feed = "http://picasaweb.google.com/data/feed/api/";;
+		private const string entry = "http://picasaweb.google.com/data/entry/api/";;
+		const string gallery = "user/{userid}?kind=album";
+		const string album_by_id = "user/{userid}/albumid/{aid}?kind=photo";
+		//const string album_by_name = entry + "user/{userid}/album/{aname}?kind=photo";
+		const string picture_by_id = "user/{userid}/albumid/{aid}/photoid/{pid}";
+		const string post_url = feed + "user/{userid}";
+		const string post_picture = feed + "user/{userid}/albumid/{aid}";
+		const string date_format = "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.000Z'";
+
+		public static string GetGalleryFeed (string user)
+		{
+			return feed + gallery.Replace ("{userid}", user);
+		}
+
+		public static string GetGalleryEntry (string user)
+		{
+			return entry + gallery.Replace ("{userid}", user);
+		}
+
+		public static string GetAlbumFeedById (string user, string aid)
+		{
+			return feed + album_by_id.Replace ("{userid}", user).Replace ("{aid}", aid);
+		}
+
+		public static string GetAlbumEntryById (string user, string aid)
+		{
+			return entry + album_by_id.Replace ("{userid}", user).Replace ("{aid}", aid);
+		}
+
+		//public static string GetAlbumByName (string user, string aname)
+		//{
+		//	return album_by_name.Replace ("{userid}", user).Replace ("{aname}", aname);
+		//}
+
+		public static string GetPictureEntry (string user, string aid, string pid)
+		{
+			return entry + picture_by_id.Replace ("{userid}", user).Replace ("{aid}", aid).Replace ("{pid}", pid);
+		}
+
+		public static string GetPictureFeed (string user, string aid, string pid)
+		{
+			return feed + picture_by_id.Replace ("{userid}", user).Replace ("{aid}", aid).Replace ("{pid}", pid);
+		}
+
+		public static string GetPostURL (string user)
+		{
+			return post_url.Replace ("{userid}", user);
+		}
+
+		public static string GetURLForUpload (string user, string aid)
+		{
+			return post_picture.Replace ("{userid}", user).Replace ("{aid}", aid);
+		}
+
+		public static string DateFormat {
+			get { return date_format; }
+		}
+	}
+}

Added: trunk/extensions/Exporters/PicasaWebExport/google-sharp/GoogleConnection.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/PicasaWebExport/google-sharp/GoogleConnection.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,211 @@
+//
+// Mono.Google.GoogleConnection.cs:
+//
+// Authors:
+//	Gonzalo Paniagua Javier (gonzalo ximian com)
+//	Stephane Delcroix (stephane delcroix org)
+//
+// (C) Copyright 2006 Novell, Inc. (http://www.novell.com)
+// (C) Copyright 2007 S. Delcroix
+//
+
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+// Check the Google Authentication Page at http://code.google.com/apis/accounts/AuthForInstalledApps.html
+//
+
+using System;
+using System.Globalization;
+using System.IO;
+using System.Net;
+using System.Text;
+using System.Reflection;
+
+namespace Mono.Google {
+	public class GoogleConnection {
+		string user;
+		GoogleService service;
+		string auth;
+		string appname;
+
+		public GoogleConnection (GoogleService service)
+		{
+			this.service = service;
+		}
+
+		public string ApplicationName {
+			get {
+				if (appname == null) {
+					Assembly assembly = Assembly.GetEntryAssembly ();
+					if (assembly == null)
+						throw new InvalidOperationException ("You need to set GoogleConnection.ApplicationName.");
+					AssemblyName aname = assembly.GetName ();
+					appname = String.Format ("{0}-{1}", aname.Name, aname.Version);
+				}
+
+				return appname;
+			}
+
+			set {
+				if (value == null || value == "")
+					throw new ArgumentException ("Cannot be null or empty", "value");
+
+				appname = value;
+			}
+		}
+
+		public void Authenticate (string user, string password)
+		{
+			if (user == null)
+				throw new ArgumentNullException ("user");
+
+			if (this.user != null)
+				throw new InvalidOperationException (String.Format ("Already authenticated for {0}", this.user));
+
+			this.user = user;
+			this.auth = Authentication.GetAuthorization (this, user, password, service, null, null);
+			if (this.auth == null) {
+				this.user = null;
+				throw new Exception (String.Format ("Authentication failed for user {0}", user));
+			}
+		}
+
+		public void Authenticate (string user, string password, string token, string captcha)
+		{
+			if (user == null)
+				throw new ArgumentNullException ("user");
+
+			if (token == null)
+				throw new ArgumentNullException ("token");
+
+			if (captcha == null)
+				throw new ArgumentNullException ("captcha");
+
+			if (this.user != null)
+				throw new InvalidOperationException (String.Format ("Already authenticated for {0}", this.user));
+
+			this.user = user;
+			this.auth = Authentication.GetAuthorization (this, user, password, service, token, captcha);
+			if (this.auth == null) {
+				this.user = null;
+				throw new Exception (String.Format ("Authentication failed for user {0}", user));
+			}
+		}
+
+		public HttpWebRequest AuthenticatedRequest (string url)
+		{
+			if (url == null)
+				throw new ArgumentNullException ("url");
+
+			HttpWebRequest req = (HttpWebRequest) WebRequest.Create (url);
+			if (auth != null)
+				req.Headers.Add ("Authorization: GoogleLogin auth=" + auth);
+			return req;
+		}
+
+		public string DownloadString (string url)
+		{
+			if (url == null)
+				throw new ArgumentNullException ("url");
+
+			string received = null;
+			HttpWebRequest req = AuthenticatedRequest (url);
+			HttpWebResponse response = (HttpWebResponse) req.GetResponse ();
+			Encoding encoding = Encoding.UTF8;
+			if (response.ContentEncoding != "") {
+				try {
+					encoding = Encoding.GetEncoding (response.ContentEncoding);
+				} catch {}
+			}
+
+			using (Stream stream = response.GetResponseStream ()) {
+				StreamReader sr = new StreamReader (stream, encoding);
+				received = sr.ReadToEnd ();
+			}
+			response.Close ();
+			return received;
+		}
+
+		public byte [] DownloadBytes (string url)
+		{
+			if (url == null)
+				throw new ArgumentNullException ("url");
+
+			HttpWebRequest req = AuthenticatedRequest (url);
+			HttpWebResponse response = (HttpWebResponse) req.GetResponse ();
+			byte [] bytes = null;
+			using (Stream stream = response.GetResponseStream ()) {
+				if (response.ContentLength != -1) {
+					bytes = new byte [response.ContentLength];
+					stream.Read (bytes, 0, bytes.Length);
+				} else {
+					MemoryStream ms = new MemoryStream ();
+					bytes = new byte [4096];
+					int nread;
+					while ((nread = stream.Read (bytes, 0, bytes.Length)) > 0) {
+						ms.Write (bytes, 0, nread);
+						if (nread < bytes.Length)
+							break;
+					}
+					bytes = ms.ToArray ();
+				}
+			}
+			response.Close ();
+
+			return bytes;
+		}
+
+		public void DownloadToStream (string url, Stream output)
+		{
+			if (url == null)
+				throw new ArgumentNullException ("url");
+
+			if (output == null)
+				throw new ArgumentNullException ("output");
+
+			if (!output.CanWrite)
+				throw new ArgumentException ("The stream is not writeable", "output");
+
+			HttpWebRequest req = AuthenticatedRequest (url);
+			HttpWebResponse response = (HttpWebResponse) req.GetResponse ();
+			byte [] bytes = null;
+			using (Stream stream = response.GetResponseStream ()) {
+				bytes = new byte [4096];
+				int nread;
+				while ((nread = stream.Read (bytes, 0, bytes.Length)) > 0) {
+					output.Write (bytes, 0, nread);
+				}
+			}
+			response.Close ();
+		}
+
+		public string User {
+			get { return user; }
+		}
+
+		public GoogleService Service {
+			get { return service; }
+		}
+
+		internal string AuthToken {
+			get { return auth; }
+		}
+	}
+}

Added: trunk/extensions/Exporters/PicasaWebExport/google-sharp/GoogleService.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/PicasaWebExport/google-sharp/GoogleService.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,53 @@
+//
+// Mono.Google.GoogleService.cs:
+//
+// Authors:
+//	Gonzalo Paniagua Javier (gonzalo ximian com)
+//	Stephane Delcroix  (stephane delcroix org)
+//
+// (C) Copyright 2006 Novell, Inc. (http://www.novell.com)
+// (C) Copyright 2007 S. Delcroix
+//
+
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using System;
+namespace Mono.Google {
+	public class GoogleService {
+		public static GoogleService Picasa {
+			get { return new GoogleService ("lh2"); }
+		}
+
+		public static GoogleService Calendar {
+			get { return new GoogleService ("cl"); }
+		}
+
+		string service_code;
+		public string ServiceCode {
+			get { return service_code; }
+		}
+
+		private GoogleService (string service_code)
+		{
+			this.service_code = service_code;
+		}
+	}
+}

Copied: trunk/extensions/Exporters/PicasaWebExport/google-sharp/Makefile.am (from r4368, /trunk/extensions/PicasaWebExport/google-sharp/Makefile.am)
==============================================================================

Added: trunk/extensions/Exporters/PicasaWebExport/google-sharp/MultipartRequest.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/PicasaWebExport/google-sharp/MultipartRequest.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,162 @@
+//
+// Mono.Google.MultipartRequest
+//
+// Authors:
+//	Gonzalo Paniagua Javier (gonzalo ximian com)
+//	Stephane Delcroix (stephane delcroix org)
+//
+// (C) Copyright 2006 Novell, Inc. (http://www.novell.com)
+// (C) Copyright 2007 S. Delcroix
+//
+
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using System;
+using System.Collections;
+using System.Globalization;
+using System.IO;
+using System.Net;
+using System.Text;
+using System.Xml;
+
+namespace Mono.Google {
+	class MultipartRequest {
+		static byte [] crlf = new byte [] { 13, 10 };
+		HttpWebRequest request;
+		Stream output_stream;
+		const string separator_string = "PART_SEPARATOR";
+		const string separator = "--" + separator_string + "\r\n";
+		const string separator_end = "--" + separator_string + "--\r\n";
+		byte [] separator_bytes = Encoding.ASCII.GetBytes (separator);
+		byte [] separator_end_bytes = Encoding.ASCII.GetBytes (separator_end);
+		bool output_set;
+
+		public MultipartRequest (GoogleConnection conn, string url)
+		{
+			request = conn.AuthenticatedRequest (url);
+			request.Method = "POST";
+			request.ContentType = "multipart/related; boundary=\"" + separator_string + "\"";
+			request.Headers.Add ("MIME-version", "1.0");
+		}
+
+		public HttpWebRequest Request {
+			get { return request; }
+		}
+
+		public Stream OutputStream {
+			get { return output_stream; }
+			set {
+				output_set = true;
+				output_stream = value;
+			}
+		}
+
+		public void BeginPart ()
+		{
+			BeginPart (false);
+		}
+
+		public void BeginPart (bool first)
+		{
+			if (!first)
+				return;
+			if (output_stream == null)
+				output_stream = request.GetRequestStream ();
+
+			string multipart = "Media multipart posting\r\n";
+			byte [] multipart_bytes = Encoding.ASCII.GetBytes (multipart);
+			output_stream.Write (multipart_bytes, 0, multipart_bytes.Length);
+			output_stream.Write (separator_bytes, 0, separator_bytes.Length);
+		}
+
+		public void AddHeader (string name, string val)
+		{
+			AddHeader (name, val, false);
+		}
+
+		public void AddHeader (string name, string val, bool last)
+		{
+			AddHeader (String.Format ("{0}: {1}"), last);
+		}
+
+		public void AddHeader (string header)
+		{
+			AddHeader (header, false);
+		}
+
+		public void AddHeader (string header, bool last)
+		{
+			bool need_crlf = !header.EndsWith ("\r\n");
+			byte [] bytes = Encoding.UTF8.GetBytes (header);
+			output_stream.Write (bytes, 0, bytes.Length);
+			if (need_crlf)
+				output_stream.Write (crlf, 0, 2);
+			if (last)
+				output_stream.Write (crlf, 0, 2);
+		}
+
+		public void WriteContent (string content)
+		{
+			WriteContent (Encoding.UTF8.GetBytes (content));
+		}
+
+		public void WriteContent (byte [] content)
+		{
+			output_stream.Write (content, 0, content.Length);
+			output_stream.Write (crlf, 0, crlf.Length);
+		}
+
+		public void WritePartialContent (byte [] content, int offset, int nbytes)
+		{
+			output_stream.Write (content, offset, nbytes);
+		}
+
+		public void EndPartialContent ()
+		{
+			output_stream.Write (crlf, 0, crlf.Length);
+		}
+
+		public void EndPart (bool last)
+		{
+			if (last) {
+				output_stream.Write (separator_end_bytes, 0, separator_end_bytes.Length);
+				if (!output_set)
+					output_stream.Close ();
+			} else {
+				output_stream.Write (separator_bytes, 0, separator_bytes.Length);
+			}
+		}
+
+		public string GetResponseAsString ()
+		{
+			HttpWebResponse response = null;
+			response = (HttpWebResponse) request.GetResponse ();
+			string received = "";
+			// FIXME: use CharacterSet?
+			using (Stream stream = response.GetResponseStream ()) {
+				StreamReader sr = new StreamReader (stream, Encoding.UTF8);
+				received = sr.ReadToEnd ();
+			}
+			response.Close ();
+			return received;
+		}
+	}
+}

Added: trunk/extensions/Exporters/PicasaWebExport/google-sharp/NoCheckCertificatePolicy.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/PicasaWebExport/google-sharp/NoCheckCertificatePolicy.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,40 @@
+//
+// Mono.Google.NoCheckCertificatePolicy
+//
+// Authors:
+//	Gonzalo Paniagua Javier (gonzalo ximian com)
+//
+// (C) Copyright 2006 Novell, Inc. (http://www.novell.com)
+//
+
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using System.Net;
+using System.Security.Cryptography.X509Certificates;
+
+namespace Mono.Google {
+	public class NoCheckCertificatePolicy : ICertificatePolicy {
+		public bool CheckValidationResult (ServicePoint a, X509Certificate b, WebRequest c, int d)
+		{
+			return true;
+		}
+	}
+}

Added: trunk/extensions/Exporters/PicasaWebExport/google-sharp/PicasaAlbum.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/PicasaWebExport/google-sharp/PicasaAlbum.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,337 @@
+//
+// Mono.Google.Picasa.PicasaAlbum.cs:
+//
+// Authors:
+//	Gonzalo Paniagua Javier (gonzalo ximian com)
+//	Stephane Delcroix (stephane delcroix org)
+//
+// (C) Copyright 2006 Novell, Inc. (http://www.novell.com)
+// (C) Copyright 2007 S. Delcroix
+//
+
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using System;
+using System.Collections;
+using System.IO;
+using System.Net;
+using System.Text;
+using System.Xml;
+
+namespace Mono.Google.Picasa {
+	public class PicasaAlbum {
+		GoogleConnection conn;
+		string user;
+		string title;
+		string description;
+		string id;
+		string link;
+		string authkey = null;
+		AlbumAccess access = AlbumAccess.Public;
+		int num_photos = -1;
+		int num_photos_remaining = -1;
+		long bytes_used = -1;
+
+		private PicasaAlbum (GoogleConnection conn)
+		{
+			if (conn == null)
+				throw new ArgumentNullException ("conn");
+
+			this.conn = conn;
+		}
+
+		public PicasaAlbum (GoogleConnection conn, string aid) : this (conn)
+		{
+			if (conn.User == null)
+				throw new ArgumentException ("Need authentication before being used.", "conn");
+
+			if (aid == null || aid == String.Empty)
+				throw new ArgumentNullException ("aid");
+
+			this.user = conn.User;
+			this.id = aid;
+
+			string received = conn.DownloadString (GDataApi.GetAlbumEntryById (conn.User, aid));
+			XmlDocument doc = new XmlDocument ();
+			doc.LoadXml (received);
+			XmlNamespaceManager nsmgr = new XmlNamespaceManager (doc.NameTable);
+			XmlUtil.AddDefaultNamespaces (nsmgr);
+			XmlNode entry = doc.SelectSingleNode ("atom:entry", nsmgr);
+			ParseAlbum (entry, nsmgr);
+		}
+
+		public PicasaAlbum (GoogleConnection conn, string user, string aid, string authkey) : this (conn)
+		{
+			if (user == null || user == String.Empty)
+				throw new ArgumentNullException ("user");
+
+			if (aid == null || aid == String.Empty)
+				throw new ArgumentNullException ("aid");
+
+			this.user = user;
+			this.id = aid;
+			this.authkey = authkey;
+
+			string download_link = GDataApi.GetAlbumEntryById (user, id);
+			if (authkey != null && authkey != "")
+				download_link += "&authkey=" + authkey;
+			string received = conn.DownloadString (download_link);
+
+			XmlDocument doc = new XmlDocument ();
+			doc.LoadXml (received);
+			XmlNamespaceManager nsmgr = new XmlNamespaceManager (doc.NameTable);
+			XmlUtil.AddDefaultNamespaces (nsmgr);
+			XmlNode entry = doc.SelectSingleNode ("atom:entry", nsmgr);
+			ParseAlbum (entry, nsmgr);
+		}
+
+		internal PicasaAlbum (GoogleConnection conn, string user, XmlNode nodeitem, XmlNamespaceManager nsmgr) : this (conn)
+		{
+			this.user = user ?? conn.User;
+
+			ParseAlbum (nodeitem, nsmgr);
+		}
+
+
+		private void ParseAlbum (XmlNode nodeitem, XmlNamespaceManager nsmgr)
+		{
+
+			title = nodeitem.SelectSingleNode ("atom:title", nsmgr).InnerText;
+			description = nodeitem.SelectSingleNode ("media:group/media:description", nsmgr).InnerText;
+			XmlNode node = nodeitem.SelectSingleNode ("gphoto:id", nsmgr);
+			if (node != null)
+				id = node.InnerText;
+
+			foreach (XmlNode xlink in nodeitem.SelectNodes ("atom:link", nsmgr)) {
+				if (xlink.Attributes.GetNamedItem ("rel").Value == "alternate") {
+					link = xlink.Attributes.GetNamedItem ("href").Value;
+					break;
+				}
+			}
+			node = nodeitem.SelectSingleNode ("gphoto:access", nsmgr);
+			if (node != null) {
+				string acc = node.InnerText;
+				access = (acc == "public") ? AlbumAccess.Public : AlbumAccess.Private;
+			}
+			node = nodeitem.SelectSingleNode ("gphoto:numphotos", nsmgr);
+			if (node != null)
+				num_photos = (int) UInt32.Parse (node.InnerText);
+
+			node = nodeitem.SelectSingleNode ("gphoto:numphotosremaining", nsmgr);
+			if (node != null)
+				num_photos_remaining = (int) UInt32.Parse (node.InnerText);
+			node = nodeitem.SelectSingleNode ("gphoto:bytesused", nsmgr);
+			if (node != null)
+				bytes_used = (long) UInt64.Parse (node.InnerText);
+		}
+
+		public PicasaPictureCollection GetPictures ()
+		{
+
+			string download_link = GDataApi.GetAlbumFeedById (user, id);
+			if (authkey != null && authkey != "")
+				download_link += "&authkey=" + authkey;
+			string received = conn.DownloadString (download_link);
+			XmlDocument doc = new XmlDocument ();
+			doc.LoadXml (received);
+			XmlNamespaceManager nsmgr = new XmlNamespaceManager (doc.NameTable);
+			XmlUtil.AddDefaultNamespaces (nsmgr);
+			XmlNode feed = doc.SelectSingleNode ("atom:feed", nsmgr);
+			PicasaPictureCollection coll = new PicasaPictureCollection ();
+			foreach (XmlNode item in feed.SelectNodes ("atom:entry", nsmgr)) {
+				coll.Add (new PicasaPicture (conn, this, item, nsmgr));
+			}
+			coll.SetReadOnly ();
+			return coll;
+		}
+
+		/* from http://code.google.com/apis/picasaweb/gdata.html#Add_Photo
+		<entry xmlns='http://www.w3.org/2005/Atom'>
+		  <title>darcy-beach.jpg</title>
+		  <summary>Darcy on the beach</summary>
+		  <category scheme="http://schemas.google.com/g/2005#kind";
+		    term="http://schemas.google.com/photos/2007#photo"/>
+		</entry>
+		*/
+
+		static string GetXmlForUpload (string title, string description)
+		{
+			XmlUtil xml = new XmlUtil ();
+			xml.WriteElementString ("title", title);
+			xml.WriteElementString ("summary", description);
+			xml.WriteElementStringWithAttributes ("category", null,
+					"scheme", "http://schemas.google.com/g/2005#kind";,
+					"term", "http://schemas.google.com/photos/2007#photo";);
+			return xml.GetDocumentString ();
+		}
+
+		public PicasaPicture UploadPicture (string title, Stream input)
+		{
+			return UploadPicture (title, null, input);
+		}
+
+		public PicasaPicture UploadPicture (string title, string description, Stream input)
+		{
+			return UploadPicture (title, description, "image/jpeg", input);
+		}
+
+		public PicasaPicture UploadPicture (string title, string description, string mime_type, Stream input)
+		{
+			if (title == null)
+				throw new ArgumentNullException ("title");
+
+			if (input == null)
+				throw new ArgumentNullException ("input");
+
+			if (!input.CanRead)
+				throw new ArgumentException ("Cannot read from stream", "input");
+
+			string url = GDataApi.GetURLForUpload (conn.User, id);
+			if (url == null)
+				throw new UnauthorizedAccessException ("You are not authorized to upload to this album.");
+
+			MultipartRequest request = new MultipartRequest (conn, url);
+			MemoryStream ms = null;
+			if (UploadProgress != null) {
+				// We do 'manual' buffering
+				request.Request.AllowWriteStreamBuffering = false;
+				ms = new MemoryStream ();
+				request.OutputStream = ms;
+			}
+
+			request.BeginPart (true);
+			request.AddHeader ("Content-Type: application/atom+xml; \r\n", true);
+			string upload = GetXmlForUpload (title, description);
+			request.WriteContent (upload);
+			request.EndPart (false);
+			request.BeginPart ();
+			request.AddHeader ("Content-Type: " + mime_type + "\r\n", true);
+
+			byte [] data = new byte [8192];
+			int nread;
+			while ((nread = input.Read (data, 0, data.Length)) > 0) {
+				request.WritePartialContent (data, 0, nread);
+			}
+			request.EndPartialContent ();
+			request.EndPart (true); // It won't call Close() on the MemoryStream
+
+			if (UploadProgress != null) {
+				int req_length = (int) ms.Length;
+				request.Request.ContentLength = req_length;
+				DoUploadProgress (title, 0, req_length);
+				using (Stream req_stream = request.Request.GetRequestStream ()) {
+					byte [] buffer = ms.GetBuffer ();
+					int nwrite = 0;
+					int offset;
+					for (offset = 0; offset < req_length; offset += nwrite) {
+						nwrite = System.Math.Min (16384, req_length - offset);
+						req_stream.Write (buffer, offset, nwrite);
+						// The progress uses the actual request size, not file size.
+						DoUploadProgress (title, offset, req_length);
+					}
+					DoUploadProgress (title, offset, req_length);
+
+				}
+			}
+
+			string received = request.GetResponseAsString ();
+			XmlDocument doc = new XmlDocument ();
+			doc.LoadXml (received);
+			XmlNamespaceManager nsmgr = new XmlNamespaceManager (doc.NameTable);
+			XmlUtil.AddDefaultNamespaces (nsmgr);
+			XmlNode entry = doc.SelectSingleNode ("atom:entry", nsmgr);
+
+			return new PicasaPicture (conn, this, entry, nsmgr);
+		}
+
+		public PicasaPicture UploadPicture (string filename)
+		{
+			return UploadPicture (filename, "");
+		}
+
+		public PicasaPicture UploadPicture (string filename, string description)
+		{
+			return UploadPicture (filename, Path.GetFileName (filename), description);
+		}
+
+		public PicasaPicture UploadPicture (string filename, string title, string description)
+		{
+			if (filename == null)
+				throw new ArgumentNullException ("filename");
+
+			if (title == null)
+				throw new ArgumentNullException ("title");
+
+			using (Stream stream = File.OpenRead (filename)) {
+				return UploadPicture (title, description, stream);
+			}
+		}
+
+		public string Title {
+			get { return title; }
+		}
+
+		public string Description {
+			get { return description; }
+		}
+
+		public string Link {
+			get { return link; }
+		}
+
+		public string UniqueID {
+			get { return id; }
+		}
+
+		public AlbumAccess Access {
+			get { return access; }
+		}
+
+		public int PicturesCount {
+			get { return num_photos; }
+		}
+
+		public int PicturesRemaining {
+			get { return num_photos_remaining; }
+		}
+
+		public string User {
+			get { return user; }
+		}
+
+		public long BytesUsed {
+			get { return bytes_used; }
+		}
+
+		internal GoogleConnection Connection {
+			get { return conn; }
+		}
+
+		void DoUploadProgress (string title, long sent, long total)
+		{
+			if (UploadProgress != null) {
+				UploadProgress (this, new UploadProgressEventArgs (title, sent, total));
+			}
+		}
+
+		public event UploadProgressEventHandler UploadProgress;
+	}
+}

Added: trunk/extensions/Exporters/PicasaWebExport/google-sharp/PicasaAlbumCollection.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/PicasaWebExport/google-sharp/PicasaAlbumCollection.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,91 @@
+//
+// Mono.Google.Picasa.PicasaAlbumCollection
+//
+// Author:
+//	Gonzalo Paniagua Javier (gonzalo novell com)
+//
+
+//
+// Copyright (C) 2005 Novell, Inc (http://www.novell.com)
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using System;
+using System.Collections.Specialized;
+
+namespace Mono.Google.Picasa {
+	public sealed class PicasaAlbumCollection : NameObjectCollectionBase {
+		internal PicasaAlbumCollection ()
+		{
+		}
+
+		internal void Add (PicasaAlbum album)
+		{
+			if (BaseGet (album.UniqueID) != null)
+				throw new Exception ("Duplicate album?");
+
+			BaseAdd (album.UniqueID, album);
+		}
+
+		internal void SetReadOnly ()
+		{
+			IsReadOnly = true;
+		}
+
+		public PicasaAlbum GetByTitle (string title)
+		{
+			if (title == null)
+				throw new ArgumentNullException ("id");
+
+			int count = Count;
+			for (int i = 0; i < count; i++) {
+				if (this [i].Title == title)
+					return this [i];
+			}
+
+			return null;
+		}
+
+		public PicasaAlbum this [int index] {
+			get { return (PicasaAlbum) BaseGet (index); }
+		}
+
+		public PicasaAlbum this [string uniqueID] {
+			get { return (PicasaAlbum) BaseGet (uniqueID); }
+		}
+
+		public string [] AllKeys {
+			get {
+				NameObjectCollectionBase.KeysCollection keys = Keys;
+				int count = keys.Count;
+				string [] result = new string [count];
+				for (int i = 0; i < count; i++)
+					result [i] = keys [i];
+
+				return result;
+			}
+		}
+
+		public PicasaAlbum [] AllValues {
+			get { return (PicasaAlbum []) BaseGetAllValues (typeof (PicasaAlbum)); }
+		}
+	}
+}

Added: trunk/extensions/Exporters/PicasaWebExport/google-sharp/PicasaPicture.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/PicasaWebExport/google-sharp/PicasaPicture.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,204 @@
+//
+// Mono.Google.Picasa.PicasaPicture.cs:
+//
+// Authors:
+//	Gonzalo Paniagua Javier (gonzalo ximian com)
+//	Stephane Delcroix (stephane delcroix org)
+//
+// (C) Copyright 2006 Novell, Inc. (http://www.novell.com)
+// (C) Copyright 2007 S. Delcroix
+//
+
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using System;
+using System.IO;
+using System.Net;
+using System.Text;
+using System.Xml;
+using System.Globalization;
+
+namespace Mono.Google.Picasa {
+	public class PicasaPicture {
+		GoogleConnection conn;
+		PicasaAlbum album;
+		string title;
+		string description;
+		DateTime pub_date;
+		string thumbnail_url;
+		string image_url;
+		int width;
+		int height;
+		int index;
+		string id;
+		string link;
+		string tags;
+
+		internal PicasaPicture (GoogleConnection conn, PicasaAlbum album, XmlNode nodeitem, XmlNamespaceManager nsmgr)
+		{
+			this.conn = conn;
+			this.album = album;
+			ParsePicture (nodeitem, nsmgr);
+		}
+
+		public PicasaPicture (GoogleConnection conn, string aid, string pid)
+		{
+			if (conn == null)
+				throw new ArgumentNullException ("conn");
+			if (conn.User == null)
+				throw new ArgumentException ("Need authentication before being used.", "conn");
+			this.conn = conn;
+
+			if (aid == null || aid == String.Empty)
+				throw new ArgumentNullException ("aid");
+			this.album = new PicasaAlbum (conn, aid);
+
+			if (pid == null || pid == String.Empty)
+				throw new ArgumentNullException ("pid");
+
+			string received = conn.DownloadString (GDataApi.GetPictureEntry (conn.User, aid, pid));
+			XmlDocument doc = new XmlDocument ();
+			doc.LoadXml (received);
+			XmlNamespaceManager nsmgr = new XmlNamespaceManager (doc.NameTable);
+			XmlUtil.AddDefaultNamespaces (nsmgr);
+			XmlNode entry = doc.SelectSingleNode ("atom:entry", nsmgr);
+			ParsePicture (entry, nsmgr);
+		}
+
+		private void ParsePicture (XmlNode nodeitem, XmlNamespaceManager nsmgr)
+		{
+			title = nodeitem.SelectSingleNode ("atom:title", nsmgr).InnerText;
+			foreach (XmlNode xlink in nodeitem.SelectNodes ("atom:link", nsmgr)) {
+				if (xlink.Attributes.GetNamedItem ("rel").Value == "alternate") {
+					link = xlink.Attributes.GetNamedItem ("href").Value;
+					break;
+				}
+			}
+			description = nodeitem.SelectSingleNode ("media:group/media:description", nsmgr).InnerText;
+			CultureInfo info = CultureInfo.InvariantCulture;
+			pub_date = DateTime.ParseExact (nodeitem.SelectSingleNode ("atom:published", nsmgr).InnerText, GDataApi.DateFormat, info);
+			thumbnail_url = nodeitem.SelectSingleNode ("media:group/media:thumbnail", nsmgr).Attributes.GetNamedItem ("url").Value;
+			image_url = nodeitem.SelectSingleNode ("media:group/media:content", nsmgr).Attributes.GetNamedItem ("url").Value;
+			width = (int) UInt32.Parse (nodeitem.SelectSingleNode ("gphoto:width", nsmgr).InnerText);
+			height = (int) UInt32.Parse (nodeitem.SelectSingleNode ("gphoto:height", nsmgr).InnerText);
+
+			XmlNode node = nodeitem.SelectSingleNode ("gphoto:index", nsmgr);
+			index = (node != null) ? (int) UInt32.Parse (node.InnerText) : -1;
+			node = nodeitem.SelectSingleNode ("gphoto:id", nsmgr);
+			id = (node != null) ? node.InnerText : "auto" + title.GetHashCode ().ToString ();
+			tags = nodeitem.SelectSingleNode ("media:group/media:keywords", nsmgr).InnerText;
+		}
+
+		static string GetXmlForTagging (string tag)
+		{
+			XmlUtil xml = new XmlUtil ();
+			xml.WriteElementString ("title", tag);
+			xml.WriteElementStringWithAttributes ("category", null,
+					"scheme", "http://schemas.google.com/g/2005#kind";,
+					"term", "http://schemas.google.com/photos/2007#tag";);
+			return xml.GetDocumentString ();
+
+		}
+
+		public void AddTag (string tag)
+		{
+			if (tag == null)
+				throw new ArgumentNullException ("title");
+
+			string op_string = GetXmlForTagging (tag);
+			byte [] op_bytes = Encoding.UTF8.GetBytes (op_string);
+			string url = GDataApi.GetPictureFeed (conn.User, album.UniqueID, UniqueID);
+			HttpWebRequest request = conn.AuthenticatedRequest (url);
+			request.ContentType = "application/atom+xml; charset=UTF-8";
+			request.Method = "POST";
+			Stream output_stream = request.GetRequestStream ();
+			output_stream.Write (op_bytes, 0, op_bytes.Length);
+			output_stream.Close ();
+
+			HttpWebResponse response = (HttpWebResponse) request.GetResponse ();
+			string received = "";
+			using (Stream stream = response.GetResponseStream ()) {
+				StreamReader sr = new StreamReader (stream, Encoding.UTF8);
+				received = sr.ReadToEnd ();
+			}
+			response.Close ();
+
+		}
+
+		public void DownloadToStream (Stream stream)
+		{
+			conn.DownloadToStream (image_url, stream);
+		}
+
+		public void DownloadThumbnailToStream (Stream stream)
+		{
+			conn.DownloadToStream (thumbnail_url, stream);
+		}
+
+		public PicasaAlbum Album {
+			get { return album; }
+		}
+
+		public string Title {
+			get { return title; }
+		}
+
+		public string Description {
+			get { return description; }
+		}
+
+		public DateTime Date {
+			get { return pub_date; }
+		}
+
+		public string ThumbnailURL {
+			get { return thumbnail_url; }
+		}
+
+		public string ImageURL {
+			get { return image_url; }
+		}
+
+		public int Width {
+			get { return width; }
+		}
+
+		public int Height {
+			get { return height; }
+		}
+
+		public int Index {
+			get { return index; }
+		}
+
+		public string UniqueID {
+			get { return id; }
+		}
+
+		public string Link {
+			get { return link; }
+		}
+
+		public string Tags {
+			get { return tags; }
+		}
+	}
+}

Added: trunk/extensions/Exporters/PicasaWebExport/google-sharp/PicasaPictureCollection.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/PicasaWebExport/google-sharp/PicasaPictureCollection.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,93 @@
+//
+// Mono.Google.Picasa.PicasaPictureCollection
+//
+// Author:
+//	Gonzalo Paniagua Javier (gonzalo novell com)
+//
+
+//
+// Copyright (C) 2006 Novell, Inc (http://www.novell.com)
+//
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using System;
+using System.Collections;
+using System.Collections.Specialized;
+
+namespace Mono.Google.Picasa {
+	public sealed class PicasaPictureCollection : NameObjectCollectionBase {
+		internal PicasaPictureCollection ()
+		{
+		}
+
+		internal void Add (PicasaPicture picture)
+		{
+			if (BaseGet (picture.UniqueID) != null)
+				throw new Exception ("Duplicate picture?");
+
+			BaseAdd (picture.UniqueID, picture);
+		}
+
+		internal void SetReadOnly ()
+		{
+			IsReadOnly = true;
+		}
+
+		public PicasaPicture GetByTitle (string title)
+		{
+			if (title == null)
+				throw new ArgumentNullException ("id");
+
+			int count = Count;
+			for (int i = 0; i < count; i++) {
+				if (this [i].Title == title)
+					return this [i];
+			}
+
+			return null;
+		}
+
+		public PicasaPicture this [int index] {
+			get { return (PicasaPicture) BaseGet (index); }
+		}
+
+		public PicasaPicture this [string uniqueID] {
+			get { return (PicasaPicture) BaseGet (uniqueID); }
+		}
+
+		public string [] AllKeys {
+			get {
+				NameObjectCollectionBase.KeysCollection keys = Keys;
+				int count = keys.Count;
+				string [] result = new string [count];
+				for (int i = 0; i < count; i++)
+					result [i] = keys [i];
+
+				return result;
+			}
+		}
+
+		public PicasaPicture [] AllValues {
+			get { return (PicasaPicture []) BaseGetAllValues (typeof (PicasaPicture)); }
+		}
+
+	}
+}

Added: trunk/extensions/Exporters/PicasaWebExport/google-sharp/PicasaWeb.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/PicasaWebExport/google-sharp/PicasaWeb.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,290 @@
+//
+// Mono.Google.Picasa.PicasaWeb.cs:
+//
+// Authors:
+//	Gonzalo Paniagua Javier (gonzalo ximian com)
+//	Stephane Delcroix  (stephane delcroix org)
+//
+// (C) Copyright 2006 Novell, Inc. (http://www.novell.com)
+// (C) Copyright 2007 S. Delcroix
+//
+
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+// Check Picasa Web Albums Data Api at http://code.google.com/apis/picasaweb/gdata.html
+//
+
+using System;
+using System.Collections;
+using System.Globalization;
+using System.IO;
+using System.Net;
+using System.Text;
+using System.Xml;
+
+namespace Mono.Google.Picasa {
+	public class PicasaWeb {
+		GoogleConnection conn;
+		string title;
+		string user;
+		string nickname;
+		long quota_used;
+		long quota_limit;
+
+		public PicasaWeb (GoogleConnection conn) : this (conn, null)
+		{
+		}
+
+		public PicasaWeb (GoogleConnection conn, string username)
+		{
+			if (conn == null)
+				throw new ArgumentNullException ("conn");
+
+			if (conn.User == null && username == null)
+				throw new ArgumentException ("The connection should be authenticated OR you should call this constructor with a non-null username argument");
+
+			this.conn = conn;
+			this.user = username ?? conn.User;
+
+			string received = conn.DownloadString (GDataApi.GetGalleryEntry (user));
+			XmlDocument doc = new XmlDocument ();
+			doc.LoadXml (received);
+			XmlNamespaceManager nsmgr = new XmlNamespaceManager (doc.NameTable);
+			XmlUtil.AddDefaultNamespaces (nsmgr);
+			XmlNode entry = doc.SelectSingleNode ("atom:entry", nsmgr);
+			ParseGallery (entry, nsmgr);
+		}
+
+		public string Title {
+			get { return title; }
+		}
+
+		public string User {
+			get { return user; }
+		}
+
+		public string NickName {
+			get { return nickname; }
+		}
+
+		public long QuotaUsed {
+			get { return quota_used; }
+		}
+
+		public long QuotaLimit {
+			get { return quota_limit; }
+		}
+
+		internal GoogleConnection Connection {
+			get { return conn; }
+		}
+
+		public PicasaAlbum CreateAlbum (string title)
+		{
+			return CreateAlbum (title, null);
+		}
+
+		public PicasaAlbum CreateAlbum (string title, AlbumAccess access)
+		{
+			return CreateAlbum (title, null, access, DateTime.Now);
+		}
+
+		public PicasaAlbum CreateAlbum (string title, string description)
+		{
+			return CreateAlbum (title, description, AlbumAccess.Public);
+		}
+
+		public PicasaAlbum CreateAlbum (string title, string description, AlbumAccess access)
+		{
+			return CreateAlbum (title, description, access, DateTime.Now);
+		}
+
+		public PicasaAlbum CreateAlbum (string title, string description, AlbumAccess access, DateTime pubDate)
+		{
+			if (title == null)
+				throw new ArgumentNullException ("title");
+
+			if (description == null)
+				description = "";
+
+			if (access != AlbumAccess.Public && access != AlbumAccess.Private)
+				throw new ArgumentException ("Invalid value.", "access");
+
+			// Check if pubDate can be in the past
+			string url = GDataApi.GetPostURL (conn.User);
+			if (url == null)
+				throw new UnauthorizedAccessException ("You are not authorized to create albums.");
+			string op_string = GetXmlForCreate (title, description, pubDate, access);
+			byte [] op_bytes = Encoding.UTF8.GetBytes (op_string);
+			HttpWebRequest request = conn.AuthenticatedRequest (url);
+			request.ContentType = "application/atom+xml; charset=UTF-8";
+			request.Method = "POST";
+			Stream output_stream = request.GetRequestStream ();
+			output_stream.Write (op_bytes, 0, op_bytes.Length);
+			output_stream.Close ();
+
+			HttpWebResponse response = (HttpWebResponse) request.GetResponse ();
+			string received = "";
+			using (Stream stream = response.GetResponseStream ()) {
+				StreamReader sr = new StreamReader (stream, Encoding.UTF8);
+				received = sr.ReadToEnd ();
+			}
+			response.Close ();
+
+			XmlDocument doc = new XmlDocument ();
+			doc.LoadXml (received);
+			XmlNamespaceManager nsmgr = new XmlNamespaceManager (doc.NameTable);
+			XmlUtil.AddDefaultNamespaces (nsmgr);
+			XmlNode entry = doc.SelectSingleNode ("atom:entry", nsmgr);
+			return new PicasaAlbum (conn, null, entry, nsmgr);
+		}
+
+		/* (from gdata documentation...)
+		<entry xmlns='http://www.w3.org/2005/Atom'
+		    xmlns:media='http://search.yahoo.com/mrss/'
+		    xmlns:gphoto='http://schemas.google.com/photos/2007'>
+		  <title type='text'>Trip To Italy</title>
+		  <summary type='text'>This was the recent trip I took to Italy.</summary>
+		  <gphoto:location>Italy</gphoto:location>
+		  <gphoto:access>public</gphoto:access>
+		  <gphoto:commentingEnabled>true</gphoto:commentingEnabled>
+		  <gphoto:timestamp>1152255600000</gphoto:timestamp>
+		  <media:group>
+		    <media:keywords>italy, vacation</media:keywords>
+		  </media:group>
+		  <category scheme='http://schemas.google.com/g/2005#kind'
+		    term='http://schemas.google.com/photos/2007#album'></category>
+		</entry>
+		*/
+
+		private static string GetXmlForCreate (string title, string desc, DateTime date, AlbumAccess access)
+		{
+			XmlUtil xml = new XmlUtil ();
+			xml.WriteElementStringWithAttributes ("title", title, "type", "text");
+			xml.WriteElementStringWithAttributes ("summary", desc, "type", "text");
+			// location ?
+			xml.WriteElementString ("access", access.ToString ().ToLower (CultureInfo.InvariantCulture), PicasaNamespaces.GPhoto);
+			// commentingEnabled ?
+			xml.WriteElementString ("timestamp", ((long)(date - new DateTime (1970, 1, 1)).TotalSeconds * 1000).ToString (), PicasaNamespaces.GPhoto);
+			//keywords ?
+			xml.WriteElementStringWithAttributes ("category", null,
+					"scheme", "http://schemas.google.com/g/2005#kind";,
+					"term", "http://schemas.google.com/photos/2007#album";);
+
+			return xml.GetDocumentString ();
+		}
+
+		/*
+			"<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n" +
+			"<rss version=\"2.0\" xmlns:gphoto=\"http://www.temp.com/\";>\n" +
+			"  <channel>\n" +
+			"    <gphoto:user>{0}</gphoto:user>\n" +
+			"    <gphoto:id>{1}</gphoto:id>\n" +
+			"    <gphoto:op>deleteAlbum</gphoto:op>\n" +
+			"  </channel>\n" +
+			"</rss>";
+		*/
+
+//		static string GetXmlForDelete (string user, string aid)
+//		{
+//			XmlUtil xml = new XmlUtil ();
+//			xml.WriteElementString ("user", user, PicasaNamespaces.GPhoto);
+//			xml.WriteElementString ("id", aid, PicasaNamespaces.GPhoto);
+//			xml.WriteElementString ("op", "deleteAlbum", PicasaNamespaces.GPhoto);
+//			return xml.GetDocumentString ();
+//		}
+
+		public void DeleteAlbum (PicasaAlbum album)
+		{
+			if (album == null)
+				throw new ArgumentNullException ("album");
+
+			DeleteAlbum (album.UniqueID);
+		}
+
+		public void DeleteAlbum (string unique_id)
+		{
+	//FIXME: implement this
+			throw new System.NotImplementedException ("but I'll implemented if you say please...");
+//			if (unique_id == null)
+//				throw new ArgumentNullException ("unique_id");
+//
+//			string url = GDataApi.GetPostURL (conn.User);
+//			if (url == null)
+//				throw new UnauthorizedAccessException ("You are not authorized to delete this album.");
+//			string op_string = GetXmlForDelete (conn.User, unique_id);
+//			byte [] op_bytes = Encoding.UTF8.GetBytes (op_string);
+//			MultipartRequest request = new MultipartRequest (conn, url);
+////			request.Request.CookieContainer = conn.Cookies;
+//			request.BeginPart ();
+//			request.AddHeader ("Content-Disposition: form-data; name=\"xml\"\r\n");
+//			request.AddHeader ("Content-Type: text/plain; charset=utf8\r\n", true);
+//			request.WriteContent (op_bytes);
+//			request.EndPart (true);
+//			string received = request.GetResponseAsString ();
+//
+//			XmlDocument doc = new XmlDocument ();
+//			doc.LoadXml (received);
+//			XmlNode node = doc.SelectSingleNode ("/response/result");
+//			if (node == null)
+//				throw new DeleteAlbumException ("Invalid response from server");
+//
+//			if (node.InnerText != "success") {
+//				node = doc.SelectSingleNode ("/response/reason");
+//				if (node == null)
+//					throw new DeleteAlbumException ("Unknown reason");
+//
+//				throw new DeleteAlbumException (node.InnerText);
+//			}
+		}
+
+		public PicasaAlbumCollection GetAlbums ()
+		{
+			string gallery_link = GDataApi.GetGalleryFeed (user);
+			string received = conn.DownloadString (gallery_link);
+
+			XmlDocument doc = new XmlDocument ();
+			doc.LoadXml (received);
+			XmlNamespaceManager nsmgr = new XmlNamespaceManager (doc.NameTable);
+			XmlUtil.AddDefaultNamespaces (nsmgr);
+			XmlNode feed = doc.SelectSingleNode ("atom:feed", nsmgr);
+			PicasaAlbumCollection coll = new PicasaAlbumCollection ();
+			foreach (XmlNode item in feed.SelectNodes ("atom:entry", nsmgr)) {
+				coll.Add (new PicasaAlbum (conn, user, item, nsmgr));
+			}
+			coll.SetReadOnly ();
+			return coll;
+		}
+
+		private void ParseGallery (XmlNode nodeitem, XmlNamespaceManager nsmgr)
+		{
+			XmlNode node = nodeitem.SelectSingleNode ("atom:title", nsmgr);
+			title = node.InnerText;
+			node = nodeitem.SelectSingleNode ("gphoto:user", nsmgr);
+			user = node.InnerText;
+			node = nodeitem.SelectSingleNode ("gphoto:nickname", nsmgr);
+			nickname = node.InnerText;
+			node = nodeitem.SelectSingleNode ("gphoto:quotacurrent", nsmgr);
+			quota_used = (node != null) ? (long) UInt64.Parse (node.InnerText) : -1;
+			node = nodeitem.SelectSingleNode ("gphoto:quotalimit", nsmgr);
+			quota_limit = (node != null) ? (long) UInt64.Parse (node.InnerText) : -1;
+		}
+	}
+}

Added: trunk/extensions/Exporters/PicasaWebExport/google-sharp/UploadPictureException.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/PicasaWebExport/google-sharp/UploadPictureException.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,43 @@
+//
+// Mono.Google.Picasa.UploadPictureException.cs:
+//
+// Authors:
+//	Gonzalo Paniagua Javier (gonzalo ximian com)
+//
+// (C) Copyright 2006 Novell, Inc. (http://www.novell.com)
+//
+
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using System;
+using System.Runtime.Serialization;
+namespace Mono.Google.Picasa {
+	public class UploadPictureException : Exception {
+		public UploadPictureException (string msg) : base (msg)
+		{
+		}
+
+		protected UploadPictureException (SerializationInfo info, StreamingContext context)
+			: base (info, context)
+		{
+		}
+	}
+}

Added: trunk/extensions/Exporters/PicasaWebExport/google-sharp/UploadProgressEventArgs.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/PicasaWebExport/google-sharp/UploadProgressEventArgs.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,56 @@
+//
+// Mono.Google.Picasa.UploadProgressEventArgs
+//
+// Authors:
+//	Gonzalo Paniagua Javier (gonzalo ximian com)
+//
+// (C) Copyright 2006 Novell, Inc. (http://www.novell.com)
+//
+
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using System;
+namespace Mono.Google.Picasa {
+	public sealed class UploadProgressEventArgs : EventArgs {
+		string title;
+		long bytes_sent;
+		long bytes_total;
+
+		internal UploadProgressEventArgs (string title, long bytes_sent, long bytes_total)
+		{
+			this.title = title;
+			this.bytes_sent = bytes_sent;
+			this.bytes_total = bytes_total;
+		}
+
+		public string Title {
+			get { return title; }
+		}
+
+		public long BytesSent {
+			get { return bytes_sent; }
+		}
+
+		public long BytesTotal {
+			get { return bytes_total; }
+		}
+	}
+}

Added: trunk/extensions/Exporters/PicasaWebExport/google-sharp/UploadProgressEventHandler.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/PicasaWebExport/google-sharp/UploadProgressEventHandler.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,32 @@
+//
+// Mono.Google.Picasa.UploadProgressEventHandler.cs:
+//
+// Authors:
+//	Gonzalo Paniagua Javier (gonzalo ximian com)
+//
+// (C) Copyright 2006 Novell, Inc. (http://www.novell.com)
+//
+
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+namespace Mono.Google.Picasa {
+	public delegate void UploadProgressEventHandler (object sender, UploadProgressEventArgs args);
+}

Added: trunk/extensions/Exporters/PicasaWebExport/google-sharp/XmlUtil.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/PicasaWebExport/google-sharp/XmlUtil.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,120 @@
+//
+// Mono.Google.Picasa.XmlUtil.cs:
+//
+// Authors:
+//	Gonzalo Paniagua Javier (gonzalo ximian com)
+//	Stephane Delcroix (stephane delcroix org)
+//
+// (C) Copyright 2006 Novell, Inc. (http://www.novell.com)
+// (C) Copyright 2007 S. Delcroix
+//
+
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+// Check Picasa Web Albums Data Api at http://code.google.com/apis/picasaweb/gdata.html
+//
+
+using System;
+using System.IO;
+using System.Text;
+using System.Xml;
+namespace Mono.Google.Picasa {
+	enum PicasaNamespaces {
+		None,
+		Atom,
+		GPhoto,
+		Photo,
+		Media
+	}
+
+	internal class XmlUtil {
+		static string [] URIs = new string [] {
+			null,
+			"http://www.w3.org/2005/Atom";,
+			"http://schemas.google.com/photos/2007";,
+			"http://www.pheed.com/pheed/";,
+			"http://search.yahoo.com/mrss/";
+		};
+
+		StringWriter sr;
+		XmlTextWriter writer;
+
+		public XmlUtil ()
+		{
+			StartDocument ();
+		}
+
+		public void StartDocument ()
+		{
+			sr = new StringWriter ();
+			writer = new XmlTextWriter (sr);
+			writer.Formatting = Formatting.Indented;
+			writer.Indentation = 2;
+			writer.Namespaces = true;
+			writer.WriteStartElement (null, "entry", null);
+			writer.WriteAttributeString ("xmlns", null, URIs [(int) PicasaNamespaces.Atom]);
+			writer.WriteAttributeString ("xmlns", "media", null, URIs [(int) PicasaNamespaces.Media]);
+			writer.WriteAttributeString ("xmlns", "gphoto", null, URIs [(int) PicasaNamespaces.GPhoto]);
+		}
+
+		public void WriteElementString (string name, string val)
+		{
+			writer.WriteElementString (name, val);
+		}
+
+		public void WriteElementString (string name, string val, string uri)
+		{
+			writer.WriteElementString (name, uri, val);
+		}
+
+		public void WriteElementString (string name, string val, PicasaNamespaces ns)
+		{
+			writer.WriteElementString (name, URIs [(int) ns], val);
+		}
+
+		public void WriteStartElement (string elem)
+		{
+			writer.WriteStartElement (elem, null);
+		}
+
+		public void WriteElementStringWithAttributes (string ename, string eval, params string[] attributes)
+		{
+			writer.WriteStartElement (null, ename, null);
+			for (int i = 0; i < attributes.Length; i+=2)
+				writer.WriteAttributeString (attributes[i], attributes[i+1]);
+			writer.WriteRaw (eval);
+			writer.WriteEndElement ();
+		}
+
+		public string GetDocumentString ()
+		{
+			writer.Close ();
+			return sr.ToString ();
+		}
+
+		internal static void AddDefaultNamespaces (XmlNamespaceManager nsmgr) {
+			nsmgr.AddNamespace ("atom", "http://www.w3.org/2005/Atom";);
+			nsmgr.AddNamespace ("photo", "http://www.pheed.com/pheed/";);
+			nsmgr.AddNamespace ("media", "http://search.yahoo.com/mrss/";);
+			nsmgr.AddNamespace ("gphoto", "http://schemas.google.com/photos/2007";);
+			nsmgr.AddNamespace ("batch", "http://schemas.google.com/gdata/batch";);
+		}
+	}
+}

Copied: trunk/extensions/Exporters/SmugMugExport/.gitignore (from r4368, /trunk/extensions/PicasaWebExport/.gitignore)
==============================================================================

Copied: trunk/extensions/Exporters/SmugMugExport/Makefile.am (from r4368, /trunk/extensions/SmugMugExport/Makefile.am)
==============================================================================

Copied: trunk/extensions/Exporters/SmugMugExport/SmugMugExport.addin.xml (from r4368, /trunk/extensions/SmugMugExport/SmugMugExport.addin.xml)
==============================================================================

Added: trunk/extensions/Exporters/SmugMugExport/SmugMugExport.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/SmugMugExport/SmugMugExport.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,819 @@
+/*
+ * SmugMugExport.cs
+ *
+ * Authors:
+ *   Thomas Van Machelen <thomas vanmachelen gmail com>
+ *
+ * Based on PicasaWebExport code from Stephane Delcroix.
+ *
+ * Copyright (C) 2006 Thomas Van Machelen
+ */
+
+using System;
+using System.Net;
+using System.IO;
+using System.Text;
+using System.Threading;
+using System.Collections;
+using System.Collections.Specialized;
+using System.Web;
+using Mono.Unix;
+using Gtk;
+
+using FSpot;
+using FSpot.Filters;
+using FSpot.Widgets;
+using FSpot.Utils;
+
+using Gnome.Keyring;
+using SmugMugNet;
+
+namespace FSpotSmugMugExport {
+	public class SmugMugAccount {
+		private string username;
+		private string password;
+		private SmugMugApi smugmug_proxy;
+
+		public SmugMugAccount (string username, string password)
+		{
+			this.username = username;
+			this.password = password;
+		}
+
+		public SmugMugApi Connect ()
+		{
+			System.Console.WriteLine ("SmugMug.Connect() {0}", username);
+			SmugMugApi proxy = new SmugMugApi (username, password);
+			ServicePointManager.CertificatePolicy = new NoCheckCertificatePolicy ();
+			proxy.Login ();
+
+			this.smugmug_proxy = proxy;
+			return proxy;
+		}
+
+		private void MarkChanged()
+		{
+			smugmug_proxy = null;
+		}
+
+		public bool Connected {
+			get {
+				return (smugmug_proxy != null && smugmug_proxy.Connected);
+			}
+		}
+
+		public string Username {
+			get {
+				return username;
+			}
+			set {
+				if (username != value) {
+					username = value;
+					MarkChanged ();
+				}
+			}
+		}
+
+		public string Password {
+			get {
+				return password;
+			}
+			set {
+				if (password != value) {
+					password = value;
+					MarkChanged ();
+				}
+			}
+		}
+
+		public SmugMugApi SmugMug {
+			get {
+				return smugmug_proxy;
+			}
+		}
+	}
+
+
+	public class SmugMugAccountManager
+	{
+		private static SmugMugAccountManager instance;
+		private const string keyring_item_name = "SmugMug Account";
+		ArrayList accounts;
+
+		public delegate void AccountListChangedHandler (SmugMugAccountManager manager, SmugMugAccount changed_account);
+		public event AccountListChangedHandler AccountListChanged;
+
+		public static SmugMugAccountManager GetInstance ()
+		{
+			if (instance == null) {
+				instance = new SmugMugAccountManager ();
+			}
+
+			return instance;
+		}
+
+		private SmugMugAccountManager ()
+		{
+			accounts = new ArrayList ();
+			ReadAccounts ();
+		}
+
+		public void MarkChanged ()
+		{
+			MarkChanged (true, null);
+		}
+
+		public void MarkChanged (bool write, SmugMugAccount changed_account)
+		{
+			if (write)
+				WriteAccounts ();
+
+			if (AccountListChanged != null)
+				AccountListChanged (this, changed_account);
+		}
+
+		public ArrayList GetAccounts ()
+		{
+			return accounts;
+		}
+
+		public void AddAccount (SmugMugAccount account)
+		{
+			AddAccount (account, true);
+		}
+
+		public void AddAccount (SmugMugAccount account, bool write)
+		{
+			accounts.Add (account);
+			MarkChanged (write, account);
+		}
+
+		public void RemoveAccount (SmugMugAccount account)
+		{
+			string keyring = Ring.GetDefaultKeyring();
+			Hashtable request_attributes = new Hashtable();
+			request_attributes["name"] = keyring_item_name;
+			request_attributes["username"] = account.Username;
+			try {
+				foreach(ItemData result in Ring.Find(ItemType.GenericSecret, request_attributes)) {
+					Ring.DeleteItem(keyring, result.ItemID);
+				}
+			} catch (Exception e) {
+				Console.WriteLine(e);
+			}
+			accounts.Remove (account);
+			MarkChanged ();
+		}
+
+		public void WriteAccounts ()
+		{
+			string keyring = Ring.GetDefaultKeyring();
+			foreach (SmugMugAccount account in accounts) {
+				Hashtable update_request_attributes = new Hashtable();
+				update_request_attributes["name"] = keyring_item_name;
+				update_request_attributes["username"] = account.Username;
+
+				Ring.CreateItem(keyring, ItemType.GenericSecret, keyring_item_name, update_request_attributes, account.Password, true);
+			}
+		}
+
+		private void ReadAccounts ()
+		{
+
+			Hashtable request_attributes = new Hashtable();
+			request_attributes["name"] = keyring_item_name;
+			try {
+				foreach(ItemData result in Ring.Find(ItemType.GenericSecret, request_attributes)) {
+					if(!result.Attributes.ContainsKey("name") || !result.Attributes.ContainsKey("username") ||
+						(result.Attributes["name"] as string) != keyring_item_name)
+						continue;
+
+					string username = (string)result.Attributes["username"];
+					string password = result.Secret;
+
+					if (username == null || username == String.Empty || password == null || password == String.Empty)
+						throw new ApplicationException ("Invalid username/password in keyring");
+
+					SmugMugAccount account = new SmugMugAccount(username, password);
+					if (account != null)
+						AddAccount (account, false);
+
+				}
+			} catch (Exception e) {
+				Console.Error.WriteLine(e);
+			}
+
+			MarkChanged ();
+		}
+	}
+
+	public class SmugMugAccountDialog {
+		public SmugMugAccountDialog (Gtk.Window parent) : this (parent, null) {
+			Dialog.Response += HandleAddResponse;
+			add_button.Sensitive = false;
+		}
+
+		public SmugMugAccountDialog (Gtk.Window parent, SmugMugAccount account)
+		{
+			xml = new Glade.XML (null, "SmugMugExport.glade", dialog_name, "f-spot");
+			xml.Autoconnect (this);
+
+			Dialog.Modal = false;
+			Dialog.TransientFor = parent;
+			Dialog.DefaultResponse = Gtk.ResponseType.Ok;
+
+			this.account = account;
+
+			password_entry.ActivatesDefault = true;
+			username_entry.ActivatesDefault = true;
+
+			if (account != null) {
+				password_entry.Text = account.Password;
+				username_entry.Text = account.Username;
+				add_button.Label = Gtk.Stock.Ok;
+				Dialog.Response += HandleEditResponse;
+			}
+
+			if (remove_button != null)
+				remove_button.Visible = account != null;
+
+			this.Dialog.Show ();
+
+			password_entry.Changed += HandleChanged;
+			username_entry.Changed += HandleChanged;
+			HandleChanged (null, null);
+		}
+
+		private void HandleChanged (object sender, System.EventArgs args)
+		{
+			password = password_entry.Text;
+			username = username_entry.Text;
+
+			add_button.Sensitive = !(password == String.Empty || username == String.Empty);
+		}
+
+		[GLib.ConnectBefore]
+		protected void HandleAddResponse (object sender, Gtk.ResponseArgs args)
+		{
+			if (args.ResponseId == Gtk.ResponseType.Ok) {
+				SmugMugAccount account = new SmugMugAccount (username, password);
+				SmugMugAccountManager.GetInstance ().AddAccount (account);
+			}
+			Dialog.Destroy ();
+		}
+
+		protected void HandleEditResponse (object sender, Gtk.ResponseArgs args)
+		{
+			if (args.ResponseId == Gtk.ResponseType.Ok) {
+				account.Username = username;
+				account.Password = password;
+				SmugMugAccountManager.GetInstance ().MarkChanged (true, account);
+			} else if (args.ResponseId == Gtk.ResponseType.Reject) {
+				// NOTE we are using Reject to signal the remove action.
+				SmugMugAccountManager.GetInstance ().RemoveAccount (account);
+			}
+			Dialog.Destroy ();
+		}
+
+		private Gtk.Dialog Dialog {
+			get {
+				if (dialog == null)
+					dialog = (Gtk.Dialog) xml.GetWidget (dialog_name);
+
+				return dialog;
+			}
+		}
+
+		private SmugMugAccount account;
+		private string password;
+		private string username;
+		private string dialog_name = "smugmug_add_dialog";
+		private Glade.XML xml;
+
+		// widgets
+		[Glade.Widget] Gtk.Dialog dialog;
+		[Glade.Widget] Gtk.Entry password_entry;
+		[Glade.Widget] Gtk.Entry username_entry;
+
+		[Glade.Widget] Gtk.Button add_button;
+		[Glade.Widget] Gtk.Button remove_button;
+	}
+
+	public class SmugMugAddAlbum {
+		//[Glade.Widget] Gtk.OptionMenu album_optionmenu;
+
+		[Glade.Widget] Gtk.Dialog dialog;
+		[Glade.Widget] Gtk.Entry title_entry;
+		[Glade.Widget] Gtk.CheckButton public_check;
+		[Glade.Widget] Gtk.ComboBox category_combo;
+
+		[Glade.Widget] Gtk.Button add_button;
+
+		private string dialog_name = "smugmug_add_album_dialog";
+		private Glade.XML xml;
+		private SmugMugExport export;
+		private SmugMugApi smugmug;
+		private string title;
+		private ListStore category_store;
+
+		public SmugMugAddAlbum (SmugMugExport export, SmugMugApi smugmug)
+		{
+			xml = new Glade.XML (null, "SmugMugExport.glade", dialog_name, "f-spot");
+			xml.Autoconnect (this);
+
+			this.export = export;
+			this.smugmug = smugmug;
+
+			this.category_store = new ListStore (typeof(int), typeof(string));
+			CellRendererText display_cell = new CellRendererText();
+			category_combo.PackStart (display_cell, true);
+			category_combo.SetCellDataFunc (display_cell, new CellLayoutDataFunc (CategoryDataFunc));
+			this.category_combo.Model = category_store;
+			PopulateCategoryCombo ();
+
+			Dialog.Response += HandleAddResponse;
+
+			title_entry.Changed += HandleChanged;
+			HandleChanged (null, null);
+		}
+
+		private void HandleChanged (object sender, EventArgs args)
+		{
+			title = title_entry.Text;
+
+			if (title == String.Empty)
+				add_button.Sensitive = false;
+			else
+				add_button.Sensitive = true;
+		}
+
+		[GLib.ConnectBefore]
+		protected void HandleAddResponse (object sender, Gtk.ResponseArgs args)
+		{
+			if (args.ResponseId == Gtk.ResponseType.Ok) {
+				smugmug.CreateAlbum (title, CurrentCategoryId, public_check.Active);
+				export.HandleAlbumAdded (title);
+			}
+			Dialog.Destroy ();
+		}
+
+		void CategoryDataFunc (CellLayout layout, CellRenderer renderer, TreeModel model, TreeIter iter)
+		{
+			string name = (string)model.GetValue (iter, 1);
+			(renderer as CellRendererText).Text = name;
+		}
+
+		protected void PopulateCategoryCombo ()
+		{
+			SmugMugNet.Category[] categories = smugmug.GetCategories ();
+
+			foreach (SmugMugNet.Category category in categories) {
+				category_store.AppendValues (category.CategoryID, category.Title);
+			}
+
+			category_combo.Active = 0;
+
+			category_combo.ShowAll ();
+		}
+
+		protected int CurrentCategoryId
+		{
+			get {
+				TreeIter current;
+				category_combo.GetActiveIter (out current);
+				return (int)category_combo.Model.GetValue (current, 0);
+			}
+		}
+
+		private Gtk.Dialog Dialog {
+			get {
+				if (dialog == null)
+					dialog = (Gtk.Dialog) xml.GetWidget (dialog_name);
+
+				return dialog;
+			}
+		}
+	}
+
+
+	public class SmugMugExport : FSpot.Extensions.IExporter {
+		public SmugMugExport ()
+		{
+		}
+		public void Run (IBrowsableCollection selection)
+		{
+			xml = new Glade.XML (null, "SmugMugExport.glade", dialog_name, "f-spot");
+			xml.Autoconnect (this);
+
+			this.items = selection.Items;
+			album_button.Sensitive = false;
+			FSpot.Widgets.IconView view = new FSpot.Widgets.IconView (selection);
+			view.DisplayDates = false;
+			view.DisplayTags = false;
+
+			Dialog.Modal = false;
+			Dialog.TransientFor = null;
+			Dialog.Close += HandleCloseEvent;
+
+			thumb_scrolledwindow.Add (view);
+			view.Show ();
+			Dialog.Show ();
+
+			SmugMugAccountManager manager = SmugMugAccountManager.GetInstance ();
+			manager.AccountListChanged += PopulateSmugMugOptionMenu;
+			PopulateSmugMugOptionMenu (manager, null);
+
+			if (edit_button != null)
+				edit_button.Clicked += HandleEditGallery;
+
+			rh = new Gtk.ResponseHandler (HandleResponse);
+			Dialog.Response += HandleResponse;
+			connect = true;
+			HandleSizeActive (null, null);
+			Connect ();
+
+			scale_check.Toggled += HandleScaleCheckToggled;
+
+			LoadPreference (SCALE_KEY);
+			LoadPreference (SIZE_KEY);
+			LoadPreference (ROTATE_KEY);
+			LoadPreference (BROWSER_KEY);
+		}
+
+		Gtk.ResponseHandler rh;
+
+		private bool scale;
+		private int size;
+		private bool browser;
+		private bool rotate;
+//		private bool meta;
+		private bool connect = false;
+
+		private long approx_size = 0;
+		private long sent_bytes = 0;
+
+		IBrowsableItem [] items;
+		int photo_index;
+		FSpot.ThreadProgressDialog progress_dialog;
+
+		ArrayList accounts;
+		private SmugMugAccount account;
+		private Album album;
+
+		private string dialog_name = "smugmug_export_dialog";
+		private Glade.XML xml;
+
+		// Dialogs
+		private SmugMugAccountDialog gallery_add;
+		private SmugMugAddAlbum album_add;
+
+		// Widgets
+		[Glade.Widget] Gtk.Dialog dialog;
+		[Glade.Widget] Gtk.OptionMenu gallery_optionmenu;
+		[Glade.Widget] Gtk.OptionMenu album_optionmenu;
+
+		[Glade.Widget] Gtk.CheckButton browser_check;
+		[Glade.Widget] Gtk.CheckButton scale_check;
+		[Glade.Widget] Gtk.CheckButton rotate_check;
+
+		[Glade.Widget] Gtk.SpinButton size_spin;
+
+		[Glade.Widget] Gtk.Button album_button;
+		[Glade.Widget] Gtk.Button edit_button;
+
+		[Glade.Widget] Gtk.Button export_button;
+
+		[Glade.Widget] Gtk.ScrolledWindow thumb_scrolledwindow;
+
+		System.Threading.Thread command_thread;
+
+		public const string EXPORT_SERVICE = "smugmug/";
+		public const string SCALE_KEY = Preferences.APP_FSPOT_EXPORT + EXPORT_SERVICE + "scale";
+		public const string SIZE_KEY = Preferences.APP_FSPOT_EXPORT + EXPORT_SERVICE + "size";
+		public const string ROTATE_KEY = Preferences.APP_FSPOT_EXPORT + EXPORT_SERVICE + "rotate";
+		public const string BROWSER_KEY = Preferences.APP_FSPOT_EXPORT + EXPORT_SERVICE + "browser";
+
+		private void HandleResponse (object sender, Gtk.ResponseArgs args)
+		{
+			if (args.ResponseId != Gtk.ResponseType.Ok) {
+				Dialog.Destroy ();
+				return;
+			}
+
+			if (scale_check != null) {
+				scale = scale_check.Active;
+				size = size_spin.ValueAsInt;
+			} else
+				scale = false;
+
+			browser = browser_check.Active;
+			rotate = rotate_check.Active;
+//			meta = meta_check.Active;
+
+			if (account != null) {
+				//System.Console.WriteLine ("history = {0}", album_optionmenu.History);
+				album = (Album) account.SmugMug.GetAlbums() [Math.Max (0, album_optionmenu.History)];
+				photo_index = 0;
+
+				Dialog.Destroy ();
+
+				command_thread = new System.Threading.Thread (new System.Threading.ThreadStart (this.Upload));
+				command_thread.Name = Mono.Unix.Catalog.GetString ("Uploading Pictures");
+
+				progress_dialog = new FSpot.ThreadProgressDialog (command_thread, items.Length);
+				progress_dialog.Start ();
+
+				// Save these settings for next time
+				Preferences.Set (SCALE_KEY, scale);
+				Preferences.Set (SIZE_KEY, size);
+				Preferences.Set (ROTATE_KEY, rotate);
+				Preferences.Set (BROWSER_KEY, browser);
+			}
+		}
+
+		public void HandleSizeActive (object sender, EventArgs args)
+		{
+			size_spin.Sensitive = scale_check.Active;
+		}
+
+		private void Upload ()
+		{
+			sent_bytes = 0;
+			approx_size = 0;
+
+			System.Uri album_uri = null;
+
+			System.Console.WriteLine ("Starting Upload to Smugmug, album {0} - {1}", album.Title, album.AlbumID);
+
+			FilterSet filters = new FilterSet ();
+			filters.Add (new JpegFilter ());
+
+			if (scale)
+				filters.Add (new ResizeFilter ((uint)size));
+
+			if (rotate)
+				filters.Add (new OrientationFilter ());
+
+			while (photo_index < items.Length) {
+				try {
+					IBrowsableItem item = items[photo_index];
+
+					FileInfo file_info;
+					Console.WriteLine ("uploading {0}", photo_index);
+
+					progress_dialog.Message = String.Format (Catalog.GetString ("Uploading picture \"{0}\" ({1} of {2})"),
+										 item.Name, photo_index+1, items.Length);
+					progress_dialog.ProgressText = string.Empty;
+					progress_dialog.Fraction = ((photo_index) / (double) items.Length);
+					photo_index++;
+
+					FilterRequest request = new FilterRequest (item.DefaultVersionUri);
+
+					filters.Convert (request);
+
+					file_info = new FileInfo (request.Current.LocalPath);
+
+					if (approx_size == 0) //first image
+						approx_size = file_info.Length * items.Length;
+					else
+						approx_size = sent_bytes * items.Length / (photo_index - 1);
+
+					int image_id = account.SmugMug.Upload (request.Current.LocalPath, album.AlbumID);
+					if (Core.Database != null && item is Photo && image_id >= 0)
+						Core.Database.Exports.Create ((item as Photo).Id,
+									      (item as Photo).DefaultVersionId,
+									      ExportStore.SmugMugExportType,
+									      account.SmugMug.GetAlbumUrl (image_id).ToString ());
+
+					sent_bytes += file_info.Length;
+
+					if (album_uri == null)
+						album_uri = account.SmugMug.GetAlbumUrl (image_id);
+				} catch (System.Exception e) {
+					progress_dialog.Message = String.Format (Mono.Unix.Catalog.GetString ("Error Uploading To Gallery: {0}"),
+										 e.Message);
+					progress_dialog.ProgressText = Mono.Unix.Catalog.GetString ("Error");
+					System.Console.WriteLine (e);
+
+					if (progress_dialog.PerformRetrySkip ()) {
+						photo_index--;
+						if (photo_index == 0)
+							approx_size = 0;
+					}
+				}
+			}
+
+			progress_dialog.Message = Catalog.GetString ("Done Sending Photos");
+			progress_dialog.Fraction = 1.0;
+			progress_dialog.ProgressText = Mono.Unix.Catalog.GetString ("Upload Complete");
+			progress_dialog.ButtonLabel = Gtk.Stock.Ok;
+
+			if (browser && album_uri != null) {
+				GnomeUtil.UrlShow (album_uri.ToString ());
+			}
+		}
+
+		private void HandleScaleCheckToggled (object o, EventArgs e)
+		{
+			rotate_check.Sensitive = !scale_check.Active;
+		}
+
+		private void PopulateSmugMugOptionMenu (SmugMugAccountManager manager, SmugMugAccount changed_account)
+		{
+			Gtk.Menu menu = new Gtk.Menu ();
+			this.account = changed_account;
+			int pos = -1;
+
+			accounts = manager.GetAccounts ();
+			if (accounts == null || accounts.Count == 0) {
+				Gtk.MenuItem item = new Gtk.MenuItem (Mono.Unix.Catalog.GetString ("(No Gallery)"));
+				menu.Append (item);
+				gallery_optionmenu.Sensitive = false;
+				edit_button.Sensitive = false;
+			} else {
+				int i = 0;
+				foreach (SmugMugAccount account in accounts) {
+					if (account == changed_account)
+						pos = i;
+
+					Gtk.MenuItem item = new Gtk.MenuItem (account.Username);
+					menu.Append (item);
+					i++;
+				}
+				gallery_optionmenu.Sensitive = true;
+				edit_button.Sensitive = true;
+			}
+
+			menu.ShowAll ();
+			gallery_optionmenu.Menu = menu;
+			gallery_optionmenu.SetHistory ((uint)pos);
+		}
+
+		private void Connect ()
+		{
+			Connect (null);
+		}
+
+		private void Connect (SmugMugAccount selected)
+		{
+			Connect (selected, null);
+		}
+
+		private void Connect (SmugMugAccount selected, string text)
+		{
+			try {
+				if (accounts.Count != 0 && connect) {
+					if (selected == null)
+						account = (SmugMugAccount) accounts [gallery_optionmenu.History];
+					else
+						account = selected;
+
+					if (!account.Connected)
+						account.Connect ();
+
+					PopulateAlbumOptionMenu (account.SmugMug);
+				}
+			} catch (System.Exception) {
+				System.Console.WriteLine ("Can not connect to SmugMug. Bad username ? password ? network connection ?");
+				//System.Console.WriteLine ("{0}",ex);
+				if (selected != null)
+					account = selected;
+
+				PopulateAlbumOptionMenu (account.SmugMug);
+
+				album_button.Sensitive = false;
+
+				new SmugMugAccountDialog (this.Dialog, account);
+			}
+		}
+
+		private void HandleAccountSelected (object sender, System.EventArgs args)
+		{
+			Connect ();
+		}
+
+		public void HandleAlbumAdded (string title) {
+			SmugMugAccount account = (SmugMugAccount) accounts [gallery_optionmenu.History];
+			PopulateAlbumOptionMenu (account.SmugMug);
+
+			// make the newly created album selected
+			Album[] albums = account.SmugMug.GetAlbums();
+			for (int i=0; i < albums.Length; i++) {
+				if (((Album)albums[i]).Title == title) {
+					album_optionmenu.SetHistory((uint)i);
+				}
+			}
+		}
+
+		private void PopulateAlbumOptionMenu (SmugMugApi smugmug)
+		{
+			Album[] albums = null;
+			if (smugmug != null) {
+				try {
+					albums = smugmug.GetAlbums();
+				} catch (Exception) {
+					Console.WriteLine("Can't get the albums");
+					smugmug = null;
+				}
+			}
+
+			Gtk.Menu menu = new Gtk.Menu ();
+
+			bool disconnected = smugmug == null || !account.Connected || albums == null;
+
+			if (disconnected || albums.Length == 0) {
+				string msg = disconnected ? Mono.Unix.Catalog.GetString ("(Not Connected)")
+					: Mono.Unix.Catalog.GetString ("(No Albums)");
+
+				Gtk.MenuItem item = new Gtk.MenuItem (msg);
+				menu.Append (item);
+
+				export_button.Sensitive = false;
+				album_optionmenu.Sensitive = false;
+				album_button.Sensitive = false;
+			} else {
+				foreach (Album album in albums) {
+					System.Text.StringBuilder label_builder = new System.Text.StringBuilder ();
+
+					label_builder.Append (album.Title);
+
+					Gtk.MenuItem item = new Gtk.MenuItem (label_builder.ToString ());
+					((Gtk.Label)item.Child).UseUnderline = false;
+					menu.Append (item);
+				}
+
+				export_button.Sensitive = items.Length > 0;
+				album_optionmenu.Sensitive = true;
+				album_button.Sensitive = true;
+			}
+
+			menu.ShowAll ();
+			album_optionmenu.Menu = menu;
+		}
+
+		public void HandleAddGallery (object sender, System.EventArgs args)
+		{
+			gallery_add = new SmugMugAccountDialog (this.Dialog);
+		}
+
+		public void HandleEditGallery (object sender, System.EventArgs args)
+		{
+			gallery_add = new SmugMugAccountDialog (this.Dialog, account);
+		}
+
+		public void HandleAddAlbum (object sender, System.EventArgs args)
+		{
+			if (account == null)
+				throw new Exception (Catalog.GetString ("No account selected"));
+
+			album_add = new SmugMugAddAlbum (this, account.SmugMug);
+		}
+
+		void LoadPreference (string key)
+		{
+			object val = Preferences.Get (key);
+
+			if (val == null)
+				return;
+
+			//System.Console.WriteLine ("Setting {0} to {1}", key, val);
+
+			switch (key) {
+			case SCALE_KEY:
+				if (scale_check.Active != (bool) val) {
+					scale_check.Active = (bool) val;
+					rotate_check.Sensitive = !(bool) val;
+				}
+				break;
+
+			case SIZE_KEY:
+				size_spin.Value = (double) (int) val;
+				break;
+
+			case BROWSER_KEY:
+				if (browser_check.Active != (bool) val)
+					browser_check.Active = (bool) val;
+				break;
+
+			case ROTATE_KEY:
+				if (rotate_check.Active != (bool) val)
+					rotate_check.Active = (bool) val;
+				break;
+			}
+		}
+
+		protected void HandleCloseEvent (object sender, System.EventArgs args)
+		{
+			account.SmugMug.Logout ();
+		}
+
+		private Gtk.Dialog Dialog {
+			get {
+				if (dialog == null)
+					dialog = (Gtk.Dialog) xml.GetWidget (dialog_name);
+
+				return dialog;
+			}
+		}
+	}
+}

Copied: trunk/extensions/Exporters/SmugMugExport/SmugMugExport.glade (from r4368, /trunk/extensions/SmugMugExport/SmugMugExport.glade)
==============================================================================

Copied: trunk/extensions/Exporters/SmugMugExport/SmugMugNet/.gitignore (from r4368, /trunk/extensions/SmugMugExport/SmugMugNet/.gitignore)
==============================================================================

Added: trunk/extensions/Exporters/SmugMugExport/SmugMugNet/Makefile.am
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/SmugMugExport/SmugMugNet/Makefile.am	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,27 @@
+include $(top_srcdir)/Makefile.include
+
+ASSEMBLY_NAME = SmugMugNet
+
+ASSEMBLY_SOURCES =				\
+	$(srcdir)/SmugMugApi.cs 		\
+	$(srcdir)/NoCheckCertificatePolicy.cs
+
+REFS =
+
+PKGS =
+
+ASSEMBLY = $(ASSEMBLY_NAME).dll
+
+all: $(ASSEMBLY)
+
+$(ASSEMBLY): $(ASSEMBLY_SOURCES)
+	$(CSC_LIB) -out:$@ $(PKGS) $(REFS) $(ASSEMBLY_SOURCES)
+
+assemblydir = $(pkglibdir)
+assembly_DATA =	$(ASSEMBLY)
+
+EXTRA_DIST = $(ASSEMBLY_SOURCES)
+
+CLEANFILES =			\
+	$(ASSEMBLY)		\
+	$(ASSEMBLY).mdb

Added: trunk/extensions/Exporters/SmugMugExport/SmugMugNet/NoCheckCertificatePolicy.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/SmugMugExport/SmugMugNet/NoCheckCertificatePolicy.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,37 @@
+// Authors:
+//	Gonzalo Paniagua Javier (gonzalo ximian com)
+//
+// (C) Copyright 2006 Novell, Inc. (http://www.novell.com)
+//
+
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using System.Net;
+using System.Security.Cryptography.X509Certificates;
+
+namespace SmugMugNet {
+	public class NoCheckCertificatePolicy : ICertificatePolicy {
+		public bool CheckValidationResult (ServicePoint a, X509Certificate b, WebRequest c, int d)
+		{
+			return true;
+		}
+	}
+}

Added: trunk/extensions/Exporters/SmugMugExport/SmugMugNet/SmugMugApi.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/SmugMugExport/SmugMugNet/SmugMugApi.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,473 @@
+/*
+ * SmugMugApi.cs
+ *
+ * Authors:
+ *   Thomas Van Machelen <thomas vanmachelen gmail com>
+ *
+ * Copyright (C) 2006 Thomas Van Machelen
+ * This is free software. See COPYING for details.
+ *
+ */
+
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Net;
+using System.IO;
+using System.Text.RegularExpressions;
+using System.Xml;
+using System.Collections.Specialized;
+
+namespace SmugMugNet
+{
+	public struct Credentials
+	{
+		public string session_id;
+		public int user_id;
+		public string password_hash;
+
+		public string SessionID {
+			get { return session_id; }
+			set { session_id = value; }
+		}
+
+		public int UserID {
+			get { return user_id; }
+		}
+
+		public string PasswordHash {
+			get { return password_hash; }
+		}
+
+		public Credentials(string session_id, int user_id, string password_hash)
+		{
+			this.session_id = session_id;
+			this.user_id = user_id;
+			this.password_hash = password_hash;
+		}
+	}
+
+	public struct Category
+	{
+		public Category( string title, int id)
+		{
+			this.title = title;
+			this.category_id = id;
+		}
+
+		private int category_id;
+		public int CategoryID
+		{
+			get { return category_id; }
+			set { category_id = value; }
+		}
+
+		private string title;
+		public string Title
+		{
+			get { return title; }
+			set { title = value; }
+		}
+	}
+
+	public struct Album
+	{
+		public Album(string title, int id)
+		{
+			this.album_id = id;
+			this.title = title;
+		}
+
+		private int album_id;
+		public int AlbumID
+		{
+			get { return album_id; }
+			set { album_id = value; }
+		}
+
+		private string title;
+		public string Title
+		{
+			get { return title; }
+			set { title = value; }
+		}
+	}
+
+	public class SmugMugApi
+	{
+		private string username = String.Empty;
+		private string password = String.Empty;
+		private bool connected = false;
+
+		private Credentials credentials;
+		private const string VERSION = "1.1.1";
+		private Category[] categories;
+
+		public bool Connected
+		{
+			get { return connected; }
+		}
+
+		public SmugMugApi (string email_address, string password)
+		{
+			this.username = email_address;
+			this.password = password;
+		}
+
+		public bool Login ()
+		{
+			if (this.username.Length == 0 | this.password.Length == 0)
+			{
+				throw new SmugMugException("There is no username or password.");
+			}
+
+			if (this.connected == false && this.credentials.UserID == 0)
+			{
+				try
+				{
+					this.credentials = SmugMugProxy.LoginWithPassword (this.username, this.password);
+					this.connected = true;
+				}
+				catch
+				{
+					return false;
+				}
+			}
+			else
+			{
+				LoginWithHash ();
+			}
+
+			return true;
+		}
+
+		private void LoginWithHash ()
+		{
+			try {
+				string session_id = SmugMugProxy.LoginWithHash (this.credentials.UserID, this.credentials.PasswordHash);
+
+				if (session_id != null && session_id.Length > 0)
+				{
+					this.credentials.SessionID = session_id;
+				}
+				else
+				{
+					throw new SmugMugException ("SessionID was empty");
+				}
+			}
+			catch (Exception ex) {
+				throw new SmugMugException ("A login error occured, SessionID may be invalid.", ex.InnerException);
+			}
+		}
+
+		public void Logout ()
+		{
+			if (!connected)
+				return;
+
+			if (this.credentials.SessionID == null && this.credentials.SessionID.Length == 0)
+				return;
+
+			SmugMugProxy.Logout (this.credentials.SessionID);
+			connected = false;
+			this.credentials = new Credentials (null, 0, null);
+		}
+
+		public Category[] GetCategories ()
+		{
+			if (this.categories == null)
+			{
+				try {
+					this.categories = SmugMugProxy.GetCategories (credentials.SessionID);
+				}
+				catch (Exception ex) {
+					throw new SmugMugException ("Could not retrieve Categories", ex.InnerException);
+				}
+			}
+			return this.categories;
+		}
+
+		public Album CreateAlbum (string title, int category_id, bool is_public)
+		{
+			try {
+				return SmugMugProxy.CreateAlbum (title, category_id, credentials.SessionID, is_public);
+			}
+			catch (Exception ex) {
+				throw new SmugMugException ("Could not create album", ex.InnerException);
+			}
+		}
+
+		public Album[] GetAlbums ()
+		{
+			try {
+				return SmugMugProxy.GetAlbums(credentials.SessionID);
+			}
+			catch (Exception ex) {
+				throw new SmugMugException ("Could not get albums", ex.InnerException);
+			}
+		}
+
+		public Uri GetAlbumUrl (int image_id)
+		{
+			try {
+				return SmugMugProxy.GetAlbumUrl (image_id, credentials.SessionID);
+			}
+			catch (Exception ex) {
+				throw new SmugMugException ("Could not get album url", ex.InnerException);
+			}
+		}
+
+		public int Upload (string path, int album_id)
+		{
+			try {
+				return SmugMugProxy.Upload (path, album_id, credentials.SessionID);
+			}
+			catch (Exception ex) {
+				throw new SmugMugException ("Could not upload file", ex.InnerException);
+			}
+		}
+	}
+
+	public class SmugMugProxy
+	{
+		// FIXME: this getting should be done over https
+		private const string GET_URL = "https://api.SmugMug.com/hack/rest/";;
+		private const string POST_URL = "https://upload.SmugMug.com/hack/rest/";;
+		// key from massis
+		private const string APIKEY = "umtr0zB2wzwTZDhF2BySidg0hY0le3K6";
+		private const string VERSION = "1.1.1";
+
+		// rest methods
+		private const string LOGIN_WITHPASS_METHOD = "smugmug.login.withPassword";
+		private const string LOGIN_WITHHASH_METHOD = "smugmug.login.withHash";
+		private const string LOGOUT_METHOD = "smugmug.logout";
+		private const string ALBUMS_CREATE_METHOD = "smugmug.albums.create";
+		private const string ALBUMS_GET_URLS_METHOD = "smugmug.images.getURLs";
+		private const string ALBUMS_GET_METHOD = "smugmug.albums.get";
+		private const string CATEGORIES_GET_METHOD = "smugmug.categories.get";
+
+		// parameter constants
+		private const string EMAIL = "EmailAddress";
+		private const string PASSWORD = "Password";
+		private const string USER_ID = "UserID";
+		private const string PASSWORD_HASH = "PasswordHash";
+		private const string SESSION_ID = "SessionID";
+		private const string CATEGORY_ID = "CategoryID";
+		private const string IMAGE_ID = "ImageID";
+		private const string TITLE = "Title";
+		private const string ID = "id";
+
+		public static Credentials LoginWithPassword (string username, string password)
+		{
+			string url = FormatGetUrl (LOGIN_WITHPASS_METHOD, new SmugMugParam (EMAIL, username), new SmugMugParam (PASSWORD, password));
+			XmlDocument doc = GetResponseXml (url);
+
+			string sessionId = doc.SelectSingleNode ("/rsp/Login/SessionID").InnerText;
+			int userId = int.Parse (doc.SelectSingleNode ("/rsp/Login/UserID").InnerText);
+			string passwordHash = doc.SelectSingleNode ("/rsp/Login/PasswordHash").InnerText;
+
+			return new Credentials (sessionId, userId, passwordHash);
+		}
+
+		public static string LoginWithHash (int user_id, string password_hash)
+		{
+			string url = FormatGetUrl (LOGIN_WITHHASH_METHOD, new SmugMugParam (USER_ID, user_id), new SmugMugParam (PASSWORD_HASH, password_hash));
+			XmlDocument doc = GetResponseXml(url);
+
+			return doc.SelectSingleNode ("/rsp/Login/SessionID").InnerText;
+		}
+
+		public static void Logout (string session_id)
+		{
+			string url = FormatGetUrl (LOGOUT_METHOD, new SmugMugParam (SESSION_ID, session_id));
+			GetResponseXml (url);
+		}
+
+		public static Album[] GetAlbums (string session_id)
+		{
+			string url = FormatGetUrl (ALBUMS_GET_METHOD, new SmugMugParam(SESSION_ID, session_id));
+			XmlDocument doc = GetResponseXml (url);
+			XmlNodeList albumNodes = doc.SelectNodes ("/rsp/Albums/Album");
+
+			Album[] albums = new Album[albumNodes.Count];
+
+			for (int i = 0; i < albumNodes.Count; i++)
+			{
+				XmlNode current = albumNodes[i];
+				albums[i] = new Album (current.SelectSingleNode (TITLE).InnerText, int.Parse (current.Attributes[ID].Value));
+			}
+			return albums;
+		}
+
+		public static Uri GetAlbumUrl (int image_id, string session_id)
+		{
+			string url = FormatGetUrl(ALBUMS_GET_URLS_METHOD, new SmugMugParam(IMAGE_ID, image_id), new SmugMugParam(SESSION_ID, session_id));
+			XmlDocument doc = GetResponseXml(url);
+
+			string album_url = doc.SelectSingleNode("/rsp/ImageURLs/Image/AlbumURL").InnerText;
+
+			return new Uri(album_url);
+		}
+
+		public static Category[] GetCategories (string session_id)
+		{
+			string url = FormatGetUrl(CATEGORIES_GET_METHOD, new SmugMugParam (SESSION_ID, session_id));
+			XmlDocument doc = GetResponseXml (url);
+
+			XmlNodeList categoryNodes = doc.SelectNodes ("/rsp/Categories/Category");
+			Category[] categories = new Category[categoryNodes.Count];
+
+			for (int i = 0; i < categoryNodes.Count; i++)
+			{
+				XmlNode current = categoryNodes[i];
+				categories[i] = new Category (current.SelectSingleNode (TITLE).InnerText, int.Parse (current.Attributes[ID].Value));
+			}
+			return categories;
+		}
+
+		public static Album CreateAlbum (string title, int category_id, string session_id)
+		{
+			return CreateAlbum (title, category_id, session_id, true);
+		}
+
+		public static Album CreateAlbum (string title, int category_id, string session_id, bool is_public)
+		{
+			int public_int = is_public ? 1 : 0;
+			string url = FormatGetUrl (ALBUMS_CREATE_METHOD, new SmugMugParam (TITLE, title), new SmugMugParam (CATEGORY_ID, category_id), new SmugMugParam (SESSION_ID, session_id), new SmugMugParam ("Public", public_int));
+			XmlDocument doc = GetResponseXml (url);
+
+			int id = int.Parse(doc.SelectSingleNode("/rsp/Create/Album").Attributes[ID].Value);
+
+			return new Album(title, id);
+		}
+
+		public static int Upload (string path, int album_id, string session_id)
+		{
+			FileInfo file = new FileInfo(path);
+
+			if (!file.Exists)
+				throw new ArgumentException("Image does not exist: " + file.FullName);
+
+			try
+			{
+				WebClient client = new WebClient ();
+				client.BaseAddress = "http://upload.smugmug.com";;
+				client.Headers.Add ("Cookie:SMSESS=" + session_id);
+
+				NameValueCollection queryStringCollection = new NameValueCollection ();
+				queryStringCollection.Add ("AlbumID", album_id.ToString());
+				// Temporarily disabled because rest doesn't seem to return the ImageID anymore
+				// queryStringCollection.Add ("ResponseType", "REST");
+				// luckily JSON still holds it
+				queryStringCollection.Add ("ResponseType", "JSON");
+				client.QueryString = queryStringCollection;
+
+				byte[] responseArray = client.UploadFile ("http://upload.smugmug.com/photos/xmladd.mg";, "POST", file.FullName);
+				string response = Encoding.ASCII.GetString (responseArray);
+
+				// JSon approach
+				Regex id_regex = new Regex ("\\\"id\\\":( )?(?<image_id>\\d+),");
+				Match m  = id_regex.Match (response);
+
+				int id = -1;
+
+				if (m.Success)
+					id = int.Parse (m.Groups["image_id"].Value);
+
+				return id;
+
+				// REST approach, disabled for now
+				//XmlDocument doc = new XmlDocument ();
+				//doc.LoadXml (response);
+				// return int.Parse (doc.SelectSingleNode ("/rsp/ImageID").InnerText);
+
+			}
+			catch (Exception ex)
+			{
+				throw new SmugMugUploadException ("Error uploading image: " + file.FullName, ex.InnerException);
+			}
+		}
+
+		private static string FormatGetUrl(string method_name, params SmugMugParam[] parameters)
+		{
+			StringBuilder builder = new StringBuilder (string.Format ("{0}{1}/?method={2}", GET_URL, VERSION, method_name));
+
+			foreach (SmugMugParam param in parameters)
+				builder.Append (param.ToString ());
+
+			builder.Append (new SmugMugParam ("APIKey", APIKEY));
+			return builder.ToString();
+		}
+
+		private static XmlDocument GetResponseXml (string url)
+		{
+			HttpWebRequest request = HttpWebRequest.Create (url) as HttpWebRequest;
+			request.Credentials = CredentialCache.DefaultCredentials;
+			WebResponse response = request.GetResponse ();
+
+			XmlDocument doc = new XmlDocument ();
+			doc.LoadXml (new StreamReader (response.GetResponseStream ()).ReadToEnd ());
+			CheckResponseXml (doc);
+
+			response.Close ();
+			return doc;
+		}
+
+		private static void CheckResponseXml (XmlDocument doc)
+		{
+			if (doc.SelectSingleNode("/rsp").Attributes["stat"].Value == "ok")
+				return;
+
+			string message = doc.SelectSingleNode ("/rsp/err").Attributes["msg"].Value;
+			throw new SmugMugException (message);
+		}
+
+		private class SmugMugParam
+		{
+			string name;
+			object value;
+
+			public SmugMugParam (string name, object value)
+			{
+				this.name = name;
+				this.value = value;
+			}
+
+			public string Name
+			{
+				get {return name;}
+			}
+
+			public object Value
+			{
+				get {return value;}
+			}
+
+			public override string ToString()
+			{
+				return string.Format("&{0}={1}", Name, Value);
+			}
+		}
+	}
+
+	public class SmugMugException : ApplicationException
+	{
+		public SmugMugException(string message) : base (message)
+		{
+			Console.WriteLine (message);
+		}
+
+		public SmugMugException (string message, Exception innerException) : base (message, innerException)
+		{
+			Console.WriteLine (message, innerException.ToString());
+		}
+	}
+
+	public sealed class SmugMugUploadException : ApplicationException
+	{
+		public SmugMugUploadException (string message, Exception innerException) : base (message, innerException)
+		{
+			Console.WriteLine (message, innerException.ToString ());
+		}
+	}
+}

Copied: trunk/extensions/Exporters/TabbloExport/.gitignore (from r4368, /trunk/extensions/TabbloExport/.gitignore)
==============================================================================

Added: trunk/extensions/Exporters/TabbloExport/ApplicationCentricCertificatePolicy.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/TabbloExport/ApplicationCentricCertificatePolicy.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,165 @@
+//
+// FSpotTabbloExport.ApplicationCentricCertificatePolicy
+//
+// Authors:
+//	Wojciech Dzierzanowski (wojciech dzierzanowski gmail com)
+//
+// (C) Copyright 2008 Wojciech Dzierzanowski
+//
+
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using System;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.IO;
+using System.IO.IsolatedStorage;
+using System.Net;
+using System.Runtime.Serialization;
+using System.Runtime.Serialization.Formatters.Binary;
+using System.Security.Cryptography.X509Certificates;
+
+using FSpot.Utils;
+
+namespace FSpotTabbloExport {
+
+	class ApplicationCentricCertificatePolicy : ICertificatePolicy {
+
+		protected enum Decision {
+			DontTrust,
+			TrustOnce,
+			TrustAlways
+		};
+
+		private Dictionary<string, int> cert_hashes;
+
+		private static readonly IsolatedStorageFile isolated_store =
+				IsolatedStorageFile.GetUserStoreForAssembly ();
+
+		private const string StoreName = "cert_hashes";
+
+
+		public bool CheckValidationResult (ServicePoint service_point,
+		                                   X509Certificate certificate,
+		                                   WebRequest request,
+						   int problem)
+		{
+			Log.DebugFormat ("Checking validation result for {0}: problem={1}", request.RequestUri, problem);
+
+			if (0 == problem) {
+				return true;
+			}
+
+			// Only try to deal with the problem if it is a trust
+			// failure.
+			if (-2146762486 != problem) {
+				return false;
+			}
+
+			LoadCertificates ();
+
+			string hash = certificate.GetCertHashString ();
+			Log.DebugFormat ("Certificate hash: " + hash);
+
+			int stored_problem = 0;
+			if (cert_hashes.TryGetValue (hash, out stored_problem)
+					&& problem == stored_problem) {
+				Log.DebugFormat ("We already trust this site");
+				return true;
+			}
+
+			Decision decision = GetDecision (certificate, request);
+			Log.DebugFormat ("Decision: " + decision);
+
+			switch (decision) {
+			case Decision.DontTrust:
+				return false;
+			case Decision.TrustOnce:
+				return true;
+			case Decision.TrustAlways:
+				SaveCertificate (hash, problem);
+				return true;
+			default:
+				Debug.Assert (false, "Unknown decision");
+				return false;
+			}
+		}
+
+
+		protected virtual Decision GetDecision (
+				X509Certificate certificate,
+				WebRequest request)
+		{
+			Decision decision = Decision.DontTrust;
+			Log.DebugFormat ("Making the default decision: " + decision);
+			return decision;
+		}
+
+
+		private void LoadCertificates ()
+		{
+			using (IsolatedStorageFileStream isol_stream =
+					new IsolatedStorageFileStream (
+							StoreName,
+							FileMode.OpenOrCreate,
+							FileAccess.Read,
+			                                isolated_store)) {
+				try {
+					BinaryFormatter formatter =
+							new BinaryFormatter ();
+					cert_hashes = (Dictionary<string, int>)
+							formatter.Deserialize (
+								isol_stream);
+				} catch (SerializationException e) {
+					// FIXME: handle
+					Log.Exception (e);
+				}
+			}
+
+			if (null == cert_hashes) {
+				cert_hashes = new Dictionary<string,int> ();
+			}
+		}
+
+
+		private void SaveCertificate (string hash, int problem)
+		{
+			cert_hashes.Add (hash, problem);
+
+			using (IsolatedStorageFileStream isolated_stream =
+					new IsolatedStorageFileStream (
+							StoreName,
+							FileMode.OpenOrCreate,
+							FileAccess.Write,
+			                                isolated_store)) {
+				try {
+					BinaryFormatter formatter =
+							new BinaryFormatter ();
+					formatter.Serialize (isolated_stream,
+							cert_hashes);
+				} catch (SerializationException e) {
+					// FIXME: handle
+					Log.Exception (e);
+				}
+			}
+		}
+	}
+}

Added: trunk/extensions/Exporters/TabbloExport/AssemblyInfo.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/TabbloExport/AssemblyInfo.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,32 @@
+#region Using directives
+
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+#endregion
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle ("TabbloExport")]
+[assembly: AssemblyDescription ("TabbloExport is an F-Spot extension "
+	+ "adding support for uploading images to Tabblo.")]
+[assembly: AssemblyConfiguration ("")]
+[assembly: AssemblyCompany ("")]
+[assembly: AssemblyProduct ("TabbloExport")]
+[assembly: AssemblyCopyright ("")]
+[assembly: AssemblyTrademark ("")]
+[assembly: AssemblyCulture ("")]
+
+// This sets the default COM visibility of types in the assembly to invisible.
+// If you need to expose a type to COM, use [ComVisible(true)] on that type.
+[assembly: ComVisible (false)]
+
+// The assembly version has following format :
+//
+// Major.Minor.Build.Revision
+//
+// You can specify all the values or you can use the default the Revision and
+// Build Numbers by using the '*' as shown below:
+[assembly: AssemblyVersion ("0.1.*")]

Added: trunk/extensions/Exporters/TabbloExport/BlindTrustCertificatePolicy.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/TabbloExport/BlindTrustCertificatePolicy.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,46 @@
+//
+// FSpotTabbloExport.BlindTrustCertificatePolicy
+//
+// Authors:
+//	Wojciech Dzierzanowski (wojciech dzierzanowski gmail com)
+//
+// (C) Copyright 2008 Wojciech Dzierzanowski
+//
+
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using System.Net;
+using System.Security.Cryptography.X509Certificates;
+
+using FSpot.Utils;
+
+namespace FSpotTabbloExport {
+	class BlindTrustCertificatePolicy : ICertificatePolicy {
+		public bool CheckValidationResult (ServicePoint service_point,
+		                                   X509Certificate certificate,
+		                                   WebRequest request,
+						   int problem)
+		{
+			Log.DebugFormat ("Blindly trusting " + request.RequestUri);
+			return true;
+		}
+	}
+}

Added: trunk/extensions/Exporters/TabbloExport/FSpotUploadProgress.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/TabbloExport/FSpotUploadProgress.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,64 @@
+//
+// FSpotTabbloExport.FSpotUploadProgress
+//
+// Authors:
+//	Wojciech Dzierzanowski (wojciech dzierzanowski gmail com)
+//
+// (C) Copyright 2008 Wojciech Dzierzanowski
+//
+
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using Mono.Tabblo;
+using Mono.Unix;
+using System;
+
+namespace FSpotTabbloExport {
+
+	class FSpotUploadProgress : TotalUploadProgress	{
+
+		private FSpot.ThreadProgressDialog progress_dialog;
+
+
+		internal FSpotUploadProgress (
+				Picture [] pictures,
+				FSpot.ThreadProgressDialog progress_dialog)
+			: base (pictures)
+		{
+			this.progress_dialog = progress_dialog;
+		}
+
+
+		protected override void ShowProgress (string title,
+		                                      long bytes_sent)
+		{
+			progress_dialog.Message = title;
+			progress_dialog.ProgressText = String.Format (
+					Catalog.GetString (
+							"{0} of approx. {1}"),
+					SizeUtil.ToHumanReadable (bytes_sent),
+					SizeUtil.ToHumanReadable (
+							(long) TotalFileSize));
+			progress_dialog.Fraction =
+					(double) bytes_sent / TotalFileSize;
+		}
+	}
+}

Copied: trunk/extensions/Exporters/TabbloExport/Makefile.am (from r4368, /trunk/extensions/TabbloExport/Makefile.am)
==============================================================================

Added: trunk/extensions/Exporters/TabbloExport/Preferences.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/TabbloExport/Preferences.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,63 @@
+//
+// FSpotTabbloExport.Preferences
+//
+// Authors:
+//	Wojciech Dzierzanowski (wojciech dzierzanowski gmail com)
+//
+// (C) Copyright 2008 Wojciech Dzierzanowski
+//
+
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using System;
+
+namespace FSpotTabbloExport {
+
+	class Preferences : Mono.Tabblo.IPreferences {
+
+		private string username;
+		private string password;
+
+		public string Username {
+			get {
+				return username;
+			}
+		}
+		public string Password {
+			get {
+				return password;
+			}
+		}
+		public string Privacy {
+			get {
+				return "circle";
+			}
+		}
+
+		internal void SetUsername (string username) {
+			this.username = username;
+		}
+
+		internal void SetPassword (string password) {
+			this.password = password;
+		}
+	}
+}

Added: trunk/extensions/Exporters/TabbloExport/Tabblo/AssemblyInfo.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/TabbloExport/Tabblo/AssemblyInfo.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,32 @@
+#region Using directives
+
+using System.Reflection;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+
+#endregion
+
+// General Information about an assembly is controlled through the following
+// set of attributes. Change these attribute values to modify the information
+// associated with an assembly.
+[assembly: AssemblyTitle ("Mono.Tabblo")]
+[assembly: AssemblyDescription ("A library implementing photo upload to Tabblo"
+	+ " over HTTP")]
+[assembly: AssemblyConfiguration ("")]
+[assembly: AssemblyCompany ("")]
+[assembly: AssemblyProduct ("Mono.Tabblo")]
+[assembly: AssemblyCopyright ("")]
+[assembly: AssemblyTrademark ("")]
+[assembly: AssemblyCulture ("")]
+
+// This sets the default COM visibility of types in the assembly to invisible.
+// If you need to expose a type to COM, use [ComVisible(true)] on that type.
+[assembly: ComVisible (false)]
+
+// The assembly version has following format :
+//
+// Major.Minor.Build.Revision
+//
+// You can specify all the values or you can use the default the Revision and
+// Build Numbers by using the '*' as shown below:
+[assembly: AssemblyVersion ("0.1.*")]

Added: trunk/extensions/Exporters/TabbloExport/Tabblo/Connection.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/TabbloExport/Tabblo/Connection.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,464 @@
+//
+// Mono.Tabblo.Connection
+//
+// Authors:
+//	Wojciech Dzierzanowski (wojciech dzierzanowski gmail com)
+//
+// (C) Copyright 2008 Wojciech Dzierzanowski
+//
+
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using Mono.Unix;
+
+using System;
+using System.Diagnostics;
+using System.IO;
+using System.Net;
+using System.Text;
+
+using FSpot.Utils;
+
+namespace Mono.Tabblo {
+
+	public class Connection {
+
+		private const string LoginUrl =
+				"https://store.tabblo.com:443/studio/authtoken";;
+		private const string AuthorizeUrl = "https://store.tabblo.com";
+				+ ":443/studio/upload/getposturl";
+		private const string RedirUrl =	"http://www.tabblo.com/studio";
+				+ "/token/{0}/?url=/studio"
+				+ "/report_upload_session";
+
+		private readonly IPreferences preferences;
+
+		private string auth_token = null;
+		private string session_upload_url = null;
+
+		private CookieCollection cookies;
+
+
+		public Connection (IPreferences preferences)
+		{
+			if (null == preferences) {
+				throw new ArgumentNullException ("preferences");
+			}
+			this.preferences = preferences;
+			this.cookies = new CookieCollection ();
+		}
+
+
+		public void UploadFile (string name, Stream data_stream,
+		                        string mime_type, string [,] arguments)
+		{
+			if (!IsAuthenticated ()) {
+				Login ();
+			}
+
+			Log.DebugFormat ("Uploading " + mime_type + " file " + name);
+			DoUploadFile (name, data_stream, mime_type, arguments);
+		}
+
+
+		private void DoUploadFile (string name, Stream data_stream,
+		                           string mime_type,
+		                           string [,] arguments)
+		{
+			string upload_url = GetUploadUrl (arguments);
+			HttpWebRequest http_request = CreateHttpRequest (
+					upload_url, "POST", true);
+			MultipartRequest request =
+					new MultipartRequest (http_request);
+
+			MemoryStream mem_stream = null;
+			if (null != UploadProgressHandler) {
+				// "Manual buffering" using a MemoryStream.
+				request.Request.AllowWriteStreamBuffering =
+						false;
+				mem_stream = new MemoryStream ();
+				request.OutputStream = mem_stream;
+			}
+
+			request.BeginPart (true);
+			request.AddHeader ("Content-Disposition",
+					"form-data; name=\"filename0\"; "
+							+ "filename=\"" + name
+							+ '"',
+					false);
+			request.AddHeader ("Content-Type", mime_type, true);
+
+			byte [] data_buffer = new byte [8192];
+			int read_count;
+			while ((read_count = data_stream.Read (
+					data_buffer, 0, data_buffer.Length))
+							> 0) {
+				request.WritePartialContent (
+						data_buffer, 0, read_count);
+			}
+			request.EndPartialContent ();
+			request.EndPart (true);
+
+			if (null != UploadProgressHandler) {
+
+				int total = (int) request.OutputStream.Length;
+				request.Request.ContentLength = total;
+
+				string progress_title = String.Format (
+						Catalog.GetString ("Uploading "
+								+ "photo "
+								+ "\"{0}\""),
+						name);
+
+				using (Stream request_stream = request.Request
+						.GetRequestStream ()) {
+					byte [] buffer =
+							mem_stream.GetBuffer ();
+					int write_count = 0;
+					for (int offset = 0; offset < total;
+							offset += write_count) {
+						FireUploadProgress (
+								progress_title,
+								offset, total);
+						write_count = System.Math.Min (
+								16384,
+								total - offset);
+						request_stream.Write (buffer,
+								offset,
+								write_count);
+					}
+					FireUploadProgress (progress_title,
+							total, total);
+				}
+			}
+
+			SendRequest ("upload", request.Request, true);
+		}
+
+
+		public event UploadProgressEventHandler UploadProgressHandler;
+
+		private void FireUploadProgress (string title, int sent,
+		                                 int total)
+		{
+			if (null != UploadProgressHandler) {
+				UploadProgressEventArgs args =
+						new UploadProgressEventArgs (
+								title, sent,
+								total);
+				UploadProgressHandler (this, args);
+			}
+		}
+
+
+		private bool IsAuthenticated ()
+		{
+			return null != auth_token;
+		}
+
+
+		private void Login ()
+		{
+			FireUploadProgress (Catalog.GetString (
+						"Logging into Tabblo"),
+					0, 0);
+
+			auth_token = null;
+
+			HttpWebRequest request = CreateHttpRequest (
+					LoginUrl, "POST");
+			request.ContentType =
+					"application/x-www-form-urlencoded";
+
+			string [,] arguments = {
+				{"username", preferences.Username},
+				{"password", preferences.Password}
+			};
+
+			try {
+				WriteRequestContent (request,
+						FormatRequestArguments (
+								arguments));
+				string response = SendRequest (
+						"login", request);
+				if ("BAD".Equals (response)) {
+					Log.DebugFormat ("Invalid username or password");
+					throw new TabbloException (
+						"Login failed: Invalid username"
+						+ " or password");
+				}
+
+				auth_token = response;
+
+			} catch (TabbloException e) {
+				// Here's us trying to produce a more
+				// descriptive message when we have... trust
+				// issues.  This doesn't work, though, at least
+				// as long as Mono bug #346635 is not fixed.
+				//
+				// TODO: When it _starts_ to work, we should
+				// think about doing the same for
+				// `GetUploadUrl()'.
+				WebException we = e.InnerException
+						as WebException;
+				if (null != we)
+					Log.DebugFormat ("Caught a WebException, status=" + we.Status);
+				if (null != we
+					&& WebExceptionStatus.TrustFailure
+							== we.Status) {
+					throw new TabbloException (
+							"Trust failure", we);
+				}
+				throw;
+			}
+
+			if  (null != auth_token)
+				Log.DebugFormat  ("Login successful. Token: " + auth_token);
+		}
+
+
+		private string GetUploadUrl (string [,] arguments)
+		{
+			FireUploadProgress (Catalog.GetString (
+						"Obtaining URL for upload"),
+					0, 0);
+
+			if (! IsAuthenticated ())
+				Log.DebugFormat ("Not authenticated");
+
+			if (null == session_upload_url) {
+
+				string [,] auth_arguments =
+						{ {"auth_token", auth_token} };
+				string url = AuthorizeUrl + "/?"
+						+ FormatRequestArguments (
+								auth_arguments);
+
+				HttpWebRequest request =
+						CreateHttpRequest (url, "GET");
+
+				string response = SendRequest (
+						"getposturl", request);
+
+				if (response.StartsWith ("@")) {
+					session_upload_url =
+							response.Substring (1);
+				} else {
+					throw new TabbloException (
+							"Session upload URL "
+							+ "retrieval failed");
+				}
+			}
+
+			string upload_url = session_upload_url;
+			upload_url += "&redir=" + String.Format (
+					RedirUrl, auth_token);
+			if (null != arguments && arguments.GetLength (0) > 0) {
+				upload_url += '&' + FormatRequestArguments (
+						arguments);
+			}
+
+			Log.DebugFormat ("Upload URL: " + upload_url);
+			return upload_url;
+		}
+
+
+		private HttpWebRequest CreateHttpRequest (string url,
+		                                          string method)
+		{
+			return CreateHttpRequest (url, method, false);
+		}
+
+		private HttpWebRequest CreateHttpRequest (string url,
+		                                          string method,
+		                                          bool with_cookies)
+		{
+			HttpWebRequest request = (HttpWebRequest)
+					WebRequest.Create (url);
+			// For some reason, POST requests are _really_ slow with
+			// HTTP 1.1.
+			request.ProtocolVersion = HttpVersion.Version10;
+			request.Method = method;
+			if (with_cookies) {
+				HandleRequestCookies (request);
+			}
+			return request;
+		}
+
+
+		private void HandleRequestCookies (HttpWebRequest request)
+		{
+			request.CookieContainer = new CookieContainer ();
+			// Instead of just doing a
+			// `request.CookieContainer.Add(cookies)', add cookies
+			// mannually to work around the fact that some cookies
+			// are not properly formatted as they are received from
+			// the server.
+			foreach (Cookie c in cookies) {
+				Cookie new_cookie = new Cookie (c.Name, c.Value,
+						"/", ".tabblo.com");
+				request.CookieContainer.Add (new_cookie);
+			}
+
+			string cookie_header = request.CookieContainer
+					.GetCookieHeader (request.RequestUri);
+			if (cookie_header.Length > 0)
+				Log.DebugFormat ("Cookie: " + cookie_header);
+		}
+
+
+		private static void WriteRequestContent (HttpWebRequest request,
+		                                         string content)
+		{
+			byte [] content_bytes =
+					Encoding.UTF8.GetBytes (content);
+
+			request.ContentLength = content_bytes.Length;
+
+			try {
+				using (Stream request_stream =
+						request.GetRequestStream ()) {
+					request_stream.Write (content_bytes, 0,
+							content_bytes.Length);
+				}
+			} catch (WebException e) {
+				Log.Exception (e);
+				throw new TabbloException (
+						"HTTP request failure: "
+								+ e.Message,
+						e);
+			}
+
+			char [] content_chars = new char [content_bytes.Length];
+			content_bytes.CopyTo (content_chars, 0);
+			Log.DebugFormat ("Request content: " + new string (content_chars));
+		}
+
+
+		private static string FormatRequestArguments (
+				string [,] arguments)
+		{
+			StringBuilder content = new StringBuilder ();
+
+			for (int i = 0; i < arguments.GetLength (0); ++i) {
+				content.AppendFormat( "{0}={1}&",
+						arguments [i, 0],
+						arguments [i, 1]);
+			}
+
+			if (content.Length > 0) {
+				content.Remove (content.Length - 1, 1);
+			}
+
+			byte [] content_bytes =	Encoding.UTF8.GetBytes (
+					content.ToString ());
+			char [] content_chars = new char [content_bytes.Length];
+			content_bytes.CopyTo (content_chars, 0);
+
+			return new string (content_chars);
+		}
+
+
+		private string SendRequest (string description,
+		                            HttpWebRequest request)
+		{
+			return SendRequest (description, request, false);
+		}
+
+		/// <summary>
+		/// Sends an HTTP request.
+		/// </summary>
+		/// <param name="description"></param>
+		/// <param name="request"></param>
+		/// <returns>the HTTP response as string</returns>
+		private string SendRequest (string description,
+		                            HttpWebRequest request,
+		                            bool keep_cookies)
+		{
+			Log.DebugFormat ("Sending " + description + ' ' + request.Method + " request to " + request.Address);
+
+			HttpWebResponse response = null;
+			try {
+				response = (HttpWebResponse)
+						request.GetResponse ();
+				if (keep_cookies) {
+					cookies.Add (response.Cookies);
+					Log.DebugFormat (response.Cookies.Count + " cookie(s)");
+					foreach (Cookie c in response.Cookies) {
+						Log.DebugFormat ("Set-Cookie: " + c.Name + '=' + c.Value + "; Domain=" + c.Domain + "; expires=" + c.Expires);
+					}
+				}
+				return GetResponseAsString (response);
+			} catch (WebException e) {
+				Log.Exception (e);
+				HttpWebResponse error_response =
+						e.Response as HttpWebResponse;
+				string response_string = null != error_response
+						? GetResponseAsString (
+								error_response)
+						: "reason unknown";
+				throw new TabbloException (description
+							+ " failed: "
+							+ response_string,
+						e);
+			} finally {
+				if (null != response) {
+					response.Close ();
+				}
+			}
+		}
+
+
+		private static string GetResponseAsString (HttpWebResponse response)
+		{
+			Log.DebugFormat ("Response: ");
+
+			Encoding encoding = Encoding.UTF8;
+			if (response.ContentEncoding.Length > 0) {
+				try {
+					encoding = Encoding.GetEncoding (
+							response
+							.ContentEncoding);
+				} catch (ArgumentException) {
+					// Swallow invalid encoding exception
+					// and use the default one.
+				}
+			}
+
+			string response_string = null;
+
+			using (Stream stream = response.GetResponseStream ()) {
+				StreamReader reader = new StreamReader (
+						stream, encoding);
+				response_string = reader.ReadToEnd ();
+				stream.Close ();
+			}
+
+			if (null != response_string)
+				try {
+					Log.DebugFormat (response_string);
+				} catch (System.FormatException e) {
+					Log.DebugFormat ("Unable to print respose string: not in correct format");
+				}
+			return response_string;
+		}
+	}
+}

Added: trunk/extensions/Exporters/TabbloExport/Tabblo/IPreferences.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/TabbloExport/Tabblo/IPreferences.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,50 @@
+//
+// Mono.Tabblo.IPreferences
+//
+// Authors:
+//	Wojciech Dzierzanowski (wojciech dzierzanowski gmail com)
+//
+// (C) Copyright 2008 Wojciech Dzierzanowski
+//
+
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using System;
+
+namespace Mono.Tabblo {
+
+	/// <summary>
+	/// Interface for a set of Tabblo user preferences.
+	/// </summary>
+	public interface IPreferences {
+		string Username {
+			get;
+		}
+		string Password {
+			get;
+		}
+		// FIXME: It _should_ be possible to set a photo's privacy when
+		// uploading, but that's not implemented yet.
+		string Privacy {
+			get;
+		}
+	}
+}

Added: trunk/extensions/Exporters/TabbloExport/Tabblo/Makefile.am
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/TabbloExport/Tabblo/Makefile.am	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,37 @@
+include $(top_srcdir)/Makefile.include
+
+ASSEMBLY_NAME = Mono.Tabblo
+
+ASSEMBLY_SOURCES = \
+	$(srcdir)/AssemblyInfo.cs \
+	$(srcdir)/Connection.cs \
+	$(srcdir)/IPreferences.cs \
+	$(srcdir)/MultipartRequest.cs \
+	$(srcdir)/Picture.cs \
+	$(srcdir)/TabbloException.cs \
+	$(srcdir)/TotalUploadProgress.cs \
+	$(srcdir)/UploadProgressEventArgs.cs \
+	$(srcdir)/UploadProgressEventHandler.cs
+
+REFS = \
+       -r:$(top_builddir)/src/FSpot.Utils.dll \
+       -r:Mono.Posix.dll
+
+PKGS =
+
+ASSEMBLY = $(ASSEMBLY_NAME).dll
+
+all: $(ASSEMBLY)
+
+$(ASSEMBLY): $(ASSEMBLY_SOURCES)
+	$(CSC_LIB) -out:$@ $(PKGS) $(REFS) $(ASSEMBLY_SOURCES)
+
+assemblydir = $(pkglibdir)
+assembly_DATA =	$(ASSEMBLY)
+
+EXTRA_DIST =			\
+	$(ASSEMBLY_SOURCES)
+
+CLEANFILES =			\
+	$(ASSEMBLY)		\
+	$(ASSEMBLY).mdb

Added: trunk/extensions/Exporters/TabbloExport/Tabblo/MultipartRequest.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/TabbloExport/Tabblo/MultipartRequest.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,223 @@
+//
+// Mono.Tabblo.MultipartRequest
+//
+// Authors:
+//	Gonzalo Paniagua Javier (gonzalo ximian com)
+//	Stephane Delcroix (stephane delcroix org)
+//	Wojciech Dzierzanowski (wojciech dzierzanowski gmail com)
+//
+// (C) Copyright 2006 Novell, Inc. (http://www.novell.com)
+// (C) Copyright 2007 S. Delcroix
+// (C) Copyright 2008 Wojciech Dzierzanowski
+//
+
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using System;
+using System.Collections;
+using System.Diagnostics;
+using System.Globalization;
+using System.IO;
+using System.Net;
+using System.Text;
+
+using FSpot.Utils;
+
+namespace Mono.Tabblo {
+
+	class MultipartRequest {
+
+		private const string VerboseSymbol =
+				"FSPOT_TABBLO_EXPORT_VERBOSE";
+
+		private static readonly byte [] CRLF = { 13, 10 };
+
+		private const string SeparatorString = "PART_SEPARATOR";
+		private const string Separator =
+				"--" + SeparatorString + "\r\n";
+		private const string SeparatorEnd =
+				"--" + SeparatorString + "--\r\n";
+
+		private static readonly byte [] SeparatorBytes =
+				Encoding.ASCII.GetBytes (Separator);
+		private static readonly byte [] SeparatorEndBytes =
+				Encoding.ASCII.GetBytes (SeparatorEnd);
+
+		private HttpWebRequest request;
+		private Stream output_stream;
+
+		bool output_set;
+
+
+		public MultipartRequest (HttpWebRequest http_request)
+		{
+			request = http_request;
+			request.ContentType = "multipart/form-data; boundary="
+					+ SeparatorString;
+		}
+
+
+		public HttpWebRequest Request {
+			get {
+				return request;
+			}
+		}
+
+		public Stream OutputStream {
+			get {
+				return output_stream;
+			}
+			set {
+				output_set = true;
+				output_stream = value;
+			}
+		}
+
+
+		public void BeginPart ()
+		{
+			BeginPart (false);
+		}
+
+		public void BeginPart (bool first)
+		{
+			if (!first) {
+				return;
+			}
+
+			LogBeginPart ();
+			InitContent ();
+			AppendContent (SeparatorBytes, 0,
+					SeparatorBytes.Length);
+		}
+
+
+		public void AddHeader (string name, string val)
+		{
+			AddHeader (name, val, false);
+		}
+
+		public void AddHeader (string name, string val, bool last)
+		{
+			AddHeader (String.Format ("{0}: {1}", name, val), last);
+		}
+
+		public void AddHeader (string header)
+		{
+			AddHeader (header, false);
+		}
+
+		public void AddHeader (string header, bool last)
+		{
+			bool need_crlf = !header.EndsWith ("\r\n");
+			byte [] bytes = Encoding.UTF8.GetBytes (header);
+			AppendContent (bytes, 0, bytes.Length);
+			if (need_crlf) {
+				AppendContent (CRLF, 0, CRLF.Length);
+			}
+			if (last) {
+				AppendContent (CRLF, 0, CRLF.Length);
+			}
+		}
+
+
+		public void WriteContent (string content)
+		{
+			WriteContent (Encoding.UTF8.GetBytes (content));
+		}
+
+		public void WriteContent (byte [] content)
+		{
+			AppendContent (content, 0, content.Length);
+			AppendContent (CRLF, 0, CRLF.Length);
+		}
+
+		public void WritePartialContent (byte [] content, int offset,
+		                                 int nbytes)
+		{
+			AppendContent (content, offset, nbytes);
+		}
+
+
+		public void EndPartialContent ()
+		{
+			AppendContent (CRLF, 0, CRLF.Length);
+		}
+
+		public void EndPart (bool last)
+		{
+			if (last) {
+				LogEndPart ();
+				AppendContent (SeparatorEndBytes, 0,
+						SeparatorEndBytes.Length);
+				CloseContent ();
+			} else {
+				AppendContent (SeparatorBytes, 0,
+						SeparatorBytes.Length);
+			}
+		}
+
+
+		private void InitContent ()
+		{
+			if (output_stream == null) {
+				output_stream = request.GetRequestStream ();
+			}
+		}
+
+		private void CloseContent ()
+		{
+			if (!output_set) {
+				output_stream.Close ();
+			}
+		}
+
+		private void AppendContent (byte [] content, int offset,
+		                            int length)
+		{
+			LogContent (content, offset, length);
+			output_stream.Write (content, offset, length);
+		}
+
+
+
+		[Conditional (VerboseSymbol)]
+		private static void LogBeginPart ()
+		{
+			Log.DebugFormat (">>>START MultipartRequest content");
+		}
+
+		[Conditional (VerboseSymbol)]
+		private static void LogEndPart ()
+		{
+			Log.DebugFormat ("<<<END MultipartRequest content");
+		}
+
+		[Conditional (VerboseSymbol)]
+		private static void LogContent (byte [] content, int offset,
+		                                int length)
+		{
+			char [] content_chars = new char [length];
+			Array.Copy (content, offset, content_chars, 0, length);
+			Log.DebugFormat (new string (content_chars));
+		}
+	}
+}

Added: trunk/extensions/Exporters/TabbloExport/Tabblo/Picture.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/TabbloExport/Tabblo/Picture.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,100 @@
+//
+// Mono.Tabblo.Picture
+//
+// Authors:
+//	Wojciech Dzierzanowski (wojciech dzierzanowski gmail com)
+//
+// (C) Copyright 2008 Wojciech Dzierzanowski
+//
+
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using System;
+using System.IO;
+
+namespace Mono.Tabblo {
+
+	public class Picture {
+
+		private readonly string name;
+		private readonly Uri uri;
+		private readonly string mime_type;
+		private readonly string privacy;
+
+
+		public string Name {
+			get {
+				return name;
+			}
+		}
+		public Uri Uri {
+			get {
+				return uri;
+			}
+		}
+		public string MimeType {
+			get {
+				return mime_type;
+			}
+		}
+		public string Privacy {
+			get {
+				return privacy;
+			}
+		}
+
+
+		public Picture (string name, Uri uri, string mime_type,
+		                string privacy)
+		{
+			if (null == name) {
+				throw new ArgumentNullException ("name");
+			}
+			if (null == uri) {
+				throw new ArgumentNullException ("uri");
+			}
+			if (null == mime_type) {
+				throw new ArgumentNullException ("mime_type");
+			}
+			if (null == privacy) {
+				throw new ArgumentNullException ("privacy");
+			}
+			this.name = name;
+			this.uri = uri;
+			this.mime_type = mime_type;
+			this.privacy = privacy;
+		}
+
+
+		public void Upload (Connection connection)
+		{
+			if (null == connection) {
+				throw new ArgumentNullException ("connection");
+			}
+
+			using (Stream data_stream =
+					File.OpenRead (Uri.LocalPath)) {
+				connection.UploadFile (Name, data_stream,
+						MimeType, null);
+			}
+		}
+	}
+}

Added: trunk/extensions/Exporters/TabbloExport/Tabblo/TabbloException.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/TabbloExport/Tabblo/TabbloException.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,53 @@
+//
+// Mono.Tabblo.TabbloException
+//
+// Authors:
+//	Wojciech Dzierzanowski (wojciech dzierzanowski gmail com)
+//
+// (C) Copyright 2008 Wojciech Dzierzanowski
+//
+
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using System;
+
+namespace Mono.Tabblo {
+
+	public class TabbloException : ApplicationException {
+
+		public TabbloException ()
+		{
+		}
+
+		public TabbloException (string message) : base (message)
+		{
+		}
+
+		public TabbloException (Exception cause) : base ("", cause)
+		{
+		}
+
+		public TabbloException (string message, Exception cause)
+			: base (message, cause)
+		{
+		}
+	}
+}

Added: trunk/extensions/Exporters/TabbloExport/Tabblo/TotalUploadProgress.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/TabbloExport/Tabblo/TotalUploadProgress.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,114 @@
+//
+// Mono.Tabblo.TotalUploadProgress
+//
+// Authors:
+//	Wojciech Dzierzanowski (wojciech dzierzanowski gmail com)
+//
+// (C) Copyright 2008 Wojciech Dzierzanowski
+//
+
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using System;
+using System.IO;
+using System.Diagnostics;
+
+using FSpot.Utils;
+
+namespace Mono.Tabblo {
+
+	public class TotalUploadProgress {
+
+		private long prev_bytes_sent = 0;
+		private long bytes_sent_total = 0;
+
+		private int total_file_count;
+		private ulong total_file_size;
+
+
+		public TotalUploadProgress (Picture [] pictures)
+		{
+			if (null == pictures) Log.DebugFormat ("No pictures!");
+
+			total_file_count = pictures.Length;
+			total_file_size = GetTotalFileSize (pictures);
+		}
+
+
+		protected int TotalFileCount {
+			get {
+				return total_file_count;
+			}
+		}
+		protected ulong TotalFileSize {
+			get {
+				return total_file_size;
+			}
+		}
+
+
+		public void HandleProgress (object sender,
+		                            UploadProgressEventArgs args)
+		{
+			bytes_sent_total += args.BytesSent - prev_bytes_sent;
+			ShowProgress (args.Title, bytes_sent_total);
+
+			int percent = (int)
+					((double) bytes_sent_total * 100
+							/ TotalFileSize);
+			Log.DebugFormat ("{0}%...", percent);
+
+			if (args.BytesTotal == args.BytesSent) {
+				prev_bytes_sent = 0;
+			} else {
+				prev_bytes_sent = args.BytesSent;
+			}
+		}
+
+
+		/// <summary>
+		/// Overriden by subclasses to display upload progress.
+		/// </summary>
+		/// <remarks>
+		/// The default implementation does nothing.
+		/// </remarks>
+		protected virtual void ShowProgress (string title,
+		                                     long bytes_sent)
+		{
+		}
+
+
+		private static ulong GetTotalFileSize (Picture [] pictures)
+		{
+			if (null == pictures) Log.DebugFormat ("No pictures!");
+
+			ulong size = 0;
+
+			foreach (Picture picture in pictures) {
+				FileInfo info = new FileInfo (
+						picture.Uri.LocalPath);
+				size += (ulong) info.Length;
+			}
+
+			return size;
+		}
+	}
+}

Added: trunk/extensions/Exporters/TabbloExport/Tabblo/UploadProgressEventArgs.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/TabbloExport/Tabblo/UploadProgressEventArgs.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,68 @@
+//
+// Mono.Tabblo.UploadProgressEventArgs
+//
+// Authors:
+//	Gonzalo Paniagua Javier (gonzalo ximian com)
+//	Wojciech Dzierzanowski (wojciech dzierzanowski gmail com)
+//
+// (C) Copyright 2006 Novell, Inc. (http://www.novell.com)
+// (C) Copyright 2008 Wojciech Dzierzanowski
+//
+
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using System;
+
+namespace Mono.Tabblo {
+
+	public sealed class UploadProgressEventArgs : EventArgs {
+
+		private readonly string title;
+		private readonly long bytes_sent;
+		private readonly long bytes_total;
+
+		internal UploadProgressEventArgs (string title, long bytes_sent,
+		                                  long bytes_total)
+		{
+			this.title = title;
+			this.bytes_sent = bytes_sent;
+			this.bytes_total = bytes_total;
+		}
+
+		public string Title {
+			get {
+				return title;
+			}
+		}
+
+		public long BytesSent {
+			get {
+				return bytes_sent;
+			}
+		}
+
+		public long BytesTotal {
+			get {
+				return bytes_total;
+			}
+		}
+	}
+}

Added: trunk/extensions/Exporters/TabbloExport/Tabblo/UploadProgressEventHandler.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/TabbloExport/Tabblo/UploadProgressEventHandler.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,37 @@
+//
+// Mono.abblo.UploadProgressEventHandler
+//
+// Authors:
+//	Gonzalo Paniagua Javier (gonzalo ximian com)
+//	Wojciech Dzierzanowski (wojciech dzierzanowski gmail com)
+//
+// (C) Copyright 2006 Novell, Inc. (http://www.novell.com)
+// (C) Copyright 2008 Wojciech Dzierzanowski
+//
+
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+namespace Mono.Tabblo {
+
+	public delegate void UploadProgressEventHandler (
+			object sender, UploadProgressEventArgs args);
+
+}

Copied: trunk/extensions/Exporters/TabbloExport/TabbloExport.addin.xml (from r4368, /trunk/extensions/TabbloExport/TabbloExport.addin.xml)
==============================================================================

Added: trunk/extensions/Exporters/TabbloExport/TabbloExport.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/TabbloExport/TabbloExport.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,284 @@
+//
+// FSpotTabbloExport.TabbloExport
+//
+// Authors:
+//	Wojciech Dzierzanowski (wojciech dzierzanowski gmail com)
+//
+// (C) Copyright 2008 Wojciech Dzierzanowski
+//
+
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using Mono.Tabblo;
+using Mono.Unix;
+
+using System;
+using System.Collections;
+using System.Diagnostics;
+using System.Net;
+using System.Threading;
+
+using FSpot.Utils;
+
+namespace FSpotTabbloExport {
+	/// <summary>
+	/// </summary>
+	public class TabbloExport : FSpot.Extensions.IExporter {
+
+		private readonly Preferences preferences;
+		private readonly Connection connection;
+
+		private FSpot.IBrowsableCollection photos;
+
+		private const string DialogName = "tabblo_export_dialog";
+		[Glade.Widget] Gtk.Dialog dialog;
+		[Glade.Widget] Gtk.Entry username_entry;
+		[Glade.Widget] Gtk.Entry password_entry;
+		[Glade.Widget] Gtk.Button export_button;
+		[Glade.Widget] Gtk.Button cancel_button;
+		[Glade.Widget] Gtk.ScrolledWindow thumb_scrolled_window;
+
+		private FSpot.ThreadProgressDialog progress_dialog;
+
+		// Keyring constants.
+		private const string KeyringItemName = "Tabblo Account";
+		private const string KeyringItemApp = "FSpotTabbloExport";
+		private const string KeyringItemNameAttr = "name";
+		private const string KeyringItemUsernameAttr = "username";
+		private const string KeyringItemAppAttr = "application";
+
+
+		public TabbloExport ()
+		{
+			preferences = new Preferences ();
+			connection = new Connection (preferences);
+		}
+
+
+		public void Run (FSpot.IBrowsableCollection photos)
+		{
+			if (null == photos) {
+				throw new ArgumentNullException ("photos");
+			}
+
+			this.photos = photos;
+
+			Glade.XML glade_xml = new Glade.XML (
+					null, "TabbloExport.glade", DialogName,
+					"f-spot");
+			glade_xml.Autoconnect (this);
+
+			dialog = (Gtk.Dialog) glade_xml.GetWidget (DialogName);
+
+			FSpot.Widgets.IconView icon_view =
+					new FSpot.Widgets.IconView (photos);
+			icon_view.DisplayDates = false;
+			icon_view.DisplayTags = false;
+
+			username_entry.Changed += HandleAccountDataChanged;
+			password_entry.Changed += HandleAccountDataChanged;
+			ReadAccountData ();
+			HandleAccountDataChanged (null, null);
+
+			dialog.Modal = false;
+			dialog.TransientFor = null;
+
+			dialog.Response += HandleResponse;
+
+			thumb_scrolled_window.Add (icon_view);
+			icon_view.Show ();
+			dialog.Show ();
+		}
+
+
+		private void HandleAccountDataChanged (object sender,
+		                                       EventArgs args)
+		{
+			preferences.SetUsername (username_entry.Text);
+			preferences.SetPassword (password_entry.Text);
+
+			export_button.Sensitive =
+					preferences.Username.Length > 0
+					&& preferences.Password.Length > 0;
+		}
+
+
+		private void HandleResponse (object sender,
+		                             Gtk.ResponseArgs args)
+		{
+			dialog.Destroy ();
+
+			if (Gtk.ResponseType.Ok != args.ResponseId) {
+				Log.DebugFormat ("Tabblo export was canceled.");
+				return;
+			}
+
+			WriteAccountData ();
+
+			Log.DebugFormat ("Starting Tabblo export");
+
+			Thread upload_thread =
+					new Thread (new ThreadStart (Upload));
+			progress_dialog = new FSpot.ThreadProgressDialog (
+					upload_thread, photos.Items.Length);
+			progress_dialog.Start ();
+		}
+
+
+		private void Upload ()
+		{
+			if (null == connection) Log.DebugFormat ("No connection");
+
+			Picture [] pictures = GetPicturesForUpload ();
+
+			FSpotUploadProgress fup = new FSpotUploadProgress (
+					pictures, progress_dialog);
+			connection.UploadProgressHandler += fup.HandleProgress;
+
+			ServicePointManager.CertificatePolicy =
+					new UserDecisionCertificatePolicy ();
+
+			try {
+				foreach (Picture picture in pictures) {
+					picture.Upload (connection);
+				}
+
+				progress_dialog.Message = Catalog.GetString (
+						"Done sending photos");
+				progress_dialog.ProgressText = Catalog
+						.GetString ("Upload complete");
+				progress_dialog.Fraction = 1;
+				progress_dialog.ButtonLabel = Gtk.Stock.Ok;
+
+			} catch (TabbloException e) {
+				progress_dialog.Message = Catalog.GetString (
+						"Error uploading to Tabblo: ")
+						+ e.Message;
+				progress_dialog.ProgressText =
+						Catalog.GetString ("Error");
+				// FIXME:  Retry logic?
+//				  progressDialog.PerformRetrySkip ();
+				Log.DebugFormat ("Error uploading:\n" + e);
+			} finally {
+				connection.UploadProgressHandler -=
+						fup.HandleProgress;
+			}
+		}
+
+
+		private Picture [] GetPicturesForUpload ()
+		{
+			if (null == photos) Log.DebugFormat ("No photos");
+			if (null == preferences) Log.DebugFormat ("No preferences");
+
+			Picture [] pictures = new Picture [photos.Items.Length];
+
+			for (int i = 0; i < pictures.Length; ++i) {
+				FSpot.IBrowsableItem photo = photos.Items [i];
+
+				// FIXME: GnomeVFS is deprecated, we should use
+				// GIO instead.  However, I don't know how to
+				// call `GLib.Content.TypeGuess ()'.
+				string path = photo.DefaultVersionUri.LocalPath;
+				string mime_type = Gnome.Vfs.MimeType
+						.GetMimeTypeForUri (path);
+
+				pictures [i] = new Picture (photo.Name,
+						photo.DefaultVersionUri,
+						mime_type,
+						preferences.Privacy);
+			}
+
+			return pictures;
+		}
+
+
+		private void ReadAccountData ()
+		{
+			Hashtable attrs = new Hashtable ();
+			attrs [KeyringItemNameAttr] = KeyringItemName;
+			attrs [KeyringItemAppAttr] = KeyringItemApp;
+
+			try {
+				Gnome.Keyring.ItemType type = Gnome.Keyring
+						.ItemType.GenericSecret;
+				Gnome.Keyring.ItemData [] items =
+						Gnome.Keyring.Ring.Find (
+								type, attrs);
+				if (items.Length > 1)
+					Log.Warning ("More than one " + KeyringItemName + "found in keyring");
+
+				if (1 <= items.Length) {
+					Log.DebugFormat (KeyringItemName + " data found in " + "keyring");
+					attrs =	items [0].Attributes;
+					username_entry.Text = (string) attrs [
+						KeyringItemUsernameAttr];
+					password_entry.Text = items [0].Secret;
+				}
+
+			} catch (Gnome.Keyring.KeyringException e) {
+				Log.DebugFormat ("Error while reading account data:\n" + e);
+			}
+		}
+
+
+		private void WriteAccountData ()
+		{
+			try {
+				string keyring = Gnome.Keyring
+						.Ring.GetDefaultKeyring ();
+
+				Hashtable attrs = new Hashtable ();
+				attrs [KeyringItemNameAttr] = KeyringItemName;
+				attrs [KeyringItemAppAttr] = KeyringItemApp;
+
+				Gnome.Keyring.ItemType type = Gnome.Keyring
+						.ItemType.GenericSecret;
+
+				try {
+					Gnome.Keyring.ItemData [] items = Gnome
+							.Keyring.Ring.Find (
+									type,
+									attrs);
+
+					foreach (Gnome.Keyring.ItemData item
+							in items) {
+						Gnome.Keyring.Ring.DeleteItem (
+								keyring,
+								item.ItemID);
+					}
+				} catch (Gnome.Keyring.KeyringException e) {
+					Log.DebugFormat ("Error while deleting old account data:\n" + e);
+				}
+
+				attrs [KeyringItemUsernameAttr] =
+						preferences.Username;
+
+				Gnome.Keyring.Ring.CreateItem (keyring, type,
+						KeyringItemName, attrs,
+						preferences.Password, true);
+
+			} catch (Gnome.Keyring.KeyringException e) {
+				Log.DebugFormat ("Error while writing account data:\n" + e);
+			}
+		}
+	}
+}

Copied: trunk/extensions/Exporters/TabbloExport/TabbloExport.glade (from r4368, /trunk/extensions/TabbloExport/TabbloExport.glade)
==============================================================================

Copied: trunk/extensions/Exporters/TabbloExport/TrustError.glade (from r4368, /trunk/extensions/TabbloExport/TrustError.glade)
==============================================================================

Added: trunk/extensions/Exporters/TabbloExport/UserDecisionCertificatePolicy.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Exporters/TabbloExport/UserDecisionCertificatePolicy.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,112 @@
+//
+// FSpotTabbloExport.UserDecisionCertificatePolicy
+//
+// Authors:
+//	Wojciech Dzierzanowski (wojciech dzierzanowski gmail com)
+//
+// (C) Copyright 2008 Wojciech Dzierzanowski
+//
+
+// Permission is hereby granted, free of charge, to any person obtaining
+// a copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to
+// permit persons to whom the Software is furnished to do so, subject to
+// the following conditions:
+//
+// The above copyright notice and this permission notice shall be
+// included in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+// LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+//
+
+using System;
+using System.Diagnostics;
+using System.Net;
+using System.Security.Cryptography.X509Certificates;
+using System.Threading;
+
+using FSpot.Utils;
+
+namespace FSpotTabbloExport {
+
+	class UserDecisionCertificatePolicy
+			: ApplicationCentricCertificatePolicy {
+
+		private const string DialogName = "trust_error_dialog";
+		[Glade.Widget] Gtk.Dialog dialog;
+		[Glade.Widget] Gtk.Label url_label;
+		[Glade.Widget] Gtk.RadioButton abort_radiobutton;
+		[Glade.Widget] Gtk.RadioButton once_radiobutton;
+		[Glade.Widget] Gtk.RadioButton always_radiobutton;
+
+		private X509Certificate certificate;
+		private WebRequest request;
+		private Decision decision;
+
+		private Object decision_lock = new Object ();
+		private ManualResetEvent decision_event;
+
+
+		protected override Decision GetDecision (
+				X509Certificate certificate,
+				WebRequest request)
+		{
+			this.certificate = certificate;
+			this.request = request;
+
+			lock (decision_lock) {
+				GLib.Idle.Add (this.DoGetDecision);
+				decision_event = new ManualResetEvent (false);
+				decision_event.WaitOne ();
+			}
+
+			return decision;
+		}
+
+		private bool DoGetDecision ()
+		{
+			Glade.XML glade_xml = new Glade.XML (
+					null, "TrustError.glade", DialogName,
+					"f-spot");
+			glade_xml.Autoconnect (this);
+
+			dialog = (Gtk.Dialog) glade_xml.GetWidget (DialogName);
+
+			url_label.Markup = String.Format (
+					url_label.Text, String.Format (
+							"<b>{0}</b>",
+							request.RequestUri));
+
+			Gtk.ResponseType response =
+					(Gtk.ResponseType) dialog.Run ();
+			Log.DebugFormat ("Decision dialog response: " + response);
+
+			dialog.Destroy ();
+
+			decision = Decision.DontTrust;
+			if (0 == response) {
+				if (abort_radiobutton.Active) {
+					decision = Decision.DontTrust;
+				} else if (once_radiobutton.Active) {
+					decision = Decision.TrustOnce;
+				} else if (always_radiobutton.Active) {
+					decision = Decision.TrustAlways;
+				} else {
+					Debug.Assert (false,
+							"Unhandled decision");
+				}
+			}
+
+			decision_event.Set ();
+			return false;
+		}
+	}
+}

Modified: trunk/extensions/Makefile.am
==============================================================================
--- trunk/extensions/Makefile.am	(original)
+++ trunk/extensions/Makefile.am	Tue Sep 16 13:11:27 2008
@@ -1,15 +1,7 @@
 SUBDIRS = 			\
-	BeagleService		\
-	CDExport		\
-	DBusService		\
-	DefaultExporters	\
-	GalleryExport		\
-	FlickrExport		\
-	FolderExport		\
-	MergeDb			\
- 	PicasaWebExport		\
- 	TabbloExport		\
- 	SmugMugExport
+	Services		\
+	Exporters		\
+	Tools
 
 addinsdir = $(pkglibdir)
 addins_DATA = f-spot.global.addins

Added: trunk/extensions/Misc/LightTable/LightTable.addin.xml
==============================================================================
--- (empty file)
+++ trunk/extensions/Misc/LightTable/LightTable.addin.xml	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,14 @@
+<Addin namespace="FSpot"
+	id="LightTable"
+	version="0.97020"
+	description="You can have a Light Table too !"
+	author="Stephane Delcroix"
+	url="http://f-spot.org/Extensions";
+	category="Viewing">
+	<Dependencies>
+		<Addin id="Core" version="0.4.4.100"/>
+	</Dependencies>
+	<Extension path = "/FSpot/Menus/PhotoPopup">
+		<Command id = "LightTable" _label = "View Selected Images on a LightTable" command_type = "LightTableExtension.LightTable"/>
+	</Extension>
+</Addin>

Added: trunk/extensions/Misc/LightTable/LightTable.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Misc/LightTable/LightTable.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,294 @@
+/*
+ * LightTable.cs
+ *
+ * Author(s)
+ * 	Stephane Delcroix  <stephane delcroix org>
+ *
+ * This is free software. See COPYING for details
+ */
+
+using System;
+using System.IO;
+using System.Reflection;
+using System.Collections.Generic;
+
+using Gtk;
+
+using FSpot;
+using FSpot.Extensions;
+
+using Gtk.Moonlight;
+using System.Windows;
+using System.Windows.Media;
+using System.Windows.Media.Animation;
+using System.Windows.Controls;
+using System.Windows.Shapes;
+using System.Windows.Input;
+
+namespace LightTableExtension
+{
+	public class LightTable: ICommand
+	{
+		public void Run (object o, EventArgs e)
+		{
+			Console.WriteLine ("EXECUTING LIGHTTABLE EXTENSION");
+
+			LightTableWidget light_table;
+
+			GtkSilver silver = new GtkSilver (1280, 855);
+			silver.Attach (light_table = new LightTableWidget ());
+
+			Gtk.Button reorganize = new Gtk.Button ("Place on grid");
+			reorganize.Clicked += delegate (object ob, EventArgs ev) {light_table.ToGrid (); };
+
+			HBox toolbar = new HBox ();
+			toolbar.Add (reorganize);
+
+			VBox vbox = new VBox ();
+			vbox.Add (silver);
+			vbox.Add (toolbar);
+
+			Window w = new Window ("Light Table");
+			w.Add (vbox);
+			w.ShowAll ();
+		}
+
+		enum Action {Select, Move, Zoom};
+
+		class LightTableWidget : Canvas
+		{
+			List <MLPhoto> photos;
+			List <MLPhoto> selected_photos;
+			bool multi_select;
+			bool mouse_down;
+			bool ctrl_down;
+			Point ref_point;
+
+			public LightTableWidget () : base ()
+			{
+				photos = new List<MLPhoto> ();
+				selected_photos = new List<MLPhoto> ();
+
+				foreach (Photo p in MainWindow.Toplevel.SelectedPhotos ()) {
+					MLPhoto p1 = new MLPhoto (p.DefaultVersionUri, this);
+					photos.Add (p1);
+				}
+
+				//hook some events
+				KeyDown += HandleKeyDown;
+				KeyUp += HandleKeyUp;
+				MouseLeftButtonDown += HandleLeftButtonDown;
+				MouseLeftButtonUp += HandleLeftButtonUp;
+				MouseMove += HandleMouseMove;
+
+				ToGrid ();
+			}
+
+			public void ToGrid ()
+			{
+				int grid = (int)Math.Ceiling (Math.Sqrt ((double)photos.Count));
+				for (int p = 0; p < photos.Count; p++) {
+					MLPhoto ph = photos [p];
+					int i = p%grid; int j = p/grid;
+					if (!Children.Contains (ph))
+						Children.Add (ph);
+					ph.Scale (1280/grid);
+					ph.SetPosition (10 + i * (10 + 1280/grid), 10 + j * (10 + 855/grid));
+					ph.HideControls ();
+				}
+			}
+
+			public void Select (MLPhoto photo)
+			{
+				if (IsSelected (photo) && multi_select) {
+					photo.HideControls ();
+					selected_photos.Remove (photo);
+					return;
+				}
+
+				//Push on top
+				Children.Remove (photo); Children.Add (photo);
+
+				if (!multi_select && selected_photos != null) {
+					foreach (MLPhoto p in selected_photos)
+						p.HideControls ();
+					selected_photos = new List<MLPhoto> ();
+				}
+				selected_photos.Add (photo);
+				photo.ShowControls ();
+			}
+
+			bool IsSelected (MLPhoto photo)
+			{
+				return (photos != null && selected_photos.Contains (photo));
+			}
+
+			void RemoveSelected ()
+			{
+				if (selected_photos == null || selected_photos.Count == 0)
+					return;
+
+				foreach (MLPhoto selected in selected_photos) {
+					Children.Remove (selected);
+					photos.Remove (selected);
+					Console.WriteLine ("Children:"+Children.Count);
+				}
+				selected_photos = new List<MLPhoto> ();
+				ToGrid ();
+			}
+
+			void HandleKeyDown (object o, KeyboardEventArgs k)
+			{
+				if (k.Key == 4) {
+					multi_select = true;
+					return;
+				}
+
+				if (k.Key == 5) {
+					ctrl_down = true;
+					return;
+				}
+
+				if (k.Key == 18) {
+					RemoveSelected ();
+					return;
+				}
+			}
+
+			void HandleKeyUp (object o, KeyboardEventArgs k)
+			{
+				if (k.Key == 4) {
+					multi_select = false;
+					return;
+				}
+
+				if (k.Key == 5) {
+					ctrl_down = false;
+					return;
+				}
+
+			}
+
+			void HandleLeftButtonDown (object o, MouseEventArgs e)
+			{
+				mouse_down = true;
+				ref_point = e.GetPosition (null);
+			}
+
+			void HandleLeftButtonUp (object o, MouseEventArgs e)
+			{
+				mouse_down = false;
+				ref_point = e.GetPosition (null);
+			}
+
+			void HandleMouseMove (object o, MouseEventArgs e)
+			{
+				if (mouse_down && ctrl_down) { //Zooming
+					Point current = e.GetPosition (null);
+					double ratio;
+					if (current.Y - ref_point.Y > 0)
+						ratio = 1.02;
+					else
+						ratio = 0.98;
+					foreach (MLPhoto selected in selected_photos)
+						selected.Zoom (ratio);
+					ref_point = current;
+					return;
+				}
+
+				if (mouse_down) { //translating
+					Point current = e.GetPosition (null);
+					foreach (MLPhoto selected in selected_photos)
+						selected.Translate ((int)(current.X - ref_point.X), (int)(current.Y - ref_point.Y));
+					ref_point = current;
+					return;
+				}
+			}
+
+		}
+
+		class MLPhoto : Control
+		{
+			LightTableWidget parent;
+
+			FrameworkElement xaml;
+			System.Windows.Controls.Image image;
+			ScaleTransform scale_transform;
+			TranslateTransform translate_transform;
+			Canvas controls;
+
+			public MLPhoto (Uri uri, LightTableWidget parent)
+			{
+				this.parent = parent;
+
+				//Load XAML
+				using (Stream stream = Assembly.GetCallingAssembly ().GetManifestResourceStream ("Photo.xaml")) {
+					using (StreamReader reader = new StreamReader (stream)) {
+						xaml = InitializeFromXaml (reader.ReadToEnd ());
+					}
+				}
+
+				//Extract Elements
+				image = xaml.FindName ("image") as System.Windows.Controls.Image;
+				scale_transform = xaml.FindName ("scaleTransform") as ScaleTransform;
+				translate_transform = xaml.FindName ("translateTransform") as TranslateTransform;
+				controls = xaml.FindName ("allControls") as Canvas;
+				Canvas translate_controls = xaml.FindName ("translateControls") as Canvas;
+
+				//Hook events
+				xaml.MouseLeftButtonDown += HandleLeftButtonDown;
+
+				//Get the image
+				Downloader downl = new Downloader ();
+				downl.Completed += DownloadComplete;
+				downl.Open ("GET", uri);
+				downl.Send ();
+			}
+
+			public void Scale (int width)
+			{
+				double ratio = (double)width / 610;
+				scale_transform.ScaleX = ratio;
+				scale_transform.ScaleY = ratio;
+			}
+
+			public void Zoom (double ratio)
+			{
+				scale_transform.ScaleX *= ratio;
+				scale_transform.ScaleY *= ratio;
+			}
+
+			public void SetPosition (int x, int y)
+			{
+				translate_transform.X = x;
+				translate_transform.Y = y;
+			}
+
+			public void Translate (int x, int y)
+			{
+				translate_transform.X += x;
+				translate_transform.Y += y;
+			}
+
+			public void ShowControls ()
+			{
+				controls.Opacity = 0.3;
+			}
+
+			public void HideControls ()
+			{
+				controls.Opacity = 0;
+			}
+
+			public void HandleLeftButtonDown (object o, MouseEventArgs e)
+			{
+				parent.Select (this);
+			}
+
+			private void DownloadComplete (object o, EventArgs e)
+			{
+				image.SetSource (o as Downloader, null);
+			}
+		}
+	}
+}

Copied: trunk/extensions/Misc/LightTable/Makefile (from r4368, /trunk/extensions/LightTable/Makefile)
==============================================================================

Added: trunk/extensions/Misc/LightTable/Photo.xaml
==============================================================================
--- (empty file)
+++ trunk/extensions/Misc/LightTable/Photo.xaml	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,17 @@
+ï<Canvas xmlns="http://schemas.microsoft.com/client/2007";
+        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml";
+        Width="600" Height="400" Background="White">
+  <Canvas.RenderTransform>
+    <TransformGroup>
+      <RotateTransform x:Name="rotateTransform" CenterX="300" CenterY="200" />
+      <ScaleTransform x:Name="scaleTransform" CenterX="0" CenterY="0" />
+      <TranslateTransform x:Name="translateTransform" />
+    </TransformGroup>
+  </Canvas.RenderTransform>
+  <Image x:Name="image" Canvas.Left="0" Canvas.Top="0" Width="600" Height="400" Stretch="Fill" />
+  <Canvas x:Name="allControls" Width="600" Height="400" Background="#00000000">
+    <Canvas x:Name="translateControls">
+      <Rectangle Canvas.Left="0" Canvas.Top="0" Width="600" Height="400" Fill="#FFFFFFFF" Stroke="#FF000000" StrokeThickness="3" Cursor="Hand" />
+    </Canvas>
+  </Canvas>
+</Canvas>

Added: trunk/extensions/Services/.gitignore
==============================================================================
--- (empty file)
+++ trunk/extensions/Services/.gitignore	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,2 @@
+/Makefile
+/Makefile.in

Copied: trunk/extensions/Services/BeagleService/.gitignore (from r4368, /trunk/extensions/PicasaWebExport/google-sharp/.gitignore)
==============================================================================

Added: trunk/extensions/Services/BeagleService/BeagleNotifier.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Services/BeagleService/BeagleNotifier.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,49 @@
+#if ENABLE_BEAGLE
+using Beagle;
+
+namespace FSpot {
+	public static class BeagleNotifier {
+		public static void SendUpdate (IBrowsableItem item)
+		{
+			Indexable indexable = new Indexable (item.DefaultVersionUri);
+			indexable.Type = IndexableType.PropertyChange;
+			Beagle.Property prop;
+
+			// Clear the existing tags
+			prop = Beagle.Property.NewKeyword ("fspot:Tag", "");
+			prop.IsMutable = true;
+			prop.IsPersistent = true;
+			indexable.AddProperty (prop);
+			prop = Beagle.Property.NewKeyword ("image:Tag", "");
+			prop.IsMutable = true;
+			prop.IsPersistent = true;
+			indexable.AddProperty (prop);
+
+			foreach (Tag t in item.Tags) {
+				prop = Beagle.Property.NewKeyword ("fspot:Tag", t.Name);
+				prop.IsMutable = true;
+				prop.IsPersistent = true;
+				indexable.AddProperty (prop);
+				prop = Beagle.Property.NewKeyword ("image:Tag", t.Name);
+				prop.IsMutable = true;
+				prop.IsPersistent = true;
+				indexable.AddProperty (prop);
+			}
+
+			prop = Beagle.Property.New ("fspot:Description", item.Description);
+			prop.IsMutable = true;
+			prop.IsPersistent = true;
+			indexable.AddProperty (prop);
+
+			// Create a message to send to the daemon with this information.
+			// The source tells it what index the existing "/home/joe/test.txt" document lives.
+			IndexingServiceRequest req = new IndexingServiceRequest ();
+			req.Keepalive = false;
+			req.Source = "Files";
+			req.Add (indexable);
+
+			req.SendAsync ();
+		}
+	}
+}
+#endif

Added: trunk/extensions/Services/BeagleService/BeagleService.addin.xml
==============================================================================
--- (empty file)
+++ trunk/extensions/Services/BeagleService/BeagleService.addin.xml	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,15 @@
+<Addin namespace="FSpot"
+	id="BeagleService"
+	version="0.4.4.100"
+	name="Beagle Service"
+	description="Notify Beagle on image changes"
+	author="Stephane Delcroix"
+	url="http://f-spot.org/Extensions";
+	category="Services">
+	<Dependencies>
+		<Addin id="Core" version="0.4.4.100"/>
+	</Dependencies>
+	<Extension path = "/FSpot/Services">
+		<Service class="BeagleService.BeagleService"/>
+	</Extension>
+</Addin>

Added: trunk/extensions/Services/BeagleService/BeagleService.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Services/BeagleService/BeagleService.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,53 @@
+/*
+ * BeagleService.BeagleService.cs
+ *
+ * Author(s):
+ *	Stephane Delcroix  <stephane delcroix org>
+ *
+ * This is free software. See COPYING for details.
+ *
+ */
+
+using System;
+using FSpot;
+using FSpot.Extensions;
+using FSpot.Utils;
+
+namespace BeagleService {
+	public class BeagleService : IService
+	{
+		public bool Start ()
+		{
+			uint timer = Log.InformationTimerStart ("Starting BeagleService");
+			try {
+				Core.Database.Photos.ItemsChanged += HandleDbItemsChanged;
+			} catch {
+				Log.Warning ("unable to hook the BeagleNotifier. are you running --view mode?");
+			}
+			Log.DebugTimerPrint (timer, "BeagleService startup took {0}");
+			return true;
+		}
+
+		public bool Stop ()
+		{
+			uint timer = Log.InformationTimerStart ("Stopping BeagleService");
+			Log.DebugTimerPrint (timer, "BeagleService shutdown took {0}");
+			return true;
+		}
+
+		private void HandleDbItemsChanged (object sender, DbItemEventArgs args)
+		{
+#if ENABLE_BEAGLE
+			Log.Debug ("Notifying beagle");
+			foreach (DbItem item in args.Items) {
+				if (item as Photo != null)
+					try {
+						BeagleNotifier.SendUpdate (item as Photo);
+					} catch (Exception e) {
+						Log.DebugFormat ("BeagleNotifier.SendUpdate failed with {0}", e.Message);
+					}
+			}
+#endif
+		}
+	}
+}

Copied: trunk/extensions/Services/BeagleService/Makefile.am (from r4368, /trunk/extensions/BeagleService/Makefile.am)
==============================================================================

Copied: trunk/extensions/Services/DBusService/.gitignore (from r4368, /trunk/extensions/SmugMugExport/.gitignore)
==============================================================================

Added: trunk/extensions/Services/DBusService/DBusProxy.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Services/DBusService/DBusProxy.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,447 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+
+using FSpot;
+using NDesk.DBus;
+
+namespace DBusService {
+	public delegate void RemoteUp();
+	public delegate void RemoteDown();
+
+	public class DBusException : Exception {
+		public DBusException (string message)
+			: base (message)
+		{}
+
+		public DBusException (string message, params object [] format_params)
+			: this (string.Format (message, format_params))
+		{}
+	}
+
+	public class DBusProxy {
+		public event RemoteUp RemoteUp;
+		public event RemoteDown RemoteDown;
+
+		internal void OnRemoteUp ()
+		{
+			if (RemoteUp != null)
+				RemoteUp ();
+		}
+
+		internal void OnRemoteDown ()
+		{
+			if (RemoteDown != null)
+				RemoteDown ();
+		}
+	}
+
+	public class DBusProxyFactory {
+		private const string SERVICE_PATH = "org.gnome.FSpot";
+		private const string TAG_PROXY_PATH = "/org/gnome/FSpot/TagRemoteControl";
+		private const string PHOTO_PROXY_PATH = "/org/gnome/FSpot/PhotoRemoteControl";
+
+		private static TagProxy tag_remote;
+		private static PhotoProxy photo_remote;
+
+		public static void Load (Db db)
+		{
+			tag_remote = new TagProxy (db.Tags);
+			Bus.Session.Register (SERVICE_PATH, new ObjectPath (TAG_PROXY_PATH), tag_remote);
+			tag_remote.OnRemoteUp ();
+
+			photo_remote = new PhotoProxy (db);
+			Bus.Session.Register (SERVICE_PATH, new ObjectPath (PHOTO_PROXY_PATH), photo_remote);
+			photo_remote.OnRemoteUp ();
+		}
+
+		public static void EmitRemoteDown ()
+		{
+			tag_remote.OnRemoteDown ();
+			photo_remote.OnRemoteDown ();
+		}
+
+	}
+
+	[Interface ("org.gnome.FSpot.TagRemoteControl")]
+	public interface ITagRemoteControl {
+		// info, included for backward compatibility with times
+		// where this was embedded in f-spot core
+		bool IsReadOnly ();
+
+		// get all
+		string[] GetTagNames ();
+		uint[] GetTagIds ();
+
+		// get info for one tag
+		IDictionary<string, object> GetTagByName (string name);
+		IDictionary<string, object> GetTagById (int id);
+
+		event RemoteUp RemoteUp;
+		event RemoteDown RemoteDown;
+
+		// tag creators
+		int CreateTag (string name);
+		int CreateTagWithParent (string parent, string name);
+
+		// tag removers
+		bool RemoveTagByName (string name);
+		bool RemoveTagById (int id);
+	}
+
+	// Class exposing all photos on the dbus
+	public class TagProxy : DBusProxy, ITagRemoteControl {
+		protected TagStore tag_store;
+
+		public TagStore Store {
+			get { return tag_store; }
+		}
+
+		internal TagProxy (TagStore store)
+		{
+			tag_store = store;
+		}
+
+		#region Interface methods
+		public bool IsReadOnly () {
+			return false;
+		}
+
+		public string[] GetTagNames ()
+		{
+			List<string> tags = new List<string> ();
+			AddTagNameToList (tags, tag_store.RootCategory);
+
+			return tags.ToArray ();
+		}
+
+		public uint[] GetTagIds ()
+		{
+			List<uint> tags = new List<uint> ();
+			AddTagIdToList (tags, tag_store.RootCategory);
+
+			return tags.ToArray ();
+		}
+
+		public IDictionary<string, object> GetTagByName (string name)
+		{
+			Tag t = tag_store.GetTagByName (name);
+
+			if (t == null)
+				throw new DBusException ("Tag with name {0} does not exist.", name);
+
+			return CreateDictFromTag (t);
+		}
+
+		public IDictionary<string, object> GetTagById (int id)
+		{
+			Tag t = tag_store.GetTagById (id);
+
+			if (t == null)
+				throw new DBusException ("Tag with id {0} does not exist.", id);
+
+			return CreateDictFromTag (t);
+		}
+
+		public int CreateTag (string name)
+		{
+			return CreateTagPriv (null, name);
+		}
+
+		public int CreateTagWithParent (string parent, string name)
+		{
+			Tag parent_tag = tag_store.GetTagByName (parent);
+
+			if (!(parent_tag is Category))
+				parent_tag = null;
+
+			return CreateTagPriv (parent_tag as Category, name);
+		}
+
+		public bool RemoveTagByName (string name)
+		{
+			Tag tag = tag_store.GetTagByName (name);
+
+			return RemoveTag (tag);
+		}
+
+		public bool RemoveTagById (int id)
+		{
+			Tag tag = tag_store.GetTagById (id);
+
+			return RemoveTag (tag);
+		}
+
+		#endregion
+
+		#region Helper methods
+		private void AddTagNameToList (List<string> list, Tag tag)
+		{
+			if (tag != tag_store.RootCategory)
+				list.Add (tag.Name);
+
+			if (tag is Category) {
+				foreach (Tag child in (tag as Category).Children)
+					AddTagNameToList (list, child);
+			}
+		}
+
+		private void AddTagIdToList (List<uint> list, Tag tag)
+		{
+			if (tag != tag_store.RootCategory)
+				list.Add (tag.Id);
+
+			if (tag is Category) {
+				foreach (Tag child in (tag as Category).Children)
+					AddTagIdToList (list, child);
+			}
+		}
+
+		private IDictionary<string, object> CreateDictFromTag (Tag t)
+		{
+			Dictionary<string, object> result = new Dictionary<string, object> ();
+
+			result.Add ("Id", t.Id);
+			result.Add ("Name", t.Name);
+
+			StringBuilder builder = new StringBuilder ();
+
+			if (t is Category) {
+				foreach (Tag child in (t as Category).Children) {
+					if (builder.Length > 0)
+						builder.Append (",");
+
+					builder.Append (child.Name);
+				}
+			}
+
+			result.Add ("Children", builder.ToString ());
+
+			return result;
+		}
+
+		private int CreateTagPriv (Category parent_tag, string name)
+		{
+			try {
+				Tag created = tag_store.CreateCategory (parent_tag, name);
+				return (int)created.Id;
+			}
+			catch {
+				throw new DBusException ("Failed to create tag.");
+			}
+		}
+
+		private bool RemoveTag (Tag t)
+		{
+			if (t == null)
+				return false;
+
+			try {
+				// remove tags from photos first
+				Core.Database.Photos.Remove (new Tag [] { t });
+				// then remove tag
+				tag_store.Remove (t);
+				return true;
+			}
+			catch {
+				return false;
+			}
+		}
+		#endregion
+	}
+
+
+	[Interface ("org.gnome.FSpot.PhotoRemoteControl")]
+	public interface IPhotoRemoteControl {
+		// info; included for backward compatibility
+		// with previous version where this was embedded in f-spot
+		bool IsReadOnly ();
+
+		// get all
+		uint[] GetPhotoIds ();
+
+		// import prepare
+		void PrepareRoll ();
+		void FinishRoll ();
+
+		// import
+		int ImportPhoto (string path, bool copy, string []tags);
+
+		// photo properties
+		IDictionary<string, object> GetPhotoProperties (uint id);
+
+		// photo remove
+		void RemovePhoto (uint id);
+
+		// query
+		uint[] Query (string []tags);
+
+		// events
+		event RemoteUp RemoteUp;
+		event RemoteDown RemoteDown;
+	}
+
+	// Class exposing all photos on the dbus
+	public class PhotoProxy : DBusProxy, IPhotoRemoteControl {
+		protected Db db;
+		private Roll current_roll;
+
+		public Db Database {
+			get { return db; }
+		}
+
+		internal PhotoProxy (Db db)
+		{
+			this.db = db;
+		}
+
+		public bool IsReadOnly ()
+		{
+			return false;
+		}
+
+		public uint[] GetPhotoIds ()
+		{
+			List<uint> ids = new List<uint> ();
+
+			foreach (Photo p in QueryAll ())
+				ids.Add (p.Id);
+
+			return ids.ToArray ();
+		}
+
+		public IDictionary<string, object> GetPhotoProperties (uint id)
+		{
+			Dictionary<string, object> dict = new Dictionary<string, object> ();
+
+			Photo p = db.Photos.Get (id) as Photo;
+
+			if (p == null)
+				throw new DBusException ("Photo with id {0} does not exist.", id);
+
+			dict.Add ("Uri", p.DefaultVersionUri.ToString());
+			dict.Add ("Id", p.Id);
+			dict.Add ("Name", p.Name);
+			dict.Add ("Description", p.Description ?? string.Empty);
+
+			StringBuilder builder = new StringBuilder ();
+
+			foreach (Tag t in p.Tags) {
+				if (builder.Length > 0)
+					builder.Append (",");
+
+				builder.AppendFormat (t.Name);
+			}
+
+			dict.Add ("Tags", builder.ToString ());
+
+			return dict;
+		}
+
+		public uint[] Query (string []tags)
+		{
+			List<Tag> tag_list = GetTagsByNames (tags);
+
+			Photo []photos = db.Photos.Query (tag_list.ToArray ());
+
+			uint []ids = new uint[photos.Length];
+
+			for (int i = 0; i < ids.Length; i++)
+				ids[i] = photos[i].Id;
+
+			return ids;
+		}
+
+		protected Photo[] QueryAll ()
+		{
+			return db.Photos.Query ((Tag [])null);
+		}
+
+		protected List<Tag> GetTagsByNames (string []names)
+		{
+			// add tags that exist in tag store
+			List<Tag> tag_list = new List<Tag> ();
+
+			foreach (string tag_name in names) {
+				Tag t = db.Tags.GetTagByName (tag_name);
+
+				if (t == null)
+					continue;
+
+				tag_list.Add (t);
+			}
+
+			return tag_list;
+		}
+
+		public void PrepareRoll ()
+		{
+			if (current_roll != null)
+				return;
+
+			current_roll = db.Rolls.Create ();
+		}
+
+		public void FinishRoll ()
+		{
+			current_roll = null;
+		}
+
+		public int ImportPhoto (string path, bool copy, string []tags)
+		{
+			if (current_roll == null)
+				throw new DBusException ("You must use PrepareRoll before you can import a photo.");
+
+			// add tags that exist in tag store
+			List<Tag> tag_list = GetTagsByNames (tags);
+
+			Gdk.Pixbuf pixbuf = null;
+
+			// FIXME: this is more or less a copy of the file import backend code
+			// this should be streamlined
+			try {
+				string new_path = path;
+
+				if (copy)
+					new_path = FileImportBackend.ChooseLocation (path);
+
+				if (new_path != path)
+					System.IO.File.Copy (path, new_path);
+
+				Photo created = db.Photos.CreateOverDBus (new_path, path, current_roll.Id, out pixbuf);
+
+				try {
+					File.SetAttributes (new_path, File.GetAttributes (new_path) & ~FileAttributes.ReadOnly);
+					DateTime create = File.GetCreationTime (path);
+					File.SetCreationTime (new_path, create);
+					DateTime mod = File.GetLastWriteTime (path);
+					File.SetLastWriteTime (new_path, mod);
+				} catch (IOException) {
+					// we don't want an exception here to be fatal.
+				}
+
+				// attach tags we got
+				if (tag_list.Count > 0) {
+					created.AddTag (tag_list.ToArray ());
+					db.Photos.Commit (created);
+				}
+
+				return (int)created.Id;
+			// indicate failure
+			} catch {
+				throw new DBusException ("Failed to import the photo.");
+			}
+		}
+
+		public void RemovePhoto (uint id)
+		{
+			Photo p = db.Photos.Get (id) as Photo;
+
+			if (p == null)
+				throw new DBusException ("Photo with id {0} does not exist.", id);
+
+			db.Photos.RemoveOverDBus (p);
+		}
+	}
+}

Added: trunk/extensions/Services/DBusService/DBusService.addin.xml
==============================================================================
--- (empty file)
+++ trunk/extensions/Services/DBusService/DBusService.addin.xml	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,16 @@
+<Addin namespace="FSpot"
+	id="DBusService"
+	version="0.4.4.100"
+	name="DBus Service"
+	description="Expose F-Spot Photos over DBus"
+	author="Thomas Van Machelen"
+	url="http://f-spot.org/Extensions";
+	defaultEnabled="false"
+	category="Services">
+	<Dependencies>
+		<Addin id="Core" version="0.4.4.100"/>
+	</Dependencies>
+	<Extension path = "/FSpot/Services">
+		<Service class="DBusService.DBusService"/>
+	</Extension>
+</Addin>

Added: trunk/extensions/Services/DBusService/DBusService.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Services/DBusService/DBusService.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,41 @@
+/*
+ * DBusService.DBusService.cs
+ *
+ * Author(s):
+ *	Thomas Van Machelen <thomas vanmachelen gmail com>
+ *
+ * This is free software. See COPYING for details.
+ *
+ */
+
+using System;
+using FSpot;
+using FSpot.Extensions;
+using FSpot.Utils;
+
+namespace DBusService {
+	public class DBusService : IService
+	{
+		public bool Start ()
+		{
+			uint timer = Log.InformationTimerStart ("Starting DBusService");
+			try {
+				DBusProxyFactory.Load (Core.Database);
+			} catch {
+				Log.Warning ("unable init DBus service");
+			}
+			Log.DebugTimerPrint (timer, "DBusService startup took {0}");
+			return true;
+		}
+
+		public bool Stop ()
+		{
+			uint timer = Log.InformationTimerStart ("Stopping DBusService");
+
+			DBusProxyFactory.EmitRemoteDown ();
+
+			Log.DebugTimerPrint (timer, "DBusService stop took {0}");
+			return true;
+		}
+	}
+}

Copied: trunk/extensions/Services/DBusService/Makefile.am (from r4368, /trunk/extensions/DBusService/Makefile.am)
==============================================================================

Added: trunk/extensions/Services/Makefile.am
==============================================================================
--- (empty file)
+++ trunk/extensions/Services/Makefile.am	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,3 @@
+SUBDIRS = 			\
+	BeagleService		\
+	DBusService

Added: trunk/extensions/Tools/.gitignore
==============================================================================
--- (empty file)
+++ trunk/extensions/Tools/.gitignore	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,2 @@
+/Makefile
+/Makefile.in

Copied: trunk/extensions/Tools/ChangePhotoPath/.gitignore (from r4368, /trunk/extensions/ChangePhotoPath/.gitignore)
==============================================================================

Added: trunk/extensions/Tools/ChangePhotoPath/ChangePhotoPath.addin.xml
==============================================================================
--- (empty file)
+++ trunk/extensions/Tools/ChangePhotoPath/ChangePhotoPath.addin.xml	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,15 @@
+<Addin namespace="FSpot"
+        id="ChangePath"
+        version="0.4.4.100"
+        name="ChangePath"
+        description="UNSTABLE - Please ensure you have a backup of your photos.db before you test this extension. This extension will allow you to change the base path to the your photos. It is very handy if you move your photos from ~/Photos to /OurPhotos for instance. Just ensure you let SqLite continue to process the changes for up to a few hours after f-spot reports finished. It will only change the path to photos which are located under the Photo directory."
+        author="Bengt Thuree"
+        url="http://f-spot.org/Extensions";
+        category="Tools">
+	<Dependencies>
+		<Addin id="Core" version="0.4.4.100"/>
+	</Dependencies>
+	<Extension path = "/FSpot/Menus/Tools">
+		<Command id="FileList" _label = "Change path to photos" command_type = "ChangePhotoPath.Dump" />
+	</Extension>
+</Addin>

Copied: trunk/extensions/Tools/ChangePhotoPath/ChangePhotoPath.glade (from r4368, /trunk/extensions/ChangePhotoPath/ChangePhotoPath.glade)
==============================================================================

Added: trunk/extensions/Tools/ChangePhotoPath/ChangePhotoPathController.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Tools/ChangePhotoPath/ChangePhotoPathController.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,306 @@
+//
+// ChangePhotoPath.IChangePhotoPathController.cs: The logic to change the photo path in photos.db
+//
+// Author:
+//   Bengt Thuree (bengt thuree com)
+//
+// Copyright (C) 2007
+//
+
+
+using FSpot;
+using FSpot.Query;
+using System;
+using System.IO;
+using System.Collections;
+using System.Collections.Specialized;
+
+/*
+	Need to
+		1) Find old base path, assuming starting /YYYY/MM/DD so look for /YY (/19 or /20)
+		2) Confirm old base path and display new base path
+		3) For each Photo, check each version, and change every instance of old base path to new path
+
+Consider!!!
+photo_store.Commit(photo) is using db.ExecuteNonQuery, which is not waiting for the command to finish. On my test set of 20.000 photos,
+it took SQLite another 1 hour or so to commit all rows after this extension had finished its execution.
+
+Consider 2!!!
+A bit of mixture between URI and path. Old and New base path are in String path. Rest in URI.
+
+*/
+
+namespace ChangePhotoPath
+{
+
+	public enum ProcessResult {
+		Ok, Cancelled, Error, SamePath, NoPhotosFound, Processing
+	}
+
+	public class ChangePathController
+	{
+		PhotoStore photo_store = FSpot.Core.Database.Photos;
+		ArrayList photo_id_array, version_id_array;
+		StringCollection old_path_array, new_path_array;
+		int total_photos;
+		string orig_base_path;
+
+		private const string BASE2000 = "/20";
+		private const string BASE1900 = "/19";
+		private const string BASE1800 = "/18";
+
+		private IChangePhotoPathGui gui_controller;
+
+
+		private bool user_cancelled;
+		public bool UserCancelled {
+		  get {return user_cancelled;}
+		  set {user_cancelled = value;}
+		}
+
+		public ChangePathController (IChangePhotoPathGui gui)
+		{
+			gui_controller = gui;
+			total_photos = photo_store.TotalPhotos;
+			orig_base_path = EnsureEndsWithOneDirectorySeparator (FindOrigBasePath());			// NOT URI
+			string new_base_path = EnsureEndsWithOneDirectorySeparator (FSpot.Global.PhotoDirectory);	// NOT URI
+			gui_controller.DisplayDefaultPaths (orig_base_path, new_base_path);
+			user_cancelled = false;
+		}
+
+		private string EnsureEndsWithOneDirectorySeparator (string tmp_str)
+		{
+			if ( (tmp_str == null) || (tmp_str.Length == 0) )
+				return String.Format ("{0}", Path.DirectorySeparatorChar);
+			while (tmp_str.EndsWith(String.Format ("{0}", Path.DirectorySeparatorChar)))
+				tmp_str = tmp_str.Remove (tmp_str.Length-1, 1);
+			return String.Format ("{0}{1}", tmp_str, Path.DirectorySeparatorChar);
+		}
+
+
+		// Should always return TRUE, since path always ends with "/"
+		public bool CanWeRun ()
+		{
+			return (orig_base_path != null);
+		}
+
+		private string IsThisPhotoOnOrigBasePath (string check_this_path)
+		{
+			int i;
+			i = check_this_path.IndexOf(BASE2000);
+			if (i > 0)
+				return (check_this_path.Substring(0, i));
+			i = check_this_path.IndexOf(BASE1900);
+			if (i > 0)
+				return (check_this_path.Substring(0, i));
+			i = check_this_path.IndexOf(BASE1800);
+			if (i > 0)
+				return (check_this_path.Substring(0, i));
+			return null;
+		}
+
+		private string FindOrigBasePath()
+		{
+			string res_path = null;
+
+			foreach ( IBrowsableItem photo in photo_store.Query ( "SELECT * FROM photos " ) ) {
+				string tmp_path = (photo as Photo).DefaultVersionUri.AbsolutePath;
+				res_path = IsThisPhotoOnOrigBasePath (tmp_path);
+				if (res_path != null)
+					break;
+			}
+			return res_path;
+		}
+
+		private void InitializeArrays()
+		{
+			photo_id_array = new ArrayList();
+			version_id_array = new ArrayList();
+			old_path_array = new StringCollection();
+			new_path_array = new StringCollection();
+		}
+
+		private void AddVersionToArrays ( uint photo_id, uint version_id, string old_path, string new_path)
+		{
+			photo_id_array.Add (photo_id);
+			version_id_array.Add (version_id);
+			old_path_array.Add (old_path);
+			new_path_array.Add (new_path);
+		}
+
+		private string CreateNewPath (string old_base, string new_base, PhotoVersion version)
+		{
+			return string.Format ("{0}{1}", new_base, version.Uri.AbsolutePath.Substring(old_base.Length));
+		}
+
+		private bool ChangeThisVersionUri (PhotoVersion version, string old_base, string new_base)
+		{
+			// Change to path from URI, since easier to compare with old_base which is not in URI format.
+			string tmp_path = System.IO.Path.GetDirectoryName (version.Uri.AbsolutePath);
+			return ( tmp_path.StartsWith (old_base) );
+		}
+
+		private void SearchVersionUriToChange (Photo photo, string old_base, string new_base)
+		{
+				foreach (uint version_id in photo.VersionIds) {
+
+					PhotoVersion version = photo.GetVersion (version_id) as PhotoVersion;
+					if ( ChangeThisVersionUri (version, old_base, new_base) )
+						AddVersionToArrays (	photo.Id,
+									version_id,
+									version.Uri.AbsolutePath,
+									CreateNewPath (old_base, new_base, version));
+//					else
+//						System.Console.WriteLine ("L : {0}", version.Uri.AbsolutePath);
+				}
+		}
+
+		public bool SearchUrisToChange (string old_base, string new_base)
+		{
+			int count = 0;
+
+			foreach ( IBrowsableItem ibrows in photo_store.Query ( "SELECT * FROM photos " ) ) {
+				count++;
+				if (gui_controller.UpdateProgressBar ("Scanning through database", "Checking photo", total_photos))
+				    return false;
+				SearchVersionUriToChange ((ibrows as Photo), old_base, new_base);
+			}
+			return true;
+		}
+
+		public bool StillOnSamePhotoId (int old_index, int current_index, ArrayList array)
+		{
+			try {
+				return (array[old_index] == array[current_index]);
+			} catch {
+				return true; // return true if out of index.
+			}
+		}
+
+		public void UpdateThisUri (int index, string path, ref Photo photo)
+		{
+			if (photo == null)
+				photo = photo_store.Get ( (uint) photo_id_array[index]) as Photo;
+			PhotoVersion version = photo.GetVersion ( (uint) version_id_array[index]) as PhotoVersion;
+			version.Uri = new System.Uri ( path );
+		}
+
+/// FIXME Refactor, try to use one common method....
+		public void RevertAllUris (int last_index)
+		{
+			gui_controller.remove_progress_dialog();
+			Photo photo = null;
+			for (int k = last_index; k >= 0; k--) {
+				if (gui_controller.UpdateProgressBar ("Reverting changes to database", "Reverting photo", last_index))
+					{} // do nothing, ignore trying to abort the revert...
+				if ( (photo != null) && !StillOnSamePhotoId (k+1, k, photo_id_array) ) {
+					photo_store.Commit (photo);
+					photo = null;
+				}
+
+				UpdateThisUri (k, old_path_array[k], ref photo);
+				System.Console.WriteLine ("R : {0} - {1}", k, old_path_array[k]);
+			}
+			if (photo != null)
+				photo_store.Commit (photo);
+			System.Console.WriteLine ("Changing path failed due to above error. Have reverted any modification that took place.");
+		}
+
+		public ProcessResult ChangeAllUris ( ref int  last_index)
+		{
+			gui_controller.remove_progress_dialog();
+			Photo photo = null;
+			last_index = 0;
+			try {
+				photo = null;
+				for (last_index = 0; last_index < photo_id_array.Count; last_index++) {
+
+					if (gui_controller.UpdateProgressBar ("Changing photos base path", "Changing photo", photo_id_array.Count)) {
+						Console.WriteLine("User aborted the change of paths...");
+						return ProcessResult.Cancelled;
+					}
+
+					if ( (photo != null) && !StillOnSamePhotoId (last_index-1, last_index, photo_id_array) ) {
+						photo_store.Commit (photo, true, false);
+						photo = null;
+					}
+
+					UpdateThisUri (last_index, new_path_array[last_index], ref photo);
+					System.Console.WriteLine ("U : {0} - {1}", last_index, new_path_array[last_index]);
+
+					// DEBUG ONLY
+					// Cause an TEST exception on 6'th URI to be changed.
+					// float apa = last_index / (last_index-6);
+				}
+				if (photo != null)
+					photo_store.Commit (photo, true, false);
+			} catch (Exception e) {
+				Console.WriteLine(e);
+				return ProcessResult.Error;
+			}
+			return ProcessResult.Ok;
+		}
+
+
+		public ProcessResult ProcessArrays()
+		{
+			int last_index = 0;
+			ProcessResult tmp_res;
+			tmp_res = ChangeAllUris(ref last_index);
+			if (!(tmp_res == ProcessResult.Ok))
+				RevertAllUris(last_index);
+			return tmp_res;
+		}
+
+/*
+		public void CheckIfUpdated (int test_index, StringCollection path_array)
+		{
+			Photo photo = photo_store.Get ( (uint) photo_id_array[test_index]) as Photo;
+			PhotoVersion version = photo.GetVersion ( (uint) version_id_array[test_index]) as PhotoVersion;
+			if (version.Uri.AbsolutePath.ToString() == path_array[ test_index ])
+				System.Console.WriteLine ("Test URI ({0}) matches --- Should be finished", test_index);
+			else
+				System.Console.WriteLine ("Test URI ({0}) DO NOT match --- Should NOT BE finished", test_index);
+		}
+*/
+
+/*
+Check paths are different
+If (Scan all photos) // user might cancel
+	If (Check there are photos on old path)
+		ChangePathsOnPhotos
+*/
+
+		public bool NewOldPathSame (ref string newpath, ref string oldpath)
+		{
+			string p1 = EnsureEndsWithOneDirectorySeparator(newpath);
+			string p2 = EnsureEndsWithOneDirectorySeparator(oldpath);
+			return (p1 == p2);
+		}
+
+		public ProcessResult ChangePathOnPhotos (string old_base, string new_base)
+		{
+			ProcessResult tmp_res = ProcessResult.Processing;
+			InitializeArrays();
+
+			if (NewOldPathSame (ref new_base, ref old_base))
+				tmp_res = ProcessResult.SamePath;
+
+			if ( (tmp_res == ProcessResult.Processing) && (!SearchUrisToChange (old_base, new_base)) )
+				tmp_res = ProcessResult.Cancelled;
+
+			if ( (tmp_res == ProcessResult.Processing) && (photo_id_array.Count == 0) )
+				tmp_res = ProcessResult.NoPhotosFound;
+
+			if (tmp_res == ProcessResult.Processing)
+				tmp_res = ProcessArrays();
+
+//			if (res)
+//				CheckIfUpdated (photo_id_array.Count-1, new_path_array);
+//			else
+//				CheckIfUpdated (0, old_path_array);
+
+			return tmp_res;
+		}
+	}
+}

Added: trunk/extensions/Tools/ChangePhotoPath/ChangePhotoPathGui.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Tools/ChangePhotoPath/ChangePhotoPathGui.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,204 @@
+//
+// ChangePhotoPath.IChangePhotoPathGui.cs: The Gui to change the photo path in photos.db
+//
+// Author:
+//   Bengt Thuree (bengt thuree com)
+//
+// Copyright (C) 2007
+//
+
+using FSpot.Extensions;
+using FSpot.UI.Dialog;
+using System;
+//using Gnome.Vfs;
+using Gtk;
+
+namespace ChangePhotoPath
+{
+
+	public class Dump : Gtk.Dialog, ICommand, IChangePhotoPathGui
+	{
+		private string dialog_name = "ChangePhotoPath";
+		private Glade.XML xml;
+		private Gtk.Dialog dialog;
+		private ChangePathController contr;
+
+		private ProgressDialog progress_dialog;
+		private int progress_dialog_total = 0;
+
+		[Glade.Widget] Gtk.Entry old_common_uri;
+		[Glade.Widget] Gtk.Label new_common_uri;
+//		[Glade.Widget] Gtk.ProgressBar progress_bar;
+
+		private bool LaunchController()
+		{
+			try {
+				contr = new ChangePathController ( this );
+			} catch (Exception e) {
+				Console.WriteLine(e);
+				return false;
+			}
+			return true;
+		}
+
+		public void create_progress_dialog(string txt, int total)
+		{
+			progress_dialog = new ProgressDialog (txt,
+							      ProgressDialog.CancelButtonType.Stop,
+							      total,
+							      dialog);
+		}
+
+
+		public void LaunchDialog()
+		{
+			CreateDialog();
+			Dialog.Modal = false;
+			Dialog.TransientFor = null;
+			if (LaunchController() && contr.CanWeRun())
+			{
+				DisplayDoNotStopFSpotMsg();
+				Dialog.ShowAll();
+				Dialog.Response += HandleResponse;
+			} else {
+				DisplayOrigBasePathNotFoundMsg();
+				Dialog.Destroy();
+			}
+		}
+
+		private void CreateDialog()
+		{
+			xml = new Glade.XML (null, "ChangePhotoPath.glade", dialog_name, "f-spot");
+			xml.Autoconnect (this);
+		}
+
+		private Gtk.Dialog Dialog {
+			get {
+				if (dialog == null)
+					dialog = (Gtk.Dialog) xml.GetWidget (dialog_name);
+				return dialog;
+			}
+		}
+
+		private void DisplayMsg(Gtk.MessageType MessageType, string msg)
+		{
+
+			HigMessageDialog.RunHigMessageDialog (	null,
+								Gtk.DialogFlags.Modal | Gtk.DialogFlags.DestroyWithParent,
+								MessageType,
+								Gtk.ButtonsType.Ok,
+								msg,
+								null);
+		}
+
+		private void DisplayDoNotStopFSpotMsg()
+		{
+			DisplayMsg (Gtk.MessageType.Info, "It will take a long time for SqLite to update the database if you have many photos." +
+							  "\nWe recommend you to let F-Spot be running during the night to ensure everything is written to disk."+
+							  "\nChanging path on 23000 photos took 2 hours until sqlite had updated all photos in the database.");
+		}
+
+		private void DisplayOrigBasePathNotFoundMsg()
+		{
+			DisplayMsg (Gtk.MessageType.Error, "Could not find an old base path. /YYYY/MM/DD need to start with /20, /19 or /18.");
+		}
+
+		private void DisplayCancelledMsg()
+		{
+			DisplayMsg (Gtk.MessageType.Warning, "Operation aborted. Database has not been modified.");
+		}
+
+		private void DisplaySamePathMsg()
+		{
+			DisplayMsg (Gtk.MessageType.Warning, "New and Old base path are the same.");
+		}
+
+		private void DisplayNoPhotosFoundMsg()
+		{
+			DisplayMsg (Gtk.MessageType.Warning, "Did not find any photos with the old base path.");
+		}
+
+		private void DisplayExecutionOkMsg()
+		{
+			DisplayMsg (Gtk.MessageType.Info, "Completed successfully. Please ensure you wait 1-2 hour before you exit f-spot. This to ensure the database cache is written to disk.");
+		}
+
+		private void DisplayExecutionNotOkMsg()
+		{
+			DisplayMsg (Gtk.MessageType.Error, "An error occured. Reverted all changes to the database.");
+		}
+
+
+		private void HandleResponse (object sender, Gtk.ResponseArgs args)
+		{
+			bool destroy_dialog = false;
+			ChangePhotoPath.ProcessResult tmp_res;
+			if (args.ResponseId == Gtk.ResponseType.Ok) {
+
+				tmp_res = contr.ChangePathOnPhotos (old_common_uri.Text, new_common_uri.Text);
+				switch (tmp_res) {
+				case ProcessResult.Ok 			: 	DisplayExecutionOkMsg();
+										destroy_dialog=true;
+										break;
+				case ProcessResult.Cancelled 		: 	DisplayCancelledMsg();
+										break;
+				case ProcessResult.Error 		: 	DisplayExecutionNotOkMsg();
+										break;
+				case ProcessResult.SamePath 		: 	DisplaySamePathMsg();
+										break;
+				case ProcessResult.NoPhotosFound 	: 	DisplayNoPhotosFoundMsg();
+										break;
+				case ProcessResult.Processing 		: 	System.Console.WriteLine ("processing");
+										break;
+				}
+			} else
+				destroy_dialog = true;
+
+			remove_progress_dialog();
+			if (destroy_dialog)
+				Dialog.Destroy();
+
+			return;
+		}
+
+		public void DisplayDefaultPaths (string oldpath, string newpath)
+		{
+			old_common_uri.Text = oldpath;
+			new_common_uri.Text = newpath;
+		}
+
+		public void remove_progress_dialog ()
+		{
+			if (progress_dialog != null) {
+				progress_dialog.Destroy();
+				progress_dialog = null;
+			}
+		}
+
+		public void check_if_remove_progress_dialog (int total)
+		{
+			if (total != progress_dialog_total)
+				remove_progress_dialog();
+		}
+
+
+		public bool UpdateProgressBar (string hdr_txt, string txt, int total)
+		{
+			if (progress_dialog != null)
+				check_if_remove_progress_dialog(total);
+			if (progress_dialog == null)
+				create_progress_dialog(hdr_txt, total);
+			progress_dialog_total = total;
+			return progress_dialog.Update (String.Format ("{0} ", txt));
+		}
+
+		public void Run (object sender, EventArgs args)
+		{
+			try {
+				LaunchDialog( );
+			} catch (Exception e) {
+				Console.WriteLine(e);
+			}
+		}
+	}
+}

Added: trunk/extensions/Tools/ChangePhotoPath/IChangePhotoPathGui.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Tools/ChangePhotoPath/IChangePhotoPathGui.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,18 @@
+//
+// ChangePhotoPath.IChangePhotoPathGui.cs: Interfaces to ChangePhotoPathGui
+//
+// Author:
+//   Bengt Thuree (bengt thuree com)
+//
+// Copyright (C) 2007
+//
+
+namespace ChangePhotoPath
+{
+	public interface IChangePhotoPathGui
+	{
+		void remove_progress_dialog();
+		bool UpdateProgressBar (string hdr_txt, string text, int total_photos);
+		void DisplayDefaultPaths (string oldpath, string newpath);
+	}
+}

Added: trunk/extensions/Tools/ChangePhotoPath/Makefile
==============================================================================
--- (empty file)
+++ trunk/extensions/Tools/ChangePhotoPath/Makefile	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,34 @@
+ADDIN = ChangePhotoPath
+
+all: $(ADDIN).dll
+
+PACKAGES = \
+	-pkg:glade-sharp-2.0 \
+	-pkg:gtk-sharp-2.0 \
+	-pkg:f-spot
+
+ASSEMBLIES =
+
+RESOURCES = \
+	-resource:ChangePhotoPath.glade \
+	-resource:ChangePhotoPath.addin.xml
+
+SOURCES = \
+	ChangePhotoPathController.cs \
+	ChangePhotoPathGui.cs \
+	IChangePhotoPathGui.cs
+
+$(ADDIN).dll: $(SOURCES) $(ADDIN).addin.xml $(ADDIN).glade
+	gmcs -target:library -out:$@ $(SOURCES) $(PACKAGES) $(ASSEMBLIES) $(RESOURCES)
+
+install: all
+	cp $(ADDIN).dll ~/.gnome2/f-spot/addins/
+
+mpack: $(ADDIN).dll
+	mautil p $(ADDIN).dll
+
+clean:
+	rm -f *.dll *~ *.bak *.gladep *.mpack
+
+PHONY:
+	install clean all mpack

Copied: trunk/extensions/Tools/DevelopInUFraw/.gitignore (from r4368, /trunk/extensions/DevelopInUFraw/.gitignore)
==============================================================================

Added: trunk/extensions/Tools/DevelopInUFraw/DevelopInUFRaw.addin.xml
==============================================================================
--- (empty file)
+++ trunk/extensions/Tools/DevelopInUFraw/DevelopInUFRaw.addin.xml	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,18 @@
+<Addin namespace="FSpot"
+	id="DevelopInUFraw"
+	version="0.4.4.102"
+	name="DevelopInUFRaw"
+	description="Develop the image in UFRaw, saves the result as a new version\n\nNote: Require ufraw 0.13 or CVS version newer than 2007-09-06 !!!"
+	author="Stephane Delcroix"
+	url="http://f-spot.org/Extensions";
+	category="Tools">
+	<Dependencies>
+		<Addin id="Core" version="0.4.4.100"/>
+	</Dependencies>
+	<Extension path = "/FSpot/Menus/PhotoPopup">
+		<Command id = "DevelopInUFRaw" _label = "Develop in UFRaw" command_type = "DevelopInUFRawExtension.DevelopInUFRaw" insertbefore="OpenWith"/>
+		<Condition id="PhotoSelection" selection="multiple">
+			<Command id = "DevelopInUFRawBatch" _label = "Batch Develop" command_type = "DevelopInUFRawExtension.DevelopInUFRawBatch" insertbefore="OpenWith"/>
+		</Condition>
+	</Extension>
+</Addin>

Added: trunk/extensions/Tools/DevelopInUFraw/DevelopInUFRaw.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Tools/DevelopInUFraw/DevelopInUFRaw.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,213 @@
+/*
+ * DevelopInUFraw.cs
+ *
+ * Author(s)
+ * 	Stephane Delcroix  <stephane delcroix org>
+ *
+ * This is free software. See COPYING for details
+ */
+
+using System;
+using System.IO;
+
+using FSpot;
+using FSpot.Utils;
+using FSpot.Extensions;
+using Mono.Unix;
+
+namespace DevelopInUFRawExtension
+{
+	// GUI Version
+	public class DevelopInUFRaw : AbstractDevelopInUFRaw {
+		public DevelopInUFRaw() : base("ufraw")
+		{
+		}
+
+		public override void Run (object o, EventArgs e)
+		{
+			Log.Information ("Executing DevelopInUFRaw extension");
+
+			foreach (Photo p in MainWindow.Toplevel.SelectedPhotos ()) {
+				DevelopPhoto (p);
+			}
+		}
+	}
+
+	// Batch Version
+	public class DevelopInUFRawBatch : AbstractDevelopInUFRaw {
+		public DevelopInUFRawBatch() : base("ufraw-batch")
+		{
+		}
+
+		public override void Run (object o, EventArgs e)
+		{
+			ProgressDialog pdialog = new ProgressDialog(Catalog.GetString ("Developing photos"),
+														ProgressDialog.CancelButtonType.Cancel,
+														MainWindow.Toplevel.SelectedPhotos ().Length,
+														MainWindow.Toplevel.Window);
+			Log.Information ("Executing DevelopInUFRaw extension in batch mode");
+
+			foreach (Photo p in MainWindow.Toplevel.SelectedPhotos ()) {
+				bool cancelled = pdialog.Update(String.Format(Catalog.GetString ("Developing {0}"), p.Name));
+				if (cancelled) {
+					break;
+				}
+
+				DevelopPhoto (p);
+			}
+			pdialog.Destroy();
+		}
+	}
+
+	// Abstract version, contains shared functionality
+	public abstract class AbstractDevelopInUFRaw : ICommand
+	{
+		// The executable used for developing RAWs
+		private string executable;
+
+		public const string APP_FSPOT_EXTENSION = Preferences.APP_FSPOT + "extension/";
+		public const string EXTENSION_DEVELOPINUFRAW = "developinufraw/";
+		public const string UFRAW_JPEG_QUALITY_KEY = APP_FSPOT_EXTENSION + EXTENSION_DEVELOPINUFRAW + "ufraw_jpeg_quality";
+		public const string UFRAW_ARGUMENTS_KEY = APP_FSPOT_EXTENSION + EXTENSION_DEVELOPINUFRAW + "ufraw_arguments";
+		public const string UFRAW_BATCH_ARGUMENTS_KEY = APP_FSPOT_EXTENSION + EXTENSION_DEVELOPINUFRAW + "ufraw_batch_arguments";
+
+		int ufraw_jpeg_quality;
+		string ufraw_args;
+		string ufraw_batch_args;
+
+		public AbstractDevelopInUFRaw(string executable)
+		{
+			this.executable = executable;
+		}
+
+		public abstract void Run (object o, EventArgs e);
+
+		protected void DevelopPhoto (Photo p)
+		{
+			LoadPreference (UFRAW_JPEG_QUALITY_KEY);
+			LoadPreference (UFRAW_ARGUMENTS_KEY);
+			LoadPreference (UFRAW_BATCH_ARGUMENTS_KEY);
+
+			PhotoVersion raw = p.GetVersion (Photo.OriginalVersionId) as PhotoVersion;
+			if (!ImageFile.IsRaw (raw.Uri.AbsolutePath)) {
+				Log.Warning ("The original version of this image is not a (supported) RAW file");
+				return;
+			}
+
+			string name = GetVersionName (p);
+			System.Uri developed = GetUriForVersionName (p, name);
+			string idfile = "";
+
+
+			if (ufraw_jpeg_quality < 1 || ufraw_jpeg_quality > 100) {
+				Log.Debug ("Invalid JPEG quality specified, defaulting to quality 98");
+				ufraw_jpeg_quality = 98;
+			}
+
+			string args = "";
+			switch (executable) {
+				case "ufraw":
+					args += ufraw_args;
+					if (new Gnome.Vfs.Uri (Path.ChangeExtension (raw.Uri.ToString (), ".ufraw")).Exists) {
+						// We found an ID file, use that instead of the raw file
+						idfile = "--conf=" + Path.ChangeExtension (raw.Uri.LocalPath, ".ufraw");
+					}
+					break;
+				case "ufraw-batch":
+					args += ufraw_batch_args;
+					if (new Gnome.Vfs.Uri (Path.Combine (FSpot.Global.BaseDirectory, "batch.ufraw")).Exists) {
+						// We found an ID file, use that instead of the raw file
+						idfile = "--conf=" + Path.Combine (FSpot.Global.BaseDirectory, "batch.ufraw");
+					}
+					break;
+			}
+
+			args += String.Format(" --overwrite --create-id=also --compression={0} --out-type=jpeg {1} --output={2} {3}",
+				ufraw_jpeg_quality,
+				idfile,
+				CheapEscape (developed.LocalPath),
+				CheapEscape (raw.Uri.ToString ()));
+			Log.Debug (executable + " " + args);
+
+			System.Diagnostics.Process ufraw = System.Diagnostics.Process.Start (executable, args);
+			ufraw.WaitForExit ();
+			if (!(new Gnome.Vfs.Uri (developed.ToString ())).Exists) {
+				Log.Warning ("UFRaw quit with an error. Check that you have UFRaw 0.13 or newer. Or did you simply clicked on Cancel?");
+				return;
+			}
+
+			if (new Gnome.Vfs.Uri (Path.ChangeExtension (developed.ToString (), ".ufraw")).Exists) {
+				// We save our own copy of the last ufraw settings, as ufraw can overwrite it's own last used settings outside f-spot
+				File.Delete (Path.Combine (FSpot.Global.BaseDirectory, "batch.ufraw"));
+				File.Copy (Path.ChangeExtension (developed.LocalPath, ".ufraw"), Path.Combine (FSpot.Global.BaseDirectory, "batch.ufraw"));
+
+				// Rename the ufraw file to match the original RAW filename, instead of the (Developed In UFRaw) filename
+				File.Delete (Path.ChangeExtension (raw.Uri.LocalPath, ".ufraw"));
+				File.Move (Path.ChangeExtension (developed.LocalPath, ".ufraw"), Path.ChangeExtension (raw.Uri.LocalPath, ".ufraw"));
+			}
+
+			p.DefaultVersionId = p.AddVersion (developed, name, true);
+			p.Changes.DataChanged = true;
+			Core.Database.Photos.Commit (p);
+		}
+
+		private static string GetVersionName (Photo p)
+		{
+			return GetVersionName (p, 1);
+		}
+
+		private static string GetVersionName (Photo p, int i)
+		{
+			string name = Catalog.GetPluralString ("Developed in UFRaw", "Developed in UFRaw ({0})", i);
+			name = String.Format (name, i);
+			if (p.VersionNameExists (name))
+				return GetVersionName (p, i + 1);
+			return name;
+		}
+
+		private System.Uri GetUriForVersionName (Photo p, string version_name)
+		{
+			string name_without_ext = System.IO.Path.GetFileNameWithoutExtension (p.Name);
+			return new System.Uri (System.IO.Path.Combine (DirectoryPath (p),  name_without_ext
+					       + " (" + version_name + ")" + ".jpg"));
+		}
+
+		private static string CheapEscape (string input)
+		{
+			string escaped = input;
+			escaped = escaped.Replace (" ", "\\ ");
+			escaped = escaped.Replace ("(", "\\(");
+			escaped = escaped.Replace (")", "\\)");
+			return escaped;
+		}
+
+		private static string DirectoryPath (Photo p)
+		{
+			System.Uri uri = p.VersionUri (Photo.OriginalVersionId);
+			return uri.Scheme + "://" + uri.Host + System.IO.Path.GetDirectoryName (uri.AbsolutePath);
+		}
+
+		void LoadPreference (string key)
+		{
+			object val = Preferences.Get (key);
+
+			if (val == null)
+				return;
+
+			Log.Debug (String.Format ("Setting {0} to {1}", key, val));
+
+			switch (key) {
+				case UFRAW_JPEG_QUALITY_KEY:
+					ufraw_jpeg_quality = (int) val;
+					break;
+				case UFRAW_ARGUMENTS_KEY:
+					ufraw_args = (string) val;
+					break;
+				case UFRAW_BATCH_ARGUMENTS_KEY:
+					ufraw_batch_args = (string) val;
+					break;
+			}
+		}
+
+	}
+}

Copied: trunk/extensions/Tools/DevelopInUFraw/Makefile (from r4368, /trunk/extensions/DevelopInUFraw/Makefile)
==============================================================================

Added: trunk/extensions/Tools/Makefile.am
==============================================================================
--- (empty file)
+++ trunk/extensions/Tools/Makefile.am	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,2 @@
+SUBDIRS = 			\
+	MergeDb

Copied: trunk/extensions/Tools/MergeDb/.gitignore (from r4368, /trunk/extensions/MergeDb/.gitignore)
==============================================================================

Copied: trunk/extensions/Tools/MergeDb/Makefile.am (from r4368, /trunk/extensions/MergeDb/Makefile.am)
==============================================================================

Copied: trunk/extensions/Tools/MergeDb/MergeDb.addin.xml (from r4368, /trunk/extensions/MergeDb/MergeDb.addin.xml)
==============================================================================

Added: trunk/extensions/Tools/MergeDb/MergeDb.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Tools/MergeDb/MergeDb.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,282 @@
+/*
+ * FSpot.MergeDb.cs
+ *
+ * Author(s):
+ *	Stephane Delcroix  <stephane delcroix org>
+ *
+ * This is free software. See COPYING for details
+ */
+
+using System;
+using System.IO;
+using System.Collections.Generic;
+
+using Gtk;
+
+using FSpot;
+using FSpot.Extensions;
+using FSpot.Utils;
+using FSpot.Query;
+using FSpot.UI.Dialog;
+using Mono.Unix;
+
+namespace MergeDbExtension
+{
+	public class MergeDb : ICommand
+	{
+
+		Db from_db;
+		Db to_db;
+		Roll [] new_rolls;
+		MergeDbDialog mdd;
+
+		Dictionary<uint, Tag> tag_map; //Key is a TagId from from_db, Value is a Tag from to_db
+		Dictionary<uint, uint> roll_map;
+
+		public void Run (object o, EventArgs e)
+		{
+			from_db = new Db ();
+			from_db.ExceptionThrown += HandleDbException;
+			to_db = Core.Database;
+
+			//ShowDialog ();
+			mdd = new MergeDbDialog (this);
+			mdd.FileChooser.FileSet += HandleFileSet;
+			mdd.Dialog.Response += HandleResponse;
+			mdd.ShowAll ();
+		}
+
+		void HandleDbException (Exception e)
+		{
+			Log.Exception (e);
+		}
+
+
+		internal Db FromDb {
+			get { return from_db; }
+		}
+
+		void HandleFileSet (object o, EventArgs e)
+		{
+			try {
+				string tempfilename = System.IO.Path.GetTempFileName ();
+				System.IO.File.Copy (mdd.FileChooser.Filename, tempfilename, true);
+
+				from_db.Init (tempfilename, true);
+
+				FillRolls ();
+				mdd.Rolls = new_rolls;
+
+				mdd.SetSensitive ();
+
+			} catch (Exception ex) {
+				string msg = Catalog.GetString ("Error opening the selected file");
+				string desc = String.Format (Catalog.GetString ("The file you selected is not a valid or supported database.\n\nReceived exception \"{0}\"."), ex.Message);
+
+				HigMessageDialog md = new HigMessageDialog (mdd.Dialog, DialogFlags.DestroyWithParent,
+									    Gtk.MessageType.Error,
+									    ButtonsType.Ok,
+									    msg,
+									    desc);
+				md.Run ();
+				md.Destroy ();
+
+				Log.Exception (ex);
+			}
+		}
+
+		void FillRolls ()
+		{
+			List<Roll> from_rolls = new List<Roll> (from_db.Rolls.GetRolls ());
+			Roll [] to_rolls = to_db.Rolls.GetRolls ();
+			foreach (Roll tr in to_rolls)
+				foreach (Roll fr in from_rolls.ToArray ())
+					if (tr.Time == fr.Time)
+						from_rolls.Remove (fr);
+			new_rolls = from_rolls.ToArray ();
+
+		}
+
+		void HandleResponse (object obj, ResponseArgs args) {
+			if (args.ResponseId == ResponseType.Accept) {
+				PhotoQuery query = new PhotoQuery (from_db.Photos);
+				query.RollSet = mdd.ActiveRolls == null ? null : new RollSet (mdd.ActiveRolls);
+				DoMerge (query, mdd.ActiveRolls, mdd.Copy);
+			}
+			mdd.Dialog.Destroy ();
+		}
+
+
+		public static void Merge (string path, Db to_db)
+		{
+			Log.WarningFormat ("Will merge db {0} into main f-spot db {1}", path, FSpot.Global.BaseDirectory + "/photos.db" );
+			Db from_db = new Db ();
+			from_db.Init (path, true);
+			//MergeDb mdb = new MergeDb (from_db, to_db);
+
+		}
+
+		void DoMerge (PhotoQuery query, Roll [] rolls, bool copy)
+		{
+			tag_map = new Dictionary<uint, Tag> ();
+			roll_map = new Dictionary<uint, uint> ();
+
+			Log.Warning ("Merging tags");
+			MergeTags (from_db.Tags.RootCategory);
+
+			Log.Warning ("Creating the rolls");
+			CreateRolls (rolls);
+
+			Log.Warning ("Importing photos");
+			ImportPhotos (query, copy);
+
+		}
+
+		void MergeTags (Tag tag_to_merge)
+		{
+			TagStore from_store = from_db.Tags;
+			TagStore to_store = to_db.Tags;
+
+			if (tag_to_merge != from_store.RootCategory) { //Do not merge RootCategory
+				Tag dest_tag = to_store.GetTagByName (tag_to_merge.Name);
+				if (dest_tag == null) {
+					Category parent = (tag_to_merge.Category == from_store.RootCategory) ?
+							to_store.RootCategory :
+							to_store.GetTagByName (tag_to_merge.Category.Name) as Category;
+					dest_tag = to_store.CreateTag (parent, tag_to_merge.Name);
+					//FIXME: copy the tag icon and commit
+				}
+				tag_map [tag_to_merge.Id] = dest_tag;
+			}
+
+			if (!(tag_to_merge is Category))
+				return;
+
+			foreach (Tag t in (tag_to_merge as Category).Children)
+				MergeTags (t);
+		}
+
+		void CreateRolls (Roll [] rolls)
+		{
+			if (rolls == null)
+				rolls = from_db.Rolls.GetRolls ();
+			RollStore from_store = from_db.Rolls;
+			RollStore to_store = to_db.Rolls;
+
+			foreach (Roll roll in rolls) {
+				if (from_store.PhotosInRoll (roll) == 0)
+					continue;
+				roll_map [roll.Id] = (to_store.Create (roll.Time).Id);
+			}
+		}
+
+		void ImportPhotos (PhotoQuery query, bool copy)
+		{
+			foreach (Photo p in query.Photos)
+				ImportPhoto (p, copy);
+		}
+
+		Dictionary<string, string> path_map = null;
+		Dictionary<string, string> PathMap {
+			get {
+				if (path_map == null)
+					path_map = new Dictionary<string, string> ();
+				return path_map;
+			}
+		}
+
+		void ImportPhoto (Photo photo, bool copy)
+		{
+			Log.WarningFormat ("Importing {0}", photo.Name);
+			PhotoStore from_store = from_db.Photos;
+			PhotoStore to_store = to_db.Photos;
+
+			string photo_path = photo.VersionUri (Photo.OriginalVersionId).AbsolutePath;
+
+			while (!System.IO.File.Exists (photo_path)) {
+				Log.Debug ("Not found, trying the mappings...");
+				foreach (string key in PathMap.Keys) {
+					string path = photo_path;
+					path = path.Replace (key, PathMap [key]);
+					Log.DebugFormat ("Replaced path {0}", path);
+					if (System.IO.File.Exists (path)) {
+						photo_path = path;
+						break;;
+					}
+				}
+
+				if (System.IO.File.Exists (photo_path)) {
+					Log.Debug ("Exists!!!");
+					continue;
+				}
+
+				string [] parts = photo_path.Split (new char[] {'/'});
+				if (parts.Length > 6) {
+					string folder = String.Join ("/", parts, 0, parts.Length - 4);
+					PickFolderDialog pfd = new PickFolderDialog (mdd.Dialog, folder);
+					string new_folder = pfd.Run ();
+					pfd.Dialog.Destroy ();
+					if (new_folder == null) //Skip
+						return;
+					Log.DebugFormat ("{0} maps to {1}", folder, new_folder);
+
+					PathMap[folder] = new_folder;
+
+				} else
+					Console.WriteLine ("point me to the file");
+				Console.WriteLine ("FNF: {0}", photo_path);
+
+			}
+
+			string destination;
+			Gdk.Pixbuf pixbuf;
+			Photo newp;
+
+			if (copy)
+				destination = FileImportBackend.ChooseLocation (photo_path, null);
+			else
+				destination = photo_path;
+
+			// Don't copy if we are already home
+			if (photo_path == destination)
+				newp = to_store.Create (destination, roll_map [photo.RollId], out pixbuf);
+			else {
+				System.IO.File.Copy (photo_path, destination);
+
+				newp = to_store.Create (destination, photo_path, roll_map [photo.RollId], out pixbuf);
+				try {
+					File.SetAttributes (destination, File.GetAttributes (destination) & ~FileAttributes.ReadOnly);
+					DateTime create = File.GetCreationTime (photo_path);
+					File.SetCreationTime (destination, create);
+					DateTime mod = File.GetLastWriteTime (photo_path);
+					File.SetLastWriteTime (destination, mod);
+				} catch (IOException) {
+					// we don't want an exception here to be fatal.
+				}
+			}
+
+			if (newp == null)
+				return;
+
+			foreach (Tag t in photo.Tags) {
+				Log.WarningFormat ("Tagging with {0}", t.Name);
+				newp.AddTag (tag_map [t.Id]);
+			}
+
+			foreach (uint version_id in photo.VersionIds)
+				if (version_id != Photo.OriginalVersionId) {
+					PhotoVersion version = photo.GetVersion (version_id) as PhotoVersion;
+					uint newv = newp.AddVersion (version.Uri, version.Name, version.IsProtected);
+					if (version_id == photo.DefaultVersionId)
+						newp.DefaultVersionId = newv;
+				}
+
+			//FIXME Import extra info (time, description, rating)
+			newp.Time = photo.Time;
+			newp.Description = photo.Description;
+			newp.Rating = photo.Rating;
+
+			to_store.Commit (newp);
+		}
+	}
+}

Added: trunk/extensions/Tools/MergeDb/MergeDb.glade
==============================================================================
--- (empty file)
+++ trunk/extensions/Tools/MergeDb/MergeDb.glade	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,352 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<!DOCTYPE glade-interface SYSTEM "glade-2.0.dtd">
+<!--Generated with glade3 3.4.3 on Tue Aug 26 16:41:20 2008 -->
+<glade-interface>
+  <widget class="GtkDialog" id="mergedb_dialog">
+    <property name="border_width">5</property>
+    <property name="title" translatable="yes">Merge another f-spot collection</property>
+    <property name="window_position">GTK_WIN_POS_CENTER_ON_PARENT</property>
+    <property name="type_hint">GDK_WINDOW_TYPE_HINT_DIALOG</property>
+    <property name="has_separator">False</property>
+    <child internal-child="vbox">
+      <widget class="GtkVBox" id="dialog-vbox1">
+        <property name="visible">True</property>
+        <property name="spacing">2</property>
+        <child>
+          <widget class="GtkTable" id="table1">
+            <property name="visible">True</property>
+            <property name="n_rows">9</property>
+            <property name="n_columns">3</property>
+            <property name="column_spacing">6</property>
+            <property name="row_spacing">6</property>
+            <child>
+              <placeholder/>
+            </child>
+            <child>
+              <placeholder/>
+            </child>
+            <child>
+              <placeholder/>
+            </child>
+            <child>
+              <placeholder/>
+            </child>
+            <child>
+              <placeholder/>
+            </child>
+            <child>
+              <widget class="GtkLabel" id="label5">
+                <property name="visible">True</property>
+                <property name="xalign">1</property>
+                <property name="label" translatable="yes">&lt;small&gt;&lt;i&gt;Copy the images locally or keep them where they are. If you chose the later, be sure that this location will stay accessible from f-spot.&lt;/i&gt;&lt;/small&gt;</property>
+                <property name="use_markup">True</property>
+                <property name="wrap">True</property>
+                <property name="ellipsize">PANGO_ELLIPSIZE_END</property>
+              </widget>
+              <packing>
+                <property name="left_attach">1</property>
+                <property name="right_attach">3</property>
+                <property name="top_attach">8</property>
+                <property name="bottom_attach">9</property>
+                <property name="y_options"></property>
+              </packing>
+            </child>
+            <child>
+              <widget class="GtkRadioButton" id="keep_radio">
+                <property name="visible">True</property>
+                <property name="sensitive">False</property>
+                <property name="can_focus">True</property>
+                <property name="label" translatable="yes">Keep the images at their original location</property>
+                <property name="response_id">0</property>
+                <property name="draw_indicator">True</property>
+                <property name="group">copy_radio</property>
+              </widget>
+              <packing>
+                <property name="right_attach">3</property>
+                <property name="top_attach">7</property>
+                <property name="bottom_attach">8</property>
+                <property name="y_options"></property>
+              </packing>
+            </child>
+            <child>
+              <widget class="GtkRadioButton" id="copy_radio">
+                <property name="visible">True</property>
+                <property name="sensitive">False</property>
+                <property name="can_focus">True</property>
+                <property name="label" translatable="yes">Copy images to Photos/</property>
+                <property name="response_id">0</property>
+                <property name="active">True</property>
+                <property name="draw_indicator">True</property>
+              </widget>
+              <packing>
+                <property name="right_attach">3</property>
+                <property name="top_attach">6</property>
+                <property name="bottom_attach">7</property>
+                <property name="y_options"></property>
+              </packing>
+            </child>
+            <child>
+              <widget class="GtkLabel" id="label4">
+                <property name="visible">True</property>
+                <property name="xalign">1</property>
+                <property name="label" translatable="yes">&lt;small&gt;&lt;i&gt;Choose what to import from the selected db.
+"Everything" will import everything, creating duplicates if you already imported from that database.
+"New Rolls only" is the smart option that will avoid re-importing you could have imported during a previous operation.
+"A Single Import Roll" let you choose which roll you want to merge back.&lt;/i&gt;&lt;/small&gt;</property>
+                <property name="use_markup">True</property>
+                <property name="wrap">True</property>
+                <property name="ellipsize">PANGO_ELLIPSIZE_END</property>
+              </widget>
+              <packing>
+                <property name="left_attach">1</property>
+                <property name="right_attach">3</property>
+                <property name="top_attach">5</property>
+                <property name="bottom_attach">6</property>
+                <property name="y_options"></property>
+                <property name="y_padding">6</property>
+              </packing>
+            </child>
+            <child>
+              <widget class="GtkComboBox" id="rolls_combo">
+                <property name="visible">True</property>
+                <property name="sensitive">False</property>
+                <property name="items"></property>
+              </widget>
+              <packing>
+                <property name="left_attach">2</property>
+                <property name="right_attach">3</property>
+                <property name="top_attach">3</property>
+                <property name="bottom_attach">4</property>
+                <property name="y_options"></property>
+              </packing>
+            </child>
+            <child>
+              <widget class="GtkRadioButton" id="allrolls_radio">
+                <property name="visible">True</property>
+                <property name="sensitive">False</property>
+                <property name="can_focus">True</property>
+                <property name="label" translatable="yes">Everything</property>
+                <property name="response_id">0</property>
+                <property name="draw_indicator">True</property>
+                <property name="group">newrolls_radio</property>
+              </widget>
+              <packing>
+                <property name="left_attach">1</property>
+                <property name="right_attach">3</property>
+                <property name="top_attach">4</property>
+                <property name="bottom_attach">5</property>
+                <property name="y_options"></property>
+              </packing>
+            </child>
+            <child>
+              <widget class="GtkRadioButton" id="singleroll_radio">
+                <property name="visible">True</property>
+                <property name="sensitive">False</property>
+                <property name="can_focus">True</property>
+                <property name="label" translatable="yes">A Single Import Roll</property>
+                <property name="response_id">0</property>
+                <property name="draw_indicator">True</property>
+                <property name="group">newrolls_radio</property>
+              </widget>
+              <packing>
+                <property name="left_attach">1</property>
+                <property name="right_attach">2</property>
+                <property name="top_attach">3</property>
+                <property name="bottom_attach">4</property>
+                <property name="y_options"></property>
+              </packing>
+            </child>
+            <child>
+              <widget class="GtkRadioButton" id="newrolls_radio">
+                <property name="visible">True</property>
+                <property name="sensitive">False</property>
+                <property name="can_focus">True</property>
+                <property name="label" translatable="yes">New Rolls Only</property>
+                <property name="response_id">0</property>
+                <property name="active">True</property>
+                <property name="draw_indicator">True</property>
+              </widget>
+              <packing>
+                <property name="left_attach">1</property>
+                <property name="right_attach">3</property>
+                <property name="top_attach">2</property>
+                <property name="bottom_attach">3</property>
+                <property name="y_options"></property>
+              </packing>
+            </child>
+            <child>
+              <widget class="GtkLabel" id="label3">
+                <property name="visible">True</property>
+                <property name="xalign">1</property>
+                <property name="label" translatable="yes">Import:</property>
+              </widget>
+              <packing>
+                <property name="top_attach">2</property>
+                <property name="bottom_attach">3</property>
+                <property name="x_options">GTK_FILL</property>
+                <property name="y_options"></property>
+              </packing>
+            </child>
+            <child>
+              <widget class="GtkLabel" id="label2">
+                <property name="visible">True</property>
+                <property name="xalign">1</property>
+                <property name="label" translatable="yes">&lt;small&gt;&lt;i&gt;Choose the location of the database you want to import from&lt;/i&gt;&lt;/small&gt;</property>
+                <property name="use_markup">True</property>
+              </widget>
+              <packing>
+                <property name="left_attach">1</property>
+                <property name="right_attach">3</property>
+                <property name="top_attach">1</property>
+                <property name="bottom_attach">2</property>
+                <property name="y_options"></property>
+                <property name="y_padding">6</property>
+              </packing>
+            </child>
+            <child>
+              <widget class="GtkFileChooserButton" id="db_filechooser">
+                <property name="visible">True</property>
+              </widget>
+              <packing>
+                <property name="left_attach">1</property>
+                <property name="right_attach">3</property>
+                <property name="y_options"></property>
+              </packing>
+            </child>
+            <child>
+              <widget class="GtkLabel" id="label1">
+                <property name="visible">True</property>
+                <property name="xalign">1</property>
+                <property name="label" translatable="yes">Database Location:</property>
+              </widget>
+              <packing>
+                <property name="x_options">GTK_FILL</property>
+                <property name="y_options"></property>
+              </packing>
+            </child>
+          </widget>
+          <packing>
+            <property name="position">1</property>
+          </packing>
+        </child>
+        <child internal-child="action_area">
+          <widget class="GtkHButtonBox" id="dialog-action_area1">
+            <property name="visible">True</property>
+            <property name="layout_style">GTK_BUTTONBOX_END</property>
+            <child>
+              <widget class="GtkButton" id="cancel_button">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="receives_default">True</property>
+                <property name="label">gtk-cancel</property>
+                <property name="use_stock">True</property>
+                <property name="response_id">-6</property>
+              </widget>
+            </child>
+            <child>
+              <widget class="GtkButton" id="apply_button">
+                <property name="visible">True</property>
+                <property name="sensitive">False</property>
+                <property name="can_focus">True</property>
+                <property name="receives_default">True</property>
+                <property name="label">gtk-ok</property>
+                <property name="use_stock">True</property>
+                <property name="response_id">-3</property>
+              </widget>
+              <packing>
+                <property name="position">1</property>
+              </packing>
+            </child>
+          </widget>
+          <packing>
+            <property name="expand">False</property>
+            <property name="pack_type">GTK_PACK_END</property>
+          </packing>
+        </child>
+      </widget>
+    </child>
+  </widget>
+  <widget class="GtkDialog" id="pickfolder_dialog">
+    <property name="border_width">5</property>
+    <property name="window_position">GTK_WIN_POS_CENTER_ON_PARENT</property>
+    <property name="default_width">736</property>
+    <property name="default_height">575</property>
+    <property name="type_hint">GDK_WINDOW_TYPE_HINT_DIALOG</property>
+    <property name="has_separator">False</property>
+    <child internal-child="vbox">
+      <widget class="GtkVBox" id="pickfolder_vbox">
+        <property name="visible">True</property>
+        <property name="spacing">2</property>
+        <child>
+          <widget class="GtkVBox" id="vbox1">
+            <property name="visible">True</property>
+            <child>
+              <placeholder/>
+            </child>
+            <child>
+              <widget class="GtkLabel" id="pickfolder_label">
+                <property name="visible">True</property>
+                <property name="xalign">0</property>
+                <property name="ypad">12</property>
+                <property name="use_markup">True</property>
+              </widget>
+              <packing>
+                <property name="expand">False</property>
+                <property name="fill">False</property>
+                <property name="position">1</property>
+              </packing>
+            </child>
+            <child>
+              <widget class="GtkFileChooserWidget" id="pickfolder_chooser">
+                <property name="visible">True</property>
+                <property name="use_preview_label">False</property>
+                <property name="show_hidden">True</property>
+                <property name="preview_widget_active">False</property>
+                <property name="action">GTK_FILE_CHOOSER_ACTION_SELECT_FOLDER</property>
+                <property name="local_only">False</property>
+              </widget>
+              <packing>
+                <property name="position">2</property>
+              </packing>
+            </child>
+          </widget>
+          <packing>
+            <property name="position">1</property>
+          </packing>
+        </child>
+        <child internal-child="action_area">
+          <widget class="GtkHButtonBox" id="dialog-action_area2">
+            <property name="visible">True</property>
+            <property name="layout_style">GTK_BUTTONBOX_END</property>
+            <child>
+              <widget class="GtkButton" id="button2">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="receives_default">True</property>
+                <property name="label" translatable="yes">Skip</property>
+                <property name="response_id">-1</property>
+              </widget>
+            </child>
+            <child>
+              <widget class="GtkButton" id="button1">
+                <property name="visible">True</property>
+                <property name="can_focus">True</property>
+                <property name="receives_default">True</property>
+                <property name="label">gtk-ok</property>
+                <property name="use_stock">True</property>
+                <property name="response_id">-6</property>
+              </widget>
+              <packing>
+                <property name="position">1</property>
+              </packing>
+            </child>
+          </widget>
+          <packing>
+            <property name="expand">False</property>
+            <property name="pack_type">GTK_PACK_END</property>
+          </packing>
+        </child>
+      </widget>
+    </child>
+  </widget>
+</glade-interface>

Added: trunk/extensions/Tools/MergeDb/MergeDbDialog.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Tools/MergeDb/MergeDbDialog.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,112 @@
+/*
+ * FSpot.MergeDbDialog.cs
+ *
+ * Author(s):
+ *	Stephane Delcroix  <stephane delcroix org>
+ *
+ * This is free software. See COPYING for details
+ */
+
+using System;
+using FSpot;
+using FSpot.Query;
+
+namespace MergeDbExtension
+{
+	internal class MergeDbDialog
+	{
+		[Glade.Widget] Gtk.Dialog mergedb_dialog;
+		[Glade.Widget] Gtk.Button apply_button;
+		[Glade.Widget] Gtk.Button cancel_button;
+		[Glade.Widget] Gtk.FileChooserButton db_filechooser;
+		[Glade.Widget] Gtk.RadioButton newrolls_radio;
+		[Glade.Widget] Gtk.RadioButton allrolls_radio;
+		[Glade.Widget] Gtk.RadioButton singleroll_radio;
+		[Glade.Widget] Gtk.ComboBox rolls_combo;
+		[Glade.Widget] Gtk.RadioButton copy_radio;
+		[Glade.Widget] Gtk.RadioButton keep_radio;
+
+		MergeDb parent;
+
+		public event EventHandler FileSet;
+
+		public MergeDbDialog (MergeDb parent) {
+			this.parent = parent;
+
+			Glade.XML xml = new Glade.XML (null, "MergeDb.glade", "mergedb_dialog", "f-spot");
+			xml.Autoconnect (this);
+			mergedb_dialog.Modal = false;
+			mergedb_dialog.TransientFor = null;
+
+			db_filechooser.LocalOnly = false;
+			db_filechooser.FileSet += OnFileSet;
+
+			newrolls_radio.Toggled += HandleRollsChanged;
+			allrolls_radio.Toggled += HandleRollsChanged;
+			singleroll_radio.Toggled += HandleRollsChanged;
+		}
+
+		void HandleRollsChanged (object o, EventArgs e)
+		{
+			rolls_combo.Sensitive = singleroll_radio.Active;
+		}
+
+		public Gtk.FileChooserButton FileChooser {
+			get { return db_filechooser; }
+		}
+
+		Roll [] rolls;
+		public Roll [] Rolls {
+			get { return rolls; }
+			set {
+				rolls = value;
+				foreach (Roll r in rolls) {
+					uint numphotos = parent.FromDb.Rolls.PhotosInRoll (r);
+					DateTime date = r.Time.ToLocalTime ();
+					rolls_combo.AppendText (String.Format ("{0} ({1})", date.ToString("%dd %MMM, %HH:%mm"), numphotos));
+					rolls_combo.Active = 0;
+				}
+			}
+		}
+
+		public Roll [] ActiveRolls {
+			get {
+				if (allrolls_radio.Active)
+					return null;
+				if (newrolls_radio.Active)
+					return rolls;
+				else
+					return new Roll [] {rolls [rolls_combo.Active]};
+			}
+		}
+
+		public bool Copy {
+			get { return copy_radio.Active; }
+		}
+
+		public void OnFileSet (object o, EventArgs e)
+		{
+			if (FileSet != null)
+				FileSet (o, e);
+		}
+
+		public void SetSensitive ()
+		{
+			newrolls_radio.Sensitive = true;
+			allrolls_radio.Sensitive = true;
+			singleroll_radio.Sensitive = true;
+			apply_button.Sensitive = true;
+			copy_radio.Sensitive = true;
+			keep_radio.Sensitive = true;
+		}
+
+		public Gtk.Dialog Dialog {
+			get { return mergedb_dialog; }
+		}
+
+		public void ShowAll ()
+		{
+			mergedb_dialog.ShowAll ();
+		}
+	}
+}

Added: trunk/extensions/Tools/MergeDb/PickFolderDialog.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Tools/MergeDb/PickFolderDialog.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,51 @@
+/*
+ * FSpot.PickFolderDialog.cs
+ *
+ * Author(s):
+ *	Stephane Delcroix  <stephane delcroix org>
+ *
+ * This is free software. See COPYING for details
+ */
+
+using System;
+using FSpot;
+using FSpot.Query;
+using Mono.Unix;
+
+namespace MergeDbExtension
+{
+	internal class PickFolderDialog
+	{
+		[Glade.Widget] Gtk.Dialog pickfolder_dialog;
+		[Glade.Widget] Gtk.FileChooserWidget pickfolder_chooser;
+		[Glade.Widget] Gtk.Label pickfolder_label;
+
+		public PickFolderDialog (Gtk.Dialog parent, string folder)
+		{
+			Glade.XML xml = new Glade.XML (null, "MergeDb.glade", "pickfolder_dialog", "f-spot");
+			xml.Autoconnect (this);
+			Console.WriteLine ("new pickfolder");
+			pickfolder_dialog.Modal = false;
+			pickfolder_dialog.TransientFor = parent;
+
+			pickfolder_chooser.LocalOnly = false;
+
+			pickfolder_label.Text = String.Format (Catalog.GetString ("<big>The database refers to files contained in the <b>{0}</b> folder.\n Please select that folder so I can do the mapping.</big>"), folder);
+			pickfolder_label.UseMarkup = true;
+		}
+
+		public string Run ()
+		{
+			pickfolder_dialog.ShowAll ();
+			if (pickfolder_dialog.Run () == -6)
+				return pickfolder_chooser.Filename;
+			else
+				return null;
+		}
+
+		public Gtk.Dialog Dialog {
+			get { return pickfolder_dialog; }
+		}
+
+	}
+}

Copied: trunk/extensions/Tools/MetaPixel/.gitignore (from r4368, /trunk/extensions/MetaPixel/.gitignore)
==============================================================================

Added: trunk/extensions/Tools/MetaPixel/Makefile
==============================================================================
--- (empty file)
+++ trunk/extensions/Tools/MetaPixel/Makefile	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,32 @@
+all: MetaPixel.dll
+
+PACKAGES = \
+	-pkg:f-spot \
+	-pkg:gnome-vfs-sharp-2.0 \
+	-pkg:gtk-sharp-2.0 \
+	-pkg:glade-sharp-2.0
+
+ASSEMBLIES = \
+	-r:Mono.Posix
+
+RESOURCES = \
+	-resource:MetaPixel.glade \
+	-resource:MetaPixel.addin.xml
+
+SOURCES = \
+	MetaPixel.cs
+
+install: all
+	cp *.dll ~/.gnome2/f-spot/addins/
+
+mpack: MetaPixel.dll
+	mautil p MetaPixel.dll
+
+%.dll: %.cs %.glade
+	gmcs -target:library $(SOURCES) $(PACKAGES) $(ASSEMBLIES) $(RESOURCES)
+
+clean:
+	rm -f *.dll *~ *.bak .mpack *.gladep
+
+PHONY:
+	install clean all mpack

Added: trunk/extensions/Tools/MetaPixel/MetaPixel.addin.xml
==============================================================================
--- (empty file)
+++ trunk/extensions/Tools/MetaPixel/MetaPixel.addin.xml	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,15 @@
+<Addin namespace="FSpot"
+	id="MetaPixelExtension"
+	version="0.4.4.100"
+	description="Create photomosaics using MetaPixel (http://www.complang.tuwien.ac.at/schani/metapixel/)"
+	author="Lorenzo Milesi"
+	name="MetaPixel"
+	url="http://f-spot.org/Extensions";
+	category="Tools">
+	<Dependencies>
+		<Addin id="Core" version="0.4.4.100"/>
+	</Dependencies>
+	<Extension path = "/FSpot/Menus/Tools">
+		<Command id = "MetaPixel" _label = "Create p_hotomosaic" command_type = "MetaPixelExtension.MetaPixel" />
+	</Extension>
+</Addin>

Added: trunk/extensions/Tools/MetaPixel/MetaPixel.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Tools/MetaPixel/MetaPixel.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,302 @@
+/*
+ * MetaPixel.cs
+ * Create photomosaics from F-Spot using MetaPixel
+ * http://www.complang.tuwien.ac.at/schani/metapixel/
+ *
+ * Author(s)
+ * 	Lorenzo Milesi <maxxer yetopen it>
+ *
+ * Many lines of this file are inspired or taken from other ext like DevelopInUFraw or SyncCatalog.
+ * So thanks to Stephane and Miguel for their help. ;)
+ *
+ * This is free software. See COPYING for details
+ *
+ * (c) YetOpen S.r.l.
+ */
+
+using System;
+using System.IO;
+using System.Collections;
+using Gtk;
+
+using FSpot;
+using FSpot.Extensions;
+using FSpot.Widgets;
+using FSpot.Filters;
+using FSpot.UI.Dialog;
+using Mono.Unix;
+
+namespace MetaPixelExtension {
+	public class MetaPixel: ICommand
+	{
+		[Glade.Widget] Gtk.Dialog metapixel_dialog;
+		[Glade.Widget] Gtk.HBox tagentry_box;
+		[Glade.Widget] Gtk.SpinButton icon_x_size;
+		[Glade.Widget] Gtk.SpinButton icon_y_size;
+		[Glade.Widget] Gtk.RadioButton tags_radio;
+		[Glade.Widget] Gtk.RadioButton current_radio;
+		FSpot.Widgets.TagEntry miniatures_tags;
+		string minidir_tmp;
+
+		public void Run (object o, EventArgs e) {
+			Console.WriteLine ("Executing MetaPixel extension");
+			if (MainWindow.Toplevel.SelectedPhotos ().Length == 0) {
+				InfoDialog (Catalog.GetString ("No selection available"),
+					    Catalog.GetString ("This tool requires an active selection. Please select one or more pictures and try again"),
+					    Gtk.MessageType.Error);
+				return;
+			} else {
+				//Check for MetaPixel executable
+				string output = "";
+				try {
+					System.Diagnostics.Process mp_check = new System.Diagnostics.Process ();
+					mp_check.StartInfo.RedirectStandardOutput = true;
+					mp_check.StartInfo.UseShellExecute = false;
+					mp_check.StartInfo.FileName = "metapixel";
+					mp_check.StartInfo.Arguments = "--version";
+					mp_check.Start ();
+					mp_check.WaitForExit ();
+					StreamReader sroutput = mp_check.StandardOutput;
+					output = sroutput.ReadLine ();
+				} catch (System.Exception) {
+				}
+				if (!System.Text.RegularExpressions.Regex.IsMatch (output, "^metapixel")) {
+					InfoDialog (Catalog.GetString ("Metapixel not available"),
+						    Catalog.GetString ("The metapixel executable was not found in path. Please check that you have it installed and that you have permissions to execute it"),
+						    Gtk.MessageType.Error);
+					return;
+				}
+
+				ShowDialog ();
+			}
+		}
+
+		public void ShowDialog () {
+			Glade.XML xml = new Glade.XML (null, "MetaPixel.glade", "metapixel_dialog", "f-spot");
+			xml.Autoconnect (this);
+			metapixel_dialog.Modal = false;
+			metapixel_dialog.TransientFor = null;
+
+			miniatures_tags = new FSpot.Widgets.TagEntry (MainWindow.Toplevel.Database.Tags, false);
+			miniatures_tags.UpdateFromTagNames (new string []{});
+			tagentry_box.Add (miniatures_tags);
+
+			metapixel_dialog.Response += on_dialog_response;
+			current_radio.Active = true;
+			tags_radio.Toggled += HandleTagsRadioToggled;
+			HandleTagsRadioToggled (null, null);
+			metapixel_dialog.ShowAll ();
+		}
+
+		void on_dialog_response (object obj, ResponseArgs args) {
+			if (args.ResponseId == ResponseType.Ok) {
+				create_mosaics ();
+			}
+			metapixel_dialog.Destroy ();
+		}
+
+		void create_mosaics () {
+			//Prepare the query
+			Db db = MainWindow.Toplevel.Database;
+			FSpot.PhotoQuery mini_query = new FSpot.PhotoQuery (db.Photos);
+			Photo [] photos;
+
+			if (tags_radio.Active) {
+				//Build tag array
+				ArrayList taglist = new ArrayList ();
+				foreach (string tag_name in miniatures_tags.GetTypedTagNames ()) {
+					Tag t = db.Tags.GetTagByName (tag_name);
+					if (t != null)
+						taglist.Add(t);
+				}
+				mini_query.Terms = FSpot.OrTerm.FromTags ((Tag []) taglist.ToArray (typeof (Tag)));
+				photos = mini_query.Photos;
+			} else {
+				photos = MainWindow.Toplevel.ActivePhotos ();
+			}
+
+			if (photos.Length == 0) {
+				//There is no photo for the selected tags! :(
+				InfoDialog (Catalog.GetString ("No photos for the selection"),
+					    Catalog.GetString ("The tags selected provided no pictures. Please select different tags"),
+					    Gtk.MessageType.Error);
+				return;
+			}
+
+			//Create minis
+			ProgressDialog progress_dialog = null;
+			progress_dialog = new ProgressDialog (Catalog.GetString ("Creating miniatures"),
+							      ProgressDialog.CancelButtonType.Stop,
+							      photos.Length, metapixel_dialog);
+
+			minidir_tmp = System.IO.Path.GetTempFileName ();
+			System.IO.File.Delete (minidir_tmp);
+			System.IO.Directory.CreateDirectory (minidir_tmp);
+			minidir_tmp += "/";
+
+			//Call MetaPixel to create the minis
+			foreach (Photo p in photos) {
+				if (progress_dialog.Update (String.Format (Catalog.GetString ("Preparing photo \"{0}\""), p.Name))) {
+					progress_dialog.Destroy ();
+					DeleteTmp ();
+					return;
+				}
+				//FIXME should switch to retry/skip
+				if (!(new Gnome.Vfs.Uri (p.DefaultVersionUri.ToString ())).Exists)
+					continue;
+				//FIXME Check if the picture's format is supproted (jpg, gif)
+
+				FilterSet filters = new FilterSet ();
+				filters.Add (new JpegFilter ());
+				filters.Add (new OrientationFilter ());
+				FilterRequest freq = new FilterRequest (p.DefaultVersionUri);
+				filters.Convert (freq);
+
+				//We use photo id for minis, instead of photo names, to avoid duplicates
+				string minifile = minidir_tmp + p.Id.ToString() + System.IO.Path.GetExtension (p.DefaultVersionUri.ToString ());
+				string prepare_command = String.Format ("--prepare -w {0} -h {1} {2} {3} {4}tables.mxt",
+									icon_x_size.Text, //Minis width
+									icon_y_size.Text, //Minis height
+									CheapEscape(freq.Current.LocalPath), //Source image
+									CheapEscape(minifile),  //Dest image
+									minidir_tmp);  //Table file
+				Console.WriteLine ("Executing: metapixel {0}", prepare_command);
+
+				System.Diagnostics.Process mp_prep = System.Diagnostics.Process.Start ("metapixel", prepare_command);
+				mp_prep.WaitForExit ();
+				if (!(new Gnome.Vfs.Uri (minifile)).Exists) {
+					Console.WriteLine ("No mini? No party! {0}", minifile);
+					continue;
+				}
+
+			} //Finished preparing!
+			if (progress_dialog != null)
+				progress_dialog.Destroy ();
+
+			progress_dialog = null;
+			progress_dialog = new ProgressDialog (Catalog.GetString ("Creating photomosaics"),
+							      ProgressDialog.CancelButtonType.Stop,
+							      MainWindow.Toplevel.SelectedPhotos ().Length, metapixel_dialog);
+
+			//Now create the mosaics!
+			uint error_count = 0;
+			foreach (Photo p in MainWindow.Toplevel.SelectedPhotos ()) {
+				if (progress_dialog.Update (String.Format (Catalog.GetString ("Processing \"{0}\""), p.Name))) {
+					progress_dialog.Destroy ();
+					DeleteTmp ();
+					return;
+				}
+				//FIXME should switch to retry/skip
+				if (!(new Gnome.Vfs.Uri (p.DefaultVersionUri.ToString ())).Exists)
+					continue;
+
+				//FIXME Check if the picture's format is supproted (jpg, gif)
+
+				FilterSet filters = new FilterSet ();
+				filters.Add (new JpegFilter ());
+				filters.Add (new OrientationFilter ());
+				FilterRequest freq = new FilterRequest (p.DefaultVersionUri);
+				filters.Convert (freq);
+
+				string name = GetVersionName (p);
+				System.Uri mosaic = GetUriForVersionName (p, name);
+
+				string mosaic_command = String.Format ("--metapixel -l {0} {1} {2}",
+									minidir_tmp,
+									CheapEscape(freq.Current.LocalPath),
+									CheapEscape(mosaic.LocalPath));
+				Console.WriteLine ("Executing: metapixel {0}", mosaic_command);
+				System.Diagnostics.Process mp_exe = System.Diagnostics.Process.Start ("metapixel", mosaic_command);
+				mp_exe.WaitForExit ();
+				if (!(new Gnome.Vfs.Uri (mosaic.ToString ())).Exists) {
+					Console.WriteLine ("Error in processing image");
+					error_count ++;
+					continue;
+				}
+
+				p.DefaultVersionId = p.AddVersion (mosaic, name, true);
+				p.Changes.DataChanged = true;
+				Core.Database.Photos.Commit (p);
+
+			} //Finished creating mosaics
+			if (progress_dialog != null)
+				progress_dialog.Destroy ();
+
+
+			string final_message = "Your mosaics have been generated as new versions of the pictures you selected";
+			if (error_count > 0)
+				final_message += String.Format (".\n{0} images out of {1} had errors",
+							error_count, MainWindow.Toplevel.SelectedPhotos ().Length);
+			InfoDialog (Catalog.GetString("PhotoMosaics generated!"),
+				    Catalog.GetString(final_message),
+				    Gtk.MessageType.Info);
+			DeleteTmp ();
+
+		}
+
+		private void HandleTagsRadioToggled (object o, EventArgs e) {
+			miniatures_tags.Sensitive = tags_radio.Active;
+		}
+
+                private static string GetVersionName (Photo p)
+                {
+                        return GetVersionName (p, 1);
+                }
+
+                private static string GetVersionName (Photo p, int i)
+                {
+                        string name = Catalog.GetPluralString ("PhotoMosaic", "PhotoMosaic ({0})", i);
+                        name = String.Format (name, i);
+                        if (p.VersionNameExists (name))
+                                return GetVersionName (p, i + 1);
+                        return name;
+                }
+
+                private System.Uri GetUriForVersionName (Photo p, string version_name)
+                {
+                        string name_without_ext = System.IO.Path.GetFileNameWithoutExtension (p.Name);
+                        return new System.Uri (System.IO.Path.Combine (DirectoryPath (p),  name_without_ext
+                                               + " (" + version_name + ")" + ".jpg"));
+                }
+
+                private static string CheapEscape (string input)
+                {
+                        string escaped = input;
+                        escaped = escaped.Replace (" ", "\\ ");
+                        escaped = escaped.Replace ("(", "\\(");
+                        escaped = escaped.Replace (")", "\\)");
+                        return escaped;
+                }
+
+                private static string DirectoryPath (Photo p)
+                {
+                        System.Uri uri = p.VersionUri (Photo.OriginalVersionId);
+                        return uri.Scheme + "://" + uri.Host + System.IO.Path.GetDirectoryName (uri.AbsolutePath);
+                }
+
+		private void DeleteTmp ()
+		{
+			//Clean temp workdir
+			DirectoryInfo dir = new DirectoryInfo(minidir_tmp);
+			FileInfo[] tmpfiles = dir.GetFiles();
+			foreach (FileInfo f in tmpfiles) {
+				if (System.IO.File.Exists(minidir_tmp + f.Name)) {
+					System.IO.File.Delete (minidir_tmp + f.Name);
+				}
+			}
+			if (System.IO.Directory.Exists(minidir_tmp)) {
+				System.IO.Directory.Delete(minidir_tmp);
+			}
+		}
+
+		private void InfoDialog (string title, string msg, Gtk.MessageType type) {
+			HigMessageDialog md = new HigMessageDialog (MainWindow.Toplevel.Window, DialogFlags.DestroyWithParent,
+						  type, ButtonsType.Ok, title, msg);
+
+			md.Run ();
+			md.Destroy ();
+
+		}
+
+	}
+}

Copied: trunk/extensions/Tools/MetaPixel/MetaPixel.glade (from r4368, /trunk/extensions/MetaPixel/MetaPixel.glade)
==============================================================================

Copied: trunk/extensions/Tools/PictureTile/.gitignore (from r4368, /trunk/extensions/PictureTile/.gitignore)
==============================================================================

Added: trunk/extensions/Tools/PictureTile/Makefile
==============================================================================
--- (empty file)
+++ trunk/extensions/Tools/PictureTile/Makefile	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,32 @@
+all: PictureTile.dll
+
+PACKAGES = \
+	-pkg:f-spot \
+	-pkg:gnome-vfs-sharp-2.0 \
+	-pkg:gtk-sharp-2.0 \
+	-pkg:glade-sharp-2.0
+
+ASSEMBLIES = \
+	-r:Mono.Posix
+
+RESOURCES = \
+	-resource:PictureTile.glade \
+	-resource:PictureTile.addin.xml
+
+SOURCES = \
+	PictureTile.cs
+
+install: all
+	cp *.dll ~/.gnome2/f-spot/addins/
+
+mpack: PictureTile.dll
+	mautil p PictureTile.dll
+
+%.dll: %.cs %.glade
+	gmcs -target:library $(SOURCES) $(PACKAGES) $(ASSEMBLIES) $(RESOURCES)
+
+clean:
+	rm -f *.dll *~ *.bak .mpack *.gladep
+
+PHONY:
+	install clean all mpack

Added: trunk/extensions/Tools/PictureTile/PictureTile.addin.xml
==============================================================================
--- (empty file)
+++ trunk/extensions/Tools/PictureTile/PictureTile.addin.xml	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,15 @@
+<Addin namespace="FSpot"
+	id="PictureTileExtension"
+	version="0.4.4.100"
+	description="Create photo wall using PictureTile by Jamie Zawinski (http://www.jwz.org/picturetile/)"
+	author="Lorenzo Milesi"
+	name="PictureTile"
+	url="http://f-spot.org/Extensions";
+	category="Tools">
+	<Dependencies>
+		<Addin id="Core" version="0.4.4.100"/>
+	</Dependencies>
+	<Extension path = "/FSpot/Menus/Tools">
+		<Command id = "PictureTile" _label = "Create photo_wall" command_type = "PictureTileExtension.PictureTile" />
+	</Extension>
+</Addin>

Added: trunk/extensions/Tools/PictureTile/PictureTile.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Tools/PictureTile/PictureTile.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,252 @@
+/*
+ * PictureTile.cs
+ * Create photowalls from F-Spot using PictureTile by Jamie Zawinski
+ * http://www.jwz.org/picturetile/
+ *
+ * Author(s)
+ * 	Lorenzo Milesi <maxxer yetopen it>
+ *
+ * This is free software. See COPYING for details
+ *
+ * (c) YetOpen S.r.l. - Lecco
+ */
+
+using System;
+using System.IO;
+using System.Collections;
+using System.Globalization;
+using Gtk;
+
+using FSpot;
+using FSpot.Extensions;
+using FSpot.Widgets;
+using FSpot.Filters;
+using FSpot.UI.Dialog;
+using Mono.Unix;
+
+namespace PictureTileExtension {
+	public class PictureTile: ICommand
+	{
+		[Glade.Widget] Gtk.Dialog picturetile_dialog;
+		[Glade.Widget] Gtk.SpinButton x_max_size;
+		[Glade.Widget] Gtk.SpinButton y_max_size;
+		[Glade.Widget] Gtk.SpinButton space_between_images;
+		[Glade.Widget] Gtk.SpinButton outside_border;
+		[Glade.Widget] Gtk.ComboBox background_color;
+		[Glade.Widget] Gtk.SpinButton image_scale;
+		[Glade.Widget] Gtk.CheckButton uniform_images;
+		[Glade.Widget] Gtk.RadioButton jpeg_radio;
+		[Glade.Widget] Gtk.RadioButton tiff_radio;
+		[Glade.Widget] Gtk.SpinButton pages;
+		[Glade.Widget] Gtk.SpinButton jpeg_quality;
+		string dir_tmp;
+		string destfile_tmp;
+		string [] colors = {"white", "black"};
+		Tag [] photo_tags;
+
+		public void Run (object o, EventArgs e) {
+			Console.WriteLine ("Executing PictureTile extension");
+			if (MainWindow.Toplevel.SelectedPhotos ().Length == 0) {
+				InfoDialog (Catalog.GetString ("No selection available"),
+					    Catalog.GetString ("This tool requires an active selection. Please select one or more pictures and try again"),
+					    Gtk.MessageType.Error);
+				return;
+			} else {
+				//Check for PictureTile executable
+				string output = "";
+				try {
+					System.Diagnostics.Process mp_check = new System.Diagnostics.Process ();
+					mp_check.StartInfo.RedirectStandardOutput = true;
+					mp_check.StartInfo.RedirectStandardError = true;
+					mp_check.StartInfo.UseShellExecute = false;
+					mp_check.StartInfo.FileName = "picturetile.pl";
+					mp_check.Start ();
+					mp_check.WaitForExit ();
+					StreamReader sroutput = mp_check.StandardError;
+					output = sroutput.ReadLine ();
+				} catch (System.Exception) {
+				}
+				if (!System.Text.RegularExpressions.Regex.IsMatch (output, "^picturetile")) {
+					InfoDialog (Catalog.GetString ("PictureTile not available"),
+						    Catalog.GetString ("The picturetile.pl executable was not found in path. Please check that you have it installed and that you have permissions to execute it"),
+						    Gtk.MessageType.Error);
+					return;
+				}
+
+				ShowDialog ();
+			}
+		}
+
+		public void ShowDialog () {
+			Glade.XML xml = new Glade.XML (null, "PictureTile.glade", "picturetile_dialog", "f-spot");
+			xml.Autoconnect (this);
+			picturetile_dialog.Modal = true;
+			picturetile_dialog.TransientFor = null;
+
+			picturetile_dialog.Response += OnDialogReponse;
+			jpeg_radio.Active = true;
+			jpeg_radio.Toggled += HandleFormatRadioToggled;
+			HandleFormatRadioToggled (null, null);
+			PopulateCombo ();
+			picturetile_dialog.ShowAll ();
+		}
+
+		void OnDialogReponse (object obj, ResponseArgs args) {
+			if (args.ResponseId == ResponseType.Ok) {
+				CreatePhotoWall ();
+			}
+			picturetile_dialog.Destroy ();
+		}
+
+		void CreatePhotoWall () {
+			dir_tmp = System.IO.Path.GetTempFileName ();
+			System.IO.File.Delete (dir_tmp);
+			System.IO.Directory.CreateDirectory (dir_tmp);
+			dir_tmp += "/";
+
+			//Prepare the pictures
+			ProgressDialog progress_dialog = null;
+			progress_dialog = new ProgressDialog (Catalog.GetString ("Preparing selected pictures"),
+							      ProgressDialog.CancelButtonType.Stop,
+							      MainWindow.Toplevel.SelectedPhotos ().Length, picturetile_dialog);
+
+			FilterSet filters = new FilterSet ();
+			filters.Add (new JpegFilter ());
+			filters.Add (new OrientationFilter ());
+			uint counter = 0;
+			ArrayList all_tags = new ArrayList ();
+			foreach (Photo p in MainWindow.Toplevel.SelectedPhotos ()) {
+				if (progress_dialog.Update (String.Format (Catalog.GetString ("Processing \"{0}\""), p.Name))) {
+					progress_dialog.Destroy ();
+					DeleteTmp ();
+					return;
+				}
+
+				//Store photo tags, to attach them later on import
+				foreach (Tag tag in p.Tags) {
+					if (! all_tags.Contains (tag))
+						all_tags.Add (tag);
+				}
+
+				//FIXME should switch to retry/skip
+				if (!(new Gnome.Vfs.Uri (p.DefaultVersionUri.ToString ())).Exists)
+					continue;
+
+				using (FilterRequest freq = new FilterRequest (p.DefaultVersionUri)) {
+					filters.Convert (freq);
+					File.Copy (freq.Current.LocalPath, String.Format ("{0}{1}.jpg", dir_tmp, counter ++));
+				}
+			}
+			if (progress_dialog != null)
+				progress_dialog.Destroy ();
+
+			photo_tags = (Tag []) all_tags.ToArray (typeof (Tag));
+
+			string uniform = "";
+			if (uniform_images.Active)
+				uniform = "--uniform";
+			string output_format = "jpeg";
+			if (tiff_radio.Active)
+				output_format = "tiff";
+			string scale = String.Format (CultureInfo.InvariantCulture, "{0,4}", (double) image_scale.Value / (double) 100);
+
+			destfile_tmp = String.Format ("{0}.{1}", System.IO.Path.GetTempFileName (), output_format);
+
+			//Execute picturetile
+			string picturetile_command = String.Format ("--size {0}x{1} " +
+									"--directory {2} " +
+									"--scale {3} " +
+									"--margin {4} " +
+									"--border {5} " +
+									"--background {6} " +
+									"--pages {7} " +
+									"{8} " +
+									"{9}",
+								x_max_size.Text,
+								y_max_size.Text,
+								dir_tmp,
+								scale,
+								space_between_images.Text,
+								outside_border.Text,
+								colors [background_color.Active],
+								pages.Text,
+								uniform,
+								destfile_tmp);
+			Console.WriteLine ("Executing: picturetile.pl {0}", picturetile_command);
+			System.Diagnostics.Process pt_exe = System.Diagnostics.Process.Start ("picturetile.pl", picturetile_command);
+			pt_exe.WaitForExit ();
+
+			// Handle multiple files generation (pages).
+			// If the user wants 2 pages (images), and the output filename is out.jpg, picturetile will create
+			// /tmp/out1.jpg and /tmp/out2.jpg.
+			System.IO.DirectoryInfo di = new System.IO.DirectoryInfo (System.IO.Path.GetDirectoryName (destfile_tmp));
+			string filemask = System.IO.Path.GetFileNameWithoutExtension (destfile_tmp) + "*" + System.IO.Path.GetExtension (destfile_tmp);
+			FileInfo [] fi = di.GetFiles (filemask);
+
+			// Move generated files to f-spot photodir
+			string [] photo_import_list = new string [fi.Length];
+			counter = 0;
+			foreach (FileInfo f in fi) {
+				string orig = System.IO.Path.Combine (f.DirectoryName, f.Name);
+				photo_import_list [counter ++] = MoveFile (orig);
+			}
+
+			//Add the pic(s) to F-Spot!
+			Db db = MainWindow.Toplevel.Database;
+			ImportCommand command = new ImportCommand (null);
+			if (command.ImportFromPaths (db.Photos, photo_import_list, photo_tags) > 0) {
+				InfoDialog (Catalog.GetString ("PhotoWall generated!"),
+					    Catalog.GetString ("Your photo wall have been generated and imported in F-Spot. Select the last roll to see it"),
+					    Gtk.MessageType.Info);
+			} else {
+				InfoDialog (Catalog.GetString ("Error importing photowall"),
+					    Catalog.GetString ("An error occurred while importing the newly generated photowall to F-Spot"),
+					    Gtk.MessageType.Error);
+			}
+			DeleteTmp ();
+
+		}
+
+		private void HandleFormatRadioToggled (object o, EventArgs e) {
+			jpeg_quality.Sensitive = jpeg_radio.Active;
+		}
+
+		private void DeleteTmp ()
+		{
+			//Clean temp workdir
+			DirectoryInfo dir = new DirectoryInfo(dir_tmp);
+			FileInfo[] tmpfiles = dir.GetFiles();
+			foreach (FileInfo f in tmpfiles) {
+				if (System.IO.File.Exists(dir_tmp + f.Name)) {
+					System.IO.File.Delete (dir_tmp + f.Name);
+				}
+			}
+			if (System.IO.Directory.Exists(dir_tmp)) {
+				System.IO.Directory.Delete(dir_tmp);
+			}
+		}
+
+		private void PopulateCombo () {
+			foreach (string c in colors) {
+				background_color.AppendText (c);
+			}
+			background_color.Active = 0;
+		}
+
+		private string MoveFile (string orig) {
+			string dest = FileImportBackend.ChooseLocation (orig);
+			System.IO.File.Move (orig, dest);
+			return dest;
+		}
+
+		private void InfoDialog (string title, string msg, Gtk.MessageType type) {
+			HigMessageDialog md = new HigMessageDialog (MainWindow.Toplevel.Window, DialogFlags.DestroyWithParent,
+						  type, ButtonsType.Ok, title, msg);
+
+			md.Run ();
+			md.Destroy ();
+
+		}
+
+	}
+}

Copied: trunk/extensions/Tools/PictureTile/PictureTile.glade (from r4368, /trunk/extensions/PictureTile/PictureTile.glade)
==============================================================================

Copied: trunk/extensions/Tools/RawPlusJpeg/.gitignore (from r4368, /trunk/extensions/RawPlusJpeg/.gitignore)
==============================================================================

Copied: trunk/extensions/Tools/RawPlusJpeg/Makefile (from r4368, /trunk/extensions/RawPlusJpeg/Makefile)
==============================================================================

Copied: trunk/extensions/Tools/RawPlusJpeg/RawPlusJpeg.addin.xml (from r4368, /trunk/extensions/RawPlusJpeg/RawPlusJpeg.addin.xml)
==============================================================================

Added: trunk/extensions/Tools/RawPlusJpeg/RawPlusJpeg.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Tools/RawPlusJpeg/RawPlusJpeg.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,123 @@
+/*
+ * RawPlusJpeg.cs
+ *
+ * Author(s)
+ * 	Stephane Delcroix  <stephane delcroix org>
+ *
+ * This is free software. See COPYING for details
+ */
+
+using System;
+using System.Collections.Generic;
+
+using Gtk;
+
+using FSpot;
+using FSpot.UI.Dialog;
+using FSpot.Extensions;
+
+namespace RawPlusJpegExtension
+{
+	public class RawPlusJpeg : ICommand
+	{
+		public void Run (object o, EventArgs e)
+		{
+			Console.WriteLine ("EXECUTING RAW PLUS JPEG EXTENSION");
+
+			if (ResponseType.Ok != HigMessageDialog.RunHigConfirmation (
+				MainWindow.Toplevel.Window,
+				DialogFlags.DestroyWithParent,
+				MessageType.Warning,
+				"Merge Raw+Jpegs",
+				"This operation will merge Raw and Jpegs versions of the same image as one unique image. The Raw image will be the Original version, the jpeg will be named 'Jpeg' and all subsequent versions will keep their original names (if possible).\n\nNote: only enabled for some formats right now.",
+				"Do it now"))
+				return;
+
+			Photo [] photos = Core.Database.Photos.Query ((Tag [])null, null, null, null);
+			Array.Sort (photos, new Photo.CompareDirectory ());
+
+			Photo raw = null;
+			Photo jpeg = null;
+
+			IList<MergeRequest> merge_requests = new List<MergeRequest> ();
+
+			for (int i = 0; i < photos.Length; i++) {
+				Photo p = photos [i];
+
+				if (!ImageFile.IsRaw (p.Name) && !ImageFile.IsJpeg (p.Name))
+					continue;
+
+				if (ImageFile.IsJpeg (p.Name))
+					jpeg = p;
+				if (ImageFile.IsRaw (p.Name))
+					raw = p;
+
+				if (raw != null && jpeg != null && SamePlaceAndName (raw, jpeg))
+					merge_requests.Add (new MergeRequest (raw, jpeg));
+			}
+
+			if (merge_requests.Count == 0)
+				return;
+
+			foreach (MergeRequest mr in merge_requests)
+				mr.Merge ();
+
+			MainWindow.Toplevel.UpdateQuery ();
+		}
+
+		private static bool SamePlaceAndName (Photo p1, Photo p2)
+		{
+			return DirectoryPath (p1) == DirectoryPath (p2) &&
+				System.IO.Path.GetFileNameWithoutExtension (p1.Name) == System.IO.Path.GetFileNameWithoutExtension (p2.Name);
+		}
+
+
+		private static string DirectoryPath (Photo p)
+		{
+			System.Uri uri = p.VersionUri (Photo.OriginalVersionId);
+			return uri.Scheme + "://" + uri.Host + System.IO.Path.GetDirectoryName (uri.AbsolutePath);
+		}
+
+		class MergeRequest
+		{
+			Photo raw;
+			Photo jpeg;
+
+			public MergeRequest (Photo raw, Photo jpeg)
+			{
+				this.raw = raw;
+				this.jpeg = jpeg;
+			}
+
+			public void Merge ()
+			{
+				Console.WriteLine ("Merging {0} and {1}", raw.VersionUri (Photo.OriginalVersionId), jpeg.VersionUri (Photo.OriginalVersionId));
+				foreach (uint version_id in jpeg.VersionIds) {
+					string name = jpeg.GetVersion (version_id).Name;
+					try {
+						raw.DefaultVersionId = raw.CreateReparentedVersion (jpeg.GetVersion (version_id) as PhotoVersion, version_id == Photo.OriginalVersionId);
+						if (version_id == Photo.OriginalVersionId)
+							raw.RenameVersion (raw.DefaultVersionId, "Jpeg");
+						else
+							raw.RenameVersion (raw.DefaultVersionId, name);
+					} catch (Exception e) {
+						Console.WriteLine (e);
+					}
+				}
+				raw.AddTag (jpeg.Tags);
+				uint [] version_ids = jpeg.VersionIds;
+				Array.Reverse (version_ids);
+				foreach (uint version_id in version_ids) {
+					try {
+						jpeg.DeleteVersion (version_id, true, true);
+					} catch (Exception e) {
+						Console.WriteLine (e);
+					}
+				}
+				raw.Changes.DataChanged = true;
+				Core.Database.Photos.Commit (raw);
+				Core.Database.Photos.Remove (jpeg);
+			}
+		}
+	}
+}

Copied: trunk/extensions/Tools/RetroactiveRoll/.gitignore (from r4368, /trunk/extensions/RetroactiveRoll/.gitignore)
==============================================================================

Copied: trunk/extensions/Tools/RetroactiveRoll/Makefile (from r4368, /trunk/extensions/RetroactiveRoll/Makefile)
==============================================================================

Added: trunk/extensions/Tools/RetroactiveRoll/RetroactiveRoll.addin.xml
==============================================================================
--- (empty file)
+++ trunk/extensions/Tools/RetroactiveRoll/RetroactiveRoll.addin.xml	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,15 @@
+<Addin namespace="FSpot"
+	id="RetroactiveRoll"
+	name="RetroactiveRoll"
+	version="0.4.4.100"
+	description="Retroactively assign old photos to import rolls"
+	author="Andy Wingo"
+	url="http://f-spot.org/Extensions";
+	category="Tools">
+	<Dependencies>
+		<Addin id="Core" version="0.4.4.100"/>
+	</Dependencies>
+	<Extension path = "/FSpot/Menus/PhotoPopup">
+		<Command id = "RetroactiveRoll" _label = "Reassign to new import roll" command_type = "RetroactiveRoll.RetroactiveRoll"/>
+	</Extension>
+</Addin>

Added: trunk/extensions/Tools/RetroactiveRoll/RetroactiveRoll.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Tools/RetroactiveRoll/RetroactiveRoll.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,49 @@
+/*
+ * RetroactiveRoll.cs
+ *
+ * Author(s)
+ * 	Andy Wingo  <wingo pobox com>
+ *
+ * This is free software. See COPYING for details
+ */
+
+
+using FSpot;
+using FSpot.Extensions;
+using Mono.Unix;
+using System;
+using Mono.Data.SqliteClient;
+using Banshee.Database;
+
+namespace RetroactiveRoll
+{
+	public class RetroactiveRoll: ICommand
+	{
+		public void Run (object o, EventArgs e)
+		{
+			Photo[] photos = MainWindow.Toplevel.SelectedPhotos ();
+
+			if (photos.Length == 0) {
+				Console.WriteLine ("no photos selected, returning");
+				return;
+			}
+
+			DateTime import_time = photos[0].Time;
+			foreach (Photo p in photos)
+				if (p.Time > import_time)
+					import_time = p.Time;
+
+			RollStore rolls = Core.Database.Rolls;
+			Roll roll = rolls.Create(import_time);
+			foreach (Photo p in photos) {
+				DbCommand cmd = new DbCommand ("UPDATE photos SET roll_id = :roll_id " +
+							       "WHERE id = :id ",
+							       "roll_id", roll.Id,
+							       "id", p.Id);
+				Core.Database.Database.ExecuteNonQuery (cmd);
+				p.RollId = roll.Id;
+			}
+			Console.WriteLine ("RetroactiveRoll done: " + photos.Length + " photos in roll " + roll.Id);
+		}
+	}
+}

Copied: trunk/extensions/Tools/SyncCatalog/.gitignore (from r4368, /trunk/extensions/SyncCatalog/.gitignore)
==============================================================================

Copied: trunk/extensions/Tools/SyncCatalog/Makefile (from r4368, /trunk/extensions/SyncCatalog/Makefile)
==============================================================================

Copied: trunk/extensions/Tools/SyncCatalog/SyncCatalog.addin.xml (from r4368, /trunk/extensions/SyncCatalog/SyncCatalog.addin.xml)
==============================================================================

Added: trunk/extensions/Tools/SyncCatalog/SyncCatalog.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Tools/SyncCatalog/SyncCatalog.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,43 @@
+/*
+ * SyncMetaData.cs
+ *
+ * Author(s)
+ * 	Miguel Aguero  <miguelaguero gmail com>
+ *
+ * This is free software. See COPYING for details
+ */
+
+using System;
+using System.Collections.Generic;
+
+using Gtk;
+
+using FSpot;
+using FSpot.UI.Dialog;
+using FSpot.Extensions;
+using FSpot.Jobs;
+
+namespace SyncCatalogExtension {
+
+	public class SyncCatalog : ICommand {
+		public void Run (object o, EventArgs e) {
+			if (ResponseType.Ok != HigMessageDialog.RunHigConfirmation (
+				MainWindow.Toplevel.Window,
+				DialogFlags.DestroyWithParent,
+				MessageType.Warning,
+				"Sync Catalog with photos",
+				"Sync operation of the entire catalog with all photos could take hours, but hopefully it will be run in background. you can stop f-spot any time you want, the sync job will be restarted next time you start f-spot",
+				"Do it now"))
+				return;
+
+			Photo [] photos = Core.Database.Photos.Query ((Tag [])null, null, null, null);
+
+			foreach (Photo photo in photos) {
+				SyncMetadataJob.Create (Core.Database.Jobs, photo);
+			}
+
+
+		}
+	}
+
+}

Copied: trunk/extensions/Tools/ZipExport/.gitignore (from r4368, /trunk/extensions/ZipExport/.gitignore)
==============================================================================

Added: trunk/extensions/Tools/ZipExport/Makefile
==============================================================================
--- (empty file)
+++ trunk/extensions/Tools/ZipExport/Makefile	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,37 @@
+NAME = ZipExport
+
+PACKAGES = \
+	-pkg:f-spot \
+	-pkg:gnome-vfs-sharp-2.0 \
+	-pkg:gtk-sharp-2.0 \
+	-pkg:glade-sharp-2.0
+
+ASSEMBLIES = \
+	-r:ICSharpCode.SharpZipLib \
+	-r:Mono.Posix
+
+RESOURCES = \
+	-resource:$(NAME).glade \
+	-resource:$(NAME).addin.xml
+
+SOURCES = \
+	ZipExport.cs
+
+ASSEMBLY = $(NAME).dll
+
+all: $(ASSEMBLY)
+
+install: all
+	cp *.dll ~/.gnome2/f-spot/addins/
+
+mpack: $(ASSEMBLY)
+	mautil p $(ASSEMBLY)
+
+$(ASSEMBLY): $(SOURCES) ZipExport.glade ZipExport.addin.xml
+	gmcs -out:$@ -target:library $(SOURCES) $(PACKAGES) $(ASSEMBLIES) $(RESOURCES)
+
+clean:
+	rm -f *.dll *~ *.bak *.mpack *.gladep
+
+PHONY:
+	install clean all mpack

Copied: trunk/extensions/Tools/ZipExport/ZipExport.addin.xml (from r4368, /trunk/extensions/ZipExport/ZipExport.addin.xml)
==============================================================================

Added: trunk/extensions/Tools/ZipExport/ZipExport.cs
==============================================================================
--- (empty file)
+++ trunk/extensions/Tools/ZipExport/ZipExport.cs	Tue Sep 16 13:11:27 2008
@@ -0,0 +1,157 @@
+/*
+ * ZipExport.cs
+ * Simple zip exporter. Creates a zip file with unmodified pictures.
+ *
+ * Author(s)
+ * 	Lorenzo Milesi <maxxer yetopen it>
+ *
+ * Many thanks to Stephane for his help and patience. :)
+ *
+ * This is free software. See COPYING for details
+ * (c) YetOpen S.r.l.
+ */
+
+
+using FSpot;
+using FSpot.UI.Dialog;
+using FSpot.Extensions;
+using FSpot.Filters;
+using System;
+using System.IO;
+using System.Collections;
+using Mono.Unix;
+using Gtk;
+using ICSharpCode.SharpZipLib.Checksums;
+using ICSharpCode.SharpZipLib.Zip;
+using ICSharpCode.SharpZipLib.GZip;
+
+namespace ZipExport {
+	public class Zip : IExporter {
+
+		[Glade.Widget] Gtk.Dialog zipdiag;
+		[Glade.Widget] Gtk.HBox dirchooser_hbox;
+		[Glade.Widget] Gtk.CheckButton scale_check;
+		[Glade.Widget] Gtk.Entry filename;
+		[Glade.Widget] Gtk.SpinButton scale_size;
+		[Glade.Widget] Gtk.Button create_button;
+
+		IBrowsableItem [] photos;
+		Gtk.FileChooserButton uri_chooser;
+
+		public void Run (IBrowsableCollection p) {
+			Console.WriteLine ("Executing ZipExport extension");
+			if (p.Count == 0) {
+				HigMessageDialog md = new HigMessageDialog (MainWindow.Toplevel.Window, DialogFlags.DestroyWithParent,
+							  Gtk.MessageType.Error, ButtonsType.Ok,
+							  Catalog.GetString ("No selection available"),
+							  Catalog.GetString ("This tool requires an active selection. Please select one or more pictures and try again"));
+
+				md.Run ();
+				md.Destroy ();
+				return;
+			}
+			photos = p.Items;
+			ShowDialog ();
+		}
+
+		public void ShowDialog () {
+			Glade.XML xml = new Glade.XML (null, "ZipExport.glade", "zipdiag", "f-spot");
+			xml.Autoconnect (this);
+			zipdiag.Modal = false;
+			zipdiag.TransientFor = null;
+
+			uri_chooser = new Gtk.FileChooserButton (Catalog.GetString ("Select export folder"),
+								 Gtk.FileChooserAction.SelectFolder);
+			uri_chooser.LocalOnly = true;
+			uri_chooser.SetFilename (System.IO.Path.Combine (FSpot.Global.HomeDirectory, "Desktop"));
+			dirchooser_hbox.PackStart (uri_chooser, false, false, 2);
+			filename.Text = "f-spot_export.zip";
+
+			zipdiag.Response += on_dialog_response;
+			filename.Changed += on_filename_change;
+			scale_check.Toggled += on_scalecheck_change;
+			on_scalecheck_change (null, null);
+
+			zipdiag.ShowAll ();
+		}
+
+		private void on_dialog_response (object sender, ResponseArgs args) {
+			if (args.ResponseId != Gtk.ResponseType.Ok) {
+				// FIXME this is to work around a bug in gtk+ where
+				// the filesystem events are still listened to when
+				// a FileChooserButton is destroyed but not finalized
+				// and an event comes in that wants to update the child widgets.
+				uri_chooser.Dispose ();
+				uri_chooser = null;
+			} else if (args.ResponseId == Gtk.ResponseType.Ok) {
+				zip ();
+			}
+			zipdiag.Destroy ();
+		}
+
+		void zip () {
+			Gnome.Vfs.Uri dest = new Gnome.Vfs.Uri (uri_chooser.Uri);
+			Crc32 crc = new Crc32 ();
+			string filedest = Gnome.Vfs.Uri.GetLocalPathFromUri (dest.ToString ()) + "/" + filename.Text;
+			Console.WriteLine ("Creating zip file {0}", filedest);
+			ZipOutputStream s = new ZipOutputStream (File.Create(filedest));
+			if (scale_check.Active)
+				Console.WriteLine ("Scaling to {0}", scale_size.ValueAsInt);
+
+			ProgressDialog progress_dialog = new ProgressDialog (Catalog.GetString ("Exporting files"),
+							      ProgressDialog.CancelButtonType.Stop,
+							      photos.Length, zipdiag);
+
+			//Pack up
+			for (int i = 0; i < photos.Length; i ++) {
+				if (progress_dialog.Update (String.Format (Catalog.GetString ("Preparing photo \"{0}\""), photos[i].Name))) {
+					progress_dialog.Destroy ();
+					return;
+				}
+				string f = null;
+				if (scale_check.Active) {
+					FilterSet filters = new FilterSet ();
+					filters.Add (new JpegFilter ());
+					filters.Add (new ResizeFilter ((uint) scale_size.ValueAsInt));
+					FilterRequest freq = new FilterRequest (photos [i].DefaultVersionUri);
+					filters.Convert (freq);
+					f = freq.Current.LocalPath;
+				} else {
+					f = photos [i].DefaultVersionUri.LocalPath;
+				}
+				FileStream fs = File.OpenRead (f);
+
+				byte [] buffer = new byte [fs.Length];
+				fs.Read (buffer, 0, buffer.Length);
+				ZipEntry entry = new ZipEntry (photos [i].Name);
+
+				entry.DateTime = DateTime.Now;
+
+				entry.Size = fs.Length;
+				fs.Close ();
+
+				crc.Reset ();
+				crc.Update (buffer);
+
+				entry.Crc = crc.Value;
+
+				s.PutNextEntry (entry);
+
+				s.Write (buffer, 0, buffer.Length);
+			}
+			s.Finish ();
+			s.Close ();
+			if (progress_dialog != null)
+				progress_dialog.Destroy ();
+
+		}
+
+		private void on_filename_change (object sender, System.EventArgs args) {
+			create_button.Sensitive = System.Text.RegularExpressions.Regex.IsMatch (filename.Text, "[.]zip$");
+		}
+
+		private void on_scalecheck_change (object sender, System.EventArgs args) {
+			scale_size.Sensitive = scale_check.Active;
+		}
+	}
+}

Copied: trunk/extensions/Tools/ZipExport/ZipExport.glade (from r4368, /trunk/extensions/ZipExport/ZipExport.glade)
==============================================================================



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