shorten-share-url #2

Merged
drew merged 5 commits from shorten-share-url into master 2026-06-16 23:25:22 -04:00
Showing only changes of commit 21c8bf5be1 - Show all commits

View File

@@ -133,22 +133,37 @@ const BLANK_BONDS: Bond[] = Array.from({ length: 6 }, () => ({
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) {
const stream = new CompressionStream("deflate-raw");
const writer = stream.writable.getWriter();
writer.write(new TextEncoder().encode(str));
writer.close();
const buf = await new Response(stream.readable).arrayBuffer();
const bytes = new Uint8Array(buf);
let binary = "";
for (let i = 0; i < bytes.length; i++)
binary += String.fromCharCode(bytes[i]);
return btoa(binary);
return bytesToBase64Url(new Uint8Array(buf));
}
async function decompressFromBase64(b64: string) {
try {
const bytes = Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
const bytes = base64UrlToBytes(b64);
const stream = new DecompressionStream("deflate-raw");
const writer = stream.writable.getWriter();
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() {
const [activeTab, setActiveTab] = useState("main");
const [urlMode, setUrlMode] = useState(false);
@@ -452,12 +477,11 @@ export default function CharacterSheet() {
);
const copyShareURL = useCallback(async () => {
const encoded = await compressToBase64(JSON.stringify(collectData()));
const encoded = await compressToBase64(
JSON.stringify(pruneEmpty(collectData())),
);
await navigator.clipboard.writeText(
window.location.origin +
window.location.pathname +
"?c=" +
encodeURIComponent(encoded),
window.location.origin + window.location.pathname + "?c=" + encoded,
);
setCopyStatus(true);
setTimeout(() => setCopyStatus(false), 2000);