您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Ctrl+1-9, auto-numbered, last profile remembered, light theme, backup/restore JSON.
// ==UserScript== // @name NovelAI Prompt Profiles (Ultimate v2.0) // @namespace http://tampermonkey.net/ // @version 2.0 // @description Ctrl+1-9, auto-numbered, last profile remembered, light theme, backup/restore JSON. // @match https://novelai.net/image* // @grant none // @license MIT // ==/UserScript== (function () { 'use strict'; const STORAGE_KEY = "nai_prompt_profiles_v2"; const LAST_PROFILE_KEY = "nai_last_profile"; // Struktur: array agar bisa urut let profiles = []; let lastProfileName = localStorage.getItem(LAST_PROFILE_KEY); try { const saved = localStorage.getItem(STORAGE_KEY); if (saved) { const parsed = JSON.parse(saved); if (Array.isArray(parsed)) { profiles = parsed; } } } catch (e) { console.error("Gagal muat profiles:", e); } function saveProfilesToStorage() { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(profiles)); } catch (e) { console.error("Gagal simpan profiles:", e); } } function updateSelectOptions(select, selectedName = null) { select.innerHTML = ""; profiles.forEach((p, i) => { const opt = document.createElement("option"); opt.value = p.name; opt.textContent = `${i + 1}. ${p.name}`; select.appendChild(opt); }); // Set pilihan ke profil terakhir jika ada if (selectedName && profiles.some(p => p.name === selectedName)) { select.value = selectedName; } else if (profiles.length > 0) { select.selectedIndex = 0; } } // Simpan nama profil terakhir function setLastProfile(name) { lastProfileName = name; localStorage.setItem(LAST_PROFILE_KEY, name); } // Cari editor ProseMirror function findSiteEditor() { return document.querySelector('.image-gen-prompt-main .ProseMirror') || document.querySelector('.prompt-input-box-prompt .ProseMirror'); } // Cari ProseMirror view function findPMView(node, maxDepth = 6) { let el = node; let depth = 0; while (el && depth < maxDepth) { try { const maybeKeys = Object.keys(el); for (const k of maybeKeys) { try { const v = el[k]; if (v && typeof v === 'object' && v.state && typeof v.dispatch === 'function') { return v; } } catch (e) {} } if (el.pmView) return el.pmView; if (el.__pmView) return el.__pmView; if (el._pmView) return el._pmView; if (el.__view) return el.__view; if (el._view) return el._view; } catch (e) {} el = el.parentNode; depth++; } return null; } // Terapkan teks ke editor async function applyTextToEditor(text, statusEl) { if (!text) { statusEl.textContent = "⚠️ Teks kosong."; return false; } statusEl.textContent = "🔍 Mencari editor..."; const editor = findSiteEditor(); if (!editor) { statusEl.textContent = "❌ Editor tidak ditemukan."; return false; } const view = findPMView(editor); if (view) { try { const tr = view.state.tr; tr.delete(0, view.state.doc.content.size); tr.insertText(text); view.dispatch(tr); statusEl.textContent = "✅ Berhasil (ProseMirror)"; return true; } catch (e) { console.error("PM view dispatch error:", e); statusEl.textContent = "⚠️ Gagal (view), coba metode lain..."; } } // Fallback: execCommand insertText try { editor.focus(); const range = document.createRange(); range.selectNodeContents(editor); const sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(range); const okIns = document.execCommand('insertText', false, text); editor.dispatchEvent(new InputEvent('input', { bubbles: true })); editor.dispatchEvent(new Event('change', { bubbles: true })); if (okIns) { statusEl.textContent = "✅ Berhasil (execCommand)"; return true; } } catch (e) { console.error("execCommand error:", e); statusEl.textContent = "⚠️ Gagal (execCommand), coba clipboard..."; } // Clipboard fallback try { await navigator.clipboard.writeText(text); statusEl.textContent = "📋 Tersalin! Paste manual (Ctrl+V)"; return false; } catch (e) { console.error("Clipboard error:", e); statusEl.textContent = "❌ Gagal salin ke clipboard."; return false; } } // Buat panel function createPanelOnce() { if (document.getElementById('nai-profiles-panel')) return; const container = document.querySelector('.image-gen-prompt-main') || document.querySelector('.prompt-input-box-prompt'); if (!container) { setTimeout(createPanelOnce, 500); return; } // Ikon draggable const toggle = document.createElement('div'); toggle.id = "nai-profiles-toggle"; toggle.style.position = "fixed"; toggle.style.top = "10px"; toggle.style.left = "10px"; toggle.style.zIndex = "10000"; toggle.style.cursor = "move"; toggle.style.fontSize = "18px"; toggle.style.padding = "6px"; toggle.style.backgroundColor = "#f3f4f6"; toggle.style.color = "#1f2937"; toggle.style.border = "1px solid #d1d5db"; toggle.style.borderRadius = "6px"; toggle.style.boxShadow = "0 1px 3px rgba(0,0,0,0.1)"; toggle.style.userSelect = "none"; toggle.title = "Drag to move | Click to open"; toggle.textContent = "📝"; let isDragging = false; let offsetX, offsetY; toggle.addEventListener("mousedown", (e) => { isDragging = true; offsetX = e.clientX - toggle.getBoundingClientRect().left; offsetY = e.clientY - toggle.getBoundingClientRect().top; toggle.style.opacity = "0.8"; }); document.addEventListener("mousemove", (e) => { if (!isDragging) return; toggle.style.left = `${e.clientX - offsetX}px`; toggle.style.top = `${e.clientY - offsetY}px`; }); document.addEventListener("mouseup", () => { isDragging = false; toggle.style.opacity = "1"; }); document.body.appendChild(toggle); // Panel utama (tema ringan) const panel = document.createElement('div'); panel.id = "nai-profiles-panel"; panel.style.position = "fixed"; panel.style.zIndex = "9999"; panel.style.width = "320px"; panel.style.maxHeight = "420px"; panel.style.overflowY = "auto"; panel.style.background = "#ffffff"; panel.style.color = "#111827"; panel.style.border = "1px solid #e5e7eb"; panel.style.borderRadius = "10px"; panel.style.padding = "12px"; panel.style.boxShadow = "0 10px 25px rgba(0,0,0,0.1)"; panel.style.fontSize = "13px"; panel.style.display = "none"; // Header const hdr = document.createElement('div'); hdr.style.display = "flex"; hdr.style.justifyContent = "space-between"; hdr.style.alignItems = "center"; hdr.style.marginBottom = "10px"; hdr.style.fontWeight = "600"; hdr.style.color = "#374151"; hdr.style.fontSize = "15px"; hdr.textContent = "Prompt Profiles"; const btnClose = document.createElement('button'); btnClose.textContent = "✕"; btnClose.style.background = "transparent"; btnClose.style.border = "none"; btnClose.style.color = "#9ca3af"; btnClose.style.cursor = "pointer"; btnClose.style.fontSize = "14px"; btnClose.onclick = () => panel.style.display = "none"; hdr.appendChild(btnClose); panel.appendChild(hdr); // Select const select = document.createElement('select'); select.style.width = "100%"; select.style.height = "60px"; select.style.marginBottom = "8px"; select.style.background = "#f9fafb"; select.style.color = "#111827"; select.style.border = "1px solid #d1d5db"; select.style.borderRadius = "6px"; select.style.fontFamily = "inherit"; updateSelectOptions(select, lastProfileName); panel.appendChild(select); // Textarea const ta = document.createElement('textarea'); ta.style.width = "100%"; ta.style.height = "100px"; ta.style.marginBottom = "8px"; ta.style.padding = "10px"; ta.style.background = "#f9fafb"; ta.style.color = "#111827"; ta.style.border = "1px solid #d1d5db"; ta.style.borderRadius = "6px"; ta.style.resize = "vertical"; ta.style.fontFamily = "inherit"; ta.placeholder = "Isi prompt profile..."; panel.appendChild(ta); // Status const status = document.createElement('div'); status.style.marginTop = "6px"; status.style.fontSize = "12px"; status.style.color = "#4b5563"; status.style.textAlign = "center"; status.style.padding = "6px"; status.style.background = "#f3f4f6"; status.style.borderRadius = "6px"; status.textContent = "Siap"; // Tombol const row = document.createElement('div'); row.style.display = "grid"; row.style.gridTemplateColumns = "1fr 1fr"; row.style.gap = "8px"; function mkBtn(label, cb, bg = "#3b82f6", color = "#fff") { const b = document.createElement('button'); b.textContent = label; b.style.padding = "7px"; b.style.border = "none"; b.style.borderRadius = "6px"; b.style.background = bg; b.style.color = color; b.style.fontSize = "12px"; b.style.fontWeight = "500"; b.style.cursor = "pointer"; b.style.transition = "background 0.2s"; b.onmouseover = () => b.style.background = darken(bg, 10); b.onmouseout = () => b.style.background = bg; b.onclick = cb; return b; } // Helper: warna gelap sedikit function darken(hex, percent) { const num = parseInt(hex.replace("#",""), 16); const amt = Math.round(2.55 * percent); const R = (num >> 16) - amt; const G = (num >> 8 & 0x00FF) - amt; const B = (num & 0x0000FF) - amt; return "#" + (0x1000000 + (R<0?0:R)*65536 + (G<0?0:G)*256 + (B<0?0:B)).toString(16).slice(1); } const btnNew = mkBtn("New", () => { const input = prompt("Nama profil baru:"); if (!input || !input.trim()) { status.textContent = "❌ Nama tidak valid."; return; } const name = input.trim(); if (profiles.some(p => p.name === name)) { status.textContent = `❌ Sudah ada "${name}".`; return; } profiles.push({ name, content: ta.value || "" }); saveProfilesToStorage(); updateSelectOptions(select, name); select.value = name; select.dispatchEvent(new Event('change')); setLastProfile(name); status.textContent = `✅ "${name}" dibuat.`; }, "#10b981"); const btnSave = mkBtn("Save", () => { const name = select.value; if (!name) { status.textContent = "❌ Pilih profil dulu."; return; } const idx = profiles.findIndex(p => p.name === name); if (idx !== -1) { profiles[idx].content = ta.value; } saveProfilesToStorage(); ta.value = profiles[idx].content; syncTextarea(); status.textContent = `💾 "${name}" tersimpan.`; }); const btnRename = mkBtn("Rename", () => { const oldName = select.value; if (!oldName) { status.textContent = "❌ Pilih profil dulu."; return; } const newName = prompt("Nama baru:", oldName); if (!newName || !newName.trim()) { status.textContent = "❌ Nama baru tidak valid."; return; } const trimmed = newName.trim(); if (profiles.some(p => p.name === trimmed)) { status.textContent = `❌ Sudah ada "${trimmed}".`; return; } const idx = profiles.findIndex(p => p.name === oldName); profiles[idx].name = trimmed; if (lastProfileName === oldName) { setLastProfile(trimmed); } saveProfilesToStorage(); updateSelectOptions(select, trimmed); select.value = trimmed; syncTextarea(); status.textContent = `✏️ "${oldName}" → "${trimmed}"`; }); const btnDelete = mkBtn("Delete", () => { const name = select.value; if (!name) { status.textContent = "❌ Pilih profil dulu."; return; } if (confirm(`Hapus "${name}"?`)) { profiles = profiles.filter(p => p.name !== name); saveProfilesToStorage(); updateSelectOptions(select); ta.value = ""; syncTextarea(); if (lastProfileName === name) { localStorage.removeItem(LAST_PROFILE_KEY); lastProfileName = null; } status.textContent = `🗑️ "${name}" dihapus.`; } }, "#ef4444"); const btnApply = mkBtn("Apply →", async () => { const name = select.value; if (!name) { status.textContent = "❌ Pilih profil dulu."; return; } const idx = profiles.findIndex(p => p.name === name); if (idx === -1) return; const text = profiles[idx].content; ta.value = text; syncTextarea(); setLastProfile(name); status.textContent = "🔧 Menerapkan..."; const ok = await applyTextToEditor(text, status); if (!ok) status.textContent = "📋 Gagal otomatis — teks disalin!"; }); // Backup / Restore const btnBackup = mkBtn("Backup", () => { const blob = new Blob([JSON.stringify(profiles, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `nai-profiles-backup-${new Date().toISOString().split('T')[0]}.json`; a.click(); URL.revokeObjectURL(url); status.textContent = "📦 Backup berhasil diunduh."; }, "#8b5cf6"); const btnRestore = mkBtn("Restore", () => { const input = document.createElement('input'); input.type = 'file'; input.accept = '.json'; input.onchange = (e) => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = (ev) => { try { const data = JSON.parse(ev.target.result); if (Array.isArray(data)) { profiles = data; saveProfilesToStorage(); updateSelectOptions(select); ta.value = ""; syncTextarea(); status.textContent = `🔄 ${data.length} profil dipulihkan.`; } else { status.textContent = "❌ Format file tidak valid."; } } catch (err) { status.textContent = "❌ Gagal baca file."; console.error(err); } }; reader.readAsText(file); }; input.click(); }, "#06b6d4"); // Masukkan semua tombol ke dalam grid 2x3 row.appendChild(btnNew); row.appendChild(btnSave); row.appendChild(btnRename); row.appendChild(btnDelete); row.appendChild(btnApply); row.appendChild(btnBackup); panel.appendChild(row); // Buat baris baru untuk Restore — full width, ukuran sama const restoreContainer = document.createElement('div'); restoreContainer.style.marginTop = "8px"; restoreContainer.style.display = "grid"; restoreContainer.style.gridTemplateColumns = "1fr"; /* Lebar penuh */ restoreContainer.style.gap = "8px"; restoreContainer.appendChild(btnRestore); panel.appendChild(restoreContainer); // Status di akhir panel.appendChild(status); // Sinkronisasi textarea function syncTextarea() { ta.dispatchEvent(new Event('input', { bubbles: true })); ta.dispatchEvent(new Event('change', { bubbles: true })); } // Muat saat pilih select.addEventListener('change', () => { const name = select.value; const profile = profiles.find(p => p.name === name); ta.value = profile ? profile.content : ""; syncTextarea(); if (name) setLastProfile(name); status.textContent = name ? `📄 Dimuat: ${name}` : "Tidak ada profil dipilih."; }); // Auto-load profil terakhir saat buka panel toggle.addEventListener('click', (e) => { e.stopPropagation(); const rect = toggle.getBoundingClientRect(); panel.style.top = `${rect.bottom + window.scrollY + 4}px`; panel.style.left = `${rect.left + window.scrollX}px`; if (panel.style.display === "none" && lastProfileName) { const profile = profiles.find(p => p.name === lastProfileName); if (profile) { select.value = lastProfileName; ta.value = profile.content; syncTextarea(); } } panel.style.display = panel.style.display === "none" ? "block" : "none"; }); // Shortcut Ctrl+1 s/d Ctrl+9 document.addEventListener('keydown', (e) => { if (!e.ctrlKey || e.altKey || e.shiftKey) return; const key = e.key; if (!/^[1-9]$/.test(key)) return; e.preventDefault(); const idx = parseInt(key) - 1; if (idx >= profiles.length || idx < 0) { status.textContent = `⚠️ Profil ${key} belum ada.`; return; } const { name, content } = profiles[idx]; ta.value = content; syncTextarea(); status.textContent = `⚡ Ctrl+${key}: Apply "${name}"`; applyTextToEditor(content, status).catch(console.error); setLastProfile(name); }); document.body.appendChild(panel); } // Tunda inisialisasi setTimeout(() => { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', createPanelOnce); } else { createPanelOnce(); } }, 1000); const observer = new MutationObserver(() => { if (!document.getElementById('nai-profiles-panel')) { createPanelOnce(); } }); observer.observe(document.body, { childList: true, subtree: true }); })();