Spaces:
Sleeping
Sleeping
| <!-- templates/index.html --> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Image Uploader</title> | |
| <link rel="icon" type="image/svg+xml" href="/static/favicon/logo-svg.png"> | |
| <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| :root { | |
| --primary-color: #4361ee; | |
| --secondary-color: #3f37c9; | |
| --accent-color: #4cc9f0; | |
| --success-color: #22cc88; | |
| --light-bg: #f8f9fa; | |
| --dark-text: #212529; | |
| --card-shadow: 0 4px 24px rgba(0, 0, 0, 0.08); | |
| --hover-shadow: 0 10px 30px rgba(0, 0, 0, 0.15); | |
| } | |
| body { | |
| background-color: #f5f7fa; | |
| color: var(--dark-text); | |
| font-family: 'Segoe UI', system-ui, -apple-system, sans-serif; | |
| padding-bottom: 40px; | |
| } | |
| .navbar { | |
| background-color: white; | |
| box-shadow: 0 2px 15px rgba(0, 0, 0, 0.05); | |
| padding: 15px 0; | |
| margin-bottom: 30px; | |
| } | |
| .navbar-brand { | |
| font-weight: 600; | |
| color: var(--primary-color); | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .navbar-brand i { | |
| font-size: 1.5em; | |
| } | |
| .container { | |
| max-width: 1200px; | |
| } | |
| .card { | |
| border: none; | |
| border-radius: 12px; | |
| box-shadow: var(--card-shadow); | |
| transition: all 0.3s ease; | |
| } | |
| .section-title { | |
| margin-bottom: 20px; | |
| font-weight: 600; | |
| color: var(--dark-text); | |
| border-left: 4px solid var(--primary-color); | |
| padding-left: 12px; | |
| } | |
| .upload-container { | |
| background-color: white; | |
| padding: 25px; | |
| border-radius: 12px; | |
| margin-bottom: 30px; | |
| box-shadow: var(--card-shadow); | |
| } | |
| .gallery-container { | |
| background-color: white; | |
| padding: 25px; | |
| border-radius: 12px; | |
| margin-bottom: 30px; | |
| box-shadow: var(--card-shadow); | |
| } | |
| .search-container { | |
| background-color: #f5f7fa; | |
| padding: 20px; | |
| border-radius: 8px; | |
| margin-bottom: 20px; | |
| } | |
| .image-card { | |
| margin-bottom: 25px; | |
| border-radius: 12px; | |
| overflow: hidden; | |
| position: relative; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| } | |
| .image-card:hover { | |
| transform: translateY(-5px); | |
| box-shadow: var(--hover-shadow); | |
| } | |
| .image-preview { | |
| max-height: 200px; | |
| object-fit: cover; | |
| width: 100%; | |
| height: 200px; | |
| } | |
| .card-body { | |
| padding: 20px; | |
| } | |
| .card-title { | |
| font-weight: 600; | |
| margin-bottom: 15px; | |
| } | |
| .hidden { | |
| display: none; | |
| } | |
| #uploadProgress { | |
| margin-top: 15px; | |
| height: 10px; | |
| border-radius: 5px; | |
| } | |
| .progress-bar { | |
| background-color: var(--primary-color); | |
| } | |
| .form-control, .form-select { | |
| border-radius: 8px; | |
| padding: 10px 15px; | |
| border: 1px solid #e0e0e0; | |
| } | |
| .form-control:focus, .form-select:focus { | |
| box-shadow: 0 0 0 3px rgba(67, 97, 238, 0.15); | |
| border-color: var(--primary-color); | |
| } | |
| .btn { | |
| border-radius: 8px; | |
| padding: 10px 20px; | |
| font-weight: 500; | |
| } | |
| .btn-primary { | |
| background-color: var(--primary-color); | |
| border-color: var(--primary-color); | |
| } | |
| .btn-primary:hover, .btn-primary:focus { | |
| background-color: var(--secondary-color); | |
| border-color: var(--secondary-color); | |
| } | |
| .btn-outline-primary { | |
| color: var(--primary-color); | |
| border-color: var(--primary-color); | |
| } | |
| .btn-outline-primary:hover, .btn-outline-primary:focus, | |
| .btn-check:checked + .btn-outline-primary { | |
| background-color: var(--primary-color); | |
| border-color: var(--primary-color); | |
| color: white; | |
| } | |
| .btn-danger { | |
| background-color: #e63946; | |
| border-color: #e63946; | |
| } | |
| .btn-danger:hover, .btn-danger:focus { | |
| background-color: #d00000; | |
| border-color: #d00000; | |
| } | |
| .hashtag { | |
| display: inline-block; | |
| background-color: rgba(67, 97, 238, 0.1); | |
| padding: 5px 12px; | |
| border-radius: 20px; | |
| font-size: 0.8rem; | |
| margin-right: 5px; | |
| margin-bottom: 5px; | |
| color: var(--primary-color); | |
| transition: all 0.2s ease; | |
| font-weight: 500; | |
| text-decoration: none; | |
| } | |
| .hashtag:hover { | |
| background-color: rgba(67, 97, 238, 0.2); | |
| color: var(--primary-color); | |
| text-decoration: none; | |
| } | |
| .new-badge { | |
| position: absolute; | |
| top: 15px; | |
| left: 15px; | |
| background-color: var(--success-color); | |
| color: white; | |
| padding: 5px 10px; | |
| border-radius: 20px; | |
| font-size: 0.7rem; | |
| font-weight: 600; | |
| z-index: 2; | |
| box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); | |
| } | |
| .delete-icon { | |
| position: absolute; | |
| top: 15px; | |
| right: 15px; | |
| background-color: rgba(255, 255, 255, 0.9); | |
| color: var(--danger-color); | |
| border-radius: 50%; | |
| width: 32px; | |
| height: 32px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| z-index: 3; | |
| box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2); | |
| cursor: pointer; | |
| transition: all 0.2s ease; | |
| } | |
| .delete-icon:hover { | |
| background-color: var(--danger-color); | |
| color: white; | |
| transform: scale(1.1); | |
| } | |
| .card-link { | |
| color: inherit; | |
| text-decoration: none; | |
| } | |
| .card-link:hover { | |
| color: inherit; | |
| text-decoration: none; | |
| } | |
| .layout-controls { | |
| margin-bottom: 15px; | |
| } | |
| /* Custom 5-column layout */ | |
| .col-5-layout { | |
| position: relative; | |
| width: 100%; | |
| padding-right: 15px; | |
| padding-left: 15px; | |
| } | |
| @media (min-width: 992px) { | |
| .col-5-layout { | |
| flex: 0 0 20%; | |
| max-width: 20%; | |
| } | |
| } | |
| @media (min-width: 768px) and (max-width: 991.98px) { | |
| .col-5-layout { | |
| flex: 0 0 25%; | |
| max-width: 25%; | |
| } | |
| } | |
| @media (max-width: 767.98px) { | |
| .col-5-layout { | |
| flex: 0 0 50%; | |
| max-width: 50%; | |
| } | |
| } | |
| .card-img-overlay { | |
| background: linear-gradient(to top, rgba(0,0,0,0.7) 0%, rgba(0,0,0,0) 50%); | |
| transition: all 0.3s ease; | |
| } | |
| .toast-container { | |
| position: fixed; | |
| bottom: 20px; | |
| right: 20px; | |
| z-index: 9999; | |
| } | |
| .toast { | |
| min-width: 250px; | |
| background-color: white; | |
| box-shadow: 0 5px 15px rgba(0, 0, 0, 0.15); | |
| border: none; | |
| border-radius: 8px; | |
| } | |
| .toast-header { | |
| border-radius: 8px 8px 0 0; | |
| } | |
| .action-buttons { | |
| display: flex; | |
| gap: 8px; | |
| } | |
| .btn-sm { | |
| padding: 5px 10px; | |
| font-size: 0.8rem; | |
| } | |
| .empty-state { | |
| text-align: center; | |
| padding: 60px 0; | |
| } | |
| .empty-state i { | |
| font-size: 3rem; | |
| color: #dee2e6; | |
| margin-bottom: 20px; | |
| } | |
| .empty-state p { | |
| color: #6c757d; | |
| font-size: 1.1rem; | |
| } | |
| .gallery-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 20px; | |
| } | |
| .site-footer { | |
| background-color: white; | |
| padding: 20px 0; | |
| text-align: center; | |
| margin-top: 40px; | |
| border-top: 1px solid #eaeaea; | |
| color: #6c757d; | |
| font-size: 0.9rem; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <nav class="navbar navbar-expand-lg navbar-light"> | |
| <div class="container"> | |
| <a class="navbar-brand" href="/"> | |
| <i class="fas fa-photo-film"></i> | |
| Image Uploader | |
| </a> | |
| <div class="ms-auto"> | |
| <a href="/logout" class="btn btn-outline-danger"> | |
| <i class="fas fa-sign-out-alt me-2"></i>Logout | |
| </a> | |
| </div> | |
| </div> | |
| </nav> | |
| <div class="container"> | |
| <!-- Upload Section (Top) --> | |
| <div class="upload-container"> | |
| <h3 class="section-title">Upload Images</h3> | |
| <form id="uploadForm" enctype="multipart/form-data"> | |
| <div class="row"> | |
| <div class="col-md-8"> | |
| <div class="mb-3"> | |
| <label for="files" class="form-label"> | |
| <i class="fas fa-images me-2"></i>Select images | |
| </label> | |
| <input class="form-control" type="file" id="files" name="files" accept="image/*" multiple> | |
| <small class="text-muted">You can select multiple images</small> | |
| </div> | |
| </div> | |
| <div class="col-md-4"> | |
| <div class="mb-3"> | |
| <label for="hashtags" class="form-label"> | |
| <i class="fas fa-hashtag me-2"></i>Add hashtags | |
| </label> | |
| <input type="text" class="form-control" id="hashtags" name="hashtags" placeholder="nature travel photography"> | |
| <small class="text-muted">Separate with spaces or commas</small> | |
| </div> | |
| </div> | |
| </div> | |
| <button type="submit" class="btn btn-primary"> | |
| <i class="fas fa-cloud-upload-alt me-2"></i>Upload Images | |
| </button> | |
| </form> | |
| <div id="uploadProgress" class="progress hidden"> | |
| <div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" style="width: 0%"></div> | |
| </div> | |
| </div> | |
| <!-- Gallery Section (Bottom with integrated search) --> | |
| <div class="gallery-container"> | |
| <div class="gallery-header"> | |
| <h3 class="section-title mb-0">Your Gallery</h3> | |
| <div class="layout-controls btn-group" role="group"> | |
| <input type="radio" class="btn-check" name="layout" id="layout3" autocomplete="off" checked> | |
| <label class="btn btn-outline-primary" for="layout3"><i class="fas fa-th-large"></i></label> | |
| <input type="radio" class="btn-check" name="layout" id="layout4" autocomplete="off"> | |
| <label class="btn btn-outline-primary" for="layout4"><i class="fas fa-th"></i></label> | |
| <input type="radio" class="btn-check" name="layout" id="layout5" autocomplete="off"> | |
| <label class="btn btn-outline-primary" for="layout5"><i class="fas fa-grip-horizontal"></i></label> | |
| </div> | |
| </div> | |
| <!-- Search and Filter (Inside Gallery) --> | |
| <div class="search-container"> | |
| <form id="searchForm" method="get" action="/" class="row g-3"> | |
| <div class="col-md-6"> | |
| <label for="search" class="form-label"> | |
| <i class="fas fa-search me-2"></i>Search by name or hashtag | |
| </label> | |
| <input type="text" class="form-control" id="search" name="search" value="{{ current_search or '' }}" placeholder="Search..."> | |
| </div> | |
| <div class="col-md-6"> | |
| <label for="tag" class="form-label"> | |
| <i class="fas fa-filter me-2"></i>Filter by hashtag | |
| </label> | |
| <select class="form-select" id="tag" name="tag"> | |
| <option value="">All hashtags</option> | |
| {% for tag in all_hashtags %} | |
| <option value="{{ tag }}" {% if current_tag == tag %}selected{% endif %}>{{ tag }}</option> | |
| {% endfor %} | |
| </select> | |
| </div> | |
| </form> | |
| </div> | |
| <!-- Image Gallery --> | |
| <div id="gallery"> | |
| {% if not uploaded_images %} | |
| <div class="empty-state"> | |
| <i class="fas fa-images"></i> | |
| <h4>No images yet</h4> | |
| <p>Upload your first image to get started!</p> | |
| </div> | |
| {% else %} | |
| <div class="row" id="imageGrid"> | |
| {# First, render new images #} | |
| {% for image in uploaded_images %} | |
| {% if image.is_new %} | |
| <div class="image-item col-md-4"> | |
| <div class="card image-card"> | |
| <a href="/view/{{ image.name }}" class="card-link"> | |
| <span class="new-badge">NEW</span> | |
| <button class="delete-icon delete-btn" data-filename="{{ image.name }}" title="Delete image"> | |
| <i class="fas fa-trash-alt"></i> | |
| </button> | |
| <img src="{{ image.url }}" class="card-img-top image-preview" alt="{{ image.original_filename }}"> | |
| <div class="card-body"> | |
| <h5 class="card-title text-truncate" title="{{ image.original_filename }}">{{ image.original_filename }}</h5> | |
| <div class="hashtags mb-3"> | |
| {% for tag in image.hashtags %} | |
| <a href="/?tag={{ tag }}" class="hashtag">#{{ tag }}</a> | |
| {% endfor %} | |
| </div> | |
| </div> | |
| </a> | |
| </div> | |
| </div> | |
| {% endif %} | |
| {% endfor %} | |
| {# Then, render viewed images #} | |
| {% for image in uploaded_images %} | |
| {% if not image.is_new %} | |
| <div class="image-item col-md-4"> | |
| <div class="card image-card"> | |
| <a href="/view/{{ image.name }}" class="card-link"> | |
| <button class="delete-icon delete-btn" data-filename="{{ image.name }}" title="Delete image"> | |
| <i class="fas fa-trash-alt"></i> | |
| </button> | |
| <img src="{{ image.url }}" class="card-img-top image-preview" alt="{{ image.original_filename }}"> | |
| <div class="card-body"> | |
| <h5 class="card-title text-truncate" title="{{ image.original_filename }}">{{ image.original_filename }}</h5> | |
| <div class="hashtags mb-3"> | |
| {% for tag in image.hashtags %} | |
| <a href="/?tag={{ tag }}" class="hashtag">#{{ tag }}</a> | |
| {% endfor %} | |
| </div> | |
| </div> | |
| </a> | |
| </div> | |
| </div> | |
| {% endif %} | |
| {% endfor %} | |
| </div> | |
| {% endif %} | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Footer --> | |
| <footer class="site-footer"> | |
| <div class="container"> | |
| <p class="mb-0">©2025 Detomo. All rights reserved</p> | |
| </div> | |
| </footer> | |
| <!-- Toast container for notifications --> | |
| <div class="toast-container"> | |
| <div id="uploadSuccessToast" class="toast" role="alert" aria-live="assertive" aria-atomic="true"> | |
| <div class="toast-header bg-success text-white"> | |
| <i class="fas fa-check-circle me-2"></i> | |
| <strong class="me-auto">Success</strong> | |
| <button type="button" class="btn-close" data-bs-dismiss="toast" aria-label="Close"></button> | |
| </div> | |
| <div class="toast-body" id="toastMessage"> | |
| Images uploaded successfully! | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Modal for Delete Confirmation --> | |
| <div class="modal fade" id="deleteConfirmModal" tabindex="-1" aria-labelledby="deleteConfirmModalLabel" aria-hidden="true"> | |
| <div class="modal-dialog modal-dialog-centered"> | |
| <div class="modal-content"> | |
| <div class="modal-header border-0"> | |
| <h5 class="modal-title" id="deleteConfirmModalLabel">Confirm Deletion</h5> | |
| <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> | |
| </div> | |
| <div class="modal-body text-center py-4"> | |
| <div class="mb-4"> | |
| <i class="fas fa-exclamation-triangle text-danger" style="font-size: 3.5rem;"></i> | |
| </div> | |
| <h5 class="mb-3">Are you sure you want to delete this image?</h5> | |
| <p class="text-muted mb-0">This action cannot be undone.</p> | |
| </div> | |
| <div class="modal-footer border-0 justify-content-center"> | |
| <button type="button" class="btn btn-light px-4" data-bs-dismiss="modal"> | |
| <i class="fas fa-times me-2"></i>Cancel | |
| </button> | |
| <button type="button" class="btn btn-danger px-4" id="confirmDeleteBtn"> | |
| <i class="fas fa-trash-alt me-2"></i>Delete | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Modal for No Files Selected --> | |
| <div class="modal fade" id="noFilesModal" tabindex="-1" aria-labelledby="noFilesModalLabel" aria-hidden="true"> | |
| <div class="modal-dialog modal-dialog-centered"> | |
| <div class="modal-content"> | |
| <div class="modal-header border-0"> | |
| <h5 class="modal-title" id="noFilesModalLabel">No Files Selected</h5> | |
| <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> | |
| </div> | |
| <div class="modal-body text-center py-4"> | |
| <div class="mb-4"> | |
| <i class="fas fa-images text-warning" style="font-size: 3.5rem;"></i> | |
| </div> | |
| <h5 class="mb-2">Please select at least one image file</h5> | |
| <p class="text-muted">You need to browse and select images before uploading.</p> | |
| </div> | |
| <div class="modal-footer border-0 justify-content-center"> | |
| <button type="button" class="btn btn-primary px-4" data-bs-dismiss="modal"> | |
| <i class="fas fa-check me-2"></i>OK | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Modal for Duplicate Filename Confirmation --> | |
| <div class="modal fade" id="duplicateFilesModal" tabindex="-1" aria-labelledby="duplicateFilesModalLabel" aria-hidden="true"> | |
| <div class="modal-dialog modal-dialog-centered modal-lg"> | |
| <div class="modal-content"> | |
| <div class="modal-header border-0"> | |
| <h5 class="modal-title" id="duplicateFilesModalLabel">Duplicate Filenames Detected</h5> | |
| <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> | |
| </div> | |
| <div class="modal-body py-4"> | |
| <div class="text-center mb-4"> | |
| <i class="fas fa-exclamation-circle text-warning" style="font-size: 3.5rem;"></i> | |
| <h5 class="mt-3 mb-4">Some files have the same names as existing images</h5> | |
| <p class="text-muted mb-4">Please confirm if you want to replace the existing images with the new ones</p> | |
| </div> | |
| <div class="table-responsive"> | |
| <table class="table table-borderless align-middle" id="duplicateFilesTable"> | |
| <thead class="table-light"> | |
| <tr> | |
| <th>Replace</th> | |
| <th>New Image</th> | |
| <th>Will Replace</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| <!-- Populated dynamically --> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| <div class="modal-footer border-0 justify-content-center"> | |
| <button type="button" class="btn btn-light px-4" data-bs-dismiss="modal"> | |
| <i class="fas fa-times me-2"></i>Cancel Upload | |
| </button> | |
| <button type="button" class="btn btn-primary px-4" id="confirmReplaceBtn"> | |
| <i class="fas fa-check me-2"></i>Continue With Selected Replacements | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"></script> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', function() { | |
| const uploadForm = document.getElementById('uploadForm'); | |
| const uploadProgress = document.getElementById('uploadProgress'); | |
| const progressBar = uploadProgress.querySelector('.progress-bar'); | |
| const toast = new bootstrap.Toast(document.getElementById('uploadSuccessToast'), { | |
| delay: 3000 | |
| }); | |
| // Store files and hashtags for potential reuse if duplicates are found | |
| let currentFiles = null; | |
| let currentHashtags = ''; | |
| let duplicateFiles = []; | |
| // Instant search functionality | |
| const searchInput = document.getElementById('search'); | |
| const tagSelect = document.getElementById('tag'); | |
| const searchForm = document.getElementById('searchForm'); | |
| // Handle search input changes | |
| searchInput.addEventListener('input', function() { | |
| // Add small delay to prevent too many requests while typing | |
| clearTimeout(searchInput.timer); | |
| searchInput.timer = setTimeout(() => { | |
| searchForm.submit(); | |
| }, 500); // Wait 500ms after typing stops | |
| }); | |
| // Handle tag selection changes | |
| tagSelect.addEventListener('change', function() { | |
| searchForm.submit(); | |
| }); | |
| // Handle layout controls | |
| const layoutControls = document.querySelectorAll('input[name="layout"]'); | |
| const imageGrid = document.getElementById('imageGrid'); | |
| const imageItems = document.querySelectorAll('.image-item'); | |
| function updateLayout(columns) { | |
| // First, remove all column classes from all items | |
| imageItems.forEach(item => { | |
| item.classList.remove('col-md-3', 'col-md-4', 'col-md-6', 'col-5-layout'); | |
| }); | |
| // Then apply the new column class | |
| imageItems.forEach(item => { | |
| if (columns === 3) { | |
| item.classList.add('col-md-4'); // 3 per row (4/12 width) | |
| } else if (columns === 4) { | |
| item.classList.add('col-md-3'); // 4 per row (3/12 width) | |
| } else if (columns === 5) { | |
| item.classList.add('col-5-layout'); // Custom 5 per row | |
| } | |
| }); | |
| // Save the preference to localStorage | |
| localStorage.setItem('preferredLayout', columns); | |
| } | |
| // Initialize layout based on localStorage or default to 3 columns | |
| const savedLayout = localStorage.getItem('preferredLayout') || '3'; | |
| const initialLayout = parseInt(savedLayout); | |
| // Make sure the correct radio button is checked | |
| const layoutButton = document.getElementById(`layout${initialLayout}`); | |
| if (layoutButton) { | |
| layoutButton.checked = true; | |
| updateLayout(initialLayout); | |
| } else { | |
| // Fallback to layout3 if saved layout is invalid | |
| document.getElementById('layout3').checked = true; | |
| updateLayout(3); | |
| } | |
| // Add event listeners to layout controls | |
| layoutControls.forEach(control => { | |
| control.addEventListener('change', function() { | |
| let columns = 3; // Default | |
| if (this.id === 'layout3') columns = 3; | |
| else if (this.id === 'layout4') columns = 4; | |
| else if (this.id === 'layout5') columns = 5; | |
| updateLayout(columns); | |
| }); | |
| }); | |
| // Handle form submission | |
| uploadForm.addEventListener('submit', function(e) { | |
| e.preventDefault(); | |
| const filesInput = document.getElementById('files'); | |
| if (!filesInput.files.length) { | |
| // Show the no files modal instead of alert | |
| const noFilesModal = new bootstrap.Modal(document.getElementById('noFilesModal')); | |
| noFilesModal.show(); | |
| return; | |
| } | |
| // Store current files and hashtags in case we need to handle duplicates | |
| currentFiles = filesInput.files; | |
| currentHashtags = document.getElementById('hashtags').value; | |
| const formData = new FormData(); | |
| // Add all files | |
| for (let i = 0; i < filesInput.files.length; i++) { | |
| formData.append('files', filesInput.files[i]); | |
| } | |
| // Add hashtags | |
| formData.append('hashtags', currentHashtags); | |
| // Show progress | |
| uploadProgress.classList.remove('hidden'); | |
| progressBar.style.width = '0%'; | |
| const xhr = new XMLHttpRequest(); | |
| xhr.upload.addEventListener('progress', function(e) { | |
| if (e.lengthComputable) { | |
| const percentComplete = (e.loaded / e.total) * 100; | |
| progressBar.style.width = percentComplete + '%'; | |
| } | |
| }); | |
| xhr.addEventListener('load', function() { | |
| // Hide progress bar | |
| uploadProgress.classList.add('hidden'); | |
| if (xhr.status === 200) { | |
| const response = JSON.parse(xhr.responseText); | |
| // Check if we need to handle duplicates | |
| if (response.success === false && response.action_required === 'confirm_replace') { | |
| // Store the duplicates | |
| duplicateFiles = response.duplicates; | |
| // Populate the duplicate files table | |
| const tableBody = document.querySelector('#duplicateFilesTable tbody'); | |
| tableBody.innerHTML = ''; | |
| duplicateFiles.forEach(function(file, index) { | |
| const row = document.createElement('tr'); | |
| row.innerHTML = ` | |
| <td> | |
| <div class="form-check"> | |
| <input class="form-check-input replace-checkbox" type="checkbox" | |
| value="${file.existing_file}" | |
| id="replace-check-${index}" | |
| data-original="${file.original_name}" checked> | |
| </div> | |
| </td> | |
| <td><strong>${file.new_file}</strong></td> | |
| <td>${file.existing_file}</td> | |
| `; | |
| tableBody.appendChild(row); | |
| }); | |
| // Show the duplicate files modal | |
| const duplicateModal = new bootstrap.Modal(document.getElementById('duplicateFilesModal')); | |
| duplicateModal.show(); | |
| return; | |
| } | |
| // Normal successful upload | |
| handleSuccessfulUpload(response); | |
| } else { | |
| alert('Upload failed. Please try again.'); | |
| } | |
| }); | |
| xhr.addEventListener('error', function() { | |
| alert('Upload failed. Please try again.'); | |
| uploadProgress.classList.add('hidden'); | |
| }); | |
| xhr.open('POST', '/upload/'); | |
| xhr.send(formData); | |
| }); | |
| // Handle confirmation of file replacements | |
| document.getElementById('confirmReplaceBtn').addEventListener('click', function() { | |
| // Get selected replacements | |
| const checkboxes = document.querySelectorAll('.replace-checkbox:checked'); | |
| const filesToReplace = Array.from(checkboxes).map(function(checkbox) { | |
| return { | |
| existing_file: checkbox.value, | |
| original_name: checkbox.dataset.original | |
| }; | |
| }); | |
| // Hide the modal | |
| const duplicateModal = bootstrap.Modal.getInstance(document.getElementById('duplicateFilesModal')); | |
| duplicateModal.hide(); | |
| // Create a new FormData with the current files | |
| const formData = new FormData(); | |
| // Add current files (those that we stored earlier) | |
| for (let i = 0; i < currentFiles.length; i++) { | |
| formData.append('files', currentFiles[i]); | |
| } | |
| // Add hashtags | |
| formData.append('hashtags', currentHashtags); | |
| // Add replacement information | |
| formData.append('replace_files', JSON.stringify(filesToReplace)); | |
| // Show progress again | |
| uploadProgress.classList.remove('hidden'); | |
| progressBar.style.width = '0%'; | |
| // Send the request to the replacement endpoint | |
| const xhr = new XMLHttpRequest(); | |
| xhr.upload.addEventListener('progress', function(e) { | |
| if (e.lengthComputable) { | |
| const percentComplete = (e.loaded / e.total) * 100; | |
| progressBar.style.width = percentComplete + '%'; | |
| } | |
| }); | |
| xhr.addEventListener('load', function() { | |
| // Hide progress bar | |
| uploadProgress.classList.add('hidden'); | |
| if (xhr.status === 200) { | |
| const response = JSON.parse(xhr.responseText); | |
| handleSuccessfulUpload(response); | |
| } else { | |
| alert('Upload failed. Please try again.'); | |
| } | |
| }); | |
| xhr.addEventListener('error', function() { | |
| alert('Upload failed. Please try again.'); | |
| uploadProgress.classList.add('hidden'); | |
| }); | |
| xhr.open('POST', '/upload-with-replace/'); | |
| xhr.send(formData); | |
| }); | |
| // Function to handle successful upload | |
| function handleSuccessfulUpload(response) { | |
| // Check if multiple files or single file | |
| let uploadCount = 1; | |
| if (response.files) { | |
| uploadCount = response.uploaded_count; | |
| } | |
| // Show success toast | |
| document.getElementById('toastMessage').textContent = | |
| `Successfully uploaded ${uploadCount} image${uploadCount > 1 ? 's' : ''}!`; | |
| toast.show(); | |
| // Reset form for next upload | |
| uploadForm.reset(); | |
| currentFiles = null; | |
| currentHashtags = ''; | |
| // Scroll to gallery section | |
| document.getElementById('gallery').scrollIntoView({ behavior: 'smooth' }); | |
| // Refresh the page after a short delay to show the new images | |
| setTimeout(() => { | |
| window.location.reload(); | |
| }, 1000); | |
| } | |
| // Handle delete buttons to stop event propagation | |
| document.querySelectorAll('.delete-btn').forEach(function(btn) { | |
| btn.addEventListener('click', function (e) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| const filename = this.dataset.filename; | |
| // Store the filename for later use | |
| document.getElementById('confirmDeleteBtn').dataset.filename = filename; | |
| // Show the delete confirmation modal | |
| const deleteModal = new bootstrap.Modal(document.getElementById('deleteConfirmModal')); | |
| deleteModal.show(); | |
| }); | |
| }); | |
| // Handle delete confirmation | |
| document.getElementById('confirmDeleteBtn').addEventListener('click', function() { | |
| const filename = this.dataset.filename; | |
| const deleteModal = bootstrap.Modal.getInstance(document.getElementById('deleteConfirmModal')); | |
| fetch(`/delete/${filename}`, { | |
| method: 'DELETE' | |
| }) | |
| .then(response => response.json()) | |
| .then(data => { | |
| if (data.success) { | |
| deleteModal.hide(); | |
| window.location.reload(); | |
| } else { | |
| alert('Failed to delete the image.'); | |
| } | |
| }) | |
| .catch(error => { | |
| console.error('Error:', error); | |
| alert('An error occurred while deleting the image.'); | |
| }); | |
| }); | |
| }); | |
| </script> | |
| </body> | |
| </html> |