Files
meme-api/public/index.html
shumengya 11d1be285f chore: 移除投稿相关文档与说明
- 删除 CONTRIBUTING.md

- README / blog-post / CHANGELOG / ASSET_LICENSE:移除所有投稿、贡献者、素材来源声明等表述

- .github/ISSUE_TEMPLATE / PULL_REQUEST_TEMPLATE:移除素材投稿选项、CONTRIBUTING.md 引用、素材来源声明

- public/readme/index.html:移除投稿指南链接与 CONTRIBUTING.md 引用

- public/index.html:补充 favicon、og:meta、GitHub 顶栏链接(上次未推送的改进)
2026-06-06 16:57:25 +08:00

956 lines
30 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-Hans">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>萌芽表情包图库(=・ω・=)</title>
<meta name="description" content="一个公开的表情包图库与 API分类浏览、一键复制链接 / Markdown / HTML51 个分类共 2600+ 张 WebP / GIF。" />
<meta name="theme-color" content="#FFCC4D" />
<meta property="og:title" content="萌芽表情包图库" />
<meta property="og:description" content="一个公开的表情包图库与 API分类浏览、一键复制链接 / Markdown / HTML。" />
<meta property="og:type" content="website" />
<link rel="icon" type="image/svg+xml" href="data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><rect width='100' height='100' rx='20' fill='%23FFCC4D'/><text x='50' y='74' text-anchor='middle' font-size='72' font-family='sans-serif' fill='%23000'>萌</text></svg>" />
<style>
:root {
color-scheme: light dark;
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto,
"PingFang SC", "Microsoft YaHei", sans-serif;
line-height: 1.45;
--bg: Canvas;
--fg: CanvasText;
--card: color-mix(in oklab, Canvas 94%, CanvasText 6%);
--border: color-mix(in oklab, Canvas 82%, CanvasText 18%);
--accent: color-mix(in oklab, CanvasText 12%, Canvas 88%);
--sidebar-w: 15rem;
--sidebar-w-collapsed: 2.75rem;
--topbar-h: 4.25rem;
}
body {
margin: 0;
background: var(--bg);
color: var(--fg);
min-height: 100vh;
display: flex;
flex-direction: column;
}
body.sidebar-collapsed {
--sidebar-w: var(--sidebar-w-collapsed);
}
.top-bar {
flex-shrink: 0;
padding: 0.85rem clamp(0.75rem, 2vw, 1.25rem);
border-bottom: 1px solid var(--border);
position: sticky;
top: 0;
background: color-mix(in oklab, var(--bg) 88%, transparent);
backdrop-filter: blur(10px);
z-index: 4;
}
.top-bar h1 {
font-size: 1.2rem;
margin: 0 0 0.25rem;
font-weight: 650;
}
.sub {
margin: 0;
font-size: 0.9rem;
opacity: 0.78;
}
.sub a {
color: inherit;
}
.layout {
display: flex;
flex: 1;
min-height: 0;
align-items: stretch;
}
.sidebar {
flex: 0 0 var(--sidebar-w);
width: var(--sidebar-w);
display: flex;
flex-direction: column;
border-right: 1px solid var(--border);
background: color-mix(in oklab, var(--bg) 96%, CanvasText 4%);
position: sticky;
top: var(--topbar-h);
height: calc(100vh - var(--topbar-h));
transition: width 0.22s ease, flex-basis 0.22s ease;
overflow: hidden;
z-index: 3;
}
.sidebar-head {
flex-shrink: 0;
display: flex;
align-items: center;
gap: 0.35rem;
padding: 0.5rem 0.45rem;
border-bottom: 1px solid var(--border);
}
.sidebar-toggle {
flex: 0 0 auto;
display: inline-flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
padding: 0;
border: 1px solid var(--border);
border-radius: 8px;
background: transparent;
color: inherit;
cursor: pointer;
font: inherit;
font-size: 1.1rem;
line-height: 1;
-webkit-tap-highlight-color: transparent;
}
.sidebar-toggle:hover {
background: color-mix(in oklab, var(--accent) 65%, transparent);
}
.sidebar-toggle:focus-visible {
outline: 2px solid color-mix(in oklab, CanvasText 40%, transparent);
outline-offset: 2px;
}
.sidebar-title {
font-size: 0.82rem;
font-weight: 600;
opacity: 0.72;
white-space: nowrap;
overflow: hidden;
transition: opacity 0.15s ease;
}
body.sidebar-collapsed .sidebar-title {
opacity: 0;
width: 0;
overflow: hidden;
}
.sidebar-search {
flex-shrink: 0;
padding: 0 0.45rem 0.4rem;
}
body.sidebar-collapsed .sidebar-search {
display: none;
}
.cat-search-input {
width: 100%;
box-sizing: border-box;
font: inherit;
font-size: 0.82rem;
padding: 0.38rem 0.55rem;
border-radius: 8px;
border: 1px solid var(--border);
background: var(--bg);
color: inherit;
}
.cat-search-input::placeholder {
opacity: 0.55;
}
.cat-search-input:focus {
outline: 2px solid color-mix(in oklab, CanvasText 28%, transparent);
outline-offset: 1px;
}
.cat-tabs-empty {
margin: 0.25rem 0.5rem;
font-size: 0.82rem;
opacity: 0.65;
}
.cat-tabs {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.15rem;
overflow-x: hidden;
overflow-y: auto;
padding: 0.45rem;
margin: 0;
scrollbar-width: thin;
-webkit-overflow-scrolling: touch;
}
.cat-tab {
flex: 0 0 auto;
width: 100%;
box-sizing: border-box;
font: inherit;
font-size: 0.84rem;
padding: 0.42rem 0.55rem;
border-radius: 8px;
border: 1px solid transparent;
background: transparent;
color: inherit;
cursor: pointer;
text-align: left;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
transition: background 0.12s ease, border-color 0.12s ease, opacity 0.15s ease;
-webkit-tap-highlight-color: transparent;
}
body.sidebar-collapsed .cat-tab {
opacity: 0;
pointer-events: none;
height: 0;
padding: 0;
margin: 0;
border: none;
overflow: hidden;
}
.cat-tab:hover {
background: color-mix(in oklab, var(--accent) 65%, transparent);
}
.cat-tab[aria-selected="true"] {
font-weight: 650;
border-color: color-mix(in oklab, CanvasText 22%, var(--border));
background: color-mix(in oklab, var(--accent) 100%, transparent);
}
.cat-tab:focus-visible {
outline: 2px solid color-mix(in oklab, CanvasText 40%, transparent);
outline-offset: 2px;
}
.content {
flex: 1;
min-width: 0;
}
main {
padding: 1.25rem clamp(0.5rem, 2vw, 1rem) 3rem;
max-width: none;
margin: 0 auto;
width: 100%;
box-sizing: border-box;
}
.sidebar-overlay {
display: none;
position: fixed;
inset: var(--topbar-h) 0 0 0;
z-index: 2;
border: none;
padding: 0;
margin: 0;
width: 100%;
background: color-mix(in oklab, CanvasText 32%, transparent);
cursor: pointer;
}
@media (max-width: 640px) {
:root {
--sidebar-w: min(78vw, 15rem);
}
.layout {
display: block;
}
.sidebar {
position: fixed;
left: 0;
top: var(--topbar-h);
flex: none;
}
body.sidebar-collapsed .sidebar {
box-shadow: none;
}
body:not(.sidebar-collapsed) .sidebar {
box-shadow: 4px 0 24px color-mix(in oklab, CanvasText 18%, transparent);
}
body:not(.sidebar-collapsed) .sidebar-overlay {
display: block;
}
.content {
width: 100%;
}
}
/* 手机:每行固定 4 列 */
.grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 0.4rem 0.35rem;
}
/* 电脑:每行最多 12 列,占满可用宽度 */
@media (min-width: 769px) {
.grid {
grid-template-columns: repeat(12, minmax(0, 1fr));
gap: 0.55rem 0.5rem;
}
}
.tile {
display: block;
width: 100%;
border: none;
margin: 0;
padding: 0;
border-radius: 0;
background: transparent;
cursor: pointer;
text-align: center;
transition: opacity 0.15s ease;
-webkit-tap-highlight-color: transparent;
}
.tile:hover {
opacity: 0.88;
}
.tile:focus-visible {
outline: 2px solid color-mix(in oklab, CanvasText 40%, transparent);
outline-offset: 3px;
}
.tile img {
display: block;
width: 100%;
aspect-ratio: 1;
height: auto;
object-fit: contain;
vertical-align: middle;
border-radius: 0;
background: transparent;
}
.tile .label {
margin-top: 0.2rem;
font-size: 0.68rem;
opacity: 0.55;
word-break: break-all;
line-height: 1.2;
}
.toast {
position: fixed;
left: 50%;
bottom: 1.25rem;
transform: translateX(-50%) translateY(120%);
opacity: 0;
padding: 0.6rem 1rem;
border-radius: 10px;
background: color-mix(in oklab, CanvasText 88%, Canvas 12%);
color: Canvas;
font-size: 0.9rem;
transition: transform 0.22s ease, opacity 0.22s ease;
z-index: 9;
pointer-events: none;
max-width: min(92vw, 420px);
text-align: center;
}
.toast.show {
transform: translateX(-50%) translateY(0);
opacity: 1;
}
.error {
padding: 1rem;
border-radius: 10px;
border: 1px solid var(--border);
background: var(--card);
}
.empty {
opacity: 0.8;
padding: 1rem 0;
}
.sheet-root {
position: fixed;
inset: 0;
z-index: 50;
display: flex;
align-items: flex-end;
justify-content: center;
padding: env(safe-area-inset-bottom, 0) 0.75rem 0.75rem;
box-sizing: border-box;
}
.sheet-root[hidden] {
display: none !important;
}
.sheet-backdrop {
position: absolute;
inset: 0;
background: color-mix(in oklab, CanvasText 38%, transparent);
border: none;
padding: 0;
cursor: pointer;
}
.sheet-panel {
position: relative;
width: min(22rem, 100%);
max-height: min(85vh, 28rem);
overflow: auto;
border-radius: 14px 14px 0 0;
background: var(--bg);
color: var(--fg);
border: 1px solid var(--border);
box-shadow: 0 -8px 32px color-mix(in oklab, CanvasText 22%, transparent);
padding: 0.65rem 0;
margin-bottom: 0;
}
@media (min-width: 480px) {
.sheet-root {
align-items: center;
padding: 1rem;
}
.sheet-panel {
border-radius: 14px;
margin-bottom: 0;
}
}
.sheet-title {
margin: 0;
padding: 0.35rem 1rem 0.65rem;
font-size: 0.82rem;
font-weight: 600;
opacity: 0.85;
word-break: break-all;
border-bottom: 1px solid var(--border);
}
.sheet-btn {
display: block;
width: 100%;
text-align: left;
font: inherit;
font-size: 0.95rem;
padding: 0.75rem 1rem;
border: none;
background: transparent;
color: inherit;
cursor: pointer;
transition: background 0.12s ease;
-webkit-tap-highlight-color: transparent;
}
.sheet-btn:hover,
.sheet-btn:focus-visible {
background: color-mix(in oklab, var(--accent) 85%, transparent);
}
.sheet-btn:active {
opacity: 0.92;
}
.sheet-cancel {
margin-top: 0.25rem;
border-top: 1px solid var(--border);
text-align: center;
font-weight: 500;
opacity: 0.78;
}
.sheet-size-bar {
padding: 0.5rem 1rem 0.65rem;
border-bottom: 1px solid var(--border);
}
.sheet-size-label {
display: block;
font-size: 0.72rem;
opacity: 0.68;
margin-bottom: 0.45rem;
}
.sheet-size-btns {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
}
.sheet-size-btn {
font: inherit;
font-size: 0.8rem;
font-weight: 600;
padding: 0.35rem 0.55rem;
min-width: 2.75rem;
border-radius: 8px;
border: 1px solid var(--border);
background: transparent;
color: inherit;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
}
.sheet-size-btn.is-active {
border-color: color-mix(in oklab, CanvasText 28%, var(--border));
background: color-mix(in oklab, var(--accent) 100%, transparent);
}
.sheet-size-btn:focus-visible {
outline: 2px solid color-mix(in oklab, CanvasText 40%, transparent);
outline-offset: 2px;
}
</style>
</head>
<body>
<header class="top-bar">
<h1>萌芽表情包图库ヘ(=^・ω・^=)ノ</h1>
<p class="sub">
<a href="/readme/">说明</a>
·
<a href="/memes.json"><code>memes.json</code></a>
·
<a href="https://github.com/shumengya/meme-api" rel="noopener" target="_blank">GitHub</a>
</p>
</header>
<button type="button" class="sidebar-overlay" id="sidebar-overlay" hidden aria-label="关闭分类"></button>
<div class="layout">
<aside class="sidebar" id="sidebar">
<div class="sidebar-head">
<button
type="button"
class="sidebar-toggle"
id="sidebar-toggle"
aria-expanded="true"
aria-controls="cat-tabs"
title="收起分类"
>
<span aria-hidden="true"></span>
</button>
<span class="sidebar-title">分类</span>
</div>
<div class="sidebar-search">
<input
type="search"
class="cat-search-input"
id="cat-search"
placeholder="搜索分类…"
autocomplete="off"
enterkeyhint="search"
/>
</div>
<nav class="cat-tabs" id="cat-tabs" role="tablist" aria-label="表情包分类"></nav>
</aside>
<div class="content">
<main id="app">
<p class="empty">加载中…</p>
</main>
</div>
</div>
<div id="toast" class="toast" role="status" aria-live="polite"></div>
<div id="sheet-root" class="sheet-root" hidden>
<button type="button" class="sheet-backdrop" id="sheet-backdrop" aria-label="关闭"></button>
<div class="sheet-panel" role="dialog" aria-modal="true" aria-labelledby="sheet-title">
<p id="sheet-title" class="sheet-title"></p>
<button type="button" class="sheet-btn" id="act-url">复制 url链接</button>
<button type="button" class="sheet-btn" id="act-md">复制 Markdown</button>
<div class="sheet-size-bar" id="sheet-size-bar" role="group" aria-label="HTML 宽度">
<span class="sheet-size-label">HTML 宽度</span>
<div class="sheet-size-btns">
<button type="button" class="sheet-size-btn" data-w="96" aria-pressed="false">96</button>
<button type="button" class="sheet-size-btn" data-w="128" aria-pressed="false">128</button>
<button type="button" class="sheet-size-btn" data-w="160" aria-pressed="false">160</button>
<button type="button" class="sheet-size-btn" data-w="200" aria-pressed="false">200</button>
<button type="button" class="sheet-size-btn" data-w="240" aria-pressed="false">240</button>
<button type="button" class="sheet-size-btn" data-w="280" aria-pressed="false">280</button>
<button type="button" class="sheet-size-btn" data-w="320" aria-pressed="false">320</button>
<button type="button" class="sheet-size-btn" data-w="400" aria-pressed="false">400</button>
<button type="button" class="sheet-size-btn" data-w="original" aria-pressed="false">原图</button>
</div>
</div>
<button type="button" class="sheet-btn" id="act-html">复制 HTML</button>
<button type="button" class="sheet-btn" id="act-download">下载</button>
<button type="button" class="sheet-btn sheet-cancel" id="act-close">取消</button>
</div>
</div>
<script>
(function () {
const app = document.getElementById("app");
const tabsEl = document.getElementById("cat-tabs");
const sidebarToggle = document.getElementById("sidebar-toggle");
const sidebarOverlay = document.getElementById("sidebar-overlay");
const toastEl = document.getElementById("toast");
var SIDEBAR_KEY = "meme_sidebar_collapsed";
var categoryFilter = "";
function toAbsoluteUrl(url) {
if (!url) return url;
if (/^https?:\/\//i.test(url)) return url;
try {
return new URL(url, location.href).href;
} catch (e) {
return url;
}
}
function isSidebarCollapsed() {
return document.body.classList.contains("sidebar-collapsed");
}
function syncSidebarUI() {
if (!sidebarToggle) return;
var collapsed = isSidebarCollapsed();
sidebarToggle.setAttribute("aria-expanded", collapsed ? "false" : "true");
sidebarToggle.title = collapsed ? "展开分类" : "收起分类";
var icon = sidebarToggle.querySelector("span");
if (icon) icon.textContent = collapsed ? "" : "";
if (sidebarOverlay) sidebarOverlay.hidden = collapsed;
}
function setSidebarCollapsed(collapsed) {
document.body.classList.toggle("sidebar-collapsed", collapsed);
syncSidebarUI();
try {
localStorage.setItem(SIDEBAR_KEY, collapsed ? "1" : "0");
} catch (e) {}
}
function loadSidebar() {
var collapsed = false;
try {
if (window.matchMedia("(max-width: 640px)").matches) {
collapsed = localStorage.getItem(SIDEBAR_KEY) !== "0";
} else {
collapsed = localStorage.getItem(SIDEBAR_KEY) === "1";
}
} catch (e) {
collapsed = window.matchMedia("(max-width: 640px)").matches;
}
setSidebarCollapsed(collapsed);
}
if (sidebarToggle) {
sidebarToggle.addEventListener("click", function () {
setSidebarCollapsed(!isSidebarCollapsed());
});
}
if (sidebarOverlay) {
sidebarOverlay.addEventListener("click", function () {
setSidebarCollapsed(true);
});
}
loadSidebar();
const sheetRoot = document.getElementById("sheet-root");
const sheetTitle = document.getElementById("sheet-title");
const sheetBackdrop = document.getElementById("sheet-backdrop");
/** @type {{ url: string; file: string; category: string } | null} */
var sheetCtx = null;
/** @type {number | null} null = 原图尺寸 */
var selectedWidthPx = 240;
var WIDTH_STORAGE = "meme_copy_img_width";
var VALID_HTML_WIDTHS = [96, 128, 160, 200, 240, 280, 320, 400];
function loadSavedWidth() {
try {
var v = localStorage.getItem(WIDTH_STORAGE);
if (v === "original") selectedWidthPx = null;
else {
var n = parseInt(v, 10);
selectedWidthPx =
VALID_HTML_WIDTHS.indexOf(n) >= 0 ? n : 240;
}
} catch (e) {
selectedWidthPx = 240;
}
}
function persistWidth() {
try {
localStorage.setItem(
WIDTH_STORAGE,
selectedWidthPx == null ? "original" : String(selectedWidthPx)
);
} catch (e) {}
}
function syncSizeBarUI() {
var bar = document.getElementById("sheet-size-bar");
if (!bar) return;
bar.querySelectorAll(".sheet-size-btn").forEach(function (btn) {
var w = btn.getAttribute("data-w");
var active =
(w === "original" && selectedWidthPx == null) ||
(w !== "original" && parseInt(w, 10) === selectedWidthPx);
btn.classList.toggle("is-active", active);
btn.setAttribute("aria-pressed", active ? "true" : "false");
});
}
function wireSizeBar() {
document.querySelectorAll("#sheet-size-bar .sheet-size-btn").forEach(function (btn) {
btn.addEventListener("click", function () {
var w = btn.getAttribute("data-w");
selectedWidthPx = w === "original" ? null : parseInt(w, 10);
persistWidth();
syncSizeBarUI();
});
});
}
loadSavedWidth();
wireSizeBar();
syncSizeBarUI();
function showToast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("show");
clearTimeout(showToast._t);
showToast._t = setTimeout(function () {
toastEl.classList.remove("show");
}, 2200);
}
async function copyText(text, toastMsg) {
try {
await navigator.clipboard.writeText(text);
showToast(toastMsg || "已复制到剪贴板");
} catch {
window.prompt("请手动复制:", text);
}
}
function escapeAttr(s) {
return String(s)
.replace(/&/g, "&amp;")
.replace(/"/g, "&quot;")
.replace(/</g, "&lt;");
}
function markdownImage(url, alt) {
var a = String(alt || "表情").replace(/\]/g, "");
return "![" + a + "](" + url + ")";
}
function htmlEmbed(url, alt, widthPx) {
var parts =
'<img src="' +
escapeAttr(url) +
'" alt="' +
escapeAttr(alt || "") +
'" loading="lazy" draggable="false"';
if (widthPx != null) {
parts +=
' width="' +
widthPx +
'" style="max-width:' +
widthPx +
'px;width:100%;height:auto"';
}
return parts + " />";
}
function openSheet(url, file, category) {
sheetCtx = {
url: toAbsoluteUrl(url),
file: file,
category: category,
};
sheetTitle.textContent = (category ? category + " / " : "") + file;
syncSizeBarUI();
sheetRoot.hidden = false;
document.body.style.overflow = "hidden";
}
function closeSheet() {
sheetRoot.hidden = true;
document.body.style.overflow = "";
sheetCtx = null;
}
function wireSheetActions() {
sheetBackdrop.addEventListener("click", closeSheet);
document.getElementById("act-close").addEventListener("click", closeSheet);
document.getElementById("act-url").addEventListener("click", function () {
if (!sheetCtx) return;
copyText(sheetCtx.url, "已复制链接");
closeSheet();
});
document.getElementById("act-md").addEventListener("click", function () {
if (!sheetCtx) return;
var alt = sheetCtx.category + "/" + sheetCtx.file;
copyText(markdownImage(sheetCtx.url, alt), "已复制 Markdown");
closeSheet();
});
document.getElementById("act-html").addEventListener("click", function () {
if (!sheetCtx) return;
var alt = sheetCtx.category + "/" + sheetCtx.file;
var tip =
selectedWidthPx == null ? "已复制 HTML" : "已复制 HTML" + selectedWidthPx + "px";
copyText(htmlEmbed(sheetCtx.url, alt, selectedWidthPx), tip);
closeSheet();
});
document.getElementById("act-download").addEventListener("click", function () {
if (!sheetCtx) return;
var u = sheetCtx.url;
var name = sheetCtx.file;
closeSheet();
(async function () {
try {
var res = await fetch(u);
if (!res.ok) throw new Error("HTTP " + res.status);
var blob = await res.blob();
var objectUrl = URL.createObjectURL(blob);
var a = document.createElement("a");
a.href = objectUrl;
a.download = name;
a.rel = "noopener";
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(objectUrl);
showToast("已开始下载");
} catch (e) {
var link = document.createElement("a");
link.href = u;
link.download = name;
link.rel = "noopener";
link.target = "_blank";
document.body.appendChild(link);
link.click();
link.remove();
showToast("已尝试下载");
}
})();
});
document.addEventListener("keydown", function (ev) {
if (ev.key === "Escape" && !sheetRoot.hidden) closeSheet();
});
}
wireSheetActions();
function readHashCategory() {
var raw = location.hash.replace(/^#/, "");
if (!raw) return null;
try {
return decodeURIComponent(raw);
} catch (e) {
return raw;
}
}
function setHashCategory(id) {
var enc = encodeURIComponent(id);
if (location.hash !== "#" + enc) {
history.replaceState(null, "", "#" + enc);
}
}
function render(data) {
/** 可选 CDN 根;未设置时清单与 /meme 相对当前页面解析,支持子路径部署与本机 file:// 打开 */
var assetBase = data.baseUrl ? String(data.baseUrl).replace(/\/?$/, "") : "";
function memeAssetUrl(catId, fileName) {
var path =
"/meme/" +
encodeURIComponent(catId) +
"/" +
encodeURIComponent(fileName);
return toAbsoluteUrl(assetBase ? assetBase + path : path);
}
var categories = data.categories || [];
if (!categories.length) {
tabsEl.innerHTML = "";
app.innerHTML =
'<div class="empty">暂无数据。构建并部署后可在此查看。</div>';
return;
}
var selectedId = readHashCategory();
var validIds = {};
for (var i = 0; i < categories.length; i++) {
validIds[categories[i].id] = true;
}
if (!selectedId || !validIds[selectedId]) {
selectedId = categories[0].id;
setHashCategory(selectedId);
}
function findCategory(id) {
for (var j = 0; j < categories.length; j++) {
if (categories[j].id === id) return categories[j];
}
return categories[0];
}
var catSearchEl = document.getElementById("cat-search");
function renderTabs() {
tabsEl.innerHTML = "";
var q = categoryFilter.trim().toLowerCase();
var visible = 0;
for (var k = 0; k < categories.length; k++) {
var cat = categories[k];
if (q) {
var hay = ((cat.name || "") + " " + cat.id).toLowerCase();
if (hay.indexOf(q) < 0) continue;
}
visible++;
var btn = document.createElement("button");
btn.type = "button";
btn.className = "cat-tab";
btn.setAttribute("role", "tab");
btn.setAttribute("aria-selected", cat.id === selectedId ? "true" : "false");
btn.textContent = (cat.name || cat.id) + " · " + (cat.items || []).length;
btn.addEventListener(
"click",
(function (id) {
return function () {
if (selectedId === id) return;
selectedId = id;
setHashCategory(id);
renderTabs();
renderGrid();
if (window.matchMedia("(max-width: 640px)").matches) {
setSidebarCollapsed(true);
}
};
})(cat.id)
);
tabsEl.appendChild(btn);
}
if (q && visible === 0) {
var empty = document.createElement("p");
empty.className = "cat-tabs-empty";
empty.textContent = "无匹配分类";
tabsEl.appendChild(empty);
}
var selectedBtn = tabsEl.querySelector('[aria-selected="true"]');
if (selectedBtn) {
selectedBtn.scrollIntoView({ block: "nearest", behavior: "smooth" });
}
}
if (catSearchEl && !catSearchEl._wired) {
catSearchEl._wired = true;
catSearchEl.addEventListener("input", function () {
categoryFilter = catSearchEl.value;
if (categoryFilter.trim() && isSidebarCollapsed()) {
setSidebarCollapsed(false);
}
renderTabs();
});
}
function renderGrid() {
var cat = findCategory(selectedId);
var items = cat.items || [];
app.innerHTML = "";
var grid = document.createElement("div");
grid.className = "grid";
for (let n = 0; n < items.length; n++) {
const it = items[n];
const url = toAbsoluteUrl(it.url || memeAssetUrl(cat.id, it.file));
const tile = document.createElement("button");
tile.type = "button";
tile.className = "tile";
tile.title = it.file;
const img = document.createElement("img");
img.loading = "lazy";
img.alt = it.file;
img.src = url;
tile.appendChild(img);
const label = document.createElement("div");
label.className = "label";
label.textContent = it.file;
tile.appendChild(label);
tile.addEventListener("click", function () {
openSheet(url, it.file, cat.name || cat.id);
});
grid.appendChild(tile);
}
app.appendChild(grid);
}
renderTabs();
renderGrid();
window.addEventListener("hashchange", function () {
var hid = readHashCategory();
if (hid && validIds[hid] && hid !== selectedId) {
selectedId = hid;
renderTabs();
renderGrid();
}
});
}
fetch("/memes.json")
.then(function (r) {
if (!r.ok) throw new Error("HTTP " + r.status);
return r.json();
})
.then(render)
.catch(function (e) {
app.innerHTML =
'<div class="error">加载失败:<code>' +
String(e.message || e) +
"</code></div>";
});
})();
</script>
</body>
</html>