init
This commit is contained in:
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
.idea/
|
||||||
|
.DS_Store
|
||||||
|
config.php
|
10
config.php.example
Normal file
10
config.php.example
Normal 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
21
license
Normal 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
2
public/images/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
234
public/index.php
Normal file
234
public/index.php
Normal 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
82
public/styles.css
Normal 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
13
public/templates/404.php
Normal 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>
|
18
public/templates/categories.php
Normal file
18
public/templates/categories.php
Normal 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>
|
28
public/templates/category.php
Normal file
28
public/templates/category.php
Normal 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>
|
18
public/templates/shared/footer.php
Normal file
18
public/templates/shared/footer.php
Normal 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>
|
12
public/templates/shared/header.php
Normal file
12
public/templates/shared/header.php
Normal 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
2
public/thumbs/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
*
|
||||||
|
!.gitignore
|
Reference in New Issue
Block a user