Files
stegasoo/frontends/web/templates/encode.html
2026-01-01 22:51:53 -05:00

955 lines
44 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 %}
<style>
/* Glowing passphrase input */
.passphrase-input-container {
position: relative;
}
.passphrase-input {
background: rgba(30, 40, 50, 0.8) !important;
border: 2px solid rgba(99, 179, 237, 0.3) !important;
color: #63b3ed !important;
font-family: 'Courier New', monospace;
font-size: 1.1rem;
letter-spacing: 0.5px;
padding: 12px 16px;
transition: border-color 0.3s ease, box-shadow 0.3s ease, background 0.3s ease;
}
.passphrase-input:focus {
border-color: rgba(99, 179, 237, 0.8) !important;
box-shadow: 0 0 20px rgba(99, 179, 237, 0.4), 0 0 40px rgba(99, 179, 237, 0.2) !important;
background: rgba(30, 40, 50, 0.95) !important;
}
.passphrase-input::placeholder {
color: rgba(99, 179, 237, 0.4);
}
/* Glowing PIN input */
.pin-input-container .form-control {
background: rgba(30, 40, 50, 0.8) !important;
border: 2px solid rgba(246, 173, 85, 0.3) !important;
color: #f6ad55 !important;
font-family: 'Courier New', monospace;
font-size: 1.2rem;
letter-spacing: 3px;
text-align: center;
transition: all 0.3s ease;
}
.pin-input-container .form-control:focus {
border-color: rgba(246, 173, 85, 0.8) !important;
box-shadow: 0 0 20px rgba(246, 173, 85, 0.4), 0 0 40px rgba(246, 173, 85, 0.2) !important;
background: rgba(30, 40, 50, 0.95) !important;
}
.pin-input-container .form-control::placeholder {
color: rgba(246, 173, 85, 0.4);
letter-spacing: 1px;
}
/* QR Crop Animation */
.qr-crop-container {
position: relative;
overflow: hidden;
border-radius: 8px;
background: rgba(0, 0, 0, 0.3);
}
.qr-crop-container img {
display: block;
max-height: 120px;
width: auto;
margin: 0 auto;
transition: all 0.6s cubic-bezier(0.4, 0, 0.2, 1);
}
.qr-crop-container .qr-original {
opacity: 1;
}
.qr-crop-container .qr-cropped {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%) scale(0.3);
opacity: 0;
max-height: 100px;
}
.qr-crop-container.animating .qr-original {
opacity: 0;
transform: scale(1.1);
filter: blur(4px);
}
.qr-crop-container.animating .qr-cropped {
opacity: 1;
transform: translate(-50%, -50%) scale(1);
}
.qr-crop-container .crop-badge {
position: absolute;
bottom: 4px;
right: 4px;
font-size: 0.65rem;
opacity: 0;
transition: opacity 0.3s ease 0.4s;
}
.qr-crop-container.animating .crop-badge {
opacity: 1;
}
</style>
<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">
<!-- Removed client_date hidden field -->
<!-- Embedding Mode Selection -->
<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>
<!-- Passphrase input with glow styling -->
<div class="mb-3">
<label class="form-label" id="passphraseLabel">
<i class="bi bi-chat-quote me-1"></i> Passphrase
</label>
<div class="passphrase-input-container">
<input type="text" name="passphrase" class="form-control passphrase-input"
placeholder="e.g., apple forest thunder mountain" required
id="passphraseInput">
</div>
<div class="form-text">
Your passphrase for this message
</div>
<div class="form-text mt-1" id="passphraseWarning" style="display: none;">
<i class="bi bi-exclamation-triangle text-warning me-1"></i>
Passphrase should have at least {{ recommended_passphrase_words }} words for good security
</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 pin-input-container">
<input type="password" name="pin" class="form-control" id="pinInput" placeholder="••••••" maxlength="9" style="max-width: 180px;">
<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" 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_key_qr" 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>
<!-- Crop animation container -->
<div class="qr-crop-container d-none" id="qrCropContainer">
<img class="qr-original" id="qrOriginal" alt="Original">
<img class="qr-cropped" id="qrCropped" alt="Cropped QR">
<span class="crop-badge badge bg-success"><i class="bi bi-crop me-1"></i>Detected</span>
</div>
</div>
</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>
// v3.2.0: Removed date detection - no longer needed!
// 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);
// Passphrase validation and auto-resize font
const passphraseInput = document.getElementById('passphraseInput');
const passphraseWarning = document.getElementById('passphraseWarning');
// Stepped font sizes (characters -> rem)
const fontSizeSteps = [
{ maxChars: 30, size: 1.1 },
{ maxChars: 45, size: 1.0 },
{ maxChars: 60, size: 0.95 },
{ maxChars: Infinity, size: 0.9 }
];
function adjustPassphraseFontSize() {
if (!passphraseInput) return;
const len = passphraseInput.value.length;
for (const step of fontSizeSteps) {
if (len <= step.maxChars) {
passphraseInput.style.fontSize = step.size + 'rem';
break;
}
}
}
if (passphraseInput) {
passphraseInput.addEventListener('input', function() {
// Word count warning
const words = this.value.trim().split(/\s+/).filter(w => w.length > 0);
const recommendedWords = {{ recommended_passphrase_words }};
if (words.length > 0 && words.length < recommendedWords) {
passphraseWarning.style.display = 'block';
} else {
passphraseWarning.style.display = 'none';
}
// Auto-resize font
adjustPassphraseFontSize();
});
// Initial font size adjustment
adjustPassphraseFontSize();
}
// 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('carrier', file);
fetch('/api/compare-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.width * data.height / 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 with crop animation
const rsaQrInput = document.getElementById('rsaQrInput');
const qrCropContainer = document.getElementById('qrCropContainer');
const qrOriginal = document.getElementById('qrOriginal');
const qrCropped = document.getElementById('qrCropped');
if (rsaQrInput) {
rsaQrInput.addEventListener('change', function() {
if (this.files && this.files[0]) {
const file = this.files[0];
if (!file.type.startsWith('image/')) return;
// Reset animation state
qrCropContainer.classList.remove('animating');
qrCropContainer.classList.add('d-none');
// Show original image first
const reader = new FileReader();
reader.onload = e => {
qrOriginal.src = e.target.result;
qrCropContainer.classList.remove('d-none');
// Hide the label
document.querySelector('#qrDropZone .drop-zone-label').classList.add('d-none');
// Now fetch cropped version and animate
const formData = new FormData();
formData.append('image', file);
fetch('/qr/crop', {
method: 'POST',
body: formData
})
.then(response => {
if (!response.ok) {
throw new Error('No QR code detected');
}
return response.blob();
})
.then(blob => {
// Load cropped image
const croppedUrl = URL.createObjectURL(blob);
qrCropped.src = croppedUrl;
// Wait a moment to show original, then animate
setTimeout(() => {
qrCropContainer.classList.add('animating');
}, 400);
// Also verify key extraction works
const keyFormData = new FormData();
keyFormData.append('qr_image', file);
return fetch('/extract-key-from-qr', {
method: 'POST',
body: keyFormData
});
})
.then(response => response.json())
.then(data => {
if (data.success) {
document.querySelector('#qrDropZone .drop-zone-label').innerHTML =
'<i class="bi bi-check-circle text-success me-1"></i>RSA Key loaded';
document.querySelector('#qrDropZone .drop-zone-label').classList.remove('d-none');
}
})
.catch(err => {
// Crop failed - just show original with error
console.log('QR crop/extract error:', err);
document.querySelector('#qrDropZone .drop-zone-label').innerHTML =
'<i class="bi bi-exclamation-triangle text-warning me-1"></i>No QR detected';
document.querySelector('#qrDropZone .drop-zone-label').classList.remove('d-none');
});
};
reader.readAsDataURL(file);
}
});
}
// 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 %}