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