Rebuild ios.armsx2.net as a Vite + React app (main-site styling, screenshot carousel + lightbox, Manual install & Checksums pages, side dots, footer)

This commit is contained in:
medievalshell
2026-06-08 21:12:43 +02:00
parent 15833788fc
commit 442fdbd58c
13 changed files with 3881 additions and 1190 deletions
+6 -3
View File
@@ -26,7 +26,7 @@ jobs:
- name: Install dependencies - name: Install dependencies
run: npm ci run: npm ci
- name: Build project - name: Generate source
run: npm run generate:source run: npm run generate:source
- name: Validate generated source - name: Validate generated source
@@ -34,13 +34,16 @@ jobs:
npm run check:source npm run check:source
npm run validate:source npm run validate:source
- name: Build site (Vite)
run: npm run build
- name: Prepare deployment files - name: Prepare deployment files
if: github.event_name == 'push' if: github.event_name == 'push'
run: | run: |
rm -rf .deploy rm -rf .deploy
mkdir -p .deploy mkdir -p .deploy
cp index.html apps.json checksums.json .nojekyll .deploy/ cp -R dist/. .deploy/
cp -R assets ipas public .deploy/ cp -R ipas .deploy/
- name: Deploy to server - name: Deploy to server
if: github.event_name == 'push' if: github.event_name == 'push'
+4 -1183
View File
File diff suppressed because it is too large Load Diff
+1678 -2
View File
File diff suppressed because it is too large Load Diff
+9 -2
View File
@@ -8,6 +8,9 @@
"node": ">=22" "node": ">=22"
}, },
"scripts": { "scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"generate:source": "node scripts/generate-source.js", "generate:source": "node scripts/generate-source.js",
"check:source": "node scripts/generate-source.js --check", "check:source": "node scripts/generate-source.js --check",
"sync:upstream": "node scripts/sync-upstream-ipa.js", "sync:upstream": "node scripts/sync-upstream-ipa.js",
@@ -17,9 +20,13 @@
"dependencies": { "dependencies": {
"adm-zip": "^0.5.16", "adm-zip": "^0.5.16",
"bplist-parser": "^0.3.2", "bplist-parser": "^0.3.2",
"plist": "^3.1.0" "plist": "^3.1.0",
"react": "^19.1.1",
"react-dom": "^19.1.1"
}, },
"devDependencies": { "devDependencies": {
"ajv": "^8.17.1" "@vitejs/plugin-react": "^4.3.1",
"ajv": "^8.17.1",
"vite": "^5.4.10"
} }
} }
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 478 KiB

+668
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.
Binary file not shown.
+87
View File
@@ -0,0 +1,87 @@
export const absoluteUrl = (urlValue) => new URL(urlValue, document.baseURI).href;
const compactFormatter = new Intl.NumberFormat(undefined, {
maximumFractionDigits: 1,
});
export 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]}`;
};
export const firstParagraph = (textValue) => textValue
?.split(/\r?\n\s*\r?\n/)
.find((paragraph) => paragraph.trim())
?.trim() || "";
export const formattedBlocks = (textValue) => textValue
?.split(/\r?\n\s*\r?\n/)
.map((block) => block.trim())
.filter(Boolean)
.map((block) => {
const lines = block
.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean);
const headingMatch = lines[0]?.match(/^([^:\n]{1,80}):$/);
return {
heading: headingMatch ? headingMatch[1] : null,
lines: headingMatch ? lines.slice(1) : lines,
};
}) || [];
export const blockByHeading = (blocks, heading) =>
blocks.find((block) => block.heading?.toLowerCase() === heading.toLowerCase());
export const segmentsFromLines = (lines) => {
const segments = [];
let paragraphLines = [];
let listItems = [];
const flushParagraph = () => {
if (paragraphLines.length) {
segments.push({ type: "p", text: paragraphLines.join(" ") });
paragraphLines = [];
}
};
const flushList = () => {
if (listItems.length) {
segments.push({ type: "ul", items: listItems.slice() });
listItems = [];
}
};
for (const line of lines) {
const listItemMatch = line.match(/^-\s+(.+)$/);
if (listItemMatch) {
flushParagraph();
listItems.push(listItemMatch[1]);
continue;
}
flushList();
paragraphLines.push(line);
}
flushParagraph();
flushList();
return segments;
};
export const releaseNoteLines = (releaseNotes) => releaseNotes
?.split(/\r?\n/)
.map((line) => line.trim())
.filter(Boolean) || [];
+1267
View File
File diff suppressed because it is too large Load Diff
+10
View File
@@ -0,0 +1,10 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
import "./index.css";
createRoot(document.getElementById("root")).render(
<StrictMode>
<App />
</StrictMode>,
);
+142
View File
@@ -0,0 +1,142 @@
import { useEffect, useState } from "react";
import { absoluteUrl, firstParagraph, formatBytes, formattedBlocks } from "./format";
const DESCRIPTION_FALLBACK = "Modern PlayStation 2 emulation for iOS.";
const HERO_FALLBACK = "Add ARMSX2 iOS to LiveContainer, SideStore, or Feather to install the latest build.";
const buildSourceView = (sourcePayload, sourceRequestUrl) => {
const [sourceApp] = sourcePayload.apps || [];
const [currentVersion] = sourceApp?.versions || [];
if (!sourceApp || !currentVersion) {
throw new Error("apps.json does not contain an app version.");
}
return {
canonicalSourceUrl: absoluteUrl(sourcePayload.sourceURL || sourceRequestUrl),
sourceName: sourcePayload.name || sourceApp.name,
appName: sourceApp.name,
subtitle: sourceApp.subtitle || DESCRIPTION_FALLBACK,
iconURL: absoluteUrl(sourceApp.iconURL || "assets/icon.png"),
heroDescription: firstParagraph(sourceApp.localizedDescription) || HERO_FALLBACK,
version: currentVersion.version || "Unavailable",
date: currentVersion.date || "Unavailable",
minOSVersion: currentVersion.minOSVersion || "Device dependent",
minOSRequirementText: currentVersion.minOSVersion
? `Requires iOS ${currentVersion.minOSVersion} or later.`
: "Minimum iOS version depends on the current build.",
whatsNewVersionLabel: currentVersion.version
? `Version ${currentVersion.version}`
: "Latest version",
releaseNotes: currentVersion.localizedDescription || "",
screenshotURLs: Array.isArray(sourceApp.screenshotURLs)
? sourceApp.screenshotURLs.map(absoluteUrl)
: [],
descriptionBlocks: formattedBlocks(sourceApp.localizedDescription),
versionManifest: currentVersion,
};
};
const buildIntegrity = (checksumPayload, versionManifest) => {
const checksumEntry = checksumPayload.files?.find((checksumFile) => {
if (!versionManifest) {
return false;
}
return checksumFile.downloadURL === versionManifest.downloadURL
|| checksumFile.version === versionManifest.version;
});
if (!checksumEntry) {
return { unavailable: true };
}
const items = [
`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);
return { items };
};
export const useSource = () => {
const [state, setState] = useState({
loading: true,
error: null,
source: null,
integrity: null,
canonicalSourceUrl: typeof document !== "undefined"
? new URL("apps.json", document.baseURI).href
: "",
});
useEffect(() => {
let cancelled = false;
const sourceRequestUrl = new URL("apps.json", document.baseURI).href;
const checksumsRequestUrl = new URL("checksums.json", document.baseURI).href;
const run = async () => {
let source;
try {
const sourceResponse = await fetch(sourceRequestUrl, {
headers: { Accept: "application/json" },
});
if (!sourceResponse.ok) {
throw new Error(`Source request failed: ${sourceResponse.status}`);
}
source = buildSourceView(await sourceResponse.json(), sourceRequestUrl);
} catch (sourceError) {
if (cancelled) return;
const message = sourceError instanceof Error ? sourceError.message : String(sourceError);
console.error("Failed to load apps.json:", sourceError);
setState((prev) => ({
...prev,
loading: false,
error: message,
source: null,
}));
return;
}
if (cancelled) return;
document.title = source.appName;
const descriptionMeta = document.querySelector("meta[name='description']");
if (descriptionMeta) {
descriptionMeta.content = source.subtitle || DESCRIPTION_FALLBACK;
}
setState((prev) => ({
...prev,
loading: false,
error: null,
source,
canonicalSourceUrl: source.canonicalSourceUrl,
}));
try {
const checksumResponse = await fetch(checksumsRequestUrl, {
headers: { Accept: "application/json" },
});
if (!checksumResponse.ok) {
throw new Error(`Checksum request failed: ${checksumResponse.status}`);
}
const integrity = buildIntegrity(await checksumResponse.json(), source.versionManifest);
if (cancelled) return;
setState((prev) => ({ ...prev, integrity }));
} catch (checksumError) {
if (cancelled) return;
console.error("Failed to load checksums.json:", checksumError);
setState((prev) => ({ ...prev, integrity: { unavailable: true } }));
}
};
run();
return () => {
cancelled = true;
};
}, []);
return state;
};
+10
View File
@@ -0,0 +1,10 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
export default defineConfig({
base: "/",
plugins: [react()],
build: {
assetsDir: "static",
},
});