Iwara에 탭 레이아웃, 시네마 모드, 사용자 지정 섹션, 개선된 동영상 페이지 경험을 더하는 향상된 UI입니다.
// ==UserScript==
// @name Iwara NeoUI
// @name:zh-CN Iwara NeoUI
// @name:zh-TW Iwara NeoUI
// @name:ja Iwara NeoUI
// @name:ko Iwara NeoUI
// @namespace codeberg.org/0xdev/neoUI-iwara
// @version 0.4.0
// @description Enhanced UI for Iwara with tabbed layout, theater mode, customizable sections, and improved video page experience.
// @description:zh-CN 为 Iwara 提供增强 UI:标签布局、影院模式、可自定义区块和改进的视频页面体验。
// @description:zh-TW 為 Iwara 提供增強 UI:分頁版面、劇院模式、可自訂區塊與改良的影片頁體驗。
// @description:ja Iwara にタブレイアウト、シネマモード、カスタマイズ可能なセクション、改善された動画ページ体験を追加します。
// @description:ko Iwara에 탭 레이아웃, 시네마 모드, 사용자 지정 섹션, 개선된 동영상 페이지 경험을 더하는 향상된 UI입니다.
// @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==
// THIS FILE IS AUTO-GENERATED — DO NOT MANUALLY EDIT.
// Source: neoui/iwara-neoui.user.js
'use strict';
const SUPPORTED_LOCALES = ['en', 'zh-CN', 'zh-TW', 'ja', 'ko'];
const DEFAULTS = Object.freeze({
hideLikedBy: true,
theaterMode: false,
cinemaHideNav: false,
newUI: false,
showLikesTab: true,
showRecsTab: true,
language: 'auto',
});
const MESSAGES = {
en: {
'settings.button': 'Options',
'settings.title': 'Iwara NeoUI - Settings',
'settings.language': 'Language',
'settings.language.auto': 'Auto',
'settings.showLikes': 'Show "Likes" tab',
'settings.showRecs': 'Show "Recommended" tab',
'settings.hideNav': 'Hide navigation in cinema mode',
'settings.newUi': 'Enable new UI (tabs + full-width video)',
'actions.close': 'Close',
'actions.save': 'Save',
'tabs.about': 'About',
'tabs.uploads': 'Uploads',
'tabs.recs': 'Recommended',
'tabs.likes': 'Likes',
'tabs.comments': 'Comments',
'tabs.aria': 'Iwara NeoUI Tabs',
'cinema.button': 'Cinema',
'cinema.title': 'Toggle cinema mode (T)',
},
'zh-CN': {
'settings.button': '选项',
'settings.title': 'Iwara NeoUI - 设置',
'settings.language': '语言',
'settings.language.auto': '自动',
'settings.showLikes': '显示“喜欢”标签',
'settings.showRecs': '显示“推荐”标签',
'settings.hideNav': '影院模式下隐藏导航',
'settings.newUi': '启用新界面(标签页 + 全宽视频)',
'actions.close': '关闭',
'actions.save': '保存',
'tabs.about': '简介',
'tabs.uploads': '上传',
'tabs.recs': '推荐',
'tabs.likes': '喜欢',
'tabs.comments': '评论',
'tabs.aria': 'Iwara NeoUI 标签页',
'cinema.button': '影院',
'cinema.title': '切换影院模式 (T)',
},
'zh-TW': {
'settings.button': '選項',
'settings.title': 'Iwara NeoUI - 設定',
'settings.language': '語言',
'settings.language.auto': '自動',
'settings.showLikes': '顯示「喜歡」分頁',
'settings.showRecs': '顯示「推薦」分頁',
'settings.hideNav': '劇院模式下隱藏導覽列',
'settings.newUi': '啟用新介面(分頁 + 全寬影片)',
'actions.close': '關閉',
'actions.save': '儲存',
'tabs.about': '簡介',
'tabs.uploads': '上傳',
'tabs.recs': '推薦',
'tabs.likes': '喜歡',
'tabs.comments': '留言',
'tabs.aria': 'Iwara NeoUI 分頁',
'cinema.button': '劇院',
'cinema.title': '切換劇院模式 (T)',
},
ja: {
'settings.button': 'オプション',
'settings.title': 'Iwara NeoUI - 設定',
'settings.language': '言語',
'settings.language.auto': '自動',
'settings.showLikes': '「いいね」タブを表示',
'settings.showRecs': '「おすすめ」タブを表示',
'settings.hideNav': 'シネマモードでナビゲーションを隠す',
'settings.newUi': '新UIを有効化(タブ + 全幅動画)',
'actions.close': '閉じる',
'actions.save': '保存',
'tabs.about': '概要',
'tabs.uploads': '投稿',
'tabs.recs': 'おすすめ',
'tabs.likes': 'いいね',
'tabs.comments': 'コメント',
'tabs.aria': 'Iwara NeoUI タブ',
'cinema.button': 'シネマ',
'cinema.title': 'シネマモードを切り替え (T)',
},
ko: {
'settings.button': '옵션',
'settings.title': 'Iwara NeoUI - 설정',
'settings.language': '언어',
'settings.language.auto': '자동',
'settings.showLikes': '"좋아요" 탭 표시',
'settings.showRecs': '"추천" 탭 표시',
'settings.hideNav': '시네마 모드에서 내비게이션 숨기기',
'settings.newUi': '새 UI 사용(탭 + 전체 폭 동영상)',
'actions.close': '닫기',
'actions.save': '저장',
'tabs.about': '정보',
'tabs.uploads': '업로드',
'tabs.recs': '추천',
'tabs.likes': '좋아요',
'tabs.comments': '댓글',
'tabs.aria': 'Iwara NeoUI 탭',
'cinema.button': '시네마',
'cinema.title': '시네마 모드 전환 (T)',
},
};
let currentLocale = 'en';
const LANGUAGE_OPTIONS = [
{ value: 'auto', nativeLabel: '' },
{ value: 'en', nativeLabel: 'English' },
{ value: 'zh-CN', nativeLabel: '简体中文' },
{ value: 'zh-TW', nativeLabel: '繁體中文' },
{ value: 'ja', nativeLabel: '日本語' },
{ value: 'ko', nativeLabel: '한국어' },
];
function isLanguageSetting(value) {
return value === 'auto' || SUPPORTED_LOCALES.includes(value);
}
function normalizeLanguageTag(tag) {
return tag.trim().replace('_', '-').toLowerCase();
}
function localeFromLanguageTag(tag) {
const n = normalizeLanguageTag(tag);
if (n === 'zh-cn' || n === 'zh-sg' || n === 'zh-hans' || n.startsWith('zh-hans-'))
return 'zh-CN';
if (n === 'zh-tw' || n === 'zh-hk' || n === 'zh-mo' || n === 'zh-hant' || n.startsWith('zh-hant-'))
return 'zh-TW';
if (n === 'zh')
return 'zh-CN';
if (n.startsWith('ja'))
return 'ja';
if (n.startsWith('ko'))
return 'ko';
if (n.startsWith('en'))
return 'en';
return null;
}
function resolveLocale(setting, browserLanguages = []) {
if (setting !== 'auto')
return setting;
for (const tag of browserLanguages) {
const locale = localeFromLanguageTag(tag);
if (locale)
return locale;
}
return 'en';
}
function t(key, locale = currentLocale) {
return MESSAGES[locale][key] || MESSAGES.en[key] || key;
}
function getBrowserLanguages(nav = typeof navigator !== 'undefined' ? navigator : undefined) {
if (!nav)
return [];
const languages = Array.isArray(nav.languages) ? nav.languages : [];
if (languages.length)
return [...languages];
return nav.language ? [nav.language] : [];
}
function createLanguageSelect(doc, value, locale) {
const select = doc.createElement('select');
select.id = 'itw-language';
select.className = 'itw-select';
for (const opt of LANGUAGE_OPTIONS) {
const option = doc.createElement('option');
option.value = opt.value;
option.textContent = opt.value === 'auto' ? t('settings.language.auto', locale) : opt.nativeLabel;
option.selected = opt.value === value;
select.appendChild(option);
}
return select;
}
function normalizeSettings(saved) {
if (!saved || typeof saved !== 'object')
return { ...DEFAULTS };
const raw = saved;
const next = { ...DEFAULTS };
for (const key of Object.keys(DEFAULTS)) {
const value = raw[key];
if (key === 'language') {
if (isLanguageSetting(value))
next.language = value;
continue;
}
if (typeof value === 'boolean')
next[key] = value;
}
if (typeof raw.hideLikedBy === 'boolean' && !('showLikesTab' in raw)) {
next.showLikesTab = !raw.hideLikedBy;
}
return next;
}
// Move `block` into `panel` unless it is already inside, or is the panel
// itself / one of its ancestors. Blocks come from closest()/common-ancestor
// walks that can overshoot the panel after a site re-render, and appending
// an ancestor into its own descendant throws HierarchyRequestError.
// Module scope so Node can require() it for unit tests (see test/); the
// browser code below is gated on `typeof window`.
function adoptIntoPanel(panel, block) {
if (!panel || !block)
return false;
if (panel.contains(block))
return false;
if (block.contains(panel))
return false;
panel.append(block);
return true;
}
if (typeof window !== 'undefined')
(() => {
'use strict';
const neoWindow = window;
if (neoWindow.__IWARA_NEOUI_ACTIVE)
return;
neoWindow.__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();
const element = t.content.firstElementChild;
if (!element)
throw new Error('empty HTML template');
return element;
},
on(el, evt, cb) {
if (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 { }
}
};
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', {});
settings = normalizeSettings(saved);
currentLocale = resolveLocale(settings.language, getBrowserLanguages());
}
catch (e) {
console.warn('[Iwara NeoUI] loadSettings failed:', e);
settings = { ...DEFAULTS };
currentLocale = resolveLocale(settings.language, getBrowserLanguages());
}
};
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-select { min-width:160px; max-width:220px; color:#e6e6e6; background:#20242b; border:1px solid #3a404b; border-radius:6px; padding:6px 8px; font:600 12px/1.2 system-ui, -apple-system, Segoe UI, Roboto, sans-serif; }
.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 = null;
let modalEl = null;
const createSwitch = (id, checked, label) => {
const row = dom.el('<div class="itw-row"></div>');
const labelText = dom.el('<div></div>');
labelText.textContent = label;
const switchLabel = dom.el(`
<label class="itw-switch" for="${id}">
<input id="${id}" type="checkbox" ${checked ? 'checked' : ''} />
<span class="itw-knob"></span>
</label>
`);
row.append(labelText, switchLabel);
return row;
};
const createLanguageRow = () => {
const row = dom.el('<div class="itw-row"></div>');
const labelText = dom.el('<div></div>');
labelText.textContent = t('settings.language');
row.append(labelText, createLanguageSelect(document, settings.language, currentLocale));
return row;
};
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>');
const title = dom.el('<h3></h3>');
title.textContent = t('settings.title');
content.appendChild(title);
// Tabs visibility
const languageRow = createLanguageRow();
const likesTabRow = createSwitch('itw-show-likes', settings.showLikesTab, t('settings.showLikes'));
const recsTabRow = createSwitch('itw-show-recs', settings.showRecsTab, t('settings.showRecs'));
const cinemaNavRow = createSwitch('itw-cinema-hide-nav', settings.cinemaHideNav, t('settings.hideNav'));
const newUiRow = createSwitch('itw-new-ui', settings.newUI, t('settings.newUi'));
const actions = dom.el('<div class="itw-actions"></div>');
const closeBtn = dom.el('<button class="itw-btn" type="button"></button>');
const saveBtn = dom.el('<button class="itw-btn" type="button"></button>');
closeBtn.textContent = t('actions.close');
saveBtn.textContent = t('actions.save');
actions.append(closeBtn, saveBtn);
content.append(languageRow, 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,
language: settings.language,
};
if (!modalEl)
return;
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 languageValue = modalEl.querySelector('#itw-language')?.value;
if (isLanguageSetting(languageValue))
settings.language = languageValue;
currentLocale = resolveLocale(settings.language, getBrowserLanguages());
const settingsBtn = document.getElementById('itw-settings-btn');
if (settingsBtn)
syncHeaderBtnText(settingsBtn);
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 ||
prev.language !== settings.language;
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);
if (prev.language !== settings.language) {
modalBackdrop?.remove();
modalEl?.remove();
modalBackdrop = null;
modalEl = null;
}
});
return modalEl;
}
catch (e) {
console.warn('[Iwara NeoUI] ensureModal failed:', e);
return null;
}
};
const toggleModal = (show) => {
ensureModal();
if (!modalBackdrop || !modalEl)
return;
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>${t('settings.button')}</span></button>`);
const syncHeaderBtnText = (btn) => {
btn.title = 'Iwara NeoUI';
const label = btn.querySelector('span');
if (label)
label.textContent = t('settings.button');
};
const findHeaderRoot = (btn) => {
const candidates = document.querySelectorAll('header, nav[role="navigation"], .header, [class*="header"]');
return [...candidates].find(candidate => candidate !== btn && !btn.contains(candidate)) ?? null;
};
const insertNextToSearch = (btn) => {
const header = findHeaderRoot(btn);
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;
const likesPanel = settings.newUI && document.querySelector('.itw-panel-likes');
// A scan/closest() hit can be the panel itself or an ancestor of it;
// tagging those mis-marks page-level containers (hiding .itw-liked-by
// would then hide the whole column), so bail out entirely.
if (likesPanel && el.contains(likesPanel))
return null;
el.classList.add('itw-liked-by');
if (likesPanel && settings.showLikesTab)
adoptIntoPanel(likesPanel, 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) => {
const event = e;
const target = event.target instanceof Element ? event.target : null;
if (event.key.toLowerCase?.() === 't' && !/input|textarea|select/i.test(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)
continue;
if (el.hasAttribute('hidden'))
el.removeAttribute('hidden');
if (el instanceof HTMLElement) {
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(...Array.from(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;
// Not redundant with adoptIntoPanel's internal check: this also gates the
// tag + unhide side effects, which must never hit an ancestor of the panel.
if (block && !block.contains(likesPanel)) {
block.classList.add('itw-liked-by');
unhideDeep(block);
adoptIntoPanel(likesPanel, 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="${t('tabs.aria')}"></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', t('tabs.about'), true);
const uploadsTab = makeTab('uploads', t('tabs.uploads'));
const recsTab = makeTab('recs', t('tabs.recs'));
const likesTab = makeTab('likes', t('tabs.likes'));
const commentsTab = makeTab('comments', t('tabs.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="${t('cinema.title')}">${t('cinema.button')}</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). These two are
// ordering hoists: the node usually already sits inside details/aboutBody
// and must be re-appended to the panel tail, so adoptIntoPanel (which
// skips already-inside nodes) doesn't fit — guard only against the
// closest() walk overshooting to a container of the panel.
const descEl = (details?.querySelector('.showMore, .page-video__description, .description, .markdown')) ||
([...mainCol.querySelectorAll('.showMore, .page-video__description, .description, .markdown')].find(el => !el.closest('.comments')));
const descBlock = descEl ? (descEl.closest('.contentBlock') || descEl) : null;
if (descBlock && !descBlock.contains(aboutBody))
aboutBody.append(descBlock);
// 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 && !tagsWrap.contains(aboutBody))
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;
adoptIntoPanel(uploadsPanel, 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;
adoptIntoPanel(recsPanel, 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);
adoptIntoPanel(likesPanel, 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');
})();
})();
if (typeof window === 'undefined' && typeof module !== 'undefined' && module.exports) {
module.exports = {
DEFAULTS,
MESSAGES,
createLanguageSelect,
getBrowserLanguages,
localeFromLanguageTag,
normalizeSettings,
resolveLocale,
t,
adoptIntoPanel,
};
}