Files
stegasoo/frontends/web/templates/encode.html

761 lines
37 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "base.html" %}
{% block title %}Encode Message - Stegasoo{% endblock %}
{% block content %}
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="card">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-lock-fill me-2"></i>Encode Secret Message or File</h5>
</div>
<div class="card-body">
<form method="POST" enctype="multipart/form-data" id="encodeForm">
<input type="hidden" name="client_date" id="clientDate" value="">
<!-- Embedding Mode Selection - NOW AT THE TOP -->
<div class="mb-4">
<label class="form-label">
<i class="bi bi-cpu me-1"></i> Embedding Mode
</label>
<div class="row g-2">
<!-- LSB Mode Card -->
<div class="col-md-6">
<label class="card p-3 h-100 border-primary border-2 cursor-pointer" id="lsbModeCard" for="modeLsb" style="cursor: pointer;">
<input class="form-check-input position-absolute" type="radio" name="embed_mode" id="modeLsb" value="lsb" checked style="top: 1rem; right: 1rem;">
<div class="d-flex align-items-center mb-2">
<i class="bi bi-grid-3x3-gap text-primary fs-4 me-2"></i>
<strong>LSB Mode</strong>
<span class="badge bg-success ms-auto me-4">Default</span>
</div>
<ul class="small text-muted mb-0 ps-3">
<li>Full color PNG output</li>
<li>Higher capacity (~375 KB/MP)</li>
<li>Best for email & file transfer</li>
</ul>
</label>
</div>
<!-- DCT Mode Card -->
<div class="col-md-6">
<label class="card p-3 h-100 {% if not has_dct %}opacity-50{% endif %} cursor-pointer" id="dctModeCard" for="modeDct" style="cursor: pointer;">
<input class="form-check-input position-absolute" type="radio" name="embed_mode" id="modeDct" value="dct" {% if not has_dct %}disabled{% endif %} style="top: 1rem; right: 1rem;">
<div class="d-flex align-items-center mb-2">
<i class="bi bi-soundwave text-warning fs-4 me-2"></i>
<strong>DCT Mode</strong>
{% if has_dct %}
<span class="badge bg-warning text-dark ms-auto me-4">Social Media</span>
{% else %}
<span class="badge bg-secondary ms-auto me-4">Unavailable</span>
{% endif %}
</div>
<ul class="small text-muted mb-0 ps-3">
<li>JPEG output, survives recompression</li>
<li>Lower capacity (~75 KB/MP)</li>
<li>Best for Instagram, WhatsApp, etc.</li>
</ul>
{% if not has_dct %}
<div class="alert alert-warning small mt-2 mb-0 py-1 px-2">
<i class="bi bi-exclamation-triangle me-1"></i>
Requires scipy: <code>pip install scipy</code>
</div>
{% endif %}
</label>
</div>
</div>
<!-- Mode hint -->
<div class="form-text mt-2" id="modeHint">
<i class="bi bi-lightbulb me-1"></i>
<strong>LSB</strong> for private channels (email, cloud storage).
<strong>DCT</strong> for social media that recompresses images.
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">
<i class="bi bi-image me-1"></i> Reference Photo
</label>
<div class="drop-zone" id="refDropZone">
<input type="file" name="reference_photo" accept="image/*" required>
<div class="drop-zone-label">
<i class="bi bi-cloud-arrow-up fs-3 d-block mb-2 text-muted"></i>
<span class="text-muted">Drop image or click to browse</span>
</div>
<img class="drop-zone-preview d-none" id="refPreview">
</div>
<div class="form-text">
The secret photo both parties have (NOT transmitted)
</div>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">
<i class="bi bi-file-image me-1"></i> Carrier Image
</label>
<div class="drop-zone" id="carrierDropZone">
<input type="file" name="carrier" accept="image/*" required id="carrierInput">
<div class="drop-zone-label">
<i class="bi bi-cloud-arrow-up fs-3 d-block mb-2 text-muted"></i>
<span class="text-muted">Drop image or click to browse</span>
</div>
<img class="drop-zone-preview d-none" id="carrierPreview">
</div>
<div class="form-text">
The image to hide your message in (e.g., a meme)
</div>
</div>
</div>
<!-- Capacity Info Panel (shown when carrier loaded) -->
<div class="alert alert-info small d-none" id="capacityPanel">
<div class="row align-items-center">
<div class="col">
<i class="bi bi-rulers me-1"></i>
<strong>Carrier:</strong> <span id="carrierDimensions">-</span>
</div>
<div class="col-auto">
<span class="badge bg-primary me-1" id="lsbCapacityBadge">LSB: -</span>
<span class="badge bg-warning text-dark" id="dctCapacityBadge">DCT: -</span>
</div>
</div>
</div>
<!-- Payload Type Selector -->
<div class="mb-3">
<label class="form-label">
<i class="bi bi-box me-1"></i> What to Encode
</label>
<div class="btn-group w-100" role="group">
<input type="radio" class="btn-check" name="payload_type" id="payloadText" value="text" checked>
<label class="btn btn-outline-primary" for="payloadText">
<i class="bi bi-chat-left-text me-1"></i> Text Message
</label>
<input type="radio" class="btn-check" name="payload_type" id="payloadFile" value="file">
<label class="btn btn-outline-primary" for="payloadFile">
<i class="bi bi-file-earmark me-1"></i> File
</label>
</div>
</div>
<!-- Text Message Input -->
<div class="mb-3" id="textPayloadSection">
<label class="form-label">
<i class="bi bi-chat-left-text me-1"></i> Secret Message
</label>
<textarea name="message" class="form-control" rows="4" id="messageInput"
placeholder="Enter your secret message here..."></textarea>
<div class="d-flex justify-content-between form-text">
<span>
<span id="charCount">0</span> / 250,000 characters
<span id="charWarning" class="text-warning d-none ms-2">
<i class="bi bi-exclamation-triangle"></i> Getting long!
</span>
</span>
<span id="charPercent" class="text-muted">0%</span>
</div>
</div>
<!-- File Upload Input -->
<div class="mb-3 d-none" id="filePayloadSection">
<label class="form-label">
<i class="bi bi-file-earmark me-1"></i> File to Embed
</label>
<div class="drop-zone" id="payloadDropZone">
<input type="file" name="payload_file" id="payloadFileInput">
<div class="drop-zone-label" id="payloadDropLabel">
<i class="bi bi-cloud-arrow-up fs-3 d-block mb-2 text-muted"></i>
<span class="text-muted">Drop any file or click to browse</span>
<div class="small text-muted mt-1">Max {{ max_payload_kb }} KB</div>
</div>
</div>
<div class="form-text">
Supports any file type: PDF, ZIP, documents, etc.
</div>
<div id="fileInfo" class="d-none mt-2 p-2 bg-dark rounded">
<i class="bi bi-file-earmark-check text-success me-2"></i>
<span id="fileInfoName"></span>
<span class="text-muted">(<span id="fileInfoSize"></span>)</span>
</div>
</div>
<div class="mb-3">
<label class="form-label" id="dayPhraseLabel">
<i class="bi bi-chat-quote me-1"></i> Day's Phrase
</label>
<input type="text" name="day_phrase" class="form-control"
placeholder="e.g., correct horse battery" required>
<div class="form-text">
Your phrase for <strong>today</strong> (based on your local timezone)
</div>
</div>
<hr class="my-4">
<h6 class="text-muted mb-3">
SECURITY FACTORS
<span class="text-warning small">(provide at least one: PIN or RSA Key)</span>
</h6>
<div class="row">
<div class="col-md-4 mb-3">
<label class="form-label"><i class="bi bi-123 me-1"></i> PIN</label>
<div class="input-group">
<input type="password" name="pin" class="form-control" id="pinInput" placeholder="6-9 digits" maxlength="9" style="max-width: 140px;">
<button class="btn btn-outline-secondary" type="button" id="togglePin">
<i class="bi bi-eye"></i>
</button>
</div>
<div class="form-text">Static 6-9 digit PIN</div>
</div>
<div class="col-md-8 mb-3">
<label class="form-label">
<i class="bi bi-file-earmark-lock me-1"></i> RSA Key
</label>
<!-- RSA Input Method Toggle -->
<div class="btn-group w-100 mb-2" role="group">
<input type="radio" class="btn-check" name="rsa_input_method" id="rsaMethodFile" value="file" checked>
<label class="btn btn-outline-secondary btn-sm" for="rsaMethodFile">
<i class="bi bi-file-earmark me-1"></i>.pem File
</label>
<input type="radio" class="btn-check" name="rsa_input_method" id="rsaMethodQr" value="qr">
<label class="btn btn-outline-secondary btn-sm" for="rsaMethodQr">
<i class="bi bi-qr-code me-1"></i>QR Code
</label>
</div>
<!-- .pem File Input -->
<div id="rsaFileSection">
<input type="file" name="rsa_key_file" class="form-control form-control-sm" accept=".pem">
</div>
<!-- QR Code Input -->
<div id="rsaQrSection" class="d-none">
<div class="drop-zone p-3" id="qrDropZone">
<input type="file" name="rsa_qr_image" accept="image/*" id="rsaQrInput">
<div class="drop-zone-label text-center">
<i class="bi bi-qr-code-scan fs-4 d-block text-muted mb-1"></i>
<span class="text-muted small">Drop QR image or click to browse</span>
</div>
</div>
<input type="hidden" name="rsa_key_pem_from_qr" id="rsaKeyFromQr">
</div>
<!-- Key Password (always visible) -->
<div class="input-group input-group-sm mt-2">
<input type="password" name="rsa_password" class="form-control" id="rsaPasswordInput" placeholder="Key password (if encrypted)">
<button class="btn btn-outline-secondary" type="button" id="toggleRsaPassword">
<i class="bi bi-eye"></i>
</button>
</div>
</div>
</div>
<!-- Advanced Options (DCT sub-options only) -->
<div class="mb-4 d-none" id="advancedOptionsContainer">
<a class="btn btn-sm btn-outline-secondary w-100" data-bs-toggle="collapse" href="#advancedOptions" role="button" aria-expanded="false">
<i class="bi bi-gear me-1"></i> DCT Options
<i class="bi bi-chevron-down ms-1" id="advancedChevron"></i>
</a>
<div class="collapse" id="advancedOptions">
<div class="card card-body mt-2 bg-dark border-secondary">
<div class="alert alert-info small mb-3">
<i class="bi bi-info-circle me-1"></i>
<strong>DCT defaults:</strong> Color mode + JPEG output for best social media compatibility.
</div>
<!-- DCT Color Mode -->
<div class="mb-3">
<label class="form-label">
<i class="bi bi-palette me-1"></i> Color Mode
</label>
<div class="row g-2">
<div class="col-6">
<div class="form-check card p-2 text-center border-success border-2" id="dctColorCard">
<input class="form-check-input mx-auto" type="radio" name="dct_color_mode" id="dctColorColor" value="color" checked>
<label class="form-check-label w-100" for="dctColorColor">
<i class="bi bi-palette-fill text-success fs-5 d-block"></i>
<strong>Color</strong>
<div class="small text-muted">Recommended</div>
</label>
</div>
</div>
<div class="col-6">
<div class="form-check card p-2 text-center" id="dctGrayscaleCard">
<input class="form-check-input mx-auto" type="radio" name="dct_color_mode" id="dctColorGrayscale" value="grayscale">
<label class="form-check-label w-100" for="dctColorGrayscale">
<i class="bi bi-circle-half text-secondary fs-5 d-block"></i>
<strong>Grayscale</strong>
<div class="small text-muted">B&W output</div>
</label>
</div>
</div>
</div>
</div>
<!-- DCT Output Format -->
<div class="mb-0">
<label class="form-label">
<i class="bi bi-file-image me-1"></i> Output Format
</label>
<div class="row g-2">
<div class="col-6">
<div class="form-check card p-2 text-center" id="dctPngCard">
<input class="form-check-input mx-auto" type="radio" name="dct_output_format" id="dctFormatPng" value="png">
<label class="form-check-label w-100" for="dctFormatPng">
<i class="bi bi-file-earmark-image text-primary fs-5 d-block"></i>
<strong>PNG</strong>
<div class="small text-muted">Lossless, larger</div>
</label>
</div>
</div>
<div class="col-6">
<div class="form-check card p-2 text-center border-warning border-2" id="dctJpegCard">
<input class="form-check-input mx-auto" type="radio" name="dct_output_format" id="dctFormatJpeg" value="jpeg" checked>
<label class="form-check-label w-100" for="dctFormatJpeg">
<i class="bi bi-file-earmark-richtext text-warning fs-5 d-block"></i>
<strong>JPEG</strong>
<div class="small text-muted">Recommended</div>
</label>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<button type="submit" class="btn btn-primary btn-lg w-100" id="encodeBtn">
<i class="bi bi-lock me-2"></i>Encode
</button>
</form>
<hr class="my-4">
<div class="row text-center text-muted small">
<div class="col-4">
<i class="bi bi-shield-check fs-4 d-block mb-1 text-success"></i>
AES-256-GCM Encryption
</div>
<div class="col-4">
<i class="bi bi-shuffle fs-4 d-block mb-1 text-info"></i>
Random Pixel Embedding
</div>
<div class="col-4">
<i class="bi bi-eye-slash fs-4 d-block mb-1 text-warning"></i>
Undetectable by Analysis
</div>
</div>
<div class="alert alert-secondary mt-4 small">
<i class="bi bi-info-circle me-1"></i>
<strong>Limits:</strong>
Carrier image max ~24 megapixels (6000x4000).
Files max 30MB upload.
Payload max {{ max_payload_kb }} KB.
</div>
</div>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
<script>
// Detect client's local date and day
const now = new Date();
const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
const localDay = dayNames[now.getDay()];
const localDate = now.getFullYear() + '-' +
String(now.getMonth() + 1).padStart(2, '0') + '-' +
String(now.getDate()).padStart(2, '0');
// Update day label to client's local day
const dayLabel = document.getElementById('dayPhraseLabel');
if (dayLabel) {
dayLabel.innerHTML = `<i class="bi bi-chat-quote me-1"></i>Secure with <span class="day-of-week-highlight">${localDay}</span>'s Phrase`;
}
// Set hidden field with client's local date for server
const dateInput = document.getElementById('clientDate');
if (dateInput) {
dateInput.value = localDate;
}
// Payload type switching
const payloadTextRadio = document.getElementById('payloadText');
const payloadFileRadio = document.getElementById('payloadFile');
const textSection = document.getElementById('textPayloadSection');
const fileSection = document.getElementById('filePayloadSection');
const messageInput = document.getElementById('messageInput');
const payloadFileInput = document.getElementById('payloadFileInput');
function updatePayloadSection() {
const isText = payloadTextRadio.checked;
textSection.classList.toggle('d-none', !isText);
fileSection.classList.toggle('d-none', isText);
if (isText) {
messageInput.setAttribute('required', '');
payloadFileInput.removeAttribute('required');
} else {
messageInput.removeAttribute('required');
payloadFileInput.setAttribute('required', '');
}
}
payloadTextRadio.addEventListener('change', updatePayloadSection);
payloadFileRadio.addEventListener('change', updatePayloadSection);
// Payload file info display
payloadFileInput.addEventListener('change', function() {
const fileInfo = document.getElementById('fileInfo');
const fileInfoName = document.getElementById('fileInfoName');
const fileInfoSize = document.getElementById('fileInfoSize');
if (this.files && this.files[0]) {
const file = this.files[0];
fileInfo.classList.remove('d-none');
fileInfoName.textContent = file.name;
const sizeKB = (file.size / 1024).toFixed(1);
fileInfoSize.textContent = sizeKB + ' KB';
// Update drop zone label
const label = document.getElementById('payloadDropLabel');
label.innerHTML = `<i class="bi bi-check-circle text-success me-1"></i>${file.name}`;
} else {
fileInfo.classList.add('d-none');
}
});
// Character counter
if (messageInput) {
messageInput.addEventListener('input', function() {
const count = this.value.length;
const max = 250000;
const percent = Math.round((count / max) * 100);
document.getElementById('charCount').textContent = count.toLocaleString();
document.getElementById('charPercent').textContent = percent + '%';
const warning = document.getElementById('charWarning');
if (percent >= 80) {
warning.classList.remove('d-none');
} else {
warning.classList.add('d-none');
}
});
}
// Carrier capacity fetching
const capacityPanel = document.getElementById('capacityPanel');
const carrierInput = document.getElementById('carrierInput');
carrierInput.addEventListener('change', function() {
if (this.files && this.files[0]) {
fetchCapacityComparison(this.files[0]);
}
});
function fetchCapacityComparison(file) {
const formData = new FormData();
formData.append('image', file);
fetch('/api/capacity', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.error) {
console.error('Capacity error:', data.error);
return;
}
document.getElementById('carrierDimensions').textContent =
`${data.width} × ${data.height} (${(data.pixels / 1000000).toFixed(1)} MP)`;
document.getElementById('lsbCapacityBadge').textContent =
`LSB: ${data.lsb_capacity_kb} KB`;
document.getElementById('dctCapacityBadge').textContent =
`DCT: ${data.dct_capacity_kb} KB`;
capacityPanel.classList.remove('d-none');
})
.catch(err => console.error('Capacity fetch failed:', err));
}
// ============================================================================
// LSB/DCT Mode Switching
// ============================================================================
const modeLsb = document.getElementById('modeLsb');
const modeDct = document.getElementById('modeDct');
const lsbModeCard = document.getElementById('lsbModeCard');
const dctModeCard = document.getElementById('dctModeCard');
const advancedOptionsContainer = document.getElementById('advancedOptionsContainer');
// DCT Options elements
const dctFormatPng = document.getElementById('dctFormatPng');
const dctFormatJpeg = document.getElementById('dctFormatJpeg');
const dctColorColor = document.getElementById('dctColorColor');
const dctColorGrayscale = document.getElementById('dctColorGrayscale');
const dctPngCard = document.getElementById('dctPngCard');
const dctJpegCard = document.getElementById('dctJpegCard');
const dctColorCard = document.getElementById('dctColorCard');
const dctGrayscaleCard = document.getElementById('dctGrayscaleCard');
function updateModeCardHighlight() {
lsbModeCard.classList.toggle('border-primary', modeLsb.checked);
lsbModeCard.classList.toggle('border-2', modeLsb.checked);
dctModeCard.classList.toggle('border-warning', modeDct.checked);
dctModeCard.classList.toggle('border-2', modeDct.checked);
// Show/hide DCT options when DCT is selected
advancedOptionsContainer.classList.toggle('d-none', !modeDct.checked);
}
function updateDctFormatCardHighlight() {
if (dctPngCard && dctJpegCard) {
dctPngCard.classList.toggle('border-primary', dctFormatPng.checked);
dctPngCard.classList.toggle('border-2', dctFormatPng.checked);
dctJpegCard.classList.toggle('border-warning', dctFormatJpeg.checked);
dctJpegCard.classList.toggle('border-2', dctFormatJpeg.checked);
}
}
function updateDctColorCardHighlight() {
if (dctColorCard && dctGrayscaleCard) {
dctColorCard.classList.toggle('border-success', dctColorColor.checked);
dctColorCard.classList.toggle('border-2', dctColorColor.checked);
dctGrayscaleCard.classList.toggle('border-secondary', dctColorGrayscale.checked);
dctGrayscaleCard.classList.toggle('border-2', dctColorGrayscale.checked);
}
}
modeLsb.addEventListener('change', updateModeCardHighlight);
modeDct.addEventListener('change', updateModeCardHighlight);
dctFormatPng?.addEventListener('change', updateDctFormatCardHighlight);
dctFormatJpeg?.addEventListener('change', updateDctFormatCardHighlight);
dctColorColor?.addEventListener('change', updateDctColorCardHighlight);
dctColorGrayscale?.addEventListener('change', updateDctColorCardHighlight);
updateModeCardHighlight(); // Initial state
updateDctFormatCardHighlight(); // Initial state
updateDctColorCardHighlight(); // Initial state
// Advanced options chevron rotation
const advancedOptionsEl = document.getElementById('advancedOptions');
if (advancedOptionsEl) {
advancedOptionsEl.addEventListener('show.bs.collapse', function() {
document.getElementById('advancedChevron').classList.add('bi-chevron-up');
document.getElementById('advancedChevron').classList.remove('bi-chevron-down');
});
advancedOptionsEl.addEventListener('hide.bs.collapse', function() {
document.getElementById('advancedChevron').classList.remove('bi-chevron-up');
document.getElementById('advancedChevron').classList.add('bi-chevron-down');
});
}
// ============================================================================
// Drag & drop with preview for images
// ============================================================================
document.querySelectorAll('.drop-zone').forEach(zone => {
const input = zone.querySelector('input[type="file"]');
const label = zone.querySelector('.drop-zone-label');
const preview = zone.querySelector('.drop-zone-preview');
const isPayloadZone = zone.id === 'payloadDropZone';
const isCarrierZone = zone.id === 'carrierDropZone';
['dragenter', 'dragover'].forEach(evt => {
zone.addEventListener(evt, e => {
e.preventDefault();
zone.classList.add('drag-over');
});
});
['dragleave', 'drop'].forEach(evt => {
zone.addEventListener(evt, e => {
e.preventDefault();
zone.classList.remove('drag-over');
});
});
zone.addEventListener('drop', e => {
if (e.dataTransfer.files.length) {
input.files = e.dataTransfer.files;
input.dispatchEvent(new Event('change'));
if (!isPayloadZone) {
showPreview(e.dataTransfer.files[0]);
}
// Trigger capacity check for carrier
if (isCarrierZone && e.dataTransfer.files[0]) {
fetchCapacityComparison(e.dataTransfer.files[0]);
}
}
});
if (!isPayloadZone) {
input.addEventListener('change', function() {
if (this.files && this.files[0]) {
showPreview(this.files[0]);
}
});
}
function showPreview(file) {
if (!file.type.startsWith('image/')) return;
const reader = new FileReader();
reader.onload = e => {
if (preview) {
preview.src = e.target.result;
preview.classList.remove('d-none');
}
label.innerHTML = '<i class="bi bi-check-circle text-success me-1"></i>' + file.name;
};
reader.readAsDataURL(file);
}
});
// PIN Toggle Logic
document.getElementById('togglePin').addEventListener('click', function() {
const input = document.getElementById('pinInput');
const icon = this.querySelector('i');
if (input.type === 'password') {
input.type = 'text';
icon.classList.replace('bi-eye', 'bi-eye-slash');
} else {
input.type = 'password';
icon.classList.replace('bi-eye-slash', 'bi-eye');
}
});
// RSA Password Toggle Logic
document.getElementById('toggleRsaPassword')?.addEventListener('click', function() {
const input = document.getElementById('rsaPasswordInput');
const icon = this.querySelector('i');
if (input.type === 'password') {
input.type = 'text';
icon.classList.replace('bi-eye', 'bi-eye-slash');
} else {
input.type = 'password';
icon.classList.replace('bi-eye-slash', 'bi-eye');
}
});
// RSA Input Method Toggle (File vs QR)
const rsaMethodFile = document.getElementById('rsaMethodFile');
const rsaMethodQr = document.getElementById('rsaMethodQr');
const rsaFileSection = document.getElementById('rsaFileSection');
const rsaQrSection = document.getElementById('rsaQrSection');
function updateRsaInputMethod() {
const isFile = rsaMethodFile.checked;
rsaFileSection.classList.toggle('d-none', !isFile);
rsaQrSection.classList.toggle('d-none', isFile);
}
rsaMethodFile?.addEventListener('change', updateRsaInputMethod);
rsaMethodQr?.addEventListener('change', updateRsaInputMethod);
// Prevent Same File Selection
function checkDuplicateFiles() {
const refInput = document.querySelector('input[name="reference_photo"]');
const carInput = document.querySelector('input[name="carrier"]');
if (refInput.files[0] && carInput.files[0]) {
if (refInput.files[0].name === carInput.files[0].name &&
refInput.files[0].size === carInput.files[0].size) {
alert("Security Warning: You cannot use the same image for both Reference and Carrier!");
carInput.value = '';
document.getElementById('carrierPreview').classList.add('d-none');
document.querySelector('#carrierDropZone .drop-zone-label').innerHTML =
'<i class="bi bi-cloud-arrow-up fs-3 d-block mb-2 text-muted"></i>' +
'<span class="text-muted">Drop image or click to browse</span>';
capacityPanel.classList.add('d-none');
}
}
}
document.querySelector('input[name="reference_photo"]').addEventListener('change', checkDuplicateFiles);
document.querySelector('input[name="carrier"]').addEventListener('change', checkDuplicateFiles);
// Paste from Clipboard
document.addEventListener('paste', function(e) {
const items = e.clipboardData.items;
for (let i = 0; i < items.length; i++) {
if (items[i].type.indexOf("image") !== -1) {
const blob = items[i].getAsFile();
const carrierInput = document.querySelector('input[name="carrier"]');
const refInput = document.querySelector('input[name="reference_photo"]');
const targetInput = (!carrierInput.files.length) ? carrierInput : refInput;
const container = new DataTransfer();
container.items.add(blob);
targetInput.files = container.files;
targetInput.dispatchEvent(new Event('change'));
// Trigger capacity check if pasted to carrier
if (targetInput === carrierInput) {
fetchCapacityComparison(blob);
}
break;
}
}
});
// QR Code RSA Key scanning
const rsaQrInput = document.getElementById('rsaQrInput');
if (rsaQrInput) {
rsaQrInput.addEventListener('change', function() {
if (this.files && this.files[0]) {
const formData = new FormData();
formData.append('qr_image', this.files[0]);
fetch('/api/decode-qr', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
if (data.error) {
alert('QR decode failed: ' + data.error);
return;
}
document.getElementById('rsaKeyFromQr').value = data.pem_data;
document.querySelector('#qrDropZone .drop-zone-label').innerHTML =
'<i class="bi bi-check-circle text-success me-1"></i>RSA Key loaded from QR';
})
.catch(err => {
alert('QR decode failed: ' + err);
});
}
});
}
// Form submission with loading state
document.getElementById('encodeForm').addEventListener('submit', function() {
const btn = document.getElementById('encodeBtn');
btn.disabled = true;
btn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Encoding...';
});
</script>
{% endblock %}