perf: Shorten shared character sheet URLs
Use URL-safe base64 (base64url) for the ?c= payload so it no longer needs percent-encoding, and prune empty string/array fields before encoding. Decoding accepts standard base64, so existing links still load. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -133,22 +133,37 @@ const BLANK_BONDS: Bond[] = Array.from({ length: 6 }, () => ({
|
|||||||
feelings: [],
|
feelings: [],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// URL-safe base64 (RFC 4648 §5): avoids +, /, = so the result needs no
|
||||||
|
// percent-encoding in a query string. Accepts standard base64 on decode too,
|
||||||
|
// so links shared before this change still load.
|
||||||
|
function bytesToBase64Url(bytes: Uint8Array) {
|
||||||
|
let binary = "";
|
||||||
|
for (let i = 0; i < bytes.length; i++)
|
||||||
|
binary += String.fromCharCode(bytes[i]);
|
||||||
|
return btoa(binary)
|
||||||
|
.replace(/\+/g, "-")
|
||||||
|
.replace(/\//g, "_")
|
||||||
|
.replace(/=+$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
function base64UrlToBytes(b64: string) {
|
||||||
|
let std = b64.replace(/-/g, "+").replace(/_/g, "/");
|
||||||
|
if (std.length % 4) std += "=".repeat(4 - (std.length % 4));
|
||||||
|
return Uint8Array.from(atob(std), (c) => c.charCodeAt(0));
|
||||||
|
}
|
||||||
|
|
||||||
async function compressToBase64(str: string) {
|
async function compressToBase64(str: string) {
|
||||||
const stream = new CompressionStream("deflate-raw");
|
const stream = new CompressionStream("deflate-raw");
|
||||||
const writer = stream.writable.getWriter();
|
const writer = stream.writable.getWriter();
|
||||||
writer.write(new TextEncoder().encode(str));
|
writer.write(new TextEncoder().encode(str));
|
||||||
writer.close();
|
writer.close();
|
||||||
const buf = await new Response(stream.readable).arrayBuffer();
|
const buf = await new Response(stream.readable).arrayBuffer();
|
||||||
const bytes = new Uint8Array(buf);
|
return bytesToBase64Url(new Uint8Array(buf));
|
||||||
let binary = "";
|
|
||||||
for (let i = 0; i < bytes.length; i++)
|
|
||||||
binary += String.fromCharCode(bytes[i]);
|
|
||||||
return btoa(binary);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function decompressFromBase64(b64: string) {
|
async function decompressFromBase64(b64: string) {
|
||||||
try {
|
try {
|
||||||
const bytes = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
|
const bytes = base64UrlToBytes(b64);
|
||||||
const stream = new DecompressionStream("deflate-raw");
|
const stream = new DecompressionStream("deflate-raw");
|
||||||
const writer = stream.writable.getWriter();
|
const writer = stream.writable.getWriter();
|
||||||
writer.write(bytes);
|
writer.write(bytes);
|
||||||
@@ -160,6 +175,16 @@ async function decompressFromBase64(b64: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Drop empty strings and empty arrays before encoding: a sheet is mostly blank
|
||||||
|
// text fields, and applyData() restores any missing key to its default.
|
||||||
|
function pruneEmpty(data: Record<string, unknown>) {
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(data).filter(
|
||||||
|
([, v]) => v !== "" && !(Array.isArray(v) && v.length === 0),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function CharacterSheet() {
|
export default function CharacterSheet() {
|
||||||
const [activeTab, setActiveTab] = useState("main");
|
const [activeTab, setActiveTab] = useState("main");
|
||||||
const [urlMode, setUrlMode] = useState(false);
|
const [urlMode, setUrlMode] = useState(false);
|
||||||
@@ -452,12 +477,11 @@ export default function CharacterSheet() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const copyShareURL = useCallback(async () => {
|
const copyShareURL = useCallback(async () => {
|
||||||
const encoded = await compressToBase64(JSON.stringify(collectData()));
|
const encoded = await compressToBase64(
|
||||||
|
JSON.stringify(pruneEmpty(collectData())),
|
||||||
|
);
|
||||||
await navigator.clipboard.writeText(
|
await navigator.clipboard.writeText(
|
||||||
window.location.origin +
|
window.location.origin + window.location.pathname + "?c=" + encoded,
|
||||||
window.location.pathname +
|
|
||||||
"?c=" +
|
|
||||||
encodeURIComponent(encoded),
|
|
||||||
);
|
);
|
||||||
setCopyStatus(true);
|
setCopyStatus(true);
|
||||||
setTimeout(() => setCopyStatus(false), 2000);
|
setTimeout(() => setCopyStatus(false), 2000);
|
||||||
|
|||||||
Reference in New Issue
Block a user