📁 Add new directory for QR code parsing functionality

This commit is contained in:
2026-04-14 00:32:08 +08:00
parent 5c27fc8b93
commit c8305bf163
5 changed files with 486 additions and 0 deletions

View File

@@ -0,0 +1,8 @@
{
"permissions": {
"allow": [
"Bash(curl *)"
]
},
"$version": 3
}

View File

@@ -0,0 +1,7 @@
{
"permissions": {
"allow": [
"Bash(curl *)"
]
}
}

460
parse-qr/index.html Normal file
View File

@@ -0,0 +1,460 @@
<!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;
}
</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>
<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');
let stream = null;
let scanInterval = null;
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');
}
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>

8
parse-qr/jsQR.min.js vendored Normal file

File diff suppressed because one or more lines are too long

3
parse-qr/site.json Normal file
View File

@@ -0,0 +1,3 @@
{
"path":"parse-qr"
}