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.
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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";
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -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";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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 image_name;
|
private String title;
|
||||||
private String path;
|
|
||||||
private String thumbnail_path;
|
|
||||||
|
|
||||||
@Column(length = 10000)
|
@Column
|
||||||
private String metadata;
|
private List<Integer> tags;
|
||||||
|
|
||||||
|
@Column
|
||||||
|
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;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,22 +1,30 @@
|
||||||
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;
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
public interface PhotoRepo extends JpaRepository<Photo, Long> {
|
public interface PhotoRepo extends JpaRepository<Photo, Long> {
|
||||||
|
|
||||||
|
boolean existsByFileName(String filName);
|
||||||
|
|
||||||
boolean existsByPath(String path);
|
|
||||||
|
|
||||||
@Query("SELECT p.thumbnail_path FROM Photo p")
|
@Query(value = "SELECT file_name FROM photos ", nativeQuery = true)
|
||||||
List<String> findAllThumbnailPaths();
|
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);
|
||||||
|
|
||||||
@Query("SELECT p.path FROM Photo p")
|
|
||||||
Set<String> findAllPaths();
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
@ -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/
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -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%;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
.aspect-ratio-4x3 {
|
||||||
|
aspect-ratio: 4/3;
|
||||||
|
}
|
||||||
|
|
||||||
|
After Width: | Height: | Size: 82 KiB |
|
After Width: | Height: | Size: 71 KiB |
|
After Width: | Height: | Size: 138 KiB |
|
After Width: | Height: | Size: 144 KiB |
|
After Width: | Height: | Size: 167 KiB |
|
After Width: | Height: | Size: 275 KiB |
|
After Width: | Height: | Size: 124 KiB |
|
After Width: | Height: | Size: 73 KiB |
|
After Width: | Height: | Size: 236 KiB |
|
After Width: | Height: | Size: 198 KiB |
|
After Width: | Height: | Size: 149 KiB |
|
|
@ -0,0 +1,3 @@
|
||||||
|
if (document.querySelectorAll('[data-bss-baguettebox]').length > 0) {
|
||||||
|
baguetteBox.run('[data-bss-baguettebox]', { animation: 'slideIn' });
|
||||||
|
}
|
||||||
|
|
@ -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%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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">❮</button>
|
|
||||||
<button id="nextImage">❯</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>
|
|
||||||
|
|
@ -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 </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>
|
||||||