Anistar.org Improvement: cleanup, change content width, change player size
// ==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);
}
})();