FANZAで作品を見る
); var cache = {}; // 画面の状態。s=コーナー、list=一覧の種類(''=トップ/rank/date/review/genre/search) var st = { s: S.sections[0].key, list: '', g: '', q: '', sort: 'rank', n: PAGE }; var cur = null; // 表示中のコーナーのデータ var $ = function (id) { return document.getElementById(id); }; var el = function (tag, cls, text) { var e = document.createElement(tag); if (cls) e.className = cls; if (text != null) e.textContent = text; return e; }; // 年齢確認(確認が済むまで作品は読み込まない) var KEY = 'fz_age_ok'; function ok() { try { return localStorage.getItem(KEY) === '1'; } catch (e) { return false; } } function start() { $('gate').hidden = true; $('app').hidden = false; init(); } var SORT = { rank: function (a, b) { return (a.r || 1e9) - (b.r || 1e9) || (a.d < b.d ? 1 : -1); }, date: function (a, b) { return a.d < b.d ? 1 : a.d > b.d ? -1 : (a.r || 1e9) - (b.r || 1e9); }, // 評価はレビュー3件以上の作品を上に、平均点の高い順 review: function (a, b) { return (b.rc >= 3) - (a.rc >= 3) || b.ra - a.ra || b.rc - a.rc; } }; var TITLE = { rank: '人気ランキング', date: '新着', review: '評価の高い作品' }; function load(key) { if (cache[key]) return Promise.resolve(cache[key]); return fetch('data/' + key + '.json?v=' + S.ver).then(function (r) { if (!r.ok) throw new Error(r.status); return r.json(); }).then(function (d) { var byId = {}, cnt = {}; var lim = new Date(Date.parse(d.updated.slice(0, 10)) - 7 * 864e5).toISOString().slice(0, 10); d.items.forEach(function (x) { byId[x.id] = x; x.k = [x.t, x.a.join(' '), x.m, x.g.join(' ')].join(' ').toLowerCase(); x.nw = x.d >= lim; x.g.forEach(function (n) { if (!META.test(n)) cnt[n] = (cnt[n] || 0) + 1; }); }); d.byId = byId; d.genres = Object.keys(cnt).filter(function (n) { return cnt[n] >= 6; }) .sort(function (a, b) { return cnt[b] - cnt[a] || a.localeCompare(b, 'ja'); }); return (cache[key] = d); }); } function card(x, rank) { var b = el('button', 'card'); b.type = 'button'; b.dataset.id = x.id; var ph = el('div', 'ph'); var im = el('img', x.w ? 'w' : ''); im.src = x.img; im.alt = ''; im.loading = 'lazy'; im.decoding = 'async'; ph.appendChild(im); if (rank && x.r) ph.appendChild(el('span', 'rk', x.r)); else if (x.nw) ph.appendChild(el('span', 'nw', 'NEW')); b.appendChild(ph); b.appendChild(el('p', 'ti', x.t)); var me = el('p', 'me'); if (x.rc >= 3) { me.appendChild(el('b', '', '★' + x.ra.toFixed(1))); me.appendChild(document.createTextNode(' ')); } var who = x.a.length ? x.a.slice(0, 2).join('・') + (x.a.length > 2 ? ' ほか' : '') : x.m; me.appendChild(document.createTextNode(who || '')); if (me.textContent) b.appendChild(me); return b; } function section(title, items, go, rank, id) { var s = el('section', 'sec'); if (id) s.id = id; var hd = el('div', 'hd'); hd.appendChild(el('h2', '', title)); var all = el('button', 'all', 'すべて見る ›'); all.type = 'button'; all.addEventListener('click', go); hd.appendChild(all); s.appendChild(hd); var row = el('div', 'row'); items.forEach(function (x) { row.appendChild(card(x, rank)); }); s.appendChild(row); return s; } function filtered() { var words = st.q.toLowerCase().split(/\s+/).filter(Boolean); return cur.items.filter(function (x) { if (st.g && x.g.indexOf(st.g) < 0) return false; for (var i = 0; i < words.length; i++) if (x.k.indexOf(words[i]) < 0) return false; return true; }); } function renderHome(v) { var by = function (k) { return cur.items.slice().sort(SORT[k]); }; v.appendChild(section(TITLE.rank, by('rank').filter(function (x) { return x.r; }).slice(0, ROW), function () { go({ list: 'rank', sort: 'rank' }); }, true)); v.appendChild(section(TITLE.date, by('date').slice(0, ROW), function () { go({ list: 'date', sort: 'date' }); })); var top = by('review').filter(function (x) { return x.rc >= 3; }).slice(0, ROW); if (top.length >= 6) v.appendChild(section(TITLE.review, top, function () { go({ list: 'review', sort: 'review' }); })); cur.genres.slice(0, GENRE_ROWS).forEach(function (g) { var items = cur.items.filter(function (x) { return x.g.indexOf(g) >= 0; }).sort(SORT.rank).slice(0, ROW); v.appendChild(section(g, items, function () { go({ list: 'genre', g: g, sort: 'rank' }); })); }); } function renderList(v) { var all = filtered().sort(SORT[st.sort]); var lh = el('div', 'lh'); var back = el('button', 'back', '‹ トップ'); back.type = 'button'; back.addEventListener('click', function () { $('q').value = ''; go({ list: '', g: '', q: '' }); }); lh.appendChild(back); lh.appendChild(el('h2', '', st.q ? '「' + st.q + '」の検索結果' : st.g || TITLE[st.list] || TITLE.rank)); lh.appendChild(el('span', 'n', all.length + ' 作品')); var seg = el('div', 'seg'); seg.setAttribute('role', 'group'); seg.setAttribute('aria-label', '並び順'); [['rank', '人気'], ['date', '新着'], ['review', '評価']].forEach(function (p) { var b = el('button', '', p[1]); b.type = 'button'; b.setAttribute('aria-pressed', String(st.sort === p[0])); b.addEventListener('click', function () { st.sort = p[0]; st.n = PAGE; render(); }); seg.appendChild(b); }); lh.appendChild(seg); v.appendChild(lh); if (!all.length) { v.appendChild(el('p', 'empty', '条件に合う作品がありません。')); return; } var g = el('div', 'grid'); var showRank = st.sort === 'rank' && !st.q && !st.g; all.slice(0, st.n).forEach(function (x) { g.appendChild(card(x, showRank)); }); v.appendChild(g); if (all.length > st.n) { var m = el('button', 'more', 'もっと見る'); m.type = 'button'; m.addEventListener('click', function () { st.n += PAGE; render(); }); v.appendChild(m); } } function renderRail() { var r = $('rail'); r.textContent = ''; cur.genres.forEach(function (g) { var b = el('button', 'chip', g); b.type = 'button'; b.setAttribute('aria-pressed', String(st.g === g)); b.addEventListener('click', function () { if (st.g === g) go({ list: '', g: '' }); else go({ list: 'genre', g: g, sort: 'rank' }); }); r.appendChild(b); }); var on = r.querySelector('[aria-pressed="true"]'); r.scrollLeft = on ? on.offsetLeft - 16 : 0; } function render() { var v = $('view'); v.textContent = ''; [].forEach.call($('tabs').children, function (b) { b.setAttribute('aria-selected', String(b.dataset.key === st.s)); }); renderRail(); if (st.list || st.q) renderList(v); else renderHome(v); } // 状態を変えて URL(#コーナー/種類/ジャンル)に残す。戻るボタンで前の画面に戻れる function go(p) { for (var k in p) st[k] = p[k]; st.n = PAGE; var h = '#' + st.s + (st.list ? '/' + st.list + (st.g ? '/' + encodeURIComponent(st.g) : '') : ''); if (location.hash !== h && !(h === '#' + S.sections[0].key && !location.hash)) history.pushState(null, '', h); show(true); } function parse() { var p = location.hash.slice(1).split('/'); var s = S.sections.filter(function (x) { return x.key === p[0]; })[0]; st.s = s ? s.key : S.sections[0].key; st.list = /^(rank|date|review|genre)$/.test(p[1] || '') ? p[1] : ''; st.g = st.list === 'genre' && p[2] ? decodeURIComponent(p[2]) : ''; if (st.list === 'genre' && !st.g) st.list = ''; st.sort = SORT[st.list] ? st.list : 'rank'; st.q = ''; $('q').value = ''; st.n = PAGE; } function show(top) { var key = st.s; if (!cache[key]) $('view').textContent = ''; load(key).then(function (d) { if (key !== st.s) return; cur = d; render(); if (top) scrollTo(0, 0); }).catch(function () { $('view').textContent = ''; $('view').appendChild(el('p', 'empty', '読み込めませんでした。時間をおいて開き直してください。')); }); } function row(dl, k, node) { dl.appendChild(el('dt', '', k)); var dd = el('dd'); if (typeof node === 'string') dd.textContent = node; else dd.appendChild(node); dl.appendChild(dd); } function open(x) { $('d-img').src = x.img; $('d-img').alt = x.t; $('d-t').textContent = x.t; $('d-go').href = x.u; var dl = $('d-meta'); dl.textContent = ''; if (x.d) row(dl, '配信開始', x.d.replace(/-/g, '/')); if (x.v) row(dl, '収録時間', x.v + '分'); if (x.a.length) row(dl, '出演', x.a.join('、')); if (x.m) row(dl, 'メーカー', x.m); if (x.rc) row(dl, 'レビュー', '★' + x.ra.toFixed(2) + '(' + x.rc + '件)'); var gs = x.g.filter(function (n) { return cur.genres.indexOf(n) >= 0; }); if (gs.length) { var t = el('div', 'tags'); gs.forEach(function (name) { var b = el('button', '', name); b.type = 'button'; b.dataset.genre = name; t.appendChild(b); }); row(dl, 'ジャンル', t); } var s = $('d-smp'); s.textContent = ''; x.s.forEach(function (u) { var im = el('img'); im.src = u; im.alt = ''; im.loading = 'lazy'; s.appendChild(im); }); var d = $('dlg'); if (!d.open) d.showModal(); d.querySelector('.dl').scrollTop = 0; } function close() { if ($('dlg').open) $('dlg').close(); } function init() { var tabs = $('tabs'); S.sections.forEach(function (s) { var b = el('button', '', s.label); b.type = 'button'; b.setAttribute('role', 'tab'); b.dataset.key = s.key; b.addEventListener('click', function () { $('q').value = ''; go({ s: s.key, list: '', g: '', q: '' }); }); tabs.appendChild(b); }); tabs.hidden = S.sections.length < 2; $('home').addEventListener('click', function () { $('q').value = ''; go({ list: '', g: '', q: '' }); }); var timer; $('q').addEventListener('input', function (e) { clearTimeout(timer); var val = e.target.value.trim(); timer = setTimeout(function () { st.q = val; st.n = PAGE; if (cur) { render(); } }, 150); }); $('view').addEventListener('click', function (e) { var b = e.target.closest('.card'); if (b && cur && cur.byId[b.dataset.id]) open(cur.byId[b.dataset.id]); }); $('d-x').addEventListener('click', close); $('dlg').addEventListener('click', function (e) { if (e.target === this) close(); }); $('d-meta').addEventListener('click', function (e) { var b = e.target.closest('button[data-genre]'); if (!b) return; close(); $('q').value = ''; go({ list: 'genre', g: b.dataset.genre, sort: 'rank', q: '' }); }); addEventListener('popstate', function () { close(); parse(); show(true); }); if (S.updated) $('upd').textContent = '最終更新:' + S.updated; $('demo').hidden = !S.sample; parse(); show(false); } // 定義がすべて済んでから始める if (ok()) start(); else { $('gate').hidden = false; $('yes').addEventListener('click', function () { try { localStorage.setItem(KEY, '1'); } catch (e) {} start(); }); } })();