Files
stegasoo/frontends/web/templates/encode.html
2025-12-31 13:11:34 -05:00

755 lines
38 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="">
<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-secondary" 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-6 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">
<button class="btn btn-outline-secondary" type="button" id="togglePin">
<i class="bi bi-eye"></i>
</button>
</div>
<div class="form-text">Your static 6-9 digit PIN (if configured)</div>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">
<i class="bi bi-file-earmark-lock me-1"></i> RSA Key
</label>
<ul class="nav nav-tabs nav-tabs-sm mb-2" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active py-1 px-2 small" data-bs-toggle="tab" data-bs-target="#rsaFileTab" type="button">
<i class="bi bi-file-earmark me-1"></i>.pem File
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link py-1 px-2 small" data-bs-toggle="tab" data-bs-target="#rsaQrTab" type="button">
<i class="bi bi-qr-code me-1"></i>QR Code
</button>
</li>
</ul>
<div class="tab-content">
<div class="tab-pane fade show active" id="rsaFileTab" role="tabpanel">
<input type="file" name="rsa_key" class="form-control form-control-sm" id="rsaKeyInput" accept=".pem,.key,application/x-pem-file">
<div class="form-text small">Shared .pem format key file.</div>
</div>
<div class="tab-pane fade" id="rsaQrTab" role="tabpanel">
<input type="file" name="rsa_key_qr" class="form-control form-control-sm" id="rsaKeyQrInput" accept="image/*">
<div class="form-text small">PNG, JPG, or other image of QR code</div>
</div>
</div>
</div>
</div>
<!-- RSA Key Password (shown when key selected) -->
<div class="mb-3 d-none" id="rsaPasswordGroup">
<label class="form-label">
<i class="bi bi-key me-1"></i> RSA Key Password
</label>
<input type="password" name="rsa_password" class="form-control"
placeholder="Password for the .pem file (if encrypted)">
<div class="form-text">
Leave blank if your key file is not password-protected (not needed for QR codes)
</div>
</div>
<!-- ================================================================
ADVANCED OPTIONS (v3.0) - Collapsible Section
================================================================ -->
<div class="mb-4">
<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> Advanced 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">
<!-- Embedding Mode Selection -->
<div class="mb-3">
<label class="form-label">
<i class="bi bi-cpu me-1"></i> Embedding Mode
<span class="badge bg-info ms-1">v3.0</span>
</label>
<div class="row g-2">
<!-- LSB Mode Card -->
<div class="col-md-6">
<div class="form-check card p-3 h-100 border-primary border-2" id="lsbModeCard">
<input class="form-check-input" type="radio" name="embed_mode" id="modeLsb" value="lsb" checked>
<label class="form-check-label w-100" for="modeLsb">
<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">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>Faster processing</li>
</ul>
</label>
</div>
</div>
<!-- DCT Mode Card -->
<div class="col-md-6">
<div class="form-check card p-3 h-100 {% if not has_dct %}opacity-50{% endif %}" id="dctModeCard">
<input class="form-check-input" type="radio" name="embed_mode" id="modeDct" value="dct" {% if not has_dct %}disabled{% endif %}>
<label class="form-check-label w-100" for="modeDct">
<div class="d-flex align-items-center mb-2">
<i class="bi bi-soundwave text-info fs-4 me-2"></i>
<strong>DCT Mode</strong>
{% if has_dct %}
<span class="badge bg-info ms-auto">Stealth</span>
{% else %}
<span class="badge bg-secondary ms-auto">Unavailable</span>
{% endif %}
</div>
<ul class="small text-muted mb-0 ps-3">
<li>Grayscale output (PNG/JPEG)</li>
<li>Lower capacity (~75 KB/MP)</li>
<li>Better detection resistance</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>
</div>
<!-- Mode comparison hint -->
<div class="form-text mt-2" id="modeHint">
<i class="bi bi-lightbulb me-1"></i>
<strong>LSB</strong> is best for most uses.
<strong>DCT</strong> provides better stealth but smaller capacity and grayscale output.
</div>
</div>
<!-- DCT Output Format (shown only when DCT selected) -->
<div class="mb-3 d-none" id="dctOutputFormatGroup">
<label class="form-label">
<i class="bi bi-file-image me-1"></i> DCT 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" checked>
<label class="form-check-label w-100" for="dctFormatPng">
<i class="bi bi-file-earmark-image text-success 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" id="dctJpegCard">
<input class="form-check-input mx-auto" type="radio" name="dct_output_format" id="dctFormatJpeg" value="jpeg">
<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">Smaller, natural</div>
</label>
</div>
</div>
</div>
<div class="form-text mt-2">
<i class="bi bi-info-circle me-1"></i>
<strong>PNG</strong> is 100% reliable. <strong>JPEG</strong> produces smaller, more natural-looking files but uses lossy compression (Q=95).
</div>
</div>
<!-- Capacity Comparison (populated by JS) -->
<div class="d-none" id="modeCapacityComparison">
<div class="alert alert-secondary small mb-0">
<div class="row text-center">
<div class="col-6 border-end">
<div class="text-muted">LSB Capacity</div>
<div class="fs-5 text-primary" id="lsbCapacityDetail">-</div>
</div>
<div class="col-6">
<div class="text-muted">DCT Capacity</div>
<div class="fs-5 text-info" id="dctCapacityDetail">-</div>
</div>
</div>
<div class="text-center mt-2 small text-muted" id="capacityRatio">
DCT is ~20% of LSB capacity
</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 (6000×4000).
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);
// Update required attribute
if (isText) {
messageInput.required = true;
payloadFileInput.required = false;
} else {
messageInput.required = false;
payloadFileInput.required = true;
}
}
payloadTextRadio.addEventListener('change', updatePayloadSection);
payloadFileRadio.addEventListener('change', updatePayloadSection);
// File payload info display
const fileInfo = document.getElementById('fileInfo');
const fileInfoName = document.getElementById('fileInfoName');
const fileInfoSize = document.getElementById('fileInfoSize');
const payloadDropLabel = document.getElementById('payloadDropLabel');
payloadFileInput.addEventListener('change', function() {
if (this.files && this.files[0]) {
const file = this.files[0];
fileInfoName.textContent = file.name;
fileInfoSize.textContent = formatFileSize(file.size);
fileInfo.classList.remove('d-none');
payloadDropLabel.innerHTML = `<i class="bi bi-check-circle text-success fs-3 d-block mb-2"></i><span>${file.name}</span>`;
} else {
fileInfo.classList.add('d-none');
payloadDropLabel.innerHTML = `<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>`;
}
});
function formatFileSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
}
// Show RSA password field when key is selected (only for .pem files, not QR)
const rsaKeyInput = document.getElementById('rsaKeyInput');
const rsaKeyQrInput = document.getElementById('rsaKeyQrInput');
const rsaPasswordGroup = document.getElementById('rsaPasswordGroup');
if (rsaKeyInput) {
rsaKeyInput.addEventListener('change', function() {
// Show password field only for .pem files
rsaPasswordGroup.classList.toggle('d-none', !this.files.length);
// Clear QR input if file is selected
if (rsaKeyQrInput && this.files.length) {
rsaKeyQrInput.value = '';
}
});
}
if (rsaKeyQrInput) {
rsaKeyQrInput.addEventListener('change', function() {
// Hide password field for QR codes (they're unencrypted)
rsaPasswordGroup.classList.add('d-none');
// Clear file input if QR is selected
if (rsaKeyInput && this.files.length) {
rsaKeyInput.value = '';
}
});
}
// Form submit loading state
document.getElementById('encodeForm').addEventListener('submit', function(e) {
const btn = document.getElementById('encodeBtn');
const selectedMode = document.querySelector('input[name="embed_mode"]:checked').value;
let modeLabel = selectedMode.toUpperCase();
if (selectedMode === 'dct') {
const outputFormat = document.querySelector('input[name="dct_output_format"]:checked')?.value || 'png';
modeLabel += `${outputFormat.toUpperCase()}`;
}
btn.innerHTML = `<span class="spinner-border spinner-border-sm me-2"></span>Encoding (${modeLabel})...`;
btn.disabled = true;
});
// Character counter for text
const charCount = document.getElementById('charCount');
const charWarning = document.getElementById('charWarning');
const charPercent = document.getElementById('charPercent');
const maxChars = 250000;
messageInput.addEventListener('input', function() {
const len = this.value.length;
charCount.textContent = len.toLocaleString();
const pct = Math.round((len / maxChars) * 100);
charPercent.textContent = pct + '%';
charWarning.classList.toggle('d-none', len < maxChars * 0.8);
charCount.classList.toggle('text-danger', len > maxChars * 0.95);
});
// ============================================================================
// v3.0 - Capacity Comparison API
// ============================================================================
const carrierInput = document.getElementById('carrierInput');
const capacityPanel = document.getElementById('capacityPanel');
const carrierDimensions = document.getElementById('carrierDimensions');
const lsbCapacityBadge = document.getElementById('lsbCapacityBadge');
const dctCapacityBadge = document.getElementById('dctCapacityBadge');
const lsbCapacityDetail = document.getElementById('lsbCapacityDetail');
const dctCapacityDetail = document.getElementById('dctCapacityDetail');
const modeCapacityComparison = document.getElementById('modeCapacityComparison');
const capacityRatio = document.getElementById('capacityRatio');
let currentCapacity = null;
async function fetchCapacityComparison(file) {
const formData = new FormData();
formData.append('carrier', file);
try {
const response = await fetch('/api/compare-capacity', {
method: 'POST',
body: formData
});
if (response.ok) {
const data = await response.json();
if (data.success) {
currentCapacity = data;
updateCapacityDisplay(data);
}
}
} catch (err) {
console.error('Capacity comparison failed:', err);
}
}
function updateCapacityDisplay(data) {
// Update top panel
carrierDimensions.textContent = `${data.width} × ${data.height}`;
lsbCapacityBadge.textContent = `LSB: ${data.lsb.capacity_kb} KB`;
if (data.dct.available) {
dctCapacityBadge.textContent = `DCT: ${data.dct.capacity_kb} KB`;
dctCapacityBadge.classList.remove('bg-secondary');
dctCapacityBadge.classList.add('bg-info');
} else {
dctCapacityBadge.textContent = `DCT: N/A`;
dctCapacityBadge.classList.remove('bg-info');
dctCapacityBadge.classList.add('bg-secondary');
}
capacityPanel.classList.remove('d-none');
// Update advanced options panel
lsbCapacityDetail.textContent = `${data.lsb.capacity_kb} KB`;
dctCapacityDetail.textContent = data.dct.available ? `${data.dct.capacity_kb} KB` : 'N/A';
capacityRatio.textContent = data.dct.available
? `DCT is ${data.dct.ratio}% of LSB capacity`
: 'DCT mode not available';
modeCapacityComparison.classList.remove('d-none');
}
// Listen for carrier file selection
if (carrierInput) {
carrierInput.addEventListener('change', function() {
if (this.files && this.files[0]) {
fetchCapacityComparison(this.files[0]);
} else {
capacityPanel.classList.add('d-none');
modeCapacityComparison.classList.add('d-none');
currentCapacity = null;
}
});
}
// ============================================================================
// Mode card highlighting & DCT output format visibility
// ============================================================================
const lsbModeCard = document.getElementById('lsbModeCard');
const dctModeCard = document.getElementById('dctModeCard');
const modeLsb = document.getElementById('modeLsb');
const modeDct = document.getElementById('modeDct');
const dctOutputFormatGroup = document.getElementById('dctOutputFormatGroup');
const dctPngCard = document.getElementById('dctPngCard');
const dctJpegCard = document.getElementById('dctJpegCard');
const dctFormatPng = document.getElementById('dctFormatPng');
const dctFormatJpeg = document.getElementById('dctFormatJpeg');
function updateModeCardHighlight() {
// Mode cards
lsbModeCard.classList.toggle('border-primary', modeLsb.checked);
lsbModeCard.classList.toggle('border-2', modeLsb.checked);
dctModeCard.classList.toggle('border-info', modeDct.checked);
dctModeCard.classList.toggle('border-2', modeDct.checked);
// Show/hide DCT output format selector
if (dctOutputFormatGroup) {
dctOutputFormatGroup.classList.toggle('d-none', !modeDct.checked);
}
}
function updateDctFormatCardHighlight() {
if (dctPngCard && dctJpegCard) {
dctPngCard.classList.toggle('border-success', dctFormatPng.checked);
dctPngCard.classList.toggle('border-2', dctFormatPng.checked);
dctJpegCard.classList.toggle('border-warning', dctFormatJpeg.checked);
dctJpegCard.classList.toggle('border-2', dctFormatJpeg.checked);
}
}
modeLsb.addEventListener('change', updateModeCardHighlight);
modeDct.addEventListener('change', updateModeCardHighlight);
dctFormatPng?.addEventListener('change', updateDctFormatCardHighlight);
dctFormatJpeg?.addEventListener('change', updateDctFormatCardHighlight);
updateModeCardHighlight(); // Initial state
updateDctFormatCardHighlight(); // Initial state
// Advanced options chevron rotation
document.getElementById('advancedOptions').addEventListener('show.bs.collapse', function() {
document.getElementById('advancedChevron').classList.add('bi-chevron-up');
document.getElementById('advancedChevron').classList.remove('bi-chevron-down');
});
document.getElementById('advancedOptions').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');
}
});
// 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;
}
}
});
</script>
{% endblock %}