(() => { "use strict"; const qs = (selector, root = document) => root.querySelector(selector); const qsa = (selector, root = document) => Array.from(root.querySelectorAll(selector)); const clone = (value) => JSON.parse(JSON.stringify(value)); const MODULES = ["site", "navigation", "composer", "threads", "recommendations", "activity"]; const MODULE_META = { site: ["站点信息", "品牌、主题与当前账号"], navigation: ["导航", "信息流、搜索与移动导航"], composer: ["发布器", "发帖弹窗和字数限制"], threads: ["串文", "主串文、引用与嵌套回复"], recommendations: ["推荐", "关注建议与热门话题"], activity: ["动态", "互动通知面板"] }; const embeddedData = JSON.parse(qs("#threadsPageData").textContent); const initialData = clone(embeddedData); const fingerprint = (text) => { let hash = 2166136261; for (let index = 0; index < text.length; index += 1) { hash ^= text.charCodeAt(index); hash = Math.imul(hash, 16777619); } return (hash >>> 0).toString(36); }; const pageFingerprint = fingerprint(JSON.stringify(initialData)); const storageKey = "threads-page-data-v1:" + pageFingerprint; const themeKey = "threads-page-theme-v1:" + pageFingerprint; const readStorage = (key) => { try { return window.localStorage ? window.localStorage.getItem(key) : null; } catch (_error) { return null; } }; const writeStorage = (key, value) => { try { if (window.localStorage) window.localStorage.setItem(key, value); return true; } catch (_error) { return false; } }; const removeStorage = (key) => { try { if (window.localStorage) window.localStorage.removeItem(key); } catch (_error) { /* Opaque sandbox previews may intentionally deny storage. */ } }; const isObject = (value) => Boolean(value && typeof value === "object" && !Array.isArray(value)); const isCompleteData = (value) => Boolean(isObject(value) && MODULES.every((name) => isObject(value[name]))); const cachedData = readStorage(storageKey); let pageData = initialData; if (cachedData) { try { const parsed = JSON.parse(cachedData); if (isCompleteData(parsed)) pageData = parsed; } catch (_error) { pageData = initialData; } } const escapeHTML = (value) => String(value == null ? "" : value).replace(/[&<>'"]/g, (character) => ({ "&": "&", "<": "<", ">": ">", "'": "'", '"': """ })[character]); const safeRemoteUrl = (value, kind = "link") => { const source = String(value || "").trim(); if (!source) return ""; const lowered = source.toLowerCase(); if (kind === "image" && lowered.startsWith("data:image/")) return source; if (kind === "video" && lowered.startsWith("data:video/")) return source; try { const url = new URL(source); return url.protocol === "http:" || url.protocol === "https:" ? url.href : ""; } catch (_error) { return ""; } }; const validateRemoteMediaTree = (value, path = "当前模块") => { if (Array.isArray(value)) return value.forEach((item, index) => validateRemoteMediaTree(item, path + "[" + index + "]")); if (!value || typeof value !== "object") return; const media = value.media; if (media && ["image", "video", "webpage"].includes(media.type)) { if ((media.type === "image" || media.type === "video") && !safeRemoteUrl(media.mediaUrl, media.type)) throw new Error(path + ".media.mediaUrl 无效"); if (media.type === "webpage" && !safeRemoteUrl(media.pageUrl)) throw new Error(path + ".media.pageUrl 无效"); if (media.posterUrl && !safeRemoteUrl(media.posterUrl, "image")) throw new Error(path + ".media.posterUrl 无效"); } Object.entries(value).forEach(([key, item]) => { if (key !== "media") validateRemoteMediaTree(item, path + "." + key); }); }; const safeColor = (value, fallback) => /^#[0-9a-f]{6}$/i.test(String(value || "")) ? value : fallback; const icon = (id) => ''; const colors = (person) => { const list = person && Array.isArray(person.colors) ? person.colors : []; return [safeColor(list[0], "#7257f5"), safeColor(list[1], "#d94fbd")]; }; const avatar = (person, className) => { const pair = colors(person || {}); return '' + escapeHTML((person && person.initial) || "T") + ""; }; const metric = (value) => { const number = Math.max(0, Number(value) || 0); return number ? '' + number.toLocaleString("zh-CN") + "" : ""; }; const toast = qs("#toast"); let toastTimer = 0; const showToast = (message) => { toast.textContent = message; toast.classList.add("show"); window.clearTimeout(toastTimer); toastTimer = window.setTimeout(() => toast.classList.remove("show"), 2800); }; const saveData = () => writeStorage(storageKey, JSON.stringify(pageData)); let themeInitialized = false; const syncThemeButtons = () => { const dark = document.documentElement.dataset.theme === "dark"; [qs("#themeToggle"), qs("#mobileTheme")].forEach((button) => { button.innerHTML = icon(dark ? "i-sun" : "i-moon"); button.setAttribute("aria-label", dark ? "切换浅色模式" : "切换深色模式"); }); qs('meta[name="theme-color"]').content = dark ? "#0a0a0a" : "#f5f5f3"; }; const applyInitialTheme = () => { if (themeInitialized) return; themeInitialized = true; const configured = pageData.site.theme; const stored = readStorage(themeKey); const prefersDark = window.matchMedia && window.matchMedia("(prefers-color-scheme:dark)").matches; const next = configured === "dark" || configured === "light" ? configured : (stored === "dark" || stored === "light" ? stored : (prefersDark ? "dark" : "light")); document.documentElement.dataset.theme = next; syncThemeButtons(); }; const toggleTheme = () => { const next = document.documentElement.dataset.theme === "dark" ? "light" : "dark"; document.documentElement.dataset.theme = next; writeStorage(themeKey, next); syncThemeButtons(); }; const renderSite = () => { const site = pageData.site || {}; const user = site.currentUser || {}; document.title = site.pageTitle || site.brand || "Threads"; qs('meta[name="description"]').content = site.description || ""; qs("#brandText").textContent = site.brand || "Threads"; qs("#brandLink").setAttribute("aria-label", (site.brand || "Threads") + " 首页"); qs("#mobileUser").innerHTML = avatar(user, "avatar-sm"); qs("#composerUser").innerHTML = avatar(user); qs("#modalUser").innerHTML = avatar(user); qs("#modalUserName").textContent = user.name || ""; const stats = user.stats || {}; qs("#profileSummary").innerHTML = '
' + avatar(user) + '' + escapeHTML(user.name) + "" + escapeHTML(user.handle) + '
' + '
' + escapeHTML(stats.following) + '关注
' + escapeHTML(stats.followers) + '关注者
' + escapeHTML(stats.threads) + "串聊
"; applyInitialTheme(); }; const renderNavigation = () => { const navigation = pageData.navigation || {}; qs("#feedTitle").textContent = navigation.feedTitle || "首页"; qs("#feedFilter").textContent = navigation.feedFilter || "为你推荐"; qs("#searchInput").placeholder = navigation.searchPlaceholder || "搜索用户或关键词"; qs("#searchHints").textContent = navigation.searchHints || ""; const tabs = Array.isArray(navigation.tabs) ? navigation.tabs : []; qs("#feedTabs").innerHTML = tabs.map((label, index) => '" ).join(""); const labels = Array.isArray(navigation.mobileLabels) ? navigation.mobileLabels : []; const items = [ ["i-home", ""], ["i-search", ' data-panel="searchPanel"'], ["i-plus", " data-open-composer"], ["i-heart", ' data-panel="activityPanel"'], ["i-user", ""] ]; qs("#mobileNav").innerHTML = items.map((item, index) => { const label = labels[index] || ""; if (index === 2) return '' + icon(item[0]) + '' + escapeHTML(label) + ""; return '"; }).join(""); const links = Array.isArray(navigation.footerLinks) ? navigation.footerLinks : []; qs("#sideFooter").innerHTML = links.map((label) => '' + escapeHTML(label) + "").join("") + "" + escapeHTML(navigation.copyright) + ""; }; const renderComposer = () => { const composer = pageData.composer || {}; qs("#composerButton").textContent = composer.inlinePlaceholder || ""; qs("#inlinePublish").textContent = composer.publishLabel || "发布"; qs("#composeTitle").textContent = composer.dialogTitle || "新串聊"; qs("#composeText").placeholder = composer.textareaPlaceholder || ""; qs("#composeText").maxLength = Math.max(1, Number(composer.maxLength) || 500); qs("#modalPublish").textContent = composer.publishLabel || "发布"; qs("#charCount").textContent = String(qs("#composeText").maxLength - qs("#composeText").value.length); }; const autumnGrid = (label) => ''; document.head.insertAdjacentHTML("beforeend", ''); const mediaMarkup = (media) => { if (!isObject(media)) return ""; if (media.type === "autumn-grid") return autumnGrid(media.label); if (media.type === "image") { const source = safeRemoteUrl(media.mediaUrl, "image"); if (!source) return ""; const pageUrl = safeRemoteUrl(media.pageUrl); return '
' + escapeHTML(media.alt || media.label || ' + (pageUrl ? '打开来源' : "") + "
"; } if (media.type === "video") { const source = safeRemoteUrl(media.mediaUrl, "video"); if (!source) return ""; const poster = safeRemoteUrl(media.posterUrl, "image"); const pageUrl = safeRemoteUrl(media.pageUrl); return '
' + (pageUrl ? '打开来源' : "") + "
"; } if (media.type === "webpage") { const pageUrl = safeRemoteUrl(media.pageUrl); if (!pageUrl) return ""; const preview = safeRemoteUrl(media.mediaUrl, "image"); const body = media.embed === true ? '' : (preview ? '' + escapeHTML(media.alt || media.title || ' : ""); return '
' + body + '' + escapeHTML(media.title || pageUrl) + '' + escapeHTML(media.description || "打开网页") + "
"; } return ""; }; const actionsMarkup = (metrics) => '
' + '" + '" + '" + '
"; const quoteMarkup = (quote) => { if (!isObject(quote)) return ""; return '
' + avatar(quote, "avatar-xs") + "" + escapeHTML(quote.name) + '' + escapeHTML(quote.handle) + "

" + escapeHTML(quote.text) + "

"; }; const rowMarkup = (thread, reply, hasNext) => { const person = reply ? reply.author : thread.author; const metrics = reply ? reply.metrics : thread.metrics; const time = reply ? "· " + reply.time : escapeHTML(person.handle) + " · " + escapeHTML(thread.time); return '
' + avatar(person, reply ? "avatar-sm" : "avatar-lg") + '
' + escapeHTML(person.name) + "" + (!reply && person.verified ? '' : "") + '' + time + '' + (!reply && thread.followable ? '' : "") + (!reply ? '" : "") + '

' + escapeHTML(reply ? reply.text : thread.text) + "

" + (!reply ? mediaMarkup(thread.media) : "") + (!reply ? quoteMarkup(thread.quote) : "") + actionsMarkup(metrics || {}) + (!reply ? '
" : "") + "
"; }; const threadMarkup = (thread) => { const replies = Array.isArray(thread.replies) ? thread.replies : []; const search = [thread.author && thread.author.name, thread.author && thread.author.handle, thread.text, thread.preview] .concat(replies.map((reply) => reply.text)).join(" "); return '
' + rowMarkup(thread, null, replies.length > 0) + replies.map((reply, index) => rowMarkup(thread, reply, index < replies.length - 1)).join("") + '
' + replies.slice(0, 3).map((reply) => avatar(reply.author, "avatar-xs")).join("") + '' + escapeHTML(thread.preview) + '
"; }; const renderThreads = () => { const data = pageData.threads || {}; const items = Array.isArray(data.items) ? data.items : []; qs("#threadList").innerHTML = items.map(threadMarkup).join(""); qs("#emptyTitle").textContent = data.emptyTitle || ""; qs("#emptyText").textContent = data.emptyText || ""; qs("#emptyState").style.display = items.length ? "none" : "block"; }; const renderRecommendations = () => { const data = pageData.recommendations || {}; qs("#followTitle").textContent = data.followTitle || ""; qs("#suggestions").innerHTML = (Array.isArray(data.people) ? data.people : []).map((person) => '
' + avatar(person, "avatar-sm") + '' + escapeHTML(person.name) + "" + escapeHTML(person.handle) + '
' ).join(""); qs("#topicTitle").textContent = data.topicTitle || ""; qs("#topics").innerHTML = (Array.isArray(data.topics) ? data.topics : []).map((topic) => '
' + escapeHTML(topic.meta) + "" + escapeHTML(topic.title) + "" + escapeHTML(topic.count) + "
" ).join(""); }; const renderActivity = () => { const data = pageData.activity || {}; qs("#activityTitle").textContent = data.title || ""; qs("#activityList").innerHTML = (Array.isArray(data.items) ? data.items : []).map((item) => '
' + avatar(item, "avatar-sm") + '

' + escapeHTML(item.name) + " " + escapeHTML(item.text) + "

" + (item.followable ? '' : "") + "
" ).join(""); }; const renderAll = () => { renderSite(); renderNavigation(); renderComposer(); renderThreads(); renderRecommendations(); renderActivity(); bindDynamicInteractions(); }; const applySearch = () => { const query = qs("#searchInput").value.trim().toLocaleLowerCase(); let visible = 0; qsa(".thread-card").forEach((thread) => { const match = !query || thread.dataset.search.toLocaleLowerCase().includes(query) || thread.textContent.toLocaleLowerCase().includes(query); thread.hidden = !match; if (match) visible += 1; }); qs("#emptyState").style.display = visible ? "none" : "block"; }; const changeCount = (button, delta) => { const count = qs(".action-count", button); if (count) count.textContent = String(Math.max(0, (Number(count.textContent.replace(/,/g, "")) || 0) + delta)); }; const bindDynamicInteractions = () => { qsa("[data-follow]").forEach((button) => button.addEventListener("click", () => { const following = button.classList.toggle("following"); button.textContent = following ? "正在关注" : "关注"; showToast(following ? "已关注" : "已取消关注"); })); qsa(".feed-tab").forEach((tab) => tab.addEventListener("click", () => { qsa(".feed-tab").forEach((item) => item.classList.remove("active")); tab.classList.add("active"); showToast("已切换到" + tab.textContent); })); qsa(".mobile-nav button").forEach((button) => button.addEventListener("click", () => { qsa(".mobile-nav button").forEach((item) => item.classList.remove("active")); button.classList.add("active"); })); qsa(".like-action").forEach((button) => button.addEventListener("click", () => { const liked = button.classList.toggle("liked"); changeCount(button, liked ? 1 : -1); button.setAttribute("aria-pressed", String(liked)); })); qsa(".repost-action").forEach((button) => button.addEventListener("click", () => { const reposted = button.classList.toggle("reposted"); changeCount(button, reposted ? 1 : -1); showToast(reposted ? "已转发" : "已撤销转发"); })); qsa(".reply-action").forEach((button) => button.addEventListener("click", () => { const form = qs(".reply-form", button.closest(".post-body")); if (!form) return; form.classList.toggle("open"); if (form.classList.contains("open")) qs("input", form).focus(); })); qsa(".reply-form").forEach((form) => form.addEventListener("submit", (event) => { event.preventDefault(); const input = qs("input", form); const value = input.value.trim(); if (!value) return; const bubble = document.createElement("div"); bubble.className = "reply-bubble"; bubble.innerHTML = "" + escapeHTML(pageData.site.currentUser.name) + " · 刚刚" + escapeHTML(value); form.before(bubble); input.value = ""; const button = qs(".reply-action", form.closest(".post-body")); if (button) changeCount(button, 1); showToast("回复已发布"); })); qsa(".share").forEach((button) => button.addEventListener("click", async () => { try { if (navigator.clipboard) await navigator.clipboard.writeText(location.href); showToast("链接已复制"); } catch (_error) { showToast("链接已准备"); } })); qsa(".thread-card").forEach((thread) => { const menu = qs(".post-menu", thread); const more = qs(".more-button", thread); if (menu && more) more.addEventListener("click", (event) => { event.stopPropagation(); qsa(".post-menu.open").forEach((other) => { if (other !== menu) other.classList.remove("open"); }); menu.classList.toggle("open"); }); }); qsa("[data-open-composer]").forEach((button) => { if (!button.dataset.composerBound) { button.dataset.composerBound = "true"; button.addEventListener("click", openCompose); } }); qsa("[data-panel]").forEach((button) => { if (!button.dataset.panelBound) { button.dataset.panelBound = "true"; button.addEventListener("click", openPanel); } }); qs("#searchInput").value = qs("#searchInput").value || ""; applySearch(); }; const overlay = qs("#composeOverlay"); const composeText = qs("#composeText"); const modalPublish = qs("#modalPublish"); const charCount = qs("#charCount"); let modalOpener = null; function openCompose(event) { modalOpener = event.currentTarget; overlay.classList.add("open"); overlay.setAttribute("aria-hidden", "false"); document.body.classList.add("modal-open"); window.setTimeout(() => composeText.focus(), 80); } const closeCompose = () => { overlay.classList.remove("open"); overlay.setAttribute("aria-hidden", "true"); document.body.classList.remove("modal-open"); if (modalOpener) modalOpener.focus(); }; composeText.addEventListener("input", () => { const left = composeText.maxLength - composeText.value.length; charCount.textContent = String(left); charCount.classList.toggle("warn", left <= 30); modalPublish.disabled = !composeText.value.trim(); }); modalPublish.addEventListener("click", () => { const value = composeText.value.trim(); if (!value) return; const user = pageData.site.currentUser; pageData.threads.items.unshift({ id: "new-" + Date.now(), author: {name: user.name, handle: user.handle, initial: user.initial, colors: user.colors, verified: false}, time: "刚刚", text: value, media: null, quote: null, metrics: {likes: 0, replies: 0, reposts: 0}, followable: false, replies: [], preview: "成为第一个回复的人" }); saveData(); renderThreads(); bindDynamicInteractions(); composeText.value = ""; charCount.textContent = String(composeText.maxLength); modalPublish.disabled = true; closeCompose(); qs(".thread-card").scrollIntoView({behavior: "smooth", block: "center"}); showToast("串聊已发布"); }); qs("#closeCompose").addEventListener("click", closeCompose); overlay.addEventListener("click", (event) => { if (event.target === overlay) closeCompose(); }); const closePanels = () => qsa(".panel.open").forEach((panel) => { panel.classList.remove("open"); panel.setAttribute("aria-hidden", "true"); }); function openPanel(event) { event.stopPropagation(); const panel = qs("#" + event.currentTarget.dataset.panel); if (!panel) return; const willOpen = !panel.classList.contains("open"); closePanels(); if (willOpen) { panel.classList.add("open"); panel.setAttribute("aria-hidden", "false"); if (panel.id === "searchPanel") window.setTimeout(() => qs("input", panel).focus(), 80); } } qsa("[data-close-panel]").forEach((button) => button.addEventListener("click", closePanels)); qs("#searchInput").addEventListener("input", applySearch); const settingsOverlay = qs("#settingsOverlay"); const jsonEditor = qs("#jsonEditor"); const jsonStatus = qs("#jsonStatus"); let activeModule = MODULES[0]; let editorDraft = clone(pageData); const validateEditor = () => { try { const parsed = JSON.parse(jsonEditor.value); if (!isObject(parsed)) throw new Error("模块必须是 JSON 对象"); validateRemoteMediaTree(parsed); jsonStatus.textContent = "JSON 有效"; jsonStatus.className = "json-status valid"; return parsed; } catch (error) { jsonStatus.textContent = "JSON 无效:" + error.message; jsonStatus.className = "json-status invalid"; return null; } }; const storeActiveDraft = () => { const parsed = validateEditor(); if (!parsed) return false; editorDraft[activeModule] = parsed; return true; }; const selectModule = (name, force) => { if (!force && !storeActiveDraft()) return; activeModule = name; qsa(".module-tab").forEach((tab) => tab.classList.toggle("active", tab.dataset.module === name)); qs("#editorModuleTitle").textContent = MODULE_META[name][0]; jsonEditor.value = JSON.stringify(editorDraft[name], null, 2); validateEditor(); }; const renderModuleTabs = () => { qs("#moduleTabs").innerHTML = "

页面模块

" + MODULES.map((name) => '" ).join(""); qsa(".module-tab").forEach((tab) => tab.addEventListener("click", () => selectModule(tab.dataset.module, false))); }; const openSettings = () => { editorDraft = clone(pageData); activeModule = MODULES[0]; renderModuleTabs(); selectModule(activeModule, true); settingsOverlay.classList.add("open"); settingsOverlay.setAttribute("aria-hidden", "false"); document.body.classList.add("modal-open", "settings-open"); window.setTimeout(() => jsonEditor.focus(), 80); }; const closeSettings = () => { settingsOverlay.classList.remove("open"); settingsOverlay.setAttribute("aria-hidden", "true"); document.body.classList.remove("modal-open", "settings-open"); qs("#openSettings").focus(); }; qs("#openSettings").addEventListener("click", openSettings); qs("#closeSettings").addEventListener("click", closeSettings); settingsOverlay.addEventListener("click", (event) => { if (event.target === settingsOverlay) closeSettings(); }); jsonEditor.addEventListener("input", validateEditor); jsonEditor.addEventListener("keydown", (event) => { if (event.key === "Tab") { event.preventDefault(); const start = jsonEditor.selectionStart; const end = jsonEditor.selectionEnd; jsonEditor.setRangeText(" ", start, end, "end"); validateEditor(); } }); qs("#applyJson").addEventListener("click", () => { if (!storeActiveDraft() || !isCompleteData(editorDraft)) { showToast("请修正 JSON 后再应用"); return; } pageData = clone(editorDraft); saveData(); renderAll(); showToast("JSON 已应用到页面"); }); qs("#resetJson").addEventListener("click", () => { pageData = clone(initialData); editorDraft = clone(initialData); removeStorage(storageKey); removeStorage(themeKey); themeInitialized = false; renderAll(); renderModuleTabs(); selectModule(activeModule, true); showToast("已恢复初始 JSON"); }); qs("#copyJson").addEventListener("click", async () => { if (!storeActiveDraft()) return; const text = JSON.stringify(editorDraft, null, 2); try { if (navigator.clipboard) await navigator.clipboard.writeText(text); showToast("完整 JSON 已复制"); } catch (_error) { jsonEditor.focus(); jsonEditor.select(); showToast("已选中当前模块,请手动复制"); } }); qs("#downloadJson").addEventListener("click", () => { if (!storeActiveDraft()) return; const blob = new Blob([JSON.stringify(editorDraft, null, 2) + "\n"], {type: "application/json"}); const link = document.createElement("a"); link.href = URL.createObjectURL(blob); link.download = "threads-page-data.json"; link.click(); window.setTimeout(() => URL.revokeObjectURL(link.href), 0); showToast("JSON 已下载"); }); [qs("#themeToggle"), qs("#mobileTheme")].forEach((button) => button.addEventListener("click", toggleTheme)); document.addEventListener("click", (event) => { if (!event.target.closest(".panel") && !event.target.closest("[data-panel]")) closePanels(); if (!event.target.closest(".post-menu") && !event.target.closest(".more-button")) { qsa(".post-menu.open").forEach((menu) => menu.classList.remove("open")); } }); document.addEventListener("keydown", (event) => { if (event.key === "Escape") { closePanels(); if (overlay.classList.contains("open")) closeCompose(); if (settingsOverlay.classList.contains("open")) closeSettings(); } }); qsa('a[href^="#"]').forEach((link) => link.addEventListener("click", (event) => { const target = link.getAttribute("href"); if (target !== "#feed") { event.preventDefault(); showToast("此页面正在准备中"); } })); renderAll(); function installJsonMediaUpload(){ const editor=document.getElementById("jsonEditor"),apply=document.getElementById("applyJson");if(!editor||!apply||document.getElementById("uploadJsonMedia"))return; const actions=apply.parentElement,button=document.createElement("button"),input=document.createElement("input"),reference=actions.querySelector("#downloadJson,#copyJson,#validateJson,#resetJson"); button.type="button";button.id="uploadJsonMedia";button.className=reference?reference.className:apply.className;button.textContent="上传图片/视频";button.title="先把光标放进要替换的内容对象,再选择图片或视频";input.type="file";input.accept="image/*,video/*";input.hidden=true;actions.insertBefore(button,apply);actions.appendChild(input); const report=(message)=>{const status=document.getElementById("settingsStatus")||document.getElementById("jsonError")||document.getElementById("jsonStatus");if(status)status.textContent=message}; const findTarget=(text,cursor)=>{const stack=[],matches=[];let quoted=false,escaped=false;for(let index=0;indexindex+1)continue;try{const value=JSON.parse(text.slice(start,index+1)),own=(key)=>Object.prototype.hasOwnProperty.call(value,key);if(value&&!Array.isArray(value)&&(own("media")||own("visual")||(own("id")&&(own("text")||own("title"))&&(own("author")||own("channel")||own("user")))))matches.push({start,end:index+1,value})}catch(_error){/* full validator handles malformed JSON */}}}return matches.sort((left,right)=>(left.end-left.start)-(right.end-right.start))[0]||null}; button.addEventListener("click",()=>{input.value="";report("请选择图片或视频;文件会写入光标所在内容对象的 media 字段。");input.click()}); input.addEventListener("change",()=>{const file=input.files&&input.files[0];if(!file)return;const type=file.type.startsWith("image/")?"image":file.type.startsWith("video/")?"video":"";if(!type){report("仅支持图片或视频文件。");return}const target=findTarget(editor.value,editor.selectionStart);if(!target){report("请先把光标放进带 visual/media 的内容对象,或带 id 的帖子/视频对象内。");return}const reader=new FileReader();reader.onerror=()=>report("文件读取失败,请重新选择。");reader.onload=()=>{const previous=target.value.media&&typeof target.value.media==="object"?target.value.media:{},media={type,mediaUrl:String(reader.result)};for(const key of ["alt","label","caption","pageUrl"])if(typeof previous[key]==="string")media[key]=previous[key];if(!media.alt)media.alt=type==="image"?"用户上传的图片":"用户上传的视频";target.value.media=media;const lineStart=editor.value.lastIndexOf("\n",target.start-1)+1,indent=(editor.value.slice(lineStart,target.start).match(/^\s*/)||[""])[0],replacement=JSON.stringify(target.value,null,2).split("\n").map((line,index)=>index?indent+line:line).join("\n");editor.value=editor.value.slice(0,target.start)+replacement+editor.value.slice(target.end);editor.dispatchEvent(new Event("input",{bubbles:true}));const mediaIndex=replacement.indexOf('"media"');editor.focus();editor.setSelectionRange(target.start+Math.max(mediaIndex,0),target.start+Math.max(mediaIndex,0));report(`已把 ${file.name} 写入 JSON,请点击“应用并渲染”。${file.size>2*1024*1024?" 文件较大,建议同时下载 JSON 保存。":""}`)};reader.readAsDataURL(file)}); } installJsonMediaUpload(); function installInlineMediaReplace() { if (document.getElementById("dfInlineMediaInput")) return; const visualHostSelector = ".post-media,.video-visual,.thumbnail-visual,.short-thumb,.watch-visual,.thread-media,.image-grid,.visual,.lead-visual,.mini-visual,.compact-media,.card-art,.story-visual,.art,.video-art,.media-art,.player-stage,.thumbnail,.thumb,.list-thumb,.focus-thumb,.involve-bg,.hero-art,#heroArt,#adminArt"; const getRootData = () => { if (typeof pageData !== "undefined") return pageData; if (typeof data !== "undefined") return data; return null; }; const input = document.createElement("input"); input.id = "dfInlineMediaInput"; input.type = "file"; input.hidden = true; document.body.appendChild(input); const style = document.createElement("style"); style.dataset.dfInlineMediaReplace = "true"; style.textContent = ".df-inline-media-host{position:relative!important}.df-inline-media-button{position:absolute;z-index:40;top:10px;right:10px;width:40px;height:40px;padding:0;border:1px solid rgba(255,255,255,.55);border-radius:50%;display:grid;place-items:center;color:#fff;background:rgba(8,15,28,.78);box-shadow:0 8px 24px rgba(0,0,0,.28);backdrop-filter:blur(8px);cursor:pointer;opacity:0;transform:translateY(-4px);transition:opacity .16s,transform .16s,background .16s}.df-inline-media-button:hover{background:rgba(20,110,240,.92)}.df-inline-media-button svg{width:20px;height:20px;fill:none;stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}.df-inline-media-host:hover>.df-inline-media-button,.df-inline-media-host:focus-within>.df-inline-media-button,.df-inline-media-button:focus-visible{opacity:1;transform:none}@media(hover:none){.df-inline-media-button{opacity:1;transform:none}}.df-inline-media-toast{position:fixed;z-index:2147483647;left:50%;bottom:28px;max-width:min(88vw,520px);padding:10px 14px;border-radius:10px;color:#fff;background:rgba(15,23,42,.94);box-shadow:0 12px 32px rgba(0,0,0,.28);font:600 13px/1.45 system-ui,sans-serif;transform:translateX(-50%)}"; document.head.appendChild(style); const notify = (message) => { if (typeof showToast === "function") { showToast(message); return; } if (typeof toast === "function") { toast(message); return; } let notice = document.getElementById("dfInlineMediaToast"); if (!notice) { notice = document.createElement("div"); notice.id = "dfInlineMediaToast"; notice.className = "df-inline-media-toast"; document.body.appendChild(notice); } notice.textContent = message; window.clearTimeout(notice._timer); notice._timer = window.setTimeout(() => notice.remove(), 2600); }; const sameSource = (value, source, absolute) => { if (typeof value !== "string") return false; if (value === source || value === absolute) return true; try { return new URL(value, location.href).href === absolute; } catch (_error) { return false; } }; const findMatches = (source, absolute) => { const matches = []; const stack = []; const visit = (value) => { if (Array.isArray(value)) { value.forEach(visit); return; } if (!value || typeof value !== "object") return; stack.push(value); Object.entries(value).forEach(([key, item]) => { if ((key === "mediaUrl" || key === "posterUrl") && sameSource(item, source, absolute)) { const ancestors = stack.slice(0, -1).reverse(); const owner = ancestors.find((candidate) => candidate && typeof candidate === "object" && !Array.isArray(candidate) && ("id" in candidate || "title" in candidate || "text" in candidate || "name" in candidate || "author" in candidate)) || ancestors[0] || value; matches.push({ owner, previous: value, key }); } if (item && typeof item === "object") visit(item); }); stack.pop(); }; visit(getRootData()); return matches; }; const visualOwners = () => { const owners = []; const seen = new Set(); const visit = (value) => { if (Array.isArray(value)) { value.forEach(visit); return; } if (!value || typeof value !== "object") return; const hasVisual = Object.prototype.hasOwnProperty.call(value, "visual") || (value.media && typeof value.media === "object"); const hasIdentity = ["id", "title", "text", "leadTitle", "feature", "name", "author"].some((key) => typeof value[key] === "string" && value[key].trim()); if (hasVisual && hasIdentity && !seen.has(value)) { seen.add(value); owners.push(value); } Object.values(value).forEach((item) => { if (item && typeof item === "object") visit(item); }); }; visit(getRootData()); return owners; }; const domIdentifiers = (element) => { const values = new Set(); let cursor = element; for (let depth = 0; cursor && cursor !== document.body && depth < 8; depth += 1, cursor = cursor.parentElement) { Object.values(cursor.dataset || {}).forEach((value) => { if (value) values.add(String(value)); }); } return values; }; const chooseVisualTarget = (element) => { const identifiers = domIdentifiers(element); const context = element.closest("article,button,[data-post-id],[data-video-id],[data-short-id],.post,.video-card,.news-card,.story-card,.lead-story,.hero") || element.parentElement || element; const contextText = String(context.textContent || "").replace(/\s+/g, " ").trim(); const visualNode = element.closest("[data-visual]") || element.querySelector && element.querySelector("[data-visual]"); const visualValue = visualNode && visualNode.dataset ? visualNode.dataset.visual : ""; let best = null; let bestScore = 0; visualOwners().forEach((owner) => { let score = 0; Object.entries(owner).forEach(([key, value]) => { if (/id$/i.test(key) && identifiers.has(String(value))) score += 100; }); ["title", "text", "leadTitle", "feature", "visualLabel", "label", "author", "name", "summary"].forEach((key) => { const value = typeof owner[key] === "string" ? owner[key].trim() : ""; if (value.length >= 2 && contextText.includes(value)) score += Math.min(40, value.length); }); if (visualValue && owner.visual === visualValue) score += 20; if (score > bestScore) { best = owner; bestScore = score; } }); if (!best || bestScore <= 0) return null; return { owner: best, previous: best.media && typeof best.media === "object" ? best.media : { type: "image" }, key: "mediaUrl" }; }; const chooseTarget = (element) => { const source = element.getAttribute("src") || ""; const absolute = element.currentSrc || element.src || source; const matches = findMatches(source, absolute); if (!matches.length) return null; const identifiers = domIdentifiers(element); return matches.find(({ owner }) => Object.entries(owner).some(([key, value]) => /id$/i.test(key) && identifiers.has(String(value)))) || matches[0]; }; const persist = () => { const root = getRootData(); if (typeof saveData === "function") return saveData(); if (typeof writeStorage === "function" && typeof storageKey !== "undefined") return writeStorage(storageKey, JSON.stringify(root)); if (typeof writeStorage === "function" && typeof dataKey !== "undefined") return writeStorage(dataKey, JSON.stringify(root)); return false; }; let active = null; const attachButton = (host, target) => { if (!host || !target || host.querySelector(":scope > .df-inline-media-button")) return; host.classList.add("df-inline-media-host"); const button = document.createElement("button"); button.type = "button"; button.className = "df-inline-media-button"; button.setAttribute("aria-label", "替换图片或视频"); button.title = "上传图片或视频并替换"; button.innerHTML = ''; button.addEventListener("click", (event) => { event.preventDefault(); event.stopPropagation(); active = { ...target }; input.accept = "image/*,video/*"; input.value = ""; input.click(); }); host.appendChild(button); }; const decorateBuiltInVisuals = () => { document.querySelectorAll(visualHostSelector).forEach((host) => { if (host.querySelector("img[src],video[src],iframe[src],:scope > .df-inline-media-button")) return; if (!host.querySelector("svg") && !host.dataset.visual) return; const target = chooseVisualTarget(host); if (!target) return; attachButton(host, target); }); }; const decorate = () => { decorateBuiltInVisuals(); document.querySelectorAll("img[src],video[src]").forEach((media) => { if (media.dataset.dfInlineMediaReady === "true") return; const target = chooseTarget(media); if (!target) return; const host = media.closest(visualHostSelector) || media.parentElement; if (!host) return; media.dataset.dfInlineMediaReady = "true"; attachButton(host, target); }); }; input.addEventListener("change", () => { const file = input.files && input.files[0]; if (!file || !active) return; const uploadedType = file.type.startsWith("image/") ? "image" : file.type.startsWith("video/") ? "video" : ""; if (!uploadedType) { notify("请选择图片或视频文件。"); return; } const reader = new FileReader(); reader.onerror = () => notify("文件读取失败,请重新选择。"); reader.onload = () => { const next = { ...(active.previous || {}) }; const result = String(reader.result); next.type = uploadedType; next.mediaUrl = result; delete next.posterUrl; if (!next.alt) next.alt = uploadedType === "video" ? "用户上传的视频" : "用户上传的图片"; active.owner.media = next; const stored = persist(); renderAll(); notify(stored === false ? "媒体已替换;浏览器未能缓存,请下载 JSON 留存。" : "媒体已替换并写入页面 JSON。"); active = null; }; reader.readAsDataURL(file); }); let queued = false; const queueDecorate = () => { if (queued) return; queued = true; requestAnimationFrame(() => { queued = false; decorate(); }); }; new MutationObserver(queueDecorate).observe(document.body, { childList: true, subtree: true }); decorate(); } installInlineMediaReplace(); })();