uploads

Log | Files | Refs

index.html (25832B)


      1 <!DOCTYPE html>
      2 <html lang="en">
      3 <head>
      4     <meta charset="UTF-8">
      5     <meta name="viewport" content="width=device-width, initial-scale=1.0">
      6     <title>Upload Hub</title>
      7     <link rel="preconnect" href="https://fonts.googleapis.com">
      8     <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
      9     <link href="https://fonts.googleapis.com/css2?family=Lexend:wght@300;400;500;600;700&display=swap" rel="stylesheet">
     10     <link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,1,0" rel="stylesheet" />
     11     <script src="https://cdn.tailwindcss.com"></script>
     12     <script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
     13     <script>
     14         tailwind.config = {
     15             theme: {
     16                 extend: {
     17                     fontFamily: { sans: ['Lexend', 'sans-serif'] },
     18                     colors: {
     19                         m3: {
     20                             background: '#FDFCFB', backgroundDark: '#1A1C19',
     21                             surface: '#FDFCFB', surfaceDark: '#1A1C19',
     22                             surfaceContainer: '#F1F1EA', surfaceContainerDark: '#2D2F2B',
     23                             surfaceContainerHigh: '#EBEBE4', surfaceContainerHighDark: '#333531',
     24                             surfaceContainerHighest: '#E5E6DE', surfaceContainerHighestDark: '#434541',
     25                             onSurface: '#1A1C19', onSurfaceDark: '#E3E3DC',
     26                             onSurfaceVariant: '#43483E', onSurfaceVariantDark: '#C3C8BC',
     27                             primary: '#386A20', primaryDark: '#9CD67D',
     28                             onPrimary: '#FFFFFF', onPrimaryDark: '#0C3900',
     29                             secondaryContainer: '#D7E8CD', secondaryContainerDark: '#374B2E',
     30                             onSecondaryContainer: '#111F0F', onSecondaryContainerDark: '#D7E8CD',
     31                             error: '#BA1A1A', errorDark: '#FFB4AB',
     32                             errorContainer: '#FFDAD6', errorContainerDark: '#93000A',
     33                             onErrorContainer: '#410002', onErrorContainerDark: '#FFDAD6',
     34                             outline: '#74796D', outlineDark: '#8D9286',
     35                         }
     36                     }
     37                 }
     38             }
     39         }
     40     </script>
     41     <style>
     42         body { background-color: #FDFCFB; margin: 0; -webkit-font-smoothing: antialiased; }
     43         @media (prefers-color-scheme: dark) { body { background-color: #1A1C19; } }
     44         input:focus, select:focus { outline: none; border-bottom-color: #386A20; }
     45         @media (prefers-color-scheme: dark) { input:focus, select:focus { border-bottom-color: #9CD67D; } }
     46         .show { display: block !important; }
     47         .show-flex { display: flex !important; }
     48         .hide { display: none !important; }
     49         .tab-active { border-bottom: 2px solid #386A20; color: #386A20; }
     50         @media (prefers-color-scheme: dark) { .tab-active { border-bottom-color: #9CD67D; color: #9CD67D; } }
     51         
     52         /* Copy button success feedback */
     53         .copy-btn .copy-icon { display: inline-block; transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1); }
     54         .copy-btn.copied .copy-icon { transform: scale(1.25); }
     55         .copy-btn.copied { background-color: #2E7D32 !important; }
     56     </style>
     57 </head>
     58 <body class="bg-m3-background dark:bg-m3-backgroundDark text-m3-onSurface dark:text-m3-onSurfaceDark flex flex-col items-center min-h-screen p-4 font-sans">
     59     
     60     <div id="ui-container" class="w-full max-w-[520px] mt-8">
     61         <div class="mb-6 flex items-end justify-between">
     62             <div>
     63                 <h1 class="text-2xl font-bold tracking-tight">Upload</h1>
     64                 <p class="text-sm text-m3-onSurfaceVariant dark:text-m3-onSurfaceVariantDark mt-1">Store files permanently on Hugging Face<br><br><a href="/docs">&bull; <b><u>Read Docs</u></b></a></p>
     65             </div>
     66         </div>
     67 
     68         <!-- Tabs -->
     69         <div class="flex gap-6 mb-6 border-b border-m3-surfaceContainerHighest dark:border-m3-surfaceContainerHighestDark">
     70             <button data-tab="upload-tab" class="tab-btn tab-active pb-2 text-sm font-semibold">Upload</button>
     71             <button data-tab="url-tab" class="tab-btn pb-2 text-sm font-semibold text-m3-onSurfaceVariant dark:text-m3-onSurfaceVariantDark">From URL</button>
     72         </div>
     73 
     74         <!-- ==================== UPLOAD TAB ==================== -->
     75         <div id="upload-tab" class="tab-panel">
     76             <div class="flex flex-col gap-1.5 mb-6">
     77                 <label for="custom-id" class="text-sm font-medium text-m3-onSurfaceVariant dark:text-m3-onSurfaceVariantDark">Custom ID (single file only, optional)</label>
     78                 <input type="text" id="custom-id" placeholder="e.g., source-code" class="px-4 py-3 bg-m3-surfaceContainerHighest dark:bg-m3-surfaceContainerHighestDark rounded-t-md border-b-2 border-m3-outline dark:border-m3-outlineDark placeholder-gray-500">
     79             </div>
     80 
     81             <div id="dropzone" class="bg-m3-surfaceContainer dark:bg-m3-surfaceContainerDark hover:bg-m3-surfaceContainerHigh dark:hover:bg-m3-surfaceContainerHighDark rounded-[24px] p-10 flex flex-col items-center justify-center cursor-pointer mb-6 border border-transparent transition-colors">
     82                 <span class="material-symbols-outlined text-[48px] text-m3-primary dark:text-m3-primaryDark mb-4">upload</span>
     83                 <span class="text-base font-semibold">Click, drag, or paste (Ctrl+V) files</span>
     84                 <span class="text-sm mt-1 text-m3-onSurfaceVariant dark:text-m3-onSurfaceVariantDark">Multiple files supported &middot; any type</span>
     85                 <span class="text-sm mt-1 text-m3-onSurfaceVariant dark:text-m3-onSurfaceVariantDark">Maximum 4.4 MB each batch</span>
     86                 <input type="file" id="file-input" class="hidden" multiple>
     87             </div>
     88 
     89             <div id="progress-container" class="hide mb-6">
     90                 <div class="flex justify-between items-center mb-2">
     91                     <span id="progress-label" class="text-sm font-semibold text-m3-onSurfaceVariant dark:text-m3-onSurfaceVariantDark">Uploading to browser...</span>
     92                     <span id="progress-text" class="text-sm font-bold text-m3-primary dark:text-m3-primaryDark">0%</span>
     93                 </div>
     94                 <div class="w-full bg-m3-surfaceContainerHighest dark:bg-m3-surfaceContainerHighestDark h-2 rounded-none overflow-hidden relative">
     95                     <div id="progress-bar" class="bg-m3-primary dark:bg-m3-primaryDark h-2 rounded-none" style="width: 0%;"></div>
     96                 </div>
     97             </div>
     98 
     99             <div id="processing-container" class="hide animate-pulse bg-m3-surfaceContainer dark:bg-m3-surfaceContainerDark p-4 rounded-[16px] mb-6">
    100                 <div class="flex items-center gap-2 mb-3">
    101                     <div class="w-5 h-5 rounded-full bg-m3-surfaceContainerHighest dark:bg-m3-surfaceContainerHighestDark"></div>
    102                     <div class="w-32 h-4 rounded bg-m3-surfaceContainerHighest dark:bg-m3-surfaceContainerHighestDark"></div>
    103                 </div>
    104                 <div class="bg-m3-background dark:bg-m3-backgroundDark p-3 rounded-[8px] flex flex-col gap-3">
    105                     <div class="w-full h-4 rounded bg-m3-surfaceContainerHighest dark:bg-m3-surfaceContainerHighestDark"></div>
    106                     <div class="w-3/4 h-4 rounded bg-m3-surfaceContainerHighest dark:bg-m3-surfaceContainerHighestDark"></div>
    107                     <div class="flex gap-2 w-full mt-1">
    108                         <div class="flex-1 h-9 rounded-[8px] bg-m3-surfaceContainerHighest dark:bg-m3-surfaceContainerHighestDark"></div>
    109                         <div class="flex-1 h-9 rounded-[8px] bg-m3-surfaceContainerHighest dark:bg-m3-surfaceContainerHighestDark"></div>
    110                     </div>
    111                 </div>
    112             </div>
    113 
    114             <div id="results-list" class="flex flex-col gap-3 mb-6"></div>
    115 
    116             <div id="error-container" class="hide bg-m3-errorContainer dark:bg-m3-errorContainerDark text-m3-onErrorContainer dark:text-m3-onErrorContainerDark p-4 rounded-[16px] mb-6">
    117                 <div class="flex items-start gap-3">
    118                     <span class="material-symbols-outlined">error</span>
    119                     <span id="error-text" class="text-sm font-medium mt-0.5"></span>
    120                 </div>
    121             </div>
    122         </div>
    123 
    124         <!-- ==================== URL TAB ==================== -->
    125         <div id="url-tab" class="tab-panel hide">
    126             <div class="flex flex-col gap-1.5 mb-4">
    127                 <label for="url-input" class="text-sm font-medium text-m3-onSurfaceVariant dark:text-m3-onSurfaceVariantDark">File URL</label>
    128                 <input type="text" id="url-input" placeholder="https://example.com/file.png" class="px-4 py-3 bg-m3-surfaceContainerHighest dark:bg-m3-surfaceContainerHighestDark rounded-t-md border-b-2 border-m3-outline dark:border-m3-outlineDark placeholder-gray-500">
    129             </div>
    130 
    131             <div class="flex flex-col gap-1.5 mb-6">
    132                 <label for="url-custom-id" class="text-sm font-medium text-m3-onSurfaceVariant dark:text-m3-onSurfaceVariantDark">Custom ID (optional)</label>
    133                 <input type="text" id="url-custom-id" placeholder="e.g., logo" class="px-4 py-3 bg-m3-surfaceContainerHighest dark:bg-m3-surfaceContainerHighestDark rounded-t-md border-b-2 border-m3-outline dark:border-m3-outlineDark placeholder-gray-500">
    134             </div>
    135 
    136             <button id="url-upload-btn" class="w-full bg-m3-primary dark:bg-m3-primaryDark text-m3-onPrimary dark:text-m3-onPrimaryDark px-4 py-3 rounded-[8px] text-sm font-semibold mb-6">Fetch &amp; Upload</button>
    137 
    138             <div id="url-results-list" class="flex flex-col gap-3 mb-6"></div>
    139 
    140             <div id="url-error-container" class="hide bg-m3-errorContainer dark:bg-m3-errorContainerDark text-m3-onErrorContainer dark:text-m3-onErrorContainerDark p-4 rounded-[16px] mb-6">
    141                 <div class="flex items-start gap-3">
    142                     <span class="material-symbols-outlined">error</span>
    143                     <span id="url-error-text" class="text-sm font-medium mt-0.5"></span>
    144                 </div>
    145             </div>
    146         </div>
    147 
    148     </div>
    149 
    150     <!-- QR modal -->
    151     <div id="qr-modal" class="hide fixed inset-0 bg-black/50 items-center justify-center z-50 p-4">
    152         <div class="bg-m3-surface dark:bg-m3-surfaceDark rounded-[24px] p-6 max-w-[320px] w-full flex flex-col items-center gap-4">
    153             <div id="qr-code-box" class="bg-white p-3 rounded-[8px]"></div>
    154             <div class="flex gap-2 w-full">
    155                 <button id="qr-download-btn" class="flex-1 bg-m3-primary dark:bg-m3-primaryDark text-m3-onPrimary dark:text-m3-onPrimaryDark px-3 py-2 rounded-[8px] text-sm font-medium">Download PNG</button>
    156                 <button id="qr-close-btn" class="flex-1 bg-m3-surfaceContainerHighest dark:bg-m3-surfaceContainerHighestDark px-3 py-2 rounded-[8px] text-sm font-medium">Close</button>
    157             </div>
    158         </div>
    159     </div>
    160 
    161     <div class="w-full text-center text-xs font-medium text-m3-outline dark:text-m3-outlineDark mt-10 mb-4">
    162         &copy; 2026 <a href="https://github.com/notamitgamer">notamitgamer</a>
    163     </div>
    164 
    165     <script>
    166         // ---------- Tabs ----------
    167         document.querySelectorAll('.tab-btn').forEach(btn => {
    168             btn.addEventListener('click', () => {
    169                 document.querySelectorAll('.tab-btn').forEach(b => {
    170                     b.classList.remove('tab-active');
    171                     b.classList.add('text-m3-onSurfaceVariant', 'dark:text-m3-onSurfaceVariantDark');
    172                 });
    173                 btn.classList.add('tab-active');
    174                 btn.classList.remove('text-m3-onSurfaceVariant', 'dark:text-m3-onSurfaceVariantDark');
    175                 
    176                 document.querySelectorAll('.tab-panel').forEach(p => p.classList.add('hide'));
    177                 document.getElementById(btn.dataset.tab).classList.remove('hide');
    178             });
    179         });
    180 
    181         // ---------- Upload tab elements ----------
    182         const dropzone = document.getElementById('dropzone');
    183         const fileInput = document.getElementById('file-input');
    184         const customIdInput = document.getElementById('custom-id');
    185         const progressContainer = document.getElementById('progress-container');
    186         const progressBar = document.getElementById('progress-bar');
    187         const progressText = document.getElementById('progress-text');
    188         const processingContainer = document.getElementById('processing-container');
    189         const resultsList = document.getElementById('results-list');
    190         const errorContainer = document.getElementById('error-container');
    191         const errorText = document.getElementById('error-text');
    192 
    193         dropzone.addEventListener('click', () => fileInput.click());
    194         dropzone.addEventListener('dragover', (e) => { e.preventDefault(); dropzone.style.borderColor = '#386A20'; });
    195         dropzone.addEventListener('dragleave', () => { dropzone.style.borderColor = 'transparent'; });
    196         dropzone.addEventListener('drop', (e) => {
    197             e.preventDefault();
    198             dropzone.style.borderColor = 'transparent';
    199             if (e.dataTransfer.files.length) uploadFiles(Array.from(e.dataTransfer.files));
    200         });
    201         fileInput.addEventListener('change', () => {
    202             if (fileInput.files.length) uploadFiles(Array.from(fileInput.files));
    203         });
    204 
    205         // Paste-to-upload
    206         document.addEventListener('paste', (e) => {
    207             if (document.getElementById('upload-tab').classList.contains('hide')) return;
    208             const items = e.clipboardData && e.clipboardData.files;
    209             if (items && items.length) uploadFiles(Array.from(items));
    210         });
    211 
    212         function resetUploadUI() {
    213             progressContainer.classList.remove('show'); progressContainer.classList.add('hide');
    214             processingContainer.classList.remove('show'); processingContainer.classList.add('hide');
    215             errorContainer.classList.remove('show-flex'); errorContainer.classList.add('hide');
    216             progressBar.style.width = '0%';
    217             progressText.innerText = '0%';
    218         }
    219 
    220         function showError(container, textEl, msg) {
    221             textEl.innerText = msg;
    222             container.classList.remove('hide');
    223             container.classList.add('show-flex');
    224         }
    225 
    226         // Creates a single DOM element for a file result
    227         function createResultCardDOM(item) {
    228             const origin = window.location.origin;
    229             const finalUrl = origin + item.link_path;
    230             
    231             const card = document.createElement('div');
    232             card.className = "bg-m3-secondaryContainer dark:bg-m3-secondaryContainerDark p-4 rounded-[16px] border border-transparent";
    233             
    234             card.innerHTML = `
    235                 <div class="flex items-center gap-2 mb-2">
    236                     <span class="material-symbols-outlined text-m3-onSecondaryContainer dark:text-m3-onSecondaryContainerDark">check_circle</span>
    237                     <span class="font-bold text-m3-onSecondaryContainer dark:text-m3-onSecondaryContainerDark text-sm">${item.item_id}</span>
    238                 </div>
    239                 <div class="flex flex-col gap-3 bg-m3-background dark:bg-m3-backgroundDark p-3 rounded-[8px]">
    240                     <div class="text-sm break-all select-all font-medium link-text">${finalUrl}</div>
    241                     <div class="flex gap-2 w-full">
    242                         <button class="copy-btn flex-1 bg-m3-primary dark:bg-m3-primaryDark text-m3-onPrimary dark:text-m3-onPrimaryDark px-3 py-2 rounded-[8px] text-sm font-medium flex items-center justify-center gap-1.5">
    243                             <span class="material-symbols-outlined copy-icon text-[18px]">content_copy</span><span class="copy-label">Copy</span>
    244                         </button>
    245                         <a href="${finalUrl}" target="_blank" class="open-btn flex-1 bg-m3-surfaceContainerHighest dark:bg-m3-surfaceContainerHighestDark px-3 py-2 rounded-[8px] text-sm font-medium flex items-center justify-center gap-1.5 no-underline">
    246                             <span class="material-symbols-outlined text-[18px]">open_in_new</span><span>Open</span>
    247                         </a>
    248                         <button class="qr-btn bg-m3-surfaceContainerHighest dark:bg-m3-surfaceContainerHighestDark px-3 py-2 rounded-[8px] text-sm font-medium flex items-center justify-center">
    249                             <span class="material-symbols-outlined text-[18px]">qr_code_2</span>
    250                         </button>
    251                     </div>
    252                 </div>`;
    253             
    254             card.querySelector('.copy-btn').addEventListener('click', function () {
    255                 navigator.clipboard.writeText(finalUrl).catch(() => {});
    256                 const btn = this;
    257                 const icon = btn.querySelector('.copy-icon');
    258                 const label = btn.querySelector('.copy-label');
    259                 btn.classList.add('copied');
    260                 icon.textContent = 'check';
    261                 label.textContent = 'Copied';
    262                 setTimeout(() => {
    263                     btn.classList.remove('copied');
    264                     icon.textContent = 'content_copy';
    265                     label.textContent = 'Copy';
    266                 }, 1500);
    267             });
    268             
    269             card.querySelector('.qr-btn').addEventListener('click', () => openQrModal(finalUrl, item.item_id));
    270             
    271             return card;
    272         }
    273 
    274         // Renders the structured batch layout or single file layout
    275         function renderUploadSession(container, files, batchPath) {
    276             const sessionDiv = document.createElement('div');
    277             sessionDiv.className = "flex flex-col mb-8 border-b-2 border-m3-surfaceContainerHigh dark:border-m3-surfaceContainerHighDark pb-6";
    278             
    279             if (batchPath) {
    280                 // Render Batch Link section
    281                 const batchHeader = document.createElement('h3');
    282                 batchHeader.className = "text-base font-bold text-m3-onSurfaceVariant dark:text-m3-onSurfaceVariantDark mb-3 uppercase tracking-wide";
    283                 batchHeader.innerText = "Batch Link:";
    284                 sessionDiv.appendChild(batchHeader);
    285                 
    286                 sessionDiv.appendChild(createResultCardDOM({
    287                     item_id: "All Files (Batch)",
    288                     link_path: batchPath
    289                 }));
    290 
    291                 // Render Individual Links section
    292                 const indHeader = document.createElement('h3');
    293                 indHeader.className = "text-base font-bold text-m3-onSurfaceVariant dark:text-m3-onSurfaceVariantDark mt-6 mb-3 uppercase tracking-wide";
    294                 indHeader.innerText = "Individual File Links:";
    295                 sessionDiv.appendChild(indHeader);
    296             } else {
    297                 const singleHeader = document.createElement('h3');
    298                 singleHeader.className = "text-base font-bold text-m3-onSurfaceVariant dark:text-m3-onSurfaceVariantDark mb-3 uppercase tracking-wide";
    299                 singleHeader.innerText = "Uploaded File:";
    300                 sessionDiv.appendChild(singleHeader);
    301             }
    302 
    303             // Render files
    304             const filesContainer = document.createElement('div');
    305             filesContainer.className = "flex flex-col gap-3";
    306             files.forEach(f => filesContainer.appendChild(createResultCardDOM(f)));
    307             sessionDiv.appendChild(filesContainer);
    308 
    309             // Prepend the whole block to the top of results
    310             container.prepend(sessionDiv);
    311         }
    312 
    313         function uploadFiles(files) {
    314             resetUploadUI();
    315             
    316             const formData = new FormData();
    317             files.forEach(f => formData.append('files', f));
    318             
    319             const customId = customIdInput.value.trim();
    320             if (customId) {
    321                 if (files.length > 1) {
    322                     showError(errorContainer, errorText, "Custom ID can only be used with a single file.");
    323                     return;
    324                 }
    325                 formData.append('custom_id', customId);
    326             }
    327             
    328             progressContainer.classList.remove('hide'); progressContainer.classList.add('show');
    329             
    330             const xhr = new XMLHttpRequest();
    331             xhr.open('POST', '/api/upload', true);
    332             
    333             xhr.upload.onprogress = function (e) {
    334                 if (e.lengthComputable) {
    335                     const pct = Math.round((e.loaded / e.total) * 100);
    336                     progressBar.style.width = pct + '%';
    337                     progressText.innerText = pct + '%';
    338                     
    339                     if (pct === 100) {
    340                         progressContainer.classList.remove('show'); progressContainer.classList.add('hide');
    341                         processingContainer.classList.remove('hide'); processingContainer.classList.add('show');
    342                     }
    343                 }
    344             };
    345             
    346             xhr.onload = function () {
    347                 progressContainer.classList.remove('show'); progressContainer.classList.add('hide');
    348                 processingContainer.classList.remove('show'); processingContainer.classList.add('hide');
    349                 
    350                 if (xhr.status >= 200 && xhr.status < 300) {
    351                     try {
    352                         const data = JSON.parse(xhr.responseText);
    353                         
    354                         // Pass to our new structured rendering function
    355                         renderUploadSession(resultsList, data.files, data.batch_path);
    356                         
    357                         fileInput.value = ''; customIdInput.value = '';
    358                     } catch (err) {
    359                         showError(errorContainer, errorText, "Invalid response from server.");
    360                     }
    361                 } else {
    362                     try {
    363                         const errData = JSON.parse(xhr.responseText);
    364                         showError(errorContainer, errorText, errData.detail || 'Upload failed.');
    365                     } catch (err) {
    366                         showError(errorContainer, errorText, "Upload failed.");
    367                     }
    368                 }
    369             };
    370             
    371             xhr.onerror = function () {
    372                 progressContainer.classList.remove('show'); progressContainer.classList.add('hide');
    373                 processingContainer.classList.remove('show'); processingContainer.classList.add('hide');
    374                 showError(errorContainer, errorText, "System error: Could not connect to server.");
    375             };
    376             
    377             xhr.send(formData);
    378         }
    379 
    380         // ---------- URL tab ----------
    381         const urlInput = document.getElementById('url-input');
    382         const urlCustomId = document.getElementById('url-custom-id');
    383         const urlUploadBtn = document.getElementById('url-upload-btn');
    384         const urlResultsList = document.getElementById('url-results-list');
    385         const urlErrorContainer = document.getElementById('url-error-container');
    386         const urlErrorText = document.getElementById('url-error-text');
    387 
    388         urlUploadBtn.addEventListener('click', async () => {
    389             urlErrorContainer.classList.remove('show-flex'); urlErrorContainer.classList.add('hide');
    390             const url = urlInput.value.trim();
    391             if (!url) { showError(urlErrorContainer, urlErrorText, "Enter a URL first."); return; }
    392             
    393             const formData = new FormData();
    394             formData.append('url', url);
    395             
    396             const cid = urlCustomId.value.trim();
    397             if (cid) formData.append('custom_id', cid);
    398             
    399             urlUploadBtn.disabled = true;
    400             urlUploadBtn.innerText = 'Fetching & uploading...';
    401             
    402             try {
    403                 const res = await fetch('/api/upload-url', { method: 'POST', body: formData });
    404                 const data = await res.json();
    405                 if (!res.ok) throw new Error(data.detail || 'Upload failed.');
    406                 
    407                 renderUploadSession(urlResultsList, data.files, null);
    408                 
    409                 urlInput.value = ''; urlCustomId.value = '';
    410             } catch (err) {
    411                 showError(urlErrorContainer, urlErrorText, err.message);
    412             } finally {
    413                 urlUploadBtn.disabled = false;
    414                 urlUploadBtn.innerText = 'Fetch & Upload';
    415             }
    416         });
    417 
    418         // ---------- QR modal ----------
    419         const qrModal = document.getElementById('qr-modal');
    420         const qrCodeBox = document.getElementById('qr-code-box');
    421         const qrDownloadBtn = document.getElementById('qr-download-btn');
    422         let qrFilename = 'qrcode';
    423 
    424         function openQrModal(url, name) {
    425             qrCodeBox.innerHTML = '';
    426             qrFilename = name || 'qrcode';
    427             new QRCode(qrCodeBox, { text: url, width: 220, height: 220 });
    428             qrModal.classList.remove('hide'); qrModal.classList.add('show-flex');
    429         }
    430 
    431         document.getElementById('qr-close-btn').addEventListener('click', () => {
    432             qrModal.classList.remove('show-flex'); qrModal.classList.add('hide');
    433         });
    434 
    435         qrDownloadBtn.addEventListener('click', () => {
    436             const canvas = qrCodeBox.querySelector('canvas');
    437             if (!canvas) return;
    438             const a = document.createElement('a');
    439             a.href = canvas.toDataURL('image/png');
    440             a.download = qrFilename + '-qr.png';
    441             document.body.appendChild(a);
    442             a.click();
    443             document.body.removeChild(a);
    444         });
    445     </script>
    446 </body>
    447 </html>
© notamitgamer • Site Built: 2026-07-21 13:58:23 UTC • git-mirror commit: 1037f62 [view raw info]
Originally created with stagit • modified by notamitgamer
Forked from github.com/notamitgamer/git-mirror