From 86827a7b10197551380367121cf4c4dc511338cb Mon Sep 17 00:00:00 2001 From: hpz937 Date: Tue, 27 Aug 2024 20:59:58 -0500 Subject: [PATCH] add routing, move code to classes; misc changes --- web/public/css/styles.css | 2 +- web/public/gallery.php | 100 ----------------------- web/public/index.php | 84 +++++++++---------- web/public/js/app.min.js | 2 +- web/scripts/process_images.php | 45 ++++++++--- web/src/Album.php | 32 ++++++++ web/src/Gallery.php | 27 +++++++ web/src/Image.php | 7 +- web/src/js/app.js | 144 ++++++++++++++++++++++++++++++--- web/src/styles/tailwind.css | 34 ++++++++ web/src/views/albums.phtml | 37 +++++++++ web/src/views/gallery.phtml | 86 ++++++++++++++++++++ web/tailwind.config.js | 2 +- 13 files changed, 434 insertions(+), 168 deletions(-) delete mode 100644 web/public/gallery.php create mode 100644 web/src/Album.php create mode 100644 web/src/Gallery.php create mode 100644 web/src/views/albums.phtml create mode 100644 web/src/views/gallery.phtml diff --git a/web/public/css/styles.css b/web/public/css/styles.css index f1adbb2..d4f34d7 100644 --- a/web/public/css/styles.css +++ b/web/public/css/styles.css @@ -1 +1 @@ -/*! tailwindcss v3.4.10 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.absolute{position:absolute}.relative{position:relative}.inset-0{inset:0}.mx-auto{margin-left:auto;margin-right:auto}.mb-4{margin-bottom:1rem}.mt-6{margin-top:1.5rem}.inline-block{display:inline-block}.flex{display:flex}.grid{display:grid}.h-48{height:12rem}.h-full{height:100%}.w-full{width:100%}.cursor-pointer{cursor:pointer}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.items-center{align-items:center}.justify-center{justify-content:center}.gap-4{gap:1rem}.overflow-hidden{overflow:hidden}.rounded-lg{border-radius:.5rem}.rounded-l{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.rounded-r{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.bg-black{--tw-bg-opacity:1;background-color:rgb(0 0 0/var(--tw-bg-opacity))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.bg-gray-300{--tw-bg-opacity:1;background-color:rgb(209 213 219/var(--tw-bg-opacity))}.bg-gray-500{--tw-bg-opacity:1;background-color:rgb(107 114 128/var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-opacity-50{--tw-bg-opacity:0.5}.object-cover{-o-object-fit:cover;object-fit:cover}.p-4{padding:1rem}.px-4{padding-left:1rem;padding-right:1rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-lg{font-size:1.125rem}.text-lg,.text-xl{line-height:1.75rem}.text-xl{font-size:1.25rem}.font-bold{font-weight:700}.font-semibold{font-weight:600}.text-blue-500{--tw-text-opacity:1;color:rgb(59 130 246/var(--tw-text-opacity))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.opacity-0{opacity:0}.shadow-md{--tw-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -2px rgba(0,0,0,.1);--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.lightbox{display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(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,.7);color:#fff;padding:10px 15px;border-radius:5px;cursor:pointer}.hover\:bg-gray-400:hover{--tw-bg-opacity:1;background-color:rgb(156 163 175/var(--tw-bg-opacity))}.group:hover .group-hover\:opacity-100{opacity:1}@media (min-width:768px){.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}} \ No newline at end of file +/*! tailwindcss v3.4.10 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}:host,html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:ui-sans-serif,system-ui,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal;-webkit-tap-highlight-color:transparent}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-feature-settings:normal;font-variation-settings:normal;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-feature-settings:inherit;font-variation-settings:inherit;font-size:100%;font-weight:inherit;line-height:inherit;letter-spacing:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}button,input:where([type=button]),input:where([type=reset]),input:where([type=submit]){-webkit-appearance:button;background-color:transparent;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}dialog{padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}*,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }::backdrop{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:rgba(59,130,246,.5);--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: ;--tw-contain-size: ;--tw-contain-layout: ;--tw-contain-paint: ;--tw-contain-style: }.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.absolute{position:absolute}.relative{position:relative}.inset-0{inset:0}.mx-auto{margin-left:auto;margin-right:auto}.mb-4{margin-bottom:1rem}.ml-2{margin-left:.5rem}.mt-6{margin-top:1.5rem}.block{display:block}.inline-block{display:inline-block}.flex{display:flex}.grid{display:grid}.h-48{height:12rem}.h-full{height:100%}.w-full{width:100%}.cursor-pointer{cursor:pointer}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.items-center{align-items:center}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-4{gap:1rem}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.overflow-hidden,.truncate{overflow:hidden}.truncate{text-overflow:ellipsis;white-space:nowrap}.rounded{border-radius:.25rem}.rounded-lg{border-radius:.5rem}.rounded-l{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.rounded-r{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.border{border-width:1px}.bg-black{--tw-bg-opacity:1;background-color:rgb(0 0 0/var(--tw-bg-opacity))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.bg-gray-200{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity))}.bg-gray-300{--tw-bg-opacity:1;background-color:rgb(209 213 219/var(--tw-bg-opacity))}.bg-gray-500{--tw-bg-opacity:1;background-color:rgb(107 114 128/var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-opacity-50{--tw-bg-opacity:0.5}.object-cover{-o-object-fit:cover;object-fit:cover}.p-1{padding:.25rem}.p-4{padding:1rem}.px-4{padding-left:1rem;padding-right:1rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.text-center{text-align:center}.text-3xl{font-size:1.875rem;line-height:2.25rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-semibold{font-weight:600}.text-blue-500{--tw-text-opacity:1;color:rgb(59 130 246/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.opacity-0{opacity:0}.shadow-md{--tw-shadow:0 4px 6px -1px rgba(0,0,0,.1),0 2px 4px -2px rgba(0,0,0,.1);--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.lightbox{display:none;position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(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-content{position:relative}.lightbox-content img{max-width:100%;max-height:100vh;display:block;margin:auto}.lightbox.active{display:flex}.lightbox-nav{position:absolute;top:50%;font-size:30px;color:#fff;text-shadow:0 0 10px rgba(0,0,0,.7);cursor:pointer;-webkit-user-select:none;-moz-user-select:none;user-select:none;z-index:10;padding:10px}.lightbox-prev{left:20px;transform:translateY(-50%)}.lightbox-next{right:20px;transform:translateY(-50%)}.title-overlay{pointer-events:none}.original-button{position:absolute;bottom:10px;right:10px;background-color:rgba(0,0,0,.7);color:#fff;padding:10px 15px;border-radius:5px;cursor:pointer}.hover\:bg-gray-400:hover{--tw-bg-opacity:1;background-color:rgb(156 163 175/var(--tw-bg-opacity))}.group:hover .group-hover\:opacity-100{opacity:1}@media (min-width:768px){.md\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}} \ No newline at end of file diff --git a/web/public/gallery.php b/web/public/gallery.php deleted file mode 100644 index 506d42d..0000000 --- a/web/public/gallery.php +++ /dev/null @@ -1,100 +0,0 @@ -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); -?> - - - - - - - Photo Gallery - <?= htmlspecialchars($album) ?> - - - -
-

- Gallery

- ← Back to Albums -
- -
- <?= htmlspecialchars($image['title']) ?> -
-

-
-
- -
- - -
- 1): ?> - - -
-
- - - - - - - diff --git a/web/public/index.php b/web/public/index.php index e657684..ded8aa3 100644 --- a/web/public/index.php +++ b/web/public/index.php @@ -4,10 +4,12 @@ require_once '../vendor/autoload.php'; use Hpz\Photogal\Database; -use Hpz\Photogal\Image; +use Hpz\Photogal\Album; +use Hpz\Photogal\Gallery; $dotenv = Dotenv\Dotenv::createImmutable('../'); $dotenv->load(); + if (!isset($_ENV['DB_FILE_PATH'])) { die('DB_FILE_PATH is not set in the .env file'); } @@ -18,48 +20,46 @@ if (!file_exists('../' . $_ENV['DB_FILE_PATH'])) { // 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']; -} +$albumClass = new Album($database); +$galleryClass = new Gallery($database); -// Fetch the first image from each album -$albumPreviews = []; -foreach ($albums as $album) { - $albumPreviews[$album] = $image->getImages($album, 1, 0)[0] ?? null; +$requestUri = trim($_SERVER['REQUEST_URI'], '/'); +$requestParts = explode('/', $requestUri); + +if ($requestParts[0] === 'gallery' && isset($requestParts[1])) { + // Gallery view + $album = urldecode($requestParts[1]); + $page = isset($requestParts[2]) ? (int)$requestParts[2] : 1; + + // Retrieve limit, sort order, and sort direction from cookies or defaults + $limit = $_COOKIE['limit'] ?? 24; + $sortOrder = $_COOKIE['sortOrder'] ?? 'modified_date'; + $sortDirection = $_COOKIE['sortDirection'] ?? 'DESC'; + + // Get images and total count for the selected album + $offset = ($page - 1) * $limit; + $images = $galleryClass->getGalleryImages($album, $limit, $offset, $sortOrder, $sortDirection); + $totalImages = $galleryClass->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); + + // Render gallery view + include '../src/views/gallery.phtml'; + +} else { + // Album view + $albums = $albumClass->getAlbums(); + $albumPreviews = []; + foreach ($albums as $album) { + $albumPreviews[$album] = $albumClass->getAlbumPreview($album); + } + + // Render album view + include '../src/views/albums.phtml'; } ?> - - - - - - - Photo Gallery - Select Album - - - -
-

Select an Album

-
- $image): ?> - - - - -
-
- - diff --git a/web/public/js/app.min.js b/web/public/js/app.min.js index 8598122..4a011c8 100644 --- a/web/public/js/app.min.js +++ b/web/public/js/app.min.js @@ -1 +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)"} \ No newline at end of file +let currentImageIndex=0,images=[];function openLightbox(element){images=Array.from(document.querySelectorAll("#gallery img")).map((img=>({big:img.dataset.big,original:img.dataset.original}))),currentImageIndex=images.findIndex((img=>img.big===element.dataset.big)),-1!==currentImageIndex?(updateLightbox(),document.addEventListener("keydown",handleKeydown)):console.error("Error: Image not found in the array")}function updateLightbox(){const lightboxImg=document.getElementById("lightbox-img");lightboxImg.src=images[currentImageIndex].big,lightboxImg.setAttribute("data-original",images[currentImageIndex].original),lightboxImg.style.transform="scale(1)",lightboxImg.style.maxWidth="100%",lightboxImg.style.maxHeight="100vh";document.getElementById("lightbox").classList.add("active");document.getElementById("original-button").style.display="block",updateNavigationButtons()}function updateNavigationButtons(){const prevButton=document.getElementById("lightbox-prev"),nextButton=document.getElementById("lightbox-next");prevButton.style.display=0===currentImageIndex?"none":"block",nextButton.style.display=currentImageIndex===images.length-1?"none":"block"}function closeLightbox(event){if("keydown"===event.type||"lightbox"===event.target.id){document.getElementById("lightbox").classList.remove("active");const lightboxImg=document.getElementById("lightbox-img");lightboxImg.src="",lightboxImg.style.transform="scale(1)",document.removeEventListener("keydown",handleKeydown)}}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)";document.getElementById("original-button").style.display="none"}function navigateLightbox(direction){currentImageIndex+=direction,currentImageIndex<0?currentImageIndex=0:currentImageIndex>=images.length&&(currentImageIndex=images.length-1),updateLightbox()}function handleKeydown(event){"ArrowLeft"===event.key?currentImageIndex>0&&navigateLightbox(-1):"ArrowRight"===event.key?currentImageIndexfunc.apply(context,args)),wait)}}document.getElementById("lightbox").addEventListener("click",(function(event){"lightbox"===event.target.id&&closeLightbox(event)})),document.addEventListener("DOMContentLoaded",(function(){const album=document.getElementById("gallery").dataset.album;function applyFilters(){const sortOrder=document.getElementById("sortOrder").value,sortDirection=document.getElementById("sortDirection").innerText.includes("Descending")?"DESC":"ASC",limit=document.getElementById("limit").value;document.cookie=`sortOrder=${sortOrder}; path=/`,document.cookie=`sortDirection=${sortDirection}; path=/`,document.cookie=`limit=${limit}; path=/`,window.location.href=`/gallery/${album}/1`}document.getElementById("sortOrder").addEventListener("change",applyFilters),document.getElementById("limit").addEventListener("change",applyFilters),document.getElementById("sortDirection").addEventListener("click",(function(){const sortButton=document.getElementById("sortDirection");sortButton.innerText.includes("Descending")?sortButton.innerText="Ascending ▲":sortButton.innerText="Descending ▼";applyFilters()}))})); \ No newline at end of file diff --git a/web/scripts/process_images.php b/web/scripts/process_images.php index 8dcf69a..133e810 100644 --- a/web/scripts/process_images.php +++ b/web/scripts/process_images.php @@ -1,5 +1,5 @@ load(); -if (!isset($_ENV['DB_FILE_PATH'])) { - die('DB_FILE_PATH is not set in the .env file'); +if (!isset($_ENV['DB_FILE_PATH']) || !isset($_ENV['IMG_THUMB_WIDTH']) || !isset($_ENV['IMG_THUMB_HEIGHT']) || !isset($_ENV['IMG_THUMB_QUALITY'])) { + die('Required environment variables are not set in the .env file'); } -// Specify the paths // Specify the paths $albumsDir = 'public/images/'; $dbFilePath = $_ENV['DB_FILE_PATH']; @@ -24,10 +23,32 @@ $dbFilePath = $_ENV['DB_FILE_PATH']; $database = new Database($dbFilePath); $image = new Image($database); +// Function to check for albums in the database and remove those that don't exist in the images folder +function cleanUpAlbums(Database $database, $albumsDir) +{ + $db = $database->getConnection(); + $albumsQuery = "SELECT DISTINCT album FROM images"; + $albumsResult = $db->query($albumsQuery); + + while ($row = $albumsResult->fetchArray(SQLITE3_ASSOC)) { + $album = $row['album']; + $albumPath = $albumsDir . $album; + + if (!is_dir($albumPath)) { + // Remove all images related to this album from the database + $deleteQuery = $db->prepare("DELETE FROM images WHERE album = :album"); + $deleteQuery->bindValue(':album', $album, SQLITE3_TEXT); + $deleteQuery->execute(); + + echo "Removed album '$album' from database as it does not exist in the images folder\n"; + } + } +} + // Function to process a directory function processDirectory($albumDir, $albumName, Image $image) { - $dirIterator = new RecursiveDirectoryIterator($albumDir); + $dirIterator = new RecursiveDirectoryIterator($albumDir, RecursiveDirectoryIterator::SKIP_DOTS); $iterator = new RecursiveIteratorIterator($dirIterator); foreach ($iterator as $file) { @@ -43,16 +64,18 @@ function processDirectory($albumDir, $albumName, Image $image) $bigPath = $bigDir . $fileInfo['filename'] . '.webp'; if (!file_exists($thumbPath)) { - if (!is_dir($thumbDir)) { - mkdir($thumbDir, 0777, true); + if (!is_dir($thumbDir) && !mkdir($thumbDir, 0777, true) && !is_dir($thumbDir)) { + error_log("Failed to create directory: $thumbDir"); + continue; } 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); + if (!is_dir($bigDir) && !mkdir($bigDir, 0777, true) && !is_dir($bigDir)) { + error_log("Failed to create directory: $bigDir"); + continue; } ThumbnailGenerator::createBigImage($filePath, $bigPath); echo "Created big image for $fileName in $albumName\n"; @@ -74,6 +97,10 @@ function processDirectory($albumDir, $albumName, Image $image) } } +// Clean up albums that no longer exist in the images directory +cleanUpAlbums($database, $albumsDir); + +// Process existing albums and images $albums = scandir($albumsDir); foreach ($albums as $album) { if ($album !== '.' && $album !== '..' && is_dir($albumsDir . $album)) { diff --git a/web/src/Album.php b/web/src/Album.php new file mode 100644 index 0000000..a75e45f --- /dev/null +++ b/web/src/Album.php @@ -0,0 +1,32 @@ +db = $database->getConnection(); + $this->database = $database; + } + + public function getAlbums() + { + $albumsQuery = "SELECT DISTINCT album FROM images"; + $albumsResult = $this->db->query($albumsQuery); + $albums = []; + while ($row = $albumsResult->fetchArray(SQLITE3_ASSOC)) { + $albums[] = $row['album']; + } + return $albums; + } + + public function getAlbumPreview($album) + { + $image = new Image($this->database); + return $image->getImages($album, 1, 0)[0] ?? null; + } +} diff --git a/web/src/Gallery.php b/web/src/Gallery.php new file mode 100644 index 0000000..e4315c5 --- /dev/null +++ b/web/src/Gallery.php @@ -0,0 +1,27 @@ +db = $database->getConnection(); + $this->database = $database; + } + + public function getGalleryImages($album, $limit, $offset, $sortOrder, $sortDirection) + { + $image = new Image($this->database); + return $image->getImages($album, $limit, $offset, $sortOrder, $sortDirection); + } + + public function countImages($album) + { + $image = new Image($this->database); + return $image->countImages($album); + } +} diff --git a/web/src/Image.php b/web/src/Image.php index 5021e21..350a27d 100644 --- a/web/src/Image.php +++ b/web/src/Image.php @@ -43,13 +43,13 @@ class Image return $stmt->execute(); } - public function getImages($album = null, $limit = 24, $offset = 0) + public function getImages($album = null, $limit = 24, $offset = 0, $sortOrder = 'modified_date', $sortDirection = 'DESC') { $query = "SELECT * FROM images"; if ($album) { $query .= " WHERE album = :album"; } - $query .= " ORDER BY modified_date DESC LIMIT :limit OFFSET :offset"; + $query .= " ORDER BY $sortOrder $sortDirection LIMIT :limit OFFSET :offset"; $stmt = $this->db->prepare($query); if ($album) { @@ -88,8 +88,7 @@ class Image $stmt->bindValue(':album', $album, SQLITE3_TEXT); } $result = $stmt->execute(); - $row = $result->fetchArray(SQLITE3_ASSOC); + $row = $stmt->execute()->fetchArray(SQLITE3_ASSOC); return $row['count']; } } - diff --git a/web/src/js/app.js b/web/src/js/app.js index 44d2f3e..948b3f2 100644 --- a/web/src/js/app.js +++ b/web/src/js/app.js @@ -1,17 +1,65 @@ +let currentImageIndex = 0; +let images = []; + 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'); + images = Array.from(document.querySelectorAll('#gallery img')).map(img => ({ + big: img.dataset.big, + original: img.dataset.original + })); + + currentImageIndex = images.findIndex(img => img.big === element.dataset.big); + + if (currentImageIndex === -1) { + console.error('Error: Image not found in the array'); + return; + } + + updateLightbox(); + + // Add event listener for keyboard navigation + document.addEventListener('keydown', handleKeydown); } -function closeLightbox() { - const lightbox = document.getElementById('lightbox'); - lightbox.classList.remove('active'); +function updateLightbox() { const lightboxImg = document.getElementById('lightbox-img'); - lightboxImg.src = ''; // Unload the current image + lightboxImg.src = images[currentImageIndex].big; + lightboxImg.setAttribute('data-original', images[currentImageIndex].original); + + // Reset zoom on the image lightboxImg.style.transform = 'scale(1)'; + lightboxImg.style.maxWidth = '100%'; + lightboxImg.style.maxHeight = '100vh'; + + const lightbox = document.getElementById('lightbox'); + lightbox.classList.add('active'); + + // Reset the "View Original" button visibility + const originalButton = document.getElementById('original-button'); + originalButton.style.display = 'block'; + + updateNavigationButtons(); +} + +function updateNavigationButtons() { + const prevButton = document.getElementById('lightbox-prev'); + const nextButton = document.getElementById('lightbox-next'); + + // Disable or enable buttons based on current index + prevButton.style.display = currentImageIndex === 0 ? 'none' : 'block'; + nextButton.style.display = currentImageIndex === images.length - 1 ? 'none' : 'block'; +} + +function closeLightbox(event) { + if (event.type === 'keydown' || event.target.id === 'lightbox') { + 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)'; + + // Remove the keyboard event listener + document.removeEventListener('keydown', handleKeydown); + } } function zoomImage(event) { @@ -37,4 +85,80 @@ function loadOriginal(event) { const originalSrc = lightboxImg.getAttribute('data-original'); lightboxImg.src = originalSrc; lightboxImg.style.transform = 'scale(1)'; -} \ No newline at end of file + + // Hide the "View Original" button when the original is shown + const originalButton = document.getElementById('original-button'); + originalButton.style.display = 'none'; +} + +function navigateLightbox(direction) { + currentImageIndex += direction; + + if (currentImageIndex < 0) { + currentImageIndex = 0; + } else if (currentImageIndex >= images.length) { + currentImageIndex = images.length - 1; + } + + updateLightbox(); +} + +function handleKeydown(event) { + if (event.key === 'ArrowLeft') { + if (currentImageIndex > 0) { + navigateLightbox(-1); + } + } else if (event.key === 'ArrowRight') { + if (currentImageIndex < images.length - 1) { + navigateLightbox(1); + } + } else if (event.key === 'Escape') { + closeLightbox({ type: 'keydown' }); + } +} + +// Make sure the lightbox only closes when clicking on the background +document.getElementById('lightbox').addEventListener('click', function(event) { + if (event.target.id === 'lightbox') { + closeLightbox(event); + } +}); + + +// Debounce function to limit rapid firing of functions +function debounce(func, wait = 200) { + let timeout; + return function(...args) { + const context = this; + clearTimeout(timeout); + timeout = setTimeout(() => func.apply(context, args), wait); + }; +} + +document.addEventListener("DOMContentLoaded", function() { + const album = document.getElementById('gallery').dataset.album; + + document.getElementById('sortOrder').addEventListener('change', applyFilters); + document.getElementById('limit').addEventListener('change', applyFilters); + document.getElementById('sortDirection').addEventListener('click', toggleSortDirection); + + function applyFilters() { + const sortOrder = document.getElementById('sortOrder').value; + const sortDirection = document.getElementById('sortDirection').innerText.includes('Descending') ? 'DESC' : 'ASC'; + const limit = document.getElementById('limit').value; + document.cookie = `sortOrder=${sortOrder}; path=/`; + document.cookie = `sortDirection=${sortDirection}; path=/`; + document.cookie = `limit=${limit}; path=/`; + window.location.href = `/gallery/${album}/1`; + } + + function toggleSortDirection() { + const sortButton = document.getElementById('sortDirection'); + if (sortButton.innerText.includes('Descending')) { + sortButton.innerText = 'Ascending ▲'; + } else { + sortButton.innerText = 'Descending ▼'; + } + applyFilters(); + } +}); \ No newline at end of file diff --git a/web/src/styles/tailwind.css b/web/src/styles/tailwind.css index 6aa511a..cfab0bc 100644 --- a/web/src/styles/tailwind.css +++ b/web/src/styles/tailwind.css @@ -21,10 +21,44 @@ cursor: zoom-in; } +.lightbox-content { + position: relative; +} + +.lightbox-content img { + max-width: 100%; + max-height: 100vh; /* Ensures the image doesn't exceed the viewport height */ + display: block; + margin: auto; +} + + .lightbox.active { display: flex; } +.lightbox-nav { + position: absolute; + top: 50%; + font-size: 30px; + color: white; + text-shadow: 0px 0px 10px rgba(0, 0, 0, 0.7); + cursor: pointer; + user-select: none; + z-index: 10; + padding: 10px; +} + +.lightbox-prev { + left: 20px; + transform: translateY(-50%); +} + +.lightbox-next { + right: 20px; + transform: translateY(-50%); +} + .title-overlay { pointer-events: none; } diff --git a/web/src/views/albums.phtml b/web/src/views/albums.phtml new file mode 100644 index 0000000..b216870 --- /dev/null +++ b/web/src/views/albums.phtml @@ -0,0 +1,37 @@ + + + + + + Photo Gallery - Select Album + + + +
+

Select an Album

+
+ $image): ?> + + + +
+
+ No images available +
+
+

+
+
+ + +
+
+ + diff --git a/web/src/views/gallery.phtml b/web/src/views/gallery.phtml new file mode 100644 index 0000000..0d516a6 --- /dev/null +++ b/web/src/views/gallery.phtml @@ -0,0 +1,86 @@ + + + + + + Photo Gallery - <?= htmlspecialchars($album) ?> + + + +
+

- Gallery

+ ← Back to Albums + + +
+
+
+ + +
+
+ + +
+
+
+ + +
+
+ + + + + +
+ 1): ?> + + +
+
+ + + + + + + diff --git a/web/tailwind.config.js b/web/tailwind.config.js index 916a6dd..cbe548e 100644 --- a/web/tailwind.config.js +++ b/web/tailwind.config.js @@ -1,6 +1,6 @@ /** @type {import('tailwindcss').Config} */ module.exports = { - content: ["./public/gallery.php","./public/index.php"], + content: ["./src/views/**/*.phtml"], theme: { extend: {}, },