")
+def photos(section, filename):
+ return send_from_directory(PHOTOS_DIR / section, filename)
+
+# --- Main entry point ---
+if __name__ == "__main__":
+ logging.info("Starting WebUI at http://127.0.0.1:5000")
+ app.run(debug=True)
diff --git a/src/webui/index.html b/src/webui/index.html
new file mode 100644
index 0000000..cc82873
--- /dev/null
+++ b/src/webui/index.html
@@ -0,0 +1,39 @@
+
+
+
+
+ Photo WebUI
+
+
+
+ Photo WebUI
+
+
+
+
+
+
+
+
+
+
Gallery
+
+
+
+
+
+
+
Hero
+
+
+
+
+
+
+
diff --git a/src/webui/js/main.js b/src/webui/js/main.js
new file mode 100644
index 0000000..3f0113d
--- /dev/null
+++ b/src/webui/js/main.js
@@ -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 = `
+
+
+
+ `;
+ 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 = `
+
+
+ `;
+ 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();
diff --git a/src/webui/js/upload.js b/src/webui/js/upload.js
new file mode 100644
index 0000000..045e625
--- /dev/null
+++ b/src/webui/js/upload.js
@@ -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
+ }
+});
diff --git a/src/webui/style/style.css b/src/webui/style/style.css
new file mode 100644
index 0000000..ec1f57a
--- /dev/null
+++ b/src/webui/style/style.css
@@ -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;
+ }
+}