improvements to appearance

additions to administrator mode

added a landing page for main page
This commit is contained in:
Brandon4466
2025-07-28 17:01:59 -07:00
parent ce8c47d50d
commit c76247119b
6 changed files with 534 additions and 130 deletions

155
README.md Normal file
View File

@@ -0,0 +1,155 @@
<h1 align="center">
<img alt="galpal logo" src=".github/images/logo.png" width="160px"/><br/>
galpal
</h1>
<p align="center">galpal gives you and your clients a quick, easy, and beautiful website to access and download photographs. Has all the features of any professional image hosting software, easy to setup, made specifically for photographers.</p>
<!-- <p align="center"><img alt="GitHub release (latest by date)" src="https://img.shields.io/github/v/release/galpal/galpal?style=for-the-badge">&nbsp;<img alt="GitHub all releases" src="https://img.shields.io/github/downloads/galpal/galpal/total?style=for-the-badge">&nbsp;<img alt="GitHub Workflow Status" src="https://img.shields.io/github/actions/workflow/status/galpal/galpal/release.yml?style=for-the-badge"></p> -->
<img alt="galpal ui" src=".github/images/front-dark.png" /><br/>
*[Light version too!](.github/images/front-light.png)*
## Documentation
Full documentation is available at [https://github.com/Brandon4466/galpal](https://github.com/Brandon4466/galpal)
## What Is galpal?
You've already done the hard work. The photography, the editing, everything. Now you're ready to distribute your photos... But how? This is where galpal steps in! No more hassel of how you're going to get the photos to the client or who you're going to have to explain how to use dropbox to. galpal gives a nice and easy to use interface, complete with password protection, to distribute photos to your clients.
## Key Features
- Album password protection
- Download all photos or only selected
- Supports multiple albums
- Easily configurable
- Supports multiple platforms (Linux, Windows, macOS) on many architectures (x86, ARM)
- Container support (Docker, Kubernetes)
## Installation
For complete installation instructions, visit our [Installation Guide](https://https://github.com/Brandon4466/galpal/installation/linux). Guides available for Windows, Linux, Docker, and more.
### Linux (One-Click Installer)
#### Installation Script
```bash
wget https://github.com/Brandon4466/galpal/install && bash install
```
### Docker Compose
Create `docker-compose.yml` and add the following. If you have an existing setup change to fit that.
```yml
version: "1.0"
services:
galpal:
container_name: galpal
image: github.com/Brandon4466/galpal/galpal:latest
restart: unless-stopped
environment:
- TZ=${TZ}
user: 1000:1000
volumes:
- ${BASE_DOCKER_DATA_PATH}/galpal/config:/config
ports:
- 7474:7474
```
Then start with:
```bash
docker compose up -d
```
### Windows
Download the latest Windows installer from [here](https://github.com/Brandon4466/galpal/installation/windows).
### MacOS
#### One-Click Installer
Also compatible with macOS.
#### App Installer
Download the latest macOS .dmg from [here](https://github.com/Brandon4466/galpal/installation/macos).
#### Install with Homebrew
Install Homebrew
```bash
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
```
Install galpal
```bash
brew install galpal
```
Run
```bash
brew services start galpal
```
#### Systemd (Recommended)
On Linux-based systems, it is recommended to run galpal as a sort of service with auto-restarting capabilities, in
order to account for potential downtime. The most common way is to do it via systemd. This is setup automatically when using the installation script.
If not using the installation script, you will need to create a service file in `/etc/systemd/system/` called `galpal.service`.
```bash
touch /etc/systemd/system/galpal@.service
```
Then place the following content inside the file (e.g. via nano/vim/ed):
```systemd title="/etc/systemd/system/galpal@.service"
[Unit]
Description=galpal service for %i
After=syslog.target network-online.target
[Service]
Type=simple
User=%i
Group=%i
ExecStart=/usr/bin/galpal --config=/home/%i/.config/galpal/
[Install]
WantedBy=multi-user.target
```
Start the service. Enable will make it startup on reboot.
```bash
systemctl enable -q --now --user galpal@$USER
```
By default, the configuration is set to listen on `127.0.0.1`. While galpal works fine as is exposed to the internet,
it is recommended to use a reverse proxy
like [nginx](https://github.com/Brandon4466/galpal/installation/linux#nginx), [caddy](https://github.com/Brandon4466/galpal/installation/linux#caddy)
or [traefik](https://github.com/Brandon4466/galpal/installation/docker#traefik).
If you are not running a reverse proxy change `host` in the `config.toml` to `0.0.0.0`.
## Community
We have a great community on [Discord](https://github.com/Brandon4466/galpal)! Connect with other galpal users, get notified of new updates, and ask questions!
## License
<a href="https://github.com/Brandon4466/galpal">galpal</a> © 2025 by <a href="https://github.com/Brandon4466">Brandon Brunson</a> is licensed under <a href="https://creativecommons.org/licenses/by-nc-sa/4.0/">CC BY-NC-SA 4.0</a><img src="https://mirrors.creativecommons.org/presskit/icons/cc.svg" alt="" style="max-width: 1em;max-height:1em;margin-left: .2em;"><img src="https://mirrors.creativecommons.org/presskit/icons/by.svg" alt="" style="max-width: 1em;max-height:1em;margin-left: .2em;"><img src="https://mirrors.creativecommons.org/presskit/icons/nc.svg" alt="" style="max-width: 1em;max-height:1em;margin-left: .2em;"><img src="https://mirrors.creativecommons.org/presskit/icons/sa.svg" alt="" style="max-width: 1em;max-height:1em;margin-left: .2em;">
- **Run:** You can run galpal in any noncommercial environment.
- **Study and Modify:** Access to the source code allows you to study and modify galpal to suit your needs.
Copyright 2025

216
admin.php
View File

@@ -1,17 +1,40 @@
<?php <?php
session_start(); session_start();
// Path to password storage // Passwords now stored per album in images/<album>/password.txt
$passwordFile = __DIR__ . '/passwords.json';
if (!file_exists($passwordFile)) {
file_put_contents($passwordFile, '{}');
}
$passwords = json_decode(file_get_contents($passwordFile), true);
// Simple admin login (hardcoded for demo) // Simple admin login (hardcoded for demo)
$adminPassword = 'admin123'; $adminPassword = 'admin123';
$loggedIn = isset($_SESSION['admin']) && $_SESSION['admin'] === true; $loggedIn = isset($_SESSION['admin']) && $_SESSION['admin'] === true;
// AJAX endpoint to get album info
if (isset($_GET['get_album_info']) && isset($_GET['album'])) {
$album = $_GET['album'];
$pwFile = __DIR__ . '/images/' . $album . '/info.yaml';
$pw = '';
$title = '';
if (file_exists($pwFile)) {
if (function_exists('yaml_parse_file')) {
$yaml = yaml_parse_file($pwFile);
$pw = isset($yaml['password']) ? $yaml['password'] : '';
$title = isset($yaml['title']) ? $yaml['title'] : '';
} else {
$lines = file($pwFile);
foreach ($lines as $line) {
if (preg_match('/^password:\s*(.+)$/', trim($line), $m)) {
$pw = $m[1];
}
if (preg_match('/^title:\s*(.+)$/', trim($line), $m)) {
$title = $m[1];
}
}
}
}
header('Content-Type: application/json');
echo json_encode(['password' => $pw, 'title' => $title]);
exit;
}
if (isset($_POST['admin_login'])) { if (isset($_POST['admin_login'])) {
if ($_POST['admin_password'] === $adminPassword) { if ($_POST['admin_password'] === $adminPassword) {
$_SESSION['admin'] = true; $_SESSION['admin'] = true;
@@ -24,10 +47,17 @@ if (isset($_POST['admin_login'])) {
if ($loggedIn && isset($_POST['set_album_password'])) { if ($loggedIn && isset($_POST['set_album_password'])) {
$album = $_POST['album_name']; $album = $_POST['album_name'];
$pw = $_POST['album_password']; $pw = $_POST['album_password'];
$title = isset($_POST['album_title']) ? $_POST['album_title'] : '';
if ($album && $pw !== null) { if ($album && $pw !== null) {
$passwords[$album] = $pw; $pwFile = __DIR__ . '/images/' . $album . '/info.yaml';
file_put_contents($passwordFile, json_encode($passwords)); $yamlArr = ['password' => $pw, 'title' => $title];
$success = "Password set for album '$album'."; $yamlContent = "password: " . $pw . "\ntitle: " . $title . "\n";
if (function_exists('yaml_emit_file')) {
yaml_emit_file($pwFile, $yamlArr);
} else {
file_put_contents($pwFile, $yamlContent);
}
$success = "Password and title set for album '$album'.";
} }
} }
@@ -53,49 +83,143 @@ if (is_dir($dir)) {
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>Admin - Album Passwords</title> <title>Admin - Album Passwords</title>
<style> <style>
body { font-family: Arial, sans-serif; background: #f0f0f0; } body { font-family: Arial, sans-serif; background: #181818; color: #e0e0e0; }
.container { max-width: 500px; margin: 40px auto; background: #fff; padding: 24px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.12); } .container { max-width: 500px; background: #222; padding: 24px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.32); margin: 0; }
h2 { text-align: center; } h2 { text-align: center; color: #4fa3ff; }
label { font-weight: bold; } label { font-weight: bold; color: #e0e0e0; }
input, select { width: 100%; padding: 8px; margin: 8px 0 16px 0; border-radius: 4px; border: 1px solid #ccc; } input, select { width: 100%; padding: 8px; margin: 8px 0 16px 0; border-radius: 4px; border: 1px solid #444; background: #181818; color: #e0e0e0; }
button { padding: 8px 16px; background: #0078d4; color: #fff; border: none; border-radius: 4px; font-weight: bold; cursor: pointer; } #admin_password { width: 76%; min-width: 180px; display: inline-block; }
.msg { color: green; } #album_password, #album_title { width: 95%; min-width: 180px; display: inline-block; }
.error { color: red; } button { padding: 8px 16px; background: #333; color: #fff; border: 1px solid #444; border-radius: 4px; font-weight: bold; cursor: pointer; }
button[name="logout"] { background: #d32f2f; }
.msg { color: #4caf50; }
.error { color: #d32f2f; }
</style> </style>
</head> </head>
<body> <body>
<div class="container"> <div style="display: flex; flex-direction: row; justify-content: center; align-items: flex-start; gap: 8px;">
<h2>Administrator Mode</h2> <?php if ($loggedIn): ?>
<?php if (!$loggedIn): ?> <!-- Album List Container -->
<form method="post"> <div class="album-list-container" style="width: 220px; min-width: 180px; background: #222; padding: 18px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.32); margin: 0;">
<label for="admin_password">Admin Password:</label> <h3 style="text-align:center;">Albums</h3>
<input type="password" name="admin_password" id="admin_password" required> <div id="albumList">
<button type="submit" name="admin_login">Login</button>
<?php if (isset($error)) echo "<div class='error'>$error</div>"; ?>
</form>
<?php else: ?>
<form method="post">
<label for="album_name">Select Album:</label>
<select name="album_name" id="album_name" required>
<?php foreach ($albums as $album): ?> <?php foreach ($albums as $album): ?>
<option value="<?= htmlspecialchars($album) ?>"><?= htmlspecialchars($album) ?></option> <?php
$thumb = '';
$albumDir = __DIR__ . '/images/' . $album . '/';
$thumbDir = $albumDir . 'thumbnails/';
$imgFile = '';
$extensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'];
foreach (scandir($albumDir) as $file) {
$ext = strtolower(pathinfo($file, PATHINFO_EXTENSION));
if (in_array($ext, $extensions)) {
if (is_dir($thumbDir) && file_exists($thumbDir . $file)) {
$thumb = 'images/' . $album . '/thumbnails/' . $file;
} else {
$thumb = 'images/' . $album . '/' . $file;
}
$imgFile = $file;
break;
}
}
?>
<div class="album-item" data-album="<?= htmlspecialchars($album) ?>" style="cursor:pointer; margin-bottom:18px; border-radius:6px; border:1px solid #444; padding:8px; display:flex; align-items:center; background:#222; transition:box-shadow 0.2s;">
<img src="<?= htmlspecialchars($thumb) ?>" alt="thumb" style="width:48px;height:48px;object-fit:cover;border-radius:4px;margin-right:12px;border:1px solid #333;">
<span style="font-weight:bold; color:#e0e0e0;"><?= htmlspecialchars($album) ?></span>
</div>
<?php endforeach; ?> <?php endforeach; ?>
</select> </div>
<label for="album_password">Set/View Password:</label> </div>
<input type="text" name="album_password" id="album_password" required>
<button type="submit" name="set_album_password">Set Password</button>
</form>
<?php if (isset($success)) echo "<div class='msg'>$success</div>"; ?>
<form method="post" style="margin-top:16px;">
<button type="submit" name="logout">Logout</button>
</form>
<h3>Current Album Passwords:</h3>
<ul>
<?php foreach ($passwords as $album => $pw): ?>
<li><strong><?= htmlspecialchars($album) ?>:</strong> <?= htmlspecialchars($pw) ?></li>
<?php endforeach; ?>
</ul>
<?php endif; ?> <?php endif; ?>
<!-- Admin Form Container -->
<div class="container" style="margin:0;">
<h2>Administrator Mode</h2>
<?php if (!$loggedIn): ?>
<form method="post">
<label for="admin_password">Admin Password:</label>
<input type="password" name="admin_password" id="admin_password" required>
<button type="submit" name="admin_login">Login</button>
<?php if (isset($error)) echo "<div class='error'>$error</div>"; ?>
</form>
<?php else: ?>
<form method="post" id="albumForm">
<input type="hidden" name="album_name" id="album_name" required>
<label for="album_password">Password:</label>
<input type="text" name="album_password" id="album_password" required>
<label for="album_title">Title:</label>
<input type="text" name="album_title" id="album_title">
<label for="album_link">Album Link:</label>
<div style="position:relative;width:100%;margin-bottom:16px;">
<input type="text" id="album_link" readonly style="background:#181818;color:#e0e0e0;width:98%;padding-right:40px;box-sizing:border-box; border: 1px solid #444;">
<button type="button" id="copyAlbumLink" style="position:absolute;right:14px;top:21%;padding:0 6px;background:#222;color:#888;border:1px solid #444;border-radius:4px;cursor:pointer;display:flex;align-items:center;justify-content:center;height:28px;min-height:0;">
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#888" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
</button>
</div>
<button type="submit" name="set_album_password">Change</button>
</form>
<script>
document.addEventListener('DOMContentLoaded', function() {
var albumNameInput = document.getElementById('album_name');
var pwInput = document.getElementById('album_password');
var titleInput = document.getElementById('album_title');
var albumLinkInput = document.getElementById('album_link');
var copyBtn = document.getElementById('copyAlbumLink');
var albumItems = document.querySelectorAll('.album-item');
function loadAlbumInfo(album) {
fetch('admin.php?get_album_info=1&album=' + encodeURIComponent(album))
.then(resp => resp.json())
.then(data => {
pwInput.value = data.password || '';
titleInput.value = data.title || '';
albumNameInput.value = album;
// Set album link (change URL as needed)
var link = window.location.origin + '/?album=' + encodeURIComponent(album);
albumLinkInput.value = link;
// Highlight selected album
albumItems.forEach(function(item) {
item.style.boxShadow = '';
item.style.background = '#222';
});
var selected = document.querySelector('.album-item[data-album="' + album.replace(/"/g, '\\"') + '"]');
if (selected) {
selected.style.boxShadow = '0 0 0 2px #4fa3ff';
selected.style.background = '#181818';
}
});
}
albumItems.forEach(function(item) {
item.addEventListener('click', function() {
var album = this.getAttribute('data-album');
loadAlbumInfo(album);
});
});
if (copyBtn) {
copyBtn.addEventListener('click', function() {
albumLinkInput.select();
albumLinkInput.setSelectionRange(0, 99999); // For mobile
try {
document.execCommand('copy');
var orig = copyBtn.innerHTML;
copyBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#4caf50" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>';
setTimeout(function(){ copyBtn.innerHTML = orig; }, 1200);
} catch (err) {
var orig = copyBtn.innerHTML;
copyBtn.innerHTML = '<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="#d32f2f" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>';
setTimeout(function(){ copyBtn.innerHTML = orig; }, 1200);
}
});
}
});
</script>
<?php if (isset($success)) echo "<div class='msg'>$success</div>"; ?>
<form method="post" style="position:absolute;top:24px;right:32px;">
<button type="submit" name="logout" style="background:#d32f2f;">Logout</button>
</form>
<?php endif; ?>
</div>
</div> </div>
</body> </body>
<footer style="text-align:center;padding:24px 0 12px 0;color:#888;font-size:16px;position:fixed;left:0;bottom:0;width:100%;background:#181818;">
Made with &copy; <a href="https://github.com/Brandon4466/galpal" target="_blank" style="color:#4fa3ff;text-decoration:none;">GalPal</a>
</footer>
</html> </html>

View File

@@ -3,11 +3,12 @@
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Album Viewer</title> <title>Photos</title>
<style> <style>
body { body {
font-family: Arial, sans-serif; font-family: Arial, sans-serif;
background: #f0f0f0; background: #181818;
color: #e0e0e0;
} }
.gallery { .gallery {
display: grid; display: grid;
@@ -19,17 +20,44 @@
width: 100%; width: 100%;
height: auto; height: auto;
border-radius: 8px; border-radius: 8px;
box-shadow: 0 2px 5px rgba(0,0,0,0.2); box-shadow: 0 2px 5px rgba(0,0,0,0.5);
background: #222;
border: 1px solid #333;
}
.gallery-btn {
position: fixed;
top: 40px;
left: 20px;
display: inline-block;
padding: 6px 10px;
background: #222;
color: #fff;
text-decoration: none;
border-radius: 5px;
font-weight: bold;
font-size: 14px;
border: 1px solid #444;
cursor: pointer;
box-shadow: 0 2px 8px rgba(0,0,0,0.4);
}
#select-images.gallery-btn {
left: 140px;
}
#download-selected.gallery-btn {
left: 160px;
} }
</style> </style>
</head> </head>
<body> <body>
<h1 style="text-align:center;">Album Viewer</h1> <h1 style="text-align:center;">Photos</h1>
<!-- Album selector removed. Album is chosen via URL parameter. --> <!-- Album selector removed. Album is chosen via URL parameter. -->
<a href="Download_All_Photos.zip" id="download-all" style="position:fixed;top:20px;left:20px;z-index:1000;display:inline-block;padding:8px 16px;background:#0078d4;color:#fff;text-decoration:none;border-radius:4px;font-weight:bold;">Download All</a> <a href="All_Photos.zip" id="download-all" class="gallery-btn">Download All</a>
<button id="select-images" style="position:fixed;top:20px;left:160px;z-index:1000;display:inline-block;padding:8px 16px;background:#0078d4;color:#fff;border:none;border-radius:4px;font-weight:bold;cursor:pointer;">Select</button> <button id="select-images" class="gallery-btn">Select</button>
<div class="gallery" id="gallery"></div> <div class="gallery" id="gallery"></div>
<footer style="text-align:center;padding:24px 0 12px 0;color:#888;font-size:16px;position:fixed;left:0;bottom:0;width:100%;background:#181818;">
Made with &copy; <a href="https://github.com/Brandon4466/galpal" target="_blank" style="color:#4fa3ff;text-decoration:none;">GalPal</a>
</footer>
<script> <script>
const gallery = document.getElementById('gallery'); const gallery = document.getElementById('gallery');
@@ -42,9 +70,15 @@
return urlParams.get(name); return urlParams.get(name);
} }
function renderGallery(images) { function renderGallery(albumData) {
gallery.innerHTML = ''; gallery.innerHTML = '';
images.forEach((imgObj, idx) => { // Show album title if present
if (albumData.title) {
const titleDiv = document.createElement('div');
titleDiv.innerHTML = `<h2 style="text-align:center;margin-bottom:24px;">${albumData.title}</h2>`;
gallery.appendChild(titleDiv);
}
albumData.images.forEach((imgObj, idx) => {
const itemDiv = document.createElement('div'); const itemDiv = document.createElement('div');
itemDiv.style.position = 'relative'; itemDiv.style.position = 'relative';
@@ -80,36 +114,66 @@
}); });
} }
function setupSelectButton() {
selectBtn.style.display = 'inline-block';
selectBtn.onclick = function() {
document.querySelectorAll('.img-checkbox').forEach(cb => {
cb.style.display = 'block';
cb.checked = false;
});
selectBtn.style.display = 'none';
if (downloadBtn) downloadBtn.remove();
downloadBtn = document.createElement('button');
downloadBtn.id = 'download-selected';
downloadBtn.textContent = 'Download Selected';
downloadBtn.className = 'gallery-btn';
// Match the style of the Select button
downloadBtn.style.left = '140px';
downloadBtn.style.position = 'fixed';
downloadBtn.style.top = '40px';
downloadBtn.style.display = 'inline-block';
downloadBtn.style.padding = '6px 10px';
downloadBtn.style.background = '#0078d4';
downloadBtn.style.color = '#fff';
downloadBtn.style.textDecoration = 'none';
downloadBtn.style.borderRadius = '5px';
downloadBtn.style.fontWeight = 'bold';
downloadBtn.style.fontSize = '14px';
downloadBtn.style.border = 'none';
downloadBtn.style.cursor = 'pointer';
document.body.appendChild(downloadBtn);
downloadBtn.onclick = function() {
const selected = Array.from(document.querySelectorAll('.img-checkbox:checked'));
if (selected.length === 0) {
alert('Please select at least one image to download.');
return;
}
selected.forEach(checkbox => {
const link = document.createElement('a');
link.href = checkbox.value;
link.download = checkbox.value.split('/').pop();
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
});
// After download, hide checkboxes and show selectBtn again
document.querySelectorAll('.img-checkbox').forEach(cb => {
cb.style.display = 'none';
cb.checked = false;
});
selectBtn.style.display = 'inline-block';
downloadBtn.remove();
};
};
}
fetch('list-images.php') fetch('list-images.php')
.then(response => response.json()) .then(response => response.json())
.then(data => { .then(data => {
albums = data; albums = data;
const albumName = getQueryParam('album'); const albumName = getQueryParam('album');
if (!albumName) { if (!albumName) {
// Landing page: show album links window.location.href = 'landing.html';
gallery.innerHTML = '<div style="text-align:center;padding:40px 0;"><h2>Welcome!</h2><p>Select an album to view photos:</p></div>';
const albumList = document.createElement('div');
albumList.style.display = 'flex';
albumList.style.flexWrap = 'wrap';
albumList.style.justifyContent = 'center';
albumList.style.gap = '20px';
Object.keys(albums).forEach(album => {
const link = document.createElement('a');
link.href = `index.html?album=${encodeURIComponent(album)}`;
link.textContent = album + (albums[album].protected ? ' 🔒' : '');
link.style.display = 'inline-block';
link.style.padding = '16px 32px';
link.style.background = '#0078d4';
link.style.color = '#fff';
link.style.borderRadius = '8px';
link.style.fontWeight = 'bold';
link.style.fontSize = '20px';
link.style.textDecoration = 'none';
link.style.boxShadow = '0 2px 8px rgba(0,0,0,0.12)';
albumList.appendChild(link);
});
gallery.appendChild(albumList);
selectBtn.style.display = 'none';
return; return;
} }
if (!albums[albumName]) { if (!albums[albumName]) {
@@ -128,14 +192,15 @@
} }
return resp.json(); return resp.json();
}) })
.then(images => { .then(albumData => {
renderGallery(images); renderGallery(albumData);
selectBtn.style.display = 'inline-block'; setupSelectButton();
localStorage.setItem('album_pw_' + albumName, pw); localStorage.setItem('album_pw_' + albumName, pw);
}) })
.catch(() => { .catch(() => {
gallery.innerHTML = `<div style='text-align:center;padding:40px 0;'><h2>Password Required</h2><form id='pwform'><input type='password' id='album_pw' placeholder='Enter album password' style='padding:8px;font-size:18px;border-radius:4px;border:1px solid #ccc;width:220px;'><button type='submit' style='margin-left:12px;padding:8px 16px;background:#0078d4;color:#fff;border:none;border-radius:4px;font-weight:bold;cursor:pointer;'>View Album</button></form><div id='pwerror' style='color:red;margin-top:12px;'></div></div>`; gallery.innerHTML = `<div style='text-align:center;padding:40px 0;'><h2>Password Required</h2><form id='pwform'><input type='password' id='album_pw' placeholder='Enter album password' style='padding:8px;font-size:18px;border-radius:4px;border:1px solid #ccc;width:220px;'><button type='submit' style='margin-left:12px;padding:8px 16px;background:#0078d4;color:#fff;border:none;border-radius:4px;font-weight:bold;cursor:pointer;'>View Album</button></form><div id='pwerror' style='color:red;margin-top:12px;'></div></div>`;
selectBtn.style.display = 'none'; selectBtn.style.display = 'none';
if (downloadBtn) downloadBtn.remove();
document.getElementById('pwform').onsubmit = function(e) { document.getElementById('pwform').onsubmit = function(e) {
e.preventDefault(); e.preventDefault();
const pwTry = document.getElementById('album_pw').value; const pwTry = document.getElementById('album_pw').value;
@@ -149,52 +214,15 @@
// Not protected, fetch images // Not protected, fetch images
fetch(`list-images.php?album=${encodeURIComponent(albumName)}`) fetch(`list-images.php?album=${encodeURIComponent(albumName)}`)
.then(resp => resp.json()) .then(resp => resp.json())
.then(images => { .then(albumData => {
renderGallery(images); renderGallery(albumData);
selectBtn.style.display = 'inline-block'; setupSelectButton();
}); });
selectBtn.addEventListener('click', () => {
document.querySelectorAll('.img-checkbox').forEach(cb => {
cb.style.display = 'block';
});
selectBtn.style.display = 'none';
downloadBtn = document.createElement('button');
downloadBtn.id = 'download-selected';
downloadBtn.textContent = 'Download Selected';
downloadBtn.style.position = 'fixed';
downloadBtn.style.top = '20px';
downloadBtn.style.left = '160px';
downloadBtn.style.zIndex = '1000';
downloadBtn.style.display = 'inline-block';
downloadBtn.style.padding = '8px 16px';
downloadBtn.style.background = '#0078d4';
downloadBtn.style.color = '#fff';
downloadBtn.style.border = 'none';
downloadBtn.style.borderRadius = '4px';
downloadBtn.style.fontWeight = 'bold';
downloadBtn.style.cursor = 'pointer';
document.body.appendChild(downloadBtn);
downloadBtn.addEventListener('click', () => {
const selected = Array.from(document.querySelectorAll('.img-checkbox:checked'));
if (selected.length === 0) {
alert('Please select at least one image to download.');
return;
}
selected.forEach(checkbox => {
const link = document.createElement('a');
link.href = checkbox.value;
link.download = checkbox.value.split('/').pop();
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
});
});
});
}) })
.catch(() => { .catch(() => {
gallery.innerHTML = '<p style="color:red;">Could not load images.</p>'; gallery.innerHTML = '<p style="color:red;">Could not load images.</p>';
selectBtn.style.display = 'none';
if (downloadBtn) downloadBtn.remove();
}); });
</script> </script>
</body> </body>

61
landing.html Normal file
View File

@@ -0,0 +1,61 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Photos</title>
<style>
body {
font-family: Arial, sans-serif;
background: #181818;
color: #e0e0e0;
}
.container {
max-width: 600px;
margin: 60px auto;
background: #222;
padding: 32px;
border-radius: 10px;
box-shadow: 0 2px 8px rgba(0,0,0,0.32);
text-align: center;
}
h1 {
color: #4fa3ff;
margin-bottom: 24px;
}
.album-list {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 20px;
margin-top: 32px;
}
.album-link {
display: inline-block;
padding: 16px 32px;
background: #333;
color: #fff;
border-radius: 8px;
font-weight: bold;
font-size: 20px;
text-decoration: none;
box-shadow: 0 2px 8px rgba(0,0,0,0.32);
transition: background 0.2s;
}
.album-link:hover {
background: #4fa3ff;
color: #222;
}
</style>
</head>
<body>
<div class="container">
<h1>Uh oh! Let's try again.</h1>
<p>That didn't work. Try clicking on the link again.</p>
</div>
<footer style="text-align:center;padding:24px 0 12px 0;color:#888;font-size:16px;position:fixed;left:0;bottom:0;width:100%;background:#181818;">
Made with &copy; <a href="https://github.com/Brandon4466/galpal" target="_blank" style="color:#4fa3ff;text-decoration:none;">GalPal</a>
</footer>
<!-- No album list displayed -->
</body>
</html>

View File

@@ -4,8 +4,7 @@
$dir = __DIR__ . '/images/'; $dir = __DIR__ . '/images/';
$extensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp']; $extensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'];
$albums = []; $albums = [];
$passwordFile = __DIR__ . '/passwords.json'; // Passwords now stored per album in images/<album>/password.txt
$passwords = file_exists($passwordFile) ? json_decode(file_get_contents($passwordFile), true) : [];
// If album is requested, check password // If album is requested, check password
if (isset($_GET['album'])) { if (isset($_GET['album'])) {
@@ -16,8 +15,29 @@ if (isset($_GET['album'])) {
echo json_encode(['error' => 'Album not found']); echo json_encode(['error' => 'Album not found']);
exit; exit;
} }
if (isset($passwords[$album]) && $passwords[$album] !== '') { $pwFile = $dir . $album . '/info.yaml';
if ($pw !== $passwords[$album]) { $albumPassword = '';
$albumTitle = '';
if (file_exists($pwFile)) {
if (function_exists('yaml_parse_file')) {
$yaml = yaml_parse_file($pwFile);
$albumPassword = isset($yaml['password']) ? $yaml['password'] : '';
$albumTitle = isset($yaml['title']) ? $yaml['title'] : '';
} else {
// Fallback: parse manually
$lines = file($pwFile);
foreach ($lines as $line) {
if (preg_match('/^password:\s*(.+)$/', trim($line), $m)) {
$albumPassword = $m[1];
}
if (preg_match('/^title:\s*(.+)$/', trim($line), $m)) {
$albumTitle = $m[1];
}
}
}
}
if ($albumPassword !== '') {
if ($pw !== $albumPassword) {
http_response_code(403); http_response_code(403);
echo json_encode(['error' => 'Password required']); echo json_encode(['error' => 'Password required']);
exit; exit;
@@ -39,7 +59,7 @@ if (isset($_GET['album'])) {
} }
} }
header('Content-Type: application/json'); header('Content-Type: application/json');
echo json_encode($albumImages); echo json_encode(['title' => $albumTitle, 'images' => $albumImages]);
exit; exit;
} }
@@ -47,8 +67,24 @@ if (isset($_GET['album'])) {
if (is_dir($dir)) { if (is_dir($dir)) {
foreach (scandir($dir) as $album) { foreach (scandir($dir) as $album) {
if ($album === '.' || $album === '..' || !is_dir($dir . $album)) continue; if ($album === '.' || $album === '..' || !is_dir($dir . $album)) continue;
$pwFile = $dir . $album . '/info.yaml';
$protected = false;
if (file_exists($pwFile)) {
if (function_exists('yaml_parse_file')) {
$yaml = yaml_parse_file($pwFile);
$protected = isset($yaml['password']) && $yaml['password'] !== '';
} else {
$lines = file($pwFile);
foreach ($lines as $line) {
if (preg_match('/^password:\s*(.+)$/', trim($line), $m)) {
if ($m[1] !== '') $protected = true;
break;
}
}
}
}
$albums[$album] = [ $albums[$album] = [
'protected' => isset($passwords[$album]) && $passwords[$album] !== '', 'protected' => $protected,
]; ];
} }
} }

View File

@@ -1 +1 @@
{} {"psu2025":"111"}