| <!DOCTYPE html> |
| <html lang="fa" dir="rtl"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>آلفا QR کد | ساخت و خواندن</title> |
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/rastikerdar/vazirmatn@v33.003/Vazirmatn-font-face.css"> |
| <style> |
| |
| :root { |
| --app-font: 'Vazirmatn', sans-serif; |
| --app-bg: #F8F9FC; |
| --panel-bg: #FFFFFF; |
| --panel-border: #EAEFF7; |
| --input-bg: #F6F8FB; |
| --input-border: #E1E7EF; |
| --text-primary: #1A202C; |
| --text-secondary: #626F86; |
| --text-tertiary: #8A94A6; |
| --accent-primary: #4A6CFA; |
| --accent-primary-hover: #3553D6; |
| --accent-primary-glow: rgba(74, 108, 250, 0.25); |
| --accent-secondary: #0FD4A8; |
| --accent-secondary-hover: #0DA986; |
| --accent-secondary-glow: rgba(15, 212, 168, 0.2); |
| --shadow-sm: 0 1px 2px 0 rgba(26, 32, 44, 0.03); |
| --shadow-md: 0 4px 6px -1px rgba(26, 32, 44, 0.05), 0 2px 4px -2px rgba(26, 32, 44, 0.04); |
| --shadow-lg: 0 10px 15px -3px rgba(26, 32, 44, 0.06), 0 4px 6px -4px rgba(26, 32, 44, 0.05); |
| --shadow-xl: 0 20px 25px -5px rgba(26, 32, 44, 0.07), 0 8px 10px -6px rgba(26, 32, 44, 0.05); |
| --radius-card: 24px; |
| --radius-btn: 14px; |
| --radius-input: 12px; |
| --transition-smooth: all 0.35s cubic-bezier(0.4, 0, 0.2, 1); |
| } |
| |
| |
| * { |
| margin: 0; |
| padding: 0; |
| box-sizing: border-box; |
| } |
| |
| body { |
| font-family: var(--app-font); |
| background-color: var(--app-bg); |
| color: var(--text-primary); |
| min-height: 100vh; |
| overflow-x: hidden; |
| display: flex; |
| justify-content: center; |
| align-items: flex-start; |
| padding: 2.5rem 1rem; |
| background-image: radial-gradient(var(--text-tertiary) 0.5px, transparent 0.5px); |
| background-size: 20px 20px; |
| background-position: -10px -10px; |
| } |
| |
| .container { |
| max-width: 480px; |
| width: 100%; |
| margin: 0 auto; |
| display: flex; |
| flex-direction: column; |
| } |
| |
| |
| .header { |
| text-align: center; |
| padding: 1rem 0 2rem; |
| } |
| |
| .logo { |
| font-size: 3rem; |
| margin-bottom: 10px; |
| filter: drop-shadow(0 4px 8px rgba(0,0,0,0.1)); |
| } |
| |
| .title { |
| font-size: 2.2rem; |
| font-weight: 900; |
| margin-bottom: 0.8rem; |
| background: linear-gradient(90deg, var(--accent-primary), var(--accent-secondary)); |
| -webkit-background-clip: text; |
| -webkit-text-fill-color: transparent; |
| letter-spacing: -1px; |
| } |
| |
| .subtitle { |
| font-size: 1rem; |
| color: var(--text-secondary); |
| opacity: 0.9; |
| } |
| |
| |
| .tabs { |
| display: flex; |
| background: var(--input-bg); |
| border-radius: var(--radius-btn); |
| padding: 6px; |
| margin-bottom: 20px; |
| border: 1px solid var(--panel-border); |
| } |
| |
| .tab { |
| flex: 1; |
| padding: 12px; |
| text-align: center; |
| border-radius: 10px; |
| cursor: pointer; |
| transition: var(--transition-smooth); |
| font-weight: 600; |
| font-size: 0.9rem; |
| color: var(--text-secondary); |
| } |
| |
| .tab.active { |
| background: var(--panel-bg); |
| color: var(--text-primary); |
| transform: translateY(-2px); |
| box-shadow: var(--shadow-lg); |
| } |
| |
| |
| .card { |
| background: var(--panel-bg); |
| border-radius: var(--radius-card); |
| padding: 30px; |
| border: 1px solid var(--panel-border); |
| box-shadow: var(--shadow-xl); |
| flex: 1; |
| display: none; |
| flex-direction: column; |
| } |
| |
| .tab-content.active { |
| display: flex; |
| } |
| |
| .form-group { |
| margin-bottom: 25px; |
| } |
| |
| .label { |
| display: block; |
| margin-bottom: 12px; |
| font-size: 1rem; |
| font-weight: 700; |
| color: var(--text-primary); |
| } |
| |
| |
| .input { |
| width: 100%; |
| padding: 15px; |
| border: 1px solid var(--input-border); |
| border-radius: var(--radius-input); |
| background: var(--input-bg); |
| color: var(--text-primary); |
| font-size: 1rem; |
| font-family: var(--app-font); |
| min-height: 120px; |
| resize: vertical; |
| outline: none; |
| transition: var(--transition-smooth); |
| box-shadow: var(--shadow-sm) inset; |
| } |
| |
| .input:focus { |
| background: var(--panel-bg); |
| border-color: var(--accent-primary); |
| box-shadow: 0 0 0 3px var(--accent-primary-glow), var(--shadow-sm) inset; |
| } |
| |
| .file-upload { |
| position: relative; |
| background: var(--input-bg); |
| border: 2px dashed var(--input-border); |
| border-radius: var(--radius-input); |
| padding: 30px 20px; |
| text-align: center; |
| cursor: pointer; |
| transition: var(--transition-smooth); |
| min-height: 140px; |
| display: flex; |
| flex-direction: column; |
| align-items: center; |
| justify-content: center; |
| } |
| |
| .file-upload.has-preview { |
| padding: 10px; |
| border-style: solid; |
| border-color: var(--accent-primary); |
| } |
| |
| .file-upload:hover, .file-upload.drag-over { |
| background: white; |
| border-color: var(--accent-primary); |
| box-shadow: 0 0 15px var(--accent-primary-glow); |
| transform: translateY(-2px); |
| } |
| |
| .file-upload input { |
| position: absolute; |
| left: -9999px; |
| } |
| |
| .file-upload img { |
| max-width: 100%; |
| max-height: 120px; |
| height: auto; |
| border-radius: 8px; |
| object-fit: contain; |
| } |
| |
| .upload-icon { |
| font-size: 2.5rem; |
| margin-bottom: 10px; |
| color: var(--accent-primary); |
| } |
| |
| .upload-text { |
| color: var(--text-secondary); |
| font-size: 0.9rem; |
| font-weight: 500; |
| } |
| |
| |
| .btn { |
| width: 100%; |
| padding: 16px; |
| border: none; |
| border-radius: var(--radius-btn); |
| font-size: 1.1rem; |
| font-weight: 700; |
| cursor: pointer; |
| margin-top: auto; |
| transition: var(--transition-smooth); |
| background: linear-gradient(95deg, var(--accent-secondary) 0%, var(--accent-primary) 100%); |
| color: white; |
| box-shadow: 0 6px 12px -3px var(--accent-primary-glow), 0 6px 12px -3px var(--accent-secondary-glow); |
| } |
| |
| .btn:hover:not(:disabled) { |
| transform: translateY(-5px) scale(1.02); |
| box-shadow: 0 8px 20px -4px var(--accent-primary-glow), 0 8px 20px -4px var(--accent-secondary-glow); |
| } |
| |
| .btn:disabled { |
| background: var(--text-tertiary); |
| color: var(--text-secondary); |
| cursor: not-allowed; |
| transform: none; |
| box-shadow: none; |
| } |
| |
| .btn:active:not(:disabled) { |
| transform: translateY(0); |
| } |
| |
| |
| .result { |
| flex: 1; |
| background: var(--input-bg); |
| border: 2px dashed var(--input-border); |
| border-radius: var(--radius-card); |
| padding: 20px; |
| min-height: 150px; |
| display: flex; |
| align-items: center; |
| justify-content: center; |
| text-align: center; |
| margin-bottom: 25px; |
| box-shadow: var(--shadow-sm) inset; |
| } |
| |
| .result-empty { |
| color: var(--text-secondary); |
| font-style: italic; |
| } |
| |
| .result img { |
| max-width: 100%; |
| height: auto; |
| border-radius: var(--radius-input); |
| background: white; |
| padding: 10px; |
| box-shadow: var(--shadow-lg); |
| animation: zoomIn 0.4s ease; |
| border: 1px solid var(--panel-border); |
| } |
| |
| .result-text-wrapper { |
| width: 100%; |
| background: var(--panel-bg); |
| border: 1px solid var(--panel-border); |
| border-radius: var(--radius-input); |
| padding: 12px 18px; |
| display: flex; |
| align-items: center; |
| justify-content: space-between; |
| animation: slideUp 0.4s ease; |
| box-shadow: var(--shadow-md); |
| } |
| .result-text { |
| flex-grow: 1; |
| margin-left: 10px; |
| word-break: break-all; |
| line-height: 1.6; |
| text-align: right; |
| color: var(--text-primary); |
| font-weight: 500; |
| } |
| |
| .copy-btn { |
| background: var(--accent-primary); |
| color: white; |
| border: none; |
| border-radius: 8px; |
| padding: 8px 14px; |
| font-family: var(--app-font); |
| font-weight: 600; |
| cursor: pointer; |
| transition: var(--transition-smooth); |
| flex-shrink: 0; |
| font-size: 0.9rem; |
| box-shadow: var(--shadow-sm); |
| } |
| .copy-btn:hover { |
| background: var(--accent-primary-hover); |
| transform: scale(1.05); |
| } |
| .copy-btn.copied { |
| background-color: var(--accent-secondary); |
| transform: scale(1.05); |
| } |
| |
| |
| .loader { |
| width: 40px; |
| height: 40px; |
| border: 4px solid var(--input-border); |
| border-top: 4px solid var(--accent-primary); |
| border-radius: 50%; |
| animation: spin 1s linear infinite; |
| } |
| |
| @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } |
| @keyframes zoomIn { from { opacity: 0; transform: scale(0.8); } to { opacity: 1; transform: scale(1); } } |
| @keyframes slideUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } } |
| |
| .icon { |
| display: inline-block; |
| margin-left: 8px; |
| vertical-align: middle; |
| } |
| </style> |
| </head> |
| <body> |
| <div class="container"> |
| <div class="header"> |
| <div class="logo">📱</div> |
| <h1 class="title">ساخت Qr Code آلفا</h1> |
| <p class="subtitle">ساخت و خواندن آسان کیوآر کد</p> |
| </div> |
|
|
| <div class="tabs"> |
| <div class="tab active" data-tab="generate"><span class="icon">➕</span>ساخت</div> |
| <div class="tab" data-tab="read"><span class="icon">👁️</span>خواندن</div> |
| </div> |
|
|
| <div class="card tab-content active" id="generate-content"> |
| <div class="form-group"> |
| <label class="label">متن یا لینک خود را بنویسید:</label> |
| <textarea id="text-input" class="input" placeholder="مثال: https://google.com یا سلام دنیا"></textarea> |
| </div> |
| <div class="result" id="qr-result"> |
| <div id="generate-loader" class="loader" style="display: none;"></div> |
| <div id="qr-display"><span class="result-empty">QR کد شما اینجا ظاهر میشود</span></div> |
| </div> |
| <button id="generate-btn" class="btn"><span class="icon">✨</span>ساخت QR کد</button> |
| </div> |
|
|
| <div class="card tab-content" id="read-content"> |
| <div class="form-group"> |
| <label class="label">تصویر QR کد را انتخاب کنید:</label> |
| <div class="file-upload" onclick="document.getElementById('file-input').click()"> |
| <input type="file" id="file-input" accept="image/*"> |
| <div class="upload-content"> |
| <div class="upload-icon">📷</div> |
| <div class="upload-text">انتخاب تصویر</div> |
| </div> |
| </div> |
| </div> |
| <div class="result" id="read-result"> |
| <div id="read-loader" class="loader" style="display: none;"></div> |
| <div id="decoded-text"><span class="result-empty">نتیجه اینجا نمایش داده میشود</span></div> |
| </div> |
| <button id="read-btn" class="btn"><span class="icon">🔍</span>خواندن QR کد</button> |
| </div> |
| </div> |
|
|
| <script> |
| |
| const SPACE_URL = "https://cultrix-qrcode-read-generate.hf.space"; |
| |
| const tabs = document.querySelectorAll('.tab'); |
| const tabContents = document.querySelectorAll('.tab-content'); |
| const generateBtn = document.getElementById('generate-btn'); |
| const readBtn = document.getElementById('read-btn'); |
| const textInput = document.getElementById('text-input'); |
| const fileInput = document.getElementById('file-input'); |
| const qrDisplay = document.getElementById('qr-display'); |
| const decodedText = document.getElementById('decoded-text'); |
| const generateLoader = document.getElementById('generate-loader'); |
| const readLoader = document.getElementById('read-loader'); |
| const fileUpload = document.querySelector('.file-upload'); |
| const uploadContent = document.querySelector('.upload-content'); |
| |
| function copyToClipboard(button, text) { |
| if (button.classList.contains('copied')) return; |
| |
| navigator.clipboard.writeText(text).then(() => { |
| const originalText = button.innerHTML; |
| button.innerHTML = '✓ کپی شد'; |
| button.classList.add('copied'); |
| button.disabled = true; |
| |
| setTimeout(() => { |
| button.innerHTML = originalText; |
| button.classList.remove('copied'); |
| button.disabled = false; |
| }, 2000); |
| }).catch(err => { |
| console.error('Failed to copy text: ', err); |
| alert('کپی کردن با خطا مواجه شد.'); |
| }); |
| } |
| |
| tabs.forEach(tab => { |
| tab.addEventListener('click', () => { |
| tabs.forEach(t => t.classList.remove('active')); |
| tabContents.forEach(tc => tc.classList.remove('active')); |
| tab.classList.add('active'); |
| document.getElementById(tab.getAttribute('data-tab') + '-content').classList.add('active'); |
| }); |
| }); |
| |
| fileInput.addEventListener('change', () => { |
| const file = fileInput.files[0]; |
| if (file) { |
| if (file.type.startsWith('image/')) { |
| const reader = new FileReader(); |
| reader.onload = function(e) { |
| const previewImage = document.createElement('img'); |
| previewImage.src = e.target.result; |
| uploadContent.style.display = 'none'; |
| if(fileUpload.querySelector('img')) fileUpload.querySelector('img').remove(); |
| fileUpload.appendChild(previewImage); |
| fileUpload.classList.add('has-preview'); |
| } |
| reader.readAsDataURL(file); |
| } else { |
| alert('لطفا یک فایل تصویری انتخاب کنید.'); |
| fileInput.value = ''; |
| } |
| } else { |
| if(fileUpload.querySelector('img')) fileUpload.querySelector('img').remove(); |
| uploadContent.style.display = 'block'; |
| uploadContent.querySelector('.upload-icon').textContent = '📷'; |
| uploadContent.querySelector('.upload-text').textContent = 'انتخاب تصویر'; |
| fileUpload.classList.remove('has-preview'); |
| } |
| }); |
| |
| const generateSessionHash = () => Math.random().toString(36).substring(2, 15); |
| |
| const listenForData = (sessionHash, onResult, onError, onFinally) => { |
| const eventSource = new EventSource(`${SPACE_URL}/gradio_api/queue/data?session_hash=${sessionHash}`); |
| eventSource.onmessage = (event) => { |
| const data = JSON.parse(event.data); |
| if (data.msg === "process_completed") { |
| eventSource.close(); |
| if (data.output.error) onError(data.output.error); |
| else onResult(data.output.data); |
| onFinally(); |
| } else if (data.msg === "process_failed") { |
| eventSource.close(); |
| onError("پردازش با خطا مواجه شد."); |
| onFinally(); |
| } |
| }; |
| eventSource.onerror = (err) => { |
| eventSource.close(); |
| onError("خطا در ارتباط با سرور."); |
| onFinally(); |
| }; |
| }; |
| |
| generateBtn.addEventListener('click', async () => { |
| const text = textInput.value.trim(); |
| if (!text) return alert("لطفاً متنی را وارد کنید."); |
| generateLoader.style.display = 'block'; |
| qrDisplay.innerHTML = ''; |
| generateBtn.disabled = true; |
| try { |
| const sessionHash = generateSessionHash(); |
| const res = await fetch(`${SPACE_URL}/gradio_api/queue/join`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ fn_index: 0, data: [text], session_hash: sessionHash }) |
| }); |
| if (!res.ok) throw new Error('خطا در ارسال درخواست'); |
| listenForData(sessionHash, |
| (result) => qrDisplay.innerHTML = (result && result[0]) ? result[0] : '<span class="result-empty">خطا در ساخت</span>', |
| (error) => { alert(`خطا: ${error}`); qrDisplay.innerHTML = '<span class="result-empty">خطا</span>'; }, |
| () => { generateLoader.style.display = 'none'; generateBtn.disabled = false; } |
| ); |
| } catch (error) { |
| alert(`خطا: ${error.message}`); |
| qrDisplay.innerHTML = '<span class="result-empty">خطا در ساخت</span>'; |
| generateLoader.style.display = 'none'; |
| generateBtn.disabled = false; |
| } |
| }); |
| |
| readBtn.addEventListener('click', async () => { |
| const file = fileInput.files[0]; |
| if (!file) return alert("لطفاً یک تصویر انتخاب کنید."); |
| if (!file.type.startsWith('image/')) return alert("لطفاً یک فایل تصویری انتخاب کنید."); |
| |
| readLoader.style.display = 'block'; |
| decodedText.innerHTML = ''; |
| readBtn.disabled = true; |
| try { |
| const formData = new FormData(); |
| formData.append('files', file); |
| const uploadRes = await fetch(`${SPACE_URL}/gradio_api/upload`, { method: 'POST', body: formData }); |
| if (!uploadRes.ok) throw new Error('خطا در آپلود فایل'); |
| const [serverFilePath] = await uploadRes.json(); |
| const fileData = { path: serverFilePath, url: `${SPACE_URL}/gradio_api/file=${serverFilePath}`, orig_name: file.name }; |
| |
| const sessionHash = generateSessionHash(); |
| const joinRes = await fetch(`${SPACE_URL}/gradio_api/queue/join`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ fn_index: 1, data: [fileData], session_hash: sessionHash }) |
| }); |
| if (!joinRes.ok) throw new Error('خطا در پردازش'); |
| |
| listenForData(sessionHash, |
| (result) => { |
| const textToCopy = result[0]; |
| const escapedText = textToCopy.replace(/'/g, "\\'").replace(/"/g, '"'); |
| decodedText.innerHTML = ` |
| <div class="result-text-wrapper"> |
| <span class="result-text">📝 ${textToCopy}</span> |
| <button class="copy-btn" onclick="copyToClipboard(this, '${escapedText}')">کپی</button> |
| </div>`; |
| }, |
| (error) => { alert(`خطا: ${error}`); decodedText.innerHTML = '<span class="result-empty">خطا در خواندن</span>'; }, |
| () => { readLoader.style.display = 'none'; readBtn.disabled = false; } |
| ); |
| } catch (error) { |
| alert(`خطا: ${error.message}`); |
| decodedText.innerHTML = '<span class="result-empty">خطا در پردازش</span>'; |
| readLoader.style.display = 'none'; |
| readBtn.disabled = false; |
| } |
| }); |
| |
| ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => { |
| fileUpload.addEventListener(eventName, e => { e.preventDefault(); e.stopPropagation(); }); |
| }); |
| ['dragenter', 'dragover'].forEach(eventName => { |
| fileUpload.addEventListener(eventName, () => fileUpload.classList.add('drag-over')); |
| }); |
| ['dragleave', 'drop'].forEach(eventName => { |
| fileUpload.addEventListener(eventName, () => fileUpload.classList.remove('drag-over')); |
| }); |
| fileUpload.addEventListener('drop', e => { |
| if (e.dataTransfer.files.length > 0) { |
| fileInput.files = e.dataTransfer.files; |
| fileInput.dispatchEvent(new Event('change')); |
| } |
| }); |
| </script> |
| </body> |
| </html> |