Gallery front

This commit is contained in:
Djeex
2025-08-16 10:29:51 +02:00
parent 41450837f2
commit 1b0b228273
17 changed files with 535 additions and 9 deletions

39
src/webui/index.html Normal file
View File

@ -0,0 +1,39 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Photo WebUI</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style/style.css') }}">
</head>
<body>
<h1>Photo WebUI</h1>
<div class="toolbar">
<button onclick="refreshGallery()">🔄 Refresh Gallery</button>
<button onclick="refreshHero()">🔄 Refresh Hero</button>
<button onclick="saveChanges()">💾 Save Changes</button>
</div>
<!-- Gallery Upload Section -->
<div class="upload-section">
<h2>Gallery</h2>
<label>
Upload Image:
<input type="file" id="upload-gallery" accept=".png,.jpg,.jpeg,.webp">
</label>
<div id="gallery"></div>
</div>
<!-- Hero Upload Section -->
<div class="upload-section">
<h2>Hero</h2>
<label>
Upload Image:
<input type="file" id="upload-hero" accept=".png,.jpg,.jpeg,.webp">
</label>
<div id="hero"></div>
</div>
<script src="{{ url_for('static', filename='js/main.js') }}"></script>
<script src="{{ url_for('static', filename='js/upload.js') }}"></script>
</body>
</html>

151
src/webui/js/main.js Normal file
View File

@ -0,0 +1,151 @@
// Arrays to store gallery and hero images
let galleryImages = [];
let heroImages = [];
// Load images data from server
async function loadData() {
try {
// Fetch gallery images from API
const galleryRes = await fetch('/api/gallery');
galleryImages = await galleryRes.json();
renderGallery(); // Render gallery images
// Fetch hero images from API
const heroRes = await fetch('/api/hero');
heroImages = await heroRes.json();
renderHero(); // Render hero images
} catch(err) {
console.error(err);
alert("Error while loading images!");
}
}
// Render gallery images in the page
function renderGallery() {
const container = document.getElementById('gallery');
container.innerHTML = ''; // Clear current content
galleryImages.forEach((img, i) => {
const div = document.createElement('div');
div.className = 'photo';
div.innerHTML = `
<img src="/photos/${img.src}">
<input type="text" value="${(img.tags || []).join(', ')}"
onchange="updateTags(${i}, this.value)">
<button onclick="deleteGalleryImage(${i})">🗑 Delete</button>
`;
container.appendChild(div); // Add image div to container
});
}
// Render hero images in the page
function renderHero() {
const container = document.getElementById('hero');
container.innerHTML = ''; // Clear current content
heroImages.forEach((img, i) => {
const div = document.createElement('div');
div.className = 'photo';
div.innerHTML = `
<img src="/photos/${img.src}">
<button onclick="deleteHeroImage(${i})">🗑 Delete</button>
`;
container.appendChild(div); // Add image div to container
});
}
// Update tags for a gallery image
function updateTags(index, value) {
// Split tags by comma, trim spaces, and remove empty values
galleryImages[index].tags = value.split(',').map(t => t.trim()).filter(t => t);
}
// Delete a gallery image
async function deleteGalleryImage(index) {
const img = galleryImages[index];
try {
// Send only the filename to the server for deletion
const res = await fetch('/api/gallery/delete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ src: img.src.split('/').pop() }) // Keep only filename
});
const data = await res.json();
if (res.ok) {
// Remove image from array and re-render gallery
galleryImages.splice(index, 1);
renderGallery();
await saveGallery(); // Save updated tags
} else {
alert("Error: " + data.error);
}
} catch(err) {
console.error(err);
alert("Server error!");
}
}
// Delete a hero image
async function deleteHeroImage(index) {
const img = heroImages[index];
try {
// Send only the filename to the server for deletion
const res = await fetch('/api/hero/delete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ src: img.src.split('/').pop() }) // Keep only filename
});
const data = await res.json();
if (res.ok) {
// Remove image from array and re-render hero section
heroImages.splice(index, 1);
renderHero();
await saveHero(); // Save updated hero images
} else {
alert("Error: " + data.error);
}
} catch(err) {
console.error(err);
alert("Server error!");
}
}
// Save gallery images (with tags) to the server
async function saveGallery() {
await fetch('/api/gallery/update', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(galleryImages)
});
}
// Save hero images to the server
async function saveHero() {
await fetch('/api/hero/update', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(heroImages)
});
}
// Save both gallery and hero changes
async function saveChanges() {
await saveGallery();
await saveHero();
alert('✅ Changes saved!');
}
// Refresh gallery images from the server folder
async function refreshGallery() {
await fetch('/api/gallery/refresh', { method: 'POST' });
await loadData(); // Reload data after refresh
alert('🔄 Gallery updated from photos/gallery folder');
}
// Refresh hero images from the server folder
async function refreshHero() {
await fetch('/api/hero/refresh', { method: 'POST' });
await loadData(); // Reload data after refresh
alert('🔄 Hero updated from photos/hero folder');
}
// Initial load of images when page opens
loadData();

61
src/webui/js/upload.js Normal file
View File

@ -0,0 +1,61 @@
// Upload handler for gallery images
document.getElementById('upload-gallery').addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return; // Exit if no file is selected
// Create a FormData object to send the file
const formData = new FormData();
formData.append('file', file); // Key must match what upload.py expects
try {
// Send POST request to the gallery upload endpoint
const res = await fetch('/api/gallery/upload', {
method: 'POST',
body: formData
});
const data = await res.json();
if (res.ok) {
alert('✅ Gallery image uploaded!');
refreshGallery(); // Refresh the gallery list from the server
} else {
alert('Error: ' + data.error); // Show server error if upload failed
}
} catch (err) {
console.error(err);
alert('Server error!'); // Network or server failure
} finally {
e.target.value = ''; // Reset file input so the same file can be uploaded again if needed
}
});
// Upload handler for hero images
document.getElementById('upload-hero').addEventListener('change', async (e) => {
const file = e.target.files[0];
if (!file) return; // Exit if no file is selected
// Create a FormData object to send the file
const formData = new FormData();
formData.append('file', file); // Key must match what upload.py expects
try {
// Send POST request to the hero upload endpoint
const res = await fetch('/api/hero/upload', {
method: 'POST',
body: formData
});
const data = await res.json();
if (res.ok) {
alert('✅ Hero image uploaded!');
refreshHero(); // Refresh the hero list from the server
} else {
alert('Error: ' + data.error); // Show server error if upload failed
}
} catch (err) {
console.error(err);
alert('Server error!'); // Network or server failure
} finally {
e.target.value = ''; // Reset file input so the same file can be uploaded again if needed
}
});

98
src/webui/style/style.css Normal file
View File

@ -0,0 +1,98 @@
body {
font-family: Arial, sans-serif;
margin: 20px;
background-color: #f9f9f9;
color: #333;
}
h1, h2 {
color: #222;
}
.toolbar {
margin-bottom: 20px;
}
.toolbar button {
margin-right: 10px;
padding: 8px 12px;
border: none;
background-color: #4CAF50;
color: white;
cursor: pointer;
border-radius: 4px;
transition: background-color 0.2s;
}
.toolbar button:hover {
background-color: #45a049;
}
.upload-section {
margin-bottom: 30px;
}
.upload-section label {
margin-right: 20px;
cursor: pointer;
}
#gallery, #hero {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 15px;
}
.photo {
background-color: white;
border: 1px solid #ddd;
border-radius: 6px;
padding: 10px;
text-align: center;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.photo img {
max-width: 100%;
border-radius: 4px;
margin-bottom: 8px;
}
.photo input[type="text"] {
width: 100%;
padding: 4px 6px;
margin-bottom: 6px;
border: 1px solid #ccc;
border-radius: 4px;
}
.photo button {
padding: 4px 8px;
border: none;
background-color: #f44336;
color: white;
border-radius: 4px;
cursor: pointer;
transition: background-color 0.2s;
}
.photo button:hover {
background-color: #d32f2f;
}
/* Responsive adjustments */
@media (max-width: 500px) {
body {
margin: 10px;
}
.toolbar button {
margin-bottom: 8px;
width: 100%;
}
.upload-section label {
display: block;
margin-bottom: 10px;
}
}