// 聊天室偏好与每日状态工具,承接从 Blade 内联脚本迁移出的纯数据规整逻辑。 export const BLOCKABLE_SYSTEM_SENDERS = ["钓鱼播报", "星海小博士", "百家乐", "跑马", "神秘箱子"]; export const BLOCKED_SYSTEM_SENDERS_STORAGE_KEY = "chat_blocked_system_senders"; export const CHAT_SOUND_MUTED_STORAGE_KEY = "chat_sound_muted"; let soundMuteEventsBound = false; let blockMenuEventsBound = false; /** * 规整聊天室偏好对象,过滤非法配置并补齐默认值。 * * @param {Record|null|undefined} raw * @param {string[]} blockableSystemSenders * @returns {{blocked_system_senders:string[],sound_muted:boolean}} */ export function normalizeChatPreferences(raw, blockableSystemSenders = BLOCKABLE_SYSTEM_SENDERS) { const blocked = Array.isArray(raw?.blocked_system_senders) ? raw.blocked_system_senders.filter((sender) => blockableSystemSenders.includes(sender)) : []; return { blocked_system_senders: Array.from(new Set(blocked)), sound_muted: Boolean(raw?.sound_muted), }; } /** * 解析并标准化状态到期时间。 * * @param {string|null|undefined} expiresAt * @returns {Date|null} */ export function parseDailyStatusExpiry(expiresAt) { if (!expiresAt) { return null; } const parsed = new Date(expiresAt); return Number.isNaN(parsed.getTime()) ? null : parsed; } /** * 将状态对象规整为前端统一结构,并过滤掉已过期状态。 * * @param {Record|null|undefined} raw * @param {number} nowTimestamp * @returns {{key:string,label:string,icon:string,group:string,expires_at:string}|null} */ export function normalizeDailyStatus(raw, nowTimestamp = Date.now()) { if (!raw || typeof raw !== "object") { return null; } const key = String(raw.key ?? raw.daily_status_key ?? ""); const label = String(raw.label ?? raw.daily_status_label ?? ""); const icon = String(raw.icon ?? raw.daily_status_icon ?? ""); const group = String(raw.group ?? raw.daily_status_group ?? ""); const expiresAt = raw.expires_at ?? raw.daily_status_expires_at ?? null; const parsedExpiry = parseDailyStatusExpiry(expiresAt); if (!key || !label || !icon || !parsedExpiry) { return null; } if (parsedExpiry.getTime() <= nowTimestamp) { return null; } return { key, label, icon, group, expires_at: parsedExpiry.toISOString(), }; } /** * 从本地缓存读取已屏蔽的系统播报发送者列表。 * * @param {string[]} blockableSystemSenders * @returns {string[]} */ export function loadBlockedSystemSenders(blockableSystemSenders = BLOCKABLE_SYSTEM_SENDERS) { try { const saved = JSON.parse(localStorage.getItem(BLOCKED_SYSTEM_SENDERS_STORAGE_KEY) || "[]"); if (!Array.isArray(saved)) { return []; } return saved.filter((sender) => blockableSystemSenders.includes(sender)); } catch (error) { return []; } } /** * 将屏蔽的系统播报发送者列表写入本地缓存。 * * @param {Iterable} senders * @returns {void} */ export function persistBlockedSystemSenders(senders) { localStorage.setItem(BLOCKED_SYSTEM_SENDERS_STORAGE_KEY, JSON.stringify(Array.from(senders))); } /** * 判断当前禁音开关是否处于打开状态。 * * @returns {boolean} */ export function isSoundMuted() { const muteCheckbox = document.getElementById("sound_muted"); if (muteCheckbox) { return Boolean(muteCheckbox.checked); } return localStorage.getItem(CHAT_SOUND_MUTED_STORAGE_KEY) === "1"; } /** * 同步禁音复选框和本地缓存。 * * @param {boolean} muted 是否禁音 * @returns {boolean} */ export function setSoundMuted(muted) { const normalizedMuted = Boolean(muted); localStorage.setItem(CHAT_SOUND_MUTED_STORAGE_KEY, normalizedMuted ? "1" : "0"); const muteCheckbox = document.getElementById("sound_muted"); if (muteCheckbox) { muteCheckbox.checked = normalizedMuted; } return normalizedMuted; } /** * 绑定禁音复选框事件,后端保存逻辑由调用方提供。 * * @param {(muted:boolean)=>void|Promise} onChange 禁音变化回调 * @returns {void} */ export function bindSoundMuteControl(onChange) { if (soundMuteEventsBound || typeof document === "undefined") { return; } soundMuteEventsBound = true; document.addEventListener("change", (event) => { if (!(event.target instanceof HTMLInputElement) || event.target.id !== "sound_muted") { return; } const muted = setSoundMuted(event.target.checked); if (muted && typeof window.EffectSounds !== "undefined") { window.EffectSounds.stop(); } if (typeof onChange === "function") { void onChange(muted); } }); } /** * 绑定系统播报屏蔽菜单打开与菜单内点击拦截事件。 * * @returns {void} */ export function bindBlockMenuControls() { if (blockMenuEventsBound || typeof document === "undefined") { return; } blockMenuEventsBound = true; document.addEventListener("change", (event) => { if (!(event.target instanceof HTMLInputElement)) { return; } const sender = event.target.dataset.chatBlockSender; if (!sender || typeof window.toggleBlockedSystemSender !== "function") { return; } window.toggleBlockedSystemSender(sender, event.target.checked); }); document.addEventListener("click", (event) => { if (!(event.target instanceof Element)) { return; } const featureMenuTrigger = event.target.closest("[data-chat-feature-menu-toggle]"); if (featureMenuTrigger) { event.preventDefault(); window.toggleFeatureMenu?.(event); return; } const dailyStatusCloseButton = event.target.closest("[data-chat-daily-status-close]"); if (dailyStatusCloseButton) { event.preventDefault(); window.closeDailyStatusEditor?.(); return; } const dailyStatusClearButton = event.target.closest("[data-chat-daily-status-clear]"); if (dailyStatusClearButton) { event.preventDefault(); window.clearDailyStatus?.(); return; } const dailyStatusItem = event.target.closest("[data-chat-daily-status-select]"); if (dailyStatusItem) { event.preventDefault(); const statusKey = dailyStatusItem.getAttribute("data-chat-daily-status-select") || ""; if (statusKey && typeof window.updateDailyStatus === "function") { window.updateDailyStatus(statusKey); } return; } if (event.target.closest("[data-chat-daily-status-editor]")) { event.stopPropagation(); return; } if (event.target.closest("[data-chat-daily-status-overlay]")) { window.closeDailyStatusEditor?.(); return; } const localClearButton = event.target.closest("[data-chat-feature-local-clear]"); if (localClearButton) { event.preventDefault(); window.handleFeatureLocalClear?.(); return; } const dailyStatusOpenButton = event.target.closest("[data-chat-daily-status-open]"); if (dailyStatusOpenButton) { event.preventDefault(); window.openDailyStatusEditor?.(); return; } const dailySignInButton = event.target.closest("[data-chat-feature-sign-in]"); if (dailySignInButton) { event.preventDefault(); window.closeFeatureMenu?.(); window.quickDailySignIn?.(); return; } const shortcutButton = event.target.closest("[data-chat-feature-shortcut]"); if (shortcutButton) { event.preventDefault(); const action = shortcutButton.getAttribute("data-chat-feature-shortcut") || ""; if (action && typeof window.runFeatureShortcut === "function") { window.runFeatureShortcut(action); } return; } if (event.target.closest("[data-chat-feature-menu]")) { event.stopPropagation(); return; } const trigger = event.target.closest("[data-chat-block-menu-toggle]"); if (trigger) { event.preventDefault(); window.toggleBlockMenu?.(event); return; } if (event.target.closest("[data-chat-block-menu]")) { event.stopPropagation(); } }); } /** * 当前登录账号没有服务端偏好时,判断是否需要迁移旧本地偏好。 * * @param {{blocked_system_senders?:string[],sound_muted?:boolean}} serverPreferences * @param {string[]} localBlockedSenders * @param {boolean} localMuted * @returns {boolean} */ export function shouldMigrateLocalChatPreferences(serverPreferences, localBlockedSenders, localMuted) { const hasServerPreferences = (serverPreferences?.blocked_system_senders || []).length > 0 || Boolean(serverPreferences?.sound_muted); return !hasServerPreferences && (localBlockedSenders.length > 0 || localMuted); }