ShikiYear

Улучшает функционал поиска по годам

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

Advertisement:

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

Advertisement:

// ==UserScript==
// @name         ShikiYear
// @author       Librake
// @namespace    https://shikimori.io/Librake
// @version      1.0
// @description  Улучшает функционал поиска по годам
// @match        https://shikimori.rip/*
// @match        https://shikimori.one/*
// @match        https://shikimori.io/*
// @match        https://shiki.one/*
// @icon         https://goo.su/AlA5
// @grant        none
// @license      MIT
// ==/UserScript==



function ready(fn) {
    document.addEventListener('page:load', fn);
    document.addEventListener('turbolinks:load', fn);

    if (document.readyState !== 'loading') fn();
    else document.addEventListener('DOMContentLoaded', fn);
}

// ---------------- scroll ----------------

function saveScroll() {
    sessionStorage.setItem('shiki_scroll_y', window.scrollY.toString());
}

function restoreScroll() {
    const y = sessionStorage.getItem('shiki_scroll_y');
    if (y !== null) {
        window.scrollTo(0, parseInt(y, 10));
        sessionStorage.removeItem('shiki_scroll_y');
    }
}

// ---------------- URL ----------------

function parsePath(pathname) {
    const parts = pathname.split('/').filter(Boolean);

    const state = {
        kind: null,
        status: null,
        season: null,
        genre: null
    };

    for (let i = 0; i < parts.length; i++) {
        if (parts[i] === 'kind') state.kind = parts[i + 1];
        if (parts[i] === 'status') state.status = parts[i + 1];
        if (parts[i] === 'season') state.season = parts[i + 1];
        if (parts[i] === 'genre') state.genre = parts[i + 1];
    }

    return state;
}

function buildPath(state) {
    const parts = window.location.pathname.split('/').filter(Boolean);

    const filtered = [];

    for (let i = 0; i < parts.length; i++) {
        if (
            parts[i] === 'kind' ||
            parts[i] === 'status' ||
            parts[i] === 'season' ||
            parts[i] === 'genre' ||
            parts[i] === 'page'
        ) {
            i++;
            continue;
        }

        filtered.push(parts[i]);
    }

    if (state.kind) filtered.push('kind', state.kind);
    if (state.status) filtered.push('status', state.status);
    if (state.season) filtered.push('season', state.season);
    if (state.genre) filtered.push('genre', state.genre);

    return '/' + filtered.join('/');
}

// ---------------- seasons ----------------

const SEASONS = ['winter', 'spring', 'summer', 'fall'];

function getSeasonByMonth(month) {
    if (month >= 1 && month <= 3) return 0;
    if (month >= 4 && month <= 6) return 1;
    if (month >= 7 && month <= 9) return 2;
    return 3;
}

function getCurrentSeason() {
    const d = new Date();
    const idx = getSeasonByMonth(d.getMonth() + 1);
    return { index: idx, year: d.getFullYear() };
}

function shiftSeason(idx, year, delta) {
    let n = idx + delta;
    let y = year;

    if (n < 0) {
        n = 3;
        y--;
    }
    if (n > 3) {
        n = 0;
        y++;
    }

    return { index: n, year: y };
}

function makeSeasonKey(name, year) {
    return `${name}_${year}`;
}

// ---------------- main ----------------

function init() {
    if (document.querySelector('#shiki-year-panel')) return;

    const seasonLi = document.querySelector('li[data-field="season"]');
    if (!seasonLi) return;

    const container = seasonLi.closest('div.block');
    const list = container.querySelector('ul.anime-params');
    if (!container || !list) return;


    const hashMatch = location.hash.match(/^#timeseason=(current|prev|next)$/);

    if (hashMatch) {
        const now = getCurrentSeason();

        const seasonMap = {
            current: now,
            prev: shiftSeason(now.index, now.year, -1),
            next: shiftSeason(now.index, now.year, +1)
        };

        const s = seasonMap[hashMatch[1]];

        go(`${SEASONS[s.index]}_${s.year}`);
        return;
    }


    list.style.display = 'none';

    const ACCENT = '#7c5cff';

    const panel = document.createElement('div');
    panel.id = 'shiki-year-panel';

    Object.assign(panel.style, {
        marginTop: '6px',
        padding: '10px',
        borderRadius: '10px',
        background: 'rgba(20, 20, 30, 0.92)',
        border: '1px solid rgba(124, 92, 255, 0.35)',
        display: 'flex',
        flexDirection: 'column',
        gap: '10px',
        backdropFilter: 'blur(6px)'
    });

    // ---------------- top nav buttons ----------------

    const navRow = document.createElement('div');
    navRow.style.display = 'flex';
    navRow.style.gap = '6px';

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

    Object.assign(inputRow.style, {
        display: 'flex',
        alignItems: 'center',
        gap: '4px'
    });

    function smallBtn(text) {
        const b = btn(text);

        Object.assign(b.style, {
            width: '26px',
            minWidth: '26px',
            padding: '6px 0'
        });

        return b;
    }

    const decBtn = smallBtn('<');
    const incBtn = smallBtn('>');

    const input = document.createElement('input');

        input.addEventListener("keydown", (e) => {
        if (e.key === "Enter") {
            e.preventDefault();
            const y = getYear();
            if (!y) return;
            go(String(y));
        }
    });

    Object.assign(input, {
        placeholder: 'Year'
    });

    Object.assign(input.style, {
        width: '70px',
        padding: '7px 6px',
        textAlign: 'center',
        fontSize: '13px',
        borderRadius: '8px',
        border: '1px solid rgba(124, 92, 255, 0.4)',
        background: 'rgba(0,0,0,0.25)',
        color: '#fff',
        outline: 'none'
    });

    function styleInput(el) {
        Object.assign(el.style, {
            width: '70px',
            padding: '7px 6px',
            textAlign: 'center',
            fontSize: '13px',
            borderRadius: '8px',
            border: '1px solid rgba(124, 92, 255, 0.4)',
            background: 'rgba(0,0,0,0.25)',
            color: '#fff',
            outline: 'none'
        });
    }

    decBtn.onclick = () => updateYear(-1);
    incBtn.onclick = () => updateYear(1);

    inputRow.appendChild(decBtn);
    inputRow.appendChild(input);
    inputRow.appendChild(incBtn);

    const row = document.createElement('div');
    const mainButtonsColumnGap = 6;
    const mainButtonsRowGap = 6;

    row.style.display = 'flex';
    row.style.columnGap = `${mainButtonsColumnGap}px`;
    row.style.rowGap = `${mainButtonsRowGap}px`;
    row.style.flexWrap = 'wrap';

    let activeBtn = null;

    function setActive(btn) {
        if (activeBtn) {
            activeBtn.style.background = 'rgba(0,0,0,0.35)';
            activeBtn.style.color = '#ddd';
            activeBtn.style.border = '1px solid rgba(255,255,255,0.12)';
        }

        activeBtn = btn;

        if (activeBtn) {
            activeBtn.style.background = ACCENT;
            activeBtn.style.color = '#fff';
            activeBtn.style.border = '1px solid rgba(124, 92, 255, 0.8)';
        }
    }

    function getYear() {
        const y = parseInt(input.value.trim(), 10);
        return Number.isFinite(y) ? y : null;
    }

    function getWebpackRequire() {
        return new Promise(resolve => {

            window.webpackChunkshikimori.push([
                [Symbol()],
                {},
                r => resolve(r)
            ]);

        });
    }

    let shikiCatalogFilters = null;

    async function applyCatalogFiltersIO(url) {
        const req = window.__shikiReq;

        const ajax = req(68292).A;
        const tracker = req(9868).A;

        history.pushState(
            {turbolinks: true, url},
            '',
            url
        );

        const absoluteUrl =
            url.startsWith('http')
                ? url
                : location.origin + url;

        const response = await ajax.fetch(absoluteUrl);

        const data = response.data;

        const {
            content,
            title,
            notice,
            page,
            pages_count,
            JS_EXPORTS
        } = data;


        const $content = $(content);

        if (JS_EXPORTS) {
            tracker.track(
                structuredClone(JS_EXPORTS),
                $content
            );
        }

        $('.l-content')
            .html($content)
            .process();

        if (title) {
            $('.head h1').html(title);
            document.title = title;
        }

        if (notice) {
            $('.head .notice').html(notice);
        }

        $('.pagination .link-current')
            .html(page);

        $('.pagination .link-total')
            .html(pages_count);

        $('.l-content')
            .trigger('ajax:success');

        console.log(
            'catalog updated',
            page,
            '/',
            pages_count
        );
    }

    async function applyFilters(url, sync = true) {
        const target = url.pathname + url.search;

        try {
            if (typeof window.applyCatalogFilters === 'function') {
                window.applyCatalogFilters(target);
                if (sync) syncFromUrl();
                return;
            }
        } catch (e) {
        }

        if (window.Turbolinks && typeof window.Turbolinks.visit === 'function') {
            window.Turbolinks.visit(target);
            return;
        }

        window.location.assign(target);

        //await applyCatalogFiltersIO(target);
        //syncFromUrl();
    }

    function go(seasonValue) {
        saveScroll();

        const url = new URL(window.location.href);
        const state = parsePath(url.pathname);

        state.season = seasonValue;

        url.pathname = buildPath(state);

        applyFilters(url);
    }

    function updateYear(delta) {
        const y = (getYear() ?? new Date().getFullYear()) + delta;
        input.value = y;

        const state = parsePath(window.location.pathname);

        if (!state.season) return;

        saveScroll();

        const url = new URL(window.location.href);

        const m = state.season.match(/^(spring|summer|fall|winter)_(\d{4})$/);

        if (m) {
            state.season = `${m[1]}_${y}`;
        } else if (/^\d{4}$/.test(state.season)) {
            state.season = String(y);
        } else {
            return;
        }

        url.pathname = buildPath(state);
        applyFilters(url);
    }

    function clearSeason() {
        saveScroll();

        const url = new URL(window.location.href);
        const state = parsePath(url.pathname);

        state.season = null;

        url.pathname = buildPath(state);
        applyFilters(url);
    }

    function btn(text, paddingX = 10, paddingY = 6) {
        const b = document.createElement('button');

        b.textContent = text;

        Object.assign(b.style, {
            fontSize: '12px',
            padding: `${paddingY}px ${paddingX}px`,
            cursor: 'pointer',
            borderRadius: '8px',
            border: '1px solid rgba(255,255,255,0.12)',
            background: 'rgba(0,0,0,0.35)',
            color: '#ddd',
            transition: '0.15s ease'
        });

        b.onmouseenter = () => {
            if (b !== activeBtn) {
                b.style.border = `1px solid ${ACCENT}`;
                b.style.color = '#fff';
            }
        };

        b.onmouseleave = () => {
            if (b !== activeBtn) {
                b.style.border = '1px solid rgba(255,255,255,0.12)';
                b.style.color = '#ddd';
            }
        };

        return b;
    }


    function applySeason(season, year) {
        go(`${season}_${year}`);
    }

    // ---------------- nav prev/current/next ----------------

    const now = getCurrentSeason();
    const prev = shiftSeason(now.index, now.year, -1);
    const next = shiftSeason(now.index, now.year, +1);

    const navButtons = {
        prev: btn(`Пред`, 15),
        current: btn(`Текущий`, 15),
        next: btn(`След`, 15)
    };

    navButtons.prev.onclick = () => applySeason(SEASONS[prev.index], prev.year);
    navButtons.current.onclick = () => applySeason(SEASONS[now.index], now.year);
    navButtons.next.onclick = () => applySeason(SEASONS[next.index], next.year);

    navRow.appendChild(navButtons.prev);
    navRow.appendChild(navButtons.current);
    navRow.appendChild(navButtons.next);

    panel.appendChild(navRow);

    // ---------------- season buttons ----------------

    const buttons = {};

    function applySeasonFromInput(season) {
        const y = getYear();
        if (!y) return;
        go(`${season}_${y}`);
    }

    buttons.spring = btn('Весна', 8);
    buttons.spring.onclick = () => applySeasonFromInput('spring');

    buttons.summer = btn('Лето', 8);
    buttons.summer.onclick = () => applySeasonFromInput('summer');

    buttons.fall = btn('Осень', 8);
    buttons.fall.onclick = () => applySeasonFromInput('fall');

    buttons.winter = btn('Зима', 8);
    buttons.winter.onclick = () => applySeasonFromInput('winter');

    buttons.yearOnly = btn('Год', 29);
    buttons.yearOnly.onclick = () => {
        const y = getYear();
        if (!y) return;
        go(String(y));
    };

    buttons.period = btn('Период', 40, 3);

    buttons.clear = btn('Сброс', 23, 3);
    buttons.clear.onclick = () => clearSeason();

    const periodPanelGap = 10;
    const periodActionGap = 6;
    const periodLabelOffset = 20;
    const periodModeKey = 'shiki_year_period_mode';

    const periodPanel = document.createElement('div');
    Object.assign(periodPanel.style, {
        display: 'none',
        flexDirection: 'column',
        gap: `${periodPanelGap}px`
    });

    function periodInputRow(labelText, field) {
        const dec = smallBtn('<');
        const inc = smallBtn('>');

        dec.onclick = () => updatePeriodYear(field, -1);
        inc.onclick = () => updatePeriodYear(field, 1);

        const r = document.createElement('div');
        Object.assign(r.style, {
            display: 'flex',
            alignItems: 'center',
            gap: '4px'
        });

        const label = document.createElement('span');
        label.textContent = labelText;
        Object.assign(label.style, {
            minWidth: '24px',
            marginLeft: `${periodLabelOffset}px`,
            fontSize: '12px',
            color: '#ddd'
        });

        r.appendChild(label);
        r.appendChild(dec);
        r.appendChild(field);
        r.appendChild(inc);
        return r;
    }

    const periodFrom = document.createElement('input');
    const periodTo = document.createElement('input');

    Object.assign(periodFrom, { placeholder: 'Year' });
    Object.assign(periodTo, { placeholder: 'Year' });
    styleInput(periodFrom);
    styleInput(periodTo);

    const periodApplyRow = document.createElement('div');
    periodApplyRow.style.display = 'flex';
    periodApplyRow.style.gap = '6px';

    const periodApply = btn('Применить', 23);

    const periodBottomRow = document.createElement('div');
    periodBottomRow.style.display = 'flex';
    periodBottomRow.style.gap = '6px';
    periodBottomRow.style.marginTop = `${periodActionGap - periodPanelGap}px`;

    const periodBack = btn('Назад', 23, 3);
    const periodClear = btn(buttons.clear.textContent, 23, 3);

    periodApplyRow.appendChild(periodApply);
    periodBottomRow.appendChild(periodBack);
    periodBottomRow.appendChild(periodClear);

    periodPanel.appendChild(periodInputRow('от:', periodFrom));
    periodPanel.appendChild(periodInputRow('до:', periodTo));
    periodPanel.appendChild(periodApplyRow);
    periodPanel.appendChild(periodBottomRow);

    function getPeriodYear(field) {
        const y = parseInt(field.value.trim(), 10);
        return Number.isFinite(y) ? y : null;
    }

    function updatePeriodYear(field, delta) {
        const currentYear = new Date().getFullYear();
        const from = getPeriodYear(periodFrom);
        const to = getPeriodYear(periodTo);
        const y = (getPeriodYear(field) ?? currentYear) + delta;

        if (field === periodFrom) {
            periodFrom.value = y;

            if (to !== null && y > to) {
                periodTo.value = y;
            }

            return;
        }

        periodTo.value = y;

        if (from !== null && y < from) {
            periodFrom.value = y;
        }
    }

    function setFixedButtonWidth(button, width) {
        button.style.width = `${width}px`;
        button.style.minWidth = `${width}px`;
        button.style.boxSizing = 'border-box';
        button.style.flex = `0 0 ${width}px`;
    }

    function syncPeriodLayout() {
        const periodWidth = buttons.period.offsetWidth;
        const clearWidth = buttons.clear.offsetWidth;
        const gap = 6;

        if (!periodWidth || !clearWidth) return;

        const totalWidth = periodWidth + clearWidth + gap;

        periodApplyRow.style.width = `${totalWidth}px`;
        periodBottomRow.style.width = `${totalWidth}px`;

        setFixedButtonWidth(periodBack, periodWidth);
        setFixedButtonWidth(periodClear, clearWidth);
        setFixedButtonWidth(periodApply, totalWidth);
    }

    function fillPeriodFromUrl() {
        const state = parsePath(window.location.pathname);
        const m = state.season?.match(/^(\d{4})_(\d{4})$/);

        if (m) {
            periodFrom.value = m[1];
            periodTo.value = m[2];
            return;
        }

        const y = getYear() ?? new Date().getFullYear();
        periodFrom.value = y;
        periodTo.value = y;
    }

    function setPeriodMode(enabled) {
        if (enabled) syncPeriodLayout();

        sessionStorage.setItem(periodModeKey, enabled ? '1' : '0');

        navRow.style.display = enabled ? 'none' : 'flex';
        inputRow.style.display = enabled ? 'none' : 'flex';
        row.style.display = enabled ? 'none' : 'flex';
        periodPanel.style.display = enabled ? 'flex' : 'none';

        if (enabled) {
            fillPeriodFromUrl();
            setActive(buttons.period);
        } else {
            syncFromUrl();
        }
    }

    function applyPeriod() {
        const from = getPeriodYear(periodFrom);
        const to = getPeriodYear(periodTo);
        if (!from || !to) return;
        sessionStorage.setItem(periodModeKey, '1');
        go(`${from}_${to}`);
    }

    buttons.period.onclick = () => setPeriodMode(true);
    periodBack.onclick = () => setPeriodMode(false);
    periodClear.onclick = () => clearSeason();
    periodApply.onclick = () => applyPeriod();

    [periodFrom, periodTo].forEach(field => {
        field.addEventListener('keydown', (e) => {
            if (e.key === 'Enter') {
                e.preventDefault();
                applyPeriod();
            }
        });
    });


    row.appendChild(buttons.winter);
    row.appendChild(buttons.spring);
    row.appendChild(buttons.summer);
    row.appendChild(buttons.fall);
    row.appendChild(buttons.period);
    row.appendChild(buttons.clear);

    inputRow.appendChild(buttons.yearOnly);


    panel.appendChild(inputRow);
    panel.appendChild(row);
    panel.appendChild(periodPanel);

    function highlightNav(state) {
        const match = state.season?.match(/(spring|summer|fall|winter)_(\d{4})/);
        const y = new Date().getFullYear();

        const current = getCurrentSeason();
        const prevS = shiftSeason(current.index, current.year, -1);
        const nextS = shiftSeason(current.index, current.year, +1);

        const key = match ? `${match[1]}_${match[2]}` : null;

        const prevKey = `${SEASONS[prevS.index]}_${prevS.year}`;
        const curKey = `${SEASONS[current.index]}_${current.year}`;
        const nextKey = `${SEASONS[nextS.index]}_${nextS.year}`;

        const map = {
            [prevKey]: navButtons.prev,
            [curKey]: navButtons.current,
            [nextKey]: navButtons.next
        };

        Object.values(navButtons).forEach(b => {
            b.style.background = 'rgba(0,0,0,0.35)';
            b.style.border = '1px solid rgba(255,255,255,0.12)';
            b.style.color = '#ddd';
        });

        const active = key ? map[key] : null;

        if (active) {
            active.style.background = ACCENT;
            active.style.border = `1px solid ${ACCENT}`;
            active.style.color = '#fff';
        }
    }

    function isPeriodSeason(season) {
        return /^\d{4}_\d{4}$/.test(season ?? '');
    }

    function syncFromUrl() {
        const state = parsePath(window.location.pathname);

        let active = null;

        if (state.season) {
            const m = state.season.match(/(spring|summer|fall|winter)_(\d{4})/);

            if (m) {
                input.value = m[2];

                active = {
                    spring: buttons.spring,
                    summer: buttons.summer,
                    fall: buttons.fall,
                    winter: buttons.winter
                }[m[1]];
            } else if (/^\d{4}$/.test(state.season)) {
                input.value = state.season;
                active = buttons.yearOnly;
            } else if (isPeriodSeason(state.season)) {
                const period = state.season.split('_');
                input.value = period[0];
                periodFrom.value = period[0];
                periodTo.value = period[1];
                active = buttons.period;
            } else {
                input.value = new Date().getFullYear();
                active = null;
            }
        } else {
                input.value = new Date().getFullYear();
                active = null;
            }

        setActive(active);
        highlightNav(state);
    }

    container.querySelector('div.subheadline.m5')?.after(panel);

    syncFromUrl();

    const state = parsePath(window.location.pathname);
    if (
        sessionStorage.getItem(periodModeKey) === '1' ||
        isPeriodSeason(state.season)
    ) {
        setPeriodMode(true);
    }

    restoreScroll();
}

ready(init);