您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
巴哈姆特哈拉區新體驗。
// ==UserScript== // @name 巴哈姆特_新版B頁板務功能Re // @namespace Bee10301 // @version 9.5 // @description 巴哈姆特哈拉區新體驗。 // @author Bee10301 // @match https://removed.www.gamer.com.tw/ // @match https://removed.www.gamer.com.tw/index2.php* // @match https://forum.gamer.com.tw/B.php?* // @match https://forum.gamer.com.tw/C.php?* // @match https://forum.gamer.com.tw/2025/?bsn* // @homepage https://home.gamer.com.tw/home.php?owner=bee10301 // @icon https://home.gamer.com.tw/favicon.ico // @connect * // @grant GM_xmlhttpRequest // @license GPL // ==/UserScript== /** * 巴哈姆特插件 - 核心配置與初始化模組 * 負責插件的基本配置、設定管理和初始化流程 */ class BahamutePlugin { constructor() { this.version = "9.0"; this.isNewVersion = false; this.settings = new SettingsManager(); this.init(); } /** * 插件初始化(舊版棄置) */ async init() { try { this.isNewVersion = await this.detectMode(); //this.settings.checkFirstRun(); //await this.addSettingElement(this.isNewVersion); //await this.initializeWorkers(); //this.checkTips(this.isNewVersion); //this.reportAlert(this.isNewVersion); } catch (error) { console.error("[ERROR] Plugin initialization failed:", error); } } /** * 檢測頁面模式(新版/舊版) * @returns {Promise<boolean>} 是否為新版 */ async detectMode() { if (document.querySelector(".forum-nav-main") !== null) { await this.waitForElement(".forum-list-normal"); this.setupNewVersionStyles(); return true; } return false; } /** * 等待特定元素出現 * @param {string} selector - CSS選擇器 */ waitForElement(selector) { return new Promise((resolve) => { const observer = new MutationObserver((mutations) => { if (document.querySelector(selector)) { observer.disconnect(); resolve(); } }); observer.observe(document.body, { childList: true, subtree: true }); }); } /** * 設置新版樣式 */ setupNewVersionStyles() { const styleSheet = document.createElement("style"); document.head.appendChild(styleSheet); const sheet = styleSheet.sheet; // C頁管理工具樣式 sheet.insertRule( ".c-section:has(div.c-section__main.managertools) { position: sticky; bottom: 0; right: 0; z-index: 100; transform: translateX(85%); transition: 0.5s cubic-bezier(0,.67,0,1.05); }", 0 ); sheet.insertRule( ".c-section:has(div.c-section__main.managertools):hover { transform: translateX(0vw); }", 0 ); // C頁避免誤點上一頁 sheet.insertRule(".article-back { display: none !important; }", 0); // 快速回覆樣式 sheet.insertRule( `.c-section:has(div.c-section__main.c-editor.c-quick-reply) { position: sticky; bottom: 50px; right: 0; z-index: 99; transform: translateX(85%); transition: 0.5s cubic-bezier(0,.67,0,1.05); }`, 0 ); sheet.insertRule( ".c-section:has(div.c-section__main.c-editor.c-quick-reply):hover { transform: translateX(0vw); }", 0 ); this.setupPreviewBox(); this.setupTopBarCover(); } /** * 設置預覽框 */ setupPreviewBox() { const previewBox = document.getElementById("article-content-box"); if (!previewBox) return; previewBox.style.width = "0%"; previewBox.style.transition = "0.5s cubic-bezier(0,.67,0,1.05)"; previewBox.style.zIndex = "999"; previewBox.style.transform = this.settings.get("preview_LR") === "true" ? "translateX(100%)" : "translateX(-150%)"; } /** * 設置頂部遮罩 */ setupTopBarCover() { const topBarCover = document.createElement("div"); topBarCover.className = "bee-top-bar-cover"; Object.assign(topBarCover.style, { width: "100vw", height: "0%", backgroundColor: "var(--f1-bg)", position: "absolute", top: "0", left: "0", transition: "0.5s cubic-bezier(0,.67,0,1.05)", zIndex: "990", }); topBarCover.addEventListener("click", (e) => { const previewBox = document.getElementById("article-content-box"); if (previewBox) { previewBox.style.transform = this.settings.get("preview_LR") === "true" ? "translateX(100%)" : "translateX(-150%)"; } // 棄用內建返回按鈕 // if ( // window.getComputedStyle(document.querySelector(".article-back")) // .display !== "none" // ) { // document.querySelector(".article-back").click(); // } // 將網址 snA sn cPage 參數移除 const url = new URL(window.location.href); url.searchParams.delete("snA"); url.searchParams.delete("sn"); url.searchParams.delete("cPage"); window.history.replaceState({}, "", url.toString()); topBarCover.style.height = "0%"; topBarCover.style.opacity = "1"; // remove skip floor btn document.getElementById("floating-skip-button")?.remove(); // remove quick replay document .querySelector(".article-content-box .c-editor.c-quick-reply") ?.remove(); // remove managertools document.querySelector(".article-content-box .managertools")?.remove(); return false; }); const topBar = document.querySelector(".main-nav"); if (topBar) { topBar.parentNode.insertBefore(topBarCover, topBar.nextSibling); document.querySelector(".main-nav__row").style.height = "auto"; } } /** * 初始化工作模組 */ async initializeWorkers() { const bPageWorker = new BPageWorker(this.isNewVersion, this.settings); const cPageWorker = new CPageWorker(this.settings); await bPageWorker.init(); await cPageWorker.init(); } /** * 檢查提示 */ checkTips(newVer) { const tipsManager = new TipsManager(this.settings); tipsManager.checkTips(newVer); } /** * 檢查檢舉提醒 */ reportAlert(newVer) { const reportManager = new ReportManager(this.settings); reportManager.checkAlert(newVer); } } /** * 設定管理器 */ class SettingsManager { constructor() { this.defaultSettings = this.getDefaultSettings(); } /** * 獲取預設設定 */ getDefaultSettings() { return { isFirstRun: "false", add_function: "true", preview_auto: "true", preview_wait_load: "false", preview_size: "80%", new_design: "true", new_design_box: "80%", new_design_box_Left: "65%", new_design_box_Right: "18%", new_design_LRSwitch: "false", bee_select_color: "#000000b3", addBorderInPicMode: "true", showTips: "true", preview_LR: "true", showAbuse: "true", addSummaryBtn: "true", oaiBaseUrl: "https://api.openai.com/v1/chat/completions", oaiKey: "sk-yourKey", oaiModel: "gpt-3.5-turbo", oaiPrompt: `## workflow 1. 總結:精確的讀懂和理解文章,然後用一句話脈絡清晰的語句,總結出[文章的主旨]。 2. 提煉重點:根據文章的邏輯和結構,清楚列出文章的主要論點,並按照下方範例的格式輸出。 總結: - 要點1: - 要點2: ...(依情況增加或減少要點) ## MUST/IMPORTANT/RULES - 不能添加其他個人觀點或註釋。 - 使用繁體中文`, oaiPromptCmd: "以下是一段群組聊天的對話,總結對話中的話題,用條列式列出使用者的想法。\n## workflow \n 1. 整理話題:理解各個使用者討論的話題並以話題為單位整理出整串對話的話題 \n 2. 將相同話題中,對同一件事有相似想法的對話整理在一起(例如 `@user1/@user2:認為太貴了`) ,不同看法則單獨列出。\n 3. 輸出:把冗餘贅字優化,但保留具體描述。(劣例:`@user1/@user2:提及角色在世界觀中的地位和特徵` 在這個例子中沒有具體描述提及了什麼樣的地位或特徵)。使用者以 @id 標記並且不再添加其他md語法。 \n ## MUST/IMPORTANT/RULES \n- 不能添加其他個人觀點或註釋。\n- 使用繁體中文\n", oaiPromptChat: "根據文章內容,使用繁體中文流暢語言,簡潔的回答使用者的問題。", custom_oaiPrompt: "", custom_oaiPromptCmd: "", custom_oaiPromptChat: "", oaiPromptSystemMode: "true", oaiPromptDate: "20241101", oaiPromptUpdateDate: "20241101", oaiPromptUpdateURL: "https://gamercomtwnew.bee.moe/gamer.prompts.json", oaiPromptUpdateSleep: "1", cleanMode: "false", cleanModeSize: "4rem", homeStyleSwitch: "true", homeTips: "true", }; } /** * 檢查首次運行 */ checkFirstRun(reset = false) { console.log("[INFO] Init data"); Object.entries(this.defaultSettings).forEach(([key, defaultValue]) => { if (this.get(key) === "" || this.get(key) === null || reset === true) { this.set(key, defaultValue); } }); // 特殊處理:更新舊版URL if ( this.get("oaiPromptUpdateURL") === "https://gamercomtwnew.bee.moe/gamer.prompts.js" ) { this.set( "oaiPromptUpdateURL", "https://gamercomtwnew.bee.moe/gamer.prompts.json" ); } } /** * 獲取設定值 */ get(key) { return localStorage.getItem(key); } /** * 設置設定值 */ set(key, value) { localStorage.setItem(key, value); } /** * 獲取布林值設定 */ getBool(key) { return this.get(key) === "true"; } } /** * B頁面工作模組 * 負責B頁面的所有功能,包括即時預覽、版面設計、功能按鈕等 */ class BPageWorker { constructor(isNewVersion, settings) { this.isNewVersion = isNewVersion; this.settings = settings; this.previewFrame = null; } /** * 初始化B頁面功能 */ async init() { if (!this.isBPage()) return; await this.initializePreview(); this.initializeLayout(); this.initializeInteractions(); this.initializeMenu(); this.initializeDesign(); this.initializeFunctions(); this.initializeStyles(); } /** * 檢查是否為B頁面 */ isBPage() { return ( document.querySelector(".forum-list-normal") || document.querySelector(".b-list__head") ); } /** * 初始化預覽功能 */ async initializePreview() { await this.setupPreviewAuto(); if (!this.isNewVersion && this.settings.getBool("preview_auto")) { await this.createPreviewFrame(); } } /** * 設置自動預覽 */ async setupPreviewAuto() { if (this.isNewVersion) { await this.setupNewVersionPreview(); } else { this.setupOldVersionPreview(); } } /** * 設置新版預覽 */ async setupNewVersionPreview() { const articleListContainer = document.querySelector("#list"); if (!articleListContainer) { console.warn("⚠️ 文章列表容器未找到!"); return; } articleListContainer.addEventListener("click", async (e) => { if (e.target.closest(".forum-item-info")) { e.preventDefault(); await this.showNewVersionPreview(); return false; } }); // 如果網址帶有 snA 參數 if (window.location.href.includes("snA=")) { await this.showNewVersionPreview(); } } /** * 顯示新版預覽 */ async showNewVersionPreview() { const previewBox = document.getElementById("article-content-box"); const topBarCover = document.querySelector(".bee-top-bar-cover"); previewBox.style.transform = "translateX(0%)"; previewBox.style.width = this.settings.get("preview_size") || "80%"; topBarCover.style.height = "100vh"; topBarCover.style.opacity = "0.7"; // 等待C頁面載入並添加AI功能 await this.waitForCPageLoad(); } /** * 等待C頁面載入 */ async waitForCPageLoad() { return new Promise((resolve) => { const observer = new MutationObserver(async (mutations) => { if (document.querySelectorAll(".c-section__main.c-post").length > 0) { observer.disconnect(); // 移動快速回覆、板務工具 let managertools = document.querySelector(".managertools"); if (managertools) { managertools = document .querySelector(".article-content-box") .appendChild(managertools.parentElement); } const quickReply = document.querySelector(".c-editor.c-quick-reply"); if (quickReply) { document .querySelector(".article-content-box") .appendChild(quickReply.parentElement); } // 添加額外功能 if (this.settings.getBool("addSummaryBtn")) { const cPageWorker = new CPageWorker(this.settings); await cPageWorker.addPostButtons(this.isNewVersion); cPageWorker.addSkipFloorButton(); } resolve(); } }); observer.observe(document.body, { childList: true, subtree: true }); }); } /** * 設置舊版預覽 */ setupOldVersionPreview() { this.handlePicModeAdjustment(); this.setupOldVersionClickHandlers(); } /** * 處理圖片模式調整 */ handlePicModeAdjustment() { const picMode = document.querySelectorAll(".imglist-text").length !== 0; if (picMode) { const switchTopics = document.querySelectorAll(".b-list__main"); switchTopics.forEach((topic) => { const topicTexts = topic.childNodes[1].childNodes[3]; if (topicTexts && topicTexts.className === "imglist-text") { topic.childNodes[1].removeChild(topicTexts); topic.insertAdjacentHTML("beforeend", topicTexts.outerHTML); } }); } } /** * 設置舊版點擊處理器 */ setupOldVersionClickHandlers() { const picMode = document.querySelectorAll(".imglist-text").length !== 0; // 標題點擊 const titleLinks = document.querySelectorAll(".b-list__main__title"); titleLinks.forEach((link) => { link.addEventListener("click", (e) => { e.preventDefault(); const href = link.getAttribute("href"); this.openInFrame(`https://forum.gamer.com.tw/${href}`); return false; }); }); // 頁面點擊 const pageSelector = picMode ? "#BH-master > form > div > table > tbody > tr > td.b-list__main > div > div > span > span" : "#BH-master > form > div > table > tbody > tr > td.b-list__main > span > a"; const pageLinks = document.querySelectorAll(pageSelector); pageLinks.forEach((link) => { link.addEventListener("click", (e) => { e.preventDefault(); const href = link.getAttribute(picMode ? "data-page" : "href"); this.openInFrame(`https://forum.gamer.com.tw/${href}`); return false; }); }); } /** * 創建預覽框架 */ async createPreviewFrame() { const previewContainer = this.createPreviewContainer(); const iframe = this.createIframe(); previewContainer.appendChild(iframe); document.body.appendChild(previewContainer); this.previewFrame = iframe; await this.initializePreviewContainer(previewContainer); this.setupPreviewCloseHandler(); } /** * 創建預覽容器 */ createPreviewContainer() { const container = document.createElement("div"); container.className = "bee_preview_wd"; Object.assign(container.style, { height: "100%", width: this.settings.get("preview_size"), transform: "scaleX(0)", zIndex: "100", position: "fixed", top: "0px", [this.settings.getBool("preview_LR") ? "right" : "left"]: "1%", }); return container; } /** * 創建iframe */ createIframe() { const iframe = document.createElement("iframe"); iframe.id = "bee_frame"; iframe.title = "bee_frame"; iframe.src = ""; Object.assign(iframe.style, { border: "0em solid rgb(170, 50, 220, 0)", width: "100%", height: "100%", }); return iframe; } /** * 初始化預覽容器 */ async initializePreviewContainer(container) { const animationDirection = this.settings.getBool("preview_LR") ? "rl" : "lr"; await this.popElementInit(container, false, animationDirection, false); } /** * 設置預覽關閉處理器 */ setupPreviewCloseHandler() { const menuPath = document.querySelector("#BH-menu-path"); if (!menuPath) return; Object.assign(menuPath.style, { transition: "all 0.5s cubic-bezier(0.21, 0.3, 0.18, 1.37) 0s", height: "40px", opacity: "1", }); menuPath.addEventListener("click", () => { const container = document.querySelector(".bee_preview_wd"); const direction = this.settings.getBool("preview_LR") ? "rl" : "lr"; this.popElement(container, "false", direction); menuPath.style.height = "40px"; menuPath.style.opacity = "1"; }); } /** * 在框架中打開URL */ openInFrame(url) { if (!this.previewFrame) return; this.previewFrame.src = url; const menuPath = document.querySelector("#BH-menu-path"); if (menuPath) { menuPath.style.height = "100%"; menuPath.style.opacity = "0.6"; } setTimeout(() => { const container = document.querySelector(".bee_preview_wd"); const direction = this.settings.getBool("preview_LR") ? "rl" : "lr"; this.popElement(container, "true", direction); }, 1000); // 設置iframe內的樣式 setTimeout(() => { this.setupIframeStyles(); }, 1000); } /** * 設置iframe內的樣式 */ setupIframeStyles() { try { const iframeDoc = this.previewFrame.contentDocument || this.previewFrame.contentWindow.document; const styleSheet = iframeDoc.createElement("style"); iframeDoc.head.appendChild(styleSheet); const sheet = styleSheet.sheet; sheet.insertRule( ".managertools { position: fixed; bottom: 0; right: 0; z-index: 100; }", 0 ); } catch (error) { console.warn("無法設置iframe樣式:", error); } } /** * 初始化版面佈局 */ initializeLayout() { if (this.isNewVersion) { this.setupNewVersionLayout(); } else { this.setupOldVersionLayout(); } } /** * 設置新版版面 */ setupNewVersionLayout() { if (this.settings.getBool("cleanMode")) { this.enableCleanMode(); } this.setupSlaveDisplay(); this.setupOrderSwitching(); } /** * 啟用清爽模式 */ enableCleanMode() { if (document.getElementById("cleanModeStyles")) { return; } const style = document.createElement("style"); style.id = "cleanModeStyles"; style.textContent = ` /* 移除描述和縮圖 */ .forum-item-desc, .forum-item-thumbnail { display: none !important; } /* 調整列表項目樣式 */ .forum-list-item { padding: 0 !important; min-height: ${this.settings.get("cleanModeSize")} !important; } `; document.head.appendChild(style); } /** * 設置從屬顯示 */ setupSlaveDisplay() { if (document.getElementById("showSlave")) { return; } const style = document.createElement("style"); style.id = "showSlave"; style.textContent = ` #BH-slave { display: block !important; } `; document.head.appendChild(style); } /** * 設置順序切換 */ setupOrderSwitching() { if (document.getElementById("orderSwitching")) { return; } const style = document.createElement("style"); style.id = "orderSwitching"; style.textContent = ` #BH-master { order: 2 !important; } #BH-slave { order: 1 !important; margin-left: 0 !important; margin-right: 12px !important; } `; document.head.appendChild(style); } /** * 設置舊版版面 */ setupOldVersionLayout() { if (this.settings.getBool("new_design_LRSwitch")) { const master = document.getElementById("BH-master"); const slave = document.getElementById("BH-slave"); if (master && slave) { master.style.float = "right"; slave.style.float = "left"; } } } /** * 初始化交互功能 */ initializeInteractions() { this.setupCheckboxSystem(); this.setupBorderInPicMode(); } /** * 設置複選框系統 */ setupCheckboxSystem() { const titleElements = this.getTitleElements(); const checkboxes = this.getCheckboxElements(); titleElements.forEach((title, index) => { this.setupTitleClickHandler(title, checkboxes, index); }); } /** * 獲取標題元素 */ getTitleElements() { const hasImgList = document.querySelectorAll(".imglist-text").length > 0; return hasImgList ? document.getElementsByClassName("b-list__main") : document.getElementsByClassName("b-list__main"); } /** * 獲取複選框元素 */ getCheckboxElements() { try { return document.getElementsByName("jsn[]"); } catch (error) { return []; } } /** * 設置標題點擊處理器 */ setupTitleClickHandler(title, checkboxes, index) { // 防止子元素觸發 const children = title.querySelectorAll("*"); children.forEach((child) => { child.addEventListener("click", (event) => { event.stopPropagation(); }); }); // 重置複選框 if (checkboxes[index]) { checkboxes[index].checked = false; } title.onclick = (e) => { this.handleTitleClick(title, checkboxes, e); }; } /** * 處理標題點擊 */ handleTitleClick(title, checkboxes, event) { const hasValidContent = title.querySelector(".b-list__main__title") || title.querySelector(".imglist-text"); if (!hasValidContent) return; // 隱藏管理器 const manager = document.querySelector(".bee_manager"); if (manager) { manager.style.display = "none"; } // 處理複選框邏輯 const snA = this.extractSnA(title.innerHTML); if (!snA) return; let hasCheckedBox = false; checkboxes.forEach((checkbox) => { if (checkbox.value === snA) { checkbox.checked = !checkbox.checked; title.style.backgroundColor = checkbox.checked ? this.settings.get("bee_select_color") : ""; if (checkbox.checked) { hasCheckedBox = true; } } }); // 顯示管理器 if (hasCheckedBox && manager) { this.showManagerAtCursor(manager, event); } } /** * 提取snA值 */ extractSnA(innerHTML) { const match = innerHTML.match(/snA=(\d*)/); return match ? match[1] : null; } /** * 在游標位置顯示管理器 */ showManagerAtCursor(manager, event) { manager.style.left = `${event.clientX + 50}px`; manager.style.top = `${event.clientY - 170}px`; manager.style.display = "block"; } /** * 設置圖片模式邊框 */ setupBorderInPicMode() { if (!this.settings.getBool("addBorderInPicMode")) return; const picModeBlocks = document.querySelectorAll( "#BH-master > form > div > table > tbody > tr > td.b-list__main > div > p" ); picModeBlocks.forEach((block) => { block.style.borderTop = "dashed"; }); } /** * 初始化選單 */ initializeMenu() { if (this.isNewVersion) return; this.createManagerMenu(); this.setupLinkClickPrevention(); } /** * 創建管理器選單 */ createManagerMenu() { try { const managertools = document.querySelector(".managertools"); if (!managertools) return; const bManagerDiv = this.createManagerContainer(); const buttonGroups = this.createButtonGroups(managertools); bManagerDiv.appendChild(this.createCheckboxContainer()); buttonGroups.forEach((group) => bManagerDiv.appendChild(group)); managertools.parentElement.appendChild(bManagerDiv); } catch (error) { console.warn("創建管理器選單失敗:", error); } } /** * 創建管理器容器 */ createManagerContainer() { const container = document.createElement("div"); container.className = "b-manager managertools bee_manager"; Object.assign(container.style, { zIndex: "100", position: "fixed", width: "auto", }); return container; } /** * 創建複選框容器 */ createCheckboxContainer() { const checkboxDiv = document.createElement("div"); checkboxDiv.className = "checkbox"; const label = document.createElement("label"); label.setAttribute("for", "check"); checkboxDiv.appendChild(label); return checkboxDiv; } /** * 創建按鈕組 */ createButtonGroups(managertools) { const buttonIndexes = [ [0, 3, 7], [2, 4], [1, 8], [5, 6], ]; return buttonIndexes.map((indexes) => { const beeDiv = document.createElement("div"); beeDiv.className = "bee"; beeDiv.style.padding = "5px"; indexes.forEach((index) => { const buttons = managertools.querySelectorAll("button"); if (buttons[index]) { const button = buttons[index].cloneNode(true); beeDiv.appendChild(button); } }); return beeDiv; }); } /** * 設置連結點擊防止 */ setupLinkClickPrevention() { const selectors = [ "#BH-master > form > div > table > tbody > tr > td.b-list__main > a", "#BH-master > form > div > table > tbody > tr > td.b-list__main", ]; selectors.forEach((selector) => { const elements = document.querySelectorAll(selector); elements.forEach((element) => { element.addEventListener("click", (event) => { event.stopPropagation(); }); }); }); } /** * 初始化設計 */ initializeDesign() { if (!this.settings.getBool("new_design")) return; this.applyNewDesign(); } /** * 應用新設計 */ applyNewDesign() { if (document.getElementById("newDesignStyles")) { return; } const style = document.createElement("style"); style.id = "newDesignStyles"; style.textContent = ` #BH-slave { width: ${this.settings.get("new_design_box_Right")} !important; max-width: 100vw !important; } #BH-master { width: ${this.settings.get("new_design_box_Left")} !important; } `; document.head.appendChild(style); if (this.isNewVersion) { this.applyNewVersionDesign(); } else { this.applyOldVersionDesign(); } } /** * 應用新版設計 */ applyNewVersionDesign() { if (document.getElementById("newVersionStyles")) { return; } const style = document.createElement("style"); style.id = "newVersionStyles"; const slaveStyleCss = this.settings.getBool("new_design_LRSwitch") ? "left: 0% !important;" : "right: 0% !important;"; const adPositionCss = this.settings.getBool("new_design_LRSwitch") ? "right: 0;" : "left: 0;"; const showFixedRightAtLeft = this.settings.getBool("new_design_LRSwitch") ? "" : "left: 300px !important; right: unset !important;"; style.textContent = ` #forum-list-box { left: 50% !important; transform: translateX(-50%) !important; } .forum-box.split .forum-list-box { width: ${this.settings.get("new_design_box")} !important; } #BH-slave { position: fixed !important; bottom: 0% !important; z-index: 100 !important; ${slaveStyleCss} } #buildingAdB { position: absolute !important; bottom: 0 !important; ${adPositionCss} } .fixed-right { ${showFixedRightAtLeft} } `; document.head.appendChild(style); const changeAdPosition = document.getElementById("buildingAdB"); const mainBox = document.querySelector(".forum-main"); mainBox.prepend(changeAdPosition); } /** * 應用舊版設計 */ applyOldVersionDesign() { const wrapper = document.getElementById("BH-wrapper"); if (wrapper) { wrapper.style.width = this.settings.get("new_design_box"); } } /** * 初始化功能 */ initializeFunctions() { if (!this.settings.getBool("add_function") || this.isNewVersion) return; this.addFunctionButtons(); } /** * 添加功能按鈕 */ addFunctionButtons() { const titleElements = document.getElementsByClassName("b-list__main"); const titleLinks = document.getElementsByClassName("b-list__main__title"); // 創建新的td元素 const newTd = document.createElement("td"); const filterElement = document.querySelector(".b-list__filter"); if (filterElement) { filterElement.insertAdjacentElement("afterend", newTd); } // 為每個標題添加功能按鈕 Array.from(titleElements).forEach((title, index) => { if (titleLinks[index]) { const buttonContainer = this.createFunctionButtonContainer( titleLinks[index] ); title.insertAdjacentElement("afterend", buttonContainer); } }); this.setupFunctionButtonHover(); } /** * 創建功能按鈕容器 */ createFunctionButtonContainer(titleLink) { const td = document.createElement("td"); td.style.width = "auto"; const hrefValue = titleLink.getAttribute("href"); const isDarkTheme = this.checkDarkTheme(); const buttons = this.createFunctionButtons(hrefValue, isDarkTheme); buttons.forEach((button) => td.appendChild(button)); return td; } /** * 檢查是否為暗色主題 */ checkDarkTheme() { const menuPath = document.getElementById("BH-menu-path"); if (!menuPath) return false; const bgColor = window.getComputedStyle(menuPath).backgroundColor; return bgColor === "rgb(28, 28, 28)"; } /** * 創建功能按鈕 */ createFunctionButtons(hrefValue, isDarkTheme) { const buttonConfigs = [ { title: "快速瀏覽", class: "bee_preview", icon: "fullscreen", onclick: () => this.openInFrame("https://forum.gamer.com.tw/" + hrefValue), }, { title: "開新視窗", class: "bee_open_new_wd", icon: "open_in_new", onclick: () => window.open(hrefValue), }, { title: "複製連結", class: "bee_link", icon: "link", onclick: () => navigator.clipboard.writeText( "https://forum.gamer.com.tw/" + hrefValue ), }, ]; return buttonConfigs.map((config) => { const button = document.createElement("a"); button.title = config.title; button.className = `btn-icon btn-icon--inverse ${config.class}`; button.style.display = "none"; button.onclick = config.onclick; const icon = document.createElement("i"); icon.className = `material-icons ${config.class}`; icon.textContent = config.icon; if (!isDarkTheme) { icon.style.color = "rgba(0, 0, 0, 0.4)"; } button.appendChild(icon); return button; }); } /** * 設置功能按鈕懸停效果 */ setupFunctionButtonHover() { const rows = document.querySelectorAll(".b-list__row"); rows.forEach((row) => { row.addEventListener("mouseover", () => { this.toggleFunctionButtons(row, true); }); row.addEventListener("mouseout", () => { this.toggleFunctionButtons(row, false); }); }); } /** * 切換功能按鈕顯示 */ toggleFunctionButtons(row, show) { const buttonClasses = [".bee_preview", ".bee_open_new_wd", ".bee_link"]; buttonClasses.forEach((className) => { const button = row.querySelector(className); if (button) { button.style.display = show ? "" : "none"; } }); } /** * 初始化樣式 */ initializeStyles() { // 樣式相關的初始化可以在這裡添加 } /** * 彈出元素初始化 */ async popElementInit(element, show = true, anime = "ud", waitAppend = true) { if (waitAppend) { requestAnimationFrame(() => { this.setElementDimensions(element); }); } else { this.setElementDimensions(element); } this.setupElementTransition(element, show, anime); } /** * 設置元素尺寸 */ setElementDimensions(element) { element.style.readHeight = element.scrollHeight === 0 ? "999px" : `${element.scrollHeight}px`; element.style.readWidth = element.scrollWidth === 0 ? "999px" : `${element.scrollWidth}px`; } /** * 設置元素過渡 */ setupElementTransition(element, show, anime) { Object.assign(element.style, { transition: "", overflow: "hidden auto", opacity: "0", }); this.popElement(element, "false", anime); element.style.transition = "all 0.5s cubic-bezier(0.21, 0.3, 0.18, 1.37) 0s"; if (!show) { element.style.beeShow = "false"; return; } element.style.beeShow = "true"; element.style.opacity = "1"; requestAnimationFrame(() => { element.style.maxHeight = element.style.readHeight; }); } /** * 彈出元素控制 */ popElement(element, show = "true", anime = "ud") { const doShow = show === "toggle" ? !(element.style.beeShow === "true") : show === "true"; if (doShow) { this.showElement(element); } else { this.hideElement(element, anime); } } /** * 顯示元素 */ showElement(element) { Object.assign(element.style, { opacity: "1", maxHeight: element.style.readHeight, maxWidth: element.style.readWidth, transform: "translateX(0px) translateY(0px)", beeShow: "true", }); } /** * 隱藏元素 */ hideElement(element, anime) { element.style.beeShow = "false"; element.style.opacity = "0"; if (anime.includes("u")) { element.style.maxHeight = "0px"; if (anime.startsWith("d")) { element.style.transform = `translateX(0px) translateY(${element.style.readWidth}px)`; } } if (anime.includes("l")) { element.style.maxWidth = "0px"; if (anime.startsWith("r")) { element.style.transform = `translateX(${element.style.readHeight}) translateY(0px)`; } } } } /** * C頁面工作模組 * 負責C頁面的AI功能,包括懶人包、留言統整、問問功能等 */ class CPageWorker { constructor(settings) { this.settings = settings; this.gptRequestQueue = []; this.isProcessingQueue = false; } /** * 初始化C頁面功能 */ async init() { if (!this.isCPage()) return; this.setupCPageStyles(); if (this.settings.getBool("addSummaryBtn")) { await this.addPostButtons(); } } /** * 檢查是否為C頁面 */ isCPage() { return window.location.href.includes("forum.gamer.com.tw/C.php"); } /** * 設置C頁面樣式 */ setupCPageStyles() { const styleSheet = document.createElement("style"); document.head.appendChild(styleSheet); const sheet = styleSheet.sheet; sheet.insertRule( ".managertools { position: fixed; bottom: 0; right: 0; z-index: 100; }", 0 ); } /** * 添加文章按鈕 */ async addPostButtons(isNewVersion = false) { await this.updatePrompts(); const postSections = this.getPostSections(); postSections.forEach((postSection) => { if (this.hasEditPermission(postSection)) return; this.addAskButton(postSection); const summaryButton = this.addSummaryButton(postSection); this.addCommentSummaryButton(postSection); // 可選功能(目前註解) // this.addSkipFloorButton(postSections, postSection); // this.addSummaryButtonLeft(postSection, summaryButton); }); } /** * 獲取文章區塊 */ getPostSections() { return Array.from(document.querySelectorAll(".c-section")).filter( (section) => section.querySelector(".c-post__body") ); } /** * 檢查是否已經新增按鈕 */ hasEditPermission(postSection) { const editIcon = postSection.querySelectorAll(".lazyBtn"); return editIcon !== null && editIcon.length > 0; } /** * 添加懶人包按鈕 */ addSummaryButton(postSection) { const postBody = postSection.querySelector(".c-post__body"); const footerRight = postBody.querySelector(".article-footer_right"); const summaryButton = this.createButton({ text: "懶人包", icon: "description", id: `lazy-summary-${postBody.querySelector(".c-article").id}`, insertPosition: "first", }); footerRight.insertBefore(summaryButton, footerRight.firstChild); summaryButton.addEventListener("click", async () => { await this.handleSummaryClick(summaryButton, postBody); }); return summaryButton; } /** * 添加跳過樓層 - 按需檢測版本 */ addSkipFloorButton() { //檢查按鈕是否已經存在 if (document.getElementById("floating-skip-button")) { // remove document.getElementById("floating-skip-button").remove(); } // 創建右側浮動的跳過按鈕 const floatingSkipButton = this.createFloatingSkipButton(); document.body.appendChild(floatingSkipButton); // 點擊事件處理 - 只在點擊時檢查當前樓層 floatingSkipButton.addEventListener("click", () => { const cPageWorker = new CPageWorker(this.settings); const postSections = cPageWorker.getPostSections(); const currentSection = this.findCurrentVisibleSection(postSections); if (currentSection) { const currentIndex = Array.from(postSections).indexOf(currentSection); const nextSection = postSections[currentIndex + 1]; if (nextSection) { // 暫時禁止點擊 floatingSkipButton.disabled = true; this.scrollToElement(nextSection, 2); // 700毫秒後恢復 setTimeout(() => { floatingSkipButton.disabled = false; }, 700); this.animateButtonClick(floatingSkipButton); } else { // 已到最後一樓 this.handleLastFloor(currentSection, floatingSkipButton); } } }); } /** * 創建浮動跳過按鈕 */ createFloatingSkipButton() { const button = document.createElement("button"); button.id = "floating-skip-button"; button.innerHTML = ` <i class="fa fa-arrow-down"></i> <span>跳過此樓</span> `; // 添加CSS動畫到頁面 if (!document.getElementById("arrow-bounce-style")) { const style = document.createElement("style"); style.id = "arrow-bounce-style"; style.textContent = ` @keyframes arrowBounce { 0%, 100% { transform: translateY(0); } 25% { transform: translateY(-3px); } 50% { transform: translateY(2px); } 75% { transform: translateY(-1px); } } `; document.head.appendChild(style); } // 設置按鈕樣式 Object.assign(button.style, { position: "fixed", right: "20px", bottom: "300px", transform: "translateY(-50%)", zIndex: "9999", backgroundColor: "var(--primary)", color: "var(--primary-text)", border: "none", borderRadius: "50px", padding: "12px 16px", cursor: "pointer", boxShadow: "0 4px 12px rgba(0,0,0,0.3)", display: "flex", alignItems: "center", gap: "8px", fontSize: "14px", fontWeight: "500", transition: "all 0.3s ease", opacity: "0.8", }); // 懸停效果 - 帶箭頭跳動動畫 button.addEventListener("mouseenter", () => { // 為箭頭添加跳動動畫 const arrow = button.querySelector("i"); if (arrow) { arrow.style.animation = "arrowBounce 0.6s ease-in-out"; } }); button.addEventListener("mouseleave", () => { // 移除箭頭動畫 const arrow = button.querySelector("i"); if (arrow) { arrow.style.animation = ""; } }); return button; } /** * 找出當前在視野中的樓層 */ findCurrentVisibleSection(postSections) { // 使用你現有的 isElementInViewport 函式 for (let i = 0; i < postSections.length; i++) { const section = postSections[i]; if (UtilityFunctions.isInViewport(section)) { return section; } } // 如果沒有找到完全可見的,找最接近視窗頂部的 return this.findClosestSection(postSections); } /** * 找出最接近視窗頂部的樓層(備用方案) */ findClosestSection(postSections) { let closestSection = null; let minDistance = Infinity; postSections.forEach((section) => { const rect = section.getBoundingClientRect(); const distance = Math.abs(rect.top); if (distance < minDistance) { minDistance = distance; closestSection = section; } }); return closestSection; } /** * 按鈕點擊動畫效果 */ animateButtonClick(button) { button.style.transform = "translateY(-50%) scale(0.95)"; setTimeout(() => { button.style.transform = "translateY(-50%) scale(1)"; }, 150); } /** * 處理到達最後一樓的情況 */ handleLastFloor(lastSection, button) { const footer = lastSection.querySelector(".c-post__footer"); if (footer) { // 滾動到 footer 元素 this.scrollToElement(footer); this.animateButtonClick(button); // 更新按鈕狀態 button.style.opacity = "0.6"; button.innerHTML = ` <i class="fa fa-anchor"></i> <span>已到樓底</span> `; // 3秒後恢復按鈕狀態 setTimeout(() => { button.style.opacity = "0.8"; button.innerHTML = ` <i class="fa fa-arrow-down"></i> <span>跳過此樓</span> `; }, 3000); } else { // 如果找不到 footer,就滾動到樓層底部 this.handleLastFloorFallback(lastSection, button); } } /** * 處理找不到 footer 的備用方案 */ handleLastFloorFallback(lastSection, button) { // 滾動到樓層元素的底部 const elementBottom = lastSection.offsetTop + lastSection.offsetHeight; window.scrollTo({ top: elementBottom - window.innerHeight + 100, // 留一點邊距 behavior: "smooth", }); this.animateButtonClick(button); // 更新按鈕狀態 button.style.opacity = "0.5"; button.innerHTML = ` <i class="fa fa-check"></i> <span>已到底部</span> `; // 3秒後恢復按鈕狀態 setTimeout(() => { button.style.opacity = "0.8"; button.innerHTML = ` <i class="fa fa-arrow-down"></i> <span>跳過此樓</span> `; }, 3000); } /** * 處理懶人包按鈕點擊 */ async handleSummaryClick(button, postBody) { this.scrollToElement(button); if (button.querySelector("p").textContent === "產生中...") return; if (!this.validateApiKey()) return; const articleId = postBody.querySelector(".c-article").id; const cleanId = `${articleId}-clean`; // 處理展開/摺疊邏輯 if (this.handleToggleLogic(button, cleanId)) return; // 生成懶人包 await this.generateSummary(button, postBody, articleId); } /** * 生成懶人包 */ async generateSummary(button, postBody, articleId) { button.querySelector("p").textContent = "產生中..."; const articleContent = this.extractArticleContent(postBody); const customPrompt = this.settings.get("custom_oaiPrompt"); const prompt = customPrompt || this.settings.get("oaiPrompt"); const { response, data } = await this.sendGptRequest( prompt, `文章內容:\`\`\`${articleContent}\`\`\`` ); if (!response) { button.querySelector("p").textContent = "懶人包"; return; } const summaryArticle = this.createSummaryArticle( `${articleId}-clean`, data.choices[0].message.content ); button.querySelector("p").textContent = "摺疊 ▲"; postBody.appendChild(summaryArticle); await this.animateElement(summaryArticle, true); } /** * 添加留言統整按鈕 */ addCommentSummaryButton(postSection) { const postBody = postSection.querySelector(".c-post__body"); const replyHead = postSection.querySelector(".c-reply__head"); if (!replyHead) return; const commentButton = this.createButton({ text: "留言統整", icon: "forum", id: `lazy-summaryCmd-${postBody.querySelector(".c-article").id}`, className: "article-footer_right-btn", style: { margin: "0.3rem 0.5rem 0rem 0rem" }, }); replyHead.appendChild(commentButton); commentButton.addEventListener("click", async () => { await this.handleCommentSummaryClick( commentButton, postBody, postSection ); }); } /** * 處理留言統整按鈕點擊 */ async handleCommentSummaryClick(button, postBody, postSection) { this.scrollToElement(button); if (button.querySelector("p").textContent === "產生中...") return; if (!this.validateApiKey()) return; const postId = postBody.querySelector(".c-article").id.replace("cf", ""); const cleanCmdId = `${postId}-cleanCmd`; // 處理展開/摺疊邏輯 if (this.handleToggleLogic(button, cleanCmdId, "留言統整")) return; // 生成留言統整 await this.generateCommentSummary(button, postId, postSection); } /** * 生成留言統整 */ async generateCommentSummary(button, postId, postSection) { button.querySelector("p").textContent = "產生中..."; const commentData = await this.getCommentData(postId); const customPrompt = this.settings.get("custom_oaiPromptCmd"); const prompt = customPrompt || this.settings.get("oaiPromptCmd"); const { response, data } = await this.sendGptRequest( prompt, `對話內容:\n \`\`\`${commentData.textContent}\n\`\`\`` ); if (!response) { button.querySelector("p").textContent = "留言統整"; return; } const processedContent = this.restoreOriginalFormat( data.choices[0].message.content, commentData.textContentOrigin ); const summaryArticle = this.createCommentSummaryArticle( `${postId}-cleanCmd`, processedContent ); button.querySelector("p").textContent = "摺疊 ▲"; const insertBefore = document.getElementById(`Commendlist_${postId}`); postSection .querySelector(".c-post__footer") .insertBefore(summaryArticle, insertBefore); await this.animateElement(summaryArticle, true); } /** * 添加問問按鈕 */ addAskButton(postSection) { const postBody = postSection.querySelector(".c-post__body"); const footerRight = postBody.querySelector(".article-footer_right"); // 創建對話輸入區 const { askInput, chatArea, askTextarea } = this.createChatInterface(postBody); // 創建問問按鈕 const askButton = this.createButton({ text: "問問 ▼", icon: "chat", id: `ask-${postBody.querySelector(".c-article").id}`, insertPosition: "first", }); footerRight.insertBefore(askButton, footerRight.firstChild); // 設置事件監聽器 this.setupAskButtonEvents( askButton, askInput, chatArea, askTextarea, postBody ); } /** * 創建聊天介面 */ createChatInterface(postBody) { // 創建輸入區 const askInput = document.createElement("div"); askInput.classList.add("c-reply__editor"); const replyInput = document.createElement("div"); replyInput.classList.add("reply-input"); const askTextarea = document.createElement("textarea"); askTextarea.classList.add("content-edit"); askTextarea.placeholder = "詢問⋯"; replyInput.appendChild(askTextarea); askInput.appendChild(replyInput); postBody.appendChild(askInput); this.animateElement(askInput, false); // 創建聊天區域 const chatArea = document.createElement("div"); chatArea.classList.add("chatArea"); chatArea.style.overflow = "hidden"; postBody.insertBefore(chatArea, askInput); this.animateElement(chatArea, false); return { askInput, chatArea, askTextarea }; } /** * 設置問問按鈕事件 */ setupAskButtonEvents(askButton, askInput, chatArea, askTextarea, postBody) { // 按鈕點擊事件 askButton.addEventListener("click", async () => { this.scrollToElement(askButton); if (!this.validateApiKey()) return; const isExpanded = askButton.querySelector("p").textContent === "問問 ▲"; this.toggleElement(askInput); this.toggleElement(chatArea); askButton.querySelector("p").textContent = isExpanded ? "問問 ▼" : "問問 ▲"; if (!isExpanded) { askTextarea.focus(); } }); // 輸入框按鍵事件 askTextarea.addEventListener("keydown", async (e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); await this.handleAskQuestion(askTextarea, chatArea, postBody); } }); } /** * 處理問問功能 */ async handleAskQuestion(askTextarea, chatArea, postBody) { if (!this.validateApiKey()) return; if (askTextarea.placeholder !== "詢問⋯") return; const userInput = askTextarea.value; if (!userInput.trim()) return; askTextarea.placeholder = "載入中⋯"; askTextarea.value = ""; const gptArray = this.buildChatGptArray(postBody, chatArea, userInput); const { response, data } = await this.sendGptArrayRequest(gptArray); if (!response) { askTextarea.placeholder = "詢問⋯"; askTextarea.value = userInput; return; } // 添加用戶問題 this.addChatMessage(chatArea, userInput, "user-ask"); // 添加AI回答 this.addChatMessage(chatArea, data.choices[0].message.content, "gpt-reply"); // 更新聊天區域高度 this.updateChatAreaHeight(chatArea); askTextarea.placeholder = "詢問⋯"; askTextarea.focus(); } /** * 構建聊天GPT陣列 */ buildChatGptArray(postBody, chatArea, userInput) { const gptArray = []; const customPrompt = this.settings.get("custom_oaiPromptChat"); const prompt = customPrompt || this.settings.get("oaiPromptChat"); const useSystemMode = this.settings.getBool("oaiPromptSystemMode"); // 添加系統提示 gptArray.push({ role: useSystemMode ? "system" : "user", content: prompt, }); if (!useSystemMode) { gptArray.push({ role: "assistant", content: "好的,請提供文章。", }); } // 添加文章內容 const articleContent = this.extractArticleContent(postBody); gptArray.push({ role: "user", content: `文章內容:\n\`\`\`\n${articleContent}\n\`\`\``, }); // 添加聊天歷史 const chatHistory = chatArea.querySelectorAll(".chatHistory"); chatHistory.forEach((chat) => { const role = chat.classList.contains("user-ask") ? "user" : "assistant"; const content = chat.querySelector(".c-article__content").textContent; gptArray.push({ role, content }); }); // 添加當前問題 gptArray.push({ role: "user", content: userInput, }); return gptArray; } /** * 添加聊天訊息 */ addChatMessage(chatArea, content, type) { const messageArticle = document.createElement("article"); messageArticle.classList.add("c-article", "FM-P2", "chatHistory", type); messageArticle.id = `chat-${Date.now()}-${Math.random() .toString(36) .substr(2, 9)}`; Object.assign(messageArticle.style, { display: "block", minHeight: "0px", marginBottom: type === "user-ask" ? "0.8rem" : "1.6rem", borderBottom: `1px solid ${ type === "user-ask" ? "var(--primary-text)" : "var(--primary)" }`, }); const messageContent = document.createElement("div"); messageContent.classList.add("c-article__content"); messageContent.style.whiteSpace = "pre-wrap"; messageContent.innerHTML = content; messageArticle.appendChild(messageContent); chatArea.appendChild(messageArticle); } /** * 更新聊天區域高度 */ updateChatAreaHeight(chatArea) { requestAnimationFrame(() => { chatArea.style.readHeight = `${chatArea.scrollHeight}px`; chatArea.style.maxHeight = `${chatArea.scrollHeight}px`; }); } /** * 獲取留言資料 */ async getCommentData(postId) { let commentElements = document .getElementById(`Commendlist_${postId}`) .querySelectorAll(".c-reply__item"); // 展開留言 const showButton = document.getElementById(`showoldCommend_${postId}`); if ( showButton && (showButton.style.display === "block" || showButton.style.display === "") ) { const initialCount = commentElements.length; showButton.click(); await new Promise((resolve) => { const observer = new MutationObserver(() => { const currentElements = document .getElementById(`Commendlist_${postId}`) .querySelectorAll(".c-reply__item"); if (currentElements.length >= initialCount) { commentElements = currentElements; document.getElementById(`closeCommend_${postId}`).click(); observer.disconnect(); resolve(); } }); observer.observe(document.getElementById(`Commendlist_${postId}`), { childList: true, subtree: true, characterData: true, }); }); document.getElementById(`closeCommend_${postId}`).click(); } // 處理留言內容 let textContent = ""; const textContentOrigin = Array.from(commentElements) .map((node) => node.innerHTML) .join(""); commentElements.forEach((node) => { const user = node.querySelector(".reply-content__user").innerHTML; const comment = node.querySelector(".comment_content").innerHTML; textContent += `@${user}:${comment}\n`; }); // 清理格式 textContent = textContent.replace(/\n+/g, "\n"); textContent = this.processCommentReferences(textContent); return { textContent, textContentOrigin }; } /** * 處理留言引用格式 */ processCommentReferences(textContent) { const patterns = [ { regex: /([^<]+)\((.*?)\)<\/a>/g, replacement: (match, prefix, name) => `回應@${name} => `, }, { regex: /([^<]+)<\/a>/g, replacement: (match, name) => `回應@${name},`, }, ]; patterns.forEach(({ regex, replacement }) => { textContent = textContent.replace(regex, replacement); }); return textContent; } /** * 還原原始格式 */ restoreOriginalFormat(textContent, originalContent) { const nameToHtmlMap = new Map(); const htmlPattern = /([^<]+)<\/a>/g; let match; while ((match = htmlPattern.exec(originalContent)) !== null) { nameToHtmlMap.set(match[1], match[0]); } let processedText = textContent; nameToHtmlMap.forEach((html, name) => { const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const atPattern = new RegExp(`@${escapedName}`, "g"); processedText = processedText.replace(atPattern, html); }); return processedText; } /** * 更新提示詞 */ async updatePrompts() { const today = new Date(); const lastUpdate = this.settings.get("oaiPromptUpdateDate"); const sleepDays = parseInt(this.settings.get("oaiPromptUpdateSleep")); const daysDiff = Math.floor( (today - new Date(lastUpdate.replace(/(\d{4})(\d{2})(\d{2})/, "$1-$2-$3"))) / (1000 * 60 * 60 * 24) ); if (daysDiff < sleepDays) return; try { const response = await fetch(this.settings.get("oaiPromptUpdateURL")); if (!response.ok) { console.error("[ERROR] fetching prompt:", response.status); return; } const data = await response.json(); const todayString = today.toISOString().slice(0, 10).replace(/-/g, ""); this.settings.set("oaiPromptUpdateDate", todayString); if (this.settings.get("oaiPromptDate") >= data.oaiPromptDate) return; // 更新提示詞 this.settings.set("oaiPromptDate", data.oaiPromptDate); this.settings.set("oaiPrompt", data.oaiPrompt); this.settings.set("oaiPromptUpdateSleep", data.oaiPromptUpdateSleep); this.settings.set("oaiPromptCmd", data.oaiPromptCmd); } catch (error) { console.error("[ERROR] fetching prompt:", error); } } /** * 發送GPT請求 */ async sendGptRequest(systemPrompt, userPrompt) { const useSystemMode = this.settings.getBool("oaiPromptSystemMode"); const messages = [ { role: useSystemMode ? "system" : "user", content: systemPrompt, }, { role: "user", content: userPrompt, }, ]; return this.makeGptRequest(messages); } /** * 發送GPT陣列請求 */ async sendGptArrayRequest(messages) { return this.makeGptRequest(messages); } /** * 執行GPT請求 */ async makeGptRequest(messages) { return new Promise((resolve) => { const apiKey = this.settings.get("oaiKey"); GM_xmlhttpRequest({ method: "POST", url: this.settings.get("oaiBaseUrl"), headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}`, }, data: JSON.stringify({ messages, max_tokens: 4090, model: this.settings.get("oaiModel"), stream: false, temperature: 0.7, presence_penalty: 0, frequency_penalty: 0, }), timeout: 30000, onload: (response) => { try { if (response.status !== 200) { console.error(`伺服器回應錯誤: ${response.status}`); alert("取得 GPT 回覆時發生錯誤,請稍後再試。"); resolve({ response: false, data: null }); return; } const data = JSON.parse(response.responseText); if (data?.choices?.[0]?.message?.content) { resolve({ response: true, data }); } else { console.error("API 返回的數據格式不正確"); alert("取得 GPT 回覆時發生錯誤,請稍後再試。"); resolve({ response: false, data: null }); } } catch (error) { console.error("取得 GPT 回覆時發生錯誤:", error); alert("取得 GPT 回覆時發生錯誤,請稍後再試。"); resolve({ response: false, data: null }); } }, onerror: (error) => { console.error("取得 GPT 回覆時發生錯誤:", error); alert("取得 GPT 回覆時發生錯誤,請稍後再試。"); resolve({ response: false, data: null }); }, ontimeout: () => { console.error("取得 GPT 回覆超時"); alert("取得 GPT 回覆時發生錯誤,請稍後再試。"); resolve({ response: false, data: null }); }, }); }); } /** * 工具方法:創建按鈕 */ createButton({ text, icon, id, className = "article-footer_right-btn", style = {}, insertPosition = null, }) { const button = document.createElement("a"); button.classList.add(className); button.innerHTML = `<i class="material-icons lazyBtn">${icon}</i><p>${text}</p>`; button.id = id; Object.assign(button.style, { display: "flex", alignItems: "center", ...style, }); return button; } /** * 工具方法:創建摘要文章 */ createSummaryArticle(id, content) { const article = document.createElement("article"); article.classList.add("c-article", "FM-P2"); article.id = id; Object.assign(article.style, { display: "block", overflow: "hidden", maxHeight: "auto", minHeight: "0px", }); const articleContent = document.createElement("div"); articleContent.classList.add("c-article__content"); articleContent.style.whiteSpace = "pre-wrap"; articleContent.innerHTML = content; article.appendChild(articleContent); return article; } /** * 工具方法:創建留言摘要文章 */ createCommentSummaryArticle(id, content) { const article = document.createElement("article"); article.classList.add("c-reply__item", "c-article", "FM-P2"); article.id = id; Object.assign(article.style, { display: "block", overflow: "hidden", maxHeight: "auto", minHeight: "0px", }); const articleContent = document.createElement("div"); articleContent.classList.add("c-article__content"); articleContent.style.whiteSpace = "pre-wrap"; articleContent.innerHTML = content; article.appendChild(articleContent); return article; } /** * 工具方法:提取文章內容 */ extractArticleContent(postBody) { const articleContent = postBody.querySelector(".c-article__content"); let textContent = ""; articleContent.childNodes.forEach((node) => { textContent += node.textContent.trim() + "\n"; }); return textContent.replace(/\n+/g, "\n"); } /** * 工具方法:驗證API密鑰 */ validateApiKey() { const apiKey = this.settings.get("oaiKey"); if (apiKey === "sk-yourKey" || apiKey === "") { alert("請先設定 API Key 才能使用 AI 功能"); return false; } return true; } /** * 工具方法:處理切換邏輯 */ handleToggleLogic(button, elementId, defaultText = null) { const element = document.getElementById(elementId); const buttonText = button.querySelector("p").textContent; if (element && buttonText === "摺疊 ▲") { this.toggleElement(element); button.querySelector("p").textContent = "展開 ▼"; return true; } if (element && buttonText === "展開 ▼") { this.toggleElement(element); button.querySelector("p").textContent = "摺疊 ▲"; return true; } if (element && buttonText !== (defaultText || "懶人包")) { return true; } return false; } /** * 工具方法:滾動到元素 */ scrollToElement(element, marginOffset = 7) { const originalMargin = element.style.marginTop || "0px"; element.style.marginTop = `-${marginOffset}rem`; element.scrollIntoView({ behavior: "smooth" }); element.style.marginTop = originalMargin; // 檢查是否在視窗內,如果不是則重新滾動 setTimeout(() => { if (!UtilityFunctions.isInViewport(element)) { this.scrollToElement(element, marginOffset); } }, 700); } /** * 工具方法:動畫元素 */ async animateElement( element, show = true, animation = "ud", waitForAppend = true ) { if (waitForAppend) { requestAnimationFrame(() => { this.setElementDimensions(element); }); } else { this.setElementDimensions(element); } this.setupElementAnimation(element, show, animation); } /** * 工具方法:設置元素尺寸 */ setElementDimensions(element) { element.style.readHeight = element.scrollHeight === 0 ? "999px" : `${element.scrollHeight}px`; element.style.readWidth = element.scrollWidth === 0 ? "999px" : `${element.scrollWidth}px`; } /** * 工具方法:設置元素動畫 */ setupElementAnimation(element, show, animation) { Object.assign(element.style, { transition: "", overflow: "hidden auto", opacity: "0", }); this.toggleElement(element, false, animation); element.style.transition = "all 0.5s cubic-bezier(0.21, 0.3, 0.18, 1.37) 0s"; if (!show) { element.style.beeShow = "false"; return; } element.style.beeShow = "true"; element.style.opacity = "1"; requestAnimationFrame(() => { element.style.maxHeight = element.style.readHeight; }); } /** * 工具方法:切換元素顯示 */ toggleElement(element, show = null, animation = "ud") { const shouldShow = show === null ? !(element.style.beeShow === "true") : show; if (shouldShow) { Object.assign(element.style, { opacity: "1", maxHeight: element.style.readHeight, maxWidth: element.style.readWidth, transform: "translateX(0px) translateY(0px)", beeShow: "true", }); } else { element.style.beeShow = "false"; element.style.opacity = "0"; if (animation.includes("u")) { element.style.maxHeight = "0px"; if (animation.startsWith("d")) { element.style.transform = `translateX(0px) translateY(${element.style.readWidth}px)`; } } if (animation.includes("l")) { element.style.maxWidth = "0px"; if (animation.startsWith("r")) { element.style.transform = `translateX(${element.style.readHeight}) translateY(0px)`; } } } } } /** * 設定管理與UI模組 * 負責插件設定介面、提示系統、檢舉提醒等功能 */ class SettingsUIManager { constructor(settings) { this.settings = settings; } /** * 添加設定元素到頁面 */ async addSettingElement(isNewVersion) { const uiElements = this.createUIElements(isNewVersion); if (!uiElements) return; const { navAdd, settingsContainer } = uiElements; this.setupEventListeners(navAdd, settingsContainer, isNewVersion); this.createSettingsContent(settingsContainer, isNewVersion); await this.initializeSettingsContainer(settingsContainer); } /** * 創建UI元素 */ createUIElements(isNewVersion) { if (isNewVersion) { return this.createNewVersionUI(); } else if (document.querySelector(".b-list") !== null) { return this.createOldVersionUI(); } return null; } /** * 創建新版UI */ createNewVersionUI() { const navAddTag = document.querySelector(".forum-nav-main"); const settingsWarp = document.querySelector(".forum-header"); if (!navAddTag || !settingsWarp) return null; // 創建設定按鈕 const navAdd = document.createElement("li"); navAdd.className = "forum-nav-link forum-nav-rules beeSettingTag"; navAdd.innerHTML = "插件設定"; navAdd.style.cursor = "pointer"; navAddTag.appendChild(navAdd); // 創建設定容器 const settingsContainer = this.createSettingsContainer( "forum-filter-box beeSettingWarp" ); settingsWarp.appendChild(settingsContainer); // 添加提示標題 const sectionTitle = document.createElement("h3"); sectionTitle.className = "section-title"; sectionTitle.textContent = "滾動下拉還有哦!"; sectionTitle.style.margin = "0.6rem 0 0.7rem 0.7rem"; settingsContainer.appendChild(sectionTitle); return { navAdd, settingsContainer }; } /** * 創建舊版UI */ createOldVersionUI() { const navAddTag = document.querySelector(".BH-menuE"); const settingsWarp = document.querySelector(".b-list-wrap"); if (!navAddTag || !settingsWarp) return null; // 創建設定按鈕 const navAdd = document.createElement("li"); navAdd.className = "beeSettingTag"; navAdd.innerHTML = "插件設定"; navAdd.style.cursor = "pointer"; navAddTag.appendChild(navAdd); // 創建設定容器 const settingsContainer = this.createSettingsContainer( "forum-filter-box beeSettingWarp" ); settingsWarp.insertBefore(settingsContainer, settingsWarp.firstChild); // 添加提示標題 const sectionTitle = document.createElement("h3"); sectionTitle.className = "section-title"; sectionTitle.textContent = "插件設定(再點一次上方的【插件設定】即可返回【文章列表】)"; sectionTitle.style.margin = "0.6rem 0 0.7rem 0.7rem"; settingsContainer.appendChild(sectionTitle); return { navAdd, settingsContainer }; } /** * 創建設定容器 */ createSettingsContainer(className) { const container = document.createElement("div"); container.className = className; Object.assign(container.style, { maxHeight: "0px", overflow: "hidden auto", }); return container; } /** * 設置事件監聽器 */ setupEventListeners(navAdd, settingsContainer, isNewVersion) { navAdd.addEventListener("click", () => { if (isNewVersion) { this.toggleNewVersionSettings(settingsContainer); } else { this.toggleOldVersionSettings(settingsContainer); } }); } /** * 切換新版設定顯示 */ toggleNewVersionSettings(container) { if (container.style.maxHeight === "0px") { container.style.maxHeight = "60vh"; container.style.opacity = "1"; } else { container.style.maxHeight = "0px"; container.style.opacity = "0"; } } /** * 切換舊版設定顯示 */ toggleOldVersionSettings(container) { const animationManager = new AnimationManager(); animationManager.popElement(container, "toggle", "ud"); const scrollTarget = document.getElementById("BH-master") || document.querySelector(".b-list-wrap"); if (scrollTarget) { this.scrollToElement(scrollTarget, 7); } } /** * 創建設定內容 */ createSettingsContent(container, isNewVersion) { this.addBasicSettings(container, isNewVersion); this.addLayoutSettings(container); this.addAISettings(container); this.addMiscSettings(container, isNewVersion); this.addReloadButton(container, isNewVersion); } /** * 添加基本設定 */ addBasicSettings(container, isNewVersion) { if (!isNewVersion) { container.appendChild( this.createItemCard("add_function", "標題後方插入功能按鈕") ); container.appendChild( this.createItemCard("preview_auto", "點擊文章時使用即時瀏覽") ); container.appendChild( this.createItemCard(null, null, { inputId: "preview_size", labelText: " └ 即時瀏覽視窗的大小", }) ); } else { container.appendChild( this.createItemCard("cleanMode", "清爽模式(隱藏文章描述和縮圖)") ); container.appendChild( this.createItemCard(null, null, { inputId: "cleanModeSize", labelText: " └ 清爽模式文章清單大小", }) ); container.appendChild( this.createItemCard(null, null, { inputId: "preview_size", labelText: "即時瀏覽視窗的大小", }) ); } container.appendChild( this.createItemCard("preview_LR", "即時瀏覽從右方彈出(取消則從左)") ); } /** * 添加版面設定 */ addLayoutSettings(container) { container.appendChild( this.createItemCard("new_design", "自訂板面大小(附加浮動型聊天室)") ); const layoutSettings = [ { inputId: "new_design_box", labelText: " └ 整體顯示區域佔比(文章+聊天室佔整個畫面的比例,< 100%)", }, { inputId: "new_design_box_Left", labelText: " ├ 文章佔比(與聊天室佔比總和 <= 100%)", }, { inputId: "new_design_box_Right", labelText: " └ 聊天室佔比", }, ]; layoutSettings.forEach((setting) => { container.appendChild(this.createItemCard(null, null, setting)); }); container.appendChild( this.createItemCard("new_design_LRSwitch", "聊天室在左方") ); } /** * 添加AI設定 */ addAISettings(container) { container.appendChild( this.createItemCard( "addSummaryBtn", "跳過樓層按鈕/AI總結(AI功能需自備KEY填入下方)" ) ); const aiSettings = [ { inputId: "oaiBaseUrl", labelText: " ├ oai URL" }, { inputId: "oaiModel", labelText: " ├ oai model" }, { inputId: "oaiKey", labelText: " ├ oai key" }, { inputId: "custom_oaiPrompt", labelText: " ├ 「懶人包」提示詞(留空=預設)", }, { inputId: "custom_oaiPromptCmd", labelText: " ├ 「留言統整」自訂提示詞(留空=預設)", }, { inputId: "custom_oaiPromptChat", labelText: " ├ 「問問」自訂提示詞(留空=預設)", }, ]; aiSettings.forEach((setting) => { container.appendChild(this.createItemCard(null, null, setting)); }); container.appendChild( this.createItemCard("oaiPromptSystemMode", "├ 自訂提示詞使用 system 模式") ); container.appendChild( this.createItemCard(null, null, { inputId: "oaiPromptUpdateURL", labelText: " └ oai prompt settings URL", }) ); } /** * 添加雜項設定 */ addMiscSettings(container, isNewVersion) { if (!isNewVersion) { container.appendChild( this.createItemCard("addBorderInPicMode", "縮圖列表模式中,加上分隔線") ); container.appendChild( this.createItemCard("showAbuse", "有檢舉時,自動以即時瀏覽開啟") ); } container.appendChild(this.createItemCard("showTips", "重新觀看TIPs")); } /** * 添加重載按鈕 */ addReloadButton(container, isNewVersion) { const reloadBtn = document.createElement("button"); reloadBtn.textContent = "重整頁面以生效"; if (!isNewVersion) { reloadBtn.style.margin = "0.5rem 0 0.7rem 0.7rem"; reloadBtn.style.color = "white"; } reloadBtn.addEventListener("click", () => { location.reload(); }); const reloadBtnDiv = document.createElement("div"); reloadBtnDiv.className = isNewVersion ? "btn btn-primary" : "BH-rbox BH-qabox1"; reloadBtnDiv.appendChild(reloadBtn); container.appendChild(reloadBtnDiv); } /** * 創建設定項目卡片 */ createItemCard(inputId, labelText, additionalContent = null) { const itemCard = document.createElement("div"); itemCard.className = "item-card management_guild-check single-choice forum-filter-group"; const checkGroup = document.createElement("div"); checkGroup.className = "check-group"; checkGroup.style.margin = "0rem 0 0.1rem 0.7rem"; if (inputId) { this.createCheckboxInput(checkGroup, inputId, labelText); } if (additionalContent) { this.createTextInput(checkGroup, additionalContent); } itemCard.appendChild(checkGroup); return itemCard; } /** * 創建複選框輸入 */ createCheckboxInput(container, inputId, labelText) { const input = document.createElement("input"); input.id = inputId; input.type = "checkbox"; input.checked = this.settings.getBool(inputId); const label = document.createElement("label"); label.htmlFor = inputId; label.className = "is-active"; const labelIcon = document.createElement("div"); labelIcon.className = "label-icon"; const icon = document.createElement("i"); icon.className = "fa fa-check"; labelIcon.appendChild(icon); const h6 = document.createElement("h6"); h6.textContent = labelText; Object.assign(h6.style, { display: "inline-block", color: "var(--primary-text)", fontSize: "100%", }); label.appendChild(labelIcon); label.appendChild(h6); container.appendChild(input); container.appendChild(label); // 添加事件監聽器 input.addEventListener("input", () => { this.settings.set(inputId, input.checked.toString()); }); } /** * 創建文字輸入 */ createTextInput(container, config) { const h6 = document.createElement("h6"); h6.textContent = config.labelText; h6.style.display = "inline-block"; container.appendChild(h6); const input = document.createElement("input"); input.className = "form-control"; input.id = config.inputId; input.type = "text"; input.size = 25; Object.assign(input.style, { margin: "0px", width: config.inputId.startsWith("custom_") ? "auto" : "70px", }); input.value = this.settings.get(config.inputId) || ""; container.appendChild(input); // 添加事件監聽器 input.addEventListener("input", () => { this.settings.set(config.inputId, input.value); }); } /** * 初始化設定容器 */ async initializeSettingsContainer(container) { const animationManager = new AnimationManager(); await animationManager.popElementInit(container, false, "ud"); } /** * 滾動到元素 */ scrollToElement(element, marginOffset = 7) { const originalMargin = element.style.marginTop || "0px"; element.style.marginTop = `-${marginOffset}rem`; element.scrollIntoView({ behavior: "smooth" }); element.style.marginTop = originalMargin; setTimeout(() => { if (!UtilityFunctions.isInViewport(element)) { this.scrollToElement(element, marginOffset); } }, 300); } } /** * 提示系統管理器 */ class TipsManager { constructor(settings) { this.settings = settings; } /** * 檢查並載入提示 */ checkTips(isNewVersion) { if (this.shouldShowBPageTips()) { this.loadBPageTips(); this.settings.set("showTips", "false"); } if (this.shouldShowHomeTips()) { this.loadHomeTips(); this.settings.set("homeTips", "false"); } } /** * 檢查是否應顯示B頁提示 */ shouldShowBPageTips() { return ( window.location.href.includes("forum.gamer.com.tw/B.php") && this.settings.getBool("showTips") ); } /** * 檢查是否應顯示首頁提示 */ shouldShowHomeTips() { return ( window.location.href.includes("www.gamer.com.tw") && this.settings.getBool("homeTips") ); } /** * 載入B頁提示 */ loadBPageTips() { this.loadDriverJS(() => { this.createBPageTour(); }); } /** * 載入首頁提示 */ loadHomeTips() { this.loadDriverJS(() => { this.createHomeTour(); }); } /** * 載入Driver.js庫 */ loadDriverJS(callback) { // 載入CSS const link = document.createElement("link"); link.rel = "stylesheet"; link.href = "https://cdn.jsdelivr.net/npm/[email protected]/dist/driver.css"; document.head.appendChild(link); // 載入JS const script = document.createElement("script"); script.src = "https://cdn.jsdelivr.net/npm/[email protected]/dist/driver.js.iife.js"; script.onload = callback; document.head.appendChild(script); } /** * 創建B頁導覽 */ createBPageTour() { const picMode = document.querySelectorAll(".imglist-text").length !== 0; const driver = window.driver.js.driver; const driverObj = driver({ showButtons: ["next", "previous"], allowClose: false, nextBtnText: "▶", prevBtnText: "◀", doneBtnText: "好耶", showProgress: true, steps: this.getBPageTourSteps(picMode), }); driverObj.drive(); } /** * 獲取B頁導覽步驟 */ getBPageTourSteps(picMode) { return [ { element: ".beeSettingTag", popover: { title: "客製化設定", description: "在這裡可以進行詳細的個人設定,設定變更後需要【重新整理】頁面才會生效。", }, }, { element: picMode ? "#BH-master > form > div > table > tbody > tr > td.b-list__main > div > div > p" : "#BH-master > form > div > table > tbody > tr > td.b-list__main > a", popover: { title: "即時瀏覽", description: "如果開啟「點擊時使用即時預覽」,文章標題的跳轉會以即時預覽的方式啟動。", side: "bottom", }, }, { element: "#BH-master > form > div > table > tbody > tr > td.b-list__main", popover: { title: "快速選取", description: "除了文章標題、縮圖模式的預覽圖,其他區域可以觸發快速選取。功能等同左方的勾選方塊。", side: "bottom", onNextClick: () => { this.showFunctionButtons(); driverObj.moveNext(); }, }, }, { element: picMode ? "#BH-master > form > div > table > tbody > tr:nth-child(2) > td:nth-child(3)" : "#BH-master > form > div > table > tbody > tr:nth-child(2) > td:nth-child(3)", popover: { title: "功能按鈕", description: "如果開啟「插入功能按鈕」,指標指向的文章後方會出現三個功能按鈕,分別是「即時預覽」「新分頁開啟」「複製連結」。", side: "bottom", onNextClick: () => { this.hideFunctionButtons(); this.clickPreviewButton(picMode); driverObj.moveNext(); }, }, }, { element: "#BH-master > form > section:last-child > div", popover: { title: "功能選單", description: "快速預覽視窗中,功能選單會漂浮在下方,方便使用!", side: "bottom", onPrevClick: () => { this.closePreviewer(); this.showFunctionButtons(); driverObj.movePrevious(); }, onNextClick: () => { this.closePreviewer(); driverObj.moveNext(); }, }, }, ]; } /** * 顯示功能按鈕 */ showFunctionButtons() { const buttons = [".bee_preview", ".bee_open_new_wd", ".bee_link"]; buttons.forEach((selector) => { const button = document.querySelector( `#BH-master > form > div > table > tbody > tr:nth-child(2) > td:nth-child(3) > a.btn-icon.btn-icon--inverse${selector}` ); if (button) { button.style.display = "inline-block"; } }); } /** * 隱藏功能按鈕 */ hideFunctionButtons() { const buttons = [".bee_preview", ".bee_open_new_wd", ".bee_link"]; buttons.forEach((selector) => { const button = document.querySelector( `#BH-master > form > div > table > tbody > tr:nth-child(2) > td:nth-child(3) > a.btn-icon.btn-icon--inverse${selector}` ); if (button) { button.style.display = "none"; } }); } /** * 點擊預覽按鈕 */ clickPreviewButton(picMode) { const selector = picMode ? "#BH-master > form > div > table > tbody > tr:nth-child(2) > td.b-list__main > div > a" : "#BH-master > form > div > table > tbody > tr:nth-child(2) > td.b-list__main > a"; const button = document.querySelector(selector); if (button) { button.click(); } } /** * 關閉預覽器 */ closePreviewer() { const closeButton = document.querySelector("#BH-menu-path"); if (closeButton) { closeButton.click(); } } /** * 創建首頁導覽 */ createHomeTour() { const driver = window.driver.js.driver; const driverObj = driver({ showButtons: ["next", "previous"], allowClose: false, nextBtnText: "▶", prevBtnText: "◀", doneBtnText: "好耶", showProgress: true, steps: [ { element: "#homeStyleSwitch", popover: { title: "滿版首頁", description: "點此可以切換首頁排版。", }, }, ], }); driverObj.drive(); } } /** * 檢舉提醒管理器 */ class ReportManager { constructor(settings) { this.settings = settings; } /** * 檢查檢舉提醒 */ checkAlert(isNewVersion) { if (!this.shouldCheckReport()) return; const isReported = this.detectReport(); if (!isReported) return; const bsn = this.extractBsn(); if (!bsn) { console.log("[WARN] 有檢舉但抓取連結失敗"); return; } this.openReportPage(bsn); } /** * 檢查是否應該檢查檢舉 */ shouldCheckReport() { return ( window.location.href.includes("forum.gamer.com.tw/B.php") && this.settings.getBool("showAbuse") ); } /** * 檢測是否有檢舉 */ detectReport() { const reportElement = document.querySelector( "#BH-slave > div.BH-rbox.FM-rbox14 > div.FM-master-btn > a > span" ); return reportElement !== null; } /** * 提取BSN參數 */ extractBsn() { const urlParams = new URLSearchParams(window.location.search); return urlParams.get("bsn"); } /** * 開啟檢舉頁面 */ openReportPage(bsn) { // 這裡需要使用全域的openInFrame函數 if (typeof openInFrame === "function") { openInFrame(`https://forum.gamer.com.tw/gemadmin/accuse.php?bsn=${bsn}`); } } } /** * 動畫管理器 */ class AnimationManager { /** * 彈出元素初始化 */ async popElementInit(element, show = true, anime = "ud", waitAppend = true) { if (waitAppend) { requestAnimationFrame(() => { this.setElementDimensions(element); }); } else { this.setElementDimensions(element); } this.setupElementTransition(element, show, anime); } /** * 設置元素尺寸 */ setElementDimensions(element) { element.style.readHeight = element.scrollHeight === 0 ? "999px" : `${element.scrollHeight}px`; element.style.readWidth = element.scrollWidth === 0 ? "999px" : `${element.scrollWidth}px`; } /** * 設置元素過渡 */ setupElementTransition(element, show, anime) { Object.assign(element.style, { transition: "", overflow: "hidden auto", opacity: "0", }); this.popElement(element, "false", anime); element.style.transition = "all 0.5s cubic-bezier(0.21, 0.3, 0.18, 1.37) 0s"; if (!show) { element.style.beeShow = "false"; return; } element.style.beeShow = "true"; element.style.opacity = "1"; requestAnimationFrame(() => { element.style.maxHeight = element.style.readHeight; }); } /** * 彈出元素控制 */ popElement(element, show = "true", anime = "ud") { const doShow = show === "toggle" ? !(element.style.beeShow === "true") : show === "true"; if (doShow) { this.showElement(element); } else { this.hideElement(element, anime); } } /** * 顯示元素 */ showElement(element) { Object.assign(element.style, { opacity: "1", maxHeight: element.style.readHeight, maxWidth: element.style.readWidth, transform: "translateX(0px) translateY(0px)", beeShow: "true", }); } /** * 隱藏元素 */ hideElement(element, anime) { element.style.beeShow = "false"; element.style.opacity = "0"; if (anime.includes("u")) { element.style.maxHeight = "0px"; if (anime.startsWith("d")) { element.style.transform = `translateX(0px) translateY(${element.style.readWidth}px)`; } } if (anime.includes("l")) { element.style.maxWidth = "0px"; if (anime.startsWith("r")) { element.style.transform = `translateX(${element.style.readHeight}) translateY(0px)`; } } } } /** * 首頁功能模組 * 負責首頁的版面切換和樣式調整功能 */ class HomePageWorker { constructor(settings) { this.settings = settings; } /** * 初始化首頁功能 */ init() { if (!this.isHomePage()) return; this.addStyleSwitchButton(); this.applyHomeStyles(); } /** * 檢查是否為首頁 */ isHomePage() { return ( window.location.href.includes("www.gamer.com.tw") && document.querySelectorAll("div.BA-lbox.BA-lbox3").length > 0 ); } /** * 添加樣式切換按鈕 */ addStyleSwitchButton() { const baServeElement = document.querySelector(".BA-serve"); if (!baServeElement) return; const switchButton = document.createElement("li"); switchButton.id = "homeStyleSwitch"; switchButton.innerHTML = "首頁滿版切換"; baServeElement.appendChild(switchButton); switchButton.addEventListener("click", () => { this.toggleHomeStyle(); }); } /** * 切換首頁樣式 */ toggleHomeStyle() { const currentValue = this.settings.getBool("homeStyleSwitch"); this.settings.set("homeStyleSwitch", (!currentValue).toString()); location.reload(); } /** * 應用首頁樣式 */ applyHomeStyles() { if (!this.settings.getBool("homeStyleSwitch")) return; this.reorganizeContainers(); this.createSecondLeftNav(); this.addCustomStyles(); } /** * 重新組織容器 */ reorganizeContainers() { const hotboardContainer = document.getElementById("hotboardContainer"); const guildContainer = document.getElementById("guildContainer"); const hothalaContainer = document.getElementById("hothalaContainer"); if (hotboardContainer && guildContainer && hothalaContainer) { hothalaContainer.appendChild(hotboardContainer); hothalaContainer.appendChild(guildContainer); } } /** * 創建第二個左側導航 */ createSecondLeftNav() { const bahaStoreContainer = document.querySelectorAll( "div.BA-lbox.BA-lbox3" )[0]; const bahaAnimeContainer = document.querySelectorAll( "div.BA-lbox.BA-lbox3" )[1]; const titles = document.querySelectorAll("h1.BA-ltitle"); const wrapper = document.querySelectorAll("div.BA-wrapper.BA-main")[0]; const center = document.querySelectorAll("div.BA-center")[0]; if (!bahaStoreContainer || !bahaAnimeContainer || !wrapper || !center) return; const secondDivLeft = document.createElement("div"); secondDivLeft.className = "BA-left"; Object.assign(secondDivLeft.style, { flex: "0 0 11em", margin: "0 0 0 1em", }); // 添加標題和容器 if (titles[1]) secondDivLeft.appendChild(titles[1]); secondDivLeft.appendChild(bahaAnimeContainer); if (titles[0]) secondDivLeft.appendChild(titles[0]); secondDivLeft.appendChild(bahaStoreContainer); wrapper.insertBefore(secondDivLeft, center); } /** * 添加自定義樣式 */ addCustomStyles() { const styleElement = document.createElement("style"); styleElement.textContent = this.getCustomCSS(); document.head.appendChild(styleElement); } /** * 獲取自定義CSS */ getCustomCSS() { return ` /* 父容器設置 */ .BA-wrapper.BA-main { display: flex; flex-wrap: wrap; width: auto; } /* 第一層左側固定寬度 */ .BA-left { flex: 0 0 11em; } /* 第一層右側容器 */ .BA-center { flex: 1; display: flex; flex-wrap: wrap; } #gnnContainer { flex: 0 0 45%; margin: 0 1% 2% 1%; order: 1; } #hothalaContainer { flex: 45%; margin: 0 1% 2% 1%; order: 2; } #homeContainer { flex: 45%; margin: 0 1% 2% 1%; order: 3; } #buyContainer { flex: 0 0 45%; margin: 0 1% 2% 1%; order: 4; } #liveContainer { flex: 45%; margin: 0 1% 2% 1%; order: 5; } #gamecrazyContainer { flex: 0 0 45%; margin: 0 1% 2% 1%; order: 6; } .BA-cbox7 p { text-align: left !important; } `; } } /** * 工具函數模組 * 提供全域使用的工具函數 */ class UtilityFunctions { /** * 在框架中打開URL(全域函數) */ static openInFrame(url) { const iframe = document.getElementById("bee_frame"); if (!iframe) return; iframe.src = url; const menuPath = document.querySelector("#BH-menu-path"); if (menuPath) { menuPath.style.height = "100%"; menuPath.style.opacity = "0.6"; } setTimeout(() => { const container = document.querySelector(".bee_preview_wd"); if (container) { const animationManager = new AnimationManager(); const direction = localStorage.getItem("preview_LR") === "true" ? "rl" : "lr"; animationManager.popElement(container, "true", direction); } }, 1000); // 設置iframe樣式 setTimeout(() => { UtilityFunctions.setupIframeStyles(iframe); }, 1000); } /** * 設置iframe樣式 */ static setupIframeStyles(iframe) { try { const iframeDoc = iframe.contentDocument || iframe.contentWindow.document; const styleSheet = iframeDoc.createElement("style"); iframeDoc.head.appendChild(styleSheet); const sheet = styleSheet.sheet; sheet.insertRule( ".managertools { position: fixed; bottom: 0; right: 0; z-index: 100; }", 0 ); } catch (error) { console.warn("無法設置iframe樣式:", error); } } /** * 滾動到指定元素 */ static scrollIntoBee(element, marginOffset = 7) { const originalMargin = element.style.marginTop || "0px"; element.style.marginTop = `-${marginOffset}rem`; element.scrollIntoView({ behavior: "smooth" }); element.style.marginTop = originalMargin; setTimeout(() => { if (!UtilityFunctions.isInViewport(element)) { UtilityFunctions.scrollIntoBee(element, marginOffset); } }, 300); } /** * 檢查元素是否在視窗內 */ static isInViewport(element) { const rect = element.getBoundingClientRect(); return ( rect.top >= 0 && rect.top <= (window.innerHeight || document.documentElement.clientHeight) ); } /** * 還原原始格式(用於留言處理) */ static restoreOriginalFormat(textContent, cmdContents) { const nameToHtmlMap = new Map(); const htmlPattern = /([^<]+)<\/a>/g; let match; while ((match = htmlPattern.exec(cmdContents)) !== null) { nameToHtmlMap.set(match[1], match[0]); } let processedText = textContent; nameToHtmlMap.forEach((html, name) => { const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const atPattern = new RegExp(`@${escapedName}`, "g"); processedText = processedText.replace(atPattern, html); }); return processedText; } /** * 格式化日期 */ static formatDate(date) { return date.toISOString().slice(0, 10).replace(/-/g, ""); } /** * 安全獲取元素 */ static safeQuerySelector(selector) { try { return document.querySelector(selector); } catch (error) { console.warn(`無法找到元素: ${selector}`, error); return null; } } /** * 安全獲取多個元素 */ static safeQuerySelectorAll(selector) { try { return document.querySelectorAll(selector); } catch (error) { console.warn(`無法找到元素: ${selector}`, error); return []; } } /** * 防抖函數 */ static debounce(func, wait) { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); }; } /** * 節流函數 */ static throttle(func, limit) { let inThrottle; return function (...args) { if (!inThrottle) { func.apply(this, args); inThrottle = true; setTimeout(() => (inThrottle = false), limit); } }; } /** * 深拷貝對象 */ static deepClone(obj) { if (obj === null || typeof obj !== "object") return obj; if (obj instanceof Date) return new Date(obj.getTime()); if (obj instanceof Array) return obj.map((item) => UtilityFunctions.deepClone(item)); if (typeof obj === "object") { const clonedObj = {}; for (const key in obj) { if (obj.hasOwnProperty(key)) { clonedObj[key] = UtilityFunctions.deepClone(obj[key]); } } return clonedObj; } } /** * 生成唯一ID */ static generateUniqueId(prefix = "id") { return `${prefix}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } /** * 等待指定時間 */ static sleep(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); } /** * 重試函數 */ static async retry(fn, maxRetries = 3, delay = 1000) { for (let i = 0; i < maxRetries; i++) { try { return await fn(); } catch (error) { if (i === maxRetries - 1) throw error; await UtilityFunctions.sleep(delay); } } } /** * 檢查是否為移動設備 */ static isMobileDevice() { return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test( navigator.userAgent ); } /** * 獲取瀏覽器信息 */ static getBrowserInfo() { const ua = navigator.userAgent; let browser = "Unknown"; if (ua.includes("Chrome")) browser = "Chrome"; else if (ua.includes("Firefox")) browser = "Firefox"; else if (ua.includes("Safari")) browser = "Safari"; else if (ua.includes("Edge")) browser = "Edge"; return { browser, userAgent: ua, isMobile: UtilityFunctions.isMobileDevice(), }; } } /** * 主插件類別 - Re版本 * 整合所有模組並提供統一的初始化入口 */ class BahamutePluginMain extends BahamutePlugin { constructor() { super(); //this.homePageWorker = new HomePageWorker(this.settings); this.settingsUIManager = new SettingsUIManager(this.settings); //this.tipsManager = new TipsManager(this.settings); this.reportManager = new ReportManager(this.settings); } /** * 重寫初始化方法 */ async init() { try { console.log("[INFO] 巴哈姆特插件開始初始化..."); // 基礎初始化 this.isNewVersion = await this.detectMode(); this.settings.checkFirstRun(); // UI初始化 await this.settingsUIManager.addSettingElement(this.isNewVersion); // 功能模組初始化 await this.initializeWorkers(); //this.homePageWorker.init(); // 輔助功能初始化 //this.tipsManager.checkTips(this.isNewVersion); this.reportManager.checkAlert(this.isNewVersion); console.log("[INFO] 巴哈姆特插件初始化完成!"); } catch (error) { console.error("[ERROR] 插件初始化失敗:", error); } } /** * 獲取插件版本信息 */ getVersionInfo() { return { version: this.version, isNewVersion: this.isNewVersion, browserInfo: UtilityFunctions.getBrowserInfo(), initTime: new Date().toISOString(), }; } /** * 重置插件設定 */ resetSettings() { if (confirm("確定要重置所有設定嗎?這將會清除所有自定義配置。")) { this.settings.checkFirstRun(true); alert("設定已重置,請重新整理頁面。"); location.reload(); } } /** * 導出設定 */ exportSettings() { const settings = {}; Object.keys(this.settings.defaultSettings).forEach((key) => { settings[key] = this.settings.get(key); }); const dataStr = JSON.stringify(settings, null, 2); const dataBlob = new Blob([dataStr], { type: "application/json" }); const url = URL.createObjectURL(dataBlob); const link = document.createElement("a"); link.href = url; link.download = "bahamute-plugin-settings.json"; link.click(); URL.revokeObjectURL(url); } /** * 導入設定 */ importSettings(file) { const reader = new FileReader(); reader.onload = (e) => { try { const settings = JSON.parse(e.target.result); Object.keys(settings).forEach((key) => { this.settings.set(key, settings[key]); }); alert("設定導入成功,請重新整理頁面。"); location.reload(); } catch (error) { alert("設定文件格式錯誤,請檢查文件內容。"); } }; reader.readAsText(file); } } // 將工具函數設為全域可用 window.openInFrame = UtilityFunctions.openInFrame; window.scrollIntoBee = UtilityFunctions.scrollIntoBee; window.isInViewport = UtilityFunctions.isInViewport; window.restoreOriginalFormat = UtilityFunctions.restoreOriginalFormat; // 初始化插件(替換原本的立即執行函數) (async function () { "use strict"; // 等待DOM載入完成 if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", () => { new BahamutePluginMain(); }); } else { new BahamutePluginMain(); } })(); // 開發者工具(僅在開發模式下可用) if (typeof GM_info !== "undefined" && GM_info.script.name.includes("dev")) { window.BahamutePluginDev = { getPlugin: () => window.bahamutePluginInstance, resetSettings: () => window.bahamutePluginInstance?.resetSettings(), exportSettings: () => window.bahamutePluginInstance?.exportSettings(), getVersionInfo: () => window.bahamutePluginInstance?.getVersionInfo(), utils: UtilityFunctions, }; }