mirror of
https://github.com/sune-org/store.git
synced 2026-01-13 16:17:58 +00:00
28 lines
16 KiB
JSON
28 lines
16 KiB
JSON
[
|
|
{
|
|
"id": "gjfq54x",
|
|
"name": "StorageManager",
|
|
"pinned": false,
|
|
"avatar": "",
|
|
"url": "gh://sune-org/store@main/storage.sune",
|
|
"updatedAt": 1756931131942,
|
|
"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": "",
|
|
"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 </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\">\n <!-- localStorage content will be injected here -->\n </div>\n <div id=\"si-panel-idb\" class=\"si-panel hidden space-y-2\">\n <!-- IndexedDB content will be injected here -->\n </div>\n </div>\n\n <script>\n (() => {\n const suneId = 'storage-inspector-sune';\n const root = document.getElementById(suneId);\n if (!root) return;\n\n const overviewEl = root.querySelector('#si-overview-content');\n const refreshBtn = root.querySelector('#si-refresh-btn');\n const loadingEl = root.querySelector('#si-loading');\n const panelsContainer = root.querySelector('#si-panels');\n const tabs = {\n ls: { btn: root.querySelector('#si-tab-ls'), panel: root.querySelector('#si-panel-ls') },\n idb: { btn: root.querySelector('#si-tab-idb'), panel: root.querySelector('#si-panel-idb') }\n };\n\n let worker;\n\n // --- Utility Functions ---\n const formatBytes = (bytes, decimals = 2) => {\n if (bytes === 0) return '0 Bytes';\n const k = 1024;\n const dm = decimals < 0 ? 0 : decimals;\n const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];\n const i = Math.floor(Math.log(bytes) / Math.log(k));\n return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i];\n };\n\n const sanitizeHTML = (str) => {\n const temp = document.createElement('div');\n temp.textContent = str;\n return temp.innerHTML;\n };\n\n // --- Worker Setup ---\n const createWorker = () => {\n const workerCode = `\n self.onmessage = (e) => {\n const { task, data } = e.data;\n if (task === 'calculateSizes') {\n const results = {\n localStorage: { items: [], totalSize: 0 },\n indexedDB: { items: [], totalSize: 0 }\n };\n\n const calculateSize = (value) => {\n // Using Blob size is more accurate for byte representation\n try {\n return new Blob([JSON.stringify(value)]).size;\n } catch (err) {\n return new Blob([String(value)]).size;\n }\n };\n\n // Process localStorage\n if (data.localStorage) {\n for (const item of data.localStorage) {\n const size = calculateSize(item.value);\n results.localStorage.items.push({ ...item, size });\n results.localStorage.totalSize += size;\n }\n results.localStorage.items.sort((a, b) => b.size - a.size);\n }\n\n // Process IndexedDB\n if (data.indexedDB) {\n for (const item of data.indexedDB) {\n const size = calculateSize(item.value);\n results.indexedDB.items.push({ ...item, size });\n results.indexedDB.totalSize += size;\n }\n results.indexedDB.items.sort((a, b) => b.size - a.size);\n }\n \n self.postMessage(results);\n }\n };\n `;\n const blob = new Blob([workerCode], { type: 'application/javascript' });\n return new Worker(URL.createObjectURL(blob));\n };\n \n // --- UI Rendering ---\n const renderItem = (item, type, dbName = '') => {\n const valueStr = JSON.stringify(item.value, null, 2);\n const valuePreview = valueStr.length > 100 ? valueStr.substring(0, 100) + '...' : valueStr;\n\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\">\n <span class=\"font-mono text-xs bg-gray-100 text-gray-700 px-2 py-1 rounded\">${sanitizeHTML(item.key)}</span>\n ${dbName ? `<span class=\"text-xs text-gray-400 font-medium hidden sm:inline-block\">(${dbName})</span>` : ''}\n </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\">\n <i data-lucide=\"trash-2\" class=\"h-4 w-4\"></i>\n </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\">\n <pre class=\"text-xs p-3 bg-gray-800 text-white rounded-md overflow-auto max-h-60\"><code>${sanitizeHTML(valueStr)}</code></pre>\n </div>\n </details>\n </div>\n `;\n };\n\n const renderPanel = (panel, items, type, totalSize) => {\n let html = `<div class=\"mb-3 p-2 bg-slate-50 rounded-md text-slate-600 font-medium text-xs\">Total Size: ${formatBytes(totalSize)}</div>`;\n if (items.length === 0) {\n html += '<p class=\"text-center text-gray-500 py-4\">No items found.</p>';\n } else {\n html += items.map(item => renderItem(item, type, item.dbName)).join('');\n }\n panel.innerHTML = html;\n };\n\n const updateOverview = async () => {\n if ('storage' in navigator && 'estimate' in navigator.storage) {\n try {\n const estimate = await navigator.storage.estimate();\n const usage = estimate.usage || 0;\n const quota = estimate.quota || 0;\n const percent = quota > 0 ? (usage / quota * 100).toFixed(2) : 0;\n \n overviewEl.innerHTML = `\n <div class=\"w-full bg-gray-200 rounded-full h-2.5 mb-2\">\n <div class=\"bg-blue-600 h-2.5 rounded-full\" style=\"width: ${percent}%\"></div>\n </div>\n <div class=\"flex justify-between text-xs text-gray-600\">\n <span>Used: <strong>${formatBytes(usage)}</strong></span>\n <span>Quota: <strong>${formatBytes(quota)}</strong></span>\n <span><strong>${percent}%</strong></span>\n </div>\n `;\n } catch (error) {\n overviewEl.innerHTML = '<p class=\"text-xs text-red-500\">Could not estimate storage. API may be unsupported or blocked.</p>';\n console.error('Storage estimation failed:', error);\n }\n } else {\n overviewEl.innerHTML = '<p class=\"text-xs text-gray-500\">Storage Estimation API not supported.</p>';\n }\n };\n \n // --- Data Fetching and Processing ---\n const refreshData = async () => {\n loadingEl.classList.remove('hidden');\n panelsContainer.classList.add('hidden');\n \n await updateOverview();\n\n // 1. Read data from main thread\n const dataToProcess = {\n localStorage: [],\n indexedDB: []\n };\n\n // Read localStorage\n for (let i = 0; i < localStorage.length; i++) {\n const key = localStorage.key(i);\n try {\n dataToProcess.localStorage.push({ key, value: JSON.parse(localStorage.getItem(key)) });\n } catch (e) {\n dataToProcess.localStorage.push({ key, value: localStorage.getItem(key) });\n }\n }\n\n // Read IndexedDB (via localforage)\n const dbNames = ['localforage', 'master_cache']; // From main app's source\n const idbPromises = dbNames.map(name => {\n return new Promise(resolve => {\n const store = localforage.createInstance({ name });\n const items = [];\n store.iterate((value, key) => {\n items.push({ key, value, dbName: name });\n }).then(() => resolve(items)).catch(() => resolve([])); // Resolve even on error\n });\n });\n\n const idbResults = await Promise.all(idbPromises);\n dataToProcess.indexedDB = idbResults.flat();\n\n // 2. Post to worker for calculation\n worker.postMessage({ task: 'calculateSizes', data: dataToProcess });\n };\n\n // --- Event Handlers ---\n const handleWorkerMessage = (e) => {\n const results = e.data;\n renderPanel(tabs.ls.panel, results.localStorage.items, 'ls', results.localStorage.totalSize);\n renderPanel(tabs.idb.panel, results.indexedDB.items, 'idb', results.indexedDB.totalSize);\n \n loadingEl.classList.add('hidden');\n panelsContainer.classList.remove('hidden');\n\n if (window.lucide) window.lucide.createIcons();\n };\n\n const handleTabClick = (e) => {\n const targetTab = e.target.closest('.si-tab-btn').dataset.tab;\n Object.keys(tabs).forEach(key => {\n const isActive = key === targetTab;\n tabs[key].btn.classList.toggle('border-slate-600', isActive);\n tabs[key].btn.classList.toggle('text-slate-700', isActive);\n tabs[key].btn.classList.toggle('border-transparent', !isActive);\n tabs[key].btn.classList.toggle('text-gray-500', !isActive);\n tabs[key].panel.classList.toggle('hidden', !isActive);\n });\n };\n\n const handleDeleteClick = async (e) => {\n const button = e.target.closest('.si-delete-btn');\n if (!button) return;\n\n const { type, key, db } = button.dataset;\n if (confirm(`Are you sure you want to delete \"${key}\"?`)) {\n if (type === 'ls') {\n localStorage.removeItem(key);\n } else if (type === 'idb' && db) {\n const store = localforage.createInstance({ name: db });\n await store.removeItem(key);\n }\n await refreshData();\n }\n };\n \n // --- Initialization ---\n const init = () => {\n worker = createWorker();\n worker.onmessage = handleWorkerMessage;\n \n refreshBtn.addEventListener('click', refreshData);\n tabs.ls.btn.addEventListener('click', handleTabClick);\n tabs.idb.btn.addEventListener('click', handleTabClick);\n panelsContainer.addEventListener('click', handleDeleteClick);\n\n // Initial load\n refreshData();\n if (window.lucide) window.lucide.createIcons();\n };\n \n init();\n\n // Cleanup when sune is removed from DOM (if needed)\n const observer = new MutationObserver((mutationsList) => {\n for (const mutation of mutationsList) {\n if (mutation.removedNodes) {\n mutation.removedNodes.forEach((removedNode) => {\n if (removedNode === root || removedNode.contains(root)) {\n if (worker) worker.terminate();\n observer.disconnect();\n }\n });\n }\n }\n });\n observer.observe(document.body, { childList: true, subtree: true });\n\n })();\n </script>\n</div>\n",
|
|
"extension_html": "<sune src='https://raw.githubusercontent.com/sune-org/store/refs/heads/main/sync.sune' private />",
|
|
"script": ""
|
|
}
|
|
}
|
|
] |