mirror of
https://github.com/smittix/intercept.git
synced 2026-04-24 06:40:00 -07:00
Add delete and download functionality to SSTV image gallery
Users can now manage decoded SSTV images with download and delete actions accessible from hover overlays on gallery cards, the full-size image modal toolbar, and a "Clear All" button in the gallery header. Both ISS and General SSTV modes are supported. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -287,6 +287,73 @@ def get_image(filename: str):
|
||||
return send_file(image_path, mimetype='image/png')
|
||||
|
||||
|
||||
@sstv_bp.route('/images/<filename>/download')
|
||||
def download_image(filename: str):
|
||||
"""
|
||||
Download a decoded SSTV image file.
|
||||
|
||||
Args:
|
||||
filename: Image filename
|
||||
|
||||
Returns:
|
||||
Image file as attachment or 404.
|
||||
"""
|
||||
decoder = get_sstv_decoder()
|
||||
|
||||
# Security: only allow alphanumeric filenames with .png extension
|
||||
if not filename.replace('_', '').replace('-', '').replace('.', '').isalnum():
|
||||
return jsonify({'status': 'error', 'message': 'Invalid filename'}), 400
|
||||
|
||||
if not filename.endswith('.png'):
|
||||
return jsonify({'status': 'error', 'message': 'Only PNG files supported'}), 400
|
||||
|
||||
image_path = decoder._output_dir / filename
|
||||
|
||||
if not image_path.exists():
|
||||
return jsonify({'status': 'error', 'message': 'Image not found'}), 404
|
||||
|
||||
return send_file(image_path, mimetype='image/png', as_attachment=True, download_name=filename)
|
||||
|
||||
|
||||
@sstv_bp.route('/images/<filename>', methods=['DELETE'])
|
||||
def delete_image(filename: str):
|
||||
"""
|
||||
Delete a decoded SSTV image.
|
||||
|
||||
Args:
|
||||
filename: Image filename
|
||||
|
||||
Returns:
|
||||
JSON confirmation.
|
||||
"""
|
||||
decoder = get_sstv_decoder()
|
||||
|
||||
# Security: only allow alphanumeric filenames with .png extension
|
||||
if not filename.replace('_', '').replace('-', '').replace('.', '').isalnum():
|
||||
return jsonify({'status': 'error', 'message': 'Invalid filename'}), 400
|
||||
|
||||
if not filename.endswith('.png'):
|
||||
return jsonify({'status': 'error', 'message': 'Only PNG files supported'}), 400
|
||||
|
||||
if decoder.delete_image(filename):
|
||||
return jsonify({'status': 'ok'})
|
||||
else:
|
||||
return jsonify({'status': 'error', 'message': 'Image not found'}), 404
|
||||
|
||||
|
||||
@sstv_bp.route('/images', methods=['DELETE'])
|
||||
def delete_all_images():
|
||||
"""
|
||||
Delete all decoded SSTV images.
|
||||
|
||||
Returns:
|
||||
JSON with count of deleted images.
|
||||
"""
|
||||
decoder = get_sstv_decoder()
|
||||
count = decoder.delete_all_images()
|
||||
return jsonify({'status': 'ok', 'deleted': count})
|
||||
|
||||
|
||||
@sstv_bp.route('/stream')
|
||||
def stream_progress():
|
||||
"""
|
||||
|
||||
@@ -217,6 +217,52 @@ def get_image(filename: str):
|
||||
return send_file(image_path, mimetype='image/png')
|
||||
|
||||
|
||||
@sstv_general_bp.route('/images/<filename>/download')
|
||||
def download_image(filename: str):
|
||||
"""Download a decoded SSTV image file."""
|
||||
decoder = get_general_sstv_decoder()
|
||||
|
||||
# Security: only allow alphanumeric filenames with .png extension
|
||||
if not filename.replace('_', '').replace('-', '').replace('.', '').isalnum():
|
||||
return jsonify({'status': 'error', 'message': 'Invalid filename'}), 400
|
||||
|
||||
if not filename.endswith('.png'):
|
||||
return jsonify({'status': 'error', 'message': 'Only PNG files supported'}), 400
|
||||
|
||||
image_path = decoder._output_dir / filename
|
||||
|
||||
if not image_path.exists():
|
||||
return jsonify({'status': 'error', 'message': 'Image not found'}), 404
|
||||
|
||||
return send_file(image_path, mimetype='image/png', as_attachment=True, download_name=filename)
|
||||
|
||||
|
||||
@sstv_general_bp.route('/images/<filename>', methods=['DELETE'])
|
||||
def delete_image(filename: str):
|
||||
"""Delete a decoded SSTV image."""
|
||||
decoder = get_general_sstv_decoder()
|
||||
|
||||
# Security: only allow alphanumeric filenames with .png extension
|
||||
if not filename.replace('_', '').replace('-', '').replace('.', '').isalnum():
|
||||
return jsonify({'status': 'error', 'message': 'Invalid filename'}), 400
|
||||
|
||||
if not filename.endswith('.png'):
|
||||
return jsonify({'status': 'error', 'message': 'Only PNG files supported'}), 400
|
||||
|
||||
if decoder.delete_image(filename):
|
||||
return jsonify({'status': 'ok'})
|
||||
else:
|
||||
return jsonify({'status': 'error', 'message': 'Image not found'}), 404
|
||||
|
||||
|
||||
@sstv_general_bp.route('/images', methods=['DELETE'])
|
||||
def delete_all_images():
|
||||
"""Delete all decoded SSTV images."""
|
||||
decoder = get_general_sstv_decoder()
|
||||
count = decoder.delete_all_images()
|
||||
return jsonify({'status': 'ok', 'deleted': count})
|
||||
|
||||
|
||||
@sstv_general_bp.route('/stream')
|
||||
def stream_progress():
|
||||
"""SSE stream of SSTV decode progress."""
|
||||
|
||||
@@ -329,12 +329,12 @@
|
||||
}
|
||||
|
||||
.sstv-general-image-card {
|
||||
position: relative;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
transition: all 0.15s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sstv-general-image-card:hover {
|
||||
@@ -343,6 +343,10 @@
|
||||
box-shadow: 0 4px 12px rgba(0, 212, 255, 0.2);
|
||||
}
|
||||
|
||||
.sstv-general-image-card-inner {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sstv-general-image-preview {
|
||||
width: 100%;
|
||||
aspect-ratio: 4/3;
|
||||
@@ -351,6 +355,48 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
.sstv-general-image-actions {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 4px;
|
||||
padding: 6px;
|
||||
background: linear-gradient(transparent, rgba(0, 0, 0, 0.8));
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.sstv-general-image-card:hover .sstv-general-image-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.sstv-general-image-actions button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.sstv-general-image-actions button:hover {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
.sstv-general-image-actions button:last-child:hover {
|
||||
background: var(--accent-red, #ff3366);
|
||||
border-color: var(--accent-red, #ff3366);
|
||||
}
|
||||
|
||||
.sstv-general-image-info {
|
||||
padding: 8px 10px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
@@ -507,6 +553,40 @@
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.sstv-general-modal-toolbar {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 60px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.sstv-general-modal-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
padding: 6px 12px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.sstv-general-modal-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.sstv-general-modal-btn.delete:hover {
|
||||
background: var(--accent-red, #ff3366);
|
||||
border-color: var(--accent-red, #ff3366);
|
||||
}
|
||||
|
||||
.sstv-general-modal-close {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
@@ -518,12 +598,33 @@
|
||||
cursor: pointer;
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.15s;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.sstv-general-modal-close:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Clear All button */
|
||||
.sstv-general-gallery-clear-btn {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 9px;
|
||||
text-transform: uppercase;
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-dim);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.sstv-general-gallery-clear-btn:hover {
|
||||
color: var(--accent-red, #ff3366);
|
||||
border-color: var(--accent-red, #ff3366);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
RESPONSIVE
|
||||
============================================ */
|
||||
|
||||
@@ -388,12 +388,12 @@
|
||||
}
|
||||
|
||||
.sstv-image-card {
|
||||
position: relative;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
transition: all 0.15s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sstv-image-card:hover {
|
||||
@@ -402,6 +402,10 @@
|
||||
box-shadow: 0 4px 12px rgba(0, 212, 255, 0.2);
|
||||
}
|
||||
|
||||
.sstv-image-card-inner {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.sstv-image-preview {
|
||||
width: 100%;
|
||||
aspect-ratio: 4/3;
|
||||
@@ -410,6 +414,48 @@
|
||||
display: block;
|
||||
}
|
||||
|
||||
.sstv-image-actions {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 4px;
|
||||
padding: 6px;
|
||||
background: linear-gradient(transparent, rgba(0, 0, 0, 0.8));
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
.sstv-image-card:hover .sstv-image-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.sstv-image-actions button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
}
|
||||
|
||||
.sstv-image-actions button:hover {
|
||||
background: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
.sstv-image-actions button:last-child:hover {
|
||||
background: var(--accent-red, #ff3366);
|
||||
border-color: var(--accent-red, #ff3366);
|
||||
}
|
||||
|
||||
.sstv-image-info {
|
||||
padding: 8px 10px;
|
||||
border-top: 1px solid var(--border-color);
|
||||
@@ -854,6 +900,40 @@
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.sstv-modal-toolbar {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
right: 60px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.sstv-modal-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
padding: 6px 12px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
border-radius: 4px;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.sstv-modal-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
.sstv-modal-btn.delete:hover {
|
||||
background: var(--accent-red, #ff3366);
|
||||
border-color: var(--accent-red, #ff3366);
|
||||
}
|
||||
|
||||
.sstv-modal-close {
|
||||
position: absolute;
|
||||
top: 20px;
|
||||
@@ -865,12 +945,33 @@
|
||||
cursor: pointer;
|
||||
opacity: 0.7;
|
||||
transition: opacity 0.15s;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.sstv-modal-close:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Clear All button */
|
||||
.sstv-gallery-clear-btn {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 9px;
|
||||
text-transform: uppercase;
|
||||
padding: 3px 8px;
|
||||
border-radius: 4px;
|
||||
background: transparent;
|
||||
border: 1px solid var(--border-color);
|
||||
color: var(--text-dim);
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.sstv-gallery-clear-btn:hover {
|
||||
color: var(--accent-red, #ff3366);
|
||||
border-color: var(--accent-red, #ff3366);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
RESPONSIVE
|
||||
============================================ */
|
||||
|
||||
@@ -425,12 +425,22 @@ const SSTVGeneral = (function() {
|
||||
}
|
||||
|
||||
gallery.innerHTML = images.map(img => `
|
||||
<div class="sstv-general-image-card" onclick="SSTVGeneral.showImage('${escapeHtml(img.url)}')">
|
||||
<img src="${escapeHtml(img.url)}" alt="SSTV Image" class="sstv-general-image-preview" loading="lazy">
|
||||
<div class="sstv-general-image-card">
|
||||
<div class="sstv-general-image-card-inner" onclick="SSTVGeneral.showImage('${escapeHtml(img.url)}', '${escapeHtml(img.filename)}')">
|
||||
<img src="${escapeHtml(img.url)}" alt="SSTV Image" class="sstv-general-image-preview" loading="lazy">
|
||||
</div>
|
||||
<div class="sstv-general-image-info">
|
||||
<div class="sstv-general-image-mode">${escapeHtml(img.mode || 'Unknown')}</div>
|
||||
<div class="sstv-general-image-timestamp">${formatTimestamp(img.timestamp)}</div>
|
||||
</div>
|
||||
<div class="sstv-general-image-actions">
|
||||
<button onclick="event.stopPropagation(); SSTVGeneral.downloadImage('${escapeHtml(img.url)}', '${escapeHtml(img.filename)}')" title="Download">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||||
</button>
|
||||
<button onclick="event.stopPropagation(); SSTVGeneral.deleteImage('${escapeHtml(img.filename)}')" title="Delete">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
@@ -438,19 +448,45 @@ const SSTVGeneral = (function() {
|
||||
/**
|
||||
* Show full-size image in modal
|
||||
*/
|
||||
function showImage(url) {
|
||||
let currentModalUrl = null;
|
||||
let currentModalFilename = null;
|
||||
|
||||
function showImage(url, filename) {
|
||||
currentModalUrl = url;
|
||||
currentModalFilename = filename || null;
|
||||
|
||||
let modal = document.getElementById('sstvGeneralImageModal');
|
||||
if (!modal) {
|
||||
modal = document.createElement('div');
|
||||
modal.id = 'sstvGeneralImageModal';
|
||||
modal.className = 'sstv-general-image-modal';
|
||||
modal.innerHTML = `
|
||||
<div class="sstv-general-modal-toolbar">
|
||||
<button class="sstv-general-modal-btn" id="sstvGeneralModalDownload" title="Download">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||||
Download
|
||||
</button>
|
||||
<button class="sstv-general-modal-btn delete" id="sstvGeneralModalDelete" title="Delete">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
<button class="sstv-general-modal-close" onclick="SSTVGeneral.closeImage()">×</button>
|
||||
<img src="" alt="SSTV Image">
|
||||
`;
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target === modal) closeImage();
|
||||
});
|
||||
modal.querySelector('#sstvGeneralModalDownload').addEventListener('click', () => {
|
||||
if (currentModalUrl && currentModalFilename) {
|
||||
downloadImage(currentModalUrl, currentModalFilename);
|
||||
}
|
||||
});
|
||||
modal.querySelector('#sstvGeneralModalDelete').addEventListener('click', () => {
|
||||
if (currentModalFilename) {
|
||||
deleteImage(currentModalFilename);
|
||||
}
|
||||
});
|
||||
document.body.appendChild(modal);
|
||||
}
|
||||
|
||||
@@ -489,6 +525,55 @@ const SSTVGeneral = (function() {
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a single image
|
||||
*/
|
||||
async function deleteImage(filename) {
|
||||
if (!confirm('Delete this image?')) return;
|
||||
try {
|
||||
const response = await fetch(`/sstv-general/images/${encodeURIComponent(filename)}`, { method: 'DELETE' });
|
||||
const data = await response.json();
|
||||
if (data.status === 'ok') {
|
||||
images = images.filter(img => img.filename !== filename);
|
||||
updateImageCount(images.length);
|
||||
renderGallery();
|
||||
closeImage();
|
||||
showNotification('SSTV', 'Image deleted');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to delete image:', err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all images
|
||||
*/
|
||||
async function deleteAllImages() {
|
||||
if (!confirm('Delete all decoded images?')) return;
|
||||
try {
|
||||
const response = await fetch('/sstv-general/images', { method: 'DELETE' });
|
||||
const data = await response.json();
|
||||
if (data.status === 'ok') {
|
||||
images = [];
|
||||
updateImageCount(0);
|
||||
renderGallery();
|
||||
showNotification('SSTV', `${data.deleted} image${data.deleted !== 1 ? 's' : ''} deleted`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to delete images:', err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download an image
|
||||
*/
|
||||
function downloadImage(url, filename) {
|
||||
const a = document.createElement('a');
|
||||
a.href = url + '/download';
|
||||
a.download = filename;
|
||||
a.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show status message
|
||||
*/
|
||||
@@ -508,6 +593,9 @@ const SSTVGeneral = (function() {
|
||||
loadImages,
|
||||
showImage,
|
||||
closeImage,
|
||||
deleteImage,
|
||||
deleteAllImages,
|
||||
downloadImage,
|
||||
selectPreset
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -860,12 +860,22 @@ const SSTV = (function() {
|
||||
}
|
||||
|
||||
gallery.innerHTML = images.map(img => `
|
||||
<div class="sstv-image-card" onclick="SSTV.showImage('${escapeHtml(img.url)}')">
|
||||
<img src="${escapeHtml(img.url)}" alt="SSTV Image" class="sstv-image-preview" loading="lazy">
|
||||
<div class="sstv-image-card">
|
||||
<div class="sstv-image-card-inner" onclick="SSTV.showImage('${escapeHtml(img.url)}', '${escapeHtml(img.filename)}')">
|
||||
<img src="${escapeHtml(img.url)}" alt="SSTV Image" class="sstv-image-preview" loading="lazy">
|
||||
</div>
|
||||
<div class="sstv-image-info">
|
||||
<div class="sstv-image-mode">${escapeHtml(img.mode || 'Unknown')}</div>
|
||||
<div class="sstv-image-timestamp">${formatTimestamp(img.timestamp)}</div>
|
||||
</div>
|
||||
<div class="sstv-image-actions">
|
||||
<button onclick="event.stopPropagation(); SSTV.downloadImage('${escapeHtml(img.url)}', '${escapeHtml(img.filename)}')" title="Download">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||||
</button>
|
||||
<button onclick="event.stopPropagation(); SSTV.deleteImage('${escapeHtml(img.filename)}')" title="Delete">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
@@ -997,19 +1007,45 @@ const SSTV = (function() {
|
||||
/**
|
||||
* Show full-size image in modal
|
||||
*/
|
||||
function showImage(url) {
|
||||
let currentModalUrl = null;
|
||||
let currentModalFilename = null;
|
||||
|
||||
function showImage(url, filename) {
|
||||
currentModalUrl = url;
|
||||
currentModalFilename = filename || null;
|
||||
|
||||
let modal = document.getElementById('sstvImageModal');
|
||||
if (!modal) {
|
||||
modal = document.createElement('div');
|
||||
modal.id = 'sstvImageModal';
|
||||
modal.className = 'sstv-image-modal';
|
||||
modal.innerHTML = `
|
||||
<div class="sstv-modal-toolbar">
|
||||
<button class="sstv-modal-btn" id="sstvModalDownload" title="Download">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
|
||||
Download
|
||||
</button>
|
||||
<button class="sstv-modal-btn delete" id="sstvModalDelete" title="Delete">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6m3 0V4a2 2 0 012-2h4a2 2 0 012 2v2"/></svg>
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
<button class="sstv-modal-close" onclick="SSTV.closeImage()">×</button>
|
||||
<img src="" alt="SSTV Image">
|
||||
`;
|
||||
modal.addEventListener('click', (e) => {
|
||||
if (e.target === modal) closeImage();
|
||||
});
|
||||
modal.querySelector('#sstvModalDownload').addEventListener('click', () => {
|
||||
if (currentModalUrl && currentModalFilename) {
|
||||
downloadImage(currentModalUrl, currentModalFilename);
|
||||
}
|
||||
});
|
||||
modal.querySelector('#sstvModalDelete').addEventListener('click', () => {
|
||||
if (currentModalFilename) {
|
||||
deleteImage(currentModalFilename);
|
||||
}
|
||||
});
|
||||
document.body.appendChild(modal);
|
||||
}
|
||||
|
||||
@@ -1048,6 +1084,55 @@ const SSTV = (function() {
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a single image
|
||||
*/
|
||||
async function deleteImage(filename) {
|
||||
if (!confirm('Delete this image?')) return;
|
||||
try {
|
||||
const response = await fetch(`/sstv/images/${encodeURIComponent(filename)}`, { method: 'DELETE' });
|
||||
const data = await response.json();
|
||||
if (data.status === 'ok') {
|
||||
images = images.filter(img => img.filename !== filename);
|
||||
updateImageCount(images.length);
|
||||
renderGallery();
|
||||
closeImage();
|
||||
showNotification('SSTV', 'Image deleted');
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to delete image:', err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete all images
|
||||
*/
|
||||
async function deleteAllImages() {
|
||||
if (!confirm('Delete all decoded images?')) return;
|
||||
try {
|
||||
const response = await fetch('/sstv/images', { method: 'DELETE' });
|
||||
const data = await response.json();
|
||||
if (data.status === 'ok') {
|
||||
images = [];
|
||||
updateImageCount(0);
|
||||
renderGallery();
|
||||
showNotification('SSTV', `${data.deleted} image${data.deleted !== 1 ? 's' : ''} deleted`);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to delete images:', err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download an image
|
||||
*/
|
||||
function downloadImage(url, filename) {
|
||||
const a = document.createElement('a');
|
||||
a.href = url + '/download';
|
||||
a.download = filename;
|
||||
a.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show status message
|
||||
*/
|
||||
@@ -1068,6 +1153,9 @@ const SSTV = (function() {
|
||||
loadIssSchedule,
|
||||
showImage,
|
||||
closeImage,
|
||||
deleteImage,
|
||||
deleteAllImages,
|
||||
downloadImage,
|
||||
useGPS,
|
||||
updateTLE,
|
||||
stopIssTracking,
|
||||
|
||||
@@ -2027,7 +2027,10 @@
|
||||
</svg>
|
||||
Decoded Images
|
||||
</div>
|
||||
<span class="sstv-gallery-count" id="sstvImageCount">0</span>
|
||||
<div style="display:flex;align-items:center;gap:4px;">
|
||||
<span class="sstv-gallery-count" id="sstvImageCount">0</span>
|
||||
<button class="sstv-gallery-clear-btn" onclick="SSTV.deleteAllImages()" title="Delete all images">Clear All</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sstv-gallery-grid" id="sstvGallery">
|
||||
<div class="sstv-gallery-empty">
|
||||
@@ -2109,7 +2112,10 @@
|
||||
</svg>
|
||||
Decoded Images
|
||||
</div>
|
||||
<span class="sstv-general-gallery-count" id="sstvGeneralImageCount">0</span>
|
||||
<div style="display:flex;align-items:center;gap:4px;">
|
||||
<span class="sstv-general-gallery-count" id="sstvGeneralImageCount">0</span>
|
||||
<button class="sstv-general-gallery-clear-btn" onclick="SSTVGeneral.deleteAllImages()" title="Delete all images">Clear All</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sstv-general-gallery-grid" id="sstvGeneralGallery">
|
||||
<div class="sstv-general-gallery-empty">
|
||||
|
||||
@@ -686,6 +686,26 @@ class SSTVDecoder:
|
||||
self._scan_images()
|
||||
return list(self._images)
|
||||
|
||||
def delete_image(self, filename: str) -> bool:
|
||||
"""Delete a single decoded image by filename."""
|
||||
filepath = self._output_dir / filename
|
||||
if not filepath.exists():
|
||||
return False
|
||||
filepath.unlink()
|
||||
self._images = [img for img in self._images if img.filename != filename]
|
||||
logger.info(f"Deleted SSTV image: {filename}")
|
||||
return True
|
||||
|
||||
def delete_all_images(self) -> int:
|
||||
"""Delete all decoded images. Returns count deleted."""
|
||||
count = 0
|
||||
for filepath in self._output_dir.glob('*.png'):
|
||||
filepath.unlink()
|
||||
count += 1
|
||||
self._images.clear()
|
||||
logger.info(f"Deleted all SSTV images ({count} files)")
|
||||
return count
|
||||
|
||||
def _scan_images(self) -> None:
|
||||
"""Scan output directory for images."""
|
||||
known_filenames = {img.filename for img in self._images}
|
||||
|
||||
Reference in New Issue
Block a user