Skip to content

Commit b42123d

Browse files
committed
fix: simplify window object access, improve error handling
1 parent cf95456 commit b42123d

File tree

8 files changed

+76
-22
lines changed

8 files changed

+76
-22
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"devDependencies": {
1212
"@types/react": "^19.2.2",
1313
"@types/react-dom": "^19.2.2",
14+
"@types/wicg-file-system-access": "^2023.10.7",
1415
"spicetify-creator": "^1.0.17"
1516
}
1617
}

pnpm-lock.yaml

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/i18n.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ export type TranslationKeys = {
131131
failedToExportJson: string;
132132
importCanceled: string;
133133
failedToImportJson: string;
134+
failedToImportJsonAccess: string;
134135
};
135136
dialogs: {
136137
saveSnapshot: {

src/locales/de.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,9 +130,10 @@ export const de: TranslationKeys = {
130130
replaceResult: "Ersetzt; einige Elemente konnten nicht in die Warteschlange eingereiht werden ({{added}}/{{total}})",
131131
exportCanceled: "Export abgebrochen ({{reason}})",
132132
exportedToDownloads: "{{filename}} in den Downloads-Ordner exportiert",
133-
failedToExportJson: "JSON-Export fehlgeschlagen",
133+
failedToExportJson: "JSON-Export fehlgeschlagen: {{reason}}",
134134
importCanceled: "Import abgebrochen ({{reason}})",
135-
failedToImportJson: "JSON-Import fehlgeschlagen",
135+
failedToImportJson: "JSON-Import fehlgeschlagen: {{reason}}",
136+
failedToImportJsonAccess: "JSON-Import fehlgeschlagen: Datei ist leer oder nicht zugänglich",
136137
},
137138
dialogs: {
138139
saveSnapshot: {

src/locales/en.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,9 +130,10 @@ export const en: TranslationKeys = {
130130
replaceResult: "Replaced; some items couldn't be queued ({{added}}/{{total}})",
131131
exportCanceled: "Export canceled ({{reason}})",
132132
exportedToDownloads: "Exported {{filename}} to Downloads folder",
133-
failedToExportJson: "Failed to export JSON",
133+
failedToExportJson: "Failed to export JSON: {{reason}}",
134134
importCanceled: "Import canceled ({{reason}})",
135-
failedToImportJson: "Failed to import JSON",
135+
failedToImportJson: "Failed to import JSON: {{reason}}",
136+
failedToImportJsonAccess: "Failed to import JSON: File is empty or inaccessible",
136137
},
137138
dialogs: {
138139
saveSnapshot: {

src/locales/es.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,9 +130,10 @@ export const es: TranslationKeys = {
130130
replaceResult: "Reemplazado; algunos elementos no se pudieron encolar ({{added}}/{{total}})",
131131
exportCanceled: "Exportación cancelada ({{reason}})",
132132
exportedToDownloads: "{{filename}} exportado a la carpeta de Descargas",
133-
failedToExportJson: "Falló la exportación JSON",
133+
failedToExportJson: "Falló la exportación JSON: {{reason}}",
134134
importCanceled: "Importación cancelada ({{reason}})",
135-
failedToImportJson: "Falló la importación JSON",
135+
failedToImportJson: "Falló la importación JSON: {{reason}}",
136+
failedToImportJsonAccess: "Falló la importación JSON: El archivo está vacío o no es accesible",
136137
},
137138
dialogs: {
138139
saveSnapshot: {

src/locales/fr.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,9 +130,10 @@ export const fr: TranslationKeys = {
130130
replaceResult: "Remplacé ; certains éléments n'ont pas pu être mis en file ({{added}}/{{total}})",
131131
exportCanceled: "Exportation annulée ({{reason}})",
132132
exportedToDownloads: "{{filename}} exporté vers le dossier Téléchargements",
133-
failedToExportJson: "Échec de l'exportation JSON",
133+
failedToExportJson: "Échec de l'exportation JSON : {{reason}}",
134134
importCanceled: "Importation annulée ({{reason}})",
135-
failedToImportJson: "Échec de l'importation JSON",
135+
failedToImportJson: "Échec de l'importation JSON : {{reason}}",
136+
failedToImportJsonAccess: "Échec de l'importation JSON : Le fichier est vide ou inaccessible",
136137
},
137138
dialogs: {
138139
saveSnapshot: {

src/utils.ts

Lines changed: 54 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,9 @@ export async function downloadJson(filename: string, data: any): Promise<void> {
6666
const json = JSON.stringify(data, null, 2);
6767
const blob = new Blob([json], { type: "application/json" });
6868

69-
const w: any = window as any;
70-
if (w && typeof w.showSaveFilePicker === "function") {
69+
if (typeof window?.showSaveFilePicker === "function") {
7170
try {
72-
const handle = await w.showSaveFilePicker({
71+
const handle = await window.showSaveFilePicker({
7372
suggestedName: filename,
7473
types: [
7574
{
@@ -82,11 +81,28 @@ export async function downloadJson(filename: string, data: any): Promise<void> {
8281
await writable.write(blob);
8382
await writable.close();
8483
return;
85-
} catch (err: any) {
86-
if (err && (err.name === "AbortError" || err.name === "NotAllowedError")) {
87-
showWarningToast(t('toasts.exportCanceled', { reason: err.name }));
84+
} catch (err: unknown) {
85+
const isError = err instanceof Error;
86+
const errorName = isError ? err.name : 'Unknown';
87+
const errorMessage = isError ? err.message : '';
88+
89+
// Check if this is a file system error disguised as AbortError
90+
const isFileSystemError = errorName === "AbortError" && (
91+
errorMessage.includes('Failed to create') ||
92+
errorMessage.includes('Failed to truncate') ||
93+
errorMessage.includes('permission') ||
94+
errorMessage.includes('access') ||
95+
errorMessage.includes('denied')
96+
);
97+
98+
// Only treat as user cancellation if it's truly a user abort
99+
if (errorName === "AbortError" && !isFileSystemError) {
88100
return;
89101
}
102+
103+
const reason = isError ? `${errorName}: ${errorMessage}` : 'Unknown error';
104+
showErrorToast(t('toasts.failedToExportJson', { reason }), { duration: 10000 });
105+
return;
90106
}
91107
}
92108

@@ -102,18 +118,19 @@ export async function downloadJson(filename: string, data: any): Promise<void> {
102118
showSuccessToast(t('toasts.exportedToDownloads', { filename }));
103119
} catch (e) {
104120
console.warn(`${APP_NAME}: downloadJson failed`, e);
105-
showErrorToast(t('toasts.failedToExportJson'));
121+
const isError = e instanceof Error;
122+
const reason = isError ? `${e.name}: ${e.message}` : 'Unknown error';
123+
showErrorToast(t('toasts.failedToExportJson', { reason }), { duration: 10000 });
106124
}
107125
}
108126

109127
export async function uploadJson<T>(): Promise<T | null> {
110128
try {
111-
const w: any = window as any;
112129
let fileContent: string | undefined;
113130

114-
if (w && typeof w.showOpenFilePicker === "function") {
131+
if (typeof window?.showOpenFilePicker === "function") {
115132
try {
116-
const [handle] = await w.showOpenFilePicker({
133+
const [handle] = await window.showOpenFilePicker({
117134
multiple: false,
118135
types: [
119136
{
@@ -124,11 +141,15 @@ export async function uploadJson<T>(): Promise<T | null> {
124141
});
125142
const file = await handle.getFile();
126143
fileContent = await file.text();
127-
} catch (err: any) {
128-
if (err && (err.name === "AbortError" || err.name === "NotAllowedError")) {
129-
showWarningToast(t('toasts.importCanceled', { reason: err.name }));
144+
} catch (err: unknown) {
145+
const isError = err instanceof Error;
146+
const errorName = isError ? err.name : 'Unknown';
147+
if (errorName === "AbortError") {
130148
return null;
131149
}
150+
const reason = isError ? `${err.name}: ${err.message}` : 'Unknown error';
151+
showErrorToast(t('toasts.failedToImportJson', { reason }), { duration: 10000 });
152+
return null;
132153
}
133154
} else {
134155
// Fallback for browsers that don't support showOpenFilePicker
@@ -141,10 +162,27 @@ export async function uploadJson<T>(): Promise<T | null> {
141162
if (input.files?.length) {
142163
try {
143164
const file = input.files[0];
165+
166+
if (file.size === 0) {
167+
showErrorToast(t('toasts.failedToImportJsonAccess'), { duration: 10000 });
168+
resolve(null);
169+
return;
170+
}
171+
144172
const text = await file.text();
173+
174+
if (text.length === 0) {
175+
showErrorToast(t('toasts.failedToImportJsonAccess'), { duration: 10000 });
176+
resolve(null);
177+
return;
178+
}
179+
145180
resolve(text);
146181
} catch (readErr) {
147182
console.error(`${APP_NAME}: file read failed`, readErr);
183+
const isError = readErr instanceof Error;
184+
const reason = isError ? `${readErr.name}: ${readErr.message}` : 'Unknown error';
185+
showErrorToast(t('toasts.failedToImportJson', { reason }), { duration: 10000 });
148186
resolve(null);
149187
}
150188
} else {
@@ -166,7 +204,9 @@ export async function uploadJson<T>(): Promise<T | null> {
166204
return data as T;
167205
} catch (e) {
168206
console.warn(`${APP_NAME}: uploadJson failed`, e);
169-
showErrorToast(t('toasts.failedToImportJson'));
207+
const isError = e instanceof Error;
208+
const reason = isError ? `${e.name}: ${e.message}` : 'Unknown error';
209+
showErrorToast(t('toasts.failedToImportJson', { reason }), { duration: 10000 });
170210
return null;
171211
}
172212
}

0 commit comments

Comments
 (0)