// ==UserScript==
// @name ShikiPlayer
// @namespace https://github.com/Onzis/ShikiPlayer
// @version 1.50
// @description видеоплеер для просмотра прямо на Shikimori (Alloha → Kodik)
// @author Onzis
// @match https://shikimori.one/*
// @homepageURL https://github.com/Onzis/ShikiPlayer
// @connect api.alloha.tv
// @connect kodikapi.com
// @connect shikimori.one
// @grant GM.xmlHttpRequest
// @license GPL-3.0 license
// ==/UserScript==
(function () {
"use strict";
let currentPath = location.pathname;
let observer = null;
let currentPlayer = "alloha";
let isInserting = false;
const KodikToken = "7f129085d2f372833fcc5e2116e4d0a4";
const AllohaToken = "96b62ea8e72e7452b652e461ab8b89";
// Объект для хранения настроек
const playerSettings = {
rememberQuality: localStorage.getItem("shiki-remember-quality") === "true",
defaultQuality: localStorage.getItem("shiki-default-quality") || "auto",
defaultPlayer: localStorage.getItem("shiki-default-player") || "alloha",
// Проверяем и очищаем порядок плееров от удаленных
playerOrder: (function () {
const savedOrder = JSON.parse(
localStorage.getItem("shiki-player-order")
) || ["alloha", "kodik"];
// Фильтруем, оставляя только поддерживаемые плееры
const validPlayers = ["alloha", "kodik"];
const filteredOrder = savedOrder.filter((player) =>
validPlayers.includes(player)
);
// Если после фильтрации пусто, возвращаем порядок по умолчанию
return filteredOrder.length > 0 ? filteredOrder : validPlayers;
})(),
disableNotifications:
localStorage.getItem("shiki-disable-notifications") === "true",
theme: localStorage.getItem("shiki-theme") || "dark",
debugMode: localStorage.getItem("shiki-debug-mode") === "true",
};
// Сохраняем очищенный порядок плееров в localStorage
localStorage.setItem(
"shiki-player-order",
JSON.stringify(playerSettings.playerOrder)
);
// Добавляем объект для хранения доступности плееров
const playerAvailability = {
alloha: false,
kodik: false,
};
// Функция для отладки
function debugLog(message, data = null) {
if (playerSettings.debugMode) {
console.log(`[ShikiPlayer Debug] ${message}`, data || "");
}
}
// Функция для определения текущего сезона
function getCurrentSeason() {
const seasonMatch = location.pathname.match(
/\/animes\/[a-z]?(\d+)(?:-s(\d+))?/
);
return seasonMatch && seasonMatch[2] ? parseInt(seasonMatch[2]) : 1;
}
function getShikimoriID() {
const match = location.pathname.match(/\/animes\/(?:[a-z])?(\d+)/);
return match ? match[1] : null;
}
function removeOldElements() {
const oldIframe = document.querySelector(
'iframe[src*="kodik.cc"], iframe[src*="alloha.tv"]'
);
oldIframe?.remove();
}
function insertPlayerContainer(attempts = 10, delay = 200) {
if (
isInserting ||
!/^\/animes\/[^/]+/.test(location.pathname) ||
document.querySelector(".kodik-container")
) {
return;
}
const relatedBlock =
document.querySelector(".cc-related-authors") ||
document.querySelector(".sidebar");
if (!relatedBlock) {
if (attempts > 0) {
setTimeout(() => insertPlayerContainer(attempts - 1, delay), delay);
}
return;
}
isInserting = true;
removeOldElements();
createAndInsertPlayer(relatedBlock).finally(() => {
isInserting = false;
});
}
function showNotification(message, type = "info") {
if (playerSettings.disableNotifications) return;
if (!document.getElementById("shikip-notif-style-modern")) {
const style = document.createElement("style");
style.id = "shikip-notif-style-modern";
style.textContent = `
.shikip-notif-modern-container {
position: fixed;
left: 50%;
bottom: 32px;
transform: translateX(-50%);
z-index: 99999;
display: flex;
flex-direction: column;
align-items: center;
max-width: 96vw;
pointer-events: none;
}
.shikip-notif-modern {
background: rgba(255, 255, 255, 0.85);
color: #ffffff;
padding: 18px 32px;
border-radius: 16px;
font-size: 1.08rem;
font-family: 'Inter', 'Segoe UI', Arial, sans-serif;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
opacity: 0;
margin-top: 8px;
margin-bottom: 2px;
display: flex;
align-items: center;
gap: 14px;
transition: opacity .5s, transform .5s;
pointer-events: auto;
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.5);
transform: translateY(20px);
}
.shikip-notif-modern.show {
opacity: 1;
transform: translateY(0);
}
.shikip-notif-modern.success { border-color: rgb(0 255 86 / 40%); background: rgb(0 0 0 / 40%); }
.shikip-notif-modern.error { border-color: rgb(255 23 0 / 40%); background: rgb(0 0 0 / 40%); }
.shikip-notif-modern.info { border-color: rgb(0 64 255 / 40%); background: rgb(0 0 0 / 40%); }
.shikip-notif-modern.warning { border-color: rgb(255 210 0 / 40%); background: rgb(0 0 0 / 40%); }
.shikip-notif-modern .notif-icon {
font-size: 1.5rem;
flex-shrink: 0;
animation: iconPulse 0.6s ease-in-out;
}
.shikip-notif-modern .notif-close {
margin-left: auto;
background: none;
border: none;
color: #666;
font-size: 1.3rem;
cursor: pointer;
opacity: .65;
transition: all 0.2s;
}
.shikip-notif-modern .notif-close:hover {
opacity: 1;
transform: rotate(90deg);
}
@keyframes iconPulse {
0% { transform: scale(0.8); opacity: 0; }
50% { transform: scale(1.1); }
100% { transform: scale(1); opacity: 1; }
}
@media (max-width: 600px) {
.shikip-notif-modern {
padding: 12px 18px;
font-size: .97rem;
gap: 10px;
}
.shikip-notif-modern-container {
max-width: 99vw;
bottom: 10px;
}
}
`;
document.head.appendChild(style);
}
let notifContainer = document.getElementById(
"shikip-notif-modern-container"
);
if (!notifContainer) {
notifContainer = document.createElement("div");
notifContainer.id = "shikip-notif-modern-container";
notifContainer.className = "shikip-notif-modern-container";
document.body.appendChild(notifContainer);
}
while (notifContainer.firstChild) {
notifContainer.removeChild(notifContainer.firstChild);
}
const icons = {
success: "✅",
error: "⛔",
info: "ℹ️",
warning: "⚠️",
};
const notifType = ["success", "error", "info", "warning"].includes(type)
? type
: "info";
const notif = document.createElement("div");
notif.className = `shikip-notif-modern ${notifType}`;
notif.innerHTML = `
<span class="notif-icon">${icons[notifType]}</span>
<span>${message}</span>
<button class="notif-close" title="Закрыть">×</button>
`;
notifContainer.appendChild(notif);
setTimeout(() => {
notif.classList.add("show");
}, 10);
const hide = () => {
notif.classList.remove("show");
setTimeout(() => notif.remove(), 500);
};
setTimeout(hide, 4500);
notif.querySelector(".notif-close").onclick = hide;
}
// ИСПРАВЛЕНА ФУНКЦИЯ: Теперь использует порядок из настроек
function playerSelectorHTML(current) {
let optionsHTML = "";
// Перебираем плееры в порядке, установленном в настройках
for (const player of playerSettings.playerOrder) {
if (playerAvailability[player]) {
const isSelected = player === current ? "selected" : "";
const playerName = player.charAt(0).toUpperCase() + player.slice(1);
optionsHTML += `<option value="${player}" ${isSelected}>${playerName}</option>`;
}
}
if (optionsHTML === "") {
optionsHTML = '<option value="" disabled>Нет доступных плееров</option>';
}
return `
<div class="player-selector-dropdown">
<select id="player-dropdown">
${optionsHTML}
</select>
</div>
`;
}
if (!document.getElementById("shikip-dropdown-style")) {
const style = document.createElement("style");
style.id = "shikip-dropdown-style";
style.textContent = `
.player-selector-dropdown {
display: flex;
align-items: center;
gap: 8px;
opacity: 0;
transform: translateY(10px);
animation: fadeInUp 0.6s ease forwards 0.3s;
}
#player-dropdown {
background: rgba(255, 255, 255, 0.7);
border: 1px solid rgba(255, 255, 255, 0.5);
color: #333;
border-radius: 8px;
padding: 8px 16px;
font-size: 14px;
outline: none;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
transition: all 0.3s ease;
}
#player-dropdown:focus {
background: rgba(255, 255, 255, 0.9);
box-shadow: 0 4px 16px rgba(105, 97, 255, 0.2);
border-color: rgba(105, 97, 255, 0.5);
transform: translateY(-2px);
}
#player-dropdown:disabled {
opacity: 0.6;
cursor: not-allowed;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
`;
document.head.appendChild(style);
}
async function checkPlayerAvailability(id) {
playerAvailability.alloha = false;
playerAvailability.kodik = false;
debugLog(`Проверка доступности плееров для аниме ID: ${id}`);
try {
const kodikResponse = await gmGetWithTimeout(
`https://kodikapi.com/search?token=${KodikToken}&shikimori_id=${id}`
);
const kodikData = JSON.parse(kodikResponse);
debugLog(`Ответ Kodik API:`, kodikData);
if (kodikData.results && kodikData.results.length > 0) {
playerAvailability.kodik = true;
debugLog("Kodik доступен");
}
} catch (e) {
console.warn("Kodik недоступен:", e);
debugLog("Ошибка при проверке Kodik:", e);
}
try {
const allohaUrl = await loadAllohaPlayer(id, 1);
if (allohaUrl) {
playerAvailability.alloha = true;
debugLog("Alloha доступен");
}
} catch (e) {
console.warn("Alloha недоступен:", e);
debugLog("Ошибка при проверке Alloha:", e);
}
// ИСПРАВЛЕНО: Теперь выбираем первый доступный плеер в порядке настроек
if (!playerAvailability[currentPlayer]) {
debugLog(
`Текущий плеер ${currentPlayer} недоступен, ищем замену в порядке настроек`
);
for (const player of playerSettings.playerOrder) {
if (playerAvailability[player]) {
currentPlayer = player;
debugLog(`Выбран новый текущий плеер: ${currentPlayer}`);
break;
}
}
}
debugLog("Итоговая доступность плееров:", playerAvailability);
debugLog("Текущий плеер после проверки:", currentPlayer);
}
// Функция для настройки drag and drop
function setupDragAndDrop(container) {
let draggedItem = null;
container.addEventListener("dragstart", (e) => {
if (e.target.classList.contains("player-order-item")) {
draggedItem = e.target;
e.target.classList.add("dragging");
}
});
container.addEventListener("dragend", (e) => {
if (e.target.classList.contains("player-order-item")) {
e.target.classList.remove("dragging");
}
});
container.addEventListener("dragover", (e) => {
e.preventDefault();
const afterElement = getDragAfterElement(container, e.clientY);
if (afterElement == null) {
container.appendChild(draggedItem);
} else {
container.insertBefore(draggedItem, afterElement);
}
});
}
function getDragAfterElement(container, y) {
const draggableElements = [
...container.querySelectorAll(".player-order-item:not(.dragging)"),
];
return draggableElements.reduce(
(closest, child) => {
const box = child.getBoundingClientRect();
const offset = y - box.top - box.height / 2;
if (offset < 0 && offset > closest.offset) {
return { offset: offset, element: child };
} else {
return closest;
}
},
{ offset: Number.NEGATIVE_INFINITY }
).element;
}
// Функция для обновления порядка плееров в модальном окне
function updatePlayerOrderInModal(settingsModal) {
const playerOrderContainer = settingsModal.querySelector(
"#player-order-container"
);
if (playerOrderContainer) {
playerOrderContainer.innerHTML = playerSettings.playerOrder
.map(
(player) => `
<div class="player-order-item" draggable="true" data-player="${player}">
<span class="drag-handle">☰</span>
<span class="player-name">${
player.charAt(0).toUpperCase() + player.slice(1)
}</span>
</div>
`
)
.join("");
// Настройка drag and drop для новых элементов
setupDragAndDrop(playerOrderContainer);
}
}
// Добавляем функцию для получения информации о просмотренных сериях
function getWatchedEpisodesInfo() {
const rateNumberElement = document.querySelector(".rate-number");
if (rateNumberElement) {
return rateNumberElement.textContent.trim();
}
return "";
}
async function createAndInsertPlayer(relatedBlock) {
if (!document.querySelector("style#kodik-styles")) {
const style = document.createElement("style");
style.id = "kodik-styles";
style.textContent = `
.kodik-container {
margin: 40px auto;
width: 100%;
max-width: 900px;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 10px 30px rgb(0 0 0 / 40%);
opacity: 0;
transform: translateY(30px);
animation: containerAppear 0.8s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
}
@keyframes containerAppear {
to {
opacity: 1;
transform: translateY(0);
}
}
.kodik-header {
display: flex;
margin-bottom: 0;
justify-content: space-between;
align-items: center;
background: rgba(255, 255, 255, 0.7);
padding: 12px 16px;
font-size: 14px;
font-weight: 600;
color: #333;
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
}
.kodik-header span:first-child {
opacity: 0;
animation: textFadeIn 0.6s ease forwards 0.2s;
}
@keyframes textFadeIn {
from {
opacity: 0;
transform: translateX(-10px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.kodik-links a {
text-decoration: none;
color: #333;
font-size: 11px;
}
.player-wrapper {
position: relative;
width: 100%;
padding-bottom: 56.25%;
overflow: hidden;
background: #000;
opacity: 0;
transform: scale(0.95);
animation: playerAppear 0.8s cubic-bezier(0.34, 1.56, 0.64, 1) forwards 0.5s;
}
@keyframes playerAppear {
to {
opacity: 1;
transform: scale(1);
}
}
.player-wrapper iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: none;
}
.loader {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #fff;
font-size: 14px;
z-index: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 15px;
}
.loader-spinner {
width: 40px;
height: 40px;
border: 4px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: #6961ff;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.error-message {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #ff6b6b;
font-size: 14px;
text-align: center;
z-index: 1;
background: rgba(255, 255, 255, 0.9);
padding: 16px 24px;
border-radius: 12px;
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.1);
animation: shake 0.5s ease-in-out;
}
@keyframes shake {
0%, 100% { transform: translate(-50%, -50%) rotate(0deg); }
25% { transform: translate(-52%, -50%) rotate(-1deg); }
75% { transform: translate(-48%, -50%) rotate(1deg); }
}
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
@keyframes fadeIn {
to { opacity: 1; }
}
.shikip-changelog {
margin-top: 0;
padding: 0;
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(12px);
border-top: 1px solid #0000004f;
-webkit-backdrop-filter: blur(12px);
border-radius: 0 0 16px 16px;
overflow: hidden;
transition: all 0.3s ease;
max-height: 40px;
opacity: 0;
animation: fadeIn 0.6s ease forwards 0.7s;
}
.shikip-changelog.expanded {
max-height: 300px;
}
.changelog-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1px 16px;
margin-top: 1px;
cursor: pointer;
border-bottom: 1px solid rgba(255, 255, 255, 0.3);
transition: background 0.3s ease;
}
.changelog-header:hover {
background: rgba(255, 255, 255, 0.5);
}
.changelog-header span {
font-weight: 600;
color: #333;
display: flex;
align-items: center;
gap: 8px;
}
.toggle-icon {
font-size: 16px;
transition: transform 0.3s ease;
}
.shikip-changelog.expanded .toggle-icon {
transform: rotate(180deg);
}
.github-link {
padding: 6px 12px;
background: rgba(105, 97, 255, 0.2);
color: #6961ff;
text-decoration: none;
border-radius: 6px;
font-size: 12px;
transition: all 0.2s;
border: 1px solid rgba(105, 97, 255, 0.3);
}
.github-link:hover {
background: rgba(105, 97, 255, 0.3);
transform: translateY(-2px);
}
.changelog-content {
padding: 0 16px;
max-height: 0;
overflow: hidden;
transition: max-height 0.3s ease, padding 0.3s ease;
}
.shikip-changelog.expanded .changelog-content {
max-height: 250px;
padding: 16px;
font-size: 14px;
overflow: auto;
}
.shikip-changelog.expanded .changelog-content::-webkit-scrollbar {
display: none;
}
.changelog-content ul {
margin: 0;
padding-left: 20px;
}
.changelog-content li {
margin-bottom: 8px;
color: #6961ff;
line-height: 1.5;
opacity: 0;
transform: translateX(-10px);
animation: slideInLeft 0.4s ease forwards;
}
.changelog-content li:nth-child(1) { animation-delay: 0.1s; }
.changelog-content li:nth-child(2) { animation-delay: 0.2s; }
.changelog-content li:nth-child(3) { animation-delay: 0.3s; }
.changelog-content li:nth-child(4) { animation-delay: 0.4s; }
.changelog-content li:nth-child(5) { animation-delay: 0.5s; }
.changelog-content li:nth-child(6) { animation-delay: 0.6s; }
.changelog-content li:nth-child(7) { animation-delay: 0.7s; }
.changelog-content li:nth-child(8) { animation-delay: 0.8s; }
@keyframes slideInLeft {
to {
opacity: 1;
transform: translateX(0);
}
}
/* Темная тема (по умолчанию) */
.kodik-container.dark-theme {
background: rgba(30, 30, 40, 0.9);
}
.kodik-container.dark-theme .kodik-header {
background: rgba(40, 40, 50, 0.8);
color: #fff;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.kodik-container.dark-theme .player-wrapper {
background: #000;
}
.kodik-container.dark-theme .loader {
color: #fff;
}
.kodik-container.dark-theme .loader-spinner {
border: 4px solid rgba(255, 255, 255, 0.2);
border-top-color: #6961ff;
}
.kodik-container.dark-theme .shikip-changelog {
background: rgba(40, 40, 50, 0.8);
border-top: 1px solid rgba(255, 255, 255, 0.1);
}
.kodik-container.dark-theme .changelog-header {
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.kodik-container.dark-theme .changelog-header:hover {
background: rgba(255, 255, 255, 0.1);
}
.kodik-container.dark-theme .changelog-header span {
color: #fff;
}
.kodik-container.dark-theme .changelog-content li {
color: #a99bff;
}
.kodik-container.dark-theme .player-selector-dropdown #player-dropdown {
background: rgba(40, 40, 50, 0.8);
border: 1px solid rgba(255, 255, 255, 0.1);
color: #fff;
}
.kodik-container.dark-theme .player-selector-dropdown #player-dropdown:focus {
background: rgba(50, 50, 60, 0.9);
border-color: #6961ff;
}
/* Светлая тема */
.kodik-container.light-theme {
background: rgba(245, 245, 250, 0.95);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
}
.kodik-container.light-theme .kodik-header {
background: rgba(255, 255, 255, 0.95);
color: #333;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}
.kodik-container.light-theme .player-wrapper {
background: #fff;
}
.kodik-container.light-theme .loader {
color: #333;
}
.kodik-container.light-theme .loader-spinner {
border: 4px solid rgba(0, 0, 0, 0.1);
border-top-color: #6961ff;
}
.kodik-container.light-theme .shikip-changelog {
background: rgba(255, 255, 255, 0.95);
border-top: 1px solid rgba(0, 0, 0, 0.1);
}
.kodik-container.light-theme .changelog-header {
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}
.kodik-container.light-theme .changelog-header:hover {
background: rgba(255, 255, 255, 0.5);
}
.kodik-container.light-theme .changelog-header span {
color: #333;
}
.kodik-container.light-theme .changelog-content li {
color: #6961ff;
}
.kodik-container.light-theme .player-selector-dropdown #player-dropdown {
background: rgba(255, 255, 255, 0.9);
border: 1px solid rgba(0, 0, 0, 0.1);
color: #333;
}
.kodik-container.light-theme .player-selector-dropdown #player-dropdown:focus {
background: #fff;
border-color: #6961ff;
}
@media (max-width: 600px) {
.changelog-header {
padding: 10px 12px;
}
.shikip-changelog.expanded .changelog-content {
padding: 12px;
}
.kodik-header {
padding: 10px 12px;
font-size: 13px;
}
#player-dropdown {
padding: 6px 12px;
font-size: 12px;
}
}
`;
document.head.appendChild(style);
}
if (!document.getElementById("shikip-theater-btn-style")) {
const style = document.createElement("style");
style.id = "shikip-theater-btn-style";
style.textContent = `
.player-buttons-container {
display: flex;
justify-content: center;
margin: 12px 0;
opacity: 0;
transform: translateY(10px);
animation: fadeInUp 0.6s ease forwards 0.7s;
}
.add-to-list-btn,
.settings-btn,
.theater-btn,
.status-btn {
background: rgba(255, 255, 255, 0.7);
border: 1px solid rgba(255, 255, 255, 0.5);
border-radius: 8px;
width: 44px;
height: 44px;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
position: relative;
display: flex;
justify-content: center;
align-items: center;
color: #000;
}
.add-to-list-btn svg,
.settings-btn svg,
.theater-btn svg,
.status-btn svg {
width: 24px;
height: 24px;
pointer-events: none;
}
.add-to-list-btn:hover,
.settings-btn:hover,
.theater-btn:hover,
.status-btn:hover {
background-color: rgba(255, 255, 255, 0.9);
box-shadow: 0 4px 12px rgba(105, 97, 255, 0.2);
border-color: rgba(105, 97, 255, 0.5);
transform: translateY(-2px);
}
/* Изменяем порядок отступов для кнопок */
.status-btn {
/* Первая кнопка, без левого отступа */
}
.theater-btn {
margin-left: 10px; /* Вторая кнопка, с левым отступом */
}
.add-to-list-btn {
margin-left: 10px; /* Третья кнопка, с левым отступом */
}
.settings-btn {
margin-left: 10px; /* Четвертая кнопка, с левым отступом */
}
.tooltip {
position: fixed;
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 10px 14px;
border-radius: 6px;
font-size: 14px;
white-space: pre-line;
z-index: 10000;
pointer-events: none;
opacity: 0;
transition: opacity 0.3s, transform 0.3s;
transform: translateX(-50%) translateY(-10px);
text-align: center;
line-height: 1.6;
}
.tooltip.show {
opacity: 1;
transform: translateX(-50%) translateY(5px);
}
.settings-modal {
display: none;
position: fixed;
z-index: 10000;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.8);
backdrop-filter: blur(5px);
animation: fadeIn 0.3s ease;
}
.settings-modal-content {
background-color: rgba(255, 255, 255, 0.95);
margin: 5% auto;
padding: 25px;
border: 1px solid rgba(255, 255, 255, 0.3);
width: 90%;
max-width: 600px;
border-radius: 16px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
animation: slideIn 0.4s ease;
max-height: 85vh;
display: flex;
flex-direction: column;
}
@keyframes slideIn {
from { transform: translateY(-50px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.settings-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}
.settings-header h2 {
margin: 0;
color: #333;
font-size: 24px;
}
.close-settings {
color: #666;
font-size: 28px;
font-weight: bold;
cursor: pointer;
transition: all 0.2s;
}
.close-settings:hover {
color: #333;
transform: rotate(90deg);
}
.settings-body {
overflow-y: auto;
flex-grow: 1;
padding-right: 5px;
}
.settings-body::-webkit-scrollbar {
width: 8px;
}
.settings-body::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.05);
border-radius: 4px;
}
.settings-body::-webkit-scrollbar-thumb {
background: rgba(105, 97, 255, 0.3);
border-radius: 4px;
}
.settings-body::-webkit-scrollbar-thumb:hover {
background: rgba(105, 97, 255, 0.5);
}
.settings-section {
margin-bottom: 25px;
}
.settings-section h3 {
margin-top: 0;
margin-bottom: 15px;
color: #444;
font-size: 18px;
}
.settings-option {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 0;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
}
.settings-option:last-child {
border-bottom: none;
}
.settings-option label {
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
color: #333;
}
.settings-option input[type="checkbox"] {
width: 20px;
height: 20px;
cursor: pointer;
}
.settings-option select {
padding: 8px 12px;
border-radius: 6px;
border: 1px solid rgba(0, 0, 0, 0.1);
background: rgba(255, 255, 255, 0.8);
color: #333;
font-size: 14px;
cursor: pointer;
}
.settings-info {
background: rgba(105, 97, 255, 0.1);
border-left: 4px solid #6961ff;
padding: 15px;
border-radius: 0 8px 8px 0;
margin-top: 20px;
font-size: 14px;
color: #555;
}
.settings-save-btn {
background: #6961ff;
color: white;
border: none;
padding: 12px 24px;
border-radius: 8px;
font-size: 16px;
cursor: pointer;
transition: all 0.3s;
margin-top: 15px;
width: 100%;
}
.settings-save-btn:hover {
background: #5a52e0;
transform: translateY(-2px);
}
.player-order-container {
margin-top: 10px;
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: 8px;
background: rgba(255, 255, 255, 0.8);
}
.player-order-item {
display: flex;
align-items: center;
padding: 10px 15px;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
cursor: move;
transition: background 0.2s;
}
.player-order-item:last-child {
border-bottom: none;
}
.player-order-item:hover {
background: rgba(105, 97, 255, 0.1);
}
.player-order-item.dragging {
opacity: 0.5;
}
.player-order-item.drag-over {
border-top: 2px solid #6961ff;
}
.drag-handle {
margin-right: 10px;
color: #999;
cursor: grab;
}
.drag-handle:active {
cursor: grabbing;
}
.player-name {
flex-grow: 1;
font-weight: 500;
}
.theme-selector {
display: flex;
gap: 10px;
}
.theme-option {
flex: 1;
padding: 10px;
border-radius: 8px;
border: 2px solid rgba(0, 0, 0, 0.1);
background: rgba(255, 255, 255, 0.8);
text-align: center;
cursor: pointer;
transition: all 0.2s;
}
.theme-option:hover {
border-color: #6961ff;
}
.theme-option.selected {
border-color: #6961ff;
background: rgba(105, 97, 255, 0.1);
}
.theme-icon {
font-size: 24px;
margin-bottom: 5px;
}
/* Темная тема для модального окна настроек */
.settings-modal.dark-theme .settings-modal-content {
background-color: rgba(40, 40, 50, 0.95);
color: #fff;
}
.settings-modal.dark-theme .settings-header {
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.settings-modal.dark-theme .settings-header h2 {
color: #fff;
}
.settings-modal.dark-theme .close-settings {
color: #ccc;
}
.settings-modal.dark-theme .close-settings:hover {
color: #fff;
}
.settings-modal.dark-theme .settings-section h3 {
color: #ddd;
}
.settings-modal.dark-theme .settings-option {
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.settings-modal.dark-theme .settings-option label {
color: #fff;
}
.settings-modal.dark-theme .settings-option select {
background: rgba(50, 50, 60, 0.8);
border: 1px solid rgba(255, 255, 255, 0.1);
color: #fff;
}
.settings-modal.dark-theme .player-order-container {
background: rgba(50, 50, 60, 0.8);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.settings-modal.dark-theme .player-order-item {
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
}
.settings-modal.dark-theme .player-order-item:hover {
background: rgba(105, 97, 255, 0.2);
}
.settings-modal.dark-theme .theme-option {
background: rgba(50, 50, 60, 0.8);
border: 1px solid rgba(255, 255, 255, 0.1);
color: #fff;
}
.settings-modal.dark-theme .theme-option:hover {
border-color: #6961ff;
}
.settings-modal.dark-theme .theme-option.selected {
background: rgba(105, 97, 255, 0.2);
border-color: #6961ff;
}
.settings-modal.dark-theme .settings-info {
background: rgba(105, 97, 255, 0.2);
border-left: 4px solid #6961ff;
color: #ddd;
}
.settings-modal.dark-theme .settings-body::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
}
.settings-modal.dark-theme .settings-body::-webkit-scrollbar-thumb {
background: rgba(105, 97, 255, 0.4);
}
.settings-modal.dark-theme .settings-body::-webkit-scrollbar-thumb:hover {
background: rgba(105, 97, 255, 0.6);
}
/* Темная тема для кнопок */
.kodik-container.dark-theme .add-to-list-btn,
.kodik-container.dark-theme .settings-btn,
.kodik-container.dark-theme .theater-btn,
.kodik-container.dark-theme .status-btn {
background: rgba(40, 40, 50, 0.8);
border: 1px solid rgba(255, 255, 255, 0.1);
color: #fff;
}
.kodik-container.dark-theme .add-to-list-btn:hover,
.kodik-container.dark-theme .settings-btn:hover,
.kodik-container.dark-theme .theater-btn:hover,
.kodik-container.dark-theme .status-btn:hover {
background: rgba(50, 50, 60, 0.9);
border-color: #6961ff;
}
/* Светлая тема для кнопок */
.kodik-container.light-theme .add-to-list-btn,
.kodik-container.light-theme .settings-btn,
.kodik-container.light-theme .theater-btn,
.kodik-container.light-theme .status-btn {
background: rgba(255, 255, 255, 0.9);
border: 1px solid rgba(0, 0, 0, 0.1);
color: #000;
}
.kodik-container.light-theme .add-to-list-btn:hover,
.kodik-container.light-theme .settings-btn:hover,
.kodik-container.light-theme .theater-btn:hover,
.kodik-container.light-theme .status-btn:hover {
background: #fff;
border-color: #6961ff;
}
/* Стили для режима кинотеатра */
.kodik-container.theater-mode {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
max-width: none;
z-index: 9999;
margin: 0;
border-radius: 0;
background: rgba(0, 0, 0, 0.95);
}
.kodik-container.theater-mode .kodik-header,
.kodik-container.theater-mode .player-buttons-container,
.kodik-container.theater-mode .shikip-changelog {
display: none;
}
.kodik-container.theater-mode .player-wrapper {
height: 100vh;
padding-bottom: 0;
width: 100%;
}
.kodik-container.theater-mode .player-wrapper iframe {
width: 100%;
height: 100%;
}
.close-theater-btn {
position: fixed;
top: 15px;
right: 15px;
z-index: 10000;
background: rgba(0, 0, 0, 0.7);
color: white;
border: none;
border-radius: 50%;
width: 40px;
height: 40px;
font-size: 24px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
.close-theater-btn:hover {
background: rgba(105, 97, 255, 0.8);
transform: scale(1.1);
}
/* Класс для запрета скролла страницы в режиме кинотеатра */
.theater-mode-active {
overflow: hidden !important;
}
/* Класс для запрета скролла страницы при открытых настройках */
.settings-modal-open {
overflow: hidden !important;
}
/* Стили для выпадающего меню статуса */
.status-dropdown {
position: fixed;
background: rgba(255, 255, 255, 0.95);
border: 1px solid rgba(255, 255, 255, 0.5);
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
min-width: 160px;
z-index: 10000;
opacity: 0;
visibility: hidden;
transform: translateY(-10px);
transition: all 0.3s ease;
}
.status-dropdown.active {
opacity: 1;
visibility: visible;
transform: translateY(0);
}
.status-dropdown-item {
padding: 10px 15px;
cursor: pointer;
transition: background 0.2s ease;
display: flex;
align-items: center;
justify-content: center;
color: #333;
font-size: 14px;
text-align: center;
}
.status-dropdown-item:first-child {
border-radius: 8px 8px 0 0;
}
.status-dropdown-item:last-child {
border-radius: 0 0 8px 8px;
}
.status-dropdown-item:hover {
background: rgba(105, 97, 255, 0.1);
}
/* Темная тема для выпадающего меню статуса */
.status-dropdown.dark-theme {
background: rgba(40, 40, 50, 0.95) !important;
border: 1px solid rgba(255, 255, 255, 0.1) !important;
}
.status-dropdown.dark-theme .status-dropdown-item {
color: #fff !important;
}
.status-dropdown.dark-theme .status-dropdown-item:hover {
background: rgba(105, 97, 255, 0.2) !important;
}
@media (max-width: 600px) {
.add-to-list-btn, .settings-btn, .theater-btn, .status-btn {
width: 40px;
height: 40px;
}
.add-to-list-btn svg,
.settings-btn svg,
.theater-btn svg,
.status-btn svg {
width: 20px;
height: 20px;
}
.settings-modal-content {
width: 95%;
margin: 10% auto;
padding: 20px;
max-height: 90vh;
}
.settings-header h2 {
font-size: 20px;
}
.settings-option {
flex-direction: column;
align-items: flex-start;
gap: 10px;
}
.settings-option select {
width: 100%;
}
}
`;
document.head.appendChild(style);
}
const playerContainer = document.createElement("div");
playerContainer.classList.add("kodik-container");
// Применяем сохраненную тему
playerContainer.classList.add(
playerSettings.theme === "light" ? "light-theme" : "dark-theme"
);
const id = getShikimoriID();
if (!id) return;
playerContainer.innerHTML = `
<div class="kodik-header">
<span>ОНЛАЙН ПРОСМОТР</span>
<span style="color: #333;">Загрузка...</span>
</div>
<div class="player-wrapper">
<div class="loader">
<div class="loader-spinner"></div>
<div>Загрузка...</div>
</div>
</div>
`;
relatedBlock.parentNode.insertBefore(playerContainer, relatedBlock);
const checkPromise = checkPlayerAvailability(id);
checkPromise
.then(() => {
if (!Object.values(playerAvailability).some(Boolean)) {
playerContainer.innerHTML = `
<div class="kodik-header">
<span>ОНЛАЙН ПРОСМОТР</span>
<span style="color: #ff6b6b;">Нет доступных плееров</span>
</div>
<div class="player-wrapper">
<div class="error-message">К сожалению, ни один из плееров недоступен для этого аниме</div>
</div>
`;
return;
}
const headerElement = document.createElement("div");
headerElement.className = "kodik-header";
headerElement.innerHTML = `
<span>ОНЛАЙН ПРОСМОТР</span>
<div style="display: flex; gap: 8px; align-items: center;">
${playerSelectorHTML(currentPlayer)}
</div>
`;
const playerWrapper = document.createElement("div");
playerWrapper.className = "player-wrapper";
playerWrapper.innerHTML = `
<div class="loader">
<div class="loader-spinner"></div>
<div>Загрузка...</div>
</div>
`;
const buttonsContainer = document.createElement("div");
buttonsContainer.className = "player-buttons-container";
// Создаем новую кнопку для изменения статуса (теперь первая)
const statusBtn = document.createElement("button");
statusBtn.className = "status-btn";
statusBtn.innerHTML = `<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="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path></svg>`;
// Создаем кнопку кинотеатра
const theaterBtn = document.createElement("button");
theaterBtn.className = "theater-btn";
theaterBtn.innerHTML = `<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>`;
// Создаем кнопки с SVG иконками
const addToListBtn = document.createElement("button");
addToListBtn.className = "add-to-list-btn";
addToListBtn.innerHTML = `<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>`;
const settingsBtn = document.createElement("button");
settingsBtn.className = "settings-btn";
settingsBtn.innerHTML = `<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"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06-.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06-.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path></svg>`;
// Изменяем порядок добавления кнопок (статус-кнопка теперь первая)
buttonsContainer.appendChild(statusBtn);
buttonsContainer.appendChild(theaterBtn);
buttonsContainer.appendChild(addToListBtn);
buttonsContainer.appendChild(settingsBtn);
const changelogBlock = document.createElement("div");
changelogBlock.className = "shikip-changelog";
changelogBlock.innerHTML = `
<div class="changelog-header">
<span>
<span class="toggle-icon">▼</span>
История изменений
</span>
<a href="https://github.com/Onzis/ShikiPlayer" target="_blank" class="github-link">
GitHub
</a>
</div>
<div class="changelog-content">
<ul>
<li><strong>v1.50</strong> - Временно убраны плеера Turbo&Lumex из за проблем с API</li>
<li><strong>v1.49</strong> - Обновлен токен для Кодик</li>
<li><strong>v1.47</strong> - Добавлена новая кнопка для изменения статуса аниме</li>
<li><strong>v1.46</strong> - Исправлены ошибки с режимом кинотеатра и прокрутки настроек</li>
<li><strong>v1.44</strong> - Исправлена анимация загрузчика (круг теперь крутится)</li>
<li><strong>v1.43</strong> - Исправлена подсветка блока "История изменений" в темной теме</li>
<li><strong>v1.42</strong> - Исправлена прокрутка страницы в настройках плеера</li>
<li><strong>v1.41</strong> - Исправлено отображение порядка плееров в выпадающем списке</li>
<li><strong>v1.40</strong> - Исправлена работа порядка плееров в настройках</li>
</ul>
</div>
`;
playerContainer.innerHTML = "";
playerContainer.appendChild(headerElement);
playerContainer.appendChild(playerWrapper);
playerContainer.appendChild(buttonsContainer);
playerContainer.appendChild(changelogBlock);
const header = changelogBlock.querySelector(".changelog-header");
header.addEventListener("click", () => {
changelogBlock.classList.toggle("expanded");
});
if (observer) observer.disconnect();
const startEpisode = 1;
const playerDropdown =
playerContainer.querySelector("#player-dropdown");
if (playerDropdown) {
playerDropdown.addEventListener("change", (e) => {
if (e.target.value) {
manualSwitchPlayer(
e.target.value,
id,
playerContainer,
startEpisode
);
}
});
}
if (addToListBtn) {
addToListBtn.addEventListener("click", () => {
const incrementButton = document.querySelector(
".item-add.increment"
);
if (incrementButton) {
incrementButton.click();
showNotification("Добавлена серия в просмотрено", "success");
// Обновляем подсказку после добавления серии
setTimeout(() => {
const watchedInfo = getWatchedEpisodesInfo();
if (watchedInfo) {
addToListTooltip.textContent = `Добавить серию в просмотрено\n${watchedInfo}`;
}
}, 500); // Небольшая задержка для обновления DOM
} else {
showNotification(
"Не найдена кнопка добавления серии в просмотрено",
"warning"
);
}
});
// Создаем всплывающую подсказку для кнопки добавления в список
const addToListTooltip = document.createElement("div");
addToListTooltip.className = "tooltip";
// Получаем текущую информацию о просмотренных сериях
const watchedInfo = getWatchedEpisodesInfo();
addToListTooltip.textContent = watchedInfo
? `Добавить серию в просмотрено\n${watchedInfo}`
: "Добавить серию в просмотрено";
document.body.appendChild(addToListTooltip);
addToListBtn.addEventListener("mouseenter", () => {
// Обновляем информацию при каждом наведении на случай изменений
const watchedInfo = getWatchedEpisodesInfo();
addToListTooltip.textContent = watchedInfo
? `Добавить серию в просмотрено\n${watchedInfo}`
: "Добавить серию в просмотрено";
const rect = addToListBtn.getBoundingClientRect();
addToListTooltip.style.left = `${rect.left + rect.width / 2}px`;
addToListTooltip.style.top = `${rect.bottom + 5}px`;
addToListTooltip.classList.add("show");
});
addToListBtn.addEventListener("mouseleave", () => {
addToListTooltip.classList.remove("show");
});
}
// Добавляем обработчики для кнопки изменения статуса
if (statusBtn) {
// Создаем выпадающее меню для статуса и добавляем его в body, а не в кнопку
const statusDropdown = document.createElement("div");
statusDropdown.className = "status-dropdown";
// Применяем класс темы к выпадающему меню
statusDropdown.classList.add(
playerSettings.theme === "light" ? "light-theme" : "dark-theme"
);
statusDropdown.innerHTML = `
<div class="status-dropdown-item" data-status="watching">Смотрю</div>
<div class="status-dropdown-item" data-status="planned">Запланировано</div>
<div class="status-dropdown-item" data-status="dropped">Брошено</div>
<div class="status-dropdown-item" data-status="remove">Удалить из списка</div>
`;
document.body.appendChild(statusDropdown);
// Функция для позиционирования выпадающего меню
function positionStatusDropdown() {
const rect = statusBtn.getBoundingClientRect();
// Центрируем меню по горизонтали относительно кнопки
const dropdownWidth = statusDropdown.offsetWidth;
const buttonCenter = rect.left + rect.width / 2;
statusDropdown.style.left = `${buttonCenter - dropdownWidth / 2}px`;
statusDropdown.style.top = `${rect.bottom + 5}px`;
}
// Обработчики для кнопки статуса
statusBtn.addEventListener("mouseenter", () => {
positionStatusDropdown();
statusDropdown.classList.add("active");
});
statusBtn.addEventListener("mouseleave", (e) => {
// Проверяем, не уходит ли мышь на выпадающее меню
setTimeout(() => {
if (!statusDropdown.matches(":hover")) {
statusDropdown.classList.remove("active");
}
}, 100);
});
// Обработчики для выпадающего меню
statusDropdown.addEventListener("mouseenter", () => {
statusDropdown.classList.add("active");
});
statusDropdown.addEventListener("mouseleave", () => {
statusDropdown.classList.remove("active");
});
// Обработчики для элементов выпадающего меню
const statusItems = statusDropdown.querySelectorAll(
".status-dropdown-item"
);
statusItems.forEach((item) => {
item.addEventListener("click", (e) => {
e.stopPropagation();
const status = item.dataset.status;
// Находим соответствующий элемент на странице и кликаем на него
let selector;
switch (status) {
case "watching":
selector = 'span.status-name[data-text="Смотрю"]';
break;
case "planned":
selector = 'span.status-name[data-text="Запланировано"]';
break;
case "dropped":
selector = 'span.status-name[data-text="Брошено"]';
break;
case "remove":
selector = 'span.status-name[data-text="Удалить из списка"]';
break;
}
if (selector) {
const statusElement = document.querySelector(selector);
if (statusElement) {
statusElement.click();
showNotification(
`Статус изменен на "${item.textContent}"`,
"success"
);
} else {
showNotification(
"Не удалось найти элемент для изменения статуса",
"error"
);
}
}
// Скрываем выпадающее меню после выбора
statusDropdown.classList.remove("active");
});
});
// Обновляем позицию при прокрутке страницы
window.addEventListener("scroll", () => {
if (statusDropdown.classList.contains("active")) {
positionStatusDropdown();
}
});
// Обновляем позицию при изменении размера окна
window.addEventListener("resize", () => {
if (statusDropdown.classList.contains("active")) {
positionStatusDropdown();
}
});
// Функция для обновления темы выпадающего меню
window.updateStatusDropdownTheme = function () {
// Удаляем оба класса темы
statusDropdown.classList.remove("light-theme");
statusDropdown.classList.remove("dark-theme");
// Добавляем класс выбранной темы
if (playerSettings.theme === "light") {
statusDropdown.classList.add("light-theme");
} else {
statusDropdown.classList.add("dark-theme");
}
// Обновляем позицию
positionStatusDropdown();
};
}
if (settingsBtn) {
// Создаем модальное окно настроек
let settingsModal = document.getElementById("player-settings-modal");
if (!settingsModal) {
settingsModal = document.createElement("div");
settingsModal.id = "player-settings-modal";
settingsModal.className = "settings-modal";
// Применяем тему к модальному окну
settingsModal.classList.add(
playerSettings.theme === "light" ? "light-theme" : "dark-theme"
);
settingsModal.innerHTML = `
<div class="settings-modal-content">
<div class="settings-header">
<h2>Настройки плеера</h2>
<span class="close-settings">×</span>
</div>
<div class="settings-body">
<div class="settings-section">
<h3>Плееры</h3>
<div class="settings-option">
<label>
Плеер по умолчанию:
<select id="default-player">
<option value="alloha" ${
playerSettings.defaultPlayer === "alloha"
? "selected"
: ""
}>Alloha</option>
<option value="kodik" ${
playerSettings.defaultPlayer === "kodik"
? "selected"
: ""
}>Kodik</option>
</select>
</label>
</div>
<div class="settings-option">
<label>
Порядок плееров:
</label>
</div>
<div class="player-order-container" id="player-order-container">
${playerSettings.playerOrder
.map(
(player) => `
<div class="player-order-item" draggable="true" data-player="${player}">
<span class="drag-handle">☰</span>
<span class="player-name">${
player.charAt(0).toUpperCase() + player.slice(1)
}</span>
</div>
`
)
.join("")}
</div>
</div>
<div class="settings-section">
<h3>Воспроизведение</h3>
<div class="settings-option">
<label>
<input type="checkbox" id="remember-quality" ${
playerSettings.rememberQuality ? "checked" : ""
}>
Запоминать качество видео
</label>
</div>
<div class="settings-option">
<label>
Качество по умолчанию:
<select id="default-quality">
<option value="auto" ${
playerSettings.defaultQuality === "auto"
? "selected"
: ""
}>Авто</option>
<option value="1080" ${
playerSettings.defaultQuality === "1080"
? "selected"
: ""
}>1080p</option>
<option value="720" ${
playerSettings.defaultQuality === "720"
? "selected"
: ""
}>720p</option>
<option value="480" ${
playerSettings.defaultQuality === "480"
? "selected"
: ""
}>480p</option>
</select>
</label>
</div>
</div>
<div class="settings-section">
<h3>Внешний вид</h3>
<div class="settings-option">
<label>
Тема плеера:
</label>
</div>
<div class="theme-selector">
<div class="theme-option ${
playerSettings.theme === "dark" ? "selected" : ""
}" data-theme="dark">
<div class="theme-icon">🌙</div>
<div>Темная</div>
</div>
<div class="theme-option ${
playerSettings.theme === "light" ? "selected" : ""
}" data-theme="light">
<div class="theme-icon">☀️</div>
<div>Светлая</div>
</div>
</div>
</div>
<div class="settings-section">
<h3>Уведомления</h3>
<div class="settings-option">
<label>
<input type="checkbox" id="disable-notifications" ${
playerSettings.disableNotifications ? "checked" : ""
}>
Отключить уведомления
</label>
</div>
</div>
<div class="settings-section">
<h3>Диагностика</h3>
<div class="settings-option">
<label>
<input type="checkbox" id="debug-mode" ${
playerSettings.debugMode ? "checked" : ""
}>
Режим отладки (вывод в консоль)
</label>
</div>
</div>
<div class="settings-info">
<p>Примечание: Некоторые настройки могут не поддерживаться всеми плеерами.</p>
</div>
</div>
<button class="settings-save-btn">Сохранить настройки</button>
</div>
`;
document.body.appendChild(settingsModal);
// Настройка drag and drop для порядка плееров
const playerOrderContainer = settingsModal.querySelector(
"#player-order-container"
);
setupDragAndDrop(playerOrderContainer);
// Обработчики событий для выбора темы
const themeOptions =
settingsModal.querySelectorAll(".theme-option");
themeOptions.forEach((option) => {
option.addEventListener("click", () => {
themeOptions.forEach((opt) => opt.classList.remove("selected"));
option.classList.add("selected");
});
});
// Обработчики событий
const closeBtn = settingsModal.querySelector(".close-settings");
closeBtn.addEventListener("click", () => {
settingsModal.style.display = "none";
// Разблокируем прокрутку страницы
document.body.classList.remove("settings-modal-open");
});
window.addEventListener("click", (e) => {
if (e.target === settingsModal) {
settingsModal.style.display = "none";
// Разблокируем прокрутку страницы
document.body.classList.remove("settings-modal-open");
}
});
const saveBtn = settingsModal.querySelector(".settings-save-btn");
saveBtn.addEventListener("click", () => {
// Сохранение настроек
playerSettings.rememberQuality =
document.getElementById("remember-quality").checked;
playerSettings.defaultQuality =
document.getElementById("default-quality").value;
playerSettings.defaultPlayer =
document.getElementById("default-player").value;
playerSettings.disableNotifications = document.getElementById(
"disable-notifications"
).checked;
playerSettings.debugMode =
document.getElementById("debug-mode").checked;
// Сохранение темы
const selectedTheme = settingsModal.querySelector(
".theme-option.selected"
);
if (selectedTheme) {
playerSettings.theme = selectedTheme.dataset.theme;
}
// Сохранение порядка плееров
const playerOrderItems =
document.querySelectorAll(".player-order-item");
playerSettings.playerOrder = Array.from(playerOrderItems).map(
(item) => item.dataset.player
);
debugLog(
"Сохраняемый порядок плееров:",
playerSettings.playerOrder
);
// Сохранение в localStorage
localStorage.setItem(
"shiki-remember-quality",
playerSettings.rememberQuality
);
localStorage.setItem(
"shiki-default-quality",
playerSettings.defaultQuality
);
localStorage.setItem(
"shiki-default-player",
playerSettings.defaultPlayer
);
localStorage.setItem(
"shiki-player-order",
JSON.stringify(playerSettings.playerOrder)
);
localStorage.setItem(
"shiki-disable-notifications",
playerSettings.disableNotifications
);
localStorage.setItem("shiki-theme", playerSettings.theme);
localStorage.setItem(
"shiki-debug-mode",
playerSettings.debugMode
);
// Применение темы
applyTheme(playerContainer, playerSettings.theme);
// Применение темы к модальному окну
applyModalTheme(settingsModal, playerSettings.theme);
// Применение темы к выпадающему меню статуса
if (window.updateStatusDropdownTheme) {
window.updateStatusDropdownTheme();
}
// Обновляем выпадающий список плееров после сохранения настроек
updatePlayerDropdown(playerContainer, currentPlayer);
showNotification("Настройки сохранены", "success");
settingsModal.style.display = "none";
// Разблокируем прокрутку страницы
document.body.classList.remove("settings-modal-open");
});
// Добавляем обработчик клавиши Escape для закрытия модального окна
document.addEventListener("keydown", function handleEscKey(e) {
if (
e.key === "Escape" &&
settingsModal.style.display === "block"
) {
settingsModal.style.display = "none";
// Разблокируем прокрутку страницы
document.body.classList.remove("settings-modal-open");
}
});
}
settingsBtn.addEventListener("click", () => {
// Обновляем порядок плееров в модальном окне перед открытием
updatePlayerOrderInModal(settingsModal);
// Обновляем тему модального окна перед открытием
applyModalTheme(settingsModal, playerSettings.theme);
settingsModal.style.display = "block";
// Блокируем прокрутку страницы
document.body.classList.add("settings-modal-open");
// Сбрасываем скролл модального окна в начало
const settingsBody = settingsModal.querySelector(".settings-body");
if (settingsBody) {
settingsBody.scrollTop = 0;
}
});
// Создаем всплывающую подсказку для кнопки настроек
const settingsTooltip = document.createElement("div");
settingsTooltip.className = "tooltip";
settingsTooltip.textContent = "Настройки плеера";
document.body.appendChild(settingsTooltip);
settingsBtn.addEventListener("mouseenter", () => {
const rect = settingsBtn.getBoundingClientRect();
settingsTooltip.style.left = `${rect.left + rect.width / 2}px`;
settingsTooltip.style.top = `${rect.bottom + 5}px`;
settingsTooltip.classList.add("show");
});
settingsBtn.addEventListener("mouseleave", () => {
settingsTooltip.classList.remove("show");
});
}
// Добавляем обработчик для кнопки кинотеатра
if (theaterBtn) {
theaterBtn.addEventListener("click", () => {
playerContainer.classList.toggle("theater-mode");
if (playerContainer.classList.contains("theater-mode")) {
// Запрещаем скролл страницы
document.body.classList.add("theater-mode-active");
// Создаем кнопку закрытия режима кинотеатра
const closeTheaterBtn = document.createElement("button");
closeTheaterBtn.className = "close-theater-btn";
closeTheaterBtn.innerHTML = "×";
// Функция для выхода из режима кинотеатра
const exitTheaterMode = () => {
playerContainer.classList.remove("theater-mode");
closeTheaterBtn.remove();
// Разрешаем скролл страницы
document.body.classList.remove("theater-mode-active");
// Удаляем обработчик Escape
document.removeEventListener("keydown", handleEscape);
};
closeTheaterBtn.addEventListener("click", exitTheaterMode);
document.body.appendChild(closeTheaterBtn);
// Добавляем обработчик клавиши Escape для выхода из режима
const handleEscape = (e) => {
if (
e.key === "Escape" &&
playerContainer.classList.contains("theater-mode")
) {
exitTheaterMode();
}
};
document.addEventListener("keydown", handleEscape);
// Убираем уведомление о входе в режим кинотеатра
} else {
// Удаляем кнопку закрытия, если она существует
const closeBtn = document.querySelector(".close-theater-btn");
if (closeBtn) closeBtn.remove();
// Разрешаем скролл страницы
document.body.classList.remove("theater-mode-active");
}
});
// Создаем всплывающую подсказку для кнопки кинотеатра
const theaterTooltip = document.createElement("div");
theaterTooltip.className = "tooltip";
theaterTooltip.textContent = "Режим кинотеатра";
document.body.appendChild(theaterTooltip);
theaterBtn.addEventListener("mouseenter", () => {
const rect = theaterBtn.getBoundingClientRect();
theaterTooltip.style.left = `${rect.left + rect.width / 2}px`;
theaterTooltip.style.top = `${rect.bottom + 5}px`;
theaterTooltip.classList.add("show");
});
theaterBtn.addEventListener("mouseleave", () => {
theaterTooltip.classList.remove("show");
});
}
setupLazyLoading(playerContainer, () =>
autoPlayerChain(id, playerContainer, startEpisode)
);
})
.catch((error) => {
console.error("Ошибка при проверке доступности плееров:", error);
playerContainer.innerHTML = `
<div class="kodik-header">
<span>ОНЛАЙН ПРОСМОТР</span>
<span style="color: #ff6b6b;">Ошибка загрузки</span>
</div>
<div class="player-wrapper">
<div class="error-message">Произошла ошибка при загрузке плееров</div>
</div>
`;
});
}
// Функция для применения темы к плееру
function applyTheme(playerContainer, theme) {
// Удаляем оба класса темы
playerContainer.classList.remove("light-theme");
playerContainer.classList.remove("dark-theme");
// Добавляем класс выбранной темы
if (theme === "light") {
playerContainer.classList.add("light-theme");
} else {
playerContainer.classList.add("dark-theme");
}
}
// Функция для применения темы к модальному окну настроек
function applyModalTheme(settingsModal, theme) {
// Удаляем оба класса темы
settingsModal.classList.remove("light-theme");
settingsModal.classList.remove("dark-theme");
// Добавляем класс выбранной темы
if (theme === "light") {
settingsModal.classList.add("light-theme");
} else {
settingsModal.classList.add("dark-theme");
}
}
// НОВАЯ ФУНКЦИЯ: Обновление выпадающего списка плееров
function updatePlayerDropdown(playerContainer, current) {
const playerDropdown = playerContainer.querySelector("#player-dropdown");
if (playerDropdown) {
// Сохраняем текущее значение
const currentValue = playerDropdown.value;
// Обновляем HTML выпадающего списка
let optionsHTML = "";
// Перебираем плееры в порядке, установленном в настройках
for (const player of playerSettings.playerOrder) {
if (playerAvailability[player]) {
const isSelected = player === current ? "selected" : "";
const playerName = player.charAt(0).toUpperCase() + player.slice(1);
optionsHTML += `<option value="${player}" ${isSelected}>${playerName}</option>`;
}
}
if (optionsHTML === "") {
optionsHTML =
'<option value="" disabled>Нет доступных плееров</option>';
}
playerDropdown.innerHTML = optionsHTML;
// Восстанавливаем выбранное значение, если оно все еще доступно
if (playerAvailability[currentValue]) {
playerDropdown.value = currentValue;
}
}
}
async function autoPlayerChain(id, playerContainer, episode) {
// Используем порядок из настроек, но фильтруем только доступные плееры
const playerOrder = playerSettings.playerOrder.filter(
(p) => playerAvailability[p]
);
debugLog("Доступные плееры в порядке приоритета:", playerOrder);
if (playerOrder.length === 0) {
showNotification("Нет доступных плееров для этого аниме", "error");
return;
}
// Если есть плеер по умолчанию и он доступен, начинаем с него
let startIndex = 0;
if (playerAvailability[playerSettings.defaultPlayer]) {
startIndex = playerOrder.indexOf(playerSettings.defaultPlayer);
if (startIndex === -1) startIndex = 0;
}
// Создаем новый порядок, начиная с плеера по умолчанию
const orderedPlayers = [
...playerOrder.slice(startIndex),
...playerOrder.slice(0, startIndex),
];
debugLog("Итоговый порядок воспроизведения:", orderedPlayers);
let lastError = null;
for (const playerType of orderedPlayers) {
try {
currentPlayer = playerType;
playerContainer.querySelector("#player-dropdown").value = playerType;
await showPlayer(playerType, id, playerContainer, episode);
return;
} catch (error) {
lastError = error;
console.warn(`Плеер ${playerType} недоступен:`, error);
showNotification(
`${playerType} недоступен, пробую следующий...`,
"warning"
);
}
}
if (lastError) {
showNotification(`Все плееры недоступны: ${lastError.message}`, "error");
}
}
async function manualSwitchPlayer(playerType, id, playerContainer, episode) {
if (!playerAvailability[playerType]) {
showNotification(`Плеер ${playerType} недоступен`, "error");
return;
}
currentPlayer = playerType;
await showPlayer(playerType, id, playerContainer, episode);
}
async function showPlayer(playerType, id, playerContainer, episode) {
const playerWrapper = playerContainer.querySelector(".player-wrapper");
playerWrapper.innerHTML = `
<div class="loader">
<div class="loader-spinner"></div>
<div>Загрузка...</div>
</div>
`;
try {
if (playerType === "alloha" && !checkVideoCodecSupport()) {
showNotification(
"Ваш браузер не поддерживает необходимые кодеки для Alloha плеера.",
"error"
);
throw new Error(
"Ваш браузер не поддерживает необходимые кодеки для Alloha"
);
}
const iframe = document.createElement("iframe");
iframe.allowFullscreen = true;
iframe.setAttribute("allow", "autoplay *; fullscreen *; encrypted-media");
iframe.setAttribute("playsinline", "true");
iframe.setAttribute("loading", "lazy");
if (playerType === "kodik") {
iframe.src = `https://kodik.cc/find-player?shikimoriID=${id}&episode=${episode}`;
} else if (playerType === "alloha") {
try {
const iframeUrl = await loadAllohaPlayer(id, episode);
iframe.src = iframeUrl;
iframe.onerror = () => {
throw new Error("Alloha 404");
};
// Добавляем обработчик для проверки ошибки "сезон недоступен"
iframe.addEventListener("load", function () {
try {
const iframeContent =
iframe.contentDocument || iframe.contentWindow.document;
const errorElements = iframeContent.querySelectorAll(
".error, .warning, .not-found"
);
for (const el of errorElements) {
if (
el.textContent.includes("сезон недоступен") ||
el.textContent.includes("сезон не найден") ||
el.textContent.includes("season not available")
) {
showNotification(
"Alloha: запрашиваемый сезон недоступен, переключаюсь на другой плеер...",
"warning"
);
throw new Error("Сезон недоступен в Alloha");
}
}
} catch (e) {
// Игнорируем ошибки доступа к iframe (из-за CORS)
console.warn("Не удалось проверить содержимое iframe:", e);
}
});
} catch (error) {
throw error;
}
} else {
showNotification("Неизвестный тип плеера.", "error");
throw new Error("Неизвестный тип плеера");
}
playerWrapper.innerHTML = "";
playerWrapper.appendChild(iframe);
setTimeout(() => {
if (
!iframe.contentWindow ||
(iframe.contentDocument &&
iframe.contentDocument.body.innerHTML.trim() === "")
) {
if (playerType === "alloha") throw new Error("Alloha 404");
}
}, 2000);
} catch (error) {
playerWrapper.innerHTML = `<div class="error-message">Ошибка загрузки плеера ${playerType}: ${error.message}. Попробуйте другой плеер.</div>`;
showNotification(
`Не работает плеер ${playerType}: ${error.message}.`,
"error"
);
throw error;
}
}
function gmGetWithTimeout(url, options = {}) {
return new Promise((resolve, reject) => {
GM.xmlHttpRequest({
method: "GET",
url,
headers: { "Cache-Control": "no-cache", ...options.headers },
onload: ({ status, responseText }) => {
status >= 200 && status < 300
? resolve(responseText)
: reject(new Error(`HTTP ${status}`));
},
onerror: (error) => {
reject(error);
},
});
});
}
function getCachedData(key) {
const cached = localStorage.getItem(key);
if (cached) {
const { data } = JSON.parse(cached);
return data;
}
return null;
}
function setCachedData(key, data) {
localStorage.setItem(key, JSON.stringify({ data }));
}
async function loadAllohaPlayer(id, episode) {
const season = getCurrentSeason();
const cacheKey = `alloha_${id}_s${season}`;
let iframeUrl = getCachedData(cacheKey);
if (iframeUrl) {
return `${iframeUrl}&episode=${episode}&season=${season}`;
}
const kodikCacheKey = `kodik_${id}`;
let kodikData = getCachedData(kodikCacheKey);
if (!kodikData) {
try {
const kodikResponse = await gmGetWithTimeout(
`https://kodikapi.com/search?token=${KodikToken}&shikimori_id=${id}`
);
kodikData = JSON.parse(kodikResponse);
setCachedData(kodikCacheKey, kodikData);
} catch (error) {
debugLog("Ошибка загрузки данных Kodik API для Alloha:", error);
showNotification(
"Ошибка загрузки данных Kodik API для Alloha.",
"error"
);
throw new Error("Ошибка загрузки данных Kodik API");
}
}
const results = kodikData.results;
if (!results?.length) {
debugLog("Нет результатов от Kodik API для Alloha");
showNotification("Нет результатов от Kodik API для Alloha.", "error");
throw new Error("Нет результатов от Kodik API");
}
const { kinopoisk_id, imdb_id } = results[0];
const allohaUrl = kinopoisk_id
? `https://api.alloha.tv?token=${AllohaToken}&kp=${kinopoisk_id}`
: `https://api.alloha.tv?token=${AllohaToken}&imdb=${imdb_id}`;
if (!allohaUrl) {
debugLog("Kinopoisk ID или IMDB ID не найдены для Alloha");
showNotification(
"Kinopoisk ID или IMDB ID не найдены для Alloha.",
"error"
);
throw new Error("Kinopoisk ID или IMDB ID не найдены");
}
async function tryFetchAlloha(retries = 3, delayMs = 1000) {
for (let i = 0; i < retries; i++) {
try {
const allohaResponse = await gmGetWithTimeout(allohaUrl);
const allohaData = JSON.parse(allohaResponse);
debugLog("Ответ Alloha API:", allohaData);
if (allohaData.status === "success" && allohaData.data?.iframe) {
return allohaData.data.iframe;
} else {
throw new Error(
"Ошибка Alloha API: " +
(allohaData.error_info || "Неизвестная ошибка")
);
}
} catch (error) {
debugLog(`Попытка ${i + 1} загрузки Alloha не удалась:`, error);
if (i === retries - 1) {
showNotification(
"Alloha API недоступен. Попробуйте позже.",
"error"
);
throw error;
}
await new Promise((res) => setTimeout(res, delayMs));
}
}
}
try {
const allohaIframeUrl = await tryFetchAlloha();
setCachedData(cacheKey, allohaIframeUrl);
return `${allohaIframeUrl}&episode=${episode}&season=${season}`;
} catch (error) {
localStorage.removeItem(cacheKey);
debugLog("Ошибка загрузки Alloha:", error);
showNotification("Ошибка загрузки Alloha: " + error.message, "error");
throw new Error("Ошибка загрузки Alloha: " + error.message);
}
}
function checkVideoCodecSupport() {
const video = document.createElement("video");
return (
video.canPlayType('video/mp4; codecs="avc1.42E01E, mp4a.40.2"') ===
"probably" ||
video.canPlayType('video/webm; codecs="vp9, vorbis"') === "probably"
);
}
function setupLazyLoading(container, callback) {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting) {
callback();
observer.disconnect();
}
},
{ rootMargin: "50px" }
);
observer.observe(container);
}
function setupDOMObserver() {
if (observer) observer.disconnect();
observer = new MutationObserver(() => {
if (document.querySelector(".kodik-container")) return;
if (/^\/animes\/[^/]+/.test(location.pathname)) {
insertPlayerContainer();
}
});
observer.observe(document.body, { childList: true, subtree: true });
}
function watchURLChanges() {
let lastPath = location.pathname;
const checkUrlChange = () => {
if (location.pathname !== lastPath) {
lastPath = location.pathname;
document.querySelector(".kodik-container")?.remove();
insertPlayerContainer();
}
};
setInterval(checkUrlChange, 300);
const pushState = history.pushState;
history.pushState = function () {
pushState.apply(this, arguments);
checkUrlChange();
};
const replaceState = history.replaceState;
history.replaceState = function () {
replaceState.apply(this, arguments);
checkUrlChange();
};
window.addEventListener("popstate", checkUrlChange);
}
window.manualInsertPlayer = function () {
document.querySelector(".kodik-container")?.remove();
insertPlayerContainer();
};
document.addEventListener("turbolinks:load", () => {
document.querySelector(".kodik-container")?.remove();
insertPlayerContainer();
});
setupDOMObserver();
watchURLChanges();
insertPlayerContainer();
})();