// ==UserScript==
// @name LocalStorage 编辑器 (Simple)
// @namespace https://example.com/
// @version 0.1
// @description 在页面上显示一个浮动面板,方便查看/编辑当前页面的 localStorage(查看/编辑/删除/导入/导出)。
// @author Generated
// @match *://*/*
// @grant none
// @run-at document-end
// @license MIT
// @downloadURL none
// ==/UserScript==
(function () {
'use strict'
// Minimal styles for the panel
const style = document.createElement('style')
style.textContent = `
#ls-editor-toggle { position: fixed; right: 12px; bottom: 12px; z-index:2147483646; }
#ls-editor-panel { position: fixed; right: 12px; bottom: 56px; width: 520px; max-height: 70vh; z-index:2147483646; background:#fff; border:1px solid #ccc; box-shadow:0 6px 24px rgba(0,0,0,0.2); font-family: Arial, Helvetica, sans-serif; color:#111; border-radius:8px; overflow:hidden; display:none; }
#ls-editor-panel .header { display:flex; align-items:center; justify-content:space-between; padding:8px 10px; background:#f5f5f5; border-bottom:1px solid #eee; }
#ls-editor-panel .body { display:flex; gap:8px; padding:8px; }
#ls-editor-keys { width: 45%; max-height:50vh; overflow:auto; border:1px solid #eee; padding:8px; border-radius:4px; }
#ls-editor-keys .key { padding:6px; border-radius:4px; cursor:pointer; margin-bottom:6px; background: #fff; }
#ls-editor-keys .key.selected { background:#e8f0ff; }
#ls-editor-right { width:55%; display:flex; flex-direction:column; gap:8px; }
#ls-editor-right textarea { width:100%; height:160px; font-family: monospace; font-size:12px; padding:8px; border-radius:4px; border:1px solid #ddd; resize:vertical; }
#ls-editor-controls { display:flex; gap:8px; flex-wrap:wrap; }
#ls-editor-panel input[type="text"], #ls-editor-panel input[type="search"] { width:100%; padding:6px 8px; border:1px solid #ddd; border-radius:4px; }
#ls-editor-panel button { padding:6px 10px; border-radius:4px; border:1px solid #bbb; background:#fff; cursor:pointer; }
#ls-editor-panel button.primary { background:#0366d6; color:#fff; border-color:#0366d6; }
#ls-editor-panel .small { font-size:12px; color:#666; }
`
document.head.appendChild(style)
// Toggle button
const toggle = document.createElement('button')
toggle.id = 'ls-editor-toggle'
toggle.textContent = 'LS'
toggle.title = 'Open LocalStorage Editor'
toggle.style.padding = '8px 10px'
toggle.style.borderRadius = '6px'
toggle.style.border = '1px solid #bbb'
toggle.style.background = '#fff'
toggle.style.cursor = 'pointer'
document.body.appendChild(toggle)
// Panel
const panel = document.createElement('div')
panel.id = 'ls-editor-panel'
panel.innerHTML = `
`
document.body.appendChild(panel)
const keysListEl = panel.querySelector('#ls-keys-list')
const searchEl = panel.querySelector('#ls-search')
const keyInputEl = panel.querySelector('#ls-key-input')
const valueEl = panel.querySelector('#ls-value')
const saveBtn = panel.querySelector('#ls-save')
const deleteBtn = panel.querySelector('#ls-delete')
const newBtn = panel.querySelector('#ls-new')
const exportBtn = panel.querySelector('#ls-export')
const importBtn = panel.querySelector('#ls-import')
const closeBtn = panel.querySelector('#ls-close')
const copyBtn = panel.querySelector('#ls-copy')
const clearBtn = panel.querySelector('#ls-clear')
const fileInput = panel.querySelector('#ls-file-input')
let selectedKey = null
function listKeys(filter = '') {
const keys = []
for (let i = 0; i < localStorage.length; i++) {
const k = localStorage.key(i)
if (k == null) continue
if (!filter || k.includes(filter)) keys.push(k)
}
keysListEl.innerHTML = ''
if (keys.length === 0) {
keysListEl.innerHTML = '(no keys)
'
return
}
keys
.sort()
.forEach(k => {
const el = document.createElement('div')
el.className = 'key'
if (k === selectedKey) el.classList.add('selected')
el.textContent = k + ' (' + (localStorage.getItem(k) || '').length + ' chars)'
el.addEventListener('click', () => {
selectKey(k)
})
keysListEl.appendChild(el)
})
}
function selectKey(k) {
selectedKey = k
keyInputEl.value = k
valueEl.value = localStorage.getItem(k) || ''
listKeys(searchEl.value.trim())
}
function saveSelected() {
const k = (keyInputEl.value || '').trim()
if (!k) return alert('请输入 key')
try {
localStorage.setItem(k, valueEl.value)
selectedKey = k
listKeys(searchEl.value.trim())
alert('保存成功')
} catch (e) {
alert('保存失败:' + e)
}
}
function deleteSelected() {
if (!selectedKey) return alert('请先选择要删除的 key')
if (!confirm('确认删除 key: ' + selectedKey + ' ?')) return
try {
localStorage.removeItem(selectedKey)
selectedKey = null
keyInputEl.value = ''
valueEl.value = ''
listKeys(searchEl.value.trim())
alert('删除成功')
} catch (e) {
alert('删除失败:' + e)
}
}
function exportAll() {
const obj = {}
for (let i = 0; i < localStorage.length; i++) {
const k = localStorage.key(i)
if (k == null) continue
obj[k] = localStorage.getItem(k) || ''
}
const text = JSON.stringify(obj, null, 2)
// copy to clipboard if possible
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(
() => alert('已复制到剪贴板'),
() => downloadText('localStorage-export.json', text)
)
} else {
downloadText('localStorage-export.json', text)
}
}
function downloadText(filename, text) {
const a = document.createElement('a')
const blob = new Blob([text], { type: 'application/json' })
a.href = URL.createObjectURL(blob)
a.download = filename
document.body.appendChild(a)
a.click()
a.remove()
}
function importFromText(text, options = { overwrite: false }) {
try {
const parsed = JSON.parse(text)
if (parsed && typeof parsed === 'object') {
const keys = Object.keys(parsed)
if (keys.length === 0) return alert('导入的 JSON 为空')
let overwritten = 0
keys.forEach(k => {
const v = String(parsed[k])
if (!options.overwrite && localStorage.getItem(k) != null) {
// skip
} else {
if (localStorage.getItem(k) != null) overwritten++
localStorage.setItem(k, v)
}
})
listKeys(searchEl.value.trim())
alert('导入完成,已覆盖 ' + overwritten + ' 项')
} else {
alert('导入失败:JSON 格式不正确')
}
} catch (e) {
alert('导入失败:解析 JSON 错误\n' + e)
}
}
// UI events
toggle.addEventListener('click', () => {
panel.style.display = panel.style.display === 'block' ? 'none' : 'block'
})
closeBtn.addEventListener('click', () => (panel.style.display = 'none'))
searchEl.addEventListener('input', () => listKeys(searchEl.value.trim()))
newBtn.addEventListener('click', () => {
const k = (keyInputEl.value || '').trim()
if (!k) return alert('请输入 key')
selectKey(k)
})
saveBtn.addEventListener('click', saveSelected)
deleteBtn.addEventListener('click', deleteSelected)
copyBtn.addEventListener('click', () => {
const v = valueEl.value
if (!navigator.clipboard) return alert('复制失败:浏览器不支持剪贴板 API')
navigator.clipboard.writeText(v).then(
() => alert('已复制值到剪贴板'),
() => alert('复制失败')
)
})
clearBtn.addEventListener('click', () => {
if (!confirm('确认清空 localStorage(当前域)?此操作不可恢复')) return
try {
localStorage.clear()
selectedKey = null
keyInputEl.value = ''
valueEl.value = ''
listKeys(searchEl.value.trim())
alert('已清空')
} catch (e) {
alert('清空失败:' + e)
}
})
exportBtn.addEventListener('click', exportAll)
importBtn.addEventListener('click', () => {
// Offer two options: paste JSON or choose file
const choice = confirm('点击 OK 从文件导入 (.json),点击 Cancel 粘贴 JSON 导入')
if (choice) {
fileInput.value = ''
fileInput.click()
} else {
const text = prompt('请粘贴 JSON 内容:')
if (text) importFromText(text, { overwrite: true })
}
})
fileInput.addEventListener('change', ev => {
const input = ev.target
if (!input.files || input.files.length === 0) return
const file = input.files[0]
const reader = new FileReader()
reader.onload = () => {
const text = String(reader.result || '')
// Ask whether to overwrite existing keys
const overwrite = confirm('是否覆盖已存在的相同 key?点击 OK 覆盖,Cancel 则跳过已存在 key')
importFromText(text, { overwrite })
}
reader.onerror = () => alert('读取文件失败')
reader.readAsText(file)
})
// initial
listKeys()
// expose for debugging in console
;(window).__localStorageEditor = {
open: () => (panel.style.display = 'block'),
close: () => (panel.style.display = 'none'),
listKeys
}
})()