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
|
- 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
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"
|
"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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
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