Crunchyroll Enhancer

Enhancements for Crunchyroll

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

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

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

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

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

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

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

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

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

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

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

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

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

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

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         Crunchyroll Enhancer
// @namespace    https://github.com/Gakai/Crunchyroll-Enhancer-UserScript
// @version      0.2.0
// @description  Enhancements for Crunchyroll
// @author       Gakai
// @icon         https://www.google.com/s2/favicons?sz=64&domain=crunchyroll.com
// @match        *://*.crunchyroll.com/*
// @grant        none
// @run-at       document-idle
// ==/UserScript==
(function () {
    'use strict';

    const CARD_SELECTOR = ".erc-my-lists-item";
    const CARD_SUBTITLE_SELECTOR = ".watchlist-card-subtitle--IROsU";
    // cSpell:disable
    const READY = {
        styles: {
            subtitle: {
                color: "red",
            },
        },
        texts: {
            en: ["Up Next:", "Start Watching"],
            de: ["Als Nächstes:", "Jetzt anschauen"],
            fr: ["À suivre:", "Lecture"],
            it: ["Successivo:", "Inizia a guardare"],
            es: ["Siguiente:", "Comenzar a ver"],
        },
    };
    const CONTINUE = {
        styles: {
            subtitle: {
                color: "orange",
            },
        },
        texts: {
            en: ["Continue:"],
            de: ["Fortsetzen:"],
            fr: ["Reprendre:"],
            it: ["Continua:"],
            es: ["Continuar:"],
        },
    };
    const FINISHED = {
        styles: {
            card: {
                opacity: "0.3",
            },
        },
        texts: {
            en: ["Watch Again:", "Up Next: E0"],
            de: ["Erneut anschauen:", "Als Nächstes: E0"],
            fr: ["Regarder à nouveau:", "À suivre: E0"],
            it: ["Guarda di nuovo:", "Successivo: E0"],
            es: ["Ver de nuevo:", "Siguiente: E0"],
        },
    };
    // cSpell:enable

    class MutationListener {
        constructor(target, options) {
            this.recordListeners = new Map();
            this.nodeListeners = new Map();
            this.observer = new MutationObserver((mutations) => {
                for (const mutation of mutations) {
                    const { addedNodes, removedNodes, type } = mutation;
                    for (const callback of this.recordListeners.get(type) ?? []) {
                        callback(mutation);
                    }
                    for (const node of addedNodes) {
                        for (const callback of this.nodeListeners.get("added") ??
                            []) {
                            callback(node);
                        }
                    }
                    for (const node of removedNodes) {
                        for (const callback of this.nodeListeners.get("removed") ??
                            []) {
                            callback(node);
                        }
                    }
                }
            });
            this.observer.observe(target, options);
        }
        stop() {
            this.observer.disconnect();
        }
        on(event, callback) {
            switch (event) {
                case "added":
                case "removed":
                    this.nodeListeners
                        .getOrInsert(event, new Array())
                        .push(callback);
                    break;
                case "attributes":
                case "characterData":
                case "childList":
                    this.recordListeners
                        .getOrInsert(event, new Array())
                        .push(callback);
                    break;
            }
            return this;
        }
        off(event, callback) {
            const recordCallbacks = this.recordListeners.get(event);
            if (recordCallbacks) {
                recordCallbacks.splice(recordCallbacks.indexOf(callback), 1);
            }
            const nodeCallbacks = this.nodeListeners.get(event);
            if (nodeCallbacks) {
                nodeCallbacks.splice(nodeCallbacks.indexOf(callback), 1);
            }
            return this;
        }
        [Symbol.dispose]() {
            this.destroy();
        }
        destroy() {
            this.stop();
            this.recordListeners.clear();
            this.nodeListeners.clear();
        }
    }

    // Order matters: we want to style the most specific state first
    const STATE = {
        CONTINUE,
        FINISHED,
        READY,
    };
    function getLanguage() {
        const language = navigator.language.split("-")[0];
        if (language && language in READY.texts) {
            return language;
        }
        return "en";
    }
    function getState(subtitle, language) {
        for (const state of Object.values(STATE)) {
            const texts = state.texts[language];
            if (texts?.some((t) => subtitle.textContent?.startsWith(t))) {
                return state;
            }
        }
        return undefined;
    }
    function styleCard({ card, subtitle }, styles) {
        card && Object.assign(card.style, styles.card);
        subtitle && Object.assign(subtitle.style, styles.subtitle);
    }
    const language = getLanguage();
    const listener = new MutationListener(document.body, {
        childList: true,
        subtree: true,
    });
    listener.on("added", (node) => {
        if (node instanceof HTMLElement) {
            const subtitles = node.querySelectorAll(CARD_SUBTITLE_SELECTOR);
            for (const subtitle of subtitles) {
                const card = subtitle.closest(CARD_SELECTOR);
                const state = getState(subtitle, language);
                if (state &&
                    card instanceof HTMLElement &&
                    subtitle instanceof HTMLElement) {
                    styleCard({ card, subtitle }, state.styles);
                    continue;
                }
            }
        }
    });
    console.log("Crunchyroll Watchlist Enhancer loaded");

})();
//# sourceMappingURL=crunchyroll-enhancer.user.js.map