shorten-share-url #2
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user