From 5327c8a648c5fb9a4bddb8274a8d7414eca0d957 Mon Sep 17 00:00:00 2001 From: Hyv Date: Mon, 16 Mar 2026 14:32:29 -0400 Subject: [PATCH] fix: save layout without showSaveFilePicker Prefer File System Access API when available, but fall back to a Blob download so Save Layout works in unsupported/blocked contexts. Only treat AbortError as a real user cancel. --- .../panes/configure-panes/save-load.tsx | 96 ++++++++++++------- 1 file changed, 59 insertions(+), 37 deletions(-) diff --git a/src/components/panes/configure-panes/save-load.tsx b/src/components/panes/configure-panes/save-load.tsx index 2c589be7..31f1b78e 100644 --- a/src/components/panes/configure-panes/save-load.tsx +++ b/src/components/panes/configure-panes/save-load.tsx @@ -110,48 +110,70 @@ export const Pane: FC = () => { } }; - const saveLayout = async () => { - const {name, vendorProductId} = selectedDefinition; - const suggestedName = - name.replace(/[^a-zA-Z0-9]/g, '_').toLowerCase() + '.layout.json'; - try { - const handle = await window.showSaveFilePicker({ - suggestedName, - }); - const encoderValues = await getEncoderValues(); - const saveFile: ViaSaveFile = { - name, - vendorProductId, - macros: [...expressions], - layers: rawLayers.map( - (layer: {keymap: number[]}) => - layer.keymap.map( - (keyByte: number) => - getCodeForByte(keyByte, basicKeyToByte, byteToKey) || '', - ), // TODO: should empty string be empty keycode instead? - ), - encoders: encoderValues, - }; - - const content = stringify(saveFile); - const blob = new Blob([content], {type: 'application/json'}); - const writable = await handle.createWritable(); - await writable.write(blob); - await writable.close(); - } catch (err) { - console.log('User cancelled save file request'); + // `showSaveFilePicker` (File System Access API) isn't supported everywhere and can + // fail without showing a prompt in some contexts. Prefer it when available, but + // fall back to a standard Blob download so "Save" always works. + const saveBlobAsFile = async (blob: Blob, suggestedName: string) => { + const showSaveFilePicker = window.showSaveFilePicker; + if (typeof showSaveFilePicker === 'function') { + try { + const handle = await showSaveFilePicker({ + suggestedName, + types: [ + { + description: 'JSON', + accept: {'application/json': ['.json']}, + }, + ], + }); + const writable = await handle.createWritable(); + await writable.write(blob); + await writable.close(); + return; + } catch (err) { + if ((err as any)?.name === 'AbortError') { + return; + } + console.error(err); + } } - /* const url = URL.createObjectURL(blob); + try { + const link = document.createElement('a'); + link.href = url; + link.download = suggestedName; + link.rel = 'noopener'; + document.body.appendChild(link); + link.click(); + link.remove(); + } finally { + setTimeout(() => URL.revokeObjectURL(url), 0); + } + }; - const link = document.createElement('a'); - link.href = url; - link.download = defaultFilename; + const saveLayout = async () => { + const {name, vendorProductId} = selectedDefinition; + const suggestedName = + name.replace(/[^a-zA-Z0-9]/g, '_').toLowerCase() + '.layout.json'; + const encoderValues = await getEncoderValues(); + const saveFile: ViaSaveFile = { + name, + vendorProductId, + macros: [...expressions], + layers: rawLayers.map( + (layer: {keymap: number[]}) => + layer.keymap.map( + (keyByte: number) => + getCodeForByte(keyByte, basicKeyToByte, byteToKey) || '', + ), // TODO: should empty string be empty keycode instead? + ), + encoders: encoderValues, + }; - link.click(); - URL.revokeObjectURL(url); -*/ + const content = stringify(saveFile); + const blob = new Blob([content], {type: 'application/json'}); + await saveBlobAsFile(blob, suggestedName); }; const loadLayout = ([file]: Blob[]) => {