Prepare support for sharing files

This commit is contained in:
momo5502
2025-07-20 16:15:02 +02:00
parent 6eb4ef33ff
commit b7333b403d
4 changed files with 216 additions and 80 deletions

View File

@@ -1,9 +1,11 @@
import { ThemeProvider } from "@/components/theme-provider";
import { TooltipProvider } from "@/components/ui/tooltip";
import { HashRouter, Route, Routes, Navigate } from "react-router-dom";
import { Playground } from "./playground";
import { Playground, PlaygroundFile, storeEmulateData } from "./playground";
import { LandingPage } from "./landing-page";
import { useParams } from "react-router-dom";
import "@fontsource/inter/100.css";
import "@fontsource/inter/200.css";
import "@fontsource/inter/300.css";
@@ -16,6 +18,12 @@ import "@fontsource/inter/900.css";
import "./App.css";
function EmulateFile() {
const { encodedData } = useParams();
storeEmulateData(encodedData);
return <Navigate to="/playground" replace />;
}
function App() {
return (
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
@@ -25,6 +33,7 @@ function App() {
<Route path="*" element={<Navigate to="/" replace />} />
<Route path="/" element={<LandingPage />} />
<Route path="/playground" element={<Playground />} />
<Route path="/emulate/:encodedData?" element={<EmulateFile />} />
</Routes>
</HashRouter>
</TooltipProvider>

86
page/src/download.ts Normal file
View File

@@ -0,0 +1,86 @@
export type DownloadProgressHandler = (
receivedBytes: number,
totalBytes: number,
) => void;
export type DownloadPercentHandler = (percent: number) => void;
export function makePercentHandler(
handler: DownloadPercentHandler,
): DownloadProgressHandler {
const progress = {
tracked: 0,
};
return (current, total) => {
if (total == 0) {
return;
}
const percent = Math.floor((current * 100) / total);
const sanePercent = Math.max(Math.min(percent, 100), 0);
if (sanePercent + 1 > progress.tracked) {
progress.tracked = sanePercent + 1;
handler(sanePercent);
}
};
}
export function downloadBinaryFile(
file: string,
progressCallback: DownloadProgressHandler,
) {
return fetch(file, {
method: "GET",
headers: {
"Content-Type": "application/octet-stream",
},
}).then((response) => {
const maybeReader = response.body?.getReader();
if (!maybeReader) {
throw new Error("Bad reader");
}
const reader = maybeReader;
const contentLength = parseInt(
response.headers?.get("Content-Length") || "0",
);
let receivedLength = 0;
let chunks: Uint8Array<ArrayBufferLike>[] = [];
function processData(
res: ReadableStreamReadResult<Uint8Array<ArrayBufferLike>>,
): Promise<ArrayBuffer> {
if (res.value) {
chunks.push(res.value);
receivedLength += res.value.length;
}
progressCallback(receivedLength, contentLength);
if (!res.done) {
return reader.read().then(processData);
}
const chunksAll = new Uint8Array(receivedLength);
let position = 0;
for (const chunk of chunks) {
chunksAll.set(new Uint8Array(chunk), position);
position += chunk.length;
}
return Promise.resolve(chunksAll.buffer);
}
return reader.read().then(processData);
});
}
export function downloadBinaryFilePercent(
file: string,
progressCallback: DownloadPercentHandler,
) {
return downloadBinaryFile(file, makePercentHandler(progressCallback));
}

View File

@@ -1,62 +1,14 @@
import { downloadBinaryFilePercent, DownloadPercentHandler } from "./download";
import { parseZipFile, ProgressHandler } from "./zip-file";
import idbfsModule, { MainModule } from "@irori/idbfs";
type DownloadProgressHandler = (
receivedBytes: number,
totalBytes: number,
) => void;
function fetchFilesystemZip(progressCallback: DownloadProgressHandler) {
return fetch("./root.zip", {
method: "GET",
headers: {
"Content-Type": "application/octet-stream",
},
}).then((response) => {
const maybeReader = response.body?.getReader();
if (!maybeReader) {
throw new Error("Bad reader");
}
const reader = maybeReader;
const contentLength = parseInt(
response.headers?.get("Content-Length") || "0",
);
let receivedLength = 0;
let chunks: Uint8Array<ArrayBufferLike>[] = [];
function processData(
res: ReadableStreamReadResult<Uint8Array<ArrayBufferLike>>,
): Promise<ArrayBuffer> {
if (res.value) {
chunks.push(res.value);
receivedLength += res.value.length;
}
progressCallback(receivedLength, contentLength);
if (!res.done) {
return reader.read().then(processData);
}
const chunksAll = new Uint8Array(receivedLength);
let position = 0;
for (const chunk of chunks) {
chunksAll.set(new Uint8Array(chunk), position);
position += chunk.length;
}
return Promise.resolve(chunksAll.buffer);
}
return reader.read().then(processData);
});
function fetchFilesystemZip(progressCallback: DownloadPercentHandler) {
return downloadBinaryFilePercent("./root.zip", progressCallback);
}
async function fetchFilesystem(
progressHandler: ProgressHandler,
downloadProgressHandler: DownloadProgressHandler,
downloadProgressHandler: DownloadPercentHandler,
) {
const filesys = await fetchFilesystemZip(downloadProgressHandler);
return await parseZipFile(filesys, progressHandler);
@@ -74,6 +26,28 @@ function synchronizeIDBFS(idbfs: MainModule, populate: boolean) {
});
}
const filesystemPrefix = "/root/filesys/";
export function internalToWindowsPath(internalPath: string): string {
if (
!internalPath.startsWith(filesystemPrefix) ||
internalPath.length <= filesystemPrefix.length
) {
throw new Error("Invalid path");
}
const winPath = internalPath.substring(filesystemPrefix.length);
return `${winPath[0]}:${winPath.substring(1)}`;
}
export function windowsToInternalPath(windowsPath: string): string {
if (windowsPath.length < 2 || windowsPath[1] != ":") {
throw new Error("Invalid path");
}
return `${filesystemPrefix}${windowsPath[0]}${windowsPath.substring(2)}`;
}
async function initializeIDBFS() {
const idbfs = await idbfsModule();
@@ -210,7 +184,7 @@ export class Filesystem {
export async function setupFilesystem(
progressHandler: ProgressHandler,
downloadProgressHandler: DownloadProgressHandler,
downloadProgressHandler: DownloadPercentHandler,
) {
const idbfs = await initializeIDBFS();
const fs = new Filesystem(idbfs);

View File

@@ -3,7 +3,11 @@ import React from "react";
import { Output } from "@/components/output";
import { Emulator, EmulationState, isFinalState } from "./emulator";
import { Filesystem, setupFilesystem } from "./filesystem";
import {
Filesystem,
setupFilesystem,
windowsToInternalPath,
} from "./filesystem";
import { memory64 } from "wasm-feature-detect";
@@ -39,11 +43,17 @@ import {
} from "@/components/ui/drawer";
import { FilesystemExplorer } from "./filesystem-explorer";
import { EmulationStatus } from "./emulator";
import { TextTooltip } from "./components/text-tooltip";
import { EmulationSummary } from "./components/emulation-summary";
import { downloadBinaryFilePercent } from "./download";
interface PlaygroundProps {}
interface PlaygroundState {
export interface PlaygroundFile {
file: string;
storage: string;
}
export interface PlaygroundProps {}
export interface PlaygroundState {
settings: Settings;
filesystemPromise?: Promise<Filesystem>;
filesystem?: Filesystem;
@@ -52,28 +62,57 @@ interface PlaygroundState {
application?: string;
drawerOpen: boolean;
allowWasm64: boolean;
file?: PlaygroundFile;
}
function makePercentHandler(
handler: (percent: number) => void,
): (current: number, total: number) => void {
const progress = {
tracked: 0,
};
function decodeFileData(data: string | null): PlaygroundFile | undefined {
if (!data) {
return undefined;
}
return (current, total) => {
if (total == 0) {
return;
}
try {
const jsonData = JSON.parse(atob(data));
const percent = Math.floor((current * 100) / total);
const sanePercent = Math.max(Math.min(percent, 100), 0);
return {
file: jsonData.file,
storage: jsonData.storage,
};
} catch (e) {
console.log(e);
}
if (sanePercent + 1 > progress.tracked) {
progress.tracked = sanePercent + 1;
handler(sanePercent);
}
};
return undefined;
}
interface GlobalThisExt {
emulateCache?: string | null;
}
function getGlobalThis() {
return globalThis as GlobalThisExt;
}
export function storeEmulateData(data?: string) {
getGlobalThis().emulateCache = undefined;
if (data) {
localStorage.setItem("emulate", data);
} else {
localStorage.removeItem("emulate");
}
}
function getEmulateData() {
const gt = getGlobalThis();
if (gt.emulateCache) {
return gt.emulateCache;
}
const emulateData = localStorage.getItem("emulate");
localStorage.removeItem("emulate");
gt.emulateCache = emulateData;
return emulateData;
}
export class Playground extends React.Component<
@@ -90,13 +129,14 @@ export class Playground extends React.Component<
this.start = this.start.bind(this);
this.resetFilesys = this.resetFilesys.bind(this);
this.createEmulator = this.createEmulator.bind(this);
this.startEmulator = this.startEmulator.bind(this);
this.toggleEmulatorState = this.toggleEmulatorState.bind(this);
this.state = {
settings: loadSettings(),
drawerOpen: false,
allowWasm64: false,
file: decodeFileData(getEmulateData()),
};
}
@@ -104,6 +144,10 @@ export class Playground extends React.Component<
memory64().then((allowWasm64) => {
this.setState({ allowWasm64 });
});
if (this.state.file) {
this.emulateRemoteFile(this.state.file);
}
}
async resetFilesys() {
@@ -151,9 +195,9 @@ export class Playground extends React.Component<
(current, total, file) => {
this.logLine(`Processing filesystem (${current}/${total}): ${file}`);
},
makePercentHandler((percent) => {
(percent) => {
this.logLine(`Downloading filesystem: ${percent}%`);
}),
},
).then(resolve);
});
@@ -167,6 +211,29 @@ export class Playground extends React.Component<
this.setState({ drawerOpen });
}
async downloadFileToFilesystem(file: PlaygroundFile) {
const fs = await this.initFilesys();
const fileData = await downloadBinaryFilePercent(
file.storage,
(percent) => {
this.logLine(`Downloading binary: ${percent}%`);
},
);
await fs.storeFiles([
{
name: windowsToInternalPath(file.file),
data: fileData,
},
]);
}
async emulateRemoteFile(file: PlaygroundFile) {
await this.downloadFileToFilesystem(file);
await this.startEmulator(file.file);
}
async start() {
await this.initFilesys();
this.setDrawerOpen(true);
@@ -195,7 +262,7 @@ export class Playground extends React.Component<
}
}
async createEmulator(userFile: string) {
async startEmulator(userFile: string) {
this.state.emulator?.stop();
this.output.current?.clear();
@@ -316,7 +383,7 @@ export class Playground extends React.Component<
<FilesystemExplorer
filesystem={this.state.filesystem}
iconCache={this.iconCache}
runFile={this.createEmulator}
runFile={this.startEmulator}
resetFilesys={this.resetFilesys}
path={["c"]}
/>