Add Web Push, guided start screen, SVG icons, chunked uploads, rename
- Web Push (VAPID + RFC 8291, pure Rust): permission/question/finished notifications while no app window is visible; settings with test button - Guided new-session flow: task, craft target (website/app/game/old software/fresh), clean room or deep, description, model, folder (new, existing or cloned git repository); originals upload to original/ - Craft phase bar (craft_phase tool) for study/specify/build/check/ready - Chunked uploads (8 MB) up to server.max_upload_mb (default 500 MB) - All UI glyphs and emojis replaced by themeable SVG icons - Rename dialog with manual name or model-generated suggestion Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
013695722a
commit
a0d0e707f2
48 files changed
+2724
-228
No files matched your search
@@ -0,0 +1,53 @@
|
||||
import { get, post } from "./api";
|
||||
|
||||
function b64ToBytes(b64: string): Uint8Array<ArrayBuffer> {
|
||||
const s = atob(b64.replace(/-/g, "+").replace(/_/g, "/") + "===".slice((b64.length + 3) % 4));
|
||||
const out = new Uint8Array(new ArrayBuffer(s.length));
|
||||
for (let i = 0; i < s.length; i++) out[i] = s.charCodeAt(i);
|
||||
return out;
|
||||
}
|
||||
|
||||
export const pushSupported = () => "serviceWorker" in navigator && "PushManager" in window && "Notification" in window;
|
||||
|
||||
/** Current push subscription of this device (null if none). */
|
||||
export async function currentSubscription(): Promise<PushSubscription | null> {
|
||||
if (!pushSupported()) return null;
|
||||
const reg = await navigator.serviceWorker.ready;
|
||||
return reg.pushManager.getSubscription();
|
||||
}
|
||||
|
||||
/** Ask for permission, subscribe this device and register it with the server. */
|
||||
export async function enablePush(): Promise<void> {
|
||||
if (!pushSupported()) throw new Error("Push is not supported by this browser (on iOS: install the app to the home screen first).");
|
||||
const perm = await Notification.requestPermission();
|
||||
if (perm !== "granted") throw new Error("Notifications were not allowed.");
|
||||
const reg = await navigator.serviceWorker.ready;
|
||||
const { key } = await get<{ key: string }>("/push/key");
|
||||
let sub = await reg.pushManager.getSubscription();
|
||||
// A subscription for another server key must be replaced.
|
||||
if (sub && sub.options.applicationServerKey) {
|
||||
const cur = new Uint8Array(sub.options.applicationServerKey);
|
||||
const want = b64ToBytes(key);
|
||||
if (cur.length !== want.length || cur.some((b, i) => b !== want[i])) {
|
||||
await sub.unsubscribe();
|
||||
sub = null;
|
||||
}
|
||||
}
|
||||
if (!sub) sub = await reg.pushManager.subscribe({ userVisibleOnly: true, applicationServerKey: b64ToBytes(key) });
|
||||
await post("/push/subscribe", sub.toJSON());
|
||||
localStorage.setItem("codecraft.push", "1");
|
||||
}
|
||||
|
||||
export async function disablePush(): Promise<void> {
|
||||
const sub = await currentSubscription();
|
||||
if (sub) {
|
||||
await fetch("/api/push/subscribe", { method: "DELETE", credentials: "same-origin", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ endpoint: sub.endpoint }) });
|
||||
await sub.unsubscribe();
|
||||
}
|
||||
localStorage.removeItem("codecraft.push");
|
||||
}
|
||||
|
||||
export async function testPush(): Promise<number> {
|
||||
const r = await post<{ sent: number }>("/push/test", {});
|
||||
return r.sent;
|
||||
}
|
||||
Reference in new issue
Block a user