ShikiPlayer

Shikimori видеоплееры для просмотра аниме Turbo → Lumex → Alloha → Kodik → Collaps → Veoveo → Vibix

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

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

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

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

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

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.

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

// ==UserScript==
// @name            ShikiPlayer
// @description     Shikimori видеоплееры для просмотра аниме Turbo → Lumex → Alloha → Kodik → Collaps → Veoveo  → Vibix
// @namespace       https://github.com/Onzis/ShikiPlayer
// @author          Onzis
// @license         GPL-3.0 license
// @version         1.67
// @homepageURL     https://github.com/Onzis/ShikiPlayer
// @grant           GM.xmlHttpRequest
// @connect         shikimori.me
// @connect         kodikapi.com
// @connect         apicollaps.cc
// @connect         api.kinobox.tv
// @match           *://shikimori.one/*
// @match           *://beggins-as.pljjalgo.online/*
// @match           *://beggins-as.allarknow.online/*
// @match           *://beggins-as.algonoew.online/*
// @run-at          document-end
// ==/UserScript==
/* jshint -W097 */
"use strict";

// --- INJECTION OF DARK THEME CSS ---
const darkThemeCSS = `
/* ==ShikiPlayer Dark Theme== */
:root {
  /* Основные цвета тёмной темы */
  --sp-bg-primary: #0a0a0a;
  --sp-bg-secondary: #1a1a1a;
  --sp-bg-tertiary: #252525;
  --sp-bg-hover: #2a2a2a;
  --sp-bg-active: #333333;
  
  /* Текстовые цвета */
  --sp-text-primary: #e0e0e0;
  --sp-text-secondary: #b0b0b0;
  --sp-text-muted: #808080;
  --sp-text-inverse: #000000;
  
  /* Акцентные цвета */
  --sp-accent: #6366f1;
  --sp-accent-hover: #818cf8;
  --sp-accent-active: #4f46e5;
  --sp-accent-light: #e0e7ff;
  
  /* Статусные цвета */
  --sp-success: #10b981;
  --sp-warning: #f59e0b;
  --sp-error: #ef4444;
  --sp-online: #22c55e;
  --sp-offline: #64748b;
  --sp-loading: #3b82f6;
  
  /* Границы */
  --sp-border-color: #333333;
  --sp-border-light: #404040;
  
  /* Радиусы и отступы */
  --sp-radius-sm: 4px;
  --sp-radius-md: 8px;
  --sp-radius-lg: 12px;
  --sp-radius-xl: 16px;
  --sp-spacing-xs: 4px;
  --sp-spacing-sm: 8px;
  --sp-spacing-md: 10px;
  --sp-spacing-lg: 24px;
  --sp-spacing-xl: 32px;
  
  /* Анимации */
  --sp-transition-fast: 150ms ease;
  --sp-transition-normal: 250ms ease;
  --sp-transition-slow: 350ms ease;
}

/* Внешний контейнер для центрирования кнопки */
.sp-outer-wrapper {
  margin: var(--sp-spacing-lg) 0 !important;
}

/* Контейнер для кнопки с фоном как у плеера */
.sp-button-container {
  background: linear-gradient(135deg, #2b2a39eb 0%, #2b2a39eb 100%) !important;
  border: 1px solid var(--sp-border-color) !important;
  border-top: none !important;
  border-radius: 0 0 var(--sp-radius-lg) var(--sp-radius-lg) !important;
  padding: var(--sp-spacing-md) !important;
  display: flex !important;
  justify-content: center !important;
  align-items: center !important;
  margin-top: -1px !important;
  gap: var(--sp-spacing-md) !important;
}

/* Базовые стили для ShikiPlayer */
.sp-wrapper {
  background: var(--sp-bg-primary) !important;
  border: 1px solid var(--sp-border-color) !important;
  border-radius: var(--sp-radius-lg) var(--sp-radius-lg) 0 0 !important;
  overflow: hidden !important;
  transition: all var(--sp-transition-normal) !important;
  position: relative !important;
}

/* Контейнер плеера */
.sp-container {
  background: var(--sp-bg-secondary) !important;
  border-radius: var(--sp-radius-lg) var(--sp-radius-lg) 0 0 !important;
  overflow: hidden !important;
}

/* Заголовок плеера */
.sp-header {
  background: linear-gradient(135deg, #2b2a39eb 0%, #2b2a39eb 100%) !important;
  padding: var(--sp-spacing-md) var(--sp-spacing-lg) !important;
  border-bottom: 1px solid var(--sp-border-color) !important;
  display: flex !important;
  align-items: center !important;
  justify-content: space-between !important;
}

.sp-title {
  color: var(--sp-text-primary) !important;
  font-size: 18px !important;
  font-weight: 600 !important;
  margin: 0 !important;
  display: flex !important;
  align-items: center !important;
  gap: var(--sp-spacing-sm) !important;
}

/* Выпадающий список плееров */
.sp-dropdown {
  position: relative !important;
  margin-left: auto !important;
}

.sp-dropdown-toggle {
  background: var(--sp-bg-tertiary) !important;
  border: 1px solid var(--sp-border-light) !important;
  border-radius: var(--sp-radius-md) !important;
  color: var(--sp-text-primary) !important;
  padding: var(--sp-spacing-sm) var(--sp-spacing-md) !important;
  cursor: pointer !important;
  display: flex !important;
  align-items: center !important;
  gap: var(--sp-spacing-sm) !important;
  transition: all var(--sp-transition-fast) !important;
  font-size: 14px !important;
  font-weight: 500 !important;
}

.sp-dropdown-toggle:hover {
  background: var(--sp-bg-hover) !important;
  border-color: var(--sp-accent) !important;
  transform: translateY(-1px) !important;
}

.sp-dropdown-toggle::after {
  content: "▼" !important;
  font-size: 12px !important;
  transition: transform var(--sp-transition-fast) !important;
}

.sp-dropdown.open .sp-dropdown-toggle::after {
  transform: rotate(180deg) !important;
}

.sp-dropdown-menu {
  position: absolute !important;
  top: 100% !important;
  right: 0 !important;
  background: var(--sp-bg-tertiary) !important;
  border: 1px solid var(--sp-border-light) !important;
  border-radius: var(--sp-radius-md) !important;
  min-width: 100px !important;
  z-index: 1000 !important;
  opacity: 0 !important;
  visibility: hidden !important;
  transform: translateY(-10px) !important;
  transition: all var(--sp-transition-fast) !important;
  margin-top: var(--sp-spacing-xs) !important;
  max-height: 300px !important;
  overflow-y: auto !important;
}

.sp-dropdown.open .sp-dropdown-menu {
  opacity: 1 !important;
  visibility: visible !important;
  transform: translateY(0) !important;
}

.sp-dropdown-item {
  padding: var(--sp-spacing-sm) var(--sp-spacing-md) !important;
  cursor: pointer !important;
  transition: all var(--sp-transition-fast) !important;
  display: flex !important;
  align-items: center !important;
  gap: var(--sp-spacing-sm) !important;
  color: var(--sp-text-secondary) !important;
  font-size: 14px !important;
  border-bottom: 1px solid var(--sp-border-color) !important;
}

.sp-dropdown-item:last-child {
  border-bottom: none !important;
}

.sp-dropdown-item:hover {
  background: var(--sp-bg-hover) !important;
  color: var(--sp-text-primary) !important;
}

.sp-dropdown-item.active {
   background: #3d3d3d !important;
   color: #ffffff !important;
}

.sp-dropdown-item.loading {
  color: var(--sp-text-muted) !important;
  cursor: not-allowed !important;
}

/* Индикаторы статуса */
.sp-status-indicator {
  width: 8px !important;
  height: 8px !important;
  border-radius: 50% !important;
  margin-left: auto !important;
  transition: all var(--sp-transition-fast) !important;
}

.sp-status-indicator.online {
  background: var(--sp-online) !important;
  animation: pulse 2s infinite !important;
}

.sp-status-indicator.offline {
  background: var(--sp-offline) !important;
}

.sp-status-indicator.loading {
  background: var(--sp-loading) !important;
  animation: spin 1s linear infinite !important;
}

@keyframes pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.5; }
}

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}

/* Область просмотра плеера */
.sp-viewer {
  background: var(--sp-bg-primary) !important;
  min-height: 500px !important;
  height: 500px !important;
  position: relative !important;
  overflow: hidden !important;
}

.sp-viewer iframe {
  border: none !important;
  width: 100% !important;
  height: 100% !important;
  min-height: 400px !important;
}

/* Оверлей загрузки */
.sp-loading-overlay {
  position: absolute !important;
  top: 0 !important;
  left: 0 !important;
  right: 0 !important;
  bottom: 0 !important;
  background: rgba(10, 10, 10, 0.9) !important;
  display: flex !important;
  align-items: center !important;
  justify-content: center !important;
  z-index: 100 !important;
  backdrop-filter: blur(4px) !important;
}

.sp-loading-overlay::after {
  content: "" !important;
  width: 40px !important;
  height: 40px !important;
  border: 3px solid var(--sp-border-color) !important;
  border-top: 3px solid var(--sp-accent) !important;
  border-radius: 50% !important;
  animation: spin 1s linear infinite !important;
}

/* Кнопка режима кинотеатра - квадратная с закругленными углами */
.sp-theater-btn {
  display: flex !important;
  align-items: center !important;
  justify-content: center !important;
  width: 48px !important;
  height: 48px !important;
  padding: var(--sp-spacing-sm) !important;
  background: var(--sp-bg-tertiary) !important;
  border: 1px solid var(--sp-border-light) !important;
  border-radius: var(--sp-radius-md) !important;
  color: var(--sp-text-primary) !important;
  cursor: pointer !important;
  transition: all var(--sp-transition-fast) !important;
  position: relative !important;
}

.sp-theater-btn:hover {
  background: var(--sp-bg-hover) !important;
  border-color: var(--sp-accent) !important;
  transform: translateY(-2px) !important;
}

.sp-theater-btn:active {
  transform: translateY(0) !important;
}

.sp-theater-btn svg {
  width: 24px !important;
  height: 24px !important;
  flex-shrink: 0 !important;
}

/* Кнопка добавления эпизода */
.sp-episode-btn {
  display: flex !important;
  align-items: center !important;
  justify-content: center !important;
  width: 48px !important;
  height: 48px !important;
  padding: var(--sp-spacing-sm) !important;
  background: var(--sp-bg-tertiary) !important;
  border: 1px solid var(--sp-border-light) !important;
  border-radius: var(--sp-radius-md) !important;
  color: var(--sp-text-primary) !important;
  cursor: pointer !important;
  transition: all var(--sp-transition-fast) !important;
  position: relative !important;
}

.sp-episode-btn:hover {
  background: var(--sp-bg-hover) !important;
  border-color: var(--sp-accent) !important;
  transform: translateY(-2px) !important;
}

.sp-episode-btn:active {
  transform: translateY(0) !important;
}

.sp-episode-btn svg {
  width: 24px !important;
  height: 24px !important;
  flex-shrink: 0 !important;
}

/* Счетчик просмотренных серий */
.sp-episode-count {
  position: absolute !important;
  bottom: -5px !important;
  right: -5px !important;
  background: var(--sp-accent) !important;
  color: #ffffff !important;
  font-size: 12px !important;
  font-weight: bold !important;
  padding: 2px 4px !important;
  border-radius: 10px !important;
  min-width: 28px !important;
  text-align: center !important;
  line-height: 12px !important;
}

/* Кнопка закрытия в режиме кинотеатра */
.sp-theater-close {
  position: absolute !important;
  top: var(--sp-spacing-md) !important;
  right: var(--sp-spacing-md) !important;
  width: 40px !important;
  height: 40px !important;
  background: rgba(0, 0, 0, 0.7) !important;
  border: 1px solid var(--sp-border-color) !important;
  border-radius: var(--sp-radius-md) !important;
  display: none !important;
  align-items: center !important;
  justify-content: center !important;
  cursor: pointer !important;
  z-index: 10000 !important;
  transition: all var(--sp-transition-fast) !important;
  backdrop-filter: blur(4px) !important;
}

.sp-theater-close:hover {
  background: rgba(239, 68, 68, 0.8) !important;
  border-color: var(--sp-error) !important;
  transform: scale(1.1) !important;
}

.sp-theater-close svg {
  width: 20px !important;
  height: 20px !important;
  color: var(--sp-text-primary) !important;
}

/* Режим кинотеатра */
.sp-wrapper.theater-mode {
  position: fixed !important;
  top: 0 !important;
  left: 0 !important;
  right: 0 !important;
  bottom: 0 !important;
  z-index: 9999 !important;
  margin: 0 !important;
  border-radius: 0 !important;
  border: none !important;
  background: var(--sp-bg-primary) !important;
}

.sp-wrapper.theater-mode .sp-viewer {
  height: 100vh !important;
  min-height: unset !important;
}

.sp-wrapper.theater-mode .sp-header {
  display: none !important;
}

.sp-wrapper.theater-mode .sp-theater-close {
  display: flex !important;
}

/* Стили для кастомных подсказок */
.sp-tooltip {
  position: absolute !important;
  background: var(--sp-bg-secondary) !important;
  color: var(--sp-text-primary) !important;
  padding: var(--sp-spacing-xs) var(--sp-spacing-sm) !important;
  border-radius: var(--sp-radius-sm) !important;
  font-size: 14px !important;
  font-weight: 500 !important;
  white-space: nowrap !important;
  z-index: 10000 !important;
  pointer-events: none !important;
  opacity: 0 !important;
  transform: translateY(-5px) !important;
  transition: all var(--sp-transition-fast) !important;
  backdrop-filter: blur(4px) !important;
  border: 1px solid var(--sp-border-color) !important;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important;
}

.sp-tooltip.visible {
  opacity: 1 !important;
  transform: translateY(0) !important;
}

.sp-tooltip::before {
  content: "" !important;
  position: absolute !important;
  bottom: 100% !important;
  left: 50% !important;
  transform: translateX(-50%) !important;
  border-width: 5px !important;
  border-style: solid !important;
  border-color: transparent transparent var(--sp-bg-secondary) transparent !important;
}

/* Скроллбар */
.sp-dropdown-menu::-webkit-scrollbar {
  width: 6px !important;
}

.sp-dropdown-menu::-webkit-scrollbar-track {
  background: var(--sp-bg-secondary) !important;
}

.sp-dropdown-menu::-webkit-scrollbar-thumb {
  background: var(--sp-border-light) !important;
  border-radius: 3px !important;
}

.sp-dropdown-menu::-webkit-scrollbar-thumb:hover {
  background: var(--sp-accent) !important;
}

/* Адаптивность */
@media (max-width: 768px) {
  .sp-header {
    padding: var(--sp-spacing-sm) var(--sp-spacing-md) !important;
    flex-direction: column !important;
    gap: var(--sp-spacing-sm) !important;
  }
  
  .sp-dropdown {
    width: 100% !important;
  }
  
  .sp-dropdown-toggle {
    width: 100% !important;
    justify-content: space-between !important;
  }
  
  .sp-dropdown-menu {
    position: absolute !important;
    top: 100% !important;
    left: 0 !important;
    transform: none !important;
    width: 100% !important;
    max-width: none !important;
    border-radius: 0 0 var(--sp-radius-md) var(--sp-radius-md) !important;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important;
  }
}

/* Дополнительные улучшения */
.sp-wrapper * {
  box-sizing: border-box !important;
}

.sp-wrapper button,
.sp-wrapper select,
.sp-wrapper input {
  background: var(--sp-bg-tertiary) !important;
  border: 1px solid var(--sp-border-light) !important;
  border-radius: var(--sp-radius-sm) !important;
  color: var(--sp-text-primary) !important;
  padding: var(--sp-spacing-xs) var(--sp-spacing-sm) !important;
  transition: all var(--sp-transition-fast) !important;
}

.sp-wrapper button:hover,
.sp-wrapper select:hover,
.sp-wrapper input:hover {
  border-color: var(--sp-accent) !important;
}

.sp-wrapper button:focus,
.sp-wrapper select:focus,
.sp-wrapper input:focus {
  outline: none !important;
  border-color: var(--sp-accent) !important;
}

/* Анимация появления */
.sp-outer-wrapper {
  animation: fadeInUp 0.5s ease-out !important;
}

@keyframes fadeInUp {
  from {
    opacity: 0;
    transform: translateY(20px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}
`;

function injectDarkTheme() {
  if (document.getElementById("shikiplayer-dark-theme")) return;
  const style = document.createElement("style");
  style.id = "shikiplayer-dark-theme";
  style.textContent = darkThemeCSS;
  document.head.appendChild(style);
}

injectDarkTheme();
// --- END OF CSS INJECTION ---

// Класс для создания кастомных подсказок
class Tooltip {
  constructor() {
    this.tooltip = null;
    this.targetElement = null;
    this.showTimeout = null;
    this.hideTimeout = null;
    this.init();
  }

  init() {
    // Создаем элемент подсказки
    this.tooltip = document.createElement("div");
    this.tooltip.className = "sp-tooltip";
    document.body.appendChild(this.tooltip);
  }

  // Добавление подсказки к элементу
  attach(element, text) {
    // Удаляем стандартный атрибут title, чтобы избежать дублирования
    const title = element.getAttribute("title");
    if (title) {
      element.setAttribute("data-tooltip-text", title);
      element.removeAttribute("title");
    } else if (text) {
      element.setAttribute("data-tooltip-text", text);
    }

    // Добавляем обработчики событий
    element.addEventListener("mouseenter", this.show.bind(this));
    element.addEventListener("mouseleave", this.hide.bind(this));
    element.addEventListener("click", this.hide.bind(this));
  }

  // Показ подсказки
  show(event) {
    const element = event.currentTarget;
    const text = element.getAttribute("data-tooltip-text");
    if (!text) return;

    // Отменяем скрытие, если оно было запланировано
    if (this.hideTimeout) {
      clearTimeout(this.hideTimeout);
      this.hideTimeout = null;
    }

    // Запланируем показ с минимальной задержкой
    this.showTimeout = setTimeout(() => {
      this.targetElement = element;
      this.tooltip.textContent = text;

      // Позиционирование подсказки
      const rect = element.getBoundingClientRect();
      const tooltipRect = this.tooltip.getBoundingClientRect();

      // Показываем подсказку снизу от элемента
      let top = rect.bottom + 10;
      let left = rect.left + (rect.width - tooltipRect.width) / 2;

      // Проверяем, не выходит ли подсказка за пределы экрана снизу
      if (top + tooltipRect.height > window.innerHeight) {
        // Если не помещается снизу, показываем сверху
        top = rect.top - tooltipRect.height - 10;
      }

      // Проверяем, не выходит ли подсказка за пределы экрана по горизонтали
      if (left < 0) {
        left = 5;
      } else if (left + tooltipRect.width > window.innerWidth) {
        left = window.innerWidth - tooltipRect.width - 5;
      }

      this.tooltip.style.top = `${top + window.scrollY}px`;
      this.tooltip.style.left = `${left + window.scrollX}px`;

      // Показываем подсказку с анимацией
      this.tooltip.classList.add("visible");
    }, 100); // Уменьшенная задержка перед показом
  }

  // Скрытие подсказки
  hide() {
    // Отменяем показ, если он был запланирован
    if (this.showTimeout) {
      clearTimeout(this.showTimeout);
      this.showTimeout = null;
    }

    // Немедленно скрываем подсказку
    this.tooltip.classList.remove("visible");
    this.targetElement = null;
  }

  // Уничтожение подсказки
  destroy() {
    if (this.showTimeout) {
      clearTimeout(this.showTimeout);
    }
    if (this.hideTimeout) {
      clearTimeout(this.hideTimeout);
    }
    if (this.tooltip && this.tooltip.parentNode) {
      this.tooltip.parentNode.removeChild(this.tooltip);
    }
  }
}

// Базовый класс для ошибок
class ErrorBase extends Error {
  constructor(message, options) {
    super(message, options);
    this.name = new.target.name;
  }
}
// Ошибка для некорректных HTTP-ответов
class ResponseError extends ErrorBase {
  constructor(response) {
    super(
      `Received response with unsuccessful code ${response.status} ${response.statusText}`
    );
    this.response = response;
  }
}
// HTTP-клиент для GM.xmlHttpRequest с таймаутом
class GMHttp {
  async fetch(input, init) {
    const methods = [
      "GET",
      "POST",
      "PUT",
      "DELETE",
      "PATCH",
      "HEAD",
      "TRACE",
      "OPTIONS",
      "CONNECT",
    ];
    let requestMethod = init?.method ?? "GET";
    if (!methods.includes(requestMethod)) {
      throw new Error(`HTTP method ${requestMethod} is not supported`);
    }
    let requestUrl = input.toString();
    let requestBody = init?.body
      ? await new Response(init.body).text()
      : undefined;
    let requestHeaders = init?.headers
      ? Object.fromEntries(new Headers(init.headers))
      : {};
    // Добавляем таймаут по умолчанию 5 секунд
    const timeout = init?.timeout || 5000;
    const timeoutId = setTimeout(() => {
      throw new Error(`Request timeout after ${timeout}ms`);
    }, timeout);
    let gmResponse = await new Promise((resolve, reject) => {
      GM.xmlHttpRequest({
        url: requestUrl,
        method: requestMethod,
        data: requestBody,
        headers: requestHeaders,
        responseType: "blob",
        timeout: timeout,
        onload: (response) => {
          clearTimeout(timeoutId);
          resolve(response);
        },
        onerror: (error) => {
          clearTimeout(timeoutId);
          reject(error);
        },
        ontimeout: () => {
          clearTimeout(timeoutId);
          reject(new Error(`Request timeout after ${timeout}ms`));
        },
      });
    });
    let responseHeaders = gmResponse.responseHeaders
      .trim()
      .split(/\r?\n/)
      .map((line) => line.split(/:\s*/, 2));
    return new Response(gmResponse.response, {
      status: gmResponse.status,
      statusText: gmResponse.statusText,
      headers: responseHeaders,
    });
  }
}
// Утилита для проверки JSON
class Json {
  static parse(text, type) {
    let value;
    try {
      value = JSON.parse(text);
    } catch (e) {
      throw new Error(`Error parsing JSON: ${text}`);
    }
    if (!type(value)) {
      throw new Error(`Invalid JSON type`);
    }
    return value;
  }
}
// Базовый класс плеера
class PlayerBase {
  getEpisode() {
    return 0;
  }
  setEpisode(value) {}
  getTime() {
    return 0;
  }
  setTime(value) {}
  getTranslation() {
    return "";
  }
  setTranslation(value) {}
  dispose() {}
}
// Kodik Player
class KodikPlayer extends PlayerBase {
  constructor(uid, results) {
    super();
    this.uid = uid;
    this._results = results;
    this.element = document.createElement("iframe");
    this.element.allowFullscreen = true;
    this.element.width = "100%";
    this.element.style.aspectRatio = "16 / 9";
    this._translation = results[0] || new Error("No translation found");
    this.rebuildIFrameSrc();
    addEventListener("message", this.onMessage);
  }
  name = "Kodik";
  element;
  _episode = 1;
  _time = 0;
  _translation;
  getEpisode() {
    return this._episode;
  }
  setEpisode(value) {
    this._episode = value;
    this.rebuildIFrameSrc();
  }
  getTime() {
    return this._time;
  }
  setTime(value) {
    this._time = value;
    this.rebuildIFrameSrc();
  }
  getTranslation() {
    return this._translation.translation.id + "";
  }
  setTranslation(value) {
    this._translation =
      this._results.find((r) => r.translation.id === +value) ||
      new Error(`Translation '${value}' not found`);
    this.rebuildIFrameSrc();
  }
  rebuildIFrameSrc() {
    let src = new URL(`https:${this._translation.link}`);
    src.searchParams.set("uid", this.uid);
    src.searchParams.set("episode", this._episode + "");
    src.searchParams.set("start_from", this._time + "");
    this.element.src = src.toString();
  }
  onMessage = (ev) => {
    if (ev.source !== this.element.contentWindow) return;
    let message;
    try {
      message = JSON.parse(ev.data);
    } catch (e) {
      return;
    }
    if (message.key === "kodik_player_time_update") {
      this._time = message.value;
    }
  };
  dispose() {
    removeEventListener("message", this.onMessage);
  }
}
// Kodik Factory
class KodikFactory {
  constructor(uid, api) {
    this.uid = uid;
    this._api = api;
  }
  name = "Kodik";
  async create(animeId, abort) {
    let results = await this._api.search(animeId, abort);
    if (results.length === 0) return null;
    return new KodikPlayer(this.uid, results);
  }
}
// Alloha Player
class AllohaPlayer extends PlayerBase {
  constructor(url, season, lastEpisode) {
    super();
    this._url = url;
    this._season = season;
    this._lastEpisode = lastEpisode;
    this.element = document.createElement("iframe");
    this.element.allowFullscreen = true;
    this.element.width = "100%";
    this.element.style.aspectRatio = "16 / 9";
    this.rebuildIFrameSrc();
    addEventListener("message", this.onMessage);
  }
  name = "Alloha";
  element;
  _translation = "";
  _episode = 1;
  _season;
  _lastEpisode;
  _time = 0;
  getEpisode() {
    return this._episode;
  }
  setEpisode(value) {
    this._episode = Math.min(value, this._lastEpisode);
    this.rebuildIFrameSrc();
  }
  getSeason() {
    return this._season;
  }
  setSeason(value) {
    this._season = value;
    this.rebuildIFrameSrc();
  }
  getTime() {
    return this._time;
  }
  setTime(value) {
    this._time = value;
    this.rebuildIFrameSrc();
  }
  getTranslation() {
    return this._translation;
  }
  setTranslation(value) {
    this._translation = value;
    this.rebuildIFrameSrc();
  }
  rebuildIFrameSrc() {
    let src = new URL(this._url);
    src.searchParams.set("season", this._season + "");
    src.searchParams.set("translation", this._translation);
    src.searchParams.set("episode", this._episode + "");
    src.searchParams.set("start", this._time + "");
    this.element.src = src.toString();
  }
  onMessage = (ev) => {
    if (ev.source !== this.element.contentWindow) return;
    let message;
    try {
      message = JSON.parse(ev.data);
    } catch (e) {
      return;
    }
    if (message.event === "timeupdate") {
      this._time = message.time;
    } else if (message.event === "sp_season") {
      this.setSeason(message.season);
    } else if (message.event === "sp_episode") {
      this.setEpisode(message.episode);
    } else if (message.event === "sp_translation") {
      this.setTranslation(message.translation);
    }
  };
  dispose() {
    removeEventListener("message", this.onMessage);
  }
}
// Alloha Factory - ИЗМЕНЕНО: Используем Kinobox API вместо Alloha API
class AllohaFactory {
  constructor(kodikApi, kinoboxApi) {
    this._kodikApi = kodikApi;
    this._kinoboxApi = kinoboxApi;
  }
  name = "Alloha";
  async create(animeId, abort) {
    let kodikResults = await this._kodikApi.search(animeId);
    let kodikResult = kodikResults[0];
    if (!kodikResult || !kodikResult.kinopoisk_id) return null;

    // Используем Kinobox API для получения плеера Alloha
    let kinoboxResult = await this._kinoboxApi.players(
      kodikResult.kinopoisk_id,
      abort
    );

    // Ищем плеер Alloha в результатах
    let alloha = kinoboxResult.data.find((p) => p.type === "Alloha");
    if (!alloha || !alloha.iframeUrl) return null;

    let season = kodikResult.last_season || 1;
    // Устанавливаем значение по умолчанию для последнего эпизода
    let lastEpisode = 12; // Значение по умолчанию, так как Kinobox API не предоставляет эту информацию

    return new AllohaPlayer(alloha.iframeUrl, season, lastEpisode);
  }
}
// Collaps Player
class CollapsPlayer extends PlayerBase {
  constructor(url, season, lastEpisode) {
    super();
    this._url = url;
    this._season = season;
    this._lastEpisode = lastEpisode;
    this.element = document.createElement("iframe");
    this.element.allowFullscreen = true;
    this.element.width = "100%";
    this.element.style.aspectRatio = "16 / 9";
    this.rebuildIFrameSrc();
  }
  name = "Collaps";
  element;
  _episode = 1;
  _time = 0;
  getEpisode() {
    return this._episode;
  }
  setEpisode(value) {
    this._episode = Math.min(value, this._lastEpisode);
    this.rebuildIFrameSrc();
  }
  getTime() {
    return this._time;
  }
  setTime(value) {
    this._time = value;
    this.rebuildIFrameSrc();
  }
  rebuildIFrameSrc() {
    let src = new URL(this._url);
    src.searchParams.set("season", this._season + "");
    src.searchParams.set("episode", this._episode + "");
    src.searchParams.set("time", this._time + "");
    this.element.src = src.toString();
  }
}
// Collaps Factory
class CollapsFactory {
  constructor(kodikApi, collapsApi) {
    this._kodikApi = kodikApi;
    this._collapsApi = collapsApi;
  }
  name = "Collaps";
  async create(animeId, abort) {
    let kodikResults = await this._kodikApi.search(animeId);
    let kodikResult = kodikResults[0];
    if (!kodikResult || !kodikResult.kinopoisk_id) return null;
    let collapsResults = await this._collapsApi.list(
      kodikResult.kinopoisk_id,
      abort
    );
    let collapsResult = collapsResults[0];
    if (!collapsResult) return null;
    let season = kodikResult.last_season || 1;
    let lastEpisode =
      collapsResult.seasons?.find((s) => s.season === season)?.episodes
        .length || 1;
    return new CollapsPlayer(collapsResult.iframe_url, season, lastEpisode);
  }
}
// Turbo Player
class TurboPlayer extends PlayerBase {
  constructor(url, season) {
    super();
    this._url = url;
    this._season = season;
    this.element = document.createElement("iframe");
    this.element.allowFullscreen = true;
    this.element.width = "100%";
    this.element.style.aspectRatio = "16 / 9";
    this.rebuildIFrameSrc();
  }
  name = "Turbo";
  element;
  rebuildIFrameSrc() {
    let src = new URL(this._url);
    this.element.src = src.toString();
  }
}
// Turbo Factory
class TurboFactory {
  constructor(kodikApi, kinoboxApi) {
    this._kodikApi = kodikApi;
    this._kinoboxApi = kinoboxApi;
  }
  name = "Turbo";
  async create(animeId, abort) {
    let kodikResults = await this._kodikApi.search(animeId);
    let kodikResult = kodikResults[0];
    if (!kodikResult || !kodikResult.kinopoisk_id) return null;
    let kinoboxResult = await this._kinoboxApi.players(
      kodikResult.kinopoisk_id,
      abort
    );
    let turbo = kinoboxResult.data.find((p) => p.type === "Turbo");
    if (!turbo || !turbo.iframeUrl) return null;
    let season = kodikResult.last_season || 1;
    return new TurboPlayer(turbo.iframeUrl, season);
  }
}
// Lumex Player
class LumexPlayer extends PlayerBase {
  constructor(url) {
    super();
    this._url = url;
    this.element = document.createElement("iframe");
    this.element.allowFullscreen = true;
    this.element.width = "100%";
    this.element.style.aspectRatio = "16 / 9";
    this.rebuildIFrameSrc();
  }
  name = "Lumex";
  element;
  rebuildIFrameSrc() {
    let src = new URL(this._url);
    this.element.src = src.toString();
  }
}
// Lumex Factory
class LumexFactory {
  constructor(kodikApi, kinoboxApi) {
    this._kodikApi = kodikApi;
    this._kinoboxApi = kinoboxApi;
  }
  name = "Lumex";
  async create(animeId, abort) {
    let kodikResults = await this._kodikApi.search(animeId);
    let kodikResult = kodikResults[0];
    if (!kodikResult || !kodikResult.kinopoisk_id) return null;
    let kinoboxResult = await this._kinoboxApi.players(
      kodikResult.kinopoisk_id,
      abort
    );
    let lumex = kinoboxResult.data.find((p) => p.type === "Lumex");
    if (!lumex || !lumex.iframeUrl) return null;
    return new LumexPlayer(lumex.iframeUrl);
  }
}
// Veoveo Player
class VeoveoPlayer extends PlayerBase {
  constructor(url) {
    super();
    this._url = url;
    this.element = document.createElement("iframe");
    this.element.allowFullscreen = true;
    this.element.width = "100%";
    this.element.style.aspectRatio = "16 / 9";
    this.rebuildIFrameSrc();
  }
  name = "Veoveo";
  element;
  rebuildIFrameSrc() {
    let src = new URL(this._url);
    this.element.src = src.toString();
  }
}
// Veoveo Factory
class VeoveoFactory {
  constructor(kodikApi, kinoboxApi) {
    this._kodikApi = kodikApi;
    this._kinoboxApi = kinoboxApi;
  }
  name = "Veoveo";
  async create(animeId, abort) {
    let kodikResults = await this._kodikApi.search(animeId);
    let kodikResult = kodikResults[0];
    if (!kodikResult || !kodikResult.kinopoisk_id) return null;
    let kinoboxResult = await this._kinoboxApi.players(
      kodikResult.kinopoisk_id,
      abort
    );
    let veoveo = kinoboxResult.data.find((p) => p.type === "Veoveo");
    if (!veoveo || !veoveo.iframeUrl) return null;
    return new VeoveoPlayer(veoveo.iframeUrl);
  }
}
// Vibix Player
class VibixPlayer extends PlayerBase {
  constructor(url) {
    super();
    this._url = url;
    this.element = document.createElement("iframe");
    this.element.allowFullscreen = true;
    this.element.width = "100%";
    this.element.style.aspectRatio = "16 / 9";
    this.rebuildIFrameSrc();
  }
  name = "Vibix";
  element;
  rebuildIFrameSrc() {
    let src = new URL(this._url);
    this.element.src = src.toString();
  }
}
// Vibix Factory
class VibixFactory {
  constructor(kodikApi, kinoboxApi) {
    this._kodikApi = kodikApi;
    this._kinoboxApi = kinoboxApi;
  }
  name = "Vibix";
  async create(animeId, abort) {
    let kodikResults = await this._kodikApi.search(animeId);
    let kodikResult = kodikResults[0];
    if (!kodikResult || !kodikResult.kinopoisk_id) return null;
    let kinoboxResult = await this._kinoboxApi.players(
      kodikResult.kinopoisk_id,
      abort
    );
    let vibix = kinoboxResult.data.find((p) => p.type === "Vibix");
    if (!vibix || !vibix.iframeUrl) return null;
    return new VibixPlayer(vibix.iframeUrl);
  }
}
// API для Kodik
class KodikApi {
  constructor(http, token) {
    this._http = http;
    this._token = token;
  }
  async search(shikimoriId, abort) {
    let url = new URL("https://kodikapi.com/search");
    url.searchParams.set("token", this._token);
    url.searchParams.set("shikimori_id", shikimoriId + "");
    let response = await this._http.fetch(url, {
      signal: abort,
      timeout: 3000,
    });
    if (!response.ok) throw new ResponseError(response);
    let text = await response.text();
    let data = Json.parse(
      text,
      (v) =>
        typeof v === "object" &&
        v !== null &&
        Array.isArray(v.results) &&
        v.results.every(
          (e) =>
            typeof e === "object" &&
            e !== null &&
            typeof e.link === "string" &&
            (typeof e.kinopoisk_id === "undefined" ||
              typeof e.kinopoisk_id === "string") &&
            (typeof e.imdb_id === "undefined" ||
              typeof e.imdb_id === "string") &&
            typeof e.translation === "object" &&
            e.translation !== null &&
            typeof e.translation.id === "number" &&
            (typeof e.last_season === "undefined" ||
              typeof e.last_season === "number")
        )
    );
    return data.results;
  }
}
// API для Kinobox (используется для Turbo, Lumex, Alloha, Veoveo и теперь Vibix)
class KinoboxApi {
  constructor(http) {
    this._http = http;
  }
  _sessionId = Math.trunc(Math.random() * 100);
  async players(kinopoisk, abort) {
    let url = new URL("https://api.kinobox.tv/api/players");
    url.searchParams.set("kinopoisk", kinopoisk + "");
    url.searchParams.set("ts", this.getTs());
    let response = await this._http.fetch(url, {
      headers: {
        Referer: "https://kinohost.web.app/",
        Origin: "https://kinohost.web.app",
        "Sec-Fetch-Site": "cross-site",
      },
      signal: abort,
      timeout: 5000,
    });
    if (!response.ok) throw new ResponseError(response);
    let text = await response.text();
    return Json.parse(
      text,
      (v) =>
        typeof v === "object" &&
        v !== null &&
        Array.isArray(v.data) &&
        v.data.every(
          (e) =>
            typeof e === "object" &&
            e !== null &&
            typeof e.type === "string" &&
            (e.iframeUrl === null || typeof e.iframeUrl === "string")
        )
    );
  }
  getTs() {
    let s = Math.ceil(Date.now() / 1e3) % 1e5;
    let i = s % 100;
    let r = i - (i % 3);
    return s - i + r + "." + this._sessionId;
  }
}
// API для Collaps
class CollapsApi {
  constructor(http, token) {
    this._http = http;
    this._token = token;
  }
  async list(kinopoiskId, abort) {
    let url = new URL("https://apicollaps.cc/list");
    url.searchParams.set("token", this._token);
    url.searchParams.set("kinopoisk_id", kinopoiskId);
    let response = await this._http.fetch(url, {
      signal: abort,
      timeout: 5000,
    });
    if (!response.ok) throw new ResponseError(response);
    let text = await response.text();
    let data = Json.parse(
      text,
      (v) =>
        typeof v === "object" &&
        v !== null &&
        Array.isArray(v.results) &&
        v.results.every(
          (e) =>
            typeof e === "object" &&
            e !== null &&
            typeof e.iframe_url === "string" &&
            (typeof e.seasons === "undefined" ||
              (Array.isArray(e.seasons) &&
                e.seasons.every(
                  (s) =>
                    typeof s === "object" &&
                    s !== null &&
                    Array.isArray(s.episodes) &&
                    typeof s.season === "number"
                )))
        )
    );
    return data.results;
  }
}
// Основной класс Shikiplayer
class Shikiplayer {
  constructor(playerFactories) {
    this._playerFactories = playerFactories;
    // Создаем внешний контейнер
    this.element = document.createElement("div");
    this.element.className = "sp-outer-wrapper";

    // ИСПРАВЛЕННАЯ HTML СТРУКТУРА: кнопка в отдельном контейнере
    this.element.innerHTML = `
<div class="sp-wrapper">
  <div class="sp-container">
    <div class="sp-header">
      <div class="sp-title">Онлайн-просмотр</div>
      <div class="sp-dropdown">
        <div class="sp-dropdown-toggle">
          <span class="sp-selected-player">Выберите плеер</span>
        </div>
        <div class="sp-dropdown-menu"></div>
      </div>
    </div>
    <div class="sp-viewer">
      <div class="sp-loading-overlay"></div>
    </div>
  </div>
  <button class="sp-theater-close" title="Закрыть режим кинотеатра">
    <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
      <line x1="18" y1="6" x2="6" y2="18"></line>
      <line x1="6" y1="6" x2="18" y2="18"></line>
    </svg>
  </button>
</div>
<div class="sp-button-container">
  <button class="sp-theater-btn" title="Театральный режим">
    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
      <path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"></path>
    </svg>
  </button>
  <button class="sp-episode-btn" title="Отметить серию как просмотренную">
    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
      <line x1="12" y1="5" x2="12" y2="19"></line>
      <line x1="5" y1="12" x2="19" y2="12"></line>
    </svg>
    <span class="sp-episode-count">0/0</span>
  </button>
</div>
        `;
    this._wrapper = this.element.querySelector(".sp-wrapper");
    this._container = this.element.querySelector(".sp-container");
    this._dropdown = this.element.querySelector(".sp-dropdown");
    this._dropdownToggle = this.element.querySelector(".sp-dropdown-toggle");
    this._dropdownMenu = this.element.querySelector(".sp-dropdown-menu");
    this._selectedPlayerText = this.element.querySelector(
      ".sp-selected-player"
    );
    this._viewer = this.element.querySelector(".sp-viewer");
    this._loadingOverlay = this.element.querySelector(".sp-loading-overlay");
    this._theaterBtn = this.element.querySelector(".sp-theater-btn");
    this._theaterCloseBtn = this.element.querySelector(".sp-theater-close");
    this._episodeBtn = this.element.querySelector(".sp-episode-btn");
    this._currentPlayer = null;
    this._playerInstances = new Map();
    this._isTheaterMode = false;

    // Инициализация системы подсказок
    this._tooltip = new Tooltip();

    // Обработчики событий для выпадающего списка
    this._dropdownToggle.addEventListener("click", () => {
      this._dropdown.classList.toggle("open");
    });
    // Закрытие выпадающего списка при клике вне его
    document.addEventListener("click", (e) => {
      if (!this._dropdown.contains(e.target)) {
        this._dropdown.classList.remove("open");
      }
    });
    // Обработчик для кнопки режима кинотеатра
    this._theaterBtn.addEventListener("click", () => {
      this.toggleTheaterMode();
    });
    // Обработчик для кнопки закрытия режима кинотеатра
    this._theaterCloseBtn.addEventListener("click", () => {
      this.toggleTheaterMode();
    });
    // Обработчик для кнопки добавления эпизода
    this._episodeBtn.addEventListener("click", () => {
      this.incrementEpisode();
    });
    // Обработчик для закрытия режима кинотеатра по клавише Esc
    document.addEventListener("keydown", (e) => {
      if (e.key === "Escape" && this._isTheaterMode) {
        this.toggleTheaterMode();
      }
    });
  }

  toggleTheaterMode() {
    this._isTheaterMode = !this._isTheaterMode;
    if (this._isTheaterMode) {
      this._wrapper.classList.add("theater-mode");
      // Сохраняем текущую позицию прокрутки
      this._scrollPosition = window.pageYOffset;
      // Блокируем прокрутку страницы
      document.body.style.overflow = "hidden";
      document.body.style.position = "fixed";
      document.body.style.top = `-${this._scrollPosition}px`;
      document.body.style.width = "100%";
    } else {
      this._wrapper.classList.remove("theater-mode");
      // Восстанавливаем прокрутку страницы
      document.body.style.overflow = "";
      document.body.style.position = "";
      document.body.style.top = "";
      document.body.style.width = "";
      window.scrollTo(0, this._scrollPosition);
    }
  }

  incrementEpisode() {
    // ИСПРАВЛЕНИЕ: Улучшенный поиск кнопки увеличения эпизода
    // Пробуем несколько возможных селекторов для кнопки
    let incrementButton = document.querySelector(".item-add.increment");
    if (!incrementButton) {
      incrementButton = document.querySelector(".b-user_rate .increment");
    }
    if (!incrementButton) {
      incrementButton = document.querySelector(".b-add_to_list .increment");
    }

    if (incrementButton) {
      // Кликаем по ней
      incrementButton.click();

      // Добавляем визуальную обратную связь
      this._episodeBtn.style.background = "var(--sp-success)";
      setTimeout(() => {
        this._episodeBtn.style.background = "";
      }, 500);

      // ИСПРАВЛЕНИЕ: Увеличиваем задержку и добавляем несколько попыток обновления счетчика
      // Обновляем счетчик серий с увеличенной задержкой
      setTimeout(() => {
        this.updateEpisodeCount();
      }, 1000);

      // Вторая попытка обновления счетчика
      setTimeout(() => {
        this.updateEpisodeCount();
      }, 2000);
    } else {
      // Если кнопка не найдена, показываем ошибку
      this._episodeBtn.style.background = "var(--sp-error)";
      setTimeout(() => {
        this._episodeBtn.style.background = "";
      }, 500);
    }
  }

  updateEpisodeCount() {
    // ИСПРАВЛЕНИЕ: Улучшенный поиск элемента с количеством просмотренных серий
    // Пробуем несколько возможных селекторов
    let rateNumber = document.querySelector(".rate-number");
    if (!rateNumber) {
      rateNumber = document.querySelector(".b-user_rate .rate-number");
    }
    if (!rateNumber) {
      rateNumber = document.querySelector(".b-add_to_list .rate-number");
    }

    if (!rateNumber) {
      console.error(
        "Не удалось найти элемент с количеством просмотренных серий"
      );
      return;
    }

    // Получаем текст из элемента
    const rateText = rateNumber.textContent;

    // Находим элемент счетчика в нашей кнопке
    const episodeCount = this._episodeBtn.querySelector(".sp-episode-count");
    if (!episodeCount) return;

    // ИСПРАВЛЕНИЕ: Добавляем анимацию при обновлении счетчика
    // Сохраняем старое значение для сравнения
    const oldValue = episodeCount.textContent;

    // Обновляем текст счетчика
    episodeCount.textContent = rateText;

    // Если значение изменилось, добавляем анимацию
    if (oldValue !== rateText) {
      episodeCount.style.transform = "scale(1.2)";
      setTimeout(() => {
        episodeCount.style.transform = "scale(1)";
      }, 300);
    }
  }

  async start(abort) {
    // Очищаем предыдущий контейнер, если он существует
    let existing = document.querySelector(".sp-outer-wrapper");
    if (existing) existing.remove();
    let before = document.querySelector(".b-db_entry");
    if (before) before.after(this.element);
    let entryText = document
      .querySelector(".b-db_entry .b-user_rate")
      ?.getAttribute("data-entry");
    if (!entryText) return;
    let entry = JSON.parse(entryText);
    if (!entry || typeof entry.id !== "number") return;

    // Добавляем подсказки только к кнопкам
    this._tooltip.attach(this._theaterBtn, "Театральный режим");
    this._tooltip.attach(
      this._theaterCloseBtn,
      "Закрыть режим кинотеатра (Esc)"
    );
    this._tooltip.attach(this._episodeBtn, "Отметить серию как просмотренную");

    // Обновляем счетчик серий
    this.updateEpisodeCount();

    // Создаем элементы для всех плееров в выпадающем списке
    for (let factory of this._playerFactories) {
      let item = document.createElement("div");
      item.className = "sp-dropdown-item loading";
      item.innerHTML = `
 ${factory.name}
<span class="sp-status-indicator loading"></span>
            `;
      item.dataset.playerName = factory.name;
      this._dropdownMenu.appendChild(item);
    }
    // Загружаем Kodik немедленно и отображаем его
    let kodikFactory = this._playerFactories.find((f) => f.name === "Kodik");
    if (kodikFactory) {
      try {
        let kodikPlayer = await kodikFactory.create(entry.id, abort);
        if (kodikPlayer) {
          this._playerInstances.set("Kodik", kodikPlayer);
          this.switchPlayer("Kodik", kodikPlayer);
          // Обновляем элемент Kodik в выпадающем списке
          let kodikItem = this._dropdownMenu.querySelector(
            "[data-player-name='Kodik']"
          );
          if (kodikItem) {
            kodikItem.classList.remove("loading");
            kodikItem.classList.add("active");
            kodikItem
              .querySelector(".sp-status-indicator")
              .classList.remove("loading");
            kodikItem
              .querySelector(".sp-status-indicator")
              .classList.add("online");
            kodikItem.addEventListener("click", () => {
              this.switchPlayer("Kodik", kodikPlayer);
              this._dropdown.classList.remove("open");
            });
          }
        } else {
          // Если Kodik не загрузился, убираем его из списка
          let kodikItem = this._dropdownMenu.querySelector(
            "[data-player-name='Kodik']"
          );
          if (kodikItem) {
            kodikItem
              .querySelector(".sp-status-indicator")
              .classList.remove("loading");
            kodikItem
              .querySelector(".sp-status-indicator")
              .classList.add("offline");
            kodikItem.classList.remove("loading");
          }
        }
      } catch (e) {
        console.error(`Error in Kodik:`, e);
        // Если Kodik не загрузился, убираем его из списка
        let kodikItem = this._dropdownMenu.querySelector(
          "[data-player-name='Kodik']"
        );
        if (kodikItem) {
          kodikItem
            .querySelector(".sp-status-indicator")
            .classList.remove("loading");
          kodikItem
            .querySelector(".sp-status-indicator")
            .classList.add("offline");
          kodikItem.classList.remove("loading");
        }
      }
    }
    // Загружаем остальные плееры в фоновом режиме
    for (let factory of this._playerFactories) {
      if (factory.name === "Kodik") continue; // Пропускаем Kodik, уже загружен
      let item = this._dropdownMenu.querySelector(
        `[data-player-name='${factory.name}']`
      );
      if (!item) continue;
      // Используем Promise без await, чтобы не блокировать выполнение
      factory
        .create(entry.id, abort)
        .then((player) => {
          item.classList.remove("loading");
          if (!player) {
            item
              .querySelector(".sp-status-indicator")
              .classList.remove("loading");
            item.querySelector(".sp-status-indicator").classList.add("offline");
            return;
          }
          this._playerInstances.set(factory.name, player);
          item
            .querySelector(".sp-status-indicator")
            .classList.remove("loading");
          item.querySelector(".sp-status-indicator").classList.add("online");
          item.addEventListener("click", () => {
            this.switchPlayer(factory.name, player);
            this._dropdown.classList.remove("open");
          });
        })
        .catch((e) => {
          console.error(`Error in ${factory.name}:`, e);
          item
            .querySelector(".sp-status-indicator")
            .classList.remove("loading");
          item.querySelector(".sp-status-indicator").classList.add("offline");
          item.classList.remove("loading");
        });
    }
  }

  switchPlayer(playerName, player) {
    // Показываем индикатор загрузки
    this._loadingOverlay.style.display = "flex";
    // Удаляем текущий плеер из viewport
    this._viewer.innerHTML = "";
    if (this._currentPlayer) {
      this._currentPlayer.dispose();
    }
    // Устанавливаем новый плеер
    this._viewer.appendChild(player.element);
    this._currentPlayer = player;
    // Обновляем текст в выпадающем списке
    this._selectedPlayerText.textContent = playerName;
    // Обновляем активный элемент в выпадающем списке
    for (let item of this._dropdownMenu.children) {
      item.classList.toggle("active", item.dataset.playerName === playerName);
    }
    // Скрываем индикатор загрузки с небольшой задержкой для плавности
    setTimeout(() => {
      this._loadingOverlay.style.display = "none";
    }, 500);
  }

  dispose() {
    if (this._currentPlayer) {
      this._currentPlayer.dispose();
      this._currentPlayer = null;
    }
    this._playerInstances.clear();

    // Уничтожаем систему подсказок
    if (this._tooltip) {
      this._tooltip.destroy();
    }

    this.element.remove();
    // Восстанавливаем прокрутку страницы, если был режим кинотеатра
    if (this._isTheaterMode) {
      document.body.style.overflow = "";
      document.body.style.position = "";
      document.body.style.top = "";
      document.body.style.width = "";
      window.scrollTo(0, this._scrollPosition);
    }
  }
}
// Запуск Alloha Helper
async function startAllohaHelper() {
  let hostnames = [
    "beggins-as.pljjalgo.online",
    "beggins-as.allarknow.online",
    "beggins-as.algonoew.online",
  ];
  if (!hostnames.includes(location.hostname)) return;
  new MutationObserver((mutations) => {
    for (let mutation of mutations) {
      let target = mutation.target;
      if (target.matches(".select__drop-item.active")) {
        let event;
        if (target.closest("[data-select='seasonType1']")) {
          event = { event: "sp_season", season: +target.dataset.id };
        } else if (target.closest("[data-select='episodeType1']")) {
          event = { event: "sp_episode", episode: +target.dataset.id };
        } else if (target.closest("[data-select='translationType1']")) {
          event = {
            event: "sp_translation",
            translation: +target.dataset.id.match(/(?<=t)\d+/)[0],
          };
        }
        if (event) parent.postMessage(JSON.stringify(event), "*");
      }
    }
  }).observe(document, { subtree: true, attributeFilter: ["class"] });
}
// Запуск Shikiplayer с поддержкой Turbolinks
async function startShikiplayer() {
  if (location.hostname !== "shikimori.one") return;
  const kodikToken = "a0457eb45312af80bbb9f3fb33de3e93";
  const kodikUid = "";
  const collapsToken = "4c250f7ac0a8c8a658c789186b9a58a5";
  let http = new GMHttp();
  let kodikApi = new KodikApi(http, kodikToken);
  let kinoboxApi = new KinoboxApi(http);
  let collapsApi = new CollapsApi(http, collapsToken);
  let factories = [
    new KodikFactory(kodikUid, kodikApi),
    // ИЗМЕНЕНО: AllohaFactory теперь использует Kinobox API вместо Alloha API
    new AllohaFactory(kodikApi, kinoboxApi),
    new TurboFactory(kodikApi, kinoboxApi),
    new LumexFactory(kodikApi, kinoboxApi),
    new VeoveoFactory(kodikApi, kinoboxApi),
    new VibixFactory(kodikApi, kinoboxApi), // Добавлен новый плеер Vibix
    new CollapsFactory(kodikApi, collapsApi),
  ];
  let shikiplayer = null;
  // Функция инициализации плеера
  async function initializePlayer() {
    if (shikiplayer) {
      shikiplayer.dispose(); // Очищаем текущий плеер
    }
    shikiplayer = new Shikiplayer(factories);
    await shikiplayer.start(new AbortController().signal);
  }
  // Первичный запуск
  initializePlayer();
  // Обработка события Turbolinks
  document.addEventListener("turbolinks:load", initializePlayer);
}
void startAllohaHelper();
void startShikiplayer();