Make Crystal an installable web app with an offline shell (#39)
A manifest and icons rendered from icon.svg make the app installable. The native build emits a service worker that precaches exactly the built files and passes everything else to the network, so ROMs never reach its cache. New versions wait for the player's Update, which saves before reloading. Savegames offers installation, iPhone steps and persistent storage, and a full disk gets its own save error. Acceptance on physical iPhone and Android devices remains. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f289615bd2
commit
ce8129703f
20 files changed
+661
-15
No files matched your search
+61
-2
@@ -45,6 +45,12 @@ import { bindControls } from "./input/bindings";
|
||||
import { validateRom } from "./rom/validate";
|
||||
import { BrowserSaveRepository, encodeSave } from "./storage/saves";
|
||||
import { acquireSessionLock } from "./platform/session-lock";
|
||||
import {
|
||||
InstallableApp,
|
||||
quotaExceeded,
|
||||
requestPersistentStorage,
|
||||
storageStatus,
|
||||
} from "./platform/pwa";
|
||||
|
||||
const root = document.querySelector<HTMLElement>("#app")!;
|
||||
renderShell(root);
|
||||
@@ -1591,6 +1597,7 @@ const tabs = bindTabs(
|
||||
(id) => {
|
||||
if (id === "pokedex") showPokedexTab();
|
||||
if (id === "map") showAtlas();
|
||||
if (id === "saves") showAppSettings();
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1650,8 +1657,9 @@ async function save() {
|
||||
saveStatus.classList.remove("error");
|
||||
})
|
||||
.catch((error) => {
|
||||
saveStatus.textContent =
|
||||
"Session could not be saved. Export a backup to keep your progress.";
|
||||
saveStatus.textContent = quotaExceeded(error)
|
||||
? "This device is out of space for saves. Export a backup, then free some storage."
|
||||
: "Session could not be saved. Export a backup to keep your progress.";
|
||||
saveStatus.classList.add("error");
|
||||
throw error;
|
||||
})
|
||||
@@ -1785,6 +1793,57 @@ get<HTMLInputElement>("save-file").addEventListener("change", async (event) => {
|
||||
}
|
||||
});
|
||||
|
||||
/** The installable app: updates wait for the player, and the Savegames
|
||||
* tab offers installation and persistent storage. */
|
||||
const app = new InstallableApp({
|
||||
onUpdateReady: () => {
|
||||
get("update-banner").hidden = false;
|
||||
},
|
||||
onInstallChange: () => showAppSettings(),
|
||||
});
|
||||
void app.register();
|
||||
// An installed app asks once at start; browsers usually grant it then.
|
||||
if (app.installed) void requestPersistentStorage();
|
||||
get("update-app").addEventListener("click", async () => {
|
||||
// Save first: the reload ends the session.
|
||||
pause();
|
||||
await save()?.catch(() => {});
|
||||
if (!app.update()) message("The update is no longer waiting. Reload the page to get it.");
|
||||
});
|
||||
function showAppSettings() {
|
||||
const install = get<HTMLButtonElement>("install-app");
|
||||
install.hidden = app.installed || !app.canPrompt;
|
||||
get("install-status").textContent = app.installed
|
||||
? "Crystal is installed. The app opens offline; select your ROM each time you open it."
|
||||
: app.canPrompt
|
||||
? "Install Crystal to open it from your home screen and play the app offline. Select your ROM each time you open it."
|
||||
: app.appleDevice
|
||||
? "To install Crystal, open this page in Safari, choose Share, then Add to Home Screen."
|
||||
: "This browser does not offer to install Crystal. It still saves here and works online.";
|
||||
void storageStatus().then((status) => {
|
||||
const button = get<HTMLButtonElement>("persist-storage");
|
||||
button.hidden = !status || status.persisted;
|
||||
const used =
|
||||
status?.usage !== undefined && status.quota
|
||||
? ` Using ${(status.usage / 1_048_576).toFixed(1)} MB of ${(status.quota / 1_048_576).toFixed(0)} MB.`
|
||||
: "";
|
||||
get("storage-status").textContent = !status
|
||||
? "This browser does not report its storage. Export backups to keep your progress safe."
|
||||
: status.persisted
|
||||
? `Saves are kept on this device even when space runs low.${used}`
|
||||
: `The browser may clear saves when space runs low. Keep them, or export backups.${used}`;
|
||||
});
|
||||
}
|
||||
get("install-app").addEventListener("click", async () => {
|
||||
await app.install();
|
||||
showAppSettings();
|
||||
});
|
||||
get("persist-storage").addEventListener("click", async () => {
|
||||
const granted = await requestPersistentStorage();
|
||||
if (!granted) message("The browser kept its storage rules. Export backups to keep your progress safe.");
|
||||
showAppSettings();
|
||||
});
|
||||
|
||||
get("help").addEventListener("click", () => {
|
||||
pause();
|
||||
showHelpKeys();
|
||||
|
||||
Reference in new issue
Block a user