471 lines
14 KiB
JavaScript
471 lines
14 KiB
JavaScript
/* 游民导航 · 小红书小工具(纯离线 classic script) */
|
||
(function () {
|
||
"use strict";
|
||
|
||
var data = window.NAV_DATA || {};
|
||
var categories = data.categories || [];
|
||
var state = {
|
||
theme: "light",
|
||
activeId: categories[0] ? categories[0].id : "",
|
||
drawerOpen: false,
|
||
sheetOpen: false,
|
||
currentLink: null,
|
||
jumping: false,
|
||
};
|
||
|
||
var els = {};
|
||
|
||
function $(id) {
|
||
return document.getElementById(id);
|
||
}
|
||
|
||
function qs(sel, root) {
|
||
return (root || document).querySelector(sel);
|
||
}
|
||
|
||
function escapeHtml(str) {
|
||
return String(str == null ? "" : str)
|
||
.replace(/&/g, "&")
|
||
.replace(/</g, "<")
|
||
.replace(/>/g, ">")
|
||
.replace(/"/g, """);
|
||
}
|
||
|
||
function applySafeArea() {
|
||
try {
|
||
var root = document.documentElement;
|
||
var probe = document.createElement("div");
|
||
probe.style.cssText =
|
||
"position:absolute;visibility:hidden;pointer-events:none;padding-top:constant(safe-area-inset-top);padding-top:env(safe-area-inset-top);";
|
||
document.body.appendChild(probe);
|
||
var envTop = parseFloat(window.getComputedStyle(probe).paddingTop) || 0;
|
||
document.body.removeChild(probe);
|
||
|
||
// 小红书宿主顶栏(返回 / 分享)常盖住内容;Android 上 env 常为 0
|
||
var status = envTop > 0 ? envTop : 28;
|
||
var hostBar = 44;
|
||
var padTop = Math.max(status + hostBar, 72);
|
||
root.style.setProperty("--app-pad-top", padTop + "px");
|
||
root.style.setProperty("--safe-area-inset-top", status + "px");
|
||
} catch (e) {
|
||
document.documentElement.style.setProperty("--app-pad-top", "80px");
|
||
}
|
||
}
|
||
|
||
function readTheme() {
|
||
try {
|
||
var saved = localStorage.getItem("navmini-xhs-theme");
|
||
if (saved === "light" || saved === "dark") return saved;
|
||
} catch (e) {}
|
||
return "light";
|
||
}
|
||
|
||
function setTheme(theme) {
|
||
state.theme = theme;
|
||
document.documentElement.setAttribute("data-theme", theme);
|
||
try {
|
||
localStorage.setItem("navmini-xhs-theme", theme);
|
||
} catch (e) {}
|
||
if (els.themeIcon) {
|
||
els.themeIcon.textContent = theme === "dark" ? "☀" : "☾";
|
||
}
|
||
}
|
||
|
||
function showToast(msg) {
|
||
if (!els.toast) return;
|
||
els.toast.textContent = msg;
|
||
els.toast.classList.add("show");
|
||
clearTimeout(showToast._t);
|
||
showToast._t = setTimeout(function () {
|
||
els.toast.classList.remove("show");
|
||
}, 1600);
|
||
}
|
||
|
||
/** 选中网址文本(容器禁用程序化写入剪贴板,仅做文本选中) */
|
||
function selectUrlText() {
|
||
try {
|
||
var node = els.sheetUrlBox;
|
||
if (!node) return false;
|
||
var range = document.createRange();
|
||
range.selectNodeContents(node);
|
||
var sel = window.getSelection();
|
||
if (!sel) return false;
|
||
sel.removeAllRanges();
|
||
sel.addRange(range);
|
||
return true;
|
||
} catch (e) {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
function setGuideStep(step) {
|
||
var items = els.guideSteps
|
||
? els.guideSteps.querySelectorAll(".guide-step")
|
||
: [];
|
||
for (var i = 0; i < items.length; i++) {
|
||
var n = i + 1;
|
||
items[i].classList.toggle("is-active", n === step);
|
||
items[i].classList.toggle("is-done", n < step);
|
||
}
|
||
}
|
||
|
||
function setGuideUi(phase) {
|
||
// phase: idle | selected
|
||
var selected = phase === "selected";
|
||
if (els.sheetUrlBox) {
|
||
els.sheetUrlBox.classList.toggle("is-pulse", !selected);
|
||
els.sheetUrlBox.classList.toggle("is-selected", selected);
|
||
}
|
||
if (els.sheetSelect) {
|
||
els.sheetSelect.classList.toggle("pulse", !selected);
|
||
els.sheetSelect.classList.toggle("is-done", selected);
|
||
}
|
||
if (els.sheetSelectLabel) {
|
||
els.sheetSelectLabel.textContent = selected
|
||
? "② 已选中 · 再点可重选"
|
||
: "① 点我选中网址";
|
||
}
|
||
if (els.sheetSelectSub) {
|
||
els.sheetSelectSub.textContent = selected
|
||
? "下一步:手指按住蓝色网址区"
|
||
: "选中后请按住网址复制";
|
||
}
|
||
// 第一步:只显示向下指按钮的小手;第二步:小手上移指网址,文案在右边
|
||
if (els.fingerBtn) {
|
||
els.fingerBtn.classList.toggle("is-hidden", selected);
|
||
els.fingerBtn.setAttribute("aria-hidden", selected ? "true" : "false");
|
||
}
|
||
if (els.fingerUrl) {
|
||
els.fingerUrl.classList.toggle("is-hidden", !selected);
|
||
els.fingerUrl.setAttribute("aria-hidden", selected ? "false" : "true");
|
||
}
|
||
if (els.sheetUrlWrap) {
|
||
els.sheetUrlWrap.classList.toggle("is-pointing", selected);
|
||
}
|
||
setGuideStep(selected ? 2 : 1);
|
||
}
|
||
|
||
function findCategory(id) {
|
||
for (var i = 0; i < categories.length; i++) {
|
||
if (categories[i].id === id) return categories[i];
|
||
}
|
||
return categories[0] || null;
|
||
}
|
||
|
||
function countLinks(cat) {
|
||
var n = 0;
|
||
var groups = cat.groups || [];
|
||
for (var i = 0; i < groups.length; i++) {
|
||
n += (groups[i].links || []).length;
|
||
}
|
||
return n;
|
||
}
|
||
|
||
function renderDrawer() {
|
||
var html = "";
|
||
for (var i = 0; i < categories.length; i++) {
|
||
var cat = categories[i];
|
||
var active = cat.id === state.activeId ? " active" : "";
|
||
html +=
|
||
'<button type="button" class="drawer-item' +
|
||
active +
|
||
'" data-cat="' +
|
||
escapeHtml(cat.id) +
|
||
'">' +
|
||
'<span class="drawer-emoji" aria-hidden="true">' +
|
||
escapeHtml(cat.emoji || "·") +
|
||
"</span>" +
|
||
'<span class="drawer-item-title">' +
|
||
escapeHtml(cat.title) +
|
||
"</span>" +
|
||
'<span class="drawer-item-count">' +
|
||
countLinks(cat) +
|
||
"</span>" +
|
||
"</button>";
|
||
}
|
||
els.drawerNav.innerHTML = html;
|
||
}
|
||
|
||
function renderContent() {
|
||
var html = "";
|
||
for (var i = 0; i < categories.length; i++) {
|
||
var cat = categories[i];
|
||
html +=
|
||
'<section class="cat-panel" id="cat-' +
|
||
escapeHtml(cat.id) +
|
||
'">' +
|
||
'<header class="cat-heading"><h2>' +
|
||
escapeHtml(cat.title) +
|
||
'</h2><span class="cat-count">' +
|
||
countLinks(cat) +
|
||
"</span></header>";
|
||
|
||
var groups = cat.groups || [];
|
||
for (var g = 0; g < groups.length; g++) {
|
||
var group = groups[g];
|
||
html +=
|
||
'<div class="group-block"><h3 class="group-title">' +
|
||
escapeHtml(group.title) +
|
||
'</h3><div class="link-grid">';
|
||
|
||
var links = group.links || [];
|
||
for (var l = 0; l < links.length; l++) {
|
||
var link = links[l];
|
||
html +=
|
||
'<button type="button" class="link-card" data-cat="' +
|
||
escapeHtml(cat.id) +
|
||
'" data-group="' +
|
||
escapeHtml(group.id) +
|
||
'" data-id="' +
|
||
escapeHtml(link.id) +
|
||
'">' +
|
||
'<span class="link-icon-wrap"><img class="link-icon" src="' +
|
||
escapeHtml(link.icon) +
|
||
'" alt="" width="34" height="34" loading="lazy" /></span>' +
|
||
'<span class="link-meta"><span class="link-title">' +
|
||
escapeHtml(link.title) +
|
||
'</span><span class="link-desc">' +
|
||
escapeHtml(link.subtitle || "") +
|
||
"</span></span></button>";
|
||
}
|
||
html += "</div></div>";
|
||
}
|
||
html += "</section>";
|
||
}
|
||
els.content.innerHTML = html;
|
||
}
|
||
|
||
function setDrawer(open) {
|
||
state.drawerOpen = open;
|
||
els.drawer.classList.toggle("open", open);
|
||
els.drawer.setAttribute("aria-hidden", open ? "false" : "true");
|
||
document.body.style.overflow = open || state.sheetOpen ? "hidden" : "";
|
||
}
|
||
|
||
function setSheet(open) {
|
||
state.sheetOpen = open;
|
||
els.sheet.classList.toggle("open", open);
|
||
els.sheet.setAttribute("aria-hidden", open ? "false" : "true");
|
||
document.body.style.overflow = open || state.drawerOpen ? "hidden" : "";
|
||
}
|
||
|
||
function findLink(catId, groupId, linkId) {
|
||
var cat = findCategory(catId);
|
||
if (!cat) return null;
|
||
var groups = cat.groups || [];
|
||
for (var i = 0; i < groups.length; i++) {
|
||
if (groups[i].id !== groupId) continue;
|
||
var links = groups[i].links || [];
|
||
for (var j = 0; j < links.length; j++) {
|
||
if (links[j].id === linkId) return links[j];
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
function openLink(link) {
|
||
// 容器禁止打开外链 / 跳转其他小工具(见能力清单 §4.2),一律走复制引导
|
||
openLinkSheet(link);
|
||
}
|
||
|
||
function openLinkSheet(link) {
|
||
state.currentLink = link;
|
||
els.sheetIcon.src = link.icon || "./icons/_brand.webp";
|
||
els.sheetTitle.textContent = link.title || "";
|
||
els.sheetSub.textContent = link.subtitle || "";
|
||
els.sheetUrlBox.textContent = link.href || "";
|
||
els.sheetStatus.textContent = "按步骤操作:先点绿色按钮选中网址";
|
||
els.sheetStatus.classList.remove("is-error");
|
||
setGuideUi("idle");
|
||
setSheet(true);
|
||
}
|
||
|
||
function markSelected() {
|
||
if (!state.currentLink) return;
|
||
if (selectUrlText()) {
|
||
setGuideUi("selected");
|
||
els.sheetStatus.textContent =
|
||
"已选中蓝色区域 · 请用手指按住网址,在菜单里点「复制」";
|
||
els.sheetStatus.classList.remove("is-error");
|
||
showToast("已选中,请按住复制");
|
||
// 短暂强调第 3 步
|
||
setTimeout(function () {
|
||
setGuideStep(3);
|
||
var s2 = els.guideSteps && els.guideSteps.querySelector('[data-step="2"]');
|
||
if (s2) s2.classList.add("is-done");
|
||
}, 1600);
|
||
} else {
|
||
setGuideUi("idle");
|
||
els.sheetStatus.textContent = "自动选中失败,请手指直接按住蓝色网址文字";
|
||
els.sheetStatus.classList.add("is-error");
|
||
setGuideStep(2);
|
||
}
|
||
}
|
||
|
||
function jumpToCategory(id) {
|
||
state.activeId = id;
|
||
setPill();
|
||
renderDrawer();
|
||
setDrawer(false);
|
||
state.jumping = true;
|
||
var el = document.getElementById("cat-" + id);
|
||
if (el) {
|
||
el.scrollIntoView({ behavior: "smooth", block: "start" });
|
||
}
|
||
setTimeout(function () {
|
||
state.jumping = false;
|
||
}, 700);
|
||
}
|
||
|
||
function setPill() {
|
||
var cat = findCategory(state.activeId);
|
||
if (els.brandCurrent && cat) {
|
||
els.brandCurrent.textContent = cat.title;
|
||
}
|
||
}
|
||
|
||
function onScroll() {
|
||
var y = window.scrollY || document.documentElement.scrollTop || 0;
|
||
els.backTop.classList.toggle("show", y > 480);
|
||
|
||
if (state.jumping) return;
|
||
var marker = 100;
|
||
var current = categories[0] ? categories[0].id : "";
|
||
for (var i = 0; i < categories.length; i++) {
|
||
var el = document.getElementById("cat-" + categories[i].id);
|
||
if (!el) continue;
|
||
if (el.getBoundingClientRect().top <= marker) {
|
||
current = categories[i].id;
|
||
} else {
|
||
break;
|
||
}
|
||
}
|
||
if (current && current !== state.activeId) {
|
||
state.activeId = current;
|
||
setPill();
|
||
renderDrawer();
|
||
}
|
||
}
|
||
|
||
function bind() {
|
||
els.btnMenu.addEventListener("click", function () {
|
||
setDrawer(true);
|
||
});
|
||
els.drawerMask.addEventListener("click", function () {
|
||
setDrawer(false);
|
||
});
|
||
els.drawerClose.addEventListener("click", function () {
|
||
setDrawer(false);
|
||
});
|
||
els.btnTheme.addEventListener("click", function () {
|
||
setTheme(state.theme === "dark" ? "light" : "dark");
|
||
});
|
||
els.backTop.addEventListener("click", function () {
|
||
window.scrollTo({ top: 0, behavior: "smooth" });
|
||
});
|
||
|
||
els.drawerNav.addEventListener("click", function (e) {
|
||
var btn = e.target.closest(".drawer-item");
|
||
if (!btn) return;
|
||
jumpToCategory(btn.getAttribute("data-cat"));
|
||
});
|
||
|
||
els.content.addEventListener("click", function (e) {
|
||
var btn = e.target.closest(".link-card");
|
||
if (!btn) return;
|
||
var link = findLink(
|
||
btn.getAttribute("data-cat"),
|
||
btn.getAttribute("data-group"),
|
||
btn.getAttribute("data-id")
|
||
);
|
||
if (link) openLink(link);
|
||
});
|
||
|
||
els.sheetMask.addEventListener("click", function () {
|
||
setSheet(false);
|
||
});
|
||
els.sheetClose.addEventListener("click", function () {
|
||
setSheet(false);
|
||
});
|
||
els.sheetSelect.addEventListener("click", function () {
|
||
markSelected();
|
||
});
|
||
|
||
els.sheetUrlBox.addEventListener("click", function () {
|
||
markSelected();
|
||
});
|
||
|
||
// 用户开始触碰网址区时,推进到第 2/3 步提示
|
||
els.sheetUrlBox.addEventListener("touchstart", function () {
|
||
if (els.sheetUrlBox.classList.contains("is-selected")) {
|
||
setGuideStep(3);
|
||
var s2 = els.guideSteps && els.guideSteps.querySelector('[data-step="2"]');
|
||
if (s2) s2.classList.add("is-done");
|
||
els.sheetStatus.textContent = "按住不放,在弹出菜单中点「复制」";
|
||
}
|
||
}, { passive: true });
|
||
|
||
window.addEventListener("scroll", onScroll, { passive: true });
|
||
}
|
||
|
||
function init() {
|
||
els = {
|
||
brandName: $("brand-name"),
|
||
brandCurrent: $("brand-current"),
|
||
brandLogo: $("brand-logo"),
|
||
footer: $("footer"),
|
||
content: $("content"),
|
||
drawer: $("drawer"),
|
||
drawerNav: $("drawer-nav"),
|
||
drawerMask: $("drawer-mask"),
|
||
drawerClose: $("drawer-close"),
|
||
drawerBrand: $("drawer-brand"),
|
||
drawerSub: $("drawer-sub"),
|
||
btnMenu: $("btn-menu"),
|
||
btnTheme: $("btn-theme"),
|
||
themeIcon: $("theme-icon"),
|
||
backTop: $("back-top"),
|
||
sheet: $("sheet"),
|
||
sheetMask: $("sheet-mask"),
|
||
sheetIcon: $("sheet-icon"),
|
||
sheetTitle: $("sheet-title"),
|
||
sheetSub: $("sheet-sub"),
|
||
sheetUrlBox: $("sheet-url-box"),
|
||
sheetStatus: $("sheet-status"),
|
||
sheetSelect: $("sheet-select"),
|
||
sheetSelectLabel: $("sheet-select-label"),
|
||
sheetSelectSub: $("sheet-select-sub"),
|
||
sheetClose: $("sheet-close"),
|
||
guideSteps: $("guide-steps"),
|
||
sheetUrlWrap: $("sheet-url-wrap"),
|
||
fingerBtn: $("finger-btn"),
|
||
fingerUrl: $("finger-url"),
|
||
toast: $("toast"),
|
||
};
|
||
|
||
applySafeArea();
|
||
setTheme(readTheme());
|
||
|
||
var brand = data.brand || "游民导航";
|
||
els.brandName.textContent = brand;
|
||
els.footer.textContent = data.footer || "";
|
||
els.drawerBrand.textContent = brand;
|
||
els.drawerSub.textContent = data.tagline || "";
|
||
if (data.brandIcon && els.brandLogo) {
|
||
els.brandLogo.src = data.brandIcon;
|
||
}
|
||
|
||
renderDrawer();
|
||
renderContent();
|
||
setPill();
|
||
bind();
|
||
onScroll();
|
||
}
|
||
|
||
if (document.readyState === "loading") {
|
||
document.addEventListener("DOMContentLoaded", init);
|
||
} else {
|
||
init();
|
||
}
|
||
})();
|