From 6058c41f32d86c57c54d3ad15529c02abfc74ca0 Mon Sep 17 00:00:00 2001 From: Kenny Colliander Date: Mon, 7 Aug 2023 23:33:47 +0200 Subject: [PATCH 1/3] Added album preview image - Moved getContentPath and getThumbnailPath to FileUtils - Created PreviewSupport with methods for generating album preview images. - Minor fixes. --- .../java/se/kecon/kalbum/AlbumController.java | 101 ++++---- src/main/java/se/kecon/kalbum/FileUtils.java | 46 ++++ .../java/se/kecon/kalbum/PreviewSupport.java | 232 ++++++++++++++++++ .../se/kecon/kalbum/AlbumControllerTest.java | 29 ++- .../java/se/kecon/kalbum/FileUtilsTest.java | 20 +- static/assets/css/styles.css | 49 +++- static/assets/js/script.js | 24 +- 7 files changed, 442 insertions(+), 59 deletions(-) create mode 100644 src/main/java/se/kecon/kalbum/PreviewSupport.java diff --git a/src/main/java/se/kecon/kalbum/AlbumController.java b/src/main/java/se/kecon/kalbum/AlbumController.java index 0a56453..9ed8401 100644 --- a/src/main/java/se/kecon/kalbum/AlbumController.java +++ b/src/main/java/se/kecon/kalbum/AlbumController.java @@ -42,7 +42,6 @@ import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; -import java.nio.file.NoSuchFileException; import java.nio.file.Path; import java.time.Instant; import java.time.ZoneId; @@ -54,8 +53,6 @@ import java.util.Optional; import static se.kecon.kalbum.FileUtils.*; -import static se.kecon.kalbum.Validation.checkValidAlbumId; -import static se.kecon.kalbum.Validation.checkValidFilename; /** * Controller for the album API. @@ -67,10 +64,6 @@ @Slf4j public class AlbumController { - private static final String CONTENT_PATH = "contents"; - - private static final String THUMBNAIL_PATH = ".thumbnails"; - @Setter @Value("${kalbum.path}") private Path albumBasePath; @@ -78,42 +71,9 @@ public class AlbumController { @Autowired private AlbumDao albumDao; - /** - * Get the path to the thumbnail for the given content - * - * @param id album id - * @param filename content filename - * @param contentFormat content format - * @return path - * @throws IllegalAlbumIdException if the id is invalid - * @throws IllegalFilenameException if the filename is invalid - */ - protected Path getThumbnailPath(final String id, final String filename, final ContentFormat contentFormat) throws IllegalAlbumIdException, IllegalFilenameException { - checkValidAlbumId(id); - checkValidFilename(filename); - - if (contentFormat.isImage()) { - return albumBasePath.resolve(id).resolve(CONTENT_PATH).resolve(THUMBNAIL_PATH).resolve(filename); - } else { - return albumBasePath.resolve(id).resolve(CONTENT_PATH).resolve(THUMBNAIL_PATH).resolve(removeSuffix(filename) + ".png"); - } - } - - /** - * Get the path to the content with the given id - * - * @param id album id - * @param filename content filename - * @return path - * @throws IllegalAlbumIdException if the id is invalid - * @throws IllegalFilenameException if the filename is invalid - */ - protected Path getContentPath(final String id, final String filename) throws IllegalAlbumIdException, IllegalFilenameException { - checkValidAlbumId(id); - checkValidFilename(filename); + @Autowired + private PreviewSupport previewSupport; - return albumBasePath.resolve(id).resolve(CONTENT_PATH).resolve(filename); - } /** * List all albums @@ -223,8 +183,8 @@ public ResponseEntity uploadContent(@PathVariable(name = "id") String id, } final ContentFormat contentFormat = ContentFormat.detectFileType(file); - final Path contentPath = getContentPath(id, filename); - final Path thumbnailPath = getThumbnailPath(id, filename, contentFormat); + final Path contentPath = getContentPath(albumBasePath, id, filename); + final Path thumbnailPath = getThumbnailPath(albumBasePath, id, filename, contentFormat); if (!Files.exists(thumbnailPath)) { Files.createDirectories(thumbnailPath.getParent()); @@ -348,8 +308,8 @@ public ResponseEntity deleteContent(@PathVariable(name = "id") String id, return new ResponseEntity<>(HttpStatus.NOT_FOUND); } - final Path contentPath = getContentPath(id, filename); - final Path thumbnailPath = getThumbnailPath(id, filename, ContentFormat.getContentFormat(filename)); + final Path contentPath = getContentPath(albumBasePath, id, filename); + final Path thumbnailPath = getThumbnailPath(albumBasePath, id, filename, ContentFormat.getContentFormat(filename)); if (Files.exists(contentPath)) { Files.delete(contentPath); @@ -422,14 +382,13 @@ public ResponseEntity patchContent(@PathVariable(name = "id") String id, @ * @param id the id of the album * @param filename the name of the content to get * @return the content - * @throws IOException if the content could not be read */ @GetMapping(path = "/albums/{id}/contents/{filename}") - public ResponseEntity getContent(@PathVariable(name = "id") String id, @PathVariable(name = "filename") String filename) throws IOException { + public ResponseEntity getContent(@PathVariable(name = "id") String id, @PathVariable(name = "filename") String filename) { try { // Input is validated in getContentPath - final Path path = getContentPath(id, filename); + final Path path = getContentPath(albumBasePath, id, filename); InputStream inputStream = Files.newInputStream(path); InputStreamResource inputStreamResource = new InputStreamResource(inputStream); @@ -439,7 +398,7 @@ public ResponseEntity getContent(@PathVariable(name = "id") headers.setContentType(MediaType.parseMediaType(contentType)); return new ResponseEntity<>(inputStreamResource, headers, HttpStatus.OK); - } catch (IllegalAlbumIdException | IllegalFilenameException | NoSuchFileException e) { + } catch (IllegalAlbumIdException | IllegalFilenameException | IOException e) { return new ResponseEntity<>(HttpStatus.NOT_FOUND); } } @@ -450,13 +409,12 @@ public ResponseEntity getContent(@PathVariable(name = "id") * @param id the id of the album * @param filename the name of the thumbnail to get * @return the thumbnail - * @throws IOException if the thumbnail could not be read */ @GetMapping(path = "/albums/{id}/contents/thumbnails/{filename}") - public ResponseEntity getThumbnail(@PathVariable(name = "id") String id, @PathVariable(name = "filename") String filename) throws IOException { + public ResponseEntity getThumbnail(@PathVariable(name = "id") String id, @PathVariable(name = "filename") String filename) { try { // Input is validated by getThumbnailPath - final Path path = getThumbnailPath(id, filename, ContentFormat.getContentFormat(filename)); + final Path path = getThumbnailPath(albumBasePath, id, filename, ContentFormat.getContentFormat(filename)); InputStream inputStream = Files.newInputStream(path); InputStreamResource inputStreamResource = new InputStreamResource(inputStream); @@ -466,8 +424,41 @@ public ResponseEntity getThumbnail(@PathVariable(name = "id headers.setContentType(MediaType.parseMediaType(contentType)); return new ResponseEntity<>(inputStreamResource, headers, HttpStatus.OK); - } catch (IllegalAlbumIdException | IllegalFilenameException | UnsupportedContentFormatException | - NoSuchFileException e) { + } catch (IllegalAlbumIdException | IllegalFilenameException | UnsupportedContentFormatException | IOException e) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + } + + @GetMapping(path = "/albums/{id}/preview.png") + public ResponseEntity getPreview(@PathVariable(name = "id") String id, @RequestParam(name = "generate", defaultValue = "false" ) boolean generate) + { + try { + final Optional album = this.albumDao.get(id); + + if (album.isEmpty()) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + + try { + // Input is validated in getContentPath + final Path path = getContentPath(albumBasePath, id, "preview.png"); + + if (generate || !Files.exists(path)) { + this.previewSupport.createPreview(album.get()); + } + + InputStream inputStream = Files.newInputStream(path); + InputStreamResource inputStreamResource = new InputStreamResource(inputStream); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.IMAGE_PNG); + + return new ResponseEntity<>(inputStreamResource, headers, HttpStatus.OK); + } catch (IllegalAlbumIdException | IllegalFilenameException | IOException e) { + return new ResponseEntity<>(HttpStatus.NOT_FOUND); + } + + } catch (IllegalAlbumIdException e) { return new ResponseEntity<>(HttpStatus.NOT_FOUND); } } diff --git a/src/main/java/se/kecon/kalbum/FileUtils.java b/src/main/java/se/kecon/kalbum/FileUtils.java index c43e51e..6f710a1 100644 --- a/src/main/java/se/kecon/kalbum/FileUtils.java +++ b/src/main/java/se/kecon/kalbum/FileUtils.java @@ -25,6 +25,9 @@ import java.nio.file.Path; import java.util.UUID; +import static se.kecon.kalbum.Validation.checkValidAlbumId; +import static se.kecon.kalbum.Validation.checkValidFilename; + /** * Utility class for files. * @@ -33,6 +36,10 @@ */ public class FileUtils { + public static final String CONTENT_PATH = "contents"; + + public static final String THUMBNAIL_PATH = ".thumbnails"; + /** * Hide constructor */ @@ -86,4 +93,43 @@ public static void copy(byte[] bs, Path path) throws IOException { Files.copy(inputStream, path); } } + + /** + * Get the path to the thumbnail for the given content + * + * @param albumBasePath base path + * @param id album id + * @param filename content filename + * @param contentFormat content format + * @return path + * @throws IllegalAlbumIdException if the id is invalid + * @throws IllegalFilenameException if the filename is invalid + */ + public static Path getThumbnailPath(final Path albumBasePath, final String id, final String filename, final ContentFormat contentFormat) throws IllegalAlbumIdException, IllegalFilenameException { + checkValidAlbumId(id); + checkValidFilename(filename); + + if (contentFormat.isImage()) { + return albumBasePath.resolve(id).resolve(CONTENT_PATH).resolve(THUMBNAIL_PATH).resolve(filename); + } else { + return albumBasePath.resolve(id).resolve(CONTENT_PATH).resolve(THUMBNAIL_PATH).resolve(removeSuffix(filename) + ".png"); + } + } + + /** + * Get the path to the content with the given id + * + * @param albumBasePath base path + * @param id album id + * @param filename content filename + * @return path + * @throws IllegalAlbumIdException if the id is invalid + * @throws IllegalFilenameException if the filename is invalid + */ + public static Path getContentPath(final Path albumBasePath, final String id, final String filename) throws IllegalAlbumIdException, IllegalFilenameException { + checkValidAlbumId(id); + checkValidFilename(filename); + + return albumBasePath.resolve(id).resolve(CONTENT_PATH).resolve(filename); + } } diff --git a/src/main/java/se/kecon/kalbum/PreviewSupport.java b/src/main/java/se/kecon/kalbum/PreviewSupport.java new file mode 100644 index 0000000..f6bad07 --- /dev/null +++ b/src/main/java/se/kecon/kalbum/PreviewSupport.java @@ -0,0 +1,232 @@ +package se.kecon.kalbum; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import javax.imageio.ImageIO; + +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Random; + +@Component +@Slf4j +public class PreviewSupport { + + private static final Color TRANSPARENT = new Color(0, 0, 0, 0); + + private static final String PREVIEW_FILENAME = "preview.png"; + + private static final int PREVIEW_WIDTH = 512; + private static final int PREVIEW_HEIGHT = 512; + + private static final int MAX_PREVIEW_IMAGE_WIDTH = 256; + private static final int MAX_PREVIEW_IMAGE_HEIGHT = 256; + + private static final int MAX_IMAGES = 10; + + @Setter(AccessLevel.PACKAGE) + private Random random = new Random(); + + /** + * Areas of where to place images in the preview + */ + @Getter + protected enum Area { + CENTER(0.45f, 0.45f, 0.55f, 0.55f), + TOP_LEFT(0f, 0f, 0.25f, 0.25f), + BOTTOM_LEFT(0f, 0.75f, 0.25f, 1f), + TOP_RIGHT(0.75f, 0f, 1f, 0.25f), + BOTTOM_RIGHT(0.75f, 0.75f, 1f, 1f), + LEFT(0f, 0.45f, 0.25f, 0.55f), + RIGHT(0.75f, 0.45f, 1f, 0.55f), + TOP(0.45f, 0f, 0.55f, 0.25f), + BOTTOM(0.45f, 0.75f, 0.55f, 1f); + + private final float x1; + private final float y1; + private final float x2; + private final float y2; + + /** + * Constructor + * + * @param x1 x1 + * @param y1 y1 + * @param x2 x2 + * @param y2 y2 + */ + Area(float x1, float y1, float x2, float y2) { + this.x1 = x1; + this.y1 = y1; + this.x2 = x2; + this.y2 = y2; + } + } + + @Setter + @Value("${kalbum.path}") + private Path albumBasePath; + + /** + * Create a preview image for the given album + * + * @param album album + * @throws IllegalAlbumIdException if the album id is invalid + * @throws IllegalFilenameException if the filename is invalid + * @throws IOException if the preview image could not be created + */ + public void createPreview(Album album) throws IllegalAlbumIdException, IllegalFilenameException, IOException { + final Path previewPath = FileUtils.getContentPath(albumBasePath, album.getId(), PREVIEW_FILENAME); + final BufferedImage bufferedImage = new BufferedImage(PREVIEW_WIDTH, PREVIEW_HEIGHT, BufferedImage.TYPE_INT_ARGB); + final List imageList = new ArrayList<>(); + + // Load a subset of random images from the album + loadImages(album, imageList); + + Graphics2D graphics = bufferedImage.createGraphics(); + try + { + // Fill with transparent color + graphics.setColor(TRANSPARENT); + graphics.fillRect(0, 0, bufferedImage.getWidth(), bufferedImage.getHeight()); + + for(int index = 0; index < imageList.size(); index++) + { + // Place images in random areas + final BufferedImage image = imageList.get(index); + + // Last image should be placed in the center + final int area = (index + 1) >= imageList.size() ? 0 : (index % Area.values().length); + randomPlaceImage(image, graphics, Area.values()[area]); + } + } + finally + { + graphics.dispose(); + } + + try(final OutputStream outputStream = Files.newOutputStream(previewPath)) + { + // Write preview image to file using NIO API. + ImageIO.write(bufferedImage, "png", outputStream); + } + } + + /** + * Load images from the album. Random images are selected. Maximum number of images is {@link #MAX_IMAGES}. Please note that only images are loaded, and not videos. + * + * @param album album + * @param imageList list of images + */ + protected void loadImages(final Album album,final List imageList) { + final List contents = new ArrayList<>(album.getContents()); + Collections.shuffle(contents); + contents.stream().filter((ContentData contentData) -> contentData.getContentType().startsWith("image/") && imageList.size() < MAX_IMAGES).forEach((ContentData contentData) -> { + try (final InputStream inputStream = Files.newInputStream(FileUtils.getContentPath(albumBasePath, album.getId(), contentData.getSrc()))) { + imageList.add(ImageIO.read(inputStream)); + } catch (IOException | IllegalAlbumIdException | IllegalFilenameException e) { + log.error("Could not load image", e); + } + }); + } + + /** + * Place image in the given area. The image will be scaled to fit in the area. It will be rotated randomly. + * + * @param source source image + * @param destination destination graphics + * @param area area + */ + protected void randomPlaceImage(final BufferedImage source, final Graphics2D destination, final Area area) { + + final float angle = ((random.nextFloat() * 40) + 340f) % 360f; + final int newWidth; + final int newHeight; + + if(source.getWidth() > source.getHeight()) { + // Landscape + final float ratio = (float) source.getWidth() / MAX_PREVIEW_IMAGE_WIDTH; + newWidth = MAX_PREVIEW_IMAGE_WIDTH; + newHeight = (int) (source.getHeight() / ratio); + } else { + // Portrait + final float ratio = (float) source.getHeight() / MAX_PREVIEW_IMAGE_HEIGHT; + newWidth = (int) (source.getWidth() / ratio); + newHeight = MAX_PREVIEW_IMAGE_HEIGHT; + } + + // Scale image to fit in preview + final BufferedImage scaledImage = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_ARGB); + final Graphics2D graphics = scaledImage.createGraphics(); + try { + graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC); + graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + graphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); + graphics.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY); + graphics.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY); + graphics.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE); + + graphics.drawImage(source, 0, 0, newWidth, newHeight, null); + } finally { + graphics.dispose(); + } + + // Rotate image randomly + final BufferedImage rotatedImage = rotateImage(scaledImage, angle); + + // Place image in random area + int x = Math.round(PREVIEW_WIDTH * area.getX1() + random.nextInt(Math.round((area.getX2() - area.getX1()) * PREVIEW_WIDTH))) - Math.round(0.5f * rotatedImage.getWidth()); + int y = Math.round(PREVIEW_HEIGHT * area.getY1() + random.nextInt(Math.round((area.getY2() - area.getY1()) * PREVIEW_HEIGHT))) - Math.round(0.5f * rotatedImage.getHeight()); + + // Make sure image is not placed outside preview area + x = (x + rotatedImage.getWidth() > PREVIEW_WIDTH ? PREVIEW_WIDTH - rotatedImage.getWidth() : x); + y = (y + rotatedImage.getHeight() > PREVIEW_HEIGHT ? PREVIEW_HEIGHT - rotatedImage.getHeight() : y); + x = Math.max(x, 0); + y = Math.max(y, 0); + + // Draw image + destination.drawImage(rotatedImage, null, x, y); + } + + /** + * Rotate image + * + * @param bufferedImage image + * @param angle angle + * @return rotated image + */ + protected BufferedImage rotateImage(final BufferedImage bufferedImage, final float angle) { + final double sin = Math.abs(Math.sin(Math.toRadians(angle))); + final double cos = Math.abs(Math.cos(Math.toRadians(angle))); + final int width = bufferedImage.getWidth(); + final int height = bufferedImage.getHeight(); + final int newWidth = (int) Math.floor(width * cos + height * sin); + final int newHeight = (int) Math.floor(height * cos + width * sin); + final BufferedImage rotated = new BufferedImage(newWidth, newHeight, bufferedImage.getType()); + final Graphics2D graphics2D = rotated.createGraphics(); + + try { + graphics2D.translate((newWidth - width) / 2, (newHeight - height) / 2); + graphics2D.rotate(Math.toRadians(angle), width / 2d, height / 2d); + graphics2D.drawRenderedImage(bufferedImage, null); + } + finally { + graphics2D.dispose(); + } + + return rotated; + } +} diff --git a/src/test/java/se/kecon/kalbum/AlbumControllerTest.java b/src/test/java/se/kecon/kalbum/AlbumControllerTest.java index 5169dab..e5bdda5 100644 --- a/src/test/java/se/kecon/kalbum/AlbumControllerTest.java +++ b/src/test/java/se/kecon/kalbum/AlbumControllerTest.java @@ -70,6 +70,9 @@ class AlbumControllerTest { @MockBean private AlbumDao albumDao; + @MockBean + private PreviewSupport previewSupport; + @Mock private Album album; @@ -79,6 +82,8 @@ class AlbumControllerTest { @Mock private ContentData contentData2; + private Path albumBasePath; + @BeforeEach void setUp() throws IOException, IllegalAlbumIdException { @@ -89,7 +94,7 @@ void setUp() throws IOException, IllegalAlbumIdException { Path rootDir = fileSystem.getPath("/"); // Create a directory - Path albumBasePath = rootDir.resolve("/var/lib/kalbum"); + albumBasePath = rootDir.resolve("/var/lib/kalbum"); Files.createDirectories(albumBasePath); albumController.setAlbumBasePath(albumBasePath); @@ -392,4 +397,26 @@ void testDeleteContentWithInvalidFilename() throws Exception { mockMvc.perform(delete("/albums/id1/contents/IMG_8654.jpg")) .andExpect(status().isNotFound()); } + + @Test + void testDeleteAlbumWithInvalidId() throws Exception { + mockMvc.perform(delete("/albums/id2/")) + .andExpect(status().isNotFound()); + } + + @Test + void testGetPreview() throws Exception { + + ClassLoader classLoader = getClass().getClassLoader(); + + try (InputStream inputStream = classLoader.getResourceAsStream("IMG_8653.jpg")) { + requireNonNull(inputStream, "Could not find file IMG_8653.jpg"); + + Files.copy(inputStream, FileUtils.getContentPath(albumBasePath, "id1", "preview.png")); + + mockMvc.perform(get("/albums/id1/preview.png")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.IMAGE_PNG_VALUE)); + } + } } \ No newline at end of file diff --git a/src/test/java/se/kecon/kalbum/FileUtilsTest.java b/src/test/java/se/kecon/kalbum/FileUtilsTest.java index cdf52d6..97032d3 100644 --- a/src/test/java/se/kecon/kalbum/FileUtilsTest.java +++ b/src/test/java/se/kecon/kalbum/FileUtilsTest.java @@ -22,7 +22,6 @@ import org.junit.jupiter.api.Test; import org.springframework.web.multipart.MultipartFile; -import java.io.ByteArrayInputStream; import java.io.IOException; import java.nio.file.FileSystem; import java.nio.file.Files; @@ -80,4 +79,23 @@ void testCopy() throws IOException { assertArrayEquals(bytes, Files.readAllBytes(path)); } } + + @Test + void testGetContentPath() throws IOException, IllegalAlbumIdException, IllegalFilenameException { + try (final FileSystem fileSystem = Jimfs.newFileSystem(UUID.randomUUID().toString(), Configuration.unix())) { + final Path path = fileSystem.getPath("/").resolve("var").resolve("lib").resolve("kalbum").resolve("id1").resolve("contents").resolve("test.png"); + + assertEquals(path, FileUtils.getContentPath(fileSystem.getPath("/var/lib/kalbum"), "id1", "test.png")); + } + } + + @Test + void testGetThumbnailPath() throws IOException, IllegalAlbumIdException, IllegalFilenameException { + try (final FileSystem fileSystem = Jimfs.newFileSystem(UUID.randomUUID().toString(), Configuration.unix())) { + final Path path = fileSystem.getPath("/").resolve("var").resolve("lib").resolve("kalbum").resolve("id1").resolve("contents").resolve(".thumbnails").resolve("test.png"); + + assertEquals(path, FileUtils.getThumbnailPath(fileSystem.getPath("/var/lib/kalbum"), "id1", "test.png", ContentFormat.PNG)); + } + } + } \ No newline at end of file diff --git a/static/assets/css/styles.css b/static/assets/css/styles.css index 9b57b42..87bd195 100644 --- a/static/assets/css/styles.css +++ b/static/assets/css/styles.css @@ -85,7 +85,7 @@ dialog { background: rgba(0, 0, 0, 0.7); } -button.close, button.previous, button.next, button.edit, button.download, button.delete { +button.close, button.previous, button.next, button.edit, button.download, button.delete, button.reload { position: absolute; width: 2em; height: 2em; @@ -136,6 +136,11 @@ button.delete { right: 0; } +button.reload { + top: 0; + right: 0; +} + #header { position: fixed; top: 0; @@ -228,6 +233,41 @@ img.icon { padding: 2em 2em 2em 2em; } + #albums ul { + list-style-type: none; + margin: 0; + padding: 0; + overflow: auto; + height: 100%; + } + + #albums ul li { + float: left; + margin: 0.5em 0.5em 0.5em 0; + padding: 0.5em 0 0.5em 0; + max-width: 580px; + min-width: 200px; + background-color: rgba(0, 0, 0, 0.4); + color: rgba(255, 255, 255, 1); + border: 1px solid rgba(255, 255, 255, 0.8); + border-radius: 0.3em; + box-shadow: 0 0 0.3em rgba(0, 0, 0, 0.4); + text-align: center; + vertical-align: middle; + overflow: hidden; + display: block; + position: relative; + } + +#albums ul li img { + width: 100%; + height: auto; + margin: 0; + padding: 0; + border: none; + vertical-align: middle; +} + a.album { color: rgba(255, 255, 255, 1); text-decoration: none; @@ -242,6 +282,13 @@ a.album:hover { font-weight: normal; } +fieldset { + border-radius: 0.3em; + margin-left: 0; + margin-right: 0.5em + +} + #editImageProperties, #editVideoProperties { position: absolute; bottom: 4em; diff --git a/static/assets/js/script.js b/static/assets/js/script.js index c00eac6..e1eabd4 100644 --- a/static/assets/js/script.js +++ b/static/assets/js/script.js @@ -9,7 +9,7 @@ function loadAlbums() { var html = ''; document.getElementById("albums").innerHTML = html; @@ -22,6 +22,12 @@ function loadAlbums() { showAlbum(this.getAttribute("data-albumId")); }); } + const reloadButtons = document.getElementsByClassName("reload"); + for (let i = 0; i < reloadButtons.length; i++) { + reloadButtons[i].addEventListener("click", function() { + reloadPreview(this); + }); + } } }) .catch((error) => { @@ -611,3 +617,19 @@ function showSelectAlbumDialog() { document.getElementById("selectAlbum").classList.add("fadeIn"); return false; } + +function reloadPreview(target) { + + console.log("Target: " + target); + for (const child of target.parentElement.childNodes) { + console.log(child.tagName); + if(child.tagName == "A") { + for(const child2 of child.childNodes) { + if (child2.tagName == "IMG") { + child2.src = ''; + child2.src = "albums/" + target.getAttribute("data-albumId") + "/preview.png?generate=true&t=" + new Date().getTime(); + } + } + } + } +} \ No newline at end of file From fc57edaae43a6151efeb7b868bbcf8072390ad2b Mon Sep 17 00:00:00 2001 From: Kenny Colliander Date: Mon, 7 Aug 2023 23:45:15 +0200 Subject: [PATCH 2/3] Changed to SecureRandom to avoid security warnings. This is however not a sensitive place, since we only use random for placing images in the preview. --- src/main/java/se/kecon/kalbum/PreviewSupport.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/se/kecon/kalbum/PreviewSupport.java b/src/main/java/se/kecon/kalbum/PreviewSupport.java index f6bad07..62f305b 100644 --- a/src/main/java/se/kecon/kalbum/PreviewSupport.java +++ b/src/main/java/se/kecon/kalbum/PreviewSupport.java @@ -16,6 +16,7 @@ import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; +import java.security.SecureRandom; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -38,7 +39,7 @@ public class PreviewSupport { private static final int MAX_IMAGES = 10; @Setter(AccessLevel.PACKAGE) - private Random random = new Random(); + private Random random = new SecureRandom(); /** * Areas of where to place images in the preview From 3eb9b9c17cb0a6130f8827091ac9904c061f61ad Mon Sep 17 00:00:00 2001 From: Kenny Colliander Date: Mon, 7 Aug 2023 23:45:47 +0200 Subject: [PATCH 3/3] Removed unnecessary try/catch --- .../java/se/kecon/kalbum/AlbumController.java | 27 ++++++++----------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/src/main/java/se/kecon/kalbum/AlbumController.java b/src/main/java/se/kecon/kalbum/AlbumController.java index 9ed8401..fc399bb 100644 --- a/src/main/java/se/kecon/kalbum/AlbumController.java +++ b/src/main/java/se/kecon/kalbum/AlbumController.java @@ -439,26 +439,21 @@ public ResponseEntity getPreview(@PathVariable(name = "id") return new ResponseEntity<>(HttpStatus.NOT_FOUND); } - try { - // Input is validated in getContentPath - final Path path = getContentPath(albumBasePath, id, "preview.png"); - - if (generate || !Files.exists(path)) { - this.previewSupport.createPreview(album.get()); - } + // Input is validated in getContentPath + final Path path = getContentPath(albumBasePath, id, "preview.png"); - InputStream inputStream = Files.newInputStream(path); - InputStreamResource inputStreamResource = new InputStreamResource(inputStream); + if (generate || !Files.exists(path)) { + this.previewSupport.createPreview(album.get()); + } - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.IMAGE_PNG); + InputStream inputStream = Files.newInputStream(path); + InputStreamResource inputStreamResource = new InputStreamResource(inputStream); - return new ResponseEntity<>(inputStreamResource, headers, HttpStatus.OK); - } catch (IllegalAlbumIdException | IllegalFilenameException | IOException e) { - return new ResponseEntity<>(HttpStatus.NOT_FOUND); - } + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.IMAGE_PNG); - } catch (IllegalAlbumIdException e) { + return new ResponseEntity<>(inputStreamResource, headers, HttpStatus.OK); + } catch (IllegalAlbumIdException | IllegalFilenameException | IOException e) { return new ResponseEntity<>(HttpStatus.NOT_FOUND); } }