Anistar Player Controls

Anistar.org Improvement: cleanup, change content width, change player size

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

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

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         Anistar Player Controls
// @name:ru      Anistar Player Controls
// @namespace    https://anistar.org/
// @version      1.1.1
// @license MIT
// @description Anistar.org Improvement: cleanup, change content width, change player size
// @description:ru  Встроенные кнопки перемотки, карточка серии и режим на ширину окна для Anistar.
// @match        https://anistar.org/*
// @match        http://anistar.org/*
// @run-at       document-idle
// ==/UserScript==

(() => {
  'use strict';

  const SEEK_SECONDS = 5;
  let mountedHost = null;
  let wideStage = null;
  let wideStagePlaceholder = null;
  let wideStageStyleSnapshot = null;

  injectStyles();
  observePage();
  mountWhenReady();

  function injectStyles() {
    if (document.getElementById('anistar-player-controls-style')) return;
    const style = document.createElement('style');
    style.id = 'anistar-player-controls-style';
    style.textContent = `
      body.anistar-controls-wide { overflow: hidden; }
      body.anistar-controls-wide::before {
        content: '';
        position: fixed !important; inset: 0 !important; z-index: 2147483645 !important;
        background: #000 !important; pointer-events: none !important;
      }
      .anistar-controls-wide-stage {
        position: fixed !important; z-index: 2147483646 !important;
        top: 50% !important; left: 0 !important; right: auto !important; bottom: auto !important;
        width: 100vw !important; max-width: none !important;
        height: 100vh !important; max-height: 100vh !important;
        margin: 0 !important; padding: 0 !important; background: #000 !important;
        box-sizing: border-box !important; overflow: hidden !important; transform: translateY(-50%) !important;
      }
      .anistar-controls-wide-stage video,
      .anistar-controls-wide-stage iframe {
        width: 100% !important; height: 100% !important; max-width: none !important;
        object-fit: contain !important;
      }
      video.anistar-controls-wide-stage,
      iframe.anistar-controls-wide-stage {
        width: 100vw !important; height: 100vh !important; max-width: none !important;
        object-fit: contain !important;
      }
      #anistar-series-card {
        position: fixed !important; z-index: 2147483647 !important;
        top: 18px !important; left: 18px !important;
        display: flex !important; align-items: flex-start !important; gap: 10px !important;
        max-width: min(430px, calc(100vw - 36px)) !important;
        color: #fff !important; pointer-events: none !important;
        text-align: left !important; text-shadow: 0 1px 4px #000 !important;
        font-family: Arial, sans-serif !important;
      }
      #anistar-series-card img {
        flex: 0 0 auto !important; width: 48px !important; height: 68px !important;
        border-radius: 3px !important; object-fit: cover !important;
        box-shadow: 0 2px 8px rgb(0 0 0 / 65%) !important;
      }
      #anistar-series-card .anistar-card-title {
        margin: 1px 0 3px !important; font-size: 20px !important; font-weight: 700 !important;
      }
      #anistar-series-card .anistar-card-subtitle {
        margin-bottom: 4px !important; color: #ddd !important; font-size: 13px !important;
      }
      #anistar-series-card .anistar-card-episode {
        color: #bbb !important; font-size: 12px !important;
      }
    `;
    document.head.append(style);
  }

  function observePage() {
    new MutationObserver(() => mountWhenReady()).observe(document.documentElement, {
      childList: true,
      subtree: true
    });
  }

  function mountWhenReady() {
    if (wideStage) return;
    const host = getPlayerHost();
    if (!host || host === mountedHost) return;

    mountedHost = host;
    connectPlayer(host);
  }

  function getPlayerHost() {
    return document.querySelector('#vid_2');
  }

  function connectPlayer(host) {
    const frame = host.querySelector('iframe');
    if (!frame) return;
    const install = () => {
      if (wideStage === host) setInnerPlayerWide(frame, true);
      installInnerControls(frame, host);
    };
    frame.addEventListener('load', install);
    install();
  }

  function installInnerControls(frame, host) {
    let doc;
    try {
      doc = frame?.contentDocument;
    } catch {
      return;
    }
    const controls = doc?.querySelector('.controls');
    const video = doc?.querySelector('video');
    if (!doc || !controls || !video) return;

    injectInnerControlsStyle(doc);
    bindInnerShortcuts(doc, host);

    const ids = ['anistar-seek-back', 'anistar-seek-forward', 'anistar-wide-toggle'];
    if (ids.every((id) => doc.getElementById(id))) {
      updateInnerWideButton(frame, wideStage === host);
      return;
    }
    ids.forEach((id) => doc.getElementById(id)?.remove());
    doc.querySelector('.anistar-inner-seek-controls')?.remove();

    const seekControls = doc.createElement('div');
    seekControls.className = 'anistar-inner-seek-controls';
    seekControls.setAttribute('role', 'group');
    seekControls.setAttribute('aria-label', 'Перемотка на 5 секунд');
    seekControls.append(
      makeInnerButton(doc, 'anistar-seek-back', '−5', 'Назад на 5 секунд', () => seek(host, -SEEK_SECONDS)),
      makeInnerButton(doc, 'anistar-seek-forward', '+5', 'Вперёд на 5 секунд', () => seek(host, SEEK_SECONDS))
    );

    const timeControl = controls.querySelector('.time, .timer, [class*="time"]');
    timeControl?.before(seekControls);
    if (!seekControls.isConnected) controls.prepend(seekControls);

    const wideButton = makeInnerButton(doc, 'anistar-wide-toggle', '↔', 'На ширину окна', () => {
      setWide(host, !getWideStage(host)?.classList.contains('anistar-controls-wide-stage'));
    });
    const fullscreenControl = controls.querySelector(
      '[class*="fullscreen"], [class*="full-screen"], [title*="полный" i], [title*="full screen" i]'
    );
    fullscreenControl?.before(wideButton);
    if (!wideButton.isConnected) controls.append(wideButton);
    updateInnerWideButton(frame, wideStage === host);
  }

  function makeInnerButton(doc, id, text, title, action) {
    const button = doc.createElement('button');
    button.type = 'button';
    button.id = id;
    button.textContent = text;
    button.title = title;
    button.setAttribute('aria-label', title);
    button.addEventListener('click', (event) => {
      event.preventDefault();
      event.stopPropagation();
      action();
    });
    return button;
  }

  function injectInnerControlsStyle(doc) {
    if (doc.getElementById('anistar-inner-controls-style')) return;
    const style = doc.createElement('style');
    style.id = 'anistar-inner-controls-style';
    style.textContent = `
      .controls { overflow: visible !important; }
      .anistar-inner-seek-controls {
        position: absolute !important; z-index: 50 !important;
        left: 12px !important; bottom: calc(100% + 6px) !important;
        display: inline-flex !important; gap: 4px !important;
      }
      #anistar-seek-back,
      #anistar-seek-forward,
      #anistar-wide-toggle {
        box-sizing: border-box !important; min-width: 32px !important; height: 28px !important;
        margin: 0 !important; padding: 0 7px !important; border: 0 !important;
        border-radius: 4px !important; color: #fff !important;
        background: rgb(15 24 28 / 88%) !important; cursor: pointer !important;
        font: 600 13px/28px Arial, sans-serif !important; text-align: center !important;
        opacity: .82 !important;
      }
      #anistar-seek-back:hover,
      #anistar-seek-forward:hover,
      #anistar-wide-toggle:hover,
      #anistar-wide-toggle[aria-pressed="true"] {
        background: #1767a9 !important; opacity: 1 !important;
      }
      #anistar-wide-toggle {
        position: absolute !important; z-index: 50 !important;
        right: 12px !important; bottom: calc(100% + 6px) !important;
      }
    `;
    (doc.head || doc.documentElement).append(style);
  }

  function bindInnerShortcuts(doc, host) {
    if (doc.documentElement.dataset.anistarShortcutsBound === 'true') return;
    doc.documentElement.dataset.anistarShortcutsBound = 'true';
    doc.addEventListener('keydown', (event) => handleShortcut(event, host), true);
  }

  function updateInnerWideButton(frame, enabled) {
    try {
      frame?.contentDocument?.getElementById('anistar-wide-toggle')
        ?.setAttribute('aria-pressed', String(enabled));
    } catch {}
  }

  function runAction(action, host) {
    if (action === 'backward' || action === 'forward') {
      seek(host, action === 'backward' ? -SEEK_SECONDS : SEEK_SECONDS);
    }
    if (action === 'wide') {
      setWide(host, !getWideStage(host)?.classList.contains('anistar-controls-wide-stage'));
    }
  }

  function seek(host, delta) {
    const video = getPlayerVideo(host);
    if (!video) return;
    const target = Math.max(0, (Number(video.currentTime) || 0) + delta);
    video.currentTime = Number.isFinite(video.duration) ? Math.min(target, video.duration) : target;
  }

  function getPlayerVideo(host) {
    const directVideo = host?.querySelector('video');
    if (directVideo) return directVideo;

    const frame = host?.querySelector('iframe');
    try {
      return frame?.contentDocument?.querySelector('video') || document.querySelector('video');
    } catch {
      return document.querySelector('video');
    }
  }

  function setWide(host, enabled) {
    const stage = getWideStage(host);
    if (!stage) return;
    const frame = stage.querySelector('iframe');
    document.body.classList.toggle('anistar-controls-wide', enabled);
    if (enabled) {
      showSeriesCard(host);
      wideStage = stage;
      wideStagePlaceholder = document.createComment('anistar-player-controls-placeholder');
      wideStageStyleSnapshot = saveInlineStyles(stage);
      stage.before(wideStagePlaceholder);
      document.body.append(stage);
      stage.classList.add('anistar-controls-wide-stage');
      stage.style.setProperty('position', 'fixed', 'important');
      stage.style.setProperty('z-index', '2147483646', 'important');
      stage.style.setProperty('top', '50%', 'important');
      stage.style.setProperty('left', '0', 'important');
      stage.style.setProperty('width', '100vw', 'important');
      stage.style.setProperty('height', '100vh', 'important');
      stage.style.setProperty('max-width', 'none', 'important');
      stage.style.setProperty('margin', '0', 'important');
      stage.style.setProperty('transform', 'translateY(-50%)', 'important');
      setInnerPlayerWide(frame, true);
      installInnerControls(frame, host);
      updateInnerWideButton(frame, true);
    } else {
      removeSeriesCard();
      setInnerPlayerWide(frame, false);
      stage.classList.remove('anistar-controls-wide-stage');
      if (wideStagePlaceholder?.parentNode) wideStagePlaceholder.replaceWith(stage);
      restoreInlineStyles(stage, wideStageStyleSnapshot);
      wideStage = null;
      wideStagePlaceholder = null;
      wideStageStyleSnapshot = null;
      installInnerControls(frame, host);
      updateInnerWideButton(frame, false);
    }
  }

  function setInnerPlayerWide(frame, enabled) {
    let doc;
    try {
      doc = frame?.contentDocument;
    } catch {
      return;
    }
    if (!doc) return;

    const styleId = 'anistar-inner-wide-style';
    const existingStyle = doc.getElementById(styleId);
    if (!enabled) {
      existingStyle?.remove();
      return;
    }
    if (existingStyle) return;

    const style = doc.createElement('style');
    style.id = styleId;
    style.textContent = `
      html, body {
        width: 100% !important; height: 100% !important; margin: 0 !important;
        overflow: hidden !important; background: #000 !important;
      }
      .videoas {
        display: block !important; width: 100% !important; height: 100% !important;
      }
      #playlist_as { display: none !important; }
      #video_as,
      #player,
      .video-container,
      #video_player,
      .middle-controll,
      .controls {
        width: 100% !important; max-width: none !important;
      }
      #video_as,
      #player,
      .video-container,
      #video_player,
      .middle-controll {
        height: 100% !important;
      }
      #player { position: relative !important; }
      #video_player {
        object-fit: contain !important; background: #000 !important;
      }
      .controls {
        position: absolute !important; left: 0 !important; right: 0 !important;
        bottom: 0 !important; top: auto !important;
      }
    `;
    (doc.head || doc.documentElement).append(style);
  }

  function readSeriesMetadata(host) {
    const rawHeading = document.querySelector('h1')?.textContent?.trim()
      || document.querySelector('meta[property="og:title"]')?.content?.trim()
      || document.title.trim();
    const cleanHeading = rawHeading.replace(/\s*[|—]\s*AniStar.*$/i, '').trim();
    const [title = '', subtitle = ''] = cleanHeading.split(/\s*\/\s*/, 2);
    const poster = document.querySelector('meta[property="og:image"]')?.content
      || document.querySelector('.poster img, .short-images img, [itemprop="image"]')?.src
      || '';

    let playlistText = '';
    try {
      const playlist = host.querySelector('iframe')?.contentDocument;
      playlistText = playlist?.querySelector(
        '#playlist_as .active, #playlist_as .selected, #playlist_as [aria-current="true"], #playlist_as [class*="select"]'
      )?.textContent || '';
    } catch {}
    const pageText = document.querySelector('.news')?.textContent || document.body.textContent || '';
    const episode = (playlistText.match(/серия\s*(\d+)/i)
      || pageText.match(/добавлена\s+(\d+)\s+серия/i))?.[1] || '';
    return { title, subtitle, episode, poster };
  }

  function showSeriesCard(host) {
    removeSeriesCard();
    const metadata = readSeriesMetadata(host);
    if (!metadata.title && !metadata.poster && !metadata.episode) return;

    const card = document.createElement('div');
    card.id = 'anistar-series-card';
    card.setAttribute('aria-hidden', 'true');
    if (metadata.poster) {
      const image = document.createElement('img');
      image.src = metadata.poster;
      image.alt = '';
      card.append(image);
    }

    const text = document.createElement('div');
    appendCardLine(text, 'anistar-card-title', metadata.title);
    appendCardLine(text, 'anistar-card-subtitle', metadata.subtitle);
    appendCardLine(text, 'anistar-card-episode', metadata.episode ? `Серия ${metadata.episode}` : '');
    if (text.childElementCount) card.append(text);
    document.body.append(card);
  }

  function appendCardLine(parent, className, value) {
    if (!value) return;
    const line = document.createElement('div');
    line.className = className;
    line.textContent = value;
    parent.append(line);
  }

  function removeSeriesCard() {
    document.getElementById('anistar-series-card')?.remove();
  }

  function getWideStage(host) {
    if (wideStage) return wideStage;
    return document.querySelector('#vid_2') || host;
  }

  function saveInlineStyles(element) {
    const properties = ['position', 'z-index', 'top', 'left', 'width', 'height', 'max-width', 'margin', 'transform'];
    return Object.fromEntries(properties.map((property) => [property, {
      value: element.style.getPropertyValue(property),
      priority: element.style.getPropertyPriority(property)
    }]));
  }

  function restoreInlineStyles(element, snapshot) {
    if (!snapshot) return;
    Object.entries(snapshot).forEach(([property, saved]) => {
      if (saved.value) element.style.setProperty(property, saved.value, saved.priority);
      else element.style.removeProperty(property);
    });
  }

  document.addEventListener('keydown', (event) => handleShortcut(event, mountedHost), true);

  function handleShortcut(event, host) {
    if (!host || event.ctrlKey || event.altKey || event.metaKey || !shouldHandleShortcut(event.target)) return;
    const action = {
      ArrowLeft: 'backward', ArrowRight: 'forward', w: 'wide', W: 'wide'
    }[event.key];
    if (!action) return;
    event.preventDefault();
    runAction(action, host);
  }

  function shouldHandleShortcut(target) {
    return !target?.isContentEditable && !['INPUT', 'TEXTAREA', 'SELECT'].includes(target?.tagName);
  }
})();