Greasy Fork is available in English.
Preview Reddit posts directly inside Google Search results — score, comments, status tags, full-text preview, and embedded media. Core data fetched client-side directly from official APIs; no intermediate middleman servers.
// ==UserScript== // @name Google Search Reddit Preview // @name:zh-TW Google 搜尋 Reddit 預覽器 // @name:ja Google 検索 Reddit プレビュー // @namespace https://greasyfork.org/en/users/1467948-stonedkhajiit // @version 0.9.5 // @author StonedKhajiit // @description Preview Reddit posts directly inside Google Search results — score, comments, status tags, full-text preview, and embedded media. Core data fetched client-side directly from official APIs; no intermediate middleman servers. // @description:zh-TW 直接在 Google 搜尋結果中預覽 Reddit 貼文 — 分數、留言、狀態標籤、全文預覽,以及內嵌媒體。資料直連官方 API 本機取得,不經由任何非官方中介伺服器。 // @description:ja Reddit の投稿を Google 検索結果内で直接プレビュー — スコア、コメント、ステータスタグ、本文プレビュー、埋め込みメディア。データは各公式 API から直接取得され、非公式の中継サーバーは経由しません。 // @license MIT // @icon https://www.google.com/s2/favicons?sz=64&domain=reddit.com // @match https://www.google.tld/search* // @match https://www.google.com/search* // @match https://cse.google.com/cse* // @exclude https://www.google.tld/search*udm=2* // @exclude https://www.google.tld/search*udm=7* // @exclude https://www.google.tld/search*udm=28* // @exclude https://www.google.tld/search*udm=36* // @exclude https://www.google.tld/search*udm=39* // @exclude https://www.google.tld/search*udm=50* // @exclude https://www.google.tld/search*tbm=50* // @exclude https://www.google.tld/search*tbm=nws* // @exclude https://www.google.com/search*udm=2* // @exclude https://www.google.com/search*udm=7* // @exclude https://www.google.com/search*udm=28* // @exclude https://www.google.com/search*udm=36* // @exclude https://www.google.com/search*udm=39* // @exclude https://www.google.com/search*udm=50* // @exclude https://www.google.com/search*tbm=50* // @exclude https://www.google.com/search*tbm=nws* // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/hls.min.js#sha256-RC9ZnDTxA8M1WzdaI73/VgWS1xF9CajIRyQuo94tQOA= // @connect reddit.com // @connect *.reddit.com // @connect api.imgur.com // @connect api.redgifs.com // @connect media.redgifs.com // @connect redgifs.com // @connect *.redgifs.com // @connect embed.bsky.app // @connect *.bsky.app // @connect translate-pa.googleapis.com // @connect generativelanguage.googleapis.com // @grant GM_addStyle // @grant GM_download // @grant GM_getValue // @grant GM_registerMenuCommand // @grant GM_setClipboard // @grant GM_setValue // @grant GM_xmlhttpRequest // @run-at document-end // @noframes // ==/UserScript== (function () { 'use strict'; const CONFIG_VERSION = 1; const DEFAULT_CONFIG = { configVersion: CONFIG_VERSION, lang: "auto", theme: "auto", commentSort: "confidence", showScore: true, showDate: true, showUpvoteRatio: true, showAwards: true, showCrossposts: true, showStatusTags: true, authorColors: true, embedMedia: true, autoExpandSingleImages: true, defaultVideoVolume: 30, videoAutoplay: true, videoAutoplaySound: false, previewFontSize: 14, timestampFormat: "absolute", autoCollapseRedacted: false, autoCollapseDeleted: false, showImageResolutionPreview: false, showImageResolutionLightbox: true, galleryDisplayMode: "carousel", clickToMaximize: false, cacheTTL: 30, previewMode: "side-panel", logLevel: "WARN", translateEngine: "google", enableTranslate: true, autoTranslate: false, bilingualTranslate: true, translateSourceLang: "auto", translateTargetLang: "auto", translateApiKey: "", imgurClientId: "546c25a59c58ad7", hoverDelay: 0, enableSummarize: false, summarizeType: "key-points", summarizeLength: "medium", summarizeEngine: "browser-ai", summarizeSource: "all", summarizeGeminiApiKey: "", summarizeGeminiModel: "gemini-3.5-flash", summarizeGeminiCustomModel: "", summarizeGeminiPrompt: "" }; const oneOf = (allowed) => (v) => typeof v === "string" && allowed.includes(v) ? v : void 0; const isBool = (v) => typeof v === "boolean" ? v : void 0; const isStr = (v) => typeof v === "string" ? v : void 0; const intInRange = (lo, hi) => (v) => typeof v === "number" && Number.isFinite(v) && v >= lo && v <= hi ? v : void 0; const VALIDATORS = { configVersion: (v) => v === CONFIG_VERSION ? v : void 0, lang: oneOf(["auto", "en", "zh-TW", "ja"]), theme: oneOf(["auto", "light", "dark"]), commentSort: oneOf(["confidence", "top", "new", "controversial", "old", "qa"]), timestampFormat: oneOf(["absolute", "absolute-local", "relative"]), galleryDisplayMode: oneOf(["carousel", "stack"]), previewMode: oneOf(["tooltip", "side-panel"]), logLevel: oneOf(["DEBUG", "INFO", "WARN", "ERROR", "SILENT"]), translateEngine: oneOf(["google", "browser-ai"]), defaultVideoVolume: intInRange(0, 100), previewFontSize: intInRange(8, 32), cacheTTL: intInRange(0, 24 * 60), hoverDelay: intInRange(0, 2e3), translateSourceLang: isStr, translateTargetLang: isStr, showScore: isBool, showDate: isBool, showUpvoteRatio: isBool, showAwards: isBool, showCrossposts: isBool, showStatusTags: isBool, authorColors: isBool, embedMedia: isBool, autoExpandSingleImages: isBool, videoAutoplay: isBool, videoAutoplaySound: isBool, autoCollapseRedacted: isBool, autoCollapseDeleted: isBool, showImageResolutionPreview: isBool, showImageResolutionLightbox: isBool, clickToMaximize: isBool, enableTranslate: isBool, autoTranslate: isBool, bilingualTranslate: isBool, translateApiKey: isStr, imgurClientId: isStr, enableSummarize: isBool, summarizeType: oneOf(["key-points", "tldr", "teaser", "headline"]), summarizeLength: oneOf(["short", "medium", "long"]), summarizeEngine: oneOf(["browser-ai", "gemini"]), summarizeSource: oneOf(["post-only", "all"]), summarizeGeminiApiKey: isStr, summarizeGeminiModel: isStr, summarizeGeminiCustomModel: isStr, summarizeGeminiPrompt: isStr }; function validateConfig(obj) { if (!obj || typeof obj !== "object") return null; const src = obj; const out = {}; for (const key of Object.keys(DEFAULT_CONFIG)) { const validator = VALIDATORS[key]; if (!validator) { if (key in src) out[key] = src[key]; continue; } const cleaned = validator(src[key]); if (cleaned !== void 0) out[key] = cleaned; } return out; } const STORAGE_KEY = "gsrp_config"; const rawConfig = Object.assign({}, DEFAULT_CONFIG); let isConfigLoaded = false; function ensureConfigLoaded() { if (isConfigLoaded) return; isConfigLoaded = true; loadInitialConfig(); } const Config = new Proxy(rawConfig, { get(target, prop, receiver) { ensureConfigLoaded(); return Reflect.get(target, prop, receiver); }, set(target, prop, value, receiver) { ensureConfigLoaded(); const oldValue = Reflect.get(target, prop, receiver); const success = Reflect.set(target, prop, value, receiver); if (success && oldValue !== value) { saveConfigDebounced(); } return success; }, ownKeys(target) { ensureConfigLoaded(); return Reflect.ownKeys(target); }, getOwnPropertyDescriptor(target, prop) { ensureConfigLoaded(); return Reflect.getOwnPropertyDescriptor(target, prop); } }); const SENSITIVE_KEYS = ["summarizeGeminiApiKey", "translateApiKey"]; function loadInitialConfig() { let saved = null; let isFromLocalStorage = false; if (typeof GM_getValue === "function") { try { saved = GM_getValue(STORAGE_KEY, null); } catch (err) { console.warn("[GSRP] GM_getValue failed, attempting localStorage fallback:", err); } } if (!saved) { try { const savedStr = localStorage.getItem(STORAGE_KEY); if (savedStr) { saved = JSON.parse(savedStr); isFromLocalStorage = true; } } catch (err) { console.warn("[GSRP] localStorage config read failed:", err); } } if (saved && typeof saved === "object" && saved.configVersion === CONFIG_VERSION) { if (isFromLocalStorage) { for (const key of SENSITIVE_KEYS) { if (key in saved) { saved[key] = ""; } } } const validated = validateConfig(saved); if (validated) Object.assign(rawConfig, validated); } } function saveConfig() { let savedSuccessfully = false; if (typeof GM_setValue === "function") { try { rawConfig.configVersion = CONFIG_VERSION; GM_setValue(STORAGE_KEY, rawConfig); savedSuccessfully = true; } catch (err) { console.warn("[GSRP] GM_setValue failed, attempting localStorage fallback:", err); } } if (!savedSuccessfully) { try { rawConfig.configVersion = CONFIG_VERSION; const safeConfig = { ...rawConfig }; for (const key of SENSITIVE_KEYS) { safeConfig[key] = ""; } localStorage.setItem(STORAGE_KEY, JSON.stringify(safeConfig)); } catch (err) { console.warn("[GSRP] localStorage config write failed:", err); } } } let saveDebounceTimeout = null; function saveConfigDebounced() { if (saveDebounceTimeout) { clearTimeout(saveDebounceTimeout); } saveDebounceTimeout = setTimeout(() => { saveConfig(); saveDebounceTimeout = null; }, 300); } const en = { loading: "Loading...", deleted: "[Deleted]", solved: "✅ Solved", archived: "🗄️ Archived", locked: "🔒 Locked", spoiler: "🚨 Spoiler", ratioTitle: "Upvote Ratio (Reddit estimate): ", commentsTitle: "Comments", dateTitle: "Post Date", editedTitle: "Edited at: ", nsfwTitle: "NSFW", rateLimitTitle: "Reddit API limit or network error", rateLimitText: "⚠️ Fetch Error", rateLimitWaitTitle: "Reddit rate-limit cool-down. Please retry in {seconds}s.", flairTitle: "Post Flair", videoTitle: "Contains Video", galleryTitle: "Contains Gallery", awardsTitle: "Awards Received", crosspostsTitle: "Crossposts", previewIcon: "📄", previewTitle: "Content", noContent: "No text content available.", topCommentsTitle: "Comments", commentsDisplayedTitle: "Displayed / Total comments", uniqueParticipantsTitle: "Number of unique participants in this discussion", topParticipantsLabel: "Top Participants:", stickiedComment: "Stickied comment", moderator: "Moderator", admin: "Reddit Admin", scorePoints: "pts", clickToPin: "Click to pin preview (enables scrolling and text selection)", closeBtn: "Close", embedYoutube: "Embed YouTube video inline", closeYoutube: "Close inline YouTube video", embedVimeo: "Embed Vimeo video inline", closeVimeo: "Close inline Vimeo video", embedX: "Embed X (Twitter) post inline", closeX: "Close inline X (Twitter) post", embedBluesky: "Embed Bluesky post inline", closeBluesky: "Close inline Bluesky post", embedReddit: "Embed Reddit post inline", closeReddit: "Close inline Reddit post", embedDailymotion: "Embed Dailymotion video inline", closeDailymotion: "Close inline Dailymotion video", embedImgur: "Embed Imgur gallery inline", closeImgur: "Close inline Imgur gallery", embedMastodon: "Embed Mastodon post inline", closeMastodon: "Close inline Mastodon post", imgurLoadFailed: "Failed to load Imgur gallery", maximizeBtn: "Maximize", restoreBtn: "Restore window", settingsTitle: "Reddit Preview Settings", lang: "Language", langAuto: "Auto", theme: "Theme", themeLight: "Light", themeDark: "Dark", commentSortTitle: "Comment Sort", sortBest: "Best", sortTop: "Top", sortNew: "New", sortControversial: "Controversial", sortOld: "Old", sortQA: "Q&A", showScore: "Show Score", showUpvoteRatio: "Show Upvote Ratio", showDate: "Show Date", showAwards: "Show Awards", showCrossposts: "Show Crossposts", showPostStatus: "Show Post Status Tags", authorColors: "Enable author color coding (track debaters)", authorColorsTooltip: "Assigns distinct HSL colors to participants with ≥ 2 replies to easily track conversation flow", embedMedia: "Embed Media in Preview (Image / Video / YouTube)", defaultVolume: "Default Video Volume", videoAutoplay: "Autoplay Video Previews on Hover (Muted)", videoAutoplaySound: "Enable Sound on Video Autoplay (Subject to Browser Policy)", previewFontSize: "Preview Text Size", timestampFormat: "Timestamp Format", timestampAbsolute: "Absolute", timestampAbsoluteLocal: "Absolute (Localized)", timestampRelative: "Relative", relTimeSeconds: "{n}s ago", relTimeMinutes: "{n}m ago", relTimeHours: "{n}h ago", relTimeDays: "{n}d ago", relTimeMonths: "{n}mo ago", relTimeYears: "{n}y ago", autoCollapseRedacted: "Auto-collapse comments deleted by Redact tool", autoCollapseDeleted: "Auto-collapse comments whose content has been deleted", autoExpandSingleImages: "Auto-expand single image links", autoExpandSingleImagesDesc: "Directly expand single Imgur/Reddit links to images in post bodies (disable to protect layout, enabling manual inline embeds instead)", showImageResolutionPreview: "Show Image Resolution (Preview)", galleryDisplayMode: "Gallery Display Mode", galleryCarousel: "Carousel (one image at a time)", galleryStack: "Vertical Stack (all images)", clickToMaximizeTitle: "Clicking a preview trigger badge directly opens the maximized window", filterOpUnavailable: "Unavailable: the post author's account has been deleted, so individual OP comments can't be identified", filterOpNoReplies: "No OP replies in this thread", statusRemovedMod: "🛡️ Removed by Mod", statusRemovedReddit: "🛡️ Removed by Reddit", statusPinned: "📌 Pinned", statusOC: "🎨 OC", statusDeleted: "🗑️ Deleted", commentRemovedByMod: "Comment removed by moderator", commentDeletedByAuthor: "Comment deleted by author", commentRedacted: "Comment batch deleted and anonymized by author via Redact", postDeletedByAuthor: "Post deleted by author", postRemovedByMod: "Post removed by moderator", postRedacted: "Post batch deleted and anonymized by author via Redact", authorDeleted: "[deleted account]", deadLinkTooltip: "This link points to a defunct service (e.g. Gfycat) and is likely dead", toggleThread: "Collapse / expand thread", repliesShort: "replies", collapsedHintTitle: "Self + direct replies / Self + every descendant", filterCommentsPlaceholder: "Filter comments…", filterOpOnly: "OP only", filterOpOnlyTitle: "Show only top-level threads that contain the post author's replies (full conversation context preserved)", filterScorePlaceholder: "> Score", filterScoreTitle: "Filter comments with score greater than or equal to this value", filterScorePlaceholderLte: "< Score", filterScoreTitleLte: "Filter comments with score less than or equal to this value", filterScoreOpTitle: "Toggle score filter operator (≥ / ≤)", filterModAdmin: "Mod/Admin only", filterModAdminTitle: "Show only top-level threads that contain comments authored by moderators or administrators", filterLinks: "Contains links", filterLinksTitle: "Show only top-level threads that contain comments with external links", filterMedia: "Contains media", filterMediaTitle: "Show only top-level threads that contain comments with attached images or videos", filterSettingsTitle: "Advanced Filter Options", filterClearAria: "Clear filter", filterPrevAria: "Previous match", filterNextAria: "Next match", noMatchingComments: "No matching comments", contestModeText: "🎲 Contest Mode", contestModeTitle: "Comments are sorted randomly for fair voting", hiddenScoreText: "🙈 Score Hidden", hiddenScoreTitle: "Score is hidden during the voting window", quarantinedText: "⚠️ Quarantined", quarantinedTitle: "Subreddit has been quarantined by Reddit", crosspostedFrom: "🔁 Crossposted from ", originalPost: "Original Post", resetDefaults: "Reset to Defaults", save: "Save & Reload", cancel: "Cancel", errHttpText: "⚠️ Server Error", errHttpTitle: "Reddit returned a non-200 response", errNetText: "⚠️ Network Error", errNetTitle: "Failed to reach Reddit", errParseText: "⚠️ Parse Error", errParseTitle: "Reddit returned unexpected data", errTimeoutText: "⏱️ Timeout", errTimeoutTitle: "Request timed out after 10 seconds", retryBtn: "Retry", retryTitle: "Click to retry once", lightboxLabel: "Image viewer", lightboxPrev: "Previous image", lightboxNext: "Next image", lightboxClose: "Close image viewer", copyCodeTitle: "Copy code", copiedCodeSuccess: "Copied!", moreRepliesCount: "{n} more replies…", moreRepliesUnknown: "More replies…", maxDepthReached: "Continue reading on Reddit", cacheTTL: "Cache Retention Time", cacheTTLNone: "No Cache (Always fetch)", cacheTTLPermanent: "Permanent Cache", minutes: "minutes", hours: "hours", days: "days", clearCacheBtn: "Clear Cache Data", clearCacheSuccess: "✓ Cleared {n} entries, freed {size} KB!", postsCached: "posts", previewModeTitle: "Preview Mode", previewModeTooltip: "Floating Tooltip (Tooltip)", previewModeSidePanel: "Right Side-Panel (Side-Panel)", expandRedditPost: "Expand full post", viewAllComments: "View all {count} comments on Reddit →", viewAllCommentsDefault: "View all comments on Reddit →", videoUnavailable: "⚠ Video unavailable", imageUnavailable: "⚠ Image unavailable", showMore: "Show more", showLess: "Show less", hudPlay: "Slideshow (Space / P)", hudDurationToggle: "Slideshow Speed", hudZoomIn: "Zoom In (+)", hudZoomOut: "Zoom Out (-)", hudZoomReset: "Reset Zoom (0)", hudRotate: "Rotate 90° (R)", hudDownload: "Download / Open Original (S / Enter)", hudFullscreen: "Toggle Fullscreen (F)", hudExitFullscreen: "Exit Fullscreen (F)", hudInfo: "Toggle Description Panel (I)", hudHelp: "Shortcut Cheatsheet (?)", shortcutsTitle: "Keyboard Shortcuts", shortcutNav: "Previous / Next Image", shortcutPlay: "Autoplay Play / Pause", shortcutZoom: "Zoom In / Out", shortcutReset: "Reset Zoom & Rotation", shortcutRotate: "Rotate image 90°", shortcutFullscreen: "Toggle Fullscreen", shortcutDownload: "Download / Open Original", shortcutInfo: "Toggle Description Panel", shortcutHelp: "Toggle Shortcuts Cheat Sheet", shortcutClose: "Exit Lightbox", translateSectionTitle: "🌐 Real-Time Translation & Browser AI", enableTranslate: "Enable real-time translation (Cloud API & Browser AI)", autoTranslate: "Auto-translate posts and comments on preview expand", translateSourceLang: "Source Language", translateTargetLang: "Target Language", translateBtnTitle: "Toggle Translation", translateTitleTranslated: "Toggle Translation (Translated)", translateTitleProgressive: "Toggle Translation (Progressive)", translateTranslating: "Translating...", translateErr: "⚠️ Translation Failed", bilingualTranslate: "Bilingual dual-language mode (Original + Translated)", translateEngine: "Translation Engine Service", translateEngineGoogle: "Google Cloud API (Fast & Stable)", translateEngineBrowser: "Browser Built-in AI (Privacy First)", translateEngineBrowserDisabled: "Browser Built-in AI (Not supported or Flag not enabled in current environment)", translateEngineTooltip: "Google Cloud API provides fast and stable multi-language translation; Browser Built-in AI uses internal neural network models to keep data strictly local", enableTranslateTooltip: "Enables the translate button in the preview header, allowing real-time translation with auto and bilingual options", autoTranslateTooltip: "Automatically translates content in the background when expanding post or comment previews", bilingualTranslateTooltip: "Displays original and translated text simultaneously in stacked rows for reading comparison", translateApiKey: "Google Translate API Key (Optional)", translateApiKeyTooltip: "Leave blank to use the default embedded API key", imgurClientId: "Imgur API Client ID (Optional)", imgurClientIdTooltip: "Custom Imgur Client ID. Leave blank to use the default embedded ID.", hoverDelay: "Hover Load Delay", hoverDelayTooltip: "Delay before loading preview content when hovering (prevents accidental queries while moving the mouse)", collapseToggleTitle: "Show top-level comments only (collapse replies)", collapseToggleActiveTitle: "Show all comments", copyMarkdownBtnTitle: "Copy as Markdown", copied: "Copied!", summarizeSectionTitle: "🤖 AI Summarization", enableSummarize: "Enable experimental AI summarization", enableSummarizeTooltip: "Allows summarizing Reddit posts and comments using built-in AI or Gemini API", summarizeType: "Summary Type", summarizeLength: "Summary Length", summarizeBtn: "AI Summary", summarizeBtnTitle: "Generate summary using AI", summarizing: "Generating summary...", summarizeErr: "⚠️ Summarization failed", summarizeUnavailable: "Built-in AI is not available in your browser.", summarizeReady: "AI summary ready", summarizeLabel: "AI Summary:", summarizeToggleTitle: "Collapse summary card", summarizeToggleActiveTitle: "Expand summary card", summarizeTypeKeyPoints: "Key Points (List)", summarizeTypeTldr: "TL;DR", summarizeTypeTeaser: "Teaser", summarizeTypeHeadline: "Headline", summarizeLengthShort: "Short", summarizeLengthMedium: "Medium", summarizeLengthLong: "Long", summarizeEngine: "Summarization Engine", summarizeEngineBrowser: "Browser Built-in AI (Gemini Nano)", summarizeEngineGemini: "Google Gemini API (Cloud)", summarizeSource: "Summary Content Source", summarizeSourcePostOnly: "Post Content Only", summarizeSourceAll: "Post Content & Comments", summarizeGeminiApiKey: "Gemini API Key", summarizeGeminiModel: "Gemini Model", summarizeGeminiCustomModel: "Custom Model Name", summarizeGeminiPrompt: "Custom System Prompt", summarizeGeminiPromptTooltip: "Custom prompt instructions for the Gemini model. Leave blank for default summary instructions.", testConnectionBtn: "Test Connection", testingConnection: "Testing...", testConnectionSuccess: "Success! API key is valid.", testConnectionErr: "Failed: ", defaultPromptPreview: "Active default prompt:", customPromptActive: "💡 Custom prompt active, default prompt is bypassed." }; const zhTW = { loading: "載入中...", deleted: "[已刪除]", solved: "✅ 已解決", archived: "🗄️ 封存", locked: "🔒 鎖定", spoiler: "🚨 劇透", ratioTitle: "推文比例(Reddit 估算):", commentsTitle: "留言數", dateTitle: "發文時間", editedTitle: "編輯於:", nsfwTitle: "18禁", rateLimitTitle: "Reddit 請求頻率過高或網路異常", rateLimitText: "⚠️ 讀取失敗", rateLimitWaitTitle: "Reddit 速率限制冷卻中,請於 {seconds} 秒後重試。", flairTitle: "貼文標籤", videoTitle: "包含影片", galleryTitle: "包含多張圖片", awardsTitle: "社群獎勵數", crosspostsTitle: "跨板分享次數", previewIcon: "📄", previewTitle: "內容", noContent: "此貼文無文字內容。", topCommentsTitle: "留言", commentsDisplayedTitle: "已顯示 / 留言總數", uniqueParticipantsTitle: "討論串實際獨立參與發言人數", topParticipantsLabel: "前十名熱門參與者:", stickiedComment: "置頂留言", moderator: "版主", admin: "Reddit 管理員", scorePoints: "推", clickToPin: "點擊固定預覽視窗(啟用捲動與內容選取)", closeBtn: "關閉", embedYoutube: "行內嵌入播放 YouTube 影片", closeYoutube: "關閉嵌入 YouTube 影片", embedVimeo: "行內嵌入播放 Vimeo 影片", closeVimeo: "關閉嵌入 Vimeo 影片", embedX: "行內嵌入 X (Twitter) 貼文", closeX: "關閉嵌入 X (Twitter) 貼文", embedBluesky: "行內嵌入 Bluesky 貼文", closeBluesky: "關閉嵌入 Bluesky 貼文", embedReddit: "行內嵌入 Reddit 貼文", closeReddit: "關閉嵌入 Reddit 貼文", embedDailymotion: "行內嵌入播放 Dailymotion 影片", closeDailymotion: "關閉嵌入 Dailymotion 影片", embedImgur: "行內嵌入 Imgur 圖集", closeImgur: "關閉嵌入 Imgur 圖集", embedMastodon: "行內嵌入 Mastodon 貼文", closeMastodon: "關閉嵌入 Mastodon 貼文", imgurLoadFailed: "載入 Imgur 圖集失敗", maximizeBtn: "最大化", restoreBtn: "還原視窗", settingsTitle: "Reddit 預覽設定", lang: "顯示語言", langAuto: "自動偵測", theme: "主題風格", themeLight: "淺色模式", themeDark: "深色模式", commentSortTitle: "留言排序方式", sortBest: "最佳", sortTop: "最高分", sortNew: "最新", sortControversial: "爭議", sortOld: "最舊", sortQA: "問答", showScore: "顯示推文分數", showUpvoteRatio: "顯示推文比例", showDate: "顯示發布日期", showAwards: "顯示社群獎勵數", showCrossposts: "顯示跨板分享數", showPostStatus: "顯示貼文狀態標籤", authorColors: "啟用作者名稱專屬顏色識別(追蹤對話交鋒)", authorColorsTooltip: "僅針對發言 ≥ 2 次的討論參與者分配高奢色彩,一眼追蹤對話脈絡", embedMedia: "在預覽中嵌入媒體(圖片 / 影片 / YouTube)", defaultVolume: "預設影片音量", videoAutoplay: "懸停時自動播放影片預覽(靜音)", videoAutoplaySound: "懸停自動播放時允許開啟聲音(依瀏覽器規範而定)", previewFontSize: "預覽文字大小", timestampFormat: "時間戳記格式", timestampAbsolute: "絕對時間", timestampAbsoluteLocal: "絕對時間(本地化)", timestampRelative: "相對時間", relTimeSeconds: "{n} 秒前", relTimeMinutes: "{n} 分前", relTimeHours: "{n} 小時前", relTimeDays: "{n} 天前", relTimeMonths: "{n} 個月前", relTimeYears: "{n} 年前", autoCollapseRedacted: "自動摺疊由 Redact 工具批次刪除的回應", autoCollapseDeleted: "自動摺疊內容已被刪除的回應", autoExpandSingleImages: "自動展開單張圖片連結", autoExpandSingleImagesDesc: "直接替換段落中單張 Imgur/Reddit 連結為圖片(關閉可防止破壞段落結構,改為行內嵌入展開)", showImageResolutionPreview: "顯示圖片解析度(預覽視窗)", galleryDisplayMode: "圖庫顯示模式", galleryCarousel: "輪播模式(一次顯示一張)", galleryStack: "垂直堆疊(顯示所有圖片)", clickToMaximizeTitle: "點擊預覽觸發徽章時,直接開啟最大化預覽視窗", filterOpUnavailable: "原 PO 已註銷帳號,無法分辨發文者回應,故停用此篩選", filterOpNoReplies: "此貼文的留言區中沒有原 PO 的回應", statusRemovedMod: "🛡️ 管理員已移除", statusRemovedReddit: "🛡️ Reddit 已移除", statusPinned: "📌 置頂", statusOC: "🎨 原創", statusDeleted: "🗑️ 作者已刪除", commentRemovedByMod: "留言已遭版主移除", commentDeletedByAuthor: "留言已被作者刪除", commentRedacted: "留言已被作者透過 Redact 批次刪除與匿名化", postDeletedByAuthor: "貼文已被作者刪除", postRemovedByMod: "貼文已遭版主移除", postRedacted: "貼文已被作者透過 Redact 批次刪除與匿名化", authorDeleted: "[已註銷帳號]", deadLinkTooltip: "此連結指向已關閉的服務(如 Gfycat),可能已失效", toggleThread: "摺疊 / 展開支線", repliesShort: "則回應", collapsedHintTitle: "自身 + 直接子留言 / 自身 + 所有子代", filterCommentsPlaceholder: "篩選留言…", filterOpOnly: "只看原 PO", filterOpOnlyTitle: "僅顯示包含原 PO 回覆的完整回應串(保留完整對話脈絡)", filterScorePlaceholder: "> 分數", filterScoreTitle: "篩選分數大於或等於此數值的留言", filterScorePlaceholderLte: "< 分數", filterScoreTitleLte: "篩選分數小於或等於此數值的留言", filterScoreOpTitle: "切換分數篩選運算子(≥ / ≤)", filterModAdmin: "版主/管理員", filterModAdminTitle: "僅顯示板主 (MOD) 或 Reddit 官方管理員 (ADMIN) 的留言", filterLinks: "包含外部連結", filterLinksTitle: "僅顯示內文附帶外部網站連結的留言", filterMedia: "包含多媒體", filterMediaTitle: "僅顯示附帶圖片、GIF 或是影片預覽的留言", filterSettingsTitle: "過濾器進階設定", filterClearAria: "清除篩選", filterPrevAria: "上一個結果", filterNextAria: "下一個結果", noMatchingComments: "沒有符合的留言", contestModeText: "🎲 競賽模式", contestModeTitle: "留言隨機排序以維持公平投票", hiddenScoreText: "🙈 分數隱藏", hiddenScoreTitle: "投票期間內,貼文分數暫時隱藏", quarantinedText: "⚠️ 已隔離", quarantinedTitle: "此 Subreddit 已被 Reddit 隔離", crosspostedFrom: "🔁 轉自 ", originalPost: "原始文章", resetDefaults: "恢復預設值", save: "儲存並重新載入", cancel: "取消", errHttpText: "⚠️ 伺服器錯誤", errHttpTitle: "Reddit 回傳非 200 狀態", errNetText: "⚠️ 網路錯誤", errNetTitle: "無法連線至 Reddit", errParseText: "⚠️ 解析錯誤", errParseTitle: "Reddit 回傳了非預期的資料", errTimeoutText: "⏱️ 逾時", errTimeoutTitle: "請求超過 10 秒未回應", retryBtn: "重試", retryTitle: "點擊重新嘗試 (限一次)", lightboxLabel: "圖片檢視器", lightboxPrev: "上一張", lightboxNext: "下一張", lightboxClose: "關閉圖片檢視器", copyCodeTitle: "複製程式碼", copiedCodeSuccess: "已複製!", moreRepliesCount: "還有 {n} 則回覆…", moreRepliesUnknown: "更多回覆…", maxDepthReached: "於 Reddit 繼續閱讀", cacheTTL: "快取保留時間", cacheTTLNone: "不使用快取 (每次重新抓取)", cacheTTLPermanent: "永久快取", minutes: "分鐘", hours: "小時", days: "天", clearCacheBtn: "清除快取資料", clearCacheSuccess: "✓ 已清除 {n} 個項目,釋放 {size} KB!", postsCached: "篇貼文", previewModeTitle: "預覽顯示模式", previewModeTooltip: "下方懸浮視窗 (Tooltip)", previewModeSidePanel: "右側預覽面板 (Side-Panel)", expandRedditPost: "展開全文", viewAllComments: "去 Reddit 瀏覽全部 {count} 則留言 →", viewAllCommentsDefault: "去 Reddit 瀏覽全部留言 →", videoUnavailable: "⚠ 影片無法載入", imageUnavailable: "⚠ 圖片無法載入", showMore: "顯示更多", showLess: "顯示更少", hudPlay: "播放幻燈片 (Space / P)", hudDurationToggle: "播放速度", hudZoomIn: "放大 (+)", hudZoomOut: "縮小 (-)", hudZoomReset: "重設縮放 (0)", hudRotate: "旋轉 90° (R)", hudDownload: "下載 / 開啟原圖 (S / Enter)", hudFullscreen: "切換全螢幕 (F)", hudExitFullscreen: "退出全螢幕 (F)", hudInfo: "顯示/隱藏說明面板 (I)", hudHelp: "快速鍵說明 (?)", shortcutsTitle: "鍵盤快速鍵說明", shortcutNav: "上一張 / 下一張圖片", shortcutPlay: "自動播放 播放 / 暫停", shortcutZoom: "放大 / 縮小", shortcutReset: "重設縮放與旋轉", shortcutRotate: "旋轉圖片 90°", shortcutFullscreen: "切換全螢幕", shortcutDownload: "下載 / 開啟原圖", shortcutInfo: "顯示/隱藏說明面板", shortcutHelp: "顯示/隱藏快速鍵說明", shortcutClose: "關閉圖片檢視器", translateSectionTitle: "🌐 即時翻譯與本機 AI 引擎", enableTranslate: "啟用即時翻譯功能(支援雲端 API 與本機離線 AI)", autoTranslate: "展開預覽時自動執行翻譯", translateSourceLang: "原始語言", translateTargetLang: "目標語言", translateBtnTitle: "切換翻譯", translateTitleTranslated: "切換翻譯 (已翻譯)", translateTitleProgressive: "切換翻譯 (陸續翻譯中)", translateTranslating: "翻譯中...", translateErr: "⚠️ 翻譯失敗", bilingualTranslate: "雙語對照模式 (原文+譯文)", translateEngine: "翻譯引擎服務", translateEngineGoogle: "Google 雲端 API 直連(極速穩定)", translateEngineBrowser: "瀏覽器本機離線 AI(隱私優先)", translateEngineBrowserDisabled: "瀏覽器本機離線 AI(目前環境未支援或未開啟 Flag)", translateEngineTooltip: "雲端 API 直連提供快速穩定的多國語言翻譯;本機離線 AI 使用瀏覽器內建神經網路模型,確保資料留在本機不外流", enableTranslateTooltip: "啟用後可在預覽視窗頂端點擊 [文] 徽章,或設定自動/雙語模式進行即時翻譯", autoTranslateTooltip: "展開貼文或留言預覽時自動執行翻譯,省去手動點擊徽章的步驟", bilingualTranslateTooltip: "同時呈現原文與譯文的上下列對照,適合比對原文與翻譯結果", translateApiKey: "Google 翻譯 API 金鑰(選填)", translateApiKeyTooltip: "留空則使用預設內嵌金鑰", imgurClientId: "Imgur API 客戶端識別碼 (可選)", imgurClientIdTooltip: "自訂 Imgur Client ID。留空則會使用預設的公共 ID。", hoverDelay: "懸停載入延遲", hoverDelayTooltip: "滑鼠懸停在卡片上時,載入預覽內容的延遲時間(防範滑鼠滑過時誤觸網路請求)", collapseToggleTitle: "僅顯示主要留言(摺疊所有子回覆)", collapseToggleActiveTitle: "顯示所有留言", copyMarkdownBtnTitle: "複製為 Markdown 格式", copied: "已複製!", summarizeSectionTitle: "🤖 AI 自動總結", enableSummarize: "啟用實驗性 AI 貼文總結功能", enableSummarizeTooltip: "允許使用瀏覽器本機內建 AI 模型(Summarizer API)或是 Google Gemini API 對貼文及留言進行自動總結", summarizeType: "總結類型方式", summarizeLength: "總結長度長短", summarizeBtn: "AI 總結", summarizeBtnTitle: "使用 AI 對此內容生成摘要", summarizing: "正在生成總結摘要...", summarizeErr: "⚠️ 生成總結失敗", summarizeUnavailable: "您的瀏覽器環境不支援或未開啟內建 AI 總結功能。", summarizeReady: "AI 總結已完成", summarizeLabel: "AI 貼文總結:", summarizeToggleTitle: "摺疊總結卡片", summarizeToggleActiveTitle: "展開總結卡片", summarizeTypeKeyPoints: "重要觀點 (清單)", summarizeTypeTldr: "TL;DR", summarizeTypeTeaser: "精采摘要 (引人入勝)", summarizeTypeHeadline: "單行標題 (主旨)", summarizeLengthShort: "短", summarizeLengthMedium: "中", summarizeLengthLong: "長", summarizeEngine: "總結引擎服務", summarizeEngineBrowser: "瀏覽器本機內建 AI (Gemini Nano)", summarizeEngineGemini: "Google Gemini API (雲端服務)", summarizeSource: "摘要範圍內容", summarizeSourcePostOnly: "僅限貼文主要內容", summarizeSourceAll: "貼文內容與全部留言", summarizeGeminiApiKey: "Gemini API 金鑰", summarizeGeminiModel: "Gemini 模型", summarizeGeminiCustomModel: "自訂模型名稱", summarizeGeminiPrompt: "自訂總結提示詞", summarizeGeminiPromptTooltip: "輸入給 Gemini 模型的系統提示詞,留空將使用系統預設總結提示詞。", testConnectionBtn: "測試連線", testingConnection: "正在測試...", testConnectionSuccess: "測試成功!API 金鑰有效。", testConnectionErr: "測試失敗:", defaultPromptPreview: "目前生效的預設提示詞:", customPromptActive: "💡 已啟用自訂提示詞,預設提示詞將失效。" }; const ja = { loading: "読み込み中...", deleted: "[削除済み]", solved: "✅ 解決済み", archived: "🗄️ アーカイブ", locked: "🔒 ロック", spoiler: "🚨 ネタバレ", ratioTitle: "高評価率(Reddit 推定値):", commentsTitle: "コメント数", dateTitle: "投稿日時", editedTitle: "編集済み:", nsfwTitle: "18禁", rateLimitTitle: "Redditの制限超過または通信エラー", rateLimitText: "⚠️ 取得エラー", rateLimitWaitTitle: "レート制限中、{seconds}秒後に再試行してください。", flairTitle: "投稿タグ", videoTitle: "動画あり", galleryTitle: "ギャラリーあり", awardsTitle: "アワード獲得数", crosspostsTitle: "クロスポスト数", previewIcon: "📄", previewTitle: "内容", noContent: "テキスト内容はありません。", topCommentsTitle: "コメント", commentsDisplayedTitle: "表示中 / コメント総数", uniqueParticipantsTitle: "この議論の実際の参加者数", topParticipantsLabel: "トップ参加者:", stickiedComment: "ピン留めコメント", moderator: "モデレーター", admin: "Reddit 管理者", scorePoints: "pt", clickToPin: "クリックしてプレビューを固定(スクロールやテキスト選択が可能になります)", closeBtn: "閉じる", embedYoutube: "YouTube動画をインラインで埋め込む", closeYoutube: "インラインYouTube動画を閉じる", embedVimeo: "Vimeo動画をインラインで埋め込む", closeVimeo: "インラインVimeo動画を閉じる", embedX: "X (Twitter) 投稿をインラインで埋め込む", closeX: "インライン X (Twitter) 投稿を閉じる", embedBluesky: "Bluesky 投稿をインラインで埋め込む", closeBluesky: "インライン Bluesky 投稿を閉じる", embedReddit: "Reddit 投稿をインラインで埋め込む", closeReddit: "インライン Reddit 投稿を閉じる", embedDailymotion: "Dailymotion動画をインラインで埋め込む", closeDailymotion: "インライン Dailymotion動画を閉じる", embedImgur: "Imgur ギャラリーをインラインで埋め込む", closeImgur: "インライン Imgur ギャラリーを閉じる", embedMastodon: "Mastodon 投稿をインラインで埋め込む", closeMastodon: "インライン Mastodon 投稿を閉じる", imgurLoadFailed: "Imgur ギャラリーの読み込みに失敗しました", maximizeBtn: "最大化", restoreBtn: "元に戻す", settingsTitle: "Reddit プレビュー設定", lang: "言語", langAuto: "自動", theme: "テーマ", themeLight: "ライト", themeDark: "ダーク", commentSortTitle: "コメントの並べ替え", sortBest: "ベスト", sortTop: "トップ", sortNew: "新着", sortControversial: "論争中", sortOld: "古い順", sortQA: "Q&A", showScore: "スコアを表示", showUpvoteRatio: "高評価率を表示", showDate: "投稿日時を表示", showAwards: "アワードを表示", showCrossposts: "クロスポスト数を表示", showPostStatus: "投稿ステータスを表示", authorColors: "投稿者ごとの色分け識別を有効にする(議論を追跡)", authorColorsTooltip: "2回以上発言した参加者に個別のHSLカラーを割り当て、会話の流れを瞬時に把握します", embedMedia: "プレビューにメディアを埋め込む(画像 / 動画 / YouTube)", defaultVolume: "既定の動画音量", videoAutoplay: "ホバー時に動画プレビューを自動再生(ミュート)", videoAutoplaySound: "動画の自動再生時に音声を有効化(ブラウザポリシーに依存)", previewFontSize: "プレビュー文字サイズ", timestampFormat: "タイムスタンプ形式", timestampAbsolute: "絶対時間", timestampAbsoluteLocal: "絶対時間(ローカル)", timestampRelative: "相対時間", relTimeSeconds: "{n}秒前", relTimeMinutes: "{n}分前", relTimeHours: "{n}時間前", relTimeDays: "{n}日前", relTimeMonths: "{n}ヶ月前", relTimeYears: "{n}年前", autoCollapseRedacted: "Redact ツールで一括削除されたコメントを自動折りたたみ", autoCollapseDeleted: "内容が削除されたコメントを自動折りたたみ", autoExpandSingleImages: "単一の画像リンクを自動展開", autoExpandSingleImagesDesc: "本文内の単一の Imgur/Reddit リンクを画像に直接置き換える(無効にすると段落構造の破壊を防ぎ、インラインでの展開に変更されます)", showImageResolutionPreview: "画像の解像度を表示(プレビュー)", galleryDisplayMode: "ギャラリー表示モード", galleryCarousel: "カルーセル(1枚ずつ表示)", galleryStack: "縦並び(すべての画像)", clickToMaximizeTitle: "プレビューバッジをクリックした際、直接最大化プレビューを表示する", filterOpUnavailable: "投稿者がアカウントを削除しているため、OP のコメントを識別できず、このフィルターは利用できません", filterOpNoReplies: "このスレッドには投稿者(OP)の返信はありません", statusRemovedMod: "🛡️ 管理者により削除", statusRemovedReddit: "🛡️ Redditにより削除", statusPinned: "📌 ピン留め", statusOC: "🎨 オリジナル", statusDeleted: "🗑️ 削除済み", commentRemovedByMod: "コメントはモデレーターにより削除されました", commentDeletedByAuthor: "コメントは投稿者により削除されました", commentRedacted: "投稿は Redact を通じて作成者によって一括削除および匿名化されました", postDeletedByAuthor: "投稿は投稿者により削除されました", postRemovedByMod: "投稿はモデレーターにより削除されました", postRedacted: "投稿は Redact を通じて作成者によって一括削除および匿名化されました", authorDeleted: "[削除済みアカウント]", deadLinkTooltip: "このリンクはサービスを終了したサイト(Gfycatなど)を指しているため、現在アクセスできません", toggleThread: "スレッドを折りたたむ / 展開する", repliesShort: "件の返信", collapsedHintTitle: "スレッドの開閉 / 子孫コメントの開閉", filterCommentsPlaceholder: "コメントを絞り込む…", filterOpOnly: "OP のみ", filterOpOnlyTitle: "投稿者本人の返信を含むトップレベルのスレッドのみ表示(会話の流れを保持)", filterScorePlaceholder: "> スコア", filterScoreTitle: "指定したスコア以上のコメントをフィルターします", filterScorePlaceholderLte: "< スコア", filterScoreTitleLte: "指定したスコア以下のコメントをフィルターします", filterScoreOpTitle: "スコアフィルター演算子の切り替え(≥ / ≤)", filterModAdmin: "モデレータ/管理者のみ", filterModAdminTitle: "モデレーター (MOD) または Reddit 公式管理者 (ADMIN) のコメントを含むトップレベルのスレッドのみ表示", filterLinks: "外部リンクを含む", filterLinksTitle: "外部サイトへのリンクを含むコメントのみ表示", filterMedia: "メディアを含む", filterMediaTitle: "画像や動画が添付されているコメントのみ表示", filterSettingsTitle: "フィルター詳細設定", filterClearAria: "フィルターをクリア", filterPrevAria: "前の一致", filterNextAria: "次の一致", noMatchingComments: "一致するコメントなし", contestModeText: "🎲 コンテストモード", contestModeTitle: "公平な投票のためコメントはランダム順に並び替えられます", hiddenScoreText: "🙈 スコア非表示", hiddenScoreTitle: "投票期間中はスコアが非表示になります", quarantinedText: "⚠️ 隔離中", quarantinedTitle: "このサブレディットはRedditにより隔離されています", crosspostedFrom: "🔁 転載元 ", originalPost: "元の投稿", resetDefaults: "デフォルトに戻す", save: "保存して再読み込み", cancel: "キャンセル", errHttpText: "⚠️ サーバーエラー", errHttpTitle: "Redditが200以外の応答を返しました", errNetText: "⚠️ ネットワークエラー", errNetTitle: "Redditに接続できません", errParseText: "⚠️ 解析エラー", errParseTitle: "Redditが予期しないデータを返しました", errTimeoutText: "⏱️ タイムアウト", errTimeoutTitle: "10秒以内に応答がありませんでした", retryBtn: "再試行", retryTitle: "クリックして再試行 (1回のみ)", lightboxLabel: "画像ビューア", lightboxPrev: "前の画像", lightboxNext: "次の画像", lightboxClose: "画像ビューアを閉じる", copyCodeTitle: "コードをコピー", copiedCodeSuccess: "コピーしました!", moreRepliesCount: "他 {n} 件の返信…", moreRepliesUnknown: "さらに返信…", maxDepthReached: "Reddit で続きを読む", cacheTTL: "キャッシュ保存時間", cacheTTLNone: "キャッシュを使用しない (常に再取得)", cacheTTLPermanent: "永続キャッシュ", minutes: "分", hours: "時間", days: "日", clearCacheBtn: "キャッシュを消去", clearCacheSuccess: "✓ {n} 個のキャッシュを消去し、{size} KB を解放しました!", postsCached: "件の投稿", previewModeTitle: "プレビュー表示モード", previewModeTooltip: "吹き出しポップアップ (Tooltip)", previewModeSidePanel: "右側プレビューパネル (Side-Panel)", expandRedditPost: "投稿をすべて展開", viewAllComments: "Reddit で {count} 件のコメントをすべて見る →", viewAllCommentsDefault: "Reddit でコメントをすべて見る →", videoUnavailable: "⚠ 動画再生不可", imageUnavailable: "⚠ 画像読み込み不可", showMore: "もっと見る", showLess: "閉じる", hudPlay: "スライドショー (Space / P)", hudDurationToggle: "再生速度", hudZoomIn: "拡大 (+)", hudZoomOut: "縮小 (-)", hudZoomReset: "ズームリセット (0)", hudRotate: "90度回転 (R)", hudDownload: "ダウンロード / 原寸表示 (S / Enter)", hudFullscreen: "全画面表示切り替え (F)", hudExitFullscreen: "全画面表示解除 (F)", hudInfo: "説明パネルを表示/非表示 (I)", hudHelp: "ショートカット一覧 (?)", shortcutsTitle: "キーボードショートカット", shortcutNav: "前 / 次の画像", shortcutPlay: "自動再生 再生 / 一時停止", shortcutZoom: "拡大 / 縮小", shortcutReset: "ズーム・回転をリセット", shortcutRotate: "画像を90度回転", shortcutFullscreen: "全画面表示切り替え", shortcutDownload: "ダウンロード / 原寸表示", shortcutInfo: "説明パネルを表示/非表示", shortcutHelp: "ショートカットヘルプを表示/非表示", shortcutClose: "画像ビューアを閉じる", translateSectionTitle: "🌐 リアルタイム翻訳 & ブラウザ AI", enableTranslate: "リアルタイム翻訳機能を有効にする(クラウド API とブラウザ AI をサポート)", autoTranslate: "プレビュー展開時に自動で翻訳を実行する", translateSourceLang: "翻訳元の言語", translateTargetLang: "翻訳先の言語", translateBtnTitle: "翻譯を切り替え", translateTitleTranslated: "翻譯を切り替え (翻訳済み)", translateTitleProgressive: "翻譯を切り替え (順次翻訳中)", translateTranslating: "翻訳中...", translateErr: "⚠️ 翻訳エラー", bilingualTranslate: "2言語対照モード (原文 + 翻訳文)", translateEngine: "翻訳エンジンサービス", translateEngineGoogle: "Google クラウド API(高速・安定)", translateEngineBrowser: "ブラウザ内蔵 AI(プライバシー優先)", translateEngineBrowserDisabled: "ブラウザ内蔵 AI(現在の環境では未対応またはフラグ無効)", translateEngineTooltip: "クラウド API は高速かつ安定した多言語翻訳を提供します。ブラウザ内蔵 AI はローカルの AI モデルを使用し、データを外部へ送信しません", enableTranslateTooltip: "プレビュー上部の翻訳ボタンを有効にし、リアルタイム翻訳や自動・2言語表示機能を提供します", autoTranslateTooltip: "投稿やコメントのプレビューを展開する際、自動的に翻訳を実行します", bilingualTranslateTooltip: "原文と訳文を上下に並べて表示し、内容の比較に役立てます", translateApiKey: "Google 翻訳 API キー (オプション)", translateApiKeyTooltip: "空白のままにすると、デフォルトの組み込み API キーを使用します", imgurClientId: "Imgur API クライアントID (任意)", imgurClientIdTooltip: "カスタム Imgur Client ID。空白のままにすると、デフォルトのパブリックIDが使用されます。", hoverDelay: "ホバー読み込み遅延", hoverDelayTooltip: "マウスをカード上に置いた後、プレビュー内容を読み込むまでの遅延時間(マウス移動時の意図しないAPIリクエストを防ぎます)", collapseToggleTitle: "トップレベルのコメントのみ表示(返信を折りたたむ)", collapseToggleActiveTitle: "すべてのコメントを表示", copyMarkdownBtnTitle: "Markdown 形式でコピー", copied: "コピーしました!", summarizeSectionTitle: "🤖 AI 自動要約", enableSummarize: "試験的な AI 要約機能を有効にする", enableSummarizeTooltip: "ブラウザの内蔵 AI モデル(Summarizer API)または Google Gemini API を使用して、投稿とコメントの要約を生成します", summarizeType: "要約のタイプ", summarizeLength: "要約の長さ", summarizeBtn: "AI 要約", summarizeBtnTitle: "AI を使用して要約を生成します", summarizing: "要約を生成中...", summarizeErr: "⚠️ 要約の生成に失敗しました", summarizeUnavailable: "お使いのブラウザは内蔵 AI 要約機能をサポートしていないか、有効になっていません。", summarizeReady: "AI 要約が完了しました", summarizeLabel: "AI 要約:", summarizeToggleTitle: "要約カードを折りたたむ", summarizeToggleActiveTitle: "要約カードを展開する", summarizeTypeKeyPoints: "重要ポイント (箇条書き)", summarizeTypeTldr: "TL;DR", summarizeTypeTeaser: "ティーザー (要約)", summarizeTypeHeadline: "見出し (ヘッドライン)", summarizeLengthShort: "短い", summarizeLengthMedium: "普通", summarizeLengthLong: "長い", summarizeEngine: "要約エンジンサービス", summarizeEngineBrowser: "ブラウザ内蔵 AI (Gemini Nano)", summarizeEngineGemini: "Google Gemini API (クラウド)", summarizeSource: "要約の対象範囲", summarizeSourcePostOnly: "投稿本文のみ", summarizeSourceAll: "投稿本文とすべてのコメント", summarizeGeminiApiKey: "Gemini API キー", summarizeGeminiModel: "Gemini モデル", summarizeGeminiCustomModel: "カスタムモデル名", summarizeGeminiPrompt: "カスタム要約プロンプト", summarizeGeminiPromptTooltip: "Gemini モデルに渡すシステムプロンプト。空白のままにすると、既定の要約プロンプトが使用されます。", testConnectionBtn: "接続テスト", testingConnection: "テスト中...", testConnectionSuccess: "成功!API キーは有効です。", testConnectionErr: "失敗:", defaultPromptPreview: "現在有効なデフォルトプロンプト:", customPromptActive: "💡 カスタムプロンプトが有効なため、デフォルトは無視されます。" }; const dictionaries = { en, "zh-TW": zhTW, ja }; function getLocalizedMessages() { const lang = (Config.lang === "auto" ? navigator.language : Config.lang).toLowerCase(); if (lang.includes("zh")) return dictionaries["zh-TW"]; if (lang.includes("ja")) return dictionaries["ja"]; return dictionaries["en"]; } const L = new Proxy({}, { get(_, prop) { const dict = getLocalizedMessages(); return dict[prop]; } }); const coreCss = "/* ==========================================================================\n Google Search Reddit Preview - Modern Nested CSS & Variables System (M3)\n ========================================================================== */\n\n/* --- Keyframes & Animations (Achromatic GPU-composited and hardware-accelerated) --- */\n@keyframes gsrp-fade {\n 0% {\n opacity: 0.5;\n }\n 50% {\n opacity: 1;\n }\n 100% {\n opacity: 0.5;\n }\n}\n\n@keyframes gsrp-shimmer {\n 0% {\n background-position: 200% 0;\n }\n 100% {\n background-position: -200% 0;\n }\n}\n\n@keyframes gsrp-pulse {\n 0% {\n opacity: 0.55;\n }\n 50% {\n opacity: 1;\n background: rgba(128, 128, 128, 0.25);\n }\n 100% {\n opacity: 0.55;\n }\n}\n\n@keyframes gsrp-modal-zoom {\n from {\n transform: scale(0.96);\n opacity: 0;\n }\n to {\n transform: scale(1);\n opacity: 1;\n }\n}\n\n@keyframes gsrp-overlay-fade {\n from {\n opacity: 0;\n backdrop-filter: blur(0px);\n -webkit-backdrop-filter: blur(0px);\n }\n to {\n opacity: 1;\n backdrop-filter: blur(12px);\n -webkit-backdrop-filter: blur(12px);\n }\n}\n\n@keyframes gsrp-img-zoom {\n from {\n transform: scale(0.95);\n opacity: 0;\n }\n to {\n transform: scale(1);\n opacity: 1;\n }\n}\n\n@keyframes gsrp-youtube-slide {\n from {\n opacity: 0;\n transform: translateY(-4px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n@keyframes gsrp-spin {\n to {\n transform: rotate(360deg);\n }\n}\n\n@keyframes gsrp-copied-pop {\n 0% {\n transform: scale(1);\n }\n 50% {\n transform: scale(1.25);\n }\n 100% {\n transform: scale(1);\n }\n}\n\n@keyframes gsrp-maximize-zoom {\n from {\n opacity: 0;\n transform: translate(-50%, -47%) scale(0.95) !important;\n }\n to {\n opacity: 1;\n transform: translate(-50%, -50%) scale(1) !important;\n }\n}\n\n/* --- Scoped Custom Properties & Design Tokens ---\n Note: In SERP environments, BEM namespaces (.gsrp-) combined with native nesting\n achieve robust layout isolation. As modern browsers (Chrome 118+, Firefox 128+) support @scope,\n the design system is fully architected to support future `@scope (.gsrp-reddit-badge)` containment.\n*/\n.gsrp-reddit-badge {\n /* Brand & Accent Design Tokens */\n --gsrp-primary: #1a73e8; /* Google Blue (Light) */\n --gsrp-primary-hover: color-mix(in srgb, var(--gsrp-primary) 10%, transparent);\n --gsrp-danger: #ea4335; /* Red for NSFW/Removed/Errors */\n --gsrp-danger-hover: color-mix(in srgb, var(--gsrp-danger) 12%, transparent);\n --gsrp-success: #34a853; /* Green for Solved/Stickied */\n --gsrp-warning: #b58200; /* Dark Amber (WCAG AA Compliant 4.5:1 against White) */\n --gsrp-award: #fbbc04; /* Gold for awards */\n --gsrp-oc: #1a73e8; /* Blue for OC status */\n\n /* Typography Color Palette */\n --gsrp-text-main: #3c4043;\n --gsrp-text-sub: #5f6368;\n --gsrp-text-link: #1a73e8;\n --gsrp-text-code: #c7254e;\n\n /* Backgrounds & Borders */\n --gsrp-bg-tooltip: #ffffff;\n --gsrp-bg-hover: color-mix(in srgb, var(--gsrp-primary) 8%, transparent);\n --gsrp-bg-code: rgba(0, 0, 0, 0.05);\n --gsrp-bg-pre: #1e1e24;\n --gsrp-border: #dadce0;\n --gsrp-border-pre: rgba(128, 128, 128, 0.2);\n --gsrp-shadow: rgba(0, 0, 0, 0.2);\n --gsrp-shadow-pinned: 0 12px 36px rgba(0, 0, 0, 0.16), 0 4px 12px rgba(0, 0, 0, 0.08);\n\n /* Animation Timing & Easings */\n --gsrp-transition-ease: cubic-bezier(0.34, 1.56, 0.64, 1);\n --gsrp-transition-fast: 0.15s ease;\n\n /* Core Layout & Positioning Rules */\n position: absolute;\n top: 100%;\n left: 0;\n margin-top: 2px; /* 2 px gap from the SERP card snippet line. Refined to prevent overlay overlaps. */\n display: inline-flex;\n align-items: center;\n gap: 4px;\n padding: 0;\n background: transparent;\n font-size: 12px;\n color: var(--gsrp-text-sub);\n white-space: nowrap;\n font-family:\n Google Sans,\n Arial,\n sans-serif;\n z-index: 10;\n user-select: none;\n\n /* Theme Engine - Dark Mode Variable Mappings */\n &.gsrp-is-dark {\n --gsrp-primary: #8ab4f8; /* Vibrant Blue (Dark) */\n --gsrp-primary-hover: color-mix(in srgb, var(--gsrp-primary) 14%, transparent);\n --gsrp-danger: #f28b82; /* Soft Red */\n --gsrp-danger-hover: color-mix(in srgb, var(--gsrp-danger) 18%, transparent);\n --gsrp-success: #81c995; /* Soft Green */\n --gsrp-warning: #fdd663; /* Soft Yellow */\n --gsrp-oc: #8ab4f8;\n\n --gsrp-text-main: #e8eaed;\n --gsrp-text-sub: #9aa0a6;\n --gsrp-text-link: #8ab4f8;\n --gsrp-text-code: #f28b82;\n\n --gsrp-bg-tooltip: #22242a;\n --gsrp-bg-hover: color-mix(in srgb, var(--gsrp-primary) 12%, transparent);\n --gsrp-bg-code: rgba(255, 255, 255, 0.08);\n --gsrp-bg-pre: #1b1c21;\n --gsrp-border: #3c4043;\n --gsrp-border-pre: rgba(154, 160, 166, 0.3);\n --gsrp-shadow: rgba(0, 0, 0, 0.6);\n --gsrp-shadow-pinned: 0 12px 36px rgba(0, 0, 0, 0.48), 0 4px 12px rgba(0, 0, 0, 0.24);\n\n color: var(--gsrp-text-sub);\n }\n\n /* Lift active tooltip above adjacent elements to avoid clip-offs. */\n &:has(.gsrp-preview-trigger:hover),\n &:has(.gsrp-preview-trigger.gsrp-hover-sticky) {\n z-index: 99999;\n }\n\n &:has(.gsrp-preview-trigger.gsrp-pinned) {\n z-index: 100000;\n }\n\n /* Subdued inline metadata separators */\n .gsrp-reddit-sep {\n opacity: 0.6;\n margin: 0 2px;\n }\n\n /* Shimmering loader text rules */\n .gsrp-spinner {\n display: inline-block;\n vertical-align: middle;\n flex: 0 0 auto;\n animation: gsrp-spin 0.8s infinite linear;\n will-change: transform;\n }\n\n .gsrp-loading-text {\n animation: gsrp-fade 1.5s infinite;\n pointer-events: none;\n }\n\n /* Error and dynamic retry interfaces */\n .gsrp-error-msg {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n color: var(--gsrp-danger);\n\n .gsrp-retry-btn {\n background: transparent;\n border: 1px solid currentColor;\n color: inherit;\n font-size: 11px;\n padding: 1px 6px;\n border-radius: 3px;\n cursor: pointer;\n font-family: inherit;\n line-height: 1.2;\n transition: background var(--gsrp-transition-fast);\n\n &:hover {\n background: var(--gsrp-danger-hover);\n }\n }\n }\n\n /* --- Core Interactions & Preview Trigger Block --- */\n .gsrp-preview-trigger {\n cursor: pointer;\n pointer-events: auto;\n position: relative;\n display: inline-flex;\n align-items: center;\n\n &.gsrp-pinned {\n background-color: var(--gsrp-bg-hover);\n border-radius: 4px;\n\n /* Override border on pinned tooltips with elegant, subtle styles to avoid visual distraction */\n .gsrp-preview-tooltip {\n border: 1px solid var(--gsrp-border) !important;\n box-shadow: var(--gsrp-shadow-pinned) !important;\n }\n }\n\n &:hover .gsrp-preview-tooltip,\n &.gsrp-pinned .gsrp-preview-tooltip,\n &.gsrp-hover-sticky .gsrp-preview-tooltip {\n display: block;\n opacity: 1;\n transform: translateY(0) scale(1);\n }\n\n /* Hover Zoom effects for thumbnails & inline media inside active trigger */\n &:hover,\n &.gsrp-pinned,\n &.gsrp-hover-sticky {\n .gsrp-gallery-item img,\n .gsrp-inline-media-img,\n .gsrp-preview-media img {\n transform: scale(1.02);\n box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);\n }\n }\n\n @starting-style {\n &:hover .gsrp-preview-tooltip,\n &.gsrp-pinned .gsrp-preview-tooltip,\n &.gsrp-hover-sticky .gsrp-preview-tooltip {\n opacity: 0;\n transform: translateY(8px) scale(0.98);\n }\n }\n\n /* --- Inline Controls Visibility on Hover/Active states --- */\n &:hover,\n &.gsrp-pinned,\n &.gsrp-hover-sticky {\n .gsrp-close-btn,\n .gsrp-maximize-btn {\n display: inline-block;\n }\n }\n }\n\n /* --- Advanced Preview Tooltip Framework --- */\n .gsrp-preview-tooltip {\n position: absolute;\n top: 100%;\n margin-top: 6px;\n left: 0;\n /* Aligns with the SERP card width dynamically, boundaries bounded between 360px and viewport size minus spacing */\n width: min(calc(100vw - 16px), max(var(--gsrp-card-width, 600px), 360px));\n max-width: calc(100vw - 16px);\n background: var(--gsrp-bg-tooltip);\n color: var(--gsrp-text-main);\n border: 1px solid var(--gsrp-border);\n border-radius: 8px;\n box-shadow: var(--gsrp-shadow);\n padding: 0;\n font-family:\n system-ui,\n -apple-system,\n BlinkMacSystemFont,\n 'Segoe UI',\n Roboto,\n 'Helvetica Neue',\n Arial,\n sans-serif;\n font-size: var(--gsrp-preview-font, 13px);\n line-height: 1.5;\n white-space: normal;\n overflow: visible !important; /* Resolve overflow-x clipping completely */\n display: none;\n opacity: 0;\n transform: translateY(8px) scale(0.98);\n transition:\n opacity 0.2s var(--gsrp-transition-ease),\n transform 0.2s var(--gsrp-transition-ease),\n display 0.2s allow-discrete;\n z-index: 1000;\n pointer-events: auto;\n box-sizing: border-box;\n cursor: text;\n user-select: text !important;\n scrollbar-width: thin;\n scrollbar-color: var(--gsrp-text-sub) transparent;\n\n /* Invisible hover bridge to eliminate hover tunneling across the 6px gap */\n &::after {\n content: '';\n position: absolute;\n top: -8px;\n left: 0;\n width: 100%;\n height: 10px;\n background: transparent;\n pointer-events: none; /* Default is inert to avoid click obstruction */\n z-index: 1001; /* Ensure tunnel lays perfectly on top of gap */\n }\n\n /* Activate pointer events on tunnel bridge only during hover states */\n .gsrp-preview-trigger:hover &::after,\n .gsrp-hover-sticky &::after {\n pointer-events: auto;\n }\n\n @media (prefers-reduced-motion: reduce) {\n transform: none !important;\n transition:\n opacity 0.1s linear,\n display 0.1s allow-discrete !important;\n }\n\n &::-webkit-scrollbar,\n *::-webkit-scrollbar {\n width: 6px;\n height: 6px;\n }\n\n &::-webkit-scrollbar-track,\n *::-webkit-scrollbar-track {\n background: transparent;\n }\n\n &::-webkit-scrollbar-thumb,\n *::-webkit-scrollbar-thumb {\n background: rgba(0, 0, 0, 0.08); /* Minimal distraction when resting */\n border-radius: 10px;\n transition: background var(--gsrp-transition-fast);\n }\n\n /* Dynamic scrollbar visibility on trigger interact */\n .gsrp-preview-trigger:hover &::-webkit-scrollbar-thumb,\n .gsrp-preview-trigger:hover *::-webkit-scrollbar-thumb,\n .gsrp-preview-trigger.gsrp-pinned &::-webkit-scrollbar-thumb,\n .gsrp-preview-trigger.gsrp-pinned *::-webkit-scrollbar-thumb {\n background: rgba(0, 0, 0, 0.22);\n }\n\n &::-webkit-scrollbar-thumb:hover,\n *::-webkit-scrollbar-thumb:hover {\n background: rgba(0, 0, 0, 0.48) !important;\n }\n\n &::-webkit-scrollbar-thumb:active,\n *::-webkit-scrollbar-thumb:active {\n background: rgba(0, 0, 0, 0.72) !important;\n }\n\n /* Dark Mode Scrollbars Integration */\n .gsrp-is-dark & {\n scrollbar-color: rgba(255, 255, 255, 0.35) transparent;\n\n &::-webkit-scrollbar-thumb,\n *::-webkit-scrollbar-thumb {\n background: rgba(255, 255, 255, 0.08);\n }\n\n .gsrp-preview-trigger:hover &::-webkit-scrollbar-thumb,\n .gsrp-preview-trigger:hover *::-webkit-scrollbar-thumb,\n .gsrp-is-dark .gsrp-preview-trigger:hover &::-webkit-scrollbar-thumb,\n .gsrp-is-dark .gsrp-preview-trigger:hover *::-webkit-scrollbar-thumb,\n .gsrp-is-dark .gsrp-preview-trigger.gsrp-pinned &::-webkit-scrollbar-thumb,\n .gsrp-is-dark .gsrp-preview-trigger.gsrp-pinned *::-webkit-scrollbar-thumb {\n background: rgba(255, 255, 255, 0.28);\n }\n\n &::-webkit-scrollbar-thumb:hover,\n *::-webkit-scrollbar-thumb:hover {\n background: rgba(255, 255, 255, 0.55) !important;\n }\n\n &::-webkit-scrollbar-thumb:active,\n *::-webkit-scrollbar-thumb:active {\n background: rgba(255, 255, 255, 0.82) !important;\n }\n }\n\n /* Invisible bridge to buffer cursor diagonal transit and mouseleaves */\n &::before {\n content: '';\n position: absolute;\n top: -12px;\n left: 0;\n width: 100%;\n height: 12px;\n background: transparent;\n }\n\n &.gsrp-force-close {\n display: none !important;\n }\n\n /* --- Maximized Full-Screen Tooltip Overlay Mode --- */\n &.gsrp-maximized {\n position: fixed !important;\n top: 50% !important;\n left: 50% !important;\n transform: translate(-50%, -50%) !important;\n transition:\n opacity 0.2s ease,\n transform 0s !important;\n width: min(1200px, calc(100vw - 32px)) !important;\n max-width: calc(100vw - 32px) !important;\n height: calc(100vh - 32px) !important;\n max-height: calc(100vh - 32px) !important;\n z-index: 2000000000 !important;\n box-shadow:\n 0 16px 48px rgba(0, 0, 0, 0.45),\n 0 0 0 1px rgba(0, 0, 0, 0.1);\n border-radius: 12px;\n animation: gsrp-maximize-zoom 0.22s var(--gsrp-transition-ease) forwards;\n\n /* Let internal scrollable body fill the entire maximized container */\n .gsrp-preview-inner-wrapper {\n max-height: 100% !important;\n height: 100% !important;\n }\n\n /* Scale media inside maximized viewports */\n .gsrp-preview-media {\n img,\n video,\n iframe {\n max-height: min(70vh, 750px);\n }\n }\n .gsrp-youtube-embed-container,\n .gsrp-vimeo-embed-container {\n max-height: min(70vh, 750px);\n }\n .gsrp-inline-media-img {\n max-height: min(70vh, 750px);\n }\n\n /* Hide transit speech-bubble indicators */\n &::before {\n display: none !important;\n }\n }\n\n /* Intercepting flip-up dynamic layouts */\n .gsrp-flip-up & {\n top: auto;\n bottom: 100%;\n margin-top: 0;\n margin-bottom: 6px;\n box-shadow: 0 -6px 16px var(--gsrp-shadow);\n\n &::before {\n top: auto;\n bottom: -12px;\n }\n\n /* Adjust hover bridge location when tooltip flips upward */\n &::after {\n top: auto;\n bottom: -8px;\n }\n\n &.gsrp-maximized {\n top: 50% !important;\n transform: translate(-50%, -50%) !important;\n\n &::before {\n display: none !important;\n }\n }\n }\n\n /* Prevent transitions during drag metrics checks */\n &.gsrp-no-transform-transition {\n transition:\n opacity 0.2s ease,\n transform 0s !important;\n }\n\n /* --- Inner Elements Styling (Selftexts, Headers) --- */\n .gsrp-preview-title-container {\n position: sticky;\n top: 0;\n display: flex;\n align-items: center;\n flex-wrap: nowrap;\n gap: 8px;\n background: var(--gsrp-bg-tooltip);\n z-index: 11;\n padding: 6px 12px 4px 12px;\n border-bottom: 1px solid var(--gsrp-border-pre);\n cursor: default;\n\n .gsrp-preview-title {\n flex: 0 1 auto;\n min-width: 0;\n color: var(--gsrp-primary);\n font-size: 1.08em;\n font-weight: bold;\n display: flex;\n align-items: center;\n gap: 8px;\n\n .gsrp-header-icon {\n flex: 0 0 auto;\n opacity: 0.9;\n }\n\n .gsrp-comments-count {\n font-size: 0.9em;\n opacity: 0.85;\n font-weight: normal;\n }\n\n .gsrp-participants-count {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n font-size: 0.9em;\n opacity: 0.85;\n font-weight: normal;\n\n .gsrp-participants-icon {\n opacity: 0.8;\n flex: 0 0 auto;\n }\n }\n }\n\n .gsrp-close-btn {\n flex: 0 0 auto;\n margin-left: 0;\n }\n }\n\n .gsrp-preview-meta-group {\n margin-left: auto;\n display: flex;\n align-items: center;\n gap: 8px;\n flex: 0 1 auto;\n min-width: 0;\n }\n\n .gsrp-translate-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n padding: 2px;\n border-radius: 4px;\n cursor: pointer;\n background: none;\n border: none;\n outline: none;\n transition:\n background 0.2s ease,\n border-color 0.2s ease,\n box-shadow 0.2s ease,\n transform 0.1s ease;\n box-sizing: border-box;\n\n &[data-gsrp-trans-status='translated'] {\n background-color: var(--gsrp-bg-hover, rgba(26, 115, 232, 0.1));\n border: 1px solid var(--gsrp-primary, #1a73e8);\n box-shadow: 0 0 4px rgba(26, 115, 232, 0.25);\n }\n\n &[data-gsrp-trans-status='translating'] {\n background-color: rgba(181, 130, 0, 0.12);\n border: 1px dashed var(--gsrp-warning, #b58200);\n animation: gsrp-pulse 1.2s infinite;\n }\n }\n\n /* Copy as Markdown button with sleek animations */\n .gsrp-copy-markdown-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n padding: 4px;\n border-radius: 4px;\n cursor: pointer;\n background: none;\n border: none;\n outline: none;\n color: var(--gsrp-text-sub);\n transition:\n background 0.2s ease,\n color 0.2s ease,\n transform 0.15s cubic-bezier(0.34, 1.56, 0.64, 1);\n box-sizing: border-box;\n\n &:hover {\n background-color: var(--gsrp-bg-hover);\n color: var(--gsrp-primary);\n }\n\n &:active {\n transform: scale(0.85);\n }\n\n /* Success feedback interaction */\n &.gsrp-copied {\n color: var(--gsrp-success) !important;\n background-color: color-mix(\n in srgb,\n var(--gsrp-success) 12%,\n transparent\n ) !important;\n animation: gsrp-copied-pop 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;\n }\n\n svg {\n display: block;\n flex: 0 0 auto;\n transition: transform 0.2s ease;\n }\n }\n\n .gsrp-preview-post-title {\n padding: 12px 16px 4px 16px;\n font-size: 1.25em;\n font-weight: 600;\n line-height: 1.25;\n color: var(--gsrp-primary);\n word-break: break-word;\n }\n\n .gsrp-preview-meta-bar {\n display: flex;\n align-items: center;\n gap: 8px;\n padding: 4px 16px 8px 16px;\n border-bottom: 1px solid var(--gsrp-border-pre);\n flex-wrap: wrap;\n font-size: 0.95em;\n color: var(--gsrp-text-sub);\n }\n\n .gsrp-preview-body {\n padding: 12px 16px;\n\n p {\n margin: 0 0 10px 0;\n &:last-child {\n margin-bottom: 0;\n }\n }\n\n a {\n color: var(--gsrp-text-link);\n text-decoration: none;\n word-break: break-all;\n\n &:hover {\n text-decoration: underline;\n }\n }\n\n hr {\n border: 0;\n border-top: 1px solid var(--gsrp-border-pre);\n margin: 8px 0;\n }\n\n h1,\n h2,\n h3,\n h4,\n h5,\n h6 {\n font-size: 1.15em;\n font-weight: bold;\n display: block;\n margin: 10px 0 4px 0;\n }\n\n ul,\n ol {\n margin: 4px 0;\n padding-left: 20px;\n }\n\n li {\n margin-bottom: 2px;\n }\n del {\n opacity: 0.7;\n }\n\n /* --- Code & Pre Formatting --- */\n code {\n font-family:\n Consolas, 'Andale Mono WT', 'Andale Mono', 'Lucida Console', 'Courier New',\n monospace;\n font-size: 0.85em;\n background: var(--gsrp-bg-code);\n padding: 2px 4px;\n border-radius: 4px;\n color: var(--gsrp-text-code);\n }\n\n /* Standardized Block-level Code Block Container */\n pre {\n background: #f6f8fa !important;\n color: #24292f !important; /* Defensively guarantee code foreground color in light mode */\n border: 1px solid rgba(0, 0, 0, 0.08) !important;\n border-left: 1px solid rgba(0, 0, 0, 0.08) !important; /* Defensively override any legacy blue left border to prevent visual clutter */\n border-radius: 8px !important;\n padding: 12px 16px !important;\n margin: 10px 0 !important;\n box-sizing: border-box !important;\n position: relative !important; /* Keep relative layout anchor for the copy button */\n overflow: visible !important; /* Prevent the container itself from scrolling to keep the button fixed! */\n\n &,\n & code {\n color: #24292f !important; /* Forces light mode foreground colors on raw code elements */\n }\n\n code {\n background: transparent !important;\n color: #24292f !important;\n padding: 0 !important;\n border-radius: 0 !important;\n font-size: 0.88em !important;\n white-space: pre !important; /* Keep original multi-line indentation */\n word-break: normal !important;\n word-wrap: normal !important;\n display: block !important;\n overflow-x: auto !important; /* Make code element the designated scrolling viewport! */\n max-width: 100% !important;\n }\n\n /* --- High-fidelity Syntax Highlighting (Light Mode / GitHub Light Style) --- */\n .gsrp-code-comment {\n color: #6a737d !important;\n font-style: italic;\n }\n .gsrp-code-string {\n color: #032f62 !important;\n }\n .gsrp-code-keyword {\n color: #d73a49 !important;\n font-weight: 600 !important;\n }\n .gsrp-code-number {\n color: #005cc5 !important;\n }\n\n .gsrp-is-dark & {\n background: #161b22 !important;\n color: #c9d1d9 !important; /* Defensively guarantee code foreground color in dark mode */\n border-color: rgba(255, 255, 255, 0.15) !important;\n border-left: 1px solid rgba(255, 255, 255, 0.15) !important;\n\n &,\n & code {\n color: #c9d1d9 !important;\n }\n\n code {\n color: #c9d1d9 !important;\n background: transparent !important; /* Defensively guarantee no background bleeding */\n }\n\n /* --- High-fidelity Syntax Highlighting (Dark Mode / GitHub Dark Style) --- */\n .gsrp-code-comment {\n color: #8b949e !important;\n }\n .gsrp-code-string {\n color: #a5d6ff !important;\n }\n .gsrp-code-keyword {\n color: #ff7b72 !important;\n font-weight: 600 !important;\n }\n .gsrp-code-number {\n color: #79c0ff !important;\n }\n }\n\n /* Reveal hover copy buttons inside code blocks */\n &:hover,\n &:focus-within {\n .gsrp-code-copy-btn {\n opacity: 1 !important;\n pointer-events: auto !important;\n }\n }\n }\n\n /* --- Spoilers click to reveal --- */\n .md-spoiler-text {\n background-color: #70757a;\n color: #70757a;\n border-radius: 4px;\n padding: 0 4px;\n transition:\n color 0.2s,\n background-color 0.2s;\n cursor: pointer;\n\n &:hover {\n color: #202124;\n background-color: transparent;\n }\n\n .gsrp-is-dark & {\n background-color: #5f6368;\n color: #5f6368;\n\n &:hover {\n color: #e8eaed;\n }\n }\n }\n\n blockquote {\n border-left: 3px solid var(--gsrp-primary);\n margin: 10px 0 10px 8px;\n padding: 10px 14px;\n color: var(--gsrp-text-sub);\n background-color: var(--gsrp-bg-code);\n border-radius: 0 4px 4px 0;\n font-style: italic;\n\n p {\n margin: 0 0 8px 0 !important;\n &:last-child {\n margin-bottom: 0 !important;\n }\n }\n }\n\n table {\n border-collapse: collapse;\n width: 100%;\n font-size: 0.92em;\n background: var(--gsrp-bg-tooltip);\n margin-bottom: 10px;\n border-radius: 4px;\n display: block;\n overflow-x: auto;\n\n th,\n td {\n border: 1px solid var(--gsrp-border);\n padding: 6px 8px;\n text-align: left;\n }\n\n th {\n background-color: var(--gsrp-bg-code);\n font-weight: bold;\n }\n }\n }\n }\n\n /* --- Status Tags Design System (M2) --- */\n .gsrp-status-tag {\n font-weight: bold;\n }\n .gsrp-status-removed {\n color: var(--gsrp-danger);\n font-weight: bold;\n }\n .gsrp-status-warning {\n color: var(--gsrp-warning);\n font-weight: bold;\n }\n .gsrp-status-success {\n color: var(--gsrp-success);\n font-weight: bold;\n }\n .gsrp-status-oc {\n color: var(--gsrp-oc);\n font-weight: bold;\n }\n .gsrp-status-spoiler {\n color: var(--gsrp-danger);\n font-weight: bold;\n }\n .gsrp-status-nsfw {\n color: var(--gsrp-danger);\n font-weight: bold;\n }\n\n /* Score, Awards & Crosspostings badges */\n .gsrp-score {\n font-weight: bold;\n .gsrp-score-ratio {\n font-size: 0.9em;\n opacity: 0.8;\n }\n }\n .gsrp-crossposts-link {\n color: inherit;\n text-decoration: none;\n cursor: pointer;\n &:hover {\n text-decoration: underline;\n }\n }\n .gsrp-awards {\n color: var(--gsrp-award);\n font-weight: bold;\n }\n\n /* Crosspost card structures */\n .gsrp-crosspost-link {\n color: var(--gsrp-text-link);\n font-weight: bold;\n text-decoration: none;\n display: inline-flex;\n align-items: center;\n }\n\n .gsrp-crosspost-card {\n padding: 12px;\n border: 1px solid var(--gsrp-border);\n border-radius: 6px;\n background: var(--gsrp-bg-code);\n\n .gsrp-crosspost-card-meta {\n font-size: 0.85em;\n margin-bottom: 6px;\n\n a {\n font-weight: bold;\n color: inherit;\n text-decoration: none;\n }\n .gsrp-crosspost-author {\n opacity: 0.7;\n }\n }\n\n .gsrp-crosspost-card-title {\n font-size: 1em;\n font-weight: bold;\n margin-bottom: 8px;\n a {\n color: inherit;\n text-decoration: none;\n }\n }\n\n .gsrp-crosspost-card-body {\n font-size: 0.92em;\n margin-bottom: 8px;\n opacity: 0.9;\n }\n\n .gsrp-crosspost-card-stats {\n font-size: 0.85em;\n opacity: 0.8;\n display: flex;\n gap: 12px;\n }\n }\n\n /* --- Interactive Usernames & Authors Tags --- */\n .gsrp-post-author,\n .gsrp-comment-author {\n text-decoration: none;\n &:hover {\n text-decoration: underline;\n }\n }\n\n .gsrp-post-author {\n font-size: 0.92em;\n color: var(--gsrp-text-sub);\n }\n\n .gsrp-comment-author {\n font-weight: bold;\n color: var(--gsrp-text-link);\n\n &.gsrp-author-promoted {\n color: var(--gsrp-author-color-light, var(--gsrp-text-link)) !important;\n font-weight: 800;\n padding: 1px 6px;\n background: rgba(128, 128, 128, 0.08);\n border-radius: 4px;\n border: 1px solid rgba(128, 128, 128, 0.15);\n\n .gsrp-is-dark & {\n color: var(--gsrp-author-color-dark, var(--gsrp-text-link)) !important;\n }\n }\n\n &.gsrp-author-deleted {\n font-weight: normal;\n opacity: 0.6;\n text-decoration: none !important;\n cursor: default;\n }\n }\n\n /* Flag OP (Post Submitter) with premium capsules and badges */\n [data-is-op='true'] > .gsrp-comment-content > .gsrp-comment-header .gsrp-comment-author {\n color: var(--gsrp-primary);\n font-weight: 800;\n background: var(--gsrp-primary-hover);\n border: 1px solid var(--gsrp-primary-hover);\n padding: 1px 8px;\n border-radius: 12px;\n margin-right: 4px;\n display: inline-flex;\n align-items: center;\n text-decoration: none;\n transition:\n background var(--gsrp-transition-fast),\n border-color var(--gsrp-transition-fast);\n\n &:hover {\n background: rgba(26, 115, 232, 0.14);\n border-color: rgba(26, 115, 232, 0.3);\n text-decoration: none;\n }\n }\n\n .gsrp-comment-op-tag {\n display: inline-flex;\n align-items: center;\n font-size: 0.77em;\n font-weight: 800;\n color: var(--gsrp-bg-tooltip);\n background: var(--gsrp-primary);\n padding: 2px 7px;\n border-radius: 12px;\n margin-left: 2px;\n vertical-align: middle;\n line-height: 1;\n letter-spacing: 0.5px;\n text-transform: uppercase;\n }\n\n .gsrp-comment-flair {\n display: inline-block;\n font-size: 0.77em;\n color: var(--gsrp-text-sub);\n background: var(--gsrp-bg-code);\n padding: 0 4px;\n border-radius: 3px;\n margin-left: 4px;\n vertical-align: middle;\n }\n\n .gsrp-comment-stickied-tag {\n color: var(--gsrp-success);\n font-size: 0.85em;\n font-weight: bold;\n margin-right: 6px;\n }\n\n .gsrp-comment-mod-tag {\n display: inline-block;\n font-size: 0.77em;\n font-weight: bold;\n color: var(--gsrp-success);\n background: rgba(52, 168, 83, 0.12);\n padding: 0 4px;\n border-radius: 3px;\n margin-left: 4px;\n vertical-align: middle;\n }\n\n .gsrp-comment-admin-tag {\n display: inline-block;\n font-size: 0.77em;\n font-weight: bold;\n color: var(--gsrp-danger);\n background: rgba(234, 67, 53, 0.12);\n padding: 0 4px;\n border-radius: 3px;\n margin-left: 4px;\n vertical-align: middle;\n }\n\n /* Subdue deleted/redacted users & entities */\n .gsrp-author-deleted {\n opacity: 0.55;\n font-style: italic;\n cursor: default;\n &:hover {\n text-decoration: none;\n }\n }\n\n .gsrp-post-deleted-placeholder,\n .gsrp-post-removed-placeholder {\n font-style: italic;\n opacity: 0.5;\n color: var(--gsrp-text-sub);\n padding: 4px 0;\n margin: 4px 0;\n font-size: 0.95em;\n }\n\n /* Stricken out external dead links indicators */\n .gsrp-dead-link {\n text-decoration: line-through !important;\n opacity: 0.55;\n cursor: not-allowed;\n &::after {\n content: ' 🚫';\n text-decoration: none !important;\n display: inline-block;\n font-style: normal;\n }\n }\n\n /* Close & maximize buttons with sleek zoom micro-animations */\n .gsrp-close-btn,\n .gsrp-maximize-btn {\n display: none;\n cursor: pointer;\n color: var(--gsrp-text-sub);\n padding: 0 4px;\n line-height: 1;\n font-weight: bold;\n user-select: none;\n vertical-align: middle;\n transition:\n transform 0.1s ease,\n color 0.15s ease;\n\n &:active {\n transform: scale(0.85);\n }\n }\n\n .gsrp-close-btn {\n font-size: 1.23em;\n &:hover {\n color: var(--gsrp-danger);\n }\n }\n\n .gsrp-maximize-btn {\n font-size: 1.08em;\n margin-right: 4px;\n line-height: 1.2;\n &:hover {\n color: var(--gsrp-primary);\n }\n }\n\n .gsrp-flair {\n background: var(--gsrp-bg-code);\n padding: 0 4px;\n border-radius: 2px;\n color: inherit;\n display: inline-block;\n max-width: 150px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n vertical-align: bottom;\n }\n\n /* --- Sophisticated Recursion-friendly Comments Hierarchy --- */\n .gsrp-comment-item,\n .gsrp-reply-item {\n display: flex;\n align-items: stretch;\n gap: 6px;\n content-visibility: auto;\n contain-intrinsic-size: auto 120px;\n\n > .gsrp-comment-content {\n background-color: transparent;\n transition: background-color 0.15s var(--gsrp-transition-ease);\n }\n\n /* Highlight only the deepest active hovered comment node to avoid stacked semi-transparent backgrounds */\n &:hover:not(:has(.gsrp-reply-item:hover)) > .gsrp-comment-content {\n background-color: var(--gsrp-bg-hover);\n }\n\n /* Prioritise stickied comments or focused elements to render instantly */\n &.gsrp-comment-stickied,\n &.gsrp-card-focus-highlight {\n content-visibility: visible;\n }\n }\n\n .gsrp-comment-item {\n margin-bottom: 12px;\n padding-bottom: 12px;\n border-bottom: 1px dashed var(--gsrp-border-pre);\n\n &:last-child {\n margin-bottom: 0;\n padding-bottom: 0;\n border-bottom: none;\n }\n\n > .gsrp-comment-content {\n padding: 4px 8px;\n border-radius: 4px;\n }\n }\n\n /* Hide replies and deep placeholders when \"Show top-level comments only\" is toggled */\n .gsrp-comments-list-wrapper.gsrp-hide-replies {\n & .gsrp-reply-item,\n & .gsrp-comment-more:not([data-depth='0']) {\n display: none !important;\n }\n }\n\n .gsrp-comment-content {\n flex: 1;\n min-width: 0;\n }\n\n .gsrp-comment-header {\n display: flex;\n justify-content: space-between;\n align-items: center;\n margin: 0 -4px 4px -4px;\n padding: 2px 4px;\n font-size: 0.92em;\n cursor: pointer;\n border-radius: 3px;\n transition: background 0.12s ease;\n\n &:hover {\n background: var(--gsrp-primary-hover);\n }\n }\n\n .gsrp-comment-date {\n font-size: 0.85em;\n color: var(--gsrp-text-sub);\n text-decoration: none;\n cursor: pointer;\n &:hover {\n text-decoration: underline;\n }\n\n &.gsrp-is-edited {\n text-decoration: underline dotted !important;\n }\n }\n\n .gsrp-comment-score {\n font-size: 0.85em;\n color: var(--gsrp-text-sub);\n margin-left: 4px;\n font-variant-numeric: tabular-nums;\n }\n\n .gsrp-is-edited {\n text-decoration: underline dotted;\n text-underline-offset: 3px;\n }\n\n .gsrp-reply-item {\n margin-top: 8px;\n > .gsrp-comment-content {\n padding: 6px 8px 6px 8px;\n border-radius: 4px;\n }\n }\n\n /* Standalone highly interactive nested colored thread lines */\n .gsrp-thread-line {\n width: 2px;\n flex-shrink: 0;\n align-self: stretch;\n background: var(--gsrp-border-pre);\n cursor: pointer;\n border-radius: 2px;\n transition:\n background 0.15s,\n width 0.15s;\n position: relative;\n\n &::before {\n content: '';\n position: absolute;\n top: 0;\n bottom: 0;\n left: -4px;\n right: -4px; /* Invisible target padding area to extend clickable hit-box to ~10px */\n }\n\n &:hover {\n background: var(--gsrp-primary);\n width: 3px;\n }\n\n &:focus-visible {\n outline: 2px solid var(--gsrp-primary);\n outline-offset: 1px;\n }\n }\n\n /* Recursion line coloration is defined globally at the end of this file to prevent nesting/compilation issues */\n\n .gsrp-comment-item.gsrp-comment-deleted,\n .gsrp-reply-item.gsrp-comment-deleted {\n > .gsrp-comment-content {\n > .gsrp-comment-header,\n > .gsrp-comment-body {\n opacity: 0.55;\n font-style: italic;\n }\n }\n }\n\n /* Load more inline threads placeholders */\n .gsrp-comment-more {\n padding: 4px 8px 4px 14px;\n font-size: 0.92em;\n font-style: italic;\n color: var(--gsrp-text-sub);\n\n .gsrp-more-link {\n color: var(--gsrp-text-link);\n text-decoration: none;\n\n &:not(:disabled):hover {\n text-decoration: underline;\n }\n }\n\n button.gsrp-more-load {\n background: none;\n border: none;\n padding: 0;\n margin: 0;\n font: inherit;\n cursor: pointer;\n\n &:disabled {\n cursor: wait;\n opacity: 0.6;\n text-decoration: none;\n }\n }\n }\n\n /* Expand / Collapse Transitions on Comments Tree */\n .gsrp-comment-body,\n .gsrp-comment-replies {\n opacity: 1;\n transition:\n opacity 0.2s ease-out,\n display 0.2s allow-discrete;\n }\n\n .gsrp-comment-collapsed {\n > .gsrp-comment-content {\n > .gsrp-comment-body,\n > .gsrp-comment-replies {\n display: none !important;\n opacity: 0;\n }\n\n > .gsrp-comment-header .gsrp-collapsed-hint {\n display: inline;\n }\n }\n }\n\n @starting-style {\n .gsrp-comment-body,\n .gsrp-comment-replies {\n opacity: 0;\n }\n }\n\n .gsrp-collapsed-hint {\n display: none;\n font-size: 0.85em;\n color: var(--gsrp-text-sub);\n margin-left: 6px;\n font-style: italic;\n }\n\n /* Displayed / total comments indices */\n .gsrp-comments-count {\n margin-left: 8px;\n font-size: 0.85em;\n font-weight: normal;\n opacity: 0.7;\n font-variant-numeric: tabular-nums;\n }\n\n /* --- Interactive Sort Select Element --- */\n .gsrp-inline-sort-select {\n margin-left: 8px;\n font-size: 0.88em;\n background: transparent;\n color: inherit;\n border: 1px solid var(--gsrp-border); /* Use a softer border color instead of currentColor */\n border-radius: 4px;\n cursor: pointer;\n padding: 1px 4px;\n outline: none;\n box-sizing: border-box;\n transition:\n border-color 0.15s ease,\n background-color 0.15s ease;\n\n &:hover {\n border-color: var(--gsrp-primary);\n background-color: rgba(128, 128, 128, 0.1);\n }\n\n &:focus {\n border-color: var(--gsrp-primary);\n }\n\n option {\n color: #000;\n background: #ffffff;\n }\n\n .gsrp-is-dark & option {\n color: #e8eaed;\n background: #22242a;\n }\n }\n\n .gsrp-preview-flair-badge {\n background: rgba(128, 128, 128, 0.15);\n padding: 2px 6px;\n border-radius: 4px;\n font-size: 0.85em;\n }\n\n /* --- Comments Keyword Multi-Filter Systems --- */\n .gsrp-comments-filter-bar {\n display: flex;\n gap: 6px;\n align-items: center;\n margin-left: auto;\n font-weight: normal;\n flex: 0 1 auto;\n min-width: 0;\n }\n\n .gsrp-comments-filter-input-wrap {\n position: relative;\n display: inline-flex;\n align-items: center;\n width: 22px;\n height: 22px;\n border: 1px solid var(--gsrp-border);\n border-radius: 4px;\n transition:\n width 0.25s cubic-bezier(0.4, 0, 0.2, 1),\n border-color 0.15s ease,\n background-color 0.15s ease,\n box-shadow 0.15s ease;\n min-width: 0;\n box-sizing: border-box;\n overflow: hidden;\n background: transparent;\n cursor: pointer;\n\n &:hover {\n border-color: var(--gsrp-primary);\n background-color: rgba(128, 128, 128, 0.15);\n }\n\n &:focus-within,\n &:has(input:not(:placeholder-shown)) {\n width: 18ch;\n cursor: default;\n background-color: transparent;\n border-color: var(--gsrp-primary) !important;\n box-shadow: 0 0 0 2px var(--gsrp-primary-hover) !important;\n }\n\n &:has(input:not(:placeholder-shown)) .gsrp-comments-filter-clear {\n display: inline-block;\n }\n }\n\n .gsrp-comments-filter-input {\n width: 100%;\n height: 100%;\n min-width: 0;\n /* Padding left 22px leaves space for the search icon, padding right 20px leaves space for clearing X mark */\n padding: 2px 20px 2px 22px;\n font-size: 0.85em;\n line-height: 1.4;\n background: transparent;\n color: inherit;\n border: none;\n font-family: inherit;\n box-sizing: border-box;\n\n &:focus,\n &:focus-visible {\n outline: none !important;\n }\n\n &::-webkit-search-cancel-button {\n -webkit-appearance: none;\n appearance: none;\n }\n }\n\n .gsrp-filter-search-icon {\n position: absolute;\n left: 4px;\n top: 50%;\n transform: translateY(-50%);\n color: var(--gsrp-text);\n opacity: 0.75;\n pointer-events: none;\n transition: opacity 0.15s ease;\n flex-shrink: 0;\n\n .gsrp-comments-filter-input-wrap:hover &,\n .gsrp-comments-filter-input-wrap:focus-within & {\n opacity: 1;\n }\n }\n\n .gsrp-comments-filter-clear {\n position: absolute;\n right: 2px;\n top: 50%;\n transform: translateY(-50%);\n background: transparent;\n border: none;\n color: inherit;\n opacity: 0.45;\n cursor: pointer;\n font-size: 1.08em;\n line-height: 1;\n padding: 0 4px;\n border-radius: 50%;\n display: none;\n font-family: inherit;\n transition: opacity var(--gsrp-transition-fast);\n\n &:hover {\n opacity: 1;\n }\n }\n\n .gsrp-comments-filter-op-label {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n font-size: 0.85em;\n cursor: pointer;\n white-space: nowrap;\n user-select: none;\n flex: 0 0 auto;\n\n &.gsrp-comments-filter-op-disabled {\n opacity: 0.5;\n cursor: not-allowed;\n\n .gsrp-comments-filter-op {\n cursor: not-allowed;\n }\n }\n }\n\n .gsrp-comments-filter-op {\n margin: 0;\n cursor: pointer;\n }\n\n .gsrp-comments-filter-score {\n width: 8ch;\n padding: 4px 6px;\n font-size: 0.85em;\n line-height: 1.4;\n background: transparent;\n color: inherit;\n border: 1px solid var(--gsrp-border);\n border-radius: 4px;\n font-family: inherit;\n box-sizing: border-box;\n text-align: center;\n transition:\n border-color 0.15s ease,\n box-shadow 0.15s ease;\n -moz-appearance: textfield;\n\n &::-webkit-outer-spin-button,\n &::-webkit-inner-spin-button {\n -webkit-appearance: none;\n margin: 0;\n }\n\n &:focus,\n &:focus-visible {\n outline: none !important;\n border-color: var(--gsrp-primary) !important;\n box-shadow: 0 0 0 2px var(--gsrp-primary-hover) !important;\n }\n }\n\n .gsrp-comments-filter-mod-admin-label {\n display: inline-flex;\n align-items: center;\n gap: 4px;\n font-size: 0.85em;\n cursor: pointer;\n white-space: nowrap;\n user-select: none;\n flex: 0 0 auto;\n\n &.gsrp-comments-filter-mod-admin-disabled {\n opacity: 0.5;\n cursor: not-allowed;\n\n .gsrp-comments-filter-mod-admin {\n cursor: not-allowed;\n }\n }\n }\n\n .gsrp-comments-filter-mod-admin {\n margin: 0;\n cursor: pointer;\n }\n\n /* --- Filter Popover & Dynamic Badge Counting --- */\n .gsrp-filter-settings-wrap {\n position: relative;\n display: inline-flex;\n align-items: center;\n }\n\n .gsrp-filter-settings-toggle-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n background: transparent;\n border: 1px solid var(--gsrp-border);\n border-radius: 4px;\n padding: 4px;\n height: 22px;\n width: 22px;\n box-sizing: border-box;\n cursor: pointer;\n color: var(--gsrp-text);\n opacity: 0.75;\n transition:\n opacity 0.15s ease,\n border-color 0.15s ease,\n background-color 0.15s ease;\n\n &:hover {\n opacity: 1;\n border-color: var(--gsrp-primary);\n background-color: rgba(128, 128, 128, 0.15);\n }\n }\n\n /* Toggle button to collapse all nested replies */\n .gsrp-comments-collapse-toggle-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n background: transparent;\n border: 1px solid var(--gsrp-border);\n border-radius: 4px;\n padding: 4px;\n height: 22px;\n width: 22px;\n box-sizing: border-box;\n cursor: pointer;\n color: var(--gsrp-text);\n opacity: 0.75;\n transition:\n opacity 0.15s ease,\n border-color 0.15s ease,\n background-color 0.15s ease;\n\n &:hover {\n opacity: 1;\n border-color: var(--gsrp-primary);\n background-color: rgba(128, 128, 128, 0.15);\n }\n\n &[aria-pressed='true'] {\n opacity: 1;\n border-color: var(--gsrp-primary);\n background-color: var(--gsrp-primary-hover);\n color: var(--gsrp-primary);\n }\n }\n\n .gsrp-filter-settings-popover {\n position: absolute;\n top: 100%;\n right: 0;\n margin-top: 6px;\n background-color: var(--gsrp-bg-tooltip);\n border: 1px solid var(--gsrp-border);\n border-radius: 6px;\n box-shadow: 0 4px 16px rgba(0, 0, 0, 0.25);\n padding: 12px;\n display: none;\n flex-direction: column;\n gap: 10px;\n min-width: 190px;\n z-index: 100;\n backdrop-filter: blur(12px);\n\n &.gsrp-active {\n display: flex;\n }\n\n label {\n display: flex;\n align-items: center;\n gap: 8px;\n font-size: 0.9em;\n color: var(--gsrp-text);\n cursor: pointer;\n line-height: 1.2;\n transition: opacity var(--gsrp-transition-fast);\n\n input[type='checkbox'] {\n margin: 0;\n cursor: pointer;\n width: 14px;\n height: 14px;\n flex: 0 0 auto;\n }\n\n &.gsrp-comments-filter-disabled {\n opacity: 0.45;\n cursor: not-allowed;\n\n input[type='checkbox'] {\n cursor: not-allowed;\n }\n }\n\n &:hover:not(.gsrp-comments-filter-disabled) {\n opacity: 0.85;\n }\n }\n }\n\n .gsrp-comments-filter-score-row {\n display: flex;\n align-items: center;\n gap: 6px;\n font-size: 0.85em;\n\n &::before {\n content: '★';\n color: var(--gsrp-primary);\n font-size: 1.1em;\n opacity: 0.8;\n }\n\n .gsrp-comments-filter-score-op-btn {\n background: transparent;\n border: 1px solid var(--gsrp-border);\n color: inherit;\n font-size: 1.1em;\n font-weight: bold;\n line-height: 1;\n padding: 2px 6px;\n border-radius: 4px;\n cursor: pointer;\n font-family: monospace;\n transition:\n background var(--gsrp-transition-fast),\n border-color var(--gsrp-transition-fast);\n\n &:hover {\n background: var(--gsrp-bg-hover);\n border-color: var(--gsrp-primary);\n }\n }\n }\n\n .gsrp-comments-filter-score-count {\n font-size: 0.9em;\n color: var(--gsrp-text-muted || #888);\n font-weight: 500;\n white-space: nowrap;\n }\n\n .gsrp-comments-filter-counter {\n display: none;\n align-items: center;\n gap: 0;\n font-size: 0.85em;\n font-weight: normal;\n flex: 0 0 auto;\n\n &.gsrp-has-matches {\n display: inline-flex;\n }\n }\n\n .gsrp-comments-filter-prev,\n .gsrp-comments-filter-next {\n background: transparent;\n border: none;\n color: inherit;\n cursor: pointer;\n padding: 0 4px;\n font-size: 1.23em;\n line-height: 1;\n opacity: 0.6;\n font-family: inherit;\n transition: opacity var(--gsrp-transition-fast);\n\n &:hover {\n opacity: 1;\n }\n }\n\n .gsrp-comments-filter-position {\n font-variant-numeric: tabular-nums;\n min-width: 28px;\n text-align: center;\n opacity: 0.85;\n }\n\n /* Highlight indices marks inside comment trees */\n mark.gsrp-filter-highlight {\n background: rgba(255, 235, 59, 0.55);\n color: inherit;\n padding: 0 1px;\n border-radius: 2px;\n\n &.gsrp-filter-highlight-current {\n background: rgba(255, 152, 0, 0.85);\n color: #1a1a1a;\n padding: 0 1px;\n border-radius: 2px;\n }\n\n .gsrp-is-dark & {\n background: rgba(255, 235, 59, 0.4);\n\n &.gsrp-filter-highlight-current {\n background: rgba(255, 152, 0, 0.75);\n }\n }\n }\n\n .gsrp-comment-filtered-out {\n display: none !important;\n }\n\n .gsrp-comments-no-matches {\n display: none;\n text-align: center;\n padding: 20px;\n opacity: 0.7;\n font-size: 1em;\n font-style: italic;\n }\n\n .gsrp-comments-list-wrapper {\n padding: 0 16px 16px 16px;\n }\n\n .gsrp-comments-list-wrapper.gsrp-no-matches .gsrp-comments-no-matches {\n display: block;\n }\n}\n\n/* --- Programmable Search Engine (CSE) — Layout adjustments for cse.google.com ---\n The default badge layout (`position: absolute; top: 100%; left: 0`)\n assumes standard Google SERP cards: transparent background, natural\n bottom margin, no internal padding under the snippet, and content\n that begins flush with the card box's left edge. CSE breaks every\n one of those assumptions:\n\n 1. CSE cards have opaque white backgrounds and stack flush — an\n absolute badge below the card box gets clipped by the next card\n and visually disconnects from the card it belongs to.\n 2. CSE has its OWN internal padding-bottom inside `.gsc-table-result`\n / `.gsc-table-cell-snippet-close`, so even at `top: 100%` the\n badge sits well below the snippet text — looks \"too far\".\n 3. CSE's content is indented from the card box's left edge (table\n cell layout, gutter), so `left: 0` lands the badge to the LEFT\n of the visible content alignment.\n\n Fix: swap the badge to in-flow positioning (`position: relative`)\n ONLY on CSE. The badge then renders as the natural last block of\n the card, inheriting CSE's own content alignment automatically. No\n guesswork about padding values or left insets. Companion z-index\n lift on `.gsrp-card-focus-highlight` (_side-panel.css) keeps hover\n shadows visible across all platforms.\n\n Scoped to `.gsc-webResult.gsc-result` — leaves non-CSE pages untouched.\n*/\n.gsc-webResult.gsc-result {\n /* Small visual breathing room between cards. The badge is in-flow\n now, so we don't need 24px+ of margin to host an absolute element. */\n margin-bottom: 6px;\n}\n\n.gsc-webResult .gsrp-reddit-badge {\n /* Take the badge out of absolute positioning so it joins the card's\n natural content flow as the last block. Inherits CSE's left\n alignment automatically — no manual `left:` guess needed. */\n position: relative !important;\n top: auto !important;\n left: auto !important;\n margin: 6px 0 0 0 !important;\n display: flex !important;\n width: fit-content !important;\n max-width: 100% !important;\n}\n\n/* --- Integrated Embedded Media Framework (M2, Carousel & Media) --- */\n.gsrp-preview-media {\n margin-bottom: 12px;\n\n img,\n video {\n max-width: 100%;\n max-height: min(30vh, 300px);\n width: auto;\n height: auto;\n object-fit: contain;\n display: block;\n border-radius: 4px;\n background: #000;\n margin: 0 auto;\n }\n\n iframe {\n width: 100%;\n aspect-ratio: 16 / 9;\n max-height: min(30vh, 300px);\n border: 0;\n border-radius: 4px;\n background: #000;\n display: block;\n }\n\n .gsrp-preview-media-gallery > * + * {\n margin-top: 8px;\n }\n}\n/* Multigallery carousel display */\n.gsrp-carousel {\n position: relative;\n min-height: 180px !important; /* Prevent vertical collapsing and flash layout shift during image load */\n background: rgba(0, 0, 0, 0.02); /* Smooth light background skeleton placeholder */\n border-radius: 8px;\n overflow: hidden;\n\n .gsrp-is-dark & {\n background: rgba(255, 255, 255, 0.02); /* Adaptive dark style placeholder */\n }\n\n > .gsrp-gallery-item {\n display: none;\n\n &.gsrp-carousel-active {\n display: block;\n }\n }\n\n > * + * {\n margin-top: 0 !important;\n }\n\n .gsrp-carousel-prev,\n .gsrp-carousel-next {\n position: absolute;\n top: 50%;\n transform: translateY(-50%);\n background: rgba(15, 15, 15, 0.6);\n backdrop-filter: blur(4px);\n -webkit-backdrop-filter: blur(4px);\n color: #ffffff;\n border: 1px solid rgba(255, 255, 255, 0.12);\n width: 32px;\n height: 32px;\n font-size: 18px;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: 50%;\n cursor: pointer;\n z-index: 2;\n transition:\n background 0.2s ease,\n transform 0.2s cubic-bezier(0.2, 0.8, 0.2, 1);\n padding: 0;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);\n\n &:hover {\n background: rgba(15, 15, 15, 0.85);\n border-color: rgba(255, 255, 255, 0.25);\n transform: translateY(-50%) scale(1.1);\n }\n\n &:active {\n transform: translateY(-50%) scale(0.95);\n }\n\n &:focus-visible {\n outline: 2px solid #8ab4f8;\n outline-offset: 2px;\n }\n }\n\n .gsrp-carousel-prev {\n left: 8px;\n }\n .gsrp-carousel-next {\n right: 8px;\n }\n}\n\n.gsrp-preview-media-caption {\n user-select: text !important;\n -webkit-user-select: text !important;\n cursor: text !important;\n padding: 4px 8px !important; /* Compact vertical spacing */\n background: rgba(0, 0, 0, 0.05) !important; /* High-contrast light grey by default */\n border-radius: 4px !important;\n margin: 4px 12px 6px 12px !important; /* Compact margins */\n font-size: 0.85em !important;\n color: var(--gsrp-text, #3c4043) !important; /* High-contrast text by default */\n line-height: 1.4 !important;\n text-align: center !important;\n}\n\n.gsrp-is-dark .gsrp-preview-media-caption {\n background: rgba(0, 0, 0, 0.4) !important; /* Sleek dark theme background */\n color: var(--gsrp-text-sub, #9aa0a6) !important; /* Comforting dark theme text */\n}\n\n/* GPU Hardware-accelerated Shimmer placeholders skeleton loaders */\n.gsrp-gallery-item img,\n.gsrp-preview-media > img,\n.gsrp-inline-media-img,\n.gsrp-lightbox-img,\n.gsrp-preview-media video,\n.gsrp-preview-media iframe {\n background: linear-gradient(\n 90deg,\n rgba(128, 128, 128, 0.06) 25%,\n rgba(128, 128, 128, 0.15) 50%,\n rgba(128, 128, 128, 0.06) 75%\n ) !important;\n background-size: 200% 100% !important;\n animation: gsrp-shimmer 1.5s infinite ease-in-out;\n\n .gsrp-is-dark & {\n background: linear-gradient(\n 90deg,\n rgba(255, 255, 255, 0.03) 25%,\n rgba(255, 255, 255, 0.08) 50%,\n rgba(255, 255, 255, 0.03) 75%\n ) !important;\n background-size: 200% 100% !important;\n }\n}\n\n/* Shimmer gradient animation keyframes */\n@keyframes gsrp-shimmer-loading {\n 0% {\n background-position: -200% 0;\n }\n 100% {\n background-position: 200% 0;\n }\n}\n\n/* Skeleton shimmer indicators when media is loading */\n.gsrp-gallery-item.gsrp-is-loading,\n.gsrp-preview-media.gsrp-is-loading {\n background: linear-gradient(\n 90deg,\n rgba(127, 127, 127, 0.04) 25%,\n rgba(127, 127, 127, 0.11) 37%,\n rgba(127, 127, 127, 0.04) 63%\n ) !important;\n background-size: 200% 100% !important;\n animation: gsrp-shimmer-loading 1.6s infinite linear !important;\n}\n\n/* Fallback displays when image loading errors occur */\n.gsrp-gallery-item {\n position: relative;\n display: block;\n cursor: zoom-in;\n border-radius: 4px;\n\n &:has(video) {\n cursor: default;\n }\n\n &:focus-visible {\n outline: 2px solid var(--gsrp-primary);\n outline-offset: 2px;\n }\n\n img {\n transition:\n transform 0.25s cubic-bezier(0.2, 0.8, 0.2, 1),\n box-shadow 0.25s ease-out;\n }\n}\n\n.gsrp-gallery-item.gsrp-img-error img,\n.gsrp-gallery-item.gsrp-video-error video,\n.gsrp-preview-media.gsrp-img-error img,\n.gsrp-preview-media.gsrp-video-error video {\n display: none !important;\n}\n\n.gsrp-gallery-item.gsrp-img-error,\n.gsrp-gallery-item.gsrp-video-error,\n.gsrp-preview-media.gsrp-img-error,\n.gsrp-preview-media.gsrp-video-error {\n min-height: 180px !important;\n height: auto !important;\n aspect-ratio: auto !important;\n background: rgba(127, 127, 127, 0.05) !important;\n border: 1px dashed rgba(255, 255, 255, 0.12) !important;\n display: flex !important;\n align-items: center !important;\n justify-content: center !important;\n cursor: default !important;\n border-radius: 4px !important;\n\n .gsrp-is-light & {\n background: rgba(0, 0, 0, 0.02) !important;\n border-color: rgba(0, 0, 0, 0.08) !important;\n }\n}\n\n.gsrp-gallery-item.gsrp-img-error::after,\n.gsrp-preview-media.gsrp-img-error::after {\n content: attr(data-error-text);\n font-size: 0.92em;\n color: var(--gsrp-text-sub);\n pointer-events: none;\n}\n\n.gsrp-gallery-item.gsrp-video-error::after,\n.gsrp-preview-media.gsrp-video-error::after {\n content: attr(data-error-text);\n font-size: 0.92em;\n color: var(--gsrp-text-sub);\n pointer-events: none;\n}\n\n.gsrp-gallery-counter,\n.gsrp-gallery-dims {\n position: absolute;\n top: 8px;\n background: rgba(15, 15, 15, 0.6) !important;\n backdrop-filter: blur(8px);\n -webkit-backdrop-filter: blur(8px);\n border: 1px solid rgba(255, 255, 255, 0.15);\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);\n color: #ffffff;\n font-size: 0.77em;\n font-weight: 600;\n padding: 2px 8px;\n border-radius: 10px;\n font-variant-numeric: tabular-nums;\n line-height: 1.4;\n pointer-events: none;\n}\n\n.gsrp-gallery-counter {\n right: 8px;\n}\n.gsrp-gallery-dims {\n left: 8px;\n}\n\n.gsrp-gallery-badge {\n background: transparent;\n border: none;\n color: inherit;\n cursor: pointer;\n font: inherit;\n padding: 0;\n line-height: inherit;\n vertical-align: baseline;\n\n &:hover {\n opacity: 0.7;\n text-decoration: underline;\n }\n\n &:focus-visible {\n outline: 2px solid var(--gsrp-primary);\n outline-offset: 1px;\n border-radius: 2px;\n }\n}\n\n.gsrp-inline-media-img {\n max-width: 100%;\n max-height: 100% !important;\n width: auto;\n height: auto;\n object-fit: contain;\n display: block;\n border-radius: 4px;\n margin: 6px 0;\n cursor: zoom-in;\n min-height: 120px;\n transition:\n transform 0.25s cubic-bezier(0.2, 0.8, 0.2, 1),\n box-shadow 0.25s ease-out;\n\n &:hover {\n transform: scale(1.02);\n box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);\n }\n}\n\n/* --- Premium High-End Copied Code Utilities inside Pre Elements --- */\n.gsrp-code-copy-btn {\n position: absolute;\n top: 6px;\n right: 6px;\n opacity: 0;\n pointer-events: none;\n background: rgba(128, 128, 128, 0.12);\n border: 1px solid rgba(128, 128, 128, 0.2);\n border-radius: 4px;\n width: 26px;\n height: 26px;\n cursor: pointer;\n color: var(--gsrp-text-sub);\n display: flex;\n align-items: center;\n justify-content: center;\n transition:\n opacity 0.2s ease,\n background-color 0.15s ease,\n border-color 0.15s ease,\n color 0.15s ease;\n z-index: 10;\n padding: 0;\n\n .gsrp-is-dark & {\n color: var(--gsrp-text-sub);\n background: rgba(255, 255, 255, 0.08);\n border-color: rgba(255, 255, 255, 0.15);\n }\n\n &:hover {\n opacity: 1;\n background-color: rgba(128, 128, 128, 0.22);\n border-color: rgba(128, 128, 128, 0.35);\n color: var(--gsrp-primary);\n }\n\n &:focus-visible {\n opacity: 1;\n pointer-events: auto;\n outline: 2px solid var(--gsrp-primary) !important;\n outline-offset: 1px !important;\n }\n\n .gsrp-copied-icon {\n display: none !important;\n }\n\n /* Green premium hook displays when copied */\n &.gsrp-copied {\n opacity: 1 !important;\n pointer-events: auto !important;\n color: #34a853 !important;\n border-color: rgba(52, 168, 83, 0.35) !important;\n background: rgba(52, 168, 83, 0.08) !important;\n\n .gsrp-copy-icon {\n display: none !important;\n }\n\n .gsrp-copied-icon {\n display: block !important;\n }\n\n .gsrp-is-dark & {\n color: #81c784 !important;\n border-color: rgba(129, 199, 132, 0.4) !important;\n background: rgba(129, 199, 132, 0.12) !important;\n }\n }\n}\n\n/* --- Fully Responsive Interactive Settings Dashboard Modal --- */\n.gsrp-settings-overlay {\n position: fixed;\n top: 0;\n left: 0;\n width: 100vw;\n height: 100vh;\n background: rgba(15, 15, 20, 0.45);\n backdrop-filter: blur(10px);\n -webkit-backdrop-filter: blur(10px);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 2147483647;\n font-family: Arial, sans-serif;\n\n --gsrp-bg: #ffffff;\n --gsrp-color: #202124;\n --gsrp-border: #dadce0;\n\n &.gsrp-is-dark {\n --gsrp-bg: #22242a;\n --gsrp-color: #e8eaed;\n --gsrp-border: #3c4043;\n }\n\n .gsrp-settings-modal {\n background: var(--gsrp-bg);\n color: var(--gsrp-color);\n border-radius: 8px;\n width: min(540px, 90vw);\n max-width: 90vw;\n box-shadow: 0 12px 32px rgba(0, 0, 0, 0.25);\n border: 1px solid var(--gsrp-border);\n display: flex;\n flex-direction: column;\n overflow: hidden;\n animation: gsrp-modal-zoom 0.25s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;\n }\n\n .gsrp-settings-header {\n padding: 10px 16px;\n font-size: 15px;\n font-weight: bold;\n border-bottom: 1px solid var(--gsrp-border);\n display: flex;\n justify-content: space-between;\n align-items: center;\n }\n\n .gsrp-settings-close {\n cursor: pointer;\n opacity: 0.7;\n background: none;\n border: none;\n color: inherit;\n font-size: 16px;\n padding: 0 4px;\n line-height: 1;\n transition:\n opacity 0.15s ease,\n transform 0.1s ease;\n\n &:hover {\n opacity: 1;\n }\n &:active {\n transform: scale(0.85);\n }\n }\n\n .gsrp-settings-body {\n padding: 16px;\n display: flex;\n flex-direction: column;\n gap: 12px;\n overflow-y: auto;\n max-height: 75vh;\n font-size: 14px;\n }\n\n .gsrp-settings-row {\n display: flex;\n justify-content: space-between;\n align-items: center;\n gap: 16px;\n min-height: 24px;\n\n label {\n flex: 1 1 auto;\n line-height: 1.4;\n word-break: break-word;\n }\n }\n\n .gsrp-settings-select {\n flex: 0 0 auto;\n max-width: min(240px, 50%);\n text-overflow: ellipsis;\n overflow: hidden;\n white-space: nowrap;\n padding: 4px 8px;\n border-radius: 4px;\n background: var(--gsrp-bg);\n color: var(--gsrp-color);\n border: 1px solid var(--gsrp-border);\n font-size: 12px;\n transition:\n border-color 0.15s ease,\n box-shadow 0.15s ease;\n cursor: pointer;\n outline: none;\n\n &:hover {\n border-color: #1a73e8;\n }\n\n .gsrp-is-dark & {\n &:hover {\n border-color: #8ab4f8;\n }\n }\n }\n\n .gsrp-settings-checkbox {\n width: 15px;\n height: 15px;\n cursor: pointer;\n margin: 0;\n accent-color: #1a73e8;\n transition: transform 0.1s ease;\n\n &:hover {\n transform: scale(1.1);\n }\n &:active {\n transform: scale(0.9);\n }\n\n .gsrp-is-dark & {\n accent-color: #8ab4f8;\n }\n }\n\n .gsrp-settings-slider-wrap {\n display: flex;\n align-items: center;\n gap: 8px;\n flex: 1;\n min-width: 0;\n }\n\n .gsrp-settings-range {\n flex: 1;\n min-width: 0;\n cursor: pointer;\n }\n\n #gsrp-default-volume-display {\n font-variant-numeric: tabular-nums;\n min-width: 36px;\n text-align: right;\n }\n\n .gsrp-settings-hr {\n border: 0;\n border-top: 1px solid var(--gsrp-border);\n margin: 2px 0;\n width: 100%;\n }\n\n .gsrp-settings-footer {\n padding: 10px 16px;\n border-top: 1px solid var(--gsrp-border);\n display: flex;\n justify-content: flex-end;\n gap: 8px;\n }\n\n .gsrp-settings-btn {\n padding: 4px 12px;\n border: 1px solid var(--gsrp-border);\n background: transparent;\n color: var(--gsrp-color);\n border-radius: 3px;\n cursor: pointer;\n font-size: 12.5px;\n transition:\n transform 0.1s ease,\n background 0.15s ease,\n border-color 0.15s ease,\n color 0.15s ease;\n\n &:hover {\n opacity: 0.8;\n }\n &:active {\n transform: scale(0.95);\n }\n\n &.gsrp-settings-btn-primary {\n border: none;\n background: #1a73e8;\n color: white;\n font-weight: bold;\n\n &:hover {\n background: #1557b0;\n opacity: 1;\n }\n &:active {\n background: #11448a;\n transform: scale(0.95);\n }\n }\n\n &.gsrp-settings-btn-danger {\n border-color: #ea4335;\n color: #ea4335;\n\n &:hover {\n background: #ea4335;\n color: white;\n opacity: 1;\n }\n &:active {\n background: #b31412;\n color: white;\n transform: scale(0.95);\n }\n }\n }\n}\n\n/* --- Premium Fullscreen Overlay Gallery Lightbox Panel --- */\n.gsrp-lightbox-overlay {\n position: fixed;\n top: 0;\n left: 0;\n width: 100vw;\n height: 100vh;\n background: rgba(10, 10, 10, 0.82);\n backdrop-filter: blur(12px);\n -webkit-backdrop-filter: blur(12px);\n display: flex;\n align-items: center;\n justify-content: center;\n z-index: 2147483647;\n font-family: Arial, sans-serif;\n user-select: none;\n animation: gsrp-overlay-fade 0.2s cubic-bezier(0.2, 0.8, 0.2, 1) forwards;\n\n &.gsrp-img-error .gsrp-lightbox-img-container,\n &.gsrp-video-error .gsrp-lightbox-video {\n display: none !important;\n }\n\n &.gsrp-img-error::before {\n content: '⚠ Image unavailable';\n color: rgba(255, 255, 255, 0.45);\n font-size: 1.1em;\n }\n\n &.gsrp-video-error::before {\n content: '⚠ Video unavailable';\n color: rgba(255, 255, 255, 0.45);\n font-size: 1.1em;\n }\n\n .gsrp-lightbox-img-container {\n display: flex;\n align-items: center;\n justify-content: center;\n width: 100%;\n height: 100%;\n max-width: calc(100vw - 120px);\n max-height: calc(100vh - 120px);\n opacity: 0;\n transform: scale(0.97);\n transition:\n opacity 0.25s cubic-bezier(0.2, 0.8, 0.2, 1),\n transform 0.25s cubic-bezier(0.2, 0.8, 0.2, 1);\n z-index: 100000;\n\n &.gsrp-is-active {\n opacity: 1;\n transform: scale(1);\n }\n }\n\n .gsrp-lightbox-img,\n .gsrp-lightbox-video {\n max-width: 100%;\n max-height: 100%;\n width: auto;\n height: auto;\n object-fit: contain;\n border-radius: 4px; /* Clean corners instead of excessive roundness */\n background: #0c0c0e;\n cursor: default;\n box-shadow:\n 0 12px 48px rgba(0, 0, 0, 0.65),\n 0 0 0 1px rgba(255, 255, 255, 0.08);\n transition: transform 0.25s cubic-bezier(0.2, 0.8, 0.2, 1);\n }\n\n .gsrp-lightbox-nav {\n position: absolute;\n top: 50%;\n transform: translateY(-50%);\n background: rgba(15, 15, 15, 0.55);\n backdrop-filter: blur(12px);\n -webkit-backdrop-filter: blur(12px);\n color: rgba(255, 255, 255, 0.85);\n border: 1px solid rgba(255, 255, 255, 0.06);\n width: 40px;\n height: 40px; /* Tighter layout */\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: 50%; /* Rounded circular button as requested */\n cursor: pointer;\n transition:\n background 0.2s ease,\n border-color 0.2s ease,\n color 0.2s ease,\n transform 0.2s cubic-bezier(0.2, 0.8, 0.2, 1);\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);\n padding: 0;\n z-index: 100003; /* FIX: Render on top of image container (100000) */\n\n svg {\n width: 18px;\n height: 18px;\n stroke-width: 2;\n stroke: currentColor;\n transition: transform 0.2s ease;\n }\n\n &:hover {\n background: rgba(255, 255, 255, 0.12);\n border-color: rgba(255, 255, 255, 0.15);\n color: #ffffff;\n transform: translateY(-50%) scale(1.05);\n\n svg {\n transform: scale(1.05);\n }\n }\n\n &:active {\n transform: translateY(-50%) scale(0.95);\n }\n\n &:focus-visible {\n outline: 2px solid #8ab4f8;\n outline-offset: 2px;\n }\n\n &.gsrp-lightbox-nav-prev {\n left: 20px;\n &:hover svg {\n transform: translateX(-1px);\n }\n }\n &.gsrp-lightbox-nav-next {\n right: 20px;\n &:hover svg {\n transform: translateX(1px);\n }\n }\n }\n\n .gsrp-lightbox-close {\n position: absolute;\n top: 16px;\n right: 16px; /* Tighter spacing */\n background: rgba(15, 15, 15, 0.55);\n backdrop-filter: blur(12px);\n -webkit-backdrop-filter: blur(12px);\n color: rgba(255, 255, 255, 0.85);\n border: 1px solid rgba(255, 255, 255, 0.06);\n width: 36px;\n height: 36px; /* Moderately upscaled from 32px */\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: 50%; /* Rounded circular button as requested */\n cursor: pointer;\n transition:\n background 0.2s ease,\n border-color 0.2s ease,\n color 0.2s ease,\n transform 0.2s cubic-bezier(0.2, 0.8, 0.2, 1);\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);\n padding: 0;\n z-index: 100005;\n\n svg {\n width: 16px;\n height: 16px; /* Moderately upscaled from 14px */\n stroke-width: 2.2;\n stroke: currentColor;\n transition: transform 0.2s ease;\n }\n\n &:hover {\n background: rgba(234, 67, 53, 0.85);\n border-color: rgba(234, 67, 53, 0.95);\n color: #ffffff;\n transform: scale(1.05) rotate(90deg);\n }\n\n &:active {\n transform: scale(0.95);\n }\n\n &:focus-visible {\n outline: 2px solid #8ab4f8;\n outline-offset: 2px;\n }\n }\n\n .gsrp-lightbox-counter,\n .gsrp-lightbox-dims {\n position: absolute;\n background: rgba(15, 15, 15, 0.55) !important;\n backdrop-filter: blur(12px);\n -webkit-backdrop-filter: blur(12px);\n border: 1px solid rgba(255, 255, 255, 0.06);\n color: rgba(255, 255, 255, 0.9);\n font-variant-numeric: tabular-nums;\n pointer-events: none;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);\n z-index: 100004;\n }\n\n .gsrp-lightbox-counter {\n top: 16px;\n left: 16px; /* Tighter spacing */\n font-size: 12px; /* Moderately upscaled from 10px */\n font-weight: 600;\n padding: 5px 12px; /* Moderately upscaled from 4px 10px */\n border-radius: 4px; /* Crisp corner instead of capsule */\n letter-spacing: 0.02em;\n }\n\n .gsrp-lightbox-dims {\n top: 52px;\n left: 16px; /* Shifted down from 48px to prevent overlapping */\n font-size: 11px; /* Moderately upscaled from 9px */\n font-weight: 500;\n padding: 4px 10px; /* Moderately upscaled from 3px 8px */\n border-radius: 4px; /* Crisp corner instead of capsule */\n transition: top 0.25s cubic-bezier(0.2, 0.8, 0.2, 1) !important;\n }\n\n /* 🎯 When the gallery counter is hidden (e.g., single image), dynamically shift dims up to fill the 16px spot */\n .gsrp-lightbox-counter[style*='display: none'] + .gsrp-lightbox-dims {\n top: 16px !important;\n }\n\n .gsrp-ai-summary-card {\n margin: 8px 16px 4px 16px;\n padding: 8px 12px;\n border: 1px solid var(--gsrp-border);\n border-radius: 8px;\n background: linear-gradient(135deg, var(--gsrp-bg-code) 0%, var(--gsrp-bg-tooltip) 100%);\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);\n transition: all 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);\n display: flex;\n flex-direction: column;\n gap: 6px;\n overflow: hidden;\n flex-shrink: 0 !important;\n\n &.gsrp-collapsed {\n padding-top: 8px !important;\n padding-bottom: 8px !important;\n gap: 0 !important;\n\n .gsrp-ai-summary-body {\n max-height: 0 !important;\n opacity: 0 !important;\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n margin-top: 0 !important;\n pointer-events: none !important;\n }\n\n .gsrp-ai-summary-toggle-icon {\n transform: rotate(180deg) !important;\n }\n }\n }\n\n .gsrp-ai-summary-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n font-weight: bold;\n color: var(--gsrp-primary);\n font-size: 1.05em;\n user-select: none;\n }\n\n .gsrp-ai-summary-title {\n display: inline-flex;\n align-items: center;\n gap: 6px;\n }\n\n .gsrp-ai-summary-timer {\n font-size: 0.85em;\n font-weight: normal;\n color: var(--gsrp-text-sub);\n margin-left: 6px;\n }\n\n .gsrp-ai-summary-toggle,\n .gsrp-ai-summary-copy {\n display: inline-flex !important;\n align-items: center !important;\n justify-content: center !important;\n width: 24px !important;\n height: 24px !important;\n border: none !important;\n background: transparent !important;\n box-shadow: none !important;\n cursor: pointer !important;\n color: var(--gsrp-text-sub) !important;\n border-radius: 50% !important;\n transition:\n background 0.2s ease,\n color 0.2s ease,\n transform 0.15s ease !important;\n padding: 0 !important;\n outline: none !important;\n box-sizing: border-box !important;\n\n &:hover {\n background: var(--gsrp-bg-hover) !important;\n color: var(--gsrp-primary) !important;\n transform: scale(1.1) !important;\n }\n\n &:active {\n transform: scale(0.9) !important;\n }\n\n &.gsrp-copied {\n color: var(--gsrp-success) !important;\n background-color: color-mix(in srgb, var(--gsrp-success) 12%, transparent) !important;\n }\n }\n\n .gsrp-ai-summary-toggle-icon {\n transition: transform 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);\n }\n\n .gsrp-ai-summary-body {\n font-size: 0.95em;\n line-height: 1.45;\n color: var(--gsrp-text-main);\n transition: all 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);\n max-height: 260px;\n opacity: 1;\n overflow-y: auto;\n scrollbar-width: thin;\n\n p {\n margin: 0 0 6px 0;\n &:last-child {\n margin-bottom: 0;\n }\n }\n\n strong {\n color: var(--gsrp-primary);\n }\n\n ul {\n margin: 2px 0 4px 0;\n padding-left: 20px;\n list-style-type: disc;\n }\n\n li {\n margin-bottom: 2px;\n }\n\n h1,\n h2,\n h3 {\n font-size: 1.1em;\n margin: 6px 0 4px 0;\n color: var(--gsrp-primary);\n font-weight: 600;\n }\n }\n\n .gsrp-ai-summary-loading {\n display: flex;\n align-items: center;\n gap: 8px;\n color: var(--gsrp-text-sub);\n padding: 8px 0;\n }\n\n .gsrp-pulse-dot {\n width: 8px;\n height: 8px;\n background-color: var(--gsrp-primary);\n border-radius: 50%;\n display: inline-block;\n animation: gsrp-pulse-animation 1.4s infinite ease-in-out both;\n }\n\n .gsrp-ai-summary-error {\n color: var(--gsrp-danger);\n padding: 8px 0;\n display: flex;\n align-items: center;\n gap: 6px;\n font-weight: 500;\n }\n\n /* Pulse animation for loading indicator */\n @keyframes gsrp-pulse-animation {\n 0%,\n 80%,\n 100% {\n transform: scale(0);\n opacity: 0.3;\n }\n 40% {\n transform: scale(1);\n opacity: 1;\n }\n }\n\n @keyframes gsrp-wiggle {\n 0%,\n 100% {\n transform: rotate(0deg);\n }\n 25% {\n transform: rotate(-8deg);\n }\n 75% {\n transform: rotate(8deg);\n }\n }\n\n /* --- Side Panel Header Controls Fix --- */\n .gsrp-side-panel {\n .gsrp-preview-title-container {\n position: sticky;\n top: 0;\n display: flex;\n align-items: center;\n flex-wrap: nowrap;\n gap: 8px;\n background: var(--gsrp-bg-tooltip);\n z-index: 11;\n padding: 6px 12px 4px 12px;\n border-bottom: 1px solid var(--gsrp-border-pre);\n cursor: default;\n\n .gsrp-preview-title {\n flex: 0 1 auto;\n min-width: 0;\n color: var(--gsrp-primary);\n font-size: 1.08em;\n font-weight: bold;\n display: flex;\n align-items: center;\n gap: 8px;\n\n .gsrp-header-icon {\n flex: 0 0 auto;\n opacity: 0.9;\n }\n }\n }\n\n .gsrp-translate-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n padding: 2px;\n border-radius: 4px;\n cursor: pointer;\n background: none;\n border: none;\n outline: none;\n transition:\n background 0.2s ease,\n border-color 0.2s ease,\n box-shadow 0.2s ease,\n transform 0.1s ease;\n box-sizing: border-box;\n\n &[data-gsrp-trans-status='translated'] {\n background-color: var(--gsrp-bg-hover, rgba(26, 115, 232, 0.1));\n border: 1px solid var(--gsrp-primary, #1a73e8);\n box-shadow: 0 0 4px rgba(26, 115, 232, 0.25);\n }\n\n &[data-gsrp-trans-status='translating'] {\n background-color: rgba(181, 130, 0, 0.12);\n border: 1px dashed var(--gsrp-warning, #b58200);\n animation: gsrp-pulse 1.2s infinite;\n }\n }\n\n .gsrp-copy-markdown-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n padding: 4px;\n border-radius: 4px;\n cursor: pointer;\n background: none;\n border: none;\n outline: none;\n color: var(--gsrp-text-sub);\n transition:\n background 0.2s ease,\n color 0.2s ease,\n transform 0.15s cubic-bezier(0.34, 1.56, 0.64, 1);\n box-sizing: border-box;\n\n &:hover {\n background-color: var(--gsrp-bg-hover);\n color: var(--gsrp-primary);\n }\n\n &:active {\n transform: scale(0.85);\n }\n\n &.gsrp-copied {\n color: var(--gsrp-success) !important;\n background-color: color-mix(\n in srgb,\n var(--gsrp-success) 12%,\n transparent\n ) !important;\n animation: gsrp-copied-pop 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;\n }\n }\n\n .gsrp-close-btn,\n .gsrp-maximize-btn {\n display: inline-block !important;\n }\n }\n}\n\n/* Reset and style summarize button globally to avoid host pollution (available in both tooltips and side-panels) */\n.gsrp-summarize-btn {\n display: inline-flex !important;\n align-items: center !important;\n justify-content: center !important;\n padding: 4px !important;\n border-radius: 4px !important;\n cursor: pointer !important;\n background: transparent !important;\n border: none !important;\n outline: none !important;\n box-shadow: none !important;\n color: var(--gsrp-text-sub) !important;\n transition:\n background 0.2s ease,\n color 0.2s ease,\n transform 0.15s ease !important;\n box-sizing: border-box !important;\n}\n\n.gsrp-summarize-btn:hover {\n background-color: var(--gsrp-bg-hover) !important;\n color: var(--gsrp-primary) !important;\n transform: scale(1.15) !important;\n animation: gsrp-wiggle 0.5s ease-in-out !important;\n}\n\n.gsrp-summarize-btn:active {\n transform: scale(0.9) !important;\n}\n"; const mediaEmbedsCss = "/* --- Advanced Premium Loading Skeleton Comments Animators --- */\n.gsrp-skeleton-comments {\n padding: 16px;\n display: flex;\n flex-direction: column;\n gap: 16px;\n\n .gsrp-skeleton-comment {\n display: flex;\n flex-direction: column;\n gap: 8px;\n }\n\n .gsrp-skeleton-header {\n display: flex;\n align-items: center;\n gap: 8px;\n }\n\n .gsrp-skeleton-avatar {\n width: 16px;\n height: 16px;\n border-radius: 50%;\n background: rgba(128, 128, 128, 0.15);\n animation: gsrp-pulse 1.5s infinite ease-in-out;\n }\n\n .gsrp-skeleton-meta {\n width: 45%;\n height: 12px;\n border-radius: 4px;\n background: rgba(128, 128, 128, 0.15);\n animation: gsrp-pulse 1.5s infinite ease-in-out;\n }\n\n .gsrp-skeleton-text-line {\n height: 10px;\n border-radius: 4px;\n background: rgba(128, 128, 128, 0.15);\n animation: gsrp-pulse 1.5s infinite ease-in-out;\n }\n}\n\n/* --- Inline Media Expansion Images --- */\n.gsrp-inline-media-img {\n display: block;\n max-width: 100%;\n max-height: 100% !important;\n width: auto;\n height: auto;\n object-fit: contain;\n border-radius: 8px;\n margin: 10px 0;\n cursor: pointer;\n border: 1px solid var(--gsrp-border);\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);\n transition:\n transform 0.2s var(--gsrp-transition-ease),\n box-shadow 0.2s var(--gsrp-transition-ease),\n filter 0.2s ease;\n\n &:hover {\n transform: scale(1.015);\n box-shadow: 0 6px 16px rgba(0, 0, 0, 0.14);\n filter: brightness(1.03);\n }\n\n &:active {\n transform: scale(0.985);\n }\n}\n\n/* Declarative custom variable architecture for inline expanded media synchronization */\n.gsrp-inline-media-container {\n --gsrp-inline-max-height: min(30vh, 300px);\n max-height: var(--gsrp-inline-max-height) !important;\n width: fit-content !important;\n}\n\n.gsrp-inline-media-container .gsrp-inline-media-img,\n.gsrp-inline-media-container .gsrp-inline-media-video {\n max-height: var(\n --gsrp-inline-max-height\n ) !important; /* Automatically inherits from parent container */\n}\n\n/* Dynamically upscale both container and media inside side panels and maximized windows */\n.gsrp-side-panel .gsrp-inline-media-container {\n --gsrp-inline-max-height: 400px;\n}\n\n/* Dynamically upscale both container and media to min(70vh, 750px) inside maximized hover tooltips */\n.gsrp-preview-tooltip.gsrp-maximized .gsrp-inline-media-container {\n --gsrp-inline-max-height: min(70vh, 750px);\n}\n\n/* --- RedGIFs & YouTube Media Integrations --- */\n.gsrp-redgifs-container {\n position: relative;\n display: flex;\n align-items: center;\n justify-content: center;\n background: #000;\n min-height: 200px;\n max-height: 480px;\n border-radius: 6px;\n overflow: hidden;\n width: 100%;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);\n margin: 8px 0;\n\n .gsrp-redgifs-spinner {\n position: absolute;\n display: flex;\n align-items: center;\n justify-content: center;\n inset: 0;\n z-index: 2;\n background: rgba(0, 0, 0, 0.4);\n }\n\n video {\n display: block;\n width: 100%;\n max-height: 480px;\n object-fit: contain;\n background: #000;\n }\n}\n\n/* --- Premium YouTube, X, Bluesky, Reddit & Dailymotion Media Integrations --- */\n.gsrp-youtube-embed-btn,\n.gsrp-vimeo-embed-btn,\n.gsrp-x-embed-btn,\n.gsrp-bsky-embed-btn,\n.gsrp-reddit-embed-btn,\n.gsrp-dailymotion-embed-btn,\n.gsrp-mastodon-embed-btn {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n gap: 4px;\n border-radius: 6px;\n cursor: pointer;\n padding: 2px 5px;\n margin-left: 6px;\n vertical-align: middle;\n position: relative;\n top: -1px;\n line-height: 1;\n transition: all 0.2s var(--gsrp-transition-ease);\n\n &:hover {\n transform: scale(1.05);\n }\n\n &:active {\n transform: scale(0.95);\n }\n}\n\n.gsrp-youtube-embed-btn {\n --gsrp-yt-logo-triangle: #ffffff;\n background: rgba(255, 0, 0, 0.08);\n color: #ff0000;\n border: 1px solid rgba(255, 0, 0, 0.18);\n\n &:hover {\n background: rgba(255, 0, 0, 0.16);\n border-color: rgba(255, 0, 0, 0.35);\n }\n\n &.gsrp-active {\n background: #ff0000 !important;\n color: #ffffff !important;\n border-color: #ff0000 !important;\n --gsrp-yt-logo-triangle: #ff0000; /* Reverse triangle to matching red background */\n }\n\n .gsrp-is-dark & {\n background: rgba(255, 107, 107, 0.12);\n color: #ff6b6b;\n border-color: rgba(255, 107, 107, 0.25);\n\n &:hover {\n background: rgba(255, 107, 107, 0.22);\n border-color: rgba(255, 107, 107, 0.4);\n }\n\n &.gsrp-active {\n background: #ff6b6b !important;\n color: #1d1f23 !important;\n border-color: #ff6b6b !important;\n --gsrp-yt-logo-triangle: #ff6b6b; /* Reverse triangle to matching soft-red background */\n }\n }\n}\n\n.gsrp-vimeo-embed-btn {\n background: rgba(0, 178, 234, 0.08);\n color: #00b2ea;\n border: 1px solid rgba(0, 178, 234, 0.18);\n\n &:hover {\n background: rgba(0, 178, 234, 0.16);\n border-color: rgba(0, 178, 234, 0.35);\n }\n\n &.gsrp-active {\n background: #00b2ea !important;\n color: #ffffff !important;\n border-color: #00b2ea !important;\n }\n\n .gsrp-is-dark & {\n background: rgba(0, 178, 234, 0.12);\n color: #00b2ea;\n border-color: rgba(0, 178, 234, 0.25);\n\n &:hover {\n background: rgba(0, 178, 234, 0.22);\n border-color: rgba(0, 178, 234, 0.4);\n }\n\n &.gsrp-active {\n background: #00b2ea !important;\n color: #1d1f23 !important;\n border-color: #00b2ea !important;\n }\n }\n}\n\n.gsrp-reddit-embed-btn {\n background: rgba(252, 71, 30, 0.08);\n color: #fc471e;\n border: 1px solid rgba(252, 71, 30, 0.18);\n --gsrp-reddit-logo-inner: #ffffff;\n\n &:hover {\n background: rgba(252, 71, 30, 0.16);\n border-color: rgba(252, 71, 30, 0.35);\n }\n\n &.gsrp-active {\n background: #fc471e !important;\n color: #ffffff !important;\n border-color: #fc471e !important;\n box-shadow: 0 2px 6px rgba(252, 71, 30, 0.22);\n --gsrp-reddit-logo-inner: #fc471e; /* Reverse alien details to brand orange in active state */\n }\n\n .gsrp-is-dark & {\n background: rgba(252, 71, 30, 0.12);\n color: #ff6a4a;\n border-color: rgba(252, 71, 30, 0.25);\n\n &:hover {\n background: rgba(252, 71, 30, 0.22);\n border-color: rgba(252, 71, 30, 0.4);\n }\n\n &.gsrp-active {\n background: #fc471e !important;\n color: #1d1f23 !important;\n border-color: #fc471e !important;\n box-shadow: 0 2px 6px rgba(252, 71, 30, 0.32);\n --gsrp-reddit-logo-inner: #fc471e;\n }\n }\n}\n\n.gsrp-dailymotion-embed-btn {\n background: rgba(0, 102, 220, 0.08);\n color: #0066dc;\n border: 1px solid rgba(0, 102, 220, 0.18);\n\n &:hover {\n background: rgba(0, 102, 220, 0.16);\n border-color: rgba(0, 102, 220, 0.35);\n }\n\n &.gsrp-active {\n background: #0066dc !important;\n color: #ffffff !important;\n border-color: #0066dc !important;\n box-shadow: 0 2px 6px rgba(0, 102, 220, 0.22);\n }\n\n .gsrp-is-dark & {\n background: rgba(60, 148, 255, 0.12);\n color: #3c94ff;\n border-color: rgba(60, 148, 255, 0.25);\n\n &:hover {\n background: rgba(60, 148, 255, 0.22);\n border-color: rgba(60, 148, 255, 0.4);\n }\n\n &.gsrp-active {\n background: #3c94ff !important;\n color: #1d1f23 !important;\n border-color: #3c94ff !important;\n box-shadow: 0 2px 6px rgba(60, 148, 255, 0.32);\n }\n }\n}\n\n.gsrp-x-embed-btn {\n background: rgba(15, 20, 25, 0.06);\n color: #0f1419;\n border: 1px solid rgba(15, 20, 25, 0.12);\n\n &:hover {\n background: rgba(15, 20, 25, 0.12);\n border-color: rgba(15, 20, 25, 0.25);\n }\n\n &.gsrp-active {\n background: #0f1419 !important;\n color: #ffffff !important;\n border-color: #0f1419 !important;\n box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);\n }\n\n .gsrp-is-dark & {\n background: rgba(255, 255, 255, 0.08);\n color: #f7f9fa;\n border-color: rgba(255, 255, 255, 0.15);\n\n &:hover {\n background: rgba(255, 255, 255, 0.15);\n border-color: rgba(255, 255, 255, 0.3);\n }\n\n &.gsrp-active {\n background: #ffffff !important;\n color: #0f1419 !important;\n border-color: #ffffff !important;\n box-shadow: 0 2px 6px rgba(255, 255, 255, 0.15);\n }\n }\n}\n\n.gsrp-bsky-embed-btn {\n background: rgba(17, 133, 254, 0.08);\n color: #1185fe;\n border: 1px solid rgba(17, 133, 254, 0.18);\n\n &:hover {\n background: rgba(17, 133, 254, 0.16);\n border-color: rgba(17, 133, 254, 0.35);\n }\n\n &.gsrp-active {\n background: #1185fe !important;\n color: #ffffff !important;\n border-color: #1185fe !important;\n box-shadow: 0 2px 6px rgba(17, 133, 254, 0.25);\n }\n\n .gsrp-is-dark & {\n background: rgba(17, 133, 254, 0.15);\n color: #49a2ff;\n border-color: rgba(17, 133, 254, 0.28);\n\n &:hover {\n background: rgba(17, 133, 254, 0.25);\n border-color: rgba(17, 133, 254, 0.45);\n }\n\n &.gsrp-active {\n background: #1185fe !important;\n color: #ffffff !important;\n border-color: #1185fe !important;\n box-shadow: 0 2px 6px rgba(17, 133, 254, 0.35);\n }\n }\n}\n\n.gsrp-mastodon-embed-btn {\n background: rgba(86, 58, 204, 0.08);\n color: #563acc;\n border: 1px solid rgba(86, 58, 204, 0.18);\n\n &:hover {\n background: rgba(86, 58, 204, 0.16);\n border-color: rgba(86, 58, 204, 0.35);\n }\n\n &.gsrp-active {\n background: #563acc !important;\n color: #ffffff !important;\n border-color: #563acc !important;\n box-shadow: 0 2px 6px rgba(86, 58, 204, 0.25);\n }\n\n .gsrp-is-dark & {\n background: rgba(99, 100, 255, 0.15);\n color: #8c8dff;\n border-color: rgba(99, 100, 255, 0.28);\n\n &:hover {\n background: rgba(99, 100, 255, 0.25);\n border-color: rgba(99, 100, 255, 0.45);\n }\n\n &.gsrp-active {\n background: #563acc !important;\n color: #ffffff !important;\n border-color: #563acc !important;\n box-shadow: 0 2px 6px rgba(86, 58, 204, 0.35);\n }\n }\n}\n\n/* Embedded Icons with fluid vector scaling */\n.gsrp-embed-btn-logo {\n width: 12px;\n height: 12px;\n display: block;\n object-fit: contain;\n flex-shrink: 0;\n}\n\n.gsrp-embed-btn-chevron {\n width: 9px;\n height: 9px;\n display: block;\n object-fit: contain;\n flex-shrink: 0;\n transform-origin: center !important; /* Ensure perfect rotating pivot across all engines */\n transition: transform 0.2s var(--gsrp-transition-ease);\n}\n\n/* GPU Accelerated smooth rotation of Chevrons when active - Defensive CSS Selectors */\n.gsrp-youtube-embed-btn.gsrp-active .gsrp-embed-btn-chevron,\n.gsrp-vimeo-embed-btn.gsrp-active .gsrp-embed-btn-chevron,\n.gsrp-x-embed-btn.gsrp-active .gsrp-embed-btn-chevron,\n.gsrp-bsky-embed-btn.gsrp-active .gsrp-embed-btn-chevron,\n.gsrp-reddit-embed-btn.gsrp-active .gsrp-embed-btn-chevron,\n.gsrp-dailymotion-embed-btn.gsrp-active .gsrp-embed-btn-chevron {\n transform: rotate(180deg) !important;\n}\n\n/* Inline Embed Playing Containers */\n.gsrp-youtube-embed-container {\n position: relative;\n width: auto;\n max-width: 100%;\n margin: 8px auto;\n border-radius: 8px;\n overflow: hidden;\n border: 1px solid var(--gsrp-border);\n box-shadow: 0 4px 14px rgba(0, 0, 0, 0.18);\n animation: gsrp-youtube-slide 0.22s cubic-bezier(0.2, 0.8, 0.2, 1) forwards;\n aspect-ratio: 16 / 9;\n max-height: min(30vh, 300px);\n background: #000;\n\n iframe {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n border: none;\n }\n}\n\n.gsrp-vimeo-embed-container {\n position: relative;\n width: auto;\n max-width: 100%;\n margin: 8px auto;\n border-radius: 8px;\n overflow: hidden;\n border: 1px solid var(--gsrp-border);\n box-shadow: 0 4px 14px rgba(0, 0, 0, 0.18);\n animation: gsrp-youtube-slide 0.22s cubic-bezier(0.2, 0.8, 0.2, 1) forwards;\n aspect-ratio: 16 / 9;\n max-height: min(30vh, 300px);\n background: #000;\n\n iframe {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n border: none;\n }\n}\n\n.gsrp-dailymotion-embed-container {\n position: relative;\n width: auto;\n max-width: 100%;\n margin: 8px auto;\n border-radius: 8px;\n overflow: hidden;\n border: 1px solid var(--gsrp-border);\n box-shadow: 0 4px 14px rgba(0, 0, 0, 0.18);\n animation: gsrp-youtube-slide 0.22s cubic-bezier(0.2, 0.8, 0.2, 1) forwards;\n aspect-ratio: 16 / 9;\n max-height: min(30vh, 300px);\n background: #000;\n\n iframe {\n position: absolute;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n border: none;\n }\n}\n\n/* Premium Native Reddit Embed Container & Comments Showcase */\n.gsrp-reddit-embed-container {\n margin: 8px 0 10px 0;\n padding: 8px 10px;\n border-radius: 8px;\n background: #f7f9fa;\n border: 1px solid rgba(0, 0, 0, 0.06);\n box-shadow: 0 4px 16px rgba(0, 0, 0, 0.04);\n animation: gsrp-reddit-slide-down 0.28s cubic-bezier(0.2, 0.8, 0.2, 1) forwards;\n text-align: left;\n\n .gsrp-is-dark & {\n background: rgba(255, 255, 255, 0.03);\n border-color: rgba(255, 255, 255, 0.08);\n box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);\n }\n\n &.gsrp-embed-error {\n border-left: 3px solid #ff4d4d;\n background: rgba(255, 77, 77, 0.05);\n padding: 8px 10px;\n }\n}\n\n.gsrp-reddit-embed-header {\n display: flex;\n align-items: center;\n flex-wrap: wrap;\n gap: 4px;\n font-size: 0.78em; /* Tightened from 0.82em to fit beautifully on one row */\n color: #536471;\n font-weight: 500;\n margin-bottom: 4px;\n\n .gsrp-is-dark & {\n color: #8b98a5;\n }\n}\n\n.gsrp-reddit-embed-logo-link {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n width: 13px !important;\n height: 13px !important;\n color: #fc471e !important; /* Force Reddit brand orange-red, bypass default page links */\n transition:\n transform 0.2s var(--gsrp-transition-ease),\n opacity 0.2s var(--gsrp-transition-ease);\n margin-right: 2px;\n\n &,\n &:visited,\n &:active,\n &:hover,\n &:focus {\n color: #fc471e !important; /* Defend against Google default hyperlink visited blue override */\n }\n\n &:hover {\n transform: scale(1.15);\n opacity: 0.95;\n }\n\n svg {\n width: 13px !important;\n height: 13px !important;\n display: block;\n }\n}\n\n.gsrp-reddit-embed-sub {\n font-weight: 700;\n color: #fc471e;\n}\n\n.gsrp-reddit-embed-author {\n color: #0f1419;\n font-weight: 600;\n\n .gsrp-is-dark & {\n color: #f7f9fa;\n }\n}\n\n.gsrp-reddit-embed-score {\n font-weight: 600;\n}\n\n.gsrp-reddit-embed-ratio {\n opacity: 0.8;\n}\n\n.gsrp-reddit-sep {\n color: inherit;\n opacity: 0.5;\n}\n\n.gsrp-reddit-embed-title {\n font-size: 0.95em; /* Tightened from 1.05em */\n font-weight: 700;\n color: #0f1419;\n line-height: 1.35;\n margin-bottom: 4px;\n\n .gsrp-is-dark & {\n color: #f7f9fa;\n }\n}\n\n.gsrp-reddit-embed-body {\n font-size: 0.88em; /* Tightened from 0.95em */\n color: #0f1419;\n line-height: 1.45;\n word-break: break-word;\n\n .gsrp-is-dark & {\n color: #eff3f4;\n }\n\n p {\n margin: 0 0 6px 0;\n &:last-child {\n margin-bottom: 0;\n }\n }\n\n /* Constrain media in nested embed container for compact card look */\n .gsrp-preview-media {\n margin: 8px auto;\n border-radius: 6px;\n overflow: hidden;\n max-width: 100%;\n max-height: min(220px, 25vh);\n background: rgba(0, 0, 0, 0.02);\n\n .gsrp-is-dark & {\n background: rgba(255, 255, 255, 0.02);\n }\n\n img,\n video {\n max-height: min(220px, 25vh);\n object-fit: contain;\n }\n\n iframe {\n height: 180px;\n }\n }\n}\n\n/* Collapsible container and fade overlay logic to prevent long embeds from bloating the panel scrollbar */\n.gsrp-reddit-embed-content {\n position: relative;\n transition: max-height 0.3s var(--gsrp-transition-ease);\n\n &.gsrp-is-collapsed {\n max-height: 280px; /* Highly compact 280px default threshold */\n overflow: hidden;\n\n /* Proposal A: Force compact height limit on media during collapsed state to prevent first-screen blockout */\n .gsrp-preview-media {\n max-height: 140px !important;\n margin: 6px auto !important;\n\n img,\n video,\n iframe {\n max-height: 140px !important;\n object-fit: cover !important; /* Prevent distortion while compacted */\n }\n }\n }\n}\n\n.gsrp-reddit-embed-fade-overlay {\n position: absolute;\n bottom: 0;\n left: 0;\n right: 0;\n height: 95px;\n background: linear-gradient(\n to bottom,\n rgba(247, 249, 250, 0) 0%,\n rgba(247, 249, 250, 0.9) 35%,\n rgba(247, 249, 250, 1) 100%\n );\n display: flex;\n align-items: flex-end;\n justify-content: center;\n padding-bottom: 12px;\n pointer-events: none; /* Let clicks pass through except on the button itself */\n z-index: 5;\n\n .gsrp-is-dark & {\n background: linear-gradient(\n to bottom,\n rgba(22, 27, 34, 0) 0%,\n rgba(22, 27, 34, 0.88) 35%,\n rgba(22, 27, 34, 1) 100%\n );\n }\n}\n\n.gsrp-reddit-embed-expand-btn {\n pointer-events: auto; /* Re-enable clicks specifically for the interactive button */\n display: inline-flex;\n align-items: center;\n gap: 4px;\n padding: 6px 12px; /* Tightened from 8px 16px */\n border-radius: 16px;\n font-size: 0.8em; /* Tightened from 0.85em */\n font-weight: 600;\n cursor: pointer;\n background: #ffffff;\n color: #1a73e8;\n border: 1px solid #dadce0;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); /* Premium shadows */\n transition:\n transform 0.2s var(--gsrp-transition-ease),\n background 0.15s ease,\n border-color 0.15s ease,\n box-shadow 0.2s ease,\n color 0.15s ease;\n\n .gsrp-is-dark & {\n background: #21262d;\n color: #58a6ff;\n border-color: #30363d;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);\n }\n\n &:hover {\n background: #f8f9fa;\n border-color: #c3e7ff;\n color: #1557b0;\n box-shadow: 0 6px 16px rgba(26, 115, 232, 0.18); /* Glow effect on hover */\n transform: translateY(-1.5px) !important;\n\n .gsrp-is-dark & {\n background: #30363d;\n border-color: #8b949e;\n color: #79c0ff;\n box-shadow: 0 6px 18px rgba(0, 0, 0, 0.45);\n }\n\n /* Proposal B: Shift the inner arrow downwards on hover */\n svg {\n transform: translateY(2px) !important;\n }\n }\n\n &:active {\n transform: translateY(0.5px) scale(0.98) !important;\n }\n\n /* Proposal C: Accessibility focus ring */\n &:focus-visible {\n outline: 2px solid #1a73e8 !important;\n outline-offset: 1px !important;\n }\n\n svg {\n transition: transform 0.2s var(--gsrp-transition-ease);\n }\n}\n\n/* Featured Top Comments Showcase inside Embedded Post Card */\n.gsrp-reddit-embed-comments-section {\n margin-top: 8px; /* Tightened from 14px */\n padding-top: 8px; /* Tightened from 12px */\n border-top: 1px dashed rgba(0, 0, 0, 0.08);\n\n .gsrp-is-dark & {\n border-color: rgba(255, 255, 255, 0.12);\n }\n}\n\n.gsrp-reddit-embed-comments-title {\n font-size: 0.78em; /* Tightened from 0.82em */\n font-weight: 700;\n color: #fc471e;\n text-transform: uppercase;\n margin-bottom: 6px; /* Tightened from 10px */\n letter-spacing: 0.5px;\n}\n\n.gsrp-reddit-embed-comments-list {\n display: flex;\n flex-direction: column;\n gap: 8px; /* Tightened from 12px */\n}\n\n.gsrp-reddit-embed-comment-item {\n padding-left: 8px; /* Tightened from 10px */\n border-left: 2px solid rgba(252, 71, 30, 0.2);\n}\n\n.gsrp-reddit-embed-comment-meta {\n display: flex;\n align-items: center;\n gap: 4px; /* Tightened from 6px */\n font-size: 0.74em; /* Tightened from 0.78em */\n color: #536471;\n margin-bottom: 2px; /* Tightened from 4px */\n\n .gsrp-is-dark & {\n color: #8b98a5;\n }\n}\n\n.gsrp-reddit-embed-comment-author {\n font-weight: 700;\n color: #0f1419;\n\n .gsrp-is-dark & {\n color: #f7f9fa;\n }\n}\n\n.gsrp-reddit-embed-comment-score {\n font-weight: 600;\n}\n\n.gsrp-reddit-embed-comment-date {\n opacity: 0.8;\n}\n\n.gsrp-reddit-embed-comment-body {\n font-size: 0.84em; /* Tightened from 0.88em */\n color: #3f4449;\n line-height: 1.4;\n\n .gsrp-is-dark & {\n color: #dbdbdb;\n }\n\n p {\n margin: 0 0 4px 0;\n &:last-child {\n margin-bottom: 0;\n }\n }\n}\n\n/* Pulsing Loading Skeletons for Immersive UX */\n.gsrp-reddit-embed-skeleton {\n margin: 12px 0 16px 0;\n padding: 14px;\n border-radius: 12px;\n background: #f7f9fa;\n border: 1px solid rgba(0, 0, 0, 0.05);\n animation: gsrp-skeleton-pulse 1.4s ease-in-out infinite;\n\n .gsrp-is-dark & {\n background: rgba(255, 255, 255, 0.03);\n border-color: rgba(255, 255, 255, 0.06);\n }\n}\n\n.gsrp-skeleton-header {\n display: flex;\n gap: 8px;\n margin-bottom: 12px;\n}\n\n.gsrp-skeleton-sub {\n width: 70px;\n height: 12px;\n border-radius: 4px;\n background: rgba(0, 0, 0, 0.08);\n\n .gsrp-is-dark & {\n background: rgba(255, 255, 255, 0.08);\n }\n}\n\n.gsrp-skeleton-author {\n width: 90px;\n height: 12px;\n border-radius: 4px;\n background: rgba(0, 0, 0, 0.05);\n\n .gsrp-is-dark & {\n background: rgba(255, 255, 255, 0.05);\n }\n}\n\n.gsrp-skeleton-title {\n width: 85%;\n height: 16px;\n border-radius: 4px;\n background: rgba(0, 0, 0, 0.08);\n margin-bottom: 14px;\n\n .gsrp-is-dark & {\n background: rgba(255, 255, 255, 0.08);\n }\n}\n\n.gsrp-skeleton-body {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.gsrp-skeleton-line {\n width: 100%;\n height: 10px;\n border-radius: 3px;\n background: rgba(0, 0, 0, 0.05);\n\n .gsrp-is-dark & {\n background: rgba(255, 255, 255, 0.05);\n }\n}\n\n@keyframes gsrp-skeleton-pulse {\n 0% {\n opacity: 0.6;\n }\n 50% {\n opacity: 1;\n }\n 100% {\n opacity: 0.6;\n }\n}\n\n@keyframes gsrp-reddit-slide-down {\n from {\n opacity: 0;\n transform: translateY(-6px);\n }\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n.gsrp-x-embed-container {\n width: 100%;\n max-width: 500px;\n height: auto;\n margin: 8px auto;\n overflow: hidden; /* Crop the iframe's corners and any bottom safety buffer */\n border-radius: 12px; /* Matches Twitter card's native rounded corners */\n animation: gsrp-youtube-slide 0.22s cubic-bezier(0.2, 0.8, 0.2, 1) forwards;\n\n iframe {\n position: static !important;\n display: block;\n width: 100% !important;\n height: 250px; /* Fallback default height during loading */\n border: none !important;\n background: transparent !important; /* Force transparent canvas to remove ugly white corners/edges */\n color-scheme: light dark;\n }\n}\n\n.gsrp-bsky-embed-container {\n width: 100%;\n max-width: 500px;\n height: auto;\n margin: 8px auto;\n overflow: hidden; /* Crop corners and any bottom safety buffer */\n border-radius: 12px; /* Matches Bluesky card's native rounded corners */\n animation: gsrp-youtube-slide 0.22s cubic-bezier(0.2, 0.8, 0.2, 1) forwards;\n\n iframe {\n position: static !important;\n width: 100% !important;\n height: auto; /* let the inline style injected by the SDK rule */\n border: none !important;\n background: transparent !important; /* Force transparent canvas to remove ugly white corners/edges */\n color-scheme: light dark;\n }\n\n .bluesky-embed {\n margin: 0 !important;\n max-width: 100% !important;\n }\n}\n\n.gsrp-mastodon-embed-container {\n width: 100%;\n max-width: 540px;\n height: auto;\n margin: 8px auto;\n overflow: hidden; /* Crop corners and any bottom safety buffer */\n border-radius: 8px; /* Matches Mastodon card's native rounded corners */\n animation: gsrp-youtube-slide 0.22s cubic-bezier(0.2, 0.8, 0.2, 1) forwards;\n\n iframe {\n position: static !important;\n width: 100% !important;\n height: 520px; /* Fallback default height during loading */\n border: none !important;\n background: transparent !important; /* Force transparent canvas to remove ugly white corners/edges */\n color-scheme: light dark;\n }\n}\n\n.gsrp-embed-loading {\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 24px;\n color: var(--gsrp-text-muted);\n font-size: 0.9em;\n font-style: italic;\n}\n\n/* --- Keyboard Accessibility Outlines Focus Rings --- */\n.gsrp-close-btn:focus-visible,\n.gsrp-maximize-btn:focus-visible,\n.gsrp-comments-filter-clear:focus-visible,\n.gsrp-comments-filter-prev:focus-visible,\n.gsrp-comments-filter-next:focus-visible,\n.gsrp-inline-sort-select:focus-visible,\n.gsrp-settings-close:focus-visible,\n.gsrp-settings-btn:focus-visible,\n.gsrp-settings-select:focus-visible,\n.gsrp-settings-checkbox:focus-visible {\n outline: 2px solid var(--gsrp-primary) !important;\n outline-offset: 2px !important;\n border-radius: 4px !important;\n}\n"; const sidePanelCss = "/* ==========================================================================\n Side-Panel Mode Styling (Premium GSRP Layout)\n ========================================================================== */\n\n/* Suppress normal tooltips under badges if side-panel mode is active */\n.gsrp-side-panel-active-body .gsrp-preview-trigger .gsrp-preview-tooltip {\n display: none !important;\n}\n\n/* Global Side-Panel Float container */\n.gsrp-side-panel {\n position: fixed !important;\n top: var(--gsrp-side-panel-top, 12px) !important;\n left: var(--gsrp-side-panel-left, calc(100vw - 544px)) !important;\n width: var(--gsrp-side-panel-width, 520px) !important;\n height: calc(100vh - var(--gsrp-side-panel-top, 12px) - 12px) !important;\n max-width: none !important;\n max-height: none !important;\n background: var(--gsrp-bg-tooltip) !important;\n color: var(--gsrp-text-main) !important;\n border: 1px solid var(--gsrp-border) !important;\n border-radius: 12px !important;\n box-shadow:\n 0 12px 48px rgba(0, 0, 0, 0.16),\n var(--gsrp-shadow) !important;\n z-index: 2147483645 !important; /* On top of search page results but below modals */\n display: none !important;\n flex-direction: column !important;\n overflow: hidden !important;\n opacity: 0 !important;\n transform: translateX(40px) !important;\n transition:\n opacity 0.3s cubic-bezier(0.2, 0.8, 0.2, 1),\n transform 0.3s cubic-bezier(0.2, 0.8, 0.2, 1) !important;\n pointer-events: none !important;\n white-space: normal !important;\n user-select: text !important;\n\n /* Theme colors */\n .gsrp-is-dark & {\n background: #22242a !important;\n border-color: #3c4043 !important;\n }\n\n &.gsrp-active {\n display: flex !important;\n opacity: 1 !important;\n transform: translateX(0) !important;\n pointer-events: auto !important;\n }\n\n &.gsrp-pinned {\n border: 1px solid var(--gsrp-border) !important;\n box-sizing: border-box !important;\n box-shadow: var(--gsrp-shadow-pinned) !important;\n }\n\n /* Invisible physical bridging tunnel extending leftward to safely capture and transition mouse cursor without leave triggers */\n &::before {\n content: '' !important;\n position: absolute !important;\n top: 0 !important;\n left: -40px !important;\n width: 40px !important;\n height: 100% !important;\n background: transparent !important;\n pointer-events: auto !important;\n z-index: -1 !important;\n }\n\n /* Disable the bridging tunnel entirely in Pinned state to prevent any invisible pointer/hover blockform on underlying elements */\n &.gsrp-pinned::before {\n pointer-events: none !important;\n display: none !important;\n }\n\n /* Invisible scrollbar safety net on the right of side-panel (extends to cover the screen edge gap) */\n\n /* Support existing tooltip styles and layout */\n font-family:\n system-ui,\n -apple-system,\n BlinkMacSystemFont,\n 'Segoe UI',\n Roboto,\n 'Helvetica Neue',\n Arial,\n sans-serif !important;\n font-size: var(--gsrp-preview-font, 13px) !important;\n line-height: 1.5 !important;\n\n /* Close & Maximize buttons should always show in side-panel */\n .gsrp-close-btn,\n .gsrp-maximize-btn {\n display: inline-block !important;\n visibility: visible !important;\n opacity: 1 !important;\n }\n\n /* Style the robotic summarize icon to be completely borderless & transparent in regular state */\n .gsrp-summarize-btn {\n background: transparent !important;\n border: none !important;\n outline: none !important;\n box-shadow: none !important;\n transition:\n transform 0.15s ease,\n background-color 0.2s ease !important;\n\n &:hover {\n background-color: var(--gsrp-bg-hover) !important;\n color: var(--gsrp-primary) !important;\n transform: scale(1.15) !important;\n }\n\n &:active {\n transform: scale(0.9) !important;\n }\n }\n\n /* Inside body of side-panel, override typical absolute positions */\n .gsrp-preview-title-container {\n position: sticky !important;\n top: 0 !important;\n background: var(--gsrp-bg-tooltip) !important;\n border-bottom: 1px solid var(--gsrp-border) !important;\n padding: 12px 16px !important;\n margin: 0 !important;\n z-index: 100 !important;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04) !important;\n display: flex !important;\n align-items: center !important;\n /* Sync border-radius with parent panel to avoid corner artifacts */\n border-top-left-radius: 12px !important;\n border-top-right-radius: 12px !important;\n\n .gsrp-is-dark & {\n background: #22242a !important;\n border-bottom-color: #3c4043 !important;\n }\n }\n\n /* Subdue internal preview title styles slightly when inside side-panel */\n .gsrp-preview-title {\n font-weight: 600 !important;\n }\n\n /* Styling for the internal comments section subtitle/filter bar with premium sticky behavior */\n .gsrp-comments-section-header {\n position: sticky !important;\n top: 0 !important;\n margin: 0 !important;\n padding: 6px 16px !important;\n background: var(\n --gsrp-bg-tooltip\n ) !important; /* Solid background to obscure comments scrolling behind */\n z-index: 10 !important;\n border-bottom: 1px solid var(--gsrp-border) !important; /* Distinct division line */\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.04); /* Subtle drop-shadow for premium depth */\n\n .gsrp-is-dark & {\n background: #22242a !important; /* Matches side-panel dark bg */\n border-bottom-color: #3c4043 !important;\n }\n\n .gsrp-preview-title-container {\n position: static !important;\n border: none !important;\n padding: 0 !important;\n background: transparent !important;\n box-shadow: none !important;\n z-index: auto !important;\n }\n }\n\n /* Maximize (Fullscreen Lightbox style) for side-panel mode */\n &.gsrp-maximized {\n position: fixed !important;\n top: 50% !important;\n left: 50% !important;\n right: auto !important;\n bottom: auto !important;\n width: min(850px, calc(100vw - 32px)) !important;\n height: calc(100vh - 32px) !important;\n transform: translate(-50%, -50%) !important;\n z-index: 2147483647 !important;\n border-radius: 16px !important;\n /* Hardware-accelerated smooth zoom-fade entrance transition (reusing established keyframes) */\n animation: gsrp-maximize-zoom 0.22s var(--gsrp-transition-ease) forwards !important;\n\n /* Override title container border radius for maximized panel */\n .gsrp-preview-title-container {\n border-top-left-radius: 16px !important;\n border-top-right-radius: 16px !important;\n }\n }\n\n /* High-density layout optimization specifically for side-panel mode */\n /* Keeps font sizes unchanged to protect readability, but reduces whitespace */\n\n .gsrp-preview-title-container {\n padding: 8px 12px !important; /* reduced from 12px 16px */\n }\n\n .gsrp-preview-body {\n padding: 8px 12px !important; /* reduced from 12px 16px */\n }\n\n .gsrp-preview-post-title {\n padding: 4px 12px !important; /* reduced from 4px 16px */\n margin-bottom: 6px !important;\n }\n\n .gsrp-preview-body p {\n margin-bottom: 6px !important; /* reduced from 10px */\n }\n\n .gsrp-comment-item {\n margin-bottom: 8px !important; /* reduced from 12px */\n padding-bottom: 8px !important; /* reduced from 12px */\n }\n\n .gsrp-reply-item {\n margin-top: 6px !important; /* reduced from 8px */\n\n > .gsrp-comment-content {\n padding: 6px 8px 6px 6px !important; /* reduced from 8px 8px 8px 6px */\n }\n }\n\n .gsrp-comment-header {\n margin-bottom: 2px !important; /* reduced from 4px */\n }\n\n .gsrp-carousel-container {\n margin-top: 6px !important;\n margin-bottom: 6px !important;\n }\n\n /* Optimize preview media dimensions to be majestic inside vertical panels */\n .gsrp-preview-media img,\n .gsrp-preview-media video {\n max-height: 400px !important;\n }\n .gsrp-preview-media iframe {\n max-height: 280px !important;\n }\n .gsrp-vimeo-embed-container {\n max-height: 280px !important;\n }\n}\n\n/* If any tooltip is pinned, or the side panel is pinned, disable pointer-events on other triggers to prevent visual clutter */\nbody:has(.gsrp-preview-trigger.gsrp-pinned) .gsrp-preview-trigger:not(.gsrp-pinned),\nbody:has(#gsrp-global-side-panel.gsrp-pinned) .gsrp-preview-trigger {\n pointer-events: none !important;\n}\n\n/* Card highlight effects when focused/pinned.\n `z-index` lifts the highlighted card above its siblings so the soft\n shadow ring (0 8px 24px) doesn't get clipped by adjacent cards with\n opaque backgrounds. Standard Google SERP cards are transparent so\n this was a no-op visually before, but CSE cards\n (`div.gsc-webResult.gsc-result`) and any future platform with\n opaque backgrounds need the explicit lift. `position: relative` is\n already set on the card from anchorCardForBadge() in inject.js. */\n.gsrp-card-focus-highlight {\n background: rgba(26, 115, 232, 0.08) !important; /* Premium light blue highlight tint */\n /* Outer soft ring (0 0 0 1px) prevents style conflicts and guarantees visibility, combined with elegant elevated shadow */\n box-shadow:\n 0 0 0 1px rgba(26, 115, 232, 0.15),\n 0 8px 24px rgba(26, 115, 232, 0.18) !important;\n border-radius: 8px !important;\n z-index: 5 !important;\n transition:\n background 0.2s ease,\n box-shadow 0.2s ease;\n\n .gsrp-is-dark & {\n background: rgba(\n 138,\n 180,\n 248,\n 0.12\n ) !important; /* Standout soft blue highlight on dark backgrounds */\n box-shadow:\n 0 0 0 1px rgba(138, 180, 248, 0.25),\n 0 8px 24px rgba(0, 0, 0, 0.35) !important;\n }\n}\n\n/* --- Bulletproof Flat Thread-Line Coloration (Guaranteed across Light/Dark and Tooltip/Side-Panel modes) --- */\n.gsrp-thread-colors .gsrp-comment-item[data-color-tier='0'] > .gsrp-thread-line,\n.gsrp-thread-colors .gsrp-reply-item[data-color-tier='0'] > .gsrp-thread-line {\n background: rgba(130, 130, 130, 0.4) !important;\n}\n.gsrp-thread-colors .gsrp-comment-item[data-color-tier='1'] > .gsrp-thread-line,\n.gsrp-thread-colors .gsrp-reply-item[data-color-tier='1'] > .gsrp-thread-line {\n background: rgba(195, 125, 115, 0.5) !important;\n}\n.gsrp-thread-colors .gsrp-comment-item[data-color-tier='2'] > .gsrp-thread-line,\n.gsrp-thread-colors .gsrp-reply-item[data-color-tier='2'] > .gsrp-thread-line {\n background: rgba(195, 165, 100, 0.5) !important;\n}\n.gsrp-thread-colors .gsrp-comment-item[data-color-tier='3'] > .gsrp-thread-line,\n.gsrp-thread-colors .gsrp-reply-item[data-color-tier='3'] > .gsrp-thread-line {\n background: rgba(145, 180, 100, 0.5) !important;\n}\n.gsrp-thread-colors .gsrp-comment-item[data-color-tier='4'] > .gsrp-thread-line,\n.gsrp-thread-colors .gsrp-reply-item[data-color-tier='4'] > .gsrp-thread-line {\n background: rgba(105, 175, 125, 0.5) !important;\n}\n.gsrp-thread-colors .gsrp-comment-item[data-color-tier='5'] > .gsrp-thread-line,\n.gsrp-thread-colors .gsrp-reply-item[data-color-tier='5'] > .gsrp-thread-line {\n background: rgba(100, 175, 165, 0.5) !important;\n}\n.gsrp-thread-colors .gsrp-comment-item[data-color-tier='6'] > .gsrp-thread-line,\n.gsrp-thread-colors .gsrp-reply-item[data-color-tier='6'] > .gsrp-thread-line {\n background: rgba(100, 150, 195, 0.5) !important;\n}\n.gsrp-thread-colors .gsrp-comment-item[data-color-tier='7'] > .gsrp-thread-line,\n.gsrp-thread-colors .gsrp-reply-item[data-color-tier='7'] > .gsrp-thread-line {\n background: rgba(130, 115, 195, 0.5) !important;\n}\n.gsrp-thread-colors .gsrp-comment-item[data-color-tier='8'] > .gsrp-thread-line,\n.gsrp-thread-colors .gsrp-reply-item[data-color-tier='8'] > .gsrp-thread-line {\n background: rgba(175, 120, 185, 0.5) !important;\n}\n.gsrp-thread-colors .gsrp-comment-item[data-color-tier='9'] > .gsrp-thread-line,\n.gsrp-thread-colors .gsrp-reply-item[data-color-tier='9'] > .gsrp-thread-line {\n background: rgba(190, 120, 150, 0.5) !important;\n}\n\n/* Dark Mode thread lines overrides (Matches whether .gsrp-is-dark is on .gsrp-side-panel, tooltip, or badge element) */\n.gsrp-thread-colors .gsrp-is-dark .gsrp-comment-item[data-color-tier='0'] > .gsrp-thread-line,\n.gsrp-thread-colors .gsrp-is-dark .gsrp-reply-item[data-color-tier='0'] > .gsrp-thread-line,\n.gsrp-thread-colors .gsrp-is-dark.gsrp-comment-item[data-color-tier='0'] > .gsrp-thread-line,\n.gsrp-thread-colors .gsrp-is-dark.gsrp-reply-item[data-color-tier='0'] > .gsrp-thread-line {\n background: rgba(155, 160, 165, 0.4) !important;\n}\n\n.gsrp-thread-colors .gsrp-is-dark .gsrp-comment-item[data-color-tier='1'] > .gsrp-thread-line,\n.gsrp-thread-colors .gsrp-is-dark .gsrp-reply-item[data-color-tier='1'] > .gsrp-thread-line,\n.gsrp-thread-colors .gsrp-is-dark.gsrp-comment-item[data-color-tier='1'] > .gsrp-thread-line,\n.gsrp-thread-colors .gsrp-is-dark.gsrp-reply-item[data-color-tier='1'] > .gsrp-thread-line {\n background: rgba(220, 155, 145, 0.5) !important;\n}\n\n.gsrp-thread-colors .gsrp-is-dark .gsrp-comment-item[data-color-tier='2'] > .gsrp-thread-line,\n.gsrp-thread-colors .gsrp-is-dark .gsrp-reply-item[data-color-tier='2'] > .gsrp-thread-line,\n.gsrp-thread-colors .gsrp-is-dark.gsrp-comment-item[data-color-tier='2'] > .gsrp-thread-line,\n.gsrp-thread-colors .gsrp-is-dark.gsrp-reply-item[data-color-tier='2'] > .gsrp-thread-line {\n background: rgba(220, 190, 130, 0.5) !important;\n}\n\n.gsrp-thread-colors .gsrp-is-dark .gsrp-comment-item[data-color-tier='3'] > .gsrp-thread-line,\n.gsrp-thread-colors .gsrp-is-dark .gsrp-reply-item[data-color-tier='3'] > .gsrp-thread-line,\n.gsrp-thread-colors .gsrp-is-dark.gsrp-comment-item[data-color-tier='3'] > .gsrp-thread-line,\n.gsrp-thread-colors .gsrp-is-dark.gsrp-reply-item[data-color-tier='3'] > .gsrp-thread-line {\n background: rgba(175, 205, 135, 0.5) !important;\n}\n\n.gsrp-thread-colors .gsrp-is-dark .gsrp-comment-item[data-color-tier='4'] > .gsrp-thread-line,\n.gsrp-thread-colors .gsrp-is-dark .gsrp-reply-item[data-color-tier='4'] > .gsrp-thread-line,\n.gsrp-thread-colors .gsrp-is-dark.gsrp-comment-item[data-color-tier='4'] > .gsrp-thread-line,\n.gsrp-thread-colors .gsrp-is-dark.gsrp-reply-item[data-color-tier='4'] > .gsrp-thread-line {\n background: rgba(140, 200, 160, 0.5) !important;\n}\n\n.gsrp-thread-colors .gsrp-is-dark .gsrp-comment-item[data-color-tier='5'] > .gsrp-thread-line,\n.gsrp-thread-colors .gsrp-is-dark .gsrp-reply-item[data-color-tier='5'] > .gsrp-thread-line,\n.gsrp-thread-colors .gsrp-is-dark.gsrp-comment-item[data-color-tier='5'] > .gsrp-thread-line,\n.gsrp-thread-colors .gsrp-is-dark.gsrp-reply-item[data-color-tier='5'] > .gsrp-thread-line {\n background: rgba(130, 200, 190, 0.5) !important;\n}\n\n.gsrp-thread-colors .gsrp-is-dark .gsrp-comment-item[data-color-tier='6'] > .gsrp-thread-line,\n.gsrp-thread-colors .gsrp-is-dark .gsrp-reply-item[data-color-tier='6'] > .gsrp-thread-line,\n.gsrp-thread-colors .gsrp-is-dark.gsrp-comment-item[data-color-tier='6'] > .gsrp-thread-line,\n.gsrp-thread-colors .gsrp-is-dark.gsrp-reply-item[data-color-tier='6'] > .gsrp-thread-line {\n background: rgba(135, 175, 220, 0.5) !important;\n}\n\n.gsrp-thread-colors .gsrp-is-dark .gsrp-comment-item[data-color-tier='7'] > .gsrp-thread-line,\n.gsrp-thread-colors .gsrp-is-dark .gsrp-reply-item[data-color-tier='7'] > .gsrp-thread-line,\n.gsrp-thread-colors .gsrp-is-dark.gsrp-comment-item[data-color-tier='7'] > .gsrp-thread-line,\n.gsrp-thread-colors .gsrp-is-dark.gsrp-reply-item[data-color-tier='7'] > .gsrp-thread-line {\n background: rgba(160, 150, 220, 0.5) !important;\n}\n\n.gsrp-thread-colors .gsrp-is-dark .gsrp-comment-item[data-color-tier='8'] > .gsrp-thread-line,\n.gsrp-thread-colors .gsrp-is-dark .gsrp-reply-item[data-color-tier='8'] > .gsrp-thread-line,\n.gsrp-thread-colors .gsrp-is-dark.gsrp-comment-item[data-color-tier='8'] > .gsrp-thread-line,\n.gsrp-thread-colors .gsrp-is-dark.gsrp-reply-item[data-color-tier='8'] > .gsrp-thread-line {\n background: rgba(200, 155, 215, 0.5) !important;\n}\n\n.gsrp-thread-colors .gsrp-is-dark .gsrp-comment-item[data-color-tier='9'] > .gsrp-thread-line,\n.gsrp-thread-colors .gsrp-is-dark .gsrp-reply-item[data-color-tier='9'] > .gsrp-thread-line,\n.gsrp-thread-colors .gsrp-is-dark.gsrp-comment-item[data-color-tier='9'] > .gsrp-thread-line,\n.gsrp-thread-colors .gsrp-is-dark.gsrp-reply-item[data-color-tier='9'] > .gsrp-thread-line {\n background: rgba(215, 150, 175, 0.5) !important;\n}\n\n/* --- High-fidelity Premium Code & Pre Block Styling for GSRP Tooltips, Panels & Embed Cards --- */\n.gsrp-preview-tooltip,\n.gsrp-preview-body,\n.gsrp-side-panel,\n.gsrp-comment-item,\n.gsrp-reddit-embed-container {\n /* Precise Inline Code Styling (Strictly isolates standalone inline code tags to avoid nesting conflicts) */\n :not(pre) > code {\n font-family: var(\n --gsrp-font-mono,\n ui-monospace,\n SFMono-Regular,\n Menlo,\n Monaco,\n Consolas,\n 'Liberation Mono',\n 'Courier New',\n monospace\n ) !important;\n font-size: 0.88em !important;\n background: rgba(0, 0, 0, 0.05) !important;\n color: #c93b2b !important;\n padding: 2px 5px !important;\n border-radius: 4px !important;\n word-break: break-word !important;\n white-space: pre-wrap !important;\n\n .gsrp-is-dark & {\n background: rgba(255, 255, 255, 0.08) !important;\n color: #ff7b72 !important;\n }\n }\n\n /* Standardized Block-level Code Block Container */\n pre {\n background: #f6f8fa !important;\n color: #24292f !important; /* Defensively guarantee code foreground color in light mode */\n border: 1px solid rgba(0, 0, 0, 0.08) !important;\n border-left: 1px solid rgba(0, 0, 0, 0.08) !important; /* Defensively override any legacy blue left border to prevent visual clutter */\n border-radius: 8px !important;\n padding: 12px 16px !important;\n margin: 10px 0 !important;\n box-sizing: border-box !important;\n position: relative !important; /* Keep relative layout anchor for the copy button */\n overflow: visible !important; /* Prevent the container itself from scrolling to keep the button fixed! */\n\n &,\n & code {\n color: #24292f !important; /* Forces light mode foreground colors on raw code elements */\n }\n\n code {\n background: transparent !important;\n color: #24292f !important;\n padding: 0 !important;\n border-radius: 0 !important;\n font-size: 0.88em !important;\n white-space: pre !important; /* Keep original multi-line indentation */\n word-break: normal !important;\n word-wrap: normal !important;\n display: block !important;\n overflow-x: auto !important; /* Make code element the designated scrolling viewport! */\n max-width: 100% !important;\n }\n\n /* --- High-fidelity Syntax Highlighting (Light Mode / GitHub Light Style) --- */\n .gsrp-code-comment {\n color: #6a737d !important;\n font-style: italic;\n }\n .gsrp-code-string {\n color: #032f62 !important;\n }\n .gsrp-code-keyword {\n color: #d73a49 !important;\n font-weight: 600 !important;\n }\n .gsrp-code-number {\n color: #005cc5 !important;\n }\n\n .gsrp-is-dark & {\n background: #161b22 !important;\n color: #c9d1d9 !important; /* Defensively guarantee code foreground color in dark mode */\n border-color: rgba(255, 255, 255, 0.15) !important;\n border-left: 1px solid rgba(255, 255, 255, 0.15) !important;\n\n &,\n & code {\n color: #c9d1d9 !important;\n }\n\n code {\n color: #c9d1d9 !important;\n background: transparent !important; /* Defensively guarantee no background bleeding */\n }\n\n /* --- High-fidelity Syntax Highlighting (Dark Mode / GitHub Dark Style) --- */\n .gsrp-code-comment {\n color: #8b949e !important;\n }\n .gsrp-code-string {\n color: #a5d6ff !important;\n }\n .gsrp-code-keyword {\n color: #ff7b72 !important;\n font-weight: 600 !important;\n }\n .gsrp-code-number {\n color: #79c0ff !important;\n }\n }\n }\n}\n\n/* Defensively force high-contrast dark text on code blocks in light mode using absolute specificity overrides */\nhtml:not(.gsrp-is-dark) .gsrp-preview-tooltip pre,\nhtml:not(.gsrp-is-dark) .gsrp-preview-body pre,\nhtml:not(.gsrp-is-dark) .gsrp-side-panel pre,\nhtml:not(.gsrp-is-dark) .gsrp-comment-item pre,\nhtml:not(.gsrp-is-dark) .gsrp-reddit-embed-container pre,\nhtml:not(.gsrp-is-dark) .gsrp-preview-tooltip pre code,\nhtml:not(.gsrp-is-dark) .gsrp-preview-body pre code,\nhtml:not(.gsrp-is-dark) .gsrp-side-panel pre code,\nhtml:not(.gsrp-is-dark) .gsrp-comment-item pre code,\nhtml:not(.gsrp-is-dark) .gsrp-reddit-embed-container pre code {\n color: #24292f !important;\n}\n\n/* --- Bulletproof Defensive List Restoration System (Guaranteed against extreme Host Page CSS resets) --- */\n/* Target UL nested LIs directly to shatter list-style: none overrides on the list item itself */\n.gsrp-preview-tooltip ul > li,\n.gsrp-preview-body ul > li,\n.gsrp-side-panel ul > li,\n.gsrp-comment-item ul > li,\n.gsrp-comment-body ul > li,\n.gsrp-reddit-embed-container ul > li,\n.md ul > li {\n list-style-type: disc !important;\n list-style-position: outside !important; /* Securely place markers outside to prevent block-level element wraps */\n display: list-item !important; /* Shatter Google's display flex or block resets */\n margin-bottom: 4px !important;\n}\n\n/* Target OL nested LIs directly to bypass direct list-style specificity overrides on list items */\n.gsrp-preview-tooltip ol > li,\n.gsrp-preview-body ol > li,\n.gsrp-side-panel ol > li,\n.gsrp-comment-item ol > li,\n.gsrp-comment-body ol > li,\n.gsrp-reddit-embed-container ol > li,\n.md ol > li {\n list-style-type: decimal !important;\n list-style-position: outside !important; /* Securely place numeric indices outside to prevent block-level element wraps */\n display: list-item !important; /* Shatter Google's display flex or block resets */\n margin-bottom: 4px !important;\n}\n\n/* Base resets on containers to ensure margins are controlled and markers are beautifully aligned */\n.gsrp-preview-tooltip ul,\n.gsrp-preview-body ul,\n.gsrp-side-panel ul,\n.gsrp-comment-item ul,\n.gsrp-comment-body ul,\n.gsrp-reddit-embed-container ul,\n.md ul,\n.gsrp-preview-tooltip ol,\n.gsrp-preview-body ol,\n.gsrp-side-panel ol,\n.gsrp-comment-item ol,\n.gsrp-comment-body ol,\n.gsrp-reddit-embed-container ol,\n.md ol {\n margin: 8px 0 8px 0 !important;\n padding-left: 20px !important; /* Contain outside list markers comfortably within GSRP container borders */\n}\n\n/* Inline nested paragraphs inside list items to guarantee correct vertical baseline alignment and prevent wrap splits */\n.gsrp-preview-tooltip li p,\n.gsrp-preview-body li p,\n.gsrp-side-panel li p,\n.gsrp-comment-item li p,\n.gsrp-comment-body li p,\n.gsrp-reddit-embed-container li p,\n.md li p {\n margin: 0 !important;\n display: inline !important;\n}\n\n/* --- Premium Inline Reddit Embed Comments Footer Bar --- */\n.gsrp-reddit-embed-comments-footer {\n display: flex !important;\n align-items: center !important;\n justify-content: space-between !important;\n margin-top: 6px !important; /* Tightened from 10px */\n padding: 6px 10px !important; /* Tightened from 10px 14px */\n background: rgba(0, 0, 0, 0.02) !important;\n border: 1px solid rgba(0, 0, 0, 0.04) !important;\n border-radius: 6px !important;\n text-decoration: none !important;\n color: #0079d3 !important; /* Reddit interactive blue */\n font-size: 0.8em !important; /* Tightened from 0.88em */\n font-weight: 600 !important;\n transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1) !important;\n box-sizing: border-box !important;\n cursor: pointer !important;\n\n .gsrp-is-dark & {\n background: rgba(255, 255, 255, 0.03) !important;\n border-color: rgba(255, 255, 255, 0.05) !important;\n color: #3897f0 !important;\n }\n\n /* Hover State with gentle elevating float, glow and signature Reddit Orange red swap */\n &:hover {\n background: rgba(255, 69, 0, 0.06) !important; /* Reddit Orange tint */\n border-color: rgba(255, 69, 0, 0.15) !important;\n color: #ff4500 !important; /* Signature Reddit Orange-Red */\n transform: translateY(-1px) !important;\n box-shadow: 0 4px 12px rgba(255, 69, 0, 0.08) !important;\n\n .gsrp-is-dark & {\n background: rgba(255, 69, 0, 0.12) !important;\n border-color: rgba(255, 69, 0, 0.25) !important;\n color: #ff5722 !important;\n box-shadow: 0 4px 12px rgba(255, 69, 0, 0.15) !important;\n }\n\n /* micro-animation: Slide the arrow SVG slightly to the right */\n svg {\n transform: translateX(3px) !important;\n }\n }\n\n /* Active State press-down micro-feedback */\n &:active {\n transform: translateY(0.5px) scale(0.99) !important;\n box-shadow: none !important;\n }\n\n /* Embedded SVG Icon specs */\n svg {\n transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1) !important;\n flex-shrink: 0 !important;\n color: currentColor !important;\n }\n\n /* Proposal C: Accessibility Focus Ring */\n &:focus-visible {\n outline: 2px solid #1a73e8 !important;\n outline-offset: 1px !important;\n }\n}\n\n/* Proposal C: Accessibility Focus Rings for Interactive Embed Trigger Buttons */\n.gsrp-reddit-embed-btn:focus-visible {\n outline: 2px solid #1a73e8 !important;\n outline-offset: 1px !important;\n}\n\n/* --- Smooth Image Carousel Cross-fade Animation --- */\n@keyframes gsrp-carousel-fade-in {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n}\n\n/* --- Premium Inline Imgur Embed Button & Container CSS --- */\n.gsrp-imgur-embed-btn {\n display: inline-flex !important;\n align-items: center !important;\n justify-content: center !important;\n padding: 2px 5px !important;\n margin: 0 4px !important;\n height: 19px !important;\n font-size: 11px !important;\n font-weight: 600 !important;\n border-radius: 4px !important;\n text-decoration: none !important;\n cursor: pointer !important;\n box-sizing: border-box !important;\n transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1) !important;\n vertical-align: middle !important;\n\n /* Imgur Mint Green Theme */\n background: rgba(27, 183, 110, 0.08) !important;\n border: 1px solid rgba(27, 183, 110, 0.15) !important;\n color: #1bb76e !important;\n}\n\n.gsrp-imgur-embed-btn .gsrp-embed-btn-logo {\n width: 14px !important;\n height: 14px !important;\n margin-right: 4px !important;\n flex-shrink: 0 !important;\n}\n\n.gsrp-imgur-embed-btn .gsrp-embed-chevron {\n width: 10px !important;\n height: 10px !important;\n margin-left: 4px !important;\n transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1) !important;\n color: currentColor !important;\n}\n\n.gsrp-imgur-embed-btn:hover {\n background: rgba(27, 183, 110, 0.15) !important;\n border-color: rgba(27, 183, 110, 0.3) !important;\n color: #148f54 !important;\n transform: translateY(-0.5px) !important;\n box-shadow: 0 2px 6px rgba(27, 183, 110, 0.12) !important;\n}\n\n.gsrp-is-dark .gsrp-imgur-embed-btn {\n background: rgba(27, 183, 110, 0.12) !important;\n border-color: rgba(27, 183, 110, 0.2) !important;\n color: #22c377 !important;\n}\n\n.gsrp-is-dark .gsrp-imgur-embed-btn:hover {\n background: rgba(27, 183, 110, 0.2) !important;\n border-color: rgba(27, 183, 110, 0.4) !important;\n color: #26d07f !important;\n box-shadow: 0 2px 8px rgba(27, 183, 110, 0.2) !important;\n}\n\n.gsrp-imgur-embed-btn:active {\n transform: translateY(0.5px) scale(0.98) !important;\n box-shadow: none !important;\n}\n\n.gsrp-imgur-embed-btn:focus-visible {\n outline: 2px solid #1bb76e !important;\n outline-offset: 1px !important;\n}\n\n.gsrp-imgur-embed-btn.gsrp-active .gsrp-embed-chevron {\n transform: rotate(180deg) !important;\n}\n\n/* Imgur Embed Container layout */\n.gsrp-imgur-embed-container {\n display: block !important;\n width: 100% !important;\n clear: both !important;\n float: none !important;\n margin-top: 8px !important;\n margin-bottom: 8px !important;\n padding: 8px !important;\n border-radius: 8px !important;\n border: 1px dashed rgba(27, 183, 110, 0.25) !important;\n background: rgba(27, 183, 110, 0.02) !important;\n box-sizing: border-box !important;\n animation: gsrp-carousel-fade-in 0.25s ease-out !important;\n}\n\n.gsrp-is-dark .gsrp-imgur-embed-container {\n background: rgba(27, 183, 110, 0.03) !important;\n border-color: rgba(27, 183, 110, 0.2) !important;\n}\n\n/* GSRP Unified Carousel & Gallery Layout spec (Applicable to both Imgur and Reddit native Gallery) */\n.gsrp-carousel {\n position: relative !important;\n overflow: hidden !important;\n}\n\n/* Limit heights and set dark backgrounds specifically for Imgur embedded carousels */\n.gsrp-imgur-embed-container .gsrp-carousel {\n max-height: 520px !important;\n background: rgba(0, 0, 0, 0.05) !important;\n border-radius: 6px !important;\n}\n\n.gsrp-imgur-embed-container .gsrp-carousel:has(.gsrp-is-expanded),\n.gsrp-imgur-embed-container .gsrp-carousel:has(.gsrp-all-captions-expanded) {\n max-height: none !important;\n}\n\n.gsrp-preview-media-gallery.gsrp-all-captions-expanded\n .gsrp-preview-media-caption.gsrp-collapsible {\n max-height: none !important;\n}\n\n.gsrp-preview-media-gallery.gsrp-all-captions-expanded .gsrp-caption-fade {\n opacity: 0 !important;\n pointer-events: none !important;\n}\n\n.gsrp-is-dark .gsrp-imgur-embed-container .gsrp-carousel {\n background: rgba(0, 0, 0, 0.2) !important;\n}\n\n/* 🎯 Universal gallery item: reset to let it expand naturally with no ugly black backgrounds */\n.gsrp-gallery-item {\n position: relative !important; /* 🎯 Ensures absolute elements like counter/dims align perfectly on the top corners! */\n}\n\n/* 🎯 Apply width, display, and positioning properties to all carousel slides globally */\n.gsrp-carousel .gsrp-gallery-item {\n width: 100% !important;\n display: none !important;\n box-sizing: border-box !important;\n}\n\n/* 🎯 Imgur embedded gallery items now keep a clean, transparent background to avoid ugly black shapes */\n.gsrp-imgur-embed-container .gsrp-gallery-item {\n background: transparent !important;\n}\n\n/* 🎯 Reddit native gallery carousel slides keep a clean, elegant, transparent backdrop to blend with themes */\n.gsrp-preview-media-gallery .gsrp-gallery-item {\n background: transparent !important;\n}\n\n.gsrp-carousel .gsrp-gallery-item.gsrp-carousel-active {\n display: block !important;\n}\n\n/* Image/video constraints inside carousels or embeds */\n.gsrp-carousel .gsrp-gallery-item img,\n.gsrp-carousel .gsrp-gallery-item video,\n.gsrp-imgur-embed-container .gsrp-gallery-item img,\n.gsrp-imgur-embed-container .gsrp-gallery-item video {\n max-width: 100% !important;\n max-height: 350px !important;\n width: auto !important;\n height: auto !important;\n object-fit: contain !important;\n margin: 0 auto !important;\n display: block !important;\n}\n\n.gsrp-imgur-embed-title {\n font-size: 0.82em !important;\n font-weight: 700 !important;\n color: var(--gsrp-primary, #1bb76e) !important;\n padding: 2px 4px !important;\n margin: 0 0 4px 0 !important;\n letter-spacing: 0.2px !important;\n white-space: nowrap !important;\n overflow: hidden !important;\n text-overflow: ellipsis !important;\n user-select: text !important;\n -webkit-user-select: text !important;\n cursor: text !important;\n}\n\n.gsrp-preview-media-caption {\n user-select: text !important;\n -webkit-user-select: text !important;\n cursor: text !important;\n padding: 5px 10px !important; /* Compact vertical padding */\n background: rgba(0, 0, 0, 0.05) !important; /* High-contrast light grey by default */\n border-radius: 6px !important;\n margin: 4px 12px 6px 12px !important; /* Compact margins */\n font-size: 0.85em !important;\n color: var(--gsrp-text, #3c4043) !important; /* High-contrast text by default */\n line-height: 1.5 !important;\n text-align: left !important;\n word-break: break-word !important;\n overflow-wrap: break-word !important;\n transition: max-height 0.25s cubic-bezier(0.2, 0.8, 0.2, 1) !important;\n}\n\n.gsrp-is-dark .gsrp-preview-media-caption {\n background: rgba(0, 0, 0, 0.4) !important; /* Sleek dark theme background */\n color: var(--gsrp-text-sub, #9aa0a6) !important; /* Comforting dark theme text */\n}\n\n.gsrp-preview-media-caption.gsrp-collapsible {\n max-height: 110px !important;\n overflow: hidden !important;\n position: relative !important;\n}\n\n.gsrp-preview-media-caption.gsrp-collapsible.gsrp-is-expanded {\n max-height: none !important;\n}\n\n/* 🎯 Elegant fading overlay cutout mask to signal truncated text gracefully without scrollbars */\n.gsrp-caption-fade {\n position: absolute !important;\n bottom: 0 !important;\n left: 0 !important;\n right: 0 !important;\n height: 42px !important;\n background: linear-gradient(to bottom, transparent, rgba(0, 0, 0, 0.65)) !important;\n pointer-events: none !important;\n border-bottom-left-radius: 6px !important;\n border-bottom-right-radius: 6px !important;\n transition: opacity 0.2s ease !important;\n}\n\n.gsrp-preview-media-caption.gsrp-is-expanded .gsrp-caption-fade {\n opacity: 0 !important;\n pointer-events: none !important;\n}\n\n/* 🎯 Luxury mint-green expand/collapse control toggle button */\n.gsrp-caption-toggle-btn {\n display: block !important;\n margin: -4px auto 12px auto !important;\n background: rgba(27, 183, 110, 0.08) !important;\n border: 1px solid rgba(27, 183, 110, 0.15) !important;\n color: var(--gsrp-primary, #1bb76e) !important;\n font-size: 11px !important;\n font-weight: 600 !important;\n padding: 4px 14px !important;\n border-radius: 20px !important;\n cursor: pointer !important;\n transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1) !important;\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08) !important;\n outline: none !important;\n user-select: none !important;\n}\n\n.gsrp-caption-toggle-btn:hover {\n background: rgba(27, 183, 110, 0.16) !important;\n border-color: rgba(27, 183, 110, 0.3) !important;\n color: #26d07f !important;\n transform: translateY(-0.5px) !important;\n box-shadow: 0 3px 6px rgba(27, 183, 110, 0.15) !important;\n}\n\n.gsrp-caption-toggle-btn:active {\n transform: translateY(0.5px) !important;\n box-shadow: none !important;\n}\n\n.gsrp-is-dark .gsrp-caption-toggle-btn {\n background: rgba(27, 183, 110, 0.12) !important;\n border-color: rgba(27, 183, 110, 0.25) !important;\n}\n\n.gsrp-is-dark .gsrp-caption-toggle-btn:hover {\n background: rgba(27, 183, 110, 0.22) !important;\n border-color: rgba(27, 183, 110, 0.45) !important;\n}\n\n.gsrp-preview-media-caption a,\n.gsrp-preview-media-caption a.gsrp-caption-link {\n color: var(--gsrp-primary, #1bb76e) !important;\n text-decoration: none !important;\n word-break: break-all !important;\n transition: color 0.15s ease !important;\n}\n\n.gsrp-preview-media-caption a:hover,\n.gsrp-preview-media-caption a.gsrp-caption-link:hover {\n color: #26d07f !important;\n text-decoration: underline !important;\n}\n\n/* Skeleton Loading screen for Imgur */\n.gsrp-imgur-skeleton {\n display: flex !important;\n flex-direction: column !important;\n align-items: center !important;\n justify-content: center !important;\n height: 180px !important;\n border-radius: 6px !important;\n background: rgba(0, 0, 0, 0.03) !important;\n color: #1bb76e !important;\n font-size: 12px !important;\n font-weight: 600 !important;\n gap: 8px !important;\n}\n\n.gsrp-is-dark .gsrp-imgur-skeleton {\n background: rgba(255, 255, 255, 0.03) !important;\n}\n\n.gsrp-imgur-skeleton svg {\n width: 24px !important;\n height: 24px !important;\n animation: gsrp-spin 1s linear infinite !important;\n}\n\n@keyframes gsrp-spin {\n from {\n transform: rotate(0deg);\n }\n to {\n transform: rotate(360deg);\n }\n}\n\n/* --- AI Summarization Card Styles inside Side-Panel --- */\n.gsrp-ai-summary-card {\n margin: 8px 12px 4px 12px !important;\n padding: 8px 12px !important;\n border: 1px solid var(--gsrp-border) !important;\n border-radius: 8px !important;\n background: linear-gradient(\n 135deg,\n var(--gsrp-bg-code) 0%,\n var(--gsrp-bg-tooltip) 100%\n ) !important;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04) !important;\n transition: all 0.3s cubic-bezier(0.2, 0.8, 0.2, 1) !important;\n display: flex !important;\n flex-direction: column !important;\n gap: 6px !important;\n overflow: hidden !important;\n flex-shrink: 0 !important;\n\n .gsrp-is-dark & {\n border-color: #3c4043 !important;\n background: linear-gradient(135deg, rgba(255, 255, 255, 0.08) 0%, #22242a 100%) !important;\n }\n\n &.gsrp-collapsed {\n padding-top: 8px !important;\n padding-bottom: 8px !important;\n gap: 0 !important;\n\n .gsrp-ai-summary-body {\n max-height: 0 !important;\n opacity: 0 !important;\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n margin-top: 0 !important;\n pointer-events: none !important;\n }\n\n .gsrp-ai-summary-toggle-icon {\n transform: rotate(180deg) !important;\n }\n }\n}\n\n.gsrp-ai-summary-header {\n display: flex !important;\n align-items: center !important;\n justify-content: space-between !important;\n font-weight: bold !important;\n color: var(--gsrp-primary) !important;\n font-size: 1.05em !important;\n user-select: none !important;\n}\n\n.gsrp-ai-summary-title {\n display: inline-flex !important;\n align-items: center !important;\n gap: 6px !important;\n}\n\n.gsrp-ai-summary-timer {\n font-size: 0.85em !important;\n font-weight: normal !important;\n color: var(--gsrp-text-sub) !important;\n margin-left: 6px !important;\n}\n\n.gsrp-ai-summary-toggle,\n.gsrp-ai-summary-copy {\n display: inline-flex !important;\n align-items: center !important;\n justify-content: center !important;\n width: 24px !important;\n height: 24px !important;\n border: none !important;\n background: transparent !important;\n box-shadow: none !important;\n cursor: pointer !important;\n color: var(--gsrp-text-sub) !important;\n border-radius: 50% !important;\n transition:\n background 0.2s ease,\n color 0.2s ease,\n transform 0.15s ease !important;\n padding: 0 !important;\n outline: none !important;\n box-sizing: border-box !important;\n\n &:hover {\n background: var(--gsrp-bg-hover) !important;\n color: var(--gsrp-primary) !important;\n transform: scale(1.1) !important;\n }\n\n &:active {\n transform: scale(0.9) !important;\n }\n\n &.gsrp-copied {\n color: var(--gsrp-success) !important;\n background-color: color-mix(in srgb, var(--gsrp-success) 12%, transparent) !important;\n }\n}\n\n.gsrp-ai-summary-toggle-icon {\n transition: transform 0.3s cubic-bezier(0.2, 0.8, 0.2, 1) !important;\n}\n\n.gsrp-ai-summary-body {\n font-size: 0.95em !important;\n line-height: 1.45 !important;\n color: var(--gsrp-text-main) !important;\n transition: all 0.3s cubic-bezier(0.2, 0.8, 0.2, 1) !important;\n max-height: 260px !important;\n opacity: 1 !important;\n overflow-y: auto !important;\n scrollbar-width: thin !important;\n\n p {\n margin: 0 0 6px 0 !important;\n &:last-child {\n margin-bottom: 0 !important;\n }\n }\n\n strong {\n color: var(--gsrp-primary) !important;\n }\n\n ul {\n margin: 2px 0 4px 0 !important;\n padding-left: 20px !important;\n list-style-type: disc !important;\n }\n\n li {\n margin-bottom: 2px !important;\n }\n}\n\n.gsrp-ai-summary-loading {\n display: flex !important;\n align-items: center !important;\n gap: 8px !important;\n color: var(--gsrp-text-sub) !important;\n padding: 8px 0 !important;\n}\n\n.gsrp-pulse-dot {\n width: 8px !important;\n height: 8px !important;\n background-color: var(--gsrp-primary) !important;\n border-radius: 50% !important;\n display: inline-block !important;\n animation: gsrp-pulse-animation 1.4s infinite ease-in-out both !important;\n}\n\n.gsrp-ai-summary-error {\n color: var(--gsrp-danger) !important;\n padding: 8px 0 !important;\n display: flex !important;\n align-items: center !important;\n gap: 6px !important;\n font-weight: 500 !important;\n}\n\n@keyframes gsrp-pulse-animation {\n 0%,\n 80%,\n 100% {\n transform: scale(0);\n opacity: 0.3;\n }\n 40% {\n transform: scale(1);\n opacity: 1;\n }\n}\n"; const lightboxCss = "/* ==========================================================================\n GSRP Lightbox Rich Metadata Info Panel & Toggle Button Styles\n ========================================================================== */\n\n/* The glassmorphism bottom panel (Sleek Compact Rectangular Design) */\n/* Bottom dynamic container to group micro-thumbnails and info panel closely */\n.gsrp-lightbox-bottom-container {\n position: absolute;\n bottom: 16px;\n left: 50%;\n transform: translateX(-50%);\n display: flex;\n flex-direction: column-reverse; /* Key: thumbnails on top, captions on bottom */\n align-items: center;\n gap: 8px; /* Perfectly tight 8px gap */\n width: 100%;\n pointer-events: none; /* Let clicks pass through empty spaces */\n z-index: 100003;\n}\n\n/* Light/Dark themes follow general container overlays. When the parent is in light mode, the bottom container children look spectacular too. */\n\n/* The glassmorphism bottom panel (Sleek Compact Rectangular Design) */\n.gsrp-lightbox-info-panel {\n width: calc(100% - 32px);\n max-width: 640px;\n background: rgba(15, 15, 15, 0.55);\n backdrop-filter: blur(12px) saturate(120%);\n -webkit-backdrop-filter: blur(12px) saturate(120%);\n border: 1px solid rgba(255, 255, 255, 0.06);\n border-radius: 4px; /* Crisp corners instead of 8px rounding */\n padding: 8px 14px;\n box-sizing: border-box;\n pointer-events: auto; /* Re-enable pointer events for interactions */\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);\n transition:\n opacity 0.3s cubic-bezier(0.2, 0.8, 0.2, 1),\n transform 0.3s cubic-bezier(0.2, 0.8, 0.2, 1),\n max-height 0.3s cubic-bezier(0.2, 0.8, 0.2, 1),\n padding 0.3s cubic-bezier(0.2, 0.8, 0.2, 1),\n border-width 0.3s cubic-bezier(0.2, 0.8, 0.2, 1),\n margin 0.3s cubic-bezier(0.2, 0.8, 0.2, 1);\n transform-origin: bottom center;\n max-height: 300px; /* Enable transition on collapse */\n overflow: hidden;\n}\n\n/* Light mode override if needed (GSRP overlay is mostly dark but keeps contrast) */\n.gsrp-lightbox-overlay.gsrp-is-light .gsrp-lightbox-info-panel {\n background: rgba(255, 255, 255, 0.85);\n border: 1px solid rgba(0, 0, 0, 0.06);\n box-shadow: 0 12px 32px rgba(0, 0, 0, 0.15);\n}\n\n/* The album total title label */\n.gsrp-lightbox-title {\n font-size: 0.82em;\n font-weight: 700;\n color: var(--gsrp-primary, #1bb76e);\n text-transform: uppercase;\n letter-spacing: 0.06em;\n margin-bottom: 3px;\n white-space: nowrap;\n overflow: hidden;\n text-overflow: ellipsis;\n line-height: 1.3;\n}\n\n/* The single image caption description text */\n.gsrp-lightbox-caption {\n font-size: 0.9em;\n line-height: 1.45;\n color: rgba(255, 255, 255, 0.92);\n font-weight: 400;\n user-select: text;\n -webkit-user-select: text;\n cursor: auto;\n word-break: break-word;\n max-height: 120px;\n overflow-y: auto;\n}\n\n.gsrp-lightbox-overlay.gsrp-is-light .gsrp-lightbox-caption {\n color: rgba(15, 15, 15, 0.9);\n}\n\n/* Custom scrollbar for caption description to preserve aesthetics */\n.gsrp-lightbox-caption::-webkit-scrollbar {\n width: 4px;\n}\n.gsrp-lightbox-caption::-webkit-scrollbar-track {\n background: transparent;\n}\n.gsrp-lightbox-caption::-webkit-scrollbar-thumb {\n background: rgba(255, 255, 255, 0.2);\n border-radius: 4px;\n}\n\n/* The Toggle Hide/Show State */\n.gsrp-lightbox-hide-info .gsrp-lightbox-info-panel {\n opacity: 0 !important;\n transform: translateY(12px) !important; /* GPU transition slide-down relative to flex */\n pointer-events: none !important;\n max-height: 0 !important;\n padding-top: 0 !important;\n padding-bottom: 0 !important;\n margin: 0 !important;\n border-width: 0 !important;\n overflow: hidden !important;\n}\n\n/* ==========================================================================\n GSRP Lightbox Premium Top HUD Toolbar & Controls Styles\n ========================================================================== */\n\n/* ==========================================================================\n GSRP Lightbox Premium Top HUD Toolbar & Controls Styles\n ========================================================================== */\n\n/* The Floating Centered Glassmorphism Control Bar (Sleek Compact Rectangular Design) */\n.gsrp-lightbox-hud {\n position: absolute;\n top: 16px; /* Tighter top spacing */\n left: 50%;\n transform: translateX(-50%);\n background: rgba(15, 15, 15, 0.55); /* Low-profile translucent background */\n backdrop-filter: blur(12px) saturate(120%);\n -webkit-backdrop-filter: blur(12px) saturate(120%);\n border: 1px solid rgba(255, 255, 255, 0.06); /* Thin refined border */\n height: 32px; /* Compact height */\n padding: 0 8px;\n border-radius: 4px; /* Crisp corners instead of round capsule */\n display: flex;\n align-items: center;\n gap: 6px; /* Tighter layout */\n z-index: 100004;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);\n transition:\n opacity 0.2s ease,\n transform 0.2s cubic-bezier(0.2, 0.8, 0.2, 1);\n}\n\n.gsrp-lightbox-overlay.gsrp-is-light .gsrp-lightbox-hud {\n background: rgba(255, 255, 255, 0.82);\n border: 1px solid rgba(0, 0, 0, 0.05);\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);\n}\n\n/* Action Buttons inside HUD */\n.gsrp-lightbox-hud-btn {\n background: none;\n border: none;\n outline: none;\n cursor: pointer;\n padding: 0;\n width: 24px; /* Compact size */\n height: 24px;\n border-radius: 3px; /* Sleek rectangular corners instead of circles */\n display: flex;\n align-items: center;\n justify-content: center;\n color: rgba(255, 255, 255, 0.75);\n transition: all 0.2s ease;\n\n svg {\n width: 13px; /* Balanced icon size */\n height: 13px;\n stroke: currentColor;\n stroke-width: 2.2;\n fill: none;\n }\n\n &[fill='currentColor'] svg {\n fill: currentColor;\n stroke: none;\n }\n\n &:hover {\n background: rgba(255, 255, 255, 0.1);\n color: #ffffff;\n transform: scale(1.05);\n }\n\n &:active {\n transform: scale(0.95);\n }\n}\n\n.gsrp-lightbox-overlay.gsrp-is-light .gsrp-lightbox-hud-btn {\n color: rgba(15, 15, 15, 0.75);\n\n &:hover {\n background: rgba(0, 0, 0, 0.04);\n color: #000000;\n }\n}\n\n/* Specific styling for Play button in active state */\n.gsrp-lightbox-hud-btn.gsrp-lightbox-hud-play.gsrp-is-active {\n color: var(--gsrp-primary, #1bb76e);\n background: rgba(27, 183, 110, 0.1);\n}\n\n/* Slideshow Duration Customizable Picker */\n.gsrp-lightbox-hud-duration-wrapper {\n position: relative;\n display: flex;\n align-items: center;\n}\n\n.gsrp-lightbox-hud-duration-toggle {\n font-size: 9px; /* Tighter typography */\n font-weight: 700;\n font-family: inherit;\n letter-spacing: 0.04em;\n padding: 0 4px;\n width: auto;\n height: 18px; /* Tighter layout */\n border-radius: 3px; /* Crisp corner */\n background: rgba(255, 255, 255, 0.04);\n border: 1px solid rgba(255, 255, 255, 0.04);\n display: flex;\n align-items: center;\n gap: 2px;\n\n span {\n font-variant-numeric: tabular-nums;\n }\n\n &:hover {\n background: rgba(255, 255, 255, 0.1);\n border-color: rgba(255, 255, 255, 0.1);\n }\n}\n\n.gsrp-lightbox-overlay.gsrp-is-light .gsrp-lightbox-hud-duration-toggle {\n background: rgba(0, 0, 0, 0.03);\n border-color: rgba(0, 0, 0, 0.03);\n\n &:hover {\n background: rgba(0, 0, 0, 0.06);\n }\n}\n\n/* Glassmorphism popover dropdown */\n.gsrp-lightbox-hud-duration-popover {\n position: absolute;\n top: 24px; /* Matches compact trigger position */\n left: 50%;\n transform: translateX(-50%);\n background: rgba(15, 15, 15, 0.85);\n backdrop-filter: blur(12px);\n -webkit-backdrop-filter: blur(12px);\n border: 1px solid rgba(255, 255, 255, 0.06);\n border-radius: 4px; /* Crisp corner */\n box-shadow: 0 4px 16px rgba(0, 0, 0, 0.35);\n padding: 3px;\n display: flex;\n flex-direction: column;\n gap: 1px;\n z-index: 100006;\n min-width: 52px;\n\n &.gsrp-hide {\n display: none !important;\n }\n}\n\n.gsrp-lightbox-overlay.gsrp-is-light .gsrp-lightbox-hud-duration-popover {\n background: rgba(255, 255, 255, 0.92);\n border: 1px solid rgba(0, 0, 0, 0.06);\n box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);\n}\n\n.gsrp-lightbox-hud-duration-option {\n font-size: 9px;\n font-weight: 600;\n text-align: center;\n padding: 4px 6px;\n border-radius: 3px; /* Crisp corner */\n color: rgba(255, 255, 255, 0.7);\n cursor: pointer;\n transition: all 0.15s ease;\n\n &:hover {\n background: rgba(255, 255, 255, 0.08);\n color: #ffffff;\n }\n\n &.gsrp-is-active {\n background: var(--gsrp-primary, #1bb76e);\n color: #ffffff;\n font-weight: 700;\n }\n}\n\n.gsrp-lightbox-overlay.gsrp-is-light .gsrp-lightbox-hud-duration-option {\n color: rgba(15, 15, 15, 0.7);\n\n &:hover {\n background: rgba(0, 0, 0, 0.03);\n color: #000000;\n }\n\n &.gsrp-is-active {\n background: var(--gsrp-primary, #1bb76e);\n color: #ffffff;\n }\n}\n\n/* Circular progress ring */\n.gsrp-lightbox-progress-ring-wrapper {\n display: flex;\n align-items: center;\n justify-content: center;\n pointer-events: none;\n width: 16px;\n height: 16px; /* Raised to match SVG canvas size precisely */\n}\n\n.gsrp-lightbox-progress-ring {\n transform: rotate(-90deg);\n}\n\n.gsrp-lightbox-progress-ring-circle {\n transition: stroke-dashoffset 0.05s linear;\n}\n\n.gsrp-lightbox-hud-divider {\n width: 1px;\n height: 12px;\n background: rgba(255, 255, 255, 0.1);\n}\n\n.gsrp-lightbox-overlay.gsrp-is-light .gsrp-lightbox-hud-divider {\n background: rgba(0, 0, 0, 0.06);\n}\n\n/* ==========================================================================\n GSRP Lightbox Bottom Horizontally Sliding Thumbnail Strip\n ========================================================================== */\n\n.gsrp-lightbox-thumbs {\n max-width: min(\n 90vw,\n 960px\n ); /* Widened to perfectly align with widescreen 16:9 presentation widths */\n width: 100%;\n height: 40px; /* Highly compact strip height */\n background: rgba(15, 15, 15, 0.55); /* Low-profile translucent */\n backdrop-filter: blur(12px);\n -webkit-backdrop-filter: blur(12px);\n border: 1px solid rgba(255, 255, 255, 0.06);\n border-radius: 4px; /* Sleek rectangular corners instead of capsule rounded ends */\n padding: 0 8px;\n box-sizing: border-box;\n display: flex;\n align-items: center;\n gap: 6px;\n overflow-x: auto;\n overflow-y: hidden;\n scrollbar-width: none; /* Hide standard Firefox scrollbar */\n pointer-events: auto; /* Active interactions */\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);\n transition:\n height 0.25s cubic-bezier(0.2, 0.8, 0.2, 1),\n background 0.25s cubic-bezier(0.2, 0.8, 0.2, 1),\n box-shadow 0.25s cubic-bezier(0.2, 0.8, 0.2, 1),\n opacity 0.25s cubic-bezier(0.2, 0.8, 0.2, 1) !important;\n\n &::-webkit-scrollbar {\n display: none; /* Hide Chrome/Safari scrollbar */\n }\n\n &:hover {\n height: 86px; /* Fluid elevation to accommodate 2.5x upscaled thumbnails (70px height + padding) */\n background: rgba(10, 10, 10, 0.75); /* Darken slightly to pop out */\n box-shadow: 0 12px 32px rgba(0, 0, 0, 0.5); /* Stronger premium depth shadow */\n }\n}\n\n.gsrp-lightbox-overlay.gsrp-is-light .gsrp-lightbox-thumbs {\n background: rgba(255, 255, 255, 0.8);\n border: 1px solid rgba(0, 0, 0, 0.05);\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);\n\n &:hover {\n background: rgba(255, 255, 255, 0.95);\n box-shadow: 0 12px 32px rgba(0, 0, 0, 0.25);\n }\n}\n\n/* Beautiful Rectangular Previews matching original image aspects */\n.gsrp-lightbox-thumb {\n flex: 0 0 44px; /* Wide rectangle ratio close to 3:2 landscape aspect */\n width: 44px;\n height: 28px; /* Tighter layout height */\n border-radius: 3px; /* Clean rectangular corners instead of circle */\n overflow: hidden;\n border: 1px solid rgba(255, 255, 255, 0.12); /* Clean thin border */\n background: #000000; /* Dark backing to display non-cropped portrait shapes beautifully */\n cursor: pointer;\n box-sizing: border-box;\n transition:\n flex-basis 0.25s cubic-bezier(0.2, 0.8, 0.2, 1),\n width 0.25s cubic-bezier(0.2, 0.8, 0.2, 1),\n height 0.25s cubic-bezier(0.2, 0.8, 0.2, 1),\n opacity 0.2s ease,\n border-color 0.2s ease !important;\n opacity: 0.6;\n\n img {\n width: 100%;\n height: 100%;\n object-fit: contain; /* Do not crop - show entire landscape/portrait aspect beautifully */\n border-radius: 2px;\n }\n\n &:hover {\n opacity: 0.95;\n border-color: rgba(255, 255, 255, 0.5);\n }\n\n &.gsrp-is-active {\n opacity: 1;\n border-color: #3b82f6; /* Premium focused blue border */\n box-shadow: 0 0 6px rgba(59, 130, 246, 0.4);\n }\n\n /* When parent thumbs strip is hovered, expand individual thumbnails to 2.5x size */\n .gsrp-lightbox-thumbs:hover & {\n flex: 0 0 110px;\n width: 110px;\n height: 70px;\n\n &.gsrp-is-active {\n box-shadow: 0 0 10px rgba(59, 130, 246, 0.6);\n }\n }\n}\n\n.gsrp-lightbox-overlay.gsrp-is-light .gsrp-lightbox-thumb {\n border-color: rgba(0, 0, 0, 0.12);\n\n &:hover {\n border-color: rgba(0, 0, 0, 0.25);\n }\n\n &.gsrp-is-active {\n border-color: #3b82f6;\n box-shadow: 0 0 6px rgba(59, 130, 246, 0.2);\n }\n\n .gsrp-lightbox-thumbs:hover & {\n &.gsrp-is-active {\n box-shadow: 0 0 10px rgba(59, 130, 246, 0.3);\n }\n }\n}\n\n/* Universal transition rule for fluid HUD fade out/in synchronization */\n.gsrp-lightbox-hud,\n.gsrp-lightbox-close,\n.gsrp-lightbox-counter,\n.gsrp-lightbox-dims,\n.gsrp-lightbox-nav,\n.gsrp-lightbox-thumbs,\n.gsrp-lightbox-info-panel {\n transition:\n opacity 0.3s cubic-bezier(0.2, 0.8, 0.2, 1),\n transform 0.3s cubic-bezier(0.2, 0.8, 0.2, 1),\n bottom 0.3s cubic-bezier(0.2, 0.8, 0.2, 1),\n background 0.2s ease,\n border-color 0.2s ease,\n color 0.2s ease !important;\n}\n\n/* 🎯 Sibling space is now automatically governed by Flexbox! Sinking down and stacking is handled with 0% runtime JS. */\n\n/* ==========================================================================\n GSRP Lightbox Premium Cinema Theater Idle Mode\n ========================================================================== */\n\n/* Automatically hide ALL controls and the mouse cursor after 2 seconds of inactivity */\n.gsrp-lightbox-overlay.gsrp-lightbox-idle {\n cursor: none !important; /* Conceals mouse cursor */\n\n .gsrp-lightbox-hud,\n .gsrp-lightbox-close,\n .gsrp-lightbox-counter,\n .gsrp-lightbox-dims,\n .gsrp-lightbox-nav,\n .gsrp-lightbox-thumbs,\n .gsrp-lightbox-info-panel {\n opacity: 0 !important;\n pointer-events: none !important;\n }\n}\n\n/* ==========================================================================\n GSRP Lightbox Keyboard Shortcut Cheat Sheet Styles\n ========================================================================== */\n\n.gsrp-lightbox-cheat-sheet {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n background: rgba(15, 15, 15, 0.88);\n backdrop-filter: blur(24px) saturate(140%);\n -webkit-backdrop-filter: blur(24px) saturate(140%);\n border: 1px solid rgba(255, 255, 255, 0.12);\n border-radius: 12px;\n padding: 18px 22px;\n box-sizing: border-box;\n width: calc(100% - 40px);\n max-width: 420px;\n z-index: 100008;\n box-shadow: 0 20px 50px rgba(0, 0, 0, 0.6);\n color: #ffffff;\n font-size: 13px;\n animation: gsrp-overlay-fade 0.25s cubic-bezier(0.2, 0.8, 0.2, 1) forwards;\n\n &.gsrp-hide {\n display: none !important;\n }\n}\n\n.gsrp-lightbox-overlay.gsrp-is-light .gsrp-lightbox-cheat-sheet {\n background: rgba(255, 255, 255, 0.95);\n border-color: rgba(0, 0, 0, 0.1);\n box-shadow: 0 20px 50px rgba(0, 0, 0, 0.2);\n color: #000000;\n}\n\n.gsrp-cheat-sheet-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n font-weight: 700;\n font-size: 1.1em;\n border-bottom: 1px solid rgba(255, 255, 255, 0.1);\n padding-bottom: 8px;\n margin-bottom: 12px;\n}\n\n.gsrp-lightbox-overlay.gsrp-is-light .gsrp-cheat-sheet-header {\n border-bottom-color: rgba(0, 0, 0, 0.08);\n}\n\n.gsrp-cheat-sheet-close-btn {\n background: none;\n border: none;\n outline: none;\n cursor: pointer;\n color: rgba(255, 255, 255, 0.6);\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 4px;\n border-radius: 4px;\n transition: all 0.15s ease;\n\n svg {\n width: 14px;\n height: 14px;\n stroke: currentColor;\n stroke-width: 2.5;\n }\n\n &:hover {\n background: rgba(255, 255, 255, 0.1);\n color: #ffffff;\n }\n}\n\n.gsrp-lightbox-overlay.gsrp-is-light .gsrp-cheat-sheet-close-btn {\n color: rgba(0, 0, 0, 0.5);\n\n &:hover {\n background: rgba(0, 0, 0, 0.05);\n color: #000000;\n }\n}\n\n.gsrp-cheat-sheet-grid {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.gsrp-cheat-row {\n display: flex;\n align-items: center;\n gap: 4px;\n line-height: 1.4;\n color: rgba(255, 255, 255, 0.85);\n\n kbd {\n background: rgba(255, 255, 255, 0.15);\n border: 1px solid rgba(255, 255, 255, 0.2);\n border-bottom-width: 2px;\n border-radius: 4px;\n font-family: inherit;\n font-size: 10px;\n font-weight: 700;\n padding: 1.5px 5px;\n color: #ffffff;\n text-shadow: 0 1px 0 rgba(0, 0, 0, 0.5);\n }\n\n span {\n margin-left: auto;\n color: rgba(255, 255, 255, 0.55);\n font-size: 0.9em;\n }\n}\n\n.gsrp-lightbox-overlay.gsrp-is-light .gsrp-cheat-row {\n color: rgba(15, 15, 15, 0.85);\n\n kbd {\n background: rgba(0, 0, 0, 0.05);\n border-color: rgba(0, 0, 0, 0.12);\n color: #000000;\n text-shadow: none;\n }\n\n span {\n color: rgba(0, 0, 0, 0.5);\n }\n}\n\n/* Imgur Embed metadata layout */\n.gsrp-imgur-header-block {\n margin: 0 0 4px 0 !important;\n padding: 0 4px !important; /* Alignment with image border */\n}\n\n.gsrp-imgur-embed-title {\n margin: 0 !important;\n padding: 0 !important;\n font-size: 1.02em !important;\n font-weight: 700 !important;\n line-height: 1.25 !important;\n text-align: left !important;\n}\n\n.gsrp-imgur-embed-meta {\n font-size: 0.72em !important;\n color: rgba(0, 0, 0, 0.5) !important; /* High contrast dark grey by default for light theme */\n font-weight: 500 !important;\n margin: 2px 0 0 0 !important; /* Tight vertically aligned */\n padding: 0 !important;\n letter-spacing: 0.02em !important;\n text-align: left !important;\n}\n\n.gsrp-is-dark .gsrp-imgur-embed-meta {\n color: rgba(\n 255,\n 255,\n 255,\n 0.55\n ) !important; /* Highly visible translucent white for dark theme */\n}\n\n/* Preserving whitespace formatting and linebreaks in single images/captions */\n.gsrp-preview-media-caption,\n.gsrp-lightbox-caption {\n white-space: pre-wrap !important;\n word-break: break-word !important;\n}\n\n/* High-contrast mint-green style for hyperlinked caption text */\n.gsrp-caption-link {\n color: var(--gsrp-primary, #1bb76e) !important;\n text-decoration: underline !important;\n word-break: break-all !important;\n cursor: pointer !important;\n}\n\n.gsrp-caption-link:hover {\n color: #159458 !important;\n text-decoration: none !important;\n}\n\n/* --- High-end Invisible Scrollbar Safe Net --- */\n.gsrp-scrollbar-safe-net {\n position: absolute !important;\n top: 0 !important;\n bottom: 0 !important;\n background: transparent !important;\n pointer-events: none !important; /* Default is inert to avoid click obstruction */\n z-index: 1001 !important;\n}\n\n/* Tooltip safe-net: 32px right buffer */\n.gsrp-preview-tooltip .gsrp-scrollbar-safe-net {\n right: -32px !important;\n width: 32px !important;\n}\n\n/* Tooltip internal scrolling wrapper to resolve overflow clipping completely */\n.gsrp-preview-inner-wrapper {\n max-height: clamp(350px, calc(50vh - 24px), 600px) !important;\n overflow-y: auto !important;\n overflow-x: hidden !important;\n width: 100% !important;\n border-radius: 8px !important;\n box-sizing: border-box !important;\n display: flex !important;\n flex-direction: column !important;\n overscroll-behavior: contain !important;\n scrollbar-width: thin;\n scrollbar-color: var(--gsrp-text-sub) transparent;\n}\n\n/* Side Panel safe-net: Covering the gap from side-panel's right edge to the screen edge */\n.gsrp-side-panel-safe-net {\n position: fixed !important;\n top: 0 !important;\n bottom: 0 !important;\n left: calc(\n var(--gsrp-side-panel-left) + var(--gsrp-side-panel-width) - 10px\n ) !important; /* Overlap panel right edge by 10px to avoid gaps */\n right: 0 !important;\n width: auto !important;\n background: transparent !important;\n pointer-events: none !important; /* Default is inert to avoid click obstruction */\n z-index: 2147483640 !important; /* Extremely high z-index but just below panel (2147483645) to avoid blocking interactions on the panel itself */\n}\n\n/* Activate tooltip safe-net only during hover/sticky state */\n.gsrp-preview-trigger:hover .gsrp-scrollbar-safe-net,\n.gsrp-hover-sticky .gsrp-scrollbar-safe-net {\n pointer-events: auto !important;\n}\n\n/* Activate side-panel safe-net via sibling selector when side-panel is active but NOT pinned to avoid blocking clicks in pinned state */\n.gsrp-side-panel.gsrp-active:not(.gsrp-pinned) ~ .gsrp-side-panel-safe-net {\n pointer-events: auto !important;\n}\n"; function applyDynamicStyles() { const fontSize = Math.max(8, Math.min(32, Number(Config.previewFontSize) || 13)); document.documentElement.style.setProperty("--gsrp-preview-font", `${fontSize}px`); } function injectStyles() { const merged = [coreCss, mediaEmbedsCss, sidePanelCss, lightboxCss].join("\n"); if (typeof GM_addStyle === "function") { GM_addStyle(merged); } else { const style = document.createElement("style"); style.textContent = merged; document.head.appendChild(style); } applyDynamicStyles(); document.documentElement.classList.add("gsrp-thread-colors"); } const LOG_PREFIX = "[GSRP]"; const LEVELS = { DEBUG: 10, INFO: 20, WARN: 30, ERROR: 40, SILENT: 100 }; function threshold() { if (typeof Config.logLevel !== "string") return LEVELS.WARN; const lvl = Config.logLevel.toUpperCase(); return LEVELS[lvl] ?? LEVELS.WARN; } const logger = { debug: (...args) => { if (threshold() <= LEVELS.DEBUG) console.debug(LOG_PREFIX, ...args); }, info: (...args) => { if (threshold() <= LEVELS.INFO) console.info(LOG_PREFIX, ...args); }, warn: (...args) => { if (threshold() <= LEVELS.WARN) console.warn(LOG_PREFIX, ...args); }, error: (...args) => { if (threshold() <= LEVELS.ERROR) console.error(LOG_PREFIX, ...args); } }; const CACHE_PREFIX = "gsrp_cache_"; function getCacheTtlMs() { const ttlMin = typeof Config.cacheTTL === "number" ? Config.cacheTTL : 30; return ttlMin * 60 * 1e3; } class MemoryStorage { map = new Map(); getItem(key) { return this.map.has(key) ? this.map.get(key) : null; } setItem(key, value) { this.map.set(key, value); } removeItem(key) { this.map.delete(key); } get length() { return this.map.size; } key(index) { const keys = Array.from(this.map.keys()); return keys[index] || null; } } let useMemoryFallback = false; try { const testKey = "__gsrp_storage_test__"; window.localStorage.setItem(testKey, "1"); window.localStorage.removeItem(testKey); } catch { logger.warn("[GSRP] localStorage is blocked or unavailable. Falling back to in-memory cache."); useMemoryFallback = true; } const memoryStorageInstance = new MemoryStorage(); const safeLocalStorage = { getItem(key) { if (useMemoryFallback) return memoryStorageInstance.getItem(key); try { return window.localStorage.getItem(key); } catch { return memoryStorageInstance.getItem(key); } }, setItem(key, value) { if (useMemoryFallback) { memoryStorageInstance.setItem(key, value); return; } try { window.localStorage.setItem(key, value); } catch (e) { if (e instanceof Error) { const name = e.name || ""; const msg = e.message || ""; if (name.includes("Quota") || name.includes("QUOTA") || msg.includes("Quota") || msg.includes("QUOTA")) { throw e; } } else if (e && typeof e === "object") { const errObj = e; const name = String(errObj.name || ""); const msg = String(errObj.message || ""); if (name.includes("Quota") || name.includes("QUOTA") || msg.includes("Quota") || msg.includes("QUOTA")) { throw e; } } memoryStorageInstance.setItem(key, value); } }, removeItem(key) { if (useMemoryFallback) { memoryStorageInstance.removeItem(key); return; } try { window.localStorage.removeItem(key); } catch { memoryStorageInstance.removeItem(key); } }, get length() { if (useMemoryFallback) return memoryStorageInstance.length; try { return window.localStorage.length; } catch { return memoryStorageInstance.length; } }, key(index) { if (useMemoryFallback) return memoryStorageInstance.key(index); try { return window.localStorage.key(index); } catch { return memoryStorageInstance.key(index); } } }; const redditDataCache = { has(key) { const ttlMs = getCacheTtlMs(); if (ttlMs === 0) return false; const raw = safeLocalStorage.getItem(CACHE_PREFIX + key); if (!raw) return false; try { const entry = JSON.parse(raw); if (Date.now() - entry.ts > ttlMs) { safeLocalStorage.removeItem(CACHE_PREFIX + key); return false; } return true; } catch { return false; } }, get(key) { const ttlMs = getCacheTtlMs(); if (ttlMs === 0) return void 0; const raw = safeLocalStorage.getItem(CACHE_PREFIX + key); if (!raw) return void 0; try { const entry = JSON.parse(raw); if (Date.now() - entry.ts > ttlMs) { safeLocalStorage.removeItem(CACHE_PREFIX + key); return void 0; } return entry.data; } catch { return void 0; } }, set(key, value) { const ttlMs = getCacheTtlMs(); if (ttlMs === 0) return; try { safeLocalStorage.setItem( CACHE_PREFIX + key, JSON.stringify({ ts: Date.now(), data: value }) ); } catch (initialErr) { const entries2 = []; for (let i = 0; i < safeLocalStorage.length; i++) { const k = safeLocalStorage.key(i); if (k && k.startsWith(CACHE_PREFIX)) entries2.push(k); } entries2.sort((a, b) => { try { const tsA = JSON.parse(safeLocalStorage.getItem(a) || "null")?.ts || 0; const tsB = JSON.parse(safeLocalStorage.getItem(b) || "null")?.ts || 0; return tsA - tsB; } catch { return 0; } }); const toRemove = Math.max(1, Math.floor(entries2.length / 3)); const initialName = initialErr instanceof Error ? initialErr.name : "error"; logger.warn( `cache: quota hit on set("${key}") (${initialName}); evicting ${toRemove}/${entries2.length} oldest entries` ); for (let i = 0; i < toRemove; i++) safeLocalStorage.removeItem(entries2[i]); try { safeLocalStorage.setItem( CACHE_PREFIX + key, JSON.stringify({ ts: Date.now(), data: value }) ); } catch (retryErr) { const retryName = retryErr instanceof Error ? retryErr.name : "error"; logger.warn( `cache: set("${key}") still failed after LRU eviction (${retryName}); entry dropped` ); } } }, getMetrics() { let count = 0; let bytes = 0; try { for (let i = 0; i < safeLocalStorage.length; i++) { const k = safeLocalStorage.key(i); if (k && k.startsWith(CACHE_PREFIX)) { count++; const val = safeLocalStorage.getItem(k); if (val) bytes += val.length * 2; } } } catch { } return { count, bytes }; }, clear() { try { const keysToRemove = []; for (let i = 0; i < safeLocalStorage.length; i++) { const k = safeLocalStorage.key(i); if (k && k.startsWith(CACHE_PREFIX)) { keysToRemove.push(k); } } keysToRemove.forEach((k) => safeLocalStorage.removeItem(k)); } catch { } }, cleanupExpired() { const ttlMs = getCacheTtlMs(); try { const keysToRemove = []; for (let i = 0; i < safeLocalStorage.length; i++) { const k = safeLocalStorage.key(i); if (k && k.startsWith(CACHE_PREFIX)) { const raw = safeLocalStorage.getItem(k); if (raw) { try { const entry = JSON.parse(raw); if (Date.now() - entry.ts > ttlMs) { keysToRemove.push(k); } } catch { keysToRemove.push(k); } } } } keysToRemove.forEach((k) => safeLocalStorage.removeItem(k)); } catch { } } }; const SOURCE_LANGS = [ "en", "zh", "ja", "ko", "es", "fr", "de", "it", "pt", "nl", "sv", "da", "fi", "no", "ru", "uk", "pl", "cs", "el", "ro", "hu", "ar", "fa", "he", "tr", "hi", "bn", "ur", "vi", "th", "id", "ms", "tl" ]; const TARGET_LANGS = [ "en", "zh-TW", "zh-CN", "ja", "ko", "es", "fr", "de", "it", "pt", "nl", "sv", "da", "fi", "no", "ru", "uk", "pl", "cs", "el", "ro", "hu", "ar", "fa", "he", "tr", "hi", "bn", "ur", "vi", "th", "id", "ms", "tl" ]; function detectDarkMode() { if (Config.theme === "dark") return true; if (Config.theme === "light") return false; const theme = document.documentElement.getAttribute("data-theme"); if (theme === "dark") return true; const bodyBg = window.getComputedStyle(document.body).backgroundColor; const rgbMatch = bodyBg.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); if (rgbMatch && bodyBg !== "rgba(0, 0, 0, 0)" && bodyBg !== "transparent") { const brightness = (parseInt(rgbMatch[1], 10) * 299 + parseInt(rgbMatch[2], 10) * 587 + parseInt(rgbMatch[3], 10) * 114) / 1e3; return brightness < 128; } return !!(window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches); } function formatScore(score) { if (typeof score !== "number") return "0"; if (score >= 1e3) return (score / 1e3).toFixed(1) + "k"; return score.toString(); } function formatDate(utc) { if (!utc) return null; const date = new Date(utc * 1e3); return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")} ${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}`; } function formatDateLocal(utc, lang) { if (!utc) return null; const date = new Date(utc * 1e3); const locale = lang && lang !== "auto" ? lang : void 0; try { return new Intl.DateTimeFormat(locale, { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" }).format(date); } catch (e) { return formatDate(utc); } } function formatRelative(utc) { if (!utc) return null; const nowSec = Date.now() / 1e3; const diff = Math.max(0, nowSec - utc); if (diff < 60) return L.relTimeSeconds.replace("{n}", String(Math.floor(diff))); if (diff < 3600) return L.relTimeMinutes.replace("{n}", String(Math.floor(diff / 60))); if (diff < 86400) return L.relTimeHours.replace("{n}", String(Math.floor(diff / 3600))); if (diff < 86400 * 30) return L.relTimeDays.replace("{n}", String(Math.floor(diff / 86400))); if (diff < 86400 * 365) return L.relTimeMonths.replace("{n}", String(Math.floor(diff / 86400 / 30))); return L.relTimeYears.replace("{n}", String(Math.floor(diff / 86400 / 365))); } function normalizeTargetLanguage(rawLang) { if (typeof rawLang !== "string" || !rawLang) return "en"; const lang = rawLang.trim(); if (/^zh-(?:TW|HK|MO|Hant)/i.test(lang)) return "zh-TW"; if (/^zh-(?:CN|SG|Hans)/i.test(lang)) return "zh-CN"; if (/^zh/i.test(lang)) return "zh-CN"; const base = lang.split("-")[0].toLowerCase(); const supported = new Set([ "en", "ja", "ko", "es", "fr", "de", "it", "pt", "nl", "sv", "da", "fi", "no", "ru", "uk", "pl", "cs", "el", "ro", "hu", "ar", "fa", "he", "tr", "hi", "bn", "ur", "vi", "th", "id", "ms", "tl" ]); if (supported.has(base)) return base; return "en"; } function openSettingsModal() { if (document.getElementById("gsrp-settings-modal")) return; const savedBodyOverflow2 = document.body.style.overflow; document.body.style.overflow = "hidden"; const overlay = document.createElement("div"); overlay.id = "gsrp-settings-modal"; overlay.className = "gsrp-settings-overlay" + (detectDarkMode() ? " gsrp-is-dark" : ""); overlay.setAttribute("role", "dialog"); overlay.setAttribute("aria-label", L.settingsTitle); const modal = document.createElement("div"); modal.className = "gsrp-settings-modal"; const header = document.createElement("div"); header.className = "gsrp-settings-header"; header.innerHTML = `<span>${L.settingsTitle}</span><button class="gsrp-settings-close" id="gsrp-close-settings" aria-label="${L.closeBtn}">✕</button>`; modal.appendChild(header); const body = document.createElement("div"); body.className = "gsrp-settings-body"; const createToggle = (id, label, current, desc = "") => { return `<div class="gsrp-settings-row" ${desc ? `title="${desc}"` : ""}> <label for="${id}">${label}${desc ? ' <span class="gsrp-settings-help-icon" style="opacity:0.6;font-size:0.85em;cursor:help;">ⓘ</span>' : ""}</label> <input type="checkbox" id="${id}" ${current ? "checked" : ""} class="gsrp-settings-checkbox"> </div>`; }; const createSelect = (id, label, options, current, desc = "") => { const currentStr = String(current); const optsHtml = options.map((opt) => { const selectedAttr = String(opt.value) === currentStr ? "selected" : ""; const disabledAttr = opt.disabled ? "disabled" : ""; return `<option value="${opt.value}" ${selectedAttr} ${disabledAttr}>${opt.text}</option>`; }).join(""); return `<div class="gsrp-settings-row" ${desc ? `title="${desc}"` : ""}> <label for="${id}">${label}${desc ? ' <span class="gsrp-settings-help-icon" style="opacity:0.6;font-size:0.85em;cursor:help;">ⓘ</span>' : ""}</label> <select id="${id}" class="gsrp-settings-select"> ${optsHtml} </select> </div>`; }; const getLangName = (code) => { try { const uiLang = Config.lang === "auto" ? navigator.language : Config.lang; return new Intl.DisplayNames([uiLang], { type: "language" }).of(code) || code; } catch { return code; } }; const { count: cacheCount, bytes: cacheBytes } = redditDataCache.getMetrics(); const cacheKB = Math.round(cacheBytes / 1024); const maxCacheKB = 5120; const cachePercent = Math.min(100, Math.round(cacheKB / maxCacheKB * 100)); body.innerHTML = ` ${createSelect( "gsrp-lang", L.lang, [ { value: "auto", text: L.langAuto }, { value: "zh-TW", text: "繁體中文" }, { value: "en", text: "English" }, { value: "ja", text: "日本語" } ], Config.lang )} ${createSelect( "gsrp-theme", L.theme, [ { value: "auto", text: L.langAuto }, { value: "light", text: L.themeLight }, { value: "dark", text: L.themeDark } ], Config.theme )} ${createSelect( "gsrp-sort", L.commentSortTitle, [ { value: "confidence", text: L.sortBest }, { value: "top", text: L.sortTop }, { value: "new", text: L.sortNew }, { value: "controversial", text: L.sortControversial }, { value: "old", text: L.sortOld }, { value: "qa", text: L.sortQA } ], Config.commentSort )} ${createSelect( "gsrp-preview-mode", L.previewModeTitle, [ { value: "tooltip", text: L.previewModeTooltip }, { value: "side-panel", text: L.previewModeSidePanel } ], Config.previewMode )} ${createSelect( "gsrp-hover-delay", L.hoverDelay, [ { value: "0", text: Config.lang === "zh-TW" ? "0 ms (無延遲)" : Config.lang === "ja" ? "0 ms (遅延なし)" : "0 ms (No delay)" }, { value: "100", text: "100 ms" }, { value: "200", text: "200 ms" }, { value: "300", text: "300 ms" }, { value: "500", text: "500 ms" } ], Config.hoverDelay, L.hoverDelayTooltip )} <hr class="gsrp-settings-hr"> <div style="font-weight:bold;margin:10px 0 5px;">${L.translateSectionTitle || "🌐 Real-Time Translation & Browser AI"}</div> ${(() => { const isBrowserAISupported = Boolean( window.ai && window.ai.translator || window.Translator || window.ai && window.ai.Translator ); const browserOptText = isBrowserAISupported ? L.translateEngineBrowser || "Browser Built-in AI" : L.translateEngineBrowserDisabled || "Browser Built-in AI (Not supported or Flag not enabled)"; return createSelect( "gsrp-translate-engine", L.translateEngine || "Translation Engine", [ { value: "google", text: L.translateEngineGoogle || "Google Cloud API (Fast & Stable)" }, { value: "browser-ai", text: browserOptText, disabled: !isBrowserAISupported } ], Config.translateEngine, L.translateEngineTooltip ); })()} ${createToggle("gsrp-enable-translate", L.enableTranslate || "Enable experimental real-time translation (Google Translate API)", Config.enableTranslate, L.enableTranslateTooltip)} ${createToggle("gsrp-auto-translate", L.autoTranslate || "Auto-translate posts and comments on preview expand", Config.autoTranslate, L.autoTranslateTooltip)} ${createToggle("gsrp-bilingual-translate", L.bilingualTranslate || "Bilingual dual-language mode (Original + Translated)", Config.bilingualTranslate, L.bilingualTranslateTooltip)} ${createSelect( "gsrp-translate-source", L.translateSourceLang || "Source Language", [ { value: "auto", text: L.langAuto || "Auto Detect" }, ...SOURCE_LANGS.map((code) => ({ value: code, text: getLangName(code) })) ], Config.translateSourceLang )} ${createSelect( "gsrp-translate-target", L.translateTargetLang || "Target Language", [ { value: "auto", text: `${L.langAuto} (${getLangName(normalizeTargetLanguage(navigator.language))})` }, ...TARGET_LANGS.map((code) => ({ value: code, text: getLangName(code) })) ], Config.translateTargetLang )} <div class="gsrp-settings-row" title="${L.translateApiKeyTooltip}"> <label for="gsrp-translate-api-key">${L.translateApiKey}</label> <input type="text" id="gsrp-translate-api-key" class="gsrp-settings-input" style="width:180px;" placeholder="AIzaSy..."> </div> <hr class="gsrp-settings-hr"> <div style="font-weight:bold;margin:10px 0 5px;">${L.summarizeSectionTitle || "🤖 AI 自動總結 (Gemini Nano)"}</div> ${createToggle("gsrp-enable-summarize", L.enableSummarize || "Enable experimental AI summarization", Config.enableSummarize, L.enableSummarizeTooltip)} ${createSelect( "gsrp-summarize-type", L.summarizeType || "Summary Type", [ { value: "key-points", text: L.summarizeTypeKeyPoints || "Key Points (List)" }, { value: "tldr", text: L.summarizeTypeTldr || "TL;DR" }, { value: "teaser", text: L.summarizeTypeTeaser || "Teaser" }, { value: "headline", text: L.summarizeTypeHeadline || "Headline" } ], Config.summarizeType )} ${createSelect( "gsrp-summarize-length", L.summarizeLength || "Summary Length", [ { value: "short", text: L.summarizeLengthShort || "Short" }, { value: "medium", text: L.summarizeLengthMedium || "Medium" }, { value: "long", text: L.summarizeLengthLong || "Long" } ], Config.summarizeLength )} ${createSelect( "gsrp-summarize-engine", L.summarizeEngine || "Summarization Engine", [ { value: "browser-ai", text: L.summarizeEngineBrowser || "Browser Built-in AI (Gemini Nano)" }, { value: "gemini", text: L.summarizeEngineGemini || "Google Gemini API (Cloud)" } ], Config.summarizeEngine )} ${createSelect( "gsrp-summarize-source", L.summarizeSource || "Summary Content Source", [ { value: "all", text: L.summarizeSourceAll || "Post Content & Comments" }, { value: "post-only", text: L.summarizeSourcePostOnly || "Post Content Only" } ], Config.summarizeSource )} <div class="gsrp-settings-row" title="${L.summarizeGeminiApiKey}"> <label for="gsrp-summarize-gemini-api-key">${L.summarizeGeminiApiKey}</label> <div style="display:flex; gap:8px; align-items:center;"> <input type="password" id="gsrp-summarize-gemini-api-key" class="gsrp-settings-input" style="width:180px;" placeholder="AIzaSy..."> <button type="button" id="gsrp-test-gemini-key-btn" class="gsrp-settings-btn" style="margin:0; padding:4px 10px; height:26px; white-space:nowrap;">${L.testConnectionBtn}</button> </div> </div> <div class="gsrp-settings-row" id="gsrp-gemini-key-test-result-row" style="display:none; justify-content:flex-end; margin-top:-8px; padding-bottom:8px;"> <div id="gsrp-gemini-key-test-result" style="font-size:0.85em; font-weight:500; word-break:break-all; max-width:100%; text-align:right;"></div> </div> ${createSelect( "gsrp-summarize-gemini-model", L.summarizeGeminiModel || "Gemini Model", [ { value: "gemini-3.5-flash", text: "gemini-3.5-flash" }, { value: "gemini-3.1-flash-lite", text: "gemini-3.1-flash-lite" }, { value: "gemini-3-flash-preview", text: "gemini-3-flash-preview" }, { value: "gemini-3.1-pro-preview", text: "gemini-3.1-pro-preview" }, { value: "custom", text: Config.lang === "zh-TW" ? "手動填入 (custom)" : Config.lang === "ja" ? "手動入力 (custom)" : "Custom / Manual" } ], Config.summarizeGeminiModel )} <div class="gsrp-settings-row" title="${L.summarizeGeminiCustomModel}"> <label for="gsrp-summarize-gemini-custom-model">${L.summarizeGeminiCustomModel}</label> <input type="text" id="gsrp-summarize-gemini-custom-model" class="gsrp-settings-input" style="width:180px;" placeholder="gemini-..."> </div> <div class="gsrp-settings-prompt-block" id="gsrp-gemini-prompt-block" style="display:none; flex-direction:column; gap:6px; margin:4px 0 8px 0;"> <label for="gsrp-summarize-gemini-prompt" style="font-weight:bold; font-size:0.95em;">${L.summarizeGeminiPrompt}</label> <textarea id="gsrp-summarize-gemini-prompt" class="gsrp-settings-textarea" style="width:100%; height:120px; box-sizing:border-box; padding:8px; border-radius:4px; border:1px dashed var(--gsrp-border); background:var(--gsrp-bg-sub, rgba(0,0,0,0.02)); color:var(--gsrp-color); font-family:monospace; font-size:0.85em; line-height:1.45; resize:vertical; outline:none; transition:border-color 0.15s ease;" title="${L.summarizeGeminiPromptTooltip}"></textarea> <div id="gsrp-gemini-prompt-status" style="font-size:0.8em; opacity:0.85; margin-top:2px;"></div> </div> <hr class="gsrp-settings-hr"> ${createToggle("gsrp-score", L.showScore, Config.showScore)} ${createToggle("gsrp-ratio", L.showUpvoteRatio, Config.showUpvoteRatio)} ${createToggle("gsrp-date", L.showDate, Config.showDate)} ${createToggle("gsrp-awards", L.showAwards, Config.showAwards)} ${createToggle("gsrp-crossposts", L.showCrossposts, Config.showCrossposts)} ${createToggle("gsrp-poststatus", L.showPostStatus, Config.showStatusTags)} ${createToggle("gsrp-author-colors", L.authorColors, Config.authorColors, L.authorColorsTooltip)} <hr class="gsrp-settings-hr"> ${createSelect( "gsrp-preview-font-size", L.previewFontSize, [ { value: "11", text: "11" }, { value: "12", text: "12" }, { value: "13", text: "13" }, { value: "14", text: "14" }, { value: "15", text: "15" }, { value: "16", text: "16" }, { value: "18", text: "18" } ], Config.previewFontSize )} ${createSelect( "gsrp-timestamp-format", L.timestampFormat, [ { value: "absolute", text: L.timestampAbsolute }, { value: "absolute-local", text: L.timestampAbsoluteLocal }, { value: "relative", text: L.timestampRelative } ], Config.timestampFormat )} ${createToggle("gsrp-embed-media", L.embedMedia, Config.embedMedia)} ${createToggle("gsrp-auto-expand-single-images", L.autoExpandSingleImages, Config.autoExpandSingleImages, L.autoExpandSingleImagesDesc)} <div class="gsrp-settings-row"> <label for="gsrp-default-volume">${L.defaultVolume}</label> <div class="gsrp-settings-slider-wrap"> <input type="range" id="gsrp-default-volume" min="0" max="100" step="5" value="${Config.defaultVideoVolume}" class="gsrp-settings-range"> <span id="gsrp-default-volume-display">${Config.defaultVideoVolume}%</span> </div> </div> ${createToggle("gsrp-video-autoplay", L.videoAutoplay, Config.videoAutoplay)} ${createToggle("gsrp-video-autoplay-sound", L.videoAutoplaySound, Config.videoAutoplaySound)} ${createToggle("gsrp-show-image-resolution-preview", L.showImageResolutionPreview, Config.showImageResolutionPreview)} ${createSelect( "gsrp-gallery-display-mode", L.galleryDisplayMode, [ { value: "carousel", text: L.galleryCarousel }, { value: "stack", text: L.galleryStack } ], Config.galleryDisplayMode )} ${createToggle("gsrp-auto-collapse-deleted", L.autoCollapseDeleted, Config.autoCollapseDeleted)} ${createToggle("gsrp-auto-collapse-redacted", L.autoCollapseRedacted, Config.autoCollapseRedacted)} ${createToggle("gsrp-click-to-maximize", L.clickToMaximizeTitle, Config.clickToMaximize)} <div class="gsrp-settings-row" title="${L.imgurClientIdTooltip}"> <label for="gsrp-imgur-client-id">${L.imgurClientId}</label> <input type="text" id="gsrp-imgur-client-id" class="gsrp-settings-input" style="width:180px;" placeholder="546c25a..."> </div> <hr class="gsrp-settings-hr"> ${createSelect( "gsrp-cache-ttl", L.cacheTTL, [ { value: "0", text: L.cacheTTLNone }, { value: "10", text: `10 ${L.minutes}` }, { value: "30", text: `30 ${L.minutes}` }, { value: "60", text: `1 ${L.hours}` }, { value: "1440", text: `1 ${L.days}` }, { value: "525600", text: L.cacheTTLPermanent } ], Config.cacheTTL )} <div class="gsrp-settings-row"> <label> ${L.clearCacheBtn} <div id="gsrp-cache-metrics-display" style="font-size: 0.85em; color: var(--gsrp-text-sub, #888); margin-top: 4px; font-weight: normal;"> ${cacheCount} ${L.postsCached || "篇貼文"} · ${cacheKB} KB / ${maxCacheKB} KB (${cachePercent}%) </div> </label> <button id="gsrp-clear-cache-btn" class="gsrp-settings-btn gsrp-settings-btn-danger" ${cacheCount === 0 ? "disabled" : ""}>${L.clearCacheBtn}</button> </div> `; modal.appendChild(body); const footer = document.createElement("div"); footer.className = "gsrp-settings-footer"; footer.innerHTML = ` <button id="gsrp-reset-btn" class="gsrp-settings-btn gsrp-settings-btn-danger">${L.resetDefaults}</button> <button id="gsrp-cancel-btn" class="gsrp-settings-btn">${L.cancel}</button> <button id="gsrp-save-btn" class="gsrp-settings-btn gsrp-settings-btn-primary">${L.save}</button> `; modal.appendChild(footer); overlay.appendChild(modal); document.body.appendChild(overlay); const secureApiKeyInput = document.getElementById( "gsrp-translate-api-key" ); if (secureApiKeyInput) { secureApiKeyInput.value = Config.translateApiKey; } const secureImgurClientIdInput = document.getElementById( "gsrp-imgur-client-id" ); if (secureImgurClientIdInput) { secureImgurClientIdInput.value = Config.imgurClientId; } const secureGeminiApiKeyInput = document.getElementById( "gsrp-summarize-gemini-api-key" ); if (secureGeminiApiKeyInput) { secureGeminiApiKeyInput.value = Config.summarizeGeminiApiKey; } const secureGeminiCustomModelInput = document.getElementById( "gsrp-summarize-gemini-custom-model" ); if (secureGeminiCustomModelInput) { secureGeminiCustomModelInput.value = Config.summarizeGeminiCustomModel; } const secureGeminiPromptInput = document.getElementById( "gsrp-summarize-gemini-prompt" ); if (secureGeminiPromptInput) { secureGeminiPromptInput.value = Config.summarizeGeminiPrompt; } const $input = (id) => document.getElementById(id); const $select = (id) => document.getElementById(id); const $el = (id) => document.getElementById(id); const $byId = (id) => document.getElementById(id); const volSlider = $input("gsrp-default-volume"); const volDisplay = $el("gsrp-default-volume-display"); if (volSlider && volDisplay) { volSlider.addEventListener("input", () => { volDisplay.textContent = `${volSlider.value}%`; }); } const autoplayToggle = $input("gsrp-video-autoplay"); const autoplaySoundToggle = $input("gsrp-video-autoplay-sound"); if (autoplayToggle && autoplaySoundToggle) { const updateSoundState = () => { autoplaySoundToggle.disabled = !autoplayToggle.checked; const row = autoplaySoundToggle.closest(".gsrp-settings-row"); if (row) { row.style.opacity = autoplayToggle.checked ? "1" : "0.5"; row.style.pointerEvents = autoplayToggle.checked ? "auto" : "none"; } }; autoplayToggle.addEventListener("change", updateSoundState); updateSoundState(); } const translateToggle = $input("gsrp-enable-translate"); const engineSelect = $select("gsrp-translate-engine"); const autoTranslateToggle = $input("gsrp-auto-translate"); const bilingualTranslateToggle = $input("gsrp-bilingual-translate"); const sourceSelect = $select("gsrp-translate-source"); const targetSelect = $select("gsrp-translate-target"); const apiKeyInput = $input("gsrp-translate-api-key"); if (translateToggle && engineSelect && autoTranslateToggle && bilingualTranslateToggle && sourceSelect && targetSelect && apiKeyInput) { const updateTranslateState = () => { const enabled = translateToggle.checked; engineSelect.disabled = !enabled; autoTranslateToggle.disabled = !enabled; bilingualTranslateToggle.disabled = !enabled; sourceSelect.disabled = !enabled; targetSelect.disabled = !enabled; apiKeyInput.disabled = !enabled; const all = [ engineSelect, autoTranslateToggle, bilingualTranslateToggle, sourceSelect, targetSelect, apiKeyInput ]; all.forEach((el) => { const row = el.closest(".gsrp-settings-row"); if (row) { row.style.opacity = enabled ? "1" : "0.5"; row.style.pointerEvents = enabled ? "auto" : "none"; } }); }; translateToggle.addEventListener("change", updateTranslateState); updateTranslateState(); } const summarizeToggle = $input("gsrp-enable-summarize"); const summarizeTypeSelect = $select("gsrp-summarize-type"); const summarizeLengthSelect = $select("gsrp-summarize-length"); const summarizeEngineSelect = $select("gsrp-summarize-engine"); const summarizeSourceSelect = $select("gsrp-summarize-source"); const summarizeGeminiApiKeyInput = $input("gsrp-summarize-gemini-api-key"); const summarizeGeminiModelSelect = $select("gsrp-summarize-gemini-model"); const summarizeGeminiCustomModelInput = $input("gsrp-summarize-gemini-custom-model"); const summarizeGeminiPromptInput = document.getElementById( "gsrp-summarize-gemini-prompt" ); const testBtn = $el("gsrp-test-gemini-key-btn"); const testResultRow = $el("gsrp-gemini-key-test-result-row"); const testResultDiv = $el("gsrp-gemini-key-test-result"); const promptBlock = $el("gsrp-gemini-prompt-block"); const promptStatus = $el("gsrp-gemini-prompt-status"); const getLanguageName2 = (langCode) => { const code = langCode.toLowerCase().split("-")[0]; const langMap = { en: "English", zh: "Traditional Chinese (zh-TW)", ja: "Japanese", es: "Spanish", fr: "French", de: "German", it: "Italian", ko: "Korean", ru: "Russian", pt: "Portuguese", vi: "Vietnamese" }; return langMap[code] || "English"; }; const generateDefaultPrompt = (lang, type, length) => { const targetLangName = getLanguageName2(lang); return `You are a helpful assistant that summarizes Reddit posts. Please summarize the provided content in ${targetLangName} only. Summary Type: ${type}. Summary Length: ${length}. Your output must be written in ${targetLangName} only.`; }; if (summarizeToggle && summarizeTypeSelect && summarizeLengthSelect && summarizeEngineSelect && summarizeSourceSelect && summarizeGeminiApiKeyInput && summarizeGeminiModelSelect && summarizeGeminiCustomModelInput && summarizeGeminiPromptInput) { const updateSummarizeState = () => { const enabled = summarizeToggle.checked; const isGemini = enabled && summarizeEngineSelect.value === "gemini"; const isCustomModel = isGemini && summarizeGeminiModelSelect.value === "custom"; summarizeTypeSelect.disabled = !enabled; summarizeLengthSelect.disabled = !enabled; summarizeEngineSelect.disabled = !enabled; summarizeSourceSelect.disabled = !enabled; const baseElements = [ summarizeTypeSelect, summarizeLengthSelect, summarizeEngineSelect, summarizeSourceSelect ]; baseElements.forEach((el) => { const row = el.closest(".gsrp-settings-row"); if (row) { row.style.opacity = enabled ? "1" : "0.5"; row.style.pointerEvents = enabled ? "auto" : "none"; } }); summarizeGeminiApiKeyInput.disabled = !isGemini; summarizeGeminiModelSelect.disabled = !isGemini; if (summarizeGeminiPromptInput) { summarizeGeminiPromptInput.disabled = !isGemini; } if (testBtn) { testBtn.disabled = !isGemini; } const geminiElements = [summarizeGeminiApiKeyInput, summarizeGeminiModelSelect]; geminiElements.forEach((el) => { const row = el.closest(".gsrp-settings-row"); if (row) { row.style.opacity = isGemini ? "1" : "0.5"; row.style.pointerEvents = isGemini ? "auto" : "none"; } }); summarizeGeminiCustomModelInput.disabled = !isCustomModel; const customModelRow = summarizeGeminiCustomModelInput.closest( ".gsrp-settings-row" ); if (customModelRow) { customModelRow.style.opacity = isCustomModel ? "1" : "0.5"; customModelRow.style.pointerEvents = isCustomModel ? "auto" : "none"; } if (testResultRow && !isGemini) { testResultRow.style.display = "none"; } if (promptBlock && summarizeGeminiPromptInput && promptStatus) { if (isGemini) { promptBlock.style.display = "flex"; const customPromptVal = (summarizeGeminiPromptInput.value || "").trim(); const selectedLangVal = $byId("gsrp-lang")?.value || Config.lang; const resolvedLang = selectedLangVal === "auto" ? navigator.language : selectedLangVal; const defaultPrompt = generateDefaultPrompt( resolvedLang, summarizeTypeSelect.value, summarizeLengthSelect.value ); if (customPromptVal) { summarizeGeminiPromptInput.style.borderColor = "var(--gsrp-border)"; promptStatus.style.color = "#d97706"; promptStatus.textContent = L.customPromptActive || "💡 Custom prompt active, default prompt is bypassed."; } else { summarizeGeminiPromptInput.style.borderColor = "rgba(26, 115, 232, 0.4)"; promptStatus.style.color = "var(--gsrp-text-sub, #888)"; promptStatus.textContent = L.defaultPromptPreview || "Active default prompt:"; } summarizeGeminiPromptInput.placeholder = defaultPrompt; } else { promptBlock.style.display = "none"; } } }; summarizeToggle.addEventListener("change", updateSummarizeState); summarizeEngineSelect.addEventListener("change", updateSummarizeState); summarizeGeminiModelSelect.addEventListener("change", updateSummarizeState); summarizeTypeSelect.addEventListener("change", updateSummarizeState); summarizeLengthSelect.addEventListener("change", updateSummarizeState); summarizeGeminiPromptInput.addEventListener("input", updateSummarizeState); const langSelect = $byId("gsrp-lang"); if (langSelect) { langSelect.addEventListener("change", updateSummarizeState); } updateSummarizeState(); if (testBtn && testResultRow && testResultDiv) { testBtn.onclick = () => { const apiKey = (summarizeGeminiApiKeyInput?.value || "").trim(); if (!apiKey) { testResultDiv.style.color = "#ea4335"; testResultDiv.textContent = `${L.testConnectionErr || "Failed: "}${Config.lang === "zh-TW" ? "API 金鑰不可為空" : Config.lang === "ja" ? "API キーは空にできません" : "API Key cannot be empty"}`; testResultRow.style.display = "flex"; return; } testBtn.disabled = true; const originalBtnText = testBtn.textContent; testBtn.textContent = L.testingConnection || "Testing..."; testResultDiv.style.color = "var(--gsrp-text-sub, #888)"; testResultDiv.textContent = L.testingConnection || "Testing..."; testResultRow.style.display = "flex"; let model = (summarizeGeminiModelSelect?.value || "").trim(); if (model === "custom") { model = (summarizeGeminiCustomModelInput?.value || "").trim(); } if (!model) { model = "gemini-3.5-flash"; } const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:generateContent?key=${apiKey}`; GM_xmlhttpRequest({ method: "POST", url, headers: { "Content-Type": "application/json" }, data: JSON.stringify({ contents: [ { parts: [{ text: "Hello" }] } ] }), timeout: 1e4, onload: (res) => { testBtn.disabled = false; testBtn.textContent = originalBtnText; if (res.status === 200) { testResultDiv.style.color = "#28a745"; testResultDiv.textContent = L.testConnectionSuccess || "Success! API key is valid."; } else { testResultDiv.style.color = "#ea4335"; let errMsg = `Status ${res.status}`; try { const errJson = JSON.parse(res.responseText); if (errJson.error?.message) { errMsg = errJson.error.message; } } catch { } testResultDiv.textContent = `${L.testConnectionErr || "Failed: "}${errMsg}`; } }, onerror: () => { testBtn.disabled = false; testBtn.textContent = originalBtnText; testResultDiv.style.color = "#ea4335"; testResultDiv.textContent = `${L.testConnectionErr || "Failed: "}${Config.lang === "zh-TW" ? "網路錯誤或請求被阻擋" : Config.lang === "ja" ? "ネットワークエラーまたはリクエストがブロックされました" : "Network error or request blocked"}`; }, ontimeout: () => { testBtn.disabled = false; testBtn.textContent = originalBtnText; testResultDiv.style.color = "#ea4335"; testResultDiv.textContent = `${L.testConnectionErr || "Failed: "}${Config.lang === "zh-TW" ? "請求逾時" : Config.lang === "ja" ? "リクエストタイムアウト" : "Request timeout"}`; } }); }; } } const getModalFocusables = () => { const list = Array.from( modal.querySelectorAll('input, select, button, [tabindex="0"]') ); return list.filter((el) => { if (el.tabIndex < 0) return false; if (el.disabled) return false; const style = window.getComputedStyle(el); if (style.display === "none" || style.visibility === "hidden") return false; return true; }); }; const handleModalTab = (e) => { if (e.key === "Tab") { const focusables = getModalFocusables(); if (focusables.length > 0) { const first = focusables[0]; const last = focusables[focusables.length - 1]; const activeIdx = focusables.indexOf(document.activeElement); if (activeIdx === -1) { e.preventDefault(); if (e.shiftKey) { last.focus(); } else { first.focus(); } } else if (e.shiftKey) { if (document.activeElement === first) { e.preventDefault(); last.focus(); } } else { if (document.activeElement === last) { e.preventDefault(); first.focus(); } } } else { e.preventDefault(); } } }; const close = () => { document.removeEventListener("keydown", handleKeydown); document.removeEventListener("keydown", handleModalTab); overlay.remove(); document.body.style.overflow = savedBodyOverflow2; }; const handleKeydown = (e) => { if (e.key === "Escape") close(); }; const closeBtn = $el("gsrp-close-settings"); const cancelBtn = $el("gsrp-cancel-btn"); if (closeBtn) closeBtn.onclick = close; if (cancelBtn) cancelBtn.onclick = close; overlay.addEventListener("click", (e) => { if (e.target === overlay) close(); }); document.addEventListener("keydown", handleKeydown); document.addEventListener("keydown", handleModalTab); const clearBtn = $byId("gsrp-clear-cache-btn"); if (clearBtn) { clearBtn.onclick = () => { try { const { count: keysCount, bytes: totalBytesFreed } = redditDataCache.getMetrics(); redditDataCache.clear(); const kbFreed = Math.round(totalBytesFreed / 1024); clearBtn.disabled = true; clearBtn.style.backgroundColor = "#28a745"; clearBtn.style.color = "#fff"; clearBtn.style.borderColor = "#28a745"; clearBtn.textContent = L.clearCacheSuccess.replace("{n}", String(keysCount)).replace("{size}", String(kbFreed)); const metricsDisplay = $el("gsrp-cache-metrics-display"); if (metricsDisplay) { metricsDisplay.textContent = `0 ${L.postsCached || "篇貼文"} · 0 KB / 5120 KB (0%)`; } } catch { clearBtn.textContent = "Error"; } }; } $byId("gsrp-reset-btn").onclick = () => { $byId("gsrp-lang").value = DEFAULT_CONFIG.lang; $byId("gsrp-theme").value = DEFAULT_CONFIG.theme; $byId("gsrp-sort").value = DEFAULT_CONFIG.commentSort; $byId("gsrp-preview-mode").value = DEFAULT_CONFIG.previewMode; $byId("gsrp-translate-engine").value = DEFAULT_CONFIG.translateEngine; $byId("gsrp-enable-translate").checked = DEFAULT_CONFIG.enableTranslate; $byId("gsrp-auto-translate").checked = DEFAULT_CONFIG.autoTranslate; $byId("gsrp-bilingual-translate").checked = DEFAULT_CONFIG.bilingualTranslate; $byId("gsrp-translate-source").value = DEFAULT_CONFIG.translateSourceLang; $byId("gsrp-translate-target").value = DEFAULT_CONFIG.translateTargetLang; $byId("gsrp-translate-api-key").value = DEFAULT_CONFIG.translateApiKey; $byId("gsrp-imgur-client-id").value = DEFAULT_CONFIG.imgurClientId; $byId("gsrp-score").checked = DEFAULT_CONFIG.showScore; $byId("gsrp-ratio").checked = DEFAULT_CONFIG.showUpvoteRatio; $byId("gsrp-date").checked = DEFAULT_CONFIG.showDate; $byId("gsrp-awards").checked = DEFAULT_CONFIG.showAwards; $byId("gsrp-crossposts").checked = DEFAULT_CONFIG.showCrossposts; $byId("gsrp-poststatus").checked = DEFAULT_CONFIG.showStatusTags; $byId("gsrp-author-colors").checked = DEFAULT_CONFIG.authorColors; $byId("gsrp-preview-font-size").value = String(DEFAULT_CONFIG.previewFontSize); $byId("gsrp-timestamp-format").value = DEFAULT_CONFIG.timestampFormat; $byId("gsrp-embed-media").checked = DEFAULT_CONFIG.embedMedia; $byId("gsrp-auto-expand-single-images").checked = DEFAULT_CONFIG.autoExpandSingleImages; const resetVol = $byId("gsrp-default-volume"); resetVol.value = String(DEFAULT_CONFIG.defaultVideoVolume); resetVol.dispatchEvent(new Event("input")); const apToggle = $byId("gsrp-video-autoplay"); apToggle.checked = DEFAULT_CONFIG.videoAutoplay; $byId("gsrp-video-autoplay-sound").checked = DEFAULT_CONFIG.videoAutoplaySound; apToggle.dispatchEvent(new Event("change")); const transToggle = $byId("gsrp-enable-translate"); if (transToggle) transToggle.dispatchEvent(new Event("change")); $byId("gsrp-show-image-resolution-preview").checked = DEFAULT_CONFIG.showImageResolutionPreview; $byId("gsrp-gallery-display-mode").value = DEFAULT_CONFIG.galleryDisplayMode; $byId("gsrp-auto-collapse-deleted").checked = DEFAULT_CONFIG.autoCollapseDeleted; $byId("gsrp-auto-collapse-redacted").checked = DEFAULT_CONFIG.autoCollapseRedacted; $byId("gsrp-click-to-maximize").checked = DEFAULT_CONFIG.clickToMaximize; $byId("gsrp-cache-ttl").value = String(DEFAULT_CONFIG.cacheTTL); $byId("gsrp-hover-delay").value = String(DEFAULT_CONFIG.hoverDelay); $byId("gsrp-enable-summarize").checked = DEFAULT_CONFIG.enableSummarize; $byId("gsrp-summarize-type").value = DEFAULT_CONFIG.summarizeType; $byId("gsrp-summarize-length").value = DEFAULT_CONFIG.summarizeLength; $byId("gsrp-summarize-engine").value = DEFAULT_CONFIG.summarizeEngine; $byId("gsrp-summarize-source").value = DEFAULT_CONFIG.summarizeSource; $byId("gsrp-summarize-gemini-api-key").value = DEFAULT_CONFIG.summarizeGeminiApiKey; $byId("gsrp-summarize-gemini-model").value = DEFAULT_CONFIG.summarizeGeminiModel; $byId("gsrp-summarize-gemini-custom-model").value = DEFAULT_CONFIG.summarizeGeminiCustomModel; $byId("gsrp-summarize-gemini-prompt").value = DEFAULT_CONFIG.summarizeGeminiPrompt; const sumToggle = $byId("gsrp-enable-summarize"); if (sumToggle) sumToggle.dispatchEvent(new Event("change")); }; $byId("gsrp-save-btn").onclick = () => { Config.lang = $byId("gsrp-lang").value; Config.theme = $byId("gsrp-theme").value; Config.commentSort = $byId("gsrp-sort").value; Config.previewMode = $byId("gsrp-preview-mode").value; Config.hoverDelay = parseInt($byId("gsrp-hover-delay").value, 10); Config.translateEngine = $byId("gsrp-translate-engine").value; Config.enableTranslate = $byId("gsrp-enable-translate").checked; Config.autoTranslate = $byId("gsrp-auto-translate").checked; Config.bilingualTranslate = $byId("gsrp-bilingual-translate").checked; Config.translateSourceLang = $byId("gsrp-translate-source").value; Config.translateTargetLang = $byId("gsrp-translate-target").value; Config.translateApiKey = $byId("gsrp-translate-api-key").value; Config.imgurClientId = $byId("gsrp-imgur-client-id").value; Config.showScore = $byId("gsrp-score").checked; Config.showUpvoteRatio = $byId("gsrp-ratio").checked; Config.showDate = $byId("gsrp-date").checked; Config.showAwards = $byId("gsrp-awards").checked; Config.showCrossposts = $byId("gsrp-crossposts").checked; Config.showStatusTags = $byId("gsrp-poststatus").checked; Config.authorColors = $byId("gsrp-author-colors").checked; Config.previewFontSize = parseInt($byId("gsrp-preview-font-size").value, 10); Config.timestampFormat = $byId("gsrp-timestamp-format").value; Config.embedMedia = $byId("gsrp-embed-media").checked; Config.autoExpandSingleImages = $byId("gsrp-auto-expand-single-images").checked; Config.defaultVideoVolume = parseInt($byId("gsrp-default-volume").value, 10); Config.videoAutoplay = $byId("gsrp-video-autoplay").checked; Config.videoAutoplaySound = $byId("gsrp-video-autoplay-sound").checked; Config.showImageResolutionPreview = $byId("gsrp-show-image-resolution-preview").checked; Config.galleryDisplayMode = $byId("gsrp-gallery-display-mode").value; Config.autoCollapseDeleted = $byId("gsrp-auto-collapse-deleted").checked; Config.autoCollapseRedacted = $byId("gsrp-auto-collapse-redacted").checked; Config.clickToMaximize = $byId("gsrp-click-to-maximize").checked; Config.cacheTTL = parseInt($byId("gsrp-cache-ttl").value, 10); Config.enableSummarize = $byId("gsrp-enable-summarize").checked; Config.summarizeType = $byId("gsrp-summarize-type").value; Config.summarizeLength = $byId("gsrp-summarize-length").value; Config.summarizeEngine = $byId("gsrp-summarize-engine").value; Config.summarizeSource = $byId("gsrp-summarize-source").value; Config.summarizeGeminiApiKey = $byId("gsrp-summarize-gemini-api-key").value; Config.summarizeGeminiModel = $byId("gsrp-summarize-gemini-model").value; Config.summarizeGeminiCustomModel = $byId("gsrp-summarize-gemini-custom-model").value; Config.summarizeGeminiPrompt = $byId("gsrp-summarize-gemini-prompt").value; saveConfig(); close(); window.location.reload(); }; modal.querySelector("select, button")?.focus(); } const FetchError = { RATE_LIMIT: "RATE_LIMIT", HTTP_ERR: "HTTP_ERR", NET_ERR: "NET_ERR", PARSE_ERR: "PARSE_ERR", TIMEOUT: "TIMEOUT" }; const inflight = new Map(); const BACKOFF_INITIAL_MS = 8 * 1e3; const BACKOFF_MAX_MS = 5 * 60 * 1e3; let backoffUntil = 0; let backoffMs = BACKOFF_INITIAL_MS; function getRateLimitWaitMs() { const remain = backoffUntil - Date.now(); return remain > 0 ? remain : 0; } function isBackoffActive() { return Date.now() < backoffUntil; } function noteRateLimit() { backoffUntil = Date.now() + backoffMs; backoffMs = Math.min(backoffMs * 2, BACKOFF_MAX_MS); } function clearBackoff() { backoffUntil = 0; backoffMs = BACKOFF_INITIAL_MS; } function registerInflight(key, caller) { let slot = inflight.get(key); const owns = !slot; if (!slot) { slot = { callers: [], controller: new AbortController() }; inflight.set(key, slot); } slot.callers.push(caller); if (caller.signal) { const onAbort = () => { const currentSlot = inflight.get(key); if (!currentSlot) return; currentSlot.callers = currentSlot.callers.filter((c) => c !== caller); if (currentSlot.callers.length === 0) { currentSlot.controller.abort(); inflight.delete(key); } }; caller.signal.addEventListener("abort", onAbort, { once: true }); caller._cleanupAbort = () => { caller.signal?.removeEventListener("abort", onAbort); }; } return { owns, signal: slot.controller.signal }; } function deliverSuccess(key, data) { const slot = inflight.get(key); if (!slot) return; inflight.delete(key); for (const caller of slot.callers) { if (caller._cleanupAbort) { caller._cleanupAbort(); } if (caller.signal?.aborted) continue; try { caller.onSuccess(data); } catch { } } } function deliverError(key, err) { const slot = inflight.get(key); if (!slot) return; inflight.delete(key); for (const caller of slot.callers) { if (caller._cleanupAbort) { caller._cleanupAbort(); } if (caller.signal?.aborted) continue; try { caller.onError(err); } catch { } } } const VERSION = "0.9.5"; const FAILOVER_HOSTS = ["old.reddit.com", "new.reddit.com"]; function classify(res) { if (res.status === 200) return { ok: true, error: null }; if (res.status === 429) return { ok: false, error: FetchError.RATE_LIMIT }; return { ok: false, error: FetchError.HTTP_ERR }; } function failoverFetch(opts) { const { url, onSuccess, onError, onRateLimit, label = "Request", signal } = opts; function tryOnce(currentUrl, attempt) { if (signal?.aborted) return; let handleAbort; const cleanupAbort = () => { if (signal && handleAbort) { signal.removeEventListener("abort", handleAbort); } }; const req = GM_xmlhttpRequest({ method: "GET", url: currentUrl, headers: { "User-Agent": `GSRP-Module/${VERSION}` }, timeout: 1e4, onload: (res) => { cleanupAbort(); if (signal?.aborted) return; const verdict = classify(res); if (verdict.ok) { try { const json = JSON.parse(res.responseText); onSuccess(json, currentUrl); } catch { promoteOrFail(FetchError.PARSE_ERR); } return; } if (verdict.error === FetchError.RATE_LIMIT) { if (typeof onRateLimit === "function") { onRateLimit(FetchError.RATE_LIMIT); } onError(FetchError.RATE_LIMIT); return; } promoteOrFail(verdict.error); }, onerror: () => { cleanupAbort(); if (signal?.aborted) return; promoteOrFail(FetchError.NET_ERR); }, ontimeout: () => { cleanupAbort(); if (signal?.aborted) return; promoteOrFail(FetchError.TIMEOUT); } }); if (signal) { handleAbort = () => { try { req.abort(); } catch { } }; signal.addEventListener("abort", handleAbort, { once: true }); } function promoteOrFail(errCode) { if (signal?.aborted) return; const nextHost = FAILOVER_HOSTS[attempt]; if (!nextHost) { onError(errCode); return; } const u = new URL(currentUrl); u.hostname = nextHost; const fallbackUrl = u.toString(); console.warn( `[GSRP] ${label} failed (${errCode}). Retrying on fallback host: ${nextHost}` ); tryOnce(fallbackUrl, attempt + 1); } } tryOnce(url, 0); } const sharedParser = new DOMParser(); function decodeHTMLEntities(text2) { if (!text2) return ""; const cleanText = text2.replace(/​/g, ""); const doc = sharedParser.parseFromString(cleanText, "text/html"); return doc.documentElement.textContent || ""; } function escapeHTML(str) { if (!str) return ""; return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'"); } const AUTO_COLLAPSE_BOTS$1 = new Set([ "AutoModerator", "SaveVideo", "savevideobot", "RemindMeBot", "haikusbot", "RepostSleuthBot", "AmputatorBot", "reputatorbot" ]); function countCommentNodes(tree) { let n = 0; for (const c of tree) { if (c.type === "more") continue; n += 1; if (c.replies && c.replies.length) n += countCommentNodes(c.replies); } return n; } function analyzeParticipants(tree, counts = new Map()) { for (const c of tree) { if (c.type === "more") continue; if (c.author && !c.isAuthorDeleted && c.author !== "[deleted]" && c.author !== "[removed]" && !AUTO_COLLAPSE_BOTS$1.has(c.author)) { counts.set(c.author, (counts.get(c.author) || 0) + 1); } if (c.replies && c.replies.length) analyzeParticipants(c.replies, counts); } return counts; } function formatTimestamp$1(utc) { if (typeof utc !== "number") return null; if (Config.timestampFormat === "relative") { return formatRelative(utc); } else if (Config.timestampFormat === "absolute-local") { return formatDateLocal(utc, Config.lang); } else { return formatDate(utc); } } const BOT_COMMAND_RE = /^\s*\/?u\/(?:SaveVideo|savevideobot|RemindMeBot|haikusbot|RepostSleuthBot|AmputatorBot|reputatorbot)\b.*/i; const BOT_COMMAND_MAX_LENGTH = 80; function isBotAuthor(d) { return AUTO_COLLAPSE_BOTS$1.has(d.author); } function isBotCommandBody(d) { const body = (d.body || "").trim(); if (!body || body.length > BOT_COMMAND_MAX_LENGTH) return false; return BOT_COMMAND_RE.test(body); } function isRedactGeneratedText(text2) { if (typeof text2 !== "string") return false; const body = text2.trim(); const signaturePattern = "[\\s*_]*This (?:post|comment) was mass deleted and anonymized with \\[(?:Redact|redact\\.dev)\\]\\(https?:\\/\\/redact\\.dev[^\\)]*\\)[\\s*_]*"; const startsWithRegex = new RegExp("^" + signaturePattern, "i"); const endsWithRegex = new RegExp(signaturePattern + "$", "i"); let trailingText; const startMatch = body.match(startsWithRegex); if (startMatch) { trailingText = body.slice(startMatch[0].length).trim(); } else { const endMatch = body.match(endsWithRegex); if (endMatch) { trailingText = body.slice(0, body.length - endMatch[0].length).trim(); } else { return false; } } if (!trailingText) return true; return /^[a-z\s]+$/.test(trailingText); } function isRedactGenerated(d) { const isEdited = d.edited !== false && d.edited !== void 0 && d.edited !== null; return isEdited && isRedactGeneratedText(d.body); } function getRemovalKind(d) { if (d.body === "[deleted]") return "deleted"; if (d.body === "[removed]") return "removed"; return null; } function isAuthorDeleted(d) { return d.author === "[deleted]"; } function mapCommentBase(d) { const isRedacted = isRedactGenerated(d); const rawRemovalKind = getRemovalKind(d); const removalKind = isRedacted ? "redacted" : rawRemovalKind; const isDeleted = isRedacted || rawRemovalKind !== null; const isAutoCollapsed = isBotAuthor(d) || isBotCommandBody(d) || Config.autoCollapseRedacted && isRedacted || Config.autoCollapseDeleted && removalKind !== null; return { id: d.id, author: d.author, score: d.score || 0, bodyHtml: d.body_html || "", body: d.body || "", date: formatTimestamp$1(d.created_utc), edited: typeof d.edited === "number" ? formatTimestamp$1(d.edited) : null, permalink: d.permalink || "", flair: decodeHTMLEntities(d.author_flair_text || ""), isOP: d.is_submitter === true, distinguished: d.distinguished || null, isDeleted, isRedacted, removalKind, isAuthorDeleted: isAuthorDeleted(d), isAutoCollapsed, isStickied: false, replies: [], mediaMetadata: d.media_metadata || null }; } function buildCommentTree(commentsData, depth = 0, postPermalink = "") { const result = []; for (const c of commentsData) { if (c.kind === "t1" && c.data && c.data.body) { const node = mapCommentBase(c.data); if (depth === 0) node.isStickied = c.data.stickied === true; const replies = c.data.replies; const children = typeof replies === "object" && replies !== null ? replies.data?.children : null; node.replies = children ? buildCommentTree(children, depth + 1, postPermalink) : []; result.push(node); } else if (c.kind === "more" && c.data) { const count = typeof c.data.count === "number" ? c.data.count : null; const parentId = c.data.parent_id || ""; let morePermalink = ""; if (parentId.startsWith("t1_") && postPermalink) { morePermalink = postPermalink + parentId.slice(3) + "/"; } else if (postPermalink) { morePermalink = postPermalink; } const moreItem = { type: "more", count, depth, permalink: morePermalink, isNested: parentId.startsWith("t1_") }; result.push(moreItem); } } return result; } function buildTopComments(commentsData, postPermalink) { return buildCommentTree(commentsData, 0, postPermalink); } const MEDIA_HOSTS = new Set([ "i.redd.it", "v.redd.it", "preview.redd.it", "external-preview.redd.it", "i.imgur.com", "imgur.com", "giphy.com", "media.giphy.com" ]); function isAllowedHost(url, hostSet) { if (typeof url !== "string" || !url) return false; let parsed; try { parsed = new URL(url); } catch { return false; } if (parsed.protocol !== "https:") return false; return hostSet.has(parsed.hostname); } function isAllowedRedditMediaHost(url) { return isAllowedHost(url, MEDIA_HOSTS); } function isDeadLink(url) { if (typeof url !== "string" || !url) return false; try { const parsed = new URL(url); const host = parsed.hostname.toLowerCase(); return /(?:^|\.)(?:gfycat\.com|gfy\.cat|vid\.me|minus\.com|tinypic\.com)$/.test(host); } catch { return false; } } const YT_ID_RE = /^[A-Za-z0-9_-]{6,20}$/; function extractYouTubeId(url) { if (typeof url !== "string" || !url) return null; let parsed; try { parsed = new URL(url); } catch { return null; } const host = parsed.hostname; let candidate = null; if (host === "youtu.be") { candidate = parsed.pathname.slice(1).split("/")[0] || null; } else if (host === "www.youtube.com" || host === "youtube.com" || host === "m.youtube.com" || host === "i.ytimg.com") { const v = parsed.searchParams.get("v"); if (v) { candidate = v; } else { const m = parsed.pathname.match(/^\/(?:embed|v|shorts|vi)\/([^/]+)/); if (m) candidate = m[1]; } } return candidate && YT_ID_RE.test(candidate) ? candidate : null; } function extractYouTubeTimestamp(url) { if (typeof url !== "string" || !url) return null; try { const parsed = new URL(url); const t = parsed.searchParams.get("t") || parsed.searchParams.get("start"); if (!t) return null; if (/^\d+$/.test(t)) { return parseInt(t, 10); } let seconds = 0; const hMatch = t.match(/(\d+)h/); const mMatch = t.match(/(\d+)m/); const sMatch = t.match(/(\d+)s/); if (hMatch) seconds += parseInt(hMatch[1], 10) * 3600; if (mMatch) seconds += parseInt(mMatch[1], 10) * 60; if (sMatch) seconds += parseInt(sMatch[1], 10); return seconds > 0 ? seconds : null; } catch { return null; } } function extractRedgifsId(url) { if (typeof url !== "string" || !url) return null; try { const parsed = new URL(url); if (!parsed.hostname.endsWith("redgifs.com")) return null; const match = parsed.pathname.match(/^\/(?:watch|ifr)\/([A-Za-z0-9_-]+)/); return match ? match[1] : null; } catch { return null; } } const VIMEO_ID_RE = /^\d+$/; const VIMEO_HASH_RE = /^[A-Za-z0-9_-]+$/; function extractVimeoIdAndHash(url) { if (typeof url !== "string" || !url) return null; try { const parsed = new URL(url); const host = parsed.hostname.toLowerCase(); if (host !== "vimeo.com" && host !== "www.vimeo.com" && host !== "player.vimeo.com") return null; const pathSegments = parsed.pathname.split("/").filter(Boolean); if (pathSegments.length === 0) return null; const firstSegment = pathSegments[0]; if (VIMEO_ID_RE.test(firstSegment)) { const videoId = firstSegment; const hash = pathSegments[1] && VIMEO_HASH_RE.test(pathSegments[1]) ? pathSegments[1] : parsed.searchParams.get("h") || null; return { videoId, hash }; } if (firstSegment === "album" || firstSegment === "showcase") { const videoIdx = pathSegments.indexOf("video"); if (videoIdx !== -1 && pathSegments[videoIdx + 1] && VIMEO_ID_RE.test(pathSegments[videoIdx + 1])) { return { videoId: pathSegments[videoIdx + 1], hash: null }; } } if ((firstSegment === "channels" || firstSegment === "ondemand") && pathSegments[2] && VIMEO_ID_RE.test(pathSegments[2])) { return { videoId: pathSegments[2], hash: null }; } if (firstSegment === "groups" && pathSegments[3] && VIMEO_ID_RE.test(pathSegments[3])) { return { videoId: pathSegments[3], hash: null }; } if (firstSegment === "video" && pathSegments[1] && VIMEO_ID_RE.test(pathSegments[1])) { return { videoId: pathSegments[1], hash: parsed.searchParams.get("h") || null }; } } catch { return null; } return null; } function extractXId(url) { if (typeof url !== "string" || !url) return null; try { const parsed = new URL(url); const host = parsed.hostname.toLowerCase(); if (host === "x.com" || host === "twitter.com" || host === "www.x.com" || host === "www.twitter.com" || host === "mobile.twitter.com" || host === "mobile.x.com") { const m = parsed.pathname.match(/^\/[A-Za-z0-9_]+\/status\/(\d+)/); return m ? m[1] : null; } } catch { return null; } return null; } function isBlueskyUrl(url) { if (typeof url !== "string" || !url) return false; try { const parsed = new URL(url); const host = parsed.hostname.toLowerCase(); if (host === "bsky.app" || host === "www.bsky.app") { return /^\/profile\/[A-Za-z0-9_.:-]+\/post\/[A-Za-z0-9_.-]+/.test(parsed.pathname); } } catch { return false; } return false; } function extractMastodonEmbedInfo(url) { if (typeof url !== "string" || !url) return null; try { const parsed = new URL(url); const m = parsed.pathname.match(/^\/(@[A-Za-z0-9_.-]+)\/(\d+)$/); if (m) { return { host: parsed.origin, username: m[1], postId: m[2], embedUrl: `${parsed.origin}/${m[1]}/${m[2]}/embed` }; } } catch { return null; } return null; } const INLINE_IMG_EXT_RE = /\.(jpe?g|png|gif|webp)(?:\?|#|$)/i; function isNakedUrlAnchor(anchor, href) { const text2 = (anchor.textContent || "").trim(); if (!text2) { return true; } if (text2 === href) return true; try { return new URL(text2).href === new URL(href).href; } catch { return false; } } function transformInlineMedia(html2, mediaMetadata = null) { if (typeof html2 !== "string" || !html2) return html2; const doc = sharedParser.parseFromString(html2, "text/html"); const anchors = Array.from(doc.body.querySelectorAll("a[href]")); let mutated = false; for (const a of anchors) { let href = a.getAttribute("href") || ""; if (!href) continue; if (href.startsWith("http://") && (href.includes("imgur.com") || href.includes("redd.it") || href.includes("redditmedia.com"))) { href = href.replace("http://", "https://"); } const isGiphy = href.includes("giphy.com/gifs/"); if (!isAllowedRedditMediaHost(href)) continue; if (!INLINE_IMG_EXT_RE.test(href) && !isGiphy) continue; if (href.includes("imgur.com") && (href.includes("/gallery/") || href.includes("/a/") || href.includes("/t/"))) { continue; } if (!Config.autoExpandSingleImages && (href.includes("imgur.com") || href.includes("redd.it") || href.includes("redditmedia.com"))) { continue; } const isRedditGif = href.includes("://preview.redd.it/") && /\.gif(?:\?|#|$)/i.test(href); const isImgurGif = href.includes("imgur.com") && /\.(gifv|gif)(?:\?|#|$)/i.test(href); let finalMediaUrl = href; let treatAsVideo = false; if (isRedditGif) { try { const parsed = new URL(href); const filenameMatch = parsed.pathname.match(/\/([^/]+\.gif)/i); if (filenameMatch) { finalMediaUrl = `https://i.redd.it/${filenameMatch[1]}`; } } catch { } } else if (isImgurGif) { finalMediaUrl = href.replace(/\.gifv(?:\?|#|$)/i, ".gif").replace("://imgur.com", "://i.imgur.com"); } else if (isGiphy) { try { const parsed = new URL(href); const parts = parsed.pathname.split("/").filter(Boolean); const lastPart = parts[parts.length - 1]; const idParts = lastPart.split("-"); const giphyId = idParts[idParts.length - 1]; if (giphyId) { finalMediaUrl = `https://media.giphy.com/media/${giphyId}/giphy.gif`; } } catch { } } else { const isTrueVideo = /\.mp4(?:\?|#|$)/i.test(href) || /\.webm(?:\?|#|$)/i.test(href); if (isTrueVideo) { treatAsVideo = true; } } let width = null; let height = null; let mediaId = null; try { const parsed = new URL(href); const pathSegments = parsed.pathname.split("/").filter(Boolean); if (pathSegments.length > 0) { const lastSegment = pathSegments[pathSegments.length - 1]; const dotIdx = lastSegment.indexOf("."); mediaId = dotIdx !== -1 ? lastSegment.slice(0, dotIdx) : lastSegment; } } catch { } if (mediaMetadata && mediaId && mediaMetadata[mediaId]) { const meta = mediaMetadata[mediaId]; if (meta?.status === "valid" && meta.s) { width = meta.s.x || meta.s.width || null; height = meta.s.y || meta.s.height || null; } } if (!width || !height) { try { const u = new URL(href); const wVal = u.searchParams.get("width") || u.searchParams.get("w"); const hVal = u.searchParams.get("height") || u.searchParams.get("h"); if (wVal && hVal) { width = parseInt(wVal, 10); height = parseInt(hVal, 10); } } catch { } } const item = doc.createElement("div"); item.setAttribute("class", "gsrp-gallery-item gsrp-inline-media-container gsrp-is-loading"); item.setAttribute("role", "button"); item.setAttribute("tabindex", "0"); item.setAttribute("data-gallery-idx", "0"); if (width && height) { item.setAttribute("data-w", String(width)); item.setAttribute("data-h", String(height)); item.style.aspectRatio = `${width} / ${height}`; item.style.maxWidth = "100%"; item.style.maxHeight = "min(30vh, 300px)"; } else { item.style.aspectRatio = "16 / 9"; item.style.maxWidth = "100%"; item.style.maxHeight = "min(30vh, 300px)"; } if (treatAsVideo) { const video = doc.createElement("video"); video.setAttribute("src", finalMediaUrl); video.setAttribute("class", "gsrp-inline-media-video"); video.setAttribute("preload", "metadata"); video.setAttribute("playsinline", ""); video.setAttribute("loop", ""); video.setAttribute("muted", ""); video.setAttribute("data-gsrp-autoplay", "1"); video.setAttribute("data-gsrp-has-audio", "false"); video.setAttribute("data-fallback-src", href); video.setAttribute("onerror", "this.parentElement.classList.add('gsrp-video-error')"); video.style.maxWidth = "100%"; video.style.maxHeight = "min(30vh, 300px)"; video.style.borderRadius = "4px"; video.style.background = "#000"; video.style.display = "block"; video.style.margin = "0 auto"; if (width && height) { video.setAttribute("width", String(width)); video.setAttribute("height", String(height)); video.style.aspectRatio = `${width} / ${height}`; } else { video.style.aspectRatio = "16 / 9"; } item.appendChild(video); } else { const img = doc.createElement("img"); img.setAttribute("src", finalMediaUrl); img.setAttribute("class", "gsrp-inline-media-img"); img.setAttribute("loading", "lazy"); img.setAttribute("decoding", "async"); img.setAttribute("alt", ""); img.setAttribute("onerror", "this.parentElement.classList.add('gsrp-img-error')"); if (width && height) { img.setAttribute("width", String(width)); img.setAttribute("height", String(height)); img.style.aspectRatio = `${width} / ${height}`; img.style.minHeight = "0"; } else { img.style.minHeight = "0"; } img.addEventListener("load", () => { item.classList.remove("gsrp-is-loading"); if (!width || !height) { item.style.aspectRatio = "auto"; } }); img.addEventListener("error", () => { item.classList.remove("gsrp-is-loading"); item.classList.add("gsrp-img-error"); item.setAttribute("data-error-text", "⚠ Image unavailable"); }); item.appendChild(img); } if (isNakedUrlAnchor(a, href)) { a.replaceWith(item); } else { a.insertAdjacentElement("afterend", item); } mutated = true; } return mutated ? doc.body.innerHTML : html2; } function decodeAndCheckMedia(url) { if (typeof url !== "string" || !url) return null; const decoded = decodeHTMLEntities(url); return isAllowedRedditMediaHost(decoded) ? decoded : null; } const IMAGE_EXT_RE = /\.(jpe?g|png|gif|webp)(?:\?|$)/i; const IMGUR_ALBUM_RE = /^https?:\/\/(?:[im]\.)?imgur\.com\/(?:a|gallery)\/([A-Za-z0-9]+)/; function extractMedia(post) { const parent = post.crosspost_parent_list?.[0]; const hasOwnMedia = post.is_video === true || post.is_gallery === true || !!post.media?.oembed; const source = !hasOwnMedia && parent ? parent : post; const redgifsId = extractRedgifsId(source.url); if (redgifsId) { return { type: "redgifs_video", redgifsId }; } if (source.media?.oembed?.provider_name === "YouTube") { const ytId = extractYouTubeId(source.url) || extractYouTubeId(source.media.oembed.thumbnail_url); if (ytId) { return { type: "embed", url: `https://www.youtube-nocookie.com/embed/${ytId}?rel=0&enablejsapi=1`, providerName: "YouTube", width: null, height: null }; } } const rv = source.media?.reddit_video; if (source.is_video && rv?.fallback_url) { const decodedFallback = decodeAndCheckMedia(rv.fallback_url); if (decodedFallback) { const hlsUrl = rv.hls_url ? decodeAndCheckMedia(rv.hls_url) : null; const posterUrl = decodeAndCheckMedia(source.preview?.images?.[0]?.source?.url); const isPerfectLoops = typeof post.subreddit === "string" && post.subreddit.toLowerCase() === "perfectloops"; const isGifv = rv.is_gif === true || isPerfectLoops; return { type: "video", url: decodedFallback, hlsUrl, hasAudio: rv.has_audio === true, poster: posterUrl, width: rv.width || null, height: rv.height || null, duration: rv.duration || null, isGifv }; } } if (source.is_gallery && source.gallery_data?.items && source.media_metadata) { const images = []; for (const item of source.gallery_data.items) { const meta = source.media_metadata[item.media_id]; if (meta?.status !== "valid" || !meta.s) continue; const rawUrl = meta.s.u || meta.s.gif; if (!rawUrl) continue; const url = decodeAndCheckMedia(rawUrl); if (!url) continue; images.push({ url, width: meta.s.x || null, height: meta.s.y || null, caption: item.caption || "" }); } if (images.length > 0) return { type: "gallery", images }; } if (!source.is_self && !source.is_video && !source.is_gallery) { const directUrl = source.url || ""; if (typeof directUrl === "string" && directUrl.includes("imgur.com/")) { if (directUrl.endsWith(".gifv")) { const mp4Url = directUrl.replace(".gifv", ".mp4").replace("//imgur.com", "//i.imgur.com"); return { type: "video", url: mp4Url, hlsUrl: null, hasAudio: false, poster: null, width: null, height: null, isGifv: true }; } const imgurPageMatch = directUrl.match( /^https?:\/\/(?:[im]\.)?imgur\.com\/([A-Za-z0-9_-]+)$/ ); if (imgurPageMatch) { const directImgUrl = `https://i.imgur.com/${imgurPageMatch[1]}.png`; const w = source.preview?.images?.[0]?.source?.width || null; const h = source.preview?.images?.[0]?.source?.height || null; return { type: "image", url: directImgUrl, width: w, height: h }; } } if (IMAGE_EXT_RE.test(directUrl) && isAllowedRedditMediaHost(directUrl)) { const w = source.preview?.images?.[0]?.source?.width || null; const h = source.preview?.images?.[0]?.source?.height || null; return { type: "image", url: directUrl, width: w, height: h }; } const previewUrl = decodeAndCheckMedia(source.preview?.images?.[0]?.source?.url); if (previewUrl) { return { type: "image", url: previewUrl, width: source.preview?.images?.[0]?.source?.width || null, height: source.preview?.images?.[0]?.source?.height || null }; } } return null; } function tryFetchImgurGallery(post, callback, signal) { const match = post.url ? post.url.match(IMGUR_ALBUM_RE) : null; if (!match) { callback(); return; } if (signal?.aborted) { return; } const albumHash = match[1]; const req = GM_xmlhttpRequest({ method: "GET", url: `https://api.imgur.com/post/v1/albums/${albumHash}?client_id=${Config.imgurClientId || "546c25a59c58ad7"}&include=media`, timeout: 8e3, onload: (res) => { if (signal?.aborted) return; if (res.status === 200) { try { const json = JSON.parse(res.responseText); if (json && json.media && Array.isArray(json.media) && json.media.length > 0) { post.is_gallery = true; post.gallery_data = { items: json.media.map((img) => ({ media_id: `imgur_${img.id}`, caption: img.metadata?.description || img.metadata?.title || "" })) }; const metaMap = {}; post.media_metadata = metaMap; json.media.forEach((img) => { metaMap[`imgur_${img.id}`] = { status: "valid", s: { u: img.url, x: img.width || void 0, y: img.height || void 0 } }; }); } } catch { } } callback(); }, onerror: () => { if (signal?.aborted) return; callback(); }, ontimeout: () => { if (signal?.aborted) return; callback(); } }); if (signal) { const handleAbort = () => { try { req.abort(); } catch { } }; signal.addEventListener("abort", handleAbort, { once: true }); } } function formatTimestamp(utc) { if (typeof utc !== "number") return null; if (Config.timestampFormat === "relative") { return formatRelative(utc); } else if (Config.timestampFormat === "absolute-local") { return formatDateLocal(utc, Config.lang); } else { return formatDate(utc); } } function buildPayload(post, topComments) { const flair = post.link_flair_text || ""; const participantCounts = analyzeParticipants(topComments); const topParticipants = Array.from(participantCounts.entries()).sort((a, b) => b[1] - a[1]).slice(0, 10); return { id: post.id, subreddit: post.subreddit, permalink: post.permalink, author: post.author || "[deleted]", title: decodeHTMLEntities(post.title || ""), score: formatScore(post.ups), ratio: Math.round((post.upvote_ratio || 0) * 100), comments: formatScore(post.num_comments), date: formatTimestamp(post.created_utc), edited: typeof post.edited === "number" ? formatTimestamp(post.edited) : null, isArchived: post.archived === true, isNSFW: post.over_18 === true, isRemoved: post.removed_by_category !== null && post.removed_by_category !== void 0 || post.selftext === "[removed]" || post.selftext === "[deleted]" || post.edited !== false && post.edited !== void 0 && post.edited !== null && isRedactGeneratedText(post.selftext || ""), isRedacted: post.edited !== false && post.edited !== void 0 && post.edited !== null && isRedactGeneratedText(post.selftext || ""), removedByCategory: post.removed_by_category ?? null, isSolved: flair.toLowerCase().includes("solved") || flair.toLowerCase().includes("resolved"), flair: decodeHTMLEntities(flair), authorFlair: decodeHTMLEntities(post.author_flair_text || ""), isLocked: post.locked === true, isStickied: post.stickied === true, isOC: post.is_original_content === true, isSpoiler: post.spoiler === true, isContestMode: post.contest_mode === true, isHiddenScore: post.hide_score === true, isQuarantined: post.quarantine === true, awards: post.total_awards_received || 0, isGallery: post.is_gallery === true, galleryCount: post.is_gallery && post.gallery_data && post.gallery_data.items ? post.gallery_data.items.length : 0, galleryCaption: post.is_gallery && post.gallery_data && post.gallery_data.items && post.gallery_data.items[0]?.caption ? post.gallery_data.items[0].caption : "", isVideo: post.is_video === true, videoDuration: post.is_video ? post.media && post.media.reddit_video && post.media.reddit_video.duration || post.secure_media && post.secure_media.reddit_video && post.secure_media.reddit_video.duration || null : null, crossposts: post.num_crossposts || 0, displayedComments: countCommentNodes(topComments), uniqueParticipants: participantCounts.size, topParticipants, selftextHtml: post.selftext_html || "", selftext: post.selftext || "", url: post.url || "", isSelf: post.is_self === true, media: extractMedia(post), mediaMetadata: post.media_metadata || null, topComments, crosspostParent: post.crosspost_parent_list && post.crosspost_parent_list.length > 0 ? { subreddit: post.crosspost_parent_list[0].subreddit_name_prefixed || "", author: post.crosspost_parent_list[0].author || "[deleted]", title: post.crosspost_parent_list[0].title || "", score: formatScore(post.crosspost_parent_list[0].ups), comments: formatScore(post.crosspost_parent_list[0].num_comments), permalink: post.crosspost_parent_list[0].permalink, selftextHtml: post.crosspost_parent_list[0].selftext_html || "", selftext: post.crosspost_parent_list[0].selftext || "", isRedacted: post.crosspost_parent_list[0].edited !== false && post.crosspost_parent_list[0].edited !== void 0 && post.crosspost_parent_list[0].edited !== null && isRedactGeneratedText(post.crosspost_parent_list[0].selftext || ""), mediaMetadata: post.crosspost_parent_list[0].media_metadata || null } : null }; } const resolvedUrlsCache = new Map(); const inflightResolutions = new Map(); const MAX_RESOLVE_CACHE_SIZE = 100; function cacheResolvedUrl(url, resolved) { if (resolvedUrlsCache.size >= MAX_RESOLVE_CACHE_SIZE) { const firstKey = resolvedUrlsCache.keys().next().value; if (firstKey !== void 0) { resolvedUrlsCache.delete(firstKey); } } resolvedUrlsCache.set(url, resolved); } function resolveRedditUrl(url, callback, signal) { if (typeof url !== "string" || !url) { callback(url); return; } if (signal?.aborted) { return; } if (resolvedUrlsCache.has(url)) { callback(resolvedUrlsCache.get(url)); return; } let onAbortListener; const wrappedCb = (res) => { if (signal && onAbortListener) { signal.removeEventListener("abort", onAbortListener); } if (!signal?.aborted) { callback(res); } }; let state2 = inflightResolutions.get(url); const isFirst = !state2; if (!state2) { state2 = { waiters: [] }; inflightResolutions.set(url, state2); } const waiter = { wrappedCb }; state2.waiters.push(waiter); if (signal) { onAbortListener = () => { const currentState = inflightResolutions.get(url); if (!currentState) return; const idx = currentState.waiters.indexOf(waiter); if (idx !== -1) { currentState.waiters.splice(idx, 1); } if (currentState.waiters.length === 0) { if (currentState.abortRequest) { currentState.abortRequest(); } inflightResolutions.delete(url); } }; signal.addEventListener("abort", onAbortListener, { once: true }); } if (!isFirst) { return; } const onResolveFinished = (finalUrl) => { cacheResolvedUrl(url, finalUrl); const currentState = inflightResolutions.get(url); inflightResolutions.delete(url); if (currentState) { currentState.waiters.forEach((w) => { w.wrappedCb(finalUrl); }); } }; try { const parsed = new URL(url); const isOfficialRedditDomain = parsed.hostname === "reddit.com" || parsed.hostname.endsWith(".reddit.com") || parsed.hostname === "redd.it" || parsed.hostname.endsWith(".redd.it"); if (!isOfficialRedditDomain) { onResolveFinished(url); return; } if (parsed.protocol !== "http:" && parsed.protocol !== "https:") { onResolveFinished(url); return; } if (parsed.pathname.includes("/s/") || parsed.hostname === "v.redd.it") { const req = GM_xmlhttpRequest({ method: "HEAD", url, timeout: 5e3, onload: (res) => { const finalUrl = res.finalUrl || url; onResolveFinished(finalUrl); }, onerror: () => { onResolveFinished(url); }, ontimeout: () => { onResolveFinished(url); } }); state2.abortRequest = () => { try { req.abort(); } catch { } }; return; } if (parsed.hostname === "redd.it") { const m = url.match(/redd\.it\/([A-Za-z0-9_-]+)/); if (m) { onResolveFinished(`https://www.reddit.com/comments/${m[1]}`); return; } } } catch { onResolveFinished(url); return; } onResolveFinished(url); } function fetchRedditData(url, sortOverride, onSuccess, onError, signal) { resolveRedditUrl( url, (resolvedUrl) => { if (signal?.aborted) return; let cleanUrl = resolvedUrl.split("?")[0]; if (cleanUrl.endsWith("/")) cleanUrl = cleanUrl.slice(0, -1); const activeSort = sortOverride || Config.commentSort; const jsonUrl = `${cleanUrl}.json?sort=${activeSort}`; if (isBackoffActive()) { onError(FetchError.RATE_LIMIT); return; } const { owns, signal: sharedSignal } = registerInflight(jsonUrl, { onSuccess, onError, signal }); if (!owns) return; failoverFetch({ url: jsonUrl, label: "Primary request", signal: sharedSignal, onRateLimit: () => noteRateLimit(), onSuccess: (json) => { if (sharedSignal.aborted) return; clearBackoff(); try { const post = json?.[0]?.data?.children?.[0]?.data; if (!post) throw new Error("Structure mismatch"); const topComments = buildTopComments( json?.[1]?.data?.children || [], post.permalink || "" ); tryFetchImgurGallery( post, () => { if (sharedSignal.aborted) return; deliverSuccess(jsonUrl, buildPayload(post, topComments)); }, sharedSignal ); } catch { deliverError(jsonUrl, FetchError.PARSE_ERR); } }, onError: (err) => deliverError(jsonUrl, err) }); }, signal ); } function fetchMoreComments(permalink, targetDepth, sortOverride, onSuccess, onError) { let cleanUrl = permalink.split("?")[0]; if (cleanUrl.endsWith("/")) cleanUrl = cleanUrl.slice(0, -1); const activeSort = sortOverride || Config.commentSort; const jsonUrl = `https://www.reddit.com${cleanUrl}.json?sort=${activeSort}`; failoverFetch({ url: jsonUrl, label: "MoreComments", onSuccess: (json) => { try { const postPermalink = json?.[0]?.data?.children?.[0]?.data?.permalink || ""; const targetComment = json?.[1]?.data?.children?.[0]?.data; if (!targetComment) throw new Error("Structure mismatch"); const repliesData = targetComment.replies?.data?.children || []; const subtree = buildCommentTree(repliesData, targetDepth, postPermalink); onSuccess(subtree); } catch { onError(FetchError.PARSE_ERR); } }, onError: (err) => onError(err) }); } const cardHandlersMap = new WeakMap(); const linkStateMap = new WeakMap(); const badgeWiringMap = new WeakMap(); const translateButtonMap = new WeakMap(); const ytVolumeTimersMap = new WeakMap(); const globalInFlightFetches = new Map(); function createDataFetcher(link, cleanUrl, getCacheKey) { return function dataFetcher(cb, errCb, signal) { const cacheKey = getCacheKey(); if (redditDataCache.has(cacheKey)) { if (cb) cb(redditDataCache.get(cacheKey)); return; } if (link.dataset.gsrpRedditStat === "fetching") { let linkState = linkStateMap.get(link); if (!linkState) { linkState = {}; linkStateMap.set(link, linkState); } if (!linkState.fetchWaiters) { linkState.fetchWaiters = []; } linkState.fetchWaiters.push({ cb: cb ?? void 0, errCb: errCb ?? void 0 }); return; } link.dataset.gsrpRedditStat = "fetching"; const wrapperWaiter = { cb: (data) => { link.dataset.gsrpRedditStat = "done"; if (cb) { try { cb(data); } catch (err) { logger.error("dataFetcher cb threw", err); } } const linkState = linkStateMap.get(link); if (linkState && linkState.fetchWaiters) { linkState.fetchWaiters.forEach((w) => { try { if (w.cb) w.cb(data); } catch (err) { logger.error("dataFetcher waiter cb threw", err); } }); linkState.fetchWaiters = null; } }, errCb: (err) => { link.dataset.gsrpRedditStat = "error"; if (errCb) { try { errCb(err); } catch (e) { logger.error("dataFetcher errCb threw", e); } } const linkState = linkStateMap.get(link); if (linkState && linkState.fetchWaiters) { linkState.fetchWaiters.forEach((w) => { try { if (w.errCb) w.errCb(err); } catch (e) { logger.error("dataFetcher waiter errCb threw", e); } }); linkState.fetchWaiters = null; } }, signal: signal ?? void 0 }; let state2 = globalInFlightFetches.get(cacheKey); const isFirst = !state2; if (!state2) { state2 = { waiters: [], controller: new AbortController() }; globalInFlightFetches.set(cacheKey, state2); } state2.waiters.push(wrapperWaiter); if (signal) { const onAbort = () => { const cur = globalInFlightFetches.get(cacheKey); if (!cur) return; cur.waiters = cur.waiters.filter((w) => w !== wrapperWaiter); if (cur.waiters.length === 0) { cur.controller.abort(); globalInFlightFetches.delete(cacheKey); } }; signal.addEventListener("abort", onAbort, { once: true }); wrapperWaiter._cleanupAbort = () => { signal.removeEventListener("abort", onAbort); }; } if (!isFirst) { return; } fetchRedditData( cleanUrl, Config.commentSort, (data) => { redditDataCache.set(cacheKey, data); const cur = globalInFlightFetches.get(cacheKey); globalInFlightFetches.delete(cacheKey); if (cur) { cur.waiters.forEach((w) => { if (w._cleanupAbort) w._cleanupAbort(); if (w.signal?.aborted) return; if (w.cb) w.cb(data); }); } }, (err) => { const cur = globalInFlightFetches.get(cacheKey); globalInFlightFetches.delete(cacheKey); if (cur) { cur.waiters.forEach((w) => { if (w._cleanupAbort) w._cleanupAbort(); if (w.signal?.aborted) return; if (w.errCb) w.errCb(err); }); } }, state2.controller.signal ); }; } function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; } function _arrayWithHoles(r) { if (Array.isArray(r)) return r; } function _iterableToArrayLimit(r, l) { var t = null == r ? null : "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (null != t) { var e, n, i, u, a = [], f = true, o = false; try { if (i = (t = t.call(r)).next, 0 === l) ; else for (; !(f = (e = i.call(t)).done) && (a.push(e.value), a.length !== l); f = true) ; } catch (r2) { o = true, n = r2; } finally { try { if (!f && null != t.return && (u = t.return(), Object(u) !== u)) return; } finally { if (o) throw n; } } return a; } } function _nonIterableRest() { throw new TypeError("Invalid attempt to destructure non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } function _slicedToArray(r, e) { return _arrayWithHoles(r) || _iterableToArrayLimit(r, e) || _unsupportedIterableToArray(r, e) || _nonIterableRest(); } function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } } const entries = Object.entries, setPrototypeOf = Object.setPrototypeOf, isFrozen = Object.isFrozen, getPrototypeOf = Object.getPrototypeOf, getOwnPropertyDescriptor = Object.getOwnPropertyDescriptor; let freeze = Object.freeze, seal = Object.seal, create = Object.create; let _ref = typeof Reflect !== "undefined" && Reflect, apply = _ref.apply, construct = _ref.construct; if (!freeze) { freeze = function freeze2(x) { return x; }; } if (!seal) { seal = function seal2(x) { return x; }; } if (!apply) { apply = function apply2(func, thisArg) { for (var _len = arguments.length, args = new Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) { args[_key - 2] = arguments[_key]; } return func.apply(thisArg, args); }; } if (!construct) { construct = function construct2(Func) { for (var _len2 = arguments.length, args = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) { args[_key2 - 1] = arguments[_key2]; } return new Func(...args); }; } const arrayForEach = unapply(Array.prototype.forEach); const arrayLastIndexOf = unapply(Array.prototype.lastIndexOf); const arrayPop = unapply(Array.prototype.pop); const arrayPush = unapply(Array.prototype.push); const arraySplice = unapply(Array.prototype.splice); const arrayIsArray = Array.isArray; const stringToLowerCase = unapply(String.prototype.toLowerCase); const stringToString = unapply(String.prototype.toString); const stringMatch = unapply(String.prototype.match); const stringReplace = unapply(String.prototype.replace); const stringIndexOf = unapply(String.prototype.indexOf); const stringTrim = unapply(String.prototype.trim); const numberToString = unapply(Number.prototype.toString); const booleanToString = unapply(Boolean.prototype.toString); const bigintToString = typeof BigInt === "undefined" ? null : unapply(BigInt.prototype.toString); const symbolToString = typeof Symbol === "undefined" ? null : unapply(Symbol.prototype.toString); const objectHasOwnProperty = unapply(Object.prototype.hasOwnProperty); const objectToString = unapply(Object.prototype.toString); const regExpTest = unapply(RegExp.prototype.test); const typeErrorCreate = unconstruct(TypeError); function unapply(func) { return function(thisArg) { if (thisArg instanceof RegExp) { thisArg.lastIndex = 0; } for (var _len3 = arguments.length, args = new Array(_len3 > 1 ? _len3 - 1 : 0), _key3 = 1; _key3 < _len3; _key3++) { args[_key3 - 1] = arguments[_key3]; } return apply(func, thisArg, args); }; } function unconstruct(Func) { return function() { for (var _len4 = arguments.length, args = new Array(_len4), _key4 = 0; _key4 < _len4; _key4++) { args[_key4] = arguments[_key4]; } return construct(Func, args); }; } function addToSet(set, array) { let transformCaseFunc = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : stringToLowerCase; if (setPrototypeOf) { setPrototypeOf(set, null); } if (!arrayIsArray(array)) { return set; } let l = array.length; while (l--) { let element = array[l]; if (typeof element === "string") { const lcElement = transformCaseFunc(element); if (lcElement !== element) { if (!isFrozen(array)) { array[l] = lcElement; } element = lcElement; } } set[element] = true; } return set; } function cleanArray(array) { for (let index = 0; index < array.length; index++) { const isPropertyExist = objectHasOwnProperty(array, index); if (!isPropertyExist) { array[index] = null; } } return array; } function clone(object) { const newObject = create(null); for (const _ref2 of entries(object)) { var _ref3 = _slicedToArray(_ref2, 2); const property = _ref3[0]; const value = _ref3[1]; const isPropertyExist = objectHasOwnProperty(object, property); if (isPropertyExist) { if (arrayIsArray(value)) { newObject[property] = cleanArray(value); } else if (value && typeof value === "object" && value.constructor === Object) { newObject[property] = clone(value); } else { newObject[property] = value; } } } return newObject; } function stringifyValue(value) { switch (typeof value) { case "string": { return value; } case "number": { return numberToString(value); } case "boolean": { return booleanToString(value); } case "bigint": { return bigintToString ? bigintToString(value) : "0"; } case "symbol": { return symbolToString ? symbolToString(value) : "Symbol()"; } case "undefined": { return objectToString(value); } case "function": case "object": { if (value === null) { return objectToString(value); } const valueAsRecord = value; const valueToString = lookupGetter(valueAsRecord, "toString"); if (typeof valueToString === "function") { const stringified = valueToString(valueAsRecord); return typeof stringified === "string" ? stringified : objectToString(stringified); } return objectToString(value); } default: { return objectToString(value); } } } function lookupGetter(object, prop) { while (object !== null) { const desc = getOwnPropertyDescriptor(object, prop); if (desc) { if (desc.get) { return unapply(desc.get); } if (typeof desc.value === "function") { return unapply(desc.value); } } object = getPrototypeOf(object); } function fallbackValue() { return null; } return fallbackValue; } function isRegex(value) { try { regExpTest(value, ""); return true; } catch (_unused) { return false; } } const html$1 = freeze(["a", "abbr", "acronym", "address", "area", "article", "aside", "audio", "b", "bdi", "bdo", "big", "blink", "blockquote", "body", "br", "button", "canvas", "caption", "center", "cite", "code", "col", "colgroup", "content", "data", "datalist", "dd", "decorator", "del", "details", "dfn", "dialog", "dir", "div", "dl", "dt", "element", "em", "fieldset", "figcaption", "figure", "font", "footer", "form", "h1", "h2", "h3", "h4", "h5", "h6", "head", "header", "hgroup", "hr", "html", "i", "img", "input", "ins", "kbd", "label", "legend", "li", "main", "map", "mark", "marquee", "menu", "menuitem", "meter", "nav", "nobr", "ol", "optgroup", "option", "output", "p", "picture", "pre", "progress", "q", "rp", "rt", "ruby", "s", "samp", "search", "section", "select", "shadow", "slot", "small", "source", "spacer", "span", "strike", "strong", "style", "sub", "summary", "sup", "table", "tbody", "td", "template", "textarea", "tfoot", "th", "thead", "time", "tr", "track", "tt", "u", "ul", "var", "video", "wbr"]); const svg$1 = freeze(["svg", "a", "altglyph", "altglyphdef", "altglyphitem", "animatecolor", "animatemotion", "animatetransform", "circle", "clippath", "defs", "desc", "ellipse", "enterkeyhint", "exportparts", "filter", "font", "g", "glyph", "glyphref", "hkern", "image", "inputmode", "line", "lineargradient", "marker", "mask", "metadata", "mpath", "part", "path", "pattern", "polygon", "polyline", "radialgradient", "rect", "stop", "style", "switch", "symbol", "text", "textpath", "title", "tref", "tspan", "view", "vkern"]); const svgFilters = freeze(["feBlend", "feColorMatrix", "feComponentTransfer", "feComposite", "feConvolveMatrix", "feDiffuseLighting", "feDisplacementMap", "feDistantLight", "feDropShadow", "feFlood", "feFuncA", "feFuncB", "feFuncG", "feFuncR", "feGaussianBlur", "feImage", "feMerge", "feMergeNode", "feMorphology", "feOffset", "fePointLight", "feSpecularLighting", "feSpotLight", "feTile", "feTurbulence"]); const svgDisallowed = freeze(["animate", "color-profile", "cursor", "discard", "font-face", "font-face-format", "font-face-name", "font-face-src", "font-face-uri", "foreignobject", "hatch", "hatchpath", "mesh", "meshgradient", "meshpatch", "meshrow", "missing-glyph", "script", "set", "solidcolor", "unknown", "use"]); const mathMl$1 = freeze(["math", "menclose", "merror", "mfenced", "mfrac", "mglyph", "mi", "mlabeledtr", "mmultiscripts", "mn", "mo", "mover", "mpadded", "mphantom", "mroot", "mrow", "ms", "mspace", "msqrt", "mstyle", "msub", "msup", "msubsup", "mtable", "mtd", "mtext", "mtr", "munder", "munderover", "mprescripts"]); const mathMlDisallowed = freeze(["maction", "maligngroup", "malignmark", "mlongdiv", "mscarries", "mscarry", "msgroup", "mstack", "msline", "msrow", "semantics", "annotation", "annotation-xml", "mprescripts", "none"]); const text = freeze(["#text"]); const html = freeze(["accept", "action", "align", "alt", "autocapitalize", "autocomplete", "autopictureinpicture", "autoplay", "background", "bgcolor", "border", "capture", "cellpadding", "cellspacing", "checked", "cite", "class", "clear", "color", "cols", "colspan", "command", "commandfor", "controls", "controlslist", "coords", "crossorigin", "datetime", "decoding", "default", "dir", "disabled", "disablepictureinpicture", "disableremoteplayback", "download", "draggable", "enctype", "enterkeyhint", "exportparts", "face", "for", "headers", "height", "hidden", "high", "href", "hreflang", "id", "inert", "inputmode", "integrity", "ismap", "kind", "label", "lang", "list", "loading", "loop", "low", "max", "maxlength", "media", "method", "min", "minlength", "multiple", "muted", "name", "nonce", "noshade", "novalidate", "nowrap", "open", "optimum", "part", "pattern", "placeholder", "playsinline", "popover", "popovertarget", "popovertargetaction", "poster", "preload", "pubdate", "radiogroup", "readonly", "rel", "required", "rev", "reversed", "role", "rows", "rowspan", "spellcheck", "scope", "selected", "shape", "size", "sizes", "slot", "span", "srclang", "start", "src", "srcset", "step", "style", "summary", "tabindex", "title", "translate", "type", "usemap", "valign", "value", "width", "wrap", "xmlns"]); const svg = freeze(["accent-height", "accumulate", "additive", "alignment-baseline", "amplitude", "ascent", "attributename", "attributetype", "azimuth", "basefrequency", "baseline-shift", "begin", "bias", "by", "class", "clip", "clippathunits", "clip-path", "clip-rule", "color", "color-interpolation", "color-interpolation-filters", "color-profile", "color-rendering", "cx", "cy", "d", "dx", "dy", "diffuseconstant", "direction", "display", "divisor", "dur", "edgemode", "elevation", "end", "exponent", "fill", "fill-opacity", "fill-rule", "filter", "filterunits", "flood-color", "flood-opacity", "font-family", "font-size", "font-size-adjust", "font-stretch", "font-style", "font-variant", "font-weight", "fx", "fy", "g1", "g2", "glyph-name", "glyphref", "gradientunits", "gradienttransform", "height", "href", "id", "image-rendering", "in", "in2", "intercept", "k", "k1", "k2", "k3", "k4", "kerning", "keypoints", "keysplines", "keytimes", "lang", "lengthadjust", "letter-spacing", "kernelmatrix", "kernelunitlength", "lighting-color", "local", "marker-end", "marker-mid", "marker-start", "markerheight", "markerunits", "markerwidth", "maskcontentunits", "maskunits", "max", "mask", "mask-type", "media", "method", "mode", "min", "name", "numoctaves", "offset", "operator", "opacity", "order", "orient", "orientation", "origin", "overflow", "paint-order", "path", "pathlength", "patterncontentunits", "patterntransform", "patternunits", "points", "preservealpha", "preserveaspectratio", "primitiveunits", "r", "rx", "ry", "radius", "refx", "refy", "repeatcount", "repeatdur", "restart", "result", "rotate", "scale", "seed", "shape-rendering", "slope", "specularconstant", "specularexponent", "spreadmethod", "startoffset", "stddeviation", "stitchtiles", "stop-color", "stop-opacity", "stroke-dasharray", "stroke-dashoffset", "stroke-linecap", "stroke-linejoin", "stroke-miterlimit", "stroke-opacity", "stroke", "stroke-width", "style", "surfacescale", "systemlanguage", "tabindex", "tablevalues", "targetx", "targety", "transform", "transform-origin", "text-anchor", "text-decoration", "text-rendering", "textlength", "type", "u1", "u2", "unicode", "values", "viewbox", "visibility", "version", "vert-adv-y", "vert-origin-x", "vert-origin-y", "width", "word-spacing", "wrap", "writing-mode", "xchannelselector", "ychannelselector", "x", "x1", "x2", "xmlns", "y", "y1", "y2", "z", "zoomandpan"]); const mathMl = freeze(["accent", "accentunder", "align", "bevelled", "close", "columnalign", "columnlines", "columnspacing", "columnspan", "denomalign", "depth", "dir", "display", "displaystyle", "encoding", "fence", "frame", "height", "href", "id", "largeop", "length", "linethickness", "lquote", "lspace", "mathbackground", "mathcolor", "mathsize", "mathvariant", "maxsize", "minsize", "movablelimits", "notation", "numalign", "open", "rowalign", "rowlines", "rowspacing", "rowspan", "rspace", "rquote", "scriptlevel", "scriptminsize", "scriptsizemultiplier", "selection", "separator", "separators", "stretchy", "subscriptshift", "supscriptshift", "symmetric", "voffset", "width", "xmlns"]); const xml = freeze(["xlink:href", "xml:id", "xlink:title", "xml:space", "xmlns:xlink"]); const MUSTACHE_EXPR = seal(/{{[\w\W]*|^[\w\W]*}}/g); const ERB_EXPR = seal(/<%[\w\W]*|^[\w\W]*%>/g); const TMPLIT_EXPR = seal(/\${[\w\W]*/g); const DATA_ATTR = seal(/^data-[\-\w.\u00B7-\uFFFF]+$/); const ARIA_ATTR = seal(/^aria-[\-\w]+$/); const IS_ALLOWED_URI = seal( /^(?:(?:(?:f|ht)tps?|mailto|tel|callto|sms|cid|xmpp|matrix):|[^a-z]|[a-z+.\-]+(?:[^a-z+.\-:]|$))/i ); const IS_SCRIPT_OR_DATA = seal(/^(?:\w+script|data):/i); const ATTR_WHITESPACE = seal( /[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205F\u3000]/g ); const DOCTYPE_NAME = seal(/^html$/i); const CUSTOM_ELEMENT = seal(/^[a-z][.\w]*(-[.\w]+)+$/i); const NODE_TYPE = { element: 1, text: 3, progressingInstruction: 7, comment: 8, document: 9 }; const getGlobal = function getGlobal2() { return typeof window === "undefined" ? null : window; }; const _createTrustedTypesPolicy = function _createTrustedTypesPolicy2(trustedTypes, purifyHostElement) { if (typeof trustedTypes !== "object" || typeof trustedTypes.createPolicy !== "function") { return null; } let suffix = null; const ATTR_NAME = "data-tt-policy-suffix"; if (purifyHostElement && purifyHostElement.hasAttribute(ATTR_NAME)) { suffix = purifyHostElement.getAttribute(ATTR_NAME); } const policyName = "dompurify" + (suffix ? "#" + suffix : ""); try { return trustedTypes.createPolicy(policyName, { createHTML(html2) { return html2; }, createScriptURL(scriptUrl) { return scriptUrl; } }); } catch (_) { console.warn("TrustedTypes policy " + policyName + " could not be created."); return null; } }; const _createHooksMap = function _createHooksMap2() { return { afterSanitizeAttributes: [], afterSanitizeElements: [], afterSanitizeShadowDOM: [], beforeSanitizeAttributes: [], beforeSanitizeElements: [], beforeSanitizeShadowDOM: [], uponSanitizeAttribute: [], uponSanitizeElement: [], uponSanitizeShadowNode: [] }; }; function createDOMPurify() { let window2 = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : getGlobal(); const DOMPurify = (root) => createDOMPurify(root); DOMPurify.version = "3.4.5"; DOMPurify.removed = []; if (!window2 || !window2.document || window2.document.nodeType !== NODE_TYPE.document || !window2.Element) { DOMPurify.isSupported = false; return DOMPurify; } let document2 = window2.document; const originalDocument = document2; const currentScript = originalDocument.currentScript; const DocumentFragment = window2.DocumentFragment, HTMLTemplateElement = window2.HTMLTemplateElement, Node2 = window2.Node, Element2 = window2.Element, NodeFilter2 = window2.NodeFilter, _window$NamedNodeMap = window2.NamedNodeMap, NamedNodeMap = _window$NamedNodeMap === void 0 ? window2.NamedNodeMap || window2.MozNamedAttrMap : _window$NamedNodeMap, HTMLFormElement = window2.HTMLFormElement, DOMParser2 = window2.DOMParser, trustedTypes = window2.trustedTypes; const ElementPrototype = Element2.prototype; const cloneNode = lookupGetter(ElementPrototype, "cloneNode"); const remove = lookupGetter(ElementPrototype, "remove"); const getNextSibling = lookupGetter(ElementPrototype, "nextSibling"); const getChildNodes = lookupGetter(ElementPrototype, "childNodes"); const getParentNode = lookupGetter(ElementPrototype, "parentNode"); const getNodeType = Node2 && Node2.prototype ? lookupGetter(Node2.prototype, "nodeType") : null; if (typeof HTMLTemplateElement === "function") { const template = document2.createElement("template"); if (template.content && template.content.ownerDocument) { document2 = template.content.ownerDocument; } } let trustedTypesPolicy; let emptyHTML = ""; const _document = document2, implementation = _document.implementation, createNodeIterator = _document.createNodeIterator, createDocumentFragment = _document.createDocumentFragment, getElementsByTagName = _document.getElementsByTagName; const importNode = originalDocument.importNode; let hooks = _createHooksMap(); DOMPurify.isSupported = typeof entries === "function" && typeof getParentNode === "function" && implementation && implementation.createHTMLDocument !== void 0; const MUSTACHE_EXPR$1 = MUSTACHE_EXPR, ERB_EXPR$1 = ERB_EXPR, TMPLIT_EXPR$1 = TMPLIT_EXPR, DATA_ATTR$1 = DATA_ATTR, ARIA_ATTR$1 = ARIA_ATTR, IS_SCRIPT_OR_DATA$1 = IS_SCRIPT_OR_DATA, ATTR_WHITESPACE$1 = ATTR_WHITESPACE, CUSTOM_ELEMENT$1 = CUSTOM_ELEMENT; let IS_ALLOWED_URI$1 = IS_ALLOWED_URI; let ALLOWED_TAGS2 = null; const DEFAULT_ALLOWED_TAGS = addToSet({}, [...html$1, ...svg$1, ...svgFilters, ...mathMl$1, ...text]); let ALLOWED_ATTR = null; const DEFAULT_ALLOWED_ATTR = addToSet({}, [...html, ...svg, ...mathMl, ...xml]); let CUSTOM_ELEMENT_HANDLING = Object.seal(create(null, { tagNameCheck: { writable: true, configurable: false, enumerable: true, value: null }, attributeNameCheck: { writable: true, configurable: false, enumerable: true, value: null }, allowCustomizedBuiltInElements: { writable: true, configurable: false, enumerable: true, value: false } })); let FORBID_TAGS = null; let FORBID_ATTR = null; const EXTRA_ELEMENT_HANDLING = Object.seal(create(null, { tagCheck: { writable: true, configurable: false, enumerable: true, value: null }, attributeCheck: { writable: true, configurable: false, enumerable: true, value: null } })); let ALLOW_ARIA_ATTR = true; let ALLOW_DATA_ATTR = true; let ALLOW_UNKNOWN_PROTOCOLS = false; let ALLOW_SELF_CLOSE_IN_ATTR = true; let SAFE_FOR_TEMPLATES = false; let SAFE_FOR_XML = true; let WHOLE_DOCUMENT = false; let SET_CONFIG = false; let FORCE_BODY = false; let RETURN_DOM = false; let RETURN_DOM_FRAGMENT = false; let RETURN_TRUSTED_TYPE = false; let SANITIZE_DOM = true; let SANITIZE_NAMED_PROPS = false; const SANITIZE_NAMED_PROPS_PREFIX = "user-content-"; let KEEP_CONTENT = true; let IN_PLACE = false; let USE_PROFILES = {}; let FORBID_CONTENTS = null; const DEFAULT_FORBID_CONTENTS = addToSet({}, ["annotation-xml", "audio", "colgroup", "desc", "foreignobject", "head", "iframe", "math", "mi", "mn", "mo", "ms", "mtext", "noembed", "noframes", "noscript", "plaintext", "script", "style", "svg", "template", "thead", "title", "video", "xmp"]); let DATA_URI_TAGS = null; const DEFAULT_DATA_URI_TAGS = addToSet({}, ["audio", "video", "img", "source", "image", "track"]); let URI_SAFE_ATTRIBUTES = null; const DEFAULT_URI_SAFE_ATTRIBUTES = addToSet({}, ["alt", "class", "for", "id", "label", "name", "pattern", "placeholder", "role", "summary", "title", "value", "style", "xmlns"]); const MATHML_NAMESPACE = "http://www.w3.org/1998/Math/MathML"; const SVG_NAMESPACE = "http://www.w3.org/2000/svg"; const HTML_NAMESPACE = "http://www.w3.org/1999/xhtml"; let NAMESPACE = HTML_NAMESPACE; let IS_EMPTY_INPUT = false; let ALLOWED_NAMESPACES = null; const DEFAULT_ALLOWED_NAMESPACES = addToSet({}, [MATHML_NAMESPACE, SVG_NAMESPACE, HTML_NAMESPACE], stringToString); let MATHML_TEXT_INTEGRATION_POINTS = addToSet({}, ["mi", "mo", "mn", "ms", "mtext"]); let HTML_INTEGRATION_POINTS = addToSet({}, ["annotation-xml"]); const COMMON_SVG_AND_HTML_ELEMENTS = addToSet({}, ["title", "style", "font", "a", "script"]); let PARSER_MEDIA_TYPE = null; const SUPPORTED_PARSER_MEDIA_TYPES = ["application/xhtml+xml", "text/html"]; const DEFAULT_PARSER_MEDIA_TYPE = "text/html"; let transformCaseFunc = null; let CONFIG = null; const formElement = document2.createElement("form"); const isRegexOrFunction = function isRegexOrFunction2(testValue) { return testValue instanceof RegExp || testValue instanceof Function; }; const _parseConfig = function _parseConfig2() { let cfg = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : {}; if (CONFIG && CONFIG === cfg) { return; } if (!cfg || typeof cfg !== "object") { cfg = {}; } cfg = clone(cfg); PARSER_MEDIA_TYPE = SUPPORTED_PARSER_MEDIA_TYPES.indexOf(cfg.PARSER_MEDIA_TYPE) === -1 ? DEFAULT_PARSER_MEDIA_TYPE : cfg.PARSER_MEDIA_TYPE; transformCaseFunc = PARSER_MEDIA_TYPE === "application/xhtml+xml" ? stringToString : stringToLowerCase; ALLOWED_TAGS2 = objectHasOwnProperty(cfg, "ALLOWED_TAGS") && arrayIsArray(cfg.ALLOWED_TAGS) ? addToSet({}, cfg.ALLOWED_TAGS, transformCaseFunc) : DEFAULT_ALLOWED_TAGS; ALLOWED_ATTR = objectHasOwnProperty(cfg, "ALLOWED_ATTR") && arrayIsArray(cfg.ALLOWED_ATTR) ? addToSet({}, cfg.ALLOWED_ATTR, transformCaseFunc) : DEFAULT_ALLOWED_ATTR; ALLOWED_NAMESPACES = objectHasOwnProperty(cfg, "ALLOWED_NAMESPACES") && arrayIsArray(cfg.ALLOWED_NAMESPACES) ? addToSet({}, cfg.ALLOWED_NAMESPACES, stringToString) : DEFAULT_ALLOWED_NAMESPACES; URI_SAFE_ATTRIBUTES = objectHasOwnProperty(cfg, "ADD_URI_SAFE_ATTR") && arrayIsArray(cfg.ADD_URI_SAFE_ATTR) ? addToSet(clone(DEFAULT_URI_SAFE_ATTRIBUTES), cfg.ADD_URI_SAFE_ATTR, transformCaseFunc) : DEFAULT_URI_SAFE_ATTRIBUTES; DATA_URI_TAGS = objectHasOwnProperty(cfg, "ADD_DATA_URI_TAGS") && arrayIsArray(cfg.ADD_DATA_URI_TAGS) ? addToSet(clone(DEFAULT_DATA_URI_TAGS), cfg.ADD_DATA_URI_TAGS, transformCaseFunc) : DEFAULT_DATA_URI_TAGS; FORBID_CONTENTS = objectHasOwnProperty(cfg, "FORBID_CONTENTS") && arrayIsArray(cfg.FORBID_CONTENTS) ? addToSet({}, cfg.FORBID_CONTENTS, transformCaseFunc) : DEFAULT_FORBID_CONTENTS; FORBID_TAGS = objectHasOwnProperty(cfg, "FORBID_TAGS") && arrayIsArray(cfg.FORBID_TAGS) ? addToSet({}, cfg.FORBID_TAGS, transformCaseFunc) : clone({}); FORBID_ATTR = objectHasOwnProperty(cfg, "FORBID_ATTR") && arrayIsArray(cfg.FORBID_ATTR) ? addToSet({}, cfg.FORBID_ATTR, transformCaseFunc) : clone({}); USE_PROFILES = objectHasOwnProperty(cfg, "USE_PROFILES") ? cfg.USE_PROFILES && typeof cfg.USE_PROFILES === "object" ? clone(cfg.USE_PROFILES) : cfg.USE_PROFILES : false; ALLOW_ARIA_ATTR = cfg.ALLOW_ARIA_ATTR !== false; ALLOW_DATA_ATTR = cfg.ALLOW_DATA_ATTR !== false; ALLOW_UNKNOWN_PROTOCOLS = cfg.ALLOW_UNKNOWN_PROTOCOLS || false; ALLOW_SELF_CLOSE_IN_ATTR = cfg.ALLOW_SELF_CLOSE_IN_ATTR !== false; SAFE_FOR_TEMPLATES = cfg.SAFE_FOR_TEMPLATES || false; SAFE_FOR_XML = cfg.SAFE_FOR_XML !== false; WHOLE_DOCUMENT = cfg.WHOLE_DOCUMENT || false; RETURN_DOM = cfg.RETURN_DOM || false; RETURN_DOM_FRAGMENT = cfg.RETURN_DOM_FRAGMENT || false; RETURN_TRUSTED_TYPE = cfg.RETURN_TRUSTED_TYPE || false; FORCE_BODY = cfg.FORCE_BODY || false; SANITIZE_DOM = cfg.SANITIZE_DOM !== false; SANITIZE_NAMED_PROPS = cfg.SANITIZE_NAMED_PROPS || false; KEEP_CONTENT = cfg.KEEP_CONTENT !== false; IN_PLACE = cfg.IN_PLACE || false; IS_ALLOWED_URI$1 = isRegex(cfg.ALLOWED_URI_REGEXP) ? cfg.ALLOWED_URI_REGEXP : IS_ALLOWED_URI; NAMESPACE = typeof cfg.NAMESPACE === "string" ? cfg.NAMESPACE : HTML_NAMESPACE; MATHML_TEXT_INTEGRATION_POINTS = objectHasOwnProperty(cfg, "MATHML_TEXT_INTEGRATION_POINTS") && cfg.MATHML_TEXT_INTEGRATION_POINTS && typeof cfg.MATHML_TEXT_INTEGRATION_POINTS === "object" ? clone(cfg.MATHML_TEXT_INTEGRATION_POINTS) : addToSet({}, ["mi", "mo", "mn", "ms", "mtext"]); HTML_INTEGRATION_POINTS = objectHasOwnProperty(cfg, "HTML_INTEGRATION_POINTS") && cfg.HTML_INTEGRATION_POINTS && typeof cfg.HTML_INTEGRATION_POINTS === "object" ? clone(cfg.HTML_INTEGRATION_POINTS) : addToSet({}, ["annotation-xml"]); const customElementHandling = objectHasOwnProperty(cfg, "CUSTOM_ELEMENT_HANDLING") && cfg.CUSTOM_ELEMENT_HANDLING && typeof cfg.CUSTOM_ELEMENT_HANDLING === "object" ? clone(cfg.CUSTOM_ELEMENT_HANDLING) : create(null); CUSTOM_ELEMENT_HANDLING = create(null); if (objectHasOwnProperty(customElementHandling, "tagNameCheck") && isRegexOrFunction(customElementHandling.tagNameCheck)) { CUSTOM_ELEMENT_HANDLING.tagNameCheck = customElementHandling.tagNameCheck; } if (objectHasOwnProperty(customElementHandling, "attributeNameCheck") && isRegexOrFunction(customElementHandling.attributeNameCheck)) { CUSTOM_ELEMENT_HANDLING.attributeNameCheck = customElementHandling.attributeNameCheck; } if (objectHasOwnProperty(customElementHandling, "allowCustomizedBuiltInElements") && typeof customElementHandling.allowCustomizedBuiltInElements === "boolean") { CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements = customElementHandling.allowCustomizedBuiltInElements; } if (SAFE_FOR_TEMPLATES) { ALLOW_DATA_ATTR = false; } if (RETURN_DOM_FRAGMENT) { RETURN_DOM = true; } if (USE_PROFILES) { ALLOWED_TAGS2 = addToSet({}, text); ALLOWED_ATTR = create(null); if (USE_PROFILES.html === true) { addToSet(ALLOWED_TAGS2, html$1); addToSet(ALLOWED_ATTR, html); } if (USE_PROFILES.svg === true) { addToSet(ALLOWED_TAGS2, svg$1); addToSet(ALLOWED_ATTR, svg); addToSet(ALLOWED_ATTR, xml); } if (USE_PROFILES.svgFilters === true) { addToSet(ALLOWED_TAGS2, svgFilters); addToSet(ALLOWED_ATTR, svg); addToSet(ALLOWED_ATTR, xml); } if (USE_PROFILES.mathMl === true) { addToSet(ALLOWED_TAGS2, mathMl$1); addToSet(ALLOWED_ATTR, mathMl); addToSet(ALLOWED_ATTR, xml); } } EXTRA_ELEMENT_HANDLING.tagCheck = null; EXTRA_ELEMENT_HANDLING.attributeCheck = null; if (objectHasOwnProperty(cfg, "ADD_TAGS")) { if (typeof cfg.ADD_TAGS === "function") { EXTRA_ELEMENT_HANDLING.tagCheck = cfg.ADD_TAGS; } else if (arrayIsArray(cfg.ADD_TAGS)) { if (ALLOWED_TAGS2 === DEFAULT_ALLOWED_TAGS) { ALLOWED_TAGS2 = clone(ALLOWED_TAGS2); } addToSet(ALLOWED_TAGS2, cfg.ADD_TAGS, transformCaseFunc); } } if (objectHasOwnProperty(cfg, "ADD_ATTR")) { if (typeof cfg.ADD_ATTR === "function") { EXTRA_ELEMENT_HANDLING.attributeCheck = cfg.ADD_ATTR; } else if (arrayIsArray(cfg.ADD_ATTR)) { if (ALLOWED_ATTR === DEFAULT_ALLOWED_ATTR) { ALLOWED_ATTR = clone(ALLOWED_ATTR); } addToSet(ALLOWED_ATTR, cfg.ADD_ATTR, transformCaseFunc); } } if (objectHasOwnProperty(cfg, "ADD_URI_SAFE_ATTR") && arrayIsArray(cfg.ADD_URI_SAFE_ATTR)) { addToSet(URI_SAFE_ATTRIBUTES, cfg.ADD_URI_SAFE_ATTR, transformCaseFunc); } if (objectHasOwnProperty(cfg, "FORBID_CONTENTS") && arrayIsArray(cfg.FORBID_CONTENTS)) { if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) { FORBID_CONTENTS = clone(FORBID_CONTENTS); } addToSet(FORBID_CONTENTS, cfg.FORBID_CONTENTS, transformCaseFunc); } if (objectHasOwnProperty(cfg, "ADD_FORBID_CONTENTS") && arrayIsArray(cfg.ADD_FORBID_CONTENTS)) { if (FORBID_CONTENTS === DEFAULT_FORBID_CONTENTS) { FORBID_CONTENTS = clone(FORBID_CONTENTS); } addToSet(FORBID_CONTENTS, cfg.ADD_FORBID_CONTENTS, transformCaseFunc); } if (KEEP_CONTENT) { ALLOWED_TAGS2["#text"] = true; } if (WHOLE_DOCUMENT) { addToSet(ALLOWED_TAGS2, ["html", "head", "body"]); } if (ALLOWED_TAGS2.table) { addToSet(ALLOWED_TAGS2, ["tbody"]); delete FORBID_TAGS.tbody; } if (cfg.TRUSTED_TYPES_POLICY) { if (typeof cfg.TRUSTED_TYPES_POLICY.createHTML !== "function") { throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a "createHTML" hook.'); } if (typeof cfg.TRUSTED_TYPES_POLICY.createScriptURL !== "function") { throw typeErrorCreate('TRUSTED_TYPES_POLICY configuration option must provide a "createScriptURL" hook.'); } trustedTypesPolicy = cfg.TRUSTED_TYPES_POLICY; emptyHTML = trustedTypesPolicy.createHTML(""); } else { if (trustedTypesPolicy === void 0) { trustedTypesPolicy = _createTrustedTypesPolicy(trustedTypes, currentScript); } if (trustedTypesPolicy !== null && typeof emptyHTML === "string") { emptyHTML = trustedTypesPolicy.createHTML(""); } } if (freeze) { freeze(cfg); } CONFIG = cfg; }; const ALL_SVG_TAGS = addToSet({}, [...svg$1, ...svgFilters, ...svgDisallowed]); const ALL_MATHML_TAGS = addToSet({}, [...mathMl$1, ...mathMlDisallowed]); const _checkValidNamespace = function _checkValidNamespace2(element) { let parent = getParentNode(element); if (!parent || !parent.tagName) { parent = { namespaceURI: NAMESPACE, tagName: "template" }; } const tagName = stringToLowerCase(element.tagName); const parentTagName = stringToLowerCase(parent.tagName); if (!ALLOWED_NAMESPACES[element.namespaceURI]) { return false; } if (element.namespaceURI === SVG_NAMESPACE) { if (parent.namespaceURI === HTML_NAMESPACE) { return tagName === "svg"; } if (parent.namespaceURI === MATHML_NAMESPACE) { return tagName === "svg" && (parentTagName === "annotation-xml" || MATHML_TEXT_INTEGRATION_POINTS[parentTagName]); } return Boolean(ALL_SVG_TAGS[tagName]); } if (element.namespaceURI === MATHML_NAMESPACE) { if (parent.namespaceURI === HTML_NAMESPACE) { return tagName === "math"; } if (parent.namespaceURI === SVG_NAMESPACE) { return tagName === "math" && HTML_INTEGRATION_POINTS[parentTagName]; } return Boolean(ALL_MATHML_TAGS[tagName]); } if (element.namespaceURI === HTML_NAMESPACE) { if (parent.namespaceURI === SVG_NAMESPACE && !HTML_INTEGRATION_POINTS[parentTagName]) { return false; } if (parent.namespaceURI === MATHML_NAMESPACE && !MATHML_TEXT_INTEGRATION_POINTS[parentTagName]) { return false; } return !ALL_MATHML_TAGS[tagName] && (COMMON_SVG_AND_HTML_ELEMENTS[tagName] || !ALL_SVG_TAGS[tagName]); } if (PARSER_MEDIA_TYPE === "application/xhtml+xml" && ALLOWED_NAMESPACES[element.namespaceURI]) { return true; } return false; }; const _forceRemove = function _forceRemove2(node) { arrayPush(DOMPurify.removed, { element: node }); try { getParentNode(node).removeChild(node); } catch (_) { remove(node); } }; const _removeAttribute = function _removeAttribute2(name, element) { try { arrayPush(DOMPurify.removed, { attribute: element.getAttributeNode(name), from: element }); } catch (_) { arrayPush(DOMPurify.removed, { attribute: null, from: element }); } element.removeAttribute(name); if (name === "is") { if (RETURN_DOM || RETURN_DOM_FRAGMENT) { try { _forceRemove(element); } catch (_) { } } else { try { element.setAttribute(name, ""); } catch (_) { } } } }; const _initDocument = function _initDocument2(dirty) { let doc = null; let leadingWhitespace = null; if (FORCE_BODY) { dirty = "<remove></remove>" + dirty; } else { const matches = stringMatch(dirty, /^[\r\n\t ]+/); leadingWhitespace = matches && matches[0]; } if (PARSER_MEDIA_TYPE === "application/xhtml+xml" && NAMESPACE === HTML_NAMESPACE) { dirty = '<html xmlns="http://www.w3.org/1999/xhtml"><head></head><body>' + dirty + "</body></html>"; } const dirtyPayload = trustedTypesPolicy ? trustedTypesPolicy.createHTML(dirty) : dirty; if (NAMESPACE === HTML_NAMESPACE) { try { doc = new DOMParser2().parseFromString(dirtyPayload, PARSER_MEDIA_TYPE); } catch (_) { } } if (!doc || !doc.documentElement) { doc = implementation.createDocument(NAMESPACE, "template", null); try { doc.documentElement.innerHTML = IS_EMPTY_INPUT ? emptyHTML : dirtyPayload; } catch (_) { } } const body = doc.body || doc.documentElement; if (dirty && leadingWhitespace) { body.insertBefore(document2.createTextNode(leadingWhitespace), body.childNodes[0] || null); } if (NAMESPACE === HTML_NAMESPACE) { return getElementsByTagName.call(doc, WHOLE_DOCUMENT ? "html" : "body")[0]; } return WHOLE_DOCUMENT ? doc.documentElement : body; }; const _createNodeIterator = function _createNodeIterator2(root) { return createNodeIterator.call( root.ownerDocument || root, root, NodeFilter2.SHOW_ELEMENT | NodeFilter2.SHOW_COMMENT | NodeFilter2.SHOW_TEXT | NodeFilter2.SHOW_PROCESSING_INSTRUCTION | NodeFilter2.SHOW_CDATA_SECTION, null ); }; const _scrubTemplateExpressions = function _scrubTemplateExpressions2(node) { node.normalize(); const walker = createNodeIterator.call( node.ownerDocument || node, node, NodeFilter2.SHOW_TEXT | NodeFilter2.SHOW_COMMENT | NodeFilter2.SHOW_CDATA_SECTION | NodeFilter2.SHOW_PROCESSING_INSTRUCTION, null ); let currentNode = walker.nextNode(); while (currentNode) { let data = currentNode.data; arrayForEach([MUSTACHE_EXPR$1, ERB_EXPR$1, TMPLIT_EXPR$1], (expr) => { data = stringReplace(data, expr, " "); }); currentNode.data = data; currentNode = walker.nextNode(); } }; const _isClobbered = function _isClobbered2(element) { return element instanceof HTMLFormElement && (typeof element.nodeName !== "string" || typeof element.textContent !== "string" || typeof element.removeChild !== "function" || !(element.attributes instanceof NamedNodeMap) || typeof element.removeAttribute !== "function" || typeof element.setAttribute !== "function" || typeof element.namespaceURI !== "string" || typeof element.insertBefore !== "function" || typeof element.hasChildNodes !== "function"); }; const _isNode = function _isNode2(value) { if (!getNodeType || typeof value !== "object" || value === null) { return false; } try { return typeof getNodeType(value) === "number"; } catch (_) { return false; } }; function _executeHooks(hooks2, currentNode, data) { arrayForEach(hooks2, (hook) => { hook.call(DOMPurify, currentNode, data, CONFIG); }); } const _sanitizeElements = function _sanitizeElements2(currentNode) { let content = null; _executeHooks(hooks.beforeSanitizeElements, currentNode, null); if (_isClobbered(currentNode)) { _forceRemove(currentNode); return true; } const tagName = transformCaseFunc(currentNode.nodeName); _executeHooks(hooks.uponSanitizeElement, currentNode, { tagName, allowedTags: ALLOWED_TAGS2 }); if (SAFE_FOR_XML && currentNode.hasChildNodes() && !_isNode(currentNode.firstElementChild) && regExpTest(/<[/\w!]/g, currentNode.innerHTML) && regExpTest(/<[/\w!]/g, currentNode.textContent)) { _forceRemove(currentNode); return true; } if (SAFE_FOR_XML && currentNode.namespaceURI === HTML_NAMESPACE && tagName === "style" && _isNode(currentNode.firstElementChild)) { _forceRemove(currentNode); return true; } if (currentNode.nodeType === NODE_TYPE.progressingInstruction) { _forceRemove(currentNode); return true; } if (SAFE_FOR_XML && currentNode.nodeType === NODE_TYPE.comment && regExpTest(/<[/\w]/g, currentNode.data)) { _forceRemove(currentNode); return true; } if (FORBID_TAGS[tagName] || !(EXTRA_ELEMENT_HANDLING.tagCheck instanceof Function && EXTRA_ELEMENT_HANDLING.tagCheck(tagName)) && !ALLOWED_TAGS2[tagName]) { if (!FORBID_TAGS[tagName] && _isBasicCustomElement(tagName)) { if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, tagName)) { return false; } if (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(tagName)) { return false; } } if (KEEP_CONTENT && !FORBID_CONTENTS[tagName]) { const parentNode = getParentNode(currentNode) || currentNode.parentNode; const childNodes = getChildNodes(currentNode) || currentNode.childNodes; if (childNodes && parentNode) { const childCount = childNodes.length; for (let i = childCount - 1; i >= 0; --i) { const childClone = cloneNode(childNodes[i], true); parentNode.insertBefore(childClone, getNextSibling(currentNode)); } } } _forceRemove(currentNode); return true; } if (currentNode instanceof Element2 && !_checkValidNamespace(currentNode)) { _forceRemove(currentNode); return true; } if ((tagName === "noscript" || tagName === "noembed" || tagName === "noframes") && regExpTest(/<\/no(script|embed|frames)/i, currentNode.innerHTML)) { _forceRemove(currentNode); return true; } if (SAFE_FOR_TEMPLATES && currentNode.nodeType === NODE_TYPE.text) { content = currentNode.textContent; arrayForEach([MUSTACHE_EXPR$1, ERB_EXPR$1, TMPLIT_EXPR$1], (expr) => { content = stringReplace(content, expr, " "); }); if (currentNode.textContent !== content) { arrayPush(DOMPurify.removed, { element: currentNode.cloneNode() }); currentNode.textContent = content; } } _executeHooks(hooks.afterSanitizeElements, currentNode, null); return false; }; const _isValidAttribute = function _isValidAttribute2(lcTag, lcName, value) { if (FORBID_ATTR[lcName]) { return false; } if (SANITIZE_DOM && (lcName === "id" || lcName === "name") && (value in document2 || value in formElement)) { return false; } const nameIsPermitted = ALLOWED_ATTR[lcName] || EXTRA_ELEMENT_HANDLING.attributeCheck instanceof Function && EXTRA_ELEMENT_HANDLING.attributeCheck(lcName, lcTag); if (ALLOW_DATA_ATTR && !FORBID_ATTR[lcName] && regExpTest(DATA_ATTR$1, lcName)) ; else if (ALLOW_ARIA_ATTR && regExpTest(ARIA_ATTR$1, lcName)) ; else if (!nameIsPermitted || FORBID_ATTR[lcName]) { if ( _isBasicCustomElement(lcTag) && (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, lcTag) || CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(lcTag)) && (CUSTOM_ELEMENT_HANDLING.attributeNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.attributeNameCheck, lcName) || CUSTOM_ELEMENT_HANDLING.attributeNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.attributeNameCheck(lcName, lcTag)) || lcName === "is" && CUSTOM_ELEMENT_HANDLING.allowCustomizedBuiltInElements && (CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof RegExp && regExpTest(CUSTOM_ELEMENT_HANDLING.tagNameCheck, value) || CUSTOM_ELEMENT_HANDLING.tagNameCheck instanceof Function && CUSTOM_ELEMENT_HANDLING.tagNameCheck(value)) ) ; else { return false; } } else if (URI_SAFE_ATTRIBUTES[lcName]) ; else if (regExpTest(IS_ALLOWED_URI$1, stringReplace(value, ATTR_WHITESPACE$1, ""))) ; else if ((lcName === "src" || lcName === "xlink:href" || lcName === "href") && lcTag !== "script" && stringIndexOf(value, "data:") === 0 && DATA_URI_TAGS[lcTag]) ; else if (ALLOW_UNKNOWN_PROTOCOLS && !regExpTest(IS_SCRIPT_OR_DATA$1, stringReplace(value, ATTR_WHITESPACE$1, ""))) ; else if (value) { return false; } else ; return true; }; const RESERVED_CUSTOM_ELEMENT_NAMES = addToSet({}, ["annotation-xml", "color-profile", "font-face", "font-face-format", "font-face-name", "font-face-src", "font-face-uri", "missing-glyph"]); const _isBasicCustomElement = function _isBasicCustomElement2(tagName) { return !RESERVED_CUSTOM_ELEMENT_NAMES[stringToLowerCase(tagName)] && regExpTest(CUSTOM_ELEMENT$1, tagName); }; const _sanitizeAttributes = function _sanitizeAttributes2(currentNode) { _executeHooks(hooks.beforeSanitizeAttributes, currentNode, null); const attributes = currentNode.attributes; if (!attributes || _isClobbered(currentNode)) { return; } const hookEvent = { attrName: "", attrValue: "", keepAttr: true, allowedAttributes: ALLOWED_ATTR, forceKeepAttr: void 0 }; let l = attributes.length; while (l--) { const attr = attributes[l]; const name = attr.name, namespaceURI = attr.namespaceURI, attrValue = attr.value; const lcName = transformCaseFunc(name); const initValue = attrValue; let value = name === "value" ? initValue : stringTrim(initValue); hookEvent.attrName = lcName; hookEvent.attrValue = value; hookEvent.keepAttr = true; hookEvent.forceKeepAttr = void 0; _executeHooks(hooks.uponSanitizeAttribute, currentNode, hookEvent); value = hookEvent.attrValue; if (SANITIZE_NAMED_PROPS && (lcName === "id" || lcName === "name") && stringIndexOf(value, SANITIZE_NAMED_PROPS_PREFIX) !== 0) { _removeAttribute(name, currentNode); value = SANITIZE_NAMED_PROPS_PREFIX + value; } if (SAFE_FOR_XML && regExpTest(/((--!?|])>)|<\/(style|script|title|xmp|textarea|noscript|iframe|noembed|noframes)/i, value)) { _removeAttribute(name, currentNode); continue; } if (lcName === "attributename" && stringMatch(value, "href")) { _removeAttribute(name, currentNode); continue; } if (hookEvent.forceKeepAttr) { continue; } if (!hookEvent.keepAttr) { _removeAttribute(name, currentNode); continue; } if (!ALLOW_SELF_CLOSE_IN_ATTR && regExpTest(/\/>/i, value)) { _removeAttribute(name, currentNode); continue; } if (SAFE_FOR_TEMPLATES) { arrayForEach([MUSTACHE_EXPR$1, ERB_EXPR$1, TMPLIT_EXPR$1], (expr) => { value = stringReplace(value, expr, " "); }); } const lcTag = transformCaseFunc(currentNode.nodeName); if (!_isValidAttribute(lcTag, lcName, value)) { _removeAttribute(name, currentNode); continue; } if (trustedTypesPolicy && typeof trustedTypes === "object" && typeof trustedTypes.getAttributeType === "function") { if (namespaceURI) ; else { switch (trustedTypes.getAttributeType(lcTag, lcName)) { case "TrustedHTML": { value = trustedTypesPolicy.createHTML(value); break; } case "TrustedScriptURL": { value = trustedTypesPolicy.createScriptURL(value); break; } } } } if (value !== initValue) { try { if (namespaceURI) { currentNode.setAttributeNS(namespaceURI, name, value); } else { currentNode.setAttribute(name, value); } if (_isClobbered(currentNode)) { _forceRemove(currentNode); } else { arrayPop(DOMPurify.removed); } } catch (_) { _removeAttribute(name, currentNode); } } } _executeHooks(hooks.afterSanitizeAttributes, currentNode, null); }; const _sanitizeShadowDOM2 = function _sanitizeShadowDOM(fragment) { let shadowNode = null; const shadowIterator = _createNodeIterator(fragment); _executeHooks(hooks.beforeSanitizeShadowDOM, fragment, null); while (shadowNode = shadowIterator.nextNode()) { _executeHooks(hooks.uponSanitizeShadowNode, shadowNode, null); _sanitizeElements(shadowNode); _sanitizeAttributes(shadowNode); if (shadowNode.content instanceof DocumentFragment) { _sanitizeShadowDOM2(shadowNode.content); } } _executeHooks(hooks.afterSanitizeShadowDOM, fragment, null); }; const _sanitizeAttachedShadowRoots2 = function _sanitizeAttachedShadowRoots(root) { if (root.nodeType === NODE_TYPE.element && root.shadowRoot instanceof DocumentFragment) { const sr = root.shadowRoot; _sanitizeAttachedShadowRoots2(sr); _sanitizeShadowDOM2(sr); } const childNodes = root.childNodes; if (!childNodes) { return; } const snapshot = []; arrayForEach(childNodes, (child) => { arrayPush(snapshot, child); }); for (const child of snapshot) { _sanitizeAttachedShadowRoots2(child); } }; DOMPurify.sanitize = function(dirty) { let cfg = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : {}; let body = null; let importedNode = null; let currentNode = null; let returnNode = null; IS_EMPTY_INPUT = !dirty; if (IS_EMPTY_INPUT) { dirty = "<!-->"; } if (typeof dirty !== "string" && !_isNode(dirty)) { dirty = stringifyValue(dirty); if (typeof dirty !== "string") { throw typeErrorCreate("dirty is not a string, aborting"); } } if (!DOMPurify.isSupported) { return dirty; } if (!SET_CONFIG) { _parseConfig(cfg); } DOMPurify.removed = []; if (typeof dirty === "string") { IN_PLACE = false; } if (IN_PLACE) { const nn = dirty.nodeName; if (typeof nn === "string") { const tagName = transformCaseFunc(nn); if (!ALLOWED_TAGS2[tagName] || FORBID_TAGS[tagName]) { throw typeErrorCreate("root node is forbidden and cannot be sanitized in-place"); } } _sanitizeAttachedShadowRoots2(dirty); } else if (_isNode(dirty)) { body = _initDocument("<!---->"); importedNode = body.ownerDocument.importNode(dirty, true); if (importedNode.nodeType === NODE_TYPE.element && importedNode.nodeName === "BODY") { body = importedNode; } else if (importedNode.nodeName === "HTML") { body = importedNode; } else { body.appendChild(importedNode); } _sanitizeAttachedShadowRoots2(importedNode); } else { if (!RETURN_DOM && !SAFE_FOR_TEMPLATES && !WHOLE_DOCUMENT && dirty.indexOf("<") === -1) { return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(dirty) : dirty; } body = _initDocument(dirty); if (!body) { return RETURN_DOM ? null : RETURN_TRUSTED_TYPE ? emptyHTML : ""; } } if (body && FORCE_BODY) { _forceRemove(body.firstChild); } const nodeIterator = _createNodeIterator(IN_PLACE ? dirty : body); while (currentNode = nodeIterator.nextNode()) { _sanitizeElements(currentNode); _sanitizeAttributes(currentNode); if (currentNode.content instanceof DocumentFragment) { _sanitizeShadowDOM2(currentNode.content); } } if (IN_PLACE) { if (SAFE_FOR_TEMPLATES) { _scrubTemplateExpressions(dirty); } return dirty; } if (RETURN_DOM) { if (SAFE_FOR_TEMPLATES) { _scrubTemplateExpressions(body); } if (RETURN_DOM_FRAGMENT) { returnNode = createDocumentFragment.call(body.ownerDocument); while (body.firstChild) { returnNode.appendChild(body.firstChild); } } else { returnNode = body; } if (ALLOWED_ATTR.shadowroot || ALLOWED_ATTR.shadowrootmode) { returnNode = importNode.call(originalDocument, returnNode, true); } return returnNode; } let serializedHTML = WHOLE_DOCUMENT ? body.outerHTML : body.innerHTML; if (WHOLE_DOCUMENT && ALLOWED_TAGS2["!doctype"] && body.ownerDocument && body.ownerDocument.doctype && body.ownerDocument.doctype.name && regExpTest(DOCTYPE_NAME, body.ownerDocument.doctype.name)) { serializedHTML = "<!DOCTYPE " + body.ownerDocument.doctype.name + ">\n" + serializedHTML; } if (SAFE_FOR_TEMPLATES) { arrayForEach([MUSTACHE_EXPR$1, ERB_EXPR$1, TMPLIT_EXPR$1], (expr) => { serializedHTML = stringReplace(serializedHTML, expr, " "); }); } return trustedTypesPolicy && RETURN_TRUSTED_TYPE ? trustedTypesPolicy.createHTML(serializedHTML) : serializedHTML; }; DOMPurify.setConfig = function() { let cfg = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : {}; _parseConfig(cfg); SET_CONFIG = true; }; DOMPurify.clearConfig = function() { CONFIG = null; SET_CONFIG = false; }; DOMPurify.isValidAttribute = function(tag, attr, value) { if (!CONFIG) { _parseConfig({}); } const lcTag = transformCaseFunc(tag); const lcName = transformCaseFunc(attr); return _isValidAttribute(lcTag, lcName, value); }; DOMPurify.addHook = function(entryPoint, hookFunction) { if (typeof hookFunction !== "function") { return; } arrayPush(hooks[entryPoint], hookFunction); }; DOMPurify.removeHook = function(entryPoint, hookFunction) { if (hookFunction !== void 0) { const index = arrayLastIndexOf(hooks[entryPoint], hookFunction); return index === -1 ? void 0 : arraySplice(hooks[entryPoint], index, 1)[0]; } return arrayPop(hooks[entryPoint]); }; DOMPurify.removeHooks = function(entryPoint) { hooks[entryPoint] = []; }; DOMPurify.removeAllHooks = function() { hooks = _createHooksMap(); }; return DOMPurify; } var purify = createDOMPurify(); const ALLOWED_TAGS = new Set([ "p", "a", "br", "strong", "em", "b", "i", "u", "s", "del", "code", "pre", "blockquote", "ul", "ol", "li", "h1", "h2", "h3", "h4", "h5", "h6", "hr", "table", "thead", "tbody", "tr", "th", "td", "div", "span", "sup", "sub", "img" ]); const ALLOWED_CLASSES = new Set([ "md-spoiler-text", "md", "gsrp-dead-link", "gsrp-inline-media-img", "gsrp-comment-media-wrapper", "gsrp-comment-image" ]); function sanitizeHtml(html2) { if (!html2) return ""; const cleanHtml = purify.sanitize(html2, { ALLOWED_TAGS: Array.from(ALLOWED_TAGS), ALLOWED_ATTR: [ "href", "src", "class", "alt", "title", "width", "height", "loading", "decoding" ], ALLOW_DATA_ATTR: false }); const container = document.createElement("div"); container.innerHTML = cleanHtml; const anchors = container.querySelectorAll("a"); for (const a of anchors) { let href = a.getAttribute("href"); if (href) { if (href.startsWith("/") && !href.startsWith("//")) { href = `https://www.reddit.com${href}`; a.setAttribute("href", href); } if (isDeadLink(href)) { a.classList.add("gsrp-dead-link"); a.setAttribute("title", L.deadLinkTooltip); } } } const imgs = container.querySelectorAll("img"); for (const img of imgs) { const src = img.getAttribute("src"); if (!src || !isAllowedRedditMediaHost(src)) { img.remove(); continue; } if (src.startsWith("http://")) { img.setAttribute("src", src.replace("http://", "https://")); } img.classList.add("gsrp-inline-media-img"); img.setAttribute("loading", "lazy"); img.setAttribute("decoding", "async"); } const allEls = container.querySelectorAll("*"); for (const el of allEls) { if (el.hasAttribute("class")) { const classes = el.getAttribute("class").split(/\s+/).filter((c) => ALLOWED_CLASSES.has(c)); if (classes.length > 0) { el.setAttribute("class", classes.join(" ")); } else { el.removeAttribute("class"); } } } return container.innerHTML; } function processRedditHtml(encodedHtml) { if (!encodedHtml) return ""; let html2 = decodeHTMLEntities(encodedHtml); html2 = html2.replace(/<!--\s*SC_OFF\s*-->/g, "").replace(/<!--\s*SC_ON\s*-->/g, ""); html2 = html2.replace(/<p>```([\s\S]*?)<\/p>/gi, (_match, content) => { const cleanContent = content.replace(/```/g, "").trim(); if (cleanContent.includes("|") || cleanContent.includes("mpv") || cleanContent.includes("youtube-dl") || cleanContent.includes("\n")) { return `<pre><code>${cleanContent}</code></pre>`; } return `<p>${cleanContent}</p>`; }); html2 = html2.replace( /<p>((?:(?!<\/p>)[\s\S])*?)<code>([\s\S]*?)<\/code>((?:(?!<\/p>)[\s\S])*?)<\/p>/gi, (match, beforeText, codeContent, afterText) => { const hasNewline = codeContent.includes("\n"); const isStandalone = !beforeText.trim() && !afterText.trim(); if (hasNewline || isStandalone && codeContent.trim().length > 25) { const partBefore = beforeText.trim() ? `<p>${beforeText.trim()}</p>` : ""; const partCode = `<pre><code>${codeContent.trim()}</code></pre>`; const partAfter = afterText.trim() ? `<p>${afterText.trim()}</p>` : ""; return `${partBefore}${partCode}${partAfter}`; } return match; } ); return sanitizeHtml(html2); } function renderPreviewTitle(data) { if (!data.title) return ""; return `<div class="gsrp-preview-post-title">${escapeHTML(data.title)}</div>`; } function dimAttrs(m) { if (!m.width || !m.height) return ""; return ` width="${m.width}" height="${m.height}"`; } function renderMediaImage(m) { const dimsChip = Config.showImageResolutionPreview && m.width && m.height ? `<span class="gsrp-gallery-dims">${m.width} × ${m.height}</span>` : ""; const styleAttr = m.width && m.height ? ` style="aspect-ratio: ${m.width} / ${m.height};"` : ""; return `<div class="gsrp-gallery-item" role="button" tabindex="0" data-gallery-idx="0" data-w="${m.width || ""}" data-h="${m.height || ""}"><img src="${escapeHTML(m.url)}"${dimAttrs(m)}${styleAttr} loading="lazy" decoding="async" alt="" onerror="this.parentElement.classList.add('gsrp-img-error')">${dimsChip}</div>`; } function renderMediaVideo(m) { const posterAttr = m.poster ? ` poster="${escapeHTML(m.poster)}"` : ""; const hlsAttr = m.hlsUrl ? ` data-gsrp-hls-url="${escapeHTML(m.hlsUrl)}"` : ""; const fallbackAttr = ` data-gsrp-fallback-url="${escapeHTML(m.url)}"`; const styleAttr = m.width && m.height ? ` style="aspect-ratio: ${m.width} / ${m.height};"` : ""; const autoplayDataAttr = Config.videoAutoplay ? ' data-gsrp-autoplay="1"' : ""; if (m.isGifv) { return `<video${posterAttr}${dimAttrs(m)}${hlsAttr}${fallbackAttr}${styleAttr} data-gsrp-is-gifv="1" preload="metadata" playsinline loop muted${autoplayDataAttr}></video>`; } const mutedAttr = Config.videoAutoplay && !Config.videoAutoplaySound ? " muted" : ""; return `<video${posterAttr}${dimAttrs(m)}${hlsAttr}${fallbackAttr}${styleAttr} controls preload="metadata" playsinline${mutedAttr}${autoplayDataAttr}></video>`; } function renderMediaGallery(m) { const total = m.images.length; const isCarousel = Config.galleryDisplayMode === "carousel" && total > 1; const items = m.images.map((img, idx) => { const counter = total > 1 ? `<span class="gsrp-gallery-counter">${idx + 1}/${total}</span>` : ""; const dims = Config.showImageResolutionPreview && img.width && img.height ? `<span class="gsrp-gallery-dims">${img.width} × ${img.height}</span>` : ""; const caption = img.caption ? `<div class="gsrp-preview-media-caption">${escapeHTML(img.caption)}</div>` : ""; const activeClass = isCarousel && idx === 0 ? " gsrp-carousel-active" : ""; const loadAttr = isCarousel && idx > 0 ? ' loading="lazy"' : ""; const styleAttr = img.width && img.height ? ` style="aspect-ratio: ${img.width} / ${img.height};"` : ""; return `<div class="gsrp-gallery-item gsrp-is-loading${activeClass}" role="button" tabindex="0" data-gallery-idx="${idx}" data-w="${img.width || ""}" data-h="${img.height || ""}"><img src="${escapeHTML(img.url)}"${loadAttr}${styleAttr} decoding="async" alt="" onerror="this.parentElement.classList.add('gsrp-img-error')">${counter}${dims}${caption}</div>`; }).join(""); if (isCarousel) { const prevBtn = `<button type="button" class="gsrp-carousel-prev" aria-label="‹">‹</button>`; const nextBtn = `<button type="button" class="gsrp-carousel-next" aria-label="›">›</button>`; return `<div class="gsrp-preview-media-gallery gsrp-carousel">${items}${prevBtn}${nextBtn}</div>`; } return `<div class="gsrp-preview-media-gallery">${items}</div>`; } function renderMediaEmbed(m) { const titleAttr = m.providerName ? ` title="${escapeHTML(m.providerName)}"` : ""; return `<iframe src="${escapeHTML(m.url)}"${titleAttr} loading="lazy" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>`; } const REDGIFS_SPINNER_SVG = `<svg viewBox="0 0 50 50" fill="none" stroke="rgba(255,255,255,.8)" stroke-width="4" style="width: 40px; height: 40px; animation: gsrp-spin 1s linear infinite;"><circle cx="25" cy="25" r="20" stroke-dasharray="80 40"/></svg>`; function renderMediaRedgifs(m) { const id = m.redgifsId; return ` <div class="gsrp-redgifs-container" data-gsrp-redgifs-id="${escapeHTML(id)}"> <div class="gsrp-redgifs-spinner">${REDGIFS_SPINNER_SVG}</div> <video controls loop playsinline style="display: none;"></video> </div> `; } function renderMedia(media) { let inner; switch (media.type) { case "image": inner = renderMediaImage(media); break; case "video": inner = renderMediaVideo(media); break; case "gallery": inner = renderMediaGallery(media); break; case "embed": inner = renderMediaEmbed(media); break; case "redgifs_video": inner = renderMediaRedgifs(media); break; default: { return ""; } } const loadingClass = media.type === "image" || media.type === "video" ? " gsrp-is-loading" : ""; return `<div class="gsrp-preview-media${loadingClass}">${inner}</div>`; } function renderPostBody(selftext = "", selftextHtml = "", isRedacted = false, mediaMetadata = null) { if (isRedacted) { return `<p class="gsrp-post-deleted-placeholder">${escapeHTML(L.postRedacted)}</p>`; } if (selftext === "[deleted]") { return `<p class="gsrp-post-deleted-placeholder">${escapeHTML(L.postDeletedByAuthor)}</p>`; } if (selftext === "[removed]") { return `<p class="gsrp-post-removed-placeholder">${escapeHTML(L.postRemovedByMod)}</p>`; } if (selftextHtml && selftextHtml.trim()) { let body = processRedditHtml(selftextHtml); if (Config.embedMedia) body = transformInlineMedia(body, mediaMetadata); return body; } if (selftext && selftext.trim()) { return `<p>${escapeHTML(selftext)}</p>`; } return ""; } function renderPreviewContent(data) { let html2 = ""; if (Config.embedMedia && data.media) { html2 += renderMedia(data.media); } if (!data.isSelf && data.url) { let fullUrl = data.url; if (fullUrl.startsWith("/r/") || fullUrl.startsWith("/u/")) { fullUrl = `https://www.reddit.com${fullUrl}`; } else if (fullUrl.startsWith("r/") || fullUrl.startsWith("u/")) { fullUrl = `https://www.reddit.com/${fullUrl}`; } const safeUrl = escapeHTML(fullUrl); const isDead = isDeadLink(fullUrl); const deadCls = isDead ? ' class="gsrp-dead-link"' : ""; const deadTitle = isDead ? ` title="${escapeHTML(L.deadLinkTooltip)}"` : ""; const isAlreadyEmbedded = data.media && data.media.type === "embed" && data.media.providerName === "YouTube"; const ytProcessedAttr = isAlreadyEmbedded ? ' data-gsrp-yt-processed="1"' : ""; html2 += `<p style="margin-bottom:8px;"><a href="${safeUrl}"${deadCls}${deadTitle}${ytProcessedAttr} target="_blank" rel="noopener noreferrer" style="word-break:break-all;">🔗 ${safeUrl}</a></p>`; } html2 += renderPostBody(data.selftext, data.selftextHtml, data.isRedacted, data.mediaMetadata); if (data.crosspostParent) { const parentBody = renderPostBody( data.crosspostParent.selftext, data.crosspostParent.selftextHtml, data.crosspostParent.isRedacted, data.crosspostParent.mediaMetadata ); html2 += ` <div class="gsrp-crosspost-card" style="margin-top:${html2 ? "12px" : "0"};"> <div class="gsrp-crosspost-card-meta"> <a href="https://www.reddit.com/${data.crosspostParent.subreddit}" target="_blank" rel="noopener noreferrer">${data.crosspostParent.subreddit}</a> <span class="gsrp-crosspost-author${data.crosspostParent.author === "[deleted]" ? " gsrp-author-deleted" : ""}"> · ${escapeHTML(data.crosspostParent.author === "[deleted]" ? L.authorDeleted : data.crosspostParent.author)}</span> </div> <div class="gsrp-crosspost-card-title"> <a href="https://www.reddit.com${data.crosspostParent.permalink}" target="_blank" rel="noopener noreferrer">${escapeHTML(data.crosspostParent.title)}</a> </div> ${parentBody ? `<div class="gsrp-crosspost-card-body">${parentBody}</div>` : ""} <div class="gsrp-crosspost-card-stats"> <span>⬆️ ${data.crosspostParent.score}</span> <span>💬 ${data.crosspostParent.comments}</span> </div> </div> `; } return html2; } const MAX_RENDER_DEPTH = 50; function renderBody(html2, mediaMetadata) { let out = processRedditHtml(html2); if (Config.embedMedia) out = transformInlineMedia(out, mediaMetadata ?? null); return out; } function renderMorePlaceholder(more) { const countText = more.count === null || more.count === 0 ? L.moreRepliesUnknown : L.moreRepliesCount.replace("{n}", String(more.count)); const linkHtml = more.isNested && more.permalink ? `<button class="gsrp-more-link gsrp-more-load" data-fetch-url="${escapeHTML(more.permalink)}" data-depth="${more.depth}">${escapeHTML(countText)} ↴</button>` : more.permalink ? `<a class="gsrp-more-link" href="https://www.reddit.com${escapeHTML(more.permalink)}" target="_blank" rel="noopener noreferrer">${escapeHTML(countText)} ↗</a>` : `<span class="gsrp-more-text">${escapeHTML(countText)}</span>`; return `<div class="gsrp-comment-more" data-depth="${more.depth}">${linkHtml}</div>`; } function renderRemovedPlaceholder(removalKind) { if (removalKind === "removed") return `<p>${escapeHTML(L.commentRemovedByMod)}</p>`; if (removalKind === "deleted") return `<p>${escapeHTML(L.commentDeletedByAuthor)}</p>`; if (removalKind === "redacted") return `<p><em>${escapeHTML(L.commentRedacted)}</em></p>`; return ""; } function getAuthorGemStyle(author) { let hash = 0; for (let i = 0; i < author.length; i++) { hash = (hash << 5) - hash + author.charCodeAt(i); hash |= 0; } hash = Math.abs(hash); const hue = hash * 222.49 % 360; const sat = [65, 75, 85][hash % 3]; const lightLight = [34, 40, 46][(hash >> 2) % 3]; const darkLight = [64, 70, 76][(hash >> 2) % 3]; return ` style="--gsrp-author-color-light: hsl(${hue}, ${sat}%, ${lightLight}%); --gsrp-author-color-dark: hsl(${hue}, ${sat}%, ${darkLight}%);"`; } function renderAuthorTag(c) { const displayName = c.isAuthorDeleted ? L.authorDeleted : c.author; const authorEsc = escapeHTML(displayName); let authorClass = "gsrp-comment-author"; let gemStyle = ""; if (Config.authorColors && !c.isAuthorDeleted && !c.isOP && c.distinguished !== "moderator" && c.distinguished !== "admin" && c.author) { const count = window.gsrpAuthorFreqCache ? window.gsrpAuthorFreqCache.get(c.author) || 1 : 1; if (count >= 2) { authorClass += " gsrp-author-promoted"; gemStyle = getAuthorGemStyle(c.author); } } const authorMarkup = c.isAuthorDeleted ? `<span class="${authorClass} gsrp-author-deleted">${authorEsc}</span>` : `<a class="${authorClass}" href="https://www.reddit.com/user/${c.author}" target="_blank" rel="noopener noreferrer"${gemStyle}>${authorEsc}</a>`; const opTag = c.isOP ? `<span class="gsrp-comment-op-tag" title="OP">OP</span>` : ""; let distTag = ""; if (c.distinguished === "moderator") { distTag = `<span class="gsrp-comment-mod-tag" title="${escapeHTML(L.moderator)}">MOD</span>`; } else if (c.distinguished === "admin") { distTag = `<span class="gsrp-comment-admin-tag" title="${escapeHTML(L.admin)}">ADMIN</span>`; } const flairTag = c.flair ? `<span class="gsrp-comment-flair">${escapeHTML(c.flair)}</span>` : ""; return `${authorMarkup}${opTag}${distTag}${flairTag}`; } function renderDateLink(c) { const title = c.edited ? `${L.editedTitle}${c.edited}` : ""; const cls = c.edited ? "gsrp-comment-date gsrp-is-edited" : "gsrp-comment-date"; return `<a href="https://www.reddit.com${c.permalink}" target="_blank" rel="noopener noreferrer" class="${cls}" title="${escapeHTML(title)}">${c.date}</a>`; } function countSubtree(c) { let n = 1; if (c.replies && c.replies.length) { for (const r of c.replies) { if (r.type === "more") continue; n += countSubtree(r); } } return n; } function renderComment(c, depth) { if (depth > MAX_RENDER_DEPTH) { const href = c.permalink ? `https://www.reddit.com${escapeHTML(c.permalink)}` : "https://www.reddit.com"; const label = L.maxDepthReached || `Continue reading on Reddit (depth ${depth}+)`; return `<div class="gsrp-comment-more" data-depth="${depth}"><a class="gsrp-more-link" href="${href}" target="_blank" rel="noopener noreferrer">${escapeHTML(label)} ↗</a></div>`; } const wrapperClass = depth === 0 ? "gsrp-comment-item" : "gsrp-reply-item"; const stateClasses = [ c.isStickied && depth === 0 ? "gsrp-comment-stickied" : "", c.isDeleted || c.isRedacted ? "gsrp-comment-deleted" : "", c.isAutoCollapsed ? "gsrp-comment-collapsed" : "" ].filter(Boolean).join(" "); const fullClass = stateClasses ? `${wrapperClass} ${stateClasses}` : wrapperClass; const stickiedTag = c.isStickied && depth === 0 ? `<span class="gsrp-comment-stickied-tag" title="${escapeHTML(L.stickiedComment)}">📌 ${escapeHTML(L.stickiedComment)}</span>` : ""; const bodyHtml = c.isDeleted ? renderRemovedPlaceholder(c.removalKind) : renderBody(c.bodyHtml, c.mediaMetadata); const repliesHtml = c.replies && c.replies.length > 0 ? `<div class="gsrp-comment-replies">${c.replies.map((r) => r.type === "more" ? renderMorePlaceholder(r) : renderComment(r, depth + 1)).join("")}</div>` : ""; const directCount = c.replies ? c.replies.length : 0; const subtreeCount = countSubtree(c); const n1 = 1 + directCount; const hintText = n1 === subtreeCount ? `+ ${n1}` : `+ ${n1} / ${subtreeCount}`; const collapsedHint = `<span class="gsrp-collapsed-hint" aria-hidden="true" title="${escapeHTML(L.collapsedHintTitle)}">[${hintText}]</span>`; return ` <div class="${fullClass}" data-comment-id="${escapeHTML(c.id)}" data-depth="${depth}" data-color-tier="${depth % 10}" data-author="${escapeHTML(c.author || "")}"${c.isOP ? ' data-is-op="true"' : ""} data-score="${c.score}"${c.distinguished ? ` data-distinguished="${escapeHTML(c.distinguished)}"` : ""}> <div class="gsrp-thread-line" role="button" tabindex="0" aria-label="${escapeHTML(L.toggleThread)}"></div> <div class="gsrp-comment-content"> <div class="gsrp-comment-header"> <div class="gsrp-comment-meta"> ${stickiedTag} ${renderAuthorTag(c)} ${renderDateLink(c)} <span class="gsrp-comment-score">${formatScore(c.score)} ${L.scorePoints}</span> ${collapsedHint} </div> </div> <div class="gsrp-comment-body">${bodyHtml}</div> ${repliesHtml} </div> </div> `; } function renderCommentsList(topComments, startDepth = 0, wrapWithBody = true) { if (!topComments || topComments.length === 0) { return wrapWithBody ? `<div class="gsrp-preview-body" style="text-align:center; padding:20px;">${L.noContent}</div>` : ""; } if (Config.authorColors) { const authorFreqCache = new Map(); window.gsrpAuthorFreqCache = authorFreqCache; const walk = (list) => { if (!list || !list.length) return; for (const item of list) { if (item.type === "more") continue; if (item.author && !item.isAuthorDeleted && !item.isOP && item.distinguished !== "moderator" && item.distinguished !== "admin") { authorFreqCache.set(item.author, (authorFreqCache.get(item.author) || 0) + 1); } if (item.replies) walk(item.replies); } }; walk(topComments); } const html2 = topComments.map((c) => c.type === "more" ? renderMorePlaceholder(c) : renderComment(c, startDepth)).join(""); return wrapWithBody ? `<div class="gsrp-preview-body">${html2}</div>` : html2; } function renderCommentSkeletonHTML() { return ` <div class="gsrp-skeleton-comments"> <div class="gsrp-skeleton-comment"> <div class="gsrp-skeleton-header"> <div class="gsrp-skeleton-avatar"></div> <div class="gsrp-skeleton-meta" style="width: 35%;"></div> </div> <div class="gsrp-skeleton-text-line" style="width: 90%;"></div> <div class="gsrp-skeleton-text-line" style="width: 75%;"></div> </div> <div class="gsrp-skeleton-comment" style="padding-left: 16px; border-left: 1px solid rgba(128,128,128,0.15); margin-left: 4px;"> <div class="gsrp-skeleton-header"> <div class="gsrp-skeleton-avatar" style="width: 14px; height: 14px;"></div> <div class="gsrp-skeleton-meta" style="width: 45%;"></div> </div> <div class="gsrp-skeleton-text-line" style="width: 85%;"></div> </div> <div class="gsrp-skeleton-comment" style="padding-left: 32px; border-left: 1px solid rgba(128,128,128,0.15); margin-left: 4px;"> <div class="gsrp-skeleton-header"> <div class="gsrp-skeleton-avatar" style="width: 12px; height: 12px;"></div> <div class="gsrp-skeleton-meta" style="width: 25%;"></div> </div> <div class="gsrp-skeleton-text-line" style="width: 70%;"></div> </div> </div> `; } function isTooltipOpen(video) { const tooltip = video.closest(".gsrp-preview-tooltip"); if (!tooltip) return false; if (tooltip.classList.contains("gsrp-side-panel")) { return tooltip.classList.contains("gsrp-active"); } const trigger = tooltip.closest(".gsrp-preview-trigger"); if (trigger) { if (trigger.classList.contains("gsrp-hover-sticky") || trigger.classList.contains("gsrp-pinned") || trigger.classList.contains("gsrp-active")) { return true; } } if (tooltip.classList.contains("gsrp-active")) { return true; } return false; } function playAutoplayVideo(video) { const item = video.closest(".gsrp-gallery-item"); const isInactiveCarouselItem = item && item.closest(".gsrp-carousel") && !item.classList.contains("gsrp-carousel-active"); if (!isTooltipOpen(video) || isInactiveCarouselItem) { video.autoplay = false; video.dataset.gsrpAutoplay = "1"; video.muted = true; video.pause(); return; } if (!video.paused) return; video.dataset.gsrpAutoplay = "1"; const hasAudioAttr = video.dataset.gsrpHasAudio; const isUpgradedGifv = video.dataset.gsrpUpgradedGifv === "1" || video.hasAttribute("data-gsrp-is-gifv"); const isSilent = hasAudioAttr === "false" || isUpgradedGifv || video.hasAttribute("loop") && video.hasAttribute("muted") && !video.hasAttribute("controls"); const wantSound = Config.videoAutoplay && Config.videoAutoplaySound && !isSilent; if (wantSound) { video.muted = false; video.play().catch(() => { video.muted = true; video.play().catch(() => { }); }); } else { video.muted = true; video.play().catch(() => { }); } } function detectAudio(video) { if (video.mozHasAudio !== void 0) { return video.mozHasAudio; } if (video.audioTracks && video.audioTracks.length > 0) { return true; } if (video.webkitAudioDecodedByteCount !== void 0) { return video.webkitAudioDecodedByteCount > 0; } return false; } function checkAndUpgradeGifv(video) { if (video.dataset.gsrpIsGifv !== "1") return; const hasAudio = detectAudio(video); if (!hasAudio) return; video.controls = true; video.removeAttribute("data-gsrp-is-gifv"); video.dataset.gsrpHasAudio = "true"; video.dataset.gsrpUpgradedGifv = "1"; if (!isTooltipOpen(video)) { video.autoplay = false; video.removeAttribute("autoplay"); video.muted = true; video.pause(); return; } const shouldAutoplay = Config.videoAutoplay; if (shouldAutoplay) { video.autoplay = true; video.setAttribute("autoplay", ""); video.muted = true; playAutoplayVideo(video); } else { video.autoplay = false; video.removeAttribute("autoplay"); video.muted = false; video.pause(); } } function bindStickyVolume(video) { if (!video || video.dataset.gsrpVolumeBound === "1") return; video.dataset.gsrpVolumeBound = "1"; video.addEventListener("volumechange", () => { if (!video.muted && video.volume !== void 0) { const volPercent = Math.round(video.volume * 100); if (Math.abs(Config.defaultVideoVolume - volPercent) >= 2) { Config.defaultVideoVolume = volPercent; saveConfigDebounced(); const slider = document.getElementById( "gsrp-default-volume" ); const display = document.getElementById("gsrp-default-volume-display"); if (slider) slider.value = String(volPercent); if (display) display.textContent = `${volPercent}%`; } } }); } function attachHlsToVideos(badge) { if (!badge) return; const videos = badge.querySelectorAll("video[data-gsrp-fallback-url]"); videos.forEach((video) => { if (video.dataset.gsrpHlsAttached === "1") return; video.dataset.gsrpHlsAttached = "1"; const runCheck = () => { checkAndUpgradeGifv(video); }; video.addEventListener( "loadedmetadata", () => { const v = Math.max(0, Math.min(100, Config.defaultVideoVolume)) / 100; const wasMuted = video.muted; video.volume = v; if (wasMuted) { video.muted = true; } runCheck(); if (video.hasAttribute("autoplay") || video.dataset.gsrpAutoplay === "1") { playAutoplayVideo(video); } }, { once: true } ); bindStickyVolume(video); if (video.dataset.gsrpIsGifv === "1") { video.addEventListener("play", runCheck, { once: true }); video.addEventListener("timeupdate", runCheck, { once: true }); } const hlsUrl = video.dataset.gsrpHlsUrl || ""; const fallbackUrl = video.dataset.gsrpFallbackUrl || ""; if (hlsUrl && video.canPlayType("application/vnd.apple.mpegurl")) { video.src = hlsUrl; return; } const HlsCtor = typeof window !== "undefined" ? window.Hls : void 0; if (hlsUrl && HlsCtor && HlsCtor.isSupported && HlsCtor.isSupported()) { const hls = new HlsCtor(); hls.loadSource(hlsUrl); hls.attachMedia(video); hls.on(HlsCtor.Events.ERROR, (_event, data) => { if (data.fatal) { console.warn( `[GSRP] HLS fatal error (${data.type}), falling back to mp4:`, data.details ); hls.destroy(); delete video.gsrpHlsInstance; if (fallbackUrl) { video.src = fallbackUrl; if (video.dataset.gsrpAutoplay === "1") { video.play().catch(() => { }); } } } }); video.gsrpHlsInstance = hls; return; } if (fallbackUrl) video.src = fallbackUrl; }); } let _redgifsTokenCache = null; function getRedgifsToken() { if (_redgifsTokenCache) return Promise.resolve(_redgifsTokenCache); return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: "https://api.redgifs.com/v2/auth/temporary", onload(resp) { try { const data = JSON.parse(resp.responseText); if (data && data.token) { _redgifsTokenCache = data.token; setTimeout( () => { _redgifsTokenCache = null; }, 23 * 3600 * 1e3 ); resolve(_redgifsTokenCache); } else { reject(new Error("No token returned")); } } catch (e) { reject(e); } }, onerror: reject }); }); } function fetchRedgifsMetadata(id, token) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: `https://api.redgifs.com/v2/gifs/${id}`, headers: { Authorization: `Bearer ${token}` }, onload(resp) { try { const data = JSON.parse(resp.responseText); if (data && data.gif) { resolve(data.gif); } else { reject(new Error("No gif metadata returned")); } } catch (e) { reject(e); } }, onerror: reject }); }); } function loadResourceAsBlob(url) { return new Promise((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url, responseType: "blob", onload(resp) { if (resp.status >= 200 && resp.status < 300 && resp.response) { resolve(URL.createObjectURL(resp.response)); } else { reject(new Error(`Bad status ${resp.status}`)); } }, onerror: reject }); }); } function attachRedgifsToVideos(badge) { if (!badge) return; const containers = badge.querySelectorAll( ".gsrp-redgifs-container[data-gsrp-redgifs-id]" ); containers.forEach(async (container) => { if (container.dataset.gsrpRedgifsAttached === "1") return; container.dataset.gsrpRedgifsAttached = "1"; const id = container.dataset.gsrpRedgifsId; if (!id) return; const spinner = container.querySelector(".gsrp-redgifs-spinner"); const video = container.querySelector("video"); const sid = Math.random(); video.gsrpSessionId = sid; try { const token = await getRedgifsToken(); if (video.gsrpSessionId !== sid) return; const gif = await fetchRedgifsMetadata(id, token); if (video.gsrpSessionId !== sid) return; const urls = gif.urls || {}; const videoSrc = urls.hd || urls.sd || urls.mobile || ""; const posterSrc = gif.poster || urls.poster || urls.thumbnail || ""; if (posterSrc) { try { const posterBlobUrl = await loadResourceAsBlob(posterSrc); if (video.gsrpSessionId !== sid) { URL.revokeObjectURL(posterBlobUrl); return; } video.setAttribute("poster", posterBlobUrl); video.style.backgroundImage = `url(${posterBlobUrl})`; video.gsrpPosterBlobUrl = posterBlobUrl; } catch { if (video.gsrpSessionId === sid) { video.setAttribute("poster", posterSrc); video.style.backgroundImage = `url(${posterSrc})`; } } if (video.gsrpSessionId === sid) { video.style.backgroundSize = "cover"; video.style.backgroundPosition = "center"; } } if (videoSrc) { try { const blobUrl = await loadResourceAsBlob(videoSrc); if (video.gsrpSessionId !== sid) { URL.revokeObjectURL(blobUrl); return; } video.src = blobUrl; video.gsrpBlobUrl = blobUrl; } catch { if (video.gsrpSessionId === sid) { video.src = videoSrc; } } if (video.gsrpSessionId !== sid) return; if (spinner) spinner.style.display = "none"; video.style.display = "block"; const hasAudio = gif.hasAudio === true; video.dataset.gsrpHasAudio = hasAudio ? "true" : "false"; const shouldAutoplay = !hasAudio || Config.videoAutoplay; const v = Math.max(0, Math.min(100, Config.defaultVideoVolume)) / 100; const wasMuted = video.muted; video.volume = v; if (wasMuted) { video.muted = true; } if (shouldAutoplay && isTooltipOpen(video)) { video.autoplay = true; video.setAttribute("autoplay", ""); if (hasAudio && Config.videoAutoplaySound) { video.muted = false; } else { video.muted = true; } playAutoplayVideo(video); } else { video.autoplay = false; video.removeAttribute("autoplay"); video.muted = true; video.pause(); } bindStickyVolume(video); } else { throw new Error("No valid video source found"); } } catch (err) { console.warn("[GSRP] Failed to attach RedGIFs video:", err); if (spinner) { spinner.innerHTML = `<span style="color: rgba(255,255,255,.7); font-size: 12px;">${escapeHTML("RedGIFs unavailable")}</span>`; } } }); } function snapshotIframeSrcs(scope) { if (!scope) return; scope.querySelectorAll("iframe").forEach((iframe) => { const parent = iframe.parentElement; if (!parent) return; delete iframe.dataset.gsrpYtVolumeBound; if (!parent.dataset.gsrpIframeHtml) { parent.dataset.gsrpIframeHtml = iframe.outerHTML; parent.classList.add("gsrp-iframe-host"); } }); } function clearMediaIn(scope) { if (!scope) return; scope.querySelectorAll(".gsrp-iframe-host").forEach((rawHost) => { const host = rawHost; if (host.gsrpRestoreTimer) { clearTimeout(host.gsrpRestoreTimer); delete host.gsrpRestoreTimer; } }); const fsEl = document.fullscreenElement || document.webkitFullscreenElement; if (fsEl && scope.contains(fsEl)) return; scope.querySelectorAll("video").forEach((v) => { delete v.gsrpSessionId; if (!v.paused) v.pause(); if (v.gsrpPendingRescueReq) { try { v.gsrpPendingRescueReq.abort(); } catch { } delete v.gsrpPendingRescueReq; } if (v.gsrpHlsInstance) { try { v.gsrpHlsInstance.destroy(); } catch (e) { console.warn("[GSRP] Error destroying Hls instance:", e); } delete v.gsrpHlsInstance; } if (v.gsrpBlobUrl) { try { URL.revokeObjectURL(v.gsrpBlobUrl); } catch (e) { console.warn("[GSRP] Error revoking blob URL:", e); } delete v.gsrpBlobUrl; v.src = ""; try { const isJsdom = typeof navigator !== "undefined" && navigator.userAgent && navigator.userAgent.includes("jsdom"); if (!isJsdom && typeof v.load === "function") { v.load(); } } catch { } } if (v.gsrpPosterBlobUrl) { try { URL.revokeObjectURL(v.gsrpPosterBlobUrl); } catch (e) { console.warn("[GSRP] Error revoking poster blob URL:", e); } delete v.gsrpPosterBlobUrl; v.removeAttribute("poster"); v.style.backgroundImage = ""; } }); scope.querySelectorAll("iframe").forEach((iframe) => { const timers = ytVolumeTimersMap.get(iframe); if (timers) { Object.values(timers).forEach((t) => clearTimeout(t)); ytVolumeTimersMap.delete(iframe); } iframe.remove(); }); cleanupEmbedFor(scope, "youtube", L.embedYoutube || "Embed YouTube Video"); cleanupEmbedFor(scope, "x", L.embedX || "Embed X Post"); cleanupEmbedFor(scope, "bsky", L.embedBluesky || "Embed Bluesky Post"); cleanupEmbedFor(scope, "mastodon", L.embedMastodon || "Embed Mastodon Post"); } function cleanupEmbedFor(scope, platform, label) { scope.querySelectorAll(`.gsrp-${platform}-embed-container`).forEach((container) => { container.remove(); }); scope.querySelectorAll(`.gsrp-${platform}-embed-btn`).forEach((btn) => { btn.classList.remove("gsrp-active"); btn.setAttribute("title", label); btn.setAttribute("aria-label", label); }); } function restoreMediaIn(scope) { if (!scope) return; scope.querySelectorAll(".gsrp-iframe-host").forEach((rawHost) => { const host = rawHost; if (host.querySelector("iframe")) return; const html2 = host.dataset.gsrpIframeHtml; if (!html2) return; if (host.gsrpRestoreTimer) { clearTimeout(host.gsrpRestoreTimer); } host.gsrpRestoreTimer = setTimeout(() => { delete host.gsrpRestoreTimer; if (host.querySelector("iframe")) return; host.insertAdjacentHTML("beforeend", html2); attachYouTubeVolume(host); }, 250); }); scope.querySelectorAll("video").forEach((v) => { if (v.paused && (v.hasAttribute("autoplay") || v.dataset.gsrpAutoplay === "1")) { playAutoplayVideo(v); } }); } function attachYouTubeVolume(scope) { if (!scope) return; const iframes = scope.querySelectorAll( 'iframe[src*="youtube-nocookie.com/embed"], iframe[src*="youtube.com/embed"]' ); iframes.forEach((iframe) => { if (iframe.dataset.gsrpYtVolumeBound === "1") return; iframe.dataset.gsrpYtVolumeBound = "1"; iframe.addEventListener( "load", () => { const volume = Math.max(0, Math.min(100, Config.defaultVideoVolume)); let targetOrigin = "https://www.youtube-nocookie.com"; try { const parsed = new URL(iframe.src); targetOrigin = parsed.origin; } catch { } const send = () => { try { if (!iframe.contentWindow) return; iframe.contentWindow.postMessage( JSON.stringify({ event: "command", func: "setVolume", args: [volume] }), targetOrigin ); } catch { } }; send(); const t1 = setTimeout(send, 500); const t2 = setTimeout(send, 1500); ytVolumeTimersMap.set(iframe, { t1, t2 }); }, { once: true } ); }); } function isSidePanelModeActive$1() { return Config.previewMode === "side-panel" && window.innerWidth >= 1e3; } function applySidePanelPosition(state2) { if (!state2.panelEl) return; const centerCol = document.getElementById("center_col") || document.getElementById("rso") || document.querySelector(".gsc-wrapper") || document.querySelector(".gsc-resultsbox-visible"); const rect = centerCol ? centerCol.getBoundingClientRect() : null; const targetEl = state2.wrapperEl || state2.panelEl; if (state2.wrapperEl) { state2.panelEl.style.removeProperty("--gsrp-side-panel-left"); state2.panelEl.style.removeProperty("--gsrp-side-panel-width"); state2.panelEl.style.removeProperty("--gsrp-side-panel-top"); } if (rect && rect.width > 0 && rect.right > 0) { const leftPos = rect.right + 16; const maxAvailableWidth = window.innerWidth - leftPos - 24; const width = Math.max(380, Math.min(680, maxAvailableWidth)); targetEl.style.setProperty("--gsrp-side-panel-left", `${leftPos}px`); targetEl.style.setProperty("--gsrp-side-panel-width", `${width}px`); } else { targetEl.style.setProperty("--gsrp-side-panel-left", "calc(100vw - 544px)"); targetEl.style.setProperty("--gsrp-side-panel-width", "520px"); } const topOffset = 12; targetEl.style.setProperty("--gsrp-side-panel-top", `${topOffset}px`); if (isSidePanelModeActive$1()) { if (!document.body.classList.contains("gsrp-side-panel-active-body")) { document.body.classList.add("gsrp-side-panel-active-body"); } } else { document.body.classList.remove("gsrp-side-panel-active-body"); } } const RE_ESCAPE = /[.*+?^${}()|[\]\\]/g; function getHighlightTerms(query) { if (!query) return []; const terms = query.toLowerCase().split(/\s+or\s+|\s*,\s*/i).flatMap((part) => part.split(/\s+/)).map((t) => t.trim()).filter((t) => t && t !== "or"); return Array.from(new Set(terms)).sort((a, b) => b.length - a.length); } function applyHighlights(wrapper, query) { const parentsToNormalize = new Set(); wrapper.querySelectorAll("mark.gsrp-filter-highlight, mark.gsrp-filter-highlight-current").forEach((m) => { const parent = m.parentNode; if (!parent) return; while (m.firstChild) parent.insertBefore(m.firstChild, m); parent.removeChild(m); parentsToNormalize.add(parent); }); parentsToNormalize.forEach((parent) => { if (typeof parent.normalize === "function") { parent.normalize(); } }); if (!query) return []; const terms = getHighlightTerms(query); if (terms.length === 0) return []; function isVisibleBody(body) { let el = body; while (el && el !== wrapper) { if (el.classList.contains("gsrp-comment-collapsed") || el.classList.contains("gsrp-comment-filtered-out")) { return false; } el = el.parentElement; } return true; } const bodies = Array.from(wrapper.querySelectorAll(".gsrp-comment-body")).filter(isVisibleBody); const escapedTerms = terms.map((t) => t.replace(RE_ESCAPE, "\\$&")); const re = new RegExp(escapedTerms.join("|"), "gi"); bodies.forEach((body) => { const walker = document.createTreeWalker(body, NodeFilter.SHOW_TEXT); const textNodes = []; let n; while (n = walker.nextNode()) textNodes.push(n); textNodes.forEach((node) => { const text2 = node.nodeValue || ""; re.lastIndex = 0; if (!re.test(text2)) return; re.lastIndex = 0; const fragment = document.createDocumentFragment(); let lastIdx = 0; let match; while ((match = re.exec(text2)) !== null) { if (match.index > lastIdx) { fragment.appendChild(document.createTextNode(text2.slice(lastIdx, match.index))); } const mark = document.createElement("mark"); mark.className = "gsrp-filter-highlight"; mark.textContent = match[0]; fragment.appendChild(mark); lastIdx = re.lastIndex; if (match.index === re.lastIndex) re.lastIndex += 1; } if (lastIdx < text2.length) { fragment.appendChild(document.createTextNode(text2.slice(lastIdx))); } node.parentNode?.replaceChild(fragment, node); }); }); return Array.from(wrapper.querySelectorAll("mark.gsrp-filter-highlight")); } function scrollMarkIntoView(mark) { let scroller = mark.closest(".gsrp-side-panel-body"); if (!scroller) { scroller = mark.closest(".gsrp-preview-inner-wrapper") || mark.closest(".gsrp-preview-tooltip"); } if (!scroller) return; let stickyHeight = 0; const stickyHeader = scroller.querySelector( ".gsrp-comments-section-header, .gsrp-preview-title-container" ); if (stickyHeader) { stickyHeight = stickyHeader.offsetHeight || 0; } const m = mark.getBoundingClientRect(); const s = scroller.getBoundingClientRect(); if (m.top >= s.top + stickyHeight && m.bottom <= s.bottom) return; const offset = m.top - (s.top + stickyHeight) - (scroller.clientHeight - stickyHeight - m.height) / 2; if (typeof scroller.scrollTo === "function") { scroller.scrollTo({ top: scroller.scrollTop + offset, behavior: "smooth" }); } else { scroller.scrollTop += offset; } } function applyCommentFilter(wrapper, query, opOnly, scoreThreshold, modAdminOnly, scoreOperator = "gte", linksOnly = false, mediaOnly = false) { if (!wrapper) return; const listWrapper = wrapper.classList.contains("gsrp-comments-list-wrapper") ? wrapper : wrapper.querySelector(".gsrp-comments-list-wrapper"); if (listWrapper && typeof listWrapper.gsrpForceFlush === "function" && listWrapper.gsrpRemainingQueue) { listWrapper.gsrpForceFlush(); } const q = (query || "").trim().toLowerCase(); const scoreVal = scoreThreshold !== void 0 && scoreThreshold !== null && scoreThreshold !== "" ? parseInt(String(scoreThreshold), 10) : null; const filterByScore = scoreVal !== null && !isNaN(scoreVal); const filterByModAdmin = !!modAdminOnly; const filterByLinks = !!linksOnly; const filterByMedia = !!mediaOnly; const noFilter = !q && !opOnly && !filterByScore && !filterByModAdmin && !filterByLinks && !filterByMedia; if (noFilter) { wrapper.querySelectorAll(".gsrp-comment-filtered-out").forEach((el) => { el.classList.remove("gsrp-comment-filtered-out"); }); wrapper.classList.remove("gsrp-no-matches"); return; } function matchText(haystack, queryStr) { if (!queryStr) return true; const orParts = queryStr.split(/\s+or\s+|\s*,\s*/i); return orParts.some((part) => { const andTerms = part.trim().split(/\s+/).filter(Boolean); if (andTerms.length === 0) return false; return andTerms.every((term) => haystack.includes(term)); }); } const allItems = wrapper.querySelectorAll(".gsrp-comment-item, .gsrp-reply-item"); const matchingTopLevels = new Set(); allItems.forEach((item) => { const author = item.dataset.author || ""; const isOpComment = item.dataset.isOp === "true"; const scoreAttr = item.dataset.score; const score = scoreAttr !== void 0 ? parseInt(scoreAttr, 10) : 0; const isModOrAdmin = item.dataset.distinguished === "moderator" || item.dataset.distinguished === "admin"; const bodyEl = item.querySelector(":scope > .gsrp-comment-content > .gsrp-comment-body"); const body = bodyEl ? bodyEl.textContent || "" : ""; const haystack = (body + " " + author).toLowerCase(); const matchesQuery = matchText(haystack, q); const matchesOp = !opOnly || isOpComment; const matchesScore = !filterByScore || scoreVal !== null && !isNaN(score) && (scoreOperator === "lte" ? score <= scoreVal : score >= scoreVal); const matchesModAdmin = !filterByModAdmin || isModOrAdmin; const linkAnchors = bodyEl ? bodyEl.querySelectorAll("a[href]") : null; const hasLinks = filterByLinks && linkAnchors ? Array.from(linkAnchors).some((a) => { try { const url = new URL(a.href, window.location.href); return !url.hostname.endsWith("reddit.com"); } catch { return false; } }) : false; const hasMedia = filterByMedia && bodyEl ? !!bodyEl.querySelector(".gsrp-comment-media-wrapper, .gsrp-inline-media-img") : false; const matchesLinks = !filterByLinks || hasLinks; const matchesMedia = !filterByMedia || hasMedia; const ownMatches = matchesQuery && matchesOp && matchesScore && matchesModAdmin && matchesLinks && matchesMedia; if (!ownMatches) return; const topLevel = item.classList.contains("gsrp-comment-item") ? item : item.closest(".gsrp-comment-item"); if (topLevel) matchingTopLevels.add(topLevel); }); const visible = new Set(); matchingTopLevels.forEach((top) => { visible.add(top); top.querySelectorAll(".gsrp-reply-item").forEach((d) => visible.add(d)); }); allItems.forEach((item) => { item.classList.toggle("gsrp-comment-filtered-out", !visible.has(item)); }); wrapper.classList.toggle("gsrp-no-matches", matchingTopLevels.size === 0); } let escKeyInstalled = false; function installEscKeyHandler() { if (escKeyInstalled) return; escKeyInstalled = true; document.addEventListener("keydown", (e) => { if (e.key !== "Escape") return; if (document.getElementById("gsrp-settings-modal")) return; const pinned = document.querySelectorAll(".gsrp-preview-trigger.gsrp-pinned"); if (pinned.length === 0) return; e.preventDefault(); pinned.forEach((trigger) => { trigger.classList.remove("gsrp-pinned"); trigger.classList.remove("gsrp-hover-sticky"); const tooltip = trigger.querySelector( ".gsrp-preview-tooltip" ); if (tooltip) { tooltip.classList.add("gsrp-force-close"); clearMediaIn(tooltip); if (tooltip.gsrpUnlockHandler) { document.removeEventListener("mousemove", tooltip.gsrpUnlockHandler); } const unlockOnMove = () => { tooltip.classList.remove("gsrp-force-close"); document.removeEventListener("mousemove", unlockOnMove); tooltip.gsrpUnlockHandler = null; }; tooltip.gsrpUnlockHandler = unlockOnMove; setTimeout(() => { if (tooltip && tooltip.classList.contains("gsrp-force-close") && (trigger.matches(":hover") || tooltip.matches(":hover"))) { document.addEventListener("mousemove", unlockOnMove); } else { tooltip.gsrpUnlockHandler = null; } }, 80); } }); }); } function unpinAllExcept(currentTrigger) { document.querySelectorAll(".gsrp-preview-trigger.gsrp-pinned").forEach((other) => { if (other === currentTrigger) return; other.classList.remove("gsrp-pinned"); other.classList.remove("gsrp-hover-sticky"); const otherTooltip = other.querySelector(".gsrp-preview-tooltip"); if (otherTooltip) { otherTooltip.classList.add("gsrp-force-close"); otherTooltip.classList.remove("gsrp-maximized"); const maxBtn = otherTooltip.querySelector(".gsrp-maximize-btn"); if (maxBtn) { maxBtn.textContent = "⛶"; maxBtn.title = L.maximizeBtn; } clearMediaIn(otherTooltip); } }); } function autoPinTriggerOf(el) { if (isSidePanelModeActive()) { const sidePanel = el && el.closest && el.closest("#gsrp-global-side-panel"); if (sidePanel) { autoPinSidePanel(); return; } } const trigger = el && el.closest && el.closest(".gsrp-preview-trigger"); if (!trigger || trigger.classList.contains("gsrp-pinned")) return; unpinAllExcept(trigger); const tooltip = trigger.querySelector(".gsrp-preview-tooltip"); if (tooltip) tooltip.classList.remove("gsrp-force-close"); trigger.classList.add("gsrp-pinned"); } let clickOutsideInstalled = false; function installClickOutsideHandler() { if (clickOutsideInstalled) return; clickOutsideInstalled = true; document.addEventListener("mousedown", (e) => { const pinnedTriggers = document.querySelectorAll(".gsrp-preview-trigger.gsrp-pinned"); if (pinnedTriggers.length === 0) return; const target = e.target; if (!(target instanceof Element)) return; if (target.closest(".gsrp-lightbox-overlay") || target.closest("#gsrp-settings-modal") || target.closest(".gsrp-settings-overlay")) { return; } let clickedInsideAnyPinned = false; pinnedTriggers.forEach((trigger) => { if (trigger.contains(target)) { clickedInsideAnyPinned = true; } }); if (!clickedInsideAnyPinned) { unpinAllExcept(null); } }); } let fullscreenAutoPinInstalled = false; function installFullscreenAutoPin() { if (fullscreenAutoPinInstalled) return; fullscreenAutoPinInstalled = true; const onChange = () => { const fsEl = document.fullscreenElement || document.webkitFullscreenElement; if (!fsEl || typeof fsEl.closest !== "function") return; autoPinTriggerOf(fsEl); }; document.addEventListener("fullscreenchange", onChange); document.addEventListener("webkitfullscreenchange", onChange); } const SVG_PLAY = `<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor" stroke="none"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg>`; const SVG_PAUSE = `<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor" stroke="none"><rect x="6" y="4" width="4" height="16"></rect><rect x="14" y="4" width="4" height="16"></rect></svg>`; const SVG_ZOOM_IN = `<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="11" y1="8" x2="11" y2="14"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>`; const SVG_ZOOM_OUT = `<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line><line x1="8" y1="11" x2="14" y2="11"></line></svg>`; const SVG_ZOOM_RESET = `<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6M9 21H3v-6M21 3l-7 7M3 21l7-7"></path></svg>`; const SVG_ROTATE = `<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"></polyline><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"></path></svg>`; const SVG_DOWNLOAD = `<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>`; const SVG_INFO = `<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg>`; const SVG_HELP = `<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"></circle><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"></path><line x1="12" y1="17" x2="12.01" y2="17"></line></svg>`; const SVG_CLOSE = `<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>`; const SVG_PREV = `<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"></polyline></svg>`; const SVG_NEXT = `<svg viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg>`; const SVG_CHEVRON_DOWN = `<svg viewBox="0 0 24 24" width="10" height="10" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="margin-left: 2px;"><polyline points="6 9 12 15 18 9"></polyline></svg>`; const SVG_FULLSCREEN = `<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"></path></svg>`; const SVG_EXIT_FULLSCREEN = `<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 14h6v6m10-6h-6v6M4 10h6V4m10 6h-6V4"></path></svg>`; const ZOOM_MIN = 1; const ZOOM_MAX = 10; const ZOOM_STEP = 1.15; const SLIDESHOW_DURATIONS = [ { label: "1s", val: 1e3 }, { label: "1.5s", val: 1500 }, { label: "2s", val: 2e3 }, { label: "3.5s", val: 3500 }, { label: "5s", val: 5e3 }, { label: "10s", val: 1e4 } ]; const DEFAULT_SLIDESHOW_DURATION_MS = 3500; const IDLE_HIDE_TIMEOUT_MS = 1200; const PROGRESS_RING_RADIUS = 7; const PROGRESS_RING_CIRCUMFERENCE = 2 * Math.PI * PROGRESS_RING_RADIUS; function filenameFor(url, indexHint) { let filename = `Reddit_image_${indexHint + 1}.png`; try { const parsed = new URL(url); const segments = parsed.pathname.split("/").filter(Boolean); if (segments.length > 0) { const last = segments[segments.length - 1]; filename = last.includes(".") ? `Reddit_${last}` : `Reddit_${last}.png`; } } catch { } return filename; } function fallbackDownload(url, name) { const a = document.createElement("a"); a.href = url; a.download = name; const isBlob = url.startsWith("blob:"); let isSameOrigin = false; try { isSameOrigin = new URL(url).origin === window.location.origin; } catch { } if (!isBlob && !isSameOrigin) { a.target = "_blank"; } document.body.appendChild(a); a.click(); a.remove(); } function downloadLightboxImage(url, indexHint) { if (!url) return; const name = filenameFor(url, indexHint); if (typeof GM_download === "function") { GM_download({ url, name, onerror: () => fallbackDownload(url, name) }); } else { fallbackDownload(url, name); } } function toggleFullscreen(element) { if (!element) return; if (!document.fullscreenElement) { if (typeof element.requestFullscreen === "function") { element.requestFullscreen(); } else if (typeof element.webkitRequestFullscreen === "function") { element.webkitRequestFullscreen(); } } else { if (typeof document.exitFullscreen === "function") { document.exitFullscreen(); } else if (typeof document.webkitExitFullscreen === "function") { document.webkitExitFullscreen(); } } } function createZoomController(imgEl) { let zoom = 1; let panX = 0; let panY = 0; let rotateAngle = 0; let isDragging = false; let dragStartX = 0; let dragStartY = 0; let panStartX = 0; let panStartY = 0; function apply2() { imgEl.style.transform = `translate(${panX}px, ${panY}px) rotate(${rotateAngle}deg) scale(${zoom})`; imgEl.style.cursor = zoom > 1 ? isDragging ? "grabbing" : "grab" : "default"; } function reset() { zoom = 1; panX = 0; panY = 0; rotateAngle = 0; isDragging = false; apply2(); } function zoomIn() { zoom = Math.min(ZOOM_MAX, zoom * ZOOM_STEP); apply2(); } function zoomOut() { zoom = Math.max(ZOOM_MIN, zoom / ZOOM_STEP); if (zoom <= 1) reset(); else apply2(); } function rotate() { rotateAngle = (rotateAngle + 90) % 360; apply2(); } function wheelZoom(deltaY, cx, cy) { const factor = deltaY < 0 ? ZOOM_STEP : 1 / ZOOM_STEP; const newZoom = Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, zoom * factor)); if (newZoom === zoom) return; const ratio = newZoom / zoom; panX += cx * (1 - ratio); panY += cy * (1 - ratio); zoom = newZoom; if (zoom <= 1) { zoom = 1; panX = 0; panY = 0; } apply2(); } function startDrag(clientX, clientY) { if (zoom <= 1) return false; isDragging = true; dragStartX = clientX; dragStartY = clientY; panStartX = panX; panStartY = panY; apply2(); return true; } function continueDrag(clientX, clientY) { if (!isDragging) return; panX = panStartX + (clientX - dragStartX); panY = panStartY + (clientY - dragStartY); apply2(); } function endDrag() { if (!isDragging) return; isDragging = false; apply2(); } function dblclickZoom(cx, cy) { if (zoom > 1) { reset(); } else { zoom = 2; panX = cx * (1 - 2); panY = cy * (1 - 2); apply2(); } } return { apply: apply2, reset, zoomIn, zoomOut, rotate, wheelZoom, startDrag, continueDrag, endDrag, dblclickZoom, get zoom() { return zoom; }, get isDragging() { return isDragging; } }; } function createPrefetcher(images, radius = 3) { const preloadedIndices = new Set(); function prefetchOne(i) { if (images.length < 2) return; const normalizedIndex = (i % images.length + images.length) % images.length; if (preloadedIndices.has(normalizedIndex)) return; preloadedIndices.add(normalizedIndex); const target = images[normalizedIndex]; if (!target || !target.url) return; const preloadImg = new Image(); preloadImg.src = target.url; } function prefetchAround(centerIdx) { for (let offset = 1; offset <= radius; offset++) { prefetchOne(centerIdx + offset); prefetchOne(centerIdx - offset); } } return { prefetchOne, prefetchAround }; } function createAutoplayController({ overlay, playBtn, onAdvance, initialDuration }) { let timer = null; let playing = false; let duration = initialDuration; let elapsed = 0; function updateProgressRing() { const ringCircle = overlay.querySelector( ".gsrp-lightbox-progress-ring-circle" ); if (!ringCircle) return; const pct = Math.min(1, elapsed / duration); ringCircle.style.strokeDashoffset = String(PROGRESS_RING_CIRCUMFERENCE * (1 - pct)); } function resetProgressRing() { const ringCircle = overlay.querySelector( ".gsrp-lightbox-progress-ring-circle" ); if (!ringCircle) return; ringCircle.style.strokeDashoffset = String(PROGRESS_RING_CIRCUMFERENCE); } function start() { playing = true; playBtn.innerHTML = SVG_PAUSE; playBtn.classList.add("gsrp-is-active"); if (timer) clearInterval(timer); const tickInterval = 50; elapsed = 0; timer = setInterval(() => { elapsed += tickInterval; if (elapsed >= duration) { elapsed = 0; onAdvance(); } updateProgressRing(); }, tickInterval); } function stop() { playing = false; playBtn.innerHTML = SVG_PLAY; playBtn.classList.remove("gsrp-is-active"); if (timer) { clearInterval(timer); timer = null; } resetProgressRing(); } function teardown2() { if (timer) { clearInterval(timer); timer = null; } playing = false; } function resetElapsed() { elapsed = 0; resetProgressRing(); } function setDuration(newDuration) { duration = newDuration; } return { get isPlaying() { return playing; }, get duration() { return duration; }, start, stop, teardown: teardown2, resetElapsed, setDuration }; } function createLightboxKeyboardHandler(deps) { return function handleKeydown(e) { if (!deps.getActiveOverlay()) return; const key = e.key.toLowerCase(); let handled = true; if (e.key === "Escape") { deps.teardown(); } else if (e.key === "ArrowRight" || key === "d") { deps.go(1); } else if (e.key === "ArrowLeft" || key === "a") { deps.go(-1); } else if (e.key === " " || key === "p") { if (deps.autoplay.isPlaying) deps.autoplay.stop(); else deps.autoplay.start(); } else if (e.key === "+" || e.key === "=") { deps.zc.zoomIn(); } else if (e.key === "-" || e.key === "_") { deps.zc.zoomOut(); } else if (e.key === "0") { deps.zc.reset(); } else if (key === "r") { deps.zc.rotate(); } else if (key === "f") { deps.toggleFullscreen(); } else if (key === "i") { deps.overlay.classList.toggle("gsrp-lightbox-hide-info"); safeLocalStorage.setItem( "gsrp-lightbox-info-hidden", deps.overlay.classList.contains("gsrp-lightbox-hide-info") ? "1" : "0" ); } else if (key === "s" || e.key === "Enter") { deps.downloadImage(); } else if (e.key === "?" || e.key === "細") { deps.toggleCheatSheet(); } else if (e.key === "Tab") { const focusables = deps.getFocusables(); if (focusables.length > 0) { const first = focusables[0]; const last = focusables[focusables.length - 1]; const activeIdx = focusables.indexOf(document.activeElement); if (activeIdx === -1) { e.preventDefault(); e.stopPropagation(); if (e.shiftKey) { last.focus(); } else { first.focus(); } } else if (e.shiftKey) { if (document.activeElement === first) { e.preventDefault(); e.stopPropagation(); last.focus(); } } else { if (document.activeElement === last) { e.preventDefault(); e.stopPropagation(); first.focus(); } } } else { e.preventDefault(); e.stopPropagation(); } handled = false; } else { handled = false; } if (handled) { e.preventDefault(); e.stopPropagation(); } }; } function createIdleHideController({ overlay, interactiveElements, zc, idleTimeoutMs, signal }) { let idleTimer = null; function resetIdleTimer() { if (idleTimer) clearTimeout(idleTimer); if (overlay.classList.contains("gsrp-hovering-controls")) return; idleTimer = setTimeout(() => { if (!zc.isDragging && !overlay.classList.contains("gsrp-hovering-controls")) { overlay.classList.add("gsrp-lightbox-idle"); } }, idleTimeoutMs); } function showControls() { overlay.classList.remove("gsrp-lightbox-idle"); resetIdleTimer(); } const onMouseEnter = () => { overlay.classList.add("gsrp-hovering-controls"); showControls(); }; const onMouseLeave = () => { overlay.classList.remove("gsrp-hovering-controls"); resetIdleTimer(); }; interactiveElements.forEach((el) => { if (!el) return; el.addEventListener("mouseenter", onMouseEnter, { signal }); el.addEventListener("mouseleave", onMouseLeave, { signal }); }); overlay.addEventListener("mousemove", showControls, { signal }); overlay.addEventListener("click", showControls, { signal }); function teardown2() { if (idleTimer) { clearTimeout(idleTimer); idleTimer = null; } interactiveElements.forEach((el) => { if (!el) return; el.removeEventListener("mouseenter", onMouseEnter); el.removeEventListener("mouseleave", onMouseLeave); }); overlay.removeEventListener("mousemove", showControls); overlay.removeEventListener("click", showControls); } return { showControls, teardown: teardown2 }; } const SWIPE_THRESHOLD_PX = 60; function installLightboxGestures({ img, zc, go, signal }) { let touchStartX = 0; img.addEventListener( "touchstart", (e) => { if (e.touches.length === 1) { touchStartX = e.touches[0].clientX; } }, { passive: true, signal } ); img.addEventListener( "touchend", (e) => { if (e.changedTouches.length !== 1) return; const touchEndX = e.changedTouches[0].clientX; const diffX = touchEndX - touchStartX; if (Math.abs(diffX) > SWIPE_THRESHOLD_PX) { go(diffX > 0 ? -1 : 1); } }, { passive: true, signal } ); img.addEventListener( "wheel", (e) => { e.preventDefault(); e.stopPropagation(); const rect = img.getBoundingClientRect(); const cx = e.clientX - rect.left - rect.width / 2; const cy = e.clientY - rect.top - rect.height / 2; zc.wheelZoom(e.deltaY, cx, cy); }, { passive: false, signal } ); const onMouseMove = (e) => { zc.continueDrag(e.clientX, e.clientY); }; const onMouseUp = () => { zc.endDrag(); document.removeEventListener("mousemove", onMouseMove); document.removeEventListener("mouseup", onMouseUp); }; img.addEventListener( "mousedown", (e) => { if (e.button !== 0) return; if (zc.zoom <= 1) return; e.preventDefault(); e.stopPropagation(); zc.startDrag(e.clientX, e.clientY); document.removeEventListener("mousemove", onMouseMove); document.removeEventListener("mouseup", onMouseUp); document.addEventListener("mousemove", onMouseMove, { signal }); document.addEventListener("mouseup", onMouseUp, { signal }); }, { signal } ); img.addEventListener( "dblclick", (e) => { e.preventDefault(); e.stopPropagation(); const rect = img.getBoundingClientRect(); const cx = e.clientX - rect.left - rect.width / 2; const cy = e.clientY - rect.top - rect.height / 2; zc.dblclickZoom(cx, cy); }, { signal } ); } function buildCheatSheetHTML(L2) { return ` <div class="gsrp-cheat-sheet-header"> <span>${L2.shortcutsTitle || "Keyboard Shortcuts"}</span> <button class="gsrp-cheat-sheet-close-btn" type="button" title="${L2.closeBtn || "Close"}">${SVG_CLOSE}</button> </div> <div class="gsrp-cheat-sheet-grid"> <div class="gsrp-cheat-row"><kbd>←</kbd> <kbd>→</kbd> / <kbd>A</kbd> <kbd>D</kbd> <span>${L2.shortcutNav || "Previous / Next Image"}</span></div> <div class="gsrp-cheat-row"><kbd>Space</kbd> / <kbd>P</kbd> <span>${L2.shortcutPlay || "Autoplay Play / Pause"}</span></div> <div class="gsrp-cheat-row"><kbd>+</kbd> / <kbd>-</kbd> <span>${L2.shortcutZoom || "Zoom In / Out"}</span></div> <div class="gsrp-cheat-row"><kbd>0</kbd> <span>${L2.shortcutReset || "Reset Zoom & Rotation"}</span></div> <div class="gsrp-cheat-row"><kbd>R</kbd> <span>${L2.shortcutRotate || "Rotate image 90°"}</span></div> <div class="gsrp-cheat-row"><kbd>F</kbd> <span>${L2.shortcutFullscreen || "Toggle Fullscreen"}</span></div> <div class="gsrp-cheat-row"><kbd>S</kbd> / <kbd>Enter</kbd> <span>${L2.shortcutDownload || "Download / Open Original"}</span></div> <div class="gsrp-cheat-row"><kbd>I</kbd> <span>${L2.shortcutInfo || "Toggle Description Panel"}</span></div> <div class="gsrp-cheat-row"><kbd>?</kbd> <span>${L2.shortcutHelp || "Toggle Shortcuts Cheat Sheet"}</span></div> <div class="gsrp-cheat-row"><kbd>Esc</kbd> <span>${L2.shortcutClose || "Exit Lightbox"}</span></div> </div> `; } function buildLightboxDom({ images, initialIdx, isDark, initialAutoplayDuration, L: L2, onDurationSelect, onThumbClick }) { const overlay = document.createElement("div"); overlay.className = "gsrp-lightbox-overlay" + (isDark ? " gsrp-is-dark" : ""); overlay.setAttribute("role", "dialog"); overlay.setAttribute("aria-modal", "true"); overlay.setAttribute("aria-label", L2.lightboxLabel || "Image viewer"); if (safeLocalStorage.getItem("gsrp-lightbox-info-hidden") === "1") { overlay.classList.add("gsrp-lightbox-hide-info"); } const imgContainer = document.createElement("div"); imgContainer.className = "gsrp-lightbox-img-container"; const img = document.createElement("img"); img.className = "gsrp-lightbox-img"; img.alt = ""; imgContainer.appendChild(img); const counter = document.createElement("div"); counter.className = "gsrp-lightbox-counter"; const dims = document.createElement("div"); dims.className = "gsrp-lightbox-dims"; const prev = document.createElement("button"); prev.type = "button"; prev.className = "gsrp-lightbox-nav gsrp-lightbox-nav-prev"; prev.setAttribute("aria-label", L2.lightboxPrev || "Previous"); prev.innerHTML = SVG_PREV; const next = document.createElement("button"); next.type = "button"; next.className = "gsrp-lightbox-nav gsrp-lightbox-nav-next"; next.setAttribute("aria-label", L2.lightboxNext || "Next"); next.innerHTML = SVG_NEXT; const close = document.createElement("button"); close.type = "button"; close.className = "gsrp-lightbox-close"; close.setAttribute("aria-label", L2.lightboxClose || L2.closeBtn || "Close"); close.innerHTML = SVG_CLOSE; const infoPanel = document.createElement("div"); infoPanel.className = "gsrp-lightbox-info-panel"; const panelTitle = document.createElement("div"); panelTitle.className = "gsrp-lightbox-title"; const panelCaption = document.createElement("div"); panelCaption.className = "gsrp-lightbox-caption"; infoPanel.appendChild(panelTitle); infoPanel.appendChild(panelCaption); const hud = document.createElement("div"); hud.className = "gsrp-lightbox-hud"; const hudPlay = document.createElement("button"); hudPlay.type = "button"; hudPlay.className = "gsrp-lightbox-hud-btn gsrp-lightbox-hud-play"; hudPlay.title = L2.hudPlay || "Slideshow (Space / P)"; hudPlay.innerHTML = SVG_PLAY; const hudDurationWrapper = document.createElement("div"); hudDurationWrapper.className = "gsrp-lightbox-hud-duration-wrapper"; const hudDurationToggle = document.createElement("button"); hudDurationToggle.type = "button"; hudDurationToggle.className = "gsrp-lightbox-hud-btn gsrp-lightbox-hud-duration-toggle"; hudDurationToggle.title = L2.hudDurationToggle || "Slideshow Speed"; hudDurationToggle.innerHTML = `<span>${(initialAutoplayDuration / 1e3).toFixed(1)}s</span>${SVG_CHEVRON_DOWN}`; const hudDurationPopover = document.createElement("div"); hudDurationPopover.className = "gsrp-lightbox-hud-duration-popover gsrp-hide"; SLIDESHOW_DURATIONS.forEach((d) => { const opt = document.createElement("div"); opt.className = "gsrp-lightbox-hud-duration-option" + (d.val === initialAutoplayDuration ? " gsrp-is-active" : ""); opt.textContent = d.label; opt.addEventListener("click", (e) => { e.stopPropagation(); hudDurationToggle.innerHTML = `<span>${d.label}</span>${SVG_CHEVRON_DOWN}`; hudDurationPopover.querySelectorAll(".gsrp-lightbox-hud-duration-option").forEach((el) => { el.classList.toggle("gsrp-is-active", el.textContent === d.label); }); hudDurationPopover.classList.add("gsrp-hide"); onDurationSelect(d.val); }); hudDurationPopover.appendChild(opt); }); hudDurationToggle.addEventListener("click", (e) => { e.stopPropagation(); hudDurationPopover.classList.toggle("gsrp-hide"); }); hudDurationWrapper.appendChild(hudDurationToggle); hudDurationWrapper.appendChild(hudDurationPopover); const progressRing = document.createElement("div"); progressRing.className = "gsrp-lightbox-progress-ring-wrapper"; progressRing.innerHTML = ` <svg class="gsrp-lightbox-progress-ring" width="16" height="16"> <circle class="gsrp-lightbox-progress-ring-bg" stroke="rgba(255, 255, 255, 0.15)" stroke-width="2" fill="transparent" r="7" cx="8" cy="8"/> <circle class="gsrp-lightbox-progress-ring-circle" stroke="#3b82f6" stroke-width="2" fill="transparent" r="7" cx="8" cy="8" stroke-dasharray="43.98" stroke-dashoffset="43.98"/> </svg> `; const hudZoomIn = makeHudBtn("zoom-in", L2.hudZoomIn || "Zoom In (+)", SVG_ZOOM_IN); const hudZoomOut = makeHudBtn("zoom-out", L2.hudZoomOut || "Zoom Out (-)", SVG_ZOOM_OUT); const hudZoomReset = makeHudBtn( "zoom-reset", L2.hudZoomReset || "Reset Zoom (0)", SVG_ZOOM_RESET ); const hudRotate = makeHudBtn("rotate", L2.hudRotate || "Rotate 90° (R)", SVG_ROTATE); const hudDownload = makeHudBtn( "download", L2.hudDownload || "Download / Open Original (S / Enter)", SVG_DOWNLOAD ); const hudFullscreen = makeHudBtn( "fullscreen", L2.hudFullscreen || "Fullscreen (F)", SVG_FULLSCREEN ); const hudInfo = makeHudBtn("info", L2.hudInfo || "Toggle Description Panel (I)", SVG_INFO); const hudHelp = makeHudBtn("help", L2.hudHelp || "Shortcut Cheatsheet (?)", SVG_HELP); if (images.length > 1) { hud.appendChild(hudPlay); hud.appendChild(hudDurationWrapper); hud.appendChild(progressRing); hud.appendChild(makeDivider()); } hud.appendChild(hudZoomIn); hud.appendChild(hudZoomOut); hud.appendChild(hudZoomReset); hud.appendChild(makeDivider()); hud.appendChild(hudRotate); hud.appendChild(hudDownload); hud.appendChild(hudFullscreen); hud.appendChild(hudInfo); hud.appendChild(hudHelp); const thumbsStrip = document.createElement("div"); thumbsStrip.className = "gsrp-lightbox-thumbs"; const thumbs = []; if (images.length > 1) { images.forEach((imgData, index) => { const thumb = document.createElement("div"); thumb.className = "gsrp-lightbox-thumb" + (index === initialIdx ? " gsrp-is-active" : ""); thumb.setAttribute("data-idx", String(index)); const thumbImg = document.createElement("img"); thumbImg.src = imgData.url; thumbImg.loading = "lazy"; thumb.appendChild(thumbImg); thumb.addEventListener("click", (e) => { e.stopPropagation(); onThumbClick(index); }); thumbsStrip.appendChild(thumb); thumbs.push(thumb); }); thumbsStrip.addEventListener( "wheel", (e) => { e.preventDefault(); e.stopPropagation(); thumbsStrip.scrollBy({ left: e.deltaY * 2.2, behavior: "auto" }); }, { passive: false } ); const centerActiveThumb = () => { if (!document.body.contains(thumbsStrip)) return; const activeThumb = thumbsStrip.querySelector(".gsrp-lightbox-thumb.gsrp-is-active"); if (activeThumb) { activeThumb.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center" }); } }; thumbsStrip.addEventListener("mouseenter", () => { centerActiveThumb(); setTimeout(centerActiveThumb, 250); }); thumbsStrip.addEventListener("mouseleave", () => { centerActiveThumb(); setTimeout(centerActiveThumb, 250); }); } const cheatSheet = document.createElement("div"); cheatSheet.className = "gsrp-lightbox-cheat-sheet gsrp-hide"; cheatSheet.innerHTML = buildCheatSheetHTML(L2); const cheatClose = cheatSheet.querySelector(".gsrp-cheat-sheet-close-btn"); if (cheatClose) { cheatClose.addEventListener("click", (e) => { e.stopPropagation(); cheatSheet.classList.add("gsrp-hide"); }); } const bottomContainer = document.createElement("div"); bottomContainer.className = "gsrp-lightbox-bottom-container"; if (images.length > 1) { bottomContainer.appendChild(thumbsStrip); } bottomContainer.appendChild(infoPanel); overlay.appendChild(cheatSheet); overlay.appendChild(imgContainer); overlay.appendChild(counter); overlay.appendChild(dims); overlay.appendChild(hud); overlay.appendChild(bottomContainer); if (images.length > 1) { overlay.appendChild(prev); overlay.appendChild(next); } overlay.appendChild(close); return { overlay, imgContainer, img, counter, dims, prev, next, close, infoPanel, panelTitle, panelCaption, hud, hudPlay, hudDurationToggle, hudDurationPopover, hudZoomIn, hudZoomOut, hudZoomReset, hudRotate, hudDownload, hudFullscreen, hudInfo, hudHelp, thumbsStrip, thumbs, cheatSheet }; } function makeHudBtn(suffix, title, svg2) { const btn = document.createElement("button"); btn.type = "button"; btn.className = `gsrp-lightbox-hud-btn gsrp-lightbox-hud-${suffix}`; btn.title = title; btn.innerHTML = svg2; return btn; } function makeDivider() { const d = document.createElement("span"); d.className = "gsrp-lightbox-hud-divider"; return d; } let activeOverlay = null; let lightboxController = null; let activeKeydownHandler = null; let activeFullscreenChangeListener = null; let savedFocus = null; let savedBodyOverflow = null; let activeIndex = 0; let lightboxTriggerElement = null; let lightboxIsManualPin = false; let activeAutoplay = null; let activeIdleHide = null; function teardown() { if (activeAutoplay) { activeAutoplay.teardown(); activeAutoplay = null; } if (activeIdleHide) { activeIdleHide.teardown(); activeIdleHide = null; } if (lightboxController) { lightboxController.abort(); lightboxController = null; } if (activeKeydownHandler) { document.removeEventListener("keydown", activeKeydownHandler, true); activeKeydownHandler = null; } if (activeFullscreenChangeListener) { document.removeEventListener("fullscreenchange", activeFullscreenChangeListener); activeFullscreenChangeListener = null; } if (document.fullscreenElement) { try { if (typeof document.exitFullscreen === "function") { document.exitFullscreen(); } else if (typeof document.webkitExitFullscreen === "function") { document.webkitExitFullscreen(); } } catch (err) { console.error("[GSRP Lightbox] Error exiting fullscreen on teardown:", err); } } if (activeOverlay) { clearMediaIn(activeOverlay); activeOverlay.remove(); activeOverlay = null; } if (savedFocus && typeof savedFocus.focus === "function") { try { const carouselContainer = savedFocus.closest(".gsrp-carousel") || savedFocus.closest(".gsrp-preview-media-gallery"); if (carouselContainer) { carouselContainer.dispatchEvent( new CustomEvent("gsrp-carousel-sync", { detail: { index: activeIndex } }) ); } } catch { } try { savedFocus.focus(); } catch { } savedFocus = null; } if (lightboxTriggerElement && lightboxIsManualPin === false) { const trigger = lightboxTriggerElement.closest(".gsrp-preview-trigger"); if (isSidePanelModeActive()) { const sidePanel = document.getElementById("gsrp-global-side-panel"); if (sidePanel) { sidePanel.classList.remove("gsrp-pinned"); } } else if (trigger) { trigger.classList.add("gsrp-hover-sticky"); trigger.classList.remove("gsrp-pinned"); trigger.dispatchEvent(new MouseEvent("mouseleave")); } } if (savedBodyOverflow !== null) { document.body.style.overflow = savedBodyOverflow; savedBodyOverflow = null; } lightboxTriggerElement = null; lightboxIsManualPin = false; } function openLightbox(images, startIndex, isDark, triggerElement) { if (!Array.isArray(images) || images.length === 0) return; savedFocus = null; teardown(); savedBodyOverflow = document.body.style.overflow; document.body.style.overflow = "hidden"; lightboxController = new AbortController(); let idx = Math.max(0, Math.min(startIndex | 0, images.length - 1)); savedFocus = triggerElement || document.activeElement; lightboxTriggerElement = triggerElement; if (triggerElement) { const trigger = triggerElement.closest(".gsrp-preview-trigger"); lightboxIsManualPin = trigger ? trigger.classList.contains("gsrp-pinned") : false; if (!lightboxIsManualPin) { autoPinTriggerOf(triggerElement); } } const initialAutoplayDuration = parseInt(safeLocalStorage.getItem("gsrp-lightbox-slideshow-duration") || "", 10) || DEFAULT_SLIDESHOW_DURATION_MS; const dom = buildLightboxDom({ images, initialIdx: idx, isDark, initialAutoplayDuration, L, onDurationSelect: (val) => { autoplay.setDuration(val); safeLocalStorage.setItem("gsrp-lightbox-slideshow-duration", val.toString()); if (autoplay.isPlaying) autoplay.start(); }, onThumbClick: (index) => { if (index !== idx) { idx = index; render(); } } }); const { overlay, imgContainer, img, counter, dims, prev, next, close, infoPanel, panelTitle, panelCaption, hud, hudPlay, hudDurationPopover, hudZoomIn, hudZoomOut, hudZoomReset, hudRotate, hudDownload, hudFullscreen, hudInfo, hudHelp, thumbsStrip, cheatSheet } = dom; document.body.appendChild(overlay); activeOverlay = overlay; const zc = createZoomController(img); const autoplay = createAutoplayController({ overlay, playBtn: hudPlay, onAdvance: () => go(1), initialDuration: initialAutoplayDuration }); activeAutoplay = autoplay; const prefetcher = createPrefetcher(images, 3); function downloadImage() { const cur = images[idx]; downloadLightboxImage(cur && cur.url, idx); } function toggleCheatSheet() { cheatSheet.classList.toggle("gsrp-hide"); } hudFullscreen.addEventListener( "click", (e) => { e.stopPropagation(); toggleFullscreen(overlay); }, { signal: lightboxController.signal } ); activeFullscreenChangeListener = () => { if (document.fullscreenElement === overlay) { hudFullscreen.innerHTML = SVG_EXIT_FULLSCREEN; hudFullscreen.title = L.hudExitFullscreen || "Exit Fullscreen (F)"; } else { hudFullscreen.innerHTML = SVG_FULLSCREEN; hudFullscreen.title = L.hudFullscreen || "Fullscreen (F)"; } }; document.addEventListener("fullscreenchange", activeFullscreenChangeListener, { signal: lightboxController.signal }); function setDims(w, h) { if (w && h) { dims.textContent = `${w} × ${h}`; dims.style.display = ""; } else { dims.style.display = "none"; } } const idleHide = createIdleHideController({ overlay, interactiveElements: [hud, thumbsStrip, infoPanel, cheatSheet, close, prev, next], zc, idleTimeoutMs: IDLE_HIDE_TIMEOUT_MS, signal: lightboxController.signal }); activeIdleHide = idleHide; const showControls = idleHide.showControls; let renderToken = 0; function render() { activeIndex = idx; const cur = images[idx]; const myToken = ++renderToken; overlay.classList.remove("gsrp-img-error"); imgContainer.classList.remove("gsrp-is-active"); const url = cur.url || ""; img.style.display = "block"; if (cur.width && cur.height) { img.style.aspectRatio = `${cur.width} / ${cur.height}`; } else { img.style.aspectRatio = ""; } img.src = url; if (cur.alt) img.alt = cur.alt; img.onload = () => { if (myToken !== renderToken) return; const w = cur.width || img.naturalWidth; const h = cur.height || img.naturalHeight; if (!cur.width || !cur.height) { img.style.aspectRatio = `${w} / ${h}`; } setDims(w, h); imgContainer.classList.add("gsrp-is-active"); }; img.onerror = () => { if (myToken !== renderToken) return; overlay.classList.add("gsrp-img-error"); }; if (images.length > 1) { counter.textContent = `${idx + 1} / ${images.length}`; counter.style.display = "block"; } else { counter.textContent = ""; counter.style.display = "none"; } prev.style.display = images.length > 1 ? "" : "none"; next.style.display = images.length > 1 ? "" : "none"; const hasTitle = !!cur.albumTitle; const hasCaption = !!cur.caption; const hasInfo = hasTitle || hasCaption; if (hasInfo) { overlay.classList.add("gsrp-lightbox-has-info"); infoPanel.style.display = ""; if (hasTitle) { panelTitle.textContent = cur.albumTitle ?? null; panelTitle.style.display = ""; } else { panelTitle.textContent = ""; panelTitle.style.display = "none"; } if (hasCaption) { panelCaption.innerHTML = cur.caption ?? ""; panelCaption.style.display = ""; } else { panelCaption.textContent = ""; panelCaption.style.display = "none"; } } else { overlay.classList.remove("gsrp-lightbox-has-info"); infoPanel.style.display = "none"; } if (images.length > 1) { const allThumbs = thumbsStrip.querySelectorAll(".gsrp-lightbox-thumb"); allThumbs.forEach((thumb, index) => { if (index === idx) { thumb.classList.add("gsrp-is-active"); thumb.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center" }); } else { thumb.classList.remove("gsrp-is-active"); } }); } zc.reset(); autoplay.resetElapsed(); prefetcher.prefetchAround(idx); showControls(); } render(); function go(delta) { if (images.length < 2) return; idx = (idx + delta + images.length) % images.length; render(); } overlay.addEventListener( "click", (e) => { const target = e.target; if (target instanceof Element && !target.closest(".gsrp-lightbox-hud-duration-wrapper")) { hudDurationPopover.classList.add("gsrp-hide"); } if (e.target === overlay || e.target === imgContainer) teardown(); }, { signal: lightboxController.signal } ); img.addEventListener("click", (e) => e.stopPropagation(), { signal: lightboxController.signal }); prev.addEventListener( "click", (e) => { e.stopPropagation(); go(-1); }, { signal: lightboxController.signal } ); next.addEventListener( "click", (e) => { e.stopPropagation(); go(1); }, { signal: lightboxController.signal } ); close.addEventListener( "click", (e) => { e.stopPropagation(); teardown(); }, { signal: lightboxController.signal } ); hudPlay.addEventListener( "click", (e) => { e.stopPropagation(); if (autoplay.isPlaying) { autoplay.stop(); } else { autoplay.start(); } }, { signal: lightboxController.signal } ); hudZoomIn.addEventListener( "click", (e) => { e.stopPropagation(); zc.zoomIn(); }, { signal: lightboxController.signal } ); hudZoomOut.addEventListener( "click", (e) => { e.stopPropagation(); zc.zoomOut(); }, { signal: lightboxController.signal } ); hudZoomReset.addEventListener( "click", (e) => { e.stopPropagation(); zc.reset(); }, { signal: lightboxController.signal } ); hudRotate.addEventListener( "click", (e) => { e.stopPropagation(); zc.rotate(); }, { signal: lightboxController.signal } ); hudDownload.addEventListener( "click", (e) => { e.stopPropagation(); downloadImage(); }, { signal: lightboxController.signal } ); hudInfo.addEventListener( "click", (e) => { e.stopPropagation(); overlay.classList.toggle("gsrp-lightbox-hide-info"); safeLocalStorage.setItem( "gsrp-lightbox-info-hidden", overlay.classList.contains("gsrp-lightbox-hide-info") ? "1" : "0" ); }, { signal: lightboxController.signal } ); hudHelp.addEventListener( "click", (e) => { e.stopPropagation(); toggleCheatSheet(); }, { signal: lightboxController.signal } ); installLightboxGestures({ img, zc, go, signal: lightboxController.signal }); activeKeydownHandler = createLightboxKeyboardHandler({ overlay, getActiveOverlay: () => activeOverlay, go, teardown, autoplay, zc, downloadImage, toggleCheatSheet, toggleFullscreen: () => toggleFullscreen(overlay), getFocusables: () => { const list = Array.from( overlay.querySelectorAll('button, [tabindex="0"]') ); return list.filter((el) => { if (el.tabIndex < 0) return false; if (el.disabled) return false; const style = window.getComputedStyle(el); if (style.display === "none" || style.visibility === "hidden") return false; let parent = el.parentElement; while (parent && parent !== overlay) { const pStyle = window.getComputedStyle(parent); if (pStyle.display === "none" || pStyle.visibility === "hidden") return false; parent = parent.parentElement; } return true; }); } }); document.addEventListener("keydown", activeKeydownHandler, { capture: true, signal: lightboxController.signal }); setTimeout(() => { try { close.focus(); } catch { } }, 0); } function openGalleryFrom(galleryItem) { const container = galleryItem.closest( ".gsrp-comment-body, .gsrp-preview-body, .gsrp-imgur-embed-container, .gsrp-preview-media-gallery" ) || galleryItem.parentElement; if (!container) return; const titleEl = container.querySelector(".gsrp-imgur-embed-title"); const albumTitle = titleEl ? (titleEl.textContent || "").trim() : ""; const items = Array.from(container.querySelectorAll(".gsrp-gallery-item")); let idx = items.indexOf(galleryItem); if (idx === -1) { idx = parseInt(galleryItem.dataset.galleryIdx || "", 10) || 0; } const images = items.map((item) => { const img = item.querySelector("img"); const w = parseInt(item.dataset.w || "", 10); const h = parseInt(item.dataset.h || "", 10); const captionEl = item.querySelector(".gsrp-preview-media-caption"); return { url: img ? img.src : "", alt: img ? img.alt || "" : "", width: Number.isFinite(w) && w > 0 ? w : null, height: Number.isFinite(h) && h > 0 ? h : null, caption: captionEl ? captionEl.innerHTML.trim() : "", albumTitle }; }); if (images.length === 0) return; openLightbox(images, idx, detectDarkMode(), galleryItem); } let lightboxClickDelegateInstalled = false; function installLightboxClickDelegate() { if (lightboxClickDelegateInstalled) return; lightboxClickDelegateInstalled = true; document.addEventListener( "click", (e) => { const raw = e.target; const target = raw instanceof Node && raw.nodeType === 3 ? raw.parentNode : raw; if (!(target instanceof Element)) return; if (target.closest(".gsrp-crossposts-link")) return; const galleryBadgeBtn = target.closest(".gsrp-gallery-badge"); if (galleryBadgeBtn) { e.stopPropagation(); e.preventDefault(); try { const rawJson = galleryBadgeBtn.dataset.galleryImages || ""; if (rawJson) { const parsed = JSON.parse(rawJson); const images = []; for (const item of parsed) { if (!item || typeof item.u !== "string") continue; images.push({ url: item.u, alt: "", width: typeof item.w === "number" ? item.w : null, height: typeof item.h === "number" ? item.h : null }); } if (images.length > 0) { openLightbox(images, 0, detectDarkMode(), galleryBadgeBtn); } } } catch { } return; } const galleryItem = target.closest(".gsrp-gallery-item"); if (galleryItem) { if (target.closest(".gsrp-preview-media-caption")) return; if (target.closest(".gsrp-caption-toggle-btn")) return; if (galleryItem.querySelector("video")) return; e.stopPropagation(); e.preventDefault(); openGalleryFrom(galleryItem); return; } if (target instanceof HTMLImageElement && target.classList.contains("gsrp-inline-media-img")) { e.stopPropagation(); e.preventDefault(); openLightbox( [{ url: target.src, alt: target.alt || "" }], 0, detectDarkMode(), target ); } }, true ); document.addEventListener("keydown", (e) => { if (e.key !== "Enter" && e.key !== " ") return; const target = e.target; if (!(target instanceof Element)) return; const galleryItem = target.closest(".gsrp-gallery-item"); if (!galleryItem) return; if (galleryItem.querySelector("video")) return; e.preventDefault(); openGalleryFrom(galleryItem); }); } let commentCollapseInstalled = false; function installCommentCollapseDelegate() { if (commentCollapseInstalled) return; commentCollapseInstalled = true; document.addEventListener( "click", (e) => { const t = e.target; if (!(t instanceof Element)) return; if (t.closest("a, select, button, input, textarea")) return; let item = null; if (t.matches(".gsrp-thread-line")) { item = t.closest(".gsrp-comment-item, .gsrp-reply-item"); } else { const header = t.closest(".gsrp-comment-header"); if (header) item = header.closest(".gsrp-comment-item, .gsrp-reply-item"); } if (!item) return; if (!item.closest(".gsrp-preview-tooltip, .gsrp-side-panel")) return; e.stopPropagation(); item.classList.toggle("gsrp-comment-collapsed"); }, true ); document.addEventListener("keydown", (e) => { if (e.key !== "Enter" && e.key !== " ") return; const t = e.target; if (!(t instanceof Element) || !t.matches(".gsrp-thread-line")) return; const item = t.closest(".gsrp-comment, .gsrp-reply-item"); if (!item) return; e.preventDefault(); item.classList.toggle("gsrp-comment-collapsed"); }); } const openEmbeds = new Map(); function activateBtn(btn, closeTitle) { btn.classList.add("gsrp-active"); btn.title = closeTitle; btn.setAttribute("aria-label", closeTitle); } function deactivateBtn(btn, openTitle) { btn.classList.remove("gsrp-active"); btn.title = openTitle; btn.setAttribute("aria-label", openTitle); } function runOnClose(entry) { if (typeof entry.onClose !== "function") return; try { entry.onClose(entry.container); } catch (err) { console.error("[GSRP] embed-factory onClose error:", err); } } function closeOtherEmbeds(keepId) { for (const [id, entry] of openEmbeds) { if (id === keepId) continue; runOnClose(entry); if (entry.container && entry.container.isConnected) entry.container.remove(); if (entry.btn) deactivateBtn(entry.btn, entry.btnTitleOpen); openEmbeds.delete(id); } } function createEmbedDelegate(config) { const { idPrefix, dataKey, urlDataKey, processedKey, btnClass, containerClass, extractDetails, buildEmbedUrl, iframeAllow, btnTitleOpen, btnTitleClose, btnIconHtml, onContainerOpen, extraInstall, contentBuilder, onClose, tooltipSelector, linkFilter } = config; const dashedDataKey = camelToDash(dataKey); const hostSelector = tooltipSelector || ".gsrp-preview-tooltip"; const isCustomMode = typeof contentBuilder === "function"; let installed = false; let counter = 0; function install() { if (installed) return; installed = true; if (typeof extraInstall === "function") { extraInstall(); } document.addEventListener( "click", (e) => { if (!(e.target instanceof Element)) return; const btn = e.target.closest(`.${btnClass}`); if (!btn) return; e.preventDefault(); e.stopPropagation(); const url = btn.dataset[urlDataKey]; const id = btn.dataset[dataKey]; if (!url || !id) return; const tooltip = btn.closest(hostSelector); if (!tooltip) return; const existing = tooltip.querySelector( `.${containerClass}[data-${dashedDataKey}="${id}"]` ); if (existing) { const entry = openEmbeds.get(id); if (entry) runOnClose(entry); existing.remove(); deactivateBtn(btn, btnTitleOpen); openEmbeds.delete(id); return; } closeOtherEmbeds(id); const details = extractDetails(url); if (!details) return; const container = document.createElement("div"); container.className = containerClass; container.dataset[dataKey] = id; const link = tooltip.querySelector(`a[data-${dashedDataKey}="${id}"]`); const anchor = link ? link.closest("p, li, blockquote, .gsrp-comment-body") || link : btn; anchor.insertAdjacentElement("afterend", container); activateBtn(btn, btnTitleClose); openEmbeds.set(id, { container, btn, btnTitleOpen, onClose }); if (isCustomMode) { const ctx = { container, btn, url, details, link }; try { const ret = contentBuilder(ctx); if (ret && typeof ret.catch === "function") { ret.catch((err) => { console.error("[GSRP] embed-factory contentBuilder rejected:", err); }); } } catch (err) { console.error("[GSRP] embed-factory contentBuilder threw:", err); } } else { if (typeof buildEmbedUrl !== "function") { console.error("[GSRP] embed-factory: iframe mode requires buildEmbedUrl"); return; } const iframe = document.createElement("iframe"); iframe.src = buildEmbedUrl(url, details); iframe.allow = iframeAllow || ""; iframe.allowFullscreen = true; iframe.setAttribute( "sandbox", "allow-scripts allow-same-origin allow-presentation allow-popups allow-popups-to-escape-sandbox" ); iframe.setAttribute("loading", "lazy"); container.appendChild(iframe); } if (typeof onContainerOpen === "function") { onContainerOpen(container); } }, true ); } function process(scope) { if (!scope) return; const links = scope.querySelectorAll("a[href]"); links.forEach((link) => { if (link.dataset[processedKey] === "1") return; const href = link.href; const details = extractDetails(href); if (!details) return; if (typeof linkFilter === "function" && !linkFilter(link)) return; link.dataset[processedKey] = "1"; const uniqueId = `${idPrefix}${++counter}`; link.dataset[dataKey] = uniqueId; const btn = document.createElement("button"); btn.className = btnClass; btn.title = btnTitleOpen; btn.dataset[urlDataKey] = href; btn.dataset[dataKey] = uniqueId; btn.setAttribute("aria-label", btnTitleOpen); btn.innerHTML = btnIconHtml; link.insertAdjacentElement("afterend", btn); }); } return { install, process }; } function camelToDash(key) { return key.replace(/([A-Z])/g, "-$1").toLowerCase(); } const YT_ICON_HTML = ` <!-- Adaptive YouTube Vector Icon without white circle background --> <svg viewBox="0 0 28.57 20" class="gsrp-embed-btn-logo"> <g> <path d="M27.9727 3.12324C27.6435 1.89323 26.6768 0.926623 25.4468 0.597366C23.2197 2.24288e-07 14.285 0 14.285 0C14.285 0 5.35042 2.24288e-07 3.12323 0.597366C1.89323 0.926623 0.926623 1.89323 0.597366 3.12324C2.24288e-07 5.35042 0 10 0 10C0 10 2.24288e-07 14.6496 0.597366 16.8768C0.926623 18.1068 1.89323 19.0734 3.12323 19.4026C5.35042 20 14.285 20 14.285 20C14.285 20 23.2197 20 25.4468 19.4026C26.6768 19.0734 27.6435 18.1068 27.9727 16.8768C28.5701 14.6496 28.5701 10 28.5701 10C28.5701 10 28.5677 5.35042 27.9727 3.12324Z" fill="currentColor"></path> <path d="M11.4253 14.2854L18.8477 10.0004L11.4253 5.71533V14.2854Z" fill="var(--gsrp-yt-logo-triangle, #ffffff)"></path> </g> </svg> <svg viewBox="0 0 24 24" class="gsrp-embed-btn-chevron"><path fill="currentColor" d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"/></svg> `; const delegate$7 = createEmbedDelegate({ idPrefix: "gsrp-yt-", dataKey: "gsrpYtId", urlDataKey: "gsrpYtUrl", processedKey: "gsrpYtProcessed", btnClass: "gsrp-youtube-embed-btn", containerClass: "gsrp-youtube-embed-container", extractDetails: extractYouTubeId, buildEmbedUrl: (href, videoId) => { const start = extractYouTubeTimestamp(href); return `https://www.youtube-nocookie.com/embed/${videoId}?autoplay=1&mute=0&enablejsapi=1${start ? `&start=${start}` : ""}`; }, iframeAllow: "autoplay; clipboard-write; encrypted-media; picture-in-picture; web-share", get btnTitleOpen() { return L.embedYoutube || "Embed YouTube Video"; }, get btnTitleClose() { return L.closeYoutube || "Close Embed"; }, btnIconHtml: YT_ICON_HTML, onContainerOpen: (container) => attachYouTubeVolume(container) }); const installYouTubeEmbedDelegate = delegate$7.install; const processYouTubeEmbeds = delegate$7.process; let copyDelegateInstalled = false; function installCodeCopyDelegate() { if (copyDelegateInstalled) return; copyDelegateInstalled = true; document.addEventListener( "click", (e) => { if (!(e.target instanceof Element)) return; const btn = e.target.closest(".gsrp-code-copy-btn"); if (!btn || btn.classList.contains("gsrp-copied")) return; const pre = btn.closest("pre"); if (!pre) return; e.preventDefault(); e.stopPropagation(); let textToCopy; const codeEl = pre.querySelector("code"); if (codeEl) { textToCopy = codeEl.textContent || ""; } else { const clone2 = pre.cloneNode(true); const btnInClone = clone2.querySelector(".gsrp-code-copy-btn"); if (btnInClone) btnInClone.remove(); textToCopy = clone2.textContent || ""; } navigator.clipboard.writeText(textToCopy).then(() => { btn.classList.add("gsrp-copied"); btn.title = L.copiedCodeSuccess || "Copied!"; btn.setAttribute("aria-label", L.copiedCodeSuccess || "Copied!"); setTimeout(() => { btn.classList.remove("gsrp-copied"); btn.title = L.copyCodeTitle || "Copy Code"; btn.setAttribute("aria-label", L.copyCodeTitle || "Copy Code"); }, 2e3); }).catch((err) => { console.error("[GSRP] Clipboard write failed:", err); }); }, true ); } function processCodeBlocks(scope) { if (!scope) return; const preBlocks = scope.querySelectorAll("pre"); preBlocks.forEach((pre) => { if (pre.querySelector(".gsrp-code-copy-btn") || pre.closest(".gsrp-more-load")) return; const codeEl = pre.querySelector("code"); if (codeEl) { const rawCode = codeEl.textContent || ""; codeEl.innerHTML = highlightCode(rawCode); } if (getComputedStyle(pre).position === "static") { pre.style.position = "relative"; } const btn = document.createElement("button"); btn.type = "button"; btn.className = "gsrp-code-copy-btn"; btn.title = L.copyCodeTitle || "Copy Code"; btn.setAttribute("aria-label", L.copyCodeTitle || "Copy Code"); btn.innerHTML = ` <svg class="gsrp-copy-icon" viewBox="0 0 24 24" width="13" height="13" stroke="currentColor" stroke-width="2.2" fill="none" stroke-linecap="round" stroke-linejoin="round"> <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect> <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path> </svg> <svg class="gsrp-copied-icon" viewBox="0 0 24 24" width="13" height="13" stroke="currentColor" stroke-width="2.5" fill="none" stroke-linecap="round" stroke-linejoin="round"> <polyline points="20 6 9 17 4 12"></polyline> </svg> `; pre.appendChild(btn); }); } function highlightCode(codeText) { if (typeof codeText !== "string" || !codeText) return ""; const escaped = codeText.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">"); const tokenRegex = /(\/\*[\s\S]*?\*\/|\/\/.*|#.*)|("(?:\\.|[^"\n\\])*"|'(?:\\.|[^'\n\\])*'|`(?:\\.|[^`\\])*`)|(\b(?:const|let|var|function|return|if|else|for|while|do|switch|case|break|continue|import|export|class|new|this|typeof|instanceof|extends|try|catch|finally|throw|async|await|def|elif|except|with|pass|lambda|fn|pub|mut|struct|impl|match|use|mod|package|interface|type|nil|true|false|null|undefined)\b)|(\b\d+(?:\.\d+)?\b)/g; return escaped.replace(tokenRegex, (match, comment, string, keyword, number) => { if (comment) return `<span class="gsrp-code-comment">${comment}</span>`; if (string) return `<span class="gsrp-code-string">${string}</span>`; if (keyword) return `<span class="gsrp-code-keyword">${keyword}</span>`; if (number) return `<span class="gsrp-code-number">${number}</span>`; return match; }); } function installXHeightAutoResize() { window.addEventListener("message", (event) => { if (event.origin !== "https://platform.twitter.com") return; if (event.data && event.data["twttr.embed"] && event.data["twttr.embed"].params) { const params = event.data["twttr.embed"].params; const height = parseInt(params[0]?.height, 10); if (height > 0) { const iframe = Array.from(document.querySelectorAll("iframe")).find( (f) => f.contentWindow === event.source ); if (iframe) { iframe.style.height = `${height + 4}px`; const container = iframe.closest( ".gsrp-x-embed-container" ); if (container) { container.style.height = `${height}px`; } } } } }); } const X_ICON_HTML = ` <svg viewBox="0 0 300 271" class="gsrp-embed-btn-logo"><path fill="currentColor" d="m236 0h46l-101 115 118 156h-92.6l-72.5-94.8-83 94.8h-46l107-123-113-148h94.9l65.5 86.6zm-16.1 244h25.5l-165-218h-27.4z"/></svg> <svg viewBox="0 0 24 24" class="gsrp-embed-btn-chevron"><path fill="currentColor" d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"/></svg> `; const delegate$6 = createEmbedDelegate({ idPrefix: "gsrp-x-", dataKey: "gsrpXId", urlDataKey: "gsrpXUrl", processedKey: "gsrpXProcessed", btnClass: "gsrp-x-embed-btn", containerClass: "gsrp-x-embed-container", extractDetails: extractXId, buildEmbedUrl: (_href, postId) => { const isDark = document.documentElement.classList.contains("gsrp-is-dark"); return `https://platform.twitter.com/embed/Tweet.html?id=${postId}&theme=${isDark ? "dark" : "light"}&dnt=true`; }, iframeAllow: "autoplay; clipboard-write; encrypted-media; picture-in-picture; web-share", get btnTitleOpen() { return L.embedX || "Embed X Post"; }, get btnTitleClose() { return L.closeX || "Close Embed"; }, btnIconHtml: X_ICON_HTML, extraInstall: installXHeightAutoResize }); const installXEmbedDelegate = delegate$6.install; const processXEmbeds = delegate$6.process; function fetchJsonCrossDomain(url) { return new Promise((resolve, reject) => { if (typeof GM_xmlhttpRequest !== "undefined") { GM_xmlhttpRequest({ method: "GET", url, timeout: 8e3, onload: function(response) { if (response.status === 200) { try { resolve(JSON.parse(response.responseText)); } catch { reject(new Error("JSON parse failure")); } } else { reject(new Error("HTTP error " + response.status)); } }, onerror: function() { reject(new Error("Network error")); }, ontimeout: function() { reject(new Error("Timeout")); } }); } else { fetch(url).then((res) => { if (!res.ok) throw new Error("HTTP error " + res.status); return res.json(); }).then(resolve).catch(reject); } }); } function installBlueskyHeightAutoResize() { window.addEventListener("message", (event) => { if (event.origin !== "https://embed.bsky.app") return; const id = event.data?.id; if (!id) return; const iframe = document.querySelector( `iframe[data-bluesky-id="${id}"]` ); if (!iframe) return; const height = event.data?.height; if (!height) return; iframe.style.height = `${height + 4}px`; const container = iframe.closest(".gsrp-bsky-embed-container"); if (container) container.style.height = `${height}px`; }); } const BLUESKY_ICON_HTML = ` <svg viewBox="0 0 600 530" class="gsrp-embed-btn-logo"><path fill="currentColor" d="m135.72 44.03c66.496 49.921 138.02 151.14 164.28 205.46 26.262-54.316 97.782-155.54 164.28-205.46 47.98-36.021 125.72-63.892 125.72 24.795 0 17.712-10.155 148.79-16.111 170.07-20.703 73.984-96.144 92.854-163.25 81.433 117.3 19.964 147.14 86.092 82.697 152.22-122.39 125.59-175.91-31.511-189.63-71.766-2.514-7.3797-3.6904-10.832-3.7077-7.8964-0.0174-2.9357-1.1937 0.51669-3.7077 7.8964-13.714 40.255-67.233 197.36-189.63 71.766-64.444-66.128-34.605-132.26 82.697-152.22-67.108 11.421-142.55-7.4491-163.25-81.433-5.9562-21.282-16.111-152.36-16.111-170.07 0-88.687 77.742-60.816 125.72-24.795z"/></svg> <svg viewBox="0 0 24 24" class="gsrp-embed-btn-chevron"><path fill="currentColor" d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"/></svg> `; async function buildBlueskyContent({ container, url }) { const loading = document.createElement("div"); loading.className = "gsrp-embed-loading"; loading.textContent = "..."; container.appendChild(loading); try { const oembedUrl = `https://embed.bsky.app/oembed?url=${encodeURIComponent(url)}`; const data = await fetchJsonCrossDomain(oembedUrl); if (!data || !data.html) throw new Error("Invalid oembed JSON response"); const parser = new DOMParser(); const doc = parser.parseFromString(data.html, "text/html"); const blockquote = doc.querySelector("blockquote"); if (!blockquote) throw new Error("No blockquote in html"); const aturi = blockquote.getAttribute("data-bluesky-uri"); if (!aturi || !aturi.startsWith("at://")) throw new Error("Invalid aturi in blockquote"); const isDark = document.documentElement.classList.contains("gsrp-is-dark"); const frameId = String(Math.random()).slice(2); const params = new URLSearchParams(); params.set("id", frameId); params.set("colorMode", isDark ? "dark" : "light"); const iframe = document.createElement("iframe"); iframe.setAttribute("data-bluesky-id", frameId); iframe.src = `https://embed.bsky.app/embed/${aturi.slice(5)}?${params.toString()}`; iframe.width = "100%"; iframe.style.border = "none"; iframe.style.display = "block"; iframe.style.flexGrow = "1"; iframe.style.height = "150px"; iframe.scrolling = "no"; iframe.frameBorder = "0"; iframe.allowFullscreen = true; iframe.setAttribute("allowtransparency", "true"); iframe.setAttribute("loading", "lazy"); iframe.setAttribute( "sandbox", "allow-scripts allow-same-origin allow-presentation allow-popups allow-popups-to-escape-sandbox" ); loading.remove(); container.appendChild(iframe); } catch (err) { console.error("[GSRP] Bluesky oembed failure:", err); if (loading.isConnected) loading.textContent = "Failed to load Bluesky post."; } } const delegate$5 = createEmbedDelegate({ idPrefix: "gsrp-bsky-", dataKey: "gsrpBskyId", urlDataKey: "gsrpBskyUrl", processedKey: "gsrpBskyProcessed", btnClass: "gsrp-bsky-embed-btn", containerClass: "gsrp-bsky-embed-container", extractDetails: (href) => isBlueskyUrl(href) ? href : null, get btnTitleOpen() { return L.embedBluesky || "Embed Bluesky Post"; }, get btnTitleClose() { return L.closeBluesky || "Close Embed"; }, btnIconHtml: BLUESKY_ICON_HTML, contentBuilder: buildBlueskyContent, extraInstall: installBlueskyHeightAutoResize }); const installBlueskyEmbedDelegate = delegate$5.install; const processBlueskyEmbeds = delegate$5.process; const MORE_LOAD_TIMEOUT_MS = 8e3; let nextRequestId = 0; const ERROR_INFO_KEYS$2 = { RATE_LIMIT: { textKey: "rateLimitText", titleKey: "rateLimitTitle" }, HTTP_ERR: { textKey: "errHttpText", titleKey: "errHttpTitle" }, NET_ERR: { textKey: "errNetText", titleKey: "errNetTitle" }, PARSE_ERR: { textKey: "errParseText", titleKey: "errParseTitle" }, TIMEOUT: { textKey: "errTimeoutText", titleKey: "errTimeoutTitle" } }; let moreLoadInstalled = false; function installMoreLoadDelegate() { if (moreLoadInstalled) return; moreLoadInstalled = true; document.addEventListener( "click", (e) => { if (!(e.target instanceof Element)) return; const btn = e.target.closest(".gsrp-more-load"); if (!btn || btn.disabled) return; const tooltip = btn.closest(".gsrp-preview-tooltip"); if (!tooltip) return; e.preventDefault(); e.stopPropagation(); const permalink = btn.dataset.fetchUrl || ""; const depth = parseInt(btn.dataset.depth || "", 10) || 0; const sortSelect = tooltip.querySelector( ".gsrp-inline-sort-select" ); const activeSort = sortSelect ? sortSelect.value : null; const requestId = ++nextRequestId; btn.dataset.gsrpMoreLoadRequestId = String(requestId); btn.disabled = true; btn.removeAttribute("title"); btn.innerHTML = renderSpinner(L.loading || "Loading..."); let timedOut = false; let timeoutHandle = setTimeout(() => { timeoutHandle = null; if (!isCurrentRequest(btn, requestId)) return; timedOut = true; showMoreLoadError(btn, "TIMEOUT"); }, MORE_LOAD_TIMEOUT_MS); const clearPendingTimeout = () => { if (timeoutHandle !== null) { clearTimeout(timeoutHandle); timeoutHandle = null; } }; fetchMoreComments( permalink, depth, activeSort, (newSubtree) => { if (timedOut) return; if (!isCurrentRequest(btn, requestId)) return; clearPendingTimeout(); const existingIds = new Set(); const existingNodes = tooltip.querySelectorAll("[data-comment-id]"); existingNodes.forEach((el) => { const id = el.dataset.commentId; if (id) existingIds.add(id); }); const filteredSubtree = newSubtree.filter( (c) => c.type === "more" ? true : !existingIds.has(c.id) ); const wrapper = btn.closest(".gsrp-comment-more"); if (!wrapper) return; if (filteredSubtree.length === 0) { wrapper.remove(); return; } const html2 = renderCommentsList(filteredSubtree, depth, false); wrapper.outerHTML = html2; const embedScope = tooltip.querySelector(".gsrp-comments-list-wrapper") || tooltip; processYouTubeEmbeds(embedScope); processCodeBlocks(embedScope); processXEmbeds(embedScope); processBlueskyEmbeds(embedScope); const countNew = countCommentNodes(filteredSubtree); if (countNew > 0) { const countEl = tooltip.querySelector(".gsrp-comments-count"); if (countEl) { const match = (countEl.textContent || "").match( /\((\d+)\s*\/\s*(.+?)\)/ ); if (match) { const newCount = parseInt(match[1], 10) + countNew; countEl.textContent = `(${newCount} / ${match[2]})`; } } } const listWrapper = tooltip.querySelector(".gsrp-comments-list-wrapper"); if (listWrapper && typeof listWrapper.gsrpReapplyFilter === "function") { listWrapper.gsrpReapplyFilter(); } if (listWrapper) { listWrapper.dispatchEvent( new CustomEvent("gsrp-comments-updated", { bubbles: true }) ); } const translateBtn = tooltip.querySelector(".gsrp-translate-btn"); if (translateBtn && translateBtn.dataset.gsrpCurrentDisplay === "translated") { if (typeof translateBtn.gsrpTriggerAutoTranslate === "function") { translateBtn.gsrpTriggerAutoTranslate(); } else { translateBtn.click(); } } }, (err) => { if (timedOut) return; if (!isCurrentRequest(btn, requestId)) return; clearPendingTimeout(); showMoreLoadError(btn, err); } ); }, true ); } function isCurrentRequest(btn, requestId) { if (!btn.isConnected) return false; return parseInt(btn.dataset.gsrpMoreLoadRequestId || "0", 10) === requestId; } function showMoreLoadError(btn, err) { const info = ERROR_INFO_KEYS$2[err] || ERROR_INFO_KEYS$2.NET_ERR; const text2 = L[info.textKey] || L.errNetText || "Network Error"; const title = L[info.titleKey] || L.errNetTitle || "Failed to reach Reddit"; const retryLabel = L.retryBtn || "Retry"; const retryTitle = L.retryTitle || "Click to retry once"; btn.disabled = false; btn.innerHTML = `${text2} · ${retryLabel}`; btn.title = `${title} — ${retryTitle}`; } let focusTrapInstalled = false; function installFocusTrapDelegate() { if (focusTrapInstalled) return; focusTrapInstalled = true; document.addEventListener("keydown", (e) => { if (e.key !== "Tab") return; let trapContainer = null; const sidePanel = document.getElementById("gsrp-global-side-panel"); if (sidePanel && sidePanel.classList.contains("gsrp-pinned")) { trapContainer = sidePanel; } else { const pinnedTrigger = document.querySelector(".gsrp-preview-trigger.gsrp-pinned"); if (pinnedTrigger) { trapContainer = pinnedTrigger.querySelector( ".gsrp-preview-tooltip" ); } } if (!trapContainer) return; if (document.getElementById("gsrp-settings-modal")) return; const focusableSelectors = [ "a[href]", "area[href]", "input:not([disabled])", "select:not([disabled])", "textarea:not([disabled])", "button:not([disabled])", "iframe", "object", "embed", '[tabindex="0"]', "[contenteditable]" ].join(","); const allFocusables = Array.from( trapContainer.querySelectorAll(focusableSelectors) ); const visibleFocusables = allFocusables.filter((el) => { const isVisible = el.offsetWidth > 0 || el.offsetHeight > 0 || el.getClientRects().length > 0; if (!isVisible) return false; const style = window.getComputedStyle(el); return style.display !== "none" && style.visibility !== "hidden"; }); if (visibleFocusables.length === 0) return; const firstElement = visibleFocusables[0]; const lastElement = visibleFocusables[visibleFocusables.length - 1]; if (e.shiftKey) { if (document.activeElement === firstElement || !trapContainer.contains(document.activeElement)) { e.preventDefault(); lastElement.focus(); } } else { if (document.activeElement === lastElement || !trapContainer.contains(document.activeElement)) { e.preventDefault(); firstElement.focus(); } } }); } function installMastodonHeightAutoResize() { window.addEventListener("message", (event) => { let data = event.data; if (typeof data === "string") { try { data = JSON.parse(data); } catch { } } if (typeof data !== "object" || !data || data.type !== "setHeight") return; const height = parseInt(data.height, 10); if (!(height > 0)) return; const iframe = Array.from(document.querySelectorAll("iframe")).find( (f) => f.contentWindow === event.source ); if (!iframe) return; iframe.style.height = `${height + 30}px`; const container = iframe.closest(".gsrp-mastodon-embed-container"); if (container) { container.style.height = `${height + 24}px`; } }); } const MASTODON_ICON_HTML = ` <svg viewBox="0 0 79 75" class="gsrp-embed-btn-logo"><path d="M63 45.3v-20c0-4.1-1-7.3-3.2-9.7-2.1-2.4-5-3.7-8.5-3.7-4.1 0-7.2 1.6-9.3 4.7l-2 3.3-2-3.3c-2-3.1-5.1-4.7-9.2-4.7-3.5 0-6.4 1.3-8.6 3.7-2.1 2.4-3.1 5.6-3.1 9.7v20h8V25.9c0-4.1 1.7-6.2 5.2-6.2 3.8 0 5.8 2.5 5.8 7.4V37.7H44V27.1c0-4.9 1.9-7.4 5.8-7.4 3.5 0 5.2 2.1 5.2 6.2V45.3h8ZM74.7 16.6c.6 6 .1 15.7.1 17.3 0 .5-.1 4.8-.1 5.3-.7 11.5-8 16-15.6 17.5-.1 0-.2 0-.3 0-4.9 1-10 1.2-14.9 1.4-1.2 0-2.4 0-3.6 0-4.8 0-9.7-.6-14.4-1.7-.1 0-.1 0-.1 0s-.1 0-.1 0 0 .1 0 .1 0 0 0 0c.1 1.6.4 3.1 1 4.5.6 1.7 2.9 5.7 11.4 5.7 5 0 9.9-.6 14.8-1.7 0 0 0 0 0 0 .1 0 .1 0 .1 0 0 .1 0 .1 0 .1.1 0 .1 0 .1.1v5.6s0 .1-.1.1c0 0 0 0 0 .1-1.6 1.1-3.7 1.7-5.6 2.3-.8.3-1.6.5-2.4.7-7.5 1.7-15.4 1.3-22.7-1.2-6.8-2.4-13.8-8.2-15.5-15.2-.9-3.8-1.6-7.6-1.9-11.5-.6-5.8-.6-11.7-.8-17.5C3.9 24.5 4 20 4.9 16 6.7 7.9 14.1 2.2 22.3 1c1.4-.2 4.1-1 16.5-1h.1C51.4 0 56.7.8 58.1 1c8.4 1.2 15.5 7.5 16.6 15.6Z" fill="currentColor"/></svg> <svg viewBox="0 0 24 24" class="gsrp-embed-btn-chevron"><path fill="currentColor" d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"/></svg> `; const delegate$4 = createEmbedDelegate({ idPrefix: "gsrp-mastodon-", dataKey: "gsrpMastodonId", urlDataKey: "gsrpMastodonUrl", processedKey: "gsrpMastodonProcessed", btnClass: "gsrp-mastodon-embed-btn", containerClass: "gsrp-mastodon-embed-container", extractDetails: extractMastodonEmbedInfo, buildEmbedUrl: (_href, info) => info.embedUrl, iframeAllow: "", get btnTitleOpen() { return L.embedMastodon || "Embed Mastodon Post"; }, get btnTitleClose() { return L.closeMastodon || "Close Embed"; }, btnIconHtml: MASTODON_ICON_HTML, extraInstall: installMastodonHeightAutoResize }); const installMastodonEmbedDelegate = delegate$4.install; const processMastodonEmbeds = delegate$4.process; const VIMEO_ICON_HTML = ` <svg viewBox="0 -3.5 48 48" class="gsrp-embed-btn-logo"> <g transform="translate(-300.000000, -365.000000)" fill="currentColor"> <path d="M347.975851,374.479329 C347.767002,379.100014 344.498808,385.41644 338.193846,393.431401 C331.668746,401.81233 326.13991,406 321.621448,406 C318.827396,406 316.459507,403.452198 314.526249,398.339832 C313.230825,393.649305 311.943867,388.958779 310.651265,384.282221 C309.211905,379.167061 307.670943,376.610878 306.022735,376.610878 C305.664306,376.610878 304.414038,377.356781 302.25782,378.85138 L300,375.971134 C302.365066,373.917807 304.696265,371.856098 306.996419,369.799977 C310.146078,367.101318 312.513967,365.684941 314.094441,365.536878 C317.819844,365.179292 320.117175,367.701951 320.983614,373.096476 C321.906498,378.921221 322.555621,382.541782 322.91405,383.960952 C323.992159,388.788367 325.17187,391.196487 326.464472,391.196487 C327.466379,391.196487 328.973474,389.637634 330.982934,386.517135 C332.992393,383.391049 334.062036,381.016453 334.208794,379.379378 C334.496666,376.680719 333.421379,375.339771 330.982934,375.339771 C329.834268,375.339771 328.648912,375.580024 327.432512,376.08288 C329.803223,368.486965 334.318863,364.793769 340.99072,365.00888 C345.932524,365.145768 348.266545,368.308172 347.975851,374.479329" id="Vimeo"></path> </g> </svg> <svg viewBox="0 0 24 24" class="gsrp-embed-btn-chevron"><path fill="currentColor" d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"/></svg> `; const delegate$3 = createEmbedDelegate({ idPrefix: "gsrp-vimeo-", dataKey: "gsrpVimeoId", urlDataKey: "gsrpVimeoUrl", processedKey: "gsrpVimeoProcessed", btnClass: "gsrp-vimeo-embed-btn", containerClass: "gsrp-vimeo-embed-container", extractDetails: extractVimeoIdAndHash, buildEmbedUrl: (_href, details) => { const { videoId, hash } = details; return `https://player.vimeo.com/video/${videoId}?autoplay=1&muted=0${hash ? `&h=${hash}` : ""}`; }, iframeAllow: "autoplay; fullscreen; picture-in-picture", get btnTitleOpen() { return L.embedVimeo || "Embed Vimeo Video"; }, get btnTitleClose() { return L.closeVimeo || "Close Embed"; }, btnIconHtml: VIMEO_ICON_HTML }); const installVimeoEmbedDelegate = delegate$3.install; const processVimeoEmbeds = delegate$3.process; const asPermalink = (s) => s; const asImgurHash = (s) => s; const REDDIT_POST_RE = /^https?:\/\/(?:[a-z0-9-]+\.)?reddit\.com\/(?:r\/[A-Za-z0-9_]+\/)?comments\/([A-Za-z0-9]+)/i; const REDDIT_SHORT_RE = /^https?:\/\/redd\.it\/([A-Za-z0-9]+)/i; const REDDIT_SHARE_RE = /^https?:\/\/(?:[a-z0-9-]+\.)?reddit\.com\/r\/[A-Za-z0-9_]+\/s\/([A-Za-z0-9]+)/i; const COLLAPSE_THRESHOLD_PX$1 = 280; function extractRedditPostPermalink(url) { if (typeof url !== "string" || !url) return null; if (url.includes("/r/") && !url.includes("/comments/") && !url.includes("/s/")) return null; if (url.includes("/user/") || url.includes("/u/")) return null; if (REDDIT_POST_RE.test(url) || REDDIT_SHORT_RE.test(url) || REDDIT_SHARE_RE.test(url)) { return asPermalink(url.split("?")[0]); } return null; } function renderCommentBody(bodyHtml, bodyText) { if (bodyHtml) { let html2 = processRedditHtml(bodyHtml); if (Config.embedMedia) html2 = transformInlineMedia(html2); return html2; } return `<p>${bodyText || ""}</p>`; } const REDDIT_SVG = ` <svg viewBox="0 0 32 32" class="gsrp-embed-btn-logo"> <path d="M16 2C8.27812 2 2 8.27812 2 16C2 23.7219 8.27812 30 16 30C23.7219 30 30 23.7219 30 16C30 8.27812 23.7219 2 16 2Z" fill="currentColor"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M20.0193 8.90951C20.0066 8.98984 20 9.07226 20 9.15626C20 10.0043 20.6716 10.6918 21.5 10.6918C22.3284 10.6918 23 10.0043 23 9.15626C23 8.30819 22.3284 7.6207 21.5 7.6207C21.1309 7.6207 20.7929 7.7572 20.5315 7.98359L16.6362 7L15.2283 12.7651C13.3554 12.8913 11.671 13.4719 10.4003 14.3485C10.0395 13.9863 9.54524 13.7629 9 13.7629C7.89543 13.7629 7 14.6796 7 15.8103C7 16.5973 7.43366 17.2805 8.06967 17.6232C8.02372 17.8674 8 18.1166 8 18.3696C8 21.4792 11.5817 24 16 24C20.4183 24 24 21.4792 24 18.3696C24 18.1166 23.9763 17.8674 23.9303 17.6232C24.5663 17.2805 25 16.5973 25 15.8103C25 14.6796 24.1046 13.7629 23 13.7629C22.4548 13.7629 21.9605 13.9863 21.5997 14.3485C20.2153 13.3935 18.3399 12.7897 16.2647 12.7423L17.3638 8.24143L20.0193 8.90951ZM12.5 18.8815C13.3284 18.8815 14 18.194 14 17.3459C14 16.4978 13.3284 15.8103 12.5 15.8103C11.6716 15.8103 11 16.4978 11 17.3459C11 18.194 11.6716 18.8815 12.5 18.8815ZM19.5 18.8815C20.3284 18.8815 21 18.194 21 17.3459C21 16.4978 20.3284 15.8103 19.5 15.8103C18.6716 15.8103 18 16.4978 18 17.3459C18 18.194 18.6716 18.8815 19.5 18.8815ZM12.7773 20.503C12.5476 20.3462 12.2372 20.4097 12.084 20.6449C11.9308 20.8802 11.9929 21.198 12.2226 21.3548C13.3107 22.0973 14.6554 22.4686 16 22.4686C17.3446 22.4686 18.6893 22.0973 19.7773 21.3548C20.0071 21.198 20.0692 20.8802 19.916 20.6449C19.7628 20.4097 19.4524 20.3462 19.2226 20.503C18.3025 21.1309 17.1513 21.4449 16 21.4449C15.3173 21.4449 14.6345 21.3345 14 21.1137C13.5646 20.9621 13.1518 20.7585 12.7773 20.503Z" fill="var(--gsrp-reddit-logo-inner, #ffffff)"/> </svg> <svg viewBox="0 0 24 24" class="gsrp-embed-btn-chevron"><path fill="currentColor" d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"/></svg> `; const SKELETON_HTML = ` <div class="gsrp-skeleton-header"> <div class="gsrp-skeleton-sub"></div> <div class="gsrp-skeleton-author"></div> </div> <div class="gsrp-skeleton-title"></div> <div class="gsrp-skeleton-body"> <div class="gsrp-skeleton-line"></div> <div class="gsrp-skeleton-line"></div> <div class="gsrp-skeleton-line" style="width: 60%"></div> </div> `; const ARROW_RIGHT_SVG = `<svg viewBox="0 0 24 24" width="14" height="14" style="display:inline-block; vertical-align:middle; margin-left:4px;"><path fill="currentColor" d="M5 13h11.86l-5.43 5.43 1.42 1.42L21.14 12l-8.29-8.29-1.42 1.42L16.86 11H5v2z"/></svg>`; const CHEVRON_DOWN_SVG = `<svg viewBox="0 0 24 24" width="14" height="14" style="display:inline-block; vertical-align:middle; margin-left:4px;"><path fill="currentColor" d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"/></svg>`; function renderRedditEmbed(container, data) { const subPart = data.subreddit ? `r/${data.subreddit}` : ""; const authorPart = data.author === "[deleted]" ? L.authorDeleted || "deleted" : `u/${data.author}`; const ratioPart = data.ratio ? `<span class="gsrp-reddit-embed-ratio">(${data.ratio}%)</span>` : ""; const datePart = data.date ? `<span class="gsrp-reddit-sep">·</span><span class="gsrp-reddit-embed-date">${data.date}</span>` : ""; const commentCountPart = data.comments ? `<span class="gsrp-reddit-sep">·</span><span class="gsrp-reddit-embed-comments-count">${data.comments} ${L.commentsTitle}</span>` : ""; const headerHtml = ` <div class="gsrp-reddit-embed-header"> <a href="https://www.reddit.com${data.permalink}" target="_blank" rel="noopener noreferrer" class="gsrp-reddit-embed-logo-link" title="Open on Reddit"> <svg viewBox="0 0 32 32"> <path d="M16 2C8.27812 2 2 8.27812 2 16C2 23.7219 8.27812 30 16 30C23.7219 30 30 23.7219 30 16C30 8.27812 23.7219 2 16 2Z" fill="currentColor"/> <path fill-rule="evenodd" clip-rule="evenodd" d="M20.0193 8.90951C20.0066 8.98984 20 9.07226 20 9.15626C20 10.0043 20.6716 10.6918 21.5 10.6918C22.3284 10.6918 23 10.0043 23 9.15626C23 8.30819 22.3284 7.6207 21.5 7.6207C21.1309 7.6207 20.7929 7.7572 20.5315 7.98359L16.6362 7L15.2283 12.7651C13.3554 12.8913 11.671 13.4719 10.4003 14.3485C10.0395 13.9863 9.54524 13.7629 9 13.7629C7.89543 13.7629 7 14.6796 7 15.8103C7 16.5973 7.43366 17.2805 8.06967 17.6232C8.02372 17.8674 8 18.1166 8 18.3696C8 21.4792 11.5817 24 16 24C20.4183 24 24 21.4792 24 18.3696C24 18.1166 23.9763 17.8674 23.9303 17.6232C24.5663 17.2805 25 16.5973 25 15.8103C25 14.6796 24.1046 13.7629 23 13.7629C22.4548 13.7629 21.9605 13.9863 21.5997 14.3485C20.2153 13.3935 18.3399 12.7897 16.2647 12.7423L17.3638 8.24143L20.0193 8.90951ZM12.5 18.8815C13.3284 18.8815 14 18.194 14 17.3459C14 16.4978 13.3284 15.8103 12.5 15.8103C11.6716 15.8103 11 16.4978 11 17.3459C11 18.194 11.6716 18.8815 12.5 18.8815ZM19.5 18.8815C20.3284 18.8815 21 18.194 21 17.3459C21 16.4978 20.3284 15.8103 19.5 15.8103C18.6716 15.8103 18 16.4978 18 17.3459C18 18.194 18.6716 18.8815 19.5 18.8815ZM12.7773 20.503C12.5476 20.3462 12.2372 20.4097 12.084 20.6449C11.9308 20.8802 11.9929 21.198 12.2226 21.3548C13.3107 22.0973 14.6554 22.4686 16 22.4686C17.3446 22.4686 18.6893 22.0973 19.7773 21.3548C20.0071 21.198 20.0692 20.8802 19.916 20.6449C19.7628 20.4097 19.4524 20.3462 19.2226 20.503C18.3025 21.1309 17.1513 21.4449 16 21.4449C15.3173 21.4449 14.6345 21.3345 14 21.1137C13.5646 20.9621 13.1518 20.7585 12.7773 20.503Z" fill="#ffffff"/> </svg> </a> <span class="gsrp-reddit-embed-sub">${subPart}</span> <span class="gsrp-reddit-sep">·</span> <span class="gsrp-reddit-embed-author">${authorPart}</span> <span class="gsrp-reddit-sep">·</span> <span class="gsrp-reddit-embed-score">⬆️ ${data.score || "0"}</span> ${ratioPart} ${datePart} ${commentCountPart} </div> `; const titleHtml = data.title ? `<div class="gsrp-reddit-embed-title">${data.title}</div>` : ""; const bodyContentHtml = renderPreviewContent(data); const bodyHtml = bodyContentHtml ? `<div class="gsrp-reddit-embed-body">${bodyContentHtml}</div>` : ""; let commentsHtml = ""; if (data.topComments && data.topComments.length > 0) { const limitComments = data.topComments.filter((x) => x.type !== "more").slice(0, 2); const commentItems = limitComments.map( (c) => ` <div class="gsrp-reddit-embed-comment-item"> <div class="gsrp-reddit-embed-comment-meta"> <span class="gsrp-reddit-embed-comment-author">${c.author === "[deleted]" ? L.authorDeleted || "deleted" : `u/${c.author}`}</span> <span class="gsrp-reddit-embed-comment-score">⬆️ ${c.score}</span> ${c.date ? `<span class="gsrp-reddit-embed-comment-date">${c.date}</span>` : ""} </div> <div class="gsrp-reddit-embed-comment-body"> ${renderCommentBody(c.bodyHtml, c.body)} </div> </div> ` ).join(""); const totalComments = data.comments; const footerText = totalComments ? (L.viewAllComments || "View all {count} comments on Reddit →").replace( "{count}", String(totalComments) ) : L.viewAllCommentsDefault || "View all comments on Reddit →"; const footerHtml = ` <a href="https://www.reddit.com${data.permalink}" target="_blank" rel="noopener noreferrer" class="gsrp-reddit-embed-comments-footer" title="${footerText}"> <span>${footerText}</span> ${ARROW_RIGHT_SVG} </a> `; commentsHtml = ` <div class="gsrp-reddit-embed-comments-section"> <div class="gsrp-reddit-embed-comments-title">💬 ${L.topCommentsTitle || "Top Comments"}</div> <div class="gsrp-reddit-embed-comments-list"> ${commentItems} ${footerHtml} </div> </div> `; } container.innerHTML = headerHtml + `<div class="gsrp-reddit-embed-content">${titleHtml}${bodyHtml}${commentsHtml}</div>`; const contentWrap = container.querySelector(".gsrp-reddit-embed-content"); if (contentWrap && contentWrap.scrollHeight > COLLAPSE_THRESHOLD_PX$1) { contentWrap.classList.add("gsrp-is-collapsed"); const overlay = document.createElement("div"); overlay.className = "gsrp-reddit-embed-fade-overlay"; const expandBtn = document.createElement("button"); expandBtn.className = "gsrp-reddit-embed-expand-btn"; expandBtn.innerHTML = `<span>${L.expandRedditPost || "Expand full post"}</span>${CHEVRON_DOWN_SVG}`; overlay.appendChild(expandBtn); contentWrap.appendChild(overlay); expandBtn.addEventListener("click", (evt) => { evt.preventDefault(); evt.stopPropagation(); contentWrap.classList.remove("gsrp-is-collapsed"); overlay.remove(); }); } } function renderRedditError(container, err) { container.classList.add("gsrp-embed-error"); let errMsg = "Failed to load Reddit post."; if (err === "RATE_LIMIT") errMsg = "Too many requests. Please wait a moment."; else if (err === "TIMEOUT") errMsg = "Request timed out."; container.innerHTML = `<p style="margin: 0; color: #ff4d4d; font-size: 0.9em; display: flex; align-items: center; gap: 6px;">⚠️ ${errMsg}</p>`; } function buildRedditContent({ container, details }) { container.innerHTML = SKELETON_HTML; const permalink = details; fetchRedditData( permalink, null, (data) => { if (!container.isConnected) return; renderRedditEmbed(container, data); }, (err) => { if (!container.isConnected) return; renderRedditError(container, err); } ); } const delegate$2 = createEmbedDelegate({ idPrefix: "gsrp-reddit-", dataKey: "gsrpRedditId", urlDataKey: "gsrpRedditUrl", processedKey: "gsrpRedditProcessed", btnClass: "gsrp-reddit-embed-btn", containerClass: "gsrp-reddit-embed-container", extractDetails: extractRedditPostPermalink, linkFilter: (link) => !(link.className || "").includes("gsrp-"), get btnTitleOpen() { return L.embedReddit || "Embed Reddit Post"; }, get btnTitleClose() { return L.closeReddit || "Close Reddit Post"; }, btnIconHtml: REDDIT_SVG, contentBuilder: buildRedditContent, tooltipSelector: ".gsrp-preview-tooltip, .gsrp-side-panel" }); const installRedditEmbedDelegate = delegate$2.install; const processRedditEmbeds = delegate$2.process; const DAILYMOTION_RE = /^https?:\/\/(?:[a-z0-9-]+\.)?dailymotion\.com\/video\/([A-Za-z0-9]+)/i; const DAILYMOTION_SHORT_RE = /^https?:\/\/dai\.ly\/([A-Za-z0-9]+)/i; function extractDailymotionId(url) { if (typeof url !== "string" || !url) return null; if (url.includes("/channels/") || url.includes("/user/")) return null; let m = url.match(DAILYMOTION_RE); if (m) return m[1]; m = url.match(DAILYMOTION_SHORT_RE); if (m) return m[1]; return null; } const DAILYMOTION_ICON_HTML = ` <svg viewBox="0 0 512 512" class="gsrp-embed-btn-logo"> <path d="M229.684 6H45.474c-7.237 0-13.158 5.921-13.158 13.158v78.947c0 3.29 1.316 6.58 3.947 9.21l78.948 78.948c2.631 2.632 5.92 3.948 9.21 3.948h105.263c36.185 0 65.79 29.605 65.79 65.79 0 36.183-29.605 65.789-65.79 65.789H84.947c-7.236 0-13.158 5.92-13.158 13.157v78.948c0 3.29 1.316 6.579 3.948 9.21l78.947 78.948c2.632 2.631 5.921 3.947 9.21 3.947h65.79c138.158 0 250-111.842 250-250 0-138.158-111.842-250-250-250zM58.632 50.737l52.631 52.631v41.448L58.632 92.184V50.737zM321.79 256c0-50.658-41.448-92.105-92.106-92.105H137.58v-52.632h92.105c79.606 0 144.737 65.132 144.737 144.737 0 79.605-65.131 144.737-144.737 144.737h-60.526l-52.632-52.632h113.158c50.658 0 92.106-41.447 92.106-92.105zM98.105 366.526l52.632 52.632v41.447l-52.632-52.631v-41.448zm131.58 113.158h-52.632v-52.631h52.631c94.08 0 171.053-76.974 171.053-171.053S323.763 84.947 229.684 84.947h-100L77.053 32.316h152.631C352.711 32.316 453.37 132.974 453.37 256S352.71 479.684 229.684 479.684z" fill="currentColor"/> </svg> <svg viewBox="0 0 24 24" class="gsrp-embed-btn-chevron"><path fill="currentColor" d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z"/></svg> `; const delegate$1 = createEmbedDelegate({ idPrefix: "gsrp-dm-", dataKey: "gsrpDailymotionId", urlDataKey: "gsrpDailymotionUrl", processedKey: "gsrpDailymotionProcessed", btnClass: "gsrp-dailymotion-embed-btn", containerClass: "gsrp-dailymotion-embed-container", extractDetails: extractDailymotionId, buildEmbedUrl: (_href, videoId) => `https://www.dailymotion.com/embed/video/${videoId}?autoplay=1&mute=0&quality=auto`, iframeAllow: "autoplay; fullscreen; picture-in-picture", get btnTitleOpen() { return L.embedDailymotion || "Embed Dailymotion Video"; }, get btnTitleClose() { return L.closeDailymotion || "Close Embed"; }, btnIconHtml: DAILYMOTION_ICON_HTML }); const installDailymotionEmbedDelegate = delegate$1.install; const processDailymotionEmbeds = delegate$1.process; function formatCommentNode(node, depth) { const prefix = ">".repeat(depth + 1) + " "; if (node.type === "more") { const countStr = node.count !== null ? `${node.count} ` : ""; const url = node.permalink.startsWith("http") ? node.permalink : `https://www.reddit.com${node.permalink}`; return `${prefix}*[${countStr}more replies on Reddit: [Link](${url})]* ${prefix} `; } let author = node.author; if (node.isAuthorDeleted) { author = "[deleted]"; } let meta = `**u/${author}**`; if (node.isOP) { meta += " (OP)"; } if (node.distinguished === "moderator") { meta += " [MOD]"; } else if (node.distinguished === "admin") { meta += " [ADMIN]"; } meta += ` (${node.score} pts) · *${node.date || "unknown"}*`; let body; if (node.isDeleted) { if (node.removalKind === "removed") { body = "*[Comment removed by moderator]*"; } else if (node.removalKind === "deleted") { body = "*[Comment deleted by author]*"; } else if (node.removalKind === "redacted") { body = "*[Comment redacted]*"; } else { body = "*[Comment deleted]*"; } } else { body = decodeHTMLEntities(node.body || ""); if (node.mediaMetadata) { for (const [mediaId, entry] of Object.entries(node.mediaMetadata)) { if (entry.status === "valid" && entry.s) { const imageUrl = entry.s.gif || entry.s.u; if (imageUrl) { const decodedUrl = decodeHTMLEntities(imageUrl); const escapedMediaId = mediaId.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); const placeholderRegex = new RegExp( `!\\[([^\\]]*)\\]\\(${escapedMediaId}\\)`, "g" ); let replaced = false; body = body.replace(placeholderRegex, (_match, altText) => { replaced = true; const label = altText || "image"; return ``; }); if (!replaced && !body.includes(decodedUrl)) { body += ` `; } } } } } } const formattedBody = body.split("\n").map((line) => prefix + line).join("\n"); let result = `${prefix}${meta} ${prefix} ${formattedBody} ${prefix} `; if (node.replies && node.replies.length > 0) { for (const reply of node.replies) { result += formatCommentNode(reply, depth + 1); } } return result; } function convertPostToMarkdown(post) { const postUrl = post.url || `https://www.reddit.com${post.permalink}`; const redditLink = `https://www.reddit.com${post.permalink}`; let md = `# ${post.title} `; md += `- **Subreddit**: r/${post.subreddit} `; md += `- **Author**: u/${post.author || "[deleted]"} `; md += `- **Score**: ${post.score} (${post.ratio}% Upvoted) `; md += `- **Date**: ${post.date || "unknown"} `; md += `- **Reddit Link**: [${redditLink}](${redditLink}) `; if (post.url && !post.isSelf) { md += `- **External URL**: [${postUrl}](${postUrl}) `; } md += "\n---\n\n"; if (post.selftext) { const decodedSelftext = decodeHTMLEntities(post.selftext); md += `${decodedSelftext} `; md += "---\n\n"; } if (post.isGallery && post.media && post.media.type === "gallery" && post.media.images && post.media.images.length > 0) { md += `🖼️ **Gallery**: `; for (const img of post.media.images) { const decodedUrl = decodeHTMLEntities(img.url); md += ` `; if (img.caption) { const decodedCaption = decodeHTMLEntities(img.caption); md += `*${decodedCaption}* `; } md += ` `; } md += "---\n\n"; } if (post.isVideo && post.media && post.media.type === "video") { const video = post.media; md += `🎥 **Video**: `; if (video.poster) { const decodedPoster = decodeHTMLEntities(video.poster); md += ` `; } const decodedUrl = decodeHTMLEntities(video.url); md += `- **Direct Link**: [Watch/Download](${decodedUrl}) `; if (video.hlsUrl) { const decodedHls = decodeHTMLEntities(video.hlsUrl); md += `- **HLS Stream**: [Stream Link](${decodedHls}) `; } if (video.duration) { md += `- **Duration**: ${video.duration}s `; } if (video.width && video.height) { md += `- **Resolution**: ${video.width}x${video.height} `; } md += `- **Audio**: ${video.hasAudio ? "Yes" : "No"} `; md += "\n---\n\n"; } if (!post.isGallery && !post.isVideo && post.media && post.media.type === "image") { const img = post.media; const decodedUrl = decodeHTMLEntities(img.url); md += `🖼️ **Image**: `; md += ` `; md += "---\n\n"; } if (post.crosspostParent) { const cp = post.crosspostParent; const cpLink = `https://www.reddit.com${cp.permalink}`; md += `### 🔁 Crossposted from r/${cp.subreddit} by u/${cp.author || "[deleted]"} `; md += `**Title**: ${cp.title} `; md += `**Score**: ${cp.score} | **Comments**: ${cp.comments} `; md += `**Original Reddit Link**: [${cpLink}](${cpLink}) `; if (cp.selftext) { const decodedCpSelftext = decodeHTMLEntities(cp.selftext); md += `${decodedCpSelftext} `; } md += "---\n\n"; } md += `## Comments (${post.displayedComments} displayed / ${post.comments} total) `; if (post.topComments && post.topComments.length > 0) { for (const comment of post.topComments) { md += formatCommentNode(comment, 0); } } else { md += "*No comments available.*\n"; } return md; } async function copyToClipboard(text2) { if (typeof navigator.clipboard?.writeText === "function") { try { await navigator.clipboard.writeText(text2); return; } catch (err) { console.warn( "[GSRP] navigator.clipboard.writeText failed, falling back to GM_setClipboard", err ); } } if (typeof GM_setClipboard === "function") { try { GM_setClipboard(text2, "text"); return; } catch (err) { console.error("[GSRP] GM_setClipboard failed", err); throw new Error("Failed to copy to clipboard via GM_setClipboard", { cause: err }); } } throw new Error("No clipboard API available"); } function getGeminiModelName() { const model = Config.summarizeGeminiModel; if (model === "custom") { return Config.summarizeGeminiCustomModel.trim() || "gemini-3.5-flash"; } return model || "gemini-3.5-flash"; } function getLanguageName(langCode) { const code = langCode.toLowerCase().split("-")[0]; const langMap = { en: "English", zh: "Traditional Chinese (zh-TW)", ja: "Japanese", es: "Spanish", fr: "French", de: "German", it: "Italian", ko: "Korean", ru: "Russian", pt: "Portuguese", vi: "Vietnamese" }; return langMap[code] || "English"; } function getGeminiSystemInstruction(options) { if (Config.summarizeGeminiPrompt.trim()) { return Config.summarizeGeminiPrompt.trim(); } const targetLangName = getLanguageName(options.targetLang || "en"); const type = options.type ?? "key-points"; const length = options.length ?? "medium"; return `You are a helpful assistant that summarizes Reddit posts. Please summarize the provided content in ${targetLangName} only. Summary Type: ${type}. Summary Length: ${length}. Your output must be written in ${targetLangName} only.`; } async function isSummarizerAvailable() { if (Config.summarizeEngine === "gemini") { return Config.summarizeGeminiApiKey.trim() !== ""; } try { const aiObj = globalThis.ai ?? globalThis; const summarizerAPI = aiObj?.summarizer ?? globalThis.Summarizer; if (!summarizerAPI) return false; const availability = await summarizerAPI.availability(); return availability !== "unavailable"; } catch (err) { logger.warn("Failed to check Summarizer availability", err); return false; } } async function* summarizeWithGeminiStream(text2, options) { const apiKey = Config.summarizeGeminiApiKey.trim(); if (!apiKey) { throw new Error("Gemini API Key is not configured"); } const model = getGeminiModelName(); const url = `https://generativelanguage.googleapis.com/v1beta/models/${model}:streamGenerateContent?key=${apiKey}`; const sysInstruction = getGeminiSystemInstruction(options); const payload = { contents: [ { role: "user", parts: [{ text: text2 }] } ], systemInstruction: { parts: [{ text: sysInstruction }] } }; const queue = []; let pendingResolve = null; const pushEvent = (ev) => { queue.push(ev); if (pendingResolve) { pendingResolve(); pendingResolve = null; } }; let lastLength = 0; let accumulated = ""; let bracketCount = 0; let objectStart = -1; let inString = false; let escapeNext = false; const processNewText = (chunkText) => { accumulated += chunkText; bracketCount = 0; objectStart = -1; inString = false; escapeNext = false; for (let i = 0; i < accumulated.length; i++) { const char = accumulated[i]; if (escapeNext) { escapeNext = false; continue; } if (char === "\\") { escapeNext = true; continue; } if (char === '"') { inString = !inString; continue; } if (!inString) { if (char === "{") { if (bracketCount === 0) { objectStart = i; } bracketCount++; } else if (char === "}") { bracketCount--; if (bracketCount === 0 && objectStart !== -1) { const objStr = accumulated.substring(objectStart, i + 1); try { const obj = JSON.parse(objStr); const textPart = obj.candidates?.[0]?.content?.parts?.[0]?.text; if (textPart) { pushEvent({ type: "chunk", text: textPart }); } } catch (err) { } objectStart = -1; } } } } if (objectStart !== -1) { accumulated = accumulated.substring(objectStart); } else { accumulated = ""; } }; const req = GM_xmlhttpRequest({ method: "POST", url, headers: { "Content-Type": "application/json" }, data: JSON.stringify(payload), onreadystatechange: (res) => { if (res.readyState === 3 || res.readyState === 4) { if (res.status === 200) { const textSoFar = res.responseText || ""; if (textSoFar.length > lastLength) { const newChunkText = textSoFar.substring(lastLength); lastLength = textSoFar.length; processNewText(newChunkText); } } } if (res.readyState === 4) { if (res.status === 200) { pushEvent({ type: "done" }); } else { let errMsg = `Gemini API returned status ${res.status}`; try { const errJson = JSON.parse(res.responseText); if (errJson.error?.message) { errMsg += `: ${errJson.error.message}`; } else { errMsg += `: ${res.responseText}`; } } catch { if (res.responseText) { errMsg += `: ${res.responseText}`; } } pushEvent({ type: "error", error: new Error(errMsg) }); } } }, onerror: () => { pushEvent({ type: "error", error: new Error("Network error calling Gemini API") }); }, ontimeout: () => { pushEvent({ type: "error", error: new Error("Gemini API request timed out") }); } }); try { while (true) { if (queue.length === 0) { await new Promise((resolve) => { pendingResolve = resolve; }); } const ev = queue.shift(); if (ev.type === "chunk") { yield ev.text; } else if (ev.type === "done") { break; } else if (ev.type === "error") { throw ev.error; } } } finally { try { req.abort(); } catch { } } } async function* summarizeTextStream(text2, options = {}) { if (Config.summarizeEngine === "gemini") { yield* summarizeWithGeminiStream(text2, options); return; } const aiObj = globalThis.ai ?? globalThis; const summarizerAPI = aiObj?.summarizer ?? globalThis.Summarizer; if (!summarizerAPI) { throw new Error("Summarizer API is not supported in this environment"); } const targetLangName = getLanguageName(options.targetLang || "en"); const instance = await summarizerAPI.create({ type: options.type ?? "key-points", format: options.format ?? "markdown", length: options.length ?? "medium", sharedContext: `The summary must be written in ${targetLangName}.` }); try { let promptText = text2; if (targetLangName !== "English") { promptText = `[IMPORTANT INSTRUCTION: You must summarize and output the summary in ${targetLangName} only. All list items and sentences must be written in ${targetLangName}.] ${text2} [REMINDER: Output must be written in ${targetLangName} only.]`; } const stream = instance.summarizeStreaming(promptText); for await (const chunk of stream) { yield chunk; } } catch (err) { logger.error("Failed to generate summary stream via Summarizer API", err); throw err; } finally { if (typeof instance.destroy === "function") { instance.destroy(); } } } let previewControlsInstalled = false; function resolveElement$1(target) { if (!target) return null; if (target instanceof Element) return target; if (target instanceof Node && target.parentNode instanceof Element) return target.parentNode; return null; } function findHost(el) { return el.closest(".gsrp-preview-tooltip, .gsrp-side-panel"); } function handleFilterClear(btn) { const host = findHost(btn); if (!host) return; const input = host.querySelector(".gsrp-comments-filter-input"); const wrapper = host.querySelector(".gsrp-comments-list-wrapper"); if (input && wrapper) { input.value = ""; if (typeof wrapper.gsrpReapplyFilter === "function") { wrapper.gsrpReapplyFilter(); } try { input.focus(); } catch { } } } function handleFilterNav(btn) { const host = findHost(btn); if (!host) return; const wrapper = host.querySelector(".gsrp-comments-list-wrapper"); if (wrapper && typeof wrapper.gsrpJumpFilterMatch === "function") { wrapper.gsrpJumpFilterMatch(btn.classList.contains("gsrp-comments-filter-next") ? 1 : -1); } } async function handleCopyMarkdown(btn) { const host = findHost(btn); if (!host) return; let data = host.gsrpPostData; if (!data) { const badge = host.closest(".gsrp-reddit-badge"); data = badge ? badge.gsrpPostData : null; } if (!data) return; try { const markdown = convertPostToMarkdown(data); await copyToClipboard(markdown); const originalHTML = btn.innerHTML; const originalTitle = btn.title; btn.classList.add("gsrp-copied"); btn.title = L.copied; btn.setAttribute("aria-label", L.copied); btn.innerHTML = ` <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" class="gsrp-copy-success-icon"> <polyline points="20 6 9 17 4 12"></polyline> </svg> `; setTimeout(() => { btn.classList.remove("gsrp-copied"); btn.title = originalTitle; btn.setAttribute("aria-label", originalTitle); btn.innerHTML = originalHTML; }, 2e3); } catch (err) { console.error("[GSRP] Failed to copy markdown:", err); } } function formatComments(comments, depth = 0) { if (!comments || comments.length === 0 || depth > 2) return ""; let result = ""; for (const comment of comments) { if (!comment || comment.type === "more") continue; const indent = " ".repeat(depth); const author = comment.author || "unknown"; const body = comment.body || ""; if (body === "[deleted]" || body === "[removed]") continue; result += `${indent}- [${author}]: ${body} `; if (comment.replies && comment.replies.length > 0) { result += formatComments(comment.replies, depth + 1); } } return result; } function extractPostText(data) { let text2 = `Title: ${data.title || ""} `; if (data.selftext) { text2 += `Content: ${data.selftext} `; } if (Config.summarizeSource === "all" && data.topComments && data.topComments.length > 0) { text2 += "\nComments:\n"; text2 += formatComments(data.topComments, 0); } return text2; } function renderMarkdownToHtml(markdown) { let html2 = markdown.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'"); html2 = html2.replace(/^###\s+(.*)$/gm, "<h3>$1</h3>"); html2 = html2.replace(/^##\s+(.*)$/gm, "<h2>$1</h2>"); html2 = html2.replace(/^#\s+(.*)$/gm, "<h1>$1</h1>"); html2 = html2.replace(/\*\*(.*?)\*\*/g, "<strong>$1</strong>"); html2 = html2.replace(/`(.*?)`/g, "<code>$1</code>"); const lines = html2.split("\n"); let listType = null; const processedLines = []; for (let line of lines) { const trimmed = line.trim(); const uListMatch = trimmed.match(/^[-*]\s+(.*)$/); const oListMatch = trimmed.match(/^(\d+)\.\s+(.*)$/); if (uListMatch) { if (listType !== "ul") { if (listType === "ol") { processedLines.push("</ol>"); } processedLines.push("<ul>"); listType = "ul"; } processedLines.push(`<li>${uListMatch[1]}</li>`); } else if (oListMatch) { if (listType !== "ol") { if (listType === "ul") { processedLines.push("</ul>"); } processedLines.push("<ol>"); listType = "ol"; } processedLines.push(`<li>${oListMatch[2]}</li>`); } else { if (listType === "ul") { processedLines.push("</ul>"); listType = null; } else if (listType === "ol") { processedLines.push("</ol>"); listType = null; } if (trimmed === "") { processedLines.push("<br>"); } else { processedLines.push(`<p>${line}</p>`); } } } if (listType === "ul") { processedLines.push("</ul>"); } else if (listType === "ol") { processedLines.push("</ol>"); } return processedLines.join("\n"); } async function handleSummarize(btn) { const host = findHost(btn); if (!host) return; let data = host.gsrpPostData; if (!data) { const badge = host.closest(".gsrp-reddit-badge"); data = badge ? badge.gsrpPostData : null; } if (!data) return; let card = host.querySelector(".gsrp-ai-summary-card"); if (card) { card.classList.toggle("gsrp-collapsed"); return; } card = document.createElement("div"); card.className = "gsrp-ai-summary-card"; card.innerHTML = ` <div class="gsrp-ai-summary-header"> <span class="gsrp-ai-summary-title"> ${escapeHTML(L.summarizeSectionTitle)} <span class="gsrp-ai-summary-timer"></span> </span> <div style="display: flex; align-items: center; gap: 4px;"> <button type="button" class="gsrp-ai-summary-copy" title="${escapeHTML(L.copyMarkdownBtnTitle || "Copy as Markdown")}" aria-label="${escapeHTML(L.copyMarkdownBtnTitle || "Copy as Markdown")}"> <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="gsrp-ai-summary-copy-icon"> <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect> <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path> </svg> </button> <button type="button" class="gsrp-ai-summary-toggle" title="${escapeHTML(L.summarizeToggleTitle)}"> <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="gsrp-ai-summary-toggle-icon"> <polyline points="18 15 12 9 6 15"></polyline> </svg> </button> </div> </div> <div class="gsrp-ai-summary-body"> <div class="gsrp-ai-summary-loading"> <span class="gsrp-pulse-dot"></span> <span>${escapeHTML(L.summarizing)}</span> </div> </div> `; const titleContainer = host.querySelector(".gsrp-preview-title-container"); if (titleContainer) { titleContainer.after(card); } else { host.prepend(card); } const startTime = performance.now(); const timerSpan = card.querySelector(".gsrp-ai-summary-timer"); const timerInterval = setInterval(() => { if (!card.parentElement) { clearInterval(timerInterval); return; } const elapsed = ((performance.now() - startTime) / 1e3).toFixed(1); if (timerSpan) { timerSpan.textContent = `(${elapsed}s)`; } }, 100); try { const available = await isSummarizerAvailable(); const bodyDiv = card.querySelector(".gsrp-ai-summary-body"); if (!bodyDiv) return; if (!available) { bodyDiv.innerHTML = `<div class="gsrp-ai-summary-error">${escapeHTML(L.summarizeUnavailable)}</div>`; return; } const textToSummarize = extractPostText(data); const options = { type: Config.summarizeType, length: Config.summarizeLength, format: "markdown", targetLang: Config.lang === "auto" ? navigator.language : Config.lang }; const stream = summarizeTextStream(textToSummarize, options); let summaryAccumulated = ""; for await (const chunk of stream) { summaryAccumulated += chunk; const currentBody = card.querySelector(".gsrp-ai-summary-body"); if (currentBody) { currentBody.innerHTML = renderMarkdownToHtml(summaryAccumulated); currentBody.dataset.markdown = summaryAccumulated; } } } catch (err) { const bodyDiv = card.querySelector(".gsrp-ai-summary-body"); if (bodyDiv) { bodyDiv.innerHTML = `<div class="gsrp-ai-summary-error">${escapeHTML(L.summarizeErr)}</div>`; delete bodyDiv.dataset.markdown; } } finally { clearInterval(timerInterval); const finalTime = ((performance.now() - startTime) / 1e3).toFixed(1); if (timerSpan) { timerSpan.textContent = `(${finalTime}s)`; } } } async function handleCopySummary(btn) { const card = btn.closest(".gsrp-ai-summary-card"); if (!card) return; const body = card.querySelector(".gsrp-ai-summary-body"); if (!body) return; const textToCopy = (body.dataset.markdown || body.textContent || "").trim(); if (!textToCopy || textToCopy === L.summarizing) return; try { await copyToClipboard(textToCopy); const originalHTML = btn.innerHTML; const originalTitle = btn.title; btn.classList.add("gsrp-copied"); btn.title = L.copied; btn.setAttribute("aria-label", L.copied); btn.innerHTML = ` <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" class="gsrp-copy-success-icon"> <polyline points="20 6 9 17 4 12"></polyline> </svg> `; setTimeout(() => { btn.classList.remove("gsrp-copied"); btn.title = originalTitle; btn.setAttribute("aria-label", originalTitle); btn.innerHTML = originalHTML; }, 2e3); } catch (err) { console.error("[GSRP] Failed to copy AI summary:", err); } } function handleMaximize(btn) { const host = findHost(btn); if (!host) return; host.classList.add("gsrp-no-transform-transition"); const isMaximized = host.classList.toggle("gsrp-maximized"); btn.textContent = isMaximized ? "⤡" : "⛶"; btn.title = isMaximized ? L.restoreBtn : L.maximizeBtn; if (isMaximized) { autoPinTriggerOf(btn); } host.getBoundingClientRect(); host.classList.remove("gsrp-no-transform-transition"); } function handleClose(btn) { const trigger = btn.closest(".gsrp-preview-trigger"); if (!trigger) return; trigger.classList.remove("gsrp-pinned"); trigger.classList.remove("gsrp-hover-sticky"); const tooltip = trigger.querySelector(".gsrp-preview-tooltip"); if (!tooltip) return; tooltip.classList.add("gsrp-force-close"); tooltip.classList.remove("gsrp-maximized"); const maxBtn = tooltip.querySelector(".gsrp-maximize-btn"); if (maxBtn) { maxBtn.textContent = "⛶"; maxBtn.title = L.maximizeBtn; } clearMediaIn(tooltip); } function resetMaxBtnToRestored(maxBtn) { if (!maxBtn) return; maxBtn.textContent = "⛶"; maxBtn.title = L.maximizeBtn; } function setMaxBtnToMaximized(maxBtn) { if (!maxBtn) return; maxBtn.textContent = "⤡"; maxBtn.title = L.restoreBtn; } function handleTriggerPin(trigger) { const tooltip = trigger.querySelector(".gsrp-preview-tooltip"); if (tooltip) { tooltip.classList.remove("gsrp-force-close"); if (trigger.classList.contains("gsrp-pinned")) { tooltip.classList.add("gsrp-no-transform-transition"); tooltip.classList.remove("gsrp-maximized"); resetMaxBtnToRestored( tooltip.querySelector(".gsrp-maximize-btn") ); tooltip.getBoundingClientRect(); tooltip.classList.remove("gsrp-no-transform-transition"); } } const willPin = !trigger.classList.contains("gsrp-pinned"); if (willPin) { unpinAllExcept(trigger); if (Config.clickToMaximize && tooltip) { tooltip.classList.add("gsrp-no-transform-transition"); tooltip.classList.add("gsrp-maximized"); setMaxBtnToMaximized(tooltip.querySelector(".gsrp-maximize-btn")); tooltip.getBoundingClientRect(); tooltip.classList.remove("gsrp-no-transform-transition"); } } trigger.classList.toggle("gsrp-pinned"); } function routeControlAction(target) { const copyBtn = target.closest(".gsrp-copy-markdown-btn"); if (copyBtn) { autoPinTriggerOf(copyBtn); handleCopyMarkdown(copyBtn); return true; } const summarizeBtn = target.closest(".gsrp-summarize-btn"); if (summarizeBtn) { autoPinTriggerOf(summarizeBtn); handleSummarize(summarizeBtn); return true; } const summaryCopyBtn = target.closest(".gsrp-ai-summary-copy"); if (summaryCopyBtn) { autoPinTriggerOf(summaryCopyBtn); handleCopySummary(summaryCopyBtn); return true; } const summaryToggleBtn = target.closest(".gsrp-ai-summary-toggle"); if (summaryToggleBtn) { autoPinTriggerOf(summaryToggleBtn); const card = summaryToggleBtn.closest(".gsrp-ai-summary-card"); if (card) { const isCollapsed = card.classList.toggle("gsrp-collapsed"); summaryToggleBtn.title = isCollapsed ? L.summarizeToggleActiveTitle : L.summarizeToggleTitle; } return true; } const filterClearBtn = target.closest(".gsrp-comments-filter-clear"); if (filterClearBtn) { handleFilterClear(filterClearBtn); return true; } const filterNavBtn = target.closest(".gsrp-comments-filter-prev, .gsrp-comments-filter-next"); if (filterNavBtn) { handleFilterNav(filterNavBtn); return true; } const maximizeBtn = target.closest(".gsrp-maximize-btn"); if (maximizeBtn) { handleMaximize(maximizeBtn); return true; } const closeBtn = target.closest(".gsrp-close-btn"); if (closeBtn && closeBtn.closest(".gsrp-side-panel")) return false; if (closeBtn) { handleClose(closeBtn); return true; } return false; } function routeTriggerPin(target) { if (target.closest(".gsrp-preview-tooltip, .gsrp-side-panel")) return false; const trigger = target.closest(".gsrp-preview-trigger"); if (!trigger) return false; handleTriggerPin(trigger); return true; } function installPreviewControlsDelegate() { if (previewControlsInstalled) return; previewControlsInstalled = true; window.addEventListener( "mousedown", (e) => { const target = resolveElement$1(e.target); if (!target) return; if (target.closest(".gsrp-crossposts-link")) return; if (routeControlAction(target)) { e.preventDefault(); e.stopPropagation(); return; } if (routeTriggerPin(target)) { e.preventDefault(); e.stopPropagation(); } }, true ); window.addEventListener( "keydown", (e) => { if (e.key !== "Enter" && e.key !== " ") return; const target = resolveElement$1(e.target); if (!target) return; const interactable = target.closest( ".gsrp-close-btn, .gsrp-maximize-btn, .gsrp-copy-markdown-btn, .gsrp-comments-filter-clear, .gsrp-comments-filter-prev, .gsrp-comments-filter-next, .gsrp-summarize-btn, .gsrp-ai-summary-toggle, .gsrp-ai-summary-copy" ); if (!interactable) return; if (routeControlAction(target)) { e.preventDefault(); e.stopPropagation(); } }, true ); } const PRELOAD_RANGE = 3; function handleMediaTransition(oldItem, newItem) { if (!oldItem || !newItem) return; const oldVideos = oldItem.querySelectorAll("video"); oldVideos.forEach((v) => { try { v.pause(); } catch { } }); const newVideos = newItem.querySelectorAll("video"); newVideos.forEach((v) => { if (v.dataset.gsrpAutoplay === "1") { try { playAutoplayVideo(v); } catch { } } }); } function setupCarousel(badge) { const carousels = badge.querySelectorAll(".gsrp-carousel"); for (const carousel of carousels) { let preloadAdjacent = function(idx) { for (let step = 1; step <= PRELOAD_RANGE; step++) { const fwd = (idx + step) % items.length; const bwd = ((idx - step) % items.length + items.length) % items.length; [fwd, bwd].forEach((i) => { const img = items[i].querySelector("img"); if (img && img.getAttribute("loading") === "lazy") { img.setAttribute("loading", "eager"); } }); if (!preloaded.has(fwd)) { preloaded.add(fwd); const img = items[fwd].querySelector("img"); if (img) new Image().src = img.src || img.dataset.src || ""; } if (!preloaded.has(bwd)) { preloaded.add(bwd); const img = items[bwd].querySelector("img"); if (img) new Image().src = img.src || img.dataset.src || ""; } } }, go = function(delta) { const current = items.findIndex((i) => i.classList.contains("gsrp-carousel-active")); const next = ((current + delta) % items.length + items.length) % items.length; const shouldFocus = document.activeElement === items[current] || document.activeElement === prevBtn || document.activeElement === nextBtn; const oldItem = items[current]; const newItem = items[next]; oldItem.classList.remove("gsrp-carousel-active"); newItem.classList.add("gsrp-carousel-active"); handleMediaTransition(oldItem, newItem); preloadAdjacent(next); if (shouldFocus) { try { items[next].focus({ preventScroll: true }); } catch { } } }; if (carousel.dataset.gsrpCarouselReady) continue; carousel.dataset.gsrpCarouselReady = "1"; const items = Array.from(carousel.querySelectorAll(".gsrp-gallery-item")); if (items.length < 2) continue; const prevBtn = carousel.querySelector(".gsrp-carousel-prev"); const nextBtn = carousel.querySelector(".gsrp-carousel-next"); if (!prevBtn || !nextBtn) continue; const preloaded = new Set([0]); preloadAdjacent(0); carousel.addEventListener("gsrp-carousel-sync", (e) => { const detail = e.detail; const targetIdx = detail?.index; if (typeof targetIdx !== "number" || targetIdx < 0 || targetIdx >= items.length) return; const current = items.findIndex((i) => i.classList.contains("gsrp-carousel-active")); if (current === targetIdx) return; const oldItem = items[current]; const newItem = items[targetIdx]; oldItem.classList.remove("gsrp-carousel-active"); newItem.classList.add("gsrp-carousel-active"); handleMediaTransition(oldItem, newItem); preloadAdjacent(targetIdx); try { items[targetIdx].focus({ preventScroll: true }); } catch { } }); carousel.addEventListener("mousedown", (e) => { if (!(e.target instanceof Element)) return; if (e.target.closest(".gsrp-preview-media-caption") || e.target.closest(".gsrp-carousel-prev") || e.target.closest(".gsrp-carousel-next")) return; const ae = document.activeElement; if (ae && (ae.tagName === "INPUT" || ae.tagName === "TEXTAREA" || ae.isContentEditable)) return; const current = items.findIndex((i) => i.classList.contains("gsrp-carousel-active")); if (current !== -1) { setTimeout(() => { try { items[current].focus({ preventScroll: true }); } catch { } }, 0); } }); items.forEach((item) => { item.addEventListener("keydown", (e) => { if (e.key === "ArrowLeft") { e.preventDefault(); e.stopPropagation(); go(-1); } else if (e.key === "ArrowRight") { e.preventDefault(); e.stopPropagation(); go(1); } }); }); [prevBtn, nextBtn].forEach((btn) => { btn.addEventListener("keydown", (e) => { if (e.key === "ArrowLeft") { e.preventDefault(); e.stopPropagation(); go(-1); } else if (e.key === "ArrowRight") { e.preventDefault(); e.stopPropagation(); go(1); } }); }); prevBtn.addEventListener("click", (e) => { e.stopPropagation(); e.preventDefault(); go(-1); }); nextBtn.addEventListener("click", (e) => { e.stopPropagation(); e.preventDefault(); go(1); }); } } const IMGUR_URL_RE = /^https?:\/\/(?:[im]\.)?imgur\.com\/(?:(?:a|gallery|t\/[a-zA-Z0-9_-]+)\/(?:[a-zA-Z0-9_-]+-)?|)?([a-zA-Z0-9]{5,10})\/?(?:\.[a-zA-Z0-9]{3,4})?(?:\/|\?|#|$)/i; const getImgurClientId = () => Config.imgurClientId || "546c25a59c58ad7"; const FETCH_TIMEOUT_MS = 8e3; const SPINNER_SVG = ` <svg viewBox="0 0 50 50" fill="none" stroke="currentColor" stroke-width="4"> <circle cx="25" cy="25" r="20" stroke-dasharray="80 40"/> </svg> `; function linkifyAndEscape(text2) { if (!text2) return ""; const safe = escapeHTML(text2); const urlPattern = /https?:\/\/[^\s)(]+[^\s.)(,?!:;]/gi; return safe.replace(urlPattern, (url) => { return `<a href="${url}" target="_blank" rel="noopener noreferrer" class="gsrp-caption-link">${url}</a>`; }); } function formatImgurDate(datetime) { if (!datetime) return ""; let date; if (typeof datetime === "number") date = new Date(datetime * 1e3); else if (typeof datetime === "string") date = new Date(datetime); else return ""; if (Number.isNaN(date.getTime())) return ""; const months = [ "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December" ]; return `${months[date.getUTCMonth()]} ${date.getUTCDate()} ${date.getUTCFullYear()}`; } function renderImgurSkeleton(container, label) { container.innerHTML = ` <div class="gsrp-imgur-skeleton"> ${SPINNER_SVG} <span>${escapeHTML(label)}...</span> </div> `; } function renderImgurGallery(container, mediaList, albumTitle = "", views = 0, datetime = null) { const total = mediaList.length; const isCarousel = total > 1; const itemsHtml = mediaList.map((img, idx) => { const counter = isCarousel ? `<span class="gsrp-gallery-counter">${idx + 1}/${total}</span>` : ""; const dims = Config.showImageResolutionPreview && img.width && img.height ? `<span class="gsrp-gallery-dims">${img.width} × ${img.height}</span>` : ""; const title = img.metadata?.title || ""; const desc = img.metadata?.description || ""; const captionText = desc || title; const caption = captionText ? `<div class="gsrp-preview-media-caption">${linkifyAndEscape(captionText)}</div>` : ""; const activeClass = isCarousel && idx === 0 ? " gsrp-carousel-active" : ""; const loadAttr = isCarousel && idx > 0 ? ' loading="lazy"' : ""; const aspectStyle = img.width && img.height ? `aspect-ratio: ${img.width} / ${img.height};` : ""; const url = img.url || ""; const isGifv = /\.gifv(?:\?|#|$)/i.test(url); const isMp4 = /\.mp4(?:\?|#|$)/i.test(url); const isWebm = /\.webm(?:\?|#|$)/i.test(url); const isGif = /\.gif(?:\?|#|$)/i.test(url); const isVideo = isGifv || isMp4 || isWebm || isGif; let mediaHtml; if (isVideo) { const videoUrl = url.replace(/\.gifv(?:\?|#|$)/i, ".mp4").replace(/\.gif(?:\?|#|$)/i, ".mp4"); const comfortVolume = Math.max(0, Math.min(100, Config.defaultVideoVolume)) / 100; const hasAudio = !isGifv && !isGif && img.has_sound !== false && img.hasSound !== false; mediaHtml = `<video src="${escapeHTML(videoUrl)}" ${loadAttr} preload="metadata" playsinline loop muted data-gsrp-comfort-volume="${comfortVolume}" data-gsrp-autoplay="1" data-gsrp-has-audio="${hasAudio ? "true" : "false"}" onerror="this.parentElement.classList.add('gsrp-video-error')" style="${aspectStyle} display:block; border-radius:4px; background:transparent;"></video>`; } else { mediaHtml = `<img src="${escapeHTML(url)}" ${loadAttr} decoding="async" alt="" onerror="this.parentElement.classList.add('gsrp-img-error')" style="${aspectStyle}">`; } return ` <div class="gsrp-gallery-item gsrp-is-loading${activeClass}" role="button" tabindex="0" data-gallery-idx="${idx}" data-w="${img.width || ""}" data-h="${img.height || ""}"> ${mediaHtml} ${counter} ${dims} ${caption} </div> `; }).join(""); const titleHtml = albumTitle ? `<div class="gsrp-imgur-embed-title">${escapeHTML(albumTitle)}</div>` : ""; let metaText = ""; if (typeof views === "number" && views > 0) { metaText += `${views.toLocaleString("en-US")} views`; } const formattedDate = formatImgurDate(datetime); if (formattedDate) { if (metaText) metaText += " • "; metaText += formattedDate; } const metaHtml = metaText ? `<div class="gsrp-imgur-embed-meta">${escapeHTML(metaText)}</div>` : ""; const headerHtml = titleHtml || metaHtml ? `<div class="gsrp-imgur-header-block">${titleHtml}${metaHtml}</div>` : ""; let carouselHtml; if (isCarousel) { const prevBtn = `<button type="button" class="gsrp-carousel-prev" aria-label="‹">‹</button>`; const nextBtn = `<button type="button" class="gsrp-carousel-next" aria-label="›">›</button>`; carouselHtml = `${headerHtml}<div class="gsrp-preview-media-gallery gsrp-carousel">${itemsHtml}${prevBtn}${nextBtn}</div>`; } else { carouselHtml = `${headerHtml}<div class="gsrp-preview-media-gallery">${itemsHtml}</div>`; } container.innerHTML = carouselHtml; if (isCarousel) { setupCarousel(container); } if (typeof window.GSRP_setupCollapsibleCaptions === "function") { window.GSRP_setupCollapsibleCaptions(container); } container.querySelectorAll("video").forEach((video) => { const applyVolume = () => { const comfortVolume = video.dataset.gsrpComfortVolume ? parseFloat(video.dataset.gsrpComfortVolume) : Math.max(0, Math.min(100, Config.defaultVideoVolume)) / 100; if (!Number.isNaN(comfortVolume)) { const wasMuted = video.muted; video.volume = comfortVolume; if (wasMuted) video.muted = true; } }; if (video.readyState >= 1) applyVolume(); else video.addEventListener("loadedmetadata", applyVolume, { once: true }); bindStickyVolume(video); }); } function directImgurFallback(hash, target) { renderImgurGallery( target, [ { url: `https://i.imgur.com/${hash}.jpg`, width: null, height: null, metadata: { title: "", description: "" } } ], "" ); } function fetchImgurSingleImage(hash, target) { GM_xmlhttpRequest({ method: "GET", url: `https://api.imgur.com/3/image/${hash}?client_id=${getImgurClientId()}`, timeout: FETCH_TIMEOUT_MS, onload: (res) => { if (res.status === 200) { try { const json = JSON.parse(res.responseText); if (json && json.data) { const img = json.data; renderImgurGallery( target, [ { url: img.link, width: img.width, height: img.height, metadata: { title: img.title || "", description: img.description || "" } } ], img.title || "", img.views || 0, img.datetime || null ); return; } } catch { } } directImgurFallback(hash, target); }, onerror: () => directImgurFallback(hash, target), ontimeout: () => directImgurFallback(hash, target) }); } function fetchImgurMedia(hash, target) { GM_xmlhttpRequest({ method: "GET", url: `https://api.imgur.com/post/v1/albums/${hash}?client_id=${getImgurClientId()}&include=media`, timeout: FETCH_TIMEOUT_MS, onload: (res) => { if (res.status === 200) { try { const json = JSON.parse(res.responseText); if (json && json.media && Array.isArray(json.media) && json.media.length > 0) { const views = json.view_count || json.views || 0; const datetime = json.created_at || json.datetime || null; renderImgurGallery(target, json.media, json.title || "", views, datetime); return; } } catch { } } fetchImgurSingleImage(hash, target); }, onerror: () => fetchImgurSingleImage(hash, target), ontimeout: () => fetchImgurSingleImage(hash, target) }); } const IMGUR_SVG_ICON = ` <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 28 28" class="gsrp-embed-btn-logo"> <g transform="translate(-6 -6)"> <rect width="28" height="28" x="6" y="6" rx="5.96" fill="#1bb76e"/> <path fill="#0a0d49" d="M28 26.84 20.85 34H12a6 6 0 0 1-6-6v-8.44l7.36-7.36Z"/> <path fill="#fff" d="M23.6 26.63a1 1 0 0 0-.54.55.09.09 0 0 1-.17 0 1.06 1.06 0 0 0-.55-.55.1.1 0 0 1 0-.17 1.09 1.09 0 0 0 .55-.55.09.09 0 0 1 .17 0 1 1 0 0 0 .54.55.09.09 0 0 1 0 .17zm-5.74 5.79a1.18 1.18 0 0 0-.64.64.11.11 0 0 1-.2 0 1.23 1.23 0 0 0-.65-.64.11.11 0 0 1 0-.2 1.29 1.29 0 0 0 .65-.65.11.11 0 0 1 .2 0 1.23 1.23 0 0 0 .64.65.11.11 0 0 1 0 .2zM9.1 29.71a.73.73 0 0 0-.4.39.06.06 0 0 1-.12 0 .73.73 0 0 0-.4-.39.07.07 0 0 1 0-.13.78.78 0 0 0 .4-.4.07.07 0 0 1 .12 0 .78.78 0 0 0 .4.4.07.07 0 0 1 0 .13zm.76-3.18a1.21 1.21 0 0 0-.61.61.1.1 0 0 1-.19 0 1.21 1.21 0 0 0-.61-.61.11.11 0 0 1 0-.19 1.14 1.14 0 0 0 .61-.61.11.11 0 0 1 .19 0 1.14 1.14 0 0 0 .61.61.11.11 0 0 1 0 .19zm3.62-9.47a1.65 1.65 0 0 0-.87.87.15.15 0 0 1-.27 0 1.65 1.65 0 0 0-.87-.87.15.15 0 0 1 0-.27 1.7 1.7 0 0 0 .87-.88.15.15 0 0 1 .27 0 1.7 1.7 0 0 0 .87.88.15.15 0 0 1 0 .27zm-2.63 3.26a2.82 2.82 0 0 0-1.48 1.48.26.26 0 0 1-.46 0 2.75 2.75 0 0 0-1.48-1.48.25.25 0 0 1 0-.46 2.85 2.85 0 0 0 1.48-1.48.25.25 0 0 1 .46 0 2.89 2.89 0 0 0 1.48 1.48.25.25 0 0 1 0 .46zm2.37 10.74a1.74 1.74 0 0 0-.9.91.15.15 0 0 1-.32.03 1.78 1.78 0 0 0-.9-.91.15.15 0 0 1 0-.28 1.72 1.72 0 0 0 .9-.9.15.15 0 0 1 .28 0 1.69 1.69 0 0 0 .9.9.15.15 0 0 1 .04.25zm9.61-.87a2.11 2.11 0 0 0-1.08 1.08.18.18 0 0 1-.34 0 2 2 0 0 0-1.08-1.08.18.18 0 0 1 0-.33 2 2 0 0 0 1.08-1.08.18.18 0 0 1 .34 0 2.11 2.11 0 0 0 1.08 1.08.18.18 0 0 1 0 .33z"/> <path fill="#fff" d="M15.38 30a31.35 31.35 0 0 1-5.08-5.09 1.75 1.75 0 0 1 .12-2.3L15.83 17l-2.46-2.42a1.75 1.75 0 0 1 .94-3 64.67 64.67 0 0 1 10.4-.92c1.06 0 2.1 0 3.11.1a1.74 1.74 0 0 1 1.62 1.63A59.55 59.55 0 0 1 28.63 26a1.75 1.75 0 0 1-3 .94l-2.42-2.46-5.55 5.42a1.75 1.75 0 0 1-2.31.11Z"/> <path fill="#1bb76e" d="M27.7 12.58a57.82 57.82 0 0 0-13.1.8L18.29 17l-6.62 6.79a29.92 29.92 0 0 0 4.8 4.81l6.8-6.6 3.64 3.7a57.87 57.87 0 0 0 .79-13.12Z"/> </g> </svg> `; const CHEVRON_SVG = ` <svg viewBox="0 0 24 24" class="gsrp-embed-chevron" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> <polyline points="6 9 12 15 18 9"></polyline> </svg> `; function imgurLinkFilter(link) { if (link.closest(".gsrp-preview-media")) return false; if ((link.textContent || "").includes("🔗")) return false; const next = link.nextElementSibling; if (next && (next.classList.contains("gsrp-inline-media-img") || next.classList.contains("gsrp-gallery-item"))) { return false; } const href = link.getAttribute("href") || ""; const isDirectImage = /\.(jpe?g|png|gif|webp)(?:\?|#|$)/i.test(href); if (isDirectImage && Config.autoExpandSingleImages) return false; return true; } function extractImgurHash(href) { const match = href.match(IMGUR_URL_RE); return match ? asImgurHash(match[1]) : null; } function buildImgurContent({ container, details }) { renderImgurSkeleton(container, L.embedImgur); fetchImgurMedia(details, container); } function teardownImgurEmbed(container) { container.querySelectorAll("video").forEach((video) => { try { video.pause(); } catch { } }); } const delegate = createEmbedDelegate({ idPrefix: "gsrp-imgur-", dataKey: "gsrpImgurId", urlDataKey: "gsrpImgurUrl", processedKey: "gsrpImgurProcessed", btnClass: "gsrp-imgur-embed-btn", containerClass: "gsrp-imgur-embed-container", extractDetails: extractImgurHash, linkFilter: imgurLinkFilter, get btnTitleOpen() { return L.embedImgur; }, get btnTitleClose() { return L.closeImgur; }, btnIconHtml: `${IMGUR_SVG_ICON}${CHEVRON_SVG}`, contentBuilder: buildImgurContent, onClose: teardownImgurEmbed }); const installImgurEmbedDelegate = delegate.install; const processImgurEmbeds = delegate.process; function upgradeImgurPrimaryMedia(container) { const primaryMedia = container.querySelector(".gsrp-preview-media"); if (!primaryMedia || primaryMedia.dataset.gsrpImgurUpgraded) return; const imgurAnchors = Array.from( container.querySelectorAll('a[href*="imgur.com"]') ); const mainImgurLink = imgurAnchors.find( (a) => !a.closest(".gsrp-comment-content") && !a.closest(".gsrp-comment-body") ); if (!mainImgurLink) return; const hash = extractImgurHash(mainImgurLink.getAttribute("href") || ""); if (!hash) return; primaryMedia.dataset.gsrpImgurUpgraded = "1"; fetchImgurMedia(hash, primaryMedia); } function setupImgurEmbeds(container) { if (!container) return; upgradeImgurPrimaryMedia(container); processImgurEmbeds(container); } function resolveElement(target) { if (!target) return null; if (target instanceof Element) return target; if (target instanceof Node && target.parentNode instanceof Element) return target.parentNode; return null; } function setupBadgeMousedown(badge) { badge.addEventListener( "mousedown", (e) => { const targetElement = resolveElement(e.target); if (!targetElement) return; if (targetElement.closest(".gsrp-crossposts-link")) return; e.stopPropagation(); }, true ); badge.addEventListener( "click", (e) => { const targetElement = resolveElement(e.target); if (!targetElement) return; if (targetElement.closest(".gsrp-crossposts-link")) return; if (targetElement.closest(".gsrp-preview-tooltip, .gsrp-side-panel")) { return; } const trigger = targetElement.closest(".gsrp-preview-trigger"); if (trigger) { e.stopPropagation(); e.preventDefault(); } }, true ); badge.addEventListener( "click", (e) => { const targetElement = resolveElement(e.target); if (!targetElement) return; if (targetElement.closest(".gsrp-crossposts-link")) return; if (targetElement.closest(".gsrp-preview-tooltip, .gsrp-side-panel")) { e.stopPropagation(); } }, false ); } const AUTO_COLLAPSE_BOTS = new Set([ "AutoModerator", "SaveVideo", "savevideobot", "RemindMeBot", "haikusbot", "RepostSleuthBot", "AmputatorBot", "reputatorbot" ]); function setupBadgeFilterWiring(badge) { const cleanups = []; let wiring = badgeWiringMap.get(badge); if (!wiring) { wiring = {}; badgeWiringMap.set(badge, wiring); } wiring.cleanupFilter = () => { cleanups.forEach((cleanup) => cleanup()); cleanups.length = 0; }; badge.querySelectorAll(".gsrp-comments-filter-bar").forEach((rawBar) => { const bar = rawBar; const tooltipOrNull = bar.closest( ".gsrp-preview-tooltip, .gsrp-side-panel" ); if (!tooltipOrNull) return; const tooltip = tooltipOrNull; const input = bar.querySelector(".gsrp-comments-filter-input"); const opCheckbox = bar.querySelector(".gsrp-comments-filter-op"); const opLabel = bar.querySelector(".gsrp-comments-filter-op-label"); const scoreInput = bar.querySelector( ".gsrp-comments-filter-score" ); const scoreOpBtn = bar.querySelector( ".gsrp-comments-filter-score-op-btn" ); const modAdminCheckbox = bar.querySelector( ".gsrp-comments-filter-mod-admin" ); const modAdminLabel = bar.querySelector( ".gsrp-comments-filter-mod-admin-label" ); const linksCheckbox = bar.querySelector( ".gsrp-comments-filter-links" ); const linksLabel = bar.querySelector( ".gsrp-comments-filter-links-label" ); const mediaCheckbox = bar.querySelector( ".gsrp-comments-filter-media" ); const mediaLabel = bar.querySelector( ".gsrp-comments-filter-media-label" ); const collapseBtn = bar.querySelector( ".gsrp-comments-collapse-toggle-btn" ); const wrapperOrNull = tooltip.querySelector( ".gsrp-comments-list-wrapper" ); if (!input || !opCheckbox || !wrapperOrNull) return; const wrapper = wrapperOrNull; function updateParticipantsCount() { const partEl = tooltip.querySelector(".gsrp-participants-count"); if (!partEl) return; const authorCounts = new Map(); wrapper.querySelectorAll("[data-author]").forEach((el) => { const author = el.getAttribute("data-author"); if (author && author !== "[deleted]" && author !== "[removed]" && !AUTO_COLLAPSE_BOTS.has(author)) { authorCounts.set(author, (authorCounts.get(author) || 0) + 1); } }); const currentCount = authorCounts.size; const topParticipants = Array.from(authorCounts.entries()).sort((a, b) => b[1] - a[1]).slice(0, 10); const partTooltip = `${L.uniqueParticipantsTitle}${topParticipants.length ? "\n\n" + L.topParticipantsLabel + "\n" + topParticipants.map((p) => `• ${p[0]} (${p[1]})`).join("\n") : ""}`; partEl.title = partTooltip; partEl.innerHTML = `<svg class="gsrp-participants-icon" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path> <circle cx="9" cy="7" r="4"></circle> <path d="M23 21v-2a4 4 0 0 0-3-3.87"></path> <path d="M16 3.13a4 4 0 0 1 0 7.75"></path> </svg> ${currentCount}`; } function updateOpFilterCount() { if (!opLabel || !opCheckbox) return; const opCount = wrapper.querySelectorAll('[data-is-op="true"]').length; const opSpan = opLabel.querySelector("span"); if (opSpan) { opSpan.textContent = `${L.filterOpOnly} (${opCount})`; } if (opCount === 0) { opCheckbox.disabled = true; opCheckbox.checked = false; if (wrapper.dataset.opAuthor === "[deleted]") { opLabel.title = L.filterOpUnavailable; } else { opLabel.title = L.filterOpNoReplies; } opLabel.classList.add("gsrp-comments-filter-disabled"); } else { opCheckbox.disabled = false; opLabel.title = L.filterOpOnlyTitle; opLabel.classList.remove("gsrp-comments-filter-disabled"); } } function updateModAdminFilterCount() { if (!modAdminLabel || !modAdminCheckbox) return; const modAdminCount = wrapper.querySelectorAll( '[data-distinguished="moderator"], [data-distinguished="admin"]' ).length; const span = modAdminLabel.querySelector("span"); if (span) { span.textContent = `${L.filterModAdmin} (${modAdminCount})`; } if (modAdminCount === 0) { modAdminCheckbox.disabled = true; modAdminCheckbox.checked = false; modAdminLabel.classList.add("gsrp-comments-filter-disabled"); } else { modAdminCheckbox.disabled = false; modAdminLabel.classList.remove("gsrp-comments-filter-disabled"); } } function updateLinksFilterCount() { if (!linksLabel || !linksCheckbox) return; let linksCount = 0; wrapper.querySelectorAll(".gsrp-comment-item, .gsrp-reply-item").forEach((el) => { const anchors = el.querySelectorAll( ":scope > .gsrp-comment-content > .gsrp-comment-body a[href]" ); const hasLink = Array.from(anchors).some((a) => { try { const url = new URL(a.href, window.location.href); return !url.hostname.endsWith("reddit.com"); } catch { return false; } }); if (hasLink) linksCount++; }); const span = linksLabel.querySelector("span"); if (span) { span.textContent = `${L.filterLinks} (${linksCount})`; } if (linksCount === 0) { linksCheckbox.disabled = true; linksCheckbox.checked = false; linksLabel.classList.add("gsrp-comments-filter-disabled"); } else { linksCheckbox.disabled = false; linksLabel.classList.remove("gsrp-comments-filter-disabled"); } } function updateMediaFilterCount() { if (!mediaLabel || !mediaCheckbox) return; let mediaCount = 0; wrapper.querySelectorAll(".gsrp-comment-item, .gsrp-reply-item").forEach((el) => { const hasMedia = !!el.querySelector( ":scope > .gsrp-comment-content .gsrp-comment-media-wrapper, :scope > .gsrp-comment-content .gsrp-inline-media-img" ); if (hasMedia) mediaCount++; }); const span = mediaLabel.querySelector("span"); if (span) { span.textContent = `${L.filterMedia} (${mediaCount})`; } if (mediaCount === 0) { mediaCheckbox.disabled = true; mediaCheckbox.checked = false; mediaLabel.classList.add("gsrp-comments-filter-disabled"); } else { mediaCheckbox.disabled = false; mediaLabel.classList.remove("gsrp-comments-filter-disabled"); } } function updateScoreFilterCount() { const scoreCountEl = bar.querySelector( ".gsrp-comments-filter-score-count" ); if (!scoreCountEl || !scoreInput) return; const val = scoreInput.value.trim(); if (val === "") { scoreCountEl.textContent = ""; scoreCountEl.style.display = "none"; return; } const minScore = parseInt(val, 10); if (isNaN(minScore)) { scoreCountEl.textContent = ""; scoreCountEl.style.display = "none"; return; } const op = scoreOpBtn ? scoreOpBtn.dataset.op || "gte" : "gte"; let matchingCount = 0; wrapper.querySelectorAll(".gsrp-comment-item").forEach((el) => { const sAttr = el.getAttribute("data-score"); if (sAttr !== null) { const s = parseInt(sAttr, 10); if (!isNaN(s) && (op === "lte" ? s <= minScore : s >= minScore)) { matchingCount++; } } }); scoreCountEl.textContent = `(${matchingCount})`; scoreCountEl.style.display = "inline"; } const counterEl = bar.querySelector(".gsrp-comments-filter-counter"); const positionEl = bar.querySelector( ".gsrp-comments-filter-position" ); let matchEls = []; let currentMatchIdx = 0; let lastQuery = ""; function refreshCurrentMark() { wrapper.querySelectorAll("mark.gsrp-filter-highlight-current").forEach((m) => { m.classList.remove("gsrp-filter-highlight-current"); m.classList.add("gsrp-filter-highlight"); }); if (matchEls.length === 0) { if (positionEl) positionEl.textContent = ""; if (counterEl) counterEl.classList.remove("gsrp-has-matches"); return; } if (counterEl) counterEl.classList.add("gsrp-has-matches"); const cur = matchEls[currentMatchIdx]; if (cur) { cur.classList.remove("gsrp-filter-highlight"); cur.classList.add("gsrp-filter-highlight-current"); if (positionEl) positionEl.textContent = `${currentMatchIdx + 1}/${matchEls.length}`; scrollMarkIntoView(cur); } } function jump(delta) { if (matchEls.length === 0) return; currentMatchIdx = (currentMatchIdx + delta + matchEls.length) % matchEls.length; refreshCurrentMark(); } wrapper.gsrpJumpFilterMatch = jump; let debounceTimer = null; const reapply = () => { const scoreVal = scoreInput ? scoreInput.value : ""; const scoreOp = scoreOpBtn && scoreOpBtn.dataset.op === "lte" ? "lte" : "gte"; const modAdminChecked = modAdminCheckbox ? modAdminCheckbox.checked : false; const linksChecked = linksCheckbox ? linksCheckbox.checked : false; const mediaChecked = mediaCheckbox ? mediaCheckbox.checked : false; applyCommentFilter( wrapper, input.value, opCheckbox.checked, scoreVal, modAdminChecked, scoreOp, linksChecked, mediaChecked ); const q = (input.value || "").trim(); if (q !== lastQuery) { currentMatchIdx = 0; lastQuery = q; } matchEls = applyHighlights(wrapper, q); if (currentMatchIdx >= matchEls.length) currentMatchIdx = 0; refreshCurrentMark(); updateScoreFilterCount(); }; input.addEventListener("input", () => { if (debounceTimer) clearTimeout(debounceTimer); debounceTimer = setTimeout(reapply, 150); }); input.addEventListener("keydown", (e) => { e.stopPropagation(); if (e.key === "Escape") { if (input.value) { e.preventDefault(); input.value = ""; reapply(); } } else if (e.key === "Enter") { e.preventDefault(); jump(e.shiftKey ? -1 : 1); } }); opCheckbox.addEventListener("change", () => { reapply(); }); if (scoreOpBtn) { scoreOpBtn.addEventListener("click", (e) => { e.stopPropagation(); if (scoreOpBtn.dataset.op === "gte") { scoreOpBtn.dataset.op = "lte"; scoreOpBtn.innerHTML = "≤"; if (scoreInput) { scoreInput.placeholder = L.filterScorePlaceholderLte || "< Score"; scoreInput.title = L.filterScoreTitleLte || "Filter comments with score less than or equal to this value"; scoreInput.setAttribute("aria-label", scoreInput.title); } } else { scoreOpBtn.dataset.op = "gte"; scoreOpBtn.innerHTML = "≥"; if (scoreInput) { scoreInput.placeholder = L.filterScorePlaceholder || "> Score"; scoreInput.title = L.filterScoreTitle || "Filter comments with score greater than or equal to this value"; scoreInput.setAttribute("aria-label", scoreInput.title); } } reapply(); }); } if (scoreInput) { scoreInput.addEventListener("input", () => { if (debounceTimer) clearTimeout(debounceTimer); debounceTimer = setTimeout(reapply, 150); }); scoreInput.addEventListener("keydown", (e) => { e.stopPropagation(); if (e.key === "Escape") { if (scoreInput.value) { e.preventDefault(); scoreInput.value = ""; reapply(); } } }); } if (modAdminCheckbox) { modAdminCheckbox.addEventListener("change", () => { reapply(); }); } if (linksCheckbox) { linksCheckbox.addEventListener("change", () => { reapply(); }); } if (mediaCheckbox) { mediaCheckbox.addEventListener("change", () => { reapply(); }); } const toggleBtn = bar.querySelector( ".gsrp-filter-settings-toggle-btn" ); const popover = bar.querySelector(".gsrp-filter-settings-popover"); if (toggleBtn && popover) { toggleBtn.addEventListener("click", (e) => { e.stopPropagation(); popover.classList.toggle("gsrp-active"); }); popover.addEventListener("click", (e) => { e.stopPropagation(); }); popover.addEventListener("mousedown", (e) => { e.stopPropagation(); }); const closeOnOutsideClick = (event) => { if (event.target instanceof Node && !bar.contains(event.target)) { popover.classList.remove("gsrp-active"); } }; document.addEventListener("mousedown", closeOnOutsideClick); cleanups.push(() => { document.removeEventListener("mousedown", closeOnOutsideClick); }); } if (collapseBtn) { collapseBtn.addEventListener("click", (e) => { e.stopPropagation(); const trigger = collapseBtn.closest(".gsrp-preview-trigger"); if (trigger) { trigger.classList.add("gsrp-hover-sticky"); } const isPressed = collapseBtn.getAttribute("aria-pressed") === "true"; const nextPressed = !isPressed; collapseBtn.setAttribute("aria-pressed", String(nextPressed)); if (nextPressed) { collapseBtn.title = L.collapseToggleActiveTitle || "Show all comments"; collapseBtn.setAttribute( "aria-label", L.collapseToggleActiveTitle || "Show all comments" ); wrapper.classList.add("gsrp-hide-replies"); } else { collapseBtn.title = L.collapseToggleTitle || "Show top-level comments only (collapse replies)"; collapseBtn.setAttribute( "aria-label", L.collapseToggleTitle || "Show top-level comments only (collapse replies)" ); wrapper.classList.remove("gsrp-hide-replies"); } }); } wrapper.gsrpReapplyFilter = reapply; const updateStaticCounts = () => { updateOpFilterCount(); updateModAdminFilterCount(); updateLinksFilterCount(); updateMediaFilterCount(); updateParticipantsCount(); }; const handleCommentsUpdated = () => { updateStaticCounts(); updateScoreFilterCount(); }; wrapper.addEventListener("gsrp-comments-updated", handleCommentsUpdated); cleanups.push(() => { wrapper.removeEventListener("gsrp-comments-updated", handleCommentsUpdated); }); handleCommentsUpdated(); }); } let tooltipObserver = null; if (typeof window !== "undefined" && typeof ResizeObserver !== "undefined") { tooltipObserver = new ResizeObserver((entries2) => { for (const entry of entries2) { const tooltip = entry.target; if (!tooltip.isConnected) { tooltipObserver?.unobserve(tooltip); continue; } const trigger = tooltip.closest(".gsrp-preview-trigger"); const badge = tooltip.closest(".gsrp-reddit-badge"); if (!trigger || !badge) continue; const triggerRect = trigger.getBoundingClientRect(); const viewHeight = window.innerHeight || document.documentElement.clientHeight || 800; const viewWidth = window.innerWidth || document.documentElement.clientWidth || 1200; const spaceBelow = viewHeight - triggerRect.bottom; const spaceAbove = triggerRect.top; const currentHeight = tooltip.offsetHeight; if (currentHeight > spaceBelow && spaceAbove > spaceBelow) { badge.classList.add("gsrp-flip-up"); } else { badge.classList.remove("gsrp-flip-up"); } const tooltipWidth = tooltip.offsetWidth || 600; const spaceRight = viewWidth - triggerRect.left; if (tooltipWidth > spaceRight) { tooltip.style.left = "auto"; tooltip.style.right = "0"; } else { tooltip.style.left = "0"; tooltip.style.right = "auto"; } } }); } function setupBadgeHoverIntent(badge) { badge.querySelectorAll(".gsrp-preview-trigger").forEach((rawTrigger) => { const trigger = rawTrigger; trigger.addEventListener("mouseenter", () => { const tooltip = trigger.querySelector(".gsrp-preview-tooltip"); if (!tooltip) return; restoreMediaIn(tooltip); if (tooltipObserver) { tooltipObserver.observe(tooltip); } }); trigger.addEventListener("mouseleave", () => { const tooltip = trigger.querySelector(".gsrp-preview-tooltip"); if (tooltip) tooltip.classList.remove("gsrp-force-close"); }); { const tooltip = trigger.querySelector(".gsrp-preview-tooltip"); if (tooltip) { if (!tooltip.querySelector(".gsrp-scrollbar-safe-net")) { const net = document.createElement("div"); net.className = "gsrp-scrollbar-safe-net"; tooltip.appendChild(net); const inner = document.createElement("div"); inner.className = "gsrp-preview-inner-wrapper"; while (tooltip.firstChild && tooltip.firstChild !== net) { inner.appendChild(tooltip.firstChild); } tooltip.insertBefore(inner, net); } let leaveTimer = null; const enter = () => { if (leaveTimer) { clearTimeout(leaveTimer); leaveTimer = null; } trigger.classList.add("gsrp-hover-sticky"); if (tooltipObserver) { tooltipObserver.observe(tooltip); } }; const leave = () => { if (leaveTimer) clearTimeout(leaveTimer); leaveTimer = setTimeout(() => { const hasActiveFocus = tooltip && tooltip.contains(document.activeElement); if (trigger.matches(":hover") || tooltip.matches(":hover") || hasActiveFocus) { leaveTimer = null; return; } trigger.classList.remove("gsrp-hover-sticky"); if (tooltipObserver) { tooltipObserver.unobserve(tooltip); } leaveTimer = null; if (!trigger.classList.contains("gsrp-pinned")) { clearMediaIn(tooltip); } }, 150); }; trigger.addEventListener("mouseenter", enter); trigger.addEventListener("mouseleave", leave); tooltip.addEventListener("mouseenter", enter); tooltip.addEventListener("mouseleave", leave); } } }); } function setupBadgeSortWiring(badge, link) { badge.querySelectorAll(".gsrp-inline-sort-select").forEach((rawSelect) => { const select = rawSelect; select.addEventListener("click", (e) => e.stopPropagation()); const sortTrigger = select.closest(".gsrp-preview-trigger"); if (sortTrigger) { select.addEventListener("focus", () => { sortTrigger.classList.add("gsrp-hover-sticky"); }); } select.addEventListener("change", (e) => { e.stopPropagation(); if (sortTrigger) sortTrigger.classList.add("gsrp-hover-sticky"); const target = e.target; const newSort = target.value; const tooltip = target.closest( ".gsrp-preview-tooltip, .gsrp-side-panel" ); if (!tooltip) return; const wrapper = tooltip.querySelector(".gsrp-comments-list-wrapper"); if (!wrapper) return; wrapper.innerHTML = renderCommentSkeletonHTML(); let currentUrl = (link.href || "").split("?")[0]; if (currentUrl.endsWith("/")) currentUrl = currentUrl.slice(0, -1); const cacheKey = `${currentUrl}?sort=${newSort}`; const updateCount = (newData) => { const countEl = tooltip.querySelector(".gsrp-comments-count"); if (countEl) countEl.textContent = `(${newData.displayedComments} / ${newData.comments})`; const partEl = tooltip.querySelector( ".gsrp-participants-count" ); if (partEl && newData.uniqueParticipants !== void 0) { const partTooltip = `${L.uniqueParticipantsTitle}${newData.topParticipants && newData.topParticipants.length ? "\n\n" + L.topParticipantsLabel + "\n" + newData.topParticipants.map((p) => `• ${p[0]} (${p[1]})`).join("\n") : ""}`; partEl.title = partTooltip; partEl.innerHTML = `<svg class="gsrp-participants-icon" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path> <circle cx="9" cy="7" r="4"></circle> <path d="M23 21v-2a4 4 0 0 0-3-3.87"></path> <path d="M16 3.13a4 4 0 0 1 0 7.75"></path> </svg> ${newData.uniqueParticipants}`; } }; const reapplyFilter = () => { if (typeof wrapper.gsrpReapplyFilter === "function") { wrapper.gsrpReapplyFilter(); } }; const reapplyTranslation = () => { const translateBtn = tooltip.querySelector(".gsrp-translate-btn"); if (!translateBtn) return; if (translateBtn.dataset.gsrpCurrentDisplay !== "translated") return; if (typeof translateBtn.gsrpTriggerAutoTranslate === "function") { translateBtn.gsrpTriggerAutoTranslate(); } else { translateBtn.click(); } }; if (redditDataCache.has(cacheKey)) { const cached = redditDataCache.get(cacheKey); const initialComments = cached.topComments ? cached.topComments.slice(0, 20) : []; const remainingComments = cached.topComments ? cached.topComments.slice(20) : []; wrapper.innerHTML = renderCommentsList(initialComments); processYouTubeEmbeds(wrapper); processCodeBlocks(wrapper); if (remainingComments.length > 0) { setupProgressiveComments(tooltip, remainingComments); } const nm = document.createElement("div"); nm.className = "gsrp-comments-no-matches"; nm.textContent = L.noMatchingComments; wrapper.appendChild(nm); updateCount(cached); wrapper.dispatchEvent(new CustomEvent("gsrp-comments-updated", { bubbles: true })); reapplyFilter(); reapplyTranslation(); } else { fetchRedditData( currentUrl, newSort, (newData) => { redditDataCache.set(cacheKey, newData); const initialComments = newData.topComments ? newData.topComments.slice(0, 20) : []; const remainingComments = newData.topComments ? newData.topComments.slice(20) : []; wrapper.innerHTML = renderCommentsList(initialComments); processYouTubeEmbeds(wrapper); processCodeBlocks(wrapper); if (remainingComments.length > 0) { setupProgressiveComments(tooltip, remainingComments); } const nm = document.createElement("div"); nm.className = "gsrp-comments-no-matches"; nm.textContent = L.noMatchingComments; wrapper.appendChild(nm); updateCount(newData); wrapper.dispatchEvent( new CustomEvent("gsrp-comments-updated", { bubbles: true }) ); reapplyFilter(); reapplyTranslation(); }, () => { wrapper.innerHTML = `<div class="gsrp-preview-body" style="text-align:center; padding:20px; color:red;">${L.rateLimitText}</div>`; } ); } }); }); } const DEFAULT_GOOGLE_TRANSLATE_API_KEY = "AIzaSyATBXajvzQLTDHEQbcpq0Ihe0vWDHmO520"; const TRANSLATE_ENDPOINT = "https://translate-pa.googleapis.com/v1/translateHtml"; class TranslateError extends Error { code; constructor(code, message) { super(message); this.name = "TranslateError"; this.code = code; } } function normalizeAiLang(iso) { if (iso === "auto") return "en"; if (/^zh-(?:TW|HK|MO|Hant)/i.test(iso)) return "zh-Hant"; if (/^zh/i.test(iso)) return "zh"; if (iso.includes("-")) return iso.split("-")[0]; return iso; } async function translateWithBrowserAI(textArray, sourceLang, targetLang, signal) { const factory = window.ai && window.ai.translator || window.Translator || window.ai && window.ai.Translator; if (!factory) { throw new TranslateError("BROWSER_AI_UNAVAILABLE", "BROWSER_AI_UNAVAILABLE"); } const options = { sourceLanguage: normalizeAiLang(sourceLang), targetLanguage: normalizeAiLang(targetLang) }; let isAvailable; if (typeof factory.availability === "function") { const avail = await factory.availability(options); isAvailable = avail !== "no"; } else if (typeof factory.capabilities === "function") { const cap = await factory.capabilities(options); isAvailable = cap.available !== "no"; } else { isAvailable = true; } if (!isAvailable) { throw new TranslateError("BROWSER_AI_UNAVAILABLE", "BROWSER_AI_UNAVAILABLE"); } const translator = await factory.create(options); try { const results = []; for (const text2 of textArray) { if (signal?.aborted) { throw new DOMException("Aborted", "AbortError"); } try { const res = await translator.translate(text2); results.push(res || text2); } catch { results.push(text2); } } return results; } finally { if (typeof translator.destroy === "function") { translator.destroy(); } } } async function translateHtmlBatch(textArray, sourceLang = "auto", targetLang = "en", signal) { if (!Array.isArray(textArray) || textArray.length === 0) { return []; } if (Config.translateEngine === "browser-ai") { return translateWithBrowserAI(textArray, sourceLang, targetLang, signal); } return new Promise((resolve, reject) => { if (signal?.aborted) { return reject(new DOMException("Aborted", "AbortError")); } const payloadObj = [[textArray, sourceLang, targetLang], "wt_lib"]; const payloadStr = JSON.stringify(payloadObj); let onAbort; const req = GM_xmlhttpRequest({ method: "POST", url: TRANSLATE_ENDPOINT, headers: { "Content-Type": "application/json+protobuf", "X-Goog-API-Key": Config.translateApiKey.trim() || DEFAULT_GOOGLE_TRANSLATE_API_KEY, "User-Agent": `GSRP-TranslateModule/${VERSION}` }, data: payloadStr, timeout: 8e3, onload: function(res) { if (signal && onAbort) signal.removeEventListener("abort", onAbort); if (res.status === 200) { try { const json = JSON.parse(res.responseText); const translatedList = json?.[0]; if (Array.isArray(translatedList)) { resolve(translatedList); } else { reject( new TranslateError( "STRUCTURE_ERR", "Translation structure mismatch" ) ); } } catch { reject( new TranslateError("PARSE_ERR", "Failed to parse translation response") ); } } else if (res.status === 429) { reject( new TranslateError("RATE_LIMIT", `Translate API rate-limited (HTTP 429)`) ); } else { reject( new TranslateError("HTTP_ERR", `Translate API returned HTTP ${res.status}`) ); } }, onerror: () => { if (signal && onAbort) signal.removeEventListener("abort", onAbort); reject(new TranslateError("NET_ERR", "Network error during translation request")); }, ontimeout: () => { if (signal && onAbort) signal.removeEventListener("abort", onAbort); reject(new TranslateError("TIMEOUT", "Translation request timed out")); } }); if (signal) { onAbort = () => { try { req.abort(); } catch { } reject(new DOMException("Aborted", "AbortError")); }; signal.addEventListener("abort", onAbort); } }); } const TRANSLATE_PACING = { CHUNK_SIZE: 30, INTER_CHUNK_MS: 250, CATCHUP_SWEEP_MS: 250, MAX_CHUNK_CHARS: 5e3 }; const TRANSLATE_OPACITY = { IDLE: "1", ACTIVE: "0.7", WORKING: "0.4" }; const ERROR_INFO_KEYS$1 = { RATE_LIMIT: { textKey: "rateLimitText", titleKey: "rateLimitTitle" }, HTTP_ERR: { textKey: "errHttpText", titleKey: "errHttpTitle" }, NET_ERR: { textKey: "errNetText", titleKey: "errNetTitle" }, PARSE_ERR: { textKey: "errParseText", titleKey: "errParseTitle" }, STRUCTURE_ERR: { textKey: "errParseText", titleKey: "errParseTitle" }, TIMEOUT: { textKey: "errTimeoutText", titleKey: "errTimeoutTitle" }, BROWSER_AI_UNAVAILABLE: { textKey: "errHttpText", titleKey: "errHttpTitle" } }; function setupBadgeTranslateWiring(badge) { if (!badge || !Config.enableTranslate) return; const translateBtns = badge.querySelectorAll(".gsrp-translate-btn"); translateBtns.forEach((translateBtn) => { if (!translateBtn.dataset.gsrpTranslateWired) { translateBtn.dataset.gsrpTranslateWired = "true"; translateBtn.addEventListener("click", (e) => { e.stopPropagation(); e.preventDefault(); toggleTranslation(badge, translateBtn); }); translateBtn.gsrpTriggerAutoTranslate = () => toggleTranslation(badge, translateBtn, true); if (Config.autoTranslate && !badge.dataset.gsrpAutoTransTriggered) { badge.dataset.gsrpAutoTransTriggered = "true"; requestAnimationFrame(() => { toggleTranslation(badge, translateBtn, true); }); } } }); let wiring = badgeWiringMap.get(badge); if (!wiring) { wiring = {}; badgeWiringMap.set(badge, wiring); } wiring.cancelTranslate = () => { translateBtns.forEach((btn) => { const btnState = translateButtonMap.get(btn); if (btnState) { if (btnState.abortController) { btnState.abortController.abort(); } if (btnState.catchupTimer) { clearTimeout(btnState.catchupTimer); } translateButtonMap.delete(btn); } }); }; } function broadcastState(badge, displayState, titleText, opacityVal, isTranslatingVal) { const btns = badge.querySelectorAll(".gsrp-translate-btn"); btns.forEach((b) => { if (displayState !== void 0) b.dataset.gsrpCurrentDisplay = displayState; if (titleText !== void 0) b.title = titleText; if (opacityVal !== void 0) b.style.opacity = opacityVal; if (isTranslatingVal !== void 0) { if (isTranslatingVal === null) delete b.dataset.gsrpIsTranslating; else b.dataset.gsrpIsTranslating = isTranslatingVal; } if (isTranslatingVal === "true") { b.dataset.gsrpTransStatus = "translating"; } else if (displayState === "translated") { b.dataset.gsrpTransStatus = "translated"; } else { b.dataset.gsrpTransStatus = "original"; } }); } function toggleTranslation(badge, btn, isAuto = false) { if (btn.dataset.gsrpIsTranslating === "true") return; const targets = Array.from( badge.querySelectorAll( ".gsrp-preview-post-title, .gsrp-preview-body p, .gsrp-preview-body li, .gsrp-comment-body p, .gsrp-comment-body li, .gsrp-crosspost-card-title a, .gsrp-crosspost-card-body p, .gsrp-post-deleted-placeholder, .gsrp-post-removed-placeholder" ) ).filter((el) => { const text2 = (el.textContent || "").trim(); if (text2.length === 0) return false; if (el.closest("pre, code")) return false; if (/^(?:🔗\s*)?(?:https?:\/\/|\/?r\/|\/?u\/)[^\s]+$/.test(text2)) return false; if (el.closest( '[class*="-embed-container"], .gsrp-inline-media-container, .gsrp-gallery-item, .gsrp-preview-media' )) return false; if (el.querySelector( '.gsrp-preview-body p, .gsrp-preview-body li, .gsrp-comment-body p, .gsrp-comment-body li, [class*="-embed-container"], .gsrp-inline-media-container, .gsrp-gallery-item, iframe, video, .gsrp-preview-media' )) return false; return true; }); if (targets.length === 0) return; const isBilingual = Config.bilingualTranslate === true; const untranslatedTargets = []; targets.forEach((el) => { if (el.dataset.gsrpTransHtml === void 0) { untranslatedTargets.push(el); } }); if (btn.dataset.gsrpCurrentDisplay === "translated" && !isAuto) { targets.forEach((el) => { if (el.dataset.gsrpOrigHtml !== void 0) { el.innerHTML = el.dataset.gsrpOrigHtml; } }); broadcastState( badge, "original", L.translateBtnTitle || "Toggle Translation", TRANSLATE_OPACITY.IDLE, null ); return; } const allCached = targets.every( (el) => (isBilingual ? el.dataset.gsrpBilingualHtml : el.dataset.gsrpTransHtml) !== void 0 ); if (btn.dataset.gsrpCurrentDisplay === "original" || untranslatedTargets.length === 0 || allCached) { targets.forEach((el) => { if (el.dataset.gsrpTransHtml !== void 0) { el.innerHTML = isBilingual ? el.dataset.gsrpBilingualHtml || "" : el.dataset.gsrpTransHtml; } }); broadcastState( badge, "translated", L.translateTitleTranslated || "Toggle Translation (Translated)", TRANSLATE_OPACITY.ACTIVE, null ); reapplyEmbeds(badge); if (untranslatedTargets.length === 0) return; } const btnState = translateButtonMap.get(btn); if (btnState) { if (btnState.abortController) { btnState.abortController.abort(); } if (btnState.catchupTimer) { clearTimeout(btnState.catchupTimer); } } const controller = new AbortController(); const signal = controller.signal; let nextBtnState = translateButtonMap.get(btn); if (!nextBtnState) { nextBtnState = {}; translateButtonMap.set(btn, nextBtnState); } nextBtnState.abortController = controller; broadcastState( badge, "translated", L.translateTranslating || "Translating...", TRANSLATE_OPACITY.WORKING, "true" ); const sourceLang = Config.translateSourceLang || "auto"; const targetLang = Config.translateTargetLang === "auto" || Config.translateTargetLang === "navigator" ? normalizeTargetLanguage(navigator.language) : Config.translateTargetLang || "en"; const chunks = []; let currentChunk = []; let currentLength = 0; for (const el of untranslatedTargets) { const textLen = (el.innerHTML || "").length; if (currentChunk.length >= TRANSLATE_PACING.CHUNK_SIZE || currentChunk.length > 0 && currentLength + textLen > TRANSLATE_PACING.MAX_CHUNK_CHARS) { chunks.push(currentChunk); currentChunk = []; currentLength = 0; } currentChunk.push(el); currentLength += textLen; } if (currentChunk.length > 0) { chunks.push(currentChunk); } let hasError = false; (async () => { for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) { if (hasError || btn.dataset.gsrpCurrentDisplay === "original" || signal.aborted) break; const chunkTargets = chunks[chunkIndex]; const textPayload = chunkTargets.map((el) => { if (el.dataset.gsrpOrigHtml === void 0) { el.dataset.gsrpOrigHtml = el.innerHTML; } const clone2 = el.cloneNode(true); clone2.querySelectorAll('button[class*="-embed-btn"]').forEach((b) => b.remove()); clone2.querySelectorAll("a").forEach((a) => { for (const key of Object.keys(a.dataset)) { if (key.startsWith("gsrp")) { delete a.dataset[key]; } } }); return clone2.innerHTML; }); try { const translatedList = await translateHtmlBatch( textPayload, sourceLang, targetLang, signal ); if (hasError || btn.dataset.gsrpCurrentDisplay === "original" || signal.aborted) break; if (translatedList.length === chunkTargets.length) { chunkTargets.forEach((el, idx) => { const transStr = translatedList[idx]; const origStr = el.dataset.gsrpOrigHtml; const bilingualHtml = `<span class="gsrp-bilingual-orig" style="display:block; margin-bottom:6px;">${origStr}</span><span class="gsrp-bilingual-trans" style="display:block; text-decoration: underline dotted currentColor; text-decoration-thickness: 1px; text-underline-offset: 3px; padding-bottom:4px; opacity:0.95;">${transStr}</span>`; el.dataset.gsrpTransHtml = transStr; el.dataset.gsrpBilingualHtml = bilingualHtml; el.innerHTML = isBilingual ? bilingualHtml : transStr; }); reapplyEmbeds(badge); if (chunkIndex === 0) { broadcastState( badge, "translated", L.translateTitleProgressive || "Toggle Translation (Progressive)", TRANSLATE_OPACITY.ACTIVE, void 0 ); } if (chunkIndex === chunks.length - 1) { broadcastState( badge, "translated", L.translateTitleTranslated || "Toggle Translation (Translated)", TRANSLATE_OPACITY.ACTIVE, null ); if (signal.aborted) break; if (nextBtnState) { nextBtnState.catchupTimer = setTimeout(() => { const state2 = translateButtonMap.get(btn); if (state2) delete state2.catchupTimer; if (btn.dataset.gsrpCurrentDisplay === "translated" && !signal.aborted) { const remainingUntranslated = badge.querySelectorAll( ".gsrp-comment-body p:not([data-gsrp-trans-html]), .gsrp-comment-body li:not([data-gsrp-trans-html])" ); if (remainingUntranslated.length > 0) { console.log( "[GSRP] Catching up progressive background comments for translation sweep..." ); toggleTranslation(badge, btn, true); } } }, TRANSLATE_PACING.CATCHUP_SWEEP_MS); } } } else { hasError = true; revertError(badge, "STRUCTURE_ERR"); break; } if (chunkIndex < chunks.length - 1) { await new Promise((r, rejectPromise) => { const onAbort = () => { clearTimeout(timer); rejectPromise(new DOMException("Aborted", "AbortError")); }; const timer = setTimeout(() => { signal.removeEventListener("abort", onAbort); r(); }, TRANSLATE_PACING.INTER_CHUNK_MS); signal.addEventListener("abort", onAbort); }).catch(() => { }); if (signal.aborted) break; } } catch (err) { if (err instanceof DOMException && err.name === "AbortError") { break; } const code = err && typeof err === "object" && "code" in err && typeof err.code === "string" ? err.code : "NET_ERR"; console.warn(`[GSRP] Chunk translation error (${code}):`, err); hasError = true; revertError(badge, code); break; } } const currentBtnState = translateButtonMap.get(btn); if (currentBtnState && currentBtnState.abortController === controller) { delete currentBtnState.abortController; } })(); } function revertError(badge, code) { const info = code && ERROR_INFO_KEYS$1[code] || null; let text2 = L.translateErr || "⚠️ Translation Failed"; let titleSuffix = ""; if (info) { text2 = L[info.textKey] || text2; const detailedTitle = L[info.titleKey]; const retryHint = L.retryTitle || "Click to retry"; titleSuffix = detailedTitle ? ` — ${detailedTitle} — ${retryHint}` : ` — ${retryHint}`; } broadcastState(badge, "original", `${text2}${titleSuffix}`, TRANSLATE_OPACITY.IDLE, null); } function reapplyEmbeds(badge) { try { processYouTubeEmbeds(badge); processXEmbeds(badge); processBlueskyEmbeds(badge); processMastodonEmbeds(badge); processCodeBlocks(badge); processVimeoEmbeds(badge); processRedditEmbeds(badge); processDailymotionEmbeds(badge); setupImgurEmbeds(badge); } catch (err) { console.error("[GSRP] Failed to reapply embeds after translation:", err); } } const CHUNK_SIZE = 30; function setupProgressiveComments(container, remainingBatch) { if (!remainingBatch || remainingBatch.length === 0) return; const listWrapper = container.querySelector( ".gsrp-comments-list-wrapper" ); if (!listWrapper) return; const commentsBody = listWrapper.querySelector(".gsrp-preview-body") || listWrapper; const queue = [...remainingBatch]; let frameId = null; let isCancelled = false; listWrapper.gsrpRemainingQueue = queue; listWrapper.gsrpCancel = () => { isCancelled = true; if (frameId !== null) { if (typeof window.requestIdleCallback === "function") { window.cancelIdleCallback(frameId); } else { clearTimeout(frameId); } frameId = null; } queue.length = 0; listWrapper.gsrpRemainingQueue = null; }; const appendNextChunk = () => { if (queue.length === 0) { listWrapper.gsrpRemainingQueue = null; return; } const chunk = queue.splice(0, CHUNK_SIZE); const chunkHtml = renderCommentsList(chunk, 0, false); requestAnimationFrame(() => { if (isCancelled || !listWrapper.isConnected) return; const tempDiv = document.createElement("div"); tempDiv.innerHTML = chunkHtml; processYouTubeEmbeds(tempDiv); processCodeBlocks(tempDiv); while (tempDiv.firstChild) { commentsBody.appendChild(tempDiv.firstChild); } if (queue.length > 0) { if (window.requestIdleCallback) { frameId = window.requestIdleCallback(appendNextChunk, { timeout: 150 }); } else { frameId = setTimeout(appendNextChunk, 50); } } else { listWrapper.gsrpRemainingQueue = null; } listWrapper.dispatchEvent(new CustomEvent("gsrp-comments-updated", { bubbles: true })); const translateBtn = container.querySelector( ".gsrp-translate-btn" ); if (translateBtn && (translateBtn.dataset.gsrpCurrentDisplay === "translated" || translateBtn.dataset.gsrpIsTranslating === "true")) { if (!container.dataset.gsrpTransDebounce) { container.dataset.gsrpTransDebounce = "true"; const attemptTranslationSync = () => { if (translateBtn.dataset.gsrpIsTranslating === "true") { setTimeout(attemptTranslationSync, 500); } else { delete container.dataset.gsrpTransDebounce; if (typeof translateBtn.gsrpTriggerAutoTranslate === "function") { translateBtn.gsrpTriggerAutoTranslate(); } else if (translateBtn instanceof HTMLElement) { translateBtn.click(); } } }; setTimeout(attemptTranslationSync, 350); } } }); }; listWrapper.gsrpForceFlush = () => { isCancelled = true; if (frameId !== null) { if (typeof window.requestIdleCallback === "function") { window.cancelIdleCallback(frameId); } else { clearTimeout(frameId); } } if (queue.length > 0) { const remainingHtml = renderCommentsList(queue, 0, false); const tempDiv = document.createElement("div"); tempDiv.innerHTML = remainingHtml; processYouTubeEmbeds(tempDiv); processCodeBlocks(tempDiv); while (tempDiv.firstChild) { commentsBody.appendChild(tempDiv.firstChild); } queue.length = 0; listWrapper.gsrpRemainingQueue = null; console.log("[GSRP] Progressive rendering force-flushed for instant search integrity."); listWrapper.dispatchEvent(new CustomEvent("gsrp-comments-updated", { bubbles: true })); } }; if (window.requestIdleCallback) { frameId = window.requestIdleCallback(appendNextChunk, { timeout: 200 }); } else { frameId = setTimeout(appendNextChunk, 100); } } function activeHealthCheck(container) { if (!container) return; const videos = container.querySelectorAll("video"); for (const video of videos) { const parent = video.closest(".gsrp-gallery-item") || video.closest(".gsrp-preview-media"); if (!parent) continue; if (video.readyState >= 1) { parent.classList.remove("gsrp-is-loading"); } if (video.error || video.networkState === 3 && !video.dataset.gsrpFallbackUrl && !video.dataset.gsrpHlsUrl) { const fallbackSrc = video.getAttribute("data-fallback-src"); if (fallbackSrc) { triggerVideoFallback(video, fallbackSrc); continue; } parent.classList.remove("gsrp-is-loading"); parent.classList.add("gsrp-video-error"); parent.setAttribute("data-error-text", L.videoUnavailable); } if (!video.dataset.gsrpErrorWired) { video.dataset.gsrpErrorWired = "true"; video.addEventListener("error", () => { const fallbackSrc = video.getAttribute("data-fallback-src"); if (fallbackSrc) { triggerVideoFallback(video, fallbackSrc); return; } parent.classList.remove("gsrp-is-loading"); parent.classList.add("gsrp-video-error"); parent.setAttribute("data-error-text", L.videoUnavailable); }); } } const imgs = container.querySelectorAll("img"); for (const img of imgs) { const parent = img.closest(".gsrp-gallery-item") || img.closest(".gsrp-preview-media"); if (!parent) continue; if (img.complete) { parent.classList.remove("gsrp-is-loading"); if (parent.classList.contains("gsrp-inline-media-container")) { if (!parent.dataset.w || !parent.dataset.h) { parent.style.aspectRatio = "auto"; } } } if (img.complete && img.naturalWidth === 0) { parent.classList.remove("gsrp-is-loading"); parent.classList.add("gsrp-img-error"); parent.setAttribute("data-error-text", L.imageUnavailable); } if (!img.dataset.gsrpErrorWired) { img.dataset.gsrpErrorWired = "true"; img.addEventListener("error", () => { parent.classList.remove("gsrp-is-loading"); parent.classList.add("gsrp-img-error"); parent.setAttribute("data-error-text", L.imageUnavailable); }); } } } let globalLoadWired = false; function installGlobalLoadWiring() { if (globalLoadWired) return; globalLoadWired = true; document.addEventListener( "load", (e) => { const target = e.target; if (!(target instanceof HTMLImageElement)) return; const img = target; handleDynamicImageResolution(img); const container = img.closest(".gsrp-gallery-item") || img.closest(".gsrp-preview-media"); if (container) { container.classList.remove("gsrp-is-loading"); if (container.classList.contains("gsrp-inline-media-container")) { if (!container.dataset.w || !container.dataset.h) { container.style.aspectRatio = "auto"; } } } }, true ); document.addEventListener( "loadedmetadata", (e) => { const target = e.target; if (!(target instanceof HTMLVideoElement)) return; const video = target; handleDynamicVideoResolution(video); const container = video.closest(".gsrp-gallery-item") || video.closest(".gsrp-preview-media"); if (container) { container.classList.remove("gsrp-is-loading"); } if (video.dataset.gsrpAutoplay === "1") { const hasAudio = video.dataset.gsrpHasAudio !== "false"; const shouldPlay = Config.videoAutoplay || !hasAudio; if (shouldPlay) { playAutoplayVideo(video); } } }, true ); document.addEventListener( "error", (e) => { const target = e.target; if (!(target instanceof HTMLElement)) return; const media = target; const container = media.closest(".gsrp-gallery-item") || media.closest(".gsrp-preview-media"); if (!container) return; container.classList.remove("gsrp-is-loading"); if (media instanceof HTMLImageElement) { container.classList.add("gsrp-img-error"); container.setAttribute("data-error-text", L.imageUnavailable); return; } if (!(media instanceof HTMLVideoElement)) return; const videoMedia = media; if (videoMedia.dataset.gsrpBlobRescued) { const fallbackSrc2 = videoMedia.getAttribute("data-fallback-src"); if (fallbackSrc2) { triggerVideoFallback(videoMedia, fallbackSrc2); return; } container.classList.add("gsrp-video-error"); container.setAttribute("data-error-text", L.videoUnavailable); return; } videoMedia.dataset.gsrpBlobRescued = "1"; const videoUrl = videoMedia.src; if (videoUrl && (videoUrl.includes("redd.it") || videoUrl.includes("imgur.com") || videoUrl.includes("redditmedia.com"))) { container.classList.add("gsrp-is-loading"); let reqReferer = "https://www.reddit.com/"; let reqOrigin = "https://www.reddit.com"; if (videoUrl.includes("imgur.com")) { reqReferer = "https://imgur.com/"; reqOrigin = "https://imgur.com"; } const req = GM_xmlhttpRequest({ method: "GET", url: videoUrl, responseType: "blob", timeout: 8e3, headers: { Referer: reqReferer, Origin: reqOrigin }, onload: function(resp) { delete videoMedia.gsrpPendingRescueReq; if (resp.status === 200 && resp.response) { try { const forcedBlob = new Blob([resp.response], { type: "video/mp4" }); const blobUrl = URL.createObjectURL(forcedBlob); videoMedia.src = blobUrl; videoMedia.gsrpBlobUrl = blobUrl; const shouldPlay = Config.videoAutoplay || videoMedia.dataset.gsrpHasAudio === "false"; if (shouldPlay) { videoMedia.play().catch(() => { }); } } catch (err) { const fallbackSrc2 = videoMedia.getAttribute("data-fallback-src"); if (fallbackSrc2) triggerVideoFallback(videoMedia, fallbackSrc2); } } else { const fallbackSrc2 = videoMedia.getAttribute("data-fallback-src"); if (fallbackSrc2) triggerVideoFallback(videoMedia, fallbackSrc2); } }, onerror: function() { delete videoMedia.gsrpPendingRescueReq; const fallbackSrc2 = videoMedia.getAttribute("data-fallback-src"); if (fallbackSrc2) triggerVideoFallback(videoMedia, fallbackSrc2); }, ontimeout: function() { delete videoMedia.gsrpPendingRescueReq; const fallbackSrc2 = videoMedia.getAttribute("data-fallback-src"); if (fallbackSrc2) triggerVideoFallback(videoMedia, fallbackSrc2); } }); videoMedia.gsrpPendingRescueReq = req; return; } const fallbackSrc = videoMedia.getAttribute("data-fallback-src"); if (fallbackSrc) { triggerVideoFallback(videoMedia, fallbackSrc); return; } container.classList.add("gsrp-video-error"); container.setAttribute("data-error-text", L.videoUnavailable); }, true ); } function handleDynamicImageResolution(img) { if (!Config.showImageResolutionPreview) return; const w = img.naturalWidth; const h = img.naturalHeight; if (!w || !h) return; const item = img.closest(".gsrp-gallery-item"); if (!item) return; if (!item.dataset.w || !item.dataset.h) { item.dataset.w = String(w); item.dataset.h = String(h); } if (!item.querySelector(".gsrp-gallery-dims")) { const dimsSpan = document.createElement("span"); dimsSpan.className = "gsrp-gallery-dims"; dimsSpan.textContent = `${w} × ${h}`; item.appendChild(dimsSpan); } } function handleDynamicVideoResolution(video) { const w = video.videoWidth; const h = video.videoHeight; const item = video.closest(".gsrp-gallery-item"); if (item) { if (w && h && (!item.dataset.w || !item.dataset.h)) { item.dataset.w = String(w); item.dataset.h = String(h); } if (Config.showImageResolutionPreview && w && h && !item.querySelector(".gsrp-gallery-dims")) { const dimsSpan = document.createElement("span"); dimsSpan.className = "gsrp-gallery-dims"; dimsSpan.textContent = `${w} × ${h}`; item.appendChild(dimsSpan); } } const hasAudio = video.audioTracks && video.audioTracks.length > 0 || typeof video.webkitAudioDecodedByteCount === "number" && video.webkitAudioDecodedByteCount > 0 || typeof video.mozHasAudio === "boolean" && video.mozHasAudio || video.hasAudio === true || video.dataset.gsrpHasAudio === "true"; if (hasAudio) { video.controls = true; } } function triggerVideoFallback(video, fallbackSrc) { if (video.dataset.gsrpFallbackTriggered) return; video.dataset.gsrpFallbackTriggered = "true"; const parent = video.closest(".gsrp-gallery-item") || video.closest(".gsrp-preview-media"); const img = document.createElement("img"); img.src = fallbackSrc; img.className = "gsrp-inline-media-img"; img.style.maxWidth = "100%"; img.style.maxHeight = "min(30vh, 300px)"; img.style.borderRadius = "4px"; img.style.display = "block"; img.style.margin = "0 auto"; img.addEventListener("error", () => { if (parent) { parent.classList.remove("gsrp-is-loading"); parent.classList.add("gsrp-img-error"); parent.setAttribute("data-error-text", L.imageUnavailable || "⚠ Image unavailable"); } }); img.addEventListener("load", () => { if (parent) { parent.classList.remove("gsrp-is-loading"); parent.classList.remove("gsrp-video-error"); } }); if (parent) { parent.classList.remove("gsrp-video-error"); } video.replaceWith(img); } const COLLAPSE_THRESHOLD_PX = 130; function setupCollapsibleCaptions(container) { if (!container) return; const captions = container.querySelectorAll(".gsrp-preview-media-caption"); for (const caption of captions) { if (caption.dataset.gsrpCaptionProcessed) continue; caption.dataset.gsrpCaptionProcessed = "true"; const observer = new ResizeObserver(() => { if (!document.body.contains(caption)) { observer.disconnect(); return; } const scrollHeight = caption.scrollHeight; if (scrollHeight === 0) return; if (scrollHeight > COLLAPSE_THRESHOLD_PX) { caption.classList.add("gsrp-collapsible"); if (!caption.querySelector(".gsrp-caption-fade")) { const fade = document.createElement("div"); fade.className = "gsrp-caption-fade"; caption.appendChild(fade); } const nextEl = caption.nextElementSibling; if (!nextEl || !nextEl.classList.contains("gsrp-caption-toggle-btn")) { attachToggleButton(caption); } } observer.disconnect(); }); observer.observe(caption); } } function attachToggleButton(caption) { const btn = document.createElement("button"); btn.type = "button"; btn.className = "gsrp-caption-toggle-btn"; const gallery = caption.closest(".gsrp-preview-media-gallery"); const isGalleryExpanded = gallery && gallery.classList.contains("gsrp-all-captions-expanded"); btn.textContent = isGalleryExpanded ? L.showLess : L.showMore; if (isGalleryExpanded) { caption.classList.add("gsrp-is-expanded"); } btn.onclick = (e) => { e.preventDefault(); e.stopPropagation(); const ownGallery = btn.closest(".gsrp-preview-media-gallery"); if (ownGallery) { const isCurrentlyExpanded = ownGallery.classList.contains("gsrp-all-captions-expanded"); if (isCurrentlyExpanded) { ownGallery.classList.remove("gsrp-all-captions-expanded"); for (const cap of ownGallery.querySelectorAll(".gsrp-preview-media-caption")) { cap.classList.remove("gsrp-is-expanded"); } for (const b of ownGallery.querySelectorAll(".gsrp-caption-toggle-btn")) { b.textContent = L.showMore; } caption.scrollIntoView({ block: "nearest", behavior: "smooth" }); } else { ownGallery.classList.add("gsrp-all-captions-expanded"); for (const cap of ownGallery.querySelectorAll(".gsrp-preview-media-caption")) { cap.classList.add("gsrp-is-expanded"); } for (const b of ownGallery.querySelectorAll(".gsrp-caption-toggle-btn")) { b.textContent = L.showLess; } } } else { const isExpanded = caption.classList.contains("gsrp-is-expanded"); if (isExpanded) { caption.classList.remove("gsrp-is-expanded"); btn.textContent = L.showMore; caption.scrollIntoView({ block: "nearest", behavior: "smooth" }); } else { caption.classList.add("gsrp-is-expanded"); btn.textContent = L.showLess; } } }; caption.insertAdjacentElement("afterend", btn); } window.GSRP_setupCollapsibleCaptions = setupCollapsibleCaptions; function safeInstall(name, fn) { try { fn(); } catch (err) { logger.error(`safeInstall: "${name}" threw`, err); } } function setupBadgeInteractions(badge, link) { safeInstall("esc-key", () => installEscKeyHandler()); safeInstall("click-outside", () => installClickOutsideHandler()); safeInstall("fullscreen-auto-pin", () => installFullscreenAutoPin()); safeInstall("lightbox-click", () => installLightboxClickDelegate()); safeInstall("comment-collapse", () => installCommentCollapseDelegate()); safeInstall("more-load", () => installMoreLoadDelegate()); safeInstall("focus-trap", () => installFocusTrapDelegate()); safeInstall("youtube-embed", () => installYouTubeEmbedDelegate()); safeInstall("x-embed", () => installXEmbedDelegate()); safeInstall("bsky-embed", () => installBlueskyEmbedDelegate()); safeInstall("mastodon-embed", () => installMastodonEmbedDelegate()); safeInstall("code-copy", () => installCodeCopyDelegate()); safeInstall("vimeo-embed", () => installVimeoEmbedDelegate()); safeInstall("reddit-embed", () => installRedditEmbedDelegate()); safeInstall("dailymotion-embed", () => installDailymotionEmbedDelegate()); safeInstall("imgur-embed", () => installImgurEmbedDelegate()); safeInstall("preview-controls", () => installPreviewControlsDelegate()); safeInstall("attach-hls", () => attachHlsToVideos(badge)); safeInstall("attach-redgifs", () => attachRedgifsToVideos(badge)); safeInstall("attach-youtube-volume", () => attachYouTubeVolume(badge)); safeInstall("snapshot-iframe-srcs", () => snapshotIframeSrcs(badge)); safeInstall("process-youtube-embeds", () => processYouTubeEmbeds(badge)); safeInstall("process-x-embeds", () => processXEmbeds(badge)); safeInstall("process-bsky-embeds", () => processBlueskyEmbeds(badge)); safeInstall("process-mastodon-embeds", () => processMastodonEmbeds(badge)); safeInstall("process-code-blocks", () => processCodeBlocks(badge)); safeInstall("process-vimeo-embeds", () => processVimeoEmbeds(badge)); safeInstall("process-reddit-embeds", () => processRedditEmbeds(badge)); safeInstall("process-dailymotion-embeds", () => processDailymotionEmbeds(badge)); if (!badge.dataset.gsrpMousedownWired) { badge.dataset.gsrpMousedownWired = "true"; safeInstall("badge-mousedown", () => setupBadgeMousedown(badge)); } safeInstall("badge-filter-wiring", () => setupBadgeFilterWiring(badge)); safeInstall("badge-hover-intent", () => setupBadgeHoverIntent(badge)); safeInstall("badge-sort-wiring", () => setupBadgeSortWiring(badge, link)); safeInstall("badge-translate-wiring", () => setupBadgeTranslateWiring(badge)); safeInstall("imgur-embeds", () => setupImgurEmbeds(badge)); safeInstall("carousel", () => setupCarousel(badge)); safeInstall("collapsible-captions", () => setupCollapsibleCaptions(badge)); safeInstall("global-load-wiring", () => installGlobalLoadWiring()); safeInstall("active-health-check", () => activeHealthCheck(badge)); if (link && link.gsrpRemainingComments && link.gsrpRemainingComments.length > 0) { const remaining = link.gsrpRemainingComments; safeInstall("progressive-comments", () => setupProgressiveComments(badge, remaining)); } } function renderSidePanelLoadingInto({ panel, onClose }) { clearMediaIn(panel); delete panel.dataset.gsrpAutoTransTriggered; panel.style.display = "flex"; requestAnimationFrame(() => panel.classList.add("gsrp-active")); panel.innerHTML = ` <div class="gsrp-preview-title-container"> <span class="gsrp-preview-title">${renderSpinner(L.loading)}</span> <span class="gsrp-close-btn" tabindex="0" role="button" title="${L.closeBtn}" aria-label="${escapeHTML(L.closeBtn)}">✕</span> </div> <div class="gsrp-side-panel-body" style="flex:1; overflow-y:auto; scrollbar-gutter:stable; padding:16px;"> <div class="gsrp-loading-text">${renderSpinner(L.loading)}</div> </div> `; const closeBtn = panel.querySelector(".gsrp-close-btn"); if (closeBtn) { const handleCloseAction = (e) => { e.stopPropagation(); e.preventDefault(); onClose(); }; closeBtn.onclick = handleCloseAction; closeBtn.onkeydown = (e) => { if (e.key === "Enter" || e.key === " ") { handleCloseAction(e); } }; } } function renderSidePanelDataInto({ panel, data, link, onClose }) { clearMediaIn(panel); delete panel.dataset.gsrpAutoTransTriggered; panel.style.display = "flex"; requestAnimationFrame(() => panel.classList.add("gsrp-active")); const isCurrentlyMaximized = panel && panel.classList.contains("gsrp-maximized"); const maxBtnIcon = isCurrentlyMaximized ? "⤡" : "⛶"; const maxBtnTitle = isCurrentlyMaximized ? L.restoreBtn : L.maximizeBtn; const previewTitleHtml = renderPreviewTitle(data); const previewBodyHtml = renderPreviewContent(data); let commentsSectionHtml = ""; if (data.topComments && data.topComments.length > 0) { const initialComments = data.topComments.slice(0, 20); link.gsrpRemainingComments = data.topComments.slice(20); const partTooltip = `${L.uniqueParticipantsTitle}${data.topParticipants && data.topParticipants.length ? "\n\n" + L.topParticipantsLabel + "\n" + data.topParticipants.map((p) => `• ${p[0]} (${p[1]})`).join("\n") : ""}`; commentsSectionHtml = ` <div class="gsrp-comments-section-header"> <div class="gsrp-preview-title-container" style="border:none; padding:0; background:none; box-shadow:none; margin:0;"> <span class="gsrp-preview-title" title="${escapeHTML(L.topCommentsTitle)}" style="display:flex; align-items:center; gap:8px;"> <svg class="gsrp-header-icon" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path> </svg> <span class="gsrp-comments-count" title="${L.commentsDisplayedTitle}">(${data.displayedComments} / ${data.comments})</span> <span class="gsrp-participants-count" title="${escapeHTML(partTooltip)}"> <svg class="gsrp-participants-icon" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path> <circle cx="9" cy="7" r="4"></circle> <path d="M23 21v-2a4 4 0 0 0-3-3.87"></path> <path d="M16 3.13a4 4 0 0 1 0 7.75"></path> </svg> ${data.uniqueParticipants} </span> <select class="gsrp-inline-sort-select"> <option value="confidence" ${Config.commentSort === "confidence" ? "selected" : ""}>${L.sortBest}</option> <option value="top" ${Config.commentSort === "top" ? "selected" : ""}>${L.sortTop}</option> <option value="new" ${Config.commentSort === "new" ? "selected" : ""}>${L.sortNew}</option> <option value="controversial" ${Config.commentSort === "controversial" ? "selected" : ""}>${L.sortControversial}</option> <option value="old" ${Config.commentSort === "old" ? "selected" : ""}>${L.sortOld}</option> <option value="qa" ${Config.commentSort === "qa" ? "selected" : ""}>${L.sortQA}</option> </select> </span> <div class="gsrp-comments-filter-bar" style="margin-left:auto; display:flex; align-items:center; gap:8px;"> <span class="gsrp-comments-filter-input-wrap"> <svg class="gsrp-filter-search-icon" viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> <circle cx="11" cy="11" r="8"></circle> <line x1="21" y1="21" x2="16.65" y2="16.65"></line> </svg> <input type="search" class="gsrp-comments-filter-input" placeholder="${escapeHTML(L.filterCommentsPlaceholder)}" aria-label="${escapeHTML(L.filterCommentsPlaceholder)}"> <button type="button" class="gsrp-comments-filter-clear" tabindex="-1" aria-label="${escapeHTML(L.filterClearAria)}">×</button> </span> <span class="gsrp-comments-filter-counter"> <button type="button" class="gsrp-comments-filter-prev" tabindex="-1" aria-label="${escapeHTML(L.filterPrevAria)}">‹</button> <span class="gsrp-comments-filter-position"></span> <button type="button" class="gsrp-comments-filter-next" tabindex="-1" aria-label="${escapeHTML(L.filterNextAria)}">›</button> </span> <span class="gsrp-filter-settings-wrap"> <button type="button" class="gsrp-filter-settings-toggle-btn" title="${escapeHTML(L.filterSettingsTitle)}" aria-label="${escapeHTML(L.filterSettingsTitle)}"> <svg class="gsrp-filter-settings-toggle-icon" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> <polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"></polygon> </svg> </button> <div class="gsrp-filter-settings-popover"> <label class="gsrp-comments-filter-op-label" title="${escapeHTML(L.filterOpOnlyTitle)}"> <input type="checkbox" class="gsrp-comments-filter-op"> <span>${escapeHTML(L.filterOpOnly)}</span> </label> <label class="gsrp-comments-filter-mod-admin-label" title="${escapeHTML(L.filterModAdminTitle)}"> <input type="checkbox" class="gsrp-comments-filter-mod-admin"> <span>${escapeHTML(L.filterModAdmin)}</span> </label> <label class="gsrp-comments-filter-links-label" title="${escapeHTML(L.filterLinksTitle)}"> <input type="checkbox" class="gsrp-comments-filter-links"> <span>${escapeHTML(L.filterLinks)}</span> </label> <label class="gsrp-comments-filter-media-label" title="${escapeHTML(L.filterMediaTitle)}"> <input type="checkbox" class="gsrp-comments-filter-media"> <span>${escapeHTML(L.filterMedia)}</span> </label> <div class="gsrp-comments-filter-score-row"> <button type="button" class="gsrp-comments-filter-score-op-btn" title="${escapeHTML(L.filterScoreOpTitle || "Score filter operator")}" data-op="gte">≥</button> <input type="number" class="gsrp-comments-filter-score" placeholder="${escapeHTML(L.filterScorePlaceholder)}" title="${escapeHTML(L.filterScoreTitle)}" aria-label="${escapeHTML(L.filterScoreTitle)}"> <span class="gsrp-comments-filter-score-count" style="display: none;"></span> </div> </div> </span> <button type="button" class="gsrp-comments-collapse-toggle-btn" title="${escapeHTML(L.collapseToggleTitle)}" aria-label="${escapeHTML(L.collapseToggleTitle)}" aria-pressed="false"> <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> <line x1="4" y1="6" x2="20" y2="6"></line> <line x1="4" y1="12" x2="14" y2="12"></line> <line x1="4" y1="18" x2="8" y2="18"></line> </svg> </button> </div> </div> </div> <div class="gsrp-comments-list-wrapper" data-op-author="${escapeHTML(data.author)}" style="margin-top: 12px;"> ${renderCommentsList(initialComments)} <div class="gsrp-comments-no-matches">${escapeHTML(L.noMatchingComments)}</div> </div> `; } const sep = `<span class="gsrp-reddit-sep">·</span>`; const metaList = []; if (data.author === "[deleted]") { metaList.push( `<span class="gsrp-post-author gsrp-author-deleted">${escapeHTML(L.authorDeleted)}</span>` ); } else if (data.author) { metaList.push( `<a class="gsrp-post-author" href="https://www.reddit.com/user/${escapeHTML(data.author)}" target="_blank" rel="noopener noreferrer">${escapeHTML(data.author)}</a>` ); } if (data.flair && Config.showStatusTags) metaList.push(`<span class="gsrp-preview-flair-badge">${escapeHTML(data.flair)}</span>`); if (data.authorFlair && Config.showStatusTags) metaList.push( `<span class="gsrp-preview-flair-badge">${escapeHTML(data.authorFlair)}</span>` ); if (Config.showDate && data.date) { const dateTitle = data.edited ? `${L.editedTitle}${data.edited}` : L.dateTitle; const dateCls = data.edited ? "gsrp-meta-date gsrp-is-edited" : "gsrp-meta-date"; metaList.push( `<span class="${dateCls}" title="${dateTitle}">${escapeHTML(data.date)}</span>` ); } if (Config.showScore && data.score !== void 0) { metaList.push( `<span class="gsrp-meta-score" title="${L.ratioTitle}${data.ratio}%">⬆️ ${data.score}${Config.showUpvoteRatio && data.ratio !== void 0 ? ` (${data.ratio}%)` : ""}</span>` ); } if (Config.showAwards && data.awards > 0) metaList.push(`<span class="gsrp-meta-awards">🏆 ${data.awards}</span>`); if (Config.showCrossposts && data.crossposts > 0 && data.crosspostParent && Config.showStatusTags) { metaList.push( `<a href="https://www.reddit.com${data.crosspostParent.permalink}" target="_blank" rel="noopener noreferrer" class="gsrp-crosspost-link" title="${L.originalPost}">🔗 ${data.crossposts}</a>` ); } if (Config.showStatusTags) { if (data.isRemoved) { if (data.removedByCategory === "moderator") metaList.push( `<span class="gsrp-status-removed">${L.statusRemovedMod || L.deleted}</span>` ); else if (data.removedByCategory === "reddit") metaList.push( `<span class="gsrp-status-removed">${L.statusRemovedReddit || L.deleted}</span>` ); else if (data.removedByCategory === "deleted") metaList.push( `<span class="gsrp-status-removed">${L.statusDeleted || L.deleted}</span>` ); else metaList.push(`<span class="gsrp-status-removed">${L.deleted}</span>`); } if (data.isQuarantined) metaList.push( `<span class="gsrp-status-removed" title="${L.quarantinedTitle}">${L.quarantinedText}</span>` ); if (data.isLocked) metaList.push( `<span class="gsrp-status-warning" title="${L.locked}">${L.locked}</span>` ); if (data.isSolved) metaList.push(`<span class="gsrp-status-success">${L.solved}</span>`); if (data.isArchived) metaList.push( `<span class="gsrp-status-warning" title="${L.archived}">${L.archived}</span>` ); if (data.isStickied) metaList.push( `<span class="gsrp-status-success" title="${L.statusPinned}">${L.statusPinned}</span>` ); if (data.isOC) metaList.push(`<span class="gsrp-status-oc" title="${L.statusOC}">OC</span>`); } if (data.isSpoiler) metaList.push(`<span class="gsrp-status-spoiler" title="${L.spoiler}">${L.spoiler}</span>`); if (data.isNSFW) metaList.push(`<span class="gsrp-status-nsfw">🔞</span>`); const metaBarHtml = metaList.length > 0 ? `<div class="gsrp-preview-meta-bar">${metaList.join(sep)}</div>` : ""; panel.innerHTML = ` <div class="gsrp-preview-title-container"> <span class="gsrp-preview-title" title="${escapeHTML(L.previewTitle)}" style="display:flex; align-items:center;"> <svg class="gsrp-header-icon" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path> <polyline points="14 2 14 8 20 8"></polyline> <line x1="16" y1="13" x2="8" y2="13"></line> <line x1="16" y1="17" x2="8" y2="17"></line> <polyline points="10 9 9 9 8 9"></polyline> </svg> </span> ${Config.enableTranslate ? `<button type="button" class="gsrp-translate-btn" title="${escapeHTML(L.translateBtnTitle || "Toggle Translation")}"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 998.1 998.3" width="16" height="16" style="vertical-align:middle;"><path fill="#DBDBDB" d="M931.7 998.3c36.5 0 66.4-29.4 66.4-65.4V265.8c0-36-29.9-65.4-66.4-65.4H283.6l260.1 797.9h388z"/><path fill="#DCDCDC" d="M931.7 230.4c9.7 0 18.9 3.8 25.8 10.6 6.8 6.7 10.6 15.5 10.6 24.8v667.1c0 9.3-3.7 18.1-10.6 24.8-6.9 6.8-16.1 10.6-25.8 10.6H565.5L324.9 230.4h606.8m0-30H283.6l260.1 797.9h388c36.5 0 66.4-29.4 66.4-65.4V265.8c0-36-29.9-65.4-66.4-65.4z"/><polygon fill="#4352B8" points="482.3,809.8 543.7,998.3 714.4,809.8"/><path fill="#607988" d="M936.1 476.1V437H747.6v-63.2h-61.2V437H566.1v39.1h239.4c-12.8 45.1-41.1 87.7-68.7 120.8-48.9-57.9-49.1-76.7-49.1-76.7h-50.8s2.1 28.2 70.7 108.6c-22.3 22.8-39.2 36.3-39.2 36.3l15.6 48.8s23.6-20.3 53.1-51.6c29.6 32.1 67.8 70.7 117.2 116.7l32.1-32.1c-52.9-48-91.7-86.1-120.2-116.7 38.2-45.2 77-102.1 85.2-154.2H936v.1z"/><path fill="#4285F4" d="M66.4 0C29.9 0 0 29.9 0 66.5v677c0 36.5 29.9 66.4 66.4 66.4h648.1L454.4 0h-388z"/><path fill="#EEEEEE" d="M371.4 430.6c-2.5 30.3-28.4 75.2-91.1 75.2-54.3 0-98.3-44.9-98.3-100.2s44-100.2 98.3-100.2c30.9 0 51.5 13.4 63.3 24.3l41.2-39.6c-27.1-25-62.4-40.6-104.5-40.6-86.1 0-156 69.9-156 156s69.9 156 156 156c90.2 0 149.8-63.3 149.8-152.6 0-12.8-1.6-22.2-3.7-31.8h-146v53.4l91 .1z"/></svg></button>` : ""} ${Config.enableSummarize ? `<button type="button" class="gsrp-summarize-btn" title="${escapeHTML(L.summarizeBtnTitle)}" style="font-size:16px; line-height:1; padding:0 2px;">🤖</button>` : ""} <button type="button" class="gsrp-copy-markdown-btn" title="${escapeHTML(L.copyMarkdownBtnTitle)}" aria-label="${escapeHTML(L.copyMarkdownBtnTitle)}" style="margin-left:auto;"> <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect> <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path> </svg> </button> <span class="gsrp-maximize-btn" tabindex="0" role="button" title="${maxBtnTitle}" aria-label="${escapeHTML(maxBtnTitle)}">${maxBtnIcon}</span> <span class="gsrp-close-btn" tabindex="0" role="button" title="${L.closeBtn}" aria-label="${escapeHTML(L.closeBtn)}">✕</span> </div> <div class="gsrp-side-panel-body" style="flex:1; overflow-y:auto; scrollbar-gutter:stable; padding:0; display:flex; flex-direction:column;"> ${previewTitleHtml} ${metaBarHtml} ${previewBodyHtml ? `<div class="gsrp-preview-body">${previewBodyHtml}</div>` : ""} ${commentsSectionHtml} </div> `; setupBadgeInteractions(panel, link); requestAnimationFrame(() => { restoreMediaIn(panel); }); const closeBtn = panel.querySelector(".gsrp-close-btn"); if (closeBtn) { const handleCloseAction = (e) => { e.stopPropagation(); e.preventDefault(); onClose(); }; closeBtn.onclick = handleCloseAction; closeBtn.onkeydown = (e) => { if (e.key === "Enter" || e.key === " ") { handleCloseAction(e); } }; } } const state = { panelEl: null, wrapperEl: null, pinnedCard: null, activeCard: null, renderedCard: null, leaveTimer: null, get phase() { if (this.pinnedCard) return "pinned"; if (this.activeCard) return "hovering"; return "idle"; } }; function transitionTo(phase, opts = {}) { const card = opts.card ?? null; switch (phase) { case "idle": state.pinnedCard = null; state.activeCard = null; state.renderedCard = null; clearLeaveTimer(); break; case "hovering": state.activeCard = card; state.renderedCard = card; break; case "pinned": state.pinnedCard = card ?? state.renderedCard; break; default: throw new Error(`[side-panel] unknown phase: ${phase}`); } return state.phase; } function clearActiveCard(card) { if (state.activeCard === card) { state.activeCard = null; } } function clearLeaveTimer() { if (state.leaveTimer) { clearTimeout(state.leaveTimer); state.leaveTimer = null; } } function findRedditHref(card) { const a = card.querySelector('a[href*="reddit.com/r/"][href*="/comments/"]') || card.querySelector('a[href*="redd.it/"]') || card.querySelector('a[href*="reddit.com/s/"]'); if (!a || !(a instanceof HTMLAnchorElement)) return null; let url = a.href.split("?")[0]; if (url.endsWith("/")) url = url.slice(0, -1); return url; } function isSameCard(cardA, cardB) { if (!cardA || !cardB) return false; if (cardA === cardB) return true; const hrefA = findRedditHref(cardA); const hrefB = findRedditHref(cardB); return hrefA !== null && hrefA === hrefB; } function isSidePanelWaitingFor(card) { if (isSameCard(state.activeCard, card)) return true; if (isSameCard(state.renderedCard, card) && state.leaveTimer !== null) return true; return false; } function isSidePanelModeActive() { return Config.previewMode === "side-panel" && window.innerWidth >= 1e3; } function updateSidePanelPosition() { applySidePanelPosition(state); } let resizeScrollRequest = null; function handleResizeScroll() { if (state.phase === "idle") return; if (resizeScrollRequest !== null) return; resizeScrollRequest = requestAnimationFrame(() => { resizeScrollRequest = null; if (state.phase !== "idle") { applySidePanelPosition(state); } }); } function handleDocumentMousedown(e) { if (!state.panelEl || !state.pinnedCard) return; if (!(e.target instanceof Element)) return; if (state.panelEl.contains(e.target)) return; if (e.target.closest(".gsrp-lightbox-overlay") || e.target.closest("#gsrp-settings-modal") || e.target.closest(".gsrp-settings-overlay")) { return; } closeSidePanel(); } function initSidePanel() { const allPanels = Array.from( document.querySelectorAll("#gsrp-global-side-panel") ); let activePanel = null; if (state.panelEl && document.contains(state.panelEl)) { activePanel = state.panelEl; } else { activePanel = allPanels.find((p) => document.contains(p)) || null; } allPanels.forEach((p) => { if (p !== activePanel) { const wrapper = p.closest(".gsrp-reddit-badge") || p.parentElement; if (wrapper && wrapper !== document.body) { wrapper.remove(); } else { p.remove(); } } }); if (activePanel) { state.panelEl = activePanel; state.wrapperEl = activePanel.closest(".gsrp-reddit-badge") || activePanel.parentElement; return activePanel; } if (state.panelEl) { state.panelEl = null; state.wrapperEl = null; } try { state.wrapperEl = document.createElement("div"); state.wrapperEl.className = "gsrp-reddit-badge" + (detectDarkMode() ? " gsrp-is-dark" : ""); state.wrapperEl.style.position = "static"; state.panelEl = document.createElement("div"); state.panelEl.id = "gsrp-global-side-panel"; state.panelEl.className = "gsrp-side-panel gsrp-preview-tooltip" + (detectDarkMode() ? " gsrp-is-dark" : ""); state.panelEl.style.display = "none"; state.wrapperEl.appendChild(state.panelEl); const net = document.createElement("div"); net.className = "gsrp-side-panel-safe-net"; net.addEventListener("mousedown", (e) => { if (e.button !== 0) return; closeSidePanel(); const target = document.elementFromPoint(e.clientX, e.clientY); if (target && target instanceof HTMLElement) { const link = target.closest("a"); if (link && link instanceof HTMLAnchorElement) { if (e.ctrlKey || e.metaKey) { window.open(link.href, "_blank"); } else { link.click(); } } else { target.click(); if (["INPUT", "TEXTAREA"].includes(target.tagName)) { target.focus(); } } } }); state.wrapperEl.appendChild(net); document.body.appendChild(state.wrapperEl); } catch (err) { console.error("Error creating side panel container:", err); } if (isSidePanelModeActive()) { if (!document.body.classList.contains("gsrp-side-panel-active-body")) { document.body.classList.add("gsrp-side-panel-active-body"); } } else { document.body.classList.remove("gsrp-side-panel-active-body"); } window.removeEventListener("resize", handleResizeScroll); window.removeEventListener("scroll", handleResizeScroll); window.addEventListener("resize", handleResizeScroll, { passive: true }); window.addEventListener("scroll", handleResizeScroll, { passive: true }); updateSidePanelPosition(); document.removeEventListener("mousedown", handleDocumentMousedown); document.addEventListener("mousedown", handleDocumentMousedown); if (state.wrapperEl) { state.wrapperEl.addEventListener("mouseenter", clearLeaveTimer); state.wrapperEl.addEventListener("mouseleave", () => { startLeaveTimer(); }); } return state.panelEl; } function renderSidePanelLoading() { const panel = initSidePanel(); updateSidePanelPosition(); renderSidePanelLoadingInto({ panel, onClose: closeSidePanel }); } function renderSidePanelData(data, link) { const panel = initSidePanel(); panel.gsrpPostData = data; updateSidePanelPosition(); renderSidePanelDataInto({ panel, data, link, onClose: closeSidePanel }); let checkCount = 0; const maxChecks = 5; const checkInterval = 100; const intervalId = setInterval(() => { checkCount++; if (checkCount > maxChecks) { clearInterval(intervalId); return; } const isClosed = !state.panelEl || state.panelEl.style.display === "none" || !state.renderedCard; if (isClosed) { const allResiduals = Array.from( document.querySelectorAll("#gsrp-global-side-panel") ); allResiduals.forEach((p) => { if (p !== state.panelEl) { const wrapper = p.closest(".gsrp-reddit-badge") || p.parentElement; if (wrapper && wrapper !== document.body) { wrapper.remove(); } else { p.remove(); } } else { p.style.display = "none"; p.classList.remove("gsrp-active", "gsrp-pinned", "gsrp-maximized"); } }); clearInterval(intervalId); return; } const allPanels = Array.from( document.querySelectorAll("#gsrp-global-side-panel") ); if (allPanels.length > 1) { allPanels.forEach((p) => { if (p !== state.panelEl) { const wrapper = p.closest(".gsrp-reddit-badge") || p.parentElement; if (wrapper && wrapper !== document.body) { wrapper.remove(); } else { p.remove(); } } }); } if (state.panelEl && state.panelEl.querySelector(".gsrp-loading-text")) { renderSidePanelDataInto({ panel: state.panelEl, data, link, onClose: closeSidePanel }); } }, checkInterval); } function autoPinSidePanel() { if (!state.renderedCard || state.pinnedCard === state.renderedCard) return; transitionTo("pinned"); if (state.panelEl) { state.panelEl.classList.add("gsrp-pinned"); } if (state.pinnedCard) { state.pinnedCard.classList.add("gsrp-card-focus-highlight"); } } function handleCardMouseEnter(cardNode, link, dataFetcher) { clearLeaveTimer(); if (state.renderedCard && state.renderedCard !== cardNode && state.pinnedCard !== state.renderedCard) { state.renderedCard.classList.remove("gsrp-card-focus-highlight"); } transitionTo("hovering", { card: cardNode }); if (state.pinnedCard) { return; } cardNode.classList.add("gsrp-card-focus-highlight"); renderSidePanelLoading(); dataFetcher((data) => { if (isSameCard(state.activeCard, cardNode) || isSameCard(state.pinnedCard, cardNode)) { renderSidePanelData(data, link); } }); } function handleCardMouseLeave(cardNode) { clearActiveCard(cardNode); startLeaveTimer(); } function startLeaveTimer() { clearLeaveTimer(); state.leaveTimer = setTimeout(() => { const isHoveringCard = state.activeCard && state.activeCard.matches(":hover"); const isHoveringPinned = state.pinnedCard && state.pinnedCard.matches(":hover"); const isHoveringPanel = state.wrapperEl && state.wrapperEl.matches(":hover"); const hasActiveFocus = state.panelEl && state.panelEl.contains(document.activeElement); let isHoveringAnyCard = false; if (!isHoveringCard && !isHoveringPinned) { const hoverSelector = ITEM_SELECTOR.split(",").map((sel) => `${sel.trim()}:hover`).join(","); const hoveredCard = document.querySelector(hoverSelector); if (hoveredCard) { isHoveringAnyCard = true; state.activeCard = hoveredCard; state.renderedCard = hoveredCard; hoveredCard.classList.add("gsrp-card-focus-highlight"); } } if (isHoveringCard || isHoveringPinned || isHoveringPanel || hasActiveFocus || isHoveringAnyCard) { clearLeaveTimer(); return; } if (!state.pinnedCard) { closeSidePanel(); } clearLeaveTimer(); }, 150); } function closeSidePanel() { const wasPinned = state.pinnedCard; const wasActive = state.activeCard; const wasRendered = state.renderedCard; transitionTo("idle"); if (wasPinned) wasPinned.classList.remove("gsrp-card-focus-highlight"); if (wasActive) wasActive.classList.remove("gsrp-card-focus-highlight"); if (wasRendered) wasRendered.classList.remove("gsrp-card-focus-highlight"); if (state.panelEl) { clearMediaIn(state.panelEl); delete state.panelEl.dataset.gsrpAutoTransTriggered; state.panelEl.classList.remove("gsrp-active", "gsrp-pinned", "gsrp-maximized"); state.panelEl.style.display = "none"; } } function renderSpinner(textStr) { return `<span class="gsrp-spinner-wrap" style="display:inline-flex; align-items:center; gap:6px; font-weight:normal;"> <svg class="gsrp-spinner" xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> <line x1="12" y1="2" x2="12" y2="6"></line><line x1="12" y1="18" x2="12" y2="22"></line> <line x1="4.93" y1="4.93" x2="7.76" y2="7.76"></line><line x1="16.24" y1="16.24" x2="19.07" y2="19.07"></line> <line x1="2" y1="12" x2="6" y2="12"></line><line x1="18" y1="12" x2="22" y2="12"></line> <line x1="4.93" y1="19.07" x2="7.76" y2="16.24"></line><line x1="16.24" y1="7.76" x2="19.07" y2="4.93"></line> </svg> <span>${escapeHTML(textStr)}</span> </span>`; } function renderBadgeContent(data) { const html2 = []; const sep = `<span class="gsrp-reddit-sep">·</span>`; const previewTitleHtml = renderPreviewTitle(data); const previewBodyHtml = renderPreviewContent(data); const isSidePanel = isSidePanelModeActive(); if (!isSidePanel && (previewTitleHtml || previewBodyHtml)) { const metaList = []; if (data.author === "[deleted]") { metaList.push( `<span class="gsrp-post-author gsrp-author-deleted">${escapeHTML(L.authorDeleted)}</span>` ); } else if (data.author) { metaList.push( `<a class="gsrp-post-author" href="https://www.reddit.com/user/${escapeHTML(data.author)}" target="_blank" rel="noopener noreferrer">${escapeHTML(data.author)}</a>` ); } if (data.flair && Config.showStatusTags) metaList.push( `<span class="gsrp-preview-flair-badge">${escapeHTML(data.flair)}</span>` ); if (data.authorFlair && Config.showStatusTags) metaList.push( `<span class="gsrp-preview-flair-badge">${escapeHTML(data.authorFlair)}</span>` ); if (Config.showDate && data.date) { const dateTitle = data.edited ? `${L.editedTitle}${data.edited}` : L.dateTitle; const dateCls = data.edited ? "gsrp-meta-date gsrp-is-edited" : "gsrp-meta-date"; metaList.push( `<span class="${dateCls}" title="${dateTitle}">${escapeHTML(data.date)}</span>` ); } if (Config.showScore && data.score !== void 0) { metaList.push( `<span class="gsrp-meta-score" title="${L.ratioTitle}${data.ratio}%">⬆️ ${data.score}${Config.showUpvoteRatio && data.ratio !== void 0 ? ` (${data.ratio}%)` : ""}</span>` ); } if (Config.showAwards && data.awards > 0) metaList.push(`<span class="gsrp-meta-awards">🏆 ${data.awards}</span>`); if (Config.showCrossposts && data.crossposts > 0 && data.crosspostParent && Config.showStatusTags) { metaList.push( `<a href="https://www.reddit.com${data.crosspostParent.permalink}" target="_blank" rel="noopener noreferrer" class="gsrp-crosspost-link" title="${L.originalPost}">🔗 ${data.crossposts}</a>` ); } if (Config.showStatusTags) { if (data.isRemoved) { if (data.removedByCategory === "moderator") metaList.push( `<span class="gsrp-status-removed">${L.statusRemovedMod || L.deleted}</span>` ); else if (data.removedByCategory === "reddit") metaList.push( `<span class="gsrp-status-removed">${L.statusRemovedReddit || L.deleted}</span>` ); else if (data.removedByCategory === "deleted") metaList.push( `<span class="gsrp-status-removed">${L.statusDeleted || L.deleted}</span>` ); else metaList.push(`<span class="gsrp-status-removed">${L.deleted}</span>`); } if (data.isQuarantined) metaList.push( `<span class="gsrp-status-removed" title="${L.quarantinedTitle}">${L.quarantinedText}</span>` ); if (data.isLocked) metaList.push( `<span class="gsrp-status-warning" title="${L.locked}">${L.locked}</span>` ); if (data.isSolved) metaList.push(`<span class="gsrp-status-success">${L.solved}</span>`); if (data.isArchived) metaList.push( `<span class="gsrp-status-warning" title="${L.archived}">${L.archived}</span>` ); if (data.isStickied) metaList.push( `<span class="gsrp-status-success" title="${L.statusPinned}">${L.statusPinned}</span>` ); if (data.isOC) metaList.push(`<span class="gsrp-status-oc" title="${L.statusOC}">OC</span>`); } if (data.isSpoiler) metaList.push( `<span class="gsrp-status-spoiler" title="${L.spoiler}">${L.spoiler}</span>` ); if (data.isNSFW) metaList.push(`<span class="gsrp-status-nsfw">🔞</span>`); const metaBarHtml = metaList.length > 0 ? `<div class="gsrp-preview-meta-bar">${metaList.join(sep)}</div>` : ""; html2.push(` <div class="gsrp-preview-trigger"> <span class="gsrp-preview-trigger-label" title="${L.clickToPin}">${L.previewIcon}</span> <div class="gsrp-preview-tooltip"> <div class="gsrp-preview-title-container"> <span class="gsrp-preview-title" title="${escapeHTML(L.previewTitle)}" style="display:flex; align-items:center;"> <svg class="gsrp-header-icon" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path> <polyline points="14 2 14 8 20 8"></polyline> <line x1="16" y1="13" x2="8" y2="13"></line> <line x1="16" y1="17" x2="8" y2="17"></line> <polyline points="10 9 9 9 8 9"></polyline> </svg> </span> ${Config.enableTranslate ? `<button type="button" class="gsrp-translate-btn" title="${escapeHTML(L.translateBtnTitle || "Toggle Translation")}"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 998.1 998.3" width="16" height="16" style="vertical-align:middle;"><path fill="#DBDBDB" d="M931.7 998.3c36.5 0 66.4-29.4 66.4-65.4V265.8c0-36-29.9-65.4-66.4-65.4H283.6l260.1 797.9h388z"/><path fill="#DCDCDC" d="M931.7 230.4c9.7 0 18.9 3.8 25.8 10.6 6.8 6.7 10.6 15.5 10.6 24.8v667.1c0 9.3-3.7 18.1-10.6 24.8-6.9 6.8-16.1 10.6-25.8 10.6H565.5L324.9 230.4h606.8m0-30H283.6l260.1 797.9h388c36.5 0 66.4-29.4 66.4-65.4V265.8c0-36-29.9-65.4-66.4-65.4z"/><polygon fill="#4352B8" points="482.3,809.8 543.7,998.3 714.4,809.8"/><path fill="#607988" d="M936.1 476.1V437H747.6v-63.2h-61.2V437H566.1v39.1h239.4c-12.8 45.1-41.1 87.7-68.7 120.8-48.9-57.9-49.1-76.7-49.1-76.7h-50.8s2.1 28.2 70.7 108.6c-22.3 22.8-39.2 36.3-39.2 36.3l15.6 48.8s23.6-20.3 53.1-51.6c29.6 32.1 67.8 70.7 117.2 116.7l32.1-32.1c-52.9-48-91.7-86.1-120.2-116.7 38.2-45.2 77-102.1 85.2-154.2H936v.1z"/><path fill="#4285F4" d="M66.4 0C29.9 0 0 29.9 0 66.5v677c0 36.5 29.9 66.4 66.4 66.4h648.1L454.4 0h-388z"/><path fill="#EEEEEE" d="M371.4 430.6c-2.5 30.3-28.4 75.2-91.1 75.2-54.3 0-98.3-44.9-98.3-100.2s44-100.2 98.3-100.2c30.9 0 51.5 13.4 63.3 24.3l41.2-39.6c-27.1-25-62.4-40.6-104.5-40.6-86.1 0-156 69.9-156 156s69.9 156 156 156c90.2 0 149.8-63.3 149.8-152.6 0-12.8-1.6-22.2-3.7-31.8h-146v53.4l91 .1z"/></svg></button>` : ""} ${Config.enableSummarize ? `<button type="button" class="gsrp-summarize-btn" title="${escapeHTML(L.summarizeBtnTitle)}" style="font-size:16px; line-height:1; padding:0 2px;">🤖</button>` : ""} <button type="button" class="gsrp-copy-markdown-btn" title="${escapeHTML(L.copyMarkdownBtnTitle)}" aria-label="${escapeHTML(L.copyMarkdownBtnTitle)}" style="margin-left:auto;"> <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect> <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path> </svg> </button> <span class="gsrp-maximize-btn" tabindex="0" role="button" title="${L.maximizeBtn}" aria-label="${escapeHTML(L.maximizeBtn)}">⛶</span><span class="gsrp-close-btn" tabindex="0" role="button" title="${L.closeBtn}" aria-label="${escapeHTML(L.closeBtn)}">✕</span> </div> ${previewTitleHtml} ${metaBarHtml} ${previewBodyHtml ? `<div class="gsrp-preview-body">${previewBodyHtml}</div>` : ""} </div> </div> `); } if (data.topComments && data.topComments.length > 0) { const partTooltip = `${L.uniqueParticipantsTitle}${data.topParticipants && data.topParticipants.length ? "\n\n" + L.topParticipantsLabel + "\n" + data.topParticipants.map((p) => `• ${p[0]} (${p[1]})`).join("\n") : ""}`; if (isSidePanel) { html2.push(`💬 ${data.comments}`); } else { html2.push(` <div class="gsrp-preview-trigger"> <span class="gsrp-preview-trigger-label" title="${L.clickToPin}">💬 ${data.comments}</span> <div class="gsrp-preview-tooltip"> <div class="gsrp-preview-title-container"> <span class="gsrp-preview-title" title="${escapeHTML(L.topCommentsTitle)}" style="display:flex; align-items:center;"> <svg class="gsrp-header-icon" viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path> </svg> <span class="gsrp-comments-count" title="${L.commentsDisplayedTitle}">(${data.displayedComments} / ${data.comments})</span> <span class="gsrp-participants-count" title="${escapeHTML(partTooltip)}"> <svg class="gsrp-participants-icon" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path> <circle cx="9" cy="7" r="4"></circle> <path d="M23 21v-2a4 4 0 0 0-3-3.87"></path> <path d="M16 3.13a4 4 0 0 1 0 7.75"></path> </svg> ${data.uniqueParticipants} </span> <select class="gsrp-inline-sort-select"> <option value="confidence" ${Config.commentSort === "confidence" ? "selected" : ""}>${L.sortBest}</option> <option value="top" ${Config.commentSort === "top" ? "selected" : ""}>${L.sortTop}</option> <option value="new" ${Config.commentSort === "new" ? "selected" : ""}>${L.sortNew}</option> <option value="controversial" ${Config.commentSort === "controversial" ? "selected" : ""}>${L.sortControversial}</option> <option value="old" ${Config.commentSort === "old" ? "selected" : ""}>${L.sortOld}</option> <option value="qa" ${Config.commentSort === "qa" ? "selected" : ""}>${L.sortQA}</option> </select> </span> <div class="gsrp-comments-filter-bar"> <span class="gsrp-comments-filter-input-wrap"> <svg class="gsrp-filter-search-icon" viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> <circle cx="11" cy="11" r="8"></circle> <line x1="21" y1="21" x2="16.65" y2="16.65"></line> </svg> <input type="search" class="gsrp-comments-filter-input" placeholder="${escapeHTML(L.filterCommentsPlaceholder)}" aria-label="${escapeHTML(L.filterCommentsPlaceholder)}"> <button type="button" class="gsrp-comments-filter-clear" tabindex="-1" aria-label="${escapeHTML(L.filterClearAria)}">×</button> </span> <span class="gsrp-comments-filter-counter"> <button type="button" class="gsrp-comments-filter-prev" tabindex="-1" aria-label="${escapeHTML(L.filterPrevAria)}">‹</button> <span class="gsrp-comments-filter-position"></span> <button type="button" class="gsrp-comments-filter-next" tabindex="-1" aria-label="${escapeHTML(L.filterNextAria)}">›</button> </span> <span class="gsrp-filter-settings-wrap"> <button type="button" class="gsrp-filter-settings-toggle-btn" title="${escapeHTML(L.filterSettingsTitle)}" aria-label="${escapeHTML(L.filterSettingsTitle)}"> <svg class="gsrp-filter-settings-toggle-icon" viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> <polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"></polygon> </svg> </button> <div class="gsrp-filter-settings-popover"> <label class="gsrp-comments-filter-op-label" title="${escapeHTML(L.filterOpOnlyTitle)}"> <input type="checkbox" class="gsrp-comments-filter-op"> <span>${escapeHTML(L.filterOpOnly)}</span> </label> <label class="gsrp-comments-filter-mod-admin-label" title="${escapeHTML(L.filterModAdminTitle)}"> <input type="checkbox" class="gsrp-comments-filter-mod-admin"> <span>${escapeHTML(L.filterModAdmin)}</span> </label> <label class="gsrp-comments-filter-links-label" title="${escapeHTML(L.filterLinksTitle)}"> <input type="checkbox" class="gsrp-comments-filter-links"> <span>${escapeHTML(L.filterLinks)}</span> </label> <label class="gsrp-comments-filter-media-label" title="${escapeHTML(L.filterMediaTitle)}"> <input type="checkbox" class="gsrp-comments-filter-media"> <span>${escapeHTML(L.filterMedia)}</span> </label> <div class="gsrp-comments-filter-score-row"> <button type="button" class="gsrp-comments-filter-score-op-btn" title="${escapeHTML(L.filterScoreOpTitle || "Score filter operator")}" data-op="gte">≥</button> <input type="number" class="gsrp-comments-filter-score" placeholder="${escapeHTML(L.filterScorePlaceholder)}" title="${escapeHTML(L.filterScoreTitle)}" aria-label="${escapeHTML(L.filterScoreTitle)}"> <span class="gsrp-comments-filter-score-count" style="display: none;"></span> </div> </div> </span> <button type="button" class="gsrp-comments-collapse-toggle-btn" title="${escapeHTML(L.collapseToggleTitle)}" aria-label="${escapeHTML(L.collapseToggleTitle)}" aria-pressed="false"> <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> <line x1="4" y1="6" x2="20" y2="6"></line> <line x1="4" y1="12" x2="14" y2="12"></line> <line x1="4" y1="18" x2="8" y2="18"></line> </svg> </button> ${Config.enableTranslate ? `<button type="button" class="gsrp-translate-btn" title="${escapeHTML(L.translateBtnTitle || "Toggle Translation")}"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 998.1 998.3" width="16" height="16" style="vertical-align:middle;"><path fill="#DBDBDB" d="M931.7 998.3c36.5 0 66.4-29.4 66.4-65.4V265.8c0-36-29.9-65.4-66.4-65.4H283.6l260.1 797.9h388z"/><path fill="#DCDCDC" d="M931.7 230.4c9.7 0 18.9 3.8 25.8 10.6 6.8 6.7 10.6 15.5 10.6 24.8v667.1c0 9.3-3.7 18.1-10.6 24.8-6.9 6.8-16.1 10.6-25.8 10.6H565.5L324.9 230.4h606.8m0-30H283.6l260.1 797.9h388c36.5 0 66.4-29.4 66.4-65.4V265.8c0-36-29.9-65.4-66.4-65.4z"/><polygon fill="#4352B8" points="482.3,809.8 543.7,998.3 714.4,809.8"/><path fill="#607988" d="M936.1 476.1V437H747.6v-63.2h-61.2V437H566.1v39.1h239.4c-12.8 45.1-41.1 87.7-68.7 120.8-48.9-57.9-49.1-76.7-49.1-76.7h-50.8s2.1 28.2 70.7 108.6c-22.3 22.8-39.2 36.3-39.2 36.3l15.6 48.8s23.6-20.3 53.1-51.6c29.6 32.1 67.8 70.7 117.2 116.7l32.1-32.1c-52.9-48-91.7-86.1-120.2-116.7 38.2-45.2 77-102.1 85.2-154.2H936v.1z"/><path fill="#4285F4" d="M66.4 0C29.9 0 0 29.9 0 66.5v677c0 36.5 29.9 66.4 66.4 66.4h648.1L454.4 0h-388z"/><path fill="#EEEEEE" d="M371.4 430.6c-2.5 30.3-28.4 75.2-91.1 75.2-54.3 0-98.3-44.9-98.3-100.2s44-100.2 98.3-100.2c30.9 0 51.5 13.4 63.3 24.3l41.2-39.6c-27.1-25-62.4-40.6-104.5-40.6-86.1 0-156 69.9-156 156s69.9 156 156 156c90.2 0 149.8-63.3 149.8-152.6 0-12.8-1.6-22.2-3.7-31.8h-146v53.4l91 .1z"/></svg></button>` : ""} </div> <button type="button" class="gsrp-copy-markdown-btn" title="${escapeHTML(L.copyMarkdownBtnTitle)}" aria-label="${escapeHTML(L.copyMarkdownBtnTitle)}" style="margin-left:auto;"> <svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"> <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect> <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path> </svg> </button> <span class="gsrp-maximize-btn" tabindex="0" role="button" title="${L.maximizeBtn}" aria-label="${escapeHTML(L.maximizeBtn)}">⛶</span><span class="gsrp-close-btn" tabindex="0" role="button" title="${L.closeBtn}" aria-label="${escapeHTML(L.closeBtn)}">✕</span> </div> <div class="gsrp-comments-list-wrapper" data-op-author="${escapeHTML(data.author)}"> ${renderCommentsList(data.topComments)} <div class="gsrp-comments-no-matches">${escapeHTML(L.noMatchingComments)}</div> </div> </div> </div> `); } } else { html2.push(`💬 ${data.comments}`); } if (Config.showScore) { let scoreHtml = `⬆️ ${data.score}`; if (Config.showUpvoteRatio && data.ratio !== void 0 && !isNaN(data.ratio)) { scoreHtml += ` <span class="gsrp-score-ratio">(${data.ratio}%)</span>`; } html2.push( `<span class="gsrp-score" title="${L.ratioTitle}${data.ratio}%">${scoreHtml}</span>` ); } if (Config.showDate) { if (data.edited) { html2.push( `<span class="gsrp-is-edited" title="${L.editedTitle}${data.edited}">📅 ${data.date}</span>` ); } else { html2.push(`<span title="${L.dateTitle}">📅 ${data.date}</span>`); } } if (Config.showStatusTags) { if (data.isRemoved) { if (data.removedByCategory === "moderator") html2.push( `<span class="gsrp-status-removed">${L.statusRemovedMod || L.deleted}</span>` ); else if (data.removedByCategory === "reddit") html2.push( `<span class="gsrp-status-removed">${L.statusRemovedReddit || L.deleted}</span>` ); else if (data.removedByCategory === "deleted") html2.push( `<span class="gsrp-status-removed">${L.statusDeleted || L.deleted}</span>` ); else html2.push(`<span class="gsrp-status-removed">${L.deleted}</span>`); } if (data.isQuarantined) html2.push( `<span class="gsrp-status-removed" title="${L.quarantinedTitle}">${L.quarantinedText}</span>` ); if (data.isLocked) html2.push(`<span class="gsrp-status-warning" title="${L.locked}">${L.locked}</span>`); if (data.isSolved) html2.push(`<span class="gsrp-status-success">${L.solved}</span>`); if (data.isArchived) html2.push( `<span class="gsrp-status-warning" title="${L.archived}">${L.archived}</span>` ); if (data.isStickied) html2.push( `<span class="gsrp-status-success" title="${L.statusPinned}">${L.statusPinned}</span>` ); if (data.isOC) html2.push(`<span class="gsrp-status-oc" title="${L.statusOC}">${L.statusOC}</span>`); if (data.isContestMode) html2.push( `<span class="gsrp-status-warning" title="${L.contestModeTitle}">${L.contestModeText}</span>` ); if (data.isHiddenScore) html2.push( `<span class="gsrp-status-warning" title="${L.hiddenScoreTitle}">${L.hiddenScoreText}</span>` ); } else { if (data.isRemoved) html2.push(`<span class="gsrp-status-removed">${L.deleted}</span>`); else if (data.isLocked) html2.push(`<span class="gsrp-status-warning" title="${L.locked}">${L.locked}</span>`); else if (data.isSolved) html2.push(`<span class="gsrp-status-success">${L.solved}</span>`); else if (data.isArchived) html2.push( `<span class="gsrp-status-warning" title="${L.archived}">${L.archived}</span>` ); } if (data.isSpoiler) html2.push(`<span class="gsrp-status-spoiler" title="${L.spoiler}">${L.spoiler}</span>`); if (data.isNSFW) html2.push(`<span class="gsrp-status-nsfw">🔞</span>`); if (data.crosspostParent && Config.showStatusTags) { html2.push( `<a href="https://www.reddit.com${data.crosspostParent.permalink}" target="_blank" rel="noopener noreferrer" class="gsrp-crosspost-link" title="${L.originalPost}">${L.crosspostedFrom}${data.crosspostParent.subreddit}</a>` ); } if (data.flair && !data.isSolved) { html2.push( `<span class="gsrp-flair" title="${L.flairTitle}: ${escapeHTML(data.flair)}">🏷️ ${escapeHTML(data.flair)}</span>` ); } if (data.isVideo) { if (data.videoDuration) { const m = Math.floor(data.videoDuration / 60); const s = (data.videoDuration % 60).toString().padStart(2, "0"); html2.push(`<span title="${L.videoTitle}">🎥 ${m}:${s}</span>`); } else { html2.push(`<span title="${L.videoTitle}">🎥</span>`); } } else if (data.isGallery) { if (data.galleryCount > 0) { const captionAttr = data.galleryCaption ? ` - ${escapeHTML(data.galleryCaption)}` : ""; const galleryPayload = data.media && data.media.type === "gallery" && Array.isArray(data.media.images) ? escapeHTML( JSON.stringify( data.media.images.map((img) => ({ u: img.url, w: img.width || null, h: img.height || null })) ) ) : ""; html2.push( `<button type="button" class="gsrp-gallery-badge" data-gallery-images="${galleryPayload}" title="${L.galleryTitle} (${data.galleryCount})${captionAttr}">🖼️ ${data.galleryCount}</button>` ); } else { html2.push(`<span title="${L.galleryTitle}">🖼️</span>`); } } if (Config.showAwards && data.awards > 0) html2.push(`<span class="gsrp-awards">🏆 ${data.awards}</span>`); if (Config.showCrossposts && data.crossposts > 0) { if (data.id && data.subreddit) { const slug = (data.permalink || "").split("/")[5] || ""; const dupUrl = `https://old.reddit.com/r/${data.subreddit}/duplicates/${data.id}/${slug}/`; html2.push( `<a class="gsrp-crossposts-link" href="${escapeHTML(dupUrl)}" target="_blank" rel="noopener noreferrer" title="${escapeHTML(L.crosspostsTitle)}">🔀 ${data.crossposts}</a>` ); } else { html2.push(`🔀 ${data.crossposts}`); } } return html2.join(sep); } const PROGRESSIVE_INITIAL_COUNT = 20; const ERROR_INFO_KEYS = { RATE_LIMIT: { textKey: "rateLimitText", titleKey: "rateLimitTitle" }, HTTP_ERR: { textKey: "errHttpText", titleKey: "errHttpTitle" }, NET_ERR: { textKey: "errNetText", titleKey: "errNetTitle" }, PARSE_ERR: { textKey: "errParseText", titleKey: "errParseTitle" }, TIMEOUT: { textKey: "errTimeoutText", titleKey: "errTimeoutTitle" } }; function createEmptyBadge() { const badge = document.createElement("div"); badge.className = "gsrp-reddit-badge"; if (detectDarkMode()) badge.classList.add("gsrp-is-dark"); return badge; } function applyProgressiveSlice(link, data) { const allComments = data.topComments || []; const initialComments = allComments.slice(0, PROGRESSIVE_INITIAL_COUNT); link.gsrpRemainingComments = allComments.slice(PROGRESSIVE_INITIAL_COUNT); return { ...data, topComments: initialComments }; } function renderCachedBadge({ badge, link, data }) { const displayData = applyProgressiveSlice(link, data); badge.innerHTML = renderBadgeContent(displayData); badge.gsrpPostData = data; setupBadgeInteractions(badge, link); link.dataset.gsrpRedditStat = "done"; } function renderLoadingBadge(badge) { badge.innerHTML = renderSpinner(L.loading); } function renderFetchedBadge({ badge, link, data, node, isSidePanelActive }) { link.dataset.gsrpRedditStat = "done"; const displayData = applyProgressiveSlice(link, data); badge.innerHTML = renderBadgeContent(displayData); badge.gsrpPostData = data; setupBadgeInteractions(badge, link); const isWaiting = isSidePanelWaitingFor(node); if (isSidePanelActive && isWaiting) { renderSidePanelData(data, link); } } function renderBadgeError({ badge, link, err, cleanUrl, canRetry, onRetry }) { link.dataset.gsrpRedditStat = "error"; const errKey = ERROR_INFO_KEYS[err] || ERROR_INFO_KEYS.HTTP_ERR; logger.warn(`Fetch failed (${err}) for ${cleanUrl}`); let titleText = L[errKey.titleKey] || ""; if (err === "RATE_LIMIT") { const waitMs = getRateLimitWaitMs(); if (waitMs > 0) { const seconds = Math.ceil(waitMs / 1e3); titleText = (L.rateLimitWaitTitle || titleText).replace("{seconds}", String(seconds)); } } const retryHtml = canRetry ? ` <button type="button" class="gsrp-retry-btn" title="${escapeHTML(L.retryTitle)}">${escapeHTML(L.retryBtn)}</button>` : ""; badge.innerHTML = `<span class="gsrp-error-msg" title="${escapeHTML(titleText)}">${L[errKey.textKey]}${retryHtml}</span>`; if (!canRetry || typeof onRetry !== "function") return; const btn = badge.querySelector(".gsrp-retry-btn"); if (btn) { btn.addEventListener("click", (ev) => { ev.stopPropagation(); onRetry(); }); } } function onCardEnter(node, link, dataFetcher) { if (isSidePanelModeActive()) { handleCardMouseEnter(node, link, dataFetcher); } } function onCardLeave(node) { if (isSidePanelModeActive()) { handleCardMouseLeave(node); } } let initializedLinks = new WeakSet(); let hoverDelegateInstalled = false; let currentlyHoveredCard = null; let domRemovalObserver = null; function setupDomRemovalObserver() { if (domRemovalObserver) return; if (typeof window === "undefined" || typeof MutationObserver === "undefined") return; domRemovalObserver = new MutationObserver((mutations) => { for (const mutation of mutations) { if (mutation.removedNodes.length === 0) continue; mutation.removedNodes.forEach((node) => { if (currentlyHoveredCard && node.contains(currentlyHoveredCard)) { currentlyHoveredCard = null; } if (typeof HTMLElement !== "undefined" && node instanceof HTMLElement) { const hasBadge = node.classList.contains("gsrp-reddit-badge") || node.querySelector(".gsrp-reddit-badge") !== null; if (!hasBadge) return; const badges = node.classList.contains("gsrp-reddit-badge") ? [node] : Array.from(node.querySelectorAll(".gsrp-reddit-badge")); badges.forEach((badge) => { clearMediaIn(badge); const listWrapper = badge.classList.contains("gsrp-comments-list-wrapper") ? badge : badge.querySelector(".gsrp-comments-list-wrapper"); if (listWrapper && typeof listWrapper.gsrpCancel === "function") { listWrapper.gsrpCancel(); } const wiring = badgeWiringMap.get(badge); if (wiring) { if (typeof wiring.cleanupFilter === "function") { wiring.cleanupFilter(); } if (typeof wiring.cancelTranslate === "function") { wiring.cancelTranslate(); } badgeWiringMap.delete(badge); } const tooltip = badge.querySelector( ".gsrp-preview-tooltip" ); if (tooltip && tooltipObserver) { tooltipObserver.unobserve(tooltip); } const trigger = badge.closest(".gsrp-preview-trigger") || badge.querySelector(".gsrp-preview-trigger"); if (trigger) { trigger.classList.remove( "gsrp-hover-sticky", "gsrp-pinned", "gsrp-active" ); } }); } }); } }); domRemovalObserver.observe(document.body || document.documentElement, { childList: true, subtree: true }); } function handleGlobalMouseover(e) { if (!(e.target instanceof Element)) return; if (!document.contains(e.target)) return; if (currentlyHoveredCard && currentlyHoveredCard.contains(e.target)) return; const card = e.target.closest("[data-gsrp-tracked-card]"); if (card === currentlyHoveredCard) return; if (currentlyHoveredCard) { const handlers = cardHandlersMap.get(currentlyHoveredCard); if (handlers && typeof handlers.gsrpHoverLeave === "function") { handlers.gsrpHoverLeave(); } } currentlyHoveredCard = card; if (card) { const handlers = cardHandlersMap.get(card); if (handlers && typeof handlers.gsrpHoverEnter === "function") { handlers.gsrpHoverEnter(); } } } function installHoverDelegate() { if (hoverDelegateInstalled) return; hoverDelegateInstalled = true; window.addEventListener("mouseover", handleGlobalMouseover, true); } function findRedditLink(node) { if (!node) return null; const stdLink = node.querySelector( 'a[href*="reddit.com/r/"][href*="/comments/"]' ); if (stdLink) return stdLink; return node.querySelector('a[href*="redd.it/"], a[href*="reddit.com/s/"]'); } const MAX_AUTO_RETRIES = 1; function injectBadge(node) { setupDomRemovalObserver(); const link = findRedditLink(node); if (!link) return; if (initializedLinks.has(link)) return; if (link.dataset.gsrpRedditStat) { node.querySelectorAll(":scope > .gsrp-reddit-badge").forEach((b) => b.remove()); link.removeAttribute("data-gsrp-reddit-stat"); delete link.dataset.gsrpRedditStat; const linkState = linkStateMap.get(link); if (linkState) { linkState.fetchWaiters = null; } } initializedLinks.add(link); link.dataset.gsrpRedditStat = "bound"; let cleanUrl = link.href.split("?")[0]; if (cleanUrl.endsWith("/")) cleanUrl = cleanUrl.slice(0, -1); const getCacheKey = () => `${cleanUrl}?sort=${Config.commentSort}`; const cacheKey = getCacheKey(); const anchorCardForBadge = () => { if (window.getComputedStyle(node).position === "static") { node.style.position = "relative"; } if (node.offsetWidth > 0) { node.style.setProperty("--gsrp-card-width", node.offsetWidth + "px"); } }; if (redditDataCache.has(cacheKey)) { const badge = createEmptyBadge(); anchorCardForBadge(); node.appendChild(badge); renderCachedBadge({ badge, link, data: redditDataCache.get(cacheKey) }); } let hoverTimeout = null; const dataFetcher = createDataFetcher(link, cleanUrl, getCacheKey); let isHovered = false; const handleEnter = () => { if (isHovered) return; isHovered = true; anchorCardForBadge(); hoverTimeout = setTimeout(() => { const isSidePanelActive = isSidePanelModeActive(); onCardEnter(node, link, (cb) => { dataFetcher(cb, null); }); const badge = node.querySelector(":scope > .gsrp-reddit-badge"); if (badge && !isSidePanelActive) { const hasTooltip = !!badge.querySelector(".gsrp-preview-tooltip"); if (!hasTooltip) { dataFetcher((data) => { renderFetchedBadge({ badge, link, data, node, isSidePanelActive: false }); }, null); } } const hasBadge = !!node.querySelector(":scope > .gsrp-reddit-badge"); if (!hasBadge && link.dataset.gsrpRedditStat !== "error") { node.querySelectorAll(":scope > .gsrp-reddit-badge").forEach((b) => b.remove()); const freshBadge = createEmptyBadge(); renderLoadingBadge(freshBadge); node.appendChild(freshBadge); let retryCount = 0; const processData = (data) => { renderFetchedBadge({ badge: freshBadge, link, data, node, isSidePanelActive }); }; const showError = (err) => { if (!isHovered) { link.removeAttribute("data-gsrp-reddit-stat"); delete link.dataset.gsrpRedditStat; node.querySelectorAll(":scope > .gsrp-reddit-badge").forEach( (b) => b.remove() ); initializedLinks.delete(link); return; } const canRetry = retryCount < MAX_AUTO_RETRIES; renderBadgeError({ badge: freshBadge, link, err, cleanUrl, canRetry, onRetry: () => { retryCount += 1; link.dataset.gsrpRedditStat = "fetching"; renderLoadingBadge(freshBadge); fetchRedditData( cleanUrl, Config.commentSort, (data) => { redditDataCache.set(cacheKey, data); processData(data); }, showError ); } }); }; dataFetcher(processData, showError); } }, Config.hoverDelay); }; const handleLeave = () => { if (!isHovered) return; isHovered = false; if (currentlyHoveredCard === node) { currentlyHoveredCard = null; } if (hoverTimeout) { clearTimeout(hoverTimeout); hoverTimeout = null; } onCardLeave(node); if (link.dataset.gsrpRedditStat === "error") { link.removeAttribute("data-gsrp-reddit-stat"); delete link.dataset.gsrpRedditStat; node.querySelectorAll(":scope > .gsrp-reddit-badge").forEach((b) => b.remove()); initializedLinks.delete(link); } }; const trackedNode = node; const oldHandlers = cardHandlersMap.get(trackedNode); if (oldHandlers) { if (oldHandlers.gsrpMouseEnter) { node.removeEventListener("mouseenter", oldHandlers.gsrpMouseEnter); } if (oldHandlers.gsrpMouseLeave) { node.removeEventListener("mouseleave", oldHandlers.gsrpMouseLeave); } } node.addEventListener("mouseenter", handleEnter); node.addEventListener("mouseleave", handleLeave); cardHandlersMap.set(trackedNode, { gsrpMouseEnter: handleEnter, gsrpMouseLeave: handleLeave, gsrpHoverEnter: handleEnter, gsrpHoverLeave: handleLeave }); node.dataset.gsrpTrackedCard = "1"; installHoverDelegate(); } const ITEM_SELECTOR = "div.g, div.hlcw0c, div.MjjYud, div.gsc-webResult.gsc-result"; const DEBOUNCE_MS = 150; const SCROLL_RESCUE_MS = 300; function scanAndInjectAll(root) { if (!root) return 0; let count = 0; try { const nodes = root.querySelectorAll(ITEM_SELECTOR); for (const node of nodes) { if (!isInsideExcludedAncestor(node)) { injectBadge(node); count++; } } } catch { } return count; } function isInsideExcludedAncestor(node) { if (!(node instanceof Element)) return false; return !!(node.closest("#gsrp-global-side-panel") || node.closest(".g-accordion-container") || node.closest(".gsrp-preview-tooltip")); } function findMainSearchContainer() { return document.getElementById("rcnt") || document.getElementById("search") || document.getElementById("center_col"); } function setupObserver(rootElement) { let pendingNodes = []; let debounceTimer = null; let activeObserver = null; let isObservingMainContainer = false; const flushPending = () => { if (pendingNodes.length === 0) return; const batch = pendingNodes; pendingNodes = []; const seen = new Set(); const run = () => { for (const node of batch) { if (!node || !(node instanceof Element) || seen.has(node)) continue; seen.add(node); if (isInsideExcludedAncestor(node)) continue; if (node.matches(ITEM_SELECTOR)) { injectBadge(node); } else { scanAndInjectAll(node); } } }; if (window.requestIdleCallback) { window.requestIdleCallback(run, { timeout: 200 }); } else { run(); } }; const startObserving = (target, deep) => { if (activeObserver) { activeObserver.disconnect(); } activeObserver = new MutationObserver((mutations) => { for (const mutation of mutations) { for (const node of mutation.addedNodes) { if (node.nodeType === 1) { pendingNodes.push(node); if (!isObservingMainContainer) { const container = findMainSearchContainer(); if (container) { isObservingMainContainer = true; startObserving(container, true); scanAndInjectAll(container); return; } } } } } if (pendingNodes.length === 0) return; if (debounceTimer != null) clearTimeout(debounceTimer); debounceTimer = setTimeout(flushPending, DEBOUNCE_MS); }); activeObserver.observe(target, { childList: true, subtree: deep }); }; const mainContainer = findMainSearchContainer(); if (mainContainer) { isObservingMainContainer = true; startObserving(mainContainer, true); } else { isObservingMainContainer = false; startObserving(rootElement, true); } let scrollTimer = null; const onScroll = () => { if (scrollTimer != null) clearTimeout(scrollTimer); scrollTimer = setTimeout(() => { scrollTimer = null; scanAndInjectAll(document); }, SCROLL_RESCUE_MS); }; window.addEventListener("scroll", onScroll, { passive: true }); return function teardown2() { if (activeObserver) { activeObserver.disconnect(); activeObserver = null; } if (debounceTimer != null) { clearTimeout(debounceTimer); debounceTimer = null; } if (scrollTimer != null) { clearTimeout(scrollTimer); scrollTimer = null; } window.removeEventListener("scroll", onScroll); }; } function init() { injectStyles(); if (detectDarkMode()) { document.body.classList.add("gsrp-is-dark"); document.documentElement.classList.add("gsrp-is-dark"); } if (isSidePanelModeActive()) { document.body.classList.add("gsrp-side-panel-active-body"); } if (typeof GM_registerMenuCommand === "function") { GM_registerMenuCommand(L.settingsTitle, openSettingsModal); } const root = document.body; scanAndInjectAll(document); const teardown2 = setupObserver(root); window.addEventListener("beforeunload", teardown2); if (typeof requestIdleCallback === "function") { requestIdleCallback(() => redditDataCache.cleanupExpired()); } else { setTimeout(() => redditDataCache.cleanupExpired(), 1); } } if (typeof window !== "undefined" && window.self === window.top) { if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init); } else { init(); } } })();