YouTube Multi-Player

Fix initialization issues. 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.

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey, Greasemonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

You will need to install an extension such as Tampermonkey to install this script.

คุณจะต้องติดตั้งส่วนขยาย เช่น Tampermonkey หรือ Violentmonkey เพื่อติดตั้งสคริปต์นี้

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

Advertisement:

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

Advertisement:

// ==UserScript==
// @name         YouTube Multi-Player
// @name:zh-TW   YouTube 多重播放器
// @namespace    http://tampermonkey.net/
// @author       Dxzy
// @version      8.12.7
// @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  Fix initialization issues. 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;};
      `.replace(/\n\s*/g, '').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 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';});};
        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();});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();});
        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();}catch(err){console.warn('[YT-Multi-Blob] Sync error:',err);}},100);}});
        window.addEventListener('beforeunload',()=>{clearTimeout(resizeTimer);clearAllElements();clearAllTimers();});
        })();
      `.replace(/\n\s*/g, '').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;}
      `.replace(/\n\s*/g, '').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);
  }
})();