Play multiple videos or chatrooms simultaneously in new tabs or windows, with the ability to pin and enlarge any item on top. Features include bookmark launch, spoofed resolution (fine-tune auto quality), homepage frame (custom links), and more instant switching. Supports list switching within the blob page.
// ==UserScript==
// @name YouTube Multi-Player
// @name:zh-TW YouTube 多重播放器
// @namespace http://tampermonkey.net/
// @author Dxzy
// @version 8.13.0
// @match https://www.youtube.com/*
// @exclude https://studio.youtube.com/*
// @exclude https://accounts.youtube.com/*
// @exclude https://www.youtube.com/live_chat*
// @exclude https://www.youtube.com/embed/*
// @exclude https://www.youtube-nocookie.com/*
// @grant GM_info
// @grant GM_registerMenuCommand
// @license MIT
// @description:zh-TW 以新分頁或新視窗同時播放多個影片或聊天室,並可將任意項目放大置頂。書籤啟動、偽造解析度(微調自動畫質)、首頁框架(自訂連結)、更多即時切換。支援 Blob 頁內自由切換清單。
// @description Play multiple videos or chatrooms simultaneously in new tabs or windows, with the ability to pin and enlarge any item on top. Features include bookmark launch, spoofed resolution (fine-tune auto quality), homepage frame (custom links), and more instant switching. Supports list switching within the blob page.
// ==/UserScript==
(function () {
'use strict';
if (window.top !== window && /^(\/watch|\/embed|\/live_chat|\/accounts|\/studio)/.test(location.pathname)) return;
const IS_SUB_FRAME = window.top !== window;
let isAddButtonEnabled = IS_SUB_FRAME || localStorage.getItem('ytMulti_addButtonEnabled') === 'true';
const CONFIG = {
MAX_PINNED: 3,
PORTRAIT_HEIGHT_THRESHOLD: 1.1,
LANDSCAPE_ASPECT_RATIO_THRESHOLD: 1.7,
LANDSCAPE_COLUMN_CONFIG: [2, 5, 10, 17, 26, 37, 50, 65],
PORTRAIT_MAX_COLUMNS: 4,
LIST_COUNT: 4,
SCREEN_WIDTH_DEFAULT: 1920,
RESOLUTION_PRESETS: [3, 4, 5, 6, 7],
RESOLUTION_LEVELS: {
1: [3840, 2160],
2: [2560, 1440],
3: [1920, 1080],
4: [1280, 720],
5: [854, 480],
6: [640, 360],
7: [432, 240],
8: [256, 144],
},
SPOOF_RESOLUTION_ENABLED: true,
HOME_PINNED_BY_DEFAULT: false,
VIDEO_PINNED_BY_DEFAULT: false,
BTN_SIZE_SCALE: 100,
ADD_BUTTON_ENABLED_STORAGE_KEY: 'ytMulti_addButtonEnabled',
SYNC_EVENT_KEY: 'ytMulti_syncEvent',
ASPECT_RATIO_STANDARD: 16 / 9,
CHAT_SUFFIX: '_chat',
VIDEO_SUFFIX: '_video',
DOMAIN_MODE_STORAGE_KEY: 'ytMulti_domainMode',
VIDEO_DOMAIN_MODE_PREFIX: 'ytMulti_vidDomain_',
DEFAULT_DOMAIN_MODE: 'YT',
DOMAIN_MODES: [
{ key: 'YT', domain: 'www.youtube.com', label: 'YT' },
{ key: 'YU', domain: 'www.youtube-nocookie.com', label: 'YU' },
],
POLLING_INTERVAL: 2000,
RESIZE_DEBOUNCE: 100,
HOVER_TIMEOUT: 5000,
HOME_TARGET_URL: '',
BTN_LAYOUT: 3,
BTN_HIDE_TIMEOUT: 5000,
LIST_NAMES: '',
BUTTON_INJECTION_STATS: false,
};
const LANG = {
zh: {
play: '▶ 播放',
playIcon: '▶',
modeCurrentTab: '分頁',
modeNewTab: '分頁+',
modeNewWindow: '視窗',
list: '清單',
noVideos: '當前清單無影片',
addButton: '添加',
addIcon: '+',
settingsTitle: '🔧 修改設定值',
settingsPrompt: '請輸入要修改的設定項目編號(輸入 0 退出):\n',
settingsValuePrompt: '\n當前值: {current}\n預設值: {default}\n說明: {desc}\n請輸入新值(留空即使用空白值,輸入 0 取消): ',
settingsSaved: '✅ 設定已儲存',
settingsCancelled: '❌ 已取消',
settingsInvalid: '⚠️ 輸入無效',
settingsExit: '👋 已退出',
descMaxPinned: '最多可同時置頂的影片數量',
descListCount: '影片清單總數量',
descPortraitMaxCols: '縱向螢幕最大欄數',
descLandscapeConfig: '橫向欄數閾值(逗號分隔)',
descPortraitThreshold: '縱向高度倍數閾值',
descLandscapeAR: '橫向寬高比閾值',
descScreenWidth: '偽造解析度時使用的螢幕寬度基準值',
descResolutionPresets: '解析度檔位 [1.3840x2160 2.2560x1440 3.1920x1080 4.1280x720 5.854x480 6.640x360 7.432x240 8.256x144]',
descSpoofResolution: '偽造解析度開關:1=開啟 2=關閉',
descHomePinnedDefault: '新增首頁時預設置頂:1=開啟 2=關閉',
descVideoPinnedDefault: '新增影片時預設置頂:1=開啟 2=關閉',
descBtnSizeScale: '懸浮按鈕大小縮放比例(百分比,預設100,輸入200為兩倍大)',
descHomeTargetUrl: '首頁框架目標 YouTube 連結(留空則恢復預設首頁)',
descBtnLayout: '懸浮按鈕排列方式:1=直列左側 2=直列右側 3=橫行',
descBtnHideTimeout: '懸浮按鈕無操作後自動隱藏的時間(毫秒)',
descListNames: '自訂清單名稱(逗號分隔,如:主清單,音樂,遊戲)',
descBtnStats: '統計按鈕成功注入選擇器的次數(僅記憶體,重啟腳本清零):1=開啟 2=關閉',
descDefaultDomainMode: '新影片預設嵌入模式:1=YT(原生) 2=YU(nocookie)',
spoofOn: '開啟',
spoofOff: '關閉',
layout1: '直列左',
layout2: '直列右',
layout3: '橫行',
emptyVal: '(空白)',
btnToggleDomain: 'T',
btnToggleFrame: 'F',
exportAll: '匯入全部清單',
},
en: {
play: '▶ Play',
playIcon: '▶',
modeCurrentTab: 'Tab',
modeNewTab: 'Tab+',
modeNewWindow: 'Window',
list: 'List',
noVideos: 'No videos in current list',
addButton: 'Add',
addIcon: '+',
settingsTitle: '🔧 Modify Settings',
settingsPrompt: 'Enter setting number (0 to exit):\n',
settingsValuePrompt: '\nCurrent: {current}\nDefault: {default}\nDesc: {desc}\nNew value (empty for blank, 0 to cancel): ',
settingsSaved: '✅ Settings saved',
settingsCancelled: '❌ Cancelled',
settingsInvalid: '⚠️ Invalid input',
settingsExit: '👋 Exited',
descMaxPinned: 'Max pinned videos',
descListCount: 'Total list count',
descPortraitMaxCols: 'Max columns in portrait',
descLandscapeConfig: 'Landscape column thresholds (comma-separated)',
descPortraitThreshold: 'Portrait height multiplier threshold',
descLandscapeAR: 'Landscape AR threshold',
descScreenWidth: 'Screen width baseline for resolution spoofing',
descResolutionPresets: 'Resolution levels [1.3840x2160 2.2560x1440 3.1920x1080 4.1280x720 5.854x480 6.640x360 7.432x240 8.256x144]',
descSpoofResolution: 'Spoof resolution toggle: 1=Enable 2=Disable',
descHomePinnedDefault: 'Pin new homepages by default: 1=Enable 2=Disable',
descVideoPinnedDefault: 'Pin new videos by default: 1=Enable 2=Disable',
descBtnSizeScale: 'Floating button size scale (percent, default 100, 200 for 2x)',
descHomeTargetUrl: 'Target YouTube URL for home frame (empty restores default)',
descBtnLayout: 'Floating button layout: 1=Vertical-Left 2=Vertical-Right 3=Horizontal',
descBtnHideTimeout: 'Auto-hide timeout for floating buttons after inactivity (ms)',
descListNames: 'Custom list names (comma-separated, e.g., Main,Music,Gaming)',
descBtnStats: 'Track successful button injections per selector (memory only): 1=Enable 2=Disable',
descDefaultDomainMode: 'Default embed mode for new videos: 1=YT(native) 2=YU(nocookie)',
spoofOn: 'ON',
spoofOff: 'OFF',
layout1: 'V-Left',
layout2: 'V-Right',
layout3: 'Horizontal',
emptyVal: '(empty)',
btnToggleDomain: 'T',
btnToggleFrame: 'F',
exportAll: 'Import All Lists',
},
};
const LANG_CODE = navigator.language.startsWith('zh') ? 'zh' : 'en';
const t = (key, params = {}) => {
let str = LANG[LANG_CODE][key] || key;
for (const [k, v] of Object.entries(params)) str = str.replace(`{${k}}`, v);
return str;
};
const CONFIG_ITEMS = {
MAX_PINNED: { default: CONFIG.MAX_PINNED, type: 'int', min: 1, max: 10, descZh: LANG.zh.descMaxPinned, descEn: LANG.en.descMaxPinned },
LIST_COUNT: { default: CONFIG.LIST_COUNT, type: 'int', min: 1, max: 10, descZh: LANG.zh.descListCount, descEn: LANG.en.descListCount },
PORTRAIT_MAX_COLUMNS: {
default: CONFIG.PORTRAIT_MAX_COLUMNS,
type: 'int',
min: 1,
max: 6,
descZh: LANG.zh.descPortraitMaxCols,
descEn: LANG.en.descPortraitMaxCols,
},
LANDSCAPE_COLUMN_CONFIG: {
default: [...CONFIG.LANDSCAPE_COLUMN_CONFIG],
type: 'array',
descZh: LANG.zh.descLandscapeConfig,
descEn: LANG.en.descLandscapeConfig,
},
PORTRAIT_HEIGHT_THRESHOLD: {
default: CONFIG.PORTRAIT_HEIGHT_THRESHOLD,
type: 'float',
min: 1.0,
max: 2.0,
step: 0.1,
descZh: LANG.zh.descPortraitThreshold,
descEn: LANG.en.descPortraitThreshold,
},
LANDSCAPE_ASPECT_RATIO_THRESHOLD: {
default: CONFIG.LANDSCAPE_ASPECT_RATIO_THRESHOLD,
type: 'float',
min: 1.0,
max: 3.0,
step: 0.1,
descZh: LANG.zh.descLandscapeAR,
descEn: LANG.en.descLandscapeAR,
},
SCREEN_WIDTH_DEFAULT: {
default: CONFIG.SCREEN_WIDTH_DEFAULT,
type: 'int',
min: 144,
max: 3840,
descZh: LANG.zh.descScreenWidth,
descEn: LANG.en.descScreenWidth,
},
RESOLUTION_PRESETS: {
default: [...CONFIG.RESOLUTION_PRESETS],
type: 'preset',
descZh: LANG.zh.descResolutionPresets,
descEn: LANG.en.descResolutionPresets,
},
SPOOF_RESOLUTION_ENABLED: {
default: CONFIG.SPOOF_RESOLUTION_ENABLED,
type: 'toggle12',
descZh: LANG.zh.descSpoofResolution,
descEn: LANG.en.descSpoofResolution,
},
HOME_PINNED_BY_DEFAULT: {
default: CONFIG.HOME_PINNED_BY_DEFAULT,
type: 'toggle12',
descZh: LANG.zh.descHomePinnedDefault,
descEn: LANG.en.descHomePinnedDefault,
},
VIDEO_PINNED_BY_DEFAULT: {
default: CONFIG.VIDEO_PINNED_BY_DEFAULT,
type: 'toggle12',
descZh: LANG.zh.descVideoPinnedDefault,
descEn: LANG.en.descVideoPinnedDefault,
},
BTN_SIZE_SCALE: {
default: CONFIG.BTN_SIZE_SCALE,
type: 'int',
min: 50,
max: 300,
step: 10,
descZh: LANG.zh.descBtnSizeScale,
descEn: LANG.en.descBtnSizeScale,
},
HOME_TARGET_URL: { default: '', type: 'string', descZh: LANG.zh.descHomeTargetUrl, descEn: LANG.en.descHomeTargetUrl },
BTN_LAYOUT: { default: 3, type: 'int', min: 1, max: 3, descZh: LANG.zh.descBtnLayout, descEn: LANG.en.descBtnLayout },
BTN_HIDE_TIMEOUT: {
default: 5000,
type: 'int',
min: 1000,
max: 30000,
step: 500,
descZh: LANG.zh.descBtnHideTimeout,
descEn: LANG.en.descBtnHideTimeout,
},
LIST_NAMES: { default: CONFIG.LIST_NAMES, type: 'string', descZh: LANG.zh.descListNames, descEn: LANG.en.descListNames },
BUTTON_INJECTION_STATS: {
default: CONFIG.BUTTON_INJECTION_STATS,
type: 'toggle12',
descZh: LANG.zh.descBtnStats,
descEn: LANG.en.descBtnStats,
},
DEFAULT_DOMAIN_MODE: {
default: 'YT',
type: 'toggleYT',
descZh: LANG.zh.descDefaultDomainMode,
descEn: LANG.en.descDefaultDomainMode,
},
};
const SETTINGS_PREFIX = 'ytMulti_setting_';
const parseSettingValue = (stored, cfg) => {
try {
if (cfg.type === 'array' || cfg.type === 'preset') {
const val = JSON.parse(stored);
if (!Array.isArray(val)) return cfg.default;
if (cfg.type === 'preset') return val.filter((n) => Number.isInteger(n) && n >= 1 && n <= 8).slice(0, 5);
return val.filter((n) => Number.isInteger(n));
} else if (cfg.type === 'bool' || cfg.type === 'toggle12') {
return stored === 'true';
} else if (cfg.type === 'toggleYT') {
return stored === 'YU' ? 'YU' : 'YT';
} else if (cfg.type === 'float') {
const val = parseFloat(stored);
if (isNaN(val) || val < cfg.min || val > cfg.max) return cfg.default;
return val;
} else if (cfg.type === 'string') {
return stored;
} else {
const val = parseInt(stored, 10);
if (isNaN(val) || val < cfg.min || val > cfg.max) return cfg.default;
return val;
}
} catch (e) {
return cfg.default;
}
};
const loadSettings = () => {
for (const [key, cfg] of Object.entries(CONFIG_ITEMS)) {
const stored = localStorage.getItem(SETTINGS_PREFIX + key);
if (stored !== null) CONFIG[key] = parseSettingValue(stored, cfg);
}
};
const safeJSONParse = (str, fallback) => {
if (str === null || str === undefined || str === '') return fallback;
try {
return JSON.parse(str);
} catch (e) {
return fallback;
}
};
const generateStorageKeys = () => {
const k = {};
for (let i = 1; i <= CONFIG.LIST_COUNT; i++) k[`list${i}`] = `ytMulti_videoList${i}`;
return k;
};
loadSettings();
let STORAGE_LISTS = generateStorageKeys();
const initializeStorage = () => {
for (let i = 1; i <= CONFIG.LIST_COUNT; i++) {
const listKey = `ytMulti_videoList${i}`;
if (localStorage.getItem(listKey) === null) localStorage.setItem(listKey, '[]');
const pinnedKey = `ytMulti_pinned_list${i}`;
if (localStorage.getItem(pinnedKey) === null) localStorage.setItem(pinnedKey, '[]');
}
};
initializeStorage();
const cleanupOrphanDomainModes = () => {
const prefix = CONFIG.VIDEO_DOMAIN_MODE_PREFIX;
const keysToRemove = [];
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key && key.startsWith(prefix)) {
const baseId = key.slice(prefix.length);
const mode = localStorage.getItem(key);
if (mode === 'YT') {
keysToRemove.push(key);
continue;
}
if (mode === 'YU') {
let exists = false;
for (let j = 1; j <= CONFIG.LIST_COUNT; j++) {
const listKey = 'ytMulti_videoList' + j;
const ids = safeJSONParse(localStorage.getItem(listKey), []);
if (ids.some((id) => getBaseId(id) === baseId)) {
exists = true;
break;
}
}
if (!exists) keysToRemove.push(key);
}
}
}
keysToRemove.forEach((k) => localStorage.removeItem(k));
};
const saveSetting = (key, val) => {
const type = CONFIG_ITEMS[key].type;
localStorage.setItem(SETTINGS_PREFIX + key, type === 'array' || type === 'preset' ? JSON.stringify(val) : String(val));
CONFIG[key] = val;
if (key === 'LIST_COUNT') {
STORAGE_LISTS = generateStorageKeys();
if (!STORAGE_LISTS[currentList]) {
currentList = Object.keys(STORAGE_LISTS)[0];
localStorage.setItem('ytMulti_currentList', currentList);
}
if (typeof updateListButtonCount === 'function') updateListButtonCount();
}
};
const isChatId = (id) => id?.includes(CONFIG.CHAT_SUFFIX);
const isVideoId = (id) => id?.endsWith(CONFIG.VIDEO_SUFFIX);
const isHomepage = (id) => id?.startsWith('homepage');
const isFrameMode = (id) => id?.startsWith('frame:');
const getFrameVideoId = (id) => (isFrameMode(id) ? id.slice(6) : null);
const getBaseId = (id) => {
if (isChatId(id)) return id.split(CONFIG.CHAT_SUFFIX)[0];
if (isVideoId(id)) return id.slice(0, -CONFIG.VIDEO_SUFFIX.length);
if (isFrameMode(id)) return getFrameVideoId(id);
return id;
};
cleanupOrphanDomainModes();
const getChatId = (id) => {
if (isChatId(id)) return id;
if (isVideoId(id)) return id.slice(0, -CONFIG.VIDEO_SUFFIX.length) + CONFIG.CHAT_SUFFIX;
return id + CONFIG.CHAT_SUFFIX;
};
const getVideoId = (id) => {
if (isChatId(id)) return getBaseId(id);
if (isVideoId(id)) return id.slice(0, -CONFIG.VIDEO_SUFFIX.length);
return id;
};
const isFullUrl = (id) => id?.startsWith('http');
const getVideoIdFromUrl = (url) => {
const m1 = url?.match(/[?&]v=([A-Za-z0-9_-]{11})/);
if (m1) return m1[1];
const m2 = url?.match(/youtu[.]be\/([A-Za-z0-9_-]{11})/);
return m2 ? m2[1] : null;
};
const getListIdFromUrl = (url) => {
const m = url?.match(/[?&]list=([^&]+)/);
return m ? m[1] : null;
};
const isWatchPage = () => /\/watch[?\/]/.test(location.href) || /\/live[?\/]/.test(location.href);
const formatPresets = (arr) => {
const heights = arr.map((l) => CONFIG.RESOLUTION_LEVELS[l]?.[1] || '?');
const levels = arr.join(',');
return `${heights.join(',')} (${levels})`;
};
const normalizeVideoId = (id) => {
if (!id) return null;
if (id.includes(CONFIG.CHAT_SUFFIX) || id.endsWith(CONFIG.VIDEO_SUFFIX) || id.startsWith('homepage') || isFrameMode(id)) return id;
if (id.startsWith('http')) {
const vid = getVideoIdFromUrl(id);
if (vid && /^[A-Za-z0-9_-]{11}$/.test(vid))
return id.includes('_chat') || id.includes('live_chat') ? vid + CONFIG.CHAT_SUFFIX : vid + CONFIG.VIDEO_SUFFIX;
return null;
}
if (/^[A-Za-z0-9_-]{11}$/.test(id)) return id + CONFIG.VIDEO_SUFFIX;
return null;
};
const formatSettingDisplay = (cfg, val, key = '') => {
if (cfg.type === 'array' || cfg.type === 'preset') return cfg.type === 'preset' ? formatPresets(val) : JSON.stringify(val);
if (cfg.type === 'toggle12') return val ? `1 (${t('spoofOn')})` : `2 (${t('spoofOff')})`;
if (cfg.type === 'toggleYT') return val === 'YU' ? `2 (YU)` : `1 (YT)`;
if (key === 'BTN_LAYOUT' && cfg.type === 'int') {
const map = { 1: t('layout1'), 2: t('layout2'), 3: t('layout3') };
return `${val} (${map[val] || val})`;
}
if (cfg.type === 'string' && (val === '' || val === null || val === undefined)) return t('emptyVal');
return String(val);
};
const getListNamesArray = () => {
const raw = CONFIG.LIST_NAMES || '';
const names = raw
.split(',')
.map((s) => s.trim())
.filter((s) => s);
const res = [];
for (let i = 0; i < CONFIG.LIST_COUNT; i++) res.push(names[i] || `${LANG_CODE === 'zh' ? '清單' : 'List'}${i + 1}`);
return res;
};
const getListDisplayName = () => {
const idx = parseInt(currentList.replace('list', ''), 10) - 1;
return getListNamesArray()[idx] || currentList;
};
const getVideoDomainMode = (videoId) => {
const baseId = getBaseId(videoId);
const stored = localStorage.getItem(CONFIG.VIDEO_DOMAIN_MODE_PREFIX + baseId);
return stored || CONFIG.DEFAULT_DOMAIN_MODE;
};
const setVideoDomainMode = (videoId, mode) => {
const baseId = getBaseId(videoId);
if (mode === 'YT' || mode === 'YU') localStorage.setItem(CONFIG.VIDEO_DOMAIN_MODE_PREFIX + baseId, mode);
};
const cleanupVideoDomainMode = (videoId, listCount = CONFIG.LIST_COUNT) => {
const baseId = getBaseId(videoId);
let exists = false;
for (let i = 1; i <= listCount; i++) {
const key = 'ytMulti_videoList' + i;
const ids = safeJSONParse(localStorage.getItem(key), []);
if (ids.some((id) => getBaseId(id) === baseId)) {
exists = true;
break;
}
}
if (!exists) localStorage.removeItem(CONFIG.VIDEO_DOMAIN_MODE_PREFIX + baseId);
};
const STORAGE_POS = 'ytMulti_btnPos',
STORAGE_MODE = 'ytMulti_openMode',
STORAGE_CURRENT = 'ytMulti_currentList',
STORAGE_PINNED_PREFIX = 'ytMulti_pinned_';
let currentList = localStorage.getItem(STORAGE_CURRENT) || 'list1';
if (!STORAGE_LISTS[currentList]) {
currentList = Object.keys(STORAGE_LISTS)[0];
localStorage.setItem(STORAGE_CURRENT, currentList);
}
const getCurrentDomainMode = () =>
CONFIG.DOMAIN_MODES.find((m) => m.key === localStorage.getItem(CONFIG.DOMAIN_MODE_STORAGE_KEY)) || CONFIG.DOMAIN_MODES[0];
const addToCurrentList = (id) => {
const k = STORAGE_LISTS[currentList],
normalizedId = normalizeVideoId(id);
if (!normalizedId) return false;
const ids = safeJSONParse(localStorage.getItem(k), []);
if (ids.includes(normalizedId)) return false;
ids.push(normalizedId);
localStorage.setItem(k, JSON.stringify(ids));
if (CONFIG.VIDEO_PINNED_BY_DEFAULT && !isHomepage(normalizedId) && !isFrameMode(normalizedId)) {
const pinnedK = STORAGE_PINNED_PREFIX + currentList;
const pinned = safeJSONParse(localStorage.getItem(pinnedK), []);
if (!pinned.includes(normalizedId)) {
if (pinned.length >= CONFIG.MAX_PINNED) pinned.pop();
pinned.push(normalizedId);
localStorage.setItem(pinnedK, JSON.stringify(pinned));
}
}
if (typeof updateListButtonCount === 'function') updateListButtonCount();
try {
localStorage.setItem(
CONFIG.SYNC_EVENT_KEY,
JSON.stringify({
type: 'videoAdded',
listKey: currentList,
videoId: normalizedId,
timestamp: Date.now(),
})
);
} catch (e) {}
return true;
};
let videoObserver = null,
isObserving = false,
styleTag = null,
pollTimer = null;
const PROCESSED_ATTR = 'data-ytmulti-processed';
const TARGET_SELECTORS = [
'ytd-rich-item-renderer',
'ytd-thumbnail',
'ytd-playlist-thumbnail',
'ytd-playlist-video-renderer',
'yt-lockup-view-model',
];
const getVideoContainerSelector = () =>
location.href.includes('/playlist?list=') ? 'ytd-playlist-video-renderer ytd-thumbnail' : TARGET_SELECTORS.join(', ');
const injectionStats = {};
TARGET_SELECTORS.forEach((s) => (injectionStats[s] = 0));
const startObservingVideos = () => {
if (!IS_SUB_FRAME && !isAddButtonEnabled) return;
if (!IS_SUB_FRAME && isCurrentWatchPage) return;
isObserving = true;
if (!styleTag) {
styleTag = document.createElement('style');
styleTag.textContent = `.ytMulti-add-btn{position:absolute;top:8px;left:8px;width:42px;height:42px;background:rgba(0,0,0,0.8);color:white;border:none;border-radius:50%;cursor:pointer;font-size:31px;display:none;z-index:10000;box-shadow:0 2px 6px rgba(0,0,0,0.4);align-items:center;justify-content:center}.ytMulti-video-hover .ytMulti-add-btn{display:flex}`;
document.head.appendChild(styleTag);
}
if (!videoObserver) {
videoObserver = new MutationObserver((mutations) => {
for (const m of mutations) {
if (m.type === 'childList')
for (const n of m.addedNodes) if (n.nodeType === Node.ELEMENT_NODE) processNode(n);
}
});
videoObserver.observe(document.body, { childList: true, subtree: true });
}
if (pollTimer) clearInterval(pollTimer);
const checkAndScan = () => {
if (document.querySelector('ytd-browse, ytd-rich-grid-renderer, ytd-playlist-sidebar-renderer')) {
processNode(document.body);
clearInterval(pollTimer);
pollTimer = null;
}
};
pollTimer = setInterval(checkAndScan, 300);
checkAndScan();
};
const processNode = (node) => {
if (!node || !node.querySelectorAll) return;
const selector = getVideoContainerSelector();
if (node.matches && node.matches(selector)) tryAddButton(node);
node.querySelectorAll(selector).forEach(tryAddButton);
};
const tryAddButton = (el) => {
if (el.hasAttribute(PROCESSED_ATTR)) return;
const vidLink = el.querySelector('a[href*="/watch?"]');
let hasVideo = false;
if (vidLink?.href) hasVideo = true;
else {
const ep = el.querySelector('[data-endpoint]');
if (ep) {
try {
const d = JSON.parse(ep.getAttribute('data-endpoint'));
if (d?.videoId) hasVideo = true;
} catch (e) {}
}
}
if (!hasVideo) return;
el.setAttribute(PROCESSED_ATTR, '1');
addButtonsToContainer(el);
if (CONFIG.BUTTON_INJECTION_STATS) for (const sel of TARGET_SELECTORS) if (el.matches(sel)) injectionStats[sel]++;
if (!IS_SUB_FRAME && typeof window.onTargetFound === 'function') window.onTargetFound();
};
const stopObservingVideos = () => {
isObserving = false;
if (pollTimer) {
clearInterval(pollTimer);
pollTimer = null;
}
document.querySelectorAll('.ytMulti-add-btn').forEach((b) => b.remove());
document.querySelectorAll(`[${PROCESSED_ATTR}]`).forEach((el) => el.removeAttribute(PROCESSED_ATTR));
if (styleTag) {
styleTag.remove();
styleTag = null;
}
if (videoObserver) {
videoObserver.disconnect();
videoObserver = null;
}
};
const addButtonsToContainer = (el) => {
if (el.querySelector('.ytMulti-add-btn')) return;
const btn = document.createElement('button');
btn.className = 'ytMulti-add-btn';
btn.textContent = '+';
btn.addEventListener('click', (e) => {
e.stopPropagation();
e.preventDefault();
let vid = el.querySelector('a[href*="/watch?"]')?.href;
if (!vid) {
const ep = el.querySelector('[data-endpoint]');
if (ep)
try {
const d = JSON.parse(ep.getAttribute('data-endpoint'));
if (d?.videoId) vid = 'https://www.youtube.com/watch?v=' + d.videoId;
} catch (e) {}
}
if (vid) addToCurrentList(vid);
});
el.style.position = 'relative';
el.appendChild(btn);
el.addEventListener('mouseenter', () => el.classList.add('ytMulti-video-hover'), { passive: true });
el.addEventListener('mouseleave', () => el.classList.remove('ytMulti-video-hover'), { passive: true });
};
let isCurrentWatchPage = isWatchPage();
if (!IS_SUB_FRAME) {
let hasFoundTargets = false,
keepPanelExpanded = false,
panelHovered = false;
let urlPollingId = null,
wprObserver = null,
wprDetected = false;
const panel = document.createElement('div');
panel.id = 'ytMulti_panel';
panel.style.cssText = `position:fixed;background:rgba(0,0,0,0.8);color:#fff;padding:6px 8px;border-radius:8px;z-index:9999;display:none;align-items:center;cursor:move;gap:6px;box-shadow:0 4px 12px rgba(0,0,0,0.2);font-family:Arial,sans-serif;backdrop-filter:blur(4px);overflow:hidden;white-space:nowrap;transition:opacity 0.2s;pointer-events:auto;`;
document.body.appendChild(panel);
const savedPos = JSON.parse(localStorage.getItem(STORAGE_POS) || 'null');
if (savedPos) {
panel.style.top = savedPos.top;
panel.style.left = savedPos.left;
panel.style.right = 'auto';
}
let isDragging = false;
panel.addEventListener(
'mousedown',
(e) => {
e.preventDefault();
isDragging = true;
const startX = e.clientX,
startY = e.clientY,
rect = panel.getBoundingClientRect();
let hasMoved = false;
const onMove = (ev) => {
panel.style.top = rect.top + ev.clientY - startY + 'px';
panel.style.left = rect.left + ev.clientX - startX + 'px';
hasMoved = true;
};
const onUp = () => {
isDragging = false;
if (hasMoved) localStorage.setItem(STORAGE_POS, JSON.stringify({ top: panel.style.top, left: panel.style.left }));
window.removeEventListener('mousemove', onMove);
window.removeEventListener('mouseup', onUp);
};
window.addEventListener('mousemove', onMove, { passive: true });
window.addEventListener('mouseup', onUp, { passive: true });
},
{ passive: true }
);
const BTN_BASE_STYLE =
'padding:6px 12px;height:36px;border:none;border-radius:6px;color:white;cursor:pointer;transition:all 0.2s;font-size:13px;font-weight:500;text-shadow:0 1px 2px rgba(0,0,0,0.2);box-shadow:0 2px 4px rgba(0,0,0,0.2);display:flex;align-items:center;justify-content:center;';
const createPanelButton = (text, options = {}) => {
const { isActive = false, activeBg = '#00aa00', activeHover = '#008800', baseBg = '#ff0000', baseHover = '#cc0000' } = options;
const btn = document.createElement('button');
btn.textContent = text;
btn.dataset.state = isActive ? 'active' : 'inactive';
btn.style.cssText = `${BTN_BASE_STYLE}background:${isActive ? activeBg : baseBg};`;
const updateStyle = () => {
btn.style.background = btn.dataset.state === 'active' ? activeBg : baseBg;
};
btn.addEventListener('mouseover', () => {
btn.style.background = btn.dataset.state === 'active' ? activeHover : baseHover;
});
btn.addEventListener('mouseout', updateStyle);
return btn;
};
const playBtn = createPanelButton(t('playIcon'));
const modeBtn = createPanelButton(t('modeCurrentTab'));
const listBtn = createPanelButton(`${getListDisplayName()} (0)`);
const addButtonToggle = createPanelButton(t('addButton'), { isActive: isAddButtonEnabled });
const otherButtons = [modeBtn, listBtn, addButtonToggle];
panel.append(playBtn, modeBtn, listBtn, addButtonToggle);
const updateListButtonCount = () => {
const k = STORAGE_LISTS[currentList];
if (!k) return;
const c = safeJSONParse(localStorage.getItem(k), []).length;
listBtn.textContent = `${getListDisplayName()} (${c})`;
};
const collapsePanel = () => {
if (isDragging || keepPanelExpanded) return;
playBtn.textContent = t('playIcon');
otherButtons.forEach((b) => (b.style.display = 'none'));
};
const expandPanel = () => {
if (isDragging) return;
playBtn.textContent = t('play');
otherButtons.forEach((b) => (b.style.display = 'flex'));
updateListButtonCount();
};
const updatePanelVisibility = () => {
if (isCurrentWatchPage) {
panel.style.display = 'none';
collapsePanel();
} else {
panel.style.display = 'flex';
if (!panelHovered && !keepPanelExpanded) collapsePanel();
}
};
window.onTargetFound = () => {
if (!hasFoundTargets) hasFoundTargets = true;
};
panel.addEventListener(
'mouseenter',
() => {
panelHovered = true;
panel.style.display = 'flex';
expandPanel();
},
{ passive: true }
);
panel.addEventListener(
'mouseleave',
() => {
panelHovered = false;
updatePanelVisibility();
},
{ passive: true }
);
const toggleAddButton = () => {
isAddButtonEnabled = !isAddButtonEnabled;
localStorage.setItem(CONFIG.ADD_BUTTON_ENABLED_STORAGE_KEY, String(isAddButtonEnabled));
addButtonToggle.dataset.state = isAddButtonEnabled ? 'active' : 'inactive';
addButtonToggle.dispatchEvent(new MouseEvent('mouseout'));
isAddButtonEnabled ? startObservingVideos() : stopObservingVideos();
};
addButtonToggle.addEventListener('click', (e) => {
e.stopPropagation();
if (isCurrentWatchPage) addToCurrentList(location.href);
else toggleAddButton();
});
modeBtn.addEventListener('click', () => {
const cur = localStorage.getItem(STORAGE_MODE) || 'current_tab';
const nxt = cur === 'current_tab' ? 'new_tab' : cur === 'new_tab' ? 'new_window' : 'current_tab';
localStorage.setItem(STORAGE_MODE, nxt);
modeBtn.textContent = nxt === 'current_tab' ? t('modeCurrentTab') : nxt === 'new_tab' ? t('modeNewTab') : t('modeNewWindow');
});
listBtn.addEventListener('click', () => {
const names = Object.keys(STORAGE_LISTS);
const idx = names.indexOf(currentList);
currentList = names[(idx + 1) % names.length];
localStorage.setItem(STORAGE_CURRENT, currentList);
updateListButtonCount();
});
const stopUrlPolling = () => {
if (urlPollingId) {
clearInterval(urlPollingId);
urlPollingId = null;
}
};
const applyPanelMode = (isWatch) => {
isCurrentWatchPage = isWatch;
if (isWatch) {
hasFoundTargets = false;
panel.style.display = 'none';
collapsePanel();
addButtonToggle.textContent = t('addIcon');
addButtonToggle.style.fontSize = '18px';
addButtonToggle.dataset.state = 'active';
stopObservingVideos();
} else {
addButtonToggle.textContent = t('addButton');
addButtonToggle.style.fontSize = '13px';
addButtonToggle.dataset.state = isAddButtonEnabled ? 'active' : 'inactive';
if (isAddButtonEnabled) startObservingVideos();
updatePanelVisibility();
}
addButtonToggle.dispatchEvent(new MouseEvent('mouseout'));
updateListButtonCount();
};
const setupNavigationObserver = () => {
wprObserver = new MutationObserver((mutations) => {
for (const m of mutations) {
if (m.type === 'attributes' && m.attributeName === 'youtube-wpr') {
const v = document.documentElement.getAttribute('youtube-wpr');
const isW = v !== null && v !== '';
if (isW !== isCurrentWatchPage) applyPanelMode(isW);
if (!wprDetected) {
wprDetected = true;
stopUrlPolling();
}
}
}
});
wprObserver.observe(document.documentElement, {
attributes: true,
attributeFilter: ['youtube-wpr'],
});
let lastHref = location.href;
urlPollingId = setInterval(() => {
if (wprDetected) {
stopUrlPolling();
return;
}
if (location.href !== lastHref) {
lastHref = location.href;
applyPanelMode(isWatchPage());
}
}, CONFIG.POLLING_INTERVAL);
};
const setupSyncListener = () => {
window.addEventListener('storage', (e) => {
if (e.key === STORAGE_CURRENT) {
currentList = e.newValue || 'list1';
if (typeof updateListButtonCount === 'function') updateListButtonCount();
}
if (e.key === CONFIG.SYNC_EVENT_KEY) {
try {
const d = JSON.parse(e.newValue);
if (d.listKey === currentList && d.type === 'videoAdded') {
const k = STORAGE_LISTS[currentList],
ids = safeJSONParse(localStorage.getItem(k), []);
if (d.videoId && !ids.includes(d.videoId)) {
ids.push(d.videoId);
localStorage.setItem(k, JSON.stringify(ids));
updateListButtonCount();
}
}
} catch (err) {}
}
});
};
if (typeof GM_registerMenuCommand !== 'undefined') GM_registerMenuCommand(t('settingsTitle'), openSettingsMenu);
function openSettingsMenu() {
loadSettings();
const items = Object.entries(CONFIG_ITEMS);
while (true) {
let menu = t('settingsPrompt');
items.forEach(([k, c], i) => {
menu += `${i + 1}. ${k}\n${LANG_CODE === 'zh' ? c.descZh : c.descEn}\n當前/Current: ${formatSettingDisplay(
c,
CONFIG[k],
k
)}\n預設/Default: ${formatSettingDisplay(c, c.default, k)}\n`;
});
if (CONFIG.BUTTON_INJECTION_STATS) {
menu += '📊 按鈕注入統計 (本會話):\n';
for (const [sel, count] of Object.entries(injectionStats)) {
const shortSel = sel.length > 45 ? sel.slice(0, 42) + '...' : sel;
menu += ` ${shortSel}: ${count}\n`;
}
menu += '\n';
}
menu += `${items.length + 1}. 📤 ${LANG_CODE === 'zh' ? '匯出清單' : 'Export Lists'}\n${
items.length + 2
}. 📥 ${LANG_CODE === 'zh' ? '匯入清單' : 'Import Lists'}\n${items.length + 3}. 📥 ${t('exportAll')}\n0 = Exit`;
const choice = prompt(menu, '0');
if (!choice || choice === '0') return;
const idx = parseInt(choice, 10) - 1;
if (idx === items.length) {
let output = '';
const names = getListNamesArray();
const parts = [];
for (let i = 1; i <= CONFIG.LIST_COUNT; i++) {
const key = STORAGE_LISTS[`list${i}`];
const ids = safeJSONParse(localStorage.getItem(key), []);
if (ids.length > 0) {
parts.push(`${names[i - 1]}:${ids.join(',')}`);
}
}
output = parts.join(';');
prompt(LANG_CODE === 'zh' ? '✅ 清單資料已生成,請複製以下內容:' : '✅ List data generated, please copy:', output.trim());
continue;
}
if (idx === items.length + 1) {
let listOptions = '';
for (let i = 1; i <= CONFIG.LIST_COUNT; i++) listOptions += `${i}. ${getListNamesArray()[i - 1]}\n`;
const targetChoice = prompt(
`${LANG_CODE === 'zh' ? '請選擇要覆蓋的清單編號:' : 'Enter list number to overwrite:'}\n${listOptions}`,
'1'
);
const tIdx = parseInt(targetChoice, 10);
if (isNaN(tIdx) || tIdx < 1 || tIdx > CONFIG.LIST_COUNT) {
alert(t('settingsInvalid'));
continue;
}
const dataInput = prompt(
LANG_CODE === 'zh'
? '請貼上清單資料(格式:清單名:ID1,ID2 或直接貼上 ID 逗號分隔):'
: 'Paste list data (format: ListName:ID1,ID2 or just comma-separated IDs):',
''
);
if (!dataInput || dataInput === '0') continue;
try {
let ids;
if (dataInput.includes(':')) {
ids = dataInput.split(':')[1].trim();
} else {
ids = dataInput.trim();
}
const parsedIds = ids
.split(',')
.map((s) => s.trim())
.filter((s) => s);
const key = STORAGE_LISTS[`list${tIdx}`];
localStorage.setItem(key, JSON.stringify(parsedIds));
if (currentList === `list${tIdx}`) updateListButtonCount();
alert(t('settingsSaved'));
} catch (e) {
alert(t('settingsInvalid'));
}
continue;
}
if (idx === items.length + 2) {
const dataInput = prompt(
LANG_CODE === 'zh'
? '請貼上全部清單資料(格式:清單名A:ID1,ID2;清單名B:ID3,ID4):'
: 'Paste all list data (format: ListA:ID1,ID2;ListB:ID3,ID4):',
''
);
if (!dataInput || dataInput === '0') continue;
try {
const listParts = dataInput.split(';').filter((s) => s.trim());
const names = getListNamesArray();
let importCount = 0;
for (const part of listParts) {
const colonIdx = part.indexOf(':');
if (colonIdx === -1) continue;
const listName = part.slice(0, colonIdx).trim();
const idsStr = part.slice(colonIdx + 1).trim();
const parsedIds = idsStr
.split(',')
.map((s) => s.trim())
.filter((s) => s);
let targetIdx = names.findIndex((n) => n === listName);
if (targetIdx === -1) {
for (let i = 0; i < CONFIG.LIST_COUNT; i++) {
const key = STORAGE_LISTS[`list${i + 1}`];
const existingIds = safeJSONParse(localStorage.getItem(key), []);
if (existingIds.length === 0) {
targetIdx = i;
const newNames = [...names];
newNames[i] = listName;
saveSetting('LIST_NAMES', newNames.join(','));
break;
}
}
}
if (targetIdx !== -1) {
const key = STORAGE_LISTS[`list${targetIdx + 1}`];
localStorage.setItem(key, JSON.stringify(parsedIds));
if (currentList === `list${targetIdx + 1}`) updateListButtonCount();
importCount++;
}
}
alert(LANG_CODE === 'zh' ? `✅ 成功匯入 ${importCount} 個清單` : `✅ Successfully imported ${importCount} lists`);
} catch (e) {
alert(t('settingsInvalid'));
}
continue;
}
if (isNaN(idx) || idx < 0 || idx >= items.length) {
alert(t('settingsInvalid'));
continue;
}
const [key, cfg] = items[idx];
let defaultDisplay = cfg.default;
if (cfg.type === 'toggle12') defaultDisplay = cfg.default ? '1' : '2';
else if (cfg.type === 'toggleYT') defaultDisplay = cfg.default === 'YU' ? '2' : '1';
const input = prompt(
t('settingsValuePrompt', {
current: formatSettingDisplay(cfg, CONFIG[key], key),
default: formatSettingDisplay(cfg, cfg.default, key),
desc: LANG_CODE === 'zh' ? cfg.descZh : cfg.descEn,
}),
defaultDisplay
);
if (input === null || input === '0') continue;
try {
let val;
if (cfg.type === 'array') {
val = input
.split(',')
.map((s) => parseInt(s.trim(), 10))
.filter((n) => !isNaN(n));
if (!val.length) throw 'E';
} else if (cfg.type === 'preset') {
val = input
.split(',')
.map((s) => parseInt(s.trim(), 10))
.filter((n) => !isNaN(n) && n >= 1 && n <= 8);
if (!val.length || val.length !== 5) throw 'E';
} else if (cfg.type === 'toggle12') {
val = input === '1';
} else if (cfg.type === 'toggleYT') {
val = input === '2' ? 'YU' : 'YT';
} else if (cfg.type === 'string') {
val = input.trim();
} else if (cfg.type === 'float') {
val = parseFloat(input);
if (isNaN(val) || val < cfg.min || val > cfg.max) throw 'E';
} else {
val = parseInt(input, 10);
if (isNaN(val) || val < cfg.min || val > cfg.max) throw 'E';
}
saveSetting(key, val);
alert(t('settingsSaved'));
updateListButtonCount();
} catch (e) {
alert(t('settingsInvalid'));
}
}
}
const makeBlobPage = (ids, listKey, initialPinnedIds = [], domainMode = 'www.youtube.com') => {
const nonce = document.querySelector('script[nonce]')?.nonce || '';
const idWithOrder = ids.map((id, i) => ({ id, order: i * 2 + 1 }));
const ytParams = 'enablejsapi=1&autoplay=1&rel=0&fs=1&playsinline=1&iv_load_policy=3&cc_load_policy=0&controls=1';
const sharedHelpers = `
const isChatId=id=>id&&id.includes(CHAT_SUFFIX);
const isVideoId=id=>id&&id.endsWith(VIDEO_SUFFIX);
const isHomepage=id=>id&&id.startsWith('homepage');
const isFrameMode=id=>id&&id.startsWith('frame:');
const getFrameVideoId=id=>isFrameMode(id)?id.slice(6):null;
const getBaseId=id=>{if(isChatId(id))return id.split(CHAT_SUFFIX)[0];if(isVideoId(id))return id.slice(0,-VIDEO_SUFFIX.length);if(isFrameMode(id))return getFrameVideoId(id);return id;};
const getChatId=id=>{if(isChatId(id))return id;if(isVideoId(id))return id.slice(0,-VIDEO_SUFFIX.length)+CHAT_SUFFIX;return id+CHAT_SUFFIX;};
const getVideoId=id=>{if(isChatId(id))return getBaseId(id);if(isVideoId(id))return id.slice(0,-VIDEO_SUFFIX.length);return id;};
const isFullUrl=id=>id&&id.startsWith('http');
const getVideoIdFromUrl=url=>{const m1=url&&url.match(/[?&]v=([A-Za-z0-9_-]{11})/);if(m1)return m1[1];const m2=url&&url.match(/youtu[.]be\\/([A-Za-z0-9_-]{11})/);return m2?m2[1]:null;};
const getListIdFromUrl=url=>{const m=url&&url.match(/[?&]list=([^&]+)/);return m?m[1]:null;};
`.trim();
const safeHomeUrl = CONFIG.HOME_TARGET_URL.replace(/'/g, "\\'");
const blobParams = {
MAX_PINNED: CONFIG.MAX_PINNED,
PORTRAIT_HEIGHT_THRESHOLD: CONFIG.PORTRAIT_HEIGHT_THRESHOLD,
LANDSCAPE_ASPECT_RATIO_THRESHOLD: CONFIG.LANDSCAPE_ASPECT_RATIO_THRESHOLD,
LANDSCAPE_COLUMN_CONFIG: CONFIG.LANDSCAPE_COLUMN_CONFIG.join(','),
PORTRAIT_MAX_COLUMNS: CONFIG.PORTRAIT_MAX_COLUMNS,
ASPECT_RATIO_STANDARD: CONFIG.ASPECT_RATIO_STANDARD,
CHAT_SUFFIX: CONFIG.CHAT_SUFFIX,
VIDEO_SUFFIX: CONFIG.VIDEO_SUFFIX,
DOMAIN_MODE: domainMode,
VIDEO_DOMAIN_MODE_PREFIX: CONFIG.VIDEO_DOMAIN_MODE_PREFIX,
DEFAULT_DOMAIN_MODE: CONFIG.DEFAULT_DOMAIN_MODE,
SYNC_EVENT_KEY: CONFIG.SYNC_EVENT_KEY,
DEBOUNCE_MS: CONFIG.RESIZE_DEBOUNCE,
RESOLUTION_LEVELS: JSON.stringify(CONFIG.RESOLUTION_LEVELS),
RESOLUTION_PRESETS: JSON.stringify(CONFIG.RESOLUTION_PRESETS),
SCREEN_WIDTH: CONFIG.SCREEN_WIDTH_DEFAULT,
SPOOF_ENABLED: CONFIG.SPOOF_RESOLUTION_ENABLED,
LIST_COUNT: CONFIG.LIST_COUNT,
HOME_PINNED_BY_DEFAULT: CONFIG.HOME_PINNED_BY_DEFAULT,
VIDEO_PINNED_BY_DEFAULT: CONFIG.VIDEO_PINNED_BY_DEFAULT,
BTN_SIZE_SCALE: CONFIG.BTN_SIZE_SCALE,
YT_PARAMS: ytParams,
HOVER_TIMEOUT: CONFIG.HOVER_TIMEOUT,
BTN_LAYOUT: CONFIG.BTN_LAYOUT,
BTN_HIDE_TIMEOUT: CONFIG.BTN_HIDE_TIMEOUT,
HOME_TARGET_URL: safeHomeUrl,
idWithOrder: JSON.stringify(idWithOrder),
listKey: listKey,
STORAGE_LISTS: JSON.stringify(STORAGE_LISTS),
INITIAL_PINNED_IDS: JSON.stringify(initialPinnedIds),
PINNED_PREFIX: STORAGE_PINNED_PREFIX,
STORAGE_CURRENT: STORAGE_CURRENT,
SHARED_HELPERS: sharedHelpers,
};
const jsCode = `
(function(){'use strict';
const CHAT_SUFFIX='${blobParams.CHAT_SUFFIX}';const VIDEO_SUFFIX='${blobParams.VIDEO_SUFFIX}';
const MAX_PINNED=${blobParams.MAX_PINNED};const PORTRAIT_HEIGHT_THRESHOLD=${blobParams.PORTRAIT_HEIGHT_THRESHOLD};
const LANDSCAPE_ASPECT_RATIO_THRESHOLD=${blobParams.LANDSCAPE_ASPECT_RATIO_THRESHOLD};
const LANDSCAPE_COLUMN_CONFIG=[${blobParams.LANDSCAPE_COLUMN_CONFIG}];
const PORTRAIT_MAX_COLUMNS=${blobParams.PORTRAIT_MAX_COLUMNS};const ASPECT_RATIO_STANDARD=${blobParams.ASPECT_RATIO_STANDARD};
const DOMAIN_MODE='${blobParams.DOMAIN_MODE}';const VIDEO_DOMAIN_MODE_PREFIX='${blobParams.VIDEO_DOMAIN_MODE_PREFIX}';
const DEFAULT_DOMAIN_MODE='${blobParams.DEFAULT_DOMAIN_MODE}';const SYNC_EVENT_KEY='${blobParams.SYNC_EVENT_KEY}';
const DEBOUNCE_MS=${blobParams.DEBOUNCE_MS};const RESOLUTION_LEVELS=${blobParams.RESOLUTION_LEVELS};
const RESOLUTION_PRESETS=${blobParams.RESOLUTION_PRESETS};const SCREEN_WIDTH=${blobParams.SCREEN_WIDTH};
const SPOOF_ENABLED=${blobParams.SPOOF_ENABLED};const LIST_COUNT=${blobParams.LIST_COUNT};
const HOME_PINNED_BY_DEFAULT=${blobParams.HOME_PINNED_BY_DEFAULT};const VIDEO_PINNED_BY_DEFAULT=${blobParams.VIDEO_PINNED_BY_DEFAULT};
const BTN_SIZE_SCALE=${blobParams.BTN_SIZE_SCALE};const YT_PARAMS='${blobParams.YT_PARAMS}';
const HOVER_TIMEOUT=${blobParams.HOVER_TIMEOUT};const BTN_LAYOUT=${blobParams.BTN_LAYOUT};
const BTN_HIDE_TIMEOUT=${blobParams.BTN_HIDE_TIMEOUT};const HOME_TARGET_URL='${blobParams.HOME_TARGET_URL}';
const HOME_DOMAIN='www.youtube.com';const STORAGE_CURRENT='${blobParams.STORAGE_CURRENT}';
${blobParams.SHARED_HELPERS}
let idOrderMap=new Map(${blobParams.idWithOrder}.map(i=>[i.id,i.order]));
const listKey='${blobParams.listKey}';const STORAGE_LISTS=${blobParams.STORAGE_LISTS};
const INITIAL_PINNED_IDS=${blobParams.INITIAL_PINNED_IDS};const PINNED_PREFIX='${blobParams.PINNED_PREFIX}';
const container=document.querySelector('.container');let pinnedIds=INITIAL_PINNED_IDS;
let elementCache=new Map();let hoverTimers=new Map();let layoutDirty=true;let resizeTimer;
let storageSyncTimer=null;let lastSyncTime=0;let blobCurrentList=listKey;
const safeJSONParse=(str,fallback)=>{if(str===null||str===undefined||str==='')return fallback;try{return JSON.parse(str);}catch(e){return fallback;}};
const clearHoverTimer=(id)=>{if(hoverTimers.has(id)){clearTimeout(hoverTimers.get(id));hoverTimers.delete(id);}};
const clearElementCache=(id)=>{const w=elementCache.get(id);if(w){const ifr=w.querySelector('iframe');if(ifr){ifr.src='about:blank';ifr.removeAttribute('src');}w.remove();}elementCache.delete(id);clearHoverTimer(id);};
const clearAllTimers=()=>{hoverTimers.forEach(clearTimeout);hoverTimers.clear();};
const clearAllElements=()=>{elementCache.forEach((w)=>{const ifr=w.querySelector('iframe');if(ifr){ifr.src='about:blank';ifr.removeAttribute('src');}w.remove();});elementCache.clear();hoverTimers.clear();};
const setupEnterShowHide=(triggerEl,targetEl,timeout)=>{let timer=null,visible=false;const show=()=>{targetEl.style.opacity='1';targetEl.style.pointerEvents='auto';targetEl.style.transform='scale(1)';};const hide=()=>{targetEl.style.opacity='0';targetEl.style.pointerEvents='none';targetEl.style.transform='scale(0.8)';};triggerEl.addEventListener('mouseenter',()=>{if(!visible){show();visible=true;clearTimeout(timer);timer=setTimeout(()=>{hide();visible=false;},timeout);}});triggerEl.addEventListener('mouseleave',()=>{clearTimeout(timer);if(visible){hide();visible=false;}});hide();};
const showButtons=wrapper=>{wrapper.querySelectorAll('.remove-btn,.pin-btn,.domain-toggle-btn,.frame-toggle-btn,.chat-toggle-btn,.add-chat-btn,.top-btn,.up-btn,.down-btn,.bottom-btn,.copy-btn,.copy-placeholder,.list-switch-btn').forEach(b=>b.style.display='flex');};
const hideButtons=wrapper=>{wrapper.querySelectorAll('.remove-btn,.pin-btn,.domain-toggle-btn,.frame-toggle-btn,.chat-toggle-btn,.add-chat-btn,.top-btn,.up-btn,.down-btn,.bottom-btn,.copy-btn,.copy-placeholder,.list-switch-btn').forEach(b=>b.style.display='none');};
const setupHoverLogic=(wrapper,id)=>{wrapper.addEventListener('mouseenter',()=>{clearHoverTimer(id);showButtons(wrapper);hoverTimers.set(id,setTimeout(()=>hideButtons(wrapper),BTN_HIDE_TIMEOUT));});wrapper.addEventListener('mouseleave',()=>{clearHoverTimer(id);hideButtons(wrapper);});};
const isLandscape=()=>(container.offsetWidth||window.innerWidth)>(container.offsetHeight||window.innerHeight)*LANDSCAPE_ASPECT_RATIO_THRESHOLD;
const getColsForArea=(itemCount,areaWidth,fullW)=>{let c=1;for(let i=0;i<LANDSCAPE_COLUMN_CONFIG.length;i++){if(itemCount>=LANDSCAPE_COLUMN_CONFIG[i])c++;else break;}return c;};
const findBestPortraitColumns=(itemCount,availableW,availableH,threshold)=>{if(availableH<=0)return 1;let best=1,minO=Infinity;for(let c=1;c<=PORTRAIT_MAX_COLUMNS;c++){const r=Math.ceil(itemCount/c);const h=(availableW/c)/ASPECT_RATIO_STANDARD;const o=(r*h)/availableH;if(o<=threshold)return c;if(o<minO){minO=o;best=c;}}return best;};
const calculateLayout=()=>{if(!layoutDirty)return{cells:[]};const W=container.offsetWidth||window.innerWidth,H=container.offsetHeight||window.innerHeight;const isLand=W>H*LANDSCAPE_ASPECT_RATIO_THRESHOLD;const all=Array.from(idOrderMap.entries()).sort((a,b)=>a[1]-b[1]);const pin=all.filter(([id])=>pinnedIds.includes(id));const vis=all.filter(([id])=>!pinnedIds.includes(id));const cells=[];const getCols=(cnt)=>{let c=1;for(let i=0;i<LANDSCAPE_COLUMN_CONFIG.length;i++){if(cnt>=LANDSCAPE_COLUMN_CONFIG[i])c++;else break;}return c;};if(isLand){const pCount=pin.length;if(pCount===0){if(vis.length>0){const cc=getCols(vis.length);const uW=W/cc;const uH=uW/ASPECT_RATIO_STANDARD;for(let i=0;i<vis.length;i++){const r=Math.floor(i/cc),col=i%cc;cells.push({id:vis[i][0],x:col*uW,y:r*uH,w:uW,h:uH});}}}else if(pCount===1){const pH=W/ASPECT_RATIO_STANDARD;cells.push({id:pin[0][0],x:0,y:0,w:W,h:pH,isPinned:true});const aH=H-pH;if(aH>0&&vis.length>0){const cc=getCols(vis.length);const uW=W/cc;for(let i=0;i<vis.length;i++){const r=Math.floor(i/cc),col=i%cc;const cw=Math.min(uW,W);const ch=cw/ASPECT_RATIO_STANDARD;cells.push({id:vis[i][0],x:col*uW,y:pH+r*ch,w:cw,h:ch});}}}else if(pCount===2){const pW=W/2,pH=pW/ASPECT_RATIO_STANDARD;cells.push({id:pin[0][0],x:0,y:0,w:pW,h:pH,isPinned:true});cells.push({id:pin[1][0],x:pW,y:0,w:pW,h:pH,isPinned:true});const aH=H-pH;if(aH>0&&vis.length>0){const cc=Math.min(vis.length,4);const uW=W/cc;for(let i=0;i<vis.length;i++){const r=Math.floor(i/cc),col=i%cc;const cw=Math.min(uW,pW);const ch=cw/ASPECT_RATIO_STANDARD;cells.push({id:vis[i][0],x:col*uW,y:pH+r*ch,w:cw,h:ch});}}}else if(pCount>=3){const pW=W/2,pH=pW/ASPECT_RATIO_STANDARD;for(let i=0;i<Math.min(3,pCount);i++){cells.push({id:pin[i][0],x:(i%2)*pW,y:Math.floor(i/2)*pH,w:pW,h:pH,isPinned:true});}if(vis.length>0){const availW=pW;const cc=getColsForArea(vis.length,availW,W);const uW=availW/cc;for(let i=0;i<vis.length;i++){const r=Math.floor(i/cc),col=i%cc;const cw=uW;const ch=cw/ASPECT_RATIO_STANDARD;cells.push({id:vis[i][0],x:pW+col*uW,y:pH+r*ch,w:cw,h:ch});}}}}else{let y=0;pin.forEach(([id])=>{const h=W/ASPECT_RATIO_STANDARD;cells.push({id,x:0,y,w:W,h,isPinned:true});y+=h;});if(vis.length>0){const aH=H-y;if(aH>0){const cc=findBestPortraitColumns(vis.length,W,aH,PORTRAIT_HEIGHT_THRESHOLD);const uW=W/cc,h=uW/ASPECT_RATIO_STANDARD;for(let i=0;i<vis.length;i++){const r=Math.floor(i/cc),col=i%cc;cells.push({id:vis[i][0],x:col*uW,y:y+r*h,w:uW,h:h});}}}}layoutDirty=false;return{cells};};
const updateLayout=()=>{if(container.offsetWidth===0||container.offsetHeight===0){requestAnimationFrame(updateLayout);return;}const{cells}=calculateLayout();cells.forEach(c=>{const w=elementCache.get(c.id);if(w){w.style.transform='translate('+Math.round(c.x)+'px,'+Math.round(c.y)+'px)';w.style.width=Math.round(c.w)+'px';w.style.height=Math.round(c.h)+'px';w.style.zIndex=c.isPinned?'100':'1';const scaler=w.querySelector('.video-scaler'),ifr=w.querySelector('iframe');if(scaler&&ifr){if(isHomepage(c.id)||isFrameMode(c.id)){scaler.style.width=c.w+'px';scaler.style.height=c.h+'px';ifr.style.width='100%';ifr.style.height='100%';scaler.style.transform='scale(1)';}else if(SPOOF_ENABLED&&!isChatId(c.id)){let levelIdx=0;for(let i=0;i<RESOLUTION_PRESETS.length;i++){if(c.w>=SCREEN_WIDTH/(i+1)){levelIdx=i;break;}}const lvl=RESOLUTION_PRESETS[levelIdx]||5;const res=RESOLUTION_LEVELS[lvl];const tw=res?res[0]:854,th=res?res[1]:480;scaler.style.width=tw+'px';scaler.style.height=th+'px';ifr.style.width=tw+'px';ifr.style.height=th+'px';scaler.style.transform='scale('+(c.w/tw)+','+(c.h/th)+')';}else{scaler.style.width=c.w+'px';scaler.style.height=c.h+'px';ifr.style.width='100%';ifr.style.height='100%';scaler.style.transform='scale(1)';}scaler.style.transformOrigin='top left';}}});};
const scheduleLayout=()=>{layoutDirty=true;clearTimeout(resizeTimer);resizeTimer=setTimeout(updateLayout,DEBOUNCE_MS);};
const swapOrder=(a,b)=>{const o1=idOrderMap.get(a),o2=idOrderMap.get(b);if(o1!==undefined&&o2!==undefined){idOrderMap.set(a,o2);idOrderMap.set(b,o1);safeSave();scheduleLayout();}};
const moveTop=id=>{const c=idOrderMap.get(id);if(c===undefined||c===0)return;const orders=Array.from(idOrderMap.values());if(orders.length===0)return;const mn=Math.min(...orders);for(let[i,o]of idOrderMap)if(o>=mn&&o<c)idOrderMap.set(i,o+1);idOrderMap.set(id,mn-1);safeSave();scheduleLayout();};
const moveBottom=id=>{const c=idOrderMap.get(id);if(c===undefined)return;const orders=Array.from(idOrderMap.values());if(orders.length===0)return;const mx=Math.max(...orders);if(c===mx)return;for(let[i,o]of idOrderMap)if(o>c&&o<=mx)idOrderMap.set(i,o-1);idOrderMap.set(id,mx+1);safeSave();scheduleLayout();};
const moveDown=id=>{const c=idOrderMap.get(id);if(c===undefined)return;let nId=null,nO=Infinity;for(let[i,o]of idOrderMap)if(o>c&&o<nO){nO=o;nId=i;}if(nId)swapOrder(id,nId);};
const mkBtn=(cls,fn)=>{const d=document.createElement('div');d.className=cls;d.onclick=e=>{e.stopPropagation();fn(e);};return d;};
const renumberOrders=()=>{const entries=Array.from(idOrderMap.entries()).sort((a,b)=>a[1]-b[1]);idOrderMap.clear();entries.forEach(([id],idx)=>idOrderMap.set(id,(idx+1)*2));};
const getVideoDomainMode=(vid)=>{const base=getBaseId(vid);const stored=localStorage.getItem(VIDEO_DOMAIN_MODE_PREFIX+base);return stored||DEFAULT_DOMAIN_MODE;};
const setVideoDomainMode=(vid,mode)=>{const base=getBaseId(vid);if(mode==='YT'||mode==='YU')localStorage.setItem(VIDEO_DOMAIN_MODE_PREFIX+base,mode);};
const cleanupVideoDomainMode=(videoId)=>{const baseId=getBaseId(videoId);let exists=false;for(let i=1;i<=LIST_COUNT;i++){const key='ytMulti_videoList'+i;const ids=safeJSONParse(localStorage.getItem(key),[]);if(ids.some(id=>getBaseId(id)===baseId)){exists=true;break;}}if(!exists){localStorage.removeItem(VIDEO_DOMAIN_MODE_PREFIX+baseId);}};
const safeSave=()=>{try{const cleanArr=[...idOrderMap.entries()].sort((a,b)=>a[1]-b[1]).map(e=>e[0]);localStorage.setItem(STORAGE_LISTS[blobCurrentList],JSON.stringify(cleanArr));localStorage.setItem(PINNED_PREFIX+blobCurrentList,JSON.stringify(pinnedIds));const orders=Array.from(idOrderMap.values());if(orders.length>0&&(Math.max(...orders)>10000||Math.min(...orders)<-10000)){renumberOrders();}}catch(e){console.warn('[Blob] SafeSave error:',e);}};
const btnScale=BTN_SIZE_SCALE/100;const btnW=Math.round(20*btnScale);const btnH=Math.round(20*btnScale);const btnFs=Math.round(14*btnScale);const btnGap=0;const baseOffset=6;
const emptySwitcher = document.createElement('div');
emptySwitcher.id = 'empty-list-switcher';
emptySwitcher.style.cssText = 'position:fixed;top:10px;left:10px;display:none;flex-direction:row;gap:8px;z-index:99999;';
document.body.appendChild(emptySwitcher);
for (let i = 1; i <= LIST_COUNT; i++) {
const btn = document.createElement('button');
btn.textContent = i;
btn.dataset.num = i;
btn.style.cssText = 'width:40px;height:40px;border-radius:5px;background:#ff4444;color:white;border:none;cursor:pointer;font-size:18px;font-weight:bold;box-shadow:0 2px 5px rgba(0,0,0,0.3);';
btn.addEventListener('click', () => { switchList('list' + i); });
emptySwitcher.appendChild(btn);
}
const updateEmptySwitcher = () => {
if (idOrderMap.size === 0) {
emptySwitcher.style.display = 'flex';
emptySwitcher.querySelectorAll('button').forEach(b => {
b.style.background = ('list' + b.dataset.num === blobCurrentList) ? '#aa0000' : '#ff4444';
});
} else {
emptySwitcher.style.display = 'none';
}
};
const switchList=(targetKey)=>{if(targetKey===blobCurrentList)return;blobCurrentList=targetKey;localStorage.setItem(STORAGE_CURRENT,targetKey);const newKey=STORAGE_LISTS[targetKey];const nIds=safeJSONParse(localStorage.getItem(newKey),[]);const nPin=safeJSONParse(localStorage.getItem(PINNED_PREFIX+targetKey),[]);pinnedIds=nPin||[];clearAllElements();idOrderMap.clear();let o=0;nIds.forEach(id=>{o+=2;idOrderMap.set(id,o);const el=createVideo(id,o);if(el)container.appendChild(el);});scheduleLayout();document.querySelectorAll('.list-switch-btn').forEach(b=>{b.style.background=(('list'+b.dataset.num)===blobCurrentList)?'#aa0000':'#ff4444';});updateEmptySwitcher();};
const createVideo=(id,order)=>{const isHome=isHomepage(id);const isFrame=isFrameMode(id);const vid=isHome||isFrame?null:(isFullUrl(id)?getVideoIdFromUrl(id):getVideoId(id));if(!isHome&&!isFrame&&(!vid||!/^[A-Za-z0-9_-]{11}$/.test(vid)))return null;const w=document.createElement('div');w.className='video-wrapper'+(isChatId(id)?' is-chat':'');w.dataset.id=id;w.style.willChange='transform,opacity';setupHoverLogic(w,id);const isC=isChatId(id);const frameDomain=(isHome||isC||isFrame)?HOME_DOMAIN:(getVideoDomainMode(id)==='YU'?'www.youtube-nocookie.com':DOMAIN_MODE);const listParam=isFullUrl(id)?getListIdFromUrl(id):null;let src;if(isHome){src=HOME_TARGET_URL||'https://'+frameDomain+'/';}else if(isFrame){const fVid=getFrameVideoId(id);src='https://'+frameDomain+'/watch?v='+fVid;}else if(isC){src='https://'+frameDomain+'/live_chat?v='+getBaseId(id);}else{if(frameDomain==='www.youtube-nocookie.com'){const baseVid=getBaseId(id);src='https://www.youtube-nocookie.com/embed/'+baseVid+'?playlist='+baseVid+'&autoplay=1&iv_load_policy=3&loop=1&start='+(listParam?'&list='+listParam:'');}else{src='https://'+frameDomain+'/embed/'+vid+'?'+YT_PARAMS+(listParam?'&list='+listParam:'');}}const scaler=document.createElement('div');scaler.className='video-scaler';scaler.style.cssText='will-change:transform;transform-origin:top left;background:#000;backface-visibility:hidden;-webkit-backface-visibility:hidden;image-rendering:crisp-edges;image-rendering:pixelated;';const ifr=document.createElement('iframe');ifr.style.cssText='display:block;border:0;outline:0;margin:0;padding:0;background:#000;transform:translateZ(0);-webkit-transform:translateZ(0);image-rendering:crisp-edges;image-rendering:pixelated;';ifr.src=src;ifr.allow='autoplay;fullscreen';ifr.sandbox='allow-same-origin allow-scripts allow-popups allow-popups-to-escape-sandbox allow-forms allow-modals';const del=mkBtn('remove-btn',()=>{clearElementCache(id);idOrderMap.delete(id);const p=pinnedIds.indexOf(id);if(p!==-1)pinnedIds.splice(p,1);cleanupVideoDomainMode(id);safeSave();scheduleLayout();updateEmptySwitcher();});const pin=mkBtn('pin-btn',()=>{const i=pinnedIds.indexOf(id);if(i!==-1)pinnedIds.splice(i,1);else{if(pinnedIds.length>=MAX_PINNED)pinnedIds.pop();pinnedIds.push(id);}safeSave();scheduleLayout();});const domainToggle=mkBtn('domain-toggle-btn',()=>{if(isHome||isFrame)return;const cur=getVideoDomainMode(id);const nxt=cur==='YT'?'YU':'YT';setVideoDomainMode(id,nxt);clearElementCache(id);const nw=createVideo(id,order);if(nw){container.appendChild(nw);elementCache.set(id,nw);}scheduleLayout();});domainToggle.textContent='';const frameToggle=mkBtn('frame-toggle-btn',()=>{let vId=null,newId=null;if(isHome||isFrame){try{vId=ifr.contentDocument?.querySelector('[video-id]')?.getAttribute('video-id');}catch(e){}if(!vId)vId=getVideoIdFromUrl(ifr.src)||getFrameVideoId(id);if(!vId||!/^[A-Za-z0-9_-]{11}$/.test(vId))return;newId=vId+VIDEO_SUFFIX;}else{const baseVid=isFullUrl(id)?getVideoIdFromUrl(id):getVideoId(id);if(!baseVid)return;newId='frame:'+baseVid;}if(idOrderMap.has(newId))return;const pIdx=pinnedIds.indexOf(id);if(pIdx!==-1){pinnedIds[pIdx]=newId;}idOrderMap.delete(id);idOrderMap.set(newId,order);clearElementCache(id);const nw=createVideo(newId,order);if(nw){container.appendChild(nw);elementCache.set(newId,nw);}safeSave();scheduleLayout();});frameToggle.textContent='';const chatT=mkBtn('chat-toggle-btn',()=>{if(isHome||isFrame)return;const p=isC?(isFullUrl(id)?id.replace(CHAT_SUFFIX,''):getBaseId(id)):(isFullUrl(id)?id+CHAT_SUFFIX:getChatId(id));if(idOrderMap.has(p))return;idOrderMap.delete(id);idOrderMap.set(p,isC?order-1:order+1);clearElementCache(id);const nw=createVideo(p,isC?order-1:order+1);if(nw){container.appendChild(nw);elementCache.set(p,nw);}scheduleLayout();});const chatA=mkBtn('add-chat-btn',()=>{if(isHome)return;if(isFrame){const fVid=getFrameVideoId(id);if(!fVid||!/^[A-Za-z0-9_-]{11}$/.test(fVid))return;const newId=fVid+VIDEO_SUFFIX;if(idOrderMap.has(newId))return;idOrderMap.set(newId,order+1);const nw=createVideo(newId,order+1);if(nw){container.appendChild(nw);elementCache.set(newId,nw);}safeSave();scheduleLayout();return;}const base=isFullUrl(id)?getVideoIdFromUrl(id):getBaseId(id);if(!base||!/^[A-Za-z0-9_-]{11}$/.test(base))return;let cnt=0;for(const k of idOrderMap.keys())if(k.includes('_chat')&&k.startsWith(base))cnt++;const newId=base+CHAT_SUFFIX+'_'+(cnt+1);idOrderMap.set(newId,order+1);const nw=createVideo(newId,order+1);if(nw){container.appendChild(nw);elementCache.set(newId,nw);}safeSave();scheduleLayout();});const top=mkBtn('top-btn',()=>{moveTop(id);});const up=mkBtn('up-btn',()=>{const c=idOrderMap.get(id);if(c===undefined)return;let nId=null,nO=-Infinity;for(let[oid,o]of idOrderMap)if(o<c&&o>nO){nO=o;nId=oid;}if(nId)swapOrder(id,nId);});const dn=mkBtn('down-btn',()=>{moveDown(id);});const bot=mkBtn('bottom-btn',()=>{moveBottom(id);});const copyCol=document.createElement('div');copyCol.className='copy-col';copyCol.style.cssText='position:absolute;top:6px;left:32px;display:flex;flex-direction:column;gap:4px;z-index:9999;';for(let i=1;i<=LIST_COUNT;i++){const targetKey='list'+i;if(targetKey===blobCurrentList){const ph=document.createElement('div');ph.className='copy-placeholder';ph.style.width=btnW+'px';ph.style.height=btnH+'px';copyCol.appendChild(ph);}else{const cb=mkBtn('copy-btn',()=>{const tk=STORAGE_LISTS[targetKey];const ti=safeJSONParse(localStorage.getItem(tk),[]);if(!ti.includes(id)){ti.push(id);localStorage.setItem(tk,JSON.stringify(ti));try{localStorage.setItem(SYNC_EVENT_KEY,JSON.stringify({type:'videoAdded',listKey:targetKey,videoId:id,timestamp:Date.now()}));}catch(e){console.warn('[YT-Multi-Blob] Copy sync error:',e);}}});cb.dataset.num=i;cb.style.cssText='position:relative;width:'+btnW+'px;height:'+btnH+'px;border-radius:3px;background:#4488ff;color:white;border:none;cursor:pointer;font-size:'+btnFs+'px;';copyCol.appendChild(cb);}}for(let i=1;i<=LIST_COUNT;i++){const targetKey='list'+i;const sb=mkBtn('list-switch-btn',()=>switchList(targetKey));sb.dataset.num=i;sb.style.cssText='position:relative;width:'+btnW+'px;height:'+btnH+'px;border-radius:3px;background:'+((targetKey===blobCurrentList)?'#aa0000':'#ff4444')+';color:white;border:none;cursor:pointer;font-size:'+btnFs+'px;';copyCol.appendChild(sb);}if(copyCol.children.length>0)w.appendChild(copyCol);scaler.appendChild(ifr);w.append(scaler,del,pin,domainToggle,frameToggle,chatT,chatA,top,up,dn,bot);const btnList=[{el:del,key:'del'},{el:pin,key:'pin'},{el:domainToggle,key:'domainToggle'},{el:frameToggle,key:'frameToggle'},{el:chatT,key:'chatT'},{el:chatA,key:'chatA'},{el:top,key:'top'},{el:up,key:'up'},{el:dn,key:'dn'},{el:bot,key:'bot'}];if(BTN_LAYOUT===1||BTN_LAYOUT===2){const isRight=BTN_LAYOUT===2;btnList.forEach((btn,idx)=>{const pos=isRight?{top:baseOffset+idx*(btnH+btnGap),right:baseOffset}:{top:baseOffset+idx*(btnH+btnGap),left:baseOffset};Object.entries(pos).forEach(([p,v])=>btn.el.style[p]=v+'px');btn.el.style.width=btnW+'px';btn.el.style.height=btnH+'px';btn.el.style.fontSize=btnFs+'px';});const copyOffset=baseOffset+btnList.length*(btnH+btnGap);copyCol.style.cssText='position:absolute;top:'+baseOffset+'px;'+(isRight?'right:':'left:')+(baseOffset+btnW)+'px;display:flex;flex-direction:column;gap:'+btnGap+'px;z-index:9999;';copyCol.querySelectorAll('.copy-btn,.copy-placeholder,.list-switch-btn').forEach(c=>{c.style.width=btnW+'px';c.style.height=btnH+'px';});}else{btnList.forEach((btn,idx)=>{const pos={top:baseOffset,left:baseOffset+idx*(btnW+btnGap)};Object.entries(pos).forEach(([p,v])=>btn.el.style[p]=v+'px');btn.el.style.width=btnW+'px';btn.el.style.height=btnH+'px';btn.el.style.fontSize=btnFs+'px;';});const copyStart=baseOffset+btnList.length*(btnW+btnGap);copyCol.style.cssText='position:absolute;top:'+baseOffset+'px;left:'+copyStart+'px;display:flex;flex-direction:row;gap:'+btnGap+'px;z-index:9999;';copyCol.querySelectorAll('.copy-btn,.copy-placeholder,.list-switch-btn').forEach(c=>{c.style.width=btnW+'px';c.style.height=btnH+'px';});}elementCache.set(id,w);return w;};
const frag=document.createDocumentFragment();Array.from(idOrderMap.entries()).sort((a,b)=>a[1]-b[1]).forEach(([id,order])=>{const el=createVideo(id,order);if(el)frag.appendChild(el);});container.appendChild(frag);updateLayout();
const homeBtn=document.createElement('div');homeBtn.className='home-add-btn';homeBtn.textContent='+';homeBtn.style.cssText='position:fixed;bottom:20px;right:20px;width:64px;height:64px;border-radius:50%;background:rgba(20,20,20,0.85);color:#fff;border:2px solid #555;font-size:36px;cursor:pointer;z-index:99999;display:flex;align-items:center;justify-content:center;box-shadow:0 4px 12px rgba(0,0,0,0.5);transition:opacity 0.2s, transform 0.2s;opacity:0;pointer-events:none;transform:scale(0.8);user-select:none;';
const homeTrigger=document.createElement('div');homeTrigger.style.cssText='position:fixed;bottom:0;right:0;width:80px;height:80px;z-index:100000;cursor:default;';homeTrigger.appendChild(homeBtn);document.body.appendChild(homeTrigger);setupEnterShowHide(homeTrigger,homeBtn,HOVER_TIMEOUT);
homeBtn.addEventListener('click',()=>{const hId='homepage_'+Date.now();const currentOrders=Array.from(idOrderMap.values());const newOrder=currentOrders.length>0?Math.max(...currentOrders)+2:1;idOrderMap.set(hId,newOrder);if(HOME_PINNED_BY_DEFAULT){if(pinnedIds.length>=MAX_PINNED)pinnedIds.pop();pinnedIds.push(hId);}const w=createVideo(hId,newOrder);if(w){container.appendChild(w);elementCache.set(hId,w);}safeSave();scheduleLayout();updateEmptySwitcher();});
window.addEventListener('resize',scheduleLayout,{passive:true});
window.addEventListener('storage',e=>{if(e.key===STORAGE_LISTS[blobCurrentList]){const now=Date.now();if(now-lastSyncTime<200)return;lastSyncTime=now;if(storageSyncTimer)clearTimeout(storageSyncTimer);storageSyncTimer=setTimeout(()=>{storageSyncTimer=null;try{const newIds=safeJSONParse(localStorage.getItem(STORAGE_LISTS[blobCurrentList]),[]);const newPinned=safeJSONParse(localStorage.getItem(PINNED_PREFIX+blobCurrentList),[]);pinnedIds=newPinned;const newSet=new Set(newIds);for(const id of[...idOrderMap.keys()]){if(!newSet.has(id))clearElementCache(id);}let currentMax=idOrderMap.size>0?Math.max(...idOrderMap.values()):0;newIds.forEach(id=>{if(!idOrderMap.has(id)){currentMax+=2;idOrderMap.set(id,currentMax);if(!container.querySelector('.video-wrapper[data-id="'+id+'"]')){const el=createVideo(id,currentMax);if(el)container.appendChild(el);}}});scheduleLayout();updateEmptySwitcher();}catch(err){console.warn('[YT-Multi-Blob] Sync error:',err);}},100);}});
window.addEventListener('beforeunload',()=>{clearTimeout(resizeTimer);clearAllElements();clearAllTimers();});
updateEmptySwitcher();
})();
`.trim();
const css = `
body{margin:0;padding:0;background:#000;overflow:hidden}
.container{position:absolute;top:0;left:0;width:100vw;height:100vh;display:flex;flex-wrap:wrap;align-content:flex-start}
.video-wrapper{position:absolute;overflow:hidden;background:#000;transition:transform 0.2s ease,opacity 0.2s ease;will-change:transform,opacity;backface-visibility:hidden;transform:translate3d(0,0,0);-webkit-transform:translate3d(0,0,0)}
.video-scaler{will-change:transform;transform-origin:top left}
.video-wrapper iframe{display:block;border:0;outline:0;margin:0;padding:0;background:#000;transform:translateZ(0);-webkit-transform:translateZ(0)}
.remove-btn,.pin-btn,.domain-toggle-btn,.frame-toggle-btn,.chat-toggle-btn,.add-chat-btn,.top-btn,.up-btn,.down-btn,.bottom-btn{position:absolute;border-radius:3px;display:none;cursor:pointer;z-index:9999;box-shadow:0 0 3px rgba(0,0,0,0.3)}
.remove-btn{background:#ff4444}
.pin-btn{background:#44aaff}
.domain-toggle-btn{background:#aa44aa}
.frame-toggle-btn{background:#44aaaa}
.chat-toggle-btn{background:#888888}
.add-chat-btn{background:#44aa44}
.top-btn{background:#ffaa44}
.up-btn{background:#88cc44}
.down-btn{background:#44cc88}
.bottom-btn{background:#aa44ff}
.remove-btn::after{content:'×';color:white;font-size:16px;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center;line-height:20px}
.pin-btn::after{content:'📌';color:white;font-size:14px;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center;line-height:20px}
.domain-toggle-btn::after{content:'T';color:white;font-size:14px;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);line-height:20px;pointer-events:none;}
.frame-toggle-btn::after{content:'F';color:white;font-size:14px;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);line-height:20px;pointer-events:none;}
.chat-toggle-btn::after{content:'🔄';color:white;font-size:14px;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center;line-height:20px}
.add-chat-btn::after{content:'+';color:white;font-size:16px;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center;line-height:20px}
.top-btn::after{content:'⤒';color:white;font-size:14px;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center;line-height:20px}
.up-btn::after{content:'↑';color:white;font-size:16px;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center;line-height:20px}
.down-btn::after{content:'↓';color:white;font-size:16px;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center;line-height:20px}
.bottom-btn::after{content:'⤓';color:white;font-size:14px;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center;line-height:20px}
.copy-col{position:absolute;display:flex;gap:4px;z-index:9999}
.copy-btn,.copy-placeholder,.list-switch-btn{border-radius:3px;display:none}
.copy-btn{position:relative;background:#4488ff;color:white;border:none;cursor:pointer}
.copy-btn::after{content:attr(data-num);color:white;font-size:14px;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center;line-height:20px}
.list-switch-btn{position:relative;background:#ff4444;color:white;border:none;cursor:pointer}
.list-switch-btn::after{content:attr(data-num);color:white;font-size:14px;position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center;line-height:20px}
.copy-placeholder{background:transparent;cursor:default}
.home-add-btn{width:64px;height:64px;border-radius:50%;background:rgba(20,20,20,0.85);color:#fff;border:2px solid #555;font-size:36px;cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:0 4px 12px rgba(0,0,0,0.5);transition:opacity 0.2s, transform 0.2s;user-select:none;}
`.trim();
const nonceAttr = nonce ? ` nonce="${nonce}"` : '';
const html = `<!DOCTYPE html><html><head><meta charset="UTF-8"><title>Multi-Player</title><style>${css}</style></head><body><div class="container"></div><script${nonceAttr}>${jsCode}<\/script></body></html>`;
return URL.createObjectURL(new Blob([html], { type: 'text/html' }));
};
const openMultiPlayer = () => {
const k = STORAGE_LISTS[currentList];
let ids = safeJSONParse(localStorage.getItem(k), []);
const pinnedK = STORAGE_PINNED_PREFIX + currentList;
let pinned = safeJSONParse(localStorage.getItem(pinnedK), []);
if (!ids || ids.length === 0) {
ids = ['homepage_1'];
localStorage.setItem(k, JSON.stringify(ids));
updateListButtonCount();
if (CONFIG.HOME_PINNED_BY_DEFAULT && !pinned.includes('homepage_1')) pinned.push('homepage_1');
}
const url = makeBlobPage(ids, currentList, pinned, getCurrentDomainMode().domain);
const mode = localStorage.getItem(STORAGE_MODE) || 'current_tab';
const cleanupBlob = () => {
try {
URL.revokeObjectURL(url);
} catch (e) {}
};
if (mode === 'current_tab') {
location.href = url;
setTimeout(cleanupBlob, 3000);
} else if (mode === 'new_window') {
window.open(url, '_blank', 'width=800,height=600,scrollbars=no,resizable=yes');
setTimeout(cleanupBlob, 2000);
} else {
window.open(url, '_blank');
setTimeout(cleanupBlob, 2000);
}
window.addEventListener('pagehide', cleanupBlob, { once: true });
};
playBtn.addEventListener('click', openMultiPlayer);
const checkAutoLaunch = () => {
const params = new URLSearchParams(window.location.search);
const autoKey = params.get('ytMulti_auto');
if (autoKey && STORAGE_LISTS[autoKey]) {
currentList = autoKey;
localStorage.setItem('ytMulti_currentList', currentList);
updateListButtonCount();
history.replaceState(null, '', window.location.pathname);
setTimeout(openMultiPlayer, 400);
}
};
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', checkAutoLaunch);
else checkAutoLaunch();
applyPanelMode(isWatchPage());
setupSyncListener();
setupNavigationObserver();
window.addEventListener('beforeunload', () => {
stopUrlPolling();
wprObserver?.disconnect();
stopObservingVideos();
});
} else {
if (document.readyState === 'loading')
document.addEventListener('DOMContentLoaded', () => setTimeout(startObservingVideos, 1000));
else setTimeout(startObservingVideos, 1000);
window.addEventListener('beforeunload', stopObservingVideos);
}
})();