ShikiYear

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

Tendrás que instalar una extensión para tu navegador como Tampermonkey, Greasemonkey o Violentmonkey si quieres utilizar este script.

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

Tendrás que instalar una extensión como Tampermonkey o Violentmonkey para instalar este script.

Necesitarás instalar una extensión como Tampermonkey o Userscripts para instalar este script.

Tendrás que instalar una extensión como Tampermonkey antes de poder instalar este script.

Necesitarás instalar una extensión para administrar scripts de usuario si quieres instalar este script.

(Ya tengo un administrador de scripts de usuario, déjame instalarlo)

Advertisement:

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Tendrás que instalar una extensión como Stylus antes de poder instalar este script.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

Para poder instalar esto tendrás que instalar primero una extensión de estilos de usuario.

(Ya tengo un administrador de estilos de usuario, déjame instalarlo)

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);