Re:AcT Schedule Filter

Re:AcT Schedule にメンバー別のフィルター機能を追加する。

// ==UserScript==
// @name         Re:AcT Schedule Filter
// @namespace    https://github.com/AyeBee/ReAcTScheduleFilter
// @version      0.1
// @description  Re:AcT Schedule にメンバー別のフィルター機能を追加する。
// @author       ayebee
// @match        https://schedule.v-react.com/
// @icon         https://www.google.com/s2/favicons?sz=64&domain=v-react.com
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    // TODO: 公式チャンネルがこのままだと表示されない

    const makeCheckbox = (label, onChangeEvent) => {
        const control = document.createElement('input');
        control.type = 'checkbox';
        control.value = label;
        control.checked = true;
        if (onChangeEvent) {
            control.addEventListener('change', onChangeEvent);
        }

        const labelText = document.createElement('span');
        labelText.textContent = label;

        const wrapper = document.createElement('label');
        wrapper.style.paddingRight = '1em';
        wrapper.style.whiteSpace = 'nowrap';
        wrapper.append(control);
        wrapper.append(labelText);

        return [control, wrapper, labelText];
    };

    const toggleVisiblityAllStream = () => {
        const visible = Array.from(filters.querySelectorAll('input')).filter(i => i.checked).map(i => i.value);

        for (const row of document.querySelectorAll('.scheduleItemArea > .row')) {
            let visibleChildrenCount = row.children.length;

            for (const col of row.children) {
                // 配信者・参加者の一覧に, 表示対象の配信者が含まれない場合, その配信を非表示にする
                const streamers = Array.from(col.querySelectorAll('.item__appearances .appearancesIcon'))
                        .map(e => e.alt?.trim());
                if (streamers.filter(v => visible.includes(v)).length === 0) {
                    col.style.display = 'none';
                    visibleChildrenCount--;
                } else {
                    col.style.display = 'block';
                }
            }

            // 当日の配信が1件もない場合, その日の列を非表示にする
            if (visibleChildrenCount === 0) {
                row.parentNode.parentNode.style.display = 'none';
            } else {
                row.parentNode.parentNode.style.display = 'flex';
            }
        }
    };

    const visibleAllStreams = () => {
        for (const row of document.querySelectorAll('.scheduleItemArea > .row')) {
            for (const col of row.children) {
                col.style.display = 'block';
            }
            row.parentNode.parentNode.style.display = 'flex';
        }
    };

    const resize = () => {
        document.querySelector('.v-main__wrap > div').style.paddingBottom = filtersWrapper.clientHeight + 'px';
    };

    const filters = document.createElement('div');

    const [filterAll, filterAllLabel,] = makeCheckbox('すべて選択', e => {
        if (e.target.checked) {
            Array.from(filters.querySelectorAll('input')).forEach(item => item.checked = true);
        } else {
            Array.from(filters.querySelectorAll('input')).forEach(item => item.checked = false);
        }
        toggleVisiblityAllStream();
    });

    filters.append(filterAllLabel);

    [   '射貫まとい', '宇佐美ユノ', '獅子神レオナ', '花鋏キョウ', '水瓶ミア', '夢川かなう',
        '天川はの', '姫熊りぼん', '丸餅つきみ', '風海みかん', '月紫アリア', '猫乃ユキノ',
        '皇ロゼ', '九楽ライ', 'かしこまり', '魔光リサ', '彩雲のの', '稀羽すう',
        '紅龍イサナ', '美睡シュカ', 'ククルア・クレイユ', '音伽ねむ', '神輿たらん',
        /*
        '雷輝アンタレス', '鞍馬つむぎ', '琴みゆり', '綺羅星ウタ', '早乙女ぽえむ',
        '折緒コウヤ', '星屑れん', '美影ルクス', '湊音みなみ',
        '丑牡てぃあ', '双理マイ', '多々星シエル', '天秤はかり', '夜霧メイ',
        '皇噛ユカリ', '唯牙コハク', 'ゆにゆにこ', '瀧上りと', '白音ゆき', '黒音よみ',
        '犬望チロル',  '出雲めぐる', '久檻夜くぅ', '碧那アイル',
        '黄葉いおり', '輝澄カレン', '猫海せな',
        */
    ].forEach(item => {
        const [,wrapper,] = makeCheckbox(item, toggleVisiblityAllStream);
        filters.append(wrapper);
    });

    const [useFilteringCheckbox, useFilteringWrapper, useFilteringLabel] = makeCheckbox(
            'フィルタを無効にする ▼', e => {
        if (useFilteringCheckbox.checked) {
            useFilteringLabel.textContent = 'フィルタを無効にする ▼';
            filters.style.display = 'block';
            toggleVisiblityAllStream();
        } else {
            useFilteringLabel.textContent = 'フィルタを有効にする ▲';
            filters.style.display = 'none';
            visibleAllStreams();
        }
        resize();
    });
    useFilteringCheckbox.style.display = 'none';

    useFilteringWrapper.style.display = 'block';
    useFilteringWrapper.style.backgroundColor = '#272727';

    const filtersWrapper = document.createElement('div');
    filtersWrapper.style.fontSize = '12px';
    filtersWrapper.style.width = '100%';
    filtersWrapper.style.backgroundColor = '#333';
    filtersWrapper.style.boxShadow = '0 0px 10px 0 rgb(0 0 0 / 80%)';
    filtersWrapper.style.color = '#fff';
    filtersWrapper.style.position = 'fixed';
    filtersWrapper.style.bottom = 0;
    filtersWrapper.style.zIndex = 9999;
    filtersWrapper.append(useFilteringWrapper);
    filtersWrapper.append(filters);
    document.querySelector('.v-main__wrap > div').append(filtersWrapper);

    window.addEventListener('resize', resize);
    useFilteringCheckbox.click();
    resize();
})();