Files
store/storage.sune

1 line
12 KiB
JSON

[{"id":"gjfq54x","name":"StorageManager","pinned":false,"avatar":"","url":"gh://sune-org/store@main/storage.sune","updatedAt":1757529630622,"settings":{"model":"openai/gpt-5","temperature":"1","top_p":"0.97","top_k":"0","frequency_penalty":"0","repetition_penalty":"1","min_p":"0","top_a":"0","verbosity":"","reasoning_effort":"default","system_prompt":"","html":"<!-- Sune: Storage Inspector -->\n<div id=\"storage-inspector-sune\" class=\"w-full mx-auto max-w-none p-3 sm:p-4 bg-white rounded-lg border border-gray-200 shadow-sm text-sm\">\n <div class=\"flex items-center justify-between mb-4\">\n <h2 class=\"text-lg font-semibold text-gray-800 flex items-center gap-2\">\n <i data-lucide=\"database\" class=\"h-5 w-5\"></i>\n <span>Storage Inspector</span>\n <span class=\"text-xs font-mono bg-gray-100 text-gray-500 px-1.5 py-0.5 rounded-md\">v1.1</span>\n </h2>\n <button id=\"si-refresh-btn\" class=\"p-2 rounded-lg hover:bg-gray-100 active:bg-gray-200 transition-colors\">\n <i data-lucide=\"refresh-cw\" class=\"h-5 w-5 text-gray-600\"></i>\n </button>\n </div>\n\n <!-- Storage Overview -->\n <div id=\"si-overview\" class=\"mb-4 p-4 bg-gray-50 rounded-lg border border-gray-200\">\n <h3 class=\"font-medium text-gray-700 mb-2\">Device Storage Quota</h3>\n <div id=\"si-overview-content\" class=\"space-y-2\">\n <p class=\"text-xs text-gray-500\">Calculating...</p>\n </div>\n </div>\n\n <!-- Tabs -->\n <div class=\"border-b border-gray-200 mb-4\">\n <nav class=\"-mb-px flex space-x-4\" aria-label=\"Tabs\">\n <button id=\"si-tab-ls\" data-tab=\"ls\" class=\"si-tab-btn whitespace-nowrap py-3 px-1 border-b-2 font-medium border-slate-600 text-slate-700\">\n localStorage\n </button>\n <button id=\"si-tab-idb\" data-tab=\"idb\" class=\"si-tab-btn whitespace-nowrap py-3 px-1 border-b-2 font-medium border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300\">\n IndexedDB (localforage)\n </button>\n </nav>\n </div>\n\n <!-- Loading Indicator -->\n <div id=\"si-loading\" class=\"hidden text-center p-8\">\n <p class=\"text-gray-500 flex items-center justify-center gap-2\">\n <i data-lucide=\"loader\" class=\"h-5 w-5 animate-spin\"></i>\n <span>Inspecting storage...</span>\n </p>\n </div>\n \n <!-- Content Panels -->\n <div id=\"si-panels\">\n <div id=\"si-panel-ls\" class=\"si-panel space-y-2\"></div>\n <div id=\"si-panel-idb\" class=\"si-panel hidden space-y-2\"></div>\n </div>\n\n <script>\n (() => {\n const suneId = 'storage-inspector-sune', root = document.getElementById(suneId);\n if (!root) return;\n\n const overviewEl=root.querySelector('#si-overview-content'),refreshBtn=root.querySelector('#si-refresh-btn'),loadingEl=root.querySelector('#si-loading'),panelsContainer=root.querySelector('#si-panels');\n const tabs = { ls: { btn: root.querySelector('#si-tab-ls'), panel: root.querySelector('#si-panel-ls') }, idb: { btn: root.querySelector('#si-tab-idb'), panel: root.querySelector('#si-panel-idb') } };\n let worker;\n\n const formatBytes = (b, d = 2) => {\n if (b === 0) return '0 Bytes'; const k=1024, dm=d<0?0:d, s=['Bytes','KB','MB','GB','TB'], i=Math.floor(Math.log(b)/Math.log(k));\n return `${parseFloat((b/Math.pow(k,i)).toFixed(dm))} ${s[i]}`;\n };\n const sanitizeHTML = s => { const e=document.createElement('div'); e.textContent=s; return e.innerHTML; };\n\n const createWorker = () => {\n const workerCode = `\n self.onmessage = (e) => {\n const { task, data } = e.data;\n if (task !== 'calculateSizes') return;\n const r = { ls: { items: [], total: 0 }, idb: { items: [], total: 0 } };\n const size = v => { try { return new Blob([JSON.stringify(v)]).size; } catch { return new Blob([String(v)]).size; } };\n if (data.ls) { for (const i of data.ls) { const s = size(i.value); r.ls.items.push({ ...i, size: s }); r.ls.total += s; } r.ls.items.sort((a,b)=>b.size-a.size); }\n if (data.idb) { for (const i of data.idb) { const s = size(i.value); r.idb.items.push({ ...i, size: s }); r.idb.total += s; } r.idb.items.sort((a,b)=>b.size-a.size); }\n self.postMessage(r);\n };\n `;\n return new Worker(URL.createObjectURL(new Blob([workerCode], { type: 'application/javascript' })));\n };\n \n const renderItem = (item, type, dbName = '') => {\n const valStr = JSON.stringify(item.value, null, 2);\n return `\n <div class=\"border border-gray-200 rounded-lg overflow-hidden\">\n <details class=\"group\">\n <summary class=\"flex items-center justify-between p-3 cursor-pointer hover:bg-gray-50 list-none\">\n <div class=\"flex items-center gap-3 overflow-hidden\"><span class=\"font-mono text-xs bg-gray-100 text-gray-700 px-2 py-1 rounded\">${sanitizeHTML(item.key)}</span>${dbName ? `<span class=\"text-xs text-gray-400 font-medium hidden sm:inline-block\">(${dbName})</span>` : ''}</div>\n <div class=\"flex items-center gap-3 flex-shrink-0\">\n <span class=\"text-gray-500 font-medium\">${formatBytes(item.size)}</span>\n <button data-type=\"${type}\" data-key=\"${sanitizeHTML(item.key)}\" data-db=\"${dbName}\" class=\"si-delete-btn p-1.5 text-gray-400 hover:text-red-600 hover:bg-red-50 rounded-md\"><i data-lucide=\"trash-2\" class=\"h-4 w-4\"></i></button>\n <i data-lucide=\"chevron-down\" class=\"h-5 w-5 text-gray-400 group-open:rotate-180 transition-transform\"></i>\n </div>\n </summary>\n <div class=\"p-3 border-t bg-white\"><pre class=\"text-xs p-3 bg-gray-800 text-white rounded-md overflow-auto max-h-60\"><code>${sanitizeHTML(valStr)}</code></pre></div>\n </details>\n </div>`;\n };\n\n const renderPanel = (panel, items, type, totalSize) => {\n let header = `<div class=\"flex items-center justify-between mb-3 p-2 bg-slate-50 rounded-md text-xs\">\n <span class=\"text-slate-600 font-medium\">Total Size: ${formatBytes(totalSize)}</span>\n ${type==='idb' ? `<button id=\"si-idb-delete-all-btn\" ${items.length===0?'disabled':''} class=\"px-2 py-1 bg-red-100 text-red-700 rounded-md hover:bg-red-200 disabled:opacity-50 disabled:cursor-not-allowed\">Delete All</button>`:''}\n </div>`;\n let body = items.length === 0 ? '<p class=\"text-center text-gray-500 py-4\">No items found.</p>' : items.map(item => renderItem(item, type, item.dbName)).join('');\n panel.innerHTML = header + body;\n };\n\n const updateOverview = async () => {\n if (!navigator.storage?.estimate) { overviewEl.innerHTML = '<p class=\"text-xs text-gray-500\">Storage Estimation API not supported.</p>'; return; }\n try {\n const {usage=0, quota=0} = await navigator.storage.estimate();\n const percent = quota > 0 ? (usage / quota * 100).toFixed(2) : 0;\n overviewEl.innerHTML = `<div class=\"w-full bg-gray-200 rounded-full h-2.5 mb-2\"><div class=\"bg-blue-600 h-2.5 rounded-full\" style=\"width:${percent}%\"></div></div>\n <div class=\"flex justify-between text-xs text-gray-600\"><span>Used: <strong>${formatBytes(usage)}</strong></span><span>Quota: <strong>${formatBytes(quota)}</strong></span><span><strong>${percent}%</strong></span></div>`;\n } catch (e) { overviewEl.innerHTML = '<p class=\"text-xs text-red-500\">Could not estimate storage.</p>'; console.error('Storage estimation failed:', e); }\n };\n \n const refreshData = async () => {\n loadingEl.classList.remove('hidden'); panelsContainer.classList.add('hidden');\n await updateOverview();\n const data = { ls: [], idb: [] };\n for (let i=0;i<localStorage.length;i++) { const k=localStorage.key(i); try { data.ls.push({key:k,value:JSON.parse(localStorage.getItem(k))}); } catch { data.ls.push({key:k,value:localStorage.getItem(k)}); } }\n const dbNames = ['localforage', 'master_cache'];\n const idbPromises = dbNames.map(name => new Promise(res => {\n const store=localforage.createInstance({name}), items=[];\n store.iterate((v,k)=>items.push({key:k,value:v,dbName:name})).then(()=>res(items)).catch(()=>res([]));\n }));\n data.idb = (await Promise.all(idbPromises)).flat();\n worker.postMessage({ task: 'calculateSizes', data });\n };\n\n const handleWorkerMessage = ({data:r}) => {\n renderPanel(tabs.ls.panel, r.ls.items, 'ls', r.ls.total);\n renderPanel(tabs.idb.panel, r.idb.items, 'idb', r.idb.total);\n loadingEl.classList.add('hidden'); panelsContainer.classList.remove('hidden');\n window.lucide?.createIcons();\n };\n\n const handleTabClick = (e) => {\n const targetTab = e.target.closest('.si-tab-btn').dataset.tab;\n Object.entries(tabs).forEach(([k,v]) => {\n const isActive = k === targetTab;\n v.btn.classList.toggle('border-slate-600', isActive); v.btn.classList.toggle('text-slate-700', isActive);\n v.btn.classList.toggle('border-transparent', !isActive); v.btn.classList.toggle('text-gray-500', !isActive);\n v.panel.classList.toggle('hidden', !isActive);\n });\n };\n\n const handlePanelClick = async (e) => {\n const target = e.target;\n const singleDelBtn = target.closest('.si-delete-btn');\n if (singleDelBtn) {\n const { type, key, db } = singleDelBtn.dataset;\n if (!confirm(`Delete \"${key}\"?`)) return;\n if (type === 'ls') localStorage.removeItem(key);\n else if (type === 'idb' && db) await localforage.createInstance({ name: db }).removeItem(key);\n await refreshData();\n return;\n }\n const allDelBtn = target.closest('#si-idb-delete-all-btn');\n if (allDelBtn) {\n if (!confirm('DELETE ALL items from IndexedDB? This cannot be undone.')) return;\n const dbNames = ['localforage', 'master_cache'];\n await Promise.all(dbNames.map(name => localforage.createInstance({ name }).clear()));\n await refreshData();\n }\n };\n \n const init = () => {\n worker = createWorker();\n worker.onmessage = handleWorkerMessage;\n refreshBtn.addEventListener('click', refreshData);\n Object.values(tabs).forEach(t => t.btn.addEventListener('click', handleTabClick));\n panelsContainer.addEventListener('click', handlePanelClick);\n refreshData();\n window.lucide?.createIcons();\n };\n \n init();\n\n const observer = new MutationObserver((mutations) => {\n for(const m of mutations) if(m.removedNodes) m.removedNodes.forEach(n => {\n if (n === root || n.contains(root)) { worker?.terminate(); observer.disconnect(); }\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n })();\n </script>\n</div>\n","extension_html":"<sune src='https://raw.githubusercontent.com/sune-org/store/refs/heads/main/sync.sune' private></sune>","hide_composer":false,"presence_penalty":0,"max_tokens":0,"script":""},"storage":{}}]