This commit is contained in:
2025-01-01 23:30:28 +01:00
parent 1106b4d942
commit 82ce2594ac
13 changed files with 444 additions and 2 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
.idea/
.DS_Store
config.php

View File

@@ -1,2 +1 @@
# docker_image_gallery

10
config.php.example Normal file
View File

@@ -0,0 +1,10 @@
<?php
return [
'base_url' => 'http://localhost:8080',
'title' => 'Danb Gallery',
'author_text' => 'Dan Brown',
'author_link' => 'https://danb.me',
'license_link' => 'https://creativecommons.org/publicdomain/zero/1.0/',
'license_text' => 'CC0 - Public Domain',
];

21
license Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2022 Dan Brown
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

2
public/images/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
*
!.gitignore

234
public/index.php Normal file
View File

@@ -0,0 +1,234 @@
<?php
declare(strict_types=1);
//ini_set('display_errors', '1');
//error_reporting(E_ALL);
// Global constants
const IMAGES_DIR = __DIR__ . DIRECTORY_SEPARATOR . 'images';
const THUMBS_DIR = __DIR__ . DIRECTORY_SEPARATOR . 'thumbs';
// Load configuration
$config = loadConfig();
// Path management
$uri = $_SERVER['REQUEST_URI'];
$routes = [
'categories' => function() {
sendPage("categories", [
'categories' => getCategories(),
]);
},
'category' => function(string $category) {
sendPage("category", [
'category' => $category,
'images' => getCategoryImages($category),
]);
},
'404' => function(string $uri) {
sendPage("404", [
'uri' => $uri,
], 404);
}
];
if ($uri === '/') {
$routes['categories']();
} else {
$category = urldecode(trim($uri, '/'));
if (categoryExists($category)) {
$routes['category']($category);
} else {
$routes['404']($uri);
}
}
/**
* Get details of all available categories.
* @returns Category[]
*/
function getCategories(): array {
$names = getCategoryFolderNames();
$categories = [];
foreach ($names as $name) {
$category = new Category(
name: $name,
thumb: getCategoryThumbnail($name)
);
if ($category->thumb) {
$categories[] = $category;
}
}
return $categories;
}
/**
* Get the thumbnail image uri for the given category.
*/
function getCategoryThumbnail(string $category): string {
$categoryImages = getCategoryImageFiles($category);
$firstImage = $categoryImages[0] ?? '';
if (empty($firstImage)) {
return '';
}
generateImageThumbnail($category, $firstImage);
return "thumbs/{$category}/{$firstImage}";
}
/**
* Generated a thumbnail for the given image filename withing
* the given category folder.
*/
function generateImageThumbnail(string $category, string $image): void {
$imagePath = buildPath(IMAGES_DIR, $category, $image);
$thumbPath = buildPath(THUMBS_DIR, $category, $image);
if (file_exists($thumbPath)) {
return;
}
if (!file_exists($imagePath)) {
error("Could not find image at {$imagePath}");
}
if (!str_ends_with(strtolower($imagePath), '.webp')) {
error("Image at {$imagePath} is not webp as expected");
}
$thumbDir = dirname($thumbPath);
if (!file_exists($thumbDir)) {
mkdir($thumbDir);
}
$originalImage = imagecreatefromwebp($imagePath);
$thumbImage = imagescale($originalImage, 1200);
imagewebp($thumbImage, $thumbPath, 50);
}
/**
* Get the categorized folder names within the image directory.
* @return string[]
*/
function getCategoryFolderNames(): array {
$dirs = glob(buildPath(IMAGES_DIR, '*'), GLOB_ONLYDIR);
$dirNames = array_map(fn(string $dir) => basename($dir), $dirs);
return array_reverse($dirNames);
}
/**
* Check that a given category exists.
*/
function categoryExists(string $category): bool {
$expectedPath = buildPath(IMAGES_DIR, $category);
return is_dir($expectedPath);
}
/**
* Get the image filenames for the images within the given
* category folder.
* @return string[]
*/
function getCategoryImageFiles(string $category): array {
$images = glob(buildPath(IMAGES_DIR, $category, '*.webp'));
return array_map(fn(string $dir) => basename($dir), $images);
}
/**
* Get the images within the given category.
* @return Image[]
*/
function getCategoryImages(string $category): array {
$files = getCategoryImageFiles($category);
$images = array_map(function(string $file) use ($category) {
$imagePath = buildPath('images', $category, $file);
[$width, $height] = getimagesize($imagePath);
return new Image(
name: $file,
width: $width,
height: $height,
uri: "./images/{$category}/{$file}",
thumb: "./thumbs/{$category}/{$file}"
);
}, $files);
foreach ($files as $fileName) {
generateImageThumbnail($category, $fileName);
}
return $images;
}
/**
* Build a directory path from the given path parts.
*/
function buildPath(...$parts): string {
return implode(DIRECTORY_SEPARATOR, $parts);
}
/**
* Render and send the page of the given name to the user.
*/
function sendPage(string $name, array $data = [], int $status = 200): void {
global $config;
$mergedData = array_merge($data, ['config' => $config]);
extract($mergedData);
header('Content-Type: text/html; charset=utf-8');
http_response_code($status);
include "templates/shared/header.php";
include "templates/{$name}.php";
include "templates/shared/footer.php";
}
/**
* Load the config file from the parent directory.
*/
function loadConfig(): array {
$configPath = buildPath(dirname(__DIR__), 'config.php');
$config = include $configPath;
return $config;
}
/**
* Error out stop the application, showing the given message.
*/
function error(string $message): never {
echo "An error occurred: {$message}";
http_response_code(500);
exit(1);
}
/**
* Dump the given arguments and exit.
* (Dump & die)
*/
function dd(...$args): never {
foreach ($args as $arg) {
print_r($arg);
}
exit(1);
}
class Category {
public function __construct(
public string $name,
public string $thumb
) {}
}
class Image {
public function __construct(
public string $name,
public int $width,
public int $height,
public string $uri,
public string $thumb,
) {}
}

82
public/styles.css Normal file
View File

@@ -0,0 +1,82 @@
body {
background: #222;
color: #FFF;
font-family: "Roboto", sans-serif;
font-size: 16px;
margin: 0;
padding: 0;
}
header {
font-size: 1rem;
background: #000;
margin: 0;
display: flex;
gap: 0.5rem;
padding: .5rem;
font-weight: 700;
position: sticky;
top: 0;
z-index: 5;
}
header h1 {
margin: 0;
font-size: 1rem;
}
header a {
color: #FFF;
text-decoration: none;
}
.gallery-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(min(480px, 48%), 1fr));
gap: 1px;
}
.gallery-item {
position: relative;
display: block;
text-decoration: none;
color: #FFF;
aspect-ratio: 1/1;
}
.gallery-item img {
position: absolute;
object-fit: cover;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.gallery-item h3 {
position: absolute;
top: 0;
left: 0;
padding: .5rem 1rem;
margin: 0;
font-size: .9rem;
box-shadow: 0 0 2px 0 rgba(0, 0, 0, 1);
background: black;
opacity: 0.7;
}
.gallery-item:hover h3 {
opacity: 1;
}
.gallery-item h3 small {
font-weight: 400;
font-size: .8rem;
}
footer {
display: flex;
gap: 1rem;
padding: 1rem;
justify-content: center;
opacity: .6;
}
footer a {
color: #fff;
text-decoration: underline;
}

13
public/templates/404.php Normal file
View File

@@ -0,0 +1,13 @@
<?php
/**
* @var string $uri
*/
?>
<h1>Not Found</h1>
<p>
Sorry, Nothing could be found at the requested
<?php echo $uri; ?>
path;
</p>

View File

@@ -0,0 +1,18 @@
<?php
/**
* @var Category[] $categories
*/
?>
<div class="gallery-grid">
<?php foreach ($categories as $category): ?>
<a href="./<?php echo $category->name; ?>" class="gallery-item">
<img src="<?php echo $category->thumb ?>" alt="<?php echo $category->name; ?>" loading="lazy">
<h3><?php echo $category->name; ?></h3>
</a>
<?php endforeach; ?>
</div>

View File

@@ -0,0 +1,28 @@
<?php
/**
* @var string $category
* @var Image[] $images
*/
?>
<header>
<a href="./">Back</a>
<span>|</span>
<h1>Category: <?php echo $category; ?></h1>
</header>
<div class="gallery-grid">
<?php foreach ($images as $image): ?>
<a href="<?php echo $image->uri; ?>" target="_blank" class="gallery-item">
<img src="<?php echo $image->thumb; ?>" alt="<?php echo $image->name; ?>" loading="lazy">
<h3>
<?php echo $image->name; ?>
<small>[<?php echo $image->width; ?>x<?php echo $image->height; ?>]</small>
</h3>
</a>
<?php endforeach; ?>
</div>

View File

@@ -0,0 +1,18 @@
<footer>
<p>
By <a href="<?php echo $config['author_link']; ?>" target="_blank" rel="noreferrer noopener"><?php echo $config['author_text']; ?></a>
</p>
<p>|</p>
<p>
Images are licensed as
<a href="<?php echo $config['license_link']; ?>" target="_blank" rel="noreferrer noopener"><?php echo $config['license_text']; ?></a>
</p>
<p>|</p>
<p>
<a href="https://github.com/ssddanbrown/gallery" target="_blank" rel="noreferrer noopener">Gallery Source Code</a>
</p>
</footer>
</body>
</html>

View File

@@ -0,0 +1,12 @@
<!doctype html>
<html lang="en">
<head>
<base href="<?php echo $config['base_url']; ?>">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title><?php echo $config['title']; ?></title>
<link rel="stylesheet" href="styles.css">
</head>
<body>

2
public/thumbs/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
*
!.gitignore