mirror of
https://github.com/ARMSX2/Armsx2-Repo.git
synced 2026-08-24 16:52:58 -07:00
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:
@@ -26,7 +26,7 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Build project
|
||||
- name: Generate source
|
||||
run: npm run generate:source
|
||||
|
||||
- name: Validate generated source
|
||||
@@ -34,13 +34,16 @@ jobs:
|
||||
npm run check:source
|
||||
npm run validate:source
|
||||
|
||||
- name: Build site (Vite)
|
||||
run: npm run build
|
||||
|
||||
- name: Prepare deployment files
|
||||
if: github.event_name == 'push'
|
||||
run: |
|
||||
rm -rf .deploy
|
||||
mkdir -p .deploy
|
||||
cp index.html apps.json checksums.json .nojekyll .deploy/
|
||||
cp -R assets ipas public .deploy/
|
||||
cp -R dist/. .deploy/
|
||||
cp -R ipas .deploy/
|
||||
|
||||
- name: Deploy to server
|
||||
if: github.event_name == 'push'
|
||||
|
||||
+4
-1183
File diff suppressed because it is too large
Load Diff
Generated
+1678
-2
File diff suppressed because it is too large
Load Diff
+9
-2
@@ -8,6 +8,9 @@
|
||||
"node": ">=22"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview",
|
||||
"generate:source": "node scripts/generate-source.js",
|
||||
"check:source": "node scripts/generate-source.js --check",
|
||||
"sync:upstream": "node scripts/sync-upstream-ipa.js",
|
||||
@@ -17,9 +20,13 @@
|
||||
"dependencies": {
|
||||
"adm-zip": "^0.5.16",
|
||||
"bplist-parser": "^0.3.2",
|
||||
"plist": "^3.1.0"
|
||||
"plist": "^3.1.0",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"ajv": "^8.17.1"
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"ajv": "^8.17.1",
|
||||
"vite": "^5.4.10"
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 478 KiB |
+668
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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>,
|
||||
);
|
||||
@@ -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;
|
||||
};
|
||||
@@ -0,0 +1,10 @@
|
||||
import { defineConfig } from "vite";
|
||||
import react from "@vitejs/plugin-react";
|
||||
|
||||
export default defineConfig({
|
||||
base: "/",
|
||||
plugins: [react()],
|
||||
build: {
|
||||
assetsDir: "static",
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user