Merge branch 'image-management' into next
authorDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Mon, 26 Sep 2011 19:31:55 +0000 (21:31 +0200)
committerDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Mon, 26 Sep 2011 19:31:55 +0000 (21:31 +0200)
55 files changed:
pom.xml
src/main/java/net/pterodactylus/sone/core/Core.java
src/main/java/net/pterodactylus/sone/core/CoreListener.java
src/main/java/net/pterodactylus/sone/core/CoreListenerManager.java
src/main/java/net/pterodactylus/sone/core/FreenetInterface.java
src/main/java/net/pterodactylus/sone/core/ImageInsertListener.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/core/ImageInserter.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/core/Options.java
src/main/java/net/pterodactylus/sone/core/SoneDownloader.java
src/main/java/net/pterodactylus/sone/core/SoneException.java
src/main/java/net/pterodactylus/sone/core/SoneInserter.java
src/main/java/net/pterodactylus/sone/data/Album.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/data/Image.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/data/Sone.java
src/main/java/net/pterodactylus/sone/data/TemporaryImage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/freenet/SimpleFieldSetBuilder.java
src/main/java/net/pterodactylus/sone/freenet/wot/IdentityManager.java
src/main/java/net/pterodactylus/sone/freenet/wot/WebOfTrustConnector.java
src/main/java/net/pterodactylus/sone/main/SonePlugin.java
src/main/java/net/pterodactylus/sone/template/AlbumAccessor.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/template/ImageLinkFilter.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/text/PartContainer.java
src/main/java/net/pterodactylus/sone/web/CreateAlbumPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/DeleteAlbumPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/DeleteImagePage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/EditAlbumPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/EditImagePage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/GetImagePage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/ImageBrowserPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/SearchPage.java
src/main/java/net/pterodactylus/sone/web/UploadImagePage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/WebInterface.java
src/main/java/net/pterodactylus/sone/web/ajax/EditAlbumAjaxPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/ajax/EditImageAjaxPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/ajax/GetNotificationAjaxPage.java
src/main/java/net/pterodactylus/sone/web/page/FreenetTemplatePage.java
src/main/resources/i18n/sone.en.properties
src/main/resources/static/css/sone.css
src/main/resources/static/images/unknown-image-0.png [new file with mode: 0644]
src/main/resources/static/javascript/sone.js
src/main/resources/templates/createAlbum.html [new file with mode: 0644]
src/main/resources/templates/deleteAlbum.html [new file with mode: 0644]
src/main/resources/templates/deleteImage.html [new file with mode: 0644]
src/main/resources/templates/imageBrowser.html [new file with mode: 0644]
src/main/resources/templates/include/browseAlbums.html [new file with mode: 0644]
src/main/resources/templates/include/createAlbum.html [new file with mode: 0644]
src/main/resources/templates/include/soneMenu.html
src/main/resources/templates/include/uploadImage.html [new file with mode: 0644]
src/main/resources/templates/insert/include/album.xml [new file with mode: 0644]
src/main/resources/templates/insert/sone.xml
src/main/resources/templates/invalid.html
src/main/resources/templates/notify/image-insert-failed-notification.html [new file with mode: 0644]
src/main/resources/templates/notify/inserted-images-notification.html [new file with mode: 0644]
src/main/resources/templates/notify/inserting-images-notification.html [new file with mode: 0644]
src/main/resources/templates/viewSone.html

diff --git a/pom.xml b/pom.xml
index 037d3be..e59d521 100644 (file)
--- a/pom.xml
+++ b/pom.xml
@@ -7,7 +7,7 @@
                <dependency>
                        <groupId>net.pterodactylus</groupId>
                        <artifactId>utils</artifactId>
-                       <version>0.10.0</version>
+                       <version>0.10.1-SNAPSHOT</version>
                </dependency>
                <dependency>
                        <groupId>junit</groupId>
index 0eaf296..8aac5d7 100644 (file)
@@ -34,12 +34,15 @@ import java.util.logging.Logger;
 import net.pterodactylus.sone.core.Options.DefaultOption;
 import net.pterodactylus.sone.core.Options.Option;
 import net.pterodactylus.sone.core.Options.OptionWatcher;
+import net.pterodactylus.sone.data.Album;
 import net.pterodactylus.sone.data.Client;
+import net.pterodactylus.sone.data.Image;
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Profile;
-import net.pterodactylus.sone.data.Profile.Field;
 import net.pterodactylus.sone.data.Reply;
 import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.data.TemporaryImage;
+import net.pterodactylus.sone.data.Profile.Field;
 import net.pterodactylus.sone.fcp.FcpInterface;
 import net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired;
 import net.pterodactylus.sone.freenet.wot.Identity;
@@ -67,7 +70,7 @@ import freenet.keys.FreenetURI;
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
-public class Core extends AbstractService implements IdentityListener, UpdateListener, SoneProvider, PostProvider, SoneInsertListener {
+public class Core extends AbstractService implements IdentityListener, UpdateListener, SoneProvider, PostProvider, SoneInsertListener, ImageInsertListener {
 
        /**
         * Enumeration for the possible states of a {@link Sone}.
@@ -116,6 +119,9 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
        /** The Sone downloader. */
        private final SoneDownloader soneDownloader;
 
+       /** The image inserter. */
+       private final ImageInserter imageInserter;
+
        /** Sone downloader thread-pool. */
        private final ExecutorService soneDownloaders = Executors.newFixedThreadPool(10);
 
@@ -182,6 +188,15 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
        /** Trusted identities, sorted by own identities. */
        private Map<OwnIdentity, Set<Identity>> trustedIdentities = Collections.synchronizedMap(new HashMap<OwnIdentity, Set<Identity>>());
 
+       /** All known albums. */
+       private Map<String, Album> albums = new HashMap<String, Album>();
+
+       /** All known images. */
+       private Map<String, Image> images = new HashMap<String, Image>();
+
+       /** All temporary images. */
+       private Map<String, TemporaryImage> temporaryImages = new HashMap<String, TemporaryImage>();
+
        /** Ticker for threads that mark own elements as known. */
        private Ticker localElementTicker = new Ticker();
 
@@ -204,6 +219,7 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
                this.freenetInterface = freenetInterface;
                this.identityManager = identityManager;
                this.soneDownloader = new SoneDownloader(this, freenetInterface);
+               this.imageInserter = new ImageInserter(this, freenetInterface);
                this.updateChecker = new UpdateChecker(freenetInterface);
        }
 
@@ -696,7 +712,6 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
         */
        public List<Reply> getReplies(Post post) {
                Set<Sone> sones = getSones();
-               @SuppressWarnings("hiding")
                List<Reply> replies = new ArrayList<Reply>();
                for (Sone sone : sones) {
                        for (Reply reply : sone.getReplies()) {
@@ -789,7 +804,6 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
         * @return All bookmarked posts
         */
        public Set<Post> getBookmarkedPosts() {
-               @SuppressWarnings("hiding")
                Set<Post> posts = new HashSet<Post>();
                synchronized (bookmarkedPosts) {
                        for (String bookmarkedPostId : bookmarkedPosts) {
@@ -802,6 +816,89 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
                return posts;
        }
 
+       /**
+        * Returns the album with the given ID, creating a new album if no album
+        * with the given ID can be found.
+        *
+        * @param albumId
+        *            The ID of the album
+        * @return The album with the given ID
+        */
+       public Album getAlbum(String albumId) {
+               return getAlbum(albumId, true);
+       }
+
+       /**
+        * Returns the album with the given ID, optionally creating a new album if
+        * an album with the given ID can not be found.
+        *
+        * @param albumId
+        *            The ID of the album
+        * @param create
+        *            {@code true} to create a new album if none exists for the
+        *            given ID
+        * @return The album with the given ID, or {@code null} if no album with the
+        *         given ID exists and {@code create} is {@code false}
+        */
+       public Album getAlbum(String albumId, boolean create) {
+               synchronized (albums) {
+                       Album album = albums.get(albumId);
+                       if (create && (album == null)) {
+                               album = new Album(albumId);
+                               albums.put(albumId, album);
+                       }
+                       return album;
+               }
+       }
+
+       /**
+        * Returns the image with the given ID, creating it if necessary.
+        *
+        * @param imageId
+        *            The ID of the image
+        * @return The image with the given ID
+        */
+       public Image getImage(String imageId) {
+               return getImage(imageId, true);
+       }
+
+       /**
+        * Returns the image with the given ID, optionally creating it if it does
+        * not exist.
+        *
+        * @param imageId
+        *            The ID of the image
+        * @param create
+        *            {@code true} to create an image if none exists with the given
+        *            ID
+        * @return The image with the given ID, or {@code null} if none exists and
+        *         none was created
+        */
+       public Image getImage(String imageId, boolean create) {
+               synchronized (images) {
+                       Image image = images.get(imageId);
+                       if (create && (image == null)) {
+                               image = new Image(imageId);
+                               images.put(imageId, image);
+                       }
+                       return image;
+               }
+       }
+
+       /**
+        * Returns the temporary image with the given ID.
+        *
+        * @param imageId
+        *            The ID of the temporary image
+        * @return The temporary image, or {@code null} if there is no temporary
+        *         image with the given ID
+        */
+       public TemporaryImage getTemporaryImage(String imageId) {
+               synchronized (temporaryImages) {
+                       return temporaryImages.get(imageId);
+               }
+       }
+
        //
        // ACTIONS
        //
@@ -1126,6 +1223,22 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
                                        }
                                }
                        }
+                       synchronized (albums) {
+                               synchronized (images) {
+                                       for (Album album : storedSone.getAlbums()) {
+                                               albums.remove(album.getId());
+                                               for (Image image : album.getImages()) {
+                                                       images.remove(image.getId());
+                                               }
+                                       }
+                                       for (Album album : sone.getAlbums()) {
+                                               albums.put(album.getId(), album);
+                                               for (Image image : album.getImages()) {
+                                                       images.put(image.getId(), image);
+                                               }
+                                       }
+                               }
+                       }
                        synchronized (storedSone) {
                                if (!soneRescueMode || (sone.getTime() > storedSone.getTime())) {
                                        storedSone.setTime(sone.getTime());
@@ -1145,11 +1258,15 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
                                        for (String likedReplyId : sone.getLikedReplyIds()) {
                                                storedSone.addLikedReplyId(likedReplyId);
                                        }
+                                       for (Album album : sone.getAlbums()) {
+                                               storedSone.addAlbum(album);
+                                       }
                                } else {
                                        storedSone.setPosts(sone.getPosts());
                                        storedSone.setReplies(sone.getReplies());
                                        storedSone.setLikePostIds(sone.getLikedPostIds());
                                        storedSone.setLikeReplyIds(sone.getLikedReplyIds());
+                                       storedSone.setAlbums(sone.getAlbums());
                                }
                                storedSone.setLatestEdition(sone.getLatestEdition());
                        }
@@ -1256,7 +1373,6 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
                }
 
                /* load posts. */
-               @SuppressWarnings("hiding")
                Set<Post> posts = new HashSet<Post>();
                while (true) {
                        String postPrefix = sonePrefix + "/Posts/" + posts.size();
@@ -1279,7 +1395,6 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
                }
 
                /* load replies. */
-               @SuppressWarnings("hiding")
                Set<Reply> replies = new HashSet<Reply>();
                while (true) {
                        String replyPrefix = sonePrefix + "/Replies/" + replies.size();
@@ -1327,6 +1442,65 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
                        friends.add(friendId);
                }
 
+               /* load albums. */
+               List<Album> topLevelAlbums = new ArrayList<Album>();
+               int albumCounter = 0;
+               while (true) {
+                       String albumPrefix = sonePrefix + "/Albums/" + albumCounter++;
+                       String albumId = configuration.getStringValue(albumPrefix + "/ID").getValue(null);
+                       if (albumId == null) {
+                               break;
+                       }
+                       String albumTitle = configuration.getStringValue(albumPrefix + "/Title").getValue(null);
+                       String albumDescription = configuration.getStringValue(albumPrefix + "/Description").getValue(null);
+                       String albumParentId = configuration.getStringValue(albumPrefix + "/Parent").getValue(null);
+                       String albumImageId = configuration.getStringValue(albumPrefix + "/AlbumImage").getValue(null);
+                       if ((albumTitle == null) || (albumDescription == null)) {
+                               logger.log(Level.WARNING, "Invalid album found, aborting load!");
+                               return;
+                       }
+                       Album album = getAlbum(albumId).setSone(sone).setTitle(albumTitle).setDescription(albumDescription).setAlbumImage(albumImageId);
+                       if (albumParentId != null) {
+                               Album parentAlbum = getAlbum(albumParentId, false);
+                               if (parentAlbum == null) {
+                                       logger.log(Level.WARNING, "Invalid parent album ID: " + albumParentId);
+                                       return;
+                               }
+                               parentAlbum.addAlbum(album);
+                       } else {
+                               topLevelAlbums.add(album);
+                       }
+               }
+
+               /* load images. */
+               int imageCounter = 0;
+               while (true) {
+                       String imagePrefix = sonePrefix + "/Images/" + imageCounter++;
+                       String imageId = configuration.getStringValue(imagePrefix + "/ID").getValue(null);
+                       if (imageId == null) {
+                               break;
+                       }
+                       String albumId = configuration.getStringValue(imagePrefix + "/Album").getValue(null);
+                       String key = configuration.getStringValue(imagePrefix + "/Key").getValue(null);
+                       String title = configuration.getStringValue(imagePrefix + "/Title").getValue(null);
+                       String description = configuration.getStringValue(imagePrefix + "/Description").getValue(null);
+                       Long creationTime = configuration.getLongValue(imagePrefix + "/CreationTime").getValue(null);
+                       Integer width = configuration.getIntValue(imagePrefix + "/Width").getValue(null);
+                       Integer height = configuration.getIntValue(imagePrefix + "/Height").getValue(null);
+                       if ((albumId == null) || (key == null) || (title == null) || (description == null) || (creationTime == null) || (width == null) || (height == null)) {
+                               logger.log(Level.WARNING, "Invalid image found, aborting load!");
+                               return;
+                       }
+                       Album album = getAlbum(albumId, false);
+                       if (album == null) {
+                               logger.log(Level.WARNING, "Invalid album image encountered, aborting load!");
+                               return;
+                       }
+                       Image image = getImage(imageId).setSone(sone).setCreationTime(creationTime).setKey(key);
+                       image.setTitle(title).setDescription(description).setWidth(width).setHeight(height);
+                       album.addImage(image);
+               }
+
                /* load options. */
                sone.getOptions().getBooleanOption("AutoFollow").set(configuration.getBooleanValue(sonePrefix + "/Options/AutoFollow").getValue(null));
                sone.getOptions().getBooleanOption("EnableSoneInsertNotifications").set(configuration.getBooleanValue(sonePrefix + "/Options/EnableSoneInsertNotifications").getValue(null));
@@ -1340,6 +1514,7 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
                        sone.setLikePostIds(likedPostIds);
                        sone.setLikeReplyIds(likedReplyIds);
                        sone.setFriends(friends);
+                       sone.setAlbums(topLevelAlbums);
                        soneInserters.get(sone).setLastInsertFingerprint(lastInsertFingerprint);
                }
                synchronized (newSones) {
@@ -1629,6 +1804,150 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
        }
 
        /**
+        * Creates a new top-level album for the given Sone.
+        *
+        * @param sone
+        *            The Sone to create the album for
+        * @return The new album
+        */
+       public Album createAlbum(Sone sone) {
+               return createAlbum(sone, null);
+       }
+
+       /**
+        * Creates a new album for the given Sone.
+        *
+        * @param sone
+        *            The Sone to create the album for
+        * @param parent
+        *            The parent of the album (may be {@code null} to create a
+        *            top-level album)
+        * @return The new album
+        */
+       public Album createAlbum(Sone sone, Album parent) {
+               Album album = new Album();
+               synchronized (albums) {
+                       albums.put(album.getId(), album);
+               }
+               album.setSone(sone);
+               if (parent != null) {
+                       parent.addAlbum(album);
+               } else {
+                       sone.addAlbum(album);
+               }
+               return album;
+       }
+
+       /**
+        * Deletes the given album. The owner of the album has to be a local Sone,
+        * and the album has to be {@link Album#isEmpty() empty} to be deleted.
+        *
+        * @param album
+        *            The album to remove
+        */
+       public void deleteAlbum(Album album) {
+               Validation.begin().isNotNull("Album", album).check().is("Local Sone", isLocalSone(album.getSone())).check();
+               if (!album.isEmpty()) {
+                       return;
+               }
+               if (album.getParent() == null) {
+                       album.getSone().removeAlbum(album);
+               } else {
+                       album.getParent().removeAlbum(album);
+               }
+               synchronized (albums) {
+                       albums.remove(album.getId());
+               }
+               saveSone(album.getSone());
+       }
+
+       /**
+        * Creates a new image.
+        *
+        * @param sone
+        *            The Sone creating the image
+        * @param album
+        *            The album the image will be inserted into
+        * @param temporaryImage
+        *            The temporary image to create the image from
+        * @return The newly created image
+        */
+       public Image createImage(Sone sone, Album album, TemporaryImage temporaryImage) {
+               Validation.begin().isNotNull("Sone", sone).isNotNull("Album", album).isNotNull("Temporary Image", temporaryImage).check().is("Local Sone", isLocalSone(sone)).check().isEqual("Owner and Album Owner", sone, album.getSone()).check();
+               Image image = new Image(temporaryImage.getId()).setSone(sone).setCreationTime(System.currentTimeMillis());
+               album.addImage(image);
+               synchronized (images) {
+                       images.put(image.getId(), image);
+               }
+               imageInserter.insertImage(temporaryImage, image);
+               return image;
+       }
+
+       /**
+        * Deletes the given image. This method will also delete a matching
+        * temporary image.
+        *
+        * @see #deleteTemporaryImage(TemporaryImage)
+        * @param image
+        *            The image to delete
+        */
+       public void deleteImage(Image image) {
+               Validation.begin().isNotNull("Image", image).check().is("Local Sone", isLocalSone(image.getSone())).check();
+               deleteTemporaryImage(image.getId());
+               image.getAlbum().removeImage(image);
+               synchronized (images) {
+                       images.remove(image.getId());
+               }
+               saveSone(image.getSone());
+       }
+
+       /**
+        * Creates a new temporary image.
+        *
+        * @param mimeType
+        *            The MIME type of the temporary image
+        * @param imageData
+        *            The encoded data of the image
+        * @return The temporary image
+        */
+       public TemporaryImage createTemporaryImage(String mimeType, byte[] imageData) {
+               TemporaryImage temporaryImage = new TemporaryImage();
+               temporaryImage.setMimeType(mimeType).setImageData(imageData);
+               synchronized (temporaryImages) {
+                       temporaryImages.put(temporaryImage.getId(), temporaryImage);
+               }
+               return temporaryImage;
+       }
+
+       /**
+        * Deletes the given temporary image.
+        *
+        * @param temporaryImage
+        *            The temporary image to delete
+        */
+       public void deleteTemporaryImage(TemporaryImage temporaryImage) {
+               Validation.begin().isNotNull("Temporary Image", temporaryImage).check();
+               deleteTemporaryImage(temporaryImage.getId());
+       }
+
+       /**
+        * Deletes the temporary image with the given ID.
+        *
+        * @param imageId
+        *            The ID of the temporary image to delete
+        */
+       public void deleteTemporaryImage(String imageId) {
+               Validation.begin().isNotNull("Temporary Image ID", imageId).check();
+               synchronized (temporaryImages) {
+                       temporaryImages.remove(imageId);
+               }
+               Image image = getImage(imageId, false);
+               if (image != null) {
+                       imageInserter.cancelImageInsert(image);
+               }
+       }
+
+       /**
         * Notifies the core that the configuration, either of the core or of a
         * single local Sone, has changed, and that the configuration should be
         * saved.
@@ -1777,6 +2096,40 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
                        }
                        configuration.getStringValue(sonePrefix + "/Friends/" + friendCounter + "/ID").setValue(null);
 
+                       /* save albums. first, collect in a flat structure, top-level first. */
+                       List<Album> albums = Sone.flattenAlbums(sone.getAlbums());
+
+                       int albumCounter = 0;
+                       for (Album album : albums) {
+                               String albumPrefix = sonePrefix + "/Albums/" + albumCounter++;
+                               configuration.getStringValue(albumPrefix + "/ID").setValue(album.getId());
+                               configuration.getStringValue(albumPrefix + "/Title").setValue(album.getTitle());
+                               configuration.getStringValue(albumPrefix + "/Description").setValue(album.getDescription());
+                               configuration.getStringValue(albumPrefix + "/Parent").setValue(album.getParent() == null ? null : album.getParent().getId());
+                               configuration.getStringValue(albumPrefix + "/AlbumImage").setValue(album.getAlbumImage() == null ? null : album.getAlbumImage().getId());
+                       }
+                       configuration.getStringValue(sonePrefix + "/Albums/" + albumCounter + "/ID").setValue(null);
+
+                       /* save images. */
+                       int imageCounter = 0;
+                       for (Album album : albums) {
+                               for (Image image : album.getImages()) {
+                                       if (!image.isInserted()) {
+                                               continue;
+                                       }
+                                       String imagePrefix = sonePrefix + "/Images/" + imageCounter++;
+                                       configuration.getStringValue(imagePrefix + "/ID").setValue(image.getId());
+                                       configuration.getStringValue(imagePrefix + "/Album").setValue(album.getId());
+                                       configuration.getStringValue(imagePrefix + "/Key").setValue(image.getKey());
+                                       configuration.getStringValue(imagePrefix + "/Title").setValue(image.getTitle());
+                                       configuration.getStringValue(imagePrefix + "/Description").setValue(image.getDescription());
+                                       configuration.getLongValue(imagePrefix + "/CreationTime").setValue(image.getCreationTime());
+                                       configuration.getIntValue(imagePrefix + "/Width").setValue(image.getWidth());
+                                       configuration.getIntValue(imagePrefix + "/Height").setValue(image.getHeight());
+                               }
+                       }
+                       configuration.getStringValue(sonePrefix + "/Images/" + imageCounter + "/ID").setValue(null);
+
                        /* save options. */
                        configuration.getBooleanValue(sonePrefix + "/Options/AutoFollow").setValue(sone.getOptions().getBooleanOption("AutoFollow").getReal());
                        configuration.getBooleanValue(sonePrefix + "/Options/EnableSoneInsertNotifications").setValue(sone.getOptions().getBooleanOption("EnableSoneInsertNotifications").getReal());
@@ -2129,12 +2482,13 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
        }
 
        //
-       // SONEINSERTLISTENER METHODS
+       // INTERFACE ImageInsertListener
        //
 
        /**
         * {@inheritDoc}
         */
+       @Override
        public void insertStarted(Sone sone) {
                coreListenerManager.fireSoneInserting(sone);
        }
@@ -2155,6 +2509,49 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
                coreListenerManager.fireSoneInsertAborted(sone, cause);
        }
 
+       //
+       // SONEINSERTLISTENER METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public void imageInsertStarted(Image image) {
+               logger.log(Level.WARNING, "Image insert started for " + image);
+               coreListenerManager.fireImageInsertStarted(image);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public void imageInsertAborted(Image image) {
+               logger.log(Level.WARNING, "Image insert aborted for " + image);
+               coreListenerManager.fireImageInsertAborted(image);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public void imageInsertFinished(Image image, FreenetURI key) {
+               logger.log(Level.WARNING, "Image insert finished for " + image + ": " + key);
+               image.setKey(key.toString());
+               deleteTemporaryImage(image.getId());
+               saveSone(image.getSone());
+               coreListenerManager.fireImageInsertFinished(image);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public void imageInsertFailed(Image image, Throwable cause) {
+               logger.log(Level.WARNING, "Image insert failed for " + image, cause);
+               coreListenerManager.fireImageInsertFailed(image, cause);
+       }
+
        /**
         * Convenience interface for external classes that want to access the core’s
         * configuration.
index d5120ac..1658745 100644 (file)
@@ -19,6 +19,7 @@ package net.pterodactylus.sone.core;
 
 import java.util.EventListener;
 
+import net.pterodactylus.sone.data.Image;
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Reply;
 import net.pterodactylus.sone.data.Sone;
@@ -164,4 +165,38 @@ public interface CoreListener extends EventListener {
         */
        public void updateFound(Version version, long releaseTime, long latestEdition);
 
+       /**
+        * Notifies a listener that an image has started being inserted.
+        *
+        * @param image
+        *            The image that is now inserted
+        */
+       public void imageInsertStarted(Image image);
+
+       /**
+        * Notifies a listener that an image insert was aborted by the user.
+        *
+        * @param image
+        *            The image that is not inserted anymore
+        */
+       public void imageInsertAborted(Image image);
+
+       /**
+        * Notifies a listener that an image was successfully inserted.
+        *
+        * @param image
+        *            The image that was inserted
+        */
+       public void imageInsertFinished(Image image);
+
+       /**
+        * Notifies a listener that an image failed to be inserted.
+        *
+        * @param image
+        *            The image that could not be inserted
+        * @param cause
+        *            The reason for the failed insert
+        */
+       public void imageInsertFailed(Image image, Throwable cause);
+
 }
index 875a2b6..5748ffc 100644 (file)
@@ -17,6 +17,7 @@
 
 package net.pterodactylus.sone.core;
 
+import net.pterodactylus.sone.data.Image;
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Reply;
 import net.pterodactylus.sone.data.Sone;
@@ -246,4 +247,58 @@ public class CoreListenerManager extends AbstractListenerManager<Core, CoreListe
                }
        }
 
+       /**
+        * Notifies all listeners that an image has started being inserted.
+        *
+        * @see CoreListener#imageInsertStarted(Image)
+        * @param image
+        *            The image that is now inserted
+        */
+       void fireImageInsertStarted(Image image) {
+               for (CoreListener coreListener : getListeners()) {
+                       coreListener.imageInsertStarted(image);
+               }
+       }
+
+       /**
+        * Notifies all listeners that an image insert was aborted by the user.
+        *
+        * @see CoreListener#imageInsertAborted(Image)
+        * @param image
+        *            The image that is not inserted anymore
+        */
+       void fireImageInsertAborted(Image image) {
+               for (CoreListener coreListener : getListeners()) {
+                       coreListener.imageInsertAborted(image);
+               }
+       }
+
+       /**
+        * Notifies all listeners that an image was successfully inserted.
+        *
+        * @see CoreListener#imageInsertFinished(Image)
+        * @param image
+        *            The image that was inserted
+        */
+       void fireImageInsertFinished(Image image) {
+               for (CoreListener coreListener : getListeners()) {
+                       coreListener.imageInsertFinished(image);
+               }
+       }
+
+       /**
+        * Notifies all listeners that an image failed to be inserted.
+        *
+        * @see CoreListener#imageInsertFailed(Image, Throwable)
+        * @param image
+        *            The image that could not be inserted
+        * @param cause
+        *            The cause of the failure
+        */
+       void fireImageInsertFailed(Image image, Throwable cause) {
+               for (CoreListener coreListener : getListeners()) {
+                       coreListener.imageInsertFailed(image, cause);
+               }
+       }
+
 }
index 20c8da7..39f342c 100644 (file)
 package net.pterodactylus.sone.core;
 
 import java.net.MalformedURLException;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
+import net.pterodactylus.sone.core.SoneException.Type;
+import net.pterodactylus.sone.data.Image;
 import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.data.TemporaryImage;
 import net.pterodactylus.util.collection.Pair;
 import net.pterodactylus.util.logging.Logging;
 
 import com.db4o.ObjectContainer;
 
+import freenet.client.ClientMetadata;
 import freenet.client.FetchException;
 import freenet.client.FetchResult;
 import freenet.client.HighLevelSimpleClient;
 import freenet.client.HighLevelSimpleClientImpl;
+import freenet.client.InsertBlock;
+import freenet.client.InsertContext;
 import freenet.client.InsertException;
+import freenet.client.async.BaseClientPutter;
 import freenet.client.async.ClientContext;
+import freenet.client.async.ClientPutCallback;
+import freenet.client.async.ClientPutter;
 import freenet.client.async.USKCallback;
 import freenet.keys.FreenetURI;
+import freenet.keys.InsertableClientSSK;
 import freenet.keys.USK;
 import freenet.node.Node;
 import freenet.node.RequestStarter;
+import freenet.support.api.Bucket;
+import freenet.support.io.ArrayBucket;
 
 /**
  * Contains all necessary functionality for interacting with the Freenet node.
@@ -115,6 +129,36 @@ public class FreenetInterface {
        }
 
        /**
+        * Inserts the image data of the given {@link TemporaryImage} and returns
+        * the given insert token that can be used to add listeners or cancel the
+        * insert.
+        *
+        * @param temporaryImage
+        *            The temporary image data
+        * @param image
+        *            The image
+        * @param insertToken
+        *            The insert token
+        * @throws SoneException
+        *             if the insert could not be started
+        */
+       public void insertImage(TemporaryImage temporaryImage, Image image, InsertToken insertToken) throws SoneException {
+               String filenameHint = image.getId() + "." + temporaryImage.getMimeType().substring(temporaryImage.getMimeType().lastIndexOf("/") + 1);
+               InsertableClientSSK key = InsertableClientSSK.createRandom(node.random, "");
+               FreenetURI targetUri = key.getInsertURI().setDocName(filenameHint);
+               InsertContext insertContext = client.getInsertContext(true);
+               Bucket bucket = new ArrayBucket(temporaryImage.getImageData());
+               ClientMetadata metadata = new ClientMetadata(temporaryImage.getMimeType());
+               InsertBlock insertBlock = new InsertBlock(bucket, metadata, targetUri);
+               try {
+                       ClientPutter clientPutter = client.insert(insertBlock, false, null, false, insertContext, insertToken, RequestStarter.INTERACTIVE_PRIORITY_CLASS);
+                       insertToken.setClientPutter(clientPutter);
+               } catch (InsertException ie1) {
+                       throw new SoneException(Type.INSERT_FAILED, "Could not start image insert.", ie1);
+               }
+       }
+
+       /**
         * Inserts a directory into Freenet.
         *
         * @param insertUri
@@ -280,4 +324,148 @@ public class FreenetInterface {
 
        }
 
+       /**
+        * Insert token that can be used to add {@link ImageInsertListener}s and
+        * cancel a running insert.
+        *
+        * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+        */
+       public class InsertToken implements ClientPutCallback {
+
+               /** The image being inserted. */
+               private final Image image;
+
+               /** The list of registered image insert listeners. */
+               private final List<ImageInsertListener> imageInsertListeners = Collections.synchronizedList(new ArrayList<ImageInsertListener>());
+
+               /** The client putter. */
+               private ClientPutter clientPutter;
+
+               /** The final URI. */
+               private volatile FreenetURI resultingUri;
+
+               /**
+                * Creates a new insert token for the given image.
+                *
+                * @param image
+                *            The image being inserted
+                */
+               public InsertToken(Image image) {
+                       this.image = image;
+               }
+
+               //
+               // LISTENER MANAGEMENT
+               //
+
+               /**
+                * Adds the given listener to the list of registered listener.
+                *
+                * @param imageInsertListener
+                *            The listener to add
+                */
+               public void addImageInsertListener(ImageInsertListener imageInsertListener) {
+                       imageInsertListeners.add(imageInsertListener);
+               }
+
+               /**
+                * Removes the given listener from the list of registered listener.
+                *
+                * @param imageInsertListener
+                *            The listener to remove
+                */
+               public void removeImageInsertListener(ImageInsertListener imageInsertListener) {
+                       imageInsertListeners.remove(imageInsertListener);
+               }
+
+               //
+               // ACCESSORS
+               //
+
+               /**
+                * Sets the client putter that is inserting the image. This will also
+                * signal all registered listeners that the image has started.
+                *
+                * @see ImageInsertListener#imageInsertStarted(Image)
+                * @param clientPutter
+                *            The client putter
+                */
+               public void setClientPutter(ClientPutter clientPutter) {
+                       this.clientPutter = clientPutter;
+                       for (ImageInsertListener imageInsertListener : imageInsertListeners) {
+                               imageInsertListener.imageInsertStarted(image);
+                       }
+               }
+
+               //
+               // ACTIONS
+               //
+
+               /**
+                * Cancels the running insert.
+                *
+                * @see ImageInsertListener#imageInsertAborted(Image)
+                */
+               @SuppressWarnings("synthetic-access")
+               public void cancel() {
+                       clientPutter.cancel(null, node.clientCore.clientContext);
+                       for (ImageInsertListener imageInsertListener : imageInsertListeners) {
+                               imageInsertListener.imageInsertAborted(image);
+                       }
+               }
+
+               //
+               // INTERFACE ClientPutCallback
+               //
+
+               /**
+                * {@inheritDoc}
+                */
+               @Override
+               public void onMajorProgress(ObjectContainer objectContainer) {
+                       /* ignore, we don’t care. */
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               @Override
+               public void onFailure(InsertException insertException, BaseClientPutter clientPutter, ObjectContainer objectContainer) {
+                       for (ImageInsertListener imageInsertListener : imageInsertListeners) {
+                               if ((insertException != null) && ("Cancelled by user".equals(insertException.getMessage()))) {
+                                       imageInsertListener.imageInsertAborted(image);
+                               } else {
+                                       imageInsertListener.imageInsertFailed(image, insertException);
+                               }
+                       }
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               @Override
+               public void onFetchable(BaseClientPutter clientPutter, ObjectContainer objectContainer) {
+                       /* ignore, we don’t care. */
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               @Override
+               public void onGeneratedURI(FreenetURI generatedUri, BaseClientPutter clientPutter, ObjectContainer objectContainer) {
+                       resultingUri = generatedUri;
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               @Override
+               public void onSuccess(BaseClientPutter clientPutter, ObjectContainer objectContainer) {
+                       for (ImageInsertListener imageInsertListener : imageInsertListeners) {
+                               imageInsertListener.imageInsertFinished(image, resultingUri);
+                       }
+               }
+
+       }
+
 }
diff --git a/src/main/java/net/pterodactylus/sone/core/ImageInsertListener.java b/src/main/java/net/pterodactylus/sone/core/ImageInsertListener.java
new file mode 100644 (file)
index 0000000..a0e8a56
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+ * Sone - ImageInsertListener.java - Copyright © 2011 David Roden
+ *
+ * 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 3 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, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.core;
+
+import java.util.EventListener;
+
+import net.pterodactylus.sone.core.FreenetInterface.InsertToken;
+import net.pterodactylus.sone.data.Image;
+import freenet.keys.FreenetURI;
+
+/**
+ * Listener interface for objects that want to be notified about the status of
+ * an image insert.
+ *
+ * @see ImageInserter#insertImage(net.pterodactylus.sone.data.TemporaryImage,
+ *      Image)
+ * @see FreenetInterface#insertImage(net.pterodactylus.sone.data.TemporaryImage,
+ *      Image, net.pterodactylus.sone.core.FreenetInterface.InsertToken)
+ * @see InsertToken
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface ImageInsertListener extends EventListener {
+
+       /**
+        * Notifies a listener that the insert of the given image started.
+        *
+        * @param image
+        *            The image that is being inserted
+        */
+       public void imageInsertStarted(Image image);
+
+       /**
+        * Notifies a listener that the insert of the given image was aborted by the
+        * user.
+        *
+        * @param image
+        *            The image that is no longer being inserted
+        */
+       public void imageInsertAborted(Image image);
+
+       /**
+        * Notifies a listener that the given image was inserted successfully.
+        *
+        * @param image
+        *            The image that was inserted
+        * @param key
+        *            The final key of the image
+        */
+       public void imageInsertFinished(Image image, FreenetURI key);
+
+       /**
+        * Notifies a listener that the given image could not be inserted.
+        *
+        * @param image
+        *            The image that could not be inserted
+        * @param cause
+        *            The cause of the insertion failure
+        */
+       public void imageInsertFailed(Image image, Throwable cause);
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/core/ImageInserter.java b/src/main/java/net/pterodactylus/sone/core/ImageInserter.java
new file mode 100644 (file)
index 0000000..b4421ce
--- /dev/null
@@ -0,0 +1,104 @@
+/*
+ * Sone - ImageInserter.java - Copyright © 2011 David Roden
+ *
+ * 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 3 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, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.core;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import net.pterodactylus.sone.core.FreenetInterface.InsertToken;
+import net.pterodactylus.sone.data.Image;
+import net.pterodactylus.sone.data.TemporaryImage;
+import net.pterodactylus.util.logging.Logging;
+import net.pterodactylus.util.validation.Validation;
+
+/**
+ * The image inserter is responsible for inserting images using
+ * {@link FreenetInterface#insertImage(TemporaryImage, Image, InsertToken)} and
+ * also tracks running inserts, giving the possibility to abort a running
+ * insert.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class ImageInserter {
+
+       /** The logger. */
+       private static final Logger logger = Logging.getLogger(ImageInserter.class);
+
+       /** The core. */
+       private final Core core;
+
+       /** The freenet interface. */
+       private final FreenetInterface freenetInterface;
+
+       /** The tokens of running inserts. */
+       private final Map<String, InsertToken> insertTokens = Collections.synchronizedMap(new HashMap<String, InsertToken>());
+
+       /**
+        * Creates a new image inserter.
+        *
+        * @param core
+        *            The Sone core
+        * @param freenetInterface
+        *            The freenet interface
+        */
+       public ImageInserter(Core core, FreenetInterface freenetInterface) {
+               this.core = core;
+               this.freenetInterface = freenetInterface;
+       }
+
+       /**
+        * Inserts the given image. The {@link #core} will automatically added as
+        * {@link ImageInsertListener} to the created {@link InsertToken}.
+        *
+        * @param temporaryImage
+        *            The temporary image data
+        * @param image
+        *            The image
+        */
+       public void insertImage(TemporaryImage temporaryImage, Image image) {
+               Validation.begin().isNotNull("Temporary Image", temporaryImage).isNotNull("Image", image).check().isEqual("Image IDs", image.getId(), temporaryImage.getId()).check();
+               try {
+                       InsertToken insertToken = freenetInterface.new InsertToken(image);
+                       insertTokens.put(image.getId(), insertToken);
+                       insertToken.addImageInsertListener(core);
+                       freenetInterface.insertImage(temporaryImage, image, insertToken);
+               } catch (SoneException se1) {
+                       logger.log(Level.WARNING, "Could not insert image!", se1);
+               }
+       }
+
+       /**
+        * Cancels a running image insert. If no insert is running for the given
+        * image, nothing happens.
+        *
+        * @param image
+        *            The image being inserted
+        */
+       public void cancelImageInsert(Image image) {
+               InsertToken insertToken = insertTokens.remove(image.getId());
+               if (insertToken == null) {
+                       return;
+               }
+               insertToken.cancel();
+               insertToken.removeImageInsertListener(core);
+       }
+
+}
index 6a945b9..1bdf21f 100644 (file)
@@ -194,7 +194,7 @@ public class Options {
                 * {@inheritDoc}
                 */
                @Override
-               public boolean validate(@SuppressWarnings("hiding") T value) {
+               public boolean validate(T value) {
                        return (validator == null) || (value == null) || validator.validate(value);
                }
 
index 0812f66..e8d452b 100644 (file)
 
 package net.pterodactylus.sone.core;
 
-import java.io.IOException;
 import java.io.InputStream;
 import java.net.MalformedURLException;
+import java.util.ArrayList;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Set;
 import java.util.logging.Level;
 import java.util.logging.Logger;
 
 import net.pterodactylus.sone.core.Core.SoneStatus;
+import net.pterodactylus.sone.data.Album;
 import net.pterodactylus.sone.data.Client;
+import net.pterodactylus.sone.data.Image;
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Profile;
 import net.pterodactylus.sone.data.Reply;
@@ -198,8 +201,8 @@ public class SoneDownloader extends AbstractService {
                                }
                        }
                        return parsedSone;
-               } catch (IOException ioe1) {
-                       logger.log(Level.WARNING, "Could not parse Sone from " + requestUri + "!", ioe1);
+               } catch (Exception e1) {
+                       logger.log(Level.WARNING, "Could not parse Sone from " + requestUri + "!", e1);
                } finally {
                        Closer.close(soneInputStream);
                        soneBucket.free();
@@ -431,6 +434,64 @@ public class SoneDownloader extends AbstractService {
                        }
                }
 
+               /* parse albums. */
+               SimpleXML albumsXml = soneXml.getNode("albums");
+               List<Album> topLevelAlbums = new ArrayList<Album>();
+               if (albumsXml != null) {
+                       for (SimpleXML albumXml : albumsXml.getNodes("album")) {
+                               String id = albumXml.getValue("id", null);
+                               String parentId = albumXml.getValue("parent", null);
+                               String title = albumXml.getValue("title", null);
+                               String description = albumXml.getValue("description", null);
+                               String albumImageId = albumXml.getValue("album-image", null);
+                               if ((id == null) || (title == null) || (description == null)) {
+                                       logger.log(Level.WARNING, "Downloaded Sone %s contains invalid album!", new Object[] { sone });
+                                       return null;
+                               }
+                               Album parent = null;
+                               if (parentId != null) {
+                                       parent = core.getAlbum(parentId, false);
+                                       if (parent == null) {
+                                               logger.log(Level.WARNING, "Downloaded Sone %s has album with invalid parent!", new Object[] { sone });
+                                               return null;
+                                       }
+                               }
+                               Album album = core.getAlbum(id).setSone(sone).setTitle(title).setDescription(description).setAlbumImage(albumImageId);
+                               if (parent != null) {
+                                       parent.addAlbum(album);
+                               } else {
+                                       topLevelAlbums.add(album);
+                               }
+                               SimpleXML imagesXml = albumXml.getNode("images");
+                               if (imagesXml != null) {
+                                       for (SimpleXML imageXml : imagesXml.getNodes("image")) {
+                                               String imageId = imageXml.getValue("id", null);
+                                               String imageCreationTimeString = imageXml.getValue("creation-time", null);
+                                               String imageKey = imageXml.getValue("key", null);
+                                               String imageTitle = imageXml.getValue("title", null);
+                                               String imageDescription = imageXml.getValue("description", "");
+                                               String imageWidthString = imageXml.getValue("width", null);
+                                               String imageHeightString = imageXml.getValue("height", null);
+                                               if ((imageId == null) || (imageCreationTimeString == null) || (imageKey == null) || (imageTitle == null) || (imageWidthString == null) || (imageHeightString == null)) {
+                                                       logger.log(Level.WARNING, "Downloaded Sone %s contains invalid images!", new Object[] { sone });
+                                                       return null;
+                                               }
+                                               long creationTime = Numbers.safeParseLong(imageCreationTimeString, 0L);
+                                               int imageWidth = Numbers.safeParseInteger(imageWidthString, 0);
+                                               int imageHeight = Numbers.safeParseInteger(imageHeightString, 0);
+                                               if ((imageWidth < 1) || (imageHeight < 1)) {
+                                                       logger.log(Level.WARNING, "Downloaded Sone %s contains image %s with invalid dimensions (%s, %s)!", new Object[] { sone, imageId, imageWidthString, imageHeightString });
+                                                       return null;
+                                               }
+                                               Image image = core.getImage(imageId).setSone(sone).setKey(imageKey).setCreationTime(creationTime);
+                                               image.setTitle(imageTitle).setDescription(imageDescription);
+                                               image.setWidth(imageWidth).setHeight(imageHeight);
+                                               album.addImage(image);
+                                       }
+                               }
+                       }
+               }
+
                /* okay, apparently everything was parsed correctly. Now import. */
                /* atomic setter operation on the Sone. */
                synchronized (sone) {
@@ -439,6 +500,7 @@ public class SoneDownloader extends AbstractService {
                        sone.setReplies(replies);
                        sone.setLikePostIds(likedPostIds);
                        sone.setLikeReplyIds(likedReplyIds);
+                       sone.setAlbums(topLevelAlbums);
                }
 
                return sone;
index c786661..271627e 100644 (file)
@@ -37,6 +37,9 @@ public class SoneException extends Exception {
                /** An invalid URI was specified. */
                INVALID_URI,
 
+               /** An insert failed. */
+               INSERT_FAILED,
+
        }
 
        /** The type of the exception. */
index f654d09..44f8bbc 100644 (file)
@@ -302,6 +302,7 @@ public class SoneInserter extends AbstractService {
                        soneProperties.put("replies", new ListBuilder<Reply>(new ArrayList<Reply>(sone.getReplies())).sort(new ReverseComparator<Reply>(Reply.TIME_COMPARATOR)).get());
                        soneProperties.put("likedPostIds", new HashSet<String>(sone.getLikedPostIds()));
                        soneProperties.put("likedReplyIds", new HashSet<String>(sone.getLikedReplyIds()));
+                       soneProperties.put("albums", Sone.flattenAlbums(sone.getAlbums()));
                }
 
                //
diff --git a/src/main/java/net/pterodactylus/sone/data/Album.java b/src/main/java/net/pterodactylus/sone/data/Album.java
new file mode 100644 (file)
index 0000000..b15bc00
--- /dev/null
@@ -0,0 +1,464 @@
+/*
+ * Sone - Album.java - Copyright © 2011 David Roden
+ *
+ * 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 3 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, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.data;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+import net.pterodactylus.util.collection.Mapper;
+import net.pterodactylus.util.collection.Mappers;
+import net.pterodactylus.util.object.Default;
+import net.pterodactylus.util.validation.Validation;
+
+/**
+ * Container for images that can also contain nested {@link Album}s.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class Album implements Fingerprintable {
+
+       /** The ID of this album. */
+       private final String id;
+
+       /** The Sone this album belongs to. */
+       private Sone sone;
+
+       /** Nested albums. */
+       private final List<Album> albums = new ArrayList<Album>();
+
+       /** The image IDs in order. */
+       private final List<String> imageIds = new ArrayList<String>();
+
+       /** The images in this album. */
+       private final Map<String, Image> images = new HashMap<String, Image>();
+
+       /** The parent album. */
+       private Album parent;
+
+       /** The title of this album. */
+       private String title;
+
+       /** The description of this album. */
+       private String description;
+
+       /** The ID of the album picture. */
+       private String albumImage;
+
+       /**
+        * Creates a new album with a random ID.
+        */
+       public Album() {
+               this(UUID.randomUUID().toString());
+       }
+
+       /**
+        * Creates a new album with the given ID.
+        *
+        * @param id
+        *            The ID of the album
+        */
+       public Album(String id) {
+               Validation.begin().isNotNull("Album ID", id).check();
+               this.id = id;
+       }
+
+       //
+       // ACCESSORS
+       //
+
+       /**
+        * Returns the ID of this album.
+        *
+        * @return The ID of this album
+        */
+       public String getId() {
+               return id;
+       }
+
+       /**
+        * Returns the Sone this album belongs to.
+        *
+        * @return The Sone this album belongs to
+        */
+       public Sone getSone() {
+               return sone;
+       }
+
+       /**
+        * Sets the owner of the album. The owner can only be set as long as the
+        * current owner is {@code null}.
+        *
+        * @param sone
+        *            The album owner
+        * @return This album
+        */
+       public Album setSone(Sone sone) {
+               Validation.begin().isNotNull("New Album Owner", sone).isEither("Old Album Owner", this.sone, null, sone).check();
+               this.sone = sone;
+               return this;
+       }
+
+       /**
+        * Returns the nested albums.
+        *
+        * @return The nested albums
+        */
+       public List<Album> getAlbums() {
+               return new ArrayList<Album>(albums);
+       }
+
+       /**
+        * Adds an album to this album.
+        *
+        * @param album
+        *            The album to add
+        */
+       public void addAlbum(Album album) {
+               Validation.begin().isNotNull("Album", album).check().isEqual("Album Owner", album.sone, sone).isEither("Old Album Parent", this.parent, null, album.parent).check();
+               album.setParent(this);
+               if (!albums.contains(album)) {
+                       albums.add(album);
+               }
+       }
+
+       /**
+        * Removes an album from this album.
+        *
+        * @param album
+        *            The album to remove
+        */
+       public void removeAlbum(Album album) {
+               Validation.begin().isNotNull("Album", album).check().isEqual("Album Owner", album.sone, sone).isEqual("Album Parent", album.parent, this).check();
+               albums.remove(album);
+               album.removeParent();
+       }
+
+       /**
+        * Moves the given album up in this album’s albums. If the album is already
+        * the first album, nothing happens.
+        *
+        * @param album
+        *            The album to move up
+        * @return The album that the given album swapped the place with, or
+        *         <code>null</code> if the album did not change its place
+        */
+       public Album moveAlbumUp(Album album) {
+               Validation.begin().isNotNull("Album", album).check().isEqual("Album Owner", album.sone, sone).isEqual("Album Parent", album.parent, this).check();
+               int oldIndex = albums.indexOf(album);
+               if (oldIndex <= 0) {
+                       return null;
+               }
+               albums.remove(oldIndex);
+               albums.add(oldIndex - 1, album);
+               return albums.get(oldIndex);
+       }
+
+       /**
+        * Moves the given album down in this album’s albums. If the album is
+        * already the last album, nothing happens.
+        *
+        * @param album
+        *            The album to move down
+        * @return The album that the given album swapped the place with, or
+        *         <code>null</code> if the album did not change its place
+        */
+       public Album moveAlbumDown(Album album) {
+               Validation.begin().isNotNull("Album", album).check().isEqual("Album Owner", album.sone, sone).isEqual("Album Parent", album.parent, this).check();
+               int oldIndex = albums.indexOf(album);
+               if ((oldIndex < 0) || (oldIndex >= (albums.size() - 1))) {
+                       return null;
+               }
+               albums.remove(oldIndex);
+               albums.add(oldIndex + 1, album);
+               return albums.get(oldIndex);
+       }
+
+       /**
+        * Returns the images in this album.
+        *
+        * @return The images in this album
+        */
+       public List<Image> getImages() {
+               return Mappers.mappedList(imageIds, new Mapper<String, Image>() {
+
+                       @Override
+                       @SuppressWarnings("synthetic-access")
+                       public Image map(String imageId) {
+                               return images.get(imageId);
+                       }
+
+               });
+       }
+
+       /**
+        * Adds the given image to this album.
+        *
+        * @param image
+        *            The image to add
+        */
+       public void addImage(Image image) {
+               Validation.begin().isNotNull("Image", image).check().isNotNull("Image Owner", image.getSone()).check().isEqual("Image Owner", image.getSone(), sone).check();
+               if (image.getAlbum() != null) {
+                       image.getAlbum().removeImage(image);
+               }
+               image.setAlbum(this);
+               if (imageIds.isEmpty() && (albumImage == null)) {
+                       albumImage = image.getId();
+               }
+               if (!imageIds.contains(image.getId())) {
+                       imageIds.add(image.getId());
+                       images.put(image.getId(), image);
+               }
+       }
+
+       /**
+        * Removes the given image from this album.
+        *
+        * @param image
+        *            The image to remove
+        */
+       public void removeImage(Image image) {
+               Validation.begin().isNotNull("Image", image).check().isEqual("Image Owner", image.getSone(), sone).check();
+               imageIds.remove(image.getId());
+               images.remove(image.getId());
+               if (image.getId().equals(albumImage)) {
+                       if (images.isEmpty()) {
+                               albumImage = null;
+                       } else {
+                               albumImage = images.values().iterator().next().getId();
+                       }
+               }
+       }
+
+       /**
+        * Moves the given image up in this album’s images. If the image is already
+        * the first image, nothing happens.
+        *
+        * @param image
+        *            The image to move up
+        * @return The image that the given image swapped the place with, or
+        *         <code>null</code> if the image did not change its place
+        */
+       public Image moveImageUp(Image image) {
+               Validation.begin().isNotNull("Image", image).check().isEqual("Image Album", image.getAlbum(), this).isEqual("Album Owner", image.getAlbum().getSone(), sone).check();
+               int oldIndex = imageIds.indexOf(image.getId());
+               if (oldIndex <= 0) {
+                       return null;
+               }
+               imageIds.remove(image.getId());
+               imageIds.add(oldIndex - 1, image.getId());
+               return images.get(imageIds.get(oldIndex));
+       }
+
+       /**
+        * Moves the given image down in this album’s images. If the image is
+        * already the last image, nothing happens.
+        *
+        * @param image
+        *            The image to move down
+        * @return The image that the given image swapped the place with, or
+        *         <code>null</code> if the image did not change its place
+        */
+       public Image moveImageDown(Image image) {
+               Validation.begin().isNotNull("Image", image).check().isEqual("Image Album", image.getAlbum(), this).isEqual("Album Owner", image.getAlbum().getSone(), sone).check();
+               int oldIndex = imageIds.indexOf(image.getId());
+               if ((oldIndex == -1) || (oldIndex >= (imageIds.size() - 1))) {
+                       return null;
+               }
+               imageIds.remove(image.getId());
+               imageIds.add(oldIndex + 1, image.getId());
+               return images.get(imageIds.get(oldIndex));
+       }
+
+       /**
+        * Returns the album image of this album, or {@code null} if no album image
+        * has been set.
+        *
+        * @return The image to show when this album is listed
+        */
+       public Image getAlbumImage() {
+               if (albumImage == null) {
+                       return null;
+               }
+               return Default.forNull(images.get(albumImage), images.values().iterator().next());
+       }
+
+       /**
+        * Sets the ID of the album image.
+        *
+        * @param id
+        *            The ID of the album image
+        * @return This album
+        */
+       public Album setAlbumImage(String id) {
+               this.albumImage = id;
+               return this;
+       }
+
+       /**
+        * Returns whether this album contains any other albums or images.
+        *
+        * @return {@code true} if this album is empty, {@code false} otherwise
+        */
+       public boolean isEmpty() {
+               return albums.isEmpty() && images.isEmpty();
+       }
+
+       /**
+        * Returns the parent album of this album.
+        *
+        * @return The parent album of this album, or {@code null} if this album
+        *         does not have a parent
+        */
+       public Album getParent() {
+               return parent;
+       }
+
+       /**
+        * Sets the parent album of this album.
+        *
+        * @param parent
+        *            The new parent album of this album
+        * @return This album
+        */
+       protected Album setParent(Album parent) {
+               Validation.begin().isNotNull("Album Parent", parent).check();
+               this.parent = parent;
+               return this;
+       }
+
+       /**
+        * Removes the parent album of this album.
+        *
+        * @return This album
+        */
+       protected Album removeParent() {
+               this.parent = null;
+               return this;
+       }
+
+       /**
+        * Returns the title of this album.
+        *
+        * @return The title of this album
+        */
+       public String getTitle() {
+               return title;
+       }
+
+       /**
+        * Sets the title of this album.
+        *
+        * @param title
+        *            The title of this album
+        * @return This album
+        */
+       public Album setTitle(String title) {
+               Validation.begin().isNotNull("Album Title", title).check();
+               this.title = title;
+               return this;
+       }
+
+       /**
+        * Returns the description of this album.
+        *
+        * @return The description of this album
+        */
+       public String getDescription() {
+               return description;
+       }
+
+       /**
+        * Sets the description of this album.
+        *
+        * @param description
+        *            The description of this album
+        * @return This album
+        */
+       public Album setDescription(String description) {
+               Validation.begin().isNotNull("Album Description", description).check();
+               this.description = description;
+               return this;
+       }
+
+       //
+       // FINGERPRINTABLE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public String getFingerprint() {
+               StringBuilder fingerprint = new StringBuilder();
+               fingerprint.append("Album(");
+               fingerprint.append("ID(").append(id).append(')');
+               fingerprint.append("Title(").append(title).append(')');
+               fingerprint.append("Description(").append(description).append(')');
+               if (albumImage != null) {
+                       fingerprint.append("AlbumImage(").append(albumImage).append(')');
+               }
+
+               /* add nested albums. */
+               fingerprint.append("Albums(");
+               for (Album album : albums) {
+                       fingerprint.append(album.getFingerprint());
+               }
+               fingerprint.append(')');
+
+               /* add images. */
+               fingerprint.append("Images(");
+               for (Image image : getImages()) {
+                       if (image.isInserted()) {
+                               fingerprint.append(image.getFingerprint());
+                       }
+               }
+               fingerprint.append(')');
+
+               fingerprint.append(')');
+               return fingerprint.toString();
+       }
+
+       //
+       // OBJECT METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public int hashCode() {
+               return id.hashCode();
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public boolean equals(Object object) {
+               if (!(object instanceof Album)) {
+                       return false;
+               }
+               Album album = (Album) object;
+               return id.equals(album.id);
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/data/Image.java b/src/main/java/net/pterodactylus/sone/data/Image.java
new file mode 100644 (file)
index 0000000..04eb349
--- /dev/null
@@ -0,0 +1,326 @@
+/*
+ * Sone - Image.java - Copyright © 2011 David Roden
+ *
+ * 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 3 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, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.data;
+
+import java.util.UUID;
+
+import net.pterodactylus.util.validation.Validation;
+
+/**
+ * Container for image metadata.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class Image implements Fingerprintable {
+
+       /** The ID of the image. */
+       private final String id;
+
+       /** The Sone the image belongs to. */
+       private Sone sone;
+
+       /** The album this image belongs to. */
+       private Album album;
+
+       /** The request key of the image. */
+       private String key;
+
+       /** The creation time of the image. */
+       private long creationTime;
+
+       /** The width of the image. */
+       private int width;
+
+       /** The height of the image. */
+       private int height;
+
+       /** The title of the image. */
+       private String title;
+
+       /** The description of the image. */
+       private String description;
+
+       /**
+        * Creates a new image with a random ID.
+        */
+       public Image() {
+               this(UUID.randomUUID().toString());
+               setCreationTime(System.currentTimeMillis());
+       }
+
+       /**
+        * Creates a new image.
+        *
+        * @param id
+        *            The ID of the image
+        */
+       public Image(String id) {
+               Validation.begin().isNotNull("Image ID", id).check();
+               this.id = id;
+       }
+
+       //
+       // ACCESSORS
+       //
+
+       /**
+        * Returns the ID of this image.
+        *
+        * @return The ID of this image
+        */
+       public String getId() {
+               return id;
+       }
+
+       /**
+        * Returns the Sone this image belongs to.
+        *
+        * @return The Sone this image belongs to
+        */
+       public Sone getSone() {
+               return sone;
+       }
+
+       /**
+        * Sets the owner of this image. The owner can only be set if no owner has
+        * yet been set.
+        *
+        * @param sone
+        *            The new owner of this image
+        * @return This image
+        */
+       public Image setSone(Sone sone) {
+               Validation.begin().isNotNull("New Image Owner", sone).isEither("Old Image Owner", this.sone, null, sone).check();
+               this.sone = sone;
+               return this;
+       }
+
+       /**
+        * Returns the album this image belongs to.
+        *
+        * @return The album this image belongs to
+        */
+       public Album getAlbum() {
+               return album;
+       }
+
+       /**
+        * Sets the album this image belongs to. The album of an image can only be
+        * set once, and it is usually called by {@link Album#addImage(Image)}.
+        *
+        * @param album
+        *            The album this image belongs to
+        * @return This image
+        */
+       public Image setAlbum(Album album) {
+               Validation.begin().isNotNull("New Album", album).check().isEqual("Album Owner and Image Owner", album.getSone(), getSone()).check();
+               this.album = album;
+               return this;
+       }
+
+       /**
+        * Returns the request key of this image.
+        *
+        * @return The request key of this image
+        */
+       public String getKey() {
+               return key;
+       }
+
+       /**
+        * Sets the request key of this image. The request key can only be set as
+        * long as no request key has yet been set.
+        *
+        * @param key
+        *            The new request key of this image
+        * @return This image
+        */
+       public Image setKey(String key) {
+               Validation.begin().isNotNull("New Image Key", key).isEither("Old Image Key", this.key, null, key).check();
+               this.key = key;
+               return this;
+       }
+
+       /**
+        * Returns whether the image has already been inserted. An image is
+        * considered as having been inserted it its {@link #getKey() key} is not
+        * {@code null}.
+        *
+        * @return {@code true} if there is a key for this image, {@code false}
+        *         otherwise
+        */
+       public boolean isInserted() {
+               return key != null;
+       }
+
+       /**
+        * Returns the creation time of this image.
+        *
+        * @return The creation time of this image (in milliseconds since 1970, Jan
+        *         1, UTC)
+        */
+       public long getCreationTime() {
+               return creationTime;
+       }
+
+       /**
+        * Sets the new creation time of this image. The creation time can only be
+        * set as long as no creation time has been set yet.
+        *
+        * @param creationTime
+        *            The new creation time of this image
+        * @return This image
+        */
+       public Image setCreationTime(long creationTime) {
+               Validation.begin().isGreater("New Image Creation Time", creationTime, 0).isEither("Old Image Creation Time", this.creationTime, 0L, creationTime).check();
+               this.creationTime = creationTime;
+               return this;
+       }
+
+       /**
+        * Returns the width of this image.
+        *
+        * @return The width of this image (in pixels)
+        */
+       public int getWidth() {
+               return width;
+       }
+
+       /**
+        * Sets the width of this image. The width can only be set as long as no
+        * width has been set yet.
+        *
+        * @param width
+        *            The new width of this image
+        * @return This image
+        */
+       public Image setWidth(int width) {
+               Validation.begin().isGreater("New Image Width", width, 0).isEither("Old Image Width", this.width, 0, width).check();
+               this.width = width;
+               return this;
+       }
+
+       /**
+        * Returns the height of this image.
+        *
+        * @return The height of this image (in pixels)
+        */
+       public int getHeight() {
+               return height;
+       }
+
+       /**
+        * Sets the new height of this image. The height can only be set as long as
+        * no height has yet been set.
+        *
+        * @param height
+        *            The new height of this image
+        * @return This image
+        */
+       public Image setHeight(int height) {
+               Validation.begin().isGreater("New Image Height", height, 0).isEither("Old Image Height", this.height, 0, height).check();
+               this.height = height;
+               return this;
+       }
+
+       /**
+        * Returns the title of this image.
+        *
+        * @return The title of this image
+        */
+       public String getTitle() {
+               return title;
+       }
+
+       /**
+        * Sets the title of this image.
+        *
+        * @param title
+        *            The title of this image
+        * @return This image
+        */
+       public Image setTitle(String title) {
+               Validation.begin().isNotNull("Image Title", title).check();
+               this.title = title;
+               return this;
+       }
+
+       /**
+        * Returns the description of this image.
+        *
+        * @return The description of this image
+        */
+       public String getDescription() {
+               return description;
+       }
+
+       /**
+        * Sets the description of this image.
+        *
+        * @param description
+        *            The description of this image
+        * @return This image
+        */
+       public Image setDescription(String description) {
+               Validation.begin().isNotNull("Image Description", description).check();
+               this.description = description;
+               return this;
+       }
+
+       //
+       // FINGERPRINTABLE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public String getFingerprint() {
+               StringBuilder fingerprint = new StringBuilder();
+               fingerprint.append("Image(");
+               fingerprint.append("ID(").append(id).append(')');
+               fingerprint.append("Title(").append(title).append(')');
+               fingerprint.append("Description(").append(description).append(')');
+               fingerprint.append(')');
+               return fingerprint.toString();
+       }
+
+       //
+       // OBJECT METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public int hashCode() {
+               return id.hashCode();
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public boolean equals(Object object) {
+               if (!(object instanceof Image)) {
+                       return false;
+               }
+               return ((Image) object).id.equals(id);
+       }
+
+}
index fa3d78f..e124387 100644 (file)
@@ -34,6 +34,7 @@ import net.pterodactylus.sone.freenet.wot.OwnIdentity;
 import net.pterodactylus.sone.template.SoneAccessor;
 import net.pterodactylus.util.filter.Filter;
 import net.pterodactylus.util.logging.Logging;
+import net.pterodactylus.util.validation.Validation;
 import freenet.keys.FreenetURI;
 
 /**
@@ -145,6 +146,9 @@ public class Sone implements Fingerprintable, Comparable<Sone> {
        /** The IDs of all liked replies. */
        private final Set<String> likedReplyIds = Collections.synchronizedSet(new HashSet<String>());
 
+       /** The albums of this Sone. */
+       private final List<Album> albums = Collections.synchronizedList(new ArrayList<Album>());
+
        /** Sone-specific options. */
        private final Options options = new Options();
 
@@ -282,7 +286,7 @@ public class Sone implements Fingerprintable, Comparable<Sone> {
         */
        public void setLatestEdition(long latestEdition) {
                if (!(latestEdition > this.latestEdition)) {
-                       logger.log(Level.INFO, "New latest edition %d is not greater than current latest edition %d!", new Object[] { latestEdition, this.latestEdition });
+                       logger.log(Level.FINE, "New latest edition %d is not greater than current latest edition %d!", new Object[] { latestEdition, this.latestEdition });
                        return;
                }
                this.latestEdition = latestEdition;
@@ -632,6 +636,91 @@ public class Sone implements Fingerprintable, Comparable<Sone> {
        }
 
        /**
+        * Returns the albums of this Sone.
+        *
+        * @return The albums of this Sone
+        */
+       public List<Album> getAlbums() {
+               return Collections.unmodifiableList(albums);
+       }
+
+       /**
+        * Adds an album to this Sone.
+        *
+        * @param album
+        *            The album to add
+        */
+       public synchronized void addAlbum(Album album) {
+               Validation.begin().isNotNull("Album", album).check().isEqual("Album Owner", album.getSone(), this).check();
+               albums.add(album);
+       }
+
+       /**
+        * Sets the albums of this Sone.
+        *
+        * @param albums
+        *            The albums of this Sone
+        */
+       public synchronized void setAlbums(Collection<? extends Album> albums) {
+               Validation.begin().isNotNull("Albums", albums).check();
+               this.albums.clear();
+               for (Album album : albums) {
+                       addAlbum(album);
+               }
+       }
+
+       /**
+        * Removes an album from this Sone.
+        *
+        * @param album
+        *            The album to remove
+        */
+       public synchronized void removeAlbum(Album album) {
+               Validation.begin().isNotNull("Album", album).check().isEqual("Album Owner", album.getSone(), this).check();
+               albums.remove(album);
+       }
+
+       /**
+        * Moves the given album up in this album’s albums. If the album is already
+        * the first album, nothing happens.
+        *
+        * @param album
+        *            The album to move up
+        * @return The album that the given album swapped the place with, or
+        *         <code>null</code> if the album did not change its place
+        */
+       public Album moveAlbumUp(Album album) {
+               Validation.begin().isNotNull("Album", album).check().isEqual("Album Owner", album.getSone(), this).isNull("Album Parent", album.getParent()).check();
+               int oldIndex = albums.indexOf(album);
+               if (oldIndex <= 0) {
+                       return null;
+               }
+               albums.remove(oldIndex);
+               albums.add(oldIndex - 1, album);
+               return albums.get(oldIndex);
+       }
+
+       /**
+        * Moves the given album down in this album’s albums. If the album is
+        * already the last album, nothing happens.
+        *
+        * @param album
+        *            The album to move down
+        * @return The album that the given album swapped the place with, or
+        *         <code>null</code> if the album did not change its place
+        */
+       public Album moveAlbumDown(Album album) {
+               Validation.begin().isNotNull("Album", album).check().isEqual("Album Owner", album.getSone(), this).isNull("Album Parent", album.getParent()).check();
+               int oldIndex = albums.indexOf(album);
+               if ((oldIndex < 0) || (oldIndex >= (albums.size() - 1))) {
+                       return null;
+               }
+               albums.remove(oldIndex);
+               albums.add(oldIndex + 1, album);
+               return albums.get(oldIndex);
+       }
+
+       /**
         * Returns Sone-specific options.
         *
         * @return The options of this Sone
@@ -658,7 +747,6 @@ public class Sone implements Fingerprintable, Comparable<Sone> {
                }
                fingerprint.append(")");
 
-               @SuppressWarnings("hiding")
                List<Reply> replies = new ArrayList<Reply>(getReplies());
                Collections.sort(replies, Reply.TIME_COMPARATOR);
                fingerprint.append("Replies(");
@@ -667,7 +755,6 @@ public class Sone implements Fingerprintable, Comparable<Sone> {
                }
                fingerprint.append(')');
 
-               @SuppressWarnings("hiding")
                List<String> likedPostIds = new ArrayList<String>(getLikedPostIds());
                Collections.sort(likedPostIds);
                fingerprint.append("LikedPosts(");
@@ -676,7 +763,6 @@ public class Sone implements Fingerprintable, Comparable<Sone> {
                }
                fingerprint.append(')');
 
-               @SuppressWarnings("hiding")
                List<String> likedReplyIds = new ArrayList<String>(getLikedReplyIds());
                Collections.sort(likedReplyIds);
                fingerprint.append("LikedReplies(");
@@ -685,10 +771,43 @@ public class Sone implements Fingerprintable, Comparable<Sone> {
                }
                fingerprint.append(')');
 
+               fingerprint.append("Albums(");
+               for (Album album : albums) {
+                       fingerprint.append(album.getFingerprint());
+               }
+               fingerprint.append(')');
+
                return fingerprint.toString();
        }
 
        //
+       // STATIC METHODS
+       //
+
+       /**
+        * Flattens the given top-level albums so that the resulting list contains
+        * parent albums before child albums and the resulting list can be parsed in
+        * a single pass.
+        *
+        * @param albums
+        *            The albums to flatten
+        * @return The flattened albums
+        */
+       public static List<Album> flattenAlbums(Collection<? extends Album> albums) {
+               List<Album> flatAlbums = new ArrayList<Album>();
+               flatAlbums.addAll(albums);
+               int lastAlbumIndex = 0;
+               while (lastAlbumIndex < flatAlbums.size()) {
+                       int previousAlbumCount = flatAlbums.size();
+                       for (Album album : new ArrayList<Album>(flatAlbums.subList(lastAlbumIndex, flatAlbums.size()))) {
+                               flatAlbums.addAll(album.getAlbums());
+                       }
+                       lastAlbumIndex = previousAlbumCount;
+               }
+               return flatAlbums;
+       }
+
+       //
        // INTERFACE Comparable<Sone>
        //
 
diff --git a/src/main/java/net/pterodactylus/sone/data/TemporaryImage.java b/src/main/java/net/pterodactylus/sone/data/TemporaryImage.java
new file mode 100644 (file)
index 0000000..ddac505
--- /dev/null
@@ -0,0 +1,113 @@
+/*
+ * Sone - TemporaryImage.java - Copyright © 2011 David Roden
+ *
+ * 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 3 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, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.data;
+
+import java.util.UUID;
+
+import net.pterodactylus.util.validation.Validation;
+
+/**
+ * A temporary image stores an uploaded image in memory until it has been
+ * inserted into Freenet and is subsequently loaded from there.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class TemporaryImage {
+
+       /** The ID of the temporary image. */
+       private final String id;
+
+       /** The MIME type of the image. */
+       private String mimeType;
+
+       /** The encoded image data. */
+       private byte[] imageData;
+
+       /**
+        * Creates a new temporary image with a random ID.
+        */
+       public TemporaryImage() {
+               this(UUID.randomUUID().toString());
+       }
+
+       /**
+        * Creates a new temporary image.
+        *
+        * @param id
+        *            The ID of the temporary image
+        */
+       public TemporaryImage(String id) {
+               this.id = id;
+       }
+
+       /**
+        * Returns the ID of the temporary image.
+        *
+        * @return The ID of the temporary image
+        */
+       public String getId() {
+               return id;
+       }
+
+       /**
+        * Returns the MIME type of the image.
+        *
+        * @return The MIME type of the image
+        */
+       public String getMimeType() {
+               return mimeType;
+       }
+
+       /**
+        * Sets the MIME type of the image. The MIME type can only be set once and
+        * it must not be {@code null}.
+        *
+        * @param mimeType
+        *            The MIME type of the image
+        * @return This temporary image
+        */
+       public TemporaryImage setMimeType(String mimeType) {
+               Validation.begin().isNotNull("MIME Type", mimeType).isNull("Previous MIME Type", this.mimeType).check();
+               this.mimeType = mimeType;
+               return this;
+       }
+
+       /**
+        * Returns the encoded image data.
+        *
+        * @return The encoded image data
+        */
+       public byte[] getImageData() {
+               return imageData;
+       }
+
+       /**
+        * Sets the encoded image data. The encoded image data can only be set once
+        * and it must not be {@code null}.
+        *
+        * @param imageData
+        *            The encoded image data
+        * @return This temporary image
+        */
+       public TemporaryImage setImageData(byte[] imageData) {
+               Validation.begin().isNotNull("Image Data", imageData).isNull("Previous Image Data", this.imageData).check();
+               this.imageData = imageData;
+               return this;
+       }
+
+}
index 37d049a..1d57454 100644 (file)
@@ -67,7 +67,7 @@ public class SimpleFieldSetBuilder {
         *            The simple field set to copy
         * @return This simple field set builder
         */
-       public SimpleFieldSetBuilder put(@SuppressWarnings("hiding") SimpleFieldSet simpleFieldSet) {
+       public SimpleFieldSetBuilder put(SimpleFieldSet simpleFieldSet) {
                this.simpleFieldSet.putAllOverwrite(simpleFieldSet);
                return this;
        }
index 803bc32..3c97e55 100644 (file)
@@ -178,7 +178,6 @@ public class IdentityManager extends AbstractService {
                Map<OwnIdentity, Map<String, Identity>> oldIdentities = Collections.emptyMap();
                while (!shouldStop()) {
                        Map<OwnIdentity, Map<String, Identity>> currentIdentities = new HashMap<OwnIdentity, Map<String, Identity>>();
-                       @SuppressWarnings("hiding")
                        Map<String, OwnIdentity> currentOwnIdentities = new HashMap<String, OwnIdentity>();
 
                        Set<OwnIdentity> ownIdentities = null;
index b4ea53d..de30a5d 100644 (file)
@@ -70,6 +70,13 @@ public class WebOfTrustConnector implements ConnectorListener {
        //
 
        /**
+        * Stops the web of trust connector.
+        */
+       public void stop() {
+               pluginConnector.removeConnectorListener(WOT_PLUGIN_NAME, PLUGIN_CONNECTION_IDENTIFIER, this);
+       }
+
+       /**
         * Loads all own identities from the Web of Trust plugin.
         *
         * @return All own identity
@@ -77,7 +84,6 @@ public class WebOfTrustConnector implements ConnectorListener {
         *             if the own identities can not be loaded
         */
        public Set<OwnIdentity> loadAllOwnIdentities() throws WebOfTrustException {
-               @SuppressWarnings("hiding")
                Reply reply = performRequest(SimpleFieldSetConstructor.create().put("Message", "GetOwnIdentities").get());
                SimpleFieldSet fields = reply.getFields();
                int ownIdentityCounter = -1;
@@ -125,7 +131,6 @@ public class WebOfTrustConnector implements ConnectorListener {
         *             if an error occured talking to the Web of Trust plugin
         */
        public Set<Identity> loadTrustedIdentities(OwnIdentity ownIdentity, String context) throws PluginException {
-               @SuppressWarnings("hiding")
                Reply reply = performRequest(SimpleFieldSetConstructor.create().put("Message", "GetIdentitiesByScore").put("TreeOwner", ownIdentity.getId()).put("Selection", "+").put("Context", (context == null) ? "" : context).get());
                SimpleFieldSet fields = reply.getFields();
                Set<Identity> identities = new HashSet<Identity>();
@@ -185,7 +190,6 @@ public class WebOfTrustConnector implements ConnectorListener {
         *             if an error occured talking to the Web of Trust plugin
         */
        public String getProperty(Identity identity, String name) throws PluginException {
-               @SuppressWarnings("hiding")
                Reply reply = performRequest(SimpleFieldSetConstructor.create().put("Message", "GetProperty").put("Identity", identity.getId()).put("Property", name).get());
                return reply.getFields().get("Property");
        }
@@ -399,7 +403,7 @@ public class WebOfTrustConnector implements ConnectorListener {
         * {@inheritDoc}
         */
        @Override
-       public void receivedReply(@SuppressWarnings("hiding") PluginConnector pluginConnector, SimpleFieldSet fields, Bucket data) {
+       public void receivedReply(PluginConnector pluginConnector, SimpleFieldSet fields, Bucket data) {
                String messageName = fields.get("Message");
                logger.log(Level.FINEST, "Received Reply from Plugin: " + messageName);
                synchronized (reply) {
index 91fb547..d04c547 100644 (file)
@@ -103,6 +103,9 @@ public class SonePlugin implements FredPlugin, FredPluginFCP, FredPluginL10n, Fr
        /** The l10n helper. */
        private PluginL10n l10n;
 
+       /** The web of trust connector. */
+       private WebOfTrustConnector webOfTrustConnector;
+
        /** The identity manager. */
        private IdentityManager identityManager;
 
@@ -145,7 +148,7 @@ public class SonePlugin implements FredPlugin, FredPluginFCP, FredPluginL10n, Fr
         * {@inheritDoc}
         */
        @Override
-       public void runPlugin(@SuppressWarnings("hiding") PluginRespirator pluginRespirator) {
+       public void runPlugin(PluginRespirator pluginRespirator) {
                this.pluginRespirator = pluginRespirator;
 
                /* create a configuration. */
@@ -181,7 +184,7 @@ public class SonePlugin implements FredPlugin, FredPluginFCP, FredPluginL10n, Fr
 
                        /* create web of trust connector. */
                        PluginConnector pluginConnector = new PluginConnector(pluginRespirator);
-                       WebOfTrustConnector webOfTrustConnector = new WebOfTrustConnector(pluginConnector);
+                       webOfTrustConnector = new WebOfTrustConnector(pluginConnector);
                        identityManager = new IdentityManager(webOfTrustConnector);
                        identityManager.setContext("Sone");
 
@@ -236,6 +239,9 @@ public class SonePlugin implements FredPlugin, FredPluginFCP, FredPluginL10n, Fr
 
                        /* stop the identity manager. */
                        identityManager.stop();
+
+                       /* stop the web of trust connector. */
+                       webOfTrustConnector.stop();
                } catch (Throwable t1) {
                        logger.log(Level.SEVERE, "Error while shutting down!", t1);
                } finally {
diff --git a/src/main/java/net/pterodactylus/sone/template/AlbumAccessor.java b/src/main/java/net/pterodactylus/sone/template/AlbumAccessor.java
new file mode 100644 (file)
index 0000000..2c0a00e
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+ * Sone - AlbumAccessor.java - Copyright © 2011 David Roden
+ *
+ * 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 3 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, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.template;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import net.pterodactylus.sone.data.Album;
+import net.pterodactylus.util.template.Accessor;
+import net.pterodactylus.util.template.ReflectionAccessor;
+import net.pterodactylus.util.template.TemplateContext;
+
+/**
+ * {@link Accessor} implementation for {@link Album}s. A property named
+ * “backlinks” is added, it returns links to all parents and the owner Sone of
+ * an album.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class AlbumAccessor extends ReflectionAccessor {
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public Object get(TemplateContext templateContext, Object object, String member) {
+               Album album = (Album) object;
+               if ("backlinks".equals(member)) {
+                       List<Map<String, String>> backlinks = new ArrayList<Map<String, String>>();
+                       Album currentAlbum = album;
+                       while (currentAlbum != null) {
+                               backlinks.add(0, createLink("imageBrowser.html?album=" + currentAlbum.getId(), currentAlbum.getTitle()));
+                               currentAlbum = currentAlbum.getParent();
+                       }
+                       backlinks.add(0, createLink("imageBrowser.html?sone=" + album.getSone().getId(), SoneAccessor.getNiceName(album.getSone())));
+                       return backlinks;
+               }
+               return super.get(templateContext, object, member);
+       }
+
+       //
+       // PRIVATE METHODS
+       //
+
+       /**
+        * Creates a map containing mappings for “target” and “link.”
+        *
+        * @param target
+        *            The target to link to
+        * @param name
+        *            The name of the link
+        * @return The created map containing the mappings
+        */
+       private Map<String, String> createLink(String target, String name) {
+               Map<String, String> link = new HashMap<String, String>();
+               link.put("target", target);
+               link.put("name", name);
+               return link;
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/template/ImageLinkFilter.java b/src/main/java/net/pterodactylus/sone/template/ImageLinkFilter.java
new file mode 100644 (file)
index 0000000..9e758cf
--- /dev/null
@@ -0,0 +1,108 @@
+/*
+ * Sone - ImageLinkFilter.java - Copyright © 2011 David Roden
+ *
+ * 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 3 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, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.template;
+
+import java.io.StringReader;
+import java.io.StringWriter;
+import java.util.Map;
+
+import net.pterodactylus.sone.data.Image;
+import net.pterodactylus.util.number.Numbers;
+import net.pterodactylus.util.object.Default;
+import net.pterodactylus.util.template.Filter;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.template.TemplateContextFactory;
+import net.pterodactylus.util.template.TemplateParser;
+
+/**
+ * Template filter that turns an {@link Image} into an HTML &lt;img&gt; tag,
+ * using some parameters to influence parameters of the image.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class ImageLinkFilter implements Filter {
+
+       /** The template to render for the &lt;img&gt; tag. */
+       private static final Template linkTemplate = TemplateParser.parse(new StringReader("<img<%ifnull !class> class=\"<%class|css>\"<%/if> src=\"<%src|html>\" alt=\"<%alt|html>\" title=\"<%title|html>\" width=\"<%width|html>\" height=\"<%height|html>\" style=\"position: relative;<%ifnull ! top>top: <% top|html>;<%/if><%ifnull ! left>left: <% left|html>;<%/if>\"/>"));
+
+       /** The template context factory. */
+       private final TemplateContextFactory templateContextFactory;
+
+       /**
+        * Creates a new image link filter.
+        *
+        * @param templateContextFactory
+        *            The template context factory
+        */
+       public ImageLinkFilter(TemplateContextFactory templateContextFactory) {
+               this.templateContextFactory = templateContextFactory;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public Object format(TemplateContext templateContext, Object data, Map<String, String> parameters) {
+               Image image = (Image) data;
+               String imageClass = parameters.get("class");
+               int maxWidth = Numbers.safeParseInteger(parameters.get("max-width"), Integer.MAX_VALUE);
+               int maxHeight = Numbers.safeParseInteger(parameters.get("max-height"), Integer.MAX_VALUE);
+               String mode = String.valueOf(parameters.get("mode"));
+               String title = parameters.get("title");
+               if ((title != null) && title.startsWith("=")) {
+                       title = String.valueOf(templateContext.get(title.substring(1)));
+               }
+
+               TemplateContext linkTemplateContext = templateContextFactory.createTemplateContext();
+               linkTemplateContext.set("class", imageClass);
+               if (image.isInserted()) {
+                       linkTemplateContext.set("src", "/" + image.getKey());
+               } else {
+                       linkTemplateContext.set("src", "getImage.html?image=" + image.getId());
+               }
+               int imageWidth = image.getWidth();
+               int imageHeight = image.getHeight();
+               if ("enlarge".equals(mode)) {
+                       double scale = Math.max(maxWidth / (double) imageWidth, maxHeight / (double) imageHeight);
+                       linkTemplateContext.set("width", (int) (imageWidth * scale + 0.5));
+                       linkTemplateContext.set("height", (int) (imageHeight * scale + 0.5));
+                       if (scale >= 1) {
+                               linkTemplateContext.set("left", String.format("%dpx", (int) ((imageWidth * scale) - maxWidth) / 2));
+                               linkTemplateContext.set("top", String.format("%dpx", (int) ((imageHeight * scale) - maxHeight) / 2));
+                       } else {
+                               linkTemplateContext.set("left", String.format("%dpx", (int) (maxWidth - (imageWidth * scale)) / 2));
+                               linkTemplateContext.set("top", String.format("%dpx", (int) (maxHeight - (imageHeight * scale)) / 2));
+                       }
+               } else {
+                       double scale = 1;
+                       if ((imageWidth > maxWidth) || (imageHeight > maxHeight)) {
+                               scale = Math.min(maxWidth / (double) imageWidth, maxHeight / (double) imageHeight);
+                       }
+                       linkTemplateContext.set("width", (int) (imageWidth * scale + 0.5));
+                       linkTemplateContext.set("height", (int) (imageHeight * scale + 0.5));
+               }
+               linkTemplateContext.set("alt", Default.forNull(title, image.getDescription()));
+               linkTemplateContext.set("title", Default.forNull(title, image.getTitle()));
+
+               StringWriter stringWriter = new StringWriter();
+               linkTemplate.render(linkTemplateContext, stringWriter);
+               return stringWriter.toString();
+       }
+
+}
index 88c889e..384e8ae 100644 (file)
@@ -107,7 +107,6 @@ public class PartContainer implements Part, Iterable<Part> {
                                }
                                noNextPart = true;
                                while (!partStack.isEmpty()) {
-                                       @SuppressWarnings("hiding")
                                        Iterator<Part> parts = partStack.pop();
                                        if (parts.hasNext()) {
                                                nextPart = parts.next();
diff --git a/src/main/java/net/pterodactylus/sone/web/CreateAlbumPage.java b/src/main/java/net/pterodactylus/sone/web/CreateAlbumPage.java
new file mode 100644 (file)
index 0000000..a695952
--- /dev/null
@@ -0,0 +1,73 @@
+/*
+ * Sone - CreateAlbumPage.java - Copyright © 2011 David Roden
+ *
+ * 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 3 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, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web;
+
+import net.pterodactylus.sone.data.Album;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.page.FreenetRequest;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
+
+/**
+ * Page that lets the user create a new album.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class CreateAlbumPage extends SoneTemplatePage {
+
+       /**
+        * Creates a new “create album” page.
+        *
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public CreateAlbumPage(Template template, WebInterface webInterface) {
+               super("createAlbum.html", template, "Page.CreateAlbum.Title", webInterface, true);
+       }
+
+       //
+       // SONETEMPLATEPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
+               super.processTemplate(request, templateContext);
+               if (request.getMethod() == Method.POST) {
+                       String name = request.getHttpRequest().getPartAsStringFailsafe("name", 64).trim();
+                       if (name.length() == 0) {
+                               templateContext.set("nameMissing", true);
+                               return;
+                       }
+                       String description = request.getHttpRequest().getPartAsStringFailsafe("description", 256).trim();
+                       Sone currentSone = getCurrentSone(request.getToadletContext());
+                       String parentId = request.getHttpRequest().getPartAsStringFailsafe("parent", 36);
+                       Album parent = webInterface.getCore().getAlbum(parentId, false);
+                       Album album = webInterface.getCore().createAlbum(currentSone, parent);
+                       album.setTitle(name).setDescription(description);
+                       webInterface.getCore().touchConfiguration();
+                       throw new RedirectException("imageBrowser.html?album=" + album.getId());
+               }
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/web/DeleteAlbumPage.java b/src/main/java/net/pterodactylus/sone/web/DeleteAlbumPage.java
new file mode 100644 (file)
index 0000000..d03e065
--- /dev/null
@@ -0,0 +1,78 @@
+/*
+ * Sone - DeleteAlbumPage.java - Copyright © 2011 David Roden
+ *
+ * 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 3 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, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web;
+
+import net.pterodactylus.sone.data.Album;
+import net.pterodactylus.sone.web.page.FreenetRequest;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
+
+/**
+ * Page that lets the user delete an {@link Album}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class DeleteAlbumPage extends SoneTemplatePage {
+
+       /**
+        * Creates a new “delete album” page.
+        *
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public DeleteAlbumPage(Template template, WebInterface webInterface) {
+               super("deleteAlbum.html", template, "Page.DeleteAlbum.Title", webInterface, true);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
+               super.processTemplate(request, templateContext);
+               if (request.getMethod() == Method.POST) {
+                       String albumId = request.getHttpRequest().getPartAsStringFailsafe("album", 36);
+                       Album album = webInterface.getCore().getAlbum(albumId, false);
+                       if (album == null) {
+                               throw new RedirectException("invalid.html");
+                       }
+                       if (!webInterface.getCore().isLocalSone(album.getSone())) {
+                               throw new RedirectException("noPermission.html");
+                       }
+                       if (request.getHttpRequest().isPartSet("abortDelete")) {
+                               throw new RedirectException("imageBrowser.html?album=" + album.getId());
+                       }
+                       Album parentAlbum = album.getParent();
+                       webInterface.getCore().deleteAlbum(album);
+                       if (parentAlbum == null) {
+                               throw new RedirectException("imageBrowser.html?sone=" + album.getSone().getId());
+                       }
+                       throw new RedirectException("imageBrowser.html?album=" + parentAlbum.getId());
+               }
+               String albumId = request.getHttpRequest().getParam("album");
+               Album album = webInterface.getCore().getAlbum(albumId, false);
+               if (album == null) {
+                       throw new RedirectException("invalid.html");
+               }
+               templateContext.set("album", album);
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/web/DeleteImagePage.java b/src/main/java/net/pterodactylus/sone/web/DeleteImagePage.java
new file mode 100644 (file)
index 0000000..66098ff
--- /dev/null
@@ -0,0 +1,73 @@
+/*
+ * Sone - DeleteImagePage.java - Copyright © 2011 David Roden
+ *
+ * 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 3 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, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web;
+
+import net.pterodactylus.sone.data.Image;
+import net.pterodactylus.sone.web.page.FreenetRequest;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
+
+/**
+ * Page that lets the user delete an {@link Image}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class DeleteImagePage extends SoneTemplatePage {
+
+       /**
+        * Creates a new “delete image” page.
+        *
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public DeleteImagePage(Template template, WebInterface webInterface) {
+               super("deleteImage.html", template, "Page.DeleteImage.Title", webInterface, true);
+       }
+
+       //
+       // SONETEMPLATEPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
+               super.processTemplate(request, templateContext);
+               String imageId = (request.getMethod() == Method.POST) ? request.getHttpRequest().getPartAsStringFailsafe("image", 36) : request.getHttpRequest().getParam("image");
+               Image image = webInterface.getCore().getImage(imageId, false);
+               if (image == null) {
+                       throw new RedirectException("invalid.html");
+               }
+               if (!webInterface.getCore().isLocalSone(image.getSone())) {
+                       throw new RedirectException("noPermission.html");
+               }
+               if (request.getMethod() == Method.POST) {
+                       if (request.getHttpRequest().isPartSet("abortDelete")) {
+                               throw new RedirectException("imageBrowser.html?image=" + image.getId());
+                       }
+                       webInterface.getCore().deleteImage(image);
+                       throw new RedirectException("imageBrowser.html?album=" + image.getAlbum().getId());
+               }
+               templateContext.set("image", image);
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/web/EditAlbumPage.java b/src/main/java/net/pterodactylus/sone/web/EditAlbumPage.java
new file mode 100644 (file)
index 0000000..fd4bf7c
--- /dev/null
@@ -0,0 +1,77 @@
+/*
+ * Sone - EditAlbumPage.java - Copyright © 2011 David Roden
+ *
+ * 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 3 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, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web;
+
+import net.pterodactylus.sone.data.Album;
+import net.pterodactylus.sone.web.page.FreenetRequest;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
+
+/**
+ * Page that lets the user edit the name and description of an album.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class EditAlbumPage extends SoneTemplatePage {
+
+       /**
+        * Creates a new “edit album” page.
+        *
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public EditAlbumPage(Template template, WebInterface webInterface) {
+               super("editAlbum.html", template, "Page.EditAlbum.Title", webInterface, true);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
+               super.processTemplate(request, templateContext);
+               if (request.getMethod() == Method.POST) {
+                       String albumId = request.getHttpRequest().getPartAsStringFailsafe("album", 36);
+                       Album album = webInterface.getCore().getAlbum(albumId, false);
+                       if (album == null) {
+                               throw new RedirectException("invalid.html");
+                       }
+                       if (!webInterface.getCore().isLocalSone(album.getSone())) {
+                               throw new RedirectException("noPermission.html");
+                       }
+                       String albumImageId = request.getHttpRequest().getPartAsStringFailsafe("album-image", 36);
+                       if (webInterface.getCore().getImage(albumImageId, false) == null) {
+                               albumImageId = null;
+                       }
+                       album.setAlbumImage(albumImageId);
+                       String title = request.getHttpRequest().getPartAsStringFailsafe("title", 100).trim();
+                       if (title.length() == 0) {
+                               templateContext.set("titleMissing", true);
+                               return;
+                       }
+                       String description = request.getHttpRequest().getPartAsStringFailsafe("description", 1000).trim();
+                       album.setTitle(title).setDescription(description);
+                       webInterface.getCore().touchConfiguration();
+                       throw new RedirectException("imageBrowser.html?album=" + album.getId());
+               }
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/web/EditImagePage.java b/src/main/java/net/pterodactylus/sone/web/EditImagePage.java
new file mode 100644 (file)
index 0000000..7e9eb7e
--- /dev/null
@@ -0,0 +1,83 @@
+/*
+ * FreenetSone - WebInterface.java - Copyright © 2010 David Roden
+ *
+ * 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 3 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, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web;
+
+import net.pterodactylus.sone.data.Image;
+import net.pterodactylus.sone.web.page.FreenetRequest;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
+
+/**
+ * Page that lets the user edit title and description of an {@link Image}.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class EditImagePage extends SoneTemplatePage {
+
+       /**
+        * Creates a new “edit image” page.
+        *
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public EditImagePage(Template template, WebInterface webInterface) {
+               super("editImage.html", template, "Page.EditImage.Title", webInterface, true);
+       }
+
+       //
+       // SONETEMPLATEPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
+               super.processTemplate(request, templateContext);
+               if (request.getMethod() == Method.POST) {
+                       String imageId = request.getHttpRequest().getPartAsStringFailsafe("image", 36);
+                       String returnPage = request.getHttpRequest().getPartAsStringFailsafe("returnPage", 256);
+                       Image image = webInterface.getCore().getImage(imageId, false);
+                       if (image == null) {
+                               throw new RedirectException("invalid.html");
+                       }
+                       if (!webInterface.getCore().isLocalSone(image.getSone())) {
+                               throw new RedirectException("noPermission.html");
+                       }
+                       if ("true".equals(request.getHttpRequest().getPartAsStringFailsafe("moveLeft", 4))) {
+                               image.getAlbum().moveImageUp(image);
+                       } else  if ("true".equals(request.getHttpRequest().getPartAsStringFailsafe("moveRight", 4))) {
+                               image.getAlbum().moveImageDown(image);
+                       } else {
+                               String title = request.getHttpRequest().getPartAsStringFailsafe("title", 100).trim();
+                               String description = request.getHttpRequest().getPartAsStringFailsafe("description", 1024).trim();
+                               if (title.length() == 0) {
+                                       templateContext.set("titleMissing", true);
+                               }
+                               image.setTitle(title);
+                               image.setDescription(description);
+                       }
+                       webInterface.getCore().touchConfiguration();
+                       throw new RedirectException(returnPage);
+               }
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/web/GetImagePage.java b/src/main/java/net/pterodactylus/sone/web/GetImagePage.java
new file mode 100644 (file)
index 0000000..29556c9
--- /dev/null
@@ -0,0 +1,77 @@
+/*
+ * Sone - GetImagePage.java - Copyright © 2011 David Roden
+ *
+ * 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 3 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, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web;
+
+import java.io.IOException;
+
+import net.pterodactylus.sone.data.TemporaryImage;
+import net.pterodactylus.sone.web.page.FreenetRequest;
+import net.pterodactylus.util.web.Page;
+import net.pterodactylus.util.web.Response;
+
+/**
+ * Page that delivers a {@link TemporaryImage} to the browser.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class GetImagePage implements Page<FreenetRequest> {
+
+       /** The Sone web interface. */
+       private final WebInterface webInterface;
+
+       /**
+        * Creates a new “get image” page.
+        *
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public GetImagePage(WebInterface webInterface) {
+               this.webInterface = webInterface;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public String getPath() {
+               return "getImage.html";
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public boolean isPrefixPage() {
+               return false;
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public Response handleRequest(FreenetRequest request, Response response) throws IOException {
+               String imageId = request.getHttpRequest().getParam("image");
+               TemporaryImage temporaryImage = webInterface.getCore().getTemporaryImage(imageId);
+               if (temporaryImage == null) {
+                       return response.setStatusCode(404).setStatusText("Not found.").setContentType("text/html; charset=utf-8");
+               }
+               String contentType= temporaryImage.getMimeType();
+               return response.setStatusCode(200).setStatusText("OK").setContentType(contentType).addHeader("Content-Disposition", "attachment; filename=" + temporaryImage.getId() + "." + contentType.substring(contentType.lastIndexOf('/') + 1)).write(temporaryImage.getImageData());
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/web/ImageBrowserPage.java b/src/main/java/net/pterodactylus/sone/web/ImageBrowserPage.java
new file mode 100644 (file)
index 0000000..ed31283
--- /dev/null
@@ -0,0 +1,79 @@
+/*
+ * Sone - ImageBrowserPage.java - Copyright © 2011 David Roden
+ *
+ * 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 3 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, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web;
+
+import net.pterodactylus.sone.data.Album;
+import net.pterodactylus.sone.data.Image;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.web.page.FreenetRequest;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+
+/**
+ * The image browser page is the entry page for the image management.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class ImageBrowserPage extends SoneTemplatePage {
+
+       /**
+        * Creates a new image browser page.
+        *
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public ImageBrowserPage(Template template, WebInterface webInterface) {
+               super("imageBrowser.html", template, "Page.ImageBrowser.Title", webInterface, true);
+       }
+
+       //
+       // SONETEMPLATEPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
+               super.processTemplate(request, templateContext);
+               String albumId = request.getHttpRequest().getParam("album", null);
+               if (albumId != null) {
+                       Album album = webInterface.getCore().getAlbum(albumId, false);
+                       templateContext.set("albumRequested", true);
+                       templateContext.set("album", album);
+                       return;
+               }
+               String imageId = request.getHttpRequest().getParam("image", null);
+               if (imageId != null) {
+                       Image image = webInterface.getCore().getImage(imageId, false);
+                       templateContext.set("imageRequested", true);
+                       templateContext.set("image", image);
+                       return;
+               }
+               Sone sone = getCurrentSone(request.getToadletContext(), false);
+               String soneId = request.getHttpRequest().getParam("sone", null);
+               if (soneId != null) {
+                       sone = webInterface.getCore().getSone(soneId, false);
+               }
+               templateContext.set("soneRequested", true);
+               templateContext.set("sone", sone);
+       }
+
+}
index 42f0a82..91e2a08 100644 (file)
@@ -457,7 +457,6 @@ public class SearchPage extends SoneTemplatePage {
                        if (!(object instanceof Phrase)) {
                                return false;
                        }
-                       @SuppressWarnings("hiding")
                        Phrase phrase = (Phrase) object;
                        return (this.optionality == phrase.optionality) && this.phrase.equals(phrase.phrase);
                }
diff --git a/src/main/java/net/pterodactylus/sone/web/UploadImagePage.java b/src/main/java/net/pterodactylus/sone/web/UploadImagePage.java
new file mode 100644 (file)
index 0000000..e0aa1e1
--- /dev/null
@@ -0,0 +1,161 @@
+/*
+ * Sone - UploadImagePage.java - Copyright © 2011 David Roden
+ *
+ * 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 3 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, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web;
+
+import java.awt.Image;
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Iterator;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import javax.imageio.ImageIO;
+import javax.imageio.ImageReader;
+import javax.imageio.stream.ImageInputStream;
+
+import net.pterodactylus.sone.data.Album;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.data.TemporaryImage;
+import net.pterodactylus.sone.web.page.FreenetRequest;
+import net.pterodactylus.util.io.Closer;
+import net.pterodactylus.util.io.StreamCopier;
+import net.pterodactylus.util.logging.Logging;
+import net.pterodactylus.util.template.Template;
+import net.pterodactylus.util.template.TemplateContext;
+import net.pterodactylus.util.web.Method;
+import freenet.support.api.Bucket;
+import freenet.support.api.HTTPUploadedFile;
+
+/**
+ * Page implementation that lets the user upload an image.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class UploadImagePage extends SoneTemplatePage {
+
+       /** The logger. */
+       private static final Logger logger = Logging.getLogger(UploadImagePage.class);
+
+       /**
+        * Creates a new “upload image” page.
+        *
+        * @param template
+        *            The template to render
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public UploadImagePage(Template template, WebInterface webInterface) {
+               super("uploadImage.html", template, "Page.UploadImage.Title", webInterface, true);
+       }
+
+       //
+       // SONETEMPLATEPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
+               super.processTemplate(request, templateContext);
+               if (request.getMethod() == Method.POST) {
+                       Sone currentSone = getCurrentSone(request.getToadletContext());
+                       String parentId = request.getHttpRequest().getPartAsStringFailsafe("parent", 36);
+                       Album parent = webInterface.getCore().getAlbum(parentId, false);
+                       if (parent == null) {
+                               /* TODO - signal error */
+                               return;
+                       }
+                       if (!currentSone.equals(parent.getSone())) {
+                               /* TODO - signal error. */
+                               return;
+                       }
+                       String name = request.getHttpRequest().getPartAsStringFailsafe("title", 200);
+                       String description = request.getHttpRequest().getPartAsStringFailsafe("description", 4000);
+                       HTTPUploadedFile uploadedFile = request.getHttpRequest().getUploadedFile("image");
+                       Bucket fileBucket = uploadedFile.getData();
+                       InputStream imageInputStream = null;
+                       ByteArrayOutputStream imageDataOutputStream = null;
+                       net.pterodactylus.sone.data.Image image = null;
+                       try {
+                               imageInputStream = fileBucket.getInputStream();
+                               /* TODO - check length */
+                               imageDataOutputStream = new ByteArrayOutputStream((int) fileBucket.size());
+                               StreamCopier.copy(imageInputStream, imageDataOutputStream);
+                       } catch (IOException ioe1) {
+                               logger.log(Level.WARNING, "Could not read uploaded image!", ioe1);
+                               return;
+                       } finally {
+                               fileBucket.free();
+                               Closer.close(imageInputStream);
+                               Closer.close(imageDataOutputStream);
+                       }
+                       byte[] imageData = imageDataOutputStream.toByteArray();
+                       ByteArrayInputStream imageDataInputStream = null;
+                       Image uploadedImage = null;
+                       try {
+                               imageDataInputStream = new ByteArrayInputStream(imageData);
+                               uploadedImage = ImageIO.read(imageDataInputStream);
+                               if (uploadedImage == null) {
+                                       templateContext.set("messages", webInterface.getL10n().getString("Page.UploadImage.Error.InvalidImage"));
+                                       return;
+                               }
+                               String mimeType = getMimeType(imageData);
+                               TemporaryImage temporaryImage = webInterface.getCore().createTemporaryImage(mimeType, imageData);
+                               image = webInterface.getCore().createImage(currentSone, parent, temporaryImage);
+                               image.setTitle(name).setDescription(description).setWidth(uploadedImage.getWidth(null)).setHeight(uploadedImage.getHeight(null));
+                       } catch (IOException ioe1) {
+                               logger.log(Level.WARNING, "Could not read uploaded image!", ioe1);
+                               return;
+                       } finally {
+                               Closer.close(imageDataInputStream);
+                               Closer.flush(uploadedImage);
+                       }
+                       throw new RedirectException("imageBrowser.html?album=" + parent.getId());
+               }
+       }
+
+       //
+       // PRIVATE METHODS
+       //
+
+       /**
+        * Tries to detect the MIME type of the encoded image.
+        *
+        * @param imageData
+        *            The encoded image
+        * @return The MIME type of the image, or “application/octet-stream” if the
+        *         image type could not be detected
+        */
+       private String getMimeType(byte[] imageData) {
+               ByteArrayInputStream imageDataInputStream = new ByteArrayInputStream(imageData);
+               try {
+                       ImageInputStream imageInputStream = ImageIO.createImageInputStream(imageDataInputStream);
+                       Iterator<ImageReader> imageReaders = ImageIO.getImageReaders(imageInputStream);
+                       if (imageReaders.hasNext()) {
+                               return imageReaders.next().getOriginatingProvider().getMIMETypes()[0];
+                       }
+               } catch (IOException ioe1) {
+                       logger.log(Level.FINE, "Could not detect MIME type for image.", ioe1);
+               }
+               return "application/octet-stream";
+       }
+
+}
index fa6f52c..283e156 100644 (file)
@@ -37,6 +37,8 @@ import java.util.logging.Logger;
 
 import net.pterodactylus.sone.core.Core;
 import net.pterodactylus.sone.core.CoreListener;
+import net.pterodactylus.sone.data.Album;
+import net.pterodactylus.sone.data.Image;
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.data.Reply;
 import net.pterodactylus.sone.data.Sone;
@@ -45,10 +47,12 @@ import net.pterodactylus.sone.freenet.wot.Identity;
 import net.pterodactylus.sone.freenet.wot.Trust;
 import net.pterodactylus.sone.main.SonePlugin;
 import net.pterodactylus.sone.notify.ListNotification;
+import net.pterodactylus.sone.template.AlbumAccessor;
 import net.pterodactylus.sone.template.CollectionAccessor;
 import net.pterodactylus.sone.template.CssClassNameFilter;
 import net.pterodactylus.sone.template.HttpRequestAccessor;
 import net.pterodactylus.sone.template.IdentityAccessor;
+import net.pterodactylus.sone.template.ImageLinkFilter;
 import net.pterodactylus.sone.template.JavascriptFilter;
 import net.pterodactylus.sone.template.ParserFilter;
 import net.pterodactylus.sone.template.PostAccessor;
@@ -71,6 +75,8 @@ import net.pterodactylus.sone.web.ajax.DeleteProfileFieldAjaxPage;
 import net.pterodactylus.sone.web.ajax.DeleteReplyAjaxPage;
 import net.pterodactylus.sone.web.ajax.DismissNotificationAjaxPage;
 import net.pterodactylus.sone.web.ajax.DistrustAjaxPage;
+import net.pterodactylus.sone.web.ajax.EditAlbumAjaxPage;
+import net.pterodactylus.sone.web.ajax.EditImageAjaxPage;
 import net.pterodactylus.sone.web.ajax.EditProfileFieldAjaxPage;
 import net.pterodactylus.sone.web.ajax.FollowSoneAjaxPage;
 import net.pterodactylus.sone.web.ajax.GetLikesAjaxPage;
@@ -111,6 +117,7 @@ import net.pterodactylus.util.template.DateFilter;
 import net.pterodactylus.util.template.FormatFilter;
 import net.pterodactylus.util.template.HtmlFilter;
 import net.pterodactylus.util.template.MatchFilter;
+import net.pterodactylus.util.template.ModFilter;
 import net.pterodactylus.util.template.Provider;
 import net.pterodactylus.util.template.ReflectionAccessor;
 import net.pterodactylus.util.template.ReplaceFilter;
@@ -127,9 +134,9 @@ import net.pterodactylus.util.web.RedirectPage;
 import net.pterodactylus.util.web.StaticPage;
 import net.pterodactylus.util.web.TemplatePage;
 import freenet.clients.http.SessionManager;
-import freenet.clients.http.SessionManager.Session;
 import freenet.clients.http.ToadletContainer;
 import freenet.clients.http.ToadletContext;
+import freenet.clients.http.SessionManager.Session;
 import freenet.l10n.BaseL10n;
 import freenet.support.api.HTTPRequest;
 
@@ -192,6 +199,15 @@ public class WebInterface implements CoreListener {
        /** The “new version” notification. */
        private final TemplateNotification newVersionNotification;
 
+       /** The “inserting images” notification. */
+       private final ListNotification<Image> insertingImagesNotification;
+
+       /** The “inserted images” notification. */
+       private final ListNotification<Image> insertedImagesNotification;
+
+       /** The “image insert failed” notification. */
+       private final ListNotification<Image> imageInsertFailedNotification;
+
        /**
         * Creates a new web interface.
         *
@@ -210,6 +226,7 @@ public class WebInterface implements CoreListener {
                templateContextFactory.addAccessor(Sone.class, new SoneAccessor(getCore()));
                templateContextFactory.addAccessor(Post.class, new PostAccessor(getCore()));
                templateContextFactory.addAccessor(Reply.class, new ReplyAccessor(getCore()));
+               templateContextFactory.addAccessor(Album.class, new AlbumAccessor());
                templateContextFactory.addAccessor(Identity.class, new IdentityAccessor(getCore()));
                templateContextFactory.addAccessor(Trust.class, new TrustAccessor());
                templateContextFactory.addAccessor(HTTPRequest.class, new HttpRequestAccessor());
@@ -228,9 +245,11 @@ public class WebInterface implements CoreListener {
                templateContextFactory.addFilter("unknown", new UnknownDateFilter(getL10n(), "View.Sone.Text.UnknownDate"));
                templateContextFactory.addFilter("format", new FormatFilter());
                templateContextFactory.addFilter("sort", new CollectionSortFilter());
+               templateContextFactory.addFilter("image-link", new ImageLinkFilter(templateContextFactory));
                templateContextFactory.addFilter("replyGroup", new ReplyGroupFilter());
                templateContextFactory.addFilter("in", new ContainsFilter());
                templateContextFactory.addFilter("unique", new UniqueElementFilter());
+               templateContextFactory.addFilter("mod", new ModFilter());
                templateContextFactory.addProvider(Provider.TEMPLATE_CONTEXT_PROVIDER);
                templateContextFactory.addProvider(new ClassPathTemplateProvider());
                templateContextFactory.addTemplateObject("webInterface", this);
@@ -260,6 +279,15 @@ public class WebInterface implements CoreListener {
 
                Template newVersionTemplate = TemplateParser.parse(createReader("/templates/notify/newVersionNotification.html"));
                newVersionNotification = new TemplateNotification("new-version-notification", newVersionTemplate);
+
+               Template insertingImagesTemplate = TemplateParser.parse(createReader("/templates/notify/inserting-images-notification.html"));
+               insertingImagesNotification = new ListNotification<Image>("inserting-images-notification", "images", insertingImagesTemplate);
+
+               Template insertedImagesTemplate = TemplateParser.parse(createReader("/templates/notify/inserted-images-notification.html"));
+               insertedImagesNotification = new ListNotification<Image>("inserted-images-notification", "images", insertedImagesTemplate);
+
+               Template imageInsertFailedTemplate = TemplateParser.parse(createReader("/templates/notify/image-insert-failed-notification.html"));
+               imageInsertFailedNotification = new ListNotification<Image>("image-insert-failed-notification", "images", imageInsertFailedTemplate);
        }
 
        //
@@ -560,6 +588,10 @@ public class WebInterface implements CoreListener {
                Template deletePostTemplate = TemplateParser.parse(createReader("/templates/deletePost.html"));
                Template deleteReplyTemplate = TemplateParser.parse(createReader("/templates/deleteReply.html"));
                Template deleteSoneTemplate = TemplateParser.parse(createReader("/templates/deleteSone.html"));
+               Template imageBrowserTemplate = TemplateParser.parse(createReader("/templates/imageBrowser.html"));
+               Template createAlbumTemplate = TemplateParser.parse(createReader("/templates/createAlbum.html"));
+               Template deleteAlbumTemplate = TemplateParser.parse(createReader("/templates/deleteAlbum.html"));
+               Template deleteImageTemplate = TemplateParser.parse(createReader("/templates/deleteImage.html"));
                Template noPermissionTemplate = TemplateParser.parse(createReader("/templates/noPermission.html"));
                Template optionsTemplate = TemplateParser.parse(createReader("/templates/options.html"));
                Template rescueTemplate = TemplateParser.parse(createReader("/templates/rescue.html"));
@@ -589,6 +621,13 @@ public class WebInterface implements CoreListener {
                pageToadlets.add(pageToadletFactory.createPageToadlet(new UnlockSonePage(emptyTemplate, this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new FollowSonePage(emptyTemplate, this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new UnfollowSonePage(emptyTemplate, this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new ImageBrowserPage(imageBrowserTemplate, this), "ImageBrowser"));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new CreateAlbumPage(createAlbumTemplate, this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new EditAlbumPage(emptyTemplate, this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new DeleteAlbumPage(deleteAlbumTemplate, this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new UploadImagePage(invalidTemplate, this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new EditImagePage(emptyTemplate, this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new DeleteImagePage(deleteImageTemplate, this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new TrustPage(emptyTemplate, this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new DistrustPage(emptyTemplate, this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new UntrustPage(emptyTemplate, this)));
@@ -610,6 +649,7 @@ public class WebInterface implements CoreListener {
                pageToadlets.add(pageToadletFactory.createPageToadlet(new StaticPage<FreenetRequest>("javascript/", "/static/javascript/", "text/javascript")));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new StaticPage<FreenetRequest>("images/", "/static/images/", "image/png")));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new TemplatePage<FreenetRequest>("OpenSearch.xml", "application/opensearchdescription+xml", templateContextFactory, openSearchTemplate)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new GetImagePage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new GetTranslationPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new GetStatusAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new GetNotificationAjaxPage(this)));
@@ -626,6 +666,8 @@ public class WebInterface implements CoreListener {
                pageToadlets.add(pageToadletFactory.createPageToadlet(new UnlockSoneAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new FollowSoneAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new UnfollowSoneAjaxPage(this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new EditAlbumAjaxPage(this)));
+               pageToadlets.add(pageToadletFactory.createPageToadlet(new EditImageAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new TrustAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new DistrustAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new UntrustAjaxPage(this)));
@@ -772,7 +814,7 @@ public class WebInterface implements CoreListener {
                }
                if (!hasFirstStartNotification()) {
                        notificationManager.addNotification(isLocal ? localReplyNotification : newReplyNotification);
-                       if (!getMentionedSones(reply.getText()).isEmpty() && !isLocal && (reply.getPost().getSone() != null)) {
+                       if (!getMentionedSones(reply.getText()).isEmpty() && !isLocal && (reply.getPost().getSone() != null) && (reply.getTime() <= System.currentTimeMillis())) {
                                mentionNotification.add(reply.getPost());
                                notificationManager.addNotification(mentionNotification);
                        }
@@ -912,6 +954,43 @@ public class WebInterface implements CoreListener {
        }
 
        /**
+        * {@inheritDoc}
+        */
+       @Override
+       public void imageInsertStarted(Image image) {
+               insertingImagesNotification.add(image);
+               notificationManager.addNotification(insertingImagesNotification);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public void imageInsertAborted(Image image) {
+               insertingImagesNotification.remove(image);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public void imageInsertFinished(Image image) {
+               insertingImagesNotification.remove(image);
+               insertedImagesNotification.add(image);
+               notificationManager.addNotification(insertedImagesNotification);
+       }
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public void imageInsertFailed(Image image, Throwable cause) {
+               insertingImagesNotification.remove(image);
+               imageInsertFailedNotification.add(image);
+               notificationManager.addNotification(imageInsertFailedNotification);
+       }
+
+       /**
         * Template provider implementation that uses
         * {@link WebInterface#createReader(String)} to load templates for
         * inclusion.
diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/EditAlbumAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/EditAlbumAjaxPage.java
new file mode 100644 (file)
index 0000000..53f0466
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+ * Sone - EditAlbumAjaxPage.java - Copyright © 2011 David Roden
+ *
+ * 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 3 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, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.ajax;
+
+import net.pterodactylus.sone.data.Album;
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
+import net.pterodactylus.util.json.JsonObject;
+
+/**
+ * Page that stores a user’s album modifications.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class EditAlbumAjaxPage extends JsonPage {
+
+       /**
+        * Creates a new edit album AJAX page.
+        *
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public EditAlbumAjaxPage(WebInterface webInterface) {
+               super("editAlbum.ajax", webInterface);
+       }
+
+       //
+       // JSONPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected JsonObject createJsonObject(FreenetRequest request) {
+               String albumId = request.getHttpRequest().getParam("album");
+               Album album = webInterface.getCore().getAlbum(albumId, false);
+               if (album == null) {
+                       return createErrorJsonObject("invalid-album-id");
+               }
+               if (!webInterface.getCore().isLocalSone(album.getSone())) {
+                       return createErrorJsonObject("not-authorized");
+               }
+               if ("true".equals(request.getHttpRequest().getParam("moveLeft"))) {
+                       Album swappedAlbum = (album.getParent() != null) ? album.getParent().moveAlbumUp(album) : album.getSone().moveAlbumUp(album);
+                       webInterface.getCore().touchConfiguration();
+                       return createSuccessJsonObject().put("sourceAlbumId", album.getId()).put("destinationAlbumId", swappedAlbum.getId());
+               }
+               if ("true".equals(request.getHttpRequest().getParam("moveRight"))) {
+                       Album swappedAlbum = (album.getParent() != null) ? album.getParent().moveAlbumDown(album) : album.getSone().moveAlbumDown(album);
+                       webInterface.getCore().touchConfiguration();
+                       return createSuccessJsonObject().put("sourceAlbumId", album.getId()).put("destinationAlbumId", swappedAlbum.getId());
+               }
+               String title = request.getHttpRequest().getParam("title").trim();
+               String description = request.getHttpRequest().getParam("description").trim();
+               album.setTitle(title).setDescription(description);
+               webInterface.getCore().touchConfiguration();
+               return createSuccessJsonObject().put("albumId", album.getId()).put("title", album.getTitle()).put("description", album.getDescription());
+       }
+
+}
diff --git a/src/main/java/net/pterodactylus/sone/web/ajax/EditImageAjaxPage.java b/src/main/java/net/pterodactylus/sone/web/ajax/EditImageAjaxPage.java
new file mode 100644 (file)
index 0000000..17d171b
--- /dev/null
@@ -0,0 +1,76 @@
+/*
+ * Sone - EditImageAjaxPage.java - Copyright © 2011 David Roden
+ *
+ * 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 3 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, see <http://www.gnu.org/licenses/>.
+ */
+
+package net.pterodactylus.sone.web.ajax;
+
+import net.pterodactylus.sone.data.Image;
+import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetRequest;
+import net.pterodactylus.util.json.JsonObject;
+
+/**
+ * Page that stores a user’s image modifications.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class EditImageAjaxPage extends JsonPage {
+
+       /**
+        * Creates a new edit image AJAX page.
+        *
+        * @param webInterface
+        *            The Sone web interface
+        */
+       public EditImageAjaxPage(WebInterface webInterface) {
+               super("editImage.ajax", webInterface);
+       }
+
+       //
+       // JSONPAGE METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       protected JsonObject createJsonObject(FreenetRequest request) {
+               String imageId = request.getHttpRequest().getParam("image");
+               Image image = webInterface.getCore().getImage(imageId, false);
+               if (image == null) {
+                       return createErrorJsonObject("invalid-image-id");
+               }
+               if (!webInterface.getCore().isLocalSone(image.getSone())) {
+                       return createErrorJsonObject("not-authorized");
+               }
+               if ("true".equals(request.getHttpRequest().getParam("moveLeft"))) {
+                       Image swappedImage = image.getAlbum().moveImageUp(image);
+                       webInterface.getCore().touchConfiguration();
+                       return createSuccessJsonObject().put("sourceImageId", image.getId()).put("destinationImageId", swappedImage.getId());
+               }
+               if ("true".equals(request.getHttpRequest().getParam("moveRight"))) {
+                       Image swappedImage = image.getAlbum().moveImageDown(image);
+                       webInterface.getCore().touchConfiguration();
+                       return createSuccessJsonObject().put("sourceImageId", image.getId()).put("destinationImageId", swappedImage.getId());
+               }
+               String title = request.getHttpRequest().getParam("title").trim();
+               String description = request.getHttpRequest().getParam("description").trim();
+               image.setTitle(title).setDescription(description);
+               webInterface.getCore().touchConfiguration();
+               return createSuccessJsonObject().put("imageId", image.getId()).put("title", image.getTitle()).put("description", image.getDescription());
+       }
+
+}
index 198a04f..dfac8e4 100644 (file)
@@ -82,6 +82,10 @@ public class GetNotificationAjaxPage extends JsonPage {
                Sone currentSone = getCurrentSone(request.getToadletContext(), false);
                for (String notificationId : notificationIds) {
                        Notification notification = webInterface.getNotifications().getNotification(notificationId);
+                       if (notification == null) {
+                               // TODO - show error
+                               continue;
+                       }
                        if ("new-post-notification".equals(notificationId)) {
                                notification = ListNotificationFilters.filterNewPostNotification((ListNotification<Post>) notification, currentSone, false);
                        } else if ("new-reply-notification".equals(notificationId)) {
index 8096a6a..5c027e5 100644 (file)
@@ -204,10 +204,9 @@ public class FreenetTemplatePage implements Page<FreenetRequest>, LinkEnabledCal
 
        /**
         * This method will be called after
-        * {@link #processTemplate(net.pterodactylus.sone.web.page.Page.Request, TemplateContext)}
-        * has processed the template and the template was rendered. This method
-        * will not be called if
-        * {@link #processTemplate(net.pterodactylus.sone.web.page.Page.Request, TemplateContext)}
+        * {@link #processTemplate(FreenetRequest, TemplateContext)} has processed
+        * the template and the template was rendered. This method will not be
+        * called if {@link #processTemplate(FreenetRequest, TemplateContext)}
         * throws a {@link RedirectException}!
         *
         * @param request
@@ -268,7 +267,7 @@ public class FreenetTemplatePage implements Page<FreenetRequest>, LinkEnabledCal
        /**
         * Exception that can be thrown to signal that a subclassed {@link Page}
         * wants to redirect the user during the
-        * {@link FreenetTemplatePage#processTemplate(net.pterodactylus.sone.web.page.Page.Request, TemplateContext)}
+        * {@link FreenetTemplatePage#processTemplate(FreenetRequest, TemplateContext)}
         * method call.
         *
         * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
index 739c615..75f9d41 100644 (file)
@@ -12,6 +12,8 @@ Navigation.Menu.Item.Bookmarks.Name=Bookmarks
 Navigation.Menu.Item.Bookmarks.Tooltip=Show bookmarked posts
 Navigation.Menu.Item.EditProfile.Name=Edit Profile
 Navigation.Menu.Item.EditProfile.Tooltip=Edit the Profile of your Sone
+Navigation.Menu.Item.ImageBrowser.Name=Images
+Navigation.Menu.Item.ImageBrowser.Tooltip=Manages your Images
 Navigation.Menu.Item.DeleteSone.Name=Delete Sone
 Navigation.Menu.Item.DeleteSone.Tooltip=Deletes the current Sone
 Navigation.Menu.Item.Logout.Name=Logout
@@ -147,6 +149,8 @@ Page.ViewSone.PostList.Title=Posts by {sone}
 Page.ViewSone.PostList.Text.NoPostYet=This Sone has not yet posted anything.
 Page.ViewSone.Profile.Title=Profile
 Page.ViewSone.Profile.Label.Name=Name
+Page.ViewSone.Profile.Label.Albums=Albums
+Page.ViewSone.Profile.Albums.Text.All=All albums
 Page.ViewSone.Profile.Name.WoTLink=web of trust profile
 Page.ViewSone.Replies.Title=Posts {sone} has replied to
 
@@ -178,6 +182,54 @@ Page.FollowSone.Title=Follow Sone - Sone
 
 Page.UnfollowSone.Title=Unfollow Sone - Sone
 
+Page.ImageBrowser.Title=Image Browser - Sone
+Page.ImageBrowser.Album.Title=Album “{album}”
+Page.ImageBrowser.Album.Error.NotFound.Text=The requested album could not be found. It is possible that it has not yet been downloaded, or that it has been deleted.
+Page.ImageBrowser.Sone.Title=Albums of {sone}
+Page.ImageBrowser.Sone.Error.NotFound.Text=The requested Sone could not be found. It is possible that it has not yet been downloaded.
+Page.ImageBrowser.Header.Albums=Albums
+Page.ImageBrowser.Header.Images=Images
+Page.ImageBrowser.CreateAlbum.Button.CreateAlbum=Create Album
+Page.ImageBrowser.Album.Edit.Title=Edit Album
+Page.ImageBrowser.Album.Delete.Title=Delete Album
+Page.ImageBrowser.Album.Label.AlbumImage=Album Image:
+Page.ImageBrowser.Album.Label.Title=Title:
+Page.ImageBrowser.Album.Label.Description=Description:
+Page.ImageBrowser.Album.AlbumImage.Choose=Choose Album Image…
+Page.ImageBrowser.Album.Button.Save=Save Album
+Page.ImageBrowser.Album.Button.Delete=Delete Album
+Page.ImageBrowser.Image.Edit.Title=Edit Image
+Page.ImageBrowser.Image.Title.Label=Title:
+Page.ImageBrowser.Image.Description.Label=Description:
+Page.ImageBrowser.Image.Button.MoveLeft=◀
+Page.ImageBrowser.Image.Button.Save=Save Image
+Page.ImageBrowser.Image.Button.MoveRight=►
+Page.ImageBrowser.Image.Delete.Title=Delete Image
+Page.ImageBrowser.Image.Button.Delete=Delete Image
+
+Page.CreateAlbum.Title=Create Album - Sone
+Page.CreateAlbum.Page.Title=Create Album
+Page.CreateAlbum.Error.NameMissing=You seem to have forgotten to enter a name for your new album.
+
+Page.UploadImage.Title=Upload Image - Sone
+Page.UploadImage.Error.InvalidImage=The image you were trying to upload could not be recognized. Please upload only JPEG (*.jpg or *.jpeg), or PNG (*.png) images.
+
+Page.EditImage.Title=Edit Image
+
+Page.DeleteImage.Title=Delete Image
+Page.DeleteImage.Page.Title=Delete Image
+Page.DeleteImage.Text.ImageWillBeGone=This will remove the image “{image}” from your album “{album}”. If it has already been inserted into Freenet it can not be removed from there forcefully. Do you want to do delete the image?
+Page.DeleteImage.Button.Yes=Yes, delete image.
+Page.DeleteImage.Button.No=No, don’t delete image.
+
+Page.EditAlbum.Title=Edit Album
+
+Page.DeleteAlbum.Title=Delete Album
+Page.DeleteAlbum.Page.Title=Delete Album
+Page.DeleteAlbum.Text.AlbumWillBeGone=This will remove your album “{title}”. Do you really want to do that?
+Page.DeleteAlbum.Button.Yes=Yes, delete album.
+Page.DeleteAlbum.Button.No=No, don’t delete album.
+
 Page.Trust.Title=Trust Sone - Sone
 
 Page.Distrust.Title=Distrust Sone - Sone
@@ -250,6 +302,8 @@ View.Sone.Status.Idle=This Sone is idle, i.e. not being inserted or downloaded.
 View.Sone.Status.Downloading=This Sone is currently being downloaded.
 View.Sone.Status.Inserting=This Sone is currently being inserted.
 
+View.SoneMenu.Link.AllAlbums=all albums
+
 View.Post.UnknownAuthor=(unknown)
 View.Post.WebOfTrustLink=web of trust profile
 View.Post.Permalink=link post
@@ -272,6 +326,15 @@ View.Trust.Tooltip.Trust=Trust this person
 View.Trust.Tooltip.Distrust=Assign negative trust to this person
 View.Trust.Tooltip.Untrust=Remove your trust assignment for this person
 
+View.CreateAlbum.Title=Create Album
+View.CreateAlbum.Label.Name=Name:
+View.CreateAlbum.Label.Description=Description:
+
+View.UploadImage.Title=Upload Image
+View.UploadImage.Label.Title=Title:
+View.UploadImage.Label.Description=Description:
+View.UploadImage.Button.UploadImage=Upload Image
+
 View.Time.InTheFuture=in the future
 View.Time.AFewSecondsAgo=a few seconds ago
 View.Time.HalfAMinuteAgo=about half a minute ago
@@ -300,12 +363,20 @@ WebInterface.DefaultText.BirthMonth=Month
 WebInterface.DefaultText.BirthYear=Year
 WebInterface.DefaultText.FieldName=Field name
 WebInterface.DefaultText.Option.InsertionDelay=Time to wait after a Sone is modified before insert (in seconds)
+WebInterface.DefaultText.Search=What are you looking for?
+WebInterface.DefaultText.CreateAlbum.Name=Album title
+WebInterface.DefaultText.CreateAlbum.Description=Album description
+WebInterface.DefaultText.EditAlbum.Title=Album title
+WebInterface.DefaultText.EditAlbum.Description=Album description
+WebInterface.DefaultText.UploadImage.Title=Image title
+WebInterface.DefaultText.UploadImage.Description=Image description
+WebInterface.DefaultText.EditImage.Title=Image title
+WebInterface.DefaultText.EditImage.Description=Image description
 WebInterface.DefaultText.Option.PostsPerPage=Number of posts to show on a page
 WebInterface.DefaultText.Option.CharactersPerPost=Number of characters per post after which to cut the post off
 WebInterface.DefaultText.Option.PositiveTrust=The positive trust to assign
 WebInterface.DefaultText.Option.NegativeTrust=The negative trust to assign
 WebInterface.DefaultText.Option.TrustComment=The comment to set in the web of trust
-WebInterface.DefaultText.Search=What are you looking for?
 WebInterface.Confirmation.DeletePostButton=Yes, delete!
 WebInterface.Confirmation.DeleteReplyButton=Yes, delete!
 WebInterface.SelectBox.Choose=Choose…
@@ -332,6 +403,9 @@ Notification.SoneRescued.Text=The following Sones have been rescued:
 Notification.SoneRescued.Text.RememberToUnlock=Please remember to control the posts and replies you have given and don’t forget to unlock your Sones!
 Notification.LockedSones.Text=The following Sones have been locked for more than 5 minutes. Please check if you really want to keep these Sones locked:
 Notification.NewVersion.Text=Version {version} of the Sone plugin was found. Download it from USK@nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI,DuQSUZiI~agF8c-6tjsFFGuZ8eICrzWCILB60nT8KKo,AQACAAE/sone/{edition}​!
+Notification.InsertingImages.Text=The following images are being inserted:
+Notification.InsertedImages.Text=The following images have been inserted:
+Notification.ImageInsertFailed.Text=The following images could not be inserted:
 Notification.Mention.ShortText=You have been mentioned.
 Notification.Mention.Text=You have been mentioned in the following posts:
 Notification.SoneInsert.Duration={0,number} {0,choice,0#seconds|1#second|1<seconds}
index a5328a0..6ad458e 100644 (file)
@@ -51,6 +51,10 @@ textarea {
        display: none;
 }
 
+#sone .toggle-link {
+       margin-top: 1em;
+}
+
 #sone #formPassword {
        display: none;
 }
@@ -97,6 +101,14 @@ textarea {
        border: none;
 }
 
+#sone .small-link {
+       font-size: 85%;
+}
+
+#sone .parsed {
+       white-space: pre-wrap;
+}
+
 #sone #main.offline {
        opacity: 0.5;
 }
@@ -628,6 +640,66 @@ textarea {
        position: relative;
 }
 
+#sone .backlinks {
+       font-size: 80%;
+       margin-bottom: 1em;
+}
+
+#sone .backlinks .backlink {
+       display: inline;
+}
+
+#sone .album {
+}
+
+#sone .image-row, #sone .album-row {
+       display: table-row;
+}
+
+#sone .image-container, #sone .album-container {
+       width: 250px;
+       height: 250px;
+       overflow: hidden;
+       padding: -1px;
+       border: solid 1px #000;
+}
+
+#sone .image, #sone .album {
+       display: table-cell;
+       vertical-align: top;
+       text-align: center;
+       padding: 0.5ex;
+}
+
+#sone .single-image img {
+       border: solid 1px #000;
+       background-color: #fff;
+}
+
+#sone .image .edit-image input, #sone .album .edit-album input {
+       width: 95%;
+}
+
+#sone .image .edit-image textarea, #sone .album .edit-album textarea {
+       width: 95%;
+}
+
+#sone .image .image-title, #sone .album .album-title {
+       font-weight: bold;
+}
+
+#sone .image .image-description, #sone .album .album-description {
+       text-align: left;
+       width: 195px;
+       word-wrap: break-word;
+       max-height: 5em;
+       overflow: auto;
+}
+
+#sone .backlinks .separator {
+       display: inline;
+}
+
 #sone #search {
        text-align: right;
 }
@@ -702,6 +774,10 @@ textarea {
        clear: both;
 }
 
+#sone h1.backlink {
+       margin-bottom: 0px;
+}
+
 #sone h2 {
        font-family: inherit;
        font-size: 150%;
@@ -714,7 +790,7 @@ textarea {
        font-weight: bold;
 }
 
-#sone input.default {
+#sone input.default, #sone textarea.default {
        color: #888;
 }
 
diff --git a/src/main/resources/static/images/unknown-image-0.png b/src/main/resources/static/images/unknown-image-0.png
new file mode 100644 (file)
index 0000000..50ba705
Binary files /dev/null and b/src/main/resources/static/images/unknown-image-0.png differ
index f6327e4..bfb925b 100644 (file)
@@ -858,11 +858,20 @@ function ajaxifyPost(postElement) {
 
        /* show Sone menu when hovering over the avatar. */
        $(postElement).find(".post-avatar").mouseover(function() {
-               $(".sone-menu:visible").fadeOut();
-               $(".sone-post-menu", postElement).mouseleave(function() {
-                       $(this).fadeOut();
-               }).fadeIn();
-               return false;
+               if (typeof currentSoneMenuTimeoutHandler != undefined) {
+                       clearTimeout(currentSoneMenuTimeoutHandler);
+               }
+               currentSoneMenuId = getPostId(this);
+               currentSoneMenuTimeoutHandler = setTimeout(function() {
+                       $(".sone-menu:visible").fadeOut();
+                       $(".sone-post-menu", postElement).mouseleave(function() {
+                               $(this).fadeOut();
+                       }).fadeIn();
+               }, 1000);
+       }).mouseleave(function() {
+               if (currentSoneMenuId = getPostId(this)) {
+                       clearTimeout(currentSoneMenuTimeoutHandler);
+               }
        });
        (function(postElement) {
                var soneId = $(".sone-menu-id", postElement).text();
@@ -988,11 +997,20 @@ function ajaxifyReply(replyElement) {
 
        /* show Sone menu when hovering over the avatar. */
        $(replyElement).find(".reply-avatar").mouseover(function() {
-               $(".sone-menu:visible").fadeOut();
-               $(".sone-reply-menu", replyElement).mouseleave(function() {
-                       $(this).fadeOut();
-               }).fadeIn();
-               return false;
+               if (typeof currentSoneMenuTimeoutHandler != undefined) {
+                       clearTimeout(currentSoneMenuTimeoutHandler);
+               }
+               currentSoneMenuId = getPostId(this) + "-" + getReplyId(this);
+               currentSoneMenuTimeoutHandler = setTimeout(function() {
+                       $(".sone-menu:visible").fadeOut();
+                       $(".sone-reply-menu", replyElement).mouseleave(function() {
+                               $(this).fadeOut();
+                       }).fadeIn();
+               }, 1000);
+       }).mouseleave(function() {
+               if (currentSoneMenuId = getPostId(this) + "-" + getReplyId(this)) {
+                       clearTimeout(currentSoneMenuTimeoutHandler);
+               }
        });
        (function(replyElement) {
                var soneId = $(".sone-menu-id", replyElement).text();
@@ -1828,6 +1846,12 @@ var online = true;
 var initiallyLoggedIn = $("#sone #loggedIn").text() == "true";
 var notLoggedIn = !initiallyLoggedIn;
 
+/** ID of the next-to-show Sone context menu. */
+var currentSoneMenuId;
+
+/** Timeout handler for the next-to-show Sone context menu. */
+var currentSoneMenuTimeoutHandler;
+
 $(document).ready(function() {
 
        /* this initializes the status update input field. */
diff --git a/src/main/resources/templates/createAlbum.html b/src/main/resources/templates/createAlbum.html
new file mode 100644 (file)
index 0000000..7df8103
--- /dev/null
@@ -0,0 +1,11 @@
+<%include include/head.html>
+
+       <h1><%= Page.CreateAlbum.Page.Title|l10n|html></h1>
+
+       <%if nameMissing>
+               <p><%= Page.CreateAlbum.Error.NameMissing|l10n|html></p>
+       <%/if>
+
+       <%include include/createAlbum.html>
+
+<%include include/tail.html>
diff --git a/src/main/resources/templates/deleteAlbum.html b/src/main/resources/templates/deleteAlbum.html
new file mode 100644 (file)
index 0000000..cb27c19
--- /dev/null
@@ -0,0 +1,14 @@
+<%include include/head.html>
+
+       <h1><%= Page.DeleteAlbum.Page.Title|l10n|html></h1>
+
+       <p><%= Page.DeleteAlbum.Text.AlbumWillBeGone|l10n|replace needle="{title}" replacementKey=album.title|html></p>
+
+       <form method="post">
+               <input type="hidden" name="formPassword" value="<% formPassword|html>" />
+               <input type="hidden" name="album" value="<%album.id|html>" />
+               <button type="submit" name="confirmDelete" value="1"><%= Page.DeleteAlbum.Button.Yes|l10n|html></button>
+               <button type="submit" name="abortDelete" value="1"><%= Page.DeleteAlbum.Button.No|l10n|html></button>
+       </form>
+
+<%include include/tail.html>
diff --git a/src/main/resources/templates/deleteImage.html b/src/main/resources/templates/deleteImage.html
new file mode 100644 (file)
index 0000000..68b403e
--- /dev/null
@@ -0,0 +1,14 @@
+<%include include/head.html>
+
+       <h1><%= Page.DeleteImage.Page.Title|l10n|html></h1>
+
+       <p><%= Page.DeleteImage.Text.ImageWillBeGone|l10n|replace needle="{image}" replacementKey=image.title|replace needle="{album}" replacementKey=image.album.title|html></p>
+
+       <form method="post">
+               <input type="hidden" name="formPassword" value="<% formPassword|html>" />
+               <input type="hidden" name="image" value="<%image.id|html>" />
+               <button type="submit" name="confirmDelete" value="1"><%= Page.DeleteImage.Button.Yes|l10n|html></button>
+               <button type="submit" name="abortDelete" value="1"><%= Page.DeleteImage.Button.No|l10n|html></button>
+       </form>
+
+<%include include/tail.html>
diff --git a/src/main/resources/templates/imageBrowser.html b/src/main/resources/templates/imageBrowser.html
new file mode 100644 (file)
index 0000000..d97fc25
--- /dev/null
@@ -0,0 +1,570 @@
+<%include include/head.html>
+
+       <div class="page-id hidden">image-browser</div>
+
+       <script language="javascript">
+
+               /* hide all those forms. */
+               function hideAndShowBlock(blockElement, clickToShowElement, clickToHideElement) {
+                       $(blockElement).hide();
+                       $(clickToShowElement).removeClass("hidden");
+                       $(clickToShowElement).click(function() {
+                               $(blockElement).slideDown();
+                               $(clickToShowElement).addClass("hidden");
+                               $(clickToHideElement).removeClass("hidden");
+                       });
+                       $(clickToHideElement).click(function() {
+                               $(blockElement).slideUp();
+                               $(clickToHideElement).addClass("hidden");
+                               $(clickToShowElement).removeClass("hidden");
+                       });
+               }
+
+               /* ID of the image currently being edited. */
+               var editingImageId = null;
+
+               /**
+                * Shows the form for editing an image.
+                *
+                * @param imageId The ID of the image to edit.
+                */
+               function editImage(imageId) {
+                       if (editingImageId != imageId) {
+                               cancelImageEditing();
+                       } else {
+                               return;
+                       }
+                       editingImageId = imageId;
+                       $(".show-data", getImage(imageId)).hide();
+                       $(".edit-data", getImage(imageId)).show();
+                       $(document).bind("click.sone", function(event) {
+                               if ($(event.target).closest("#image-" + imageId).size() == 0) {
+                                       cancelImageEditing();
+                               }
+                       });
+               }
+
+               /**
+                * Cancels all image editing.
+                */
+               function cancelImageEditing() {
+                       $(".image .show-data").show();
+                       $(".image .edit-data").hide();
+                       $("form.edit-image").each(function() {
+                               this.reset();
+                       });
+                       $(document).unbind("click.sone");
+                       editingImageId = null;
+               }
+
+               /**
+                * Returns the image element with the given ID.
+                *
+                * @param imageId The ID of the image
+                * @return The image element
+                */
+               function getImage(imageId) {
+                       return $("#sone .image .image-id:contains('" + imageId + "')").closest(".image");
+               }
+
+               /**
+                * Swaps two images.
+                *
+                * @param sourceId The ID of the source image
+                * @param destinationId The ID of the destionation image
+                */
+               function swapImage(sourceId, destinationId) {
+                       sourceElement = getImage(sourceId);
+                       destinationElement = getImage(destinationId);
+                       sourceParent = sourceElement.closest(".image-row");
+                       sourcePrevSibling = sourceElement.prev();
+                       sourceElement.detach();
+                       destinationElement.before(sourceElement);
+                       if (sourcePrevSibling.get(0) != destinationElement.get(0)) {
+                               destinationElement.detach();
+                               (sourcePrevSibling.size() > 0) ? sourcePrevSibling.after(destinationElement) : sourceParent.prepend(destinationElement);
+                       }
+                       if ($("button[name='moveLeft']", sourceElement).hasClass("hidden") != $("button[name='moveLeft']", destinationElement).hasClass("hidden")) {
+                               $("button[name='moveLeft']", sourceElement).toggleClass("hidden");
+                               $("button[name='moveLeft']", destinationElement).toggleClass("hidden");
+                       }
+                       if ($("button[name='moveRight']", sourceElement).hasClass("hidden") != $("button[name='moveRight']", destinationElement).hasClass("hidden")) {
+                               $("button[name='moveRight']", sourceElement).toggleClass("hidden");
+                               $("button[name='moveRight']", destinationElement).toggleClass("hidden");
+                       }
+               }
+
+               /**
+                * Prepare all images for inline editing.
+                */
+               function prepareImages() {
+                       $(".image").each(function() {
+                               imageId = $(this).closest(".image").find(".image-id").text();
+                               (function(element, imageId) {
+                                       $(".show-data", element).click(function() {
+                                               editImage(imageId);
+                                       });
+                                       $("button[name='moveLeft'], button[name='moveRight']", element).click(function() {
+                                               ajaxGet("editImage.ajax", { "formPassword": getFormPassword(), "image": imageId, "moveLeft": this.name == "moveLeft", "moveRight": this.name == "moveRight" }, function(data) {
+                                                       if (data && data.success) {
+                                                               swapImage(data.sourceImageId, data.destinationImageId);
+                                                       }
+                                               });
+                                               return false;
+                                       });
+                                       $("button[name='submit']", element).click(function() {
+                                               title = $(":input[name='title']:enabled", this.form).val();
+                                               description = $(":input[name='description']:enabled", this.form).val();
+                                               ajaxGet("editImage.ajax", { "formPassword": getFormPassword(), "image": imageId, "title": title, "description": description }, function(data) {
+                                                       if (data && data.success) {
+                                                               getImage(data.imageId).find(".image-title").text(data.title);
+                                                               getImage(data.imageId).find(".image-description").text(data.description);
+                                                               getImage(data.imageId).find(":input[name='title']").attr("defaultValue", title);
+                                                               getImage(data.imageId).find(":input[name='description']").attr("defaultValue", description);
+                                                               cancelImageEditing();
+                                                       }
+                                               });
+                                               return false;
+                                       });
+                               })(this, imageId);
+                       });
+               }
+
+               /* ID of the album currently being edited. */
+               var editingAlbumId = null;
+
+               /**
+                * Shows the form for editing an album.
+                *
+                * @param albumId The ID of the album to edit.
+                */
+               function editAlbum(albumId) {
+                       if (editingAlbumId != albumId) {
+                               if (editingAlbumId != null) {
+                                       cancelAlbumEditing();
+                               }
+                       } else {
+                               console.log("already editing " + albumId);
+                               return;
+                       }
+                       editingAlbumId = albumId;
+                       $(".show-data", getAlbum(albumId)).hide();
+                       $(".edit-data", getAlbum(albumId)).show();
+                       console.log(getAlbum(albumId));
+                       $(document).bind("click.sone", function(event) {
+                               if ($(event.target).closest("#album-" + albumId).size() == 0) {
+                                       cancelAlbumEditing();
+                               }
+                       });
+               }
+
+               /**
+                * Cancels all album editing.
+                */
+               function cancelAlbumEditing() {
+                       console.log("cancel-album-edit");
+                       $(".album .show-data").show();
+                       $(".album .edit-data").hide();
+                       $("form.edit-album").each(function() {
+                               this.reset();
+                       });
+                       $(document).unbind("click.sone");
+                       editingAlbumId = null;
+               }
+
+               /**
+                * Returns the album element with the given ID.
+                *
+                * @param albumId The ID of the album
+                * @return The album element
+                */
+               function getAlbum(albumId) {
+                       return $("#sone .album .album-id:contains('" + albumId + "')").closest(".album");
+               }
+
+               /**
+                * Swaps two albums.
+                *
+                * @param sourceId The ID of the source album
+                * @param destinationId The ID of the destionation album
+                */
+               function swapAlbum(sourceId, destinationId) {
+                       sourceElement = getAlbum(sourceId);
+                       destinationElement = getAlbum(destinationId);
+                       sourceParent = sourceElement.closest(".album-row");
+                       sourcePrevSibling = sourceElement.prev();
+                       sourceElement.detach();
+                       destinationElement.before(sourceElement);
+                       if (sourcePrevSibling.get(0) != destinationElement.get(0)) {
+                               destinationElement.detach();
+                               (sourcePrevSibling.size() > 0) ? sourcePrevSibling.after(destinationElement) : sourceParent.prepend(destinationElement);
+                       }
+                       if ($("button[name='moveLeft']", sourceElement).hasClass("hidden") != $("button[name='moveLeft']", destinationElement).hasClass("hidden")) {
+                               $("button[name='moveLeft']", sourceElement).toggleClass("hidden");
+                               $("button[name='moveLeft']", destinationElement).toggleClass("hidden");
+                       }
+                       if ($("button[name='moveRight']", sourceElement).hasClass("hidden") != $("button[name='moveRight']", destinationElement).hasClass("hidden")) {
+                               $("button[name='moveRight']", sourceElement).toggleClass("hidden");
+                               $("button[name='moveRight']", destinationElement).toggleClass("hidden");
+                       }
+               }
+
+               /**
+                * Prepare all albums for inline editing.
+                */
+               function prepareAlbums() {
+                       $(".album").each(function() {
+                               albumId = $(this).closest(".album").find(".album-id").text();
+                               (function(element, albumId) {
+                                       $(".show-data", element).click(function() {
+                                               console.log("show-data");
+                                               editAlbum(albumId);
+                                       });
+                                       $("button[name='moveLeft'], button[name='moveRight']", element).click(function() {
+                                               ajaxGet("editAlbum.ajax", { "formPassword": getFormPassword(), "album": albumId, "moveLeft": this.name == "moveLeft", "moveRight": this.name == "moveRight" }, function(data) {
+                                                       if (data && data.success) {
+                                                               swapAlbum(data.sourceAlbumId, data.destinationAlbumId);
+                                                       }
+                                               });
+                                               return false;
+                                       });
+                                       $("button[name='submit']", element).click(function() {
+                                               title = $(":input[name='title']:enabled", this.form).val();
+                                               description = $(":input[name='description']:enabled", this.form).val();
+                                               ajaxGet("editAlbum.ajax", { "formPassword": getFormPassword(), "album": albumId, "title": title, "description": description }, function(data) {
+                                                       if (data && data.success) {
+                                                               getAlbum(data.albumId).find(".album-title").text(data.title);
+                                                               getAlbum(data.albumId).find(".album-description").text(data.description);
+                                                               getAlbum(data.albumId).find(":input[name='title']").attr("defaultValue", title);
+                                                               getAlbum(data.albumId).find(":input[name='description']").attr("defaultValue", description);
+                                                               cancelAlbumEditing();
+                                                       }
+                                               });
+                                               return false;
+                                       });
+                               })(this, albumId);
+                       });
+               }
+
+       </script>
+
+       <%if albumRequested>
+
+               <%ifnull album>
+
+                       <p><%= Page.ImageBrowser.Album.Error.NotFound.Text|l10n|html></p>
+
+               <%elseifnull album.title>
+
+                       <p><%= Page.ImageBrowser.Album.Error.NotFound.Text|l10n|html></p>
+
+               <%else>
+
+                       <%if album.sone.local>
+                               <script language="javascript">
+
+                                       $(function() {
+                                               getTranslation("WebInterface.DefaultText.UploadImage.Title", function(text) {
+                                                       $("#upload-image :input[name='title']").each(function() {
+                                                               registerInputTextareaSwap(this, text, "title", false, true);
+                                                       });
+                                               });
+                                               getTranslation("WebInterface.DefaultText.UploadImage.Description", function(text) {
+                                                       $("#upload-image :input[name='description']").each(function() {
+                                                               registerInputTextareaSwap(this, text, "description", true, false);
+                                                       });
+                                               });
+                                               $("#upload-image label").hide();
+                                               getTranslation("WebInterface.DefaultText.CreateAlbum.Name", function(text) {
+                                                       $("#create-album input[name='name']").each(function() {
+                                                               registerInputTextareaSwap(this, text, "name", false, true);
+                                                       });
+                                               });
+                                               getTranslation("WebInterface.DefaultText.CreateAlbum.Description", function(text) {
+                                                       $("#create-album input[name='description']").each(function() {
+                                                               registerInputTextareaSwap(this, text, "description", true, true);
+                                                       });
+                                               });
+                                               $("#create-album label").hide();
+                                               getTranslation("WebInterface.DefaultText.EditAlbum.Title", function(text) {
+                                                       $("#edit-album input[name='title']").each(function() {
+                                                               registerInputTextareaSwap(this, text, "title", false, true);
+                                                       });
+                                               });
+                                               getTranslation("WebInterface.DefaultText.EditAlbum.Description", function(text) {
+                                                       $("#edit-album :input[name='description']").each(function() {
+                                                               registerInputTextareaSwap(this, text, "description", true, false);
+                                                       });
+                                               });
+                                               $("#edit-album label").hide();
+
+                                               hideAndShowBlock("div.edit-album", ".show-edit-album", ".hide-edit-album");
+                                               hideAndShowBlock("div.create-album", ".show-create-album", ".hide-create-album");
+                                               hideAndShowBlock("div.upload-image", ".show-upload-image", ".hide-upload-image");
+                                               hideAndShowBlock("div.delete-album", ".show-delete-album", ".hide-delete-album");
+
+                                               prepareAlbums();
+                                               prepareImages();
+                                       });
+                               </script>
+                       <%/if>
+
+                       <h1 class="backlink"><%= Page.ImageBrowser.Album.Title|l10n|replace needle='{album}' replacementKey=album.title|html></h1>
+
+                       <div class="backlinks">
+                               <%foreach album.backlinks backlink backlinks>
+                                       <div class="backlink">
+                                               <a href="<% backlink.target|html>"><% backlink.name|html></a>
+                                       </div>
+                                       <%if ! backlinks.last>
+                                               <div class="separator">&gt;</div>
+                                       <%/if>
+                               <%/foreach>
+                       </div>
+
+                       <p id="description"><% album.description|html></p>
+
+                       <%if album.sone.local>
+                               <div class="show-edit-album hidden toggle-link"><a class="small-link">» <%= Page.ImageBrowser.Album.Edit.Title|l10n|html></a></div>
+                               <div class="hide-edit-album hidden toggle-link"><a class="small-link">« <%= Page.ImageBrowser.Album.Edit.Title|l10n|html></a></div>
+                               <div class="edit-album">
+                                       <h2><%= Page.ImageBrowser.Album.Edit.Title|l10n|html></h2>
+
+                                       <form id="edit-album" action="editAlbum.html" method="post">
+                                               <input type="hidden" name="formPassword" value="<%formPassword|html>" />
+                                               <input type="hidden" name="album" value="<%album.id|html>" />
+
+                                               <%if ! album.images.empty>
+                                                       <div>
+                                                               <label for="album-image"><%= Page.ImageBrowser.Album.Label.AlbumImage|l10n|html></label>
+                                                               <select name="album-image">
+                                                                       <option disabled="disabled"><%= Page.ImageBrowser.Album.AlbumImage.Choose|l10n|html></option>
+                                                                       <%foreach album.images image>
+                                                                               <option value="<% image.id|html>"<%if album.albumImage.id|match key=image.id> selected="selected"<%/if>><% image.title|html></option>
+                                                                       <%/foreach>
+                                                               </select>
+                                                       </div>
+                                               <%/if>
+                                               <div>
+                                                       <label for="title"><%= Page.ImageBrowser.Album.Label.Title|l10n|html></label>
+                                                       <input type="text" name="title" value="<%album.title|html>" />
+                                               </div>
+                                               <div>
+                                                       <label for="description"><%= Page.ImageBrowser.Album.Label.Description|l10n|html></label>
+                                                       <textarea name="description"><%album.description|html></textarea>
+                                               </div>
+                                               <button type="submit"><%= Page.ImageBrowser.Album.Button.Save|l10n|html></button>
+                                       </form>
+                               </div>
+                       <%/if>
+
+                       <%include include/browseAlbums.html albums=album.albums>
+
+                       <%if album.sone.local>
+                               <div class="show-create-album hidden toggle-link"><a class="small-link">» <%= View.CreateAlbum.Title|l10n|html></a></div>
+                               <div class="hide-create-album hidden toggle-link"><a class="small-link">« <%= View.CreateAlbum.Title|l10n|html></a></div>
+                               <div class="create-album">
+                                       <%include include/createAlbum.html>
+                               </div>
+                       <%/if>
+
+                       <%foreach album.images image>
+                               <%first><h2><%= Page.ImageBrowser.Header.Images|l10n|html></h2><%/first>
+                               <%if loop.count|mod divisor=3><div class="image-row"><%/if>
+                               <div id="image-<% image.id|html>" class="image">
+                                       <div class="image-id hidden"><% image.id|html></div>
+                                       <div class="image-container">
+                                               <a href="imageBrowser.html?image=<%image.id|html>"><% image|image-link max-width=250 max-height=250 mode=enlarge title==image.title></a>
+                                       </div>
+                                       <div class="show-data">
+                                               <div class="image-title"><% image.title|html></div>
+                                               <div class="image-description"><% image.description|html></div>
+                                       </div>
+                                       <%if album.sone.local>
+                                               <form class="edit-image" action="editImage.html" method="post">
+                                                       <input type="hidden" name="formPassword" value="<%formPassword|html>" />
+                                                       <input type="hidden" name="returnPage" value="<%request.uri|html>" />
+                                                       <input type="hidden" name="image" value="<%image.id|html>" />
+
+                                                       <div class="edit-data hidden">
+                                                               <div>
+                                                                       <input type="text" name="title" value="<%image.title|html>" />
+                                                               </div>
+                                                               <div>
+                                                                       <textarea name="description"><%image.description|html></textarea>
+                                                               </div>
+                                                               <div>
+                                                                       <button <%first>class="hidden" <%/first>type="submit" name="moveLeft" value="true"><%= Page.ImageBrowser.Image.Button.MoveLeft|l10n|html></button>
+                                                                       <button type="submit" name="submit"><%= Page.ImageBrowser.Image.Button.Save|l10n|html></button>
+                                                                       <button <%last>class="hidden" <%/last>type="submit" name="moveRight" value="true"><%= Page.ImageBrowser.Image.Button.MoveRight|l10n|html></button>
+                                                               </div>
+                                                       </div>
+                                               </form>
+                                       <%/if>
+                               </div>
+                               <%= false|store key=endRow>
+                               <%if loop.count|mod divisor=3 offset=1><%= true|store key=endRow><%/if>
+                               <%last><%= true|store key=endRow><%/last>
+                               <%if endRow></div><%/if>
+                       <%/foreach>
+
+                       <%if album.sone.local>
+                               <div class="show-upload-image hidden toggle-link"><a class="small-link">» <%= View.UploadImage.Title|l10n|html></a></div>
+                               <div class="hide-upload-image hidden toggle-link"><a class="small-link">« <%= View.UploadImage.Title|l10n|html></a></div>
+                               <div class="upload-image">
+                                       <%include include/uploadImage.html>
+                               </div>
+
+                               <%if album.empty>
+                                       <div class="show-delete-album hidden toggle-link"><a class="small-link">» <%= Page.ImageBrowser.Album.Delete.Title|l10n|html></a></div>
+                                       <div class="hide-delete-album hidden toggle-link"><a class="small-link">« <%= Page.ImageBrowser.Album.Delete.Title|l10n|html></a></div>
+                                       <div class="delete-album">
+                                               <form id="delete-album" action="deleteAlbum.html" method="get">
+                                                       <input type="hidden" name="album" value="<%album.id|html>" />
+                                                       <button type="submit"><%= Page.ImageBrowser.Album.Button.Delete|l10n|html></button>
+                                               </form>
+                                       </div>
+                               <%/if>
+
+                       <%/if>
+
+               <%/if>
+
+       <%elseif imageRequested>
+
+               <h1 class="backlink"><%image.title|html></h1>
+
+               <div class="backlinks">
+                       <%foreach image.album.backlinks backlink backlinks>
+                               <div class="backlink">
+                                       <a href="<% backlink.target|html>"><% backlink.name|html></a>
+                               </div>
+                               <%if ! backlinks.last>
+                                       <div class="separator">&gt;</div>
+                               <%/if>
+                       <%/foreach>
+               </div>
+
+               <%ifnull image>
+
+               <%else>
+
+                       <%if image.sone.local>
+                               <script language="javascript">
+                                       $(function() {
+                                               getTranslation("WebInterface.DefaultText.EditImage.Title", function(text) {
+                                                       $("#edit-image input[name='title']").each(function() {
+                                                               registerInputTextareaSwap(this, text, "title", false, true);
+                                                       });
+                                               });
+                                               getTranslation("WebInterface.DefaultText.EditImage.Description", function(text) {
+                                                       $("#edit-image :input[name='description']").each(function() {
+                                                               registerInputTextareaSwap(this, text, "description", true, false);
+                                                       });
+                                               });
+                                               $("#edit-image label").hide();
+
+                                               hideAndShowBlock(".edit-image", ".show-edit-image", ".hide-edit-image");
+                                               hideAndShowBlock(".delete-image", ".show-delete-image", ".hide-delete-image");
+                                       });
+                               </script>
+                       <%/if>
+
+                       <div class="single-image">
+                               <%ifnull !image.key>
+                                       <a href="/<%image.key|html>"><% image|image-link max-width=640 max-height=480></a>
+                               <%else>
+                                       <a href="imageBrowser.html?image=<%image.id|html>"><% image|image-link max-width=640 max-height=480></a>
+                               <%/if>
+                       </div>
+
+                       <p class="parsed"><%image.description|parse sone=image.sone></p>
+
+                       <%if image.sone.local>
+
+                               <div class="show-edit-image hidden toggle-link"><a class="small-link">» <%= Page.ImageBrowser.Image.Edit.Title|l10n|html></a></div>
+                               <div class="hide-edit-image hidden toggle-link"><a class="small-link">« <%= Page.ImageBrowser.Image.Edit.Title|l10n|html></a></div>
+                               <div class="edit-image">
+                                       <h2><%= Page.ImageBrowser.Image.Edit.Title|l10n|html></h2>
+
+                                       <form id="edit-image" action="editImage.html" method="post">
+                                               <input type="hidden" name="formPassword" value="<%formPassword|html>" />
+                                               <input type="hidden" name="returnPage" value="<%request.uri|html>" />
+                                               <input type="hidden" name="image" value="<%image.id|html>" />
+
+                                               <div>
+                                                       <label for="title"><%= Page.ImageBrowser.Image.Title.Label|l10n|html></label>
+                                                       <input type="text" name="title" value="<%image.title|html>" />
+                                               </div>
+                                               <div>
+                                                       <label for="description"><%= Page.ImageBrowser.Image.Description.Label|l10n|html></label>
+                                                       <textarea name="description"><%image.description|html></textarea>
+                                               </div>
+                                               <div>
+                                                       <button type="submit"><%= Page.ImageBrowser.Image.Button.Save|l10n|html></button>
+                                               </div>
+                                       </form>
+                               </div>
+
+                               <div class="show-delete-image hidden toggle-link"><a class="small-link">» <%= Page.ImageBrowser.Image.Delete.Title|l10n|html></a></div>
+                               <div class="hide-delete-image hidden toggle-link"><a class="small-link">« <%= Page.ImageBrowser.Image.Delete.Title|l10n|html></a></div>
+                               <div class="delete-image">
+                                       <h2><%= Page.ImageBrowser.Image.Delete.Title|l10n|html></h2>
+
+                                       <form id="delete-image" action="deleteImage.html" method="get">
+                                               <input type="hidden" name="image" value="<%image.id|html>" />
+                                               <button type="submit"><%= Page.ImageBrowser.Image.Button.Delete|l10n|html></button>
+                                       </form>
+                               </div>
+
+                       <%/if>
+
+               <%/if>
+
+       <%elseif soneRequested>
+
+               <%if sone.local>
+                       <script language="javascript">
+                               $(function() {
+                                       getTranslation("WebInterface.DefaultText.CreateAlbum.Name", function(text) {
+                                               $("#create-album input[name='name']").each(function() {
+                                                       registerInputTextareaSwap(this, text, "name", false, true);
+                                               });
+                                       });
+                                       getTranslation("WebInterface.DefaultText.CreateAlbum.Description", function(text) {
+                                               $("#create-album input[name='description']").each(function() {
+                                                       registerInputTextareaSwap(this, text, "description", true, true);
+                                               });
+                                       });
+                                       $("#create-album label").hide();
+
+                                       hideAndShowBlock(".create-album", ".show-create-album", ".hide-create-album");
+
+                                       prepareAlbums();
+                               });
+                       </script>
+               <%/if>
+
+               <%ifnull sone>
+
+                       <p><%= Page.ImageBrowser.Sone.Error.NotFound.Text|l10n|html></p>
+
+               <%else>
+
+                       <h1><%= Page.ImageBrowser.Sone.Title|l10n|replace needle='{sone}' replacementKey=sone.niceName|html></h1>
+
+                       <%include include/browseAlbums.html albums=sone.albums>
+
+                       <%if sone.local>
+                               <div class="show-create-album hidden toggle-link"><a class="small-link">» <%= View.CreateAlbum.Title|l10n|html></a></div>
+                               <div class="hide-create-album hidden toggle-link"><a class="small-link">« <%= View.CreateAlbum.Title|l10n|html></a></div>
+                               <div class="create-album">
+                                       <%include include/createAlbum.html>
+                               </div>
+                       <%/if>
+
+               <%/if>
+
+       <%/if>
+
+<%include include/tail.html>
diff --git a/src/main/resources/templates/include/browseAlbums.html b/src/main/resources/templates/include/browseAlbums.html
new file mode 100644 (file)
index 0000000..b77bcd1
--- /dev/null
@@ -0,0 +1,45 @@
+<%foreach albums album>
+       <%first><h2><%= Page.ImageBrowser.Header.Albums|l10n|html></h2><%/first>
+       <%if loop.count|mod divisor=3><div class="album-row"><%/if>
+       <div id="album-<% album.id|html>" class="album">
+               <div class="album-id hidden"><% album.id|html></div>
+               <div class="album-container">
+                       <a href="imageBrowser.html?album=<% album.id|html>" title="<% album.title|html>">
+                               <%ifnull album.albumImage>
+                                       <img src="images/unknown-image-0.png" width="333" height="250" alt="<% album.title|html>" title="<% album.title|html>" style="position: relative; top: 0px; left: -41px;" />
+                               <%else><!-- TODO -->
+                                       <% album.albumImage|image-link max-width=250 max-height=250 mode=enlarge title==album.title>
+                               <%/if>
+                       </a>
+               </div>
+               <div class="show-data">
+                       <div class="album-title"><% album.title|html></div>
+                       <div class="album-description"><% album.description|html></div>
+               </div>
+               <%if album.sone.local>
+                       <form class="edit-album" action="editAlbum.html" method="post">
+                               <input type="hidden" name="formPassword" value="<%formPassword|html>" />
+                               <input type="hidden" name="returnPage" value="<%request.uri|html>" />
+                               <input type="hidden" name="album" value="<%album.id|html>" />
+
+                               <div class="edit-data hidden">
+                                       <div>
+                                               <input type="text" name="title" value="<%album.title|html>" />
+                                       </div>
+                                       <div>
+                                               <textarea name="description"><%album.description|html></textarea>
+                                       </div>
+                                       <div>
+                                               <button <%first>class="hidden" <%/first>type="submit" name="moveLeft" value="true"><%= Page.ImageBrowser.Image.Button.MoveLeft|l10n|html></button>
+                                               <button type="submit" name="submit"><%= Page.ImageBrowser.Album.Button.Save|l10n|html></button>
+                                               <button <%last>class="hidden" <%/last>type="submit" name="moveRight" value="true"><%= Page.ImageBrowser.Image.Button.MoveRight|l10n|html></button>
+                                       </div>
+                               </div>
+                       </form>
+               <%/if>
+       </div>
+       <%= false|store key=endRow>
+       <%if loop.count|mod divisor=3 offset=1><%= true|store key=endRow><%/if>
+       <%last><%= true|store key=endRow><%/last>
+       <%if endRow></div><%/if>
+<%/foreach>
diff --git a/src/main/resources/templates/include/createAlbum.html b/src/main/resources/templates/include/createAlbum.html
new file mode 100644 (file)
index 0000000..4887199
--- /dev/null
@@ -0,0 +1,11 @@
+<h2><%= View.CreateAlbum.Title|l10n|html></h2>
+
+<form id="create-album" method="post" action="createAlbum.html">
+       <input type="hidden" name="formPassword" value="<% formPassword|html>" />
+       <input type="hidden" name="parent" value="<%ifnull ! album><% album.id|html><%/if>" />
+       <label for="album"><%= View.CreateAlbum.Label.Name|l10n|html></label>
+       <input type="text" name="name" value="" />
+       <label for="description"><%= View.CreateAlbum.Label.Description|l10n|html></label>
+       <input type="text" name="description" value="" />
+       <button type="submit" name="uploadImage" value="1"><%= Page.ImageBrowser.CreateAlbum.Button.CreateAlbum|l10n|html></button>
+</form>
index e755662..a1dd944 100644 (file)
@@ -4,13 +4,18 @@
        <div class="inner-menu">
                <div>
                        <a class="author" href="viewSone.html?sone=<%sone.id|html>"><%sone.niceName|html></a>
-                       <span class="author-wot-link">(<a href="/WebOfTrust/ShowIdentity?id=<%sone.id|html>"><% =View.Post.WebOfTrustLink|l10n|html></a>)</span>
+                       (<%= View.Sone.Stats.Posts|l10n 0=sone.posts.size>, <%= View.Sone.Stats.Replies|l10n 0=sone.replies.size>)
                </div>
-               <div><%= View.Sone.Stats.Posts|l10n 0=sone.posts.size>, <%= View.Sone.Stats.Replies|l10n 0=sone.replies.size></div>
+               <div><a href="/WebOfTrust/ShowIdentity?id=<%sone.id|html>">» <% =View.Post.WebOfTrustLink|l10n|html></a></div>
+               <%foreach sone.albums album>
+                       <%first>
+                               <div><a href="imageBrowser.html?sone=<% sone.id|html>">» <% =View.SoneMenu.Link.AllAlbums|l10n|html></a></div>
+                       <%/first>
+               <%/foreach>
                <%if !sone.local>
                        <div>
-                               <a class="follow<%if sone.friend> hidden<%/if>"><%= View.Sone.Button.FollowSone|l10n|html></a>
-                               <a class="unfollow<%if !sone.friend> hidden<%/if>"><%= View.Sone.Button.UnfollowSone|l10n|html></a>
+                               <button class="follow<%if sone.friend> hidden<%/if>"><%= View.Sone.Button.FollowSone|l10n|html></button>
+                               <button class="unfollow<%if !sone.friend> hidden<%/if>"><%= View.Sone.Button.UnfollowSone|l10n|html></buton>
                        </div>
                <%/if>
        </div>
diff --git a/src/main/resources/templates/include/uploadImage.html b/src/main/resources/templates/include/uploadImage.html
new file mode 100644 (file)
index 0000000..fcee2f2
--- /dev/null
@@ -0,0 +1,12 @@
+<h2><%= View.UploadImage.Title|l10n|html></h2>
+
+<form id="upload-image" method="post" action="uploadImage.html" enctype="multipart/form-data">
+       <input type="hidden" name="formPassword" value="<% formPassword|html>" />
+       <input type="hidden" name="parent" value="<% album.id|html>" />
+       <label for="title"><%= View.UploadImage.Label.Title|l10n|html></label>
+       <input type="text" name="title" value="" />
+       <label for="description"><%= View.UploadImage.Label.Description|l10n|html></label>
+       <textarea name="description"></textarea>
+       <input type="file" name="image" />
+       <button type="submit" name="uploadImage" value="1"><%= View.UploadImage.Button.UploadImage|l10n|html></button>
+</form>
diff --git a/src/main/resources/templates/insert/include/album.xml b/src/main/resources/templates/insert/include/album.xml
new file mode 100644 (file)
index 0000000..cd845da
--- /dev/null
@@ -0,0 +1,23 @@
+<album>
+       <id><% album.id|xml></id>
+       <name><% album.name|xml></name>
+       <description><% album.description|xml></description>
+       <albums>
+               <%foreach album.albums album>
+               <%include insert/include/album.xml>
+               <%/foreach>
+       </albums>
+       <images>
+               <%foreach album.images image>
+               <image>
+                       <id><% image.id|xml></id>
+                       <creation-time><% image.creationTime|xml></creation-time>
+                       <key><% image.key|xml></key>
+                       <width><% image.width|xml></width>
+                       <height><% image.height|xml></height>
+                       <title><% image.title|xml></title>
+                       <description><% image.description|xml></description>
+               </image>
+               <%/foreach>
+       </images>
+</album>
index e25cac9..2e4f298 100644 (file)
                <%/foreach>
        </reply-likes>
 
+       <%foreach currentSone.albums album>
+       <%first>
+       <albums>
+               <%/first>
+               <album>
+                       <id><%album.id|xml></id>
+                       <%ifnull !album.parent>
+                       <parent><%album.parent.id|xml></parent>
+                       <%/if>
+                       <title><%album.title|xml></title>
+                       <description><%album.description|xml></description>
+                       <album-image><%album.albumImage.id|xml></album-image>
+                       <%foreach album.images image>
+                       <%first>
+                       <images>
+                               <%/first>
+                               <image>
+                                       <id><%image.id|xml></id>
+                                       <creation-time><%image.creationTime|xml></creation-time>
+                                       <key><%image.key|xml></key>
+                                       <title><%image.title|xml></title>
+                                       <description><%image.description|xml></description>
+                                       <width><%image.width|xml></width>
+                                       <height><%image.height|xml></height>
+                               </image>
+                               <%last>
+                       </images>
+                       <%/last>
+                       <%/foreach>
+               </album>
+               <%last>
+       </albums>
+       <%/last>
+       <%/foreach>
+
 </sone>
index d838134..ea97d00 100644 (file)
@@ -2,6 +2,14 @@
 
        <h1><%= Page.Invalid.Page.Title|l10n|html></h1>
 
-       <p><%= Page.Invalid.Text|l10n|html|replace needle="{link}" replacement='<a href="index.html">'|replace needle="{/link}" replacement='</a>'></p>
+       <%foreach messages message>
+               <%if message|substring start=0 length=1|match value='!'>
+                       <p class="error"><% message|substring start=1|parse></p>
+               <%else>
+                       <p><% message|parse></p>
+               <%/if>
+       <%foreachelse>
+               <p><%= Page.Invalid.Text|l10n|html|replace needle="{link}" replacement='<a href="index.html">'|replace needle="{/link}" replacement='</a>'></p>
+       <%/foreach>
 
 <%include include/tail.html>
diff --git a/src/main/resources/templates/notify/image-insert-failed-notification.html b/src/main/resources/templates/notify/image-insert-failed-notification.html
new file mode 100644 (file)
index 0000000..c4b3c67
--- /dev/null
@@ -0,0 +1,6 @@
+<div class="text">
+       <%= Notification.ImageInsertFailed.Text|l10n|html>
+       <%foreach images image>
+               <a href="imageBrowser.html?image=<%image.id|html>" title="<%image.title|html>"><%image.title|html></a><%notlast>,<%/notlast><%last>.<%/last>
+       <%/foreach>
+</div>
diff --git a/src/main/resources/templates/notify/inserted-images-notification.html b/src/main/resources/templates/notify/inserted-images-notification.html
new file mode 100644 (file)
index 0000000..f388d59
--- /dev/null
@@ -0,0 +1,6 @@
+<div class="text">
+       <%= Notification.InsertedImages.Text|l10n|html>
+       <%foreach images image>
+               <a href="imageBrowser.html?image=<%image.id|html>" title="<%image.title|html>"><%image.title|html></a><%notlast>,<%/notlast><%last>.<%/last>
+       <%/foreach>
+</div>
diff --git a/src/main/resources/templates/notify/inserting-images-notification.html b/src/main/resources/templates/notify/inserting-images-notification.html
new file mode 100644 (file)
index 0000000..efe930d
--- /dev/null
@@ -0,0 +1,6 @@
+<div class="text">
+       <%= Notification.InsertingImages.Text|l10n|html>
+       <%foreach images image>
+               <a href="imageBrowser.html?image=<%image.id|html>" title="<%image.title|html>"><%image.title|html></a><%notlast>,<%/notlast><%last>.<%/last>
+       <%/foreach>
+</div>
index 55cc2a8..1d87e46 100644 (file)
                                <div class="value"><% sone.niceName|html> (<a href="/WebOfTrust/ShowIdentity?id=<% sone.id|html>"><%= Page.ViewSone.Profile.Name.WoTLink|l10n|html></a>)</div>
                        </div>
 
+                       <%foreach sone.albums album>
+                               <%first>
+                                       <div class="profile-field">
+                                               <div class="name"><%= Page.ViewSone.Profile.Label.Albums|l10n|html></div>
+                                               <div class="value">
+                                                       <a href="imageBrowser.html?sone=<% sone.id|html>"><% =Page.ViewSone.Profile.Albums.Text.All|l10n|html></a>,
+                               <%/first>
+                                       <a href="imageBrowser.html?album=<%album.id|html>"><%album.title|html></a><%notlast>, <%/notlast>
+                               <%last>
+                                               </div>
+                                       </div>
+                               <%/last>
+                       <%/foreach>
+
                        <%foreach sone.profile.fields field>
                                <div class="profile-field">
                                        <div class="name"><% field.name|html></div>