mirror of
https://github.com/sune-org/store.git
synced 2026-01-13 16:17:58 +00:00
1 line
15 KiB
JSON
1 line
15 KiB
JSON
[{"id":"csx7463","name":"Github Sync","pinned":false,"avatar":"","url":"gh://sune-org/store@main/sync.sune","updatedAt":1757100982085,"settings":{"model":"openai/gpt-5","temperature":1,"top_p":0.97,"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":"To use this simply put this in the extension.html of your sune:\n\n<sune src='https://raw.githubusercontent.com/sune-org/store/refs/heads/main/sync.sune' private />","html":"<div id=\"githubSyncSune\" class=\"p-4 mx-4 mb-4 bg-gray-50/50 border rounded-lg\">\n <!-- Sune version v1.7.0 -->\n <div class=\"space-y-3\">\n <div>\n <p id=\"infoText\" class=\"p-3 text-sm text-center text-gray-600 border border-dashed rounded-md break-all\"></p>\n </div>\n <div>\n <label for=\"logArea\" class=\"block mb-1 text-xs font-medium text-gray-500\">Sync Logs</label>\n <pre id=\"logArea\" class=\"w-full h-48 p-2.5 overflow-auto font-mono text-xs text-gray-600 bg-white border border-gray-200 rounded-md whitespace-pre-wrap\"></pre>\n </div>\n </div>\n</div>\n\n<script>\n'use strict';\n(() => {\n const HIDE_UI = true; // Set true to hide the sync UI panel.\n\n const el = {\n root: document.getElementById('githubSyncSune'),\n info: document.getElementById('infoText'),\n log: document.getElementById('logArea'),\n mainBtn: document.getElementById('syncSune')\n };\n\n if (!el.root || el.root.dataset.init) return;\n el.root.dataset.init = '1';\n\n if (!window.SUNE?.active || !window.USER) return el.log && (el.log.textContent = 'ERROR: Core environment not found.');\n\n const state = {\n busy: false,\n remoteSha: undefined, // Use undefined to signify \"not checked yet\"\n pathInfo: null,\n worker: null,\n timeout: null,\n popover: null,\n badgeTimer: null,\n badgeCounter: 0,\n listeners: new Map()\n };\n\n const log = msg => el.log && !HIDE_UI && (el.log.textContent += `[${new Date().toLocaleTimeString()}] ${msg}\\n`, el.log.scrollTop = el.log.scrollHeight);\n const hasPat = () => !!window.USER?.PAT;\n const parseGhPath = p => (p || '').trim().match(/^gh:\\/\\/([^\\/]+)\\/([^@\\/]+)(?:@([^\\/]+))?\\/(.+)$/);\n const lucideRender = nodes => window.lucide?.createIcons({ nodes });\n\n const updateBadge = text => {\n const badge = document.getElementById('suneSyncBadge');\n if (!badge) return;\n badge.textContent = text;\n badge.classList.toggle('hidden', !text);\n };\n\n const stopBadgeTimer = () => {\n if (state.badgeTimer) clearInterval(state.badgeTimer);\n state.badgeTimer = null;\n updateBadge(null);\n };\n\n const startBadgeTimer = prefix => {\n stopBadgeTimer();\n state.badgeCounter = 0;\n const update = () => updateBadge(`${prefix}:${++state.badgeCounter}s`);\n update();\n state.badgeTimer = setInterval(update, 1000);\n };\n\n const setBusy = (busy, opPrefix = '') => {\n state.busy = busy;\n el.mainBtn?.querySelector('svg')?.classList.toggle('animate-spin', busy);\n if (busy && opPrefix) startBadgeTimer(opPrefix);\n else if (!busy) stopBadgeTimer();\n };\n\n const createWorker = () => {\n const code = `\n const GITHUB_API = 'https://api.github.com';\n async function gh(method, path, token, body = null) {\n const h = { 'Accept': 'application/vnd.github.v3+json', ...(token && {'Authorization': \\`token \\${token}\\`}), ...(body && {'Content-Type': 'application/json'}) };\n try {\n const r = await fetch(GITHUB_API + path, { method, headers: h, body: body ? JSON.stringify(body) : null });\n const d = r.status === 204 ? {} : await r.json().catch(() => ({}));\n return { ok: r.ok, status: r.status, data: d };\n } catch (e) { return { ok: false, status: 0, error: e.message }; }\n }\n self.onmessage = async ({ data: { type, pat, pathInfo, contentB64, sha, name } }) => {\n const { owner, repo, branch, path } = pathInfo;\n const ref = branch ? \\`?ref=\\${encodeURIComponent(branch)}\\` : '';\n const cPath = \\`/repos/\\${owner}/\\${repo}/contents/\\${path}\\${ref}\\`;\n if (type === 'check') {\n const res = await gh('GET', cPath, pat);\n if (res.status === 404) return self.postMessage({ type: 'status', exists: false });\n if ((res.status === 403 || res.status === 401) && !pat) return self.postMessage({ type: 'error', reason: 'Private repo or rate limit. PAT required.' });\n if (!res.ok) return self.postMessage({ type: 'error', reason: \\`API Error (\\${res.status}): \\${res.data?.message || res.error}\\` });\n const comPath = \\`/repos/\\${owner}/\\${repo}/commits?sha=\\${branch || 'HEAD'}&path=\\${encodeURIComponent(path)}&per_page=1\\`;\n const comRes = await gh('GET', comPath, pat);\n const date = comRes.ok && comRes.data.length > 0 ? comRes.data[0].commit.committer.date : null;\n return self.postMessage({ type: 'status', exists: true, sha: res.data.sha, date });\n }\n if (type === 'fetch') {\n const res = await gh('GET', cPath, pat);\n return res.ok ? self.postMessage({ type: 'fetched', contentB64: res.data.content, newSha: res.data.sha })\n : self.postMessage({ type: 'error', reason: \\`Download failed (\\${res.status}): \\${res.data?.message || res.error}\\` });\n }\n if (type === 'sync') {\n if (!pat) return self.postMessage({ type: 'error', reason: 'PAT required to upload.' });\n const msg = sha ? \\`Sync: Update sune '\\${name}'\\` : \\`Sync: Create sune '\\${name}'\\`;\n const res = await gh('PUT', cPath, pat, { message: msg, content: contentB64, sha, ...(branch && { branch }) });\n if (res.ok) {\n const newSha = res.data.content.sha;\n self.postMessage({ type: 'log', msg: \\`✅ Sync successful! New SHA: \\${newSha.substring(0,7)}\\` });\n return self.postMessage({ type: 'synced', newSha });\n }\n self.postMessage({ type: 'log', msg: \\`ERROR (\\${res.status}): \\${res.data?.message || res.error}\\` });\n return self.postMessage({ type: 'error', reason: res.data?.message || 'Sync failed' });\n }\n };`;\n return new Worker(URL.createObjectURL(new Blob([code], { type: 'application/javascript' })));\n };\n \n const startWorker = (msg, timeout = 20000) => {\n if (!state.worker) return log('ERROR: Worker not running.');\n clearTimeout(state.timeout);\n state.worker.postMessage(msg);\n state.timeout = setTimeout(() => {\n log('ERROR: Operation timed out.');\n setBusy(false);\n cleanup();\n init();\n }, timeout);\n };\n\n const performUpload = () => {\n if (!state.pathInfo || !hasPat()) return log('ERROR: Sync path not set or PAT missing.');\n hidePopover();\n if (!confirm(\"This will overwrite the remote file on GitHub with your local version. Continue?\")) return;\n setBusy(true, 'U');\n log('Preparing sune for upload...');\n try {\n const contentB64 = btoa(unescape(encodeURIComponent(JSON.stringify([window.SUNE.active]))));\n startWorker({ type: 'sync', pat: window.USER.PAT, pathInfo: state.pathInfo, contentB64, name: window.SUNE.active.name, sha: state.remoteSha });\n } catch (e) {\n log(`FATAL: Failed to prepare data. ${e.message}`);\n setBusy(false);\n }\n };\n \n const performDownload = () => {\n if (!state.pathInfo) return log('ERROR: Sync path not set.');\n hidePopover();\n if (!confirm(\"This will overwrite your local version with the file from GitHub. All local changes will be lost. Continue?\")) return;\n setBusy(true, 'D');\n log('Force download initiated...');\n startWorker({ type: 'fetch', pat: window.USER.PAT, pathInfo: state.pathInfo });\n };\n\n const handleMsg = (e) => {\n clearTimeout(state.timeout);\n const { type, msg, reason } = e.data;\n if (type === 'log') return log(msg);\n if (type === 'error') {\n log(`Worker error: ${reason || 'Unknown'}`);\n return setBusy(false);\n }\n\n if (type === 'status') {\n const { exists, sha, date } = e.data;\n state.remoteSha = exists ? sha : null;\n \n const upBtn = state.popover?.querySelector('#suneSyncUploadBtn');\n const downBtn = state.popover?.querySelector('#suneSyncDownloadBtn');\n if(upBtn) upBtn.disabled = false;\n if(downBtn) downBtn.disabled = !exists;\n\n if (!exists) {\n log('File not on GitHub. Ready to create via Sync Menu.');\n setBusy(false);\n } else {\n const localUpdate = window.SUNE.active.updatedAt;\n const remoteUpdate = date ? new Date(date).getTime() : 0;\n \n if (remoteUpdate > localUpdate + 5000) { // 5s grace period\n log('Remote is newer. Auto-downloading...');\n setBusy(true, 'D');\n startWorker({ type: 'fetch', pat: window.USER.PAT, pathInfo: state.pathInfo });\n } else {\n log(localUpdate > remoteUpdate + 5000 ? 'Local is newer. Use Sync Menu to upload.' : '✅ Sune is in sync.');\n setBusy(false);\n }\n }\n } else if (type === 'fetched') {\n try {\n const suneArr = JSON.parse(decodeURIComponent(escape(atob(e.data.contentB64))));\n if (!suneArr?.[0]?.id) throw new Error(\"Invalid sune format.\");\n Object.assign(window.SUNE.active, suneArr[0], { updatedAt: Date.now() });\n window.SUNE.save();\n log('✅ Sune updated from GitHub.');\n state.remoteSha = e.data.newSha;\n ['closeSettings', 'renderSidebar', 'reflectActiveSune'].forEach(fn => window[fn]?.());\n } catch (err) { log(`ERROR processing download: ${err.message}`); }\n finally { setBusy(false); }\n } else if (type === 'synced') {\n window.SUNE.active.updatedAt = Date.now();\n window.SUNE.save();\n state.remoteSha = e.data.newSha;\n setBusy(false);\n }\n };\n\n const startCheck = () => {\n const pathArr = parseGhPath(window.SUNE.active.url);\n if (!pathArr) return;\n const [, owner, repo, branch, path] = pathArr;\n state.pathInfo = { owner, repo, branch, path };\n if (!HIDE_UI) {\n el.info.innerHTML = `Sync Target: <br><span class=\"font-mono text-xs bg-gray-200 px-1 py-0.5 rounded\">${owner}/${repo}${branch ? `@${branch}`:''}/${path}</span>`;\n }\n setBusy(true, 'C');\n log('Checking remote status...');\n startWorker({ type: 'check', pat: window.USER.PAT, pathInfo: state.pathInfo });\n };\n\n const addListener = (element, event, handler, options) => {\n if (!element) return;\n element.addEventListener(event, handler, options);\n const handlers = state.listeners.get(element) || {};\n handlers[event] = handler;\n state.listeners.set(element, handlers);\n };\n\n const cleanup = () => {\n clearTimeout(state.timeout);\n stopBadgeTimer();\n state.worker?.terminate();\n state.popover?.remove();\n state.listeners.forEach((handler, el) => Object.keys(handler).forEach(evt => el.removeEventListener(evt, handler[evt])));\n state.listeners.clear();\n el.root.dataset.init = '';\n };\n\n const createPopover = () => {\n const p = document.createElement('div');\n p.id = 'suneSyncActionPopover';\n p.className = 'menu-card hidden';\n p.innerHTML = `\n <button id=\"suneSyncUploadBtn\" class=\"menu-item\" disabled><i data-lucide=\"upload-cloud\" class=\"h-4 w-4\"></i><span>Upload to GitHub</span></button>\n <button id=\"suneSyncDownloadBtn\" class=\"menu-item\" disabled><i data-lucide=\"download-cloud\" class=\"h-4 w-4\"></i><span>Download from GitHub</span></button>`;\n document.body.appendChild(p);\n state.popover = p;\n lucideRender([p]);\n addListener(p.querySelector('#suneSyncUploadBtn'), 'click', performUpload);\n addListener(p.querySelector('#suneSyncDownloadBtn'), 'click', performDownload);\n };\n\n const showPopover = btn => {\n if (!state.popover || state.busy) return;\n const r = btn.getBoundingClientRect();\n state.popover.style.top = `${r.bottom + 4}px`;\n state.popover.style.left = `${Math.min(window.innerWidth - 248, Math.max(8, r.left - 120))}px`;\n state.popover.classList.remove('hidden');\n };\n const hidePopover = () => state.popover?.classList.add('hidden');\n\n function init() {\n if (HIDE_UI) el.root.style.display = 'none';\n log(`GitHub Sync v1.7.0 ready for \"${window.SUNE.active.name}\".`);\n \n state.worker = createWorker();\n state.worker.onmessage = handleMsg;\n createPopover();\n \n const pathInfoArr = parseGhPath(window.SUNE.active.url);\n if (pathInfoArr) {\n startCheck();\n } else {\n if (!HIDE_UI) el.info.innerHTML = 'Set a `gh://owner/repo@branch/path.sune` URL in Sune settings (click ✺) to enable sync.';\n }\n\n addListener(el.mainBtn, 'click', e => { e.preventDefault(); e.stopPropagation(); showPopover(el.mainBtn); });\n addListener(document.body, 'click', e => !state.popover?.classList.contains('hidden') && !state.popover?.contains(e.target) && !el.mainBtn?.contains(e.target) && hidePopover(), true);\n\n if (el.mainBtn) {\n el.mainBtn.style.position = 'relative';\n if (!document.getElementById('suneSyncBadge')) {\n const badge = document.createElement('span');\n badge.id = 'suneSyncBadge';\n badge.className = 'hidden absolute -top-1 -right-1 h-4 min-w-[1rem] px-1.5 rounded-full bg-indigo-600 text-white text-[10px] font-bold leading-4 flex items-center justify-center whitespace-nowrap';\n el.mainBtn.appendChild(badge);\n }\n }\n new MutationObserver((_, obs) => {\n if (!document.contains(el.root)) {\n cleanup();\n obs.disconnect();\n }\n }).observe(document.body, { childList: true, subtree: true });\n \n lucideRender();\n };\n\n init();\n})();\n</script>\n","extension_html":""},"storage":{}}] |