256 lines
9.9 KiB
JavaScript
256 lines
9.9 KiB
JavaScript
// Paca Official Site Script
|
|
// 1) IntersectionObserver Scroll Reveal
|
|
// 2) Mobile Navigation Menu Controller
|
|
// 3) Interactive Hero Workstation Simulator (Multi-Tab Switcher & Live Telemetry)
|
|
// 4) Dynamic Releases Renderer (releases.json)
|
|
|
|
// ── 1. Scroll Reveal Controller ──────────────────────────────
|
|
(function initReveal() {
|
|
const items = document.querySelectorAll(".reveal");
|
|
if (!items.length) return;
|
|
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches || !("IntersectionObserver" in window)) {
|
|
return;
|
|
}
|
|
document.documentElement.classList.add("js");
|
|
const io = new IntersectionObserver((entries) => {
|
|
for (const e of entries) {
|
|
if (e.isIntersecting) {
|
|
e.target.classList.add("in");
|
|
io.unobserve(e.target);
|
|
}
|
|
}
|
|
}, { threshold: 0.12, rootMargin: "0px 0px -6% 0px" });
|
|
|
|
items.forEach((el) => io.observe(el));
|
|
})();
|
|
|
|
// ── 2. Mobile Nav Drawer Controller ──────────────────────────
|
|
(function initMobileNav() {
|
|
const toggleBtn = document.getElementById("nav-toggle");
|
|
const overlay = document.getElementById("mobile-menu");
|
|
if (!toggleBtn || !overlay) return;
|
|
|
|
function closeMenu() {
|
|
overlay.classList.remove("is-active");
|
|
toggleBtn.setAttribute("aria-expanded", "false");
|
|
document.body.style.overflow = "";
|
|
}
|
|
|
|
function toggleMenu() {
|
|
const isActive = overlay.classList.toggle("is-active");
|
|
toggleBtn.setAttribute("aria-expanded", String(isActive));
|
|
document.body.style.overflow = isActive ? "hidden" : "";
|
|
}
|
|
|
|
toggleBtn.addEventListener("click", toggleMenu);
|
|
|
|
overlay.addEventListener("click", (e) => {
|
|
if (e.target === overlay) closeMenu();
|
|
});
|
|
|
|
document.addEventListener("keydown", (e) => {
|
|
if (e.key === "Escape" && overlay.classList.contains("is-active")) {
|
|
closeMenu();
|
|
toggleBtn.focus();
|
|
}
|
|
});
|
|
|
|
overlay.querySelectorAll(".mobile-menu-link").forEach((link) => {
|
|
link.addEventListener("click", () => {
|
|
overlay.classList.remove("is-active");
|
|
toggleBtn.setAttribute("aria-expanded", "false");
|
|
document.body.style.overflow = "";
|
|
});
|
|
});
|
|
})();
|
|
|
|
// ── 3. Interactive Hero Workstation Simulator ────────────────
|
|
(function initHeroWorkstation() {
|
|
// Tab switcher
|
|
const tabBtns = document.querySelectorAll(".tab-btn");
|
|
const tabPanels = document.querySelectorAll(".tab-panel");
|
|
|
|
tabBtns.forEach((btn) => {
|
|
btn.addEventListener("click", () => {
|
|
const targetId = btn.getAttribute("data-tab");
|
|
tabBtns.forEach((b) => {
|
|
b.classList.remove("is-active");
|
|
b.setAttribute("aria-selected", "false");
|
|
});
|
|
tabPanels.forEach((p) => p.classList.remove("is-active"));
|
|
|
|
btn.classList.add("is-active");
|
|
btn.setAttribute("aria-selected", "true");
|
|
const targetPanel = document.getElementById(targetId);
|
|
if (targetPanel) targetPanel.classList.add("is-active");
|
|
});
|
|
});
|
|
|
|
// Live Progress & Telemetry simulation
|
|
const progressEl = document.getElementById("sim-progress");
|
|
const percentEl = document.getElementById("sim-percent");
|
|
const speedEl = document.getElementById("sim-speed");
|
|
if (!progressEl || !percentEl || !speedEl) return;
|
|
|
|
let current = 72;
|
|
let direction = 1;
|
|
|
|
setInterval(() => {
|
|
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
|
|
|
|
if (direction === 1) {
|
|
current += 0.8;
|
|
if (current >= 96) direction = -1;
|
|
} else {
|
|
current = 64;
|
|
direction = 1;
|
|
}
|
|
|
|
const rounded = Math.min(99, Math.floor(current));
|
|
progressEl.style.width = rounded + "%";
|
|
percentEl.textContent = rounded + "%";
|
|
|
|
const baseSpeed = 18.2 + (Math.random() * 2.4 - 1.2);
|
|
speedEl.textContent = baseSpeed.toFixed(1) + " MB/s";
|
|
}, 1200);
|
|
})();
|
|
|
|
// ── 5. Releases Renderer ─────────────────────────────────────
|
|
const PLATFORMS = [
|
|
{ id: "windows", label: "Windows", icon: "windows-logo", sub: "x64 및 ARM64 네이티브 설치자 (exe, msi)",
|
|
match: (n) => /setup.*\.(exe|msi)$/i.test(n) || /\.msi$/i.test(n) || /win/i.test(n) },
|
|
{ id: "macos", label: "macOS 프리뷰", icon: "apple-logo", sub: "Apple Silicon & Intel tar.gz 프리뷰 빌드",
|
|
match: (n) => /osx|darwin|mac/i.test(n) },
|
|
{ id: "linux", label: "Linux 프리뷰", icon: "linux-logo", sub: "Ubuntu, Fedora, Arch tar.gz 프리뷰 빌드",
|
|
match: (n) => /linux/i.test(n) },
|
|
{ id: "android", label: "Android", icon: "android-logo", sub: "스마트폰 로컬 스트리밍 컴패니언 APK",
|
|
match: (n) => /\.apk$/i.test(n) },
|
|
];
|
|
|
|
function detectPreferredAsset(name) {
|
|
const ua = navigator.userAgent;
|
|
if (/Windows/i.test(ua)) return /setup-x64\.(exe|msi)$/i.test(name);
|
|
if (/Android/i.test(ua)) return /\.apk$/i.test(name);
|
|
if (/Mac/i.test(ua)) return /osx-arm64/i.test(name);
|
|
if (/Linux/i.test(ua)) return /linux-x64/i.test(name);
|
|
return false;
|
|
}
|
|
|
|
function fmtSize(bytes) {
|
|
if (!bytes) return "";
|
|
const mb = bytes / 1048576;
|
|
return mb >= 1024 ? (mb / 1024).toFixed(1) + " GB" : mb.toFixed(1) + " MB";
|
|
}
|
|
|
|
function fmtDate(iso) {
|
|
if (!iso) return "";
|
|
const d = new Date(iso);
|
|
return `${d.getFullYear()}년 ${d.getMonth() + 1}월 ${d.getDate()}일`;
|
|
}
|
|
|
|
function esc(s) {
|
|
return String(s).replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
|
|
}
|
|
|
|
function assetRow(a, recommended) {
|
|
return `<div class="dl-asset">
|
|
<div class="rel-asset-info">
|
|
<div class="dl-asset__name">${esc(a.name)}</div>
|
|
<div class="dl-asset__size text-mono">${fmtSize(a.size)} ${recommended ? '<span style="color:var(--accent);font-weight:700">· 현재 기기에 권장</span>' : ''}</div>
|
|
</div>
|
|
<a class="btn btn--primary btn--sm ${recommended ? "dl-reco" : ""}" href="${esc(a.url)}" title="${recommended ? "현재 기기에 권장" : ""}">
|
|
<span>다운로드</span>
|
|
<span class="btn__icon-badge"><svg class="icon" aria-hidden="true"><use href="assets/icons.svg#download-simple"/></svg></span>
|
|
</a>
|
|
</div>`;
|
|
}
|
|
|
|
function groupsHtml(assets) {
|
|
const used = new Set();
|
|
const groups = PLATFORMS.map((p) => {
|
|
const list = assets.filter((a) => !used.has(a.name) && p.match(a.name));
|
|
list.forEach((a) => used.add(a.name));
|
|
if (!list.length) return "";
|
|
return `<div class="dl-group" id="${p.id}">
|
|
<div class="dl-group__head">
|
|
<svg class="icon" aria-hidden="true"><use href="assets/icons.svg#${p.icon}"/></svg>
|
|
<span>${p.label}</span>
|
|
</div>
|
|
<div class="dl-group__sub">${p.sub}</div>
|
|
${list.map((a) => assetRow(a, detectPreferredAsset(a.name))).join("")}
|
|
</div>`;
|
|
}).filter(Boolean);
|
|
|
|
const rest = assets.filter((a) => !used.has(a.name));
|
|
if (rest.length) {
|
|
groups.push(`<div class="dl-group">
|
|
<div class="dl-group__head">기타 아카이브</div>
|
|
<div class="dl-group__sub">추가 바이너리 및 체크섬 파일</div>
|
|
${rest.map((a) => assetRow(a, false)).join("")}
|
|
</div>`);
|
|
}
|
|
return `<div class="dl-groups">${groups.join("")}</div>`;
|
|
}
|
|
|
|
function releaseCard(r, featured) {
|
|
const pill = r.prerelease ? `<span class="rel-pill rel-pill--pre">프리뷰 빌드</span>` : `<span class="rel-pill">공식 안정판</span>`;
|
|
const meta = `<div class="rel-featured__meta">
|
|
<span class="rel-featured__tag">${esc(r.name || r.tag)}</span>
|
|
${pill}
|
|
<span class="rel-featured__date text-mono">${fmtDate(r.published_at)}</span>
|
|
</div>
|
|
${r.notes ? `<p class="rel-featured__notes">${esc(r.notes)}</p>` : ""}
|
|
${groupsHtml(r.assets)}`;
|
|
|
|
if (featured) {
|
|
return `<article class="card rel-featured">${meta}</article>`;
|
|
}
|
|
return `<div class="card"><details>
|
|
<summary>
|
|
<span class="rel-featured__tag" style="font-size:1.15rem">${esc(r.name || r.tag)}</span>
|
|
${pill}
|
|
<span class="rel-featured__date text-mono">${fmtDate(r.published_at)}</span>
|
|
<svg class="icon" style="margin-left:auto;width:14px;height:14px"><use href="assets/icons.svg#arrow-right"/></svg>
|
|
</summary>
|
|
<div class="rel-old__body">
|
|
${r.notes ? `<p class="rel-featured__notes">${esc(r.notes)}</p>` : ""}
|
|
${groupsHtml(r.assets)}
|
|
</div>
|
|
</details></div>`;
|
|
}
|
|
|
|
function emptyState() {
|
|
return `<div class="card rel-empty">
|
|
<div class="detection-pulse" style="margin:0 auto 12px" aria-hidden="true"></div>
|
|
<h2>첫 공개 릴리스를 준비하고 있습니다</h2>
|
|
<p style="color:var(--text-secondary);max-width:54ch">빌드 파이프라인이 정상 작동 중이며 곧 공식 설치자가 배포됩니다. 먼저 <a href="guide.html" style="color:var(--accent);font-weight:700">사용 안내</a>를 둘러보세요.</p>
|
|
</div>`;
|
|
}
|
|
|
|
(async function initReleases() {
|
|
const root = document.getElementById("release-root") || document.getElementById("releases-container");
|
|
if (!root) return;
|
|
|
|
try {
|
|
const res = await fetch("releases.json", { cache: "no-cache" });
|
|
if (!res.ok) throw new Error("HTTP " + res.status);
|
|
const data = await res.json();
|
|
const releases = (data.releases || []);
|
|
if (!releases.length) {
|
|
root.innerHTML = emptyState();
|
|
return;
|
|
}
|
|
const [latest, ...older] = releases;
|
|
let html = releaseCard(latest, true);
|
|
if (older.length) {
|
|
html += `<div class="rel-old"><h2 style="font-size:1.35rem;margin-bottom:14px">이전 버전 기록</h2>
|
|
${older.map((r) => releaseCard(r, false)).join("")}</div>`;
|
|
}
|
|
root.innerHTML = html;
|
|
} catch (err) {
|
|
root.innerHTML = `<div class="card rel-empty">
|
|
<h2>릴리스 목록을 불러오지 못했습니다</h2>
|
|
<p style="color:var(--text-secondary)">네트워크 상태를 확인하고 새로고침해 주세요. 급하신 경우 <a class="link-inline" href="https://git.chanpaca.net/Paca/releases" style="color:var(--accent);font-weight:700" target="_blank" rel="noopener">릴리스 저장소</a>에서 직접 다운로드하실 수 있습니다.</p>
|
|
</div>`;
|
|
}
|
|
})();
|