Skip to content

Instantly share code, notes, and snippets.

@frankmeeuwsen
Last active June 29, 2025 10:19
Show Gist options
  • Save frankmeeuwsen/bc8e54ce301dcaba785d73f2a4ec057d to your computer and use it in GitHub Desktop.
Save frankmeeuwsen/bc8e54ce301dcaba785d73f2a4ec057d to your computer and use it in GitHub Desktop.

📸 Screenshot Manager App


👤 Gebruiker (29 juni 2025, 10:52 AM)

Ik mijn map ~/Downloads staan allerlei bestanden en vooral veel screenshots. Kan ik een lokale webpagina of app maken waarmee ik al die screenshots zie als thumbnail en ze direct kan verwijderen als ik op een knop druk?


🤖 Assistent (29 juni 2025, 10:52 AM)

Ja, dat kan zeker! Ik zal je een eenvoudige lokale webapplicatie maken waarmee je screenshots uit je Downloads map kunt bekijken en verwijderen. Hier is een oplossing met HTML, CSS en JavaScript met een Python backend:

Python Backend (Flask)

Maak eerst een bestand app.py:

from flask import Flask, render_template, send_file, jsonify, request
import os
from pathlib import Path
import mimetypes
from PIL import Image
import io
import base64

app = Flask(__name__)

# Path naar Downloads folder
DOWNLOADS_PATH = Path.home() / "Downloads"

# Ondersteunde afbeeldingsformaten
IMAGE_EXTENSIONS = {'.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp', '.tiff'}

def is_image(filepath):
    """Check of bestand een afbeelding is"""
    return filepath.suffix.lower() in IMAGE_EXTENSIONS

def get_images():
    """Haal alle afbeeldingen uit Downloads folder"""
    images = []
    for file in DOWNLOADS_PATH.iterdir():
        if file.is_file() and is_image(file):
            images.append({
                'name': file.name,
                'path': str(file),
                'size': file.stat().st_size,
                'modified': file.stat().st_mtime
            })
    # Sorteer op wijzigingsdatum (nieuwste eerst)
    images.sort(key=lambda x: x['modified'], reverse=True)
    return images

@app.route('/')
def index():
    return render_template('index.html')

@app.route('/api/images')
def get_images_list():
    """API endpoint om lijst met afbeeldingen te krijgen"""
    images = get_images()
    return jsonify(images)

@app.route('/api/thumbnail/<path:filename>')
def get_thumbnail(filename):
    """Genereer en stuur thumbnail"""
    filepath = DOWNLOADS_PATH / filename
    
    if not filepath.exists() or not filepath.is_file():
        return "File not found", 404
    
    try:
        # Open afbeelding en maak thumbnail
        img = Image.open(filepath)
        img.thumbnail((300, 300))
        
        # Converteer naar bytes
        img_io = io.BytesIO()
        img.save(img_io, 'PNG')
        img_io.seek(0)
        
        return send_file(img_io, mimetype='image/png')
    except Exception as e:
        return f"Error: {str(e)}", 500

@app.route('/api/delete/<path:filename>', methods=['DELETE'])
def delete_image(filename):
    """Verwijder afbeelding"""
    filepath = DOWNLOADS_PATH / filename
    
    if not filepath.exists() or not filepath.is_file():
        return jsonify({'error': 'File not found'}), 404
    
    try:
        filepath.unlink()  # Verwijder bestand
        return jsonify({'success': True, 'message': f'{filename} verwijderd'})
    except Exception as e:
        return jsonify({'error': str(e)}), 500

if __name__ == '__main__':
    app.run(debug=True, port=5000)

HTML Frontend

Maak een map templates en daarin een bestand index.html:

<!DOCTYPE html>
<html lang="nl">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Downloads Screenshot Manager</title>
    <style>
        * {
            box-sizing: border-box;
            margin: 0;
            padding: 0;
        }
        
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            background-color: #f5f5f5;
            padding: 20px;
        }
        
        .header {
            text-align: center;
            margin-bottom: 30px;
        }
        
        h1 {
            color: #333;
            margin-bottom: 10px;
        }
        
        .stats {
            color: #666;
            font-size: 14px;
        }
        
        .controls {
            margin-bottom: 20px;
            text-align: center;
        }
        
        .btn {
            background-color: #007bff;
            color: white;
            border: none;
            padding: 10px 20px;
            border-radius: 5px;
            cursor: pointer;
            font-size: 14px;
            margin: 0 5px;
        }
        
        .btn:hover {
            background-color: #0056b3;
        }
        
        .btn-danger {
            background-color: #dc3545;
        }
        
        .btn-danger:hover {
            background-color: #c82333;
        }
        
        .gallery {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
            gap: 20px;
        }
        
        .image-card {
            background: white;
            border-radius: 8px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
            overflow: hidden;
            transition: transform 0.2s, box-shadow 0.2s;
        }
        
        .image-card:hover {
            transform: translateY(-2px);
            box-shadow: 0 4px 8px rgba(0,0,0,0.15);
        }
        
        .image-container {
            position: relative;
            padding-bottom: 75%; /* 4:3 aspect ratio */
            overflow: hidden;
            background: #f0f0f0;
        }
        
        .image-container img {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            object-fit: cover;
            cursor: pointer;
        }
        
        .image-info {
            padding: 12px;
        }
        
        .filename {
            font-size: 14px;
            color: #333;
            font-weight: 500;
            margin-bottom: 5px;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
        }
        
        .file-size {
            font-size: 12px;
            color: #666;
        }
        
        .image-actions {
            padding: 0 12px 12px;
            display: flex;
            gap: 10px;
        }
        
        .loading {
            text-align: center;
            padding: 50px;
            color: #666;
        }
        
        .modal {
            display: none;
            position: fixed;
            z-index: 1000;
            left: 0;
            top: 0;
            width: 100%;
            height: 100%;
            background-color: rgba(0,0,0,0.9);
        }
        
        .modal-content {
            margin: auto;
            display: block;
            max-width: 90%;
            max-height: 90%;
            margin-top: 50px;
        }
        
        .close {
            position: absolute;
            top: 15px;
            right: 35px;
            color: #f1f1f1;
            font-size: 40px;
            font-weight: bold;
            cursor: pointer;
        }
        
        .close:hover {
            color: #bbb;
        }
        
        .no-images {
            text-align: center;
            padding: 50px;
            color: #666;
        }
        
        .selected {
            outline: 3px solid #007bff;
        }
        
        .checkbox-container {
            position: absolute;
            top: 10px;
            right: 10px;
            background: white;
            border-radius: 4px;
            padding: 5px;
        }
        
        .refresh-btn {
            background-color: #28a745;
        }
        
        .refresh-btn:hover {
            background-color: #218838;
        }
    </style>
</head>
<body>
    <div class="header">
        <h1>📸 Downloads Screenshot Manager</h1>
        <p class="stats">
            <span id="imageCount">0</span> afbeeldingen gevonden
        </p>
    </div>
    
    <div class="controls">
        <button class="btn refresh-btn" onclick="loadImages()">🔄 Vernieuwen</button>
        <button class="btn btn-danger" id="deleteSelectedBtn" onclick="deleteSelected()" style="display:none;">
            🗑️ Verwijder geselecteerde (<span id="selectedCount">0</span>)
        </button>
    </div>
    
    <div id="loading" class="loading">Afbeeldingen laden...</div>
    <div id="gallery" class="gallery" style="display:none;"></div>
    <div id="noImages" class="no-images" style="display:none;">
        <p>Geen afbeeldingen gevonden in ~/Downloads</p>
    </div>
    
    <!-- Modal voor grote afbeelding -->
    <div id="imageModal" class="modal">
        <span class="close" onclick="closeModal()">&times;</span>
        <img class="modal-content" id="modalImage">
    </div>
    
    <script>
        let images = [];
        let selectedImages = new Set();
        
        // Laad afbeeldingen bij het laden van de pagina
        window.onload = () => {
            loadImages();
        };
        
        function loadImages() {
            document.getElementById('loading').style.display = 'block';
            document.getElementById('gallery').style.display = 'none';
            document.getElementById('noImages').style.display = 'none';
            selectedImages.clear();
            updateSelectedCount();
            
            fetch('/api/images')
                .then(response => response.json())
                .then(data => {
                    images = data;
                    displayImages();
                })
                .catch(error => {
                    console.error('Error loading images:', error);
                    alert('Fout bij het laden van afbeeldingen');
                });
        }
        
        function displayImages() {
            const gallery = document.getElementById('gallery');
            const loading = document.getElementById('loading');
            const noImages = document.getElementById('noImages');
            const imageCount = document.getElementById('imageCount');
            
            loading.style.display = 'none';
            
            if (images.length === 0) {
                noImages.style.display = 'block';
                gallery.style.display = 'none';
                imageCount.textContent = '0';
                return;
            }
            
            imageCount.textContent = images.length;
            gallery.innerHTML = '';
            gallery.style.display = 'grid';
            
            images.forEach((image, index) => {
                const card = createImageCard(image, index);
                gallery.appendChild(card);
            });
        }
        
        function createImageCard(image, index) {
            const card = document.createElement('div');
            card.className = 'image-card';
            card.id = `card-${index}`;
            
            const fileSize = formatFileSize(image.size);
            
            card.innerHTML = `
                <div class="image-container">
                    <img src="/api/thumbnail/${encodeURIComponent(image.name)}" 
                         alt="${image.name}"
                         loading="lazy"
                         onclick="showModal('${encodeURIComponent(image.name)}')">
                    <div class="checkbox-container">
                        <input type="checkbox" id="check-${index}" 
                               onchange="toggleSelection(${index}, '${image.name}')">
                    </div>
                </div>
                <div class="image-info">
                    <div class="filename" title="${image.name}">${image.name}</div>
                    <div class="file-size">${fileSize}</div>
                </div>
                <div class="image-actions">
                    <button class="btn btn-danger" onclick="deleteImage('${image.name}', ${index})">
                        🗑️ Verwijder
                    </button>
                </div>
            `;
            
            return card;
        }
        
        function toggleSelection(index, filename) {
            const checkbox = document.getElementById(`check-${index}`);
            const card = document.getElementById(`card-${index}`);
            
            if (checkbox.checked) {
                selectedImages.add(filename);
                card.classList.add('selected');
            } else {
                selectedImages.delete(filename);
                card.classList.remove('selected');
            }
            
            updateSelectedCount();
        }
        
        function updateSelectedCount() {
            const count = selectedImages.size;
            document.getElementById('selectedCount').textContent = count;
            document.getElementById('deleteSelectedBtn').style.display = 
                count > 0 ? 'inline-block' : 'none';
        }
        
        function deleteSelected() {
            if (selectedImages.size === 0) return;
            
            if (!confirm(`Weet je zeker dat je ${selectedImages.size} afbeelding(en) wilt verwijderen?`)) {
                return;
            }
            
            const promises = Array.from(selectedImages).map(filename => 
                fetch(`/api/delete/${encodeURIComponent(filename)}`, { method: 'DELETE' })
            );
            
            Promise.all(promises)
                .then(() => {
                    alert(`${selectedImages.size} afbeelding(en) verwijderd`);
                    loadImages();
                })
                .catch(error => {
                    console.error('Error deleting images:', error);
                    alert('Fout bij het verwijderen van sommige afbeeldingen');
                    loadImages();
                });
        }
        
        function deleteImage(filename, index) {
            if (!confirm(`Weet je zeker dat je "${filename}" wilt verwijderen?`)) {
                return;
            }
            
            fetch(`/api/delete/${encodeURIComponent(filename)}`, {
                method: 'DELETE'
            })
            .then(response => response.json())
            .then(data => {
                if (data.success) {
                    // Verwijder uit array en update display
                    images.splice(index, 1);
                    displayImages();
                } else {
                    alert('Fout bij verwijderen: ' + data.error);
                }
            })
            .catch(error => {
                console.error('Error:', error);
                alert('Fout bij verwijderen van bestand');
            });
        }
        
        function formatFileSize(bytes) {
            if (bytes < 1024) return bytes + ' B';
            if (bytes < 1024 * 1024) return Math.round(bytes / 1024) + ' KB';
            return Math.round(bytes / (1024 * 1024) * 10) / 10 + ' MB';
        }
        
        function showModal(filename) {
            const modal = document.getElementById('imageModal');
            const modalImage = document.getElementById('modalImage');
            modalImage.src = `/api/thumbnail/${filename}`;
            modal.style.display = 'block';
        }
        
        function closeModal() {
            document.getElementById('imageModal').style.display = 'none';
        }
        
        // Sluit modal bij klik buiten afbeelding
        window.onclick = function(event) {
            const modal = document.getElementById('imageModal');
            if (event.target === modal) {
                closeModal();
            }
        }
        
        // Keyboard shortcuts
        document.addEventListener('keydown', (e) => {
            if (e.key === 'Escape') {
                closeModal();
            }
            if (e.key === 'F5' || (e.ctrlKey && e.key === 'r')) {
                e.preventDefault();
                loadImages();
            }
        });
    </script>
</body>
</html>

Installatie en gebruik

  1. Installeer benodigde Python packages:
pip install flask pillow
  1. Maak de projectstructuur:
mkdir screenshot-manager
cd screenshot-manager
mkdir templates
  1. Kopieer de code:
  • Sla app.py op in de hoofdmap
  • Sla index.html op in de templates map
  1. Start de applicatie:
python app.py
  1. Open in je browser:
  • Ga naar http://localhost:5000

Features

  • 📸 Thumbnail preview van alle afbeeldingen
  • 🗑️ Direct verwijderen met één klik
  • Bulk selectie voor meerdere afbeeldingen tegelijk
  • 🔍 Klik op thumbnail voor grotere weergave
  • 🔄 Vernieuw knop om nieuwe screenshots te laden
  • 📏 Bestandsgrootte weergave
  • 📅 Gesorteerd op wijzigingsdatum (nieuwste eerst)

Veiligheidsnotitie

Deze applicatie is bedoeld voor lokaal gebruik. Als je hem toegankelijk wilt maken voor andere apparaten op je netwerk, pas dan de laatste regel in app.py aan:

app.run(debug=False, host='0.0.0.0', port=5000)

Wees voorzichtig met het delen van deze app omdat het directe toegang geeft tot het verwijderen van bestanden!


👤 Gebruiker (29 juni 2025, 10:58 AM)

pip3 install flask pillow  1 ↵  1007  10:57:21 Requirement already satisfied: flask in /Users/frank/mijn-project-venv/lib/python3.13/site-packages (3.1.1) Requirement already satisfied: pillow in /Users/frank/mijn-project-venv/lib/python3.13/site-packages (11.2.1) Requirement already satisfied: blinker>=1.9.0 in /Users/frank/mijn-project-venv/lib/python3.13/site-packages (from flask) (1.9.0) Requirement already satisfied: click>=8.1.3 in /Users/frank/mijn-project-venv/lib/python3.13/site-packages (from flask) (8.2.1) Requirement already satisfied: itsdangerous>=2.2.0 in /Users/frank/mijn-project-venv/lib/python3.13/site-packages (from flask) (2.2.0) Requirement already satisfied: jinja2>=3.1.2 in /Users/frank/mijn-project-venv/lib/python3.13/site-packages (from flask) (3.1.6) Requirement already satisfied: markupsafe>=2.1.1 in /Users/frank/mijn-project-venv/lib/python3.13/site-packages (from flask) (3.0.2) Requirement already satisfied: werkzeug>=3.1.0 in /Users/frank/mijn-project-venv/lib/python3.13/site-packages (from flask) (3.1.3) [notice] A new release of pip is available: 25.0.1 -> 25.1.1 [notice] To update, run: pip install --upgrade pip frank@Mac  ~/Documents/Hobbies/scripts/downloads-screenshots  python app.py  ✔  1008  10:57:38 Traceback (most recent call last): File "/Users/frank/Documents/Hobbies/scripts/downloads-screenshots/app.py", line 1, in from flask import Flask, render_template, send_file, jsonify, request ModuleNotFoundError: No module named 'flask'


🤖 Assistent (29 juni 2025, 10:58 AM)

Het lijkt erop dat je Flask hebt geïnstalleerd in een virtual environment (mijn-project-venv), maar je probeert de app te draaien zonder die virtual environment geactiveerd te hebben. Hier zijn een paar oplossingen:

Optie 1: Activeer je bestaande virtual environment

source ~/mijn-project-venv/bin/activate
cd ~/Documents/Hobbies/scripts/downloads-screenshots
python app.py

Optie 2: Maak een nieuwe virtual environment in je project directory

cd ~/Documents/Hobbies/scripts/downloads-screenshots
python3 -m venv venv
source venv/bin/activate
pip install flask pillow
python app.py

Optie 3: Installeer Flask globaal (niet aanbevolen)

pip3 install --user flask pillow
python3 app.py

Optie 4: Gebruik de Python interpreter van je virtual environment direct

cd ~/Documents/Hobbies/scripts/downloads-screenshots
~/mijn-project-venv/bin/python app.py

Ik raad Optie 2 aan zodat je een dedicated virtual environment hebt voor dit project. Dit houdt je dependencies netjes gescheiden.

Na het activeren van de virtual environment zou je prompt moeten veranderen naar iets als:

(venv) frank@Mac ~/Documents/Hobbies/scripts/downloads-screenshots $

Dan kun je de app starten:

python app.py

En je zou moeten zien:

 * Running on http://127.0.0.1:5000
 * Debug mode: on

Open dan http://localhost:5000 in je browser om de app te gebruiken!


👤 Gebruiker (29 juni 2025, 10:59 AM)

Het werkt nu. Zou ik de selectie van de images ook met het toetsenbord kunnen doen? Dus met pijlen navigeren en met spatiebalk selectie togglen?


🤖 Assistent (29 juni 2025, 11:00 AM)

Ja, dat is een geweldige toevoeging! Ik zal keyboard navigatie toevoegen aan de applicatie. Hier is de aangepaste index.html met volledige toetsenbord ondersteuning:

<!DOCTYPE html>
<html lang="nl">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Downloads Screenshot Manager</title>
    <style>
        * {
            box-sizing: border-box;
            margin: 0;
            padding: 0;
        }
        
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            background-color: #f5f5f5;
            padding: 20px;
        }
        
        .header {
            text-align: center;
            margin-bottom: 30px;
        }
        
        h1 {
            color: #333;
            margin-bottom: 10px;
        }
        
        .stats {
            color: #666;
            font-size: 14px;
        }
        
        .controls {
            margin-bottom: 20px;
            text-align: center;
        }
        
        .btn {
            background-color: #007bff;
            color: white;
            border: none;
            padding: 10px 20px;
            border-radius: 5px;
            cursor: pointer;
            font-size: 14px;
            margin: 0 5px;
        }
        
        .btn:hover {
            background-color: #0056b3;
        }
        
        .btn-danger {
            background-color: #dc3545;
        }
        
        .btn-danger:hover {
            background-color: #c82333;
        }
        
        .gallery {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
            gap: 20px;
        }
        
        .image-card {
            background: white;
            border-radius: 8px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
            overflow: hidden;
            transition: transform 0.2s, box-shadow 0.2s;
            position: relative;
        }
        
        .image-card:hover {
            transform: translateY(-2px);
            box-shadow: 0 4px 8px rgba(0,0,0,0.15);
        }
        
        .image-card.focused {
            outline: 3px solid #007bff;
            outline-offset: 2px;
        }
        
        .image-card.selected {
            background-color: #e7f3ff;
        }
        
        .image-container {
            position: relative;
            padding-bottom: 75%; /* 4:3 aspect ratio */
            overflow: hidden;
            background: #f0f0f0;
        }
        
        .image-container img {
            position: absolute;
            top: 0;
            left: 0;
            width: 100%;
            height: 100%;
            object-fit: cover;
            cursor: pointer;
        }
        
        .image-info {
            padding: 12px;
        }
        
        .filename {
            font-size: 14px;
            color: #333;
            font-weight: 500;
            margin-bottom: 5px;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
        }
        
        .file-size {
            font-size: 12px;
            color: #666;
        }
        
        .image-actions {
            padding: 0 12px 12px;
            display: flex;
            gap: 10px;
        }
        
        .loading {
            text-align: center;
            padding: 50px;
            color: #666;
        }
        
        .modal {
            display: none;
            position: fixed;
            z-index: 1000;
            left: 0;
            top: 0;
            width: 100%;
            height: 100%;
            background-color: rgba(0,0,0,0.9);
        }
        
        .modal-content {
            margin: auto;
            display: block;
            max-width: 90%;
            max-height: 90%;
            margin-top: 50px;
        }
        
        .close {
            position: absolute;
            top: 15px;
            right: 35px;
            color: #f1f1f1;
            font-size: 40px;
            font-weight: bold;
            cursor: pointer;
        }
        
        .close:hover {
            color: #bbb;
        }
        
        .no-images {
            text-align: center;
            padding: 50px;
            color: #666;
        }
        
        .checkbox-container {
            position: absolute;
            top: 10px;
            right: 10px;
            background: white;
            border-radius: 4px;
            padding: 5px;
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
        }
        
        .refresh-btn {
            background-color: #28a745;
        }
        
        .refresh-btn:hover {
            background-color: #218838;
        }
        
        .keyboard-hint {
            background: #f8f9fa;
            border: 1px solid #dee2e6;
            border-radius: 5px;
            padding: 15px;
            margin: 20px auto;
            max-width: 800px;
            font-size: 13px;
            color: #666;
        }
        
        .keyboard-hint h3 {
            color: #333;
            margin-bottom: 10px;
            font-size: 14px;
        }
        
        .keyboard-hint kbd {
            background-color: #eee;
            border: 1px solid #ccc;
            border-radius: 3px;
            padding: 2px 6px;
            font-family: monospace;
            font-size: 12px;
            box-shadow: 0 1px 0 rgba(0,0,0,0.2);
        }
        
        .hint-grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
            gap: 10px;
            margin-top: 10px;
        }
    </style>
</head>
<body>
    <div class="header">
        <h1>📸 Downloads Screenshot Manager</h1>
        <p class="stats">
            <span id="imageCount">0</span> afbeeldingen gevonden
        </p>
    </div>
    
    <div class="controls">
        <button class="btn refresh-btn" onclick="loadImages()">🔄 Vernieuwen</button>
        <button class="btn btn-danger" id="deleteSelectedBtn" onclick="deleteSelected()" style="display:none;">
            🗑️ Verwijder geselecteerde (<span id="selectedCount">0</span>)
        </button>
    </div>
    
    <div class="keyboard-hint">
        <h3>⌨️ Keyboard shortcuts:</h3>
        <div class="hint-grid">
            <div><kbd></kbd> <kbd></kbd> <kbd></kbd> <kbd></kbd> Navigeer</div>
            <div><kbd>Spatie</kbd> Selecteer/deselecteer</div>
            <div><kbd>Enter</kbd> Open afbeelding</div>
            <div><kbd>Delete</kbd> Verwijder huidige</div>
            <div><kbd>A</kbd> Selecteer alles</div>
            <div><kbd>D</kbd> Deselecteer alles</div>
            <div><kbd>Shift + Delete</kbd> Verwijder geselecteerde</div>
            <div><kbd>Esc</kbd> Sluit preview</div>
        </div>
    </div>
    
    <div id="loading" class="loading">Afbeeldingen laden...</div>
    <div id="gallery" class="gallery" style="display:none;"></div>
    <div id="noImages" class="no-images" style="display:none;">
        <p>Geen afbeeldingen gevonden in ~/Downloads</p>
    </div>
    
    <!-- Modal voor grote afbeelding -->
    <div id="imageModal" class="modal">
        <span class="close" onclick="closeModal()">&times;</span>
        <img class="modal-content" id="modalImage">
    </div>
    
    <script>
        let images = [];
        let selectedImages = new Set();
        let currentFocusIndex = -1;
        let isModalOpen = false;
        
        // Laad afbeeldingen bij het laden van de pagina
        window.onload = () => {
            loadImages();
        };
        
        function loadImages() {
            document.getElementById('loading').style.display = 'block';
            document.getElementById('gallery').style.display = 'none';
            document.getElementById('noImages').style.display = 'none';
            selectedImages.clear();
            currentFocusIndex = -1;
            updateSelectedCount();
            
            fetch('/api/images')
                .then(response => response.json())
                .then(data => {
                    images = data;
                    displayImages();
                })
                .catch(error => {
                    console.error('Error loading images:', error);
                    alert('Fout bij het laden van afbeeldingen');
                });
        }
        
        function displayImages() {
            const gallery = document.getElementById('gallery');
            const loading = document.getElementById('loading');
            const noImages = document.getElementById('noImages');
            const imageCount = document.getElementById('imageCount');
            
            loading.style.display = 'none';
            
            if (images.length === 0) {
                noImages.style.display = 'block';
                gallery.style.display = 'none';
                imageCount.textContent = '0';
                return;
            }
            
            imageCount.textContent = images.length;
            gallery.innerHTML = '';
            gallery.style.display = 'grid';
            
            images.forEach((image, index) => {
                const card = createImageCard(image, index);
                gallery.appendChild(card);
            });
            
            // Focus eerste item als er nog geen focus is
            if (currentFocusIndex === -1 && images.length > 0) {
                setFocus(0);
            }
        }
        
        function createImageCard(image, index) {
            const card = document.createElement('div');
            card.className = 'image-card';
            card.id = `card-${index}`;
            card.dataset.index = index;
            
            const fileSize = formatFileSize(image.size);
            const isSelected = selectedImages.has(image.name);
            
            if (isSelected) {
                card.classList.add('selected');
            }
            
            card.innerHTML = `
                <div class="image-container">
                    <img src="/api/thumbnail/${encodeURIComponent(image.name)}" 
                         alt="${image.name}"
                         loading="lazy"
                         onclick="showModal('${encodeURIComponent(image.name)}')">
                    <div class="checkbox-container">
                        <input type="checkbox" id="check-${index}" 
                               ${isSelected ? 'checked' : ''}
                               onchange="toggleSelection(${index}, '${image.name}')">
                    </div>
                </div>
                <div class="image-info">
                    <div class="filename" title="${image.name}">${image.name}</div>
                    <div class="file-size">${fileSize}</div>
                </div>
                <div class="image-actions">
                    <button class="btn btn-danger" onclick="deleteImage('${image.name}', ${index})">
                        🗑️ Verwijder
                    </button>
                </div>
            `;
            
            // Voeg click event toe voor focus
            card.addEventListener('click', (e) => {
                if (!e.target.matches('button, input, img')) {
                    setFocus(index);
                }
            });
            
            return card;
        }
        
        function setFocus(index) {
            // Verwijder oude focus
            if (currentFocusIndex >= 0) {
                const oldCard = document.getElementById(`card-${currentFocusIndex}`);
                if (oldCard) oldCard.classList.remove('focused');
            }
            
            // Set nieuwe focus
            currentFocusIndex = index;
            const newCard = document.getElementById(`card-${index}`);
            if (newCard) {
                newCard.classList.add('focused');
                newCard.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
            }
        }
        
        function getGridDimensions() {
            const gallery = document.getElementById('gallery');
            const cards = gallery.querySelectorAll('.image-card');
            if (cards.length === 0) return { cols: 0, rows: 0 };
            
            const galleryRect = gallery.getBoundingClientRect();
            const firstCardRect = cards[0].getBoundingClientRect();
            const cardWidth = firstCardRect.width;
            
            const cols = Math.floor(galleryRect.width / cardWidth);
            const rows = Math.ceil(cards.length / cols);
            
            return { cols, rows };
        }
        
        function navigateGrid(direction) {
            if (images.length === 0) return;
            
            const { cols } = getGridDimensions();
            let newIndex = currentFocusIndex;
            
            switch(direction) {
                case 'ArrowLeft':
                    newIndex = Math.max(0, currentFocusIndex - 1);
                    break;
                case 'ArrowRight':
                    newIndex = Math.min(images.length - 1, currentFocusIndex + 1);
                    break;
                case 'ArrowUp':
                    newIndex = Math.max(0, currentFocusIndex - cols);
                    break;
                case 'ArrowDown':
                    newIndex = Math.min(images.length - 1, currentFocusIndex + cols);
                    break;
            }
            
            if (newIndex !== currentFocusIndex) {
                setFocus(newIndex);
            }
        }
        
        function toggleCurrentSelection() {
            if (currentFocusIndex >= 0 && currentFocusIndex < images.length) {
                const image = images[currentFocusIndex];
                const checkbox = document.getElementById(`check-${currentFocusIndex}`);
                checkbox.checked = !checkbox.checked;
                toggleSelection(currentFocusIndex, image.name);
            }
        }
        
        function selectAll() {
            images.forEach((image, index) => {
                selectedImages.add(image.name);
                const checkbox = document.getElementById(`check-${index}`);
                const card = document.getElementById(`card-${index}`);
                if (checkbox) checkbox.checked = true;
                if (card) card.classList.add('selected');
            });
            updateSelectedCount();
        }
        
        function deselectAll() {
            selectedImages.clear();
            images.forEach((image, index) => {
                const checkbox = document.getElementById(`check-${index}`);
                const card = document.getElementById(`card-${index}`);
                if (checkbox) checkbox.checked = false;
                if (card) card.classList.remove('selected');
            });
            updateSelectedCount();
        }
        
        function toggleSelection(index, filename) {
            const checkbox = document.getElementById(`check-${index}`);
            const card = document.getElementById(`card-${index}`);
            
            if (checkbox.checked) {
                selectedImages.add(filename);
                card.classList.add('selected');
            } else {
                selectedImages.delete(filename);
                card.classList.remove('selected');
            }
            
            updateSelectedCount();
        }
        
        function updateSelectedCount() {
            const count = selectedImages.size;
            document.getElementById('selectedCount').textContent = count;
            document.getElementById('deleteSelectedBtn').style.display = 
                count > 0 ? 'inline-block' : 'none';
        }
        
        function deleteSelected() {
            if (selectedImages.size === 0) return;
            
            if (!confirm(`Weet je zeker dat je ${selectedImages.size} afbeelding(en) wilt verwijderen?`)) {
                return;
            }
            
            const promises = Array.from(selectedImages).map(filename => 
                fetch(`/api/delete/${encodeURIComponent(filename)}`, { method: 'DELETE' })
            );
            
            Promise.all(promises)
                .then(() => {
                    alert(`${selectedImages.size} afbeelding(en) verwijderd`);
                    loadImages();
                })
                .catch(error => {
                    console.error('Error deleting images:', error);
                    alert('Fout bij het verwijderen van sommige afbeeldingen');
                    loadImages();
                });
        }
        
        function deleteImage(filename, index) {
            if (!confirm(`Weet je zeker dat je "${filename}" wilt verwijderen?`)) {
                return;
            }
            
            fetch(`/api/delete/${encodeURIComponent(filename)}`, {
                method: 'DELETE'
            })
            .then(response => response.json())
            .then(data => {
                if (data.success) {
                    // Verwijder uit array en update display
                    images.splice(index, 1);
                    displayImages();
                } else {
                    alert('Fout bij verwijderen: ' + data.error);
                }
            })
            .catch(error => {
                console.error('Error:', error);
                alert('Fout bij verwijderen van bestand');
            });
        }
        
        function deleteFocused() {
            if (currentFocusIndex >= 0 && currentFocusIndex < images.length) {
                const image = images[currentFocusIndex];
                deleteImage(image.name, currentFocusIndex);
            }
        }
        
        function formatFileSize(bytes) {
            if (bytes < 1024) return bytes + ' B';
            if (bytes < 1024 * 1024) return Math.round(bytes / 1024) + ' KB';
            return Math.round(bytes / (1024 * 1024) * 10) / 10 + ' MB';
        }
        
        function showModal(filename) {
            const modal = document.getElementById('imageModal');
            const modalImage = document.getElementById('modalImage');
            modalImage.src = `/api/thumbnail/${filename}`;
            modal.style.display = 'block';
            isModalOpen = true;
        }
        
        function showCurrentInModal() {
            if (currentFocusIndex >= 0 && currentFocusIndex < images.length) {
                const image = images[currentFocusIndex];
                showModal(encodeURIComponent(image.name));
            }
        }
        
        function closeModal() {
            document.getElementById('imageModal').style.display = 'none';
            isModalOpen = false;
        }
        
        // Sluit modal bij klik buiten afbeelding
        window.onclick = function(event) {
            const modal = document.getElementById('imageModal');
            if (event.target === modal) {
                closeModal();
            }
        }
        
        // Keyboard event handler
        document.addEventListener('keydown', (e) => {
            // Voorkom default browser gedrag voor sommige keys
            if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', ' '].includes(e.key)) {
                e.preventDefault();
            }
            
            // Modal controls
            if (isModalOpen) {
                if (e.key === 'Escape') {
                    closeModal();
                }
                return;
            }
            
            // Gallery navigation
            switch(e.key) {
                case 'ArrowLeft':
                case 'ArrowRight':
                case 'ArrowUp':
                case 'ArrowDown':
                    navigateGrid(e.key);
                    break;
                    
                case ' ': // Spatiebalk
                    toggleCurrentSelection();
                    break;
                    
                case 'Enter':
                    showCurrentInModal();
                    break;
                    
                case 'Delete':
                    if (e.shiftKey && selectedImages.size > 0) {
                        deleteSelected();
                    } else {
                        deleteFocused();
                    }
                    break;
                    
                case 'a':
                case 'A':
                    if (e.ctrlKey || e.metaKey) {
                        e.preventDefault();
                    }
                    selectAll();
                    break;
                    
                case 'd':
                case 'D':
                    deselectAll();
                    break;
                    
                case 'F5':
                    e.preventDefault();
                    loadImages();
                    break;
                    
                case 'r':
                case 'R':
                    if (e.ctrlKey || e.metaKey) {
                        e.preventDefault();
                        loadImages();
                    }
                    break;
            }
        });
    </script>
</body>
</html>

Nieuwe Keyboard Features:

Navigatie:

  • Pijltjestoetsen (←→↑↓): Navigeer door de afbeeldingen
  • De navigatie houdt rekening met het grid layout

Selectie:

  • Spatiebalk: Toggle selectie van huidige afbeelding
  • A: Selecteer alle afbeeldingen
  • D: Deselecteer alle afbeeldingen

Acties:

  • Enter: Open huidige afbeelding in preview
  • Delete: Verwijder huidige afbeelding
  • Shift + Delete: Verwijder alle geselecteerde afbeeldingen
  • Escape: Sluit de preview modal

Andere shortcuts:

  • F5 of Ctrl/Cmd + R: Ververs de lijst

Visual Feedback:

  1. Focus indicator: De huidige geselecteerde afbeelding heeft een blauwe outline
  2. Selected state: Geselecteerde afbeeldingen hebben een licht blauwe achtergrond
  3. Keyboard hints: Een sectie bovenaan toont alle beschikbare keyboard shortcuts

De keyboard navigatie werkt intelligent met het grid layout - als je naar boven/beneden navigeert, springt het naar de afbeelding in dezelfde kolom in de rij erboven/eronder.


👤 Gebruiker (29 juni 2025, 11:04 AM)

Het werkt fantastisch! Wat zou nog meer kunnen?


🤖 Assistent (29 juni 2025, 11:04 AM)

Geweldig dat het zo goed werkt! Hier zijn nog meer features die je app nóg krachtiger kunnen maken:

🚀 Extra Features die je kunt toevoegen:

1. Filter & Zoek Functionaliteit

// Voeg een zoekbalk toe
<input type="text" id="searchBox" placeholder="🔍 Zoek op bestandsnaam..." />

// Filter op datum (vandaag, deze week, deze maand)
<select id="dateFilter">
  <option value="">Alle datums</option>
  <option value="today">Vandaag</option>
  <option value="week">Deze week</option>
  <option value="month">Deze maand</option>
</select>

2. Drag & Drop Upload

  • Sleep nieuwe screenshots direct naar de webpagina
  • Automatisch verplaatsen naar Downloads folder

3. Batch Operaties

  • Hernoemen: Selecteer meerdere files en hernoem ze in bulk (bijv. "Screenshot_001", "Screenshot_002")
  • Verplaatsen: Verplaats geselecteerde items naar submappen (bijv. "Work", "Personal", "Archive")
  • ZIP downloaden: Download geselecteerde afbeeldingen als ZIP

4. Sorteren

// Sorteer opties
<select id="sortBy">
  <option value="date-desc">Nieuwste eerst</option>
  <option value="date-asc">Oudste eerst</option>
  <option value="name-asc">Naam (A-Z)</option>
  <option value="name-desc">Naam (Z-A)</option>
  <option value="size-desc">Grootste eerst</option>
  <option value="size-asc">Kleinste eerst</option>
</select>

5. View Modes

  • Grid view (huidige)
  • List view (compacte lijst met details)
  • Masonry layout (Pinterest-stijl)
  • Slideshow mode (automatisch doorlopen)

6. Quick Actions

# In app.py - OCR tekst extractie
@app.route('/api/ocr/<path:filename>')
def extract_text(filename):
    import pytesseract
    text = pytesseract.image_to_string(Image.open(filepath))
    return jsonify({'text': text})

# Copy to clipboard functie
# Share via email/slack
# Quick edit (crop, rotate)

7. Smart Categorization

# Automatisch categoriseren op basis van:
- Inhoud (met ML: code screenshots, chat screenshots, etc.)
- Applicatie (Safari, Chrome, Terminal, etc.)
- Resolutie/aspect ratio
- Bevat tekst of niet

8. Duplicate Detection

# Vind en markeer duplicate screenshots
def find_duplicates():
    import imagehash
    hashes = {}
    for image in images:
        hash = imagehash.average_hash(Image.open(image.path))
        if hash in hashes:
            # Mark als duplicate

9. Statistieken Dashboard

<div class="stats-panel">
  📊 Screenshots deze week: 47
  💾 Totale ruimte: 234 MB
  🔥 Meest productieve dag: Dinsdag
  📸 Gemiddeld per dag: 12
</div>

10. Advanced Keyboard Shortcuts

  • 1-9: Quick tag toewijzen
  • C: Copy bestandspad
  • O: Open in Finder
  • E: Quick edit mode
  • / : Focus op zoekbalk
  • G: Go to date

11. Export Opties

# Export naar verschillende formaten
- PDF rapport van geselecteerde screenshots  
- HTML gallery voor delen
- Markdown met thumbnails
- CSV met metadata

12. Auto-Cleanup Rules

# Automatisch opruimen
rules = {
    'delete_after_30_days': True,
    'auto_archive_large_files': True,
    'move_duplicates_to_trash': True
}

13. Multi-Monitor Support

  • Groepeer screenshots per monitor
  • Toon van welk scherm de screenshot komt

14. Integration Features

# Integreer met andere tools
- Upload naar Imgur/CloudApp
- Stuur naar Notion/Obsidian
- Post naar Slack
- Save naar Google Drive

15. Power User Features

// Vim-style navigation
j/k voor up/down
h/l voor left/right
gg voor top
G voor bottom
/ voor search

// Multi-select modes
- Shift+click voor range select
- Ctrl/Cmd+click voor individual
- Rectangular selection met mouse drag

🎯 Mijn Top 3 Aanbevelingen:

1. Quick Search (gemakkelijk te implementeren)

function filterImages(searchTerm) {
    const filtered = images.filter(img => 
        img.name.toLowerCase().includes(searchTerm.toLowerCase())
    );
    displayFilteredImages(filtered);
}

2. Duplicate Detection (zeer nuttig)

# Voorkomt disk space verspilling
pip install imagehash

3. Quick Preview Info

// Hover voor metadata
- Datum genomen
- Resolutie  
- Applicatie bron
- OCR preview van tekst

Welke van deze features spreekt je het meest aan? Ik kan je helpen met de implementatie!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment