Files
Armsx2-Repo/index.html
T
2026-06-01 23:28:04 +02:00

1018 lines
31 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="dark">
<meta
name="description"
content="ARMSX2 iOS source for LiveContainer, SideStore, and Feather."
>
<title>ARMSX2 iOS</title>
<link rel="icon" href="assets/icon.png">
<style>
:root {
color-scheme: dark;
--background: #121316;
--surface: rgba(31, 32, 35, 0.5);
--surface-strong: rgba(31, 32, 35, 0.72);
--surface-soft: rgba(31, 32, 35, 0.34);
--border: rgba(255, 255, 255, 0.12);
--border-strong: rgba(150, 112, 233, 0.56);
--text: #f8fafc;
--muted: #d4d4d8;
--soft: #a1a1aa;
--accent: #a48fff;
--accent-soft: #9670e9;
--accent-strong: #5a1ae5;
--accent-blue: #1a50e5;
--green: #89f2b7;
--amber: #ffd166;
}
* {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
margin: 0;
min-height: 100vh;
background:
radial-gradient(120vh at 24% -14%, rgba(90, 26, 229, 0.2), rgba(90, 26, 229, 0.12) 30%, transparent 68%),
radial-gradient(135vh at 82% 20%, rgba(26, 80, 229, 0.15), rgba(26, 80, 229, 0.06) 34%, transparent 70%),
radial-gradient(95vh at 45% 88%, rgba(150, 112, 233, 0.08), transparent 64%),
linear-gradient(180deg, #121316 0%, #0d0e12 46%, #121316 100%);
color: var(--text);
font-family:
ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
"Segoe UI", sans-serif;
overflow-x: hidden;
}
body::before {
position: fixed;
inset: 0;
z-index: -1;
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.035), transparent 18rem),
radial-gradient(70rem 38rem at 50% 0%, rgba(164, 143, 255, 0.08), transparent 72%);
content: "";
pointer-events: none;
}
a {
color: inherit;
}
button {
min-height: 48px;
border: 1px solid var(--border);
border-radius: 8px;
background: rgba(31, 32, 35, 0.62);
color: var(--text);
cursor: pointer;
font: inherit;
font-size: 0.96rem;
font-weight: 750;
letter-spacing: 0;
backdrop-filter: blur(18px);
transition:
background 140ms ease,
border-color 140ms ease,
color 140ms ease,
transform 140ms ease;
}
button:focus-visible,
a:focus-visible {
outline: 3px solid rgba(164, 143, 255, 0.42);
outline-offset: 3px;
}
button:hover {
border-color: rgba(164, 143, 255, 0.38);
background: rgba(31, 32, 35, 0.82);
transform: translateY(-1px);
}
.page {
width: min(100%, 1120px);
margin: 0 auto;
padding: 22px 16px 52px;
}
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
min-height: 44px;
padding: 0 2px;
color: var(--soft);
font-size: 0.88rem;
}
.topbar strong {
color: var(--text);
font-weight: 800;
}
.topbar a {
color: var(--muted);
text-decoration: none;
}
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.hero {
display: grid;
gap: 30px;
padding: 46px 0 38px;
border-bottom: 1px solid var(--border);
}
.hero-copy {
max-width: 720px;
}
.identity {
display: flex;
align-items: center;
gap: 18px;
margin-bottom: 22px;
}
.app-icon {
width: clamp(72px, 10vw, 86px);
flex: 0 0 auto;
aspect-ratio: 1;
border: 1px solid rgba(164, 143, 255, 0.22);
border-radius: 24px;
box-shadow:
0 18px 44px rgba(0, 0, 0, 0.36),
0 0 30px rgba(90, 26, 229, 0.22);
}
.platforms {
margin: 0 0 8px;
color: var(--accent);
font-size: 0.71rem;
font-weight: 850;
letter-spacing: 0.14em;
text-transform: uppercase;
}
h1,
h2,
h3,
p {
margin-block-start: 0;
}
h1 {
margin-block-end: 0;
background: linear-gradient(135deg, #ffffff 28%, rgba(255, 255, 255, 0.72));
background-clip: text;
color: transparent;
font-size: clamp(2.35rem, 7.4vw, 4.1rem);
font-weight: 780;
line-height: 0.98;
letter-spacing: 0;
}
h2 {
margin-block-end: 16px;
font-size: clamp(1.45rem, 5vw, 2.2rem);
line-height: 1.08;
letter-spacing: 0;
}
h3 {
margin-block-end: 6px;
font-size: 1rem;
line-height: 1.2;
}
.subtitle {
max-width: 660px;
margin-block-end: 14px;
color: var(--muted);
font-size: clamp(1.08rem, 3.8vw, 1.36rem);
line-height: 1.42;
}
.hero-description {
max-width: 660px;
margin-block-end: 0;
color: var(--soft);
font-size: 1rem;
line-height: 1.72;
}
.install-panel {
display: grid;
gap: 16px;
align-self: start;
min-width: 0;
padding: 18px;
border: 1px solid var(--border-strong);
border-radius: 8px;
background:
linear-gradient(145deg, rgba(164, 143, 255, 0.12), rgba(26, 80, 229, 0.08) 42%, transparent 82%),
var(--surface-strong);
box-shadow:
0 24px 72px rgba(0, 0, 0, 0.34),
0 0 0 1px rgba(164, 143, 255, 0.08),
0 0 34px rgba(90, 26, 229, 0.14);
backdrop-filter: blur(24px);
}
.source-url {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: center;
gap: 10px;
min-width: 0;
padding: 13px;
border: 1px solid rgba(255, 255, 255, 0.11);
border-radius: 8px;
background: rgba(18, 19, 22, 0.72);
overflow: hidden;
}
.source-url code {
display: block;
min-width: 0;
max-width: 100%;
color: var(--muted);
font-family:
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
"Liberation Mono", monospace;
font-size: 0.86rem;
line-height: 1.5;
white-space: normal;
word-break: normal;
overflow-wrap: anywhere;
}
.source-copy {
display: inline-grid;
width: 36px;
min-height: 36px;
place-items: center;
padding: 0;
border-color: rgba(164, 143, 255, 0.34);
background: rgba(31, 32, 35, 0.64);
color: var(--muted);
}
.source-copy svg {
width: 16px;
height: 16px;
}
.source-copy:hover {
color: var(--text);
transform: none;
}
.install-actions {
display: grid;
gap: 10px;
}
.primary-action {
min-height: 58px;
border-color: var(--border-strong);
background: linear-gradient(135deg, var(--accent-soft), var(--accent-strong));
color: #ffffff;
font-size: 1.04rem;
box-shadow:
0 14px 28px rgba(90, 26, 229, 0.24),
inset 0 -7px 11px rgba(164, 143, 255, 0.12);
}
.primary-action:hover {
background: linear-gradient(135deg, #a48fff, #6932e0);
}
.secondary-actions {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
.note,
.status {
margin: 0;
color: var(--soft);
font-size: 0.9rem;
line-height: 1.5;
}
.status {
min-height: 1.4em;
}
.metrics {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
}
.metric {
min-width: 0;
padding: 13px;
border: 1px solid var(--border);
border-radius: 8px;
background: var(--surface);
}
.metric span {
display: block;
color: var(--soft);
font-size: 0.76rem;
}
.metric strong {
display: block;
margin-top: 4px;
color: var(--text);
font-size: 0.98rem;
overflow-wrap: anywhere;
}
.section {
padding: 32px 0;
border-bottom: 1px solid var(--border);
}
.section-header {
display: flex;
align-items: end;
justify-content: space-between;
gap: 16px;
margin-bottom: 18px;
}
.section-kicker {
margin: 0 0 6px;
color: var(--accent);
font-size: 0.74rem;
font-weight: 900;
letter-spacing: 0.14em;
text-transform: uppercase;
}
.screenshot-track {
display: grid;
grid-auto-columns: min(76vw, 310px);
grid-auto-flow: column;
gap: 14px;
margin: 0 -16px;
padding: 0 16px 10px;
overflow-x: auto;
overscroll-behavior-x: contain;
scroll-snap-type: x mandatory;
scrollbar-color: rgba(255, 255, 255, 0.26) transparent;
}
.screenshot-frame {
margin: 0;
scroll-snap-align: start;
}
.screenshot-frame img {
display: block;
width: 100%;
aspect-ratio: 9 / 19.5;
border: 1px solid var(--border);
border-radius: 8px;
background: #050509;
object-fit: cover;
box-shadow: 0 22px 60px rgba(0, 0, 0, 0.32);
}
.content-grid {
display: grid;
gap: 28px;
padding-top: 32px;
}
.copy-block {
color: var(--muted);
font-size: 1rem;
line-height: 1.75;
}
.copy-block p {
margin-block-end: 14px;
}
.copy-block p:last-child {
margin-block-end: 0;
}
.change-list,
.requirement-list {
display: grid;
gap: 10px;
margin: 0;
padding: 0;
list-style: none;
}
.change-list li,
.requirement-list li {
position: relative;
padding-left: 18px;
color: var(--muted);
line-height: 1.55;
}
.change-list li::before,
.requirement-list li::before {
position: absolute;
top: 0.68em;
left: 0;
width: 6px;
height: 6px;
border-radius: 99px;
background: var(--green);
content: "";
}
.requirement-list li::before {
background: var(--amber);
}
.requirement-list a {
color: var(--muted);
font-weight: 800;
text-decoration-color: rgba(164, 143, 255, 0.62);
text-underline-offset: 3px;
}
.requirement-list a:hover {
color: var(--text);
}
.version-line {
margin-bottom: 14px;
color: var(--soft);
}
.integrity {
display: grid;
gap: 10px;
padding-top: 16px;
color: var(--soft);
font-size: 0.9rem;
}
.integrity-row {
display: flex;
flex-wrap: wrap;
gap: 8px 14px;
}
.integrity code {
color: var(--muted);
font-family:
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas,
"Liberation Mono", monospace;
font-size: 0.82rem;
}
.integrity a {
color: var(--accent);
font-weight: 800;
text-decoration: none;
}
.loading-surface {
min-height: 1.2em;
border-radius: 6px;
background:
linear-gradient(
90deg,
rgba(255, 255, 255, 0.06),
rgba(255, 255, 255, 0.13),
rgba(255, 255, 255, 0.06)
);
background-size: 220% 100%;
animation: loading 1.2s ease-in-out infinite;
}
@keyframes loading {
from {
background-position: 100% 0;
}
to {
background-position: -100% 0;
}
}
@media (max-width: 720px) {
.topbar {
align-items: center;
flex-direction: row;
}
.identity {
align-items: flex-start;
flex-direction: column;
gap: 18px;
}
.app-icon {
width: 82px;
}
.install-panel {
padding: 16px;
}
.source-url {
grid-template-columns: minmax(0, 1fr);
}
.source-url code {
font-size: 0.8rem;
line-height: 1.45;
word-break: break-all;
}
.source-copy {
width: 100%;
}
.metrics,
.secondary-actions {
grid-template-columns: 1fr;
}
}
@media (min-width: 820px) {
.page {
padding: 32px 24px 70px;
}
.screenshot-track {
grid-auto-columns: minmax(235px, 1fr);
grid-template-columns: repeat(3, minmax(0, 1fr));
grid-auto-flow: unset;
margin: 0;
padding: 0;
overflow: visible;
}
.content-grid {
grid-template-columns: minmax(0, 1.1fr) minmax(310px, 0.9fr);
gap: 48px;
}
.description-section {
grid-column: 1 / -1;
}
}
@media (min-width: 940px) {
.hero {
grid-template-columns: minmax(0, 1fr) minmax(340px, 430px);
align-items: center;
gap: clamp(32px, 5vw, 56px);
min-height: auto;
padding: clamp(58px, 8vw, 84px) 0 68px;
}
h1 {
white-space: nowrap;
}
.install-panel {
position: sticky;
top: 24px;
}
}
</style>
</head>
<body>
<div class="page">
<header class="topbar" aria-label="Page">
<strong id="source-name">ARMSX2 iOS</strong>
<a href="#integrity">Checksums</a>
</header>
<main>
<section class="hero" aria-labelledby="app-title">
<div class="hero-copy">
<div class="identity">
<img class="app-icon" id="app-icon" src="assets/icon.png" alt="">
<div>
<p class="platforms">LiveContainer / SideStore / Feather</p>
<h1 id="app-title">ARMSX2 iOS</h1>
</div>
</div>
<p class="subtitle" id="app-subtitle">
<span class="loading-surface" aria-hidden="true">&nbsp;</span>
</p>
<p class="hero-description" id="hero-description">
<span class="loading-surface" aria-hidden="true">&nbsp;</span>
</p>
</div>
<aside class="install-panel" aria-label="Install options">
<div class="source-url">
<code id="source-url">Loading apps.json</code>
<button class="source-copy" type="button" data-action="copy" aria-label="Copy install link" title="Copy install link">
<span class="visually-hidden">Copy install link</span>
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none">
<path d="M8 8.5A2.5 2.5 0 0 1 10.5 6h6A2.5 2.5 0 0 1 19 8.5v6a2.5 2.5 0 0 1-2.5 2.5h-6A2.5 2.5 0 0 1 8 14.5v-6Z" stroke="currentColor" stroke-width="1.8"/>
<path d="M6 13.5h-.5A2.5 2.5 0 0 1 3 11V5.5A2.5 2.5 0 0 1 5.5 3H11a2.5 2.5 0 0 1 2.5 2.5V6" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>
</svg>
</button>
</div>
<div class="metrics" aria-label="Latest version">
<div class="metric">
<span>Version</span>
<strong id="version">Loading</strong>
</div>
<div class="metric">
<span>Updated</span>
<strong id="date">Loading</strong>
</div>
<div class="metric">
<span>Minimum iOS</span>
<strong id="minimum-os">Loading</strong>
</div>
</div>
<div class="install-actions" aria-label="Install actions">
<button class="primary-action" type="button" data-action="livecontainer">
Install via LiveContainer
</button>
<div class="secondary-actions">
<button type="button" data-action="sidestore">SideStore</button>
<button type="button" data-action="feather">Feather</button>
</div>
</div>
<p class="note">
New here? Start with LiveContainer.
</p>
<p class="status" id="status" aria-live="polite"></p>
</aside>
</section>
<section class="section" aria-labelledby="screenshots-title">
<div class="section-header">
<div>
<p class="section-kicker">Preview</p>
<h2 id="screenshots-title">Screenshots</h2>
</div>
</div>
<div class="screenshot-track" id="screenshots" aria-label="App screenshots"></div>
</section>
<div class="content-grid">
<section class="section" aria-labelledby="whats-new-title">
<p class="section-kicker">Latest Update</p>
<h2 id="whats-new-title">What&rsquo;s New</h2>
<p class="version-line" id="whats-new-version">Loading release notes</p>
<div class="copy-block" id="whats-new"></div>
</section>
<section class="section" aria-labelledby="requirements-title">
<p class="section-kicker">Before You Install</p>
<h2 id="requirements-title">Requirements</h2>
<ul class="requirement-list">
<li id="minimum-os-requirement">Minimum iOS version loads from apps.json.</li>
<li>A <a href="https://livecontainer.github.io/docs/guides/jit-support" target="_blank" rel="noopener noreferrer">JIT-capable setup</a> is needed for playable performance.</li>
<li>LiveContainer automatic import requires LiveContainer 3.7.0 or later.</li>
<li>You&rsquo;ll need your own PS2 BIOS file and dumped game disc images, such as ISO files.</li>
</ul>
</section>
<section class="section description-section" aria-labelledby="description-title">
<p class="section-kicker">About</p>
<h2 id="description-title">Description</h2>
<div class="copy-block" id="description"></div>
<div class="integrity" id="integrity">
<div class="integrity-row" id="integrity-summary">
<span>Checksum details load from checksums.json.</span>
</div>
<a href="checksums.json">View checksums.json</a>
</div>
</section>
</div>
</main>
</div>
<script type="module">
const sourceRequestUrl = new URL("apps.json", document.baseURI).href;
const checksumsRequestUrl = new URL("checksums.json", document.baseURI).href;
const elements = {
appIcon: document.getElementById("app-icon"),
appTitle: document.getElementById("app-title"),
sourceName: document.getElementById("source-name"),
appSubtitle: document.getElementById("app-subtitle"),
heroDescription: document.getElementById("hero-description"),
sourceUrl: document.getElementById("source-url"),
version: document.getElementById("version"),
date: document.getElementById("date"),
minimumOs: document.getElementById("minimum-os"),
minimumOsRequirement: document.getElementById("minimum-os-requirement"),
screenshots: document.getElementById("screenshots"),
whatsNewVersion: document.getElementById("whats-new-version"),
whatsNew: document.getElementById("whats-new"),
description: document.getElementById("description"),
integritySummary: document.getElementById("integrity-summary"),
status: document.getElementById("status"),
};
let canonicalSourceUrl = sourceRequestUrl;
let latestVersionManifest;
const compactFormatter = new Intl.NumberFormat(undefined, {
maximumFractionDigits: 1,
});
const absoluteUrl = (urlValue) => new URL(urlValue, document.baseURI).href;
const formatBytes = (byteCount) => {
if (!Number.isFinite(byteCount)) {
return "Unknown size";
}
const sizeUnits = ["B", "KB", "MB", "GB"];
const unitIndex = Math.min(
Math.floor(Math.log(byteCount) / Math.log(1024)),
sizeUnits.length - 1,
);
const unitValue = byteCount / (1024 ** unitIndex);
return `${compactFormatter.format(unitValue)} ${sizeUnits[unitIndex]}`;
};
const firstParagraph = (textValue) => textValue
?.split(/\r?\n\s*\r?\n/)
.find((paragraph) => paragraph.trim())
?.trim() || "";
const clearElement = (element) => {
while (element.firstChild) {
element.firstChild.remove();
}
};
const renderParagraphs = (targetElement, textValue) => {
clearElement(targetElement);
const paragraphs = textValue
?.split(/\r?\n\s*\r?\n/)
.map((paragraph) => paragraph.trim())
.filter(Boolean) || [];
if (!paragraphs.length) {
targetElement.textContent = "Details are temporarily unavailable.";
return;
}
for (const paragraph of paragraphs) {
const paragraphElement = document.createElement("p");
paragraphElement.textContent = paragraph;
targetElement.append(paragraphElement);
}
};
const renderWhatsNew = (targetElement, releaseNotes) => {
clearElement(targetElement);
const releaseNoteLines = releaseNotes
?.split(/\r?\n/)
.map((releaseNoteLine) => releaseNoteLine.trim())
.filter(Boolean) || [];
if (!releaseNoteLines.length) {
targetElement.textContent = "Release notes are temporarily unavailable.";
return;
}
const summaryElement = document.createElement("p");
summaryElement.textContent = releaseNoteLines[0];
targetElement.append(summaryElement);
if (releaseNoteLines.length === 1) {
return;
}
const releaseList = document.createElement("ul");
releaseList.className = "change-list";
for (const releaseNoteLine of releaseNoteLines.slice(1)) {
const releaseItem = document.createElement("li");
releaseItem.textContent = releaseNoteLine;
releaseList.append(releaseItem);
}
targetElement.append(releaseList);
};
const renderScreenshots = (screenshotUrls, appName) => {
clearElement(elements.screenshots);
if (!Array.isArray(screenshotUrls) || !screenshotUrls.length) {
elements.screenshots.textContent = "Screenshots are temporarily unavailable.";
return;
}
screenshotUrls.forEach((screenshotUrl, screenshotIndex) => {
const screenshotFrame = document.createElement("figure");
const screenshotImage = document.createElement("img");
screenshotFrame.className = "screenshot-frame";
screenshotImage.src = absoluteUrl(screenshotUrl);
screenshotImage.alt = `${appName} screenshot ${screenshotIndex + 1}`;
screenshotImage.loading = screenshotIndex === 0 ? "eager" : "lazy";
screenshotImage.decoding = "async";
screenshotFrame.append(screenshotImage);
elements.screenshots.append(screenshotFrame);
});
};
const writeClipboard = async (clipboardText) => {
if (!navigator.clipboard?.writeText) {
throw new Error("Clipboard API is unavailable.");
}
await navigator.clipboard.writeText(clipboardText);
};
const renderSource = (sourcePayload) => {
const [sourceApp] = sourcePayload.apps || [];
const [currentVersion] = sourceApp?.versions || [];
if (!sourceApp || !currentVersion) {
throw new Error("apps.json does not contain an app version.");
}
latestVersionManifest = currentVersion;
canonicalSourceUrl = absoluteUrl(sourcePayload.sourceURL || sourceRequestUrl);
document.title = sourceApp.name;
document.querySelector("meta[name='description']").content = sourceApp.subtitle
|| "ARMSX2 iOS source for LiveContainer, SideStore, and Feather.";
elements.appIcon.src = absoluteUrl(sourceApp.iconURL || "assets/icon.png");
elements.appTitle.textContent = sourceApp.name;
elements.sourceName.textContent = sourcePayload.name || sourceApp.name;
elements.appSubtitle.textContent = sourceApp.subtitle || "PlayStation 2 emulation, built for iOS.";
elements.heroDescription.textContent = firstParagraph(sourceApp.localizedDescription)
|| "Add ARMSX2 iOS to LiveContainer, SideStore, or Feather to install the latest build.";
elements.sourceUrl.textContent = canonicalSourceUrl;
elements.version.textContent = currentVersion.version || "Unavailable";
elements.date.textContent = currentVersion.date || "Unavailable";
elements.minimumOs.textContent = currentVersion.minOSVersion || "Device dependent";
elements.minimumOsRequirement.textContent = currentVersion.minOSVersion
? `Requires iOS ${currentVersion.minOSVersion} or later.`
: "Minimum iOS version depends on the current build.";
elements.whatsNewVersion.textContent = currentVersion.version
? `Version ${currentVersion.version}`
: "Latest version";
renderScreenshots(sourceApp.screenshotURLs, sourceApp.name);
renderWhatsNew(elements.whatsNew, currentVersion.localizedDescription);
renderParagraphs(elements.description, sourceApp.localizedDescription);
};
const renderIntegrity = (checksumPayload) => {
const checksumEntry = checksumPayload.files?.find((checksumFile) => {
if (!latestVersionManifest) {
return false;
}
return checksumFile.downloadURL === latestVersionManifest.downloadURL
|| checksumFile.version === latestVersionManifest.version;
});
if (!checksumEntry) {
elements.integritySummary.textContent = "Integrity metadata is temporarily unavailable.";
return;
}
clearElement(elements.integritySummary);
const integrityItems = [
`SHA-256 ${checksumEntry.sha256?.slice(0, 16) || "unavailable"}...`,
formatBytes(checksumEntry.size),
checksumEntry.buildVersion ? `Build ${checksumEntry.buildVersion}` : "",
checksumPayload.generatedAt ? `Generated ${checksumPayload.generatedAt.slice(0, 10)}` : "",
].filter(Boolean);
for (const integrityItem of integrityItems) {
const integrityElement = document.createElement("span");
if (integrityItem.startsWith("SHA-256")) {
const codeElement = document.createElement("code");
codeElement.textContent = integrityItem;
integrityElement.append(codeElement);
} else {
integrityElement.textContent = integrityItem;
}
elements.integritySummary.append(integrityElement);
}
};
const loadSource = async () => {
try {
const sourceResponse = await fetch(sourceRequestUrl, {
headers: { Accept: "application/json" },
});
if (!sourceResponse.ok) {
throw new Error(`Source request failed: ${sourceResponse.status}`);
}
renderSource(await sourceResponse.json());
} catch (sourceError) {
const message = sourceError instanceof Error ? sourceError.message : String(sourceError);
console.error("Failed to load apps.json:", sourceError);
elements.sourceUrl.textContent = canonicalSourceUrl;
elements.version.textContent = "Unavailable";
elements.date.textContent = "Unavailable";
elements.minimumOs.textContent = "Unavailable";
elements.appSubtitle.textContent = "Install details are temporarily unavailable.";
elements.heroDescription.textContent = "Try again in a moment or copy the install link manually.";
elements.whatsNewVersion.textContent = "Release notes unavailable";
elements.whatsNew.textContent = "apps.json could not be loaded.";
elements.description.textContent = "apps.json could not be loaded.";
elements.status.textContent = `Install details could not be loaded: ${message}`;
return;
}
try {
const checksumResponse = await fetch(checksumsRequestUrl, {
headers: { Accept: "application/json" },
});
if (!checksumResponse.ok) {
throw new Error(`Checksum request failed: ${checksumResponse.status}`);
}
renderIntegrity(await checksumResponse.json());
} catch (checksumError) {
const message = checksumError instanceof Error ? checksumError.message : String(checksumError);
console.error("Failed to load checksums.json:", checksumError);
elements.integritySummary.textContent = "Integrity metadata is temporarily unavailable.";
elements.status.textContent = `Integrity metadata could not be loaded: ${message}`;
}
};
document.querySelector("[data-action='livecontainer']").addEventListener("click", () => {
window.location.href = `livecontainer://source?url=${encodeURIComponent(canonicalSourceUrl)}`;
});
document.querySelector("[data-action='sidestore']").addEventListener("click", () => {
window.location.href = `sidestore://source?url=${encodeURIComponent(canonicalSourceUrl)}`;
});
document.querySelector("[data-action='feather']").addEventListener("click", () => {
window.location.href = `feather://source/${encodeURIComponent(canonicalSourceUrl)}`;
});
document.querySelector("[data-action='copy']").addEventListener("click", async () => {
try {
await writeClipboard(canonicalSourceUrl);
elements.status.textContent = "Install link copied.";
} catch (clipboardError) {
const message = clipboardError instanceof Error ? clipboardError.message : String(clipboardError);
console.error("Failed to copy install link:", clipboardError);
elements.status.textContent = `Copy failed: ${message}`;
}
});
loadSource();
</script>
</body>
</html>