688 lines
20 KiB
HTML
688 lines
20 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
|
|
<title>QR Code Parser</title>
|
|
<script src="jsQR.min.js"></script>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
html {
|
|
height: 100%;
|
|
overscroll-behavior: auto;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
}
|
|
|
|
body {
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
min-height: 100dvh;
|
|
min-height: 100%;
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: flex-start;
|
|
padding: calc(env(safe-area-inset-top) + 20px) env(safe-area-inset-right) calc(env(safe-area-inset-bottom) + 20px) env(safe-area-inset-left);
|
|
margin: 0;
|
|
overflow-y: auto;
|
|
-webkit-overflow-scrolling: touch;
|
|
}
|
|
|
|
.container {
|
|
background: white;
|
|
border-radius: 16px;
|
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
|
padding: 40px;
|
|
max-width: 600px;
|
|
width: 100%;
|
|
margin: 20px;
|
|
}
|
|
|
|
h1 {
|
|
text-align: center;
|
|
color: #333;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.subtitle {
|
|
text-align: center;
|
|
color: #666;
|
|
margin-bottom: 30px;
|
|
}
|
|
|
|
.drop-zone {
|
|
border: 3px dashed #667eea;
|
|
border-radius: 12px;
|
|
padding: 60px 20px;
|
|
text-align: center;
|
|
background: #f8f9ff;
|
|
transition: all 0.3s ease;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.drop-zone.drag-over {
|
|
background: #e8eaff;
|
|
border-color: #764ba2;
|
|
transform: scale(1.02);
|
|
}
|
|
|
|
.drop-zone.has-image {
|
|
padding: 20px;
|
|
}
|
|
|
|
.drop-zone-icon {
|
|
font-size: 48px;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.drop-zone-text {
|
|
color: #555;
|
|
font-size: 16px;
|
|
}
|
|
|
|
.drop-zone-text strong {
|
|
color: #667eea;
|
|
}
|
|
|
|
.camera-btn {
|
|
display: block;
|
|
margin: 20px auto 0;
|
|
padding: 12px 30px;
|
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
color: white;
|
|
border: none;
|
|
border-radius: 8px;
|
|
font-size: 16px;
|
|
cursor: pointer;
|
|
transition: transform 0.2s;
|
|
}
|
|
|
|
.camera-btn:hover {
|
|
transform: scale(1.05);
|
|
}
|
|
|
|
.camera-modal {
|
|
display: none;
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: rgba(0, 0, 0, 0.9);
|
|
z-index: 1000;
|
|
justify-content: center;
|
|
align-items: center;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.camera-modal.visible {
|
|
display: flex;
|
|
}
|
|
|
|
#video {
|
|
max-width: 100%;
|
|
max-height: 80vh;
|
|
border-radius: 8px;
|
|
}
|
|
|
|
.close-camera {
|
|
margin-top: 20px;
|
|
padding: 12px 30px;
|
|
background: white;
|
|
color: #333;
|
|
border: none;
|
|
border-radius: 8px;
|
|
font-size: 16px;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.camera-status {
|
|
color: white;
|
|
margin-bottom: 15px;
|
|
font-size: 14px;
|
|
}
|
|
|
|
#fileInput {
|
|
display: none;
|
|
}
|
|
|
|
#preview {
|
|
max-width: 100%;
|
|
max-height: 300px;
|
|
border-radius: 8px;
|
|
display: none;
|
|
margin: 20px auto;
|
|
}
|
|
|
|
#preview.visible {
|
|
display: block;
|
|
}
|
|
|
|
.result {
|
|
margin-top: 30px;
|
|
padding: 20px;
|
|
background: #f0f4ff;
|
|
border-radius: 8px;
|
|
display: none;
|
|
}
|
|
|
|
.result.visible {
|
|
display: block;
|
|
}
|
|
|
|
.result-label {
|
|
font-weight: 600;
|
|
color: #667eea;
|
|
margin-bottom: 10px;
|
|
}
|
|
|
|
.result-text {
|
|
background: white;
|
|
padding: 15px;
|
|
border-radius: 6px;
|
|
word-break: break-all;
|
|
color: #333;
|
|
font-size: 16px;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
.copy-btn {
|
|
margin-top: 10px;
|
|
padding: 10px 20px;
|
|
background: #667eea;
|
|
color: white;
|
|
border: none;
|
|
border-radius: 6px;
|
|
font-size: 14px;
|
|
cursor: pointer;
|
|
transition: background 0.2s;
|
|
}
|
|
|
|
.copy-btn:hover {
|
|
background: #5568d3;
|
|
}
|
|
|
|
.copy-btn.copied {
|
|
background: #00b894;
|
|
}
|
|
|
|
.error {
|
|
margin-top: 20px;
|
|
padding: 15px;
|
|
background: #ffe6e6;
|
|
border-radius: 8px;
|
|
color: #d63031;
|
|
display: none;
|
|
}
|
|
|
|
.error.visible {
|
|
display: block;
|
|
}
|
|
|
|
.loading {
|
|
text-align: center;
|
|
color: #667eea;
|
|
margin-top: 20px;
|
|
display: none;
|
|
}
|
|
|
|
.loading.visible {
|
|
display: block;
|
|
}
|
|
|
|
.history-section {
|
|
margin-top: 30px;
|
|
padding-top: 20px;
|
|
border-top: 2px solid #f0f4ff;
|
|
}
|
|
|
|
.history-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: center;
|
|
margin-bottom: 15px;
|
|
}
|
|
|
|
.history-title {
|
|
font-weight: 600;
|
|
color: #667eea;
|
|
font-size: 18px;
|
|
}
|
|
|
|
.clear-history-btn {
|
|
padding: 8px 16px;
|
|
background: #ffe6e6;
|
|
color: #d63031;
|
|
border: none;
|
|
border-radius: 6px;
|
|
font-size: 14px;
|
|
cursor: pointer;
|
|
transition: background 0.2s;
|
|
}
|
|
|
|
.clear-history-btn:hover {
|
|
background: #ffd6d6;
|
|
}
|
|
|
|
.history-list {
|
|
max-height: 300px;
|
|
overflow-y: auto;
|
|
-webkit-overflow-scrolling: touch;
|
|
}
|
|
|
|
.history-item {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: flex-start;
|
|
padding: 12px;
|
|
background: #f8f9ff;
|
|
border-radius: 8px;
|
|
margin-bottom: 10px;
|
|
transition: transform 0.2s;
|
|
}
|
|
|
|
.history-item:hover {
|
|
transform: translateX(4px);
|
|
}
|
|
|
|
.history-text {
|
|
flex: 1;
|
|
word-break: break-all;
|
|
color: #333;
|
|
font-size: 14px;
|
|
line-height: 1.5;
|
|
margin-right: 10px;
|
|
}
|
|
|
|
.history-time {
|
|
font-size: 12px;
|
|
color: #999;
|
|
margin-top: 4px;
|
|
}
|
|
|
|
.history-actions {
|
|
display: flex;
|
|
gap: 6px;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.delete-btn, .copy-history-btn {
|
|
padding: 6px 10px;
|
|
background: #fff;
|
|
border: 1px solid #ffe6e6;
|
|
border-radius: 4px;
|
|
font-size: 12px;
|
|
cursor: pointer;
|
|
transition: all 0.2s;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.delete-btn {
|
|
color: #d63031;
|
|
}
|
|
|
|
.delete-btn:hover {
|
|
background: #d63031;
|
|
color: white;
|
|
border-color: #d63031;
|
|
}
|
|
|
|
.copy-history-btn {
|
|
color: #667eea;
|
|
border-color: #e8eaff;
|
|
}
|
|
|
|
.copy-history-btn:hover {
|
|
background: #667eea;
|
|
color: white;
|
|
border-color: #667eea;
|
|
}
|
|
|
|
.copy-history-btn.copied {
|
|
background: #00b894;
|
|
color: white;
|
|
border-color: #00b894;
|
|
}
|
|
|
|
.empty-history {
|
|
text-align: center;
|
|
color: #999;
|
|
padding: 20px;
|
|
font-size: 14px;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="container">
|
|
<h1>🔍 QR Code Parser</h1>
|
|
<p class="subtitle">Drag & drop, paste, or click to browse an image</p>
|
|
|
|
<div class="drop-zone" id="dropZone">
|
|
<div class="drop-zone-icon">📁</div>
|
|
<p class="drop-zone-text">
|
|
<strong>Drop image here</strong> or click to browse
|
|
</p>
|
|
<img id="preview" alt="Preview">
|
|
</div>
|
|
<button class="camera-btn" id="cameraBtn">📷 Scan with Camera</button>
|
|
<input type="file" id="fileInput" accept="image/*">
|
|
|
|
<div class="loading" id="loading">Processing...</div>
|
|
<div class="error" id="error"></div>
|
|
|
|
<div class="result" id="result">
|
|
<div class="result-label">📄 Decoded Content:</div>
|
|
<div class="result-text" id="resultText"></div>
|
|
<button class="copy-btn" id="copyBtn">📋 Copy</button>
|
|
</div>
|
|
|
|
<div class="history-section" id="historySection">
|
|
<div class="history-header">
|
|
<div class="history-title">📜 Scan History</div>
|
|
<button class="clear-history-btn" id="clearHistoryBtn">🗑️ Clear All</button>
|
|
</div>
|
|
<div class="history-list" id="historyList"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="camera-modal" id="cameraModal">
|
|
<div class="camera-status" id="cameraStatus">Starting camera...</div>
|
|
<video id="video" autoplay playsinline></video>
|
|
<button class="close-camera" id="closeCamera">Close</button>
|
|
</div>
|
|
|
|
<script>
|
|
const dropZone = document.getElementById('dropZone');
|
|
const fileInput = document.getElementById('fileInput');
|
|
const preview = document.getElementById('preview');
|
|
const result = document.getElementById('result');
|
|
const resultText = document.getElementById('resultText');
|
|
const error = document.getElementById('error');
|
|
const loading = document.getElementById('loading');
|
|
const cameraBtn = document.getElementById('cameraBtn');
|
|
const cameraModal = document.getElementById('cameraModal');
|
|
const closeCamera = document.getElementById('closeCamera');
|
|
const video = document.getElementById('video');
|
|
const cameraStatus = document.getElementById('cameraStatus');
|
|
const copyBtn = document.getElementById('copyBtn');
|
|
const historyList = document.getElementById('historyList');
|
|
const clearHistoryBtn = document.getElementById('clearHistoryBtn');
|
|
const HISTORY_KEY = 'qr_scan_history';
|
|
let stream = null;
|
|
let scanInterval = null;
|
|
|
|
function loadHistory() {
|
|
const stored = localStorage.getItem(HISTORY_KEY);
|
|
return stored ? JSON.parse(stored) : [];
|
|
}
|
|
|
|
function saveHistory(history) {
|
|
localStorage.setItem(HISTORY_KEY, JSON.stringify(history));
|
|
}
|
|
|
|
function addToHistory(text) {
|
|
const history = loadHistory();
|
|
const existingIndex = history.findIndex(item => item.text === text);
|
|
if (existingIndex !== -1) {
|
|
history.splice(existingIndex, 1);
|
|
}
|
|
history.unshift({ text, timestamp: Date.now() });
|
|
if (history.length > 50) {
|
|
history.pop();
|
|
}
|
|
saveHistory(history);
|
|
renderHistory();
|
|
}
|
|
|
|
function deleteFromHistory(index) {
|
|
const history = loadHistory();
|
|
history.splice(index, 1);
|
|
saveHistory(history);
|
|
renderHistory();
|
|
}
|
|
|
|
function clearHistory() {
|
|
localStorage.removeItem(HISTORY_KEY);
|
|
renderHistory();
|
|
}
|
|
|
|
function renderHistory() {
|
|
const history = loadHistory();
|
|
if (history.length === 0) {
|
|
historyList.innerHTML = '<div class="empty-history">No scan history yet</div>';
|
|
return;
|
|
}
|
|
historyList.innerHTML = history.map((item, index) => {
|
|
const date = new Date(item.timestamp);
|
|
const timeStr = date.toLocaleString();
|
|
const displayText = item.text.length > 100 ? item.text.substring(0, 100) + '...' : item.text;
|
|
return `
|
|
<div class="history-item">
|
|
<div class="history-text">
|
|
<div>${escapeHtml(displayText)}</div>
|
|
<div class="history-time">${timeStr}</div>
|
|
</div>
|
|
<div class="history-actions">
|
|
<button class="copy-history-btn" data-index="${index}">📋 Copy</button>
|
|
<button class="delete-btn" data-index="${index}">🗑️ Delete</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
historyList.querySelectorAll('.delete-btn').forEach(btn => {
|
|
btn.addEventListener('click', (e) => {
|
|
const index = parseInt(e.target.dataset.index);
|
|
deleteFromHistory(index);
|
|
});
|
|
});
|
|
historyList.querySelectorAll('.copy-history-btn').forEach(btn => {
|
|
btn.addEventListener('click', (e) => {
|
|
const index = parseInt(e.target.dataset.index);
|
|
const text = history[index].text;
|
|
navigator.clipboard.writeText(text).then(() => {
|
|
e.target.textContent = '✓ Copied!';
|
|
e.target.classList.add('copied');
|
|
setTimeout(() => {
|
|
e.target.textContent = '📋 Copy';
|
|
e.target.classList.remove('copied');
|
|
}, 2000);
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
function escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
|
|
clearHistoryBtn.addEventListener('click', () => {
|
|
if (confirm('Are you sure you want to clear all scan history?')) {
|
|
clearHistory();
|
|
}
|
|
});
|
|
|
|
renderHistory();
|
|
|
|
dropZone.addEventListener('click', () => fileInput.click());
|
|
|
|
dropZone.addEventListener('dragover', (e) => {
|
|
e.preventDefault();
|
|
dropZone.classList.add('drag-over');
|
|
});
|
|
|
|
dropZone.addEventListener('dragleave', () => {
|
|
dropZone.classList.remove('drag-over');
|
|
});
|
|
|
|
dropZone.addEventListener('drop', (e) => {
|
|
e.preventDefault();
|
|
dropZone.classList.remove('drag-over');
|
|
const files = e.dataTransfer.files;
|
|
if (files.length > 0) {
|
|
handleFile(files[0]);
|
|
}
|
|
});
|
|
|
|
document.addEventListener('paste', (e) => {
|
|
const items = e.clipboardData.items;
|
|
for (let i = 0; i < items.length; i++) {
|
|
if (items[i].type.startsWith('image/')) {
|
|
const file = items[i].getAsFile();
|
|
handleFile(file);
|
|
break;
|
|
}
|
|
}
|
|
});
|
|
|
|
fileInput.addEventListener('change', (e) => {
|
|
if (e.target.files.length > 0) {
|
|
handleFile(e.target.files[0]);
|
|
}
|
|
});
|
|
|
|
function handleFile(file) {
|
|
if (!file.type.startsWith('image/')) {
|
|
showError('Please drop an image file');
|
|
return;
|
|
}
|
|
|
|
hideError();
|
|
hideResult();
|
|
showLoading();
|
|
|
|
const reader = new FileReader();
|
|
reader.onload = (e) => {
|
|
preview.src = e.target.result;
|
|
preview.classList.add('visible');
|
|
dropZone.classList.add('has-image');
|
|
|
|
decodeQR(e.target.result);
|
|
};
|
|
reader.readAsDataURL(file);
|
|
}
|
|
|
|
function decodeQR(dataURL) {
|
|
const img = new Image();
|
|
img.onload = () => {
|
|
const canvas = document.createElement('canvas');
|
|
const ctx = canvas.getContext('2d');
|
|
canvas.width = img.width;
|
|
canvas.height = img.height;
|
|
ctx.drawImage(img, 0, 0);
|
|
|
|
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
const code = jsQR(imageData.data, imageData.width, imageData.height);
|
|
|
|
hideLoading();
|
|
|
|
if (code) {
|
|
showResult(code.data);
|
|
} else {
|
|
showError('No QR code found in the image. Please try with a clearer image.');
|
|
}
|
|
};
|
|
img.onerror = () => {
|
|
hideLoading();
|
|
showError('Failed to load image');
|
|
};
|
|
img.src = dataURL;
|
|
}
|
|
|
|
function showResult(text) {
|
|
resultText.textContent = text;
|
|
result.classList.add('visible');
|
|
addToHistory(text);
|
|
}
|
|
|
|
function showError(message) {
|
|
error.textContent = message;
|
|
error.classList.add('visible');
|
|
}
|
|
|
|
function hideError() {
|
|
error.classList.remove('visible');
|
|
}
|
|
|
|
function showLoading() {
|
|
loading.classList.add('visible');
|
|
}
|
|
|
|
function hideLoading() {
|
|
loading.classList.remove('visible');
|
|
}
|
|
|
|
function hideResult() {
|
|
result.classList.remove('visible');
|
|
}
|
|
|
|
copyBtn.addEventListener('click', () => {
|
|
navigator.clipboard.writeText(resultText.textContent).then(() => {
|
|
copyBtn.textContent = '✓ Copied!';
|
|
copyBtn.classList.add('copied');
|
|
setTimeout(() => {
|
|
copyBtn.textContent = '📋 Copy';
|
|
copyBtn.classList.remove('copied');
|
|
}, 2000);
|
|
}).catch(err => {
|
|
console.error('Copy failed:', err);
|
|
});
|
|
});
|
|
|
|
cameraBtn.addEventListener('click', async () => {
|
|
try {
|
|
stream = await navigator.mediaDevices.getUserMedia({
|
|
video: { facingMode: 'environment' }
|
|
});
|
|
video.srcObject = stream;
|
|
cameraModal.classList.add('visible');
|
|
cameraStatus.textContent = 'Scanning...';
|
|
startScan();
|
|
} catch (err) {
|
|
cameraStatus.textContent = 'Camera access denied or not available';
|
|
console.error('Camera error:', err);
|
|
}
|
|
});
|
|
|
|
closeCamera.addEventListener('click', stopCamera);
|
|
|
|
function stopCamera() {
|
|
if (stream) {
|
|
stream.getTracks().forEach(track => track.stop());
|
|
stream = null;
|
|
}
|
|
if (scanInterval) {
|
|
clearInterval(scanInterval);
|
|
scanInterval = null;
|
|
}
|
|
video.srcObject = null;
|
|
cameraModal.classList.remove('visible');
|
|
}
|
|
|
|
function startScan() {
|
|
scanInterval = setInterval(() => {
|
|
if (video.readyState === video.HAVE_ENOUGH_DATA) {
|
|
const canvas = document.createElement('canvas');
|
|
canvas.width = video.videoWidth;
|
|
canvas.height = video.videoHeight;
|
|
const ctx = canvas.getContext('2d');
|
|
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
const code = jsQR(imageData.data, imageData.width, imageData.height);
|
|
|
|
if (code) {
|
|
showResult(code.data);
|
|
stopCamera();
|
|
}
|
|
}
|
|
}, 100);
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|