Merge branch 'release-0.7.3' 0.7.3
authorDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Wed, 16 Nov 2011 19:20:39 +0000 (20:20 +0100)
committerDavid ‘Bombe’ Roden <bombe@pterodactylus.net>
Wed, 16 Nov 2011 19:20:39 +0000 (20:20 +0100)
43 files changed:
pom.xml
src/main/java/net/pterodactylus/sone/core/Core.java
src/main/java/net/pterodactylus/sone/core/FreenetInterface.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/SoneInsertException.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/core/SoneInserter.java
src/main/java/net/pterodactylus/sone/data/Sone.java
src/main/java/net/pterodactylus/sone/freenet/wot/DefaultOwnIdentity.java
src/main/java/net/pterodactylus/sone/freenet/wot/IdentityManager.java
src/main/java/net/pterodactylus/sone/main/SonePlugin.java
src/main/java/net/pterodactylus/sone/notify/ListNotificationFilters.java
src/main/java/net/pterodactylus/sone/template/ImageAccessor.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/template/ImageLinkFilter.java
src/main/java/net/pterodactylus/sone/web/BookmarksPage.java
src/main/java/net/pterodactylus/sone/web/CreateAlbumPage.java
src/main/java/net/pterodactylus/sone/web/EditAlbumPage.java
src/main/java/net/pterodactylus/sone/web/EditImagePage.java
src/main/java/net/pterodactylus/sone/web/FollowSonePage.java
src/main/java/net/pterodactylus/sone/web/GetImagePage.java
src/main/java/net/pterodactylus/sone/web/ImageBrowserPage.java
src/main/java/net/pterodactylus/sone/web/OptionsPage.java
src/main/java/net/pterodactylus/sone/web/UnfollowSonePage.java
src/main/java/net/pterodactylus/sone/web/UploadImagePage.java
src/main/java/net/pterodactylus/sone/web/ViewPostPage.java
src/main/java/net/pterodactylus/sone/web/ViewSonePage.java
src/main/java/net/pterodactylus/sone/web/WebInterface.java
src/main/java/net/pterodactylus/sone/web/ajax/EditAlbumAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/EditImageAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/FollowSoneAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/GetStatusAjaxPage.java
src/main/java/net/pterodactylus/sone/web/ajax/JsonPage.java
src/main/java/net/pterodactylus/sone/web/ajax/UnfollowSoneAjaxPage.java
src/main/java/net/pterodactylus/sone/web/page/FreenetPage.java [new file with mode: 0644]
src/main/java/net/pterodactylus/sone/web/page/FreenetTemplatePage.java
src/main/java/net/pterodactylus/sone/web/page/PageToadlet.java
src/main/resources/i18n/sone.en.properties
src/main/resources/static/css/sone.css
src/main/resources/static/javascript/sone.js
src/main/resources/templates/bookmarks.html
src/main/resources/templates/imageBrowser.html
src/main/resources/templates/include/browseAlbums.html
src/main/resources/templates/options.html

diff --git a/pom.xml b/pom.xml
index 35203cf..a0a267d 100644 (file)
--- a/pom.xml
+++ b/pom.xml
@@ -2,12 +2,12 @@
        <modelVersion>4.0.0</modelVersion>
        <groupId>net.pterodactylus</groupId>
        <artifactId>sone</artifactId>
-       <version>0.7.2</version>
+       <version>0.7.3</version>
        <dependencies>
                <dependency>
                        <groupId>net.pterodactylus</groupId>
                        <artifactId>utils</artifactId>
-                       <version>0.11.1</version>
+                       <version>0.11.2</version>
                </dependency>
                <dependency>
                        <groupId>junit</groupId>
@@ -18,7 +18,7 @@
                <dependency>
                        <groupId>org.freenetproject</groupId>
                        <artifactId>fred</artifactId>
-                       <version>0.7.5.1336</version>
+                       <version>0.7.5.1405</version>
                        <scope>provided</scope>
                </dependency>
                <dependency>
index ee5ec87..d3f50a1 100644 (file)
@@ -136,6 +136,9 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
        /* synchronize access on itself. */
        private final Map<Sone, SoneStatus> soneStatuses = new HashMap<Sone, SoneStatus>();
 
+       /** The times Sones were followed. */
+       private final Map<Sone, Long> soneFollowingTimes = new HashMap<Sone, Long>();
+
        /** Locked local Sones. */
        /* synchronize on itself. */
        private final Set<Sone> lockedSones = new HashSet<Sone>();
@@ -588,6 +591,23 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
        }
 
        /**
+        * Returns the time when the given was first followed by any local Sone.
+        *
+        * @param sone
+        *            The Sone to get the time for
+        * @return The time (in milliseconds since Jan 1, 1970) the Sone has first
+        *         been followed, or {@link Long#MAX_VALUE}
+        */
+       public long getSoneFollowingTime(Sone sone) {
+               synchronized (soneFollowingTimes) {
+                       if (soneFollowingTimes.containsKey(sone)) {
+                               return soneFollowingTimes.get(sone);
+                       }
+                       return Long.MAX_VALUE;
+               }
+       }
+
+       /**
         * Returns whether the target Sone is trusted by the origin Sone.
         *
         * @param origin
@@ -1008,7 +1028,11 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
                }
                Sone sone = addLocalSone(ownIdentity);
                sone.getOptions().addBooleanOption("AutoFollow", new DefaultOption<Boolean>(false));
-               sone.addFriend("nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI");
+               sone.getOptions().addBooleanOption("EnableSoneInsertNotifications", new DefaultOption<Boolean>(false));
+               sone.getOptions().addBooleanOption("ShowNotification/NewSones", new DefaultOption<Boolean>(true));
+               sone.getOptions().addBooleanOption("ShowNotification/NewPosts", new DefaultOption<Boolean>(true));
+               sone.getOptions().addBooleanOption("ShowNotification/NewReplies", new DefaultOption<Boolean>(true));
+               followSone(sone, getSone("nwa8lHa271k2QvJ8aa0Ov7IHAV-DFOCFgmDt3X6BpCI"));
                touchConfiguration();
                return sone;
        }
@@ -1041,13 +1065,11 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
                                        coreListenerManager.fireNewSoneFound(sone);
                                        for (Sone localSone : getLocalSones()) {
                                                if (localSone.getOptions().getBooleanOption("AutoFollow").get()) {
-                                                       localSone.addFriend(sone.getId());
-                                                       touchConfiguration();
+                                                       followSone(localSone, sone);
                                                }
                                        }
                                }
                        }
-                       remoteSones.put(identity.getId(), sone);
                        soneDownloader.addSone(sone);
                        setSoneStatus(sone, SoneStatus.unknown);
                        soneDownloaders.execute(new Runnable() {
@@ -1064,6 +1086,90 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
        }
 
        /**
+        * Lets the given local Sone follow the Sone with the given ID.
+        *
+        * @param sone
+        *            The local Sone that should follow another Sone
+        * @param soneId
+        *            The ID of the Sone to follow
+        */
+       public void followSone(Sone sone, String soneId) {
+               Validation.begin().isNotNull("Sone", sone).isNotNull("Sone ID", soneId).check();
+               followSone(sone, getSone(soneId));
+       }
+
+       /**
+        * Lets the given local Sone follow the other given Sone. If the given Sone
+        * was not followed by any local Sone before, this will mark all elements of
+        * the followed Sone as read that have been created before the current
+        * moment.
+        *
+        * @param sone
+        *            The local Sone that should follow the other Sone
+        * @param followedSone
+        *            The Sone that should be followed
+        */
+       public void followSone(Sone sone, Sone followedSone) {
+               Validation.begin().isNotNull("Sone", sone).isNotNull("Followed Sone", followedSone).check();
+               sone.addFriend(followedSone.getId());
+               synchronized (soneFollowingTimes) {
+                       if (!soneFollowingTimes.containsKey(followedSone)) {
+                               long now = System.currentTimeMillis();
+                               soneFollowingTimes.put(followedSone, now);
+                               for (Post post : followedSone.getPosts()) {
+                                       if (post.getTime() < now) {
+                                               markPostKnown(post);
+                                       }
+                               }
+                               for (PostReply reply : followedSone.getReplies()) {
+                                       if (reply.getTime() < now) {
+                                               markReplyKnown(reply);
+                                       }
+                               }
+                       }
+               }
+               touchConfiguration();
+       }
+
+       /**
+        * Lets the given local Sone unfollow the Sone with the given ID.
+        *
+        * @param sone
+        *            The local Sone that should unfollow another Sone
+        * @param soneId
+        *            The ID of the Sone being unfollowed
+        */
+       public void unfollowSone(Sone sone, String soneId) {
+               Validation.begin().isNotNull("Sone", sone).isNotNull("Sone ID", soneId).check();
+               unfollowSone(sone, getSone(soneId, false));
+       }
+
+       /**
+        * Lets the given local Sone unfollow the other given Sone. If the given
+        * local Sone is the last local Sone that followed the given Sone, its
+        * following time will be removed.
+        *
+        * @param sone
+        *            The local Sone that should unfollow another Sone
+        * @param unfollowedSone
+        *            The Sone being unfollowed
+        */
+       public void unfollowSone(Sone sone, Sone unfollowedSone) {
+               Validation.begin().isNotNull("Sone", sone).isNotNull("Unfollowed Sone", unfollowedSone).check();
+               sone.removeFriend(unfollowedSone.getId());
+               boolean unfollowedSoneStillFollowed = false;
+               for (Sone localSone : getLocalSones()) {
+                       unfollowedSoneStillFollowed |= localSone.hasFriend(unfollowedSone.getId());
+               }
+               if (!unfollowedSoneStillFollowed) {
+                       synchronized (soneFollowingTimes) {
+                               soneFollowingTimes.remove(unfollowedSone);
+                       }
+               }
+               touchConfiguration();
+       }
+
+       /**
         * Retrieves the trust relationship from the origin to the target. If the
         * trust relationship can not be retrieved, {@code null} is returned.
         *
@@ -1195,9 +1301,13 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
                                synchronized (newPosts) {
                                        for (Post post : sone.getPosts()) {
                                                post.setSone(storedSone);
-                                               if (!storedPosts.contains(post) && !knownPosts.contains(post.getId())) {
-                                                       newPosts.add(post.getId());
-                                                       coreListenerManager.fireNewPostFound(post);
+                                               if (!storedPosts.contains(post)) {
+                                                       if (post.getTime() < getSoneFollowingTime(sone)) {
+                                                               knownPosts.add(post.getId());
+                                                       } else if (!knownPosts.contains(post.getId())) {
+                                                               newPosts.add(post.getId());
+                                                               coreListenerManager.fireNewPostFound(post);
+                                                       }
                                                }
                                                posts.put(post.getId(), post);
                                        }
@@ -1216,9 +1326,13 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
                                synchronized (newReplies) {
                                        for (PostReply reply : sone.getReplies()) {
                                                reply.setSone(storedSone);
-                                               if (!storedReplies.contains(reply) && !knownReplies.contains(reply.getId())) {
-                                                       newReplies.add(reply.getId());
-                                                       coreListenerManager.fireNewReplyFound(reply);
+                                               if (!storedReplies.contains(reply)) {
+                                                       if (reply.getTime() < getSoneFollowingTime(sone)) {
+                                                               knownReplies.add(reply.getId());
+                                                       } else if (!knownReplies.contains(reply.getId())) {
+                                                               newReplies.add(reply.getId());
+                                                               coreListenerManager.fireNewReplyFound(reply);
+                                                       }
                                                }
                                                replies.put(reply.getId(), reply);
                                        }
@@ -1343,6 +1457,9 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
                /* initialize options. */
                sone.getOptions().addBooleanOption("AutoFollow", new DefaultOption<Boolean>(false));
                sone.getOptions().addBooleanOption("EnableSoneInsertNotifications", new DefaultOption<Boolean>(false));
+               sone.getOptions().addBooleanOption("ShowNotification/NewSones", new DefaultOption<Boolean>(true));
+               sone.getOptions().addBooleanOption("ShowNotification/NewPosts", new DefaultOption<Boolean>(true));
+               sone.getOptions().addBooleanOption("ShowNotification/NewReplies", new DefaultOption<Boolean>(true));
 
                /* load Sone. */
                String sonePrefix = "Sone/" + sone.getId();
@@ -1505,6 +1622,9 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
                /* 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));
+               sone.getOptions().getBooleanOption("ShowNotification/NewSones").set(configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewSones").getValue(null));
+               sone.getOptions().getBooleanOption("ShowNotification/NewPosts").set(configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewPosts").getValue(null));
+               sone.getOptions().getBooleanOption("ShowNotification/NewReplies").set(configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewReplies").getValue(null));
 
                /* if we’re still here, Sone was loaded successfully. */
                synchronized (sone) {
@@ -1514,7 +1634,9 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
                        sone.setReplies(replies);
                        sone.setLikePostIds(likedPostIds);
                        sone.setLikeReplyIds(likedReplyIds);
-                       sone.setFriends(friends);
+                       for (String friendId : friends) {
+                               followSone(sone, friendId);
+                       }
                        sone.setAlbums(topLevelAlbums);
                        soneInserters.get(sone).setLastInsertFingerprint(lastInsertFingerprint);
                }
@@ -2131,6 +2253,9 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
 
                        /* save options. */
                        configuration.getBooleanValue(sonePrefix + "/Options/AutoFollow").setValue(sone.getOptions().getBooleanOption("AutoFollow").getReal());
+                       configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewSones").setValue(sone.getOptions().getBooleanOption("ShowNotification/NewSones").getReal());
+                       configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewPosts").setValue(sone.getOptions().getBooleanOption("ShowNotification/NewPosts").getReal());
+                       configuration.getBooleanValue(sonePrefix + "/Options/ShowNotification/NewReplies").setValue(sone.getOptions().getBooleanOption("ShowNotification/NewReplies").getReal());
                        configuration.getBooleanValue(sonePrefix + "/Options/EnableSoneInsertNotifications").setValue(sone.getOptions().getBooleanOption("EnableSoneInsertNotifications").getReal());
 
                        configuration.save();
@@ -2183,6 +2308,17 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
                                configuration.getStringValue("KnownSone/" + soneCounter + "/ID").setValue(null);
                        }
 
+                       /* save Sone following times. */
+                       soneCounter = 0;
+                       synchronized (soneFollowingTimes) {
+                               for (Entry<Sone, Long> soneFollowingTime : soneFollowingTimes.entrySet()) {
+                                       configuration.getStringValue("SoneFollowingTimes/" + soneCounter + "/Sone").setValue(soneFollowingTime.getKey().getId());
+                                       configuration.getLongValue("SoneFollowingTimes/" + soneCounter + "/Time").setValue(soneFollowingTime.getValue());
+                                       ++soneCounter;
+                               }
+                               configuration.getStringValue("SoneFollowingTimes/" + soneCounter + "/Sone").setValue(null);
+                       }
+
                        /* save known posts. */
                        int postCounter = 0;
                        synchronized (newPosts) {
@@ -2299,6 +2435,20 @@ public class Core extends AbstractService implements IdentityListener, UpdateLis
                        }
                }
 
+               /* load Sone following times. */
+               soneCounter = 0;
+               while (true) {
+                       String soneId = configuration.getStringValue("SoneFollowingTimes/" + soneCounter + "/Sone").getValue(null);
+                       if (soneId == null) {
+                               break;
+                       }
+                       long time = configuration.getLongValue("SoneFollowingTimes/" + soneCounter + "/Time").getValue(Long.MAX_VALUE);
+                       synchronized (soneFollowingTimes) {
+                               soneFollowingTimes.put(getSone(soneId), time);
+                       }
+                       ++soneCounter;
+               }
+
                /* load known posts. */
                int postCounter = 0;
                while (true) {
index 39f342c..c63231a 100644 (file)
@@ -26,7 +26,6 @@ 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;
@@ -154,7 +153,7 @@ public class FreenetInterface {
                        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);
+                       throw new SoneInsertException("Could not start image insert.", ie1);
                }
        }
 
@@ -175,7 +174,7 @@ public class FreenetInterface {
                try {
                        return client.insertManifest(insertUri, manifestEntries, defaultFile);
                } catch (InsertException ie1) {
-                       throw new SoneException(null, ie1);
+                       throw new SoneException(ie1);
                }
        }
 
@@ -452,6 +451,14 @@ public class FreenetInterface {
                 * {@inheritDoc}
                 */
                @Override
+               public void onGeneratedMetadata(Bucket metadata, BaseClientPutter clientPutter, ObjectContainer objectContainer) {
+                       /* ignore, we don’t care. */
+               }
+
+               /**
+                * {@inheritDoc}
+                */
+               @Override
                public void onGeneratedURI(FreenetURI generatedUri, BaseClientPutter clientPutter, ObjectContainer objectContainer) {
                        resultingUri = generatedUri;
                }
index db26d80..b085ade 100644 (file)
@@ -219,8 +219,10 @@ public class SoneDownloader extends AbstractService {
         * @param soneInputStream
         *            The input stream to parse the Sone from
         * @return The parsed Sone
+        * @throws SoneException
+        *             if a parse error occurs, or the protocol is invalid
         */
-       public Sone parseSone(Sone originalSone, InputStream soneInputStream) {
+       public Sone parseSone(Sone originalSone, InputStream soneInputStream) throws SoneException {
                /* TODO - impose a size limit? */
 
                Document document;
@@ -336,8 +338,8 @@ public class SoneDownloader extends AbstractService {
                if (profileFieldsXml != null) {
                        for (SimpleXML fieldXml : profileFieldsXml.getNodes("field")) {
                                String fieldName = fieldXml.getValue("field-name", null);
-                               String fieldValue = fieldXml.getValue("field-value", null);
-                               if ((fieldName == null) || (fieldValue == null)) {
+                               String fieldValue = fieldXml.getValue("field-value", "");
+                               if (fieldName == null) {
                                        logger.log(Level.WARNING, "Downloaded profile field for Sone %s with missing data! Name: %s, Value: %s", new Object[] { sone, fieldName, fieldValue });
                                        return null;
                                }
index 271627e..683a148 100644 (file)
@@ -25,88 +25,42 @@ package net.pterodactylus.sone.core;
 public class SoneException extends Exception {
 
        /**
-        * Defines the different error. This is an enum instead of custom exceptions
-        * to keep the number of exceptions down. Specialized exceptions might still
-        * exist, though.
-        */
-       public static enum Type {
-
-               /** An invalid Sone name was specified. */
-               INVALID_SONE_NAME,
-
-               /** An invalid URI was specified. */
-               INVALID_URI,
-
-               /** An insert failed. */
-               INSERT_FAILED,
-
-       }
-
-       /** The type of the exception. */
-       private final Type type;
-
-       /**
         * Creates a new Sone exception.
-        *
-        * @param type
-        *            The type of the occured error
         */
-       public SoneException(Type type) {
-               this.type = type;
+       public SoneException() {
+               super();
        }
 
        /**
         * Creates a new Sone exception.
         *
-        * @param type
-        *            The type of the occured error
         * @param message
         *            The message of the exception
         */
-       public SoneException(Type type, String message) {
+       public SoneException(String message) {
                super(message);
-               this.type = type;
        }
 
        /**
         * Creates a new Sone exception.
         *
-        * @param type
-        *            The type of the occured error
         * @param cause
         *            The cause of the exception
         */
-       public SoneException(Type type, Throwable cause) {
+       public SoneException(Throwable cause) {
                super(cause);
-               this.type = type;
        }
 
        /**
         * Creates a new Sone exception.
         *
-        * @param type
-        *            The type of the occured error
         * @param message
         *            The message of the exception
         * @param cause
         *            The cause of the exception
         */
-       public SoneException(Type type, String message, Throwable cause) {
+       public SoneException(String message, Throwable cause) {
                super(message, cause);
-               this.type = type;
-       }
-
-       //
-       // ACCESSORS
-       //
-
-       /**
-        * Returns the type of this exception.
-        *
-        * @return The type of this exception (may be {@code null})
-        */
-       public Type getType() {
-               return type;
        }
 
 }
diff --git a/src/main/java/net/pterodactylus/sone/core/SoneInsertException.java b/src/main/java/net/pterodactylus/sone/core/SoneInsertException.java
new file mode 100644 (file)
index 0000000..350c3d8
--- /dev/null
@@ -0,0 +1,66 @@
+/*
+ * Sone - SoneInsertException.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;
+
+/**
+ * Exception that signals a problem with an insert.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class SoneInsertException extends SoneException {
+
+       /**
+        * Creates a new Sone insert exception.
+        */
+       public SoneInsertException() {
+               super();
+       }
+
+       /**
+        * Creates a new Sone insert exception.
+        *
+        * @param message
+        *            The message of the exception
+        */
+       public SoneInsertException(String message) {
+               super(message);
+       }
+
+       /**
+        * Creates a new Sone insert exception.
+        *
+        * @param cause
+        *            The cause of the exception
+        */
+       public SoneInsertException(Throwable cause) {
+               super(cause);
+       }
+
+       /**
+        * Creates a new Sone insert exception.
+        *
+        * @param message
+        *            The message of the exception
+        * @param cause
+        *            The cause of the exception
+        */
+       public SoneInsertException(String message, Throwable cause) {
+               super(message, cause);
+       }
+
+}
index eb74dc0..4d9878f 100644 (file)
@@ -190,6 +190,7 @@ public class SoneInserter extends AbstractService {
        @Override
        protected void serviceRun() {
                long lastModificationTime = 0;
+               String lastInsertedFingerprint = lastInsertFingerprint;
                String lastFingerprint = "";
                while (!shouldStop()) { try {
                        /* check every seconds. */
@@ -199,7 +200,7 @@ public class SoneInserter extends AbstractService {
                        if (core.isLocked(sone)) {
                                /* trigger redetection when the Sone is unlocked. */
                                synchronized (sone) {
-                                       modified = !sone.getFingerprint().equals(lastInsertFingerprint);
+                                       modified = !sone.getFingerprint().equals(lastInsertedFingerprint);
                                }
                                lastFingerprint = "";
                                lastModificationTime = 0;
@@ -210,7 +211,7 @@ public class SoneInserter extends AbstractService {
                        synchronized (sone) {
                                String fingerprint = sone.getFingerprint();
                                if (!fingerprint.equals(lastFingerprint)) {
-                                       if (fingerprint.equals(lastInsertFingerprint)) {
+                                       if (fingerprint.equals(lastInsertedFingerprint)) {
                                                modified = false;
                                                lastModificationTime = 0;
                                                logger.log(Level.FINE, "Sone %s has been reverted to last insert state.", sone);
@@ -222,7 +223,7 @@ public class SoneInserter extends AbstractService {
                                        lastFingerprint = fingerprint;
                                }
                                if (modified && (lastModificationTime > 0) && ((System.currentTimeMillis() - lastModificationTime) > (insertionDelay * 1000))) {
-                                       lastInsertFingerprint = fingerprint;
+                                       lastInsertedFingerprint = fingerprint;
                                        insertInformation = new InsertInformation(sone);
                                }
                        }
@@ -261,9 +262,11 @@ public class SoneInserter extends AbstractService {
                                 */
                                if (success) {
                                        synchronized (sone) {
-                                               if (lastInsertFingerprint.equals(sone.getFingerprint())) {
+                                               if (lastInsertedFingerprint.equals(sone.getFingerprint())) {
                                                        logger.log(Level.FINE, "Sone “%s” was not modified further, resetting counter…", new Object[] { sone });
                                                        lastModificationTime = 0;
+                                                       lastInsertFingerprint = lastInsertedFingerprint;
+                                                       core.touchConfiguration();
                                                        modified = false;
                                                }
                                        }
index ca970c2..8b2ec2a 100644 (file)
@@ -115,6 +115,15 @@ public class Sone implements Fingerprintable, Comparable<Sone> {
 
        };
 
+       /** Filter that matches Sones that have at least one album. */
+       public static final Filter<Sone> HAS_ALBUM_FILTER = new Filter<Sone>() {
+
+               @Override
+               public boolean filterObject(Sone sone) {
+                       return !sone.getAlbums().isEmpty();
+               }
+       };
+
        /** The logger. */
        private static final Logger logger = Logging.getLogger(Sone.class);
 
@@ -380,19 +389,6 @@ public class Sone implements Fingerprintable, Comparable<Sone> {
        }
 
        /**
-        * Sets all friends of this Sone at once.
-        *
-        * @param friends
-        *            The new (and only) friends of this Sone
-        * @return This Sone (for method chaining)
-        */
-       public Sone setFriends(Collection<String> friends) {
-               friendSones.clear();
-               friendSones.addAll(friends);
-               return this;
-       }
-
-       /**
         * Returns whether this Sone has the given Sone as a friend Sone.
         *
         * @param friendSoneId
index ab46b43..be2a3e7 100644 (file)
@@ -57,6 +57,22 @@ public class DefaultOwnIdentity extends DefaultIdentity implements OwnIdentity {
                this.insertUri = insertUri;
        }
 
+       /**
+        * Copy constructor for an own identity.
+        *
+        * @param webOfTrustConnector
+        *            The web of trust connector
+        * @param ownIdentity
+        *            The own identity to copy
+        */
+       public DefaultOwnIdentity(WebOfTrustConnector webOfTrustConnector, OwnIdentity ownIdentity) {
+               super(webOfTrustConnector, ownIdentity.getId(), ownIdentity.getNickname(), ownIdentity.getRequestUri());
+               this.webOfTrustConnector = webOfTrustConnector;
+               this.insertUri = ownIdentity.getInsertUri();
+               setContextsPrivate(ownIdentity.getContexts());
+               setPropertiesPrivate(ownIdentity.getProperties());
+       }
+
        //
        // ACCESSORS
        //
index 3c97e55..69af0ca 100644 (file)
@@ -26,6 +26,8 @@ import java.util.logging.Level;
 import java.util.logging.Logger;
 
 import net.pterodactylus.sone.freenet.plugin.PluginException;
+import net.pterodactylus.util.collection.Mapper;
+import net.pterodactylus.util.collection.Mappers;
 import net.pterodactylus.util.logging.Logging;
 import net.pterodactylus.util.service.AbstractService;
 
@@ -140,7 +142,7 @@ public class IdentityManager extends AbstractService {
                Set<OwnIdentity> allOwnIdentities = getAllOwnIdentities();
                for (OwnIdentity ownIdentity : allOwnIdentities) {
                        if (ownIdentity.getId().equals(id)) {
-                               return ownIdentity;
+                               return new DefaultOwnIdentity(webOfTrustConnector, ownIdentity);
                        }
                }
                return null;
@@ -159,7 +161,17 @@ public class IdentityManager extends AbstractService {
                                newOwnIdentities.put(ownIdentity.getId(), ownIdentity);
                        }
                        checkOwnIdentities(newOwnIdentities);
-                       return ownIdentities;
+                       return Mappers.mappedSet(ownIdentities, new Mapper<OwnIdentity, OwnIdentity>() {
+
+                               /**
+                                * {@inheritDoc}
+                                */
+                               @Override
+                               @SuppressWarnings("synthetic-access")
+                               public OwnIdentity map(OwnIdentity input) {
+                                       return new DefaultOwnIdentity(webOfTrustConnector, input);
+                               }
+                       });
                } catch (WebOfTrustException wote1) {
                        logger.log(Level.WARNING, "Could not load all own identities!", wote1);
                        return Collections.emptySet();
@@ -295,15 +307,17 @@ public class IdentityManager extends AbstractService {
 
                        /* find removed own identities: */
                        for (OwnIdentity oldOwnIdentity : currentOwnIdentities.values()) {
-                               if (!newOwnIdentities.containsKey(oldOwnIdentity.getId())) {
-                                       identityListenerManager.fireOwnIdentityRemoved(oldOwnIdentity);
+                               OwnIdentity newOwnIdentity = newOwnIdentities.get(oldOwnIdentity.getId());
+                               if ((newOwnIdentity == null) || ((context != null) && oldOwnIdentity.hasContext(context) && !newOwnIdentity.hasContext(context))) {
+                                       identityListenerManager.fireOwnIdentityRemoved(new DefaultOwnIdentity(webOfTrustConnector, oldOwnIdentity));
                                }
                        }
 
                        /* find added own identities. */
                        for (OwnIdentity currentOwnIdentity : newOwnIdentities.values()) {
-                               if (!currentOwnIdentities.containsKey(currentOwnIdentity.getId())) {
-                                       identityListenerManager.fireOwnIdentityAdded(currentOwnIdentity);
+                               OwnIdentity oldOwnIdentity = currentOwnIdentities.get(currentOwnIdentity.getId());
+                               if (((oldOwnIdentity == null) && ((context == null) || currentOwnIdentity.hasContext(context))) || ((oldOwnIdentity != null) && (context != null) && (!oldOwnIdentity.hasContext(context) && currentOwnIdentity.hasContext(context)))) {
+                                       identityListenerManager.fireOwnIdentityAdded(new DefaultOwnIdentity(webOfTrustConnector, currentOwnIdentity));
                                }
                        }
 
index d93317a..b35c556 100644 (file)
@@ -83,7 +83,7 @@ public class SonePlugin implements FredPlugin, FredPluginFCP, FredPluginL10n, Fr
        }
 
        /** The version. */
-       public static final Version VERSION = new Version(0, 7, 2);
+       public static final Version VERSION = new Version(0, 7, 3);
 
        /** The logger. */
        private static final Logger logger = Logging.getLogger(SonePlugin.class);
index beeb81e..7b9eb49 100644 (file)
@@ -55,12 +55,23 @@ public class ListNotificationFilters {
        public static List<Notification> filterNotifications(Collection<? extends Notification> notifications, Sone currentSone) {
                List<Notification> filteredNotifications = new ArrayList<Notification>();
                for (Notification notification : notifications) {
-                       if (notification.getId().equals("new-post-notification")) {
+                       if (notification.getId().equals("new-sone-notification")) {
+                               if ((currentSone != null) && (!currentSone.getOptions().getBooleanOption("ShowNotification/NewSones").get())) {
+                                       continue;
+                               }
+                               filteredNotifications.add(notification);
+                       } else if (notification.getId().equals("new-post-notification")) {
+                               if ((currentSone != null) && (!currentSone.getOptions().getBooleanOption("ShowNotification/NewPosts").get())) {
+                                       continue;
+                               }
                                ListNotification<Post> filteredNotification = filterNewPostNotification((ListNotification<Post>) notification, currentSone, true);
                                if (filteredNotification != null) {
                                        filteredNotifications.add(filteredNotification);
                                }
                        } else if (notification.getId().equals("new-reply-notification")) {
+                               if ((currentSone != null) && (!currentSone.getOptions().getBooleanOption("ShowNotification/NewReplies").get())) {
+                                       continue;
+                               }
                                ListNotification<PostReply> filteredNotification = filterNewReplyNotification((ListNotification<PostReply>) notification, currentSone);
                                if (filteredNotification != null) {
                                        filteredNotifications.add(filteredNotification);
diff --git a/src/main/java/net/pterodactylus/sone/template/ImageAccessor.java b/src/main/java/net/pterodactylus/sone/template/ImageAccessor.java
new file mode 100644 (file)
index 0000000..1c06d31
--- /dev/null
@@ -0,0 +1,64 @@
+/*
+ * Sone - ImageAccessor.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 net.pterodactylus.sone.data.Album;
+import net.pterodactylus.sone.data.Image;
+import net.pterodactylus.util.template.Accessor;
+import net.pterodactylus.util.template.ReflectionAccessor;
+import net.pterodactylus.util.template.TemplateContext;
+
+/**
+ * {@link Accessor} implementation for {@link Image} objects. It adds the
+ * following properties:
+ * <ul>
+ * <li>{@code previous}: returns the previous image in the image’s album, or
+ * {@code null} if the image is the first image of its album.</li>
+ * <li>{@code next}: returns the next image in the image’s album, or {@code
+ * null} if the image is the last image of its album.</li>
+ * </ul>
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public class ImageAccessor extends ReflectionAccessor {
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public Object get(TemplateContext templateContext, Object object, String member) {
+               Image image = (Image) object;
+               if ("next".equals(member)) {
+                       Album album = image.getAlbum();
+                       int imagePosition = album.getImages().indexOf(image);
+                       if (imagePosition < album.getImages().size() - 1) {
+                               return album.getImages().get(imagePosition + 1);
+                       }
+                       return null;
+               } else if ("previous".equals(member)) {
+                       Album album = image.getAlbum();
+                       int imagePosition = album.getImages().indexOf(image);
+                       if (imagePosition > 0) {
+                               return album.getImages().get(imagePosition - 1);
+                       }
+                       return null;
+               }
+               return super.get(templateContext, object, member);
+       }
+
+}
index 063a1de..d7b5eea 100644 (file)
@@ -39,7 +39,7 @@ import net.pterodactylus.util.template.TemplateParser;
 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>\"/>"));
+       private static final Template linkTemplate = TemplateParser.parse(new StringReader("<img<%ifnull !class> class=\"<%class|css>\"<%/if> src=\"<%src|html><%if forceDownload>?forcedownload=true<%/if>\" 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;
@@ -73,6 +73,7 @@ public class ImageLinkFilter implements Filter {
                linkTemplateContext.set("class", imageClass);
                if (image.isInserted()) {
                        linkTemplateContext.set("src", "/" + image.getKey());
+                       linkTemplateContext.set("forceDownload", true);
                } else {
                        linkTemplateContext.set("src", "getImage.html?image=" + image.getId());
                }
index ad1717b..41a3e65 100644 (file)
@@ -70,7 +70,7 @@ public class BookmarksPage extends SoneTemplatePage {
                });
                List<Post> sortedPosts = new ArrayList<Post>(loadedPosts);
                Collections.sort(sortedPosts, Post.TIME_COMPARATOR);
-               Pagination<Post> pagination = new Pagination<Post>(sortedPosts, 25).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("page"), 0));
+               Pagination<Post> pagination = new Pagination<Post>(sortedPosts, webInterface.getCore().getPreferences().getPostsPerPage()).setPage(Numbers.safeParseInteger(request.getHttpRequest().getParam("page"), 0));
                templateContext.set("pagination", pagination);
                templateContext.set("posts", pagination.getItems());
                templateContext.set("postsNotLoaded", allPosts.size() != loadedPosts.size());
index a695952..9072ea4 100644 (file)
@@ -19,6 +19,7 @@ package net.pterodactylus.sone.web;
 
 import net.pterodactylus.sone.data.Album;
 import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.text.TextFilter;
 import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
@@ -64,7 +65,7 @@ public class CreateAlbumPage extends SoneTemplatePage {
                        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);
+                       album.setTitle(name).setDescription(TextFilter.filter(request.getHttpRequest().getHeader("host"), description));
                        webInterface.getCore().touchConfiguration();
                        throw new RedirectException("imageBrowser.html?album=" + album.getId());
                }
index fd4bf7c..dc5c8fa 100644 (file)
@@ -18,6 +18,8 @@
 package net.pterodactylus.sone.web;
 
 import net.pterodactylus.sone.data.Album;
+import net.pterodactylus.sone.data.Sone;
+import net.pterodactylus.sone.text.TextFilter;
 import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
@@ -49,6 +51,7 @@ public class EditAlbumPage extends SoneTemplatePage {
        protected void processTemplate(FreenetRequest request, TemplateContext templateContext) throws RedirectException {
                super.processTemplate(request, templateContext);
                if (request.getMethod() == Method.POST) {
+                       Sone currentSone = getCurrentSone(request.getToadletContext());
                        String albumId = request.getHttpRequest().getPartAsStringFailsafe("album", 36);
                        Album album = webInterface.getCore().getAlbum(albumId, false);
                        if (album == null) {
@@ -57,6 +60,25 @@ public class EditAlbumPage extends SoneTemplatePage {
                        if (!webInterface.getCore().isLocalSone(album.getSone())) {
                                throw new RedirectException("noPermission.html");
                        }
+                       if ("true".equals(request.getHttpRequest().getPartAsStringFailsafe("moveLeft", 4))) {
+                               if (album.getParent() == null) {
+                                       currentSone.moveAlbumUp(album);
+                                       webInterface.getCore().touchConfiguration();
+                                       throw new RedirectException("imageBrowser.html?sone=" + currentSone.getId());
+                               }
+                               album.getParent().moveAlbumUp(album);
+                               webInterface.getCore().touchConfiguration();
+                               throw new RedirectException("imageBrowser.html?album=" + album.getParent().getId());
+                       } else if ("true".equals(request.getHttpRequest().getPartAsStringFailsafe("moveRight", 4))) {
+                               if (album.getParent() == null) {
+                                       currentSone.moveAlbumDown(album);
+                                       webInterface.getCore().touchConfiguration();
+                                       throw new RedirectException("imageBrowser.html?sone=" + currentSone.getId());
+                               }
+                               album.getParent().moveAlbumDown(album);
+                               webInterface.getCore().touchConfiguration();
+                               throw new RedirectException("imageBrowser.html?album=" + album.getParent().getId());
+                       }
                        String albumImageId = request.getHttpRequest().getPartAsStringFailsafe("album-image", 36);
                        if (webInterface.getCore().getImage(albumImageId, false) == null) {
                                albumImageId = null;
@@ -68,7 +90,7 @@ public class EditAlbumPage extends SoneTemplatePage {
                                return;
                        }
                        String description = request.getHttpRequest().getPartAsStringFailsafe("description", 1000).trim();
-                       album.setTitle(title).setDescription(description);
+                       album.setTitle(title).setDescription(TextFilter.filter(request.getHttpRequest().getHeader("host"), description));
                        webInterface.getCore().touchConfiguration();
                        throw new RedirectException("imageBrowser.html?album=" + album.getId());
                }
index 7e9eb7e..a0939db 100644 (file)
@@ -18,6 +18,7 @@
 package net.pterodactylus.sone.web;
 
 import net.pterodactylus.sone.data.Image;
+import net.pterodactylus.sone.text.TextFilter;
 import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.template.Template;
 import net.pterodactylus.util.template.TemplateContext;
@@ -73,7 +74,7 @@ public class EditImagePage extends SoneTemplatePage {
                                        templateContext.set("titleMissing", true);
                                }
                                image.setTitle(title);
-                               image.setDescription(description);
+                               image.setDescription(TextFilter.filter(request.getHttpRequest().getHeader("host"), description));
                        }
                        webInterface.getCore().touchConfiguration();
                        throw new RedirectException(returnPage);
index 143e0ba..4083b99 100644 (file)
@@ -55,9 +55,11 @@ public class FollowSonePage extends SoneTemplatePage {
                        Sone currentSone = getCurrentSone(request.getToadletContext());
                        String soneIds = request.getHttpRequest().getPartAsStringFailsafe("sone", 1200);
                        for (String soneId : soneIds.split("[ ,]+")) {
-                               currentSone.addFriend(soneId);
+                               if (webInterface.getCore().hasSone(soneId)) {
+                                       webInterface.getCore().followSone(currentSone, soneId);
+                                       webInterface.getCore().markSoneKnown(webInterface.getCore().getSone(soneId));
+                               }
                        }
-                       webInterface.getCore().touchConfiguration();
                        throw new RedirectException(returnPage);
                }
        }
index 29556c9..8f0c474 100644 (file)
 package net.pterodactylus.sone.web;
 
 import java.io.IOException;
+import java.net.URI;
 
 import net.pterodactylus.sone.data.TemporaryImage;
+import net.pterodactylus.sone.web.page.FreenetPage;
 import net.pterodactylus.sone.web.page.FreenetRequest;
-import net.pterodactylus.util.web.Page;
 import net.pterodactylus.util.web.Response;
 
 /**
@@ -29,7 +30,7 @@ import net.pterodactylus.util.web.Response;
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
-public class GetImagePage implements Page<FreenetRequest> {
+public class GetImagePage implements FreenetPage {
 
        /** The Sone web interface. */
        private final WebInterface webInterface;
@@ -74,4 +75,12 @@ public class GetImagePage implements Page<FreenetRequest> {
                return response.setStatusCode(200).setStatusText("OK").setContentType(contentType).addHeader("Content-Disposition", "attachment; filename=" + temporaryImage.getId() + "." + contentType.substring(contentType.lastIndexOf('/') + 1)).write(temporaryImage.getImageData());
        }
 
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public boolean isLinkExcepted(URI link) {
+               return false;
+       }
+
 }
index ed31283..406a762 100644 (file)
 
 package net.pterodactylus.sone.web;
 
+import java.net.URI;
+import java.util.HashSet;
+import java.util.Set;
+
 import net.pterodactylus.sone.data.Album;
 import net.pterodactylus.sone.data.Image;
 import net.pterodactylus.sone.data.Sone;
@@ -67,13 +71,34 @@ public class ImageBrowserPage extends SoneTemplatePage {
                        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);
+                       Sone sone = webInterface.getCore().getSone(soneId, false);
+                       templateContext.set("soneRequested", true);
+                       templateContext.set("sone", sone);
+                       return;
                }
+               String mode = request.getHttpRequest().getParam("mode", null);
+               if ("gallery".equals(mode)) {
+                       templateContext.set("galleryRequested", true);
+                       Set<Album> albums = new HashSet<Album>();
+                       for (Sone sone : webInterface.getCore().getSones()) {
+                               albums.addAll(sone.getAllAlbums());
+                       }
+                       templateContext.set("albums", albums);
+                       return;
+               }
+               Sone sone = getCurrentSone(request.getToadletContext(), false);
                templateContext.set("soneRequested", true);
                templateContext.set("sone", sone);
        }
 
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public boolean isLinkExcepted(URI link) {
+               return true;
+       }
+
 }
index 7b4a634..49b1275 100644 (file)
@@ -67,6 +67,12 @@ public class OptionsPage extends SoneTemplatePage {
                                currentSone.getOptions().getBooleanOption("AutoFollow").set(autoFollow);
                                boolean enableSoneInsertNotifications = request.getHttpRequest().isPartSet("enable-sone-insert-notifications");
                                currentSone.getOptions().getBooleanOption("EnableSoneInsertNotifications").set(enableSoneInsertNotifications);
+                               boolean showNotificationNewSones = request.getHttpRequest().isPartSet("show-notification-new-sones");
+                               currentSone.getOptions().getBooleanOption("ShowNotification/NewSones").set(showNotificationNewSones);
+                               boolean showNotificationNewPosts = request.getHttpRequest().isPartSet("show-notification-new-posts");
+                               currentSone.getOptions().getBooleanOption("ShowNotification/NewPosts").set(showNotificationNewPosts);
+                               boolean showNotificationNewReplies = request.getHttpRequest().isPartSet("show-notification-new-replies");
+                               currentSone.getOptions().getBooleanOption("ShowNotification/NewReplies").set(showNotificationNewReplies);
                                webInterface.getCore().touchConfiguration();
                        }
                        Integer insertionDelay = Numbers.safeParseInteger(request.getHttpRequest().getPartAsStringFailsafe("insertion-delay", 16));
@@ -130,6 +136,9 @@ public class OptionsPage extends SoneTemplatePage {
                if (currentSone != null) {
                        templateContext.set("auto-follow", currentSone.getOptions().getBooleanOption("AutoFollow").get());
                        templateContext.set("enable-sone-insert-notifications", currentSone.getOptions().getBooleanOption("EnableSoneInsertNotifications").get());
+                       templateContext.set("show-notification-new-sones", currentSone.getOptions().getBooleanOption("ShowNotification/NewSones").get());
+                       templateContext.set("show-notification-new-posts", currentSone.getOptions().getBooleanOption("ShowNotification/NewPosts").get());
+                       templateContext.set("show-notification-new-replies", currentSone.getOptions().getBooleanOption("ShowNotification/NewReplies").get());
                }
                templateContext.set("insertion-delay", preferences.getInsertionDelay());
                templateContext.set("posts-per-page", preferences.getPostsPerPage());
index d8e53ce..f07880d 100644 (file)
@@ -55,9 +55,8 @@ public class UnfollowSonePage extends SoneTemplatePage {
                        Sone currentSone = getCurrentSone(request.getToadletContext());
                        String soneIds = request.getHttpRequest().getPartAsStringFailsafe("sone", 2000);
                        for (String soneId : soneIds.split("[ ,]+")) {
-                               currentSone.removeFriend(soneId);
+                               webInterface.getCore().unfollowSone(currentSone, soneId);
                        }
-                       webInterface.getCore().touchConfiguration();
                        throw new RedirectException(returnPage);
                }
        }
index e0aa1e1..e160c32 100644 (file)
@@ -33,6 +33,7 @@ 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.text.TextFilter;
 import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.io.Closer;
 import net.pterodactylus.util.io.StreamCopier;
@@ -120,7 +121,7 @@ public class UploadImagePage extends SoneTemplatePage {
                                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));
+                               image.setTitle(name).setDescription(TextFilter.filter(request.getHttpRequest().getHeader("host"), description)).setWidth(uploadedImage.getWidth(null)).setHeight(uploadedImage.getHeight(null));
                        } catch (IOException ioe1) {
                                logger.log(Level.WARNING, "Could not read uploaded image!", ioe1);
                                return;
index 4cc9864..d0ca904 100644 (file)
@@ -17,6 +17,8 @@
 
 package net.pterodactylus.sone.web;
 
+import java.net.URI;
+
 import net.pterodactylus.sone.data.Post;
 import net.pterodactylus.sone.template.SoneAccessor;
 import net.pterodactylus.sone.web.page.FreenetRequest;
@@ -75,4 +77,12 @@ public class ViewPostPage extends SoneTemplatePage {
                templateContext.set("raw", raw);
        }
 
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public boolean isLinkExcepted(URI link) {
+               return true;
+       }
+
 }
index 5fe356d..57d4070 100644 (file)
@@ -17,6 +17,7 @@
 
 package net.pterodactylus.sone.web;
 
+import java.net.URI;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
@@ -115,4 +116,12 @@ public class ViewSonePage extends SoneTemplatePage {
                templateContext.set("repliedPosts", repliedPostPagination.getItems());
        }
 
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public boolean isLinkExcepted(URI link) {
+               return true;
+       }
+
 }
index 7320ad5..5ba3be7 100644 (file)
@@ -53,6 +53,7 @@ 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.ImageAccessor;
 import net.pterodactylus.sone.template.ImageLinkFilter;
 import net.pterodactylus.sone.template.JavascriptFilter;
 import net.pterodactylus.sone.template.ParserFilter;
@@ -170,6 +171,9 @@ public class WebInterface implements CoreListener {
        /** The Sone text parser. */
        private final SoneTextParser soneTextParser;
 
+       /** The parser filter. */
+       private final ParserFilter parserFilter;
+
        /** The “new Sone” notification. */
        private final ListNotification<Sone> newSoneNotification;
 
@@ -228,6 +232,7 @@ public class WebInterface implements CoreListener {
                templateContextFactory.addAccessor(Post.class, new PostAccessor(getCore()));
                templateContextFactory.addAccessor(Reply.class, new ReplyAccessor(getCore()));
                templateContextFactory.addAccessor(Album.class, new AlbumAccessor());
+               templateContextFactory.addAccessor(Image.class, new ImageAccessor());
                templateContextFactory.addAccessor(Identity.class, new IdentityAccessor(getCore()));
                templateContextFactory.addAccessor(Trust.class, new TrustAccessor());
                templateContextFactory.addAccessor(HTTPRequest.class, new HttpRequestAccessor());
@@ -242,7 +247,7 @@ public class WebInterface implements CoreListener {
                templateContextFactory.addFilter("match", new MatchFilter());
                templateContextFactory.addFilter("css", new CssClassNameFilter());
                templateContextFactory.addFilter("js", new JavascriptFilter());
-               templateContextFactory.addFilter("parse", new ParserFilter(getCore(), templateContextFactory, soneTextParser));
+               templateContextFactory.addFilter("parse", parserFilter = new ParserFilter(getCore(), templateContextFactory, soneTextParser));
                templateContextFactory.addFilter("unknown", new UnknownDateFilter(getL10n(), "View.Sone.Text.UnknownDate"));
                templateContextFactory.addFilter("format", new FormatFilter());
                templateContextFactory.addFilter("sort", new CollectionSortFilter());
@@ -668,7 +673,7 @@ public class WebInterface implements CoreListener {
                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 EditImageAjaxPage(this, parserFilter)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new TrustAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new DistrustAjaxPage(this)));
                pageToadlets.add(pageToadletFactory.createPageToadlet(new UntrustAjaxPage(this)));
index 53f0466..8bc07ef 100644 (file)
@@ -18,6 +18,7 @@
 package net.pterodactylus.sone.web.ajax;
 
 import net.pterodactylus.sone.data.Album;
+import net.pterodactylus.sone.text.TextFilter;
 import net.pterodactylus.sone.web.WebInterface;
 import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.json.JsonObject;
@@ -68,7 +69,7 @@ public class EditAlbumAjaxPage extends JsonPage {
                }
                String title = request.getHttpRequest().getParam("title").trim();
                String description = request.getHttpRequest().getParam("description").trim();
-               album.setTitle(title).setDescription(description);
+               album.setTitle(title).setDescription(TextFilter.filter(request.getHttpRequest().getHeader("host"), description));
                webInterface.getCore().touchConfiguration();
                return createSuccessJsonObject().put("albumId", album.getId()).put("title", album.getTitle()).put("description", album.getDescription());
        }
index 17d171b..3c4de8e 100644 (file)
 package net.pterodactylus.sone.web.ajax;
 
 import net.pterodactylus.sone.data.Image;
+import net.pterodactylus.sone.template.ParserFilter;
+import net.pterodactylus.sone.text.TextFilter;
 import net.pterodactylus.sone.web.WebInterface;
 import net.pterodactylus.sone.web.page.FreenetRequest;
+import net.pterodactylus.util.collection.MapBuilder;
 import net.pterodactylus.util.json.JsonObject;
+import net.pterodactylus.util.template.TemplateContext;
 
 /**
  * Page that stores a user’s image modifications.
@@ -29,14 +33,20 @@ import net.pterodactylus.util.json.JsonObject;
  */
 public class EditImageAjaxPage extends JsonPage {
 
+       /** Parser for image descriptions. */
+       private final ParserFilter parserFilter;
+
        /**
         * Creates a new edit image AJAX page.
         *
         * @param webInterface
         *            The Sone web interface
+        * @param parserFilter
+        *            The parser filter for image descriptions
         */
-       public EditImageAjaxPage(WebInterface webInterface) {
+       public EditImageAjaxPage(WebInterface webInterface, ParserFilter parserFilter) {
                super("editImage.ajax", webInterface);
+               this.parserFilter = parserFilter;
        }
 
        //
@@ -68,9 +78,9 @@ public class EditImageAjaxPage extends JsonPage {
                }
                String title = request.getHttpRequest().getParam("title").trim();
                String description = request.getHttpRequest().getParam("description").trim();
-               image.setTitle(title).setDescription(description);
+               image.setTitle(title).setDescription(TextFilter.filter(request.getHttpRequest().getHeader("host"), description));
                webInterface.getCore().touchConfiguration();
-               return createSuccessJsonObject().put("imageId", image.getId()).put("title", image.getTitle()).put("description", image.getDescription());
+               return createSuccessJsonObject().put("imageId", image.getId()).put("title", image.getTitle()).put("description", image.getDescription()).put("parsedDescription", (String) parserFilter.format(new TemplateContext(), image.getDescription(), new MapBuilder<String, String>().put("sone", image.getSone().getId()).get()));
        }
 
 }
index 5c3e5f4..764fa28 100644 (file)
@@ -52,8 +52,8 @@ public class FollowSoneAjaxPage extends JsonPage {
                if (currentSone == null) {
                        return createErrorJsonObject("auth-required");
                }
-               currentSone.addFriend(soneId);
-               webInterface.getCore().touchConfiguration();
+               webInterface.getCore().followSone(currentSone, soneId);
+               webInterface.getCore().markSoneKnown(webInterface.getCore().getSone(soneId));
                return createSuccessJsonObject();
        }
 
index 038cfb8..5f2103a 100644 (file)
@@ -145,7 +145,7 @@ public class GetStatusAjaxPage extends JsonPage {
                        jsonReply.put("postSone", reply.getPost().getSone().getId());
                        jsonReplies.add(jsonReply);
                }
-               return createSuccessJsonObject().put("loggedIn", currentSone != null).put("sones", jsonSones).put("notifications", jsonNotificationInformations).put("newPosts", jsonPosts).put("newReplies", jsonReplies);
+               return createSuccessJsonObject().put("loggedIn", currentSone != null).put("options", createJsonOptions(currentSone)).put("sones", jsonSones).put("notifications", jsonNotificationInformations).put("newPosts", jsonPosts).put("newReplies", jsonReplies);
        }
 
        /**
@@ -209,4 +209,23 @@ public class GetStatusAjaxPage extends JsonPage {
                return jsonNotification;
        }
 
+       /**
+        * Creates a JSON object that contains all options that are currently in
+        * effect for the given Sone (or overall, if the given Sone is {@code null}
+        * ).
+        *
+        * @param currentSone
+        *            The current Sone (may be {@code null})
+        * @return The current options
+        */
+       private JsonObject createJsonOptions(Sone currentSone) {
+               JsonObject options = new JsonObject();
+               if (currentSone != null) {
+                       options.put("ShowNotification/NewSones", currentSone.getOptions().getBooleanOption("ShowNotification/NewSones").get());
+                       options.put("ShowNotification/NewPosts", currentSone.getOptions().getBooleanOption("ShowNotification/NewPosts").get());
+                       options.put("ShowNotification/NewReplies", currentSone.getOptions().getBooleanOption("ShowNotification/NewReplies").get());
+               }
+               return options;
+       }
+
 }
index 1e5e8ed..4ee3b99 100644 (file)
 package net.pterodactylus.sone.web.ajax;
 
 import java.io.IOException;
+import java.net.URI;
 
 import net.pterodactylus.sone.data.Sone;
 import net.pterodactylus.sone.web.WebInterface;
+import net.pterodactylus.sone.web.page.FreenetPage;
 import net.pterodactylus.sone.web.page.FreenetRequest;
 import net.pterodactylus.util.json.JsonObject;
 import net.pterodactylus.util.json.JsonUtils;
 import net.pterodactylus.util.web.Page;
 import net.pterodactylus.util.web.Response;
-import freenet.clients.http.SessionManager.Session;
 import freenet.clients.http.ToadletContext;
+import freenet.clients.http.SessionManager.Session;
 
 /**
  * A JSON page is a specialized {@link Page} that will always return a JSON
@@ -35,7 +37,7 @@ import freenet.clients.http.ToadletContext;
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
-public abstract class JsonPage implements Page<FreenetRequest> {
+public abstract class JsonPage implements FreenetPage {
 
        /** The path of the page. */
        private final String path;
@@ -218,4 +220,12 @@ public abstract class JsonPage implements Page<FreenetRequest> {
                return response.setStatusCode(200).setStatusText("OK").setContentType("application/json").write(JsonUtils.format(jsonObject));
        }
 
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public boolean isLinkExcepted(URI link) {
+               return false;
+       }
+
 }
index 26f1a11..5d94d52 100644 (file)
@@ -52,8 +52,7 @@ public class UnfollowSoneAjaxPage extends JsonPage {
                if (currentSone == null) {
                        return createErrorJsonObject("auth-required");
                }
-               currentSone.removeFriend(soneId);
-               webInterface.getCore().touchConfiguration();
+               webInterface.getCore().unfollowSone(currentSone, soneId);
                return createSuccessJsonObject();
        }
 
diff --git a/src/main/java/net/pterodactylus/sone/web/page/FreenetPage.java b/src/main/java/net/pterodactylus/sone/web/page/FreenetPage.java
new file mode 100644 (file)
index 0000000..d4bbc82
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+ * Sone - FreenetPage.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.page;
+
+import java.net.URI;
+
+import net.pterodactylus.util.web.Page;
+
+/**
+ * Freenet-specific {@link Page} extension that adds the capability to allow a
+ * link to a page to be unharmed by Freenet’s content filter.
+ *
+ * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
+ */
+public interface FreenetPage extends Page<FreenetRequest> {
+
+       /**
+        * Returns whether the given should be excepted from being filtered.
+        *
+        * @param link
+        *            The link to check
+        * @return {@code true} if the link should not be filtered, {@code false} if
+        *         it should be filtered
+        */
+       public boolean isLinkExcepted(URI link);
+
+}
index 5c027e5..e7d6e28 100644 (file)
@@ -19,6 +19,7 @@ package net.pterodactylus.sone.web.page;
 
 import java.io.IOException;
 import java.io.StringWriter;
+import java.net.URI;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
@@ -47,7 +48,7 @@ import freenet.support.HTMLNode;
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
-public class FreenetTemplatePage implements Page<FreenetRequest>, LinkEnabledCallback {
+public class FreenetTemplatePage implements FreenetPage, LinkEnabledCallback {
 
        /** The logger. */
        private static final Logger logger = Logging.getLogger(FreenetTemplatePage.class);
@@ -252,6 +253,14 @@ public class FreenetTemplatePage implements Page<FreenetRequest>, LinkEnabledCal
                return false;
        }
 
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public boolean isLinkExcepted(URI link) {
+               return false;
+       }
+
        //
        // INTERFACE LinkEnabledCallback
        //
index 2d1f35a..8245251 100644 (file)
@@ -26,6 +26,7 @@ import net.pterodactylus.util.web.Page;
 import net.pterodactylus.util.web.Response;
 import freenet.client.HighLevelSimpleClient;
 import freenet.clients.http.LinkEnabledCallback;
+import freenet.clients.http.LinkFilterExceptedToadlet;
 import freenet.clients.http.Toadlet;
 import freenet.clients.http.ToadletContext;
 import freenet.clients.http.ToadletContextClosedException;
@@ -39,7 +40,7 @@ import freenet.support.io.Closer;
  *
  * @author <a href="mailto:bombe@pterodactylus.net">David ‘Bombe’ Roden</a>
  */
-public class PageToadlet extends Toadlet implements LinkEnabledCallback {
+public class PageToadlet extends Toadlet implements LinkEnabledCallback, LinkFilterExceptedToadlet {
 
        /** The name of the menu item. */
        private final String menuName;
@@ -174,4 +175,16 @@ public class PageToadlet extends Toadlet implements LinkEnabledCallback {
                return true;
        }
 
+       //
+       // LINKFILTEREXCEPTEDTOADLET METHODS
+       //
+
+       /**
+        * {@inheritDoc}
+        */
+       @Override
+       public boolean isLinkExcepted(URI link) {
+               return (page instanceof FreenetPage) ? ((FreenetPage) page).isLinkExcepted(link) : false;
+       }
+
 }
index 4210930..afc445a 100644 (file)
@@ -40,6 +40,9 @@ Page.Options.Section.SoneSpecificOptions.NotLoggedIn=These options are only avai
 Page.Options.Section.SoneSpecificOptions.LoggedIn=These options are only available while you are logged in and they are only valid for the Sone you are logged in as.
 Page.Options.Option.AutoFollow.Description=If a new Sone is discovered, follow it automatically. Note that this will only follow Sones that are discovered after you activate this option!
 Page.Options.Option.EnableSoneInsertNotifications.Description=If enabled, this will display notifications every time your Sone is being inserted or finishes inserting.
+Page.Options.Option.ShowNotificationNewSones.Description=Show notifications for new Sones.
+Page.Options.Option.ShowNotificationNewPosts.Description=Show notifications for new posts.
+Page.Options.Option.ShowNotificationNewReplies.Description=Show notifications for new replies.
 Page.Options.Section.RuntimeOptions.Title=Runtime Behaviour
 Page.Options.Option.InsertionDelay.Description=The number of seconds the Sone inserter waits after a modification of a Sone before it is being inserted.
 Page.Options.Option.PostsPerPage.Description=The number of posts to display on a page before pagination controls are being shown.
@@ -98,8 +101,8 @@ Page.KnownSones.Sort.Order.Descending=Descending
 Page.KnownSones.FollowedSones.ShowOnly=Show only followed Sones
 Page.KnownSones.FollowedSones.Hide=Hide followed Sones
 Page.KnownSones.Button.Apply=Apply
-Page.KnownSones.Button.FollowAllSones=Follow all Sones
-Page.KnownSones.Button.UnfollowAllSones=Unfollow all Sones
+Page.KnownSones.Button.FollowAllSones=Follow all Sones on this page
+Page.KnownSones.Button.UnfollowAllSones=Unfollow all Sones on this page
 
 Page.EditProfile.Title=Edit Profile - Sone
 Page.EditProfile.Page.Title=Edit Profile
@@ -201,6 +204,7 @@ 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.Link.All=All Sones
 Page.ImageBrowser.CreateAlbum.Button.CreateAlbum=Create Album
 Page.ImageBrowser.Album.Edit.Title=Edit Album
 Page.ImageBrowser.Album.Delete.Title=Delete Album
index f30bb03..e46540a 100644 (file)
@@ -684,6 +684,10 @@ textarea {
        width: 95%;
 }
 
+#sone .image .album-sone {
+       font-size: 80%;
+}
+
 #sone .image .image-title, #sone .album .album-title {
        font-weight: bold;
 }
index bbd039c..a3dbc64 100644 (file)
@@ -1193,17 +1193,17 @@ function getStatus() {
                                        }
                                });
                                if (!foundNotification) {
-                                       if (notificationId == "new-sone-notification") {
+                                       if (notificationId == "new-sone-notification" && (data.options["ShowNotification/NewSones"] == true)) {
                                                $(".new-sone-id", this).each(function(index, element) {
                                                        soneId = $(this).text();
                                                        markSoneAsKnown(getSone(soneId), true);
                                                });
-                                       } else if (notificationId == "new-post-notification") {
+                                       } else if (notificationId == "new-post-notification" && (data.options["ShowNotification/NewPosts"] == true)) {
                                                $(".post-id", this).each(function(index, element) {
                                                        postId = $(this).text();
                                                        markPostAsKnown(getPost(postId), true);
                                                });
-                                       } else if (notificationId == "new-reply-notification") {
+                                       } else if (notificationId == "new-reply-notification" && (data.options["ShowNotification/NewReplies"] == true)) {
                                                $(".reply-id", this).each(function(index, element) {
                                                        replyId = $(this).text();
                                                        markReplyAsKnown(getReply(replyId), true);
@@ -1347,7 +1347,7 @@ function isViewSonePage() {
  * @returns The ID of the currently shown Sone
  */
 function getShownSoneId() {
-       return $("#sone .sone-id").text();
+       return $("#sone .sone-id").first().text();
 }
 
 /**
index 8768b5a..cf4e7ce 100644 (file)
@@ -5,6 +5,7 @@
        <h1><%= Page.Bookmarks.Page.Title|l10n|html></h1>
 
        <div id="posts">
+               <%= page|store key=pageParameter>
                <%include include/pagination.html>
                <%foreach posts post>
                        <%include include/viewPost.html>
index d97fc25..58ed8ab 100644 (file)
                                                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(".image-description").html(data.parsedDescription);
                                                                getImage(data.imageId).find(":input[name='title']").attr("defaultValue", title);
-                                                               getImage(data.imageId).find(":input[name='description']").attr("defaultValue", description);
+                                                               getImage(data.imageId).find(":input[name='description']").attr("defaultValue", data.description);
                                                                cancelImageEditing();
                                                        }
                                                });
                                                });
                                                $("#edit-album label").hide();
 
+                                               /* hide non-js image move buttons. */
+                                               $(".move-buttons").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");
                        <h1 class="backlink"><%= Page.ImageBrowser.Album.Title|l10n|replace needle='{album}' replacementKey=album.title|html></h1>
 
                        <div class="backlinks">
+                               <div class="backlink"><a href="imageBrowser.html?mode=gallery"><%= Page.ImageBrowser.Link.All|l10n|html></a></div>
+                               <div class="separator">&gt;</div>
                                <%foreach album.backlinks backlink backlinks>
                                        <div class="backlink">
                                                <a href="<% backlink.target|html>"><% backlink.name|html></a>
                                <%/foreach>
                        </div>
 
-                       <p id="description"><% album.description|html></p>
+                       <p id="description"><% album.description|parse sone=album.sone></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>
                                        <div class="show-data">
                                                <div class="image-title"><% image.title|html></div>
-                                               <div class="image-description"><% image.description|html></div>
+                                               <div class="image-description"><% image.description|parse sone=image.sone></div>
                                        </div>
                                        <%if album.sone.local>
                                                <form class="edit-image" action="editImage.html" method="post">
                                                        <input type="hidden" name="returnPage" value="<%request.uri|html>" />
                                                        <input type="hidden" name="image" value="<%image.id|html>" />
 
+                                                       <div class="move-buttons">
+                                                                       <button <%first>class="hidden" <%/first>type="submit" name="moveLeft" value="true"><%= Page.ImageBrowser.Image.Button.MoveLeft|l10n|html></button>
+                                                                       <button <%last>class="hidden" <%/last>type="submit" name="moveRight" value="true"><%= Page.ImageBrowser.Image.Button.MoveRight|l10n|html></button>
+                                                       </div>
+
                                                        <div class="edit-data hidden">
                                                                <div>
                                                                        <input type="text" name="title" value="<%image.title|html>" />
                <h1 class="backlink"><%image.title|html></h1>
 
                <div class="backlinks">
+                       <div class="backlink"><a href="imageBrowser.html?mode=gallery"><%= Page.ImageBrowser.Link.All|l10n|html></a></div>
+                       <div class="separator">&gt;</div>
                        <%foreach image.album.backlinks backlink backlinks>
                                <div class="backlink">
                                        <a href="<% backlink.target|html>"><% backlink.name|html></a>
                                        <div class="separator">&gt;</div>
                                <%/if>
                        <%/foreach>
+                       <%ifnull !image.previous><div class="backlink"><a href="imageBrowser.html?image=<%image.previous.id|html>">« <%image.previous.title|html></a></div><%/if>
+                       <%ifnull !image.next><div class="backlink"><a href="imageBrowser.html?image=<%image.next.id|html>">» <%image.next.title|html></a></div><%/if>
                </div>
 
                <%ifnull image>
                                        });
                                        $("#create-album label").hide();
 
+                                       /* hide non-js move buttons. */
+                                       $(".move-buttons").hide();
+
                                        hideAndShowBlock(".create-album", ".show-create-album", ".hide-create-album");
 
                                        prepareAlbums();
 
                        <h1><%= Page.ImageBrowser.Sone.Title|l10n|replace needle='{sone}' replacementKey=sone.niceName|html></h1>
 
+                       <div class="backlinks">
+                               <div class="backlink"><a href="imageBrowser.html?mode=gallery"><%= Page.ImageBrowser.Link.All|l10n|html></a></div>
+                               <div class="separator">&gt;</div>
+                               <div class="backlink"><a href="imageBrowser.html?sone=<%sone.id|html>"><%sone.niceName|l10n|html></a></div>
+                       </div>
+
                        <%include include/browseAlbums.html albums=sone.albums>
 
                        <%if sone.local>
 
                <%/if>
 
+       <%elseif galleryRequested>
+
+               <%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> (<%album.sone.niceName|html>)" title="<% album.title|html> (<%album.sone.niceName|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-sone"><a href="imageBrowser.html?sone=<%album.sone.id|html>"><%album.sone.niceName|html></a></div>
+                                       <div class="album-title"><% album.title|html> (<%= View.Sone.Stats.Images|l10n 0=album.images.size>)</div>
+                                       <div class="album-description"><% album.description|parse sone=album.sone></div>
+                               </div>
+                       </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>
 
 <%include include/tail.html>
index d925a3c..cb9abba 100644 (file)
@@ -14,7 +14,7 @@
                </div>
                <div class="show-data">
                        <div class="album-title"><% album.title|html> (<%= View.Sone.Stats.Images|l10n 0=album.images.size>)</div>
-                       <div class="album-description"><% album.description|html></div>
+                       <div class="album-description"><% album.description|parse sone=album.sone></div>
                </div>
                <%if album.sone.local>
                        <form class="edit-album" action="editAlbum.html" method="post">
                                <input type="hidden" name="returnPage" value="<%request.uri|html>" />
                                <input type="hidden" name="album" value="<%album.id|html>" />
 
+                               <div class="move-buttons">
+                                               <button <%first>class="hidden" <%/first>type="submit" name="moveLeft" value="true"><%= Page.ImageBrowser.Image.Button.MoveLeft|l10n|html></button>
+                                               <button <%last>class="hidden" <%/last>type="submit" name="moveRight" value="true"><%= Page.ImageBrowser.Image.Button.MoveRight|l10n|html></button>
+                               </div>
+
                                <div class="edit-data hidden">
                                        <div>
                                                <input type="text" name="title" value="<%album.title|html>" />
index 2adf57e..57f3c5f 100644 (file)
                        <%= Page.Options.Option.EnableSoneInsertNotifications.Description|l10n|html>
                </p>
 
+               <p>
+                       <input type="checkbox" name="show-notification-new-sones"<%ifnull currentSone> disabled="disabled"<%/if><%if show-notification-new-sones> checked="checked"<%/if>/>
+                       <%= Page.Options.Option.ShowNotificationNewSones.Description|l10n|html>
+               </p>
+
+               <p>
+                       <input type="checkbox" name="show-notification-new-posts"<%ifnull currentSone> disabled="disabled"<%/if><%if show-notification-new-posts> checked="checked"<%/if>/>
+                       <%= Page.Options.Option.ShowNotificationNewPosts.Description|l10n|html>
+               </p>
+
+               <p>
+                       <input type="checkbox" name="show-notification-new-replies"<%ifnull currentSone> disabled="disabled"<%/if><%if show-notification-new-replies> checked="checked"<%/if>/>
+                       <%= Page.Options.Option.ShowNotificationNewReplies.Description|l10n|html>
+               </p>
+
                <h2><%= Page.Options.Section.RuntimeOptions.Title|l10n|html></h2>
 
                <p><%= Page.Options.Option.InsertionDelay.Description|l10n|html></p>