👤 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:
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)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()">×</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>- Installeer benodigde Python packages:
pip install flask pillow- Maak de projectstructuur:
mkdir screenshot-manager
cd screenshot-manager
mkdir templates- Kopieer de code:
- Sla
app.pyop in de hoofdmap - Sla
index.htmlop in detemplatesmap
- Start de applicatie:
python app.py- Open in je browser:
- Ga naar
http://localhost:5000
- 📸 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)
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:
source ~/mijn-project-venv/bin/activate
cd ~/Documents/Hobbies/scripts/downloads-screenshots
python app.pycd ~/Documents/Hobbies/scripts/downloads-screenshots
python3 -m venv venv
source venv/bin/activate
pip install flask pillow
python app.pypip3 install --user flask pillow
python3 app.pycd ~/Documents/Hobbies/scripts/downloads-screenshots
~/mijn-project-venv/bin/python app.pyIk 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.pyEn 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()">×</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>- Pijltjestoetsen (←→↑↓): Navigeer door de afbeeldingen
- De navigatie houdt rekening met het grid layout
- Spatiebalk: Toggle selectie van huidige afbeelding
- A: Selecteer alle afbeeldingen
- D: Deselecteer alle afbeeldingen
- Enter: Open huidige afbeelding in preview
- Delete: Verwijder huidige afbeelding
- Shift + Delete: Verwijder alle geselecteerde afbeeldingen
- Escape: Sluit de preview modal
- F5 of Ctrl/Cmd + R: Ververs de lijst
- Focus indicator: De huidige geselecteerde afbeelding heeft een blauwe outline
- Selected state: Geselecteerde afbeeldingen hebben een licht blauwe achtergrond
- 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:
// 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>- Sleep nieuwe screenshots direct naar de webpagina
- Automatisch verplaatsen naar Downloads folder
- 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
// 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>- Grid view (huidige)
- List view (compacte lijst met details)
- Masonry layout (Pinterest-stijl)
- Slideshow mode (automatisch doorlopen)
# 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)# 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# 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<div class="stats-panel">
📊 Screenshots deze week: 47
💾 Totale ruimte: 234 MB
🔥 Meest productieve dag: Dinsdag
📸 Gemiddeld per dag: 12
</div>- 1-9: Quick tag toewijzen
- C: Copy bestandspad
- O: Open in Finder
- E: Quick edit mode
- / : Focus op zoekbalk
- G: Go to date
# Export naar verschillende formaten
- PDF rapport van geselecteerde screenshots
- HTML gallery voor delen
- Markdown met thumbnails
- CSV met metadata# Automatisch opruimen
rules = {
'delete_after_30_days': True,
'auto_archive_large_files': True,
'move_duplicates_to_trash': True
}- Groepeer screenshots per monitor
- Toon van welk scherm de screenshot komt
# Integreer met andere tools
- Upload naar Imgur/CloudApp
- Stuur naar Notion/Obsidian
- Post naar Slack
- Save naar Google Drive// 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 dragfunction filterImages(searchTerm) {
const filtered = images.filter(img =>
img.name.toLowerCase().includes(searchTerm.toLowerCase())
);
displayFilteredImages(filtered);
}# Voorkomt disk space verspilling
pip install imagehash// Hover voor metadata
- Datum genomen
- Resolutie
- Applicatie bron
- OCR preview van tekstWelke van deze features spreekt je het meest aan? Ik kan je helpen met de implementatie!