// ==UserScript== // @name HF Clone Modal - Add hf download with --local-dir // @namespace https://kyu.sh/ // @version 1.0 // @description Hugging Face clone repository 모달에 --local-dir 포함 hf download 명령어 + 복사 버튼 추가 // @match https://huggingface.co/* // @grant none // @run-at document-idle // ==/UserScript== (function () { 'use strict'; const PROCESSED = 'data-hf-localdir-added'; function makeLocalDir(repoId) { // lcw99/wikipedia-korean-20240501 -> lcw99-wikipedia-korean-20240501 return repoId.replace(/\//g, '-'); } function buildCopyButton(getText) { // 기존 복사 버튼 마크업을 모방 const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'z-1 ml-4 mt-4 sm:mr-6 sm:absolute sm:top-0 sm:right-0 focus:outline-hidden inline-flex cursor-pointer items-center text-sm bg-white shadow-xs rounded-md border px-2 py-1 text-gray-600'; btn.title = 'Copy snippet to clipboard'; btn.innerHTML = ''; btn.addEventListener('click', async () => { const text = getText(); try { await navigator.clipboard.writeText(text); } catch { const ta = document.createElement('textarea'); ta.value = text; ta.style.position = 'fixed'; ta.style.left = '-9999px'; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); } const orig = btn.style.color; btn.style.color = '#16a34a'; setTimeout(() => (btn.style.color = orig), 1000); }); return btn; } function processModal(modal) { const pres = modal.querySelectorAll('pre'); for (const pre of pres) { const text = pre.textContent || ''; const m = text.match( /hf\s+download\s+(\S+)(?:\s+--repo-type=(\S+))?/ ); if (!m) continue; // 이미 --local-dir 이 들어있으면 skip if (text.includes('--local-dir')) continue; const repoId = m[1]; const repoType = m[2]; // undefined 가능 const localDir = makeLocalDir(repoId); const repoTypeFlag = repoType ? ` --repo-type=${repoType}` : ''; const newCmd = `hf download ${repoId}${repoTypeFlag} --local-dir="${localDir}"`; const container = pre.closest('.relative.border-t') || pre.parentElement; if (!container || container.hasAttribute(PROCESSED)) continue; container.setAttribute(PROCESSED, '1'); const block = document.createElement('div'); block.className = 'relative border-t border-gray-100'; block.setAttribute(PROCESSED, '1'); const copyBtn = buildCopyButton(() => newCmd); const inner = document.createElement('div'); inner.className = 'overflow-x-auto pl-4 pb-6 sm:pl-6'; const codeWrap = document.createElement('div'); codeWrap.setAttribute('translate', 'no'); codeWrap.className = 'inline-block text-sm text-gray-700 pt-5 mr-4 sm:mr-6'; const comment = document.createElement('pre'); comment.className = 'text-gray-400'; comment.textContent = '# Download into a named local directory'; const cmdPre = document.createElement('pre'); cmdPre.textContent = newCmd; codeWrap.appendChild(comment); codeWrap.appendChild(cmdPre); inner.appendChild(codeWrap); block.appendChild(copyBtn); block.appendChild(inner); container.parentNode.insertBefore(block, container.nextSibling); } } function scan() { document.querySelectorAll('dialog[open]').forEach((dialog) => { const h3 = dialog.querySelector('h3'); if (h3 && /clone this/i.test(h3.textContent)) { processModal(dialog); } }); } // 모달은 동적으로 열리므로 MutationObserver 로 감시 const observer = new MutationObserver(() => { // 약간의 디바운스 clearTimeout(observer._t); observer._t = setTimeout(scan, 50); }); observer.observe(document.body, { childList: true, subtree: true }); scan(); })();