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