Elias207 commited on
Commit
942c2eb
·
verified ·
1 Parent(s): 7ad82f5

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +544 -680
index.html CHANGED
@@ -1,705 +1,569 @@
1
  <!DOCTYPE html>
2
  <html lang="fa" dir="rtl">
3
  <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>ابزار مدرن QR Code</title>
7
- <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/rastikerdar/vazirmatn@v33.003/Vazirmatn-font-face.css">
8
- <style>
9
- :root {
10
- --primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
11
- --secondary-gradient: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
12
- --background: linear-gradient(135deg, #1e3c72 0%, #2a5298 50%, #1e3c72 100%);
13
- --card-bg: rgba(255, 255, 255, 0.1);
14
- --text-primary: #ffffff;
15
- --text-secondary: #e8eaed;
16
- --border-color: rgba(255, 255, 255, 0.15);
17
- --shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
18
- --glow: 0 0 30px rgba(102, 126, 234, 0.5);
19
- }
20
-
21
- * {
22
- margin: 0;
23
- padding: 0;
24
- box-sizing: border-box;
25
- }
26
-
27
- body {
28
- font-family: 'Vazirmatn', sans-serif;
29
- background: var(--background);
30
- color: var(--text-primary);
31
- min-height: 100vh;
32
- padding: 20px;
33
- overflow-x: hidden;
34
- }
35
-
36
- /* Background Animations */
37
- body::before {
38
- content: '';
39
- position: fixed;
40
- top: 0;
41
- left: 0;
42
- width: 100%;
43
- height: 100%;
44
- background: url('data:image/svg+xml,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><defs><pattern id="qr" x="0" y="0" width="20" height="20" patternUnits="userSpaceOnUse"><rect width="8" height="8" fill="rgba(255,255,255,0.03)"/><rect x="12" y="0" width="8" height="8" fill="rgba(255,255,255,0.03)"/><rect x="0" y="12" width="8" height="8" fill="rgba(255,255,255,0.03)"/><rect x="12" y="12" width="8" height="8" fill="rgba(255,255,255,0.03)"/></pattern></defs><rect width="100" height="100" fill="url(%23qr)"/></svg>');
45
- z-index: -1;
46
- animation: backgroundMove 20s ease-in-out infinite;
47
- }
48
-
49
- @keyframes backgroundMove {
50
- 0%, 100% { transform: translate(0, 0) rotate(0deg); }
51
- 25% { transform: translate(-10px, -10px) rotate(1deg); }
52
- 50% { transform: translate(10px, -5px) rotate(-1deg); }
53
- 75% { transform: translate(-5px, 10px) rotate(0.5deg); }
54
- }
55
-
56
- .main-header {
57
- text-align: center;
58
- margin-bottom: 40px;
59
- animation: slideDown 1s ease-out;
60
- }
61
-
62
- .main-title {
63
- font-size: clamp(2rem, 5vw, 3.5rem);
64
- font-weight: 300;
65
- margin-bottom: 10px;
66
- text-shadow: 0 4px 8px rgba(0,0,0,0.5);
67
- position: relative;
68
- }
69
-
70
- .main-title::after {
71
- content: '';
72
- position: absolute;
73
- bottom: -10px;
74
- left: 50%;
75
- transform: translateX(-50%);
76
- width: 80px;
77
- height: 4px;
78
- background: var(--primary-gradient);
79
- border-radius: 2px;
80
- box-shadow: var(--glow);
81
- }
82
-
83
- .main-title .highlight {
84
- background: var(--primary-gradient);
85
- -webkit-background-clip: text;
86
- -webkit-text-fill-color: transparent;
87
- background-clip: text;
88
- font-weight: 700;
89
- }
90
-
91
- .subtitle {
92
- font-size: 1.1rem;
93
- color: var(--text-secondary);
94
- opacity: 0.8;
95
- }
96
-
97
- .app-container {
98
- display: flex;
99
- gap: 30px;
100
- max-width: 1200px;
101
- margin: 0 auto;
102
- flex-wrap: wrap;
103
- justify-content: center;
104
- }
105
-
106
- .card {
107
- flex: 1;
108
- min-width: 320px;
109
- max-width: 500px;
110
- background: var(--card-bg);
111
- backdrop-filter: blur(20px);
112
- -webkit-backdrop-filter: blur(20px);
113
- border: 1px solid var(--border-color);
114
- border-radius: 20px;
115
- padding: 30px;
116
- box-shadow: var(--shadow);
117
- transition: all 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94);
118
- position: relative;
119
- overflow: hidden;
120
- animation: slideUp 1s ease-out;
121
- }
122
-
123
- .card::before {
124
- content: '';
125
- position: absolute;
126
- top: 0;
127
- left: 0;
128
- right: 0;
129
- height: 4px;
130
- background: var(--primary-gradient);
131
- transform: scaleX(0);
132
- transition: transform 0.4s ease;
133
- }
134
-
135
- .card:hover {
136
- transform: translateY(-10px) scale(1.02);
137
- box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
138
- }
139
-
140
- .card:hover::before {
141
- transform: scaleX(1);
142
- }
143
-
144
- .card-header {
145
- text-align: center;
146
- margin-bottom: 25px;
147
- position: relative;
148
- }
149
-
150
- .card-icon {
151
- width: 60px;
152
- height: 60px;
153
- margin: 0 auto 15px;
154
- border-radius: 50%;
155
- display: flex;
156
- align-items: center;
157
- justify-content: center;
158
- font-size: 24px;
159
- color: white;
160
- box-shadow: 0 8px 20px rgba(0, 0, 0, 0.3);
161
- animation: pulse 2s infinite;
162
- }
163
-
164
- .generate-card .card-icon {
165
- background: var(--primary-gradient);
166
- }
167
-
168
- .read-card .card-icon {
169
- background: var(--secondary-gradient);
170
- }
171
-
172
- @keyframes pulse {
173
- 0%, 100% { transform: scale(1); }
174
- 50% { transform: scale(1.1); }
175
- }
176
-
177
- .card-title {
178
- font-size: 1.5rem;
179
- font-weight: 600;
180
- margin-bottom: 8px;
181
- }
182
-
183
- .card-subtitle {
184
- color: var(--text-secondary);
185
- font-size: 0.9rem;
186
- opacity: 0.8;
187
- }
188
-
189
- .form-group {
190
- margin-bottom: 20px;
191
- }
192
-
193
- .form-label {
194
- display: block;
195
- margin-bottom: 8px;
196
- font-size: 0.9rem;
197
- color: var(--text-secondary);
198
- font-weight: 500;
199
- }
200
-
201
- .form-input {
202
- width: 100%;
203
- padding: 15px;
204
- border: 2px solid transparent;
205
- border-radius: 12px;
206
- background: rgba(255, 255, 255, 0.08);
207
- color: var(--text-primary);
208
- font-size: 1rem;
209
- transition: all 0.3s ease;
210
- backdrop-filter: blur(10px);
211
- }
212
-
213
- .form-input:focus {
214
- outline: none;
215
- border-color: #667eea;
216
- background: rgba(255, 255, 255, 0.12);
217
- box-shadow: 0 0 20px rgba(102, 126, 234, 0.3);
218
- }
219
-
220
- .form-input::placeholder {
221
- color: rgba(255, 255, 255, 0.5);
222
- }
223
-
224
- .file-input-wrapper {
225
- position: relative;
226
- cursor: pointer;
227
- }
228
-
229
- .file-input {
230
- display: none;
231
- }
232
-
233
- .file-label {
234
- display: flex;
235
- align-items: center;
236
- justify-content: center;
237
- gap: 10px;
238
- padding: 15px;
239
- border: 2px dashed rgba(255, 255, 255, 0.3);
240
- border-radius: 12px;
241
- background: rgba(255, 255, 255, 0.05);
242
- transition: all 0.3s ease;
243
- cursor: pointer;
244
- }
245
-
246
- .file-label:hover {
247
- border-color: #f5576c;
248
- background: rgba(245, 87, 108, 0.1);
249
- transform: scale(1.02);
250
- }
251
-
252
- .file-name {
253
- margin-top: 10px;
254
- font-size: 0.85rem;
255
- color: var(--text-secondary);
256
- text-align: center;
257
- min-height: 20px;
258
- }
259
-
260
- .btn {
261
- width: 100%;
262
- padding: 15px;
263
- border: none;
264
- border-radius: 12px;
265
- font-size: 1.1rem;
266
- font-weight: 600;
267
- cursor: pointer;
268
- transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
269
- position: relative;
270
- overflow: hidden;
271
- display: flex;
272
- align-items: center;
273
- justify-content: center;
274
- gap: 10px;
275
- color: white;
276
- text-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
277
- }
278
-
279
- .btn::before {
280
- content: '';
281
- position: absolute;
282
- top: 0;
283
- left: -100%;
284
- width: 100%;
285
- height: 100%;
286
- background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
287
- transition: left 0.5s;
288
- }
289
-
290
- .btn:hover::before {
291
- left: 100%;
292
- }
293
-
294
- .btn-generate {
295
- background: var(--primary-gradient);
296
- box-shadow: 0 8px 20px rgba(102, 126, 234, 0.4);
297
- }
298
-
299
- .btn-read {
300
- background: var(--secondary-gradient);
301
- box-shadow: 0 8px 20px rgba(245, 87, 108, 0.4);
302
- }
303
-
304
- .btn:hover:not(:disabled) {
305
- transform: translateY(-3px);
306
- box-shadow: 0 12px 30px rgba(0, 0, 0, 0.4);
307
- }
308
-
309
- .btn:disabled {
310
- opacity: 0.6;
311
- cursor: not-allowed;
312
- transform: none !important;
313
- }
314
-
315
- .result-container {
316
- margin-top: 20px;
317
- padding: 20px;
318
- background: rgba(255, 255, 255, 0.05);
319
- border-radius: 12px;
320
- border: 1px solid var(--border-color);
321
- min-height: 120px;
322
- display: flex;
323
- align-items: center;
324
- justify-content: center;
325
- text-align: center;
326
- position: relative;
327
- overflow: hidden;
328
- }
329
-
330
- .loader {
331
- width: 40px;
332
- height: 40px;
333
- border: 4px solid rgba(255, 255, 255, 0.1);
334
- border-top: 4px solid #667eea;
335
- border-radius: 50%;
336
- animation: spin 1s linear infinite;
337
- display: none;
338
- }
339
-
340
- @keyframes spin {
341
- 0% { transform: rotate(0deg); }
342
- 100% { transform: rotate(360deg); }
343
- }
344
-
345
- .qr-result img {
346
- max-width: 100%;
347
- height: auto;
348
- border-radius: 8px;
349
- box-shadow: 0 4px 15px rgba(0, 0, 0, 0.3);
350
- animation: zoomIn 0.5s ease-out;
351
- }
352
-
353
- @keyframes zoomIn {
354
- from {
355
- opacity: 0;
356
- transform: scale(0.8);
357
- }
358
- to {
359
- opacity: 1;
360
- transform: scale(1);
361
- }
362
- }
363
-
364
- .text-result {
365
- word-wrap: break-word;
366
- line-height: 1.6;
367
- font-size: 1rem;
368
- }
369
-
370
- /* انیمیشن‌های ورود */
371
- @keyframes slideDown {
372
- from {
373
- opacity: 0;
374
- transform: translateY(-50px);
375
- }
376
- to {
377
- opacity: 1;
378
- transform: translateY(0);
379
- }
380
- }
381
-
382
- @keyframes slideUp {
383
- from {
384
- opacity: 0;
385
- transform: translateY(50px);
386
- }
387
- to {
388
- opacity: 1;
389
- transform: translateY(0);
390
- }
391
- }
392
-
393
- /* Responsive Design */
394
- @media (max-width: 768px) {
395
- .app-container {
396
- flex-direction: column;
397
- gap: 20px;
398
- }
399
-
400
- .card {
401
- min-width: 100%;
402
- padding: 25px;
403
- }
404
-
405
- body {
406
- padding: 15px;
407
- }
408
- }
409
-
410
- @media (max-width: 480px) {
411
- .main-title {
412
- font-size: 2rem;
413
- }
414
-
415
- .card {
416
- padding: 20px;
417
- }
418
- }
419
- </style>
420
  </head>
421
  <body>
422
- <header class="main-header">
423
- <h1 class="main-title">ابزار مدرن <span class="highlight">QR Code</span></h1>
424
- <p class="subtitle">ساخت و خواندن کدهای QR به سادگی</p>
 
425
  </header>
426
 
427
- <div class="app-container">
428
- <!-- کارت ساخت QR Code -->
429
- <div class="card generate-card">
430
- <div class="card-header">
431
- <div class="card-icon">
432
- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
433
- <rect x="3" y="3" width="7" height="7"/>
434
- <rect x="14" y="3" width="7" height="7"/>
435
- <rect x="14" y="14" width="7" height="7"/>
436
- <rect x="3" y="14" width="7" height="7"/>
437
- </svg>
438
- </div>
439
- <h2 class="card-title">ساخت QR Code</h2>
440
- <p class="card-subtitle">متن خود را به کد QR تبدیل کنید</p>
441
- </div>
442
-
443
- <div class="form-group">
444
- <label class="form-label" for="text-input">متن یا لینک خود را وارد کنید:</label>
445
- <textarea id="text-input" class="form-input" rows="4" placeholder="مثال: https://google.com یا سلام دنیا">سلام دنیا</textarea>
446
- </div>
447
-
448
- <button id="generate-btn" class="btn btn-generate">
449
- <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
450
- <path d="M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2v0z"/>
451
- <path d="M12 8l4 4-4 4"/>
452
- <path d="M8 12h7"/>
453
- </svg>
454
- <span>ساخت QR Code</span>
455
- </button>
456
-
457
- <div class="result-container">
458
- <div id="generate-loader" class="loader"></div>
459
- <div id="qr-code-display" class="qr-result"></div>
460
- </div>
461
- </div>
462
 
463
- <!-- کارت خواندن QR Code -->
464
- <div class="card read-card">
465
- <div class="card-header">
466
- <div class="card-icon">
467
- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
468
- <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
469
- <circle cx="12" cy="12" r="3"/>
470
- </svg>
471
- </div>
472
- <h2 class="card-title">خواندن QR Code</h2>
473
- <p class="card-subtitle">تصویر QR Code را آپلود کرده و متن آن را بخوانید</p>
474
- </div>
475
-
476
- <div class="form-group">
477
- <label class="form-label" for="file-input">تصویر QR Code را انتخاب کنید:</label>
478
- <div class="file-input-wrapper">
479
- <input type="file" id="file-input" class="file-input" accept="image/*">
480
- <label for="file-input" class="file-label">
481
- <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
482
- <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
483
- <polyline points="17 8 12 3 7 8"/>
484
- <line x1="12" y1="3" x2="12" y2="15"/>
485
- </svg>
486
- <span>انتخاب تصویر</span>
487
- </label>
488
- </div>
489
- <p id="file-name" class="file-name"></p>
490
- </div>
491
-
492
- <button id="read-btn" class="btn btn-read">
493
- <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
494
- <circle cx="11" cy="11" r="8"/>
495
- <path d="m21 21-4.35-4.35"/>
496
- </svg>
497
- <span>خواندن QR Code</span>
498
- </button>
499
-
500
- <div class="result-container">
501
- <div id="read-loader" class="loader"></div>
502
- <p id="decoded-text" class="text-result">نتیجه در اینجا نمایش داده می‌شود</p>
503
- </div>
504
  </div>
505
- </div>
506
 
507
- <script>
508
- const SPACE_URL = "https://cultrix-qrcode-read-generate.hf.space";
509
-
510
- // --- دریافت عناصر DOM ---
511
- const generateBtn = document.getElementById('generate-btn');
512
- const readBtn = document.getElementById('read-btn');
513
- const textInput = document.getElementById('text-input');
514
- const fileInput = document.getElementById('file-input');
515
- const qrCodeDisplay = document.getElementById('qr-code-display');
516
- const decodedText = document.getElementById('decoded-text');
517
- const generateLoader = document.getElementById('generate-loader');
518
- const readLoader = document.getElementById('read-loader');
519
- const fileNameDisplay = document.getElementById('file-name');
520
-
521
- // نمایش نام فایل انتخاب شده
522
- fileInput.addEventListener('change', () => {
523
- if (fileInput.files.length > 0) {
524
- fileNameDisplay.textContent = `فایل انتخاب شده: ${fileInput.files[0].name}`;
525
- fileNameDisplay.style.color = '#4ade80';
526
- } else {
527
- fileNameDisplay.textContent = '';
528
- }
529
- });
530
 
531
- // --- توابع کمکی ---
532
- const generateSessionHash = () => Math.random().toString(36).substring(2, 15);
533
-
534
- const listenForData = (sessionHash, onResult, onError) => {
535
- const eventSource = new EventSource(`${SPACE_URL}/gradio_api/queue/data?session_hash=${sessionHash}`);
536
-
537
- eventSource.onmessage = (event) => {
538
- const data = JSON.parse(event.data);
539
- if (data.msg === "process_completed") {
540
- eventSource.close();
541
- if (data.output.error) {
542
- onError(data.output.error);
543
- } else {
544
- onResult(data.output.data);
545
- }
546
- } else if (data.msg === "process_starts") {
547
- console.log("پردازش شروع شد...");
548
- } else if (data.msg === "process_failed") {
549
- eventSource.close();
550
- onError("پردازش با خطا مواجه شد.");
551
- }
552
- };
553
-
554
- eventSource.onerror = (err) => {
555
- console.error("EventSource failed:", err);
556
- eventSource.close();
557
- onError("خطا در ارتباط با سرور.");
558
- };
559
- };
 
 
 
 
 
 
 
560
 
561
- // --- منطق ساخت QR Code ---
562
- generateBtn.addEventListener('click', async () => {
563
- const text = textInput.value.trim();
564
- if (!text) {
565
- textInput.focus();
566
- return;
567
- }
 
 
568
 
569
- generateLoader.style.display = 'block';
570
- qrCodeDisplay.innerHTML = '';
571
- generateBtn.disabled = true;
572
-
573
- const sessionHash = generateSessionHash();
574
-
575
- try {
576
- const joinResponse = await fetch(`${SPACE_URL}/gradio_api/queue/join`, {
577
- method: 'POST',
578
- headers: { 'Content-Type': 'application/json' },
579
- body: JSON.stringify({ fn_index: 0, data: [text], session_hash: sessionHash })
580
- });
581
-
582
- if (!joinResponse.ok) throw new Error(`خطا در ارسال درخواست: ${joinResponse.statusText}`);
583
-
584
- listenForData(
585
- sessionHash,
586
- (result) => {
587
- if (result && result[0]) {
588
- qrCodeDisplay.innerHTML = result[0];
589
- } else {
590
- qrCodeDisplay.innerHTML = '<p style="color: #ef4444;">خطا: خروجی معتبر دریافت نشد</p>';
591
- }
592
- },
593
- (error) => {
594
- qrCodeDisplay.innerHTML = `<p style="color: #ef4444;">خطا در ساخت: ${error}</p>`;
595
- }
596
- );
597
- } catch (error) {
598
- qrCodeDisplay.innerHTML = `<p style="color: #ef4444;">خطا: ${error.message}</p>`;
599
- } finally {
600
- generateLoader.style.display = 'none';
601
- generateBtn.disabled = false;
602
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
603
  });
604
 
605
- // --- منطق خواندن QR Code ---
606
- readBtn.addEventListener('click', async () => {
607
- const file = fileInput.files[0];
608
- if (!file) {
609
- fileInput.click();
610
- return;
611
- }
612
 
613
- readLoader.style.display = 'block';
614
- decodedText.innerHTML = '';
615
- readBtn.disabled = true;
616
-
617
- try {
618
- const formData = new FormData();
619
- formData.append('files', file);
620
-
621
- const uploadResponse = await fetch(`${SPACE_URL}/gradio_api/upload`, {
622
- method: 'POST',
623
- body: formData
624
- });
625
- if (!uploadResponse.ok) throw new Error(`خطا در آپلود فایل: ${uploadResponse.statusText}`);
626
-
627
- const uploadResult = await uploadResponse.json();
628
- const serverFilePath = uploadResult[0];
629
-
630
- const fileDataObject = {
631
- path: serverFilePath,
632
- url: `${SPACE_URL}/gradio_api/file=${serverFilePath}`,
633
- orig_name: file.name,
634
- };
635
-
636
- const sessionHash = generateSessionHash();
637
- const joinResponse = await fetch(`${SPACE_URL}/gradio_api/queue/join`, {
638
- method: 'POST',
639
- headers: { 'Content-Type': 'application/json' },
640
- body: JSON.stringify({ fn_index: 1, data: [fileDataObject], session_hash: sessionHash })
641
- });
642
- if (!joinResponse.ok) throw new Error(`خطا در ارسال به صف: ${joinResponse.statusText}`);
643
-
644
- listenForData(
645
- sessionHash,
646
- (result) => {
647
- decodedText.innerHTML = `<strong style="color: #4ade80;">متن خوانده شده:</strong><br><br>${result[0]}`;
648
- },
649
- (error) => {
650
- decodedText.innerHTML = `<p style="color: #ef4444;">خطا در خواندن: ${error}</p>`;
651
- }
652
- );
653
- } catch (error) {
654
- decodedText.innerHTML = `<p style="color: #ef4444;">خطا: ${error.message}</p>`;
655
- } finally {
656
- readLoader.style.display = 'none';
657
- readBtn.disabled = false;
658
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
659
  });
 
660
 
661
- // اضافه کردن قابلیت drag and drop برای فایل
662
- const fileLabel = document.querySelector('.file-label');
663
-
664
- ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
665
- fileLabel.addEventListener(eventName, preventDefaults, false);
666
- });
667
 
668
- function preventDefaults(e) {
669
- e.preventDefault();
670
- e.stopPropagation();
671
- }
 
672
 
673
- ['dragenter', 'dragover'].forEach(eventName => {
674
- fileLabel.addEventListener(eventName, highlight, false);
 
 
 
675
  });
676
-
677
- ['dragleave', 'drop'].forEach(eventName => {
678
- fileLabel.addEventListener(eventName, unhighlight, false);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
679
  });
680
-
681
- function highlight(e) {
682
- fileLabel.style.borderColor = '#f5576c';
683
- fileLabel.style.backgroundColor = 'rgba(245, 87, 108, 0.2)';
684
- }
685
-
686
- function unhighlight(e) {
687
- fileLabel.style.borderColor = 'rgba(255, 255, 255, 0.3)';
688
- fileLabel.style.backgroundColor = 'rgba(255, 255, 255, 0.05)';
689
- }
690
-
691
- fileLabel.addEventListener('drop', handleDrop, false);
692
-
693
- function handleDrop(e) {
694
- const dt = e.dataTransfer;
695
- const files = dt.files;
696
-
697
- if (files.length > 0) {
698
- fileInput.files = files;
699
- fileNameDisplay.textContent = `فایل انتخاب شده: ${files[0].name}`;
700
- fileNameDisplay.style.color = '#4ade80';
701
- }
702
- }
703
- </script>
704
  </body>
705
  </html>
 
1
  <!DOCTYPE html>
2
  <html lang="fa" dir="rtl">
3
  <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>استودیوی QR | ساخت و خواندن QR Code</title>
7
+
8
+ <!-- فونت فارسی -->
9
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/rastikerdar/vazirmatn@v33.003/Vazirmatn-font-face.css" />
10
+
11
+ <style>
12
+ :root{
13
+ --bg-1:#0b0f17;
14
+ --bg-2:#0a0d14;
15
+ --grid:#ffffff12;
16
+ --text:#eaf2ff;
17
+ --muted:#9aa7b7;
18
+ --accent:#06b6d4; /* فیروزه‌ای */
19
+ --accent-2:#7c3aed; /* بنفش */
20
+ --card-bg:rgba(255,255,255,0.06);
21
+ --card-stroke:rgba(255,255,255,0.12);
22
+ --input-bg:rgba(255,255,255,0.08);
23
+ --shadow:0 20px 70px rgba(0,0,0,.45);
24
+ }
25
+
26
+ *{box-sizing:border-box}
27
+ html,body{height:100%}
28
+ body{
29
+ margin:0;
30
+ font-family:"Vazirmatn", system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
31
+ color:var(--text);
32
+ background:
33
+ radial-gradient(1000px 600px at 90% -10%, #3b82f633 0%, transparent 50%),
34
+ radial-gradient(900px 500px at -10% 110%, #a855f733 0%, transparent 55%),
35
+ linear-gradient(180deg, var(--bg-1), var(--bg-2));
36
+ position:relative;
37
+ -webkit-font-smoothing:antialiased;
38
+ -moz-osx-font-smoothing:grayscale;
39
+ }
40
+
41
+ /* الگوی ظریف شبکه‌ای شبیه ماتریس QR */
42
+ body::before{
43
+ content:"";
44
+ position:fixed;
45
+ inset:0;
46
+ pointer-events:none;
47
+ background:
48
+ repeating-linear-gradient(0deg, var(--grid) 0 1px, transparent 1px 22px),
49
+ repeating-linear-gradient(90deg, var(--grid) 0 1px, transparent 1px 22px);
50
+ mask-image: radial-gradient(80% 80% at 50% 50%, black 55%, transparent 100%);
51
+ opacity:.5;
52
+ }
53
+
54
+ .shell{
55
+ max-width:1200px;
56
+ margin-inline:auto;
57
+ padding:28px 18px 40px;
58
+ }
59
+
60
+ .hero{
61
+ text-align:center;
62
+ margin-bottom:16px;
63
+ }
64
+ .title{
65
+ margin:0 0 8px 0;
66
+ font-weight:800;
67
+ font-size:clamp(22px, 5vw, 34px);
68
+ letter-spacing:-.3px;
69
+ background:linear-gradient(90deg, var(--accent), var(--accent-2));
70
+ -webkit-background-clip:text;
71
+ background-clip:text;
72
+ color:transparent;
73
+ filter:drop-shadow(0 6px 24px rgba(124,58,237,.25));
74
+ }
75
+ .subtitle{
76
+ margin:0 auto;
77
+ max-width:680px;
78
+ color:var(--muted);
79
+ font-weight:400;
80
+ line-height:1.8;
81
+ }
82
+
83
+ /* ناوبری بین دو پنل (مخصوص موبایل) */
84
+ .nav-pills{
85
+ display:flex;
86
+ justify-content:center;
87
+ gap:10px;
88
+ margin:18px auto 10px;
89
+ padding:6px;
90
+ width:fit-content;
91
+ background:linear-gradient(180deg, rgba(255,255,255,0.08), rgba(255,255,255,0.05));
92
+ border:1px solid var(--card-stroke);
93
+ border-radius:999px;
94
+ backdrop-filter:blur(10px) saturate(140%);
95
+ -webkit-backdrop-filter:blur(10px) saturate(140%);
96
+ box-shadow:0 10px 30px rgba(0,0,0,.25);
97
+ }
98
+ .pill{
99
+ border:0;
100
+ color:var(--text);
101
+ background:transparent;
102
+ padding:10px 16px;
103
+ border-radius:999px;
104
+ font-weight:700;
105
+ cursor:pointer;
106
+ transition:all .25s ease;
107
+ font-size:.98rem;
108
+ }
109
+ .pill:hover{color:#fff; transform:translateY(-1px)}
110
+ .pill.active{
111
+ background:linear-gradient(90deg, var(--accent), var(--accent-2));
112
+ box-shadow:0 6px 18px rgba(6,182,212,.35);
113
+ }
114
+
115
+ /* محفظه‌ی اسکرول افقی (موبایل) */
116
+ .carousel{
117
+ display:flex;
118
+ overflow-x:auto;
119
+ gap:16px;
120
+ scroll-snap-type:x mandatory;
121
+ scroll-behavior:smooth;
122
+ padding:8px 10px;
123
+ -webkit-overflow-scrolling:touch;
124
+ border-radius:16px;
125
+ }
126
+ /* جهت اسکرول LTR تا رفتار اسکرول اینترویویو دقیق باشه */
127
+ .carousel[dir="ltr"]{direction:ltr}
128
+
129
+ /* کارت/پنل‌ها */
130
+ .panel{
131
+ position:relative;
132
+ scroll-snap-align:start;
133
+ min-width:calc(100vw - 40px); /* هر بار یک پنل کامل در موبایل */
134
+ background:linear-gradient(180deg, rgba(255,255,255,0.10), rgba(255,255,255,0.06));
135
+ border:1px solid var(--card-stroke);
136
+ border-radius:18px;
137
+ padding:22px 18px;
138
+ box-shadow:var(--shadow);
139
+ backdrop-filter:blur(12px) saturate(150%);
140
+ -webkit-backdrop-filter:blur(12px) saturate(150%);
141
+ }
142
+
143
+ /* تزئین گوشه‌ها شبیه فایندر QR */
144
+ .panel .qr-corner{
145
+ position:absolute;
146
+ width:34px; height:34px; border-radius:8px;
147
+ border:3px solid var(--accent);
148
+ opacity:.7; pointer-events:none;
149
+ box-shadow:0 0 0 3px rgba(6,182,212,.12) inset, 0 0 24px rgba(6,182,212,.15);
150
+ }
151
+ .panel .qr-corner::after{
152
+ content:""; position:absolute; inset:6px;
153
+ border-radius:4px; border:3px solid var(--accent-2); opacity:.9;
154
+ }
155
+ .panel .qr-corner.tl{top:12px; right:12px}
156
+ .panel .qr-corner.tr{top:12px; left:12px}
157
+ .panel .qr-corner.bl{bottom:12px; right:12px}
158
+ .panel .qr-corner.br{bottom:12px; left:12px}
159
+
160
+ .panel h2{
161
+ margin:4px 0 14px 0;
162
+ font-weight:800;
163
+ font-size:1.25rem;
164
+ display:flex; align-items:center; gap:10px;
165
+ color:#fff;
166
+ }
167
+ .hint{
168
+ color:var(--muted);
169
+ font-size:.88rem;
170
+ margin:-4px 0 16px 0;
171
+ }
172
+
173
+ .group{display:flex; flex-direction:column; gap:8px; margin-bottom:14px}
174
+ label{color:var(--muted); font-size:.92rem}
175
+
176
+ textarea{
177
+ width:100%;
178
+ min-height:110px;
179
+ resize:vertical;
180
+ padding:12px 14px;
181
+ color:var(--text);
182
+ background:var(--input-bg);
183
+ border:1px solid var(--card-stroke);
184
+ border-radius:12px;
185
+ outline:none;
186
+ transition:border-color .25s, box-shadow .25s;
187
+ }
188
+ textarea::placeholder{color:#98a3b3}
189
+ textarea:focus{
190
+ border-color: color-mix(in oklab, var(--accent) 65%, #fff 15%);
191
+ box-shadow:0 0 0 6px rgba(6,182,212,.15);
192
+ }
193
+
194
+ /* ورودی فایل شیک */
195
+ .file-wrap{position:relative}
196
+ input[type="file"]{display:none}
197
+ .file-label{
198
+ display:flex; align-items:center; justify-content:center; gap:10px;
199
+ width:100%;
200
+ padding:14px 16px;
201
+ background:var(--input-bg);
202
+ border:1px dashed color-mix(in oklab, var(--accent) 60%, #000 15%);
203
+ color:var(--text);
204
+ border-radius:12px;
205
+ cursor:pointer;
206
+ transition:all .25s ease;
207
+ }
208
+ .file-label:hover{
209
+ border-color:var(--accent);
210
+ box-shadow:0 0 0 6px rgba(124,58,237,.14);
211
+ transform:translateY(-1px);
212
+ }
213
+ #file-name{
214
+ min-height:1.2em; margin:8px 6px 0 0; font-size:.86rem; color:var(--muted);
215
+ }
216
+
217
+ .actions{
218
+ display:flex; gap:10px; flex-wrap:wrap;
219
+ }
220
+ button{
221
+ padding:12px 16px;
222
+ border-radius:12px;
223
+ border:0; cursor:pointer;
224
+ color:#fff; font-weight:800; font-size:1rem;
225
+ background:linear-gradient(90deg, var(--accent), var(--accent-2));
226
+ box-shadow:0 10px 26px rgba(6,182,212,.25);
227
+ transition:transform .2s ease, box-shadow .25s ease, opacity .25s ease;
228
+ display:flex; align-items:center; gap:10px;
229
+ }
230
+ button:hover:not(:disabled){
231
+ transform:translateY(-2px);
232
+ box-shadow:0 14px 34px rgba(124,58,237,.28);
233
+ }
234
+ button:disabled{opacity:.6; cursor:not-allowed}
235
+
236
+ .result{
237
+ margin-top:12px;
238
+ padding:18px;
239
+ background:linear-gradient(180deg, rgba(20,20,28,.35), rgba(20,20,28,.15));
240
+ border:1px solid var(--card-stroke);
241
+ border-radius:14px;
242
+ min-height:120px;
243
+ display:grid;
244
+ place-items:center;
245
+ text-align:center;
246
+ color:#eaf2ff;
247
+ }
248
+ #qr-code-display img{
249
+ max-width:100%; height:auto; border-radius:10px;
250
+ border:2px solid #fff; box-shadow:0 8px 30px rgba(0,0,0,.4);
251
+ animation:fade .5s ease;
252
+ }
253
+ @keyframes fade{from{opacity:0; transform:scale(.96)} to{opacity:1; transform:scale(1)}}
254
+
255
+ .loader{
256
+ width:32px; height:32px; border-radius:50%;
257
+ border:3px solid rgba(255,255,255,.15);
258
+ border-top-color: color-mix(in oklab, var(--accent) 70%, #fff 10%);
259
+ animation:spin 1s linear infinite; display:none;
260
+ }
261
+ @keyframes spin{to{transform:rotate(360deg)}}
262
+
263
+ .muted{color:var(--muted)}
264
+ .divider{
265
+ height:1px; background:var(--card-stroke);
266
+ margin:4px 0 12px 0; opacity:.7;
267
+ }
268
+
269
+ /* دسکتاپ: نمایش دو ستونه و بدون اسکرول افقی */
270
+ @media (min-width: 980px){
271
+ .carousel{
272
+ overflow:visible;
273
+ display:grid;
274
+ grid-template-columns:1fr 1fr;
275
+ gap:22px;
276
+ padding-inline:0;
277
+ }
278
+ .panel{min-width:auto}
279
+ .nav-pills{display:none}
280
+ .hero{margin-bottom:18px}
281
+ }
282
+ </style>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
283
  </head>
284
  <body>
285
+ <div class="shell">
286
+ <header class="hero">
287
+ <h1 class="title">استودیوی QR</h1>
288
+ <p class="subtitle">ساخت سریع QR Code از متن یا لینک، و خواندن متن از تصویر QR — طراحی مدرن، سبک و مینیمال با تمرکز روی تجربه موبایل</p>
289
  </header>
290
 
291
+ <!-- ناوبری سریع بین پنل‌ها (روی موبایل) -->
292
+ <div class="nav-pills" role="tablist" aria-label="جابه‌جایی بین پنل‌ها">
293
+ <button class="pill active" data-target="panel-generate">ساخت</button>
294
+ <button class="pill" data-target="panel-read">خواندن</button>
295
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
296
 
297
+ <!-- پنل‌ها: اسکرول افقی روی موبایل، دو ستونه روی دسکتاپ -->
298
+ <div class="carousel" id="carousel" dir="ltr">
299
+ <!-- پنل ساخت -->
300
+ <section class="panel" id="panel-generate" dir="rtl">
301
+ <span class="qr-corner tl"></span>
302
+ <span class="qr-corner tr"></span>
303
+ <span class="qr-corner bl"></span>
304
+ <span class="qr-corner br"></span>
305
+
306
+ <h2>
307
+ <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor"
308
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
309
+ <rect x="3" y="3" width="7" height="7"></rect><rect x="14" y="3" width="7" height="7"></rect>
310
+ <rect x="14" y="14" width="7" height="7"></rect><path d="M3 14h7v7H3z"></path>
311
+ </svg>
312
+ ساخت QR Code
313
+ </h2>
314
+ <p class="hint">متن یا لینک خود را وارد کنید. برای مثال: https://example.com</p>
315
+
316
+ <div class="group">
317
+ <label for="text-input">متن یا لینک</label>
318
+ <textarea id="text-input" rows="4" placeholder="مثلاً: https://google.com">سلام دنیا</textarea>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
319
  </div>
 
320
 
321
+ <div class="actions">
322
+ <button id="generate-btn" type="button" title="ساخت QR Code">
323
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor"
324
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
325
+ <path d="M12 2a10 10 0 1 0 10 10A10 10 0 0 0 12 2v0z"/><path d="M12 8l4 4-4 4"/><path d="M8 12h7"/>
326
+ </svg>
327
+ ساخت QR
328
+ </button>
329
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
330
 
331
+ <div class="result">
332
+ <div id="generate-loader" class="loader"></div>
333
+ <div id="qr-code-display"></div>
334
+ </div>
335
+ </section>
336
+
337
+ <!-- پنل خواندن -->
338
+ <section class="panel" id="panel-read" dir="rtl">
339
+ <span class="qr-corner tl"></span>
340
+ <span class="qr-corner tr"></span>
341
+ <span class="qr-corner bl"></span>
342
+ <span class="qr-corner br"></span>
343
+
344
+ <h2>
345
+ <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor"
346
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
347
+ <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/>
348
+ </svg>
349
+ خواندن QR Code
350
+ </h2>
351
+ <p class="hint">تصویر QR Code را انتخاب کنید تا متن داخل آن خوانده شود.</p>
352
+
353
+ <div class="group">
354
+ <label for="file-input">انتخاب تصویر QR</label>
355
+ <div class="file-wrap">
356
+ <input type="file" id="file-input" accept="image/*" />
357
+ <label class="file-label" for="file-input">
358
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor"
359
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
360
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/>
361
+ </svg>
362
+ لمس برای انتخاب فایل تصویر
363
+ </label>
364
+ </div>
365
+ <p id="file-name"></p>
366
+ </div>
367
 
368
+ <div class="actions">
369
+ <button id="read-btn" type="button" title="خواندن QR Code">
370
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor"
371
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
372
+ <path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/><circle cx="12" cy="12" r="3"/>
373
+ </svg>
374
+ خواندن
375
+ </button>
376
+ </div>
377
 
378
+ <div class="result">
379
+ <div id="read-loader" class="loader"></div>
380
+ <p id="decoded-text" class="muted">نتیجه در اینجا نمایش داده می‌شود.</p>
381
+ </div>
382
+ </section>
383
+ </div>
384
+ </div>
385
+
386
+ <script>
387
+ // Endpoint سرویس
388
+ const SPACE_URL = "https://cultrix-qrcode-read-generate.hf.space";
389
+
390
+ // المان‌ها
391
+ const generateBtn = document.getElementById('generate-btn');
392
+ const readBtn = document.getElementById('read-btn');
393
+ const textInput = document.getElementById('text-input');
394
+ const fileInput = document.getElementById('file-input');
395
+ const qrCodeDisplay = document.getElementById('qr-code-display');
396
+ const decodedText = document.getElementById('decoded-text');
397
+ const generateLoader = document.getElementById('generate-loader');
398
+ const readLoader = document.getElementById('read-loader');
399
+ const fileNameDisplay = document.getElementById('file-name');
400
+
401
+ // نمایش نام فایل انتخاب‌شده
402
+ fileInput.addEventListener('change', () => {
403
+ if (fileInput.files.length > 0) {
404
+ fileNameDisplay.textContent = `فایل انتخاب شده: ${fileInput.files[0].name}`;
405
+ } else {
406
+ fileNameDisplay.textContent = '';
407
+ }
408
+ });
409
+
410
+ // توابع کمکی Gradio Queue
411
+ const generateSessionHash = () => Math.random().toString(36).substring(2, 15);
412
+
413
+ const listenForData = (sessionHash, onResult, onError) => {
414
+ const eventSource = new EventSource(`${SPACE_URL}/gradio_api/queue/data?session_hash=${sessionHash}`);
415
+
416
+ eventSource.onmessage = (event) => {
417
+ const data = JSON.parse(event.data);
418
+ if (data.msg === "process_completed") {
419
+ eventSource.close();
420
+ if (data.output.error) {
421
+ onError(data.output.error);
422
+ } else {
423
+ onResult(data.output.data);
424
+ }
425
+ } else if (data.msg === "process_starts") {
426
+ // شروع پردازش
427
+ console.log("پردازش شروع شد...");
428
+ } else if (data.msg === "process_failed") {
429
+ eventSource.close();
430
+ onError("پردازش با خطا مواجه شد.");
431
+ }
432
+ };
433
+
434
+ eventSource.onerror = (err) => {
435
+ console.error("EventSource failed:", err);
436
+ eventSource.close();
437
+ onError("خطا در ارتباط با سرور.");
438
+ };
439
+ };
440
+
441
+ // ساخت QR
442
+ generateBtn.addEventListener('click', async () => {
443
+ const text = textInput.value.trim();
444
+ if (!text) return alert("لطفاً متنی را وارد کنید.");
445
+
446
+ generateLoader.style.display = 'block';
447
+ qrCodeDisplay.innerHTML = '';
448
+ generateBtn.disabled = true;
449
+
450
+ const sessionHash = generateSessionHash();
451
+
452
+ try {
453
+ const joinResponse = await fetch(`${SPACE_URL}/gradio_api/queue/join`, {
454
+ method: 'POST',
455
+ headers: { 'Content-Type': 'application/json' },
456
+ body: JSON.stringify({ fn_index: 0, data: [text], session_hash: sessionHash })
457
  });
458
 
459
+ if (!joinResponse.ok) throw new Error(`خطا در ارسال درخواست: ${joinResponse.statusText}`);
 
 
 
 
 
 
460
 
461
+ listenForData(
462
+ sessionHash,
463
+ (result) => {
464
+ if (result && result[0]) {
465
+ qrCodeDisplay.innerHTML = result[0];
466
+ } else {
467
+ qrCodeDisplay.innerText = "خطا: خروجی معتبر دریافت نشد.";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
468
  }
469
+ },
470
+ (error) => {
471
+ alert(`خطا در پردازش: ${error}`);
472
+ qrCodeDisplay.innerText = "خطا در ساخت تصویر.";
473
+ }
474
+ );
475
+ } catch (error) {
476
+ alert(`یک خطا رخ داد: ${error.message}`);
477
+ } finally {
478
+ generateLoader.style.display = 'none';
479
+ generateBtn.disabled = false;
480
+ }
481
+ });
482
+
483
+ // خواندن QR
484
+ readBtn.addEventListener('click', async () => {
485
+ const file = fileInput.files[0];
486
+ if (!file) return alert("لطفاً یک فایل تصویر انتخاب کنید.");
487
+
488
+ readLoader.style.display = 'block';
489
+ decodedText.innerText = '';
490
+ readBtn.disabled = true;
491
+
492
+ try {
493
+ const formData = new FormData();
494
+ formData.append('files', file);
495
+
496
+ const uploadResponse = await fetch(`${SPACE_URL}/gradio_api/upload`, {
497
+ method: 'POST',
498
+ body: formData
499
  });
500
+ if (!uploadResponse.ok) throw new Error(`خطا در آپلود فایل: ${uploadResponse.statusText}`);
501
 
502
+ const uploadResult = await uploadResponse.json();
503
+ const serverFilePath = uploadResult[0];
 
 
 
 
504
 
505
+ const fileDataObject = {
506
+ path: serverFilePath,
507
+ url: `${SPACE_URL}/gradio_api/file=${serverFilePath}`,
508
+ orig_name: file.name,
509
+ };
510
 
511
+ const sessionHash = generateSessionHash();
512
+ const joinResponse = await fetch(`${SPACE_URL}/gradio_api/queue/join`, {
513
+ method: 'POST',
514
+ headers: { 'Content-Type': 'application/json' },
515
+ body: JSON.stringify({ fn_index: 1, data: [fileDataObject], session_hash: sessionHash })
516
  });
517
+ if (!joinResponse.ok) throw new Error(`خطا در ارسال به صف: ${joinResponse.statusText}`);
518
+
519
+ listenForData(
520
+ sessionHash,
521
+ (result) => {
522
+ decodedText.innerText = `متن خوانده شده:\n${result[0]}`;
523
+ },
524
+ (error) => {
525
+ alert(`خطا در پردازش: ${error}`);
526
+ decodedText.innerText = "خطا در خواندن کد.";
527
+ }
528
+ );
529
+ } catch (error) {
530
+ alert(`یک خطا رخ داد: ${error.message}`);
531
+ decodedText.innerText = "نتیجه در اینجا نمایش داده می‌شود.";
532
+ } finally {
533
+ readLoader.style.display = 'none';
534
+ readBtn.disabled = false;
535
+ }
536
+ });
537
+
538
+ // ناوبری بین پنل‌ها (موبایل): کلیک روی قرص‌ها => اسکرول به پنل هدف
539
+ const carousel = document.getElementById('carousel');
540
+ const pills = document.querySelectorAll('.pill');
541
+ const panels = Array.from(carousel.querySelectorAll('.panel'));
542
+
543
+ pills.forEach(btn=>{
544
+ btn.addEventListener('click', ()=>{
545
+ const el = document.getElementById(btn.dataset.target);
546
+ if (el) el.scrollIntoView({behavior:'smooth', inline:'start', block:'nearest'});
547
+ });
548
+ });
549
+
550
+ // به‌روزرسانی حالت Active بر اساس اسکرول
551
+ const setActiveByScroll = ()=>{
552
+ const bounds = panels.map(el => Math.abs(el.getBoundingClientRect().left - carousel.getBoundingClientRect().left));
553
+ const nearestIndex = bounds.indexOf(Math.min(...bounds));
554
+ pills.forEach((p,i)=>p.classList.toggle('active', i===nearestIndex));
555
+ };
556
+
557
+ let ticking = false;
558
+ carousel.addEventListener('scroll', ()=>{
559
+ if(!ticking){
560
+ window.requestAnimationFrame(()=>{
561
+ setActiveByScroll();
562
+ ticking = false;
563
  });
564
+ ticking = true;
565
+ }
566
+ });
567
+ </script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
568
  </body>
569
  </html>