Greasy Fork is available in English.

Youtube/Holotools/Holostats, ホロライブ配信一覧をYoutubeで表示

ホロライブの直近のスケジュールをYoutubeで確認できます

質問やレビューの投稿はこちらへ、スクリプトの通報はこちらへお寄せください。
// ==UserScript==
// @name         Youtube/Holotools/Holostats, Display Hololive live streaming on youtube
// @name:ja      Youtube/Holotools/Holostats, ホロライブ配信一覧をYoutubeで表示
// @namespace    http://tampermonkey.net/
// @version      0.4.1
// @description  You can check schedule of recently hololive stream on youtube
// @description:ja ホロライブの直近のスケジュールをYoutubeで確認できます
// @author       You
// @match        https://www.youtube.com*
// @match        https://www.youtube.com/*
// @match        https://hololive.jetri.co/*
// @match        https://archive.net/*
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @run-at       document-end
// @noframes
// @unwrap
// ==/UserScript==

(function() {
    'use strict';
    const VERSION = "0.4.1", APPNAME = "DHLSOY";
    const REFRESH_INTERVAL = 5 * 60 * 1000; // 5 mins
    const HOLOSTATS_INTERVAL = 15 * 60 * 1000; // 15 mins
    const FREECHAT_SCHEDULE = 7 * 24 * 60 * 60 * 1000; // 7 days
    const ARCHIVE_ENDHOURS = 36 * 60 * 60 * 1000; // 36 hours
    const ERROR_TIMEOUT = 3000; // 3 secs
    const holotoolsHideChannels = "holotoolsHideChannels";

    const storage = {
        schedule: {
            get updated() { return catchParseError(() => parseInt(localStorage.ht_s_updated), 0); },
            set updated(value) { localStorage.ht_s_updated = new String(value); },
            get cache() { return catchParseError(() => JSON.parse(localStorage.ht_s_cache), null); },
            set cache(value) { localStorage.ht_s_cache = JSON.stringify(value); },
            clear: () => {
                localStorage.ht_s_updated = 0;
                delete localStorage.ht_s_cache;
            }
        },
        archive: {
            get updated() { return catchParseError(() => parseInt(localStorage.ht_a_updated), 0); },
            set updated(value) { localStorage.ht_a_updated = new String(value); },
            get cache() { return catchParseError(() => JSON.parse(localStorage.ht_a_cache), null); },
            set cache(value) { localStorage.ht_a_cache = JSON.stringify(value); },
            clear: () => {
                localStorage.ht_a_updated = 0;
                delete localStorage.ht_a_cache;
            }
        },
        get mute() { return catchParseError(() => JSON.parse(GM_getValue(holotoolsHideChannels) || "[]") || [], []); },
        set mute(value) { GM_setValue(holotoolsHideChannels, value); },
        holostats: {
            favoriteLoaded_: false,
            favorite_: {
                hololive: true,
                sora: true,
                roboco: true,
                miko: true,
                suisei: true,
                fubuki: true,
                matsuri: true,
                haato: true,
                aki: true,
                mel: true,
                choco: true,
                choco_alt: true,
                shion: true,
                aqua: true,
                subaru: true,
                ayame: true,
                pekora: true,
                rushia: true,
                flare: true,
                marine: true,
                noel: true,
                kanata: true,
                coco: true,
                watame: true,
                towa: true,
                himemoriluna: true,
                lamy: true,
                nene: true,
                botan: true,
                polka: true,
                mio: true,
                okayu: true,
                korone: true,
                azki: true,
                risu: true,
                moona: true,
                iofi: true,
                ollie: true,
                melfissa: true,
                reine: true,
                amelia: true,
                calliope: true,
                gura: true,
                inanis: true,
                kiara: true,
                irys: true,
                sana: true,
                ceres: true,
                ouro: true,
                mumei: true,
                hakos: true,
                shien: true,
                oga: true,
                astel: true,
                temma: true,
                roberu: true,
                miyabi: true,
                izuru: true,
                aruran: true,
                rikka: true,
            },
            get favorite() {
                if (this.favoriteLoaded_) {
                    return this.favorite_;
                } else {
                    this.favoriteLoaded_ = true;
                    this.favorite_ = catchParseError(() => Object.assign({}, this.favorite_, JSON.parse(localStorage.ht_stats_favorite)), this.favorite_);
                    return this.favorite_;
                }
            },
            set favorite(value) {
                localStorage.ht_stats_favorite = JSON.stringify(value);
            },
            get updated() { return catchParseError(() => parseInt(localStorage.ht_stats_updated), 0); },
            set updated(value) { localStorage.ht_stats_updated = new String(value); },
            get cache() { return catchParseError(() => JSON.parse(localStorage.ht_stats_cache), null); },
            set cache(value) { localStorage.ht_stats_cache = JSON.stringify(value); },
        }
    }

    // holotools
    if (location.host == "hololive.jetri.co") {
        storage.mute = localStorage.hideChannels;
        return;
    }

    var isRefreshing = false,
        scheduleUpdated = 0,
        archiveUpdated = 0,
        hideChannels = storage.mute;
    // console.log(hideChannels);

    initilize();

    function catchParseError(func, defaultValue) {
        try {
            return func();
        } catch {
            return defaultValue;
        }
    }

    function initilize() {
        if (document.enabledHololiveSchedule) return;
        document.enabledHololiveSchedule = true;

        buildScheduleBase();
    }

    function onScrollContainer(ev) {
        // console.log(ev);

        ev.preventDefault();
        ev.stopPropagation();
        ev.stopImmediatePropagation();

        // console.log("on wheel", ev, document.querySelector("#hololive-schedule").clientWidth, document.documentElement.clientWidth, container.style.left);

        const contents = document.querySelector("#hololive-schedule #contents"),
              lp = parseFloat(contents.style.left) || 0,
              dw = document.documentElement.clientWidth,
              cw = contents.clientWidth;

        // console.log (contents, lp, dw, cw);

        if (ev.deltaY > 0) {
            contents.style.left = Math.max(lp - dw / 5, - cw + dw) + "px";
        } else if (ev.deltaY < 0) {
            contents.style.left = Math.min(lp + dw / 5, 0) + "px";
        } else {
            contents.style.left = - cw + dw;
        }

        return false;
    }

    function buildScheduleBase() {
        const style = document.createElement("style");
        style.id = "hololive-schedule-style";
        style.innerHTML = `
#hololive-schedule {
    --background-color: #ffffff;
    --icon-hover-background-color: #d9d9d9;
    --icon-fill: #212121;
}
[dark=true] #hololive-schedule {
    --background-color: #212121;
    --icon-hover-background-color: #4c4c4c;
    --icon-fill: #ffffff;
}

#hololive-schedule {
    position: fixed;
    bottom: 0;
    left: 0;
    min-width: 100%;
    opacity: 0;
    scrollbar-width: none;
    box-sizing: border-box;
    z-index: 20000;
    transition: opacity .1s linear;
}
#hololive-schedule:hover {
    opacity: 1;
}
.no-scroll #hololive-schedule {
    pointer-events: none;
}
.no-scroll #hololive-schedule.visible {
    pointer-events: unset;
}
#hololive-schedule #contents {
    position: absolute;
    bottom: 0;
    left: 0;
    min-width: 100%;
    max-height: 60px;
    padding: 8px 32px 8px 0;
    white-space: nowrap;
    scrollbar-width: none;
    box-sizing: border-box;
    overflow-y: hidden;
    overflow-x: scroll;
    background-color: var(--background-color);
    transition: left .2s ease, max-height .5s linear;
}
#hololive-schedule:hover #contents {
    max-height: 200px;
    scrollbar-width: none;
}
#hololive-schedule #contents::-webkit-scrollbar {
    display: none;
}
.hololive-stream {
    display: inline-block;
    position: relative;
    border: solid 3px;
    border-raduis: 3px;
    font-size: 10px;
    margin: 0 0 0 8px;
}
.hololive-stream.hide-channel {
    display: none;
}
.hololive-stream .thumbnail {
    vertical-align: middle;
    width: 128px;
    height: 72px;
    background-size: contain;
    background-repeat: 1;
    background-position: center center;
    opacity: 0;
    transition: opacity 0.1s ease;
}
.hololive-stream .thumbnail.loaded {
    opacity: 1;
}
.hololive-stream .title-wrapper {
    position: absolute;
    bottom: 0;
    left: 0;
    right: 0;
    overflow: hidden;
    color: #202020;
    background: #fffa;
    z-index: 1;
}
.hololive-stream:hover .title {
    animation: textAnimation 4s linear infinite;
}
@keyframes textAnimation {
    0% { margin-left: 0; }
    100% { margin-left: -350%; }
}
.hololive-stream .date {
    position: absolute;
    padding: 1px 3px;
    color: #202020;
    background: #fffd;
    font-weight: bold;
    z-index: 1;
}
.hololive-stream .photo {
    position: absolute;
    right: -8px;
    top: -8px;
    width: 32px;
    height: 32px;
    border: solid 2px #fff;
    border-radius: 18px;
    z-index: 1;
    background-color: var(--background-color);
    background-size: cover;
    opacity: 0;
    transition: opacity 0.1s ease;
}
.hololive-stream .photo.loaded {
    opacity: 1;
}
.hololive-stream.live,
.hololive-stream.live .photo {
    border-color: crimson;
}
.hololive-stream.upcoming,
.hololive-stream.upcoming .photo {
    border-color: deepskyblue;
}
.hololive-stream.ended,
.hololive-stream.ended .photo {
    border-color: gray;
}
.hololive-stream.dummy {
    pointer-events: none;
}

#hololive-schedule #tools {
    display: display;
    position: absolute;
    right: 0;
    bottom: 0;
    width: 32px;
    height: 97px;
    z-index: 2;
    transition: opacity .2s linear, width .1s linear;
}
#hololive-schedule:hover #tools {
    max-height: 200px;
    opacity: 1;
}
#hololive-schedule #tools:hover {
    width: 60px;
}
#hololive-schedule #tools a {
    position: relative;
    display: block;
    cursor: pointer;
    vertical-align: middle;
    text-align: center;
    height: 100%;
    background: linear-gradient(to right, #fff0 0%, var(--background-color) 30%, var(--background-color) 100%);
    transition: background .1s linear, padding .1s linear;
}
#hololive-schedule #tools a:hover {
    background: linear-gradient(to right, #fff0 0%, var(--icon-hover-background-color) 30%, var(--icon-hover-background-color) 100%);
}
#hololive-schedule #tools a svg {
    display: inlnie-block;
    height: 100%;
}
#hololive-schedule #tools a svg .shape {
    fill: var(--icon-fill);
    transition: fill .1s linear;
}
#hololive-schedule #tools a:hover svg .shape {
}
#hololive-schedule #tools #powered {
    display: block;
    position: absolute;
    width: 140px;
    top: -24px;
    right: 0;
    height: 24px;
    line-height: 24px;
    background-color: var(--background-color);
    color: var(--icon-fill);
    border-radius: 5px 0 0 0;
}

#hololive-schedule #contents .contents.hidden {
    display: none;
    width: 0;
}
#hololive-schedule #thumbnail-preview {
    position: fixed;
    width: 512px;
    height: 288px;
    bottom: 144px;
    background-size: cover;
    background-position: center;
    border-radius: 4px;
    z-index: 9;
    pointer-events: none;
    box-shadow: #000 0px 0px 12px;
}
#hololive-schedule #thumbnail-preview.hidden {
    opacity: 0;
}
#hololive-schedule #title-preview {
    position: fixed;
    bottom: 100px;
    font-size: 20px;
    padding: 6px 12px;
    border-radius: 4px;
    color: #303030;
    background: #efefef;
    z-index: 10;
    pointer-events: none;
    transition: opacity 0.1s ease;
}
#hololive-schedule #title-preview.hidden {
    opacity: 0;
}
#archive-home {
    position: fixed;
    top: 0;
    bottom: 0;
    left: 0;
    right: 0;
    z-index: 10000;
    background-color: #000a;
    backdrop-filter: blur(3px);
    transition: opacity 0.25s ease;
}
#archive-home::-webkit-scrollbar {
    display: none;
}
#archive-home .content {
    width: 100%;
    height: 100%;
    max-width: calc(100% - 64px);
    max-height: calc(100% - 48px);
    margin: 24px 32px;
    overflow-x: hidden;
    overflow-y: scroll;
    scrollbar-width: none;
}
#archive-home[order-name] .content {
    display: grid;
    grid-auto-flow: row dense;
}
#archive-home .content::-webkit-scrollbar {
    display: none;
}
#archive-home.hidden {
    opacity: 0;
    pointer-events: none;
}
#archive-home .hidden {
    opacity: 0;
}
#archive-home[order-name] .category {
    display: inline-block;
    vertical-align: top;
}
/* thumbnail */
#archive-home .thumbnail {
    display: inline-block;
    position: relative;
    height: 96px;
    width: 168px;
    background-color: #fff3;
    background-size: cover;
    background-position: center;
    border: 2px solid gray;
    border-color: var(--image-color, gray);
    margin-left: 5px;
    margin-top: 4px;
    box-sizing: border-box;
    transition: opacity 0.1s ease;
}
#archive-home .category .header {
    color: #fff;
    font-size: 18px;
    margin: 16px 32px;
    content: attr(--name);
}
#archive-home[order-name] .category .header {
    display: none;
}
#archive-home .thumbnail.hidden {
    display: inline-block;
    opacity: 0;
}
#archive-home .thumbnail.preview {
    opacity: 1;
}
/* blur */
#archive-home .thumbnail::after {
    position: absolute;
    top: 0;
    bottom: 0;
    right: 0;
    left: 0;
    content: "";
    background: linear-gradient(#000a, #000a, #0008, #0000);
    z-index: 3;
    opacity: 0;
    backdrop-filter: blur(2px);
    transition: opacity 0.1s ease;
    pointer-events: none;
}
#archive-home .thumbnail:hover::after {
    opacity: 1;
}
/* user icon */
#archive-home .thumbnail::before {
    content: "";
    position: absolute;
    right: -16px;
    top: -16px;
    background-position: center;
    background-size: cover;
    background-image: var(--icon);
    border: solid 2px gray;
    border-color: var(--image-color);
    border-radius: 36px;
    height: 48px;
    width: 48px;
    z-index: 5;
    transition: transform 0.1s ease,
        top 0.1s ease,right 0.1s ease,
        width 0.1s ease, height 0.1s ease,
        border-width 0.1s ease, border-radius 0.1s ease;
    box-sizing: border-box;
    pointer-events: none;
}
#archive-home .thumbnail:hover::before {
    top: 0;
    right: 0;
    width: 100%;
    height: 100%;
    border-width: 0;
    border-radius: 0;
    pointer-events: none;
    z-index: 2;
}
/* title */
#archive-home .thumbnail span {
    display: block;
    position: absolute;
    font-size: 13px;
    color: #fff;
    max-height: 6em;
    top: 6px;
    right: 6px;
    bottom: 6px;
    left: 6px;
    z-index: 4;
    opacity: 0;
    overflow: hidden;
    transition: opacity 0.1s ease;
}
#archive-home .thumbnail:hover span {
    opacity: 1;
}
#archive-home .error {
    color: #fff;
    text-align: center;
    vertical-align: middle;
}
#archive-home .menu {
    position: absolute;
    top: 8px;
    right: 8px;
    color: #fff;
    font-size: 12px;
}
#archive-home .menu a {
    color: #fff;
    margin-left: 16px;
    text-decoration: none;
    cursor: pointer;
}
#archive-home .sort::after {
    display: inline;
    content: attr(data-sort1);
}
#archive-home[order-name] .sort::after {
    content: attr(data-sort2);
}
#archive-home .loading {
    position: absolute;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    width: 100px;
    z-index: 100000;
}
#archive-home .loading .fragment {
    position: relative;
    height: 8px;
    background: #fffc;
    border-radius: 4px;
    margin-top: 3px;
}
#archive-home .loading:not(.hidden) .fragment {
    animation: archiveLoadingFragment infinite 3s 0s both;
}
#archive-home .loading .fragment:nth-child(0) {
    animation-delay: 0s !important;
}
#archive-home .loading .fragment:nth-child(1) {
    animation-delay: -0.5s !important;
}
#archive-home .loading .fragment:nth-child(2) {
    animation-delay: -1s !important;
}
#archive-home .loading .fragment:nth-child(3) {
    animation-delay: -1.5s !important;
}
#archive-home .loading .fragment:nth-child(4) {
    animation-delay: -2s !important;
}
#archive-home .loading .fragment:nth-child(5) {
    animation-delay: -2.5s !important;
}
@keyframes archiveLoadingFragment {
    0% {
        left: 0;
        width: 0%;
    }
    50% {
        left: 0;
        width: 100%;
    }
    100% {
        left: 100%;
        width: 0%;
    }
}
#archive-home .face-list {
    display: grid;
    grid-template-columns: repeat(40, max-content);
    grid-template-rows: repeat(2, max-content);
}
#archive-home[order-name] .face-list {
    grid-column: span 2;
}
#archive-home .face-list .face {
    display: inline-grid;
    width: 32px;
    height: 40px;
    background-size: cover;
    background-position: center;
    background-image: var(--icon);
    border: 2px solid gray;
    border-color: var(--image-color);
    border-radius: 32px;
    grid-column: var(--column);
    grid-row: var(--row);
    box-sizing: border-box;
    cursor: pointer;
}
#archive-home .face-list .face.mute {
    filter: grayscale();
    border-color: gray;
    opacity: 0.5;
}
#archive-home .next {
    border: solid 1px #fff;
    padding: 8px;
    margin: 0 auto 80px auto;
    color: #fff;
    width: 50%;
    max-width: 500px;
    cursor: pointer;
    position: relative;
    text-align: center;
    font-size: 13px;
    background-color: #0000;
    transition: background-color 0.15s ease;
}
#archive-home .next::before {
    content: "";
    height: 16px;
}
#archive-home .next::after {
    content: "";
    height: 100px;
}
#archive-home[order-name] .next {
    grid-column: span 2;
}
#archive-home .next:hover {
    background-color: #fffa;
}
#archive-home .next.hidden {
    pointer-events: none;
    display: block;
}

#archive-home {
    --count-en: 11;
    --count-id: 6;
    --count-holostars: 9;
}
/* JP */
#archive-home [data-name="sora"] {
    --name: "ときのそら";
    --image-color: cornflowerblue;
    --icon: url('https://yt3.ggpht.com/ytc/AKedOLQO9Vyz7ysAwPSio5xvkw6n0xvlyDu7A9eawqIH3w=s72-c-k-c0x00ffffff-no-rj');
    counter-increment: gen-0;
    --column: 1;
    --row: 1;
}
#archive-home [data-name="roboco"] {
    counter-increment: gen-0;
    --name: "ロボ子さん";
    --image-color: red;
    --icon: url('https://yt3.ggpht.com/ytc/AKedOLTVWKjrovP0tGtguup9TYZicykceA45olVmEr2kvQ=s72-c-k-c0x00ffffff-no-rj-mo');
    --column: 2;
    --row: 1;
}
#archive-home [data-name="miko"] {
    --name: "さくらみこ";
    --image-color: pink;
    --icon: url('https://yt3.ggpht.com/ytc/AKedOLRNGCUT1awYh91CbTr6r_v_6KspwpyAS4ZUxlucFQ=s72-c-k-c0x00ffffff-no-rj-mo');
    --column: 3;
    --row: 1;
}
#archive-home [data-name="suisei"] {
    counter-increment: gen-0;
    --name: "星街すいせい";
    --image-color: navy;
    --icon: url('https://yt3.ggpht.com/ytc/AKedOLSAm13gTESsu39zgJ1TYb649BiGqYa_XCv5C6Lu=s72-c-k-c0x00ffffff-no-rj-mo');
    --column: 4;
    --row: 1;
}
#archive-home [data-name="fubuki"] {
    counter-increment: gen-1;
    --name: "白上フブキ";
    --image-color: whitesmoke !important;
    --icon: url('https://yt3.ggpht.com/ytc/AKedOLQWDSaTmBOPMp-zXxHeLYpKI7KG9q6t191LkMnkWg=s72-c-k-c0x00ffffff-no-rj-mo');
    --column: 9;
    --row: 1;
}
#archive-home [data-name="matsuri"] {
    counter-increment: gen-1;
    --name: "夏色まつり";
    --image-color: orange !important;
    --icon: url('https://yt3.ggpht.com/ytc/AKedOLQCXDfJbZoEZ-gtUiF4nSaGU8-qiq--BSTd92Sw=s72-c-k-c0x00ffffff-no-rj-mo');
    --column: 10;
    --row: 1;
}
#archive-home [data-name="haato"] {
    counter-increment: gen-1;
    --name: "赤井はあと";
    --image-color: deeppink !important;
    --icon: url('https://yt3.ggpht.com/ytc/AKedOLSq5U6B_Jfp0yVntj7BWcChY7uUqjDv2Cufa0isew=s72-c-k-c0x00ffffff-no-rj-mo');
    --column: 11;
    --row: 1;
}
#archive-home [data-name="aki"] {
    counter-increment: gen-1;
    --name: "アキ・ローゼンタール";
    --image-color: yellow !important;
    --icon: url('https://yt3.ggpht.com/ytc/AKedOLT4XEPRFwXpb4gZ1qco_xCOt7ems7SrUsGOkmXX=s72-c-k-c0x00ffffff-no-rj-mo');
    --column: 12;
    --row: 1;
}
#archive-home [data-name="mel"] {
    counter-increment: gen-1;
    --name: "夜空メル";
    --image-color: gold !important;
    --icon: url('https://yt3.ggpht.com/ytc/AKedOLSwGrjnpZYhSj5yOV35k_CaXHeCbW1222lgtgbW=s72-c-k-c0x00ffffff-no-rj-mo');
    --column: 13;
    --row: 1;
}
#archive-home [data-name="choco"] {
    counter-increment: gen-2;
    --name: "癒月ちょこ";
    --image-color: mediumvioletred !important;
    --icon: url('https://yt3.ggpht.com/ytc/AKedOLQn_VxZ1ApMgQahrkcTtSdSAr6Jpxi4eHQiMnIlsw=s72-c-k-c0x00ffffff-no-rj-mo');
    --column: 14;
    --row: 1;
}
#archive-home [data-name="choco_alt"] {
    counter-increment: gen-2;
    --name: "癒月ちょこ サブ";
    --image-color: mediumvioletred !important;
    --icon: url('https://yt3.ggpht.com/ytc/AKedOLT8t9_USgt289gErw3cmDEHmTXkXEhs6VAabZSf=s72-c-k-c0x00ffffff-no-rj-mo');
    --column: 15;
    --row: 1;
}
#archive-home [data-name="shion"] {
    counter-increment: gen-2;
    --name: "紫咲シオン";
    --image-color: mediumorchid !important;
    --icon: url('https://yt3.ggpht.com/ytc/AKedOLRJYi-cOhqB22oEjWfdz__fHcs9iGjwz5UkPzvd-w=s72-c-k-c0x00ffffff-no-rj-mo');
    --column: 16;
    --row: 1;
}
#archive-home [data-name="aqua"] {
    counter-increment: gen-2;
    --name: "湊あくあ";
    --image-color: violet !important;
    --icon: url('https://yt3.ggpht.com/ytc/AKedOLTbU5ET3bgn0Iuz1jUBNjgSe9EW8kLxIhDUrtJlPw=s72-c-k-c0x00ffffff-no-rj-mo');
    --column: 17;
    --row: 1;
}
#archive-home [data-name="subaru"] {
    counter-increment: gen-2;
    --name: "大空スバル";
    --image-color: dodgerblue !important;
    --icon: url('https://yt3.ggpht.com/ytc/AKedOLRaQJl61Pxhsnrzz50wirogPn18pPUYL0YFAauj=s72-c-k-c0x00ffffff-no-rj-mo');
    --column: 18;
    --row: 1;
}
#archive-home [data-name="ayame"] {
    counter-increment: gen-2;
    --name: "百鬼あやめ";
    --image-color: darkred !important;
    --icon: url('https://yt3.ggpht.com/ytc/AKedOLT7tdqZvDuxprLyB0qjVHKV53iGJ39a2FTbxX8X=s72-c-k-c0x00ffffff-no-rj-mo');
    --column: 19;
    --row: 1;
}
#archive-home [data-name="pekora"] {
    counter-increment: gen-3;
    --name: "兎田ぺこら";
    --image-color: skyblue !important;
    --icon: url('https://yt3.ggpht.com/ytc/AKedOLSmHTeNNQp8A4AwsUPKzBa2ubDBWe6RSaG39mAYTw=s72-c-k-c0x00ffffff-no-rj');
    --column: 20;
    --row: 1;
}
#archive-home [data-name="rushia"] {
    counter-increment: gen-3;
    --name: "潤羽るしあ";
    --image-color: mediumseagreen !important;
    --icon: url('https://yt3.ggpht.com/ytc/AKedOLR1en3cN55loPrFL1C5K19o5xGhcKkmr0noD4cO=s72-c-k-c0x00ffffff-no-rj');
    --column: 21;
    --row: 1;
}
#archive-home [data-name="flare"] {
    counter-increment: gen-3;
    --name: "不知火フレア";
    --image-color: tan !important;
    --icon: url('https://yt3.ggpht.com/ytc/AKedOLS3w5WmjGJ4C-VOvMR7g3eVImLfwSTlQCofJCCkqA=s72-c-k-c0x00ffffff-no-rj-mo');
    --column: 22;
    --row: 1;
}
#archive-home [data-name="marine"] {
    counter-increment: gen-3;
    --name: "宝鐘マリン";
    --image-color: mediumvioletred !important;
    --icon: url('https://yt3.ggpht.com/ytc/AKedOLQFVN7wLaJFbdPU56qOkNlbkrMneYpTmGpneRig=s72-c-k-c0x00ffffff-no-rj');
    --column: 23;
    --row: 1;
}
#archive-home [data-name="noel"] {
    counter-increment: gen-3;
    --name: "白銀ノエル";
    --image-color: silver !important;
    --icon: url('https://yt3.ggpht.com/ytc/AKedOLS1MTrG3Gn7-Vf_rVNAZ2Ou8KrmUGUXO6TmkLxe=s72-c-k-c0x00ffffff-no-rj-mo');
    --column: 24;
    --row: 1;
}
#archive-home [data-name="kanata"] {
    counter-increment: gen-4;
    --name: "天音かなた";
    --image-color: gold !important;
    --icon: url('https://yt3.ggpht.com/ytc/AKedOLTgDWWow5gGLYfPKHxF8oNHegeUngIdT5HxDBo4=s72-c-k-c0x00ffffff-no-rj-mo');
    --column: 25;
    --row: 1;
}
#archive-home [data-name="coco"] {
    counter-increment: gen-4;
    --name: "桐生ココ";
    --image-color: darkorange !important;
    --icon: url('https://yt3.ggpht.com/ytc/AKedOLQUA2htopp_nUXZScvJvh17wlOhSvcjAc-L75meCg=s72-c-k-c0x00ffffff-no-rj-mo');
    --column: 26;
    --row: 1;
}
#archive-home [data-name="watame"] {
    counter-increment: gen-4;
    --name: "角巻わため";
    --image-color: lemonchiffon !important;
    --icon: url('https://yt3.ggpht.com/ytc/AKedOLRWpyqOZzCmuSfmKGNo8TD2L_IRUYSw1wyhHXw-=s72-c-k-c0x00ffffff-no-rj-mo');
    --column: 27;
    --row: 1;
}
#archive-home [data-name="towa"] {
    counter-increment: gen-4;
    --name: "常闇トワ";
    --image-color: orchid !important;
    --icon: url('https://yt3.ggpht.com/ytc/AKedOLRL8tkRgdRpjzb1wlAajrJwXGg3p1hzt__ncFas=s72-c-k-c0x00ffffff-no-rj');
    --column: 28;
    --row: 1;
}
#archive-home [data-name="himemoriluna"] {
    counter-increment: gen-4;
    --name: "姫森ルーナ";
    --image-color: hotpink !important;
    --icon: url('https://yt3.ggpht.com/YeYOdM-vuMjkwFrBAKxDKEsZ-UZ-tWGnx6T_1-2JJ3dJvQ604-VRdEon6cjEOmBcDn8pn4d_Bg=s72-c-k-c0x00ffffff-no-rj');
    --column: 29;
    --row: 1;
}
#archive-home [data-name="lamy"] {
    counter-increment: gen-5;
    --name: "雪花ラミィ";
    --image-color: lightskyblue !important;
    --icon: url('https://yt3.ggpht.com/ytc/AKedOLQDR06gp26jxNNXh88Hhv1o-pNrnlKrYruqUIOx=s72-c-k-c0x00ffffff-no-rj-mo');
    --column: 30;
    --row: 1;
}
#archive-home [data-name="nene"] {
    counter-increment: gen-5;
    --name: "桃鈴ねね";
    --image-color: darksalmon !important;
    --icon: url('https://yt3.ggpht.com/ytc/AKedOLQZBw4jihwhA_0JsKR8tRPOV1vHFdqI73hGE3OZ=s72-c-k-c0x00ffffff-no-rj-mo');
    --column: 31;
    --row: 1;
}
#archive-home [data-name="botan"] {
    counter-increment: gen-5;
    --name: "獅白ぼたん";
    --image-color: dimgray !important;
    --icon: url('https://yt3.ggpht.com/ytc/AKedOLQXr6MeUpHI0-yNZIAaGXHvBVowhCWMwGx-zXYs=s72-c-k-c0x00ffffff-no-rj-mo');
    --column: 32;
    --row: 1;
}
#archive-home [data-name="polka"] {
    counter-increment: gen-5;
    --name: "尾丸ポルカ";
    --image-color: red !important;
    --icon: url('https://yt3.ggpht.com/ytc/AKedOLQI_iYxOpfP8bJklQ_VnS4a9jdrwRRlre_JP1Yp=s72-c-k-c0x00ffffff-no-rj-mo');
    --column: 33;
    --row: 1;
}
#archive-home [data-name="mio"] {
    counter-increment: gen-gamers;
    --name: "大神ミオ";
    --image-color: maroon !important;
    --icon: url('https://yt3.ggpht.com/ytc/AKedOLRP0h31urAKtYcu_j1foVuGyPU65_Y-VNBqLgHB5Q=s72-c-k-c0x00ffffff-no-rj-mo');
    --column: 6;
    --row: 1;
}
#archive-home [data-name="okayu"] {
    counter-increment: gen-gamers;
    --name: "猫又おかゆ";
    --image-color: violet;
    --icon: url('https://yt3.ggpht.com/ytc/AKedOLT_TLZsRHyNXj_3v1QIfF5Z1LOEIKQPL_7HGH29=s72-c-k-c0x00ffffff-no-rj-mo');
    --column: 7;
    --row: 1;
}
#archive-home [data-name="korone"] {
    counter-increment: gen-gamers;
    --name: "戌神ころね";
    --image-color: sienna !important;
    --icon: url('https://yt3.ggpht.com/ytc/AKedOLSegxVNNn4QGDwO-jO89ZDcYLSyPUQS3a4KU6QPCw=s72-c-k-c0x00ffffff-no-rj-mo');
    --column: 8;
    --row: 1;
}
#archive-home [data-name="azki"] {
    counter-increment: gen-0;
    --name: "Azki";
    --image-color: deeppink;
    --icon: url('https://yt3.ggpht.com/ytc/AKedOLQQhnWKHLOLxjnXksGHHC8bnVS2UniL8Od6JTEPWQ=s72-c-k-c0x00ffffff-no-rj-mo');
    --column: 5;
    --row: 1;
}
/* ID */
#archive-home [data-name="risu"] {
    counter-increment: gen-id1;
    --name: "アユンダ・リス";
    --image-color: lightpink !important;
    --icon: url('https://yt3.ggpht.com/ytc/AKedOLTjqfaFS9JlspGjiIah2kkxOtl4vRrxBCYKMEY5Kw=s72-c-k-c0x00ffffff-no-rj-mo');
    --column: calc(var(--count-en) + 1);
    --row: 2;
}
#archive-home [data-name="moona"] {
    counter-increment: gen-id1;
    --name: "ムーナ・ホシノヴァ";
    --image-color: mediumorchid !important;
    --icon: url('https://yt3.ggpht.com/ytc/AKedOLS-qVI5LiWrtbyPF6wGK-nph-vEWf9BbDWoSDGYbA=s72-c-k-c0x00ffffff-no-rj-mo');
    --column: calc(var(--count-en) + 2);
    --row: 2;
}
#archive-home [data-name="iofi"] {
    counter-increment: gen-id1;
    --name: "アイラニ・イオフィフティーン";
    --image-color: pink !important;
    --icon: url('https://yt3.ggpht.com/ytc/AKedOLQNhNLAE1ECJuVKg9sO7PpiRd2g-kaq6VWB6Q69=s72-c-k-c0x00ffffff-no-rj');
    --column: calc(var(--count-en) + 3);
    --row: 2;
}
#archive-home [data-name="ollie"] {
    counter-increment: gen-id2;
    --name: "クレイジー・オリー";
    --image-color: crimson !important;
    --icon: url('https://yt3.ggpht.com/jWxru6sHDDSuKF-gztFg_WSoMp2da_d019iH0xz0MDWc7TIhetK8id_mVKV0PxWKp-QS23AzfQ=s72-c-k-c0x00ffffff-no-rj-mo');
    --column: calc(var(--count-en) + 4);
    --row: 2;
}
#archive-home [data-name="melfissa"] {
    counter-increment: gen-id2;
    --name: "アーニャ・メルフィッサ";
    --image-color: tan !important;
    --icon: url('https://yt3.ggpht.com/ytc/AKedOLR0AplPQyxSjGhqMxJy7vAvXn-9hyaiXBoBE5vy=s72-c-k-c0x00ffffff-no-rj-mo');
    --column: calc(var(--count-en) + 5);
    --row: 2;
}
#archive-home [data-name="reine"] {
    counter-increment: gen-id2;
    --name: "パヴォリア・レイネ";
    --border-color: mediumblue !important;
    --icon: url('https://yt3.ggpht.com/ytc/AKedOLQfMF4yIvgoapLc07bB6a7ASN9iyGMgyE2UbwEM=s72-c-k-c0x00ffffff-no-rj-mo');
    --column: calc(var(--count-en) + 6);
    --row: 2;
}
/* EN */
#archive-home [data-name="amelia"] {
    counter-increment: gen-en1;
    --name: "ワトソン・アメリア";
    --image-color: khaki !important;
    --icon: url('https://yt3.ggpht.com/ytc/AKedOLTvS-8gomaJywaEKp3hnCmY92vQ9uKpy8rMAx3a=s72-c-k-c0x00ffffff-no-rj-mo');
    --column: 1;
    --row: 2;
}
#archive-home [data-name="calliope"] {
    counter-increment: gen-en1;
    --name: "森美声";
    --image-color: lightpink !important;
    --icon: url('https://yt3.ggpht.com/ytc/AKedOLQi2hR9UdCcWoDLz4sJYqAu9BkaYBGWex_th5ic=s72-c-k-c0x00ffffff-no-rj-mo');
    --column: 2;
    --row: 2;
}
#archive-home [data-name="gura"] {
    counter-increment: gen-en1;
    --name: "がうる・ぐら";
    --image-color: royalblue !important;
    --icon: url('https://yt3.ggpht.com/ytc/AKedOLQpQAbNCTnAULoN1rWO6wBkhFNHTbMqDkd7gFhq=s72-c-k-c0x00ffffff-no-rj-mo');
    --column: 3;
    --row: 2;
}
#archive-home [data-name="inanis"] {
    counter-increment: gen-en1;
    --name: "一伊那尓栖";
    --image-color: indigo !important;
    --icon: url('https://yt3.ggpht.com/ytc/AKedOLRFAFjEvIwiZ_MrQvdY8-QbJkqvahsi3La78Jf7=s72-c-k-c0x00ffffff-no-rj-mo');
    --column: 4;
    --row: 2;
}
#archive-home [data-name="kiara"] {
    counter-increment: gen-en1;
    --name: "小鳥遊キアラ";
    --image-color: orange !important;
    --icon: url('https://yt3.ggpht.com/ytc/AKedOLRlcr4d2hFSydP7swZwUfZM5J3VWA3M41ucCVES=s72-c-k-c0x00ffffff-no-rj-mo');
    --column: 5;
    --row: 2;
}
#archive-home [data-name="irys"] {
    counter-increment: gen-vsinger;
    --name: "IRyS";
    --image-color: darkred !important;
    --icon: url('https://yt3.ggpht.com/UwxlX1PuB_RwJyEUW_ofbBR6saY8n5_p8A9_1bY65zygFrfqIb1GM8dIK33EJboDDnRVyw=s72-c-k-c0x00ffffff-no-rj-mo');
    --column: 6;
    --row: 2;
}
#archive-home [data-name="sana"] {
    counter-increment: gen-engikai;
    --name: "九十九 佐命";
    --image-color: deeppink !important;
    --icon: url('https://yt3.ggpht.com/d7xnzbFVWHHMrxORQ6w5U6hwtg_NWB2rT6CMZWsQvILip_IH8ukeNkqj9FNmvYm7XfcdyZcLgxA=s72-c-k-c0x00ffffff-no-rj');
    --column: 7;
    --row: 2;
}
#archive-home [data-name="ceres"] {
    counter-increment: gen-engikai;
    --name: "セレス・ファウナ";
    --image-color: palegreen !important;
    --icon: url('https://yt3.ggpht.com/0lkccaVapSr1Z3uuXWbnaQxeqRWr9Tcs4R9rLBRSrAsN9gLacpiT2OFWfFKr4NhF97_hqK3eTg=s72-c-k-c0x00ffffff-no-rj');
    --column: 8;
    --row: 2;
}
#archive-home [data-name="ouro"] {
    counter-increment: gen-engikai;
    --name: "オーロ・クロニー";
    --image-color: navy !important;
    --icon: url('https://yt3.ggpht.com/6670YE31bbAtAi7m_UL-KWZBdL5wvmfHlLtcS4HxsBZBQNqmAk7Y-iiIOjawO_0HYXpS4HfC=s72-c-k-c0x00ffffff-no-rj');
    --column: 9;
    --row: 2;
}
#archive-home [data-name="mumei"] {
    counter-increment: gen-engikai;
    --name: "七詩ムメイ";
    --image-color: darkslategray !important;
    --icon: url('https://yt3.ggpht.com/MI8E8Wfmc_ngNZXUwu8ad0D-OtqDhmqGVULEu25z-ccscwzJpAw-7ewFXzZYLK2jHB9d5OgQDq4=s72-c-k-c0x00ffffff-no-rj');
    --column: 10;
    --row: 2;
}
#archive-home [data-name="hakos"] {
    counter-increment: gen-engikai;
    --name: "ハコス・ベールズ";
    --image-color: crimson !important;
    --icon: url('https://yt3.ggpht.com/GWIwRbtVQ2TAlvH8Mf37FMpemTrwmUSbTSazp9Aul6KwdKQmvx7IbLZepDk0sp8ReW3qBhsU=s72-c-k-c0x00ffffff-no-rj');
    --column: 11;
    --row: 2;
}

/* STARS */
#archive-home [data-name="shien"] {
    counter-increment: gen-star3;
    --name: "影山シエン";
    --image-color: plum !important;
    --icon: url('https://yt3.ggpht.com/N20GfJbuG8BBl9CexUek023y2DXQAYqgYoRRqsVGAYoS-gZsGuH7W1Il0y-8TnIul19rBPG78Jo=s72-c-k-c0x00ffffff-no-rj-mo');
    --column: calc(var(--count-en) + var(--count-id) + 9);
    --row: 2;
}
#archive-home [data-name="oga"] {
    counter-increment: gen-star3;
    --name: "荒咬オウガ";
    --image-color: greenyellow !important;
    --icon: url('https://yt3.ggpht.com/ytc/AKedOLTn3QGX09ZS4rzV54zwhFWKbnJtrM5cPGsXfZPi=s72-c-k-c0x00ffffff-no-rj-mo');
    --column: calc(var(--count-en) + var(--count-id) + 8);
    --row: 2;
}
#archive-home [data-name="astel"] {
    counter-increment: gen-star3;
    --name: "アステル レダ";
    --image-color: yellow !important;
    --icon: url('https://yt3.ggpht.com/ytc/AKedOLQZtNrwtu1KawCZz7ph1erMEC8ZSGESvpjc_XMZ=s72-c-k-c0x00ffffff-no-rj-mo');
    --column: calc(var(--count-en) + var(--count-id) + 7);
    --row: 2;
}
#archive-home [data-name="temma"] {
    counter-increment: gen-star2;
    --name: "岸堂天真";
    --image-color: khaki;
    --icon: url('https://yt3.ggpht.com/ytc/AKedOLRUGIQekUO7Yyzzx49nHpRozhPKLUmNDnEAL4Go=s72-c-k-c0x00ffffff-no-rj-mo');
    --column: calc(var(--count-en) + var(--count-id) + 6);
    --row: 2;
}
#archive-home [data-name="roberu"] {
    counter-increment: gen-star2;
    --name: "夕刻ロベル";
    --image-color: chocolate !important;
    --icon: url('https://yt3.ggpht.com/ytc/AKedOLTqeDflgueIvILxwFZSxbSdGIFTwdshGwEbrYN1=s72-c-k-c0x00ffffff-no-rj-mo');
    --column: calc(var(--count-en) + var(--count-id) + 5);
    --row: 2;
}
#archive-home [data-name="miyabi"] {
    counter-increment: gen-star2;
    --name: "花咲みやび";
    --image-color: crimson;
    --icon: url('https://yt3.ggpht.com/ytc/AKedOLQ9BDQIqfO728ZI5PmsMKb67thOI2NvpHqYFK5o=s72-c-k-c0x00ffffff-no-rj-mo');
    --column: calc(var(--count-en) + var(--count-id) + 4);
    --row: 2;
}
#archive-home [data-name="izuru"] {
    counter-increment: gen-star1;
    --name: "奏手イヅル";
    --image-color: mediumblue !important;
    --icon: url('https://yt3.ggpht.com/ytc/AKedOLRG60Hg-7N1Mmh47K7P6ghMf2hVaivdVk9I9fEWYA=s72-c-k-c0x00ffffff-no-rj-mo');
    --column: calc(var(--count-en) + var(--count-id) + 3);
    --row: 2;
}
#archive-home [data-name="aruran"] {
    counter-increment: gen-star1;
    --name: "アルランディス";
    --image-color: forestgreen !important;
    --icon: url('https://yt3.ggpht.com/ytc/AKedOLQH3CqU4dL9EWjrYl6aKn26_DAAHbCXEBVyMTaWZA=s72-c-k-c0x00ffffff-no-rj-mo');
    --column: calc(var(--count-en) + var(--count-id) + 2);
    --row: 2;
}
#archive-home [data-name="rikka"] {
    counter-increment: gen-star1;
    --name: "律可";
    --image-color: mistyrose;
    --icon: url('https://yt3.ggpht.com/ytc/AKedOLRs_MgPrlX2xo_2dvQQ05wbMkq--u_CIjR5Wt3T=s72-c-k-c0x00ffffff-no-rj-mo');
    --column: calc(var(--count-en) + var(--count-id) + 1);
    --row: 2;
}
/* OFFICIAL */
#archive-home [data-name="hololive"] {
    counter-increment: official;
    --name: "Hololive";
    --image-color: deepskyblue;
    --icon: url('https://yt3.ggpht.com/ytc/AKedOLTj0OSWM9TvPy4e8v1_o99OtP3Bg7FXthdkgr2bCQ=s72-c-k-c0x00ffffff-no-rj-mo');
    --column: calc(var(--count-en) + var(--count-id) + var(--count-holostars) + 1);
    --row: 2;
}
#archive-home [data-name="english"] {
    counter-increment: official;
    --name: "Hololive English";
    --image-color: dimgray;
    --icon: url('https://yt3.ggpht.com/ytc/AKedOLRduH6Fb3Vz3FMl2-e0wNte81epj3w-LubC0nu2=s72-c-k-c0x00ffffff-no-rj');
    --column: calc(var(--count-en) + var(--count-id) + var(--count-holostars) + 2);;
    --row: 2;
}
#archive-home [data-name="id"] {
    counter-increment: official;
    --name: "Hololive ID";
    --image-color: dimgray;
    --icon: url('https://yt3.ggpht.com/ytc/AKedOLSILukXkRWs4cQHfyrX1HxdxAFTroZG7NpW-m8N=s72-c-k-c0x00ffffff-no-rj');
    --column: calc(var(--count-en) + var(--count-id) + var(--count-holostars) + 3);;
    --row: 2;
}
`;
        document.body.appendChild(style);

        const localize = {
            ja: {
                schedule: "スケジュールを見る",
                archive: "終了した放送を見る",
                powered: "参照元: Holotools",
                archive: "参照元: HoloStats",
                sort1: "配信者別に表示",
                sort2: "配信日別に表示",
            },
            en: {
                schedule: "View scheulde",
                archive: "View archive list",
                powered: "Source: Holotools",
                archive: "Source: HoloStats",
                sort1: "Sorted by streamer",
                sort2: "Sorted by date",
            }
        }
        const t = localize[window.navigator.language] || localize.en;

        const container = document.createElement("div");
        container.id = "hololive-schedule";
        container.onmouseenter = onMouseEnter;
        container.innerHTML = `
<div id="tools">
<a id="archive" title="${t.archive}">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" x="0px" y="0px" width="28px" height="28px" viewBox="0 0 512 512">
	<path class="shape" d="M464,56v40h-40V56H88v40H48V56H0v400h48v-40h40v40h336v-40h40v40h48V56H464z M88,354.672H48v-64h40V354.672z
		 M88,221.328H48v-64h40V221.328z M308.094,266.047l-101.734,60.734c-0.75,0.438-1.656,0.469-2.406,0.031
		c-0.734-0.422-1.203-1.219-1.203-2.094V264v-60.719c0-0.859,0.469-1.656,1.203-2.094c0.75-0.406,1.656-0.391,2.406,0.031
		l101.734,60.75c0.719,0.406,1.156,1.203,1.156,2.031C309.25,264.844,308.813,265.625,308.094,266.047z M464,354.672h-40v-64h40
		V354.672z M464,221.328h-40v-64h40V221.328z"></path>
</svg>
</a>
<a id="powered" href="https://hololive.jetri.co/" target="blank_">${t.powered}</a>
</div>
<div id="contents" style="left: 0px;">
    <div id="schedule" class="contents hidden"></div>
    <div id="archive" class="contents hidden"></div>
    <div id="dummy" class="contents"></div>
</div>
<div id="title-preview" class="hidden"></div>
<div id="thumbnail-preview" class="hidden"></div>
`;
        document.body.appendChild(container);

        const home = document.createElement("div");
        home.id = "archive-home";
        home.classList.add("hidden");
        home.innerHTML = `
        <div class="content">
        <div class="face-list"></div>
        <div class="next hidden no-exit">NEXT</div>
        </div>
        <div class="menu">
            <a style="display: none;" class="sort no-exit" target="blank_" data-sort1="${t.sort1}" data-sort2="${t.sort2}"></a>
            <a class="powered" href="https://holo.poi.cat/youtube-stream" target="blank_">${t.archive}</a>
        </div>
        <div class="loading hidden">
            <div class="fragment"></div>
            <div class="fragment"></div>
            <div class="fragment"></div>
            <div class="fragment"></div>
            <div class="fragment"></div>
            <div class="fragment"></div>
        </div>`;

        document.body.appendChild(home);

        var members = storage.holostats.favorite;
        for (var name in members) {
            var isMute = !members[name];
            home.querySelector(".face-list").insertAdjacentHTML("beforeend", `<div class="face no-exit ${isMute ? "mute" : ""}" data-name="${name}"></div>`);
        }
        home.querySelectorAll(".face").forEach((e) => e.addEventListener("click", onToggleMuteChannel));

        home.addEventListener("click", onExitArchiveHome);
        home.querySelector(".next").addEventListener("click", onClickNextArchiveHome);
        home.querySelector(".sort").addEventListener("click", onToggleSortArchiveHome);

        container.addEventListener("wheel", onScrollContainer, true);
        document.querySelector("ytd-app").addEventListener("scroll", (e) => {
            container.classList.toggle("visible", e.target.scrollTop > 80);
        });

        container.querySelector("#tools #archive").addEventListener("click", (ev) => {
            displayArchiveHome(false);
            // refreshArchive(ev.ctrlKey);
        });
        container.addEventListener("mouseenter", (ev) => {
            if (ev.target.classList.contains("hololive-stream")) {
                const title = document.querySelector("#hololive-schedule #title-preview");
                // 左端に合わせておかないと長いタイトルで改行が含まれる場合に
                // clientWidth の値が小さくなって中央寄せに失敗する
                title.style.left = "0px";
                title.innerText = ev.target.querySelector(".title").innerText;
                title.classList.remove("hidden");
                title.style.left = `${document.body.clientWidth / 2 - title.clientWidth / 2}px`;

                const thumbnail = document.querySelector("#hololive-schedule #thumbnail-preview");
                thumbnail.style.backgroundImage = ev.target.querySelector(".thumbnail").style.backgroundImage;
                thumbnail.classList.remove("hidden");
                thumbnail.style.left = `${document.body.clientWidth / 2 - thumbnail.clientWidth / 2}px`;
            }
        }, true);
        container.addEventListener("mouseleave", (ev) => {
            document.querySelector("#hololive-schedule #thumbnail-preview").classList.add("hidden");
            document.querySelector("#hololive-schedule #title-preview").classList.add("hidden");
        });
        drawLoadingDummy();
    }

    function onMouseEnter() {
        if (isRefreshing) return;

        refreshSchedule();

        /*
        if (document.querySelector("#hololive-schedule #contents #archive.hidden")) {
            refreshSchedule();
        } else {
            refreshArchive();
        }*/
    }

    // 枠をつくる
    function createStream(details) {
        // console.log(details);
        var stream = document.createElement("a");
        stream.data = details;
        stream.classList.add("hololive-stream");
        if (hideChannels.find((x) => x == details.channel.yt_channel_id)) {
            stream.classList.add("hide-channel");
        }
        stream.href = "/watch/" + details.yt_video_key;
        stream.innerHTML = `
<div class="date">${toLiveDate(details.live_schedule, details.live_start, details.live_end, details.published_at)}</div>
<div class="thumbnail" style="background-image: url('https://i.ytimg.com/vi/${details.yt_video_key}/mqdefault.jpg?${new Date(details.live_start || details.live_schedule || details.published_at).getTime()}')"></div>
<div class="title-wrapper"><div class="title">${details.title}</div></div>
<div class="photo" style="background-image: url('${details.channel.photo.replace("=s800", "=s72")}');" />
`;

        return stream;
    }

    function drawSchedule(streams) {
        // console.log(streams);

        try {
            const contents = document.querySelector("#hololive-schedule #contents #schedule");
            contents.innerHTML = "";
            contents.style.left = "0px";

            var key, stream;

            if (streams) {
                if (streams.live) {
                    streams.live = streams.live
                        .filter(x => x.status != "missing");
                    streams.live
                        .sort((a, b) => (a.live_start || a.live_schedule) < (b.live_start || b.live_schedule) ? 1 : -1);
                    for (key in streams.live) {
                        stream = createStream(streams.live[key]);
                        stream.classList.add("live");
                        contents.appendChild(stream);
                    }
                }

                if (streams.upcoming) {
                    streams.upcoming = streams.upcoming
                        .filter(x => x.status != "missing")
                        .filter(x => new Date(x.live_schedule).getTime() - new Date().getTime() < FREECHAT_SCHEDULE);
                    streams.upcoming
                        .sort((a, b) => a.live_schedule > b.live_schedule ? 1 : -1);
                    for (key in streams.upcoming) {
                        stream = createStream(streams.upcoming[key]);
                        stream.classList.add("upcoming");
                        contents.appendChild(stream);
                    }
                }

                if (streams.ended) {
                    streams.ended = streams.ended
                        .filter(x => x.status != "missing");
                    streams.ended
                        .sort((a, b) => a.live_start < b.live_start ? 1 : -1);
                    for (key in streams.ended) {
                        stream = createStream(streams.ended[key]);
                        stream.classList.add("ended");
                        contents.appendChild(stream);
                    }
                }
            }

            contents.querySelectorAll(".thumbnail").forEach((element, index) => {
                setTimeout(() => element.classList.add("loaded"), (index + 1) * 25);
            });
            contents.querySelectorAll(".photo").forEach((element, index) => {
                setTimeout(() => element.classList.add("loaded"), (index + 1) * 25);
            });
        } finally {
            toggleDisplay("schedule");
        }
    }

    function drawLoadingDummy() {
        const contents = document.querySelector("#hololive-schedule #contents #dummy");
        contents.innerHTML = "";
        contents.style.left = "0px";
        const data = {
            bb_video_id: null,
            channel:
            {
                bb_space_id: null,
                id: 0,
                name: "Loading",
                photo: "https://www.youtube.com/s/desktop/fe7279a7/img/favicon_144.png",
                published_at: "2000-01-01T00:00:00.000Z",
                subscriber_count: 0,
                twitter_link: "",
                video_count: 0,
                view_count: 0,
                yt_channel_id: "",
            },
            id: 0,
            live_end: null,
            live_schedule: "2222-22-22T22:22:22.222Z",
            live_start: null,
            live_viewers: null,
            status: "upcoming",
            thumbnail: null,
            title: "Comming soon ...",
            yt_video_key: "",
        }
        const dummy = createStream(data);
        dummy.classList.add("dummy");

        for (var i = 0; i < 20; i++) {
            contents.appendChild(dummy.cloneNode(true));
        }

        contents.querySelectorAll(".thumbnail").forEach((element, index) => {
            setTimeout(() => element.classList.add("loaded"), 25);
        });
        contents.querySelectorAll(".photo").forEach((element, index) => {
            setTimeout(() => element.classList.add("loaded"), 25);
        });
    }

    function drawArchive(streams) {
        // console.log(streams);

        try {
            const contents = document.querySelector("#hololive-schedule #contents #archive");
            contents.style.left = "0px";
            contents.innerHTML = "";

            if (streams && streams.videos) {
                streams.videos = streams.videos
                    .filter(x => x.status == "past");
                streams.videos
                    .sort((a, b) => (a.live_end || a.published_at) < (b.live_end || b.published_at) ? 1 : -1);
                for (var key in streams.videos) {
                    const stream = createStream(streams.videos[key]);
                    stream.classList.add("ended");
                    contents.appendChild(stream);
                }
            }

            contents.querySelectorAll(".photo").forEach((element, index) => {
                setTimeout(() => element.classList.add("loaded"), (index + 1) * 25);
            });
            contents.querySelectorAll(".thumbnail").forEach((element, index) => {
                setTimeout(() => element.classList.add("loaded"), (index + 1) * 25);
            });
        } finally {
            toggleDisplay("archive");
        }
    }

    function toggleDisplay(target) {
        document.querySelectorAll(`#hololive-schedule .contents`).forEach((element, index) => {
            element.classList.toggle("hidden", element.id != target);
        });
        document.querySelector("#hololive-schedule #contents").style.left = 0;
    }

    function refreshSchedule(force) {
        //console.log(`refreshSchedule(${force})`);

        if (isRefreshing) return;
        isRefreshing = true;

        if (force) {
            storage.schedule.clear();
            scheduleUpdated = 0;
        }

        try {
            const cacheUpdated = storage.schedule.updated;
            const newestUpdated = Math.max(cacheUpdated, scheduleUpdated);

            //console.log(`c:${cacheUpdated}, s:${scheduleUpdated}`);

            // 十分に新しい
            if (new Date().getTime() - newestUpdated < REFRESH_INTERVAL) {
                // キャッシュのが新しい
                if (cacheUpdated > scheduleUpdated) {
                    const cache = storage.schedule.cache;
                    if (cache) {
                        drawSchedule(cache);
                        scheduleUpdated = cacheUpdated;
                        isRefreshing = false;
                        return;
                    }
                } else {
                    isRefreshing = false;
                    toggleDisplay("schedule");
                    return;
                }
            }
        } catch (ex) {
            console.log(ex);
            storage.schedule.clear();
        }

        toggleDisplay("dummy");

        GM_xmlhttpRequest({
            method: "GET",
            url: "https://api.holotools.app/v1/live?max_upcoming_hours=2190&hide_channel_desc=1",
            headers: { "user-agent": `${APPNAME}/${VERSION}` },
            onload: (context) => {
                // console.log(context);
                try {
                    const response = JSON.parse(context.responseText);
                    if (!response.message) {
                        storage.schedule.updated = scheduleUpdated = new Date().getTime();
                        storage.schedule.cache = response;

                        drawSchedule(response);
                    }
                } catch (ex) {
                    console.log(ex, context);
                } finally {
                    isRefreshing = false;
                }
            },
            onerror: setTimeoutToRefresh,
            onabort: setTimeoutToRefresh,
            ontimeout: setTimeoutToRefresh,
        });
    }

    function refreshArchive(force) {
        // console.log("refreshArchive()");
        if (isRefreshing) return;
        isRefreshing = true;

        if (force) {
            storage.archive.clear();
            archiveUpdated = 0;
        }

        try {
            const cacheUpdated = storage.archive.updated;
            const newestUpdated = Math.max(cacheUpdated, archiveUpdated);

            // console.log(`c:${cacheUpdated}, a:${archiveUpdated}`);

            // 十分に新しい
            if (new Date().getTime() - newestUpdated < REFRESH_INTERVAL) {
                // キャッシュのが新しい
                if (cacheUpdated > archiveUpdated) {
                    const cache = storage.archive.cache;
                    if (cache) {
                        drawArchive(cache);
                        archiveUpdated = cacheUpdated;
                        isRefreshing = false;
                        return;
                    }
                } else {
                    isRefreshing = false;
                    toggleDisplay("archive");
                    return;
                }
            }
        } catch (ex) {
            console.log(ex);

            storage.archive.clear();
        }

        const now = new Date().getTime();
        const offset = new Date().getTimezoneOffset() * 60 * 1000;

        const start = new Date();
        start.setTime(now - ARCHIVE_ENDHOURS - offset);
        const end = new Date();
        end.setTime(now - offset);

        // console.log(`${start.toISOString()}${end.toISOString()}`);
        toggleDisplay("dummy");

        GM_xmlhttpRequest({
            method: "GET",
            url: `https://api.holotools.app/v1/videos?start_date=${start.toISOString()}&end_date=${end.toISOString()}&limit=50&sort=live_schedule&order=desc`,
            headers: { "user-agent": `${APPNAME}/${VERSION}` },
            onload: (context) => {
                // console.log(context);
                try {
                    var response = JSON.parse(context.responseText);
                    if (!response.message) {
                        storage.archive.updated = archiveUpdated = new Date().getTime();
                        storage.archive.cache = response;
                        drawArchive(response);
                    }
                } catch (ex) {
                    console.log(ex, context);
                } finally {
                    isRefreshing = false;
                }

            },
            onerror: setTimeoutToRefresh,
            onabort: setTimeoutToRefresh,
            ontimeout: setTimeoutToRefresh,
        });
    }

    function setTimeoutToRefresh() {
        setTimeout(() => {
            isRefreshing = false;
        }, ERROR_TIMEOUT);
    }

    function toLiveDate(schedule, start, end, publish) {
        // console.log(schedule, start, end, publish);

        if (end) {
            const diff = new Date(end).getTime() - new Date(start).getTime();
            const dm = Math.floor(diff / 1000 / 60 % 60);
            const dh = Math.floor(diff / 1000 / 60 / 60);

            var span = "";
            if (dh > 0) {
                span += dh + "h ";
            }
            span += dm + "m";

            return span;
        }

        start = new Date(start || schedule || publish);
        const h = ("0" + start.getHours()).slice(-2);
        const m = ("0" + start.getMinutes()).slice(-2);
        if (start.getDate() != new Date().getDate()) {
            return `${start.getDate()}-${h}:${m}`;
        } else {
            return `${h}:${m}`;
        }
    }

    function onExitArchiveHome(ev) {
        if (ev.target.classList.contains("no-exit")) return;
        this.classList.add("hidden");
    }

    function onToggleMuteChannel() {
        var name = this.getAttribute("data-name");
        var fav = storage.holostats.favorite;
        if (fav[name] != null) {
            fav[name] = !fav[name];
            this.classList.toggle("mute", !fav[name]);
        }
        storage.holostats.favorite = fav;
        storage.holostats.updated = 0; // キャッシュ無効

        displayArchiveHome(true);
    }

    function onClickNextArchiveHome() {
        continueArchiveHome(2);
    }

    function onToggleSortArchiveHome() {
        archiveSortedByName = document.querySelector("#archive-home").toggleAttribute("order-name");
        document.querySelector("#archive-home").toggleAttribute("order-date", !archiveSortedByName);
        document.querySelectorAll("#archive-home .thumbnail, #archive-home .category").forEach((element) => element.remove());
        displayArchiveHome(true, 0);
        document.querySelector("#archive-home .next").classList.remove("hidden");
    }

    var holoStatsTimeout = 0;
    function setHoloStatsTimeout() {
        holoStatsTimeout = clearTimeout(holoStatsTimeout);
        holoStatsTimeout = setTimeout(() => {
            holoStatsTimeout = 0;
        }, 3000);
    }

    function selectArchiveCategory(category) {
        var target = document.querySelector(`#archive-home .category[data-name='${category}']`);
        if (target) return target;

        var div = document.createElement("div");
        div.classList.add("category");
        div.setAttribute("data-name", category);
        div.innerHTML = `<div class="header" data-name="${category}">${category}</div>`;
        document.querySelector("#archive-home .next").before(div);

        return div;
    }

    var archiveSortedByName = false;
    function buildArchiveHome(data) {
        if (!data) return;

        var stream;
        for (var i = 0; i < data.streams.length; i++) {
            stream = data.streams[i];
            if (!stream.startTime) continue;
            if (!storage.holostats.favorite[stream.vtuberId]) continue;

            var startDate = new Date(stream.startTime);
            var date = `${startDate.getMonth() + 1}/${startDate.getDate()}`;

            var category = selectArchiveCategory(archiveSortedByName ? stream.vtuberId : date);
            category.insertAdjacentHTML("beforeend", `<a class="thumbnail ${stream.status}"
                href="/watch?v=${stream.streamId}" data-date="${date}" data-name="${stream.vtuberId}" data-startTime="${stream.startTime}"
                style="background-image: url('https://img.youtube.com/vi/${stream.streamId}/mqdefault.jpg')"><span>${stream.title}</span></a>`);
            category.style.setProperty("--thumbnail-num", category.querySelectorAll(".thumbnail"));
        }
    }

    function continueArchiveHome(continueCount, isReload) {
        setHoloStatsTimeout();
        var c = storage.holostats.cache;

        if (isReload) {
            c = null;
            document.querySelectorAll("#archive-home .thumbnail, #archive-home .category").forEach((element) => element.remove());
            document.querySelector("#archive-home .loading").classList.remove("hidden");
        }
        document.querySelector("#archive-home .next").classList.add("hidden");

        loadHoloStats((context) => {
            var response = JSON.parse(context.responseText);
             // console.log(response);

            if (c) {
                c.streams = c.streams.concat(response.streams);
            } else {
                c = response;
            }
            storage.holostats.cache = c;
            buildArchiveHome(response);

            document.querySelector("#archive-home .loading").classList.add("hidden");
            if (continueCount-- > 0) {
                continueArchiveHome(continueCount);
            } else {
                document.querySelector("#archive-home .next").classList.remove("hidden");
            }
        }, setHoloStatsTimeout, c ? c.streams.slice(-1)[0].startTime : 0);
    }

    function displayArchiveHome(force) {
        if (!force && holoStatsTimeout > 0) return false;
        setHoloStatsTimeout();

        document.querySelectorAll("#archive-home .thumbnail, #archive-home .category").forEach((element) => element.remove());
        document.querySelector("#archive-home .next").classList.add("hidden");
        document.querySelector("#archive-home").classList.remove("hidden");

        if ((new Date().getTime() - storage.holostats.updated) < HOLOSTATS_INTERVAL) {
            buildArchiveHome(storage.holostats.cache);
            document.querySelector("#archive-home .next").classList.remove("hidden");
            return true;
        }

        storage.holostats.updated = new Date().getTime();
        storage.holostats.cache = null;

        continueArchiveHome(0, true);
    }

    function loadHoloStats(loaded, errored, endAt) {
        var ids = "";
        var f = storage.holostats.favorite;
        for (var member in f) {
            if (f[member]) {
                ids += member + ",";
            }
        }
        ids = ids.trim(",");

        var url = `https://holoapi.poi.cat/api/v4/youtube_streams?ids=${ids}&status=ended`;
        if (endAt > 0) {
            url += "&endAt=" + endAt;
        }
        // console.log(url);

        GM_xmlhttpRequest({
            method: "GET",
            url: url,
            headers: { "user-agent": `${APPNAME}/${VERSION}` },
            onload: loaded,
            onerror: errored,
            onabort: errored,
            ontimeout: errored,
        });
    }
})();