mirror of
https://github.com/sune-org/store.git
synced 2026-01-13 16:17:58 +00:00
27 lines
14 KiB
JSON
27 lines
14 KiB
JSON
[
|
|
{
|
|
"id": "28qvo5s",
|
|
"name": "File History",
|
|
"pinned": true,
|
|
"avatar": "",
|
|
"url": "gh://sune-org/store@main/commit-history.sune",
|
|
"updatedAt": 1756931360563,
|
|
"settings": {
|
|
"model": "openai/gpt-5",
|
|
"temperature": 1,
|
|
"top_p": 0.96,
|
|
"top_k": 0,
|
|
"frequency_penalty": 0,
|
|
"presence_penalty": 0,
|
|
"repetition_penalty": 1,
|
|
"min_p": 0,
|
|
"top_a": 0,
|
|
"max_tokens": 0,
|
|
"verbosity": "",
|
|
"reasoning_effort": "default",
|
|
"system_prompt": "",
|
|
"html": "<div id=\"ghHistorySune\" class=\"p-4 bg-white rounded-lg border border-gray-200 shadow-sm max-w-2xl mx-auto font-sans\">\n \n <!-- Header -->\n <div class=\"flex justify-between items-center mb-4\">\n <h2 class=\"font-bold text-lg text-gray-800\">GitHub File History</h2>\n <span class=\"text-xs text-gray-400\">v-0.1.0</span>\n </div>\n\n <!-- Input Form -->\n <div class=\"flex flex-col sm:flex-row gap-2 mb-4\">\n <div class=\"relative flex-1\">\n <i data-lucide=\"github\" class=\"absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400\"></i>\n <input type=\"text\" id=\"ghPathInput\" placeholder=\"org/repo@branch/file/path\" class=\"w-full pl-9 pr-3 py-2 text-sm border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-colors\">\n </div>\n <button id=\"fetchHistoryBtn\" class=\"flex items-center justify-center px-4 py-2 bg-gray-800 text-white rounded-lg hover:bg-gray-900 active:scale-95 transition-all disabled:opacity-50 disabled:cursor-not-allowed\">\n <i data-lucide=\"search\" class=\"h-4 w-4\"></i>\n <span class=\"ml-2\">Fetch</span>\n </button>\n </div>\n\n <!-- Status & Results Area -->\n <div id=\"resultsArea\" class=\"space-y-3\">\n <div id=\"statusArea\" class=\"text-sm text-center text-gray-500 p-4 border-2 border-dashed border-gray-200 rounded-lg hidden\"></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 \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 hljs\"></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 // Ensure sune runs only once and within its container\n const sune = document.getElementById('ghHistorySune');\n if (!sune || sune.dataset.initialized) return;\n sune.dataset.initialized = 'true';\n\n const SUNE_ID = window.SUNE ? window.SUNE.id : 'gh-history-sune';\n const CACHE_KEY = `${SUNE_ID}_gh_path`;\n\n // --- DOM Elements ---\n const pathInput = sune.querySelector('#ghPathInput');\n const fetchBtn = sune.querySelector('#fetchHistoryBtn');\n const statusArea = sune.querySelector('#statusArea');\n const commitList = sune.querySelector('#commitList');\n \n const modal = sune.querySelector('#fileContentModal');\n const modalHeader = sune.querySelector('#modalHeader');\n const modalCode = sune.querySelector('#fileContentCode');\n const copyContentBtn = sune.querySelector('#copyContentBtn');\n const closeModalBtn = sune.querySelector('#closeModalBtn');\n\n // --- Utility Functions ---\n const showStatus = (message, isError = false) => {\n statusArea.innerHTML = message;\n statusArea.classList.toggle('text-red-600', isError);\n statusArea.classList.remove('hidden');\n commitList.innerHTML = '';\n };\n\n const hideStatus = () => statusArea.classList.add('hidden');\n \n const setLoading = (isLoading) => {\n fetchBtn.disabled = isLoading;\n if (isLoading) {\n fetchBtn.innerHTML = `<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 class=\"ml-2\">Fetching...</span>`;\n } else {\n fetchBtn.innerHTML = `<i data-lucide=\"search\" class=\"h-4 w-4\"></i><span class=\"ml-2\">Fetch</span>`;\n window.lucide && window.lucide.createIcons();\n }\n };\n\n const parsePath = (input) => {\n if (!input) return null;\n const match = input.match(/^([a-zA-Z0-9\\-_]+)\\/([a-zA-Z0-9\\-_.]+)(?:@([^\\/]+))?\\/(.+)$/);\n if (!match) return null;\n\n return {\n owner: match[1],\n repo: match[2],\n branch: match[3] || null, // null will use default branch\n path: match[4]\n };\n };\n\n const b64_to_utf8 = (str) => {\n try {\n return decodeURIComponent(atob(str).split('').map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)).join(''));\n } catch (e) {\n console.error(\"Base64 decoding failed\", e);\n return \"Error: Could not decode file content. It might be a binary file.\";\n }\n };\n\n const copyToClipboard = async (element, text) => {\n const originalText = element.textContent;\n try {\n await navigator.clipboard.writeText(text);\n element.textContent = 'Copied!';\n } catch (err) {\n element.textContent = 'Failed!';\n console.error('Failed to copy text: ', err);\n } finally {\n setTimeout(() => { element.textContent = originalText; }, 2000);\n }\n };\n \n // --- API Functions ---\n const ghFetch = async (url) => {\n const headers = { 'Accept': 'application/vnd.github.v3+json' };\n if (window.USER && window.USER.PAT) {\n headers['Authorization'] = `token ${window.USER.PAT}`;\n }\n const response = await fetch(url, { headers });\n if (!response.ok) {\n const errorData = await response.json().catch(() => ({ message: `API Error: ${response.status}` }));\n throw new Error(errorData.message || `API Error: ${response.status}`);\n }\n return response.json();\n };\n\n const fetchHistory = async () => {\n const pathValue = pathInput.value.trim();\n if (!pathValue) {\n showStatus(\"Please enter a GitHub file path.\", true);\n return;\n }\n\n const parsed = parsePath(pathValue);\n if (!parsed) {\n showStatus(\"Invalid format. Use: org/repo@branch/file/path\", true);\n return;\n }\n \n localStorage.setItem(CACHE_KEY, pathValue);\n setLoading(true);\n hideStatus();\n commitList.innerHTML = '';\n\n try {\n const { owner, repo, branch, path } = parsed;\n let url = `https://api.github.com/repos/${owner}/${repo}/commits?path=${encodeURIComponent(path)}`;\n if (branch) {\n url += `&sha=${encodeURIComponent(branch)}`;\n }\n \n const commits = await ghFetch(url);\n \n if (!commits || commits.length === 0) {\n showStatus(\"No commits found for this file path. Check if the path and branch are correct.\", false);\n } else {\n renderCommits(commits, parsed);\n }\n } catch (error) {\n console.error(\"Error fetching history:\", error);\n showStatus(`Error: ${error.message}`, true);\n } finally {\n setLoading(false);\n }\n };\n\n const fetchFileContent = async (btn) => {\n const { owner, repo, path, sha } = btn.dataset;\n if (!owner || !repo || !path || !sha) return;\n\n modalHeader.textContent = `Loading... (${sha.substring(0, 7)})`;\n modalCode.textContent = 'Loading content...';\n modal.classList.remove('hidden');\n\n try {\n const url = `https://api.github.com/repos/${owner}/${repo}/contents/${path}?ref=${sha}`;\n const fileData = await ghFetch(url);\n\n if (fileData.content) {\n const decodedContent = b64_to_utf8(fileData.content);\n modalHeader.textContent = `${path} @ ${sha.substring(0, 7)}`;\n modalCode.textContent = decodedContent;\n if (window.hljs) {\n modalCode.className = 'block p-4 text-xs font-mono leading-relaxed hljs';\n window.hljs.highlightElement(modalCode);\n }\n } else {\n modalHeader.textContent = `Error loading file @ ${sha.substring(0, 7)}`;\n modalCode.textContent = \"Could not retrieve file content. It might be a submodule or a large/binary file.\";\n }\n } catch (error) {\n console.error(\"Error fetching file content:\", error);\n modalHeader.textContent = `Error loading file @ ${sha.substring(0, 7)}`;\n modalCode.textContent = `Error: ${error.message}`;\n }\n };\n\n // --- Render Functions ---\n const renderCommits = (commits, parsedInfo) => {\n const commitHtml = commits.map(item => {\n const commit = item.commit;\n const author = item.author || commit.author;\n const date = new Date(commit.author.date).toLocaleString([], { dateStyle: 'medium', timeStyle: 'short' });\n \n return `\n <div class=\"p-3 border border-gray-200 rounded-lg bg-gray-50/50 hover:bg-gray-100 hover:border-gray-300 transition-all\">\n <p class=\"font-medium text-sm text-gray-800 truncate\">${commit.message.split('\\n')[0]}</p>\n <div class=\"flex flex-col sm:flex-row justify-between items-start sm:items-center mt-2 gap-2\">\n <div class=\"flex items-center gap-2 text-xs text-gray-500\">\n ${author && author.avatar_url ? `<img src=\"${author.avatar_url}\" class=\"h-5 w-5 rounded-full\" alt=\"${author.login || ''}\">` : '<div class=\"h-5 w-5 rounded-full bg-gray-300\"></div>'}\n <span class=\"font-medium\">${commit.author.name}</span>\n <span class=\"hidden md:inline\">•</span>\n <span class=\"flex-shrink-0\">${date}</span>\n </div>\n <button \n class=\"view-content-btn self-end sm:self-center flex-shrink-0 px-3 py-1 text-xs bg-white border border-gray-300 rounded-md shadow-sm hover:bg-gray-200 active:scale-95 transition-all\"\n data-owner=\"${parsedInfo.owner}\"\n data-repo=\"${parsedInfo.repo}\"\n data-path=\"${parsedInfo.path}\"\n data-sha=\"${item.sha}\"\n >\n View File\n </button>\n </div>\n </div>\n `;\n }).join('');\n commitList.innerHTML = commitHtml;\n };\n\n // --- Initialization ---\n const init = () => {\n // Load cached path\n const cachedPath = localStorage.getItem(CACHE_KEY);\n if (cachedPath) {\n pathInput.value = cachedPath;\n }\n\n // Event Listeners\n fetchBtn.addEventListener('click', fetchHistory);\n pathInput.addEventListener('keydown', (e) => {\n if (e.key === 'Enter') {\n e.preventDefault();\n fetchHistory();\n }\n });\n\n commitList.addEventListener('click', (e) => {\n const btn = e.target.closest('.view-content-btn');\n if (btn) {\n fetchFileContent(btn);\n }\n });\n\n closeModalBtn.addEventListener('click', () => modal.classList.add('hidden'));\n modal.addEventListener('click', (e) => {\n if (e.target === modal) {\n modal.classList.add('hidden');\n }\n });\n\n copyContentBtn.addEventListener('click', (e) => {\n copyToClipboard(e.target, modalCode.textContent);\n });\n\n // Initial icon render\n window.lucide && window.lucide.createIcons();\n };\n\n // Run after DOM is ready\n if (document.readyState === 'loading') {\n document.addEventListener('DOMContentLoaded', init);\n } else {\n init();\n }\n})();\n</script>\n",
|
|
"extension_html": "\n<sune src='https://raw.githubusercontent.com/sune-org/store/refs/heads/main/sync.sune' private></sune"
|
|
}
|
|
}
|
|
] |