Refactor photo gallery structure: remove old index.html, add new home.html and uploadPhoto.html templates, and delete unused styling.css. Introduce images.html for image components.

This commit is contained in:
Kiyan 2025-12-14 21:03:47 +02:00
parent 2c253251c7
commit 8320954344
38 changed files with 773 additions and 555 deletions

View File

@ -1,56 +0,0 @@
package com.example.PhotoGallery.Controller;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.nio.file.*;
@RestController
public class ImageController {
@Value("${photogallery.paths.thumbnails}")
private String thumbnailsPath;
@Value("${photogallery.paths.originals}")
private String originalsPath;
@GetMapping("/thumbnails/{filename:.+}")
public ResponseEntity<Resource> getThumbnail(@PathVariable String filename) {
return serveFile(thumbnailsPath, filename);
}
@GetMapping("/images/{filename:.+}")
public ResponseEntity<Resource> getFullImage(@PathVariable String filename) {
return serveFile(originalsPath, filename);
}
private ResponseEntity<Resource> serveFile(String baseDir, String filename) {
try {
Path basePath = Paths.get(baseDir).toAbsolutePath().normalize();
Path file = basePath.resolve(filename).normalize();
// Prevent path traversal
if (!file.startsWith(basePath)) {
return ResponseEntity.badRequest().build();
}
Resource resource = new UrlResource(file.toUri());
if (resource.exists() && resource.isReadable()) {
String contentType = Files.probeContentType(file);
if (contentType == null) {
contentType = "application/octet-stream";
}
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType(contentType))
.body(resource);
}
} catch (Exception e) {
// Log exception if needed
}
return ResponseEntity.notFound().build();
}
}

View File

@ -1,24 +0,0 @@
package com.example.PhotoGallery.Controller;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.List;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import com.example.PhotoGallery.Services.PhotoService;
@Controller
public class PageController {
@Autowired
private PhotoService photoService;
@GetMapping("/")
public String index(Model model) {
List<String> imageFileNames = photoService.getImageFileNames();
model.addAttribute("imageFileNames", imageFileNames);
return "index";
}
}

View File

@ -1,32 +0,0 @@
package com.example.PhotoGallery.Controller;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.example.PhotoGallery.Models.Photo;
import com.example.PhotoGallery.Services.PhotoService;
@RestController
@RequestMapping("/")
public class PhotoController {
@Autowired
private PhotoService photoService;
// HTTP requests handling
@GetMapping("/photos")
public List<Photo> getAllPhotos() {
return photoService.getAllPhotos();
}
@GetMapping("/updateDB")
public void updateDatabase() {
photoService.populateDatabase();
}
}

View File

@ -0,0 +1,38 @@
package com.example.PhotoGallery.Controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import com.example.PhotoGallery.Services.PhotoService;
@Controller
public class PhotosController {
@Autowired
PhotoService photoService;
private final int RANDOM_NUMBER_IMAGES = 5;
private final int FIRST_PAGE = 0;
private final int INITIAL_PAGE_SIZE = 30;
@GetMapping("/home")
private String home(Model model) {
photoService.WriteImagesFromDirToDB();
model.addAttribute("images", photoService.getPagedPhotos(FIRST_PAGE, INITIAL_PAGE_SIZE));
model.addAttribute("carouselImages", photoService.getRandomPhotos(RANDOM_NUMBER_IMAGES));
return "home";
}
@GetMapping("/images")
public String test(Model model) {
model.addAttribute("images", photoService.getPagedPhotos(0, 10));
return "home :: images";
}
}

View File

@ -0,0 +1,14 @@
package com.example.PhotoGallery.Controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class UploadPhoto {
@GetMapping("/uploadPhoto")
private String uploadPhoto(){
return "uploadPhoto";
}
}

View File

@ -1,35 +1,53 @@
package com.example.PhotoGallery.Models; package com.example.PhotoGallery.Models;
import java.util.List;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.ElementCollection;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue; import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType; import jakarta.persistence.GenerationType;
import jakarta.persistence.Id; import jakarta.persistence.Id;
import jakarta.persistence.Table; import jakarta.persistence.Table;
import lombok.AllArgsConstructor; import lombok.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity @Entity
@Table(name = "photos") @Table(name = "Photos")
@Getter @Data
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Photo { public class Photo {
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; private Long id;
@Column(nullable = false)
private String title;
private String image_name; @Column
private String path; private List<Integer> tags;
private String thumbnail_path;
@Column(length = 10000) @Column
private String metadata; private Integer width;
@Column
private Integer height;
@Column(unique = true, nullable = false)
private String fileName;
@Column(unique = true, nullable = false)
private String filePath;
@Column(unique = true, nullable = false)
private String thumbnailPath;
@Column
private String mimeType;
@Column
private Double file_size;
@Column
private boolean isFavourite;
} }

View File

@ -0,0 +1,24 @@
package com.example.PhotoGallery.Models;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Data;
@Entity
@Table(name = "tags")
@Data
public class Tags {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column
private String name;
}

View File

@ -0,0 +1,31 @@
package com.example.PhotoGallery.Models;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Data;
@Entity
@Table(name = "users")
@Data
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long Id;
@Column(name = "username", unique = true, nullable = false)
private String username;
@Column
private String password;
@Column
private String email;
}

View File

@ -1,10 +1,12 @@
package com.example.PhotoGallery.Repos; package com.example.PhotoGallery.Repos;
import java.util.List; import java.util.List;
import java.util.Set;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import com.example.PhotoGallery.Models.Photo; import com.example.PhotoGallery.Models.Photo;
@ -12,11 +14,17 @@ import com.example.PhotoGallery.Models.Photo;
@Repository @Repository
public interface PhotoRepo extends JpaRepository<Photo, Long> { public interface PhotoRepo extends JpaRepository<Photo, Long> {
boolean existsByPath(String path); boolean existsByFileName(String filName);
@Query("SELECT p.thumbnail_path FROM Photo p")
List<String> findAllThumbnailPaths();
@Query("SELECT p.path FROM Photo p") @Query(value = "SELECT file_name FROM photos ", nativeQuery = true)
Set<String> findAllPaths(); List<String> getAllFileNames();
@Query(value = "SELECT * FROM photos", nativeQuery = true)
Page<Photo> getPagedPhotos(Pageable pageable);
@Query(value = "SELECT * FROM photos ORDER BY RAND() LIMIT :numRecords", nativeQuery = true)
List<Photo> getRandomPhotos(@Param("numRecords") int numRandRecords);
} }

View File

@ -1,100 +0,0 @@
package com.example.PhotoGallery.Services;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.json.simple.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import com.example.PhotoGallery.Models.Photo;
import com.example.PhotoGallery.Repos.PhotoRepo;
@Service
public class DatabasePopulation {
@Autowired
private PhotoRepo photoRepo;
@Autowired
private ImageCompression imageCompressor;
@Autowired
private ExtractImageMetadata extractImageMetadata;
@Autowired
private JsonParser jsonParser;
@Value("${photogallery.paths.thumbnails}")
private String compressedImgPath;
@Value("${photogallery.paths.originals}")
private String originalImgPath;
public void populateDatabase() {
//Fetch all existing photo paths from the DB in one query.
Set<String> existingPaths = photoRepo.findAllPaths();
//Get all image paths from the file system.
List<String> allImagePaths = getImagePaths();
for (String path : allImagePaths) {
if (!existingPaths.contains(path)) {
if (!imageCompressor.compressImage(path, compressedImgPath)) {
continue;
}
String imageMetaData = extractImageMetadata.getImageMetaData(path);
Photo photo = new Photo();
photo.setPath(path);
String thumbnailFile = compressedImgPath + new File(path).getName();
photo.setThumbnail_path(thumbnailFile);
photo.setMetadata(imageMetaData);
// extract photo title from metaData
JSONObject imageDetails = jsonParser.getValue(jsonParser.parseJson(imageMetaData), "File");
String imageName = (String) imageDetails.get("File Name");
photo.setImage_name(imageName);
photoRepo.save(photo);
}
}
}
private List<String> getImagePaths() {
List<String> photos = new ArrayList<>();
try {
Path imagesPath = new File(originalImgPath).toPath();
try (Stream<Path> paths = Files.walk(imagesPath)) {
photos = paths
.filter(Files::isRegularFile)
.map(Path::toString) // The path object is already the full path
.collect(Collectors.toList());
return photos;
}
} catch (
IOException e) {
e.printStackTrace();
}
return photos;
}
}

View File

@ -1,32 +0,0 @@
package com.example.PhotoGallery.Services;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.example.PhotoGallery.Models.Photo;
import com.example.PhotoGallery.Repos.PhotoRepo;
@Service
public class ImageHtmlBuilder {
@Autowired
private PhotoRepo photoRepo;
public List<String> getImageFileNames() {
List<String> fileNames = new ArrayList<>();
List<Photo> allPhotos = photoRepo.findAll();
for (Photo photo : allPhotos) {
String thumbnailPath = photo.getThumbnail_path();
String filename = new File(thumbnailPath).getName();
fileNames.add(filename);
}
return fileNames;
}
}

View File

@ -1,8 +1,17 @@
package com.example.PhotoGallery.Services; package com.example.PhotoGallery.Services;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List; import java.util.List;
import java.util.stream.Stream;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.example.PhotoGallery.Models.Photo; import com.example.PhotoGallery.Models.Photo;
@ -11,35 +20,94 @@ import com.example.PhotoGallery.Repos.PhotoRepo;
@Service @Service
public class PhotoService { public class PhotoService {
@Autowired
private ExtractImageMetadata extractMetaData;
@Autowired
private ImageCompression imageCompression;
@Autowired @Autowired
private PhotoRepo photoRepo; private PhotoRepo photoRepo;
@Autowired private Path originalFilesPath;
private DatabasePopulation databasePopulation; private Path thumbnailsPath;
@Autowired public PhotoService(@Value("${photogallery.paths.originals}") String originalFilesPath,
private ImageHtmlBuilder imageHtmlBuilder; @Value("${photogallery.paths.thumbnails}") String thumbnailsPath) {
this.originalFilesPath = Paths.get(originalFilesPath);
public void populateDatabase() { this.thumbnailsPath = Paths.get(thumbnailsPath);
databasePopulation.populateDatabase();
} }
public List<String> getImageFileNames() { /**
return imageHtmlBuilder.getImageFileNames(); * Get all the original images from the server
* then compress each image
* and write the compressed image(thumbnail) to the thumnails directory.
* We also save a record of the image to the DB (writePhotoDataToDB)
*
*
*/
public void WriteImagesFromDirToDB() {
// initially get all the file names from the DB so we dont have to hit the DB
// for each photo
List<String> allFileName = getAllFileNames();
try (Stream<Path> imagePaths = Files.list(originalFilesPath)) {
imagePaths.forEach((imagePath) -> {
//only create the thumbnail and write to the DB if the photo doesnt exist in the DB
if (!allFileName.contains(imagePath.getFileName().toString())) {
//compress the image and write the new image to the thumbnail folder
imageCompression.compressImage(imagePath.toString(), thumbnailsPath.toString());
writePhotoDataToDB(imagePath);
}
});
} catch (IOException e) {
e.printStackTrace();
}
} }
private void writePhotoDataToDB(Path imagePath) {
public List<Photo> getAllPhotos(){ Photo photo = new Photo();
photo.setFilePath("images" + "/" + imagePath.getFileName().toString());
photo.setFileName(imagePath.getFileName().toString());
photo.setTitle("");
photo.setFavourite(false);
// get info from metadata TODO: extract details from metadata string
photo.setFile_size(0.0);
photo.setHeight(0);
photo.setWidth(0);
photo.setMimeType("");
photo.setTags(null);
photo.setThumbnailPath("thumbnails" + "/" + imagePath.getFileName().toString());
photoRepo.save(photo);
}
public List<String> getAllFileNames() {
return photoRepo.getAllFileNames();
}
public List<Photo> getAllPhotots(){
return photoRepo.findAll(); return photoRepo.findAll();
} }
public List<String> getAllThumbnailsPaths(){ public Page<Photo> getPagedPhotos(int pageNo, int pageSize){
return photoRepo.findAllThumbnailPaths(); Pageable pageable = PageRequest.of(pageNo, pageSize);
return photoRepo.getPagedPhotos(pageable);
} }
public boolean existsByPath(String path){ public List<Photo> getRandomPhotos(int numRandRecords){
return photoRepo.existsByPath(path); return photoRepo.getRandomPhotos(numRandRecords);
} }
} }

View File

@ -0,0 +1,25 @@
package com.example.PhotoGallery.WebConfig;
import java.util.concurrent.TimeUnit;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.CacheControl;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class StaticResourceConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/images/**")
.addResourceLocations("file:/srv/nas/PhotoGalleryImages/images/")
.setCacheControl(CacheControl.maxAge(30, TimeUnit.DAYS));
registry.addResourceHandler("/thumbnails/**")
.addResourceLocations("file:/srv/nas/PhotoGalleryImages/thumbnails/")
.setCacheControl(CacheControl.maxAge(30, TimeUnit.DAYS));
}
}

View File

@ -1,16 +0,0 @@
package com.example.PhotoGallery.WebConfig;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**")
.addResourceLocations("file:/srv/nas/PhotoGalleryImages/thumbnails/")
.setCachePeriod(3600 * 24 * 30); // 30 days cache
}
}

View File

@ -8,8 +8,9 @@ spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.show-sql=true spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=update spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect
# ===================================================================
# PhotoGallery Application Properties
# =================================================================== #===========================================================================
#Photos directories
photogallery.paths.originals=/srv/nas/PhotoGalleryImages/images/ photogallery.paths.originals=/srv/nas/PhotoGalleryImages/images/
photogallery.paths.thumbnails=/srv/nas/PhotoGalleryImages/thumbnails/ photogallery.paths.thumbnails=/srv/nas/PhotoGalleryImages/thumbnails/

View File

@ -0,0 +1,208 @@
/* !
* baguetteBox.js
* @author feimosi
* @version 1.11.1
* @url https://github.com/feimosi/baguetteBox.js */
#baguetteBox-overlay {
display: none;
opacity: 0;
position: fixed;
overflow: hidden;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1000000;
background-color: #222;
background-color: rgba(0,0,0,.8);
-webkit-transition: opacity .5s ease;
transition: opacity .5s ease;
}
#baguetteBox-overlay.visible {
opacity: 1;
}
#baguetteBox-overlay .full-image {
display: inline-block;
position: relative;
width: 100%;
height: 100%;
text-align: center;
}
#baguetteBox-overlay .full-image figure {
display: inline;
margin: 0;
height: 100%;
}
#baguetteBox-overlay .full-image img {
display: inline-block;
width: auto;
height: auto;
max-height: 100%;
max-width: 100%;
vertical-align: middle;
-webkit-box-shadow: 0 0 8px rgba(0,0,0,.6);
-moz-box-shadow: 0 0 8px rgba(0,0,0,.6);
box-shadow: 0 0 8px rgba(0,0,0,.6);
}
#baguetteBox-overlay .full-image figcaption {
display: block;
position: absolute;
bottom: 0;
width: 100%;
text-align: center;
line-height: 1.8;
white-space: normal;
color: #ccc;
background-color: #000;
background-color: rgba(0,0,0,.6);
font-family: sans-serif;
}
#baguetteBox-overlay .full-image:before {
content: "";
display: inline-block;
height: 50%;
width: 1px;
margin-right: -1px;
}
#baguetteBox-slider {
position: absolute;
left: 0;
top: 0;
height: 100%;
width: 100%;
white-space: nowrap;
-webkit-transition: left .4s ease,-webkit-transform .4s ease;
transition: left .4s ease,-webkit-transform .4s ease;
transition: left .4s ease,transform .4s ease;
transition: left .4s ease,transform .4s ease,-webkit-transform .4s ease,-moz-transform .4s ease;
}
#baguetteBox-slider.bounce-from-right {
-webkit-animation: bounceFromRight .4s ease-out;
animation: bounceFromRight .4s ease-out;
}
#baguetteBox-slider.bounce-from-left {
-webkit-animation: bounceFromLeft .4s ease-out;
animation: bounceFromLeft .4s ease-out;
}
@keyframes bounceFromRight {
0%, 100% {
margin-left: 0;
}
50% {
margin-left: -30px;
}
}
@keyframes bounceFromLeft {
0%, 100% {
margin-left: 0;
}
50% {
margin-left: 30px;
}
}
.baguetteBox-button#next-button, .baguetteBox-button#previous-button {
top: 50%;
top: calc(50% - 30px);
width: 44px;
height: 60px;
}
.baguetteBox-button {
position: absolute;
cursor: pointer;
outline: 0;
padding: 0;
margin: 0;
border: 0;
-moz-border-radius: 15%;
border-radius: 15%;
background-color: #323232;
background-color: rgba(50,50,50,.5);
color: #ddd;
font: 1.6em sans-serif;
-webkit-transition: background-color .4s ease;
transition: background-color .4s ease;
}
.baguetteBox-button:focus, .baguetteBox-button:hover {
background-color: rgba(50,50,50,.9);
}
.baguetteBox-button#next-button {
right: 2%;
}
.baguetteBox-button#previous-button {
left: 2%;
}
.baguetteBox-button#close-button {
top: 20px;
right: 2%;
right: calc(2% + 6px);
width: 30px;
height: 30px;
}
.baguetteBox-button svg {
position: absolute;
left: 0;
top: 0;
}
.baguetteBox-spinner {
width: 40px;
height: 40px;
display: inline-block;
position: absolute;
top: 50%;
left: 50%;
margin-top: -20px;
margin-left: -20px;
}
.baguetteBox-double-bounce1, .baguetteBox-double-bounce2 {
width: 100%;
height: 100%;
-moz-border-radius: 50%;
border-radius: 50%;
background-color: #fff;
opacity: .6;
position: absolute;
top: 0;
left: 0;
-webkit-animation: bounce 2s infinite ease-in-out;
animation: bounce 2s infinite ease-in-out;
}
.baguetteBox-double-bounce2 {
-webkit-animation-delay: -1s;
animation-delay: -1s;
}
@keyframes bounce {
0%, 100% {
-webkit-transform: scale(0);
-moz-transform: scale(0);
transform: scale(0);
}
50% {
-webkit-transform: scale(1);
-moz-transform: scale(1);
transform: scale(1);
}
}

View File

@ -0,0 +1,57 @@
.bs-icon {
--bs-icon-size: .75rem;
display: flex;
flex-shrink: 0;
justify-content: center;
align-items: center;
font-size: var(--bs-icon-size);
width: calc(var(--bs-icon-size) * 2);
height: calc(var(--bs-icon-size) * 2);
color: var(--bs-primary);
}
.bs-icon-xs {
--bs-icon-size: 1rem;
width: calc(var(--bs-icon-size) * 1.5);
height: calc(var(--bs-icon-size) * 1.5);
}
.bs-icon-sm {
--bs-icon-size: 1rem;
}
.bs-icon-md {
--bs-icon-size: 1.5rem;
}
.bs-icon-lg {
--bs-icon-size: 2rem;
}
.bs-icon-xl {
--bs-icon-size: 2.5rem;
}
.bs-icon.bs-icon-primary {
color: var(--bs-white);
background: var(--bs-primary);
}
.bs-icon.bs-icon-primary-light {
color: var(--bs-primary);
background: rgba(var(--bs-primary-rgb), .2);
}
.bs-icon.bs-icon-semi-white {
color: var(--bs-primary);
background: rgba(255, 255, 255, .5);
}
.bs-icon.bs-icon-rounded {
border-radius: .5rem;
}
.bs-icon.bs-icon-circle {
border-radius: 50%;
}

View File

@ -0,0 +1,4 @@
.aspect-ratio-4x3 {
aspect-ratio: 4/3;
}

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,3 @@
if (document.querySelectorAll('[data-bss-baguettebox]').length > 0) {
baguetteBox.run('[data-bss-baguettebox]', { animation: 'slideIn' });
}

File diff suppressed because one or more lines are too long

View File

@ -1,55 +0,0 @@
body {
background-color: black;
font-family: "Segoe UI", sans-serif;
}
h2 {
font-weight: 500;
color: #343a40;
}
.gallery-container .row {
margin-bottom: 1rem;
}
.gallery-container img {
transition: transform 0.3s ease;
cursor: pointer;
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 0.25rem;
}
.gallery-container img:hover {
transform: scale(1.05);
}
/* Responsive Column Widths for Bootstrap */
@media (min-width: 576px) {
.gallery-container .col {
flex: 0 0 50%;
max-width: 50%;
}
}
@media (min-width: 768px) {
.gallery-container .col {
flex: 0 0 33.33%;
max-width: 33.33%;
}
}
@media (min-width: 992px) {
.gallery-container .col {
flex: 0 0 25%;
max-width: 25%;
}
}
@media (min-width: 1200px) {
.gallery-container .col {
flex: 0 0 20%;
max-width: 20%;
}
}

View File

@ -0,0 +1,125 @@
<!DOCTYPE html>
<html data-bs-theme="light" lang="en" style="width: 100%;height: 100%;">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>Untitled</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.6/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="/css/bss-overrides.css">
<link rel="stylesheet" href="/css/Lightbox-Gallery-baguetteBox.min.css">
<link rel="stylesheet" href="/css/Navbar-Centered-Links-icons.css">
</head>
<body style="height: 100%;width: 100%;">
<nav class="navbar navbar-expand-md bg-body mb-3 py-3">
<div class="container">
<a class="navbar-brand d-flex align-items-center" href="#">
<span
class="bs-icon-sm bs-icon-rounded bs-icon-primary d-flex justify-content-center align-items-center me-2 bs-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor"
viewBox="0 0 16 16" class="bi bi-camera">
<path
d="M15 12a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h1.172a3 3 0 0 0 2.12-.879l.83-.828A1 1 0 0 1 6.827 3h2.344a1 1 0 0 1 .707.293l.828.828A3 3 0 0 0 12.828 5H14a1 1 0 0 1 1 1zM2 4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2h-1.172a2 2 0 0 1-1.414-.586l-.828-.828A2 2 0 0 0 9.172 2H6.828a2 2 0 0 0-1.414.586l-.828.828A2 2 0 0 1 3.172 4z">
</path>
<path
d="M8 11a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5m0 1a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7M3 6.5a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0">
</path>
</svg>
</span>
<span>Photo Gallery</span>
</a>
<button data-bs-toggle="collapse" class="navbar-toggler" data-bs-target="#navcol-3">
<span class="visually-hidden">Toggle navigation</span>
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navcol-3">
<ul class="navbar-nav align-items-center mx-auto">
<li class="nav-item">
<a class="nav-link active my-0 py-2" th:href="@{/uploadPhoto}">First Item</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Second Item</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Third Item</a>
</li>
</ul>
</div>
</div>
</nav>
<div class="container ps-0 pe-0">
<div class="carousel slide" data-bs-ride="carousel" data-bs-interval="2500" data-bs-pause="false"
id="carousel-1">
<div class="carousel-inner">
<div th:each="carouselImage, iterStat : ${carouselImages}" class="carousel-item"
th:classappend="${iterStat.first} ? active : ''">
<img class="object-fit-cover w-100 d-block" alt="Slide Image" th:src="@{${carouselImage.filePath}}"
height="500" loading="eager" width="1080">
</div>
</div>
<div>
<a class="carousel-control-prev" href="#carousel-1" role="button" data-bs-slide="prev"><span
class="carousel-control-prev-icon"></span>
<span class="visually-hidden">Previous</span>
</a>
<a class="carousel-control-next" href="#carousel-1" role="button" data-bs-slide="next">
<span class="carousel-control-next-icon"></span>
<span class="visually-hidden">Next</span>
</a>
</div>
<div class="carousel-indicators">
<button th:each="carouselImage,iterStat : ${carouselImages}" th:class="${iterStat.first} ? active : ''"
type="button" data-bs-target="#carousel-1"
th:attr="data-bs-slide-to=${iterStat.count - 1}"></button>
</div>
</div>
<div>
<section class="pt-5 pb-0 my-0 py-xl-5" style="padding: 0;">
<div class="d-flex">
<div class="w-100">
<input class="mx-2 px-2 my-3 py-1" type="search" placeholder="Search Photos...">
</div>
<div class="d-flex w-100 align-items-center justify-content-end ">
<select class="p-2 me-3" name="" id="">
<option value="">People</option>
<option value="">Landscapes</option>
<option value="">Animals</option>
<option value="">Black & White</option>
</select>
</div>
</div>
<div class="container">
<div th:fragment="images" class="row gx-2 gy-2 row-cols-1 row-cols-md-2 row-cols-xl-3 mt-0"
data-bss-baguettebox="">
<div class="col" th:each="image : ${images}">
<a th:href="@{${image.filePath}}">
<img class="img-fluid aspect-ratio-4x3 object-fit-cover w-100 h-100"
alt="Replace with image description" th:src="@{${image.thumbnailPath}}"
loading="lazy">
</a>
</div>
<!-- HTMX infinite-scroll sentinel -->
<div th:if="${images}" hx-get="/images" hx-trigger="revealed"
hx-swap="afterend" style="height: 1px">
</div>
</div>
</div>
</section>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.6/dist/js/bootstrap.bundle.min.js"></script>
<script src="/js/Lightbox-Gallery-baguetteBox.min.js"></script>
<script src="/js/Lightbox-Gallery.js"></script>
<script src="https://unpkg.com/htmx.org@1.9.12"></script>
</body>
</html>

View File

@ -1,201 +0,0 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Photo Gallery</title>
<!-- Bootstrap CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body {
background: linear-gradient(160deg, #e0eafc, #cfdef3);
font-family: 'Segoe UI', sans-serif;
color: #333;
}
h2 {
font-weight: 700;
color: #1b1f3b;
}
.navbar-brand {
font-weight: 600;
font-size: 1.5rem;
}
/* Masonry Grid */
.gallery-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
grid-auto-rows: 10px;
gap: 10px;
}
.masonry-item img {
width: 100%;
height: auto;
border-radius: 10px;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.15);
cursor: pointer;
transition: transform 0.3s ease, box-shadow 0.3s ease;
}
.masonry-item img:hover {
transform: scale(1.03);
box-shadow: 0 16px 30px rgba(0, 0, 0, 0.25);
}
/* Modal styling */
.modal-content {
background-color: rgba(0, 0, 0, 0.95);
border: none;
border-radius: 10px;
}
.modal-body img {
max-height: 85vh;
border-radius: 10px;
}
.modal-navigation {
position: absolute;
top: 50%;
width: 100%;
display: flex;
justify-content: space-between;
transform: translateY(-50%);
padding: 0 1rem;
}
.modal-navigation button {
background: rgba(0, 0, 0, 0.5);
border: none;
color: #fff;
font-size: 2rem;
border-radius: 50%;
width: 50px;
height: 50px;
cursor: pointer;
transition: background 0.2s;
}
.modal-navigation button:hover {
background: rgba(0, 0, 0, 0.8);
}
</style>
</head>
<body>
<!-- Navbar -->
<nav class="navbar navbar-expand-lg navbar-dark bg-dark shadow-sm">
<div class="container-fluid">
<a class="navbar-brand" href="#">PhotoGallery</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item"><a class="nav-link active disabled" href="#">All</a></li>
<li class="nav-item"><a class="nav-link disabled" href="#">Favorites</a></li>
<li class="nav-item"><a class="nav-link disabled" href="#">Recent</a></li>
</ul>
<form class="d-flex" role="search">
<input class="form-control me-2 disabled" type="search" placeholder="Search">
<button class="btn btn-outline-light disabled" type="submit">Search</button>
</form>
</div>
</div>
</nav>
<!-- Hero Section -->
<div class="container text-center my-5">
<h2>Welcome To The Gallery</h2>
<p class="text-muted">Memories</p>
</div>
<!-- Gallery Container -->
<div class="container gallery-container" id="gallery-container">
<!--/* Thymeleaf loop to generate image elements */-->
<div class="masonry-item" th:each="fileName : ${imageFileNames}">
<img th:data-src="@{/thumbnails/{name}(name=${fileName})}"
th:data-full="@{/images/{name}(name=${fileName})}" class="lazy-img" loading="lazy" />
</div>
</div>
<!-- Image Modal -->
<div class="modal fade" id="imageModal" tabindex="-1" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered modal-xl">
<div class="modal-content">
<div class="modal-body text-center p-0 position-relative">
<img id="modalImage" src="" class="img-fluid" alt="Full size">
<div class="modal-navigation">
<button id="prevImage">&#10094;</button>
<button id="nextImage">&#10095;</button>
</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', () => {
const galleryContainer = document.getElementById('gallery-container');
const lazyImages = galleryContainer.querySelectorAll('img.lazy-img');
function resizeMasonryItem(img) {
const rowHeight = parseInt(getComputedStyle(galleryContainer).getPropertyValue('grid-auto-rows'));
const rowGap = parseInt(getComputedStyle(galleryContainer).getPropertyValue('gap'));
const rowSpan = Math.ceil((img.getBoundingClientRect().height + rowGap) / (rowHeight + rowGap));
img.parentElement.style.gridRowEnd = `span ${rowSpan}`;
}
function resizeAllMasonry() {
galleryContainer.querySelectorAll('.masonry-item img').forEach(img => {
if (img.complete) resizeMasonryItem(img);
});
}
// Lazy load + masonry setup
const observer = new IntersectionObserver((entries, obs) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
obs.unobserve(img);
img.onload = () => resizeMasonryItem(img);
}
});
});
lazyImages.forEach(img => observer.observe(img));
// Modal setup
const modal = new bootstrap.Modal(document.getElementById('imageModal'));
const modalImg = document.getElementById('modalImage');
let currentIndex = -1;
const images = Array.from(lazyImages);
function showModal(idx) { currentIndex = idx; modalImg.src = images[idx].dataset.full; modal.show(); }
images.forEach((img, idx) => img.addEventListener('click', () => showModal(idx)));
document.getElementById('prevImage').addEventListener('click', () => {
currentIndex = (currentIndex - 1 + images.length) % images.length;
modalImg.src = images[currentIndex].dataset.full;
});
document.getElementById('nextImage').addEventListener('click', () => {
currentIndex = (currentIndex + 1) % images.length;
modalImg.src = images[currentIndex].dataset.full;
});
// Recalculate on resize
window.addEventListener('resize', resizeAllMasonry);
window.addEventListener('load', resizeAllMasonry);
});
</script>
</body>
</html>

View File

@ -0,0 +1,92 @@
<!DOCTYPE html>
<html data-bs-theme="light" lang="en" style="width: 100%;height: 100%;">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>Untitled</title>
<link rel="stylesheet" href="/css/uploadPhoto.css">
</head>
<body style="width: 100%;height: 100%;">
<nav class="navbar navbar-expand-md bg-body pt-0 mb-0 pb-0" style="height: 10%;">
<div class="container"><a class="navbar-brand d-flex align-items-center" href="#"><span
class="bs-icon-sm bs-icon-rounded bs-icon-primary d-flex justify-content-center align-items-center me-2 bs-icon"><svg
xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" fill="currentColor"
viewBox="0 0 16 16" class="bi bi-camera">
<path
d="M15 12a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1h1.172a3 3 0 0 0 2.12-.879l.83-.828A1 1 0 0 1 6.827 3h2.344a1 1 0 0 1 .707.293l.828.828A3 3 0 0 0 12.828 5H14a1 1 0 0 1 1 1zM2 4a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V6a2 2 0 0 0-2-2h-1.172a2 2 0 0 1-1.414-.586l-.828-.828A2 2 0 0 0 9.172 2H6.828a2 2 0 0 0-1.414.586l-.828.828A2 2 0 0 1 3.172 4z">
</path>
<path
d="M8 11a2.5 2.5 0 1 1 0-5 2.5 2.5 0 0 1 0 5m0 1a3.5 3.5 0 1 0 0-7 3.5 3.5 0 0 0 0 7M3 6.5a.5.5 0 1 1-1 0 .5.5 0 0 1 1 0">
</path>
</svg></span><span>Photo Gallery</span></a><button data-bs-toggle="collapse" class="navbar-toggler"
data-bs-target="#navcol-3"><span class="visually-hidden">Toggle navigation</span><span
class="navbar-toggler-icon"></span></button>
<div class="collapse navbar-collapse" id="navcol-3">
<ul class="navbar-nav align-items-center mx-auto">
<li class="nav-item">
<div class="nav-item dropdown"><a class="dropdown-toggle px-2 py-2" aria-expanded="false"
data-bs-toggle="dropdown" href="#" style="color: black;">Filter&nbsp;</a>
<div class="dropdown-menu"><a class="dropdown-item" href="#">First Item</a><a
class="dropdown-item" href="#">Second Item</a><a class="dropdown-item"
href="#">Third Item</a></div>
</div>
</li>
<li class="nav-item"><a class="nav-link active my-0 py-2" href="#">First Item</a></li>
<li class="nav-item"><a class="nav-link" href="#">Second Item</a></li>
<li class="nav-item"><a class="nav-link" href="#">Third Item</a></li>
</ul>
</div>
</div>
</nav>
<section class="position-relative d-flex justify-content-center py-4 py-xl-5" style="width: 100%;height: 90%;">
<div style="width: 100%;">
<div>
<div class="container position-relative" style="width: 100%;">
<div class="row d-flex justify-content-center">
<div class="col-md-8 col-lg-6 col-xl-5 col-xxl-4">
<div class="card mb-5">
<div class="card-body p-sm-5">
<h2 class="text-center mb-4">Add Photo</h2>
<form method="post">
<div class="mb-3">
<input class="form-control" type="text" id="title" name="title"
placeholder="Title" required="">
</div>
<div class="d-flex mb-3">
<input class="form-control me-2" type="number" id="width" name="width"
placeholder="Width" inputmode="numeric">
<input class="form-control ms-2" type="number" id="height" name="height"
placeholder="Height" inputmode="numeric">
</div>
<div class="mb-3">
<input class="form-control" type="datetime" id="datetime" name="datetime"
placeholder="Capture Date" required="">
</div>
<div class="mb-3">
<textarea class="form-control" id="Tags-2" name="Tags" rows="6"
placeholder="Tags"></textarea>
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="formCheck-1">
<label class="form-check-label" for="formCheck-1">Favourite</label>
</div>
</div>
<div class="mb-3"><input class="form-control" type="file"></div>
<div><button class="btn btn-primary w-100 d-block" type="submit">Upload
Photo</button></div>
</form>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<script src="/js/uploadPhoto.js"></script>
</body>
</html>