Browse Source

feat(media): add support for HEIC/HEIF images and PDF files

- Extend media file detection to include HEIC/HEIF images and PDF documents
- Implement HEIC/HEIF conversion using heic2any library
- Add PDF viewer with download and fullscreen capabilities
- Enhance media viewer UI with custom styles for different media types
Geovanny Andrés Vega Mite 3 months ago
parent
commit
ca95544ee3
3 changed files with 286 additions and 10 deletions
  1. 7 3
      app.py
  2. 113 0
      static/css/styles.css
  3. 166 7
      templates/download.html

+ 7 - 3
app.py

@@ -121,13 +121,13 @@ def is_archive_file(file_path, mime_type, filename):
     return False
 
 def is_media_file(mime_type, filename):
-    """Check if file is a supported media format (image, video, audio)"""
+    """Check if file is a supported media format (image, video, audio, pdf)"""
     if not mime_type:
         return False, None
     
-    # Image files
+    # Image files (including HEIC/HEIF)
     if mime_type.startswith('image/'):
-        supported_images = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml', 'image/bmp']
+        supported_images = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml', 'image/bmp', 'image/heic', 'image/heif']
         if mime_type in supported_images:
             return True, 'image'
     
@@ -143,6 +143,10 @@ def is_media_file(mime_type, filename):
         if mime_type in supported_audio:
             return True, 'audio'
     
+    # PDF files
+    if mime_type == 'application/pdf':
+        return True, 'pdf'
+    
     return False, None
 
 def parse_arguments():

+ 113 - 0
static/css/styles.css

@@ -106,6 +106,7 @@
 .fancybox__container {
     --fancybox-bg: transparent;
     --fancybox-color: #ffffff;
+    z-index: 9999 !important;
 }
 
 .fancybox__content {
@@ -184,6 +185,90 @@
     transform: scale(1.1) !important;
 }
 
+/* GLightbox Custom Styles */
+.glightbox-container {
+    z-index: 9999 !important;
+}
+
+.glightbox-clean .goverlay {
+    background: rgba(0, 0, 0, 0.9) !important;
+    backdrop-filter: blur(10px);
+}
+
+.glightbox-clean .gslide-media {
+    border-radius: 8px;
+    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
+}
+
+.glightbox-clean .gnext,
+.glightbox-clean .gprev {
+    background: rgba(0, 0, 0, 0.8) !important;
+    backdrop-filter: blur(10px);
+    border-radius: 50% !important;
+    width: 50px !important;
+    height: 50px !important;
+    color: white !important;
+    transition: all 0.3s ease !important;
+}
+
+.glightbox-clean .gnext:hover,
+.glightbox-clean .gprev:hover {
+    background: rgba(0, 0, 0, 0.9) !important;
+    transform: scale(1.1);
+}
+
+.glightbox-clean .gclose {
+    background: rgba(0, 0, 0, 0.8) !important;
+    backdrop-filter: blur(10px);
+    border-radius: 50% !important;
+    color: white !important;
+    transition: all 0.3s ease !important;
+}
+
+.glightbox-clean .gclose:hover {
+    background: rgba(0, 0, 0, 0.9) !important;
+    transform: scale(1.1);
+}
+
+/* Video play button overlay */
+.video-preview-overlay {
+    position: absolute;
+    top: 50%;
+    left: 50%;
+    transform: translate(-50%, -50%);
+    background: rgba(0, 0, 0, 0.7);
+    border-radius: 50%;
+    width: 80px;
+    height: 80px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    transition: all 0.3s ease;
+    backdrop-filter: blur(10px);
+}
+
+.video-preview-overlay:hover {
+    background: rgba(0, 0, 0, 0.8);
+    transform: translate(-50%, -50%) scale(1.1);
+}
+
+.video-preview-overlay svg {
+    width: 32px;
+    height: 32px;
+    color: white;
+    margin-left: 4px; /* Slight offset for play icon */
+}
+
+/* PDF Modal Styles */
+.pdf-modal {
+    z-index: 10000;
+}
+
+.pdf-modal iframe {
+    background: white;
+    border-radius: 4px;
+}
+
 /* Dark mode specific adjustments */
 .dark .fancybox__backdrop {
     background: rgba(0, 0, 0, 0.95) !important;
@@ -204,6 +289,18 @@
     border-top-color: rgba(75, 85, 99, 0.3) !important;
 }
 
+.dark .glightbox-clean .goverlay {
+    background: rgba(0, 0, 0, 0.95) !important;
+}
+
+.dark .video-preview-overlay {
+    background: rgba(17, 24, 39, 0.8);
+}
+
+.dark .video-preview-overlay:hover {
+    background: rgba(17, 24, 39, 0.9);
+}
+
 /* Responsive adjustments */
 @media (max-width: 768px) {
     .fancybox__nav .fancybox__button {
@@ -220,6 +317,22 @@
         padding: 6px !important;
         margin: 1px !important;
     }
+    
+    .glightbox-clean .gnext,
+    .glightbox-clean .gprev {
+        width: 40px !important;
+        height: 40px !important;
+    }
+    
+    .video-preview-overlay {
+        width: 60px;
+        height: 60px;
+    }
+    
+    .video-preview-overlay svg {
+        width: 24px;
+        height: 24px;
+    }
 }
 
 /* Image hover effects */

+ 166 - 7
templates/download.html

@@ -12,6 +12,16 @@
     <!-- Fancybox CSS and JS -->
     <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@fancyapps/ui@5.0/dist/fancybox/fancybox.css">
     <script src="https://cdn.jsdelivr.net/npm/@fancyapps/ui@5.0/dist/fancybox/fancybox.umd.js"></script>
+    
+    <!-- GLightbox for videos -->
+    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/glightbox/dist/css/glightbox.min.css">
+    <script src="https://cdn.jsdelivr.net/npm/glightbox/dist/js/glightbox.min.js"></script>
+    
+    <!-- PDF.js -->
+    <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
+    
+    <!-- HEIC2ANY for iPhone images -->
+    <script src="https://cdn.jsdelivr.net/npm/heic2any@0.0.4/dist/heic2any.min.js"></script>
     <script src="https://cdn.tailwindcss.com"></script>
     <script>
         (function() {
@@ -142,11 +152,11 @@
             <div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-4 flex justify-center">
                  {% if media_type == 'image' %}
                  <div class="relative group">
-                     <a href="/preview/{{ file_id }}" data-fancybox="gallery" data-caption="{{ filename }}">
-                         <img src="/preview/{{ file_id }}" alt="{{ filename }}" class="max-w-full max-h-96 rounded-lg shadow-sm cursor-pointer transition-transform duration-200 hover:scale-105" loading="lazy">
+                     <a href="/preview/{{ file_id }}" data-fancybox="gallery" data-caption="{{ filename }}" id="image-link-{{ file_id }}">
+                         <img src="/preview/{{ file_id }}" alt="{{ filename }}" class="max-w-full max-h-96 rounded-lg shadow-sm cursor-pointer transition-transform duration-200 hover:scale-105" loading="lazy" id="preview-image-{{ file_id }}">
                      </a>
                      <div class="mt-3 flex flex-col items-center space-y-2">
-                          <p class="text-xs text-gray-500 dark:text-gray-400">Click to view with zoom, fullscreen & advanced controls</p>
+                          <p class="text-xs text-gray-500 dark:text-gray-400">Click to view with zoom, fullscreen & advanced controls (supports HEIC/HEIF)</p>
                           <div class="flex space-x-2">
                               <button onclick="downloadImage('{{ file_id }}', '{{ filename }}')" class="inline-flex items-center px-2 py-1 text-xs font-medium text-white bg-green-600 hover:bg-green-700 rounded transition-colors duration-200">
                                   <svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -164,10 +174,16 @@
                       </div>
                  </div>
                 {% elif media_type == 'video' %}
-                <video controls class="max-w-full max-h-96 rounded-lg shadow-sm">
-                    <source src="/preview/{{ file_id }}" type="{{ mime_type }}">
-                    Your browser does not support the video tag.
-                </video>
+                <div class="w-full max-w-2xl mx-auto">
+                    <video controls class="w-full rounded-lg shadow-lg" preload="metadata">
+                        <source src="/preview/{{ file_id }}" type="{{ mime_type }}">
+                        Your browser does not support the video tag.
+                    </video>
+                    <div class="mt-3 text-center">
+                        <p class="text-sm font-medium text-gray-900 dark:text-white">{{ filename }}</p>
+                        <p class="text-xs text-gray-500 dark:text-gray-400">{{ mime_type }}</p>
+                    </div>
+                </div>
                 {% elif media_type == 'audio' %}
                 <div class="w-full max-w-md">
                     <div class="flex items-center space-x-4 mb-4">
@@ -184,6 +200,42 @@
                         Your browser does not support the audio tag.
                     </audio>
                 </div>
+                {% elif media_type == 'pdf' %}
+                <div class="relative group w-full">
+                    <div class="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 overflow-hidden">
+                        <div class="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
+                            <div class="flex items-center space-x-3">
+                                <svg class="w-8 h-8 text-red-500" fill="currentColor" viewBox="0 0 20 20">
+                                    <path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4z" clip-rule="evenodd"></path>
+                                </svg>
+                                <div>
+                                    <h4 class="font-medium text-gray-900 dark:text-white">{{ filename }}</h4>
+                                    <p class="text-sm text-gray-500 dark:text-gray-400">PDF Document</p>
+                                </div>
+                            </div>
+                            <button onclick="openPDFViewer('/preview/{{ file_id }}', '{{ filename }}')" class="inline-flex items-center px-3 py-2 text-sm font-medium text-white bg-red-600 hover:bg-red-700 rounded-lg transition-colors duration-200">
+                                <svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
+                                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
+                                </svg>
+                                View PDF
+                            </button>
+                        </div>
+                        <div id="pdf-preview-{{ file_id }}" class="p-4 bg-gray-50 dark:bg-gray-900">
+                            <div class="flex items-center justify-center h-48 text-gray-500 dark:text-gray-400">
+                                <div class="text-center">
+                                    <svg class="w-12 h-12 mx-auto mb-2 text-red-500" fill="currentColor" viewBox="0 0 20 20">
+                                        <path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4z" clip-rule="evenodd"></path>
+                                    </svg>
+                                    <p class="text-sm">Click "View PDF" to open with full viewer controls</p>
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                    <div class="mt-3 flex flex-col items-center space-y-2">
+                        <p class="text-xs text-gray-500 dark:text-gray-400">PDF viewer with zoom, search, navigation & print support</p>
+                    </div>
+                </div>
                 {% endif %}
             </div>
             
@@ -471,5 +523,112 @@
             }
         });
     </script>
+    
+    <!-- HEIC/HEIF Support -->
+    <script>
+        // Function to handle HEIC/HEIF images
+        async function handleHEICImage(imgElement, fileId) {
+            try {
+                const response = await fetch(`/preview/${fileId}`);
+                const blob = await response.blob();
+                
+                if (blob.type === 'image/heic' || blob.type === 'image/heif') {
+                    // Convert HEIC to JPEG using heic2any
+                    const convertedBlob = await heic2any({
+                        blob: blob,
+                        toType: 'image/jpeg',
+                        quality: 0.8
+                    });
+                    
+                    const convertedUrl = URL.createObjectURL(convertedBlob);
+                    imgElement.src = convertedUrl;
+                    
+                    // Update the Fancybox link as well
+                    const linkElement = imgElement.closest('a[data-fancybox]');
+                    if (linkElement) {
+                        linkElement.href = convertedUrl;
+                    }
+                    
+                    console.log('HEIC image converted successfully');
+                }
+            } catch (error) {
+                console.error('Error converting HEIC image:', error);
+                // Fallback: show error message
+                imgElement.alt = 'HEIC image conversion failed';
+            }
+        }
+        
+        // Check for HEIC images on page load
+        document.addEventListener('DOMContentLoaded', function() {
+            const images = document.querySelectorAll('img[id^="preview-image-"]');
+            images.forEach(img => {
+                img.addEventListener('error', function() {
+                    // If image fails to load, it might be HEIC
+                    const fileId = this.id.replace('preview-image-', '');
+                    handleHEICImage(this, fileId);
+                });
+            });
+        });
+    </script>
+    
+
+    
+    <!-- PDF.js Support -->
+    <script>
+        // PDF viewer function
+        function openPDFViewer(pdfUrl, filename) {
+            // Create a modal for PDF viewing
+            const modal = document.createElement('div');
+            modal.className = 'fixed inset-0 bg-black bg-opacity-75 flex items-center justify-center z-50';
+            modal.innerHTML = `
+                <div class="bg-white dark:bg-gray-800 rounded-lg w-11/12 h-5/6 max-w-6xl flex flex-col">
+                    <div class="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-700">
+                        <h3 class="text-lg font-semibold text-gray-900 dark:text-white">${filename}</h3>
+                        <div class="flex items-center space-x-2">
+                            <button id="pdf-download" class="px-3 py-1 text-sm bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors duration-200">
+                                Download
+                            </button>
+                            <button onclick="this.closest('.fixed').remove()" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
+                                <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
+                                </svg>
+                            </button>
+                        </div>
+                    </div>
+                    <div class="flex-1 p-4">
+                        <iframe src="${pdfUrl}" class="w-full h-full border-0 rounded" title="${filename}"></iframe>
+                    </div>
+                </div>
+            `;
+            
+            document.body.appendChild(modal);
+            
+            // Add download functionality
+            modal.querySelector('#pdf-download').addEventListener('click', function() {
+                const link = document.createElement('a');
+                link.href = pdfUrl;
+                link.download = filename;
+                document.body.appendChild(link);
+                link.click();
+                document.body.removeChild(link);
+            });
+            
+            // Close modal when clicking outside
+            modal.addEventListener('click', function(e) {
+                if (e.target === modal) {
+                    modal.remove();
+                }
+            });
+            
+            // Close modal with Escape key
+            const handleEscape = function(e) {
+                if (e.key === 'Escape') {
+                    modal.remove();
+                    document.removeEventListener('keydown', handleEscape);
+                }
+            };
+            document.addEventListener('keydown', handleEscape);
+        }
+    </script>
 </body>
 </html>