您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Save and manage WIPs on Picrew's image makers
// ==UserScript== // @name Picrew WIP manager // @namespace https://github.com/7evy // @version 1.1.0 // @description Save and manage WIPs on Picrew's image makers // @author 7evy // @run-at document-idle // @include /^https://picrew\.me/(../)?image_maker/[0-9]+/ // @grant GM_addStyle // @grant GM_getValue // @grant GM_setValue // ==/UserScript== (function() { 'use strict'; /////////////////////////////// // UI Setup /////////////////////////////// const uiHTML = ` <div id="wip-toolbox"> <div id="wip-toolbox-bar-1"> <input type="text" id="wip-name" placeholder="WIP name" /> <button id="save-wip">💾 Save</button> <select id="wip-list"><option value="">-- Load WIP --</option></select> <button id="load-wip">📂 Load</button> <button id="delete-wip">🗑️ Delete</button> </div> <div id="wip-toolbox-bar-2"> <button id="export-wip">📤 Export</button> <button id="export-all">📤 Export all</button> <input type="file" id="file-input" /> <button id="import">📥 Import</button> </div> </div> `; const css = ` #wip-toolbox { display: flex; flex-direction: column; position: fixed; top: 10px; right: 10px; background: rgba(0,0,0,0.8); border-radius: 8px; padding: 10px; } #wip-toolbox-bar-1, #wip-toolbox-bar-2 { display: flex; flex-direction: row; z-index: 9999; font-size: 14px; color: white; font-family: sans-serif; } #wip-toolbox-bar-1 { gap: 10px; } #wip-toolbox-bar-2 { gap: 20px; margin-top: 4px; } #wip-toolbox input, #wip-toolbox select, #wip-toolbox button { margin: 3px 0; padding: 4px; } #wip-toolbox input, #wip-toolbox select { width: 120px; } #file-input { display: none; } #wip-toolbox button { color: white !important; } #wip-toolbox button:hover { background: rgba(100,100,100,0.8); } #wip-toolbox button:active { background: rgba(150,150,150,0.8); } `; GM_addStyle(css); if (window.top === window.self) { document.body.insertAdjacentHTML('beforeend', uiHTML); } const saveButton = document.getElementById('save-wip'); const loadButton = document.getElementById('load-wip'); const deleteButton = document.getElementById('delete-wip'); const exportButton = document.getElementById('export-wip'); const exportAllButton = document.getElementById('export-all'); const importButton = document.getElementById('import'); const importFileInput = document.getElementById('file-input'); const wipInput = document.getElementById('wip-name'); const wipSelect = document.getElementById('wip-list'); function detectImageMakerId() { const match = location.href.match(/image_maker\/([0-9]+)/); return match ? parseInt(match[1], 10) : null; } const imageMakerId = detectImageMakerId(); if (!imageMakerId) return; const GMkey = `picrew_wips_${imageMakerId}` /////////////////////////////// // Event Listeners /////////////////////////////// saveButton.onclick = () => { const name = wipInput.value.trim(); if (!name) return alert('A name is required'); saveWIP(name); }; loadButton.onclick = () => { const name = wipSelect.value; if (!name) return alert('Select a WIP to load'); loadWIP(name); location.reload(); }; deleteButton.onclick = () => { const name = wipSelect.value; if (!name) return alert('Select a WIP to delete'); deleteWIP(name); }; exportButton.onclick = () => { const name = wipSelect.value; if (!name) return alert('Select a WIP to export'); exportWIP(name); }; exportAllButton.onclick = () => { exportAllWIPs(); }; importButton.onclick = () => { importFileInput.click(); }; importFileInput.addEventListener("change", function (e) { const selectedImportFile = e.target.files[0]; if (selectedImportFile) importFromFile(selectedImportFile); }); /////////////////////////////// // WIP Logic /////////////////////////////// async function saveWIP(name) { const wipData = await exportParts(imageMakerId); const allWIPs = await GM_getValue(GMkey, {}); allWIPs[name] = wipData; await GM_setValue(GMkey, allWIPs); await refreshDropdown(allWIPs); } async function loadWIP(name) { const allWIPs = await GM_getValue(GMkey, {}); const wip = allWIPs[name]; if (!wip) return alert("WIP not found."); await restoreParts(wip); } async function deleteWIP(name) { const allWIPs = await GM_getValue(GMkey, {}); delete allWIPs[name]; await GM_setValue(GMkey, allWIPs); await removeFromDropdown(name); } async function refreshDropdown(allWIPs) { wipSelect.innerHTML = `<option value="">-- Load WIP --</option>`; for (const name of Object.keys(allWIPs)) { const option = document.createElement('option'); option.value = name; option.textContent = name; wipSelect.appendChild(option); } } async function removeFromDropdown(name) { const option = wipSelect.querySelector(`option[value="${name}"]`); option.remove(); wipSelect.selectedIndex = 0; wipSelect.dispatchEvent(new Event("option deleted")); } async function exportWIP(name) { const allWIPs = await GM_getValue(GMkey, {}); const wip = allWIPs[name]; if (!wip) return alert("WIP not found."); const exportData = [{ name: name, data: wip }]; const json = JSON.stringify(exportData, null, 2); const blob = new Blob([json], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `${imageMakerId + '_' + name}.json`; a.click(); URL.revokeObjectURL(url); } async function exportAllWIPs() { const allWIPs = await GM_getValue(GMkey, {}); const exportData = Object.entries(allWIPs).map(([name, data]) => ({ name, data })); const json = JSON.stringify(exportData, null, 2); const blob = new Blob([json], { type: "application/json" }); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `${imageMakerId}.json`; a.click(); URL.revokeObjectURL(url); } async function importFromFile(file) { const allWIPs = await GM_getValue(GMkey, {}); const reader = new FileReader(); reader.onload = async function (e) { try { const jsonImports = JSON.parse(e.target.result); if (!Array.isArray(jsonImports)) { alert("Invalid import file."); return; } for (const jsonImport of jsonImports) { if (typeof jsonImport.name === "string" && Array.isArray(jsonImport.data)) { if (allWIPs.hasOwnProperty(jsonImport.name)) { allWIPs[jsonImport.name + ' (backup)'] = allWIPs[jsonImport.name]; } allWIPs[jsonImport.name] = jsonImport.data; } else { console.warn("Skipping invalid context:", jsonImport.name); } } await GM_setValue(GMkey, allWIPs); location.reload(); } catch (err) { alert("Failed to import WIPs: " + err.message); } }; reader.readAsText(file); } /////////////////////////////// // IndexedDB Access /////////////////////////////// // Picrew's image maker history is stored in the indexed DB under picrew > image_maker_parts function openPicrewDB() { return new Promise((resolve, reject) => { const request = indexedDB.open("picrew"); request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } async function exportParts(imageMakerId) { const db = await openPicrewDB(); const tx = db.transaction("image_maker_parts", "readonly"); const store = tx.objectStore("image_maker_parts"); return new Promise(resolve => { store.getAll().onsuccess = (valuesEvent) => resolve(valuesEvent.target.result); }); } async function restoreParts(wipData) { const db = await openPicrewDB(); const tx = db.transaction("image_maker_parts", "readwrite"); const store = tx.objectStore("image_maker_parts"); // keyPath is computed from value wipData.forEach((value) => store.put(value)); return new Promise(resolve => { tx.oncomplete = resolve; }); } /////////////////////////////// // Init /////////////////////////////// refreshDropdown(GM_getValue(GMkey, {})); })();