// ==UserScript==
// @name Irodori-TTS Emoji Palette (Invincible v2.8)
// @namespace http://tampermonkey.net/
// @version 2.8
// @description 絶対に見切れない&画面更新で消えない最強版パレット
// @author Gemini
// @match http://127.0.0.1:7860/*
// @match http://localhost:7860
// @grant none
// ==/UserScript==
(function() {
'use strict';
// 公式ドキュメントに基づいた正確な39種類
const CATEGORIES = [
{
name: "感情・表情",
emojis: [
{ char: "😊", label: "楽しげ" }, { char: "😆", label: "喜び" }, { char: "🤭", label: "笑い" },
{ char: "😏", label: "からかう" }, { char: "😌", label: "安堵" }, { char: "😭", label: "嗚咽" },
{ char: "😠", label: "怒り" }, { char: "😱", label: "悲鳴" }, { char: "😲", label: "驚き" },
{ char: "😰", label: "動揺" }, { char: "😒", label: "舌打ち" }, { char: "🤔", label: "疑問" },
{ char: "🥴", label: "酔っ払い" }, { char: "🫣", label: "恥ずかしそう" }, { char: "🥺", label: "震える声" },
{ char: "😪", label: "眠そうに" }, { char: "😖", label: "苦しげ" }, { char: "😟", label: "心配" },
{ char: "🙄", label: "呆れ" }, { char: "🥵", label: "喘ぎ・うめき" }, { char: "🫶", label: "優しく" }
]
},
{
name: "呼吸・生理・発声",
emojis: [
{ char: "🌬️", label: "息切れ" }, { char: "😮💨", label: "吐息" }, { char: "😮", label: "息をのむ" },
{ char: "🥱", label: "あくび" }, { char: "🤧", label: "咳・くしゃみ" }, { char: "🥤", label: "飲む音" },
{ char: "👅", label: "舐める音" }, { char: "💋", label: "リップノイズ" }, { char: "🤐", label: "口を塞がれて" },
{ char: "🎵", label: "鼻歌" }
]
},
{
name: "動作・記号・演出",
emojis: [
{ char: "👂", label: "囁き" }, { char: "📢", label: "エコー" }, { char: "📞", label: "電話越し" },
{ char: "🐢", label: "ゆっくり" }, { char: "⏸️", label: "間・沈黙" }, { char: "⏩", label: "早口" },
{ char: "👌", label: "相槌" }, { char: "🙏", label: "懇願" }
]
}
];
let lastActiveTextarea = null;
document.addEventListener('focusin', (e) => {
if (e.target.tagName === 'TEXTAREA') lastActiveTextarea = e.target;
});
function injectEmoji(emoji) {
const textarea = lastActiveTextarea || document.querySelector('textarea[data-testid="textbox"]');
if (!textarea) return;
const start = textarea.selectionStart;
const text = textarea.value;
const newValue = text.substring(0, start) + emoji + text.substring(textarea.selectionEnd);
const descriptor = Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value");
descriptor.set.call(textarea, newValue);
textarea.dispatchEvent(new Event('input', { bubbles: true }));
textarea.dispatchEvent(new Event('change', { bubbles: true }));
textarea.focus();
setTimeout(() => {
const pos = start + emoji.length;
textarea.setSelectionRange(pos, pos);
window.getSelection().removeAllRanges();
}, 20);
}
// パレットの開閉状態を記憶しておく変数
let isPaletteOpen = false;
function createPalette() {
if (document.getElementById('irodori-emoji-fixed-bar')) return;
const wrapper = document.createElement('div');
wrapper.id = 'irodori-emoji-fixed-bar';
Object.assign(wrapper.style, {
position: 'fixed', bottom: '0', left: '0', width: '100%',
backgroundColor: '#262626', borderTop: '2px solid #ff9800',
zIndex: '2147483647', // 絶対に他の要素に隠れない最強のz-index
boxShadow: '0 -2px 10px rgba(0,0,0,0.8)',
display: 'flex', flexDirection: 'column'
});
const toggleBtn = document.createElement('div');
toggleBtn.innerText = isPaletteOpen ? '▼ Close Palette ▼' : '▲ Irodori Emoji Palette ▲';
Object.assign(toggleBtn.style, {
height: '25px', backgroundColor: '#ff9800', color: '#000',
fontSize: '12px', fontWeight: 'bold', textAlign: 'center',
lineHeight: '25px', cursor: 'pointer', userSelect: 'none', flexShrink: '0'
});
const content = document.createElement('div');
Object.assign(content.style, {
padding: '10px 10px 20px 10px',
maxHeight: '40vh', // 画面の高さの40%に制限(絶対に見切れない)
overflowY: 'auto',
display: isPaletteOpen ? 'block' : 'none'
});
toggleBtn.onclick = () => {
isPaletteOpen = !isPaletteOpen;
content.style.display = isPaletteOpen ? 'block' : 'none';
toggleBtn.innerText = isPaletteOpen ? '▼ Close Palette ▼' : '▲ Irodori Emoji Palette ▲';
};
CATEGORIES.forEach(cat => {
const catLabel = document.createElement('div');
catLabel.innerText = cat.name;
Object.assign(catLabel.style, { color: '#ff9800', fontSize: '11px', margin: '8px 0 4px', borderBottom: '1px solid #444' });
content.appendChild(catLabel);
const grid = document.createElement('div');
Object.assign(grid.style, {
display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(110px, 1fr))', gap: '6px'
});
cat.emojis.forEach(item => {
const btn = document.createElement('div');
Object.assign(btn.style, {
display: 'flex', alignItems: 'center', gap: '5px', padding: '5px',
backgroundColor: '#333', borderRadius: '4px', cursor: 'pointer', border: '1px solid #444'
});
btn.innerHTML = `${item.char}${item.label}`;
btn.onclick = () => injectEmoji(item.char);
btn.onmouseover = () => { btn.style.backgroundColor = '#444'; btn.style.borderColor = '#ff9800'; };
btn.onmouseout = () => { btn.style.backgroundColor = '#333'; btn.style.borderColor = '#444'; };
grid.appendChild(btn);
});
content.appendChild(grid);
});
wrapper.appendChild(toggleBtn);
wrapper.appendChild(content);
document.body.appendChild(wrapper);
}
// 初回起動
setTimeout(createPalette, 1000);
// 【重要】画面遷移などでパレットが消されても、2秒ごとに生存確認して自動復活させる
setInterval(() => {
if (!document.getElementById('irodori-emoji-fixed-bar')) {
createPalette();
}
}, 2000);
})();