您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
A one-click project downloader for Claude.
// ==UserScript== // @name Claude Project Downloader // @name:en Claude Project Downloader // @name:ja Claudeプロジェクトダウンローダー // @namespace https://nomin.jp/ // @version 1.0 // @description A one-click project downloader for Claude. // @description:en A one-click project downloader for Claude. // @description:ja ワンクリック式のClaudeプロジェクトダウンローダー。 // @author nomin // @license All Rights Reserved // @match https://claude.ai/* // @require https://unpkg.com/fflate/umd/index.js // @require https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js // @grant GM_addStyle // @icon data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0IiBmaWxsPSJub25lIiBzdHJva2U9IiMxMTExMTEiIHN0cm9rZS13aWR0aD0iMiIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIj48cGF0aCBkPSJNMjEgMTV2NGEyIDIgMCAwIDEtMiAySDVhMiAyIDAgMCAxLTItMnYtNCIvPjxwb2x5bGluZSBwb2ludHM9IjcgMTAgMTIgMTUgMTcgMTAiLz48bGluZSB4MT0iMTIiIHkxPSIxNSIgeDI9IjEyIiB5Mj0iMyIvPjwvc3ZnPg== // ==/UserScript== (function() { 'use strict'; let isInitialized = false; function initializeDownloaderUI() { if (typeof fflate === 'undefined' || typeof saveAs === 'undefined') { return; } if (document.getElementById('downloader-corner-container')) return; const ICONS = { DOWNLOAD: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>`, SPINNER: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="animate-spin"><line x1="12" y1="2" x2="12" y2="6"/><line x1="12" y1="18" x2="12" y2="22"/><line x1="4.93" y1="4.93" x2="7.76" y2="7.76"/><line x1="16.24" y1="16.24" x2="19.07" y2="19.07"/><line x1="2" y1="12" x2="6" y2="12"/><line x1="18" y1="12" x2="22" y2="12"/><line x1="4.93" y1="19.07" x2="7.76" y2="16.24"/><line x1="16.24" y1="7.76" x2="19.07" y2="4.93"/></svg>`, SUCCESS: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>`, ERROR: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>`, CANCEL: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>` }; const cornerContainer = document.createElement('div'); cornerContainer.id = 'downloader-corner-container'; cornerContainer.innerHTML = `<button id="downloader-start-btn" class="downloader-btn"><span class="icon">${ICONS.DOWNLOAD}</span><span>プロジェクトをダウンロード</span></button>`; document.body.appendChild(cornerContainer); const modalContainer = document.createElement('div'); modalContainer.id = 'downloader-modal-container'; modalContainer.innerHTML = ` <div id="downloader-modal-card"> <div id="downloader-main-status"><span class="icon"></span><span class="text"></span></div> <div id="downloader-progress-bar-container"><div class="progress-bar-fill"></div></div> <div id="downloader-detail-status"></div> <button id="downloader-cancel-btn">キャンセル</button> </div> `; document.body.appendChild(modalContainer); GM_addStyle(` :root{--color-text:#FFFFFF;--color-background:#111111;--color-overlay:rgba(10,10,10,0.75);--color-border:rgba(255,255,255,0.15);--color-progress:#FFFFFF;--color-cancel-text:rgba(255,255,255,0.6);--transition-speed:0.6s;--transition-curve:cubic-bezier(0.2,0.8,0.2,1)} @keyframes spin{from{transform:rotate(0deg)}to{transform:rotate(360deg)}}.animate-spin{animation:spin 1s linear infinite} @keyframes fade-in-up{from{opacity:0;transform:translateY(15px)}to{opacity:1;transform:translateY(0)}} @keyframes progress-shimmer{0%{background-position:-200% 0}100%{background-position:200% 0}} #downloader-corner-container{position:fixed;bottom:25px;right:25px;z-index:9998;transition:opacity .3s,transform .3s;display:none} #downloader-corner-container.visible{display:block} .downloader-btn{display:flex;align-items:center;gap:12px;border:1px solid var(--color-border);border-radius:12px;background:rgba(30,30,30,0.8);backdrop-filter:blur(10px);color:var(--color-text);padding:0 24px;cursor:pointer;height:54px;box-shadow:0 8px 25px -5px rgba(0,0,0,0.2);transition:all .3s var(--transition-curve);font-size:16px;font-weight:500} .downloader-btn:hover{transform:translateY(-4px);box-shadow:0 12px 30px -8px rgba(0,0,0,0.3);background:rgba(40,40,40,0.9)} .downloader-btn .icon{display:flex;align-items:center;width:20px;height:20px} #downloader-modal-container{position:fixed;inset:0;z-index:9999;display:flex;justify-content:center;align-items:center;background:var(--color-overlay);backdrop-filter:blur(8px);opacity:0;pointer-events:none;transition:opacity var(--transition-speed) var(--transition-curve)} #downloader-modal-container.active{opacity:1;pointer-events:auto} #downloader-modal-card{display:flex;flex-direction:column;align-items:center;gap:18px;background:var(--color-background);padding:40px 56px;border-radius:20px;box-shadow:0 25px 50px -12px rgba(0,0,0,0.7);width:440px;border:1px solid var(--color-border);animation:fade-in-up .5s var(--transition-curve);cursor:default} #downloader-modal-container.is-dismissible #downloader-modal-card{cursor:pointer} #downloader-main-status{display:flex;align-items:center;gap:16px;font-size:20px;font-weight:500;color:var(--color-text)} #downloader-main-status .icon{display:flex;align-items:center;width:26px;height:26px} #downloader-progress-bar-container{width:100%;height:8px;background:rgba(255,255,255,0.08);border-radius:4px;overflow:hidden} .progress-bar-fill{width:0%;height:100%;background:var(--color-progress);background-image:linear-gradient(90deg,rgba(255,255,255,0) 0%,rgba(255,255,255,0.2) 50%,rgba(255,255,255,0) 100%);background-size:200% 100%;animation:progress-shimmer 2s linear infinite;transition:width .3s ease-out} #downloader-detail-status{height:20px;font-size:14px;color:rgba(255,255,255,0.7);text-align:center;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;width:100%;transition:opacity .3s} #downloader-cancel-btn{border:none;background:transparent;color:var(--color-cancel-text);padding:8px 16px;border-radius:8px;cursor:pointer;font-size:14px;transition:all .3s} #downloader-cancel-btn:hover{background:rgba(255,255,255,0.1);color:var(--color-text)} `); const startBtn = document.getElementById('downloader-start-btn'); const modalIcon = modalContainer.querySelector('#downloader-main-status .icon'); const modalText = modalContainer.querySelector('#downloader-main-status .text'); const progressBarFill = modalContainer.querySelector('.progress-bar-fill'); const detailStatus = modalContainer.querySelector('#downloader-detail-status'); const cancelBtn = document.getElementById('downloader-cancel-btn'); let isCancelled = false; let closeTimer = null; startBtn.addEventListener('click', startFullDownloadProcess); cancelBtn.addEventListener('click', () => { isCancelled = true; }); modalContainer.addEventListener('click', (e) => { if (modalContainer.classList.contains('is-dismissible') && e.target === modalContainer) { clearTimeout(closeTimer); updateUI('idle'); } }); function animateText(element, newText) { if (element.textContent === newText) return; element.style.opacity = '0'; setTimeout(() => { element.textContent = newText; element.style.opacity = '1'; }, 200); } function updateUI(state, mainText = '', detailText = '', progress = 0) { clearTimeout(closeTimer); modalContainer.classList.remove('is-dismissible'); if (state === 'idle') { modalContainer.classList.remove('active'); return; } modalContainer.classList.add('active'); let icon = ''; let autoCloseDelay = null; cancelBtn.style.display = 'none'; switch(state) { case 'processing': icon = ICONS.SPINNER; cancelBtn.style.display = 'block'; break; case 'zipping': icon = ICONS.SPINNER; break; case 'success': icon = ICONS.SUCCESS; progress = 100; mainText='成功'; detailText='ダウンロードが完了しました'; autoCloseDelay = 3000; break; case 'error': icon = ICONS.ERROR; progress = 100; mainText='エラー'; detailText=detailText||'不明なエラーが発生しました'; autoCloseDelay = 5000; break; case 'cancelled': icon = ICONS.CANCEL; progress = 100; mainText='キャンセル'; detailText='処理が中断されました'; autoCloseDelay = 1500; break; } modalIcon.innerHTML = icon; animateText(modalText, mainText); animateText(detailStatus, detailText); progressBarFill.style.width = `${progress}%`; if(autoCloseDelay !== null) { modalContainer.classList.add('is-dismissible'); closeTimer = setTimeout(() => updateUI('idle'), autoCloseDelay); } } async function startFullDownloadProcess() { isCancelled = false; updateUI('processing', '準備中', 'ファイル一覧をスキャン中...'); try { const fileButtons = Array.from(document.querySelectorAll('button.rounded-lg')).filter(btn => btn.querySelector('h3.text-\\[12px\\]')); if (fileButtons.length === 0) throw new Error("対象のプロジェクトファイルが見つかりませんでした。"); let collectedFiles = []; for (let i = 0; i < fileButtons.length; i++) { if(isCancelled) throw new Error('cancelled'); const fileName = fileButtons[i].querySelector('h3')?.textContent.trim() || `untitled-${i+1}`; updateUI('processing', 'ファイル収集中', `${i+1}/${fileButtons.length}: ${fileName}`, (i/fileButtons.length)*100); fileButtons[i].click(); const contentContainer = await waitForElement('div.whitespace-pre-wrap.break-all.font-mono'); collectedFiles.push({ name: fileName, content: contentContainer.textContent }); const closeButton = document.querySelector('path[d^="M15.1465"]')?.closest('button'); if (closeButton) { closeButton.click(); await waitForElementToDisappear('div.whitespace-pre-wrap.break-all.font-mono'); } } updateUI('zipping', '圧縮処理中', 'ZIPファイルを生成しています...', 100); const filesToZip = {}; const encoder = new TextEncoder(); for (const file of collectedFiles) { filesToZip[file.name] = encoder.encode(file.content); } const zipData = fflate.zipSync(filesToZip, { level: 6 }); const blob = new Blob([zipData], { type: "application/zip" }); saveAs(blob, "claude_project_files.zip"); updateUI('success'); } catch (error) { console.error('Downloader Error:', error); if (isCancelled || error.message === 'cancelled') { updateUI('cancelled'); } else { updateUI('error', 'エラー', error.message); } } } isInitialized = true; } function sentinel() { if (!isInitialized) initializeDownloaderUI(); const cornerContainer = document.getElementById('downloader-corner-container'); if(!cornerContainer) return; const isProjectVisible = document.querySelector('h2[id^="radix-"]') || document.querySelector('button.rounded-lg h3.text-\\[12px\\]'); cornerContainer.classList.toggle('visible', !!isProjectVisible); } setInterval(sentinel, 1000); function waitForElement(s,t=10000){return new Promise((r,j)=>{let i,m=()=>{let e=document.querySelector(s);if(e){clearInterval(i);clearTimeout(n);r(e)}};i=setInterval(m,100);let n=setTimeout(()=>{clearInterval(i);j(new Error(`Element "${s}" not found`))},t)})} function waitForElementToDisappear(s,t=10000){return new Promise((r,j)=>{let i,m=()=>{if(!document.querySelector(s)){clearInterval(i);clearTimeout(n);r()}};i=setInterval(m,100);let n=setTimeout(()=>{clearInterval(i);j(new Error(`"${s}" did not disappear`))},t)})} })();