ShikiYear

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

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

Advertisement:

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

ستحتاج إلى تثبيت إضافة مثل 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);