diff --git a/site/site-v6-core.js b/site/site-v6-core.js index cc6f3e7..5d9511e 100644 --- a/site/site-v6-core.js +++ b/site/site-v6-core.js @@ -7,169 +7,66 @@ const REPOSITORY = 'masarray/vst-enhancer'; const RELEASES_ROOT = `https://github.com/${REPOSITORY}/releases`; const RELEASE_FALLBACK = `${RELEASES_ROOT}/latest`; - const LATEST_API = `https://api.github.com/repos/${REPOSITORY}/releases/latest`; - const OFFICIAL_RELEASE_PATH = `/${REPOSITORY}/releases`; - const OFFICIAL_DOWNLOAD_PREFIX = `/${REPOSITORY}/releases/download/`; const VERSION_RE = /^v\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/; const copy = language === 'id' ? { enabled: 'Rilis resmi tersedia', paused: 'Unduhan resmi sedang dijeda', - error: 'Status rilis tidak dapat diperiksa', - installer: 'Unduh gratis untuk Windows', - installerShort: 'Unduh gratis', - macHero: 'Unduh gratis untuk Mac', - eyebrow: 'Rilis terbaru', - heading: 'Apa yang meningkat di', - notes: 'Buka catatan rilis lengkap', - published: 'Diterbitkan', - liveSource: 'Data langsung GitHub Releases', - reviewedSource: 'Metadata rilis tervalidasi', - fallbackHighlight: 'Catatan peningkatan lengkap tersedia di halaman GitHub Release resmi.' + error: 'Status rilis tidak dapat diperiksa' } : { enabled: 'Official release available', paused: 'Official download is temporarily paused', - error: 'Release status could not be checked', - installer: 'Download free for Windows', - installerShort: 'Download free', - macHero: 'Download free for Mac', - eyebrow: 'Latest release', - heading: 'What improved in', - notes: 'Open full release notes', - published: 'Published', - liveSource: 'Live GitHub Releases data', - reviewedSource: 'Reviewed release metadata', - fallbackHighlight: 'The complete improvement notes are available on the official GitHub Release page.' + error: 'Release status could not be checked' }; - const officialReleaseUrl = (value, asset = false) => { - if (typeof value !== 'string' || value.length > 800) return null; + const officialReleaseUrl = (value, version, asset = false) => { + if (typeof value !== 'string' || value.length > 800 || !VERSION_RE.test(version || '')) return null; try { const url = new URL(value); - const validPath = url.pathname === OFFICIAL_RELEASE_PATH || url.pathname.startsWith(`${OFFICIAL_RELEASE_PATH}/`); - if (url.protocol !== 'https:' || url.hostname !== 'github.com' || !validPath) return null; - if (asset && !url.pathname.startsWith(OFFICIAL_DOWNLOAD_PREFIX)) return null; - return url.href; + if (url.protocol !== 'https:' || url.hostname !== 'github.com') return null; + const releasePath = `/${REPOSITORY}/releases/tag/${version}`; + const assetPrefix = `/${REPOSITORY}/releases/download/${version}/`; + const valid = asset ? url.pathname.startsWith(assetPrefix) : url.pathname === releasePath; + return valid ? url.href : null; } catch (_) { return null; } }; - const officialAsset = (asset) => { - if (!asset || typeof asset.name !== 'string' || typeof asset.browser_download_url !== 'string') return null; - if (asset.state && asset.state !== 'uploaded') return null; - const url = officialReleaseUrl(asset.browser_download_url, true); - return url ? { name: asset.name, url } : null; - }; + const parseManifest = (payload) => { + if (!payload || typeof payload !== 'object') return null; + const version = String(payload.version || ''); + if (!VERSION_RE.test(version) || Number(payload.schemaVersion || 0) < 3) return null; - const scoreInstaller = (name) => { - const lower = name.toLowerCase(); - if (!lower.endsWith('.exe')) return -1; - let score = 0; - if (lower.includes('arsonkupik')) score += 20; - if (lower.includes('windows')) score += 10; - if (lower.includes('x64') || lower.includes('win64')) score += 8; - if (lower.includes('setup')) score += 12; - if (lower.includes('installer')) score += 10; - if (lower.includes('portable')) score -= 30; - if (lower.includes('activator') || lower.includes('key')) score -= 100; - return score; - }; + const platforms = new Set(Array.isArray(payload.platforms) ? payload.platforms : []); + if (!platforms.has('windows-x64') || !platforms.has('macos-universal')) return null; - const chooseAsset = (assets, predicate, scorer = () => 0) => assets - .filter((asset) => predicate(asset.name.toLowerCase())) - .sort((a, b) => scorer(b.name) - scorer(a.name))[0] || null; + const releaseUrl = officialReleaseUrl(payload.releaseUrl, version, false); + if (!releaseUrl) return null; - const cleanMarkdown = (value) => String(value || '') - .replace(/!\[[^\]]*\]\([^)]*\)/g, '') - .replace(/\[([^\]]+)\]\([^)]*\)/g, '$1') - .replace(/[`*_~>#]/g, '') - .replace(/<[^>]+>/g, '') - .replace(/\s+/g, ' ') - .trim(); + const asset = (key) => officialReleaseUrl(payload[key], version, true); + const enabled = payload.distributionEnabled === true && payload.distributionStatus !== 'paused'; + const installerUrl = asset('installerUrl'); - const extractHighlights = (body, maximum = 6) => { - if (typeof body !== 'string') return []; - const ignored = /^(full changelog|contributors?|new contributors?|assets?|checksums?|sha-?256)\b/i; - const unique = new Set(); - const highlights = []; - const add = (value) => { - const cleaned = cleanMarkdown(value).replace(/^[-+*]\s+/, '').replace(/^\d+[.)]\s+/, ''); - const key = cleaned.toLowerCase(); - if (!cleaned || cleaned.length < 8 || ignored.test(cleaned) || unique.has(key)) return; - unique.add(key); - highlights.push(cleaned.length > 220 ? `${cleaned.slice(0, 217).trim()}…` : cleaned); - }; + if (enabled && !installerUrl) return null; - body.split(/\r?\n/).forEach((line) => { - const trimmed = line.trim(); - if (/^[-+*]\s+/.test(trimmed) || /^\d+[.)]\s+/.test(trimmed)) add(trimmed); - }); - if (!highlights.length) { - body.split(/\r?\n\s*\r?\n/).forEach((paragraph) => { - const trimmed = paragraph.trim(); - if (!trimmed.startsWith('#')) add(trimmed); - }); - } - return highlights.slice(0, maximum); - }; - - const parseGitHubRelease = (payload) => { - if (!payload || payload.draft === true || payload.prerelease === true) return null; - if (typeof payload.tag_name !== 'string' || !VERSION_RE.test(payload.tag_name)) return null; - const releaseUrl = officialReleaseUrl(payload.html_url); - const assets = Array.isArray(payload.assets) ? payload.assets.map(officialAsset).filter(Boolean) : []; - const installer = chooseAsset(assets, (name) => name.endsWith('.exe'), scoreInstaller); - if (!releaseUrl || !installer || scoreInstaller(installer.name) < 20) return null; return { - type: 'enabled', - source: 'github-latest-api', - version: payload.tag_name, - releaseName: cleanMarkdown(payload.name || payload.tag_name), - publishedAt: payload.published_at || payload.created_at || null, + type: enabled ? 'enabled' : 'paused', + source: 'local-release-manifest', + version, releaseUrl, - installerUrl: installer.url, - installerName: installer.name, - vst3Url: chooseAsset(assets, (name) => name.endsWith('.zip') && name.includes('windows') && name.includes('vst3'))?.url || null, - standaloneUrl: chooseAsset(assets, (name) => name.endsWith('.zip') && name.includes('windows') && name.includes('standalone'))?.url || null, - macDmgUrl: chooseAsset(assets, (name) => name.endsWith('.dmg') && name.includes('macos'))?.url || null, - macVst3Url: chooseAsset(assets, (name) => name.endsWith('.zip') && name.includes('macos') && name.includes('vst3'))?.url || null, - macStandaloneUrl: chooseAsset(assets, (name) => name.endsWith('.zip') && name.includes('macos') && name.includes('standalone'))?.url || null, - checksumsUrl: chooseAsset(assets, (name) => name === 'sha256sums.txt')?.url || null, - highlights: extractHighlights(payload.body) + installerUrl, + vst3Url: asset('vst3Url'), + standaloneUrl: asset('standaloneUrl'), + macDmgUrl: asset('macDmgUrl'), + macVst3Url: asset('macVst3Url'), + macStandaloneUrl: asset('macStandaloneUrl'), + checksumsUrl: asset('checksumsUrl') }; }; - const parseManifest = (release) => { - if (!release || typeof release !== 'object' || !VERSION_RE.test(String(release.version || ''))) return null; - const releaseUrl = officialReleaseUrl(release.releaseUrl) || RELEASE_FALLBACK; - const installerUrl = officialReleaseUrl(release.installerUrl, true); - const highlights = Array.isArray(release.releaseHighlights) - ? release.releaseHighlights.map(cleanMarkdown).filter(Boolean).slice(0, 6) - : []; - return release.distributionEnabled === true && installerUrl - ? { - type: 'enabled', - source: 'local-release-manifest', - version: release.version, - releaseName: cleanMarkdown(release.releaseName || release.version), - publishedAt: release.publishedAt || null, - releaseUrl, - installerUrl, - installerName: decodeURIComponent(new URL(installerUrl).pathname.split('/').pop()), - vst3Url: officialReleaseUrl(release.vst3Url, true), - standaloneUrl: officialReleaseUrl(release.standaloneUrl, true), - macDmgUrl: officialReleaseUrl(release.macDmgUrl, true), - macVst3Url: officialReleaseUrl(release.macVst3Url, true), - macStandaloneUrl: officialReleaseUrl(release.macStandaloneUrl, true), - checksumsUrl: officialReleaseUrl(release.checksumsUrl, true), - highlights - } - : { type: 'paused', source: 'local-release-manifest', version: release.version, releaseUrl, highlights }; - }; - const setLink = (element, href, enabled = true) => { if (!element) return; if (enabled && href) { @@ -177,243 +74,87 @@ element.removeAttribute('aria-disabled'); element.removeAttribute('data-release-pending'); } else { - element.href = '#download'; + element.href = RELEASE_FALLBACK; element.setAttribute('aria-disabled', 'true'); element.setAttribute('data-release-pending', 'true'); } }; - const lockDirectDownloadCtas = () => { - document.querySelectorAll('[data-installer-cta]').forEach((button) => setLink(button, null, false)); - ['vst3-link', 'standalone-link', 'mac-dmg-link', 'mac-vst3-link', 'mac-standalone-link'] - .forEach((id) => setLink(document.getElementById(id), null, false)); - }; - - const updateStructuredData = (release) => { - const script = document.getElementById('software-structured-data'); - if (!script || !release) return; - try { - const data = JSON.parse(script.textContent); - const software = data['@graph']?.find((entry) => entry['@type'] === 'SoftwareApplication'); - if (!software) return; - software.softwareVersion = release.version?.replace(/^v/i, '') || software.softwareVersion; - software.downloadUrl = release.installerUrl || release.releaseUrl; - software.releaseNotes = release.releaseUrl; - if (software.offers) software.offers.url = release.installerUrl || release.releaseUrl; - script.textContent = JSON.stringify(data, null, 2); - } catch (_) { - // Keep the reviewed static JSON-LD when runtime enhancement fails. - } - }; - - const ensureReleasePanelStyles = () => { - if (document.getElementById('latest-release-panel-style')) return; - const style = document.createElement('style'); - style.id = 'latest-release-panel-style'; - style.textContent = ` - .release-update-section{padding-top:clamp(42px,6vw,74px);padding-bottom:clamp(42px,6vw,74px)} - .release-update-shell{display:grid;grid-template-columns:minmax(0,.78fr) minmax(0,1.22fr);gap:clamp(24px,4vw,56px);align-items:start;padding:clamp(22px,3vw,34px);border:1px solid rgba(255,255,255,.11);border-radius:22px;background:linear-gradient(135deg,rgba(255,255,255,.055),rgba(255,255,255,.018));box-shadow:0 18px 50px rgba(0,0,0,.18)} - .release-update-copy h2{margin:.45rem 0 .7rem;font-size:clamp(1.65rem,3vw,2.5rem);line-height:1.08;font-weight:650;letter-spacing:-.03em} - .release-update-copy p{margin:0;color:var(--muted,#b7b2c2);font-size:.94rem} - .release-update-list{display:grid;gap:10px;margin:0;padding:0;list-style:none} - .release-update-list li{position:relative;padding:12px 14px 12px 38px;border:1px solid rgba(255,255,255,.08);border-radius:14px;background:rgba(0,0,0,.14);font-size:.96rem;line-height:1.48} - .release-update-list li::before{content:'↗';position:absolute;left:14px;top:11px;color:#d58cff;font-weight:700} - .release-update-actions{display:flex;align-items:center;gap:14px;flex-wrap:wrap;margin-top:18px} - .release-update-source{font-size:.78rem;color:var(--muted,#9892a4)} - .hero-mac-download svg{width:1em;height:1em;flex:0 0 1em;fill:currentColor} - @media (max-width:760px){.release-update-shell{grid-template-columns:1fr;border-radius:18px;padding:20px}.release-update-copy h2{font-size:1.65rem}.release-update-list li{font-size:.91rem}} - `; - document.head.appendChild(style); - }; - - const ensureReleasePanel = () => { - let section = document.getElementById('latest-release'); - if (section) return section; - ensureReleasePanelStyles(); - section = document.createElement('section'); - section.id = 'latest-release'; - section.className = 'section release-update-section'; - section.setAttribute('aria-labelledby', 'latest-release-title'); - section.innerHTML = ` -
-
-
- ${copy.eyebrow} -

${copy.heading} —

-

- -
- -
-
`; - const download = document.getElementById('download'); - if (download?.parentNode) download.parentNode.insertBefore(section, download); - else document.querySelector('main')?.appendChild(section); - return section; - }; - - const ensureHeroMacCta = () => { - let link = document.getElementById('mac-dmg-link-hero'); - if (link) return link; - const actions = document.querySelector('.landing-hero .hero-copy .actions'); - if (!actions) return null; - link = document.createElement('a'); - link.id = 'mac-dmg-link-hero'; - link.className = 'button secondary hero-mac-download'; - link.hidden = true; - link.setAttribute('aria-disabled', 'true'); - link.href = '#download'; - link.innerHTML = `${copy.macHero}`; - const explore = actions.querySelector('a[href^="#"]'); - actions.insertBefore(link, explore || null); - return link; - }; - - const renderHighlights = (release) => { - const panel = ensureReleasePanel(); - if (!panel) return; - panel.querySelector('[data-latest-release-version]').textContent = release.version || '—'; - panel.querySelector('[data-latest-release-name]').textContent = release.releaseName && release.releaseName !== release.version ? release.releaseName : ''; - setLink(panel.querySelector('[data-latest-release-link]'), release.releaseUrl || RELEASE_FALLBACK, true); - const list = panel.querySelector('[data-release-highlights]'); - list.replaceChildren(); - const highlights = release.highlights?.length ? release.highlights : [copy.fallbackHighlight]; - highlights.forEach((text) => { - const item = document.createElement('li'); - item.textContent = text; - list.appendChild(item); - }); - const publishedWrap = panel.querySelector('[data-release-published-wrap]'); - const published = panel.querySelector('[data-release-published]'); - if (release.publishedAt && publishedWrap && published) { - const date = new Date(release.publishedAt); - if (!Number.isNaN(date.getTime())) { - published.dateTime = date.toISOString(); - published.textContent = new Intl.DateTimeFormat(language === 'id' ? 'id-ID' : 'en-US', { - day: 'numeric', month: 'short', year: 'numeric' - }).format(date); - publishedWrap.hidden = false; - } - } - const source = panel.querySelector('[data-release-source-label]'); - if (source) source.textContent = release.source === 'github-latest-api' ? copy.liveSource : copy.reviewedSource; - }; - - const updateChecksumCommand = (installerUrl, version, installerName) => { + const updateChecksumCommand = (installerUrl) => { const command = document.getElementById('checksum-command'); - if (!command) return; - let filename = installerName || (version ? `ArSonKuPik-${version}-Windows-x64-Setup.exe` : 'ArSonKuPik-Windows-x64-Setup.exe'); + if (!command || !installerUrl) return; try { - if (installerUrl) filename = decodeURIComponent(new URL(installerUrl).pathname.split('/').pop()); + const filename = decodeURIComponent(new URL(installerUrl).pathname.split('/').pop()); + command.textContent = `Get-FileHash .\\${filename} -Algorithm SHA256`; } catch (_) {} - command.textContent = `Get-FileHash .\\${filename} -Algorithm SHA256`; - }; - - const ensureMacDownloadOption = (release) => { - if (!release?.macDmgUrl) return; - const grid = document.querySelector('#download .download-grid'); - if (!grid) return; - let option = document.getElementById('mac-download-option'); - if (!option) { - option = document.createElement('article'); - option.id = 'mac-download-option'; - option.className = 'download-option'; - const isId = language === 'id'; - option.innerHTML = ` - macOS Universal -

${isId ? 'Paket Mac' : 'Mac package'}

-

${isId - ? 'Universal untuk Apple Silicon dan Intel. Ad-hoc signed, tanpa Developer ID dan tanpa notarization.' - : 'Universal for Apple Silicon and Intel. Ad-hoc signed, without Developer ID signing or notarization.'}

- ${isId ? 'Unduh DMG Mac' : 'Download Mac DMG'} -
- VST3 ZIP - Standalone ZIP -
`; - grid.appendChild(option); - } - setLink(document.getElementById('mac-dmg-link'), release.macDmgUrl, true); - setLink(document.getElementById('mac-vst3-link'), release.macVst3Url, Boolean(release.macVst3Url)); - setLink(document.getElementById('mac-standalone-link'), release.macStandaloneUrl, Boolean(release.macStandaloneUrl)); }; const renderRelease = (release) => { - const state = release || { type: 'error', source: 'fallback', releaseUrl: RELEASE_FALLBACK, highlights: [] }; + const state = release || { + type: 'error', + source: 'local-release-manifest-error', + releaseUrl: RELEASE_FALLBACK + }; const enabled = state.type === 'enabled' && Boolean(state.installerUrl); document.querySelectorAll('[data-installer-cta]').forEach((button) => { setLink(button, state.installerUrl, enabled); - button.textContent = button.closest('#mobile-download-bar') || button.classList.contains('nav-download') - ? copy.installerShort - : copy.installer; }); - setLink(document.getElementById('vst3-link'), state.vst3Url, Boolean(state.vst3Url)); setLink(document.getElementById('standalone-link'), state.standaloneUrl, Boolean(state.standaloneUrl)); + setLink(document.getElementById('mac-dmg-link'), state.macDmgUrl, Boolean(state.macDmgUrl)); + setLink(document.getElementById('mac-vst3-link'), state.macVst3Url, Boolean(state.macVst3Url)); + setLink(document.getElementById('mac-standalone-link'), state.macStandaloneUrl, Boolean(state.macStandaloneUrl)); setLink(document.getElementById('checksums-link'), state.checksumsUrl, Boolean(state.checksumsUrl)); - ensureMacDownloadOption(state); - - const heroMac = ensureHeroMacCta(); - if (heroMac && state.macDmgUrl) { - setLink(heroMac, state.macDmgUrl, true); - heroMac.hidden = false; - } else if (heroMac) { - setLink(heroMac, null, false); - heroMac.hidden = true; - } - setLink(document.getElementById('release-link'), state.releaseUrl || RELEASE_FALLBACK, true); setLink(document.getElementById('distribution-link'), state.releaseUrl || RELEASE_FALLBACK, true); - document.querySelectorAll('[data-release-version]').forEach((element) => { - if (state.version) element.textContent = state.version; - }); - const statusText = state.type === 'enabled' ? copy.enabled : state.type === 'paused' ? copy.paused : copy.error; + if (state.version) { + document.querySelectorAll('[data-release-version]').forEach((element) => { + element.textContent = state.version; + }); + } + + const statusText = state.type === 'enabled' + ? copy.enabled + : state.type === 'paused' + ? copy.paused + : copy.error; document.querySelectorAll('[data-release-status]').forEach((element) => { element.textContent = statusText; }); + const banner = document.getElementById('distribution-banner'); if (banner) banner.dataset.state = state.type; - root.dataset.releaseSource = state.source || 'fallback'; - - updateChecksumCommand(state.installerUrl, state.version, state.installerName); - updateStructuredData(state); - renderHighlights(state); + root.dataset.releaseSource = state.source; + updateChecksumCommand(state.installerUrl); document.dispatchEvent(new CustomEvent('askp:release-ready', { detail: state })); }; - const fetchJson = async (url, options = {}, timeoutMs = 4500) => { + const resolveRelease = async () => { const controller = new AbortController(); - const timer = window.setTimeout(() => controller.abort(), timeoutMs); + const timer = window.setTimeout(() => controller.abort(), 3000); try { - const response = await fetch(url, { ...options, signal: controller.signal }); - if (!response.ok) throw new Error(`Release metadata returned ${response.status}`); - return await response.json(); + const response = await fetch(`${siteBase}/release.json`, { + cache: 'no-store', + credentials: 'same-origin', + signal: controller.signal + }); + if (!response.ok) throw new Error(`release.json returned ${response.status}`); + return parseManifest(await response.json()); + } catch (_) { + return null; } finally { window.clearTimeout(timer); } }; - const resolveRelease = async () => { - const localPromise = fetchJson(`${siteBase}/release.json`, { - cache: 'no-store', credentials: 'same-origin' - }, 3000).then(parseManifest).catch(() => null); - const latestPromise = fetchJson(LATEST_API, { - cache: 'no-cache', - headers: { Accept: 'application/vnd.github+json' }, - referrerPolicy: 'no-referrer' - }, 4500).then(parseGitHubRelease).catch(() => null); - const [latest, local] = await Promise.all([latestPromise, localPromise]); - return latest || local || null; - }; - const setupMobileNavigation = () => { const menu = document.querySelector('.mobile-nav'); if (!menu) return; - menu.querySelectorAll('a').forEach((link) => link.addEventListener('click', () => menu.removeAttribute('open'))); + menu.querySelectorAll('a').forEach((link) => { + link.addEventListener('click', () => menu.removeAttribute('open')); + }); document.addEventListener('click', (event) => { if (menu.open && !menu.contains(event.target)) menu.removeAttribute('open'); }); @@ -423,11 +164,13 @@ const bar = document.getElementById('mobile-download-bar'); const heroCta = document.getElementById('installer-link-bottom'); if (!bar || !heroCta) return; + bar.hidden = false; const setVisible = (visible) => { bar.dataset.visible = String(visible); bar.setAttribute('aria-hidden', String(!visible)); }; + if ('IntersectionObserver' in window) { new IntersectionObserver(([entry]) => setVisible(!entry.isIntersecting), { threshold: .15 }).observe(heroCta); } else { @@ -435,10 +178,7 @@ } }; - lockDirectDownloadCtas(); setupMobileNavigation(); setupMobileDownload(); - ensureReleasePanel(); - ensureHeroMacCta(); resolveRelease().then(renderRelease).catch(() => renderRelease(null)); })();