Initial commit

This commit is contained in:
2024-08-27 19:03:24 -05:00
commit 5438f9e358
27 changed files with 2693 additions and 0 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
web/public/images/*
web/vendor/*
web/node_modules/*
*.db

2
bin/bash Executable file
View File

@@ -0,0 +1,2 @@
#!/bin/bash
docker-compose exec app bash

2
bin/buildcss Executable file
View File

@@ -0,0 +1,2 @@
#!/bin/bash
docker-compose exec app npm run build:css

2
bin/init_db Executable file
View File

@@ -0,0 +1,2 @@
#!/bin/bash
docker-compose exec app php scripts/init_db.php

2
bin/minifyjs Executable file
View File

@@ -0,0 +1,2 @@
#!/bin/bash
docker-compose exec app npm run minify:js

2
bin/process_images Executable file
View File

@@ -0,0 +1,2 @@
#!/bin/bash
docker-compose exec app php scripts/process_images.php

32
docker-compose.yml Normal file
View File

@@ -0,0 +1,32 @@
version: '3.3'
services:
app:
build:
context: ./php
container_name: photo_gallery_php
volumes:
- ./web:/var/www/html
user: "1000:1000"
#ports:
# - "9000:9000"
networks:
- photo-gallery-network
nginx:
image: nginx:latest
container_name: photo_gallery_nginx
volumes:
- ./web:/var/www/html
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf
ports:
- "8080:80"
depends_on:
- app
networks:
- photo-gallery-network
networks:
photo-gallery-network:
driver: bridge

24
nginx/default.conf Normal file
View File

@@ -0,0 +1,24 @@
server {
listen 80;
server_name localhost;
root /var/www/html/public;
index index.php index.html;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
#include snippets/fastcgi-php.conf;
fastcgi_pass app:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.ht {
deny all;
}
}

23
php/Dockerfile Normal file
View File

@@ -0,0 +1,23 @@
FROM php:8.2-fpm
# Install dependencies
RUN apt-get update && apt-get install -y \
libfreetype6-dev \
libjpeg62-turbo-dev \
libpng-dev \
libwebp-dev \
libsqlite3-dev \
&& docker-php-ext-configure gd --with-freetype --with-jpeg --with-webp \
&& docker-php-ext-install gd \
&& docker-php-ext-install mysqli
# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
RUN apt-get install npm vim git -y && \
useradd -M -d /var/www -s /usr/sbin/nologin www & \
chown -R www /var/www
# Set working directory
WORKDIR /var/www/html

5
web/.env Normal file
View File

@@ -0,0 +1,5 @@
DB_FILE_PATH=database/gallery.db
PAGINATION_RANGE=7
IMG_THUMB_WIDTH=364
IMG_THUMB_HEIGHT=364
IMG_THUMB_QUALITY=40

12
web/composer.json Normal file
View File

@@ -0,0 +1,12 @@
{
"name": "hpz/photogal",
"type": "project",
"autoload": {
"psr-4": {
"Hpz\\Photogal\\": "src/"
}
},
"require": {
"vlucas/phpdotenv": "^5.6"
}
}

479
web/composer.lock generated Normal file
View File

@@ -0,0 +1,479 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "328949c3f92d87a4170180a4a7e683fe",
"packages": [
{
"name": "graham-campbell/result-type",
"version": "v1.1.3",
"source": {
"type": "git",
"url": "https://github.com/GrahamCampbell/Result-Type.git",
"reference": "3ba905c11371512af9d9bdd27d99b782216b6945"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/3ba905c11371512af9d9bdd27d99b782216b6945",
"reference": "3ba905c11371512af9d9bdd27d99b782216b6945",
"shasum": ""
},
"require": {
"php": "^7.2.5 || ^8.0",
"phpoption/phpoption": "^1.9.3"
},
"require-dev": {
"phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28"
},
"type": "library",
"autoload": {
"psr-4": {
"GrahamCampbell\\ResultType\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
}
],
"description": "An Implementation Of The Result Type",
"keywords": [
"Graham Campbell",
"GrahamCampbell",
"Result Type",
"Result-Type",
"result"
],
"support": {
"issues": "https://github.com/GrahamCampbell/Result-Type/issues",
"source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.3"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type",
"type": "tidelift"
}
],
"time": "2024-07-20T21:45:45+00:00"
},
{
"name": "phpoption/phpoption",
"version": "1.9.3",
"source": {
"type": "git",
"url": "https://github.com/schmittjoh/php-option.git",
"reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/schmittjoh/php-option/zipball/e3fac8b24f56113f7cb96af14958c0dd16330f54",
"reference": "e3fac8b24f56113f7cb96af14958c0dd16330f54",
"shasum": ""
},
"require": {
"php": "^7.2.5 || ^8.0"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
"phpunit/phpunit": "^8.5.39 || ^9.6.20 || ^10.5.28"
},
"type": "library",
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": false
},
"branch-alias": {
"dev-master": "1.9-dev"
}
},
"autoload": {
"psr-4": {
"PhpOption\\": "src/PhpOption/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"authors": [
{
"name": "Johannes M. Schmitt",
"email": "schmittjoh@gmail.com",
"homepage": "https://github.com/schmittjoh"
},
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
}
],
"description": "Option Type for PHP",
"keywords": [
"language",
"option",
"php",
"type"
],
"support": {
"issues": "https://github.com/schmittjoh/php-option/issues",
"source": "https://github.com/schmittjoh/php-option/tree/1.9.3"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption",
"type": "tidelift"
}
],
"time": "2024-07-20T21:41:07+00:00"
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.30.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "0424dff1c58f028c451efff2045f5d92410bd540"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/0424dff1c58f028c451efff2045f5d92410bd540",
"reference": "0424dff1c58f028c451efff2045f5d92410bd540",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"provide": {
"ext-ctype": "*"
},
"suggest": {
"ext-ctype": "For best performance"
},
"type": "library",
"extra": {
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Ctype\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Gert de Pagter",
"email": "BackEndTea@gmail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for ctype functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"ctype",
"polyfill",
"portable"
],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.30.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-05-31T15:07:36+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.30.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fd22ab50000ef01661e2a31d850ebaa297f8e03c",
"reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"provide": {
"ext-mbstring": "*"
},
"suggest": {
"ext-mbstring": "For best performance"
},
"type": "library",
"extra": {
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Mbstring\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for the Mbstring extension",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"mbstring",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.30.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-06-19T12:30:46+00:00"
},
{
"name": "symfony/polyfill-php80",
"version": "v1.30.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
"reference": "77fa7995ac1b21ab60769b7323d600a991a90433"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/77fa7995ac1b21ab60769b7323d600a991a90433",
"reference": "77fa7995ac1b21ab60769b7323d600a991a90433",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"type": "library",
"extra": {
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Php80\\": ""
},
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ion Bazan",
"email": "ion.bazan@gmail.com"
},
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php80/tree/v1.30.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-05-31T15:07:36+00:00"
},
{
"name": "vlucas/phpdotenv",
"version": "v5.6.1",
"source": {
"type": "git",
"url": "https://github.com/vlucas/phpdotenv.git",
"reference": "a59a13791077fe3d44f90e7133eb68e7d22eaff2"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/a59a13791077fe3d44f90e7133eb68e7d22eaff2",
"reference": "a59a13791077fe3d44f90e7133eb68e7d22eaff2",
"shasum": ""
},
"require": {
"ext-pcre": "*",
"graham-campbell/result-type": "^1.1.3",
"php": "^7.2.5 || ^8.0",
"phpoption/phpoption": "^1.9.3",
"symfony/polyfill-ctype": "^1.24",
"symfony/polyfill-mbstring": "^1.24",
"symfony/polyfill-php80": "^1.24"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
"ext-filter": "*",
"phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2"
},
"suggest": {
"ext-filter": "Required to use the boolean validator."
},
"type": "library",
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": false
},
"branch-alias": {
"dev-master": "5.6-dev"
}
},
"autoload": {
"psr-4": {
"Dotenv\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
},
{
"name": "Vance Lucas",
"email": "vance@vancelucas.com",
"homepage": "https://github.com/vlucas"
}
],
"description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.",
"keywords": [
"dotenv",
"env",
"environment"
],
"support": {
"issues": "https://github.com/vlucas/phpdotenv/issues",
"source": "https://github.com/vlucas/phpdotenv/tree/v5.6.1"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv",
"type": "tidelift"
}
],
"time": "2024-07-20T21:52:34+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": [],
"platform-dev": [],
"plugin-api-version": "2.6.0"
}

1496
web/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

19
web/package.json Normal file
View File

@@ -0,0 +1,19 @@
{
"name": "html",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build:css": "npx tailwindcss -i ./src/styles/tailwind.css -o ./public/css/styles.css --minify",
"minify:js": "terser ./src/js/app.js -o ./public/js/app.min.js --compress"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"autoprefixer": "^10.4.20",
"postcss": "^8.4.41",
"tailwindcss": "^3.4.10",
"terser": "^5.31.6"
}
}

8
web/postcss.config.js Normal file
View File

@@ -0,0 +1,8 @@
// postcss.config.js
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
}
}

File diff suppressed because one or more lines are too long

100
web/public/gallery.php Normal file
View File

@@ -0,0 +1,100 @@
<?php
// public/gallery.php
use Hpz\Photogal\Database;
use Hpz\Photogal\Image;
require_once '../vendor/autoload.php';
$dotenv = Dotenv\Dotenv::createImmutable('../');
$dotenv->load();
if (!isset($_ENV['DB_FILE_PATH'])) {
die('DB_FILE_PATH is not set in the .env file');
}
if (!file_exists('../' . $_ENV['DB_FILE_PATH'])) {
die('Database file does not exist');
}
// Get the selected album
$album = $_GET['album'] ?? '';
$album = urldecode($album);
if (empty($album)) {
header("Location: index.php");
exit;
}
// Pagination settings
$limit = 24; // Number of images per page
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
$offset = ($page - 1) * $limit;
// Create a new Database connection
$dbFilePath = '../' . $_ENV['DB_FILE_PATH'];
$database = new Database($dbFilePath);
$image = new Image($database);
// Get images and total count for the selected album
$images = $image->getImages($album, $limit, $offset);
$totalImages = $image->countImages($album);
$totalPages = ceil($totalImages / $limit);
// Limit the number of pages displayed in the pagination
$paginationRange = $_ENV['PAGINATION_RANGE'] ?? 5;
$startPage = max(1, $page - floor($paginationRange / 2));
$endPage = min($totalPages, $startPage + $paginationRange - 1);
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Photo Gallery - <?= htmlspecialchars($album) ?></title>
<link href="/css/styles.css" rel="stylesheet">
</head>
<body class="bg-gray-100">
<div class="container mx-auto p-4">
<h1 class="text-3xl font-bold mb-4"><?= htmlspecialchars($album) ?> - Gallery</h1>
<a href="index.php" class="text-blue-500 mb-4 inline-block">← Back to Albums</a>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<?php foreach ($images as $image): ?>
<div class="relative bg-white shadow-md rounded-lg overflow-hidden group">
<img src="images/<?= $album ?>/thumbs/<?= $image['title'] ?>.webp" alt="<?= htmlspecialchars($image['title']) ?>" class="w-full h-full object-cover cursor-pointer" data-big="images/<?= $album ?>/big/<?= $image['title'] ?>.webp" data-original="images/<?= $album ?>/photos/<?= $image['filename'] ?>" onclick="openLightbox(this)">
<div class="absolute inset-0 bg-black bg-opacity-50 opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center title-overlay">
<h2 class="text-lg font-semibold text-white"><?= htmlspecialchars($image['title']) ?></h2>
</div>
</div>
<?php endforeach; ?>
</div>
<!-- Pagination Controls -->
<div class="mt-6">
<?php if ($totalPages > 1): ?>
<nav class="flex justify-center">
<?php if ($page > 1): ?>
<a href="?album=<?= urlencode($album) ?>&page=<?= $page - 1 ?>" class="px-4 py-2 bg-gray-300 rounded-l hover:bg-gray-400">Previous</a>
<?php endif; ?>
<?php for ($i = $startPage; $i <= $endPage; $i++): ?>
<a href="?album=<?= urlencode($album) ?>&page=<?= $i ?>" class="px-4 py-2 <?= $i == $page ? 'bg-gray-500 text-white' : 'bg-gray-300' ?> hover:bg-gray-400"><?= $i ?></a>
<?php endfor; ?>
<?php if ($page < $totalPages): ?>
<a href="?album=<?= urlencode($album) ?>&page=<?= $page + 1 ?>" class="px-4 py-2 bg-gray-300 rounded-r hover:bg-gray-400">Next</a>
<?php endif; ?>
</nav>
<?php endif; ?>
</div>
</div>
<!-- Lightbox -->
<div id="lightbox" class="lightbox" onclick="closeLightbox()">
<img id="lightbox-img" src="" alt="Lightbox Image" onwheel="zoomImage(event)">
<div id="original-button" class="original-button" onclick="loadOriginal(event)">View Original</div>
</div>
<script type="text/javascript" src="/js/app.min.js"></script>
</body>
</html>

65
web/public/index.php Normal file
View File

@@ -0,0 +1,65 @@
<?php
// public/index.php
require_once '../vendor/autoload.php';
use Hpz\Photogal\Database;
use Hpz\Photogal\Image;
$dotenv = Dotenv\Dotenv::createImmutable('../');
$dotenv->load();
if (!isset($_ENV['DB_FILE_PATH'])) {
die('DB_FILE_PATH is not set in the .env file');
}
if (!file_exists('../' . $_ENV['DB_FILE_PATH'])) {
die('Database file does not exist');
}
// Create a new Database connection
$dbFilePath = '../' . $_ENV['DB_FILE_PATH'];
$database = new Database($dbFilePath);
$image = new Image($database);
// Get the list of albums
$albumsQuery = "SELECT DISTINCT album FROM images";
$albumsResult = $database->getConnection()->query($albumsQuery);
$albums = [];
while ($row = $albumsResult->fetchArray(SQLITE3_ASSOC)) {
$albums[] = $row['album'];
}
// Fetch the first image from each album
$albumPreviews = [];
foreach ($albums as $album) {
$albumPreviews[$album] = $image->getImages($album, 1, 0)[0] ?? null;
}
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Photo Gallery - Select Album</title>
<link href="/css/styles.css" rel="stylesheet">
</head>
<body class="bg-gray-100">
<div class="container mx-auto p-4">
<h1 class="text-3xl font-bold mb-4">Select an Album</h1>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<?php foreach ($albumPreviews as $album => $image): ?>
<?php if ($image): ?>
<div class="bg-white shadow-md rounded-lg overflow-hidden">
<a href="gallery.php?album=<?= urlencode($album) ?>">
<img src="images/<?= $album ?>/thumbs/<?= $image['title'] ?>.webp" alt="<?= htmlspecialchars($album) ?>" class="w-full h-48 object-cover">
<div class="p-4">
<h2 class="text-xl font-semibold"><?= htmlspecialchars($album) ?></h2>
</div>
</a>
</div>
<?php endif; ?>
<?php endforeach; ?>
</div>
</div>
</body>
</html>

1
web/public/js/app.min.js vendored Normal file
View File

@@ -0,0 +1 @@
function openLightbox(element){const lightbox=document.getElementById("lightbox"),lightboxImg=document.getElementById("lightbox-img");lightboxImg.src=element.dataset.big,lightboxImg.setAttribute("data-original",element.dataset.original),lightbox.classList.add("active")}function closeLightbox(){document.getElementById("lightbox").classList.remove("active");const lightboxImg=document.getElementById("lightbox-img");lightboxImg.src="",lightboxImg.style.transform="scale(1)"}function zoomImage(event){event.preventDefault();const img=event.target;let scale=img.style.transform?parseFloat(img.style.transform.replace("scale(","").replace(")","")):1;event.deltaY<0?scale+=.1:scale-=.1,scale<1&&(scale=1),img.style.transform=`scale(${scale})`,img.style.transformOrigin=`${event.offsetX}px ${event.offsetY}px`}function loadOriginal(event){event.stopPropagation();const lightboxImg=document.getElementById("lightbox-img"),originalSrc=lightboxImg.getAttribute("data-original");lightboxImg.src=originalSrc,lightboxImg.style.transform="scale(1)"}

28
web/scripts/init_db.php Normal file
View File

@@ -0,0 +1,28 @@
<?php
// .php
require_once 'vendor/autoload.php';
use Hpz\Photogal\Database;
use Hpz\Photogal\Image;
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__ . '/..');
$dotenv->load();
if (!isset($_ENV['DB_FILE_PATH'])) {
die('DB_FILE_PATH is not set in the .env file');
}
// Specify the path to the SQLite database file
$dbFilePath = $_ENV['DB_FILE_PATH'];
// Create a new Database connection
$database = new Database($dbFilePath);
// Create the Image table
$image = new Image($database);
if ($image->createTable()) {
echo "Database and table initialized successfully.";
} else {
echo "Failed to initialize database and table.";
}

View File

@@ -0,0 +1,84 @@
<?php
// web/process_images.php
require_once 'vendor/autoload.php';
use Hpz\Photogal\Database;
use Hpz\Photogal\Image;
use Hpz\Photogal\ThumbnailGenerator;
ini_set('memory_limit', '512M');
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__ . '/..');
$dotenv->load();
if (!isset($_ENV['DB_FILE_PATH'])) {
die('DB_FILE_PATH is not set in the .env file');
}
// Specify the paths
// Specify the paths
$albumsDir = 'public/images/';
$dbFilePath = $_ENV['DB_FILE_PATH'];
// Create a new Database connection
$database = new Database($dbFilePath);
$image = new Image($database);
// Function to process a directory
function processDirectory($albumDir, $albumName, Image $image)
{
$dirIterator = new RecursiveDirectoryIterator($albumDir);
$iterator = new RecursiveIteratorIterator($dirIterator);
foreach ($iterator as $file) {
if ($file->isFile()) {
$filePath = $file->getPathname();
$fileInfo = pathinfo($filePath);
$fileName = $fileInfo['basename'];
if (in_array(strtolower($fileInfo['extension']), ['jpg', 'jpeg', 'png', 'gif'])) {
$thumbDir = 'public/images/' . $albumName . '/thumbs/';
$bigDir = 'public/images/' . $albumName . '/big/';
$thumbPath = $thumbDir . $fileInfo['filename'] . '.webp';
$bigPath = $bigDir . $fileInfo['filename'] . '.webp';
if (!file_exists($thumbPath)) {
if (!is_dir($thumbDir)) {
mkdir($thumbDir, 0777, true);
}
ThumbnailGenerator::createThumbnail($filePath, $thumbPath, $_ENV['IMG_THUMB_WIDTH'], $_ENV['IMG_THUMB_HEIGHT'], $_ENV['IMG_THUMB_QUALITY']);
echo "Created thumbnail for $fileName in $albumName\n";
}
if (!file_exists($bigPath)) {
if (!is_dir($bigDir)) {
mkdir($bigDir, 0777, true);
}
ThumbnailGenerator::createBigImage($filePath, $bigPath);
echo "Created big image for $fileName in $albumName\n";
}
$relativePath = $albumName . '/photos';
$modifiedDate = filemtime($filePath);
$addedDate = time();
$title = $fileInfo['filename'];
$exists = $image->imageExists($albumName, $fileName, $relativePath);
if (!$exists) {
$image->addImage($albumName, $fileName, $relativePath, $modifiedDate, $addedDate, $title);
echo "Added $fileName to database in album $albumName\n";
}
}
}
}
}
$albums = scandir($albumsDir);
foreach ($albums as $album) {
if ($album !== '.' && $album !== '..' && is_dir($albumsDir . $album)) {
if (strpos($album, 'videos') === false) {
processDirectory($albumsDir . $album . '/photos/', $album, $image);
}
}
}

27
web/src/Database.php Normal file
View File

@@ -0,0 +1,27 @@
<?php
// src/Database.php
namespace Hpz\Photogal;
use SQLite3;
class Database
{
private $connection;
public function __construct($dbFilePath)
{
$this->connection = new SQLite3($dbFilePath);
}
public function getConnection()
{
return $this->connection;
}
public function __destruct()
{
$this->connection->close();
}
}

95
web/src/Image.php Normal file
View File

@@ -0,0 +1,95 @@
<?php
// src/Image.php
namespace Hpz\Photogal;
class Image
{
private $db;
public function __construct(Database $database)
{
$this->db = $database->getConnection();
}
public function createTable()
{
$query = "
CREATE TABLE IF NOT EXISTS images (
id INTEGER PRIMARY KEY AUTOINCREMENT,
album TEXT NOT NULL,
filename TEXT NOT NULL,
path TEXT NOT NULL,
modified_date INTEGER NOT NULL,
added_date INTEGER NOT NULL,
title TEXT NOT NULL
);
";
return $this->db->exec($query);
}
public function addImage($album, $filename, $path, $modifiedDate, $addedDate, $title)
{
$stmt = $this->db->prepare("
INSERT INTO images (album, filename, path, modified_date, added_date, title)
VALUES (:album, :filename, :path, :modified_date, :added_date, :title)
");
$stmt->bindValue(':album', $album, SQLITE3_TEXT);
$stmt->bindValue(':filename', $filename, SQLITE3_TEXT);
$stmt->bindValue(':path', $path, SQLITE3_TEXT);
$stmt->bindValue(':modified_date', $modifiedDate, SQLITE3_INTEGER);
$stmt->bindValue(':added_date', $addedDate, SQLITE3_INTEGER);
$stmt->bindValue(':title', $title, SQLITE3_TEXT);
return $stmt->execute();
}
public function getImages($album = null, $limit = 24, $offset = 0)
{
$query = "SELECT * FROM images";
if ($album) {
$query .= " WHERE album = :album";
}
$query .= " ORDER BY modified_date DESC LIMIT :limit OFFSET :offset";
$stmt = $this->db->prepare($query);
if ($album) {
$stmt->bindValue(':album', $album, SQLITE3_TEXT);
}
$stmt->bindValue(':limit', $limit, SQLITE3_INTEGER);
$stmt->bindValue(':offset', $offset, SQLITE3_INTEGER);
$result = $stmt->execute();
$images = [];
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
$images[] = $row;
}
return $images;
}
public function imageExists($album, $filename, $path)
{
$stmt = $this->db->prepare("SELECT COUNT(*) as count FROM images WHERE album = :album AND filename = :filename AND path = :path");
$stmt->bindValue(':album', $album, SQLITE3_TEXT);
$stmt->bindValue(':filename', $filename, SQLITE3_TEXT);
$stmt->bindValue(':path', $path, SQLITE3_TEXT);
$result = $stmt->execute();
$row = $result->fetchArray(SQLITE3_ASSOC);
return $row['count'] > 0;
}
public function countImages($album = null)
{
$query = "SELECT COUNT(*) as count FROM images";
if ($album) {
$query .= " WHERE album = :album";
}
$stmt = $this->db->prepare($query);
if ($album) {
$stmt->bindValue(':album', $album, SQLITE3_TEXT);
}
$result = $stmt->execute();
$row = $result->fetchArray(SQLITE3_ASSOC);
return $row['count'];
}
}

View File

@@ -0,0 +1,91 @@
<?php
// src/ThumbnailGenerator.php
namespace Hpz\Photogal;
class ThumbnailGenerator
{
public static function createThumbnail($sourcePath, $destPath, $thumbWidth = 200, $thumbHeight = 200, $quality = 40)
{
list($width, $height, $type) = getimagesize($sourcePath);
$thumb = imagecreatetruecolor($thumbWidth, $thumbHeight);
switch ($type) {
case IMAGETYPE_JPEG:
$source = imagecreatefromjpeg($sourcePath);
break;
case IMAGETYPE_PNG:
$source = imagecreatefrompng($sourcePath);
break;
case IMAGETYPE_GIF:
$source = imagecreatefromgif($sourcePath);
break;
default:
return false;
}
$srcAspect = $width / $height;
$thumbAspect = $thumbWidth / $thumbHeight;
if ($srcAspect >= $thumbAspect) {
$newHeight = (int)$thumbHeight;
$newWidth = (int)($width / ($height / $thumbHeight));
} else {
$newWidth = (int)$thumbWidth;
$newHeight = (int)($height / ($width / $thumbWidth));
}
$xOffset = (int)(($thumbWidth - $newWidth) / 2);
$yOffset = (int)(($thumbHeight - $newHeight) / 2);
imagecopyresampled(
$thumb,
$source,
$xOffset,
$yOffset,
0,
0,
$newWidth,
$newHeight,
$width,
$height
);
return imagewebp($thumb, $destPath, $quality);
}
public static function createBigImage($sourcePath, $destPath, $maxDimension = 2000, $quality = 80)
{
list($width, $height, $type) = getimagesize($sourcePath);
switch ($type) {
case IMAGETYPE_JPEG:
$source = imagecreatefromjpeg($sourcePath);
break;
case IMAGETYPE_PNG:
$source = imagecreatefrompng($sourcePath);
break;
case IMAGETYPE_GIF:
$source = imagecreatefromgif($sourcePath);
break;
default:
return false;
}
$srcAspect = $width / $height;
if ($width > $height) {
$newWidth = $maxDimension;
$newHeight = (int)($maxDimension / $srcAspect);
} else {
$newHeight = $maxDimension;
$newWidth = (int)($maxDimension * $srcAspect);
}
$bigImage = imagecreatetruecolor($newWidth, $newHeight);
imagecopyresampled($bigImage, $source, 0, 0, 0, 0, $newWidth, $newHeight, $width, $height);
return imagewebp($bigImage, $destPath, $quality);
}
}

40
web/src/js/app.js Normal file
View File

@@ -0,0 +1,40 @@
function openLightbox(element) {
const lightbox = document.getElementById('lightbox');
const lightboxImg = document.getElementById('lightbox-img');
lightboxImg.src = element.dataset.big;
lightboxImg.setAttribute('data-original', element.dataset.original);
lightbox.classList.add('active');
}
function closeLightbox() {
const lightbox = document.getElementById('lightbox');
lightbox.classList.remove('active');
const lightboxImg = document.getElementById('lightbox-img');
lightboxImg.src = ''; // Unload the current image
lightboxImg.style.transform = 'scale(1)';
}
function zoomImage(event) {
event.preventDefault();
const img = event.target;
let scale = img.style.transform ? parseFloat(img.style.transform.replace('scale(', '').replace(')', '')) : 1;
if (event.deltaY < 0) {
scale += 0.1; // Zoom in
} else {
scale -= 0.1; // Zoom out
}
if (scale < 1) scale = 1; // Prevent zoom out below original size
img.style.transform = `scale(${scale})`;
img.style.transformOrigin = `${event.offsetX}px ${event.offsetY}px`;
}
function loadOriginal(event) {
event.stopPropagation();
const lightboxImg = document.getElementById('lightbox-img');
const originalSrc = lightboxImg.getAttribute('data-original');
lightboxImg.src = originalSrc;
lightboxImg.style.transform = 'scale(1)';
}

View File

@@ -0,0 +1,41 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
.lightbox {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.9);
justify-content: center;
align-items: center;
z-index: 50;
}
.lightbox img {
max-width: 100%;
max-height: 100%;
cursor: zoom-in;
}
.lightbox.active {
display: flex;
}
.title-overlay {
pointer-events: none;
}
.original-button {
position: absolute;
bottom: 10px;
right: 10px;
background-color: rgba(0, 0, 0, 0.7);
color: white;
padding: 10px 15px;
border-radius: 5px;
cursor: pointer;
}

8
web/tailwind.config.js Normal file
View File

@@ -0,0 +1,8 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./public/gallery.php","./public/index.php"],
theme: {
extend: {},
},
plugins: [],
}