Procházet zdrojové kódy

feat(media): add media preview support for images, videos and audio

- Implement is_media_file function to check supported media formats
- Add preview_file endpoint to serve media files securely
- Enhance download page with media preview UI and controls
- Include Fancybox for advanced image viewing capabilities
- Add dark mode styles for media preview components
Geovanny Andrés Vega Mite před 4 měsíci
rodič
revize
7b77eb1a9e
3 změnil soubory, kde provedl 476 přidání a 4 odebrání
  1. 66 1
      app.py
  2. 184 1
      static/css/styles.css
  3. 226 2
      templates/download.html

+ 66 - 1
app.py

@@ -120,6 +120,31 @@ 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)"""
+    if not mime_type:
+        return False, None
+    
+    # Image files
+    if mime_type.startswith('image/'):
+        supported_images = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp', 'image/svg+xml', 'image/bmp']
+        if mime_type in supported_images:
+            return True, 'image'
+    
+    # Video files
+    if mime_type.startswith('video/'):
+        supported_videos = ['video/mp4', 'video/webm', 'video/ogg', 'video/avi', 'video/mov', 'video/quicktime']
+        if mime_type in supported_videos:
+            return True, 'video'
+    
+    # Audio files
+    if mime_type.startswith('audio/'):
+        supported_audio = ['audio/mpeg', 'audio/mp3', 'audio/wav', 'audio/ogg', 'audio/webm', 'audio/aac', 'audio/flac']
+        if mime_type in supported_audio:
+            return True, 'audio'
+    
+    return False, None
+
 def parse_arguments():
     """Parse command line arguments"""
     parser = argparse.ArgumentParser(description='Flask Temporary File Upload Server')
@@ -360,6 +385,9 @@ def download_page(file_id):
     if is_archive_file_flag:
         archive_contents = get_archive_contents(full_path, mime_type)
     
+    # Check if it's a media file for preview
+    is_media_file_flag, media_type = is_media_file(mime_type, original_filename)
+    
     return render_template('download.html', 
                          file_id=file_id,
                          filename=original_filename,
@@ -369,7 +397,44 @@ def download_page(file_id):
                          is_text_file=is_text_file,
                          file_content=file_content,
                          is_archive_file=is_archive_file_flag,
-                         archive_contents=archive_contents)
+                         archive_contents=archive_contents,
+                         is_media_file=is_media_file_flag,
+                         media_type=media_type)
+
+@app.route('/preview/<file_id>')
+def preview_file(file_id):
+    """Serve media files for preview"""
+    conn = sqlite3.connect('files.db')
+    cursor = conn.cursor()
+    cursor.execute('''
+        SELECT original_filename, file_path, expiration_date, mime_type
+        FROM files 
+        WHERE id = ?
+    ''', (file_id,))
+    
+    file_data = cursor.fetchone()
+    conn.close()
+    
+    if not file_data:
+        abort(404)
+    
+    original_filename, file_path, expiration_date, mime_type = file_data
+    
+    # Check if file has expired
+    if datetime.fromisoformat(expiration_date) < datetime.now():
+        abort(410)
+    
+    # Check if file exists
+    full_path = os.path.join(app.config['UPLOAD_FOLDER'], file_path)
+    if not os.path.exists(full_path):
+        abort(404)
+    
+    # Only allow media files for preview
+    is_media_file_flag, media_type = is_media_file(mime_type, original_filename)
+    if not is_media_file_flag:
+        abort(403)
+    
+    return send_from_directory(app.config['UPLOAD_FOLDER'], file_path, mimetype=mime_type)
 
 @app.route('/download/<file_id>/direct')
 def download_file_direct(file_id):

+ 184 - 1
static/css/styles.css

@@ -76,4 +76,187 @@
     max-width: 180px;
     display: inline-block;
     vertical-align: bottom;
-} 
+}
+
+/* Custom styles for the Flask Temporary File Upload application */
+
+/* Dark mode compatibility */
+.dark .bg-white {
+    background-color: #1f2937;
+}
+
+.dark .text-gray-900 {
+    color: #f9fafb;
+}
+
+.dark .text-gray-600 {
+    color: #d1d5db;
+}
+
+.dark .border-gray-300 {
+    border-color: #4b5563;
+}
+
+/* Fancybox Dark Theme Customization */
+.fancybox__backdrop {
+    background: rgba(0, 0, 0, 0.9) !important;
+    backdrop-filter: blur(10px) !important;
+}
+
+.fancybox__container {
+    --fancybox-bg: transparent;
+    --fancybox-color: #ffffff;
+}
+
+.fancybox__content {
+    background: transparent !important;
+    border-radius: 12px !important;
+    overflow: hidden !important;
+    box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.8) !important;
+}
+
+.fancybox__image {
+    border-radius: 8px !important;
+    box-shadow: 0 20px 40px -12px rgba(0, 0, 0, 0.6) !important;
+}
+
+.fancybox__toolbar {
+    background: rgba(0, 0, 0, 0.8) !important;
+    backdrop-filter: blur(10px) !important;
+    border-radius: 8px !important;
+    margin: 10px !important;
+    border: 1px solid rgba(255, 255, 255, 0.1) !important;
+}
+
+.fancybox__button {
+    background: rgba(255, 255, 255, 0.1) !important;
+    border: 1px solid rgba(255, 255, 255, 0.2) !important;
+    color: white !important;
+    border-radius: 6px !important;
+    transition: all 0.2s ease !important;
+    margin: 2px !important;
+}
+
+.fancybox__button:hover {
+    background: rgba(255, 255, 255, 0.2) !important;
+    border-color: rgba(255, 255, 255, 0.4) !important;
+    transform: translateY(-1px) !important;
+    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3) !important;
+}
+
+.fancybox__button svg {
+    color: white !important;
+    filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.5)) !important;
+}
+
+.fancybox__infobar {
+    background: rgba(0, 0, 0, 0.8) !important;
+    color: white !important;
+    border-radius: 6px !important;
+    padding: 8px 12px !important;
+    margin: 10px !important;
+    backdrop-filter: blur(10px) !important;
+    border: 1px solid rgba(255, 255, 255, 0.1) !important;
+}
+
+.fancybox__caption {
+    background: rgba(0, 0, 0, 0.8) !important;
+    color: white !important;
+    border-radius: 0 0 12px 12px !important;
+    backdrop-filter: blur(10px) !important;
+    border-top: 1px solid rgba(255, 255, 255, 0.1) !important;
+    text-align: center !important;
+    font-size: 14px !important;
+    font-weight: 500 !important;
+    line-height: 1.4 !important;
+}
+
+.fancybox__nav .fancybox__button {
+    background: rgba(255, 255, 255, 0.1) !important;
+    border-radius: 50% !important;
+    width: 48px !important;
+    height: 48px !important;
+    backdrop-filter: blur(10px) !important;
+}
+
+.fancybox__nav .fancybox__button:hover {
+    background: rgba(255, 255, 255, 0.2) !important;
+    transform: scale(1.1) !important;
+}
+
+/* Dark mode specific adjustments */
+.dark .fancybox__backdrop {
+    background: rgba(0, 0, 0, 0.95) !important;
+}
+
+.dark .fancybox__toolbar {
+    background: rgba(17, 24, 39, 0.9) !important;
+    border-color: rgba(75, 85, 99, 0.3) !important;
+}
+
+.dark .fancybox__infobar {
+    background: rgba(17, 24, 39, 0.9) !important;
+    border-color: rgba(75, 85, 99, 0.3) !important;
+}
+
+.dark .fancybox__caption {
+    background: rgba(17, 24, 39, 0.9) !important;
+    border-top-color: rgba(75, 85, 99, 0.3) !important;
+}
+
+/* Responsive adjustments */
+@media (max-width: 768px) {
+    .fancybox__nav .fancybox__button {
+        width: 44px !important;
+        height: 44px !important;
+    }
+    
+    .fancybox__toolbar {
+        margin: 5px !important;
+        padding: 5px !important;
+    }
+    
+    .fancybox__button {
+        padding: 6px !important;
+        margin: 1px !important;
+    }
+}
+
+/* Image hover effects */
+.image-preview-container {
+    position: relative;
+    overflow: hidden;
+    border-radius: 0.5rem;
+}
+
+.image-preview-overlay {
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    background: linear-gradient(45deg, rgba(79, 70, 229, 0.1), rgba(6, 182, 212, 0.1));
+    opacity: 0;
+    transition: opacity 0.3s ease;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    border-radius: 0.5rem;
+}
+
+.image-preview-container:hover .image-preview-overlay {
+    opacity: 1;
+}
+
+.zoom-icon {
+    background: rgba(255, 255, 255, 0.9);
+    border-radius: 50%;
+    padding: 0.5rem;
+    box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
+    transform: scale(0.8);
+    transition: transform 0.2s ease;
+}
+
+.image-preview-container:hover .zoom-icon {
+    transform: scale(1);
+}

+ 226 - 2
templates/download.html

@@ -9,6 +9,9 @@
     <link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
     
     <link rel="stylesheet" href="/static/css/styles.css">
+    <!-- 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>
     <script src="https://cdn.tailwindcss.com"></script>
     <script>
         (function() {
@@ -99,11 +102,19 @@
         <div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 transition-colors duration-300">
             <div class="flex items-center justify-between mb-4">
                 <h3 class="text-lg font-semibold text-gray-900 dark:text-white">File Preview</h3>
-                <span class="text-sm text-gray-500 dark:text-gray-400">Text file preview</span>
+                <div class="flex items-center space-x-2">
+                    <button onclick="copyTextToClipboard()" class="inline-flex items-center px-3 py-1 text-xs font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md transition-colors duration-200">
+                        <svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"></path>
+                        </svg>
+                        Copy Text
+                    </button>
+                    <span class="text-sm text-gray-500 dark:text-gray-400">Text file preview</span>
+                </div>
             </div>
             
             <div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-4 overflow-auto max-h-96">
-                <pre class="text-sm text-gray-800 dark:text-gray-200 whitespace-pre-wrap font-mono">{{ file_content }}</pre>
+                <pre id="file-content" class="text-sm text-gray-800 dark:text-gray-200 whitespace-pre-wrap font-mono">{{ file_content }}</pre>
             </div>
             
             <div class="mt-4 text-xs text-gray-500 dark:text-gray-400">
@@ -120,6 +131,66 @@
                 <p class="text-gray-600 dark:text-gray-400">This text file couldn't be previewed. Please download it to view the content.</p>
             </div>
         </div>
+        <!-- Media File Preview -->
+        {% elif is_media_file %}
+        <div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 transition-colors duration-300">
+            <div class="flex items-center justify-between mb-4">
+                <h3 class="text-lg font-semibold text-gray-900 dark:text-white">Media Preview</h3>
+                <span class="text-sm text-gray-500 dark:text-gray-400">{{ media_type|title }} file preview</span>
+            </div>
+            
+            <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>
+                     <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>
+                          <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">
+                                      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3M3 17V7a2 2 0 012-2h6l2 2h6a2 2 0 012 2v10a2 2 0 01-2 2H5a2 2 0 01-2-2z"></path>
+                                  </svg>
+                                  Save Image
+                              </button>
+                              <button onclick="showImageInfo('{{ filename }}', '{{ file_size }}', '{{ mime_type }}')" class="inline-flex items-center px-2 py-1 text-xs font-medium text-white bg-blue-600 hover:bg-blue-700 rounded transition-colors duration-200">
+                                  <svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                                      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
+                                  </svg>
+                                  Info
+                              </button>
+                          </div>
+                      </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>
+                {% elif media_type == 'audio' %}
+                <div class="w-full max-w-md">
+                    <div class="flex items-center space-x-4 mb-4">
+                        <svg class="w-12 h-12 text-blue-500" fill="currentColor" viewBox="0 0 20 20">
+                            <path fill-rule="evenodd" d="M9.383 3.076A1 1 0 0110 4v12a1 1 0 01-1.707.707L4.586 13H2a1 1 0 01-1-1V8a1 1 0 011-1h2.586l3.707-3.707a1 1 0 011.09-.217zM15.657 6.343a1 1 0 011.414 0A9.972 9.972 0 0119 12a9.972 9.972 0 01-1.929 5.657 1 1 0 11-1.414-1.414A7.971 7.971 0 0017 12c0-2.21-.895-4.21-2.343-5.657a1 1 0 010-1.414zm-2.829 2.828a1 1 0 011.415 0A5.983 5.983 0 0115 12a5.983 5.983 0 01-.757 2.829 1 1 0 01-1.415-1.415A3.987 3.987 0 0013 12a3.987 3.987 0 00-.172-1.414 1 1 0 010-1.415z" 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">Audio file</p>
+                        </div>
+                    </div>
+                    <audio controls class="w-full">
+                        <source src="/preview/{{ file_id }}" type="{{ mime_type }}">
+                        Your browser does not support the audio tag.
+                    </audio>
+                </div>
+                {% endif %}
+            </div>
+            
+            <div class="mt-4 text-xs text-gray-500 dark:text-gray-400">
+                <p>Media preview is displayed above. Download the file for full quality or offline viewing.</p>
+            </div>
+        </div>
         <!-- Archive File Preview -->
         {% elif is_archive_file and archive_contents %}
         <div class="bg-white dark:bg-gray-800 rounded-lg shadow-md p-6 transition-colors duration-300">
@@ -241,11 +312,164 @@
             }, 3000);
         }
 
+        // Copy text to clipboard
+        function copyTextToClipboard() {
+            const fileContent = document.getElementById('file-content');
+            if (!fileContent) {
+                showToast('No content to copy', 'error');
+                return;
+            }
+            
+            const textToCopy = fileContent.textContent || fileContent.innerText;
+            
+            if (navigator.clipboard && window.isSecureContext) {
+                // Use modern clipboard API
+                navigator.clipboard.writeText(textToCopy).then(() => {
+                    showToast('Text copied to clipboard!', 'success');
+                }).catch(err => {
+                    console.error('Failed to copy text: ', err);
+                    fallbackCopyTextToClipboard(textToCopy);
+                });
+            } else {
+                // Fallback for older browsers
+                fallbackCopyTextToClipboard(textToCopy);
+            }
+        }
+        
+        function fallbackCopyTextToClipboard(text) {
+             const textArea = document.createElement('textarea');
+             textArea.value = text;
+             textArea.style.position = 'fixed';
+             textArea.style.left = '-999999px';
+             textArea.style.top = '-999999px';
+             document.body.appendChild(textArea);
+             textArea.focus();
+             textArea.select();
+             
+             try {
+                 const successful = document.execCommand('copy');
+                 if (successful) {
+                     showToast('Text copied to clipboard!', 'success');
+                 } else {
+                     showToast('Failed to copy text', 'error');
+                 }
+             } catch (err) {
+                 console.error('Fallback: Could not copy text: ', err);
+                 showToast('Failed to copy text', 'error');
+             }
+             
+             document.body.removeChild(textArea);
+         }
+         
+         // Image functions
+         function downloadImage(fileId, filename) {
+             const link = document.createElement('a');
+             link.href = `/download/${fileId}/direct`;
+             link.download = filename;
+             document.body.appendChild(link);
+             link.click();
+             document.body.removeChild(link);
+             showToast('Image download started!', 'success');
+         }
+         
+         function showImageInfo(filename, fileSize, mimeType) {
+             const sizeInKB = Math.round(fileSize / 1024);
+             const sizeInMB = (fileSize / (1024 * 1024)).toFixed(2);
+             const sizeText = fileSize > 1024 * 1024 ? `${sizeInMB} MB` : `${sizeInKB} KB`;
+             
+             const infoMessage = `File: ${filename}\nSize: ${sizeText}\nType: ${mimeType}`;
+             
+             // Create a custom modal for image info
+             const modal = document.createElement('div');
+             modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50';
+             modal.innerHTML = `
+                 <div class="bg-white dark:bg-gray-800 rounded-lg p-6 max-w-md mx-4 shadow-xl">
+                     <div class="flex items-center justify-between mb-4">
+                         <h3 class="text-lg font-semibold text-gray-900 dark:text-white">Image Information</h3>
+                         <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 class="space-y-3">
+                         <div>
+                             <span class="text-sm font-medium text-gray-500 dark:text-gray-400">Filename:</span>
+                             <p class="text-gray-900 dark:text-white break-all">${filename}</p>
+                         </div>
+                         <div>
+                             <span class="text-sm font-medium text-gray-500 dark:text-gray-400">File Size:</span>
+                             <p class="text-gray-900 dark:text-white">${sizeText}</p>
+                         </div>
+                         <div>
+                             <span class="text-sm font-medium text-gray-500 dark:text-gray-400">MIME Type:</span>
+                             <p class="text-gray-900 dark:text-white">${mimeType}</p>
+                         </div>
+                     </div>
+                     <div class="mt-6 flex justify-end">
+                         <button onclick="this.closest('.fixed').remove()" class="px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700 transition-colors duration-200">
+                             Close
+                         </button>
+                     </div>
+                 </div>
+             `;
+             
+             document.body.appendChild(modal);
+             
+             // Close modal when clicking outside
+             modal.addEventListener('click', function(e) {
+                 if (e.target === modal) {
+                     modal.remove();
+                 }
+             });
+         }
+
         // Initialize
         document.addEventListener('DOMContentLoaded', function() {
             initTheme();
             document.getElementById('themeToggle').addEventListener('click', toggleTheme);
         });
     </script>
+    
+    <!-- Fancybox Configuration -->
+    <script>
+        // Initialize Fancybox
+        document.addEventListener('DOMContentLoaded', function() {
+            if (typeof Fancybox !== 'undefined') {
+                Fancybox.bind('[data-fancybox]', {
+                    // UI options
+                    animated: true,
+                    showClass: 'f-fadeIn',
+                    hideClass: 'f-fadeOut',
+                    
+                    // Toolbar options
+                    Toolbar: {
+                        display: {
+                            left: ['infobar'],
+                            middle: ['zoomIn', 'zoomOut', 'toggle1to1', 'rotateCCW', 'rotateCW', 'flipX', 'flipY'],
+                            right: ['slideshow', 'fullscreen', 'thumbs', 'close']
+                        }
+                    },
+                    
+                    // Image options
+                    Images: {
+                        zoom: true,
+                        wheel: 'zoom'
+                    },
+                    
+                    // Thumbs options
+                    Thumbs: {
+                        showOnStart: false
+                    },
+                    
+                    // Slideshow options
+                    Slideshow: {
+                        speed: 3000
+                    }
+                });
+                console.log('Fancybox initialized successfully');
+            }
+        });
+    </script>
 </body>
 </html>