// ==UserScript==
// @name Discord Midjourney 参数面板
// @namespace https://github.com/cwser
// @version 0.3.0
// @license MIT
// @description 在 Discord 中添加 Midjourney 参数设置面板,支持完整卡片式 UI 和最新参数功能(⚠️⚠️⚠️需开启开发者模式)
// @author cwser
// @match https://discord.com/*
// @icon https://www.midjourney.com/favicon.ico
// @grant unsafeWindow
// @supportURL https://github.com/cwser
// @homepageURL https://github.com/cwser
// @downloadURL https://update.greasyfork.icu/scripts/535676/Discord%20Midjourney%20%E5%8F%82%E6%95%B0%E9%9D%A2%E6%9D%BF.user.js
// @updateURL https://update.greasyfork.icu/scripts/535676/Discord%20Midjourney%20%E5%8F%82%E6%95%B0%E9%9D%A2%E6%9D%BF.meta.js
// ==/UserScript==
(function() {
'use strict';
// 参数定义
let params = {
prompt: '',
ar: '1:1',
stylize: 100,
weird: 0,
chaos: 0,
mode: 'standard',
version: 'v7',
speed: 'relax',
draft: false,
noPrompt: '',
cref: [],
sref: [],
oref: [],
iref: [], // iref 似乎未在UI中使用,但保留定义
directImages: [],
tile: false,
seed: '',
quality: 1,
stop: 100,
visibility: '',
personalParams: '',
r: 1
};
// 主题相关变量
let currentThemeMode = 'discord'; // 选项: 'light', 'dark', 'discord', 'system'
const themeStorageKey = 'mjPanelThemePreference_v3'; // 更新存储键
const themeModes = ['light', 'dark', 'discord', 'system'];
const themeTextMap = {
'light': '浅色模式',
'dark': '深色模式',
'discord': '跟随Discord',
'system': '跟随系统'
};
const sunIconSVG = ``;
const moonIconSVG = ``;
const discordIconSVG = ``;
const systemIconSVG = ``;
const themeIcons = {
'light': sunIconSVG,
'dark': moonIconSVG,
'discord': discordIconSVG,
'system': systemIconSVG
};
let systemThemeMediaQuery = null;
let discordThemeObserver = null;
// 定义权重参数前缀的映射
const weightPrefixes = {
'cref': '--cw',
'sref': '--sw',
'oref': '--ow',
'directImages': '--iw'
};
// 显示 Toast 提示
function showToast(message) {
const toast = document.createElement('div');
toast.textContent = message;
toast.className = 'mj-toast'; // 使用类进行样式化
toast.style.cssText = `transform: translateY(-20px); opacity: 0;`; // 初始状态,CSS会覆盖
document.body.appendChild(toast);
// 触发入场动画
setTimeout(() => { toast.style.transform = 'translateY(0)'; toast.style.opacity = '1'; }, 10);
// 触发离场动画并在动画结束后移除
setTimeout(() => {
toast.style.transform = 'translateY(-20px)'; toast.style.opacity = '0';
setTimeout(() => { if (document.body.contains(toast)) document.body.removeChild(toast); }, 300);
}, 2000);
}
// 创建设置按钮
function createSettingButton() {
const button = document.createElement('button');
button.textContent = 'MJ参数';
button.id = 'mj-floating-settings-button';
button.addEventListener('click', toggleControlPanel);
document.body.appendChild(button);
button.addEventListener('mouseenter', () => button.style.transform = 'scale(1.05)');
button.addEventListener('mouseleave', () => button.style.transform = 'scale(1)');
}
// 切换控制面板显示/隐藏
function toggleControlPanel() {
const panel = document.getElementById('mj-control-panel');
if (panel) {
if (panel.style.display === 'none') {
panel.style.display = 'block'; panel.style.opacity = '0'; panel.style.transform = 'translateY(10px)';
setTimeout(() => { panel.style.opacity = '1'; panel.style.transform = 'translateY(0)'; }, 10); // 入场动画
} else {
const themeMenu = document.getElementById('theme-options-menu');
if (themeMenu) themeMenu.style.display = 'none'; // 关闭主题菜单
panel.style.opacity = '0'; panel.style.transform = 'translateY(10px)'; // 离场动画
setTimeout(() => { panel.style.display = 'none'; }, 200); // 动画完成后隐藏
}
}
}
// 重置参数为默认值
function resetParams() {
params = {
prompt: '', ar: '1:1', stylize: 100, weird: 0, chaos: 0, mode: 'standard',
version: 'v7', speed: 'relax', draft: false, noPrompt: '', cref: [],
sref: [], oref: [], iref: [], directImages: [], tile: false, seed: '',
quality: 1, stop: 100, visibility: '', personalParams: '', r: 1
};
}
// 更新最终的提示词参数字符串
function updatePromptParams() {
const { prompt, ar, stylize, weird, chaos, mode, draft, noPrompt, version, cref, sref, speed, oref, directImages, tile, seed, quality, stop, visibility, personalParams } = params;
const otherParts = [
`--ar ${ar}`, `--s ${stylize}`,
weird !== 0 ? `--w ${weird}` : '', chaos !== 0 ? `--c ${chaos}` : '',
mode !== 'standard' ? `--${mode}` : '', draft ? '--draft' : '',
noPrompt ? `--no ${noPrompt}` : '',
version.startsWith('niji') ? `--niji ${version.replace('niji', '')}` : `--v ${version.replace('v', '')}`,
speed !== 'relax' ? `--${speed}` : '', tile ? '--tile' : '',
seed ? `--seed ${seed}` : '', quality !== 1 ? `--q ${quality}` : '', // 只有非默认值才添加
stop !== 100 ? `--stop ${stop}` : '', visibility ? `--${visibility}` : '', // public 或 stealth
personalParams ? `--p ${personalParams}` : '', params.r > 1 ? `--r ${params.r}` : ''
];
const formatImageWithWeight = (url, weight, prefix) => `${url}${weight && weight.trim() !== '' ? ` ${prefix} ${weight.trim()}` : ''}`;
const directImageUrls = directImages.map(item => formatImageWithWeight(item.url, item.weight, '--iw')).join(' ');
const promptField = document.getElementById('prompt-params');
if (promptField) {
const allParts = [
directImageUrls,
prompt.trim(),
...cref.map(item => `--cref ${formatImageWithWeight(item.url, item.weight, '--cw')}`),
...sref.map(item => `--sref ${formatImageWithWeight(item.url, item.weight, '--sw')}`),
...oref.map(item => `--oref ${formatImageWithWeight(item.url, item.weight, '--ow')}`),
...otherParts.filter(Boolean)
].filter(Boolean);
promptField.value = allParts.join(' ').trim();
}
}
// 获取实际生效的暗黑模式状态
function getEffectiveDarkModeState() {
switch (currentThemeMode) {
case 'light': return false;
case 'dark': return true;
case 'discord': return document.documentElement.classList.contains('theme-dark');
case 'system':
return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
default:
return document.documentElement.classList.contains('theme-dark');
}
}
// 应用当前主题
function applyCurrentTheme() {
const panel = document.getElementById('mj-control-panel');
if (!panel) return;
const effectiveDarkMode = getEffectiveDarkModeState();
panel.classList.toggle('dark-mode', effectiveDarkMode);
localStorage.setItem(themeStorageKey, currentThemeMode);
const themeTriggerButton = document.getElementById('theme-dropdown-trigger');
const themeTriggerIcon = document.getElementById('theme-trigger-icon');
const themeTriggerText = document.getElementById('theme-trigger-text');
if (themeTriggerIcon) {
let iconToShow = themeIcons[currentThemeMode] || sunIconSVG;
// If discord or system, show actual light/dark icon
if (currentThemeMode === 'discord' || currentThemeMode === 'system') {
iconToShow = effectiveDarkMode ? moonIconSVG : sunIconSVG;
}
themeTriggerIcon.innerHTML = iconToShow;
}
if (themeTriggerText) {
themeTriggerText.textContent = themeTextMap[currentThemeMode] || '未知主题';
}
if (themeTriggerButton) {
themeTriggerButton.title = `切换主题 (当前: ${themeTextMap[currentThemeMode]})`;
}
// 更新主题菜单中选项的激活状态
const themeOptions = document.querySelectorAll('#theme-options-menu button');
themeOptions.forEach(opt => {
opt.classList.toggle('active', opt.dataset.theme === currentThemeMode);
});
refreshPreviews();
setInitialActiveButtons();
updateToggleVisuals('tile-toggle-switch', 'tile');
updateToggleVisuals('draft-toggle-switch', 'draft');
setupDynamicThemeListeners();
}
// 处理系统主题变化
function handleSystemThemeChange() {
if (currentThemeMode === 'system') applyCurrentTheme();
}
// 处理Discord主题变化
function handleDiscordThemeChange(mutationsList) {
if (currentThemeMode === 'discord') {
for (const mutation of mutationsList) {
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
applyCurrentTheme();
break;
}
}
}
}
// 设置动态主题监听器
function setupDynamicThemeListeners() {
if (systemThemeMediaQuery) {
systemThemeMediaQuery.removeEventListener ? systemThemeMediaQuery.removeEventListener('change', handleSystemThemeChange) : systemThemeMediaQuery.removeListener(handleSystemThemeChange);
systemThemeMediaQuery = null;
}
if (discordThemeObserver) {
discordThemeObserver.disconnect();
discordThemeObserver = null;
}
if (currentThemeMode === 'system' && window.matchMedia) {
systemThemeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
systemThemeMediaQuery.addEventListener ? systemThemeMediaQuery.addEventListener('change', handleSystemThemeChange) : systemThemeMediaQuery.addListener(handleSystemThemeChange);
} else if (currentThemeMode === 'discord' && typeof MutationObserver !== "undefined") {
discordThemeObserver = new MutationObserver(handleDiscordThemeChange);
discordThemeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] });
}
}
// 创建控制面板HTML结构
function createControlPanel() {
const panel = document.createElement('div');
panel.id = 'mj-control-panel';
panel.style.cssText = `
display: none; position: fixed; right: 20px; bottom: 80px;
width: 1080px; max-width: calc(100% - 40px);
border-radius: 12px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
padding: 20px; z-index: 10000; border: 1px solid #E5E7EB;
max-height: 90vh; overflow-y: auto; font-family: sans-serif;
transition: opacity 0.2s ease, transform 0.2s ease;
`;
panel.innerHTML = `
模型设置
更多参数
可见性 (--public, --stealth)
`;
document.body.appendChild(panel);
const savedTheme = localStorage.getItem(themeStorageKey);
if (savedTheme && themeModes.includes(savedTheme)) {
currentThemeMode = savedTheme;
} else {
currentThemeMode = 'discord';
}
bindControlEvents();
applyCurrentTheme();
// 点击外部关闭主题菜单
document.addEventListener('click', function(event) {
const themeMenu = document.getElementById('theme-options-menu');
const themeTrigger = document.getElementById('theme-dropdown-trigger');
if (themeMenu && themeTrigger && themeMenu.style.display === 'block') {
if (!themeMenu.contains(event.target) && !themeTrigger.contains(event.target)) {
themeMenu.style.display = 'none';
}
}
});
}
// 设置按钮的激活类。CSS处理特定主题的外观。
function setInitialActiveButtons() {
const buttonGroups = [
{ className: 'speed-btn', param: 'speed', defaultValue: 'relax' },
{ className: 'mode-btn', param: 'mode', defaultValue: 'standard' },
{ className: 'visibility-btn', param: 'visibility', defaultValue: '' }
];
buttonGroups.forEach(group => {
const buttons = document.querySelectorAll(`#mj-control-panel .${group.className}`);
let currentParamValue = params[group.param] === undefined ? group.defaultValue : params[group.param];
buttons.forEach(btn => {
btn.classList.toggle('active', btn.dataset.value === currentParamValue);
});
});
const ratioSlider = document.getElementById('ratio-slider');
if (ratioSlider) ratioSlider.dispatchEvent(new Event('input'));
}
// 通过添加/移除 'active' 类来更新切换开关的视觉效果。CSS处理外观。
function updateToggleVisuals(switchId, property) {
const switchEl = document.getElementById(switchId);
if (!switchEl) return;
switchEl.classList.toggle('active', !!params[property]);
}
// 设置参考图/代码区域的功能
const setupRefSection = (idPrefix, paramKey) => {
const $ = id => document.getElementById(id);
const addBtn = $(`${idPrefix}-add`);
const urlInput = $(`${idPrefix}-url`);
const weightInput = $(`${idPrefix}-weight`);
const previewContainerId = `${idPrefix}-preview`;
if (!addBtn || !urlInput || !weightInput) return;
addBtn.onclick = () => {
const url = urlInput.value.trim();
const weight = weightInput.value.trim();
let toastMessage = `请输入${paramKey === 'sref' ? 'URL或代码' : 'URL'}`;
if (!url) { showToast(toastMessage); return; }
const isImageUrl = /^https?:\/\/.+\.(jpg|jpeg|png|webp|gif|svg|bmp|tiff|ico)(\?.*)?$/i.test(url);
const isSrefCode = paramKey === 'sref' && (/^\d+$/.test(url) || url.toLowerCase() === 'random');
if (paramKey !== 'sref' && !isImageUrl) { showToast('请输入有效的图片URL'); return; }
if (paramKey === 'sref' && !isImageUrl && !isSrefCode) { showToast("sref请输入有效图片URL, 'random'或数字代码"); return; }
const targetArray = params[paramKey];
if (!Array.isArray(targetArray)) return;
const itemUrl = (paramKey === 'sref' && isSrefCode) ? (url.toLowerCase() === 'random' ? 'random' : url) : url;
if (!targetArray.some(item => item.url === itemUrl)) {
targetArray.push({ url: itemUrl, weight: weight });
const currentWeightPrefix = weightPrefixes[paramKey];
if (paramKey === 'sref' && isSrefCode) {
addPreviewSrefCode(previewContainerId, itemUrl, weight);
} else {
addPreviewImage(previewContainerId, itemUrl, weight, paramKey, currentWeightPrefix);
}
urlInput.value = ''; weightInput.value = '';
updatePromptParams();
} else {
showToast(`该${(paramKey === 'sref' && isSrefCode) ? '代码' : 'URL'}已添加`);
}
};
};
// 绑定控制面板的事件
function bindControlEvents() {
const $ = id => document.getElementById(id);
// 主题下拉菜单逻辑
const themeTriggerButton = $('theme-dropdown-trigger');
const themeOptionsMenu = $('theme-options-menu');
if (themeTriggerButton && themeOptionsMenu) {
themeTriggerButton.addEventListener('click', (event) => {
event.stopPropagation(); // 防止触发全局点击监听器
themeOptionsMenu.style.display = themeOptionsMenu.style.display === 'none' ? 'block' : 'none';
});
document.querySelectorAll('.theme-option-button').forEach(button => {
button.addEventListener('click', () => {
currentThemeMode = button.dataset.theme;
applyCurrentTheme();
themeOptionsMenu.style.display = 'none';
});
});
}
if ($('main-prompt')) $('main-prompt').oninput = e => { params.prompt = e.target.value; updatePromptParams(); };
const bindSliderEvent = (id, property, displayId = null, isFloat = false) => {
const slider = $(id); if (!slider) return;
slider.oninput = e => {
const value = isFloat ? parseFloat(e.target.value) : parseInt(e.target.value, 10);
params[property] = value;
if (displayId && $(displayId)) $(displayId).textContent = e.target.value;
updatePromptParams();
};
};
bindSliderEvent('stylize', 'stylize', 'stylize-value');
bindSliderEvent('weird', 'weird', 'weird-value');
bindSliderEvent('chaos', 'chaos', 'chaos-value');
bindSliderEvent('stop-slider', 'stop', 'stop-value');
bindSliderEvent('r-slider', 'r', 'r-value');
const qualitySlider = $('quality-slider'); const qualityValueDisplay = $('quality-value');
const qualityMap = [0.25, 0.5, 1, 2, 4];
if (qualitySlider && qualityValueDisplay) {
const initialQualityValue = params.quality;
const initialSliderIndex = qualityMap.indexOf(initialQualityValue);
if (initialSliderIndex !== -1) {
qualitySlider.value = initialSliderIndex;
qualityValueDisplay.textContent = initialQualityValue;
} else {
params.quality = 1;
qualitySlider.value = qualityMap.indexOf(1);
qualityValueDisplay.textContent = params.quality;
}
qualitySlider.oninput = e => {
const sliderIndex = parseInt(e.target.value, 10);
if (sliderIndex >= 0 && sliderIndex < qualityMap.length) {
params.quality = qualityMap[sliderIndex];
if (qualityValueDisplay) qualityValueDisplay.textContent = params.quality;
updatePromptParams();
}
};
}
const bindRadioGroup = (groupClass, property) => {
document.querySelectorAll(`#mj-control-panel .${groupClass}`).forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll(`#mj-control-panel .${groupClass}`).forEach(b => b.classList.remove('active'));
btn.classList.add('active');
params[property] = btn.dataset.value;
updatePromptParams();
});
});
};
bindRadioGroup('speed-btn', 'speed');
bindRadioGroup('mode-btn', 'mode');
bindRadioGroup('visibility-btn', 'visibility');
const bindToggleSwitchEvent = (switchId, property) => {
const switchEl = $(switchId); if (!switchEl) return;
switchEl.addEventListener('click', () => {
params[property] = !params[property];
updateToggleVisuals(switchId, property);
updatePromptParams();
});
};
bindToggleSwitchEvent('tile-toggle-switch', 'tile');
bindToggleSwitchEvent('draft-toggle-switch', 'draft');
if ($('version-select')) $('version-select').onchange = e => { params.version = e.target.value; updatePromptParams(); };
if ($('no-prompt')) $('no-prompt').oninput = e => { params.noPrompt = e.target.value.trim(); updatePromptParams(); };
if ($('copy-btn')) {
$('copy-btn').onclick = () => {
const textarea = $('prompt-params');
if (!textarea || !textarea.value) { showToast('没有参数可以拷贝'); return; }
textarea.select();
try {
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(textarea.value)
.then(() => showToast('参数已复制!'))
.catch(() => { showToast('复制失败!请尝试手动复制。'); legacyCopy(textarea); });
} else {
legacyCopy(textarea);
}
} catch (err) {
console.error('复制尝试失败:', err);
}
};
}
function legacyCopy(textareaElement) {
try {
const successful = document.execCommand('copy');
showToast(successful ? '参数已复制 (兼容模式)!' : '复制失败 (兼容模式)!请手动复制。');
} catch (err) {
showToast('复制异常 (兼容模式)!请手动复制。');
}
}
const sizeMap = ['1:2', '9:16', '2:3', '3:4', '5:6', '1:1', '6:5', '4:3', '3:2', '16:9', '2:1'];
if ($('clear-btn')) {
$('clear-btn').onclick = () => {
resetParams();
if ($('main-prompt')) $('main-prompt').value = params.prompt;
if ($('stylize')) $('stylize').value = params.stylize; if ($('stylize-value')) $('stylize-value').textContent = params.stylize;
if ($('weird')) $('weird').value = params.weird; if ($('weird-value')) $('weird-value').textContent = params.weird;
if ($('chaos')) $('chaos').value = params.chaos; if ($('chaos-value')) $('chaos-value').textContent = params.chaos;
if ($('version-select')) $('version-select').value = params.version;
if ($('no-prompt')) $('no-prompt').value = params.noPrompt;
if ($('ratio-slider')) {
const defaultRatioIndex = sizeMap.indexOf(params.ar);
$('ratio-slider').value = defaultRatioIndex !== -1 ? defaultRatioIndex : 5;
$('ratio-slider').dispatchEvent(new Event('input'));
}
['cref-url', 'cref-weight', 'sref-url', 'sref-weight', 'oref-url', 'oref-weight', 'direct-image-url', 'direct-image-weight']
.forEach(id => { if ($(id)) $(id).value = ''; });
if ($('seed-input')) $('seed-input').value = params.seed;
if ($('quality-slider')) {
const resetSliderIndex = qualityMap.indexOf(params.quality);
$('quality-slider').value = resetSliderIndex !== -1 ? resetSliderIndex : qualityMap.indexOf(1);
const qvDisplay = $('quality-value');
if(qvDisplay) qvDisplay.textContent = params.quality;
}
if ($('stop-slider')) $('stop-slider').value = params.stop; if ($('stop-value')) $('stop-value').textContent = params.stop;
if ($('personal-params')) $('personal-params').value = params.personalParams;
if ($('r-slider')) $('r-slider').value = params.r; if ($('r-value')) $('r-value').textContent = params.r;
updateToggleVisuals('tile-toggle-switch', 'tile');
updateToggleVisuals('draft-toggle-switch', 'draft');
setInitialActiveButtons();
refreshPreviews();
updatePromptParams();
showToast('所有参数已重置为默认值');
};
}
setupRefSection('cref', 'cref');
setupRefSection('sref', 'sref');
setupRefSection('oref', 'oref');
setupRefSection('direct-image', 'directImages');
const ratioPresets = { '1:2': { width: 50, height: 100 }, '9:16': { width: 56.25, height: 100 }, '2:3': { width: 66.67, height: 100 }, '3:4': { width: 75, height: 100 }, '5:6': { width: 83.33, height: 100 }, '1:1': { width: 100, height: 100 }, '6:5': { width: 100, height: 83.33 }, '4:3': { width: 100, height: 75 }, '3:2': { width: 100, height: 66.67 }, '16:9': { width: 100, height: 56.25 }, '2:1': { width: 100, height: 50 }};
const ratioSlider = $('ratio-slider');
if (ratioSlider) {
ratioSlider.oninput = e => {
const ratio = sizeMap[+e.target.value] || '1:1'; params.ar = ratio;
const box = $('ratio-box'); const preset = ratioPresets[ratio] || { width: 100, height: 100 };
if(box) { box.style.width = `${preset.width}px`; box.style.height = `${preset.height}px`; box.textContent = ratio; }
document.querySelectorAll('#size-buttons button').forEach(btn => {
btn.classList.toggle('active', btn.dataset.value === ratio);
});
updatePromptParams();
};
}
const sizeButtonGroup = $('size-buttons');
if (sizeButtonGroup) {
const presetMap = { '纵向': '1:2', '正方形': '1:1', '横向': '2:1' };
['纵向', '正方形', '横向'].forEach((label) => {
const btn = document.createElement('button'); btn.textContent = label;
btn.dataset.value = presetMap[label];
btn.onclick = () => {
const ratio = presetMap[label]; const sliderIndex = sizeMap.indexOf(ratio);
if (sliderIndex !== -1 && ratioSlider) {
ratioSlider.value = sliderIndex;
ratioSlider.dispatchEvent(new Event('input'));
}
};
sizeButtonGroup.appendChild(btn);
});
}
if ($('seed-input')) {
$('seed-input').oninput = e => {
const value = e.target.value.trim();
if (/^\d*$/.test(value) && (value === '' || (parseInt(value) >= 0 && parseInt(value) <= 4294967295))) {
params.seed = value;
} else {
e.target.value = params.seed;
}
updatePromptParams();
};
}
if ($('personal-params')) $('personal-params').oninput = e => { params.personalParams = e.target.value.trim(); updatePromptParams(); };
const promptParamsTextarea = $('prompt-params');
if (promptParamsTextarea) {
promptParamsTextarea.addEventListener('click', () => {
if (!promptParamsTextarea.value) { showToast('没有参数可以拷贝'); return; }
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(promptParamsTextarea.value)
.then(() => showToast('参数已复制!'))
.catch(() => { showToast('复制失败!'); legacyCopy(promptParamsTextarea);});
} else {
legacyCopy(promptParamsTextarea);
}
});
}
document.querySelectorAll('#mj-control-panel input[type="text"], #mj-control-panel input[type="number"], #mj-control-panel textarea, #mj-control-panel select').forEach(el => {
el.addEventListener('focus', (e) => e.target.classList.add('focused'));
el.addEventListener('blur', (e) => e.target.classList.remove('focused'));
});
updateToggleVisuals('tile-toggle-switch', 'tile');
updateToggleVisuals('draft-toggle-switch', 'draft');
setInitialActiveButtons();
if (ratioSlider) {
const initialRatioIndex = sizeMap.indexOf(params.ar);
ratioSlider.value = initialRatioIndex !== -1 ? initialRatioIndex : 5;
ratioSlider.dispatchEvent(new Event('input'));
}
}
// 添加图片预览到容器
function addPreviewImage(containerId, url, weight, paramKey, currentWeightPrefix) {
const container = document.getElementById(containerId); if (!container) return;
const imgContainer = document.createElement('div');
imgContainer.className = 'ref-image-container';
imgContainer.style.animation = 'fadeIn 0.3s ease';
const img = document.createElement('img'); img.src = url;
img.className = 'ref-image-preview';
img.onerror = () => {
const errorDiv = document.createElement('div'); errorDiv.textContent = '无效图';
errorDiv.className = 'ref-image-error';
imgContainer.innerHTML = '';
imgContainer.appendChild(errorDiv);
};
const weightBadge = document.createElement('div');
weightBadge.className = 'ref-weight-badge';
const wP = currentWeightPrefix ? currentWeightPrefix.replace('--', '') : '';
weightBadge.textContent = weight && weight.trim() !== '' ? `${wP}:${weight}` : `${wP}:默认`;
const deleteBtn = document.createElement('button');
deleteBtn.className = 'ref-delete-btn';
deleteBtn.innerHTML = '×';
deleteBtn.onclick = function(e) {
e.stopPropagation();
imgContainer.style.animation = 'fadeOut 0.2s ease forwards';
setTimeout(() => {
const targetArray = params[paramKey];
if (Array.isArray(targetArray)) {
const index = targetArray.findIndex(item => item.url === url);
if (index !== -1) {
targetArray.splice(index, 1);
if (container.contains(imgContainer)) container.removeChild(imgContainer);
updatePromptParams();
}
}
}, 200);
};
imgContainer.appendChild(img); imgContainer.appendChild(weightBadge); imgContainer.appendChild(deleteBtn);
container.appendChild(imgContainer);
}
// 添加sref代码预览到容器
function addPreviewSrefCode(containerId, code, weight) {
const container = document.getElementById(containerId); if (!container) return;
const codeContainer = document.createElement('div');
codeContainer.className = 'ref-code-container';
codeContainer.style.animation = 'fadeIn 0.3s ease';
const codeText = document.createElement('span'); codeText.textContent = `sref:${code}`;
const weightBadge = document.createElement('div');
weightBadge.className = 'ref-code-weight-badge';
weightBadge.textContent = weight && weight.trim() !== '' ? `sw:${weight}` : 'sw:默认';
const deleteBtn = document.createElement('button');
deleteBtn.className = 'ref-code-delete-btn';
deleteBtn.innerHTML = '×';
deleteBtn.onclick = function(e) {
e.stopPropagation(); codeContainer.style.animation = 'fadeOut 0.2s ease forwards';
setTimeout(() => {
const index = params.sref.findIndex(item => item.url === code);
if (index !== -1) {
params.sref.splice(index, 1);
if (container.contains(codeContainer)) container.removeChild(codeContainer);
updatePromptParams();
}
}, 200);
};
codeContainer.appendChild(codeText); codeContainer.appendChild(weightBadge); codeContainer.appendChild(deleteBtn);
container.appendChild(codeContainer);
}
// 刷新所有参考图/代码的预览区域
function refreshPreviews() {
const refTypes = [
{ idPrefix: 'cref', paramKey: 'cref', previewId: 'cref-preview' },
{ idPrefix: 'sref', paramKey: 'sref', previewId: 'sref-preview' },
{ idPrefix: 'oref', paramKey: 'oref', previewId: 'oref-preview' },
{ idPrefix: 'direct-image', paramKey: 'directImages', previewId: 'direct-image-preview' }
];
refTypes.forEach(ref => {
const container = document.getElementById(ref.previewId); if (!container) return;
container.innerHTML = '';
const items = params[ref.paramKey];
if (items && Array.isArray(items)) {
items.forEach(item => {
const currentWeightPrefix = weightPrefixes[ref.paramKey];
if (ref.paramKey === 'sref' && (item.url === 'random' || /^\d+$/.test(item.url))) {
addPreviewSrefCode(ref.previewId, item.url, item.weight);
} else {
addPreviewImage(ref.previewId, item.url, item.weight, ref.paramKey, currentWeightPrefix);
}
});
}
});
}
// 注入CSS样式
function injectStyles() {
const styleSheet = document.createElement("style");
styleSheet.type = "text/css";
styleSheet.innerText = `
/* 入场/出场动画 */
@keyframes fadeIn { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } }
@keyframes fadeOut { from { opacity: 1; transform: translateY(0); } to { opacity: 0; transform: translateY(5px); } }
/* MJ参数浮动按钮 */
#mj-floating-settings-button {
position: fixed; right: 20px; bottom: 20px; padding: 10px 20px;
background-color: #5865F2; color: white; border: none; border-radius: 8px;
cursor: pointer; z-index: 9999; box-shadow: 0 4px 12px rgba(0,0,0,0.15);
transition: background-color 0.2s ease, transform 0.2s ease; font-family: sans-serif;
}
#mj-floating-settings-button:hover { background-color: #4752C4; transform: scale(1.05); }
/* Toast提示样式 */
.mj-toast {
position: fixed; top: 20px; right: 20px; padding: 10px 16px; border-radius: 6px;
z-index: 99999; transition: all 0.3s ease; font-family: sans-serif;
}
#mj-control-panel:not(.dark-mode) ~ body .mj-toast, .mj-toast {
background: #2B2D31; color: #DCDDDE; box-shadow: 0 4px 12px rgba(0,0,0,0.15);
}
#mj-control-panel.dark-mode ~ body .mj-toast {
background: #DCDDDE; color: #2B2D31; box-shadow: 0 4px 12px rgba(0,0,0,0.3);
}
/* 面板基础样式 */
#mj-control-panel { background: white; color: #111827; border-color: #E0E0E0; }
#mj-control-panel .panel-title { color: #111827; }
#mj-control-panel .panel-divider { background: #e5e7eb; }
#mj-control-panel .panel-section { background: #f9fafb; padding:16px; border-radius:8px;}
#mj-control-panel .section-title { margin:0 0 8px 0; font-weight:500; color: #1F2937; }
#mj-control-panel label, #mj-control-panel .param-value-display { color: #1F2937; }
#mj-control-panel input[type="text"], #mj-control-panel input[type="number"],
#mj-control-panel textarea, #mj-control-panel select {
background: white; color: #111827; border: 1px solid #D1D5DB;
padding: 6px 10px; border-radius: 6px; box-sizing: border-box;
transition: border-color 0.2s ease, background-color 0.2s ease, color 0.2s ease;
font-size: 14px; /* 统一字体大小 */
}
#mj-control-panel select {
-webkit-appearance: none; /* Remove default Mac/iOS styling */
-moz-appearance: none; /* Remove default Firefox styling */
appearance: none; /* Remove default styling */
background-image: url('data:image/svg+xml;charset=US-ASCII,');
background-repeat: no-repeat;
background-position: right 0.7em top 50%;
background-size: 0.8em auto;
padding-right: 2.5em; /* Make space for the arrow */
}
#mj-control-panel input[type="text"].focused, #mj-control-panel input[type="number"].focused,
#mj-control-panel textarea.focused, #mj-control-panel select.focused { border-color: #4f46e5; }
#mj-control-panel textarea { resize: vertical; height: 80px; width:100%; padding:10px; }
#mj-control-panel #prompt-params { background:#f9fafb; cursor:pointer; }
#mj-control-panel .results-section-divider { border-top-color: #e5e7eb; }
/* 主题切换触发按钮 */
#mj-control-panel .theme-trigger-btn {
padding: 6px 12px; border-radius: 6px; border: 1px solid #D1D5DB;
background-color: white; color: #1F2937; font-size: 13px;
cursor: pointer; display: inline-flex; align-items: center;
transition: border-color 0.2s ease, background-color 0.2s ease, color 0.2s ease;
}
#mj-control-panel .theme-trigger-btn:hover { border-color: #4f46e5; }
#mj-control-panel .theme-trigger-btn svg { width: 16px; height: 16px; }
/* 主题选项菜单 */
#mj-control-panel .theme-options-menu {
position: absolute; top: calc(100% + 5px); right: 0;
background-color: white; border: 1px solid #D1D5DB;
border-radius: 6px; box-shadow: 0 2px 8px rgba(0,0,0,0.1);
z-index: 10010; min-width: 180px; padding: 4px;
}
#mj-control-panel .theme-option-button {
display: flex; align-items: center; width: 100%;
padding: 8px 12px; text-align: left; background: none; border: none;
cursor: pointer; font-size: 13px; color: #1F2937; border-radius: 4px;
transition: background-color 0.15s ease, color 0.15s ease;
}
#mj-control-panel .theme-option-button svg { margin-right: 8px; width:16px; height:16px; opacity: 0.7; }
#mj-control-panel .theme-option-button:hover { background-color: #f0f0f0; }
#mj-control-panel .theme-option-button.active { background-color: #eef2ff; color: #4338ca; font-weight: 500; }
#mj-control-panel .theme-option-button.active svg { opacity: 1; }
/* 比例预览 浅色 */
#mj-control-panel .ratio-preview-bg { position:absolute; top:0; left:0; width:100px; height:100px; border:2px dashed #d1d5db; border-radius:12px; }
#mj-control-panel .ratio-box { position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); background:#f3f4f6; border:2px solid #374151; border-radius:6px; display:flex; align-items:center; justify-content:center; font-size:12px; color:#374151; padding:2px 4px; min-width:20px; min-height:12px; box-sizing: border-box; }
/* 滑块 浅色 */
#mj-control-panel input[type="range"] { -webkit-appearance: none; appearance: none; background: transparent; cursor: pointer; width: 100%;}
#mj-control-panel input[type="range"]::-webkit-slider-runnable-track { background: #E5E7EB; height: 0.4rem; border-radius: 0.2rem; }
#mj-control-panel input[type="range"]::-moz-range-track { background: #E5E7EB; height: 0.4rem; border-radius: 0.2rem; }
#mj-control-panel input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; margin-top: -0.3rem; background-color: #4f46e5; height: 1rem; width: 1rem; border-radius: 50%; border: 1px solid #3730a3; }
#mj-control-panel input[type="range"]::-moz-range-thumb { border: none; border-radius: 50%; background-color: #4f46e5; height: 1rem; width: 1rem; border: 1px solid #3730a3;}
/* 按钮组 浅色 (通用) */
#mj-control-panel .btn-group { display:flex; border-radius:6px; overflow:hidden; border:1px solid #d1d5db; }
#mj-control-panel .btn-group button { flex:1; padding:6px 10px; background:white; border:none; cursor:pointer; color: #374151; transition: background-color 0.2s, color 0.2s; font-size: 13px;}
#mj-control-panel .btn-group button:not(:last-child) { border-right: 1px solid #d1d5db; }
#mj-control-panel .btn-group button.active { background: #4f46e5; color: white; }
/* 切换开关 浅色 */
#mj-control-panel .toggle-switch { position:relative; width:40px; height:20px; border-radius:10px; background:#e5e7eb; cursor:pointer; margin-top: 4px; transition: background-color 0.2s ease; }
#mj-control-panel .toggle-switch .toggle-dot { position:absolute; top:2px; left:2px; width:16px; height:16px; border-radius:50%; background:white; box-shadow:0 1px 3px rgba(0,0,0,0.2); transition:all 0.2s ease; }
#mj-control-panel .toggle-switch.active { background:#4f46e5; }
#mj-control-panel .toggle-switch.active .toggle-dot { transform: translateX(20px); }
/* 操作按钮 浅色 */
#mj-control-panel .action-button, #mj-control-panel .action-button-primary, #mj-control-panel .action-button-secondary { padding:4px 8px; border:none; border-radius:4px; cursor:pointer; transition:all 0.2s ease; }
#mj-control-panel .action-button { background:#4f46e5; color:white; }
#mj-control-panel .action-button:hover { background:#4338CA; }
#mj-control-panel .action-button-primary { flex:1; padding:8px; border-radius:6px; background:#4f46e5; color:white; }
#mj-control-panel .action-button-primary:hover { background:#4338CA; }
#mj-control-panel .action-button-secondary { flex:1; padding:8px; border-radius:6px; background:#E5E7EB; color:#374151; border: 1px solid #D1D5DB; }
#mj-control-panel .action-button-secondary:hover { background:#D1D5DB; }
#mj-control-panel #size-buttons button { padding: 4px 12px; border-radius: 6px; cursor: pointer; transition: all 0.2s ease; background: white; color: #374151; border: 1px solid #D1D5DB; font-size: 13px;}
#mj-control-panel #size-buttons button.active { background: #4f46e5; color: white; border-color: #4f46e5; }
#mj-control-panel #size-buttons button:hover:not(.active) { background: #f0f0f0; }
/* 参考图预览 浅色 */
#mj-control-panel .ref-preview-container { margin-top:10px; display:flex; flex-wrap:wrap; gap:8px; }
#mj-control-panel .ref-image-container { position: relative; margin: 4px; }
#mj-control-panel .ref-image-preview { width: 60px; height: 60px; object-fit: cover; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); border: 1px solid #E5E7EB; transition: all 0.2s ease; }
#mj-control-panel .ref-image-preview:hover { transform: scale(1.05); box-shadow: 0 2px 6px rgba(0,0,0,0.15); }
#mj-control-panel .ref-image-error { width:60px; height:60px; display:flex; align-items:center; justify-content:center; border-radius:4px; font-size:10px; text-align:center; background: #F3F4F6; color: #757575; border: 1px solid #E5E7EB; }
#mj-control-panel .ref-weight-badge { position: absolute; bottom: 0; left: 0; background: rgba(255,255,255,0.8); color: #374151; font-size: 10px; padding: 1px 3px; border-radius: 0 4px 0 0; border-top: 1px solid #4f46e5; border-right: 1px solid #4f46e5; }
#mj-control-panel .ref-delete-btn { position: absolute; top: -5px; right: -5px; background: rgba(239, 68, 68, 0.85); color: white; border: none; border-radius: 50%; width: 18px; height: 18px; font-size: 12px; line-height: 18px; text-align:center; cursor:pointer; transition:all 0.2s ease; opacity: 0.8; display:flex; align-items:center; justify-content:center; box-shadow: 0 1px 2px rgba(0,0,0,0.2); }
#mj-control-panel .ref-delete-btn:hover { opacity: 1; transform: scale(1.1); background: rgb(220, 38, 38); }
#mj-control-panel .ref-code-container { position: relative; margin: 4px; border-radius: 4px; padding: 6px 10px; display: inline-flex; align-items: center; font-family: monospace; font-size: 12px; background: #F3F4F6; color: #111827; border: 1px solid #E0E0E0; }
#mj-control-panel .ref-code-weight-badge { margin-left: 8px; font-size: 10px; padding: 2px 4px; border-radius: 3px; background: rgba(79, 70, 229, 0.1); color: #4f46e5; }
#mj-control-panel .ref-code-delete-btn { margin-left: 10px; border: none; border-radius: 4px; padding: 2px 5px; font-size: 10px; line-height:1; cursor:pointer; transition:all 0.2s ease; background: rgba(239, 68, 68, 0.1); color: #ef4444; }
#mj-control-panel .ref-code-delete-btn:hover { background: rgba(239, 68, 68, 0.25); }
/* 暗黑模式样式 */
#mj-control-panel.dark-mode { background: #2B2D31; color: #DCDDDE; border-color: #202225; }
#mj-control-panel.dark-mode .panel-title { color: #DCDDDE; }
#mj-control-panel.dark-mode .panel-divider { background: #40444B; }
#mj-control-panel.dark-mode .panel-section { background: #202225; }
#mj-control-panel.dark-mode .section-title { color: #DCDDDE; }
#mj-control-panel.dark-mode label, #mj-control-panel.dark-mode .param-value-display { color: #DCDDDE; }
#mj-control-panel.dark-mode input[type="text"],
#mj-control-panel.dark-mode input[type="number"],
#mj-control-panel.dark-mode textarea {
background: #202225; color: #DCDDDE; border-color: #40444B;
}
/* ===== BEGIN MODIFIED SECTION FOR DARK MODE SELECT ===== */
#mj-control-panel.dark-mode select {
/* Use dark mode colors */
background-color: #202225 !important; /* Dark background from other dark inputs */
color: #DCDDDE !important; /* Light text from other dark inputs */
border: 1px solid #40444B !important;/* Dark border from other dark inputs */
box-shadow: none !important; /* Crucial for removing visual artifacts */
/* Appearance and custom arrow */
-webkit-appearance: none !important;
-moz-appearance: none !important;
appearance: none !important;
/* Arrow styling - use a LIGHT arrow on the DARK background.
fill color should be light, e.g., #DCDDDE (matches dark mode text color)
*/
background-image: url('data:image/svg+xml;charset=US-ASCII,') !important;
background-repeat: no-repeat !important;
background-position: right 0.7em top 50% !important;
background-size: 0.8em auto !important;
/* Padding from light mode select (base input padding + specific right padding for arrow) */
padding: 6px 10px !important;
padding-right: 2.5em !important; /* Make space for the arrow */
/* Other base properties from light mode select */
border-radius: 6px !important;
box-sizing: border-box !important;
font-size: 14px !important;
transition: border-color 0.2s ease, background-color 0.2s ease, color 0.2s ease; /* Retain transitions */
}
/* ===== END MODIFIED SECTION FOR DARK MODE SELECT ===== */
#mj-control-panel.dark-mode input[type="text"]::placeholder,
#mj-control-panel.dark-mode input[type="number"]::placeholder,
#mj-control-panel.dark-mode textarea::placeholder { color: #72767d; opacity: 1; }
/* Adjust focus for select in dark mode and ensure other inputs also use a consistent dark mode focus */
#mj-control-panel.dark-mode input[type="text"].focused,
#mj-control-panel.dark-mode input[type="number"].focused,
#mj-control-panel.dark-mode textarea.focused,
#mj-control-panel.dark-mode select.focused {
border-color: #7289DA !important; /* Example dark mode focus color (e.g., Discord's accent blue) */
}
#mj-control-panel.dark-mode #prompt-params { background:#202225; } /* This is a textarea, follows general dark input rules */
#mj-control-panel.dark-mode .results-section-divider { border-top-color: #40444B; }
/* 暗黑模式下的主题触发按钮和菜单 */
#mj-control-panel.dark-mode .theme-trigger-btn {
border-color: #2D2F34; background-color: #2B2D31; color: #DCDDDE;
}
#mj-control-panel.dark-mode .theme-trigger-btn:hover { border-color: #7289DA; }
#mj-control-panel.dark-mode .theme-options-menu {
background-color: #2B2D31; border-color: #202225;
}
#mj-control-panel.dark-mode .theme-option-button { color: #DCDDDE; }
#mj-control-panel.dark-mode .theme-option-button:hover { background-color: #393c43; }
#mj-control-panel.dark-mode .theme-option-button.active { background-color: #404EED; color: white; }
/* 比例预览 暗黑 */
#mj-control-panel.dark-mode .ratio-preview-bg { border-color: #40444B; }
#mj-control-panel.dark-mode .ratio-box { background: #40444B; color: #DCDDDE; border-color: #70747A; }
/* 滑块 暗黑 */
#mj-control-panel.dark-mode input[type="range"]::-webkit-slider-runnable-track { background: #40444B; }
#mj-control-panel.dark-mode input[type="range"]::-moz-range-track { background: #40444B; }
#mj-control-panel.dark-mode input[type="range"]::-webkit-slider-thumb { background-color: #7289DA; border-color: #5865F2; }
#mj-control-panel.dark-mode input[type="range"]::-moz-range-thumb { background-color: #7289DA; border-color: #5865F2; }
/* 按钮组 暗黑 (通用) */
#mj-control-panel.dark-mode .btn-group { border-color: #2D2F34; }
#mj-control-panel.dark-mode .btn-group button { background: #40444B; color: #DCDDDE; }
#mj-control-panel.dark-mode .btn-group button:not(:last-child) { border-right-color: #2D2F34; }
#mj-control-panel.dark-mode .btn-group button.active { background: #5865F2; color: white; }
/* 切换开关 暗黑 */
#mj-control-panel.dark-mode .toggle-switch { background: #4E4F52; }
#mj-control-panel.dark-mode .toggle-switch .toggle-dot { background: #B9BBBE; }
#mj-control-panel.dark-mode .toggle-switch.active { background: #5865F2; }
#mj-control-panel.dark-mode .toggle-switch.active .toggle-dot { transform: translateX(20px); }
/* 操作按钮 暗黑 */
#mj-control-panel.dark-mode .action-button { background:#5865F2; color:white; }
#mj-control-panel.dark-mode .action-button:hover { background:#4752C4; }
#mj-control-panel.dark-mode .action-button-primary { background:#5865F2; color:white; border: 1px solid #5865F2;}
#mj-control-panel.dark-mode .action-button-primary:hover { background:#4752C4; }
#mj-control-panel.dark-mode .action-button-secondary { background:#40444B; color:#DCDDDE; border: 1px solid #2D2F34; }
#mj-control-panel.dark-mode .action-button-secondary:hover { background:#4F545C; }
#mj-control-panel.dark-mode #size-buttons button { background: #40444B; color: #DCDDDE; border-color: #2D2F34; }
#mj-control-panel.dark-mode #size-buttons button.active { background: #5865F2; color: white; border-color: #5865F2; }
#mj-control-panel.dark-mode #size-buttons button:hover:not(.active) { background: #4f545c; }
/* 参考图预览 暗黑 */
#mj-control-panel.dark-mode .ref-image-preview { border-color: #40444B; }
#mj-control-panel.dark-mode .ref-image-preview:hover { box-shadow: 0 2px 6px rgba(0,0,0,0.3); }
#mj-control-panel.dark-mode .ref-image-error { background: #40444B; color: #aaa; border: 1px solid #555; }
#mj-control-panel.dark-mode .ref-weight-badge { background: rgba(0,0,0,0.7); color: white; border-top-color: #5865F2; border-right-color: #5865F2; }
#mj-control-panel.dark-mode .ref-delete-btn { background: rgba(239, 68, 68, 0.7); }
#mj-control-panel.dark-mode .ref-delete-btn:hover { background: rgb(239, 68, 68); }
#mj-control-panel.dark-mode .ref-code-container { background: #202225; color: #DCDDDE; border-color: #2D2F34; }
#mj-control-panel.dark-mode .ref-code-weight-badge { background: rgba(88, 101, 242, 0.3); color: #B9BBBE; }
#mj-control-panel.dark-mode .ref-code-delete-btn { background: rgba(239, 68, 68, 0.3); color: #FAA; }
#mj-control-panel.dark-mode .ref-code-delete-btn:hover { background: rgba(239, 68, 68, 0.5); }
`;
document.head.appendChild(styleSheet);
}
// 初始化函数
function init() {
injectStyles();
resetParams(); // 或从存储中加载
createSettingButton();
createControlPanel(); // 这也会处理初始主题检测和UI更新
updatePromptParams();
}
const discordAppMount = document.getElementById('app-mount');
if (discordAppMount) {
setTimeout(init, 500);
} else {
window.addEventListener('load', () => setTimeout(init, 500), { once: true });
}
})();