Greasy Fork is available in English.
Улучшает функционал поиска по годам
// ==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);