Iwara NeoUI

Enhanced UI for Iwara with tabbed layout, theater mode, customizable sections, and improved video page experience.

Du musst eine Erweiterung wie Tampermonkey, Greasemonkey oder Violentmonkey installieren, um dieses Skript zu installieren.

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

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

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.

Sie müssten eine Skript Manager Erweiterung installieren damit sie dieses Skript installieren können

(Ich habe schon ein Skript Manager, Lass mich es installieren!)

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!)

// ==UserScript==
// @name         Iwara NeoUI
// @namespace    neoUI-iwara
// @version      0.3.0
// @description  Enhanced UI for Iwara with tabbed layout, theater mode, customizable sections, and improved video page experience.
// @author       0xdev
// @license      LGPL-3.0-or-later
// @match        https://www.iwara.tv/*
// @match        https://iwara.tv/*
// @grant        GM.getValue
// @grant        GM.setValue
// @run-at       document-idle
// ==/UserScript==

(() => {
  'use strict';
  if (window.__IWARA_NEOUI_ACTIVE) return;
  window.__IWARA_NEOUI_ACTIVE = true;

  // ---- Constants ----
  const TIMEOUTS = {
    WAIT_FOR: 20000,
    POLL_INTERVAL: 250,
    SLEEP_SHORT: 500,
    OBSERVER_DEBOUNCE: 150
  };

  const SELECTORS = {
    VIDEO_COL: '.col-12.col-md-9',
    PAGE_VIDEO_CONTENT: '.page-video__content',
    COL_MD_9: '[class*="col-"][class*="md-9"]',
    SIDEBAR: '.page-video__sidebar',
    LIKES_LIST: '.likesList',
    LIKED_BY: '.itw-liked-by',
    RECS: '.itw-recs',
    TABBAR: '.itw-tabbar',
    PANELS: '.itw-panels'
  };

  const CSS_CLASSES = {
    TABS_ACTIVE: 'itw-tabs-active',
    THEATER: 'itw-theater'
  };

  // ---- DOM Cache ----
  const domCache = {
    cache: new Map(),
    get(selector, root = document) {
      const key = `${selector}:${root === document ? 'doc' : 'custom'}`;
      if (this.cache.has(key)) {
        const cached = this.cache.get(key);
        // Verify element is still in DOM
        if (cached && cached.isConnected) {
          return cached;
        }
        this.cache.delete(key);
      }
      const element = root.querySelector(selector);
      if (element) {
        this.cache.set(key, element);
      }
      return element;
    },
    clear() {
      this.cache.clear();
    }
  };

  // ---- Observer Manager ----
  const observerManager = {
    observers: new Map(),
    
    create(name, callback) {
      if (this.observers.has(name)) {
        this.disconnect(name);
      }
      
      const observer = new MutationObserver(callback);
      this.observers.set(name, observer);
      return observer;
    },
    
    observe(name, target = document.documentElement, options = { childList: true, subtree: true }) {
      const observer = this.observers.get(name);
      if (observer) {
        observer.observe(target, options);
      }
    },
    
    disconnect(name) {
      const observer = this.observers.get(name);
      if (observer) {
        observer.disconnect();
        this.observers.delete(name);
      }
    }
  };

  // ---- Utilities ----
  const dom = {
    el(html) {
      const t = document.createElement('template');
      t.innerHTML = html.trim();
      return t.content.firstElementChild;
    },
    on(el, evt, cb) { el && el.addEventListener(evt, cb, { passive: true }); },
    qs(sel, root = document) { return root.querySelector(sel); },
    qsa(sel, root = document) { return [...root.querySelectorAll(sel)]; },
  };

  const sleep = (ms = TIMEOUTS.SLEEP_SHORT) => new Promise(r => setTimeout(r, ms));

  const waitFor = async (selector, { root = document, timeout = TIMEOUTS.WAIT_FOR, interval = TIMEOUTS.POLL_INTERVAL } = {}) => {
    const end = Date.now() + timeout;
    while (Date.now() < end) {
      const node = root.querySelector(selector);
      if (node) return node;
      await sleep(interval);
    }
    return null;
  };

  const addStyle = (css) => {
    const s = document.createElement('style');
    s.textContent = css;
    document.documentElement.appendChild(s);
    return s;
  };

  // Persistent observer for Likes relocation (initialized only when new UI is active)
  let __itwLikesObserver = null;

  // ---- Storage (GM or localStorage fallback) ----
  const store = {
    async get(key, def) {
      try { if (typeof GM?.getValue === 'function') return await GM.getValue(key, def); } catch {}
      try { const v = localStorage.getItem(`itw:${key}`); return v == null ? def : JSON.parse(v); } catch { return def; }
    },
    async set(key, val) {
      try { if (typeof GM?.setValue === 'function') return await GM.setValue(key, val); } catch {}
      try { localStorage.setItem(`itw:${key}`, JSON.stringify(val)); } catch {}
    }
  };

  // ---- Defaults ----
  const DEFAULTS = Object.freeze({
    hideLikedBy: true,
    theaterMode: false,
    cinemaHideNav: false,
    newUI: false,
    showLikesTab: true,
    showRecsTab: true,
  });

  let settings = { ...DEFAULTS };
  // Runtime-only cinema state (not persisted when cinemaHideNav is true)
  let cinemaActive = false;

  const loadSettings = async () => {
    try {
      const saved = await store.get('settings', {});
      
      // Validate saved settings
      if (typeof saved !== 'object' || saved === null) {
        console.warn('[Iwara NeoUI] Invalid settings format, using defaults');
        settings = { ...DEFAULTS };
        return;
      }
      
      // Validate individual setting types
      const validatedSettings = { ...DEFAULTS };
      for (const [key, value] of Object.entries(saved)) {
        if (key in DEFAULTS) {
          const expectedType = typeof DEFAULTS[key];
          if (typeof value === expectedType) {
            validatedSettings[key] = value;
          } else {
            console.warn(`[Iwara NeoUI] Invalid type for setting '${key}', expected ${expectedType}, got ${typeof value}`);
          }
        }
      }
      
      settings = validatedSettings;
      
      // Migration: if legacy hideLikedBy was true and showLikesTab not explicitly set, hide the Likes tab by default
      if (saved && 'hideLikedBy' in saved && !('showLikesTab' in saved)) {
        settings.showLikesTab = !saved.hideLikedBy;
      }
    } catch (e) {
      console.warn('[Iwara NeoUI] loadSettings failed:', e);
      settings = { ...DEFAULTS };
    }
  };

  const saveSettings = async () => {
    try {
      await store.set('settings', settings);
    } catch (e) {
      console.warn('[Iwara NeoUI] saveSettings failed:', e);
    }
  };

  // ---- CSS for features ----
  addStyle(`
    /* Header button */
    .itw-btn { display:inline-flex; align-items:center; gap:.35rem; padding:.35rem .55rem; border-radius:.5rem; border:1px solid rgba(255,255,255,.12); color:inherit; background:rgba(255,255,255,.06); cursor:pointer; font:600 12px/1.2 system-ui, -apple-system, Segoe UI, Roboto, sans-serif; }
    .itw-btn:hover { background:rgba(255,255,255,.12); }
    .itw-gear { width:16px; height:16px; fill:currentColor; }
    .itw-btn.itw-float { position: fixed; top: 12px; right: 12px; z-index: 2147483646; }
    .itw-btn.itw-in-header { margin-left: 8px; }

    /* Modal */
    .itw-modal-backdrop { position:fixed; inset:0; background:rgba(0,0,0,.5); display:none; z-index: 99999; }
    .itw-modal { position:fixed; inset:auto auto 0 0; left:50%; top:50%; transform:translate(-50%,-50%); width:min(520px, calc(100vw - 24px)); background:#16181d; color:#e6e6e6; border:1px solid #2a2f36; border-radius:12px; box-shadow:0 10px 40px rgba(0,0,0,.45); padding:16px; display:none; z-index: 100000; }
    .itw-modal h3 { margin:0 0 12px; font:600 16px/1.3 system-ui, -apple-system, Segoe UI, Roboto, sans-serif; }
    .itw-row { display:flex; align-items:center; justify-content:space-between; gap:12px; padding:10px 0; border-top:1px solid #232831; }
    .itw-row:first-of-type { border-top:none; }
    .itw-actions { display:flex; gap:8px; justify-content:flex-end; padding-top:12px; }
    .itw-switch { position:relative; width:44px; height:26px; border-radius:999px; background:#3a404b; transition:.2s; flex:0 0 auto; }
    .itw-switch input { position:absolute; inset:0; opacity:0; }
    .itw-knob { position:absolute; top:3px; left:3px; width:20px; height:20px; border-radius:50%; background:#c9c9c9; transition:.2s; }
    .itw-switch input:checked + .itw-knob { left:21px; background:#64d36d; }

    /* Feature styles */
    .itw-hide-liked-by .itw-liked-by, .itw-hide-liked-by section:has(> h2.itw-liked-by-title) { display:none !important; }
    /* Hide the likes block used by iwara.tv preview/site as well */
    .itw-hide-liked-by .block:has(.likesList), .itw-hide-liked-by .likesList { display:none !important; }
    /* Hide Recommended (More like this) when its tab is disabled in new UI */
    .itw-hide-recs .itw-recs, .itw-hide-recs .moreLikeThis { display:none !important; }
    /* When tabs UI is active, ensure any stray Recommended blocks are hidden outside the Recommended panel */
    body.itw-tabs-active .moreLikeThis { display:none !important; }
    body.itw-tabs-active .itw-panel-recs .moreLikeThis { display:block !important; }

    /* Cinema Mode — base (always applied when cinema is active) */
    body.itw-theater { overflow-y: auto; }
    body.itw-theater .page-video__sidebar,
    body.itw-theater aside { display: none !important; }
    /* Expand video content area — scoped to .page so header is untouched */
    body.itw-theater .page .container-fluid { max-width: 100% !important; width: 100% !important; padding-left: 0 !important; padding-right: 0 !important; }
    body.itw-theater .page .row { margin-left: 0 !important; margin-right: 0 !important; }
    body.itw-theater .col-12.col-md-9 { max-width: 100% !important; flex: 0 0 100% !important; padding-left: 0 !important; padding-right: 0 !important; }
    /* Player: black backdrop, fill width */
    body.itw-theater .page-video__player { background: #000 !important; }
    body.itw-theater .video-js { width: 100% !important; aspect-ratio: 16/9 !important; max-height: 85vh !important; margin: 0 auto !important; }
    body.itw-theater .vjs-tech { width: 100% !important; height: 100% !important; object-fit: contain !important; }
    /* Restore padding below the player for tabs/content */
    body.itw-theater .itw-tabbar,
    body.itw-theater .itw-panels { padding-left: 16px !important; padding-right: 16px !important; }
    /* Cinema Mode — full immersive (hide nav when setting is on) */
    body.itw-theater-hide-nav header.header,
    body.itw-theater-hide-nav div.menu { display: none !important; }

    /* Tabs layout for video page */
    body.itw-tabs-active .col-12.col-md-9 { max-width: 100% !important; flex: 0 0 100% !important; }
    body.itw-tabs-active .page-video__sidebar { display: none !important; }
    body.itw-tabs-active .page-video__player, body.itw-tabs-active .video-js, body.itw-tabs-active .vjs_video_3-dimensions { width: 100% !important; }

    .itw-tabbar { display:flex; align-items:center; gap:8px; border-bottom:1px solid #2a2f36; margin-top:12px; }
    .itw-tabbar .itw-tab { appearance:none; background:none; border:none; color:#e6e6e6; cursor:pointer; padding:10px 12px; font:600 13px/1 system-ui, -apple-system, Segoe UI, Roboto, sans-serif; border-bottom:2px solid transparent; opacity:.85; }
    .itw-tabbar .itw-tab:hover { opacity:1; }
    .itw-tabbar .itw-tab[aria-selected="true"] { color:#ff4b4b; border-color:#ff4b4b; opacity:1; }

    .itw-panels { padding-top:12px; }
    .itw-panel { display:none; }
    .itw-panel.active { display:block; }
    /* Cinema toggle button in tab bar */
    .itw-cinema-btn { appearance:none; background:none; border:1px solid rgba(255,255,255,.15); color:#e6e6e6; cursor:pointer; padding:6px 10px; font:600 12px/1 system-ui, -apple-system, Segoe UI, Roboto, sans-serif; border-radius:6px; margin-left:auto; opacity:.7; transition: opacity .2s, background .2s; }
    .itw-cinema-btn:hover { opacity:1; background:rgba(255,255,255,.08); }
    .itw-cinema-btn.active { color:#ff4b4b; border-color:#ff4b4b; opacity:1; }
    /* Safety: if for any reason tabs markup lingers but layout class is absent, hide the UI */
    body:not(.itw-tabs-active) .itw-tabbar, body:not(.itw-tabs-active) .itw-panels { display:none !important; }

    /* Force visibility for Likes content inside the Likes panel */
    body.itw-tabs-active .itw-panel-likes .itw-liked-by,
    body.itw-tabs-active .itw-panel-likes .block,
    body.itw-tabs-active .itw-panel-likes .block__content,
    body.itw-tabs-active .itw-panel-likes .likesList { display:block !important; visibility:visible !important; opacity:1 !important; height:auto !important; max-height:none !important; min-height:auto !important; width:auto !important; min-width:0 !important; }
    /* Two-row layout: users row + pagination row */
    body.itw-tabs-active .itw-panel-likes .block { display:flex !important; flex-direction:column !important; gap:16px !important; }
    /* Override Bootstrap row layout for horizontal scrolling */
    body.itw-tabs-active .itw-panel-likes .likesList .row { display:flex !important; flex-direction:row !important; flex-wrap:nowrap !important; gap:16px !important; overflow-x:auto !important; padding:8px 0 !important; margin:0 !important; }
    /* Override Bootstrap columns to become flex items */
    body.itw-tabs-active .itw-panel-likes .likesList .row > [class*="col-"] { flex:0 0 auto !important; width:160px !important; max-width:160px !important; min-width:160px !important; padding:0 !important; }
    /* Individual user items - compact horizontal cards */
    body.itw-tabs-active .itw-panel-likes .likesList__item { display:block !important; width:100% !important; height:100% !important; padding:8px 12px !important; background:rgba(255,255,255,0.05) !important; border-radius:8px !important; text-decoration:none !important; box-sizing:border-box !important; }
    /* Inner content layout for anchor elements */
    body.itw-tabs-active .itw-panel-likes .likesList__item > * { display:flex !important; }
    /* Pagination row - centered below users */
    body.itw-tabs-active .itw-panel-likes .pagination { display:flex !important; justify-content:center !important; margin-top:8px !important; }
    /* Ensure user avatars are properly sized */
    body.itw-tabs-active .itw-panel-likes .likesList img { width:60px !important; height:60px !important; border-radius:50% !important; object-fit:cover !important; }
  `);

  // ---- Modal creation ----
  let modalBackdrop, modalEl;

  const createSwitch = (id, checked, label) => dom.el(`
    <div class="itw-row">
      <div>${label}</div>
      <label class="itw-switch" for="${id}">
        <input id="${id}" type="checkbox" ${checked ? 'checked' : ''} />
        <span class="itw-knob"></span>
      </label>
    </div>
  `);

  const ensureModal = () => {
    try {
      if (modalEl) return modalEl;
      modalBackdrop = dom.el('<div class="itw-modal-backdrop"></div>');
      modalEl = dom.el('<div class="itw-modal" role="dialog" aria-modal="true"></div>');

      const content = dom.el('<div></div>');
      content.appendChild(dom.el('<h3>Iwara NeoUI — Settings</h3>'));

      // Tabs visibility
      const likesTabRow = createSwitch('itw-show-likes', settings.showLikesTab, 'Show "Likes" tab');
      const recsTabRow = createSwitch('itw-show-recs', settings.showRecsTab, 'Show "Recommended" tab');

      const cinemaNavRow = createSwitch('itw-cinema-hide-nav', settings.cinemaHideNav, 'Hide navigation in cinema mode');
      const newUiRow = createSwitch('itw-new-ui', settings.newUI, 'Enable new UI (tabs + full-width video)');

      const actions = dom.el('<div class="itw-actions"></div>');
      const closeBtn = dom.el('<button class="itw-btn" type="button">Close</button>');
      const saveBtn = dom.el('<button class="itw-btn" type="button">Save</button>');
      actions.append(closeBtn, saveBtn);

      content.append(likesTabRow, recsTabRow, cinemaNavRow, newUiRow, actions);
      modalEl.append(content);

      document.body.append(modalBackdrop, modalEl);

      dom.on(closeBtn, 'click', () => toggleModal(false));
      dom.on(modalBackdrop, 'click', () => toggleModal(false));
      dom.on(saveBtn, 'click', async () => {
        const prev = { newUI: settings.newUI, showLikesTab: settings.showLikesTab, showRecsTab: settings.showRecsTab };
        settings.showLikesTab = modalEl.querySelector('#itw-show-likes')?.checked ?? settings.showLikesTab;
        settings.showRecsTab = modalEl.querySelector('#itw-show-recs')?.checked ?? settings.showRecsTab;
        settings.cinemaHideNav = modalEl.querySelector('#itw-cinema-hide-nav')?.checked ?? settings.cinemaHideNav;
        settings.newUI = modalEl.querySelector('#itw-new-ui')?.checked ?? settings.newUI;
        const hadTabs = !!document.querySelector('.itw-tabbar');
        const newUiChanged = prev.newUI !== settings.newUI;
        await saveSettings();
        applySettings();
        // If staying in New UI and tab composition changed, rebuild tabs
        const tabsChanged = prev.showLikesTab !== settings.showLikesTab || prev.showRecsTab !== settings.showRecsTab;
        if (!newUiChanged && hadTabs && settings.newUI && tabsChanged) {
          teardownVideoTabs();
        }
        if (newUiChanged) {
          // Force a single reload to guarantee full revert/apply of layout across SPA hydration
          location.reload();
          return;
        }
        applyUiMode();
        toggleModal(false);
      });

      return modalEl;
    } catch (e) {
      console.warn('[Iwara NeoUI] ensureModal failed:', e);
      return null;
    }
  };

  const toggleModal = (show) => {
    ensureModal();
    modalBackdrop.style.display = show ? 'block' : 'none';
    modalEl.style.display = show ? 'block' : 'none';
  };

  // ---- Insert header button (before coin indicator when possible) ----
  const GEAR_SVG = '<svg class="itw-gear" viewBox="0 0 24 24" aria-hidden="true"><path d="M19.14,12.94a7.43,7.43,0,0,0,.05-.94,7.43,7.43,0,0,0-.05-.94l2-1.56a.5.5,0,0,0,.12-.64l-1.9-3.29a.5.5,0,0,0-.6-.22l-2.36,1a7.39,7.39,0,0,0-1.63-.94l-.36-2.5A.5.5,0,0,0,12.47,2H9.53a.5.5,0,0,0-.5.42l-.36,2.5a7.39,7.39,0,0,0-1.63.94l-2.36-1a.5.5,0,0,0-.6.22L2.22,8.88a.5.5,0,0,0,.12.64l2,1.56a7.43,7.43,0,0,0-.05.94,7.43,7.43,0,0,0,.05.94l-2,1.56a.5.5,0,0,0-.12.64l1.9,3.29a.5.5,0,0,0,.6.22l2.36-1a7.39,7.39,0,0,0,1.63.94l.36,2.5a.5.5,0,0,0,.5.42h2.94a.5.5,0,0,0,.5-.42l.36-2.5a7.39,7.39,0,0,0,1.63-.94l2.36,1a.5.5,0,0,0,.6-.22l1.9-3.29a.5.5,0,0,0-.12-.64ZM11,15.5A3.5,3.5,0,1,1,14.5,12,3.5,3.5,0,0,1,11,15.5Z"/></svg>';

  const makeHeaderBtn = () => dom.el(`<button id="itw-settings-btn" class="itw-btn" type="button" title="Iwara NeoUI">${GEAR_SVG}<span>Options</span></button>`);

  const insertNextToSearch = (btn) => {
    const header = document.querySelector('header, nav[role="navigation"], .header, [class*="header"]');
    if (!header) return { placed: false, anchorEl: null };

    // Prefer the container that wraps the search UI
    const searchContainer = header.querySelector('.header__content__items__search, [class*="items__search" i], .header__search, [role="search"]');

    if (searchContainer?.parentElement) {
      // Already placed correctly?
      if (btn.previousElementSibling === searchContainer) {
        return { placed: true, anchorEl: searchContainer };
      }
      searchContainer.insertAdjacentElement('afterend', btn);
      return { placed: true, anchorEl: searchContainer };
    }

    // Fallback: try to locate a search form and place after it
    const searchForm = header.querySelector('form.header__search, form[action*="search" i]') || header.querySelector('input[type="search"]')?.closest('form');
    if (searchForm?.parentElement) {
      searchForm.insertAdjacentElement('afterend', btn);
      return { placed: true, anchorEl: searchForm };
    }

    // Final fallback: append into a central items container or header
    const items = header.querySelector('.header__content__items, [class*="content__items" i]') || header;
    if (btn.parentElement !== items) items.appendChild(btn);
    return { placed: false, anchorEl: items };
  };

  const ensureHeaderButton = async () => {
    try {
      let btn = document.getElementById('itw-settings-btn');
      if (!btn) {
        btn = makeHeaderBtn();
        dom.on(btn, 'click', () => { ensureModal(); toggleModal(true); });
        btn.classList.add('itw-float');
        document.body.appendChild(btn);
      }

      let debounce = null;

      const reanchor = () => {
        // Ensure the button exists in the DOM
        if (!btn.isConnected) document.body.appendChild(btn);

        const { placed } = insertNextToSearch(btn);
        if (placed) {
          btn.classList.remove('itw-float');
          btn.classList.add('itw-in-header');
        } else {
          btn.classList.add('itw-float');
          btn.classList.remove('itw-in-header');
        }
      };

      // Initial attempt
      reanchor();

      // Observe DOM continuously (SPA hydration or header rerenders)
      const mo = new MutationObserver(() => {
        if (debounce) return;
        debounce = setTimeout(() => { debounce = null; reanchor(); }, 300);
      });
      mo.observe(document.documentElement, { childList: true, subtree: true });

      // Handle SPA route changes via pushState/popstate interception
      const onRouteChange = () => {
        domCache.clear();
        observerManager.disconnect('likes');
        if (cinemaActive && settings.cinemaHideNav) {
          cinemaActive = false;
          applyCinema();
        }
        reanchor();
        applyUiMode();
      };
      const origPushState = history.pushState;
      history.pushState = function(...args) {
        origPushState.apply(this, args);
        onRouteChange();
      };
      const origReplaceState = history.replaceState;
      history.replaceState = function(...args) {
        origReplaceState.apply(this, args);
        onRouteChange();
      };
      window.addEventListener('popstate', onRouteChange);

      // Additional safety hooks
      window.addEventListener('load', reanchor, { once: true });
      document.addEventListener('visibilitychange', () => { if (!document.hidden) reanchor(); });
    } catch (e) {
      console.warn('[Iwara NeoUI] ensureHeaderButton failed:', e);
    }
  };

  // ---- Liked by detection and toggle ----
  const LIKED_BY_TEXTS = [
    'liked by','likes by','liked-by','liked',
    '喜欢', '赞过',
    'いいね',
    '좋아요',
  ];

  const tagAndMoveLiked = (el) => {
    if (!el) return null;
    el.classList.add('itw-liked-by');
    const likesPanel = settings.newUI && document.querySelector('.itw-panel-likes');
    if (likesPanel && settings.showLikesTab && !likesPanel.contains(el)) {
      likesPanel.append(el);
    }
    return el;
  };

  const findLikedBySection = () => {
    try {
      const likesList = dom.qs('.likesList');
      if (likesList) {
        const container = likesList.closest('.block, .contentBlock, .block--padding, .card, .panel') || likesList.parentElement;
        if (container) return tagAndMoveLiked(container);
      }
      // Targeted fallback: scan only structural containers (not all divs)
      for (const el of dom.qsa('.block, .contentBlock, .card, .panel, section')) {
        const title = el.querySelector('h2, h3, .text--h3');
        const text = (title?.textContent || '').trim().toLowerCase();
        if (text && LIKED_BY_TEXTS.some(t => text.includes(t))) {
          return tagAndMoveLiked(el);
        }
      }
      return null;
    } catch (e) {
      console.warn('[Iwara NeoUI] findLikedBySection failed:', e);
      return null;
    }
  };

  const applyHideLikedBy = () => {
    const root = document.documentElement;
    // Vanilla mode should be vanilla: never hide on newUI=false
    const hide = settings.newUI ? !settings.showLikesTab : false;
    root.classList.toggle('itw-hide-liked-by', hide);
  };

  // Tag and hide Recommended (More like this)
  const findRecsSection = () => {
    try {
      const el = dom.qs('.moreLikeThis') || [...document.querySelectorAll('.text--h3, h2, h3')].find(h => /more like this|recommended|related/i.test(h.textContent || ''))?.closest('.block, .contentBlock, .panel, .card, .section, .moreLikeThis');
      if (el) { el.classList.add('itw-recs'); return el; }
      return null;
    } catch (e) {
      console.warn('[Iwara NeoUI] findRecsSection failed:', e);
      return null;
    }
  };

  const applyHideRecs = () => {
    const root = document.documentElement;
    const hide = settings.newUI ? !settings.showRecsTab : false;
    root.classList.toggle('itw-hide-recs', hide);
  };

  // ---- Cinema Mode ----
  const applyCinema = () => {
    try {
      const root = document.body || document.documentElement;
      root.classList.toggle('itw-theater', cinemaActive);
      root.classList.toggle('itw-theater-hide-nav', cinemaActive && settings.cinemaHideNav);

      // Move settings button to body when cinema is active so it's always reachable
      const settingsBtn = document.getElementById('itw-settings-btn');
      if (settingsBtn) {
        if (cinemaActive) {
          settingsBtn.classList.remove('itw-in-header');
          settingsBtn.classList.add('itw-float');
          document.body.appendChild(settingsBtn);
        }
        // Button will be re-anchored by reanchor() when cinema turns off
      }

      // Scroll to top of player when entering cinema mode
      if (cinemaActive) {
        const player = dom.qs('.page-video__player, .video-js');
        if (player) player.scrollIntoView({ behavior: 'smooth', block: 'start' });
      }
    } catch (e) {
      console.warn('[Iwara NeoUI] applyCinema failed:', e);
    }
  };

  const toggleCinema = () => {
    cinemaActive = !cinemaActive;
    applyCinema();
    // Persist only when nav is NOT hidden (light cinema mode)
    if (!settings.cinemaHideNav) {
      settings.theaterMode = cinemaActive;
      saveSettings();
    }
  };

  // ---- Keyboard shortcut (T) for Cinema Mode ----
  dom.on(document, 'keydown', (e) => {
    if (e.key.toLowerCase?.() === 't' && !/input|textarea|select/i.test(e.target.tagName)) {
      toggleCinema();
      // Sync cinema button state if it exists
      const btn = dom.qs('.itw-cinema-btn');
      if (btn) btn.classList.toggle('active', cinemaActive);
    }
  });

  // ---- Apply settings helpers ----
  const applySettings = () => {
    applyHideLikedBy();
    applyHideRecs();
    applyCinema();
  };

  // ---- Page type detection ----
  const isVideoPage = () => {
    // Quick route-based hint
    const path = location.pathname;
    if (/^\/(video|videos)\//i.test(path)) return true;
    if (/^\/(search|users|image|images|posts|forums|messages|notifications|settings|login|register)\b/i.test(path)) return false;
    // Structural markers: presence of a player and typical video-page sections
    const hasPlayer = !!document.querySelector('.page-video__player, .plyr__video-wrapper, .video-js, .jwplayer, [data-plyr], video');
    const hasMarkers = !!document.querySelector('.page-video__details, .moreFromUser, .moreLikeThis, .page-video__bottom, .page-video__tags, #comments, .comments');
    return hasPlayer && hasMarkers;
  };

  // ---- Likes relocation helpers (module-level to avoid recreation) ----
  const nearestCommonAncestor = (a, b) => {
    if (!a || !b) return null;
    const aChain = new Set();
    for (let n = a; n; n = n.parentElement) aChain.add(n);
    for (let n = b; n; n = n.parentElement) if (aChain.has(n)) return n;
    return null;
  };

  const unhideDeep = (rootEl) => {
    const stack = [rootEl];
    while (stack.length) {
      const el = stack.pop();
      if (!el || el.nodeType !== 1) continue;
      if (el.hasAttribute('hidden')) el.removeAttribute('hidden');
      if (el.style) {
        if (el.style.display === 'none') el.style.display = '';
        if (el.style.visibility === 'hidden') el.style.visibility = '';
        if (el.style.opacity === '0') el.style.opacity = '';
        if (el.style.height === '0px') el.style.height = '';
        if (el.style.maxHeight === '0px') el.style.maxHeight = '';
      }
      stack.push(...el.children);
    }
  };

  const moveLikesIntoPanel = () => {
    if (!settings.showLikesTab) return;
    const likesPanel = document.querySelector('.itw-panel-likes');
    if (!likesPanel) return;
    const list = document.querySelector('.likesList');
    const title = [...document.querySelectorAll('.text--h3, h2, h3')]
      .find(el => /liked by/i.test(el.textContent || ''));
    let block = null;
    // 1) If title and list share a .block__content ancestor, move that
    const listBC = list?.closest('.block__content') || null;
    const titleBC = title?.closest('.block__content') || null;
    if (listBC && titleBC && listBC === titleBC) block = listBC;
    // 2) Nearest common ancestor if reasonable
    if (!block && list && title) {
      const nca = nearestCommonAncestor(list, title);
      if (nca && !nca.classList.contains('itw-panels') && nca !== document.body) block = nca;
    }
    // 3) .block__content of either
    if (!block) block = listBC || titleBC;
    // 4) Fall back to typical blocks
    if (!block && list) {
      block = list.closest('.block, .contentBlock, .block--padding, .card, .panel')
           || (title ? title.parentElement : null)
           || list.parentElement;
    }
    if (!block && title) {
      block = title.closest('.block, .contentBlock, .block--padding, .card, .panel') || title.parentElement;
    }
    // Prefer outer block/card over inner .block__content
    if (block?.matches('.block__content') && block.parentElement?.matches('.block, .contentBlock, .block--padding, .card, .panel, .section')) {
      block = block.parentElement;
    }
    // Defer if list exists but is empty (site may lazy-render)
    if (!likesPanel.contains(block || document.body) && list && list.childElementCount === 0) return;
    if (block) {
      block.classList.add('itw-liked-by');
      unhideDeep(block);
      if (!likesPanel.contains(block)) likesPanel.append(block);
    }
  };

  // ---- Tabs: About / Uploads / Recommended / Likes / Comments ----
  const setupVideoTabs = () => {
    try {
      // Do not build tabs in vanilla mode
      if (!settings.newUI) return;
      // Safety: if previous class lingered but no UI is present, clear it
      if (!document.querySelector('.itw-tabbar') && !document.querySelector('.itw-panels')) {
        document.body.classList.remove(CSS_CLASSES.TABS_ACTIVE);
      }
      if (document.querySelector('.itw-tabbar')) return; // already set up
      if (!isVideoPage()) return; // only on actual video pages

      const mainCol = domCache.get(SELECTORS.VIDEO_COL) || domCache.get(SELECTORS.PAGE_VIDEO_CONTENT) || domCache.get(SELECTORS.COL_MD_9);
      if (!mainCol) return;
      // Require a real player host; do not fall back to arbitrary first child
      const playerHost = mainCol.querySelector('.page-video__player') ||
                         mainCol.querySelector('.video-js, .plyr__video-wrapper, .jwplayer, [data-plyr]')?.closest('.page-video__player') ||
                         mainCol.querySelector('.video-js, .plyr__video-wrapper, .jwplayer, [data-plyr]');
      if (!playerHost) return;

      // Panels
      const panels = dom.el('<div class="itw-panels"></div>');
      const aboutPanel = dom.el('<section class="itw-panel itw-panel-about" role="tabpanel" aria-labelledby="itw-tab-about"></section>');
      const aboutBody = dom.el('<div class="itw-panel-body itw-about-body"></div>');
      aboutPanel.append(aboutBody);
      const uploadsPanel = dom.el('<section class="itw-panel itw-panel-uploads" role="tabpanel" aria-labelledby="itw-tab-uploads"></section>');
      const recsPanel = dom.el('<section class="itw-panel itw-panel-recs" role="tabpanel" aria-labelledby="itw-tab-recs"></section>');
      const likesPanel = dom.el('<section class="itw-panel itw-panel-likes" role="tabpanel" aria-labelledby="itw-tab-likes"></section>');
      const commentsPanel = dom.el('<section class="itw-panel itw-panel-comments" role="tabpanel" aria-labelledby="itw-tab-comments"></section>');
      panels.append(aboutPanel, uploadsPanel, recsPanel, likesPanel, commentsPanel);

      // Tab bar
      const tabbar = dom.el('<div class="itw-tabbar" role="tablist" aria-label="Iwara NeoUI Tabs"></div>');
      const makeTab = (id, text, selected=false) => dom.el(`<button class="itw-tab" role="tab" id="itw-tab-${id}" aria-selected="${selected}" aria-controls="itw-panel-${id}">${text}</button>`);
      const aboutTab = makeTab('about', 'About', true);
      const uploadsTab = makeTab('uploads', 'Uploads');
      const recsTab = makeTab('recs', 'Recommended');
      const likesTab = makeTab('likes', 'Likes');
      const commentsTab = makeTab('comments', 'Comments');

      tabbar.append(aboutTab);
      tabbar.append(uploadsTab);
      if (settings.showRecsTab) tabbar.append(recsTab);
      if (settings.showLikesTab) tabbar.append(likesTab);
      tabbar.append(commentsTab);

      // Cinema mode toggle (pushed to the right via margin-left:auto)
      const cinemaBtn = dom.el(`<button class="itw-cinema-btn${cinemaActive ? ' active' : ''}" type="button" title="Toggle cinema mode (T)">Cinema</button>`);
      dom.on(cinemaBtn, 'click', () => {
        toggleCinema();
        cinemaBtn.classList.toggle('active', cinemaActive);
      });
      tabbar.append(cinemaBtn);

      // Mount tab UI before moving content to avoid ancestor insertion errors
      if (playerHost && playerHost.parentElement === mainCol) {
        mainCol.insertBefore(tabbar, playerHost.nextSibling);
      } else {
        mainCol.appendChild(tabbar);
      }
      mainCol.insertBefore(panels, tabbar.nextSibling);

      // Move content into About panel in the desired order:
      // 1) the main details div, 2) description, 3) tags, 4) bottom
      const details = mainCol.querySelector('.page-video__details');
      if (details) aboutBody.append(details);

      // Description (prefer the full wrapper within details)
      const descEl = (details?.querySelector('.showMore, .page-video__description, .description, .markdown')) ||
                     ([...mainCol.querySelectorAll('.showMore, .page-video__description, .description, .markdown')].find(el => !el.closest('.comments')));
      if (descEl) aboutBody.append(descEl.closest('.contentBlock') || descEl);

      // Tags (prefer the wrapper within details)
      const tagsWrap = (details?.querySelector('.page-video__tags')?.closest('.mt-4')) ||
                       mainCol.querySelector('.page-video__tags')?.closest('.mt-4') ||
                       mainCol.querySelector('.page-video__tags');
      if (tagsWrap) aboutBody.append(tagsWrap);

      // Bottom actions/stats row
      const bottom = mainCol.querySelector('.page-video__bottom');
      if (bottom) aboutBody.append(bottom);

      const comments = mainCol.querySelector('.comments');
      if (comments) commentsPanel.append(comments);

      // Uploads from sidebar
      const moreFrom = document.querySelector('.page-video__sidebar .moreFromUser');
      if (moreFrom) {
        const block = moreFrom.closest('.block, .contentBlock, .panel, .card') || moreFrom;
        uploadsPanel.append(block);
      }

      // Recommended
      const moreLike = dom.qs('.itw-recs') || dom.qs('.moreLikeThis') || [...document.querySelectorAll('.text--h3, h2, h3')].find(h => /more like this|recommended|related/i.test(h.textContent || ''))?.closest('.block, .contentBlock, .panel, .card, .section, .moreLikeThis');
      if (moreLike && settings.showRecsTab) {
        const block = moreLike.closest('.block, .contentBlock, .panel, .card, .section') || moreLike;
        recsPanel.append(block);
      }

      // Initial Likes relocation
      moveLikesIntoPanel();
      // Keep trying as site re-renders/paginates the Likes block (debounced)
      if (__itwLikesObserver) { try { __itwLikesObserver.disconnect(); } catch {} }
      let likesDebounce = null;
      __itwLikesObserver = new MutationObserver(() => {
        if (likesDebounce) return;
        likesDebounce = setTimeout(() => { likesDebounce = null; moveLikesIntoPanel(); }, TIMEOUTS.OBSERVER_DEBOUNCE);
      });
      __itwLikesObserver.observe(document.body, { childList: true, subtree: true });

      // Ensure late-loaded Likes populate into the panel as soon as they appear
      if (settings.showLikesTab) {
        (async () => {
          const node = await waitFor(`${SELECTORS.LIKES_LIST}, ${SELECTORS.LIKED_BY}`, { timeout: TIMEOUTS.WAIT_FOR, interval: TIMEOUTS.POLL_INTERVAL });
          if (!node) return;
          moveLikesIntoPanel();
        })();
      }

      // Activate About by default
      const activate = (which) => {
        const btns = [aboutTab, uploadsTab];
        if (settings.showRecsTab) btns.push(recsTab);
        if (settings.showLikesTab) btns.push(likesTab);
        btns.push(commentsTab);
        for (const btn of btns) btn.setAttribute('aria-selected', String(btn === which));

        const allPanels = [aboutPanel, uploadsPanel, recsPanel, likesPanel, commentsPanel];
        for (const p of allPanels) p.classList.remove('active');
        if (which === aboutTab) aboutPanel.classList.add('active');
        else if (which === uploadsTab) uploadsPanel.classList.add('active');
        else if (which === recsTab) recsPanel.classList.add('active');
        else if (which === likesTab) likesPanel.classList.add('active');
        else if (which === commentsTab) commentsPanel.classList.add('active');
      };
      activate(aboutTab);

      dom.on(aboutTab, 'click', () => activate(aboutTab));
      dom.on(uploadsTab, 'click', () => activate(uploadsTab));
      if (settings.showRecsTab) dom.on(recsTab, 'click', () => activate(recsTab));
      if (settings.showLikesTab) dom.on(likesTab, 'click', () => { moveLikesIntoPanel(); activate(likesTab); });
      dom.on(commentsTab, 'click', () => activate(commentsTab));

      document.body.classList.add(CSS_CLASSES.TABS_ACTIVE);
    } catch (e) {
      console.warn('[Iwara NeoUI] Tabs setup skipped:', e);
    }
  };

  const teardownVideoTabs = () => {
    try {
      // Clean up observers
      observerManager.disconnect('likes');
      
      const tabbar = document.querySelector(SELECTORS.TABBAR);
      const panels = document.querySelector(SELECTORS.PANELS);
      // Do NOT return early — always ensure we clear layout class below

      const mainCol = domCache.get(SELECTORS.VIDEO_COL) || domCache.get(SELECTORS.PAGE_VIDEO_CONTENT) || domCache.get(SELECTORS.COL_MD_9) || document.body;
      const sidebar = domCache.get(SELECTORS.SIDEBAR);

      if (panels) {
        const aboutBody = panels.querySelector('.itw-about-body');
        if (aboutBody) {
          [...aboutBody.children].forEach(node => mainCol.appendChild(node));
        }
        const uploadsPanel = panels.querySelector('.itw-panel-uploads');
        if (uploadsPanel && uploadsPanel.children.length) {
          [...uploadsPanel.children].forEach(node => mainCol.appendChild(node));
        }
        const recsPanel = panels.querySelector('.itw-panel-recs');
        if (recsPanel && recsPanel.children.length) {
          const block = recsPanel.querySelector('.itw-recs');
          if (block && sidebar) sidebar.appendChild(block);
          else if (recsPanel.children.length) {
            [...recsPanel.children].forEach(node => (sidebar || mainCol).appendChild(node));
          }
        }
        const likesPanel = panels.querySelector('.itw-panel-likes');
        if (likesPanel && likesPanel.children.length) {
          const block = likesPanel.querySelector('.itw-liked-by');
          if (block && sidebar) sidebar.appendChild(block);
          else if (likesPanel.children.length) {
            [...likesPanel.children].forEach(node => (sidebar || mainCol).appendChild(node));
          }
        }
        const commentsPanel = panels.querySelector('.itw-panel-comments');
        if (commentsPanel && commentsPanel.children.length) {
          [...commentsPanel.children].forEach(node => mainCol.appendChild(node));
        }
        panels.remove();
      }
      if (tabbar) tabbar.remove();
      // Always clear the layout class to avoid full-width/hidden-sidebar in vanilla mode
      document.body.classList.remove(CSS_CLASSES.TABS_ACTIVE);
    } catch (e) {
      console.warn('[Iwara NeoUI] Tabs teardown skipped:', e);
      // Still ensure layout class is not left behind on error paths
      document.body.classList.remove('itw-tabs-active');
    }
  };

  const applyUiMode = () => {
    if (settings.newUI && isVideoPage()) {
      setupVideoTabs();
    } else {
      teardownVideoTabs();
      // Safety: explicitly ensure classes are removed when new UI is disabled
      document.body.classList.remove('itw-tabs-active');
      document.body.classList.remove('itw-theater');
      // Ensure Likes observer is stopped in vanilla mode
      if (__itwLikesObserver) { try { __itwLikesObserver.disconnect(); } catch {} __itwLikesObserver = null; }
    }
  };

  // ---- Initialize ----
  (async () => {
    await loadSettings();
    // Restore cinema state from persisted theaterMode (only for light cinema)
    if (settings.theaterMode && !settings.cinemaHideNav) {
      cinemaActive = true;
    }
    ensureModal();
    await ensureHeaderButton();
    findLikedBySection();
    findRecsSection();
    applySettings();
    applyUiMode();

    let mainDebounce = null;
    const onDomChange = () => {
      domCache.clear();

      // Always correct stray class if UI not present
      if (!document.querySelector(SELECTORS.TABBAR) && !document.querySelector(SELECTORS.PANELS)) {
        document.body.classList.remove(CSS_CLASSES.TABS_ACTIVE);
      }
      if (!isVideoPage() && document.querySelector('.itw-tabbar')) teardownVideoTabs();

      if (isVideoPage()) {
        if (!document.querySelector(SELECTORS.LIKED_BY)) findLikedBySection();
        if (!document.querySelector(SELECTORS.RECS)) findRecsSection();
        if (cinemaActive && !document.body.classList.contains(CSS_CLASSES.THEATER)) applyCinema();
        if (settings.newUI && !document.querySelector(SELECTORS.TABBAR)) setupVideoTabs();
        if (settings.newUI && settings.showLikesTab) {
          const likesPanel = domCache.get('.itw-panel-likes');
          const likedNode = domCache.get(SELECTORS.LIKED_BY) || domCache.get(SELECTORS.LIKES_LIST);
          if (likesPanel && likedNode) {
            const block = likedNode.classList?.contains('itw-liked-by') ? likedNode : (likedNode.closest('.block, .contentBlock, .block--padding, .card, .panel') || likedNode);
            if (!likesPanel.contains(block)) likesPanel.append(block);
          }
        }
      }
      if (!settings.newUI) {
        if (document.querySelector('.itw-tabbar')) teardownVideoTabs();
        document.body.classList.remove(CSS_CLASSES.THEATER);
      }
    };
    observerManager.create('main', () => {
      if (mainDebounce) return;
      mainDebounce = setTimeout(() => { mainDebounce = null; onDomChange(); }, TIMEOUTS.OBSERVER_DEBOUNCE);
    });
    observerManager.observe('main');
  })();
})();