Files
store/github.sune

1 line
16 KiB
JSON

[{"id":"37uz2i8","name":"History","pinned":false,"avatar":"","url":"gh://sune-org/store/github.sune","updatedAt":1757192405563,"settings":{"model":"openai/gpt-5-chat","temperature":"","top_p":"","top_k":"","frequency_penalty":"","presence_penalty":"","repetition_penalty":"","min_p":"","top_a":"","max_tokens":"","verbosity":"","reasoning_effort":"default","system_prompt":"","html":"<div id=\"ghRepoBrowserSune\" class=\"relative p-4 bg-white/80 backdrop-blur-xl rounded-lg border border-gray-200 shadow-sm max-w-2xl mx-auto font-sans overflow-hidden\">\n <!-- Background Blob -->\n <div class=\"absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-96 h-96 bg-gradient-to-tr from-cyan-200 via-blue-300 to-purple-300 rounded-full opacity-30 filter blur-3xl -z-10\" aria-hidden=\"true\"></div>\n\n <!-- Header -->\n <div class=\"flex justify-between items-center mb-4\">\n <h2 class=\"font-bold text-lg text-gray-800\">GitHub Repo Browser</h2>\n <span class=\"text-xs text-gray-400\">v-0.4.0</span>\n </div>\n\n <!-- Input Area -->\n <div class=\"relative flex w-full items-center mb-3\">\n <i data-lucide=\"github\" class=\"pointer-events-none absolute left-3 top-1/2 z-10 h-4 w-4 -translate-y-1/2 text-gray-400\"></i>\n <input type=\"text\" id=\"repoPathInput\" placeholder=\"org/repo@branch\" class=\"w-full rounded-lg border border-gray-300 py-2 pl-9 pr-24 text-sm outline-none transition-colors focus:border-blue-500 focus:ring-2 focus:ring-blue-500/50\">\n <button id=\"browseRepoBtn\" class=\"absolute right-1.5 flex items-center justify-center gap-2 rounded-md bg-blue-600 px-3 py-1.5 text-xs font-semibold text-white transition-all hover:bg-blue-700 active:scale-[0.98] disabled:cursor-not-allowed disabled:bg-blue-400\">\n Browse\n </button>\n </div>\n \n <!-- Divider -->\n <hr class=\"my-4 border-gray-200/80\">\n\n <!-- Results Area -->\n <div id=\"resultsArea\" class=\"space-y-4\">\n <!-- Status Area -->\n <div id=\"statusArea\" class=\"text-sm text-center text-gray-600 p-4 border-2 border-dashed border-gray-200 rounded-lg hidden\"></div>\n\n <!-- File Browser -->\n <div id=\"fileBrowser\" class=\"hidden\">\n <div id=\"breadcrumbs\" class=\"flex items-center gap-1.5 text-sm text-gray-500 mb-2 overflow-x-auto pb-2 -mr-4 pr-4\"></div>\n <div id=\"fileTree\" class=\"space-y-1 max-h-[35vh] overflow-y-auto border border-gray-200 rounded-lg p-2 bg-gray-50/50\">\n <!-- File/Dir items will be injected here -->\n </div>\n </div>\n \n <!-- File History -->\n <div id=\"fileHistory\" class=\"hidden\">\n <div id=\"fileHistoryHeader\" class=\"text-sm font-medium text-gray-700 mb-2 truncate\"></div>\n <div id=\"commitList\" class=\"space-y-2 max-h-[50vh] overflow-y-auto -mr-2 pr-2\">\n <!-- Commit items will be injected here -->\n </div>\n </div>\n </div>\n\n <!-- File Content Modal -->\n <div id=\"fileContentModal\" class=\"fixed inset-0 bg-black/60 backdrop-blur-sm z-50 flex items-center justify-center p-4 hidden\">\n <div class=\"bg-white rounded-xl shadow-2xl w-full max-w-4xl flex flex-col max-h-[90vh]\">\n <div class=\"flex justify-between items-center p-3 border-b border-gray-200\">\n <p id=\"modalHeader\" class=\"text-sm font-medium text-gray-700 truncate\">File Content</p>\n <button id=\"closeModalBtn\" class=\"p-1 rounded-full hover:bg-gray-200 active:bg-gray-300 transition-colors\">\n <i data-lucide=\"x\" class=\"h-5 w-5 text-gray-600\"></i>\n </button>\n </div>\n <div class=\"flex-1 overflow-auto relative bg-gray-900\">\n <pre><code id=\"fileContentCode\" class=\"block p-4 text-xs font-mono leading-relaxed\"></code></pre>\n <button id=\"copyContentBtn\" class=\"absolute top-2 right-12 px-3 py-1 text-xs bg-gray-700/80 text-white rounded-md hover:bg-gray-600\">\n Copy\n </button>\n </div>\n </div>\n </div>\n</div>\n\n<script>\n(function() {\n const sune = document.getElementById('ghRepoBrowserSune');\n if (!sune || sune.dataset.initialized) return;\n sune.dataset.initialized = 'true';\n\n const SUNE_ID = window.SUNE?.id || 'gh-repo-browser-sune';\n const CACHE_KEY = `${SUNE_ID}_gh_repo`;\n\n // --- DOM Elements ---\n const input = sune.querySelector('#repoPathInput'), browseBtn = sune.querySelector('#browseRepoBtn');\n const statusArea = sune.querySelector('#statusArea'), resultsArea = sune.querySelector('#resultsArea');\n const fileBrowser = sune.querySelector('#fileBrowser'), breadcrumbs = sune.querySelector('#breadcrumbs'), fileTree = sune.querySelector('#fileTree');\n const fileHistory = sune.querySelector('#fileHistory'), fileHistoryHeader = sune.querySelector('#fileHistoryHeader'), commitList = sune.querySelector('#commitList');\n const modal = sune.querySelector('#fileContentModal'), modalHeader = sune.querySelector('#modalHeader');\n const modalCode = sune.querySelector('#fileContentCode'), copyContentBtn = sune.querySelector('#copyContentBtn');\n const closeModalBtn = sune.querySelector('#closeModalBtn');\n\n // --- State ---\n let currentRepoInfo = {}, pathHistory = [];\n\n // --- Utility Functions ---\n const showStatus=(m,e=!1)=>{statusArea.innerHTML=m;statusArea.classList.toggle('text-red-600',e);statusArea.classList.remove('hidden');fileBrowser.classList.add('hidden');fileHistory.classList.add('hidden')};\n const hideStatus=()=>statusArea.classList.add('hidden');\n const setLoading=(l)=>{browseBtn.disabled=l;browseBtn.innerHTML=l?`<svg class=\"animate-spin h-4 w-4 text-white\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\"><circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"></circle><path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"></path></svg>`:'Browse'};\n const parseRepoPath=p=>p?.match(/^([a-z\\d\\-_]+)\\/([a-z\\d\\-_.]+)(?:@([^\\/]+))?$/i)?.slice(1);\n const b64_to_utf8=s=>{try{return decodeURIComponent(atob(s).split('').map(c=>'%'+('00'+c.charCodeAt(0).toString(16)).slice(-2)).join(''))}catch(e){return\"Err: Could not decode.\"}};\n const copyToClipboard=async(l,t)=>{const o=l.textContent;try{await navigator.clipboard.writeText(t);l.textContent='Copied!'}catch(r){l.textContent='Failed!'}finally{setTimeout(()=>{l.textContent=o},2e3)}};\n const highlightModalCode=()=>{if(window.hljs){delete modalCode.dataset.highlighted;window.hljs.highlightElement(modalCode)}};\n \n // --- API Functions ---\n const ghFetch=async(u)=>{const h={'Accept':'application/vnd.github.v3+json'};if(window.USER?.PAT)h['Authorization']=`token ${window.USER.PAT}`;const r=await fetch(u,{headers:h});if(!r.ok){const e=await r.json().catch(()=>({message:`API Error: ${r.status}`}));throw new Error(e.message||`API Err: ${r.status}`)};return r.json()};\n const fetchHistory=async(fPath,btn)=>{fileHistoryHeader.innerHTML=`<div class=\"flex items-center gap-2\"><svg class=\"animate-spin h-4 w-4\" xmlns=\"http://www.w3.org/2000/svg\" fill=\"none\" viewBox=\"0 0 24 24\"><circle class=\"opacity-25\" cx=\"12\" cy=\"12\" r=\"10\" stroke=\"currentColor\" stroke-width=\"4\"></circle><path class=\"opacity-75\" fill=\"currentColor\" d=\"M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z\"></path></svg> <span>Loading history for <b>${fPath}</b>...</span></div>`;fileHistory.classList.remove('hidden');commitList.innerHTML='';try{const{owner,repo,branch}=currentRepoInfo;const url=`https://api.github.com/repos/${owner}/${repo}/commits?path=${encodeURIComponent(fPath)}&sha=${encodeURIComponent(branch)}`;const commits=await ghFetch(url);fileHistoryHeader.innerHTML=`Commit history for <b>${fPath}</b>`;if(!commits?.length)commitList.innerHTML=`<div class=\"text-xs text-center text-gray-500 p-3 bg-gray-50 rounded-md\">No commits found for this file.</div>`;else renderCommits(commits,fPath)}catch(e){fileHistoryHeader.innerHTML=`Error loading history for <b>${fPath}</b>`;commitList.innerHTML=`<div class=\"text-xs text-center text-red-600 p-3 bg-red-50 rounded-md\">${e.message}</div>`}finally{btn?.classList.add('bg-blue-100','font-semibold')}};\n const fetchFileContent=async(b)=>{const{path,sha}=b.dataset;const{owner,repo}=currentRepoInfo;if(!owner||!repo||!path||!sha)return;modalHeader.textContent=`Loading... (${sha.substring(0,7)})`;modalCode.textContent='Loading...';modalCode.className='block p-4 text-xs font-mono leading-relaxed hljs';modal.classList.remove('hidden');try{const d=await ghFetch(`https://api.github.com/repos/${owner}/${repo}/contents/${path}?ref=${sha}`);modalHeader.textContent=`${path} @ ${sha.substring(0,7)}`;modalCode.textContent=d.content?b64_to_utf8(d.content):\"Could not get content.\"}catch(e){modalHeader.textContent=`Error @ ${sha.substring(0,7)}`;modalCode.textContent=`Error: ${e.message}`}finally{highlightModalCode()}};\n const fetchDiffContent=async(b)=>{const{path,sha}=b.dataset;const{owner,repo}=currentRepoInfo;if(!owner||!repo||!path||!sha)return;modalHeader.textContent=`Loading diff... (${sha.substring(0,7)})`;modalCode.textContent='Loading...';modalCode.className='block p-4 text-xs font-mono leading-relaxed hljs language-diff';modal.classList.remove('hidden');try{const d=await ghFetch(`https://api.github.com/repos/${owner}/${repo}/commits/${sha}`);const f=d.files?.find(f=>f.filename===path);modalHeader.textContent=`Diff for ${path} @ ${sha.substring(0,7)}`;modalCode.textContent=f?.patch||\"No diff found.\"}catch(e){modalHeader.textContent=`Error diff @ ${sha.substring(0,7)}`;modalCode.textContent=`Error: ${e.message}`}finally{highlightModalCode()}};\n\n // --- Render Functions ---\n const renderCommits=(c,p)=>{commitList.innerHTML=c.map(i=>{const m=i.commit,a=i.author||m.author,d=new Date(m.author.date).toLocaleString([],{dateStyle:'medium',timeStyle:'short'});return`<div class=\"p-3 border border-gray-200 rounded-lg bg-gray-50/50 hover:bg-white hover:shadow-md transition-all\"><p class=\"font-medium text-sm text-gray-800 truncate\">${m.message.split('\\n')[0]}</p><div class=\"flex flex-col sm:flex-row justify-between items-start sm:items-center mt-2 gap-2\"><div class=\"flex items-center gap-2 text-xs text-gray-500\">${a?.avatar_url?`<img src=\"${a.avatar_url}\" class=\"h-5 w-5 rounded-full\" alt=\"${a.login||''}\">`:'<div class=\"h-5 w-5 rounded-full bg-gray-300\"></div>'}<span class=\"font-medium\">${m.author.name}</span><span class=\"hidden md:inline\">&bull;</span><span class=\"flex-shrink-0\">${d}</span></div><div class=\"flex items-center gap-2 self-end sm:self-center flex-shrink-0\"><button class=\"view-diff-btn px-3 py-1 text-xs bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-100 active:scale-95\" data-path=\"${p}\" data-sha=\"${i.sha}\">View Diff</button><button class=\"view-content-btn px-3 py-1 text-xs bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-100 active:scale-95\" data-path=\"${p}\" data-sha=\"${i.sha}\">View File</button></div></div></div>`}).join('')};\n const renderBreadcrumbs=()=>{breadcrumbs.innerHTML=pathHistory.map((p,i)=>`<button class=\"breadcrumb-item inline-flex items-center gap-1.5 hover:text-blue-600 disabled:text-gray-800 disabled:hover:text-gray-800\" data-index=\"${i}\" ${i===pathHistory.length-1?'disabled':''}>${p.name==='root'?'<i data-lucide=\"git-branch\" class=\"h-4 w-4\"></i>':''}${p.name}${i<pathHistory.length-1?'<i data-lucide=\"chevron-right\" class=\"h-4 w-4 text-gray-400 -mr-1\"></i>':''}</button>`).join('');window.lucide?.createIcons()};\n const renderTree=items=>{const sorted=items.sort((a,b)=>{if(a.type===b.type)return a.path.localeCompare(b.path);return a.type==='tree'?-1:1});fileTree.innerHTML=sorted.map(i=>`<button class=\"file-item w-full flex items-center gap-2 p-2 text-left text-sm rounded-md hover:bg-gray-200 transition-colors\" data-path=\"${i.path}\" data-sha=\"${i.sha}\" data-type=\"${i.type}\"><i data-lucide=\"${i.type==='tree'?'folder':'file-text'}\" class=\"h-4 w-4 text-gray-500 flex-shrink-0\"></i><span class=\"truncate\">${i.path.split('/').pop()}</span></button>`).join('')||'<div class=\"text-xs text-center text-gray-500 p-3\">This directory is empty.</div>';window.lucide?.createIcons()};\n\n // --- Core Logic ---\n const fetchAndRenderTree = async(sha, name) => {\n fileHistory.classList.add('hidden');\n if(name) pathHistory.push({name, sha}); else pathHistory = [{ name:'root', sha }];\n renderBreadcrumbs();\n fileTree.innerHTML = '<div class=\"text-sm text-center p-4\">Loading tree...</div>';\n try {\n const { tree } = await ghFetch(`https://api.github.com/repos/${currentRepoInfo.owner}/${currentRepoInfo.repo}/git/trees/${sha}`);\n renderTree(tree);\n } catch (e) {\n showStatus(`Error fetching tree: ${e.message}`, true);\n }\n };\n\n const startRepoBrowse = async () => {\n const val = input.value.trim();\n if (!val) return showStatus(\"Repo path cannot be empty.\", true);\n const p = parseRepoPath(val);\n if (!p) return showStatus(\"Invalid format: org/repo@branch (branch is optional)\", true);\n \n localStorage.setItem(CACHE_KEY, val);\n setLoading(true);\n hideStatus();\n fileBrowser.classList.add('hidden');\n fileHistory.classList.add('hidden');\n\n try {\n currentRepoInfo = { owner: p[0], repo: p[1], branch: p[2] || null };\n if (!currentRepoInfo.branch) {\n const repoInfo = await ghFetch(`https://api.github.com/repos/${currentRepoInfo.owner}/${currentRepoInfo.repo}`);\n currentRepoInfo.branch = repoInfo.default_branch;\n input.value = `${currentRepoInfo.owner}/${currentRepoInfo.repo}@${currentRepoInfo.branch}`;\n }\n const branchInfo = await ghFetch(`https://api.github.com/repos/${currentRepoInfo.owner}/${currentRepoInfo.repo}/branches/${currentRepoInfo.branch}`);\n const rootTreeSha = branchInfo.commit.commit.tree.sha;\n fileBrowser.classList.remove('hidden');\n await fetchAndRenderTree(rootTreeSha);\n } catch (e) {\n showStatus(`Error: ${e.message}`, true);\n } finally {\n setLoading(false);\n }\n };\n \n // --- Initialization ---\n const init = () => {\n const cachedRepo = localStorage.getItem(CACHE_KEY);\n if(cachedRepo) input.value = cachedRepo;\n \n browseBtn.addEventListener('click', startRepoBrowse);\n input.addEventListener('keydown', e => e.key === 'Enter' && browseBtn.click());\n \n fileTree.addEventListener('click', e => {\n const item = e.target.closest('.file-item');\n if (!item) return;\n sune.querySelectorAll('.file-item').forEach(el => el.classList.remove('bg-blue-100','font-semibold'));\n const { path, sha, type } = item.dataset;\n if (type === 'tree') fetchAndRenderTree(sha, path.split('/').pop());\n else if (type === 'blob') fetchHistory(path, item);\n });\n\n breadcrumbs.addEventListener('click', e => {\n const item = e.target.closest('.breadcrumb-item');\n if (!item || item.disabled) return;\n const index = parseInt(item.dataset.index, 10);\n pathHistory = pathHistory.slice(0, index + 1);\n fetchAndRenderTree(pathHistory[index].sha);\n });\n\n commitList.addEventListener('click', e => {\n const v = e.target.closest('.view-content-btn'); if (v) return fetchFileContent(v);\n const d = e.target.closest('.view-diff-btn'); if (d) return fetchDiffContent(d);\n });\n \n closeModalBtn.addEventListener('click', () => modal.classList.add('hidden'));\n modal.addEventListener('click', e => e.target === modal && modal.classList.add('hidden'));\n copyContentBtn.addEventListener('click', e => copyToClipboard(e.target, modalCode.textContent));\n \n window.lucide?.createIcons();\n };\n \n if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init); else init();\n})();\n</script>\n","extension_html":"<sune src='https://raw.githubusercontent.com/sune-org/store/refs/heads/main/sync.sune' private />"},"storage":{}}]