diff --git a/locales/ar-SA/opencadstudio.ftl b/locales/ar-SA/opencadstudio.ftl index 61bf72640..70a9274a2 100644 --- a/locales/ar-SA/opencadstudio.ftl +++ b/locales/ar-SA/opencadstudio.ftl @@ -290,6 +290,8 @@ common = .autosave-copy-could-not-be-stored = تعذر تخزين نسخة الحفظ التلقائي: __ocs_fmt_0__ .restored-from-the-autosave-copy = تم استرداد "__ocs_fmt_0__" من نسخة الحفظ التلقائي .autosave-copy-could-not-be-read = تعذر قراءة نسخة الحفظ التلقائي: __ocs_fmt_0__ + .an-open-is-already-in-progress = هناك عملية فتح جارية بالفعل. + .autosave-copy-could-not-be-queued-too-many-pending-opens = تعذر وضع نسخة الحفظ التلقائي في قائمة الانتظار: عدد عمليات الفتح المعلقة كبير جدًا .jogged-radius = نصف قطر منكسر .none-use-shortcuts-set-key-command = (لا شيء — استخدم: SHORTCUTS SET ) .alignment-indented-label-title-case = المحاذاة: diff --git a/locales/bg-BG/opencadstudio.ftl b/locales/bg-BG/opencadstudio.ftl index 73e9b07ef..1019d86cf 100644 --- a/locales/bg-BG/opencadstudio.ftl +++ b/locales/bg-BG/opencadstudio.ftl @@ -277,6 +277,8 @@ common = .autosave-copy-could-not-be-stored = Копието от автоматичното записване не можа да бъде съхранено: __ocs_fmt_0__ .restored-from-the-autosave-copy = "__ocs_fmt_0__" беше възстановен от копието от автоматичното записване .autosave-copy-could-not-be-read = Копието от автоматичното записване не можа да бъде прочетено: __ocs_fmt_0__ + .an-open-is-already-in-progress = Вече се отваря файл. + .autosave-copy-could-not-be-queued-too-many-pending-opens = Копието от автоматичното записване не можа да бъде поставено на опашка: твърде много чакащи отваряния .jogged-radius = Радиус с чупка .none-use-shortcuts-set-key-command = (няма — използвай: SHORTCUTS SET <клавиш> <команда>) .alignment-indented-label-title-case = Подравняване: diff --git a/locales/cs-CZ/opencadstudio.ftl b/locales/cs-CZ/opencadstudio.ftl index c24b628ca..8e33eada4 100644 --- a/locales/cs-CZ/opencadstudio.ftl +++ b/locales/cs-CZ/opencadstudio.ftl @@ -277,6 +277,8 @@ common = .autosave-copy-could-not-be-stored = Kopii automatického uložení nelze uložit: __ocs_fmt_0__ .restored-from-the-autosave-copy = "__ocs_fmt_0__" byl obnoven z kopie automatického uložení .autosave-copy-could-not-be-read = Kopii automatického uložení nelze přečíst: __ocs_fmt_0__ + .an-open-is-already-in-progress = Otevírání souboru již probíhá. + .autosave-copy-could-not-be-queued-too-many-pending-opens = Kopii automatického uložení nelze zařadit do fronty: příliš mnoho čekajících otevření .jogged-radius = Zalomený poloměr .none-use-shortcuts-set-key-command =(žádné — použijte: SHORTCUTS SET ) .alignment-indented-label-title-case = Zarovnání: diff --git a/locales/de-DE/opencadstudio.ftl b/locales/de-DE/opencadstudio.ftl index d74ca35f7..7048cfcd3 100644 --- a/locales/de-DE/opencadstudio.ftl +++ b/locales/de-DE/opencadstudio.ftl @@ -278,6 +278,8 @@ common = .autosave-copy-could-not-be-stored = Automatische Sicherungskopie konnte nicht gespeichert werden: __ocs_fmt_0__ .restored-from-the-autosave-copy = „__ocs_fmt_0__“ wurde aus der automatischen Sicherungskopie wiederhergestellt .autosave-copy-could-not-be-read = Automatische Sicherungskopie konnte nicht gelesen werden: __ocs_fmt_0__ + .an-open-is-already-in-progress = Es wird bereits eine Datei geöffnet. + .autosave-copy-could-not-be-queued-too-many-pending-opens = Automatische Sicherungskopie konnte nicht in die Warteschlange gestellt werden: zu viele ausstehende Öffnungsvorgänge .jogged-radius = Geknickter Radius .none-use-shortcuts-set-key-command = (keine — verwenden: SHORTCUTS SET ) .alignment-indented-label-title-case = Ausrichtung: diff --git a/locales/el-GR/opencadstudio.ftl b/locales/el-GR/opencadstudio.ftl index 72268449c..8e34d27f2 100644 --- a/locales/el-GR/opencadstudio.ftl +++ b/locales/el-GR/opencadstudio.ftl @@ -277,6 +277,8 @@ common = .autosave-copy-could-not-be-stored = Δεν ήταν δυνατή η αποθήκευση του αντιγράφου αυτόματης αποθήκευσης: __ocs_fmt_0__ .restored-from-the-autosave-copy = Το "__ocs_fmt_0__" επαναφέρθηκε από το αντίγραφο αυτόματης αποθήκευσης .autosave-copy-could-not-be-read = Δεν ήταν δυνατή η ανάγνωση του αντιγράφου αυτόματης αποθήκευσης: __ocs_fmt_0__ + .an-open-is-already-in-progress = Μια διαδικασία ανοίγματος βρίσκεται ήδη σε εξέλιξη. + .autosave-copy-could-not-be-queued-too-many-pending-opens = Το αντίγραφο αυτόματης αποθήκευσης δεν μπόρεσε να μπει στην ουρά: υπερβολικά πολλές εκκρεμείς ανοίξεις .revolve-created-objects-failed-sources = REVOLVE: δημιουργήθηκαν __ocs_arg_created__ αντικείμενα· __ocs_arg_failed__ αρχικά αντικείμενα δεν μπόρεσαν να περιστραφούν. .revolve-no-selected-object-could-be-revolved = REVOLVE: κανένα επιλεγμένο αντικείμενο δεν μπόρεσε να περιστραφεί με τις ζητούμενες επιλογές. .none-use-shortcuts-set-key-command = (κανένα — χρησιμοποιήστε: SHORTCUTS SET <πλήκτρο> <εντολή>) diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl index 6e320bd2d..5244af9bb 100644 --- a/locales/en-US/opencadstudio.ftl +++ b/locales/en-US/opencadstudio.ftl @@ -277,6 +277,8 @@ common = .autosave-copy-could-not-be-stored = Autosave copy could not be stored: __ocs_fmt_0__ .restored-from-the-autosave-copy = Restored "__ocs_fmt_0__" from the autosave copy .autosave-copy-could-not-be-read = Autosave copy could not be read: __ocs_fmt_0__ + .an-open-is-already-in-progress = An open is already in progress. + .autosave-copy-could-not-be-queued-too-many-pending-opens = Autosave copy could not be queued: too many pending opens .none-use-shortcuts-set-key-command = (none — use: SHORTCUTS SET ) .alignment-indented-label-title-case = Alignment: .background-fill-enabled = Background fill enabled diff --git a/locales/es-ES/opencadstudio.ftl b/locales/es-ES/opencadstudio.ftl index c0f0f22d7..14d142b25 100644 --- a/locales/es-ES/opencadstudio.ftl +++ b/locales/es-ES/opencadstudio.ftl @@ -278,6 +278,8 @@ common = .autosave-copy-could-not-be-stored = No se pudo guardar la copia de autoguardado: __ocs_fmt_0__ .restored-from-the-autosave-copy = "__ocs_fmt_0__" restaurado desde la copia de autoguardado .autosave-copy-could-not-be-read = No se pudo leer la copia de autoguardado: __ocs_fmt_0__ + .an-open-is-already-in-progress = Ya hay una apertura en curso. + .autosave-copy-could-not-be-queued-too-many-pending-opens = No se pudo poner en cola la copia de autoguardado: demasiadas aperturas pendientes .jogged-radius = Radio con recodo .none-use-shortcuts-set-key-command = (ninguno — use: SHORTCUTS SET ) .alignment-indented-label-title-case = Alineación: diff --git a/locales/fi-FI/opencadstudio.ftl b/locales/fi-FI/opencadstudio.ftl index 5a6dcf367..7f804b402 100644 --- a/locales/fi-FI/opencadstudio.ftl +++ b/locales/fi-FI/opencadstudio.ftl @@ -277,6 +277,8 @@ common = .autosave-copy-could-not-be-stored = Automaattisen tallennuksen kopiota ei voitu tallentaa: __ocs_fmt_0__ .restored-from-the-autosave-copy = "__ocs_fmt_0__" palautettiin automaattisen tallennuksen kopiosta .autosave-copy-could-not-be-read = Automaattisen tallennuksen kopiota ei voitu lukea: __ocs_fmt_0__ + .an-open-is-already-in-progress = Tiedoston avaaminen on jo käynnissä. + .autosave-copy-could-not-be-queued-too-many-pending-opens = Automaattisen tallennuksen kopiota ei voitu jonottaa: liikaa odottavia avauksia .jogged-radius = Katkaistu säde .none-use-shortcuts-set-key-command =(ei mitään — käytä: SHORTCUTS SET ) .alignment-indented-label-title-case = Tasaus: diff --git a/locales/fr-FR/opencadstudio.ftl b/locales/fr-FR/opencadstudio.ftl index 69b44a8de..5e98ae7f9 100644 --- a/locales/fr-FR/opencadstudio.ftl +++ b/locales/fr-FR/opencadstudio.ftl @@ -278,6 +278,8 @@ common = .autosave-copy-could-not-be-stored = Impossible de stocker la copie de sauvegarde automatique : __ocs_fmt_0__ .restored-from-the-autosave-copy = «__ocs_fmt_0__» restauré depuis la copie de sauvegarde automatique .autosave-copy-could-not-be-read = Impossible de lire la copie de sauvegarde automatique : __ocs_fmt_0__ + .an-open-is-already-in-progress = Une ouverture est déjà en cours. + .autosave-copy-could-not-be-queued-too-many-pending-opens = Impossible de mettre la copie de sauvegarde automatique en file d'attente : trop d'ouvertures en attente .jogged-radius = Rayon raccourci .none-use-shortcuts-set-key-command = (aucune utilisation: SHORTCUTS SET ) .alignment-indented-label-title-case = Alignement : diff --git a/locales/hi-IN/opencadstudio.ftl b/locales/hi-IN/opencadstudio.ftl index c4018afcd..4cf683129 100644 --- a/locales/hi-IN/opencadstudio.ftl +++ b/locales/hi-IN/opencadstudio.ftl @@ -266,6 +266,8 @@ common = .autosave-copy-could-not-be-stored = ऑटोसेव प्रतिलिपि संग्रहित नहीं की जा सकी: __ocs_fmt_0__ .restored-from-the-autosave-copy = "__ocs_fmt_0__" को ऑटोसेव प्रतिलिपि से पुनर्स्थापित किया गया .autosave-copy-could-not-be-read = ऑटोसेव प्रतिलिपि पढ़ी नहीं जा सकी: __ocs_fmt_0__ + .an-open-is-already-in-progress = एक फ़ाइल पहले से ही खोली जा रही है। + .autosave-copy-could-not-be-queued-too-many-pending-opens = ऑटोसेव प्रतिलिपि कतार में नहीं रखी जा सकी: बहुत अधिक लंबित खोलने की क्रियाएँ .jogged-radius = मोड़ वाला त्रिज्या माप .none-use-shortcuts-set-key-command = (कोई नहीं — उपयोग: SHORTCUTS SET ) .alignment-indented-label-title-case = संरेखण: diff --git a/locales/hu-HU/opencadstudio.ftl b/locales/hu-HU/opencadstudio.ftl index 3276c2ed6..95f3337db 100644 --- a/locales/hu-HU/opencadstudio.ftl +++ b/locales/hu-HU/opencadstudio.ftl @@ -289,6 +289,20 @@ common = .autosave-copy-could-not-be-stored = Az automatikus mentési másolatot nem sikerült tárolni: __ocs_fmt_0__ .restored-from-the-autosave-copy = A(z) "__ocs_fmt_0__" helyreállítva az automatikus mentési másolatból .autosave-copy-could-not-be-read = Az automatikus mentési másolatot nem sikerült elolvasni: __ocs_fmt_0__ + .an-open-is-already-in-progress = Már folyamatban van egy megnyitás. + .autosave-copy-could-not-be-queued-too-many-pending-opens = Az automatikus mentési másolatot nem sikerült sorba állítani: túl sok függőben lévő megnyitás + .jogged-radius = Tört sugarú méret + .none-use-shortcuts-set-key-command =(nincs – használat: SHORTCUTS SET ) + .alignment-indented-label-title-case = Igazítás: + .background-fill-enabled = Háttérkitöltés engedélyezve + .borders-type-weight-color-spacing-hidden = Szegélyek (típus / súly / szín / térköz / rejtett) + .data-type = Adattípus: + .fill-color-indented-label-sentence-case = Kitöltés színe: + .format-string = Karakterlánc formázása: + .off-color-ltype = Ki Szín Ltípus + .text-color-indented-label-sentence-case = Szöveg színe: + .text-height-indented-label-sentence-case = Szöveg magassága: + .unit-type = Egység típusa: .name-already-in-use = A „__ocs_fmt_0__” név már használatban van .rrggbb-or-blank = #RRGGBB vagy üres .default-parenthesized-lowercase = (alapértelmezett) diff --git a/locales/it-IT/opencadstudio.ftl b/locales/it-IT/opencadstudio.ftl index 6bf8c0282..6c63c4bb6 100644 --- a/locales/it-IT/opencadstudio.ftl +++ b/locales/it-IT/opencadstudio.ftl @@ -277,6 +277,8 @@ common = .autosave-copy-could-not-be-stored = Impossibile salvare la copia di salvataggio automatico: __ocs_fmt_0__ .restored-from-the-autosave-copy = "__ocs_fmt_0__" ripristinato dalla copia di salvataggio automatico .autosave-copy-could-not-be-read = Impossibile leggere la copia di salvataggio automatico: __ocs_fmt_0__ + .an-open-is-already-in-progress = Un'apertura è già in corso. + .autosave-copy-could-not-be-queued-too-many-pending-opens = Impossibile accodare la copia di salvataggio automatico: troppe aperture in sospeso .jogged-radius = Raggio spezzato .none-use-shortcuts-set-key-command =(nessuno — utilizzare: SHORTCUTS SET ) .alignment-indented-label-title-case = Allineamento: diff --git a/locales/ja-JP/opencadstudio.ftl b/locales/ja-JP/opencadstudio.ftl index 995fc1173..70d5a2e21 100644 --- a/locales/ja-JP/opencadstudio.ftl +++ b/locales/ja-JP/opencadstudio.ftl @@ -268,6 +268,8 @@ common = .autosave-copy-could-not-be-stored = 自動保存コピーを保存できませんでした: __ocs_fmt_0__ .restored-from-the-autosave-copy = "__ocs_fmt_0__" を自動保存コピーから復元しました .autosave-copy-could-not-be-read = 自動保存コピーを読み込めませんでした: __ocs_fmt_0__ + .an-open-is-already-in-progress = 開く操作がすでに進行中です。 + .autosave-copy-could-not-be-queued-too-many-pending-opens = 自動保存コピーをキューに入れられませんでした:保留中の開く操作が多すぎます .jogged-radius = 折れ曲がり半径 .none-use-shortcuts-set-key-command = (なし — SHORTCUTS SET を使用) .alignment-indented-label-title-case = 配置: diff --git a/locales/ko-KR/opencadstudio.ftl b/locales/ko-KR/opencadstudio.ftl index 662bada54..6c9e5c1cd 100644 --- a/locales/ko-KR/opencadstudio.ftl +++ b/locales/ko-KR/opencadstudio.ftl @@ -277,6 +277,8 @@ common = .autosave-copy-could-not-be-stored = 자동 저장 사본을 저장할 수 없습니다: __ocs_fmt_0__ .restored-from-the-autosave-copy = "__ocs_fmt_0__"을(를) 자동 저장 사본에서 복원했습니다 .autosave-copy-could-not-be-read = 자동 저장 사본을 읽을 수 없습니다: __ocs_fmt_0__ + .an-open-is-already-in-progress = 열기 작업이 이미 진행 중입니다. + .autosave-copy-could-not-be-queued-too-many-pending-opens = 자동 저장 사본을 큐에 넣지 못했습니다: 대기 중인 열기가 너무 많습니다 .jogged-radius = 꺾인 반지름 .none-use-shortcuts-set-key-command = (없음 — 사용: SHORTCUTS SET ) .alignment-indented-label-title-case =정렬: diff --git a/locales/nl-NL/opencadstudio.ftl b/locales/nl-NL/opencadstudio.ftl index b260f37c4..54fca65db 100644 --- a/locales/nl-NL/opencadstudio.ftl +++ b/locales/nl-NL/opencadstudio.ftl @@ -278,6 +278,8 @@ common = .autosave-copy-could-not-be-stored = Automatische opslagkopie kon niet worden bewaard: __ocs_fmt_0__ .restored-from-the-autosave-copy = "__ocs_fmt_0__" is hersteld uit de automatische opslagkopie .autosave-copy-could-not-be-read = Automatische opslagkopie kon niet worden gelezen: __ocs_fmt_0__ + .an-open-is-already-in-progress = Er wordt al een bestand geopend. + .autosave-copy-could-not-be-queued-too-many-pending-opens = Automatische opslagkopie kon niet in de wachtrij worden geplaatst: te veel wachtende openingen .jogged-radius = Geknikte straal .none-use-shortcuts-set-key-command = (geen gebruik: SHORTCUTS SET ) .alignment-indented-label-title-case = Uitlijning: diff --git a/locales/pl-PL/opencadstudio.ftl b/locales/pl-PL/opencadstudio.ftl index 8e2a94fb7..65d055025 100644 --- a/locales/pl-PL/opencadstudio.ftl +++ b/locales/pl-PL/opencadstudio.ftl @@ -277,6 +277,8 @@ common = .autosave-copy-could-not-be-stored = Nie można zapisać kopii autozapisu: __ocs_fmt_0__ .restored-from-the-autosave-copy = Przywrócono "__ocs_fmt_0__" z kopii autozapisu .autosave-copy-could-not-be-read = Nie można odczytać kopii autozapisu: __ocs_fmt_0__ + .an-open-is-already-in-progress = Otwieranie jest już w toku. + .autosave-copy-could-not-be-queued-too-many-pending-opens = Nie można ustawić kopii autozapisu w kolejce: zbyt wiele oczekujących otwarć .jogged-radius = Promień łamany .none-use-shortcuts-set-key-command =(brak — użyj: SHORTCUTS SET ) .alignment-indented-label-title-case = Wyrównanie: diff --git a/locales/pt-BR/opencadstudio.ftl b/locales/pt-BR/opencadstudio.ftl index 980e51b67..a9a4043d9 100644 --- a/locales/pt-BR/opencadstudio.ftl +++ b/locales/pt-BR/opencadstudio.ftl @@ -278,6 +278,8 @@ common = .autosave-copy-could-not-be-stored = Não foi possível armazenar a cópia de salvamento automático: __ocs_fmt_0__ .restored-from-the-autosave-copy = "__ocs_fmt_0__" restaurado da cópia de salvamento automático .autosave-copy-could-not-be-read = Não foi possível ler a cópia de salvamento automático: __ocs_fmt_0__ + .an-open-is-already-in-progress = Uma abertura já está em andamento. + .autosave-copy-could-not-be-queued-too-many-pending-opens = Não foi possível enfileirar a cópia de salvamento automático: aberturas pendentes em excesso .jogged-radius = Raio com desvio .none-use-shortcuts-set-key-command = (nenhum — use: SHORTCUTS SET ) .alignment-indented-label-title-case = Alinhamento: diff --git a/locales/ru-RU/opencadstudio.ftl b/locales/ru-RU/opencadstudio.ftl index df6d69e35..c3354f80b 100644 --- a/locales/ru-RU/opencadstudio.ftl +++ b/locales/ru-RU/opencadstudio.ftl @@ -281,6 +281,8 @@ common = .autosave-copy-could-not-be-stored = Не удалось сохранить копию автосохранения: __ocs_fmt_0__ .restored-from-the-autosave-copy = «__ocs_fmt_0__» восстановлен из копии автосохранения .autosave-copy-could-not-be-read = Не удалось прочитать копию автосохранения: __ocs_fmt_0__ + .an-open-is-already-in-progress = Открытие файла уже выполняется. + .autosave-copy-could-not-be-queued-too-many-pending-opens = Не удалось поставить копию автосохранения в очередь: слишком много ожидающих открытий .jogged-radius = Радиус с изломом .none-use-shortcuts-set-key-command = (нет — используйте: SHORTCUTS SET ) .alignment-indented-label-title-case = Выравнивание: diff --git a/locales/tr-TR/opencadstudio.ftl b/locales/tr-TR/opencadstudio.ftl index 12f4488b8..2251b961b 100644 --- a/locales/tr-TR/opencadstudio.ftl +++ b/locales/tr-TR/opencadstudio.ftl @@ -263,6 +263,8 @@ common = .autosave-copy-could-not-be-stored = Otomatik kayıt kopyası depolanamadı: __ocs_fmt_0__ .restored-from-the-autosave-copy = "__ocs_fmt_0__" otomatik kayıt kopyasından geri yüklendi .autosave-copy-could-not-be-read = Otomatik kayıt kopyası okunamadı: __ocs_fmt_0__ + .an-open-is-already-in-progress = Bir açma işlemi zaten sürüyor. + .autosave-copy-could-not-be-queued-too-many-pending-opens = Otomatik kayıt kopyası sıraya alınamadı: çok fazla bekleyen açma işlemi .none-use-shortcuts-set-key-command = (none – use: SHORTCUTS SET ) .alignment-indented-label-title-case = Adapt: .background-fill-enabled = Arka plan doldurma etkinleştir etkinleştirildi diff --git a/locales/zh-CN/opencadstudio.ftl b/locales/zh-CN/opencadstudio.ftl index 0ec3249ca..6e01329ed 100644 --- a/locales/zh-CN/opencadstudio.ftl +++ b/locales/zh-CN/opencadstudio.ftl @@ -265,6 +265,8 @@ common = .autosave-copy-could-not-be-stored = 无法存储自动保存副本: __ocs_fmt_0__ .restored-from-the-autosave-copy = 已从自动保存副本恢复 "__ocs_fmt_0__" .autosave-copy-could-not-be-read = 无法读取自动保存副本: __ocs_fmt_0__ + .an-open-is-already-in-progress = 已有打开操作正在进行。 + .autosave-copy-could-not-be-queued-too-many-pending-opens = 无法排队自动保存副本:待打开的文件过多 .jogged-radius = 折弯半径 .none-use-shortcuts-set-key-command = (无 — 使用:SHORTCUTS SET ) .alignment-indented-label-title-case = 对齐: diff --git a/locales/zh-TW/opencadstudio.ftl b/locales/zh-TW/opencadstudio.ftl index a9c090d70..49514b49b 100644 --- a/locales/zh-TW/opencadstudio.ftl +++ b/locales/zh-TW/opencadstudio.ftl @@ -277,6 +277,8 @@ common = .autosave-copy-could-not-be-stored = 無法儲存自動儲存副本: __ocs_fmt_0__ .restored-from-the-autosave-copy = 已從自動儲存副本恢復 "__ocs_fmt_0__" .autosave-copy-could-not-be-read = 無法讀取自動儲存副本: __ocs_fmt_0__ + .an-open-is-already-in-progress = 已有開啟作業正在進行中。 + .autosave-copy-could-not-be-queued-too-many-pending-opens = 無法將自動儲存副本排入佇列:待開啟的檔案過多 .jogged-radius = 折彎半徑 .none-use-shortcuts-set-key-command = (無 — 使用:SHORTCUTS SET ) .alignment-indented-label-title-case = 對齊: diff --git a/src/app/mod.rs b/src/app/mod.rs index 120eacd44..f68a06b7f 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -2337,6 +2337,11 @@ pub enum Message { /// Web: delete the offered autosave snapshot (and offer the next, if any). #[cfg(target_arch = "wasm32")] WebAutosaveDiscard, + /// Web: close the restore prompt WITHOUT deleting the offered snapshot + /// (Escape/✕): it is simply re-offered on the next boot. Only the + /// Discard button deletes. + #[cfg(target_arch = "wasm32")] + WebAutosaveDefer, /// Web: the offered snapshot's bytes are in; queue them for the guarded /// open poll. The OPFS copy is consumed only once that open succeeds. #[cfg(target_arch = "wasm32")] diff --git a/src/app/update/dialog.rs b/src/app/update/dialog.rs index 436f7a5dc..c22914b8b 100644 --- a/src/app/update/dialog.rs +++ b/src/app/update/dialog.rs @@ -191,6 +191,22 @@ impl OpenCADStudio { // Discarded — drop this tab's autosave recovery copy. #[cfg(not(target_arch = "wasm32"))] let _ = std::fs::remove_file(self.autosave_target(idx)); + // Web counterpart: the OPFS snapshot must go too, or the + // drawing the user just declined to save is offered back at + // the next boot. A snapshot round still writing this tab's key + // would resurrect it after the remove, so defer to the round's + // end (the same guard the save flow uses). + #[cfg(target_arch = "wasm32")] + { + let key = Self::web_autosave_key(&self.tabs[idx]); + if self.web_autosave_in_flight { + self.web_autosave_clear_after.push(key); + } else { + wasm_bindgen_futures::spawn_local(async move { + let _ = crate::io::web_autosave::remove(&key).await; + }); + } + } if self.tabs.len() == 1 { self.tab_counter += 1; self.tabs[0] = crate::app::document::DocumentTab::new_drawing(self.tab_counter); diff --git a/src/app/update/file.rs b/src/app/update/file.rs index cf2fc1ad0..ce534fb22 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -1471,6 +1471,19 @@ impl OpenCADStudio { // Native: pick a path, then load on a worker thread. Web: the // browser hands back bytes, so pick + parse in one step and feed // the shared `FileOpened` handler directly. + // Web guard: a second open (Ctrl+O, the OPEN command, host + // automation) while a drop/restore/picker open is still in flight + // would overwrite `self.opening` — the in-flight open's + // `WebFileOpened` then misses the id check and its bytes are lost, + // and any pending restore prompt stalls for the session. Native + // keeps the old behaviour: its `OpenPathPicked` round-trip is + // guarded downstream. + #[cfg(target_arch = "wasm32")] + if self.opening.is_some() { + self.command_line + .push_info(crate::tf!("An open is already in progress.").as_ref()); + return Task::none(); + } #[cfg(not(target_arch = "wasm32"))] { Task::perform(crate::io::pick_open_path(), Message::OpenPathPicked) @@ -2496,12 +2509,7 @@ impl OpenCADStudio { let mut recent_task = Task::none(); // The name the autosave snapshot for this tab is keyed by — capture it // before the save re-points `current_path` at the download name. - let autosave_key = self.tabs[i] - .current_path - .as_ref() - .and_then(|path| path.file_name()) - .map(|name| name.to_string_lossy().into_owned()) - .unwrap_or_else(|| self.tabs[i].tab_display_name()); + let autosave_key = Self::web_autosave_key(&self.tabs[i]); let document = self.tabs[i].scene.document_for_save(); let saved = match crate::io::save_to_bytes(&document, &ext, version) { Ok(bytes) => { @@ -3525,6 +3533,21 @@ impl OpenCADStudio { } } + /// The OPFS autosave snapshot key for `tab`: the drawing's display name + /// (the recent copy's name), or the tab title for untitled tabs — the + /// single derivation every web snapshot/remove site must agree on, or a + /// remove misses the snapshot a round wrote (and vice versa). + #[cfg(target_arch = "wasm32")] + pub(super) fn web_autosave_key( + tab: &crate::app::document::DocumentTab, + ) -> String { + tab.current_path + .as_ref() + .and_then(|path| path.file_name()) + .map(|name| name.to_string_lossy().into_owned()) + .unwrap_or_else(|| tab.tab_display_name()) + } + /// Periodic autosave (SAVETIME): write a `.sv$` recovery copy for every /// dirty tab — beside the file if it's saved, else under the temp dir — at /// the document's own DWG version. Best-effort and non-destructive: it never @@ -3580,12 +3603,7 @@ impl OpenCADStudio { // Key by the drawing's display name (the recent copy's name), so // the boot scan can compare the two; untitled tabs use the tab // title, which is what Save would download as. - let name = tab - .current_path - .as_ref() - .and_then(|path| path.file_name()) - .map(|name| name.to_string_lossy().into_owned()) - .unwrap_or_else(|| tab.tab_display_name()); + let name = Self::web_autosave_key(tab); let is_dxf = crate::io::source_is_dxf(tab.current_path.as_deref(), &tab.scene.document); let ext = if is_dxf { "dxf" } else { "dwg" }.to_string(); let version = tab.scene.document.version; @@ -3725,6 +3743,21 @@ impl OpenCADStudio { Task::none() } + /// Defer the offered snapshot (Escape/✕ on the restore modal): unlike + /// Discard the OPFS copy is NOT removed — one stray keystroke must not + /// destroy the only recovery copy — it is simply re-offered on the next + /// boot. The prompt moves on to the next queued snapshot, if any. + #[cfg(target_arch = "wasm32")] + pub(super) fn on_web_autosave_defer(&mut self) -> Task { + if !self.web_autosave_pending.is_empty() { + self.web_autosave_pending.remove(0); + } + if self.web_autosave_pending.is_empty() { + self.close_active_modal(); + } + Task::none() + } + /// The offered snapshot's bytes arrived (or did not). On success the /// bytes ride the 50 ms poll's guarded open queue — the poll starts an /// open only when the slot is free, so the recovered drawing can @@ -3745,7 +3778,19 @@ impl OpenCADStudio { // (`web_autosave_opened` / the failure hook), not here — the // poll has not started the open yet, so queueing now would // show the modal before the recovered drawing loads. - crate::io::web_drop::enqueue_restore(open_name, bytes, key); + // Exception: a rejected enqueue (queue full) means no open + // will ever start for these bytes, so the prompt cycle must + // continue here or stall for the session. The snapshot + // survives in OPFS and is re-offered on the next boot. + if !crate::io::web_drop::enqueue_restore(open_name, bytes, key) { + self.command_line.push_warning( + crate::tf!( + "Autosave copy could not be queued: too many pending opens" + ) + .as_ref(), + ); + self.queue_next_web_autosave_prompt(); + } Task::none() } Err(error) => { @@ -3772,7 +3817,7 @@ impl OpenCADStudio { /// Delete the `.sv$` autosave recovery files for all open drawings. They /// exist only to survive a crash, so a clean save or exit removes them. - pub(in crate::app) fn cleanup_autosaves(&self) { + pub(in crate::app) fn cleanup_autosaves(&mut self) { #[cfg(not(target_arch = "wasm32"))] for i in 0..self.tabs.len() { let _ = std::fs::remove_file(self.autosave_target(i)); @@ -3781,25 +3826,34 @@ impl OpenCADStudio { for path in &self.sheet_set.locks { crate::app::commands::sheet_set::release_lock(path); } - // Web counterpart: drop each open tab's OPFS snapshot. A tab close is - // not an exit, so this only runs on the explicit exit path; anything - // the teardown race leaves behind is caught by the next boot scan. + // Web counterpart: drop each open tab's OPFS snapshot, plus any + // removes a save deferred while a snapshot round was still writing. + // A tab close is not an exit, so this only runs on the explicit exit + // path; anything the teardown race leaves behind is caught by the + // next boot scan. #[cfg(target_arch = "wasm32")] - for tab in &self.tabs { - let name = tab - .current_path - .as_ref() - .and_then(|path| path.file_name()) - .map(|name| name.to_string_lossy().into_owned()) - .unwrap_or_else(|| tab.tab_display_name()); + { + let mut keys = std::mem::take(&mut self.web_autosave_clear_after); + // An in-flight round may still be writing snapshots right now; + // its late writes would land AFTER these removes and resurrect + // exactly what was removed, so the per-tab sweep is pointless in + // that race — skip it entirely (the boot scan bounds leftovers: + // `SNAPSHOT_CAP` eviction discards the oldest overflow). + if !self.web_autosave_in_flight { + for tab in &self.tabs { + keys.push(Self::web_autosave_key(tab)); + } + } wasm_bindgen_futures::spawn_local(async move { - let _ = crate::io::web_autosave::remove(&name).await; + for key in &keys { + let _ = crate::io::web_autosave::remove(key).await; + } }); } } /// Remove the autosave recovery files, then quit the application. - pub(in crate::app) fn exit_app(&self) -> Task { + pub(in crate::app) fn exit_app(&mut self) -> Task { self.cleanup_autosaves(); iced::exit() } diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index 75246d331..783916ca4 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -1136,12 +1136,15 @@ impl OpenCADStudio { // not be clobbered (its bytes are gone once dequeued), so the // entry goes back to the queue for the next poll tick. if self.opening.is_some() { - match restore_key { + // A full queue rejects the bytes here; io-side console + // warning only (the drop path has no user-facing + // surface for rejections by design — see QUEUE_CAP). + let _ = match restore_key { Some(key) => { crate::io::web_drop::enqueue_restore(name, bytes, key) } None => crate::io::web_drop::enqueue_open(name, bytes), - } + }; return Task::none(); } let state = std::sync::Arc::new(crate::io::OpenProgressState::new( @@ -1312,6 +1315,8 @@ impl OpenCADStudio { #[cfg(target_arch = "wasm32")] Message::WebAutosaveDiscard => self.on_web_autosave_discard(), #[cfg(target_arch = "wasm32")] + Message::WebAutosaveDefer => self.on_web_autosave_defer(), + #[cfg(target_arch = "wasm32")] Message::WebAutosaveBytes(name, key, result) => { self.on_web_autosave_bytes(name, key, result) } @@ -9022,11 +9027,13 @@ impl OpenCADStudio { if self.active_modal == Some(super::ModalKind::Options) { return self.update(Message::OptionsClose); } - // Closing the crash-recovery prompt (✕ or Escape) discards - // the offered snapshot — otherwise it would requeue itself. + // Closing the crash-recovery prompt (✕ or Escape) DEFERS: + // one stray keystroke must not delete the only recovery + // copy, so the snapshot survives and is re-offered on the + // next boot. Only the Discard button deletes. #[cfg(target_arch = "wasm32")] if self.active_modal == Some(super::ModalKind::AutosaveRestore) { - return self.update(Message::WebAutosaveDiscard); + return self.update(Message::WebAutosaveDefer); } // Closing the shortcut editor with un-applied rows needs an // explicit discard; a second close attempt (or the overlay's diff --git a/src/io/pdf_export.rs b/src/io/pdf_export.rs index 3628b3ebd..52a230f05 100644 --- a/src/io/pdf_export.rs +++ b/src/io/pdf_export.rs @@ -472,6 +472,17 @@ fn append_pdf_page( fallback_plot_style: Option<&PlotStyleTable>, fonts: &SearchableFontSet, ) -> Result<(), String> { + // Paper dimensions are validated like the content coordinates below: a + // NaN/inf/zero sheet (`width < MIN` compares false for NaN) would be + // serialized verbatim into a MediaBox no conforming viewer can display, + // and printpdf's own parser would still accept it. + if !page.paper_w.is_finite() + || !page.paper_h.is_finite() + || page.paper_w <= 0.0 + || page.paper_h <= 0.0 + { + return Err("plot sheet size is not valid".into()); + } let PlotContent { wires, hatches, diff --git a/src/io/web_autosave.rs b/src/io/web_autosave.rs index cdfc53a6e..775ebd21e 100644 --- a/src/io/web_autosave.rs +++ b/src/io/web_autosave.rs @@ -83,24 +83,44 @@ pub(crate) fn cap_evictions( (entries, evicted) } +/// The marker `recovered_name` appends (and strips when it is already +/// there, so it never stacks across crash cycles). +#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))] // unit-tested on native +const RECOVERED_MARKER: &str = " (recovered)"; + /// The name a restored drawing carries as a tab and download: the drawing's -/// stem plus " (recovered)" and its own extension (DWG when the stored name -/// has none, matching how autosave serializes untitled tabs). The extension -/// must survive intact because the web parser picks the format from it. +/// stem plus " (recovered)" and its own extension. The extension split +/// follows the WORKER convention (`name.rsplit('.').next()`, the whole name +/// when there is no dot): a dot-file like ".dxf" is a DXF name to the parse +/// worker, and `Path::extension` would misread it as extensionless — +/// restoring DXF bytes under a ".dwg" name jams the restore door. A name +/// with no extension at all defaults to DXF, matching how autosave +/// serializes untitled tabs (`source_is_dxf` → `dwg_source_version.is_none()`). +/// An existing trailing marker is stripped first, so "(recovered)" stacks +/// at most once no matter how many crash cycles re-snapshotted a previously +/// recovered name. #[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))] // unit-tested on native pub(crate) fn recovered_name(drawing: &str) -> String { - let path = std::path::Path::new(drawing); - let stem = path - .file_stem() - .map(|stem| stem.to_string_lossy().into_owned()) - .filter(|stem| !stem.is_empty()) - .unwrap_or_else(|| drawing.to_string()); - let ext = path - .extension() - .map(|ext| ext.to_string_lossy().into_owned()) - .filter(|ext| !ext.is_empty()) - .unwrap_or_else(|| "dwg".to_string()); - format!("{stem} (recovered).{ext}") + let (head, ext) = match drawing.rsplit_once('.') { + Some((head, ext)) if !ext.is_empty() => (head, ext.to_string()), + // No dot (or a trailing one): the whole name is the stem. + _ => (drawing, "dxf".to_string()), + }; + // A hostile stored name must not smuggle path separators into the tab + // title / download name: keep only the final component of the head. + let head = head.rsplit(['/', '\\']).next().unwrap_or_default(); + // When the split consumed everything (".dxf") fall back to the whole + // stored name, like `Path::file_stem`'s empty-stem fallback did. + let mut stem = if head.is_empty() { + drawing + } else { + head + }; + // Idempotent: strip every trailing marker, then append exactly one. + while let Some(stripped) = stem.strip_suffix(RECOVERED_MARKER) { + stem = stripped; + } + format!("{stem}{RECOVERED_MARKER}.{ext}") } /// Overwrite (or create) the snapshot for `name`. The optional thumbnail is @@ -108,7 +128,7 @@ pub(crate) fn recovered_name(drawing: &str) -> String { #[cfg(target_arch = "wasm32")] pub async fn snapshot(name: &str, bytes: &[u8], thumbnail: Option<&[u8]>) -> Result<(), String> { let directory = autosave_directory(true).await?; - let mut names = read_index(&directory).await; + let mut names = read_index(&directory).await?; if !names.iter().any(|stored| stored == name) { names.push(name.to_string()); write_index(&directory, &names).await?; @@ -140,7 +160,9 @@ pub async fn remove(name: &str) -> Result<(), String> { .await .map_err(super::web_recent::js_error); let _ = JsFuture::from(directory.remove_entry(&thumbnail_key(name))).await; - let mut names = read_index(&directory).await; + // A corrupt index must abort the rewrite (not reset it to `names`), + // or this remove would orphan every other row's snapshot. + let mut names = read_index(&directory).await?; let before = names.len(); names.retain(|stored| stored != name); if names.len() != before { @@ -161,7 +183,9 @@ pub async fn scan() -> Result, String> { let directory = autosave_directory(false).await?; let mut entries = Vec::new(); let mut dead: Vec = Vec::new(); - for name in read_index(&directory).await { + // An unreadable index fails the whole boot scan: guessing an empty list + // would cap-evict and remove snapshots the index can no longer name. + for name in read_index(&directory).await? { let file = match super::web_recent::get_file(&directory, &snapshot_key(&name)).await { Ok(file) => file, // Crash between the index and snapshot writes: the row points @@ -212,12 +236,23 @@ fn thumbnail_key(name: &str) -> String { } #[cfg(target_arch = "wasm32")] -async fn read_index(directory: &web_sys::FileSystemDirectoryHandle) -> Vec { - super::web_recent::read_entry(directory, INDEX_KEY) - .await - .ok() - .and_then(|bytes| serde_json::from_slice(&bytes).ok()) - .unwrap_or_default() +async fn read_index( + directory: &web_sys::FileSystemDirectoryHandle, +) -> Result, String> { + let bytes = match super::web_recent::read_entry(directory, INDEX_KEY).await { + Ok(bytes) => bytes, + // A missing index is a fresh store. + Err(_) => return Ok(Vec::new()), + }; + // An empty (freshly created or truncated) index is an empty store. + if bytes.is_empty() { + return Ok(Vec::new()); + } + // A non-empty file that fails to parse is real data we cannot name: + // returning it as empty would let the next write reset the index and + // silently orphan every other row's snapshot. Surface the failure so + // callers skip the round/boot and leave the file untouched. + serde_json::from_slice(&bytes).map_err(|error| error.to_string()) } #[cfg(target_arch = "wasm32")] @@ -266,10 +301,42 @@ mod tests { fn recovered_names_keep_a_parseable_extension() { assert_eq!(recovered_name("plan.dwg"), "plan (recovered).dwg"); assert_eq!(recovered_name("sketch.dxf"), "sketch (recovered).dxf"); - assert_eq!(recovered_name("Drawing1"), "Drawing1 (recovered).dwg"); + // Extensionless names (untitled tabs) restore as DXF: that is what + // autosave serializes them as (`source_is_dxf` → no DWG source + // version), and the parse worker dispatches on the final extension. + assert_eq!(recovered_name("Drawing1"), "Drawing1 (recovered).dxf"); // Multi-dot names keep everything but the final extension in the stem. assert_eq!(recovered_name("a.b.dwg"), "a.b (recovered).dwg"); - assert_eq!(recovered_name(""), " (recovered).dwg"); + // A dot-file like ".dxf" is a DXF name under the worker's + // `rsplit('.')` convention — the extension must not flip to dwg. + assert_eq!(recovered_name(".dxf"), ".dxf (recovered).dxf"); + assert_eq!(recovered_name(".dwg"), ".dwg (recovered).dwg"); + assert_eq!(recovered_name(""), " (recovered).dxf"); + } + + #[test] + fn the_recovered_marker_never_stacks() { + // Restoring sets the tab name to the recovered name; a later + // autosave round snapshots under it. One marker per drawing: + assert_eq!( + recovered_name("plan (recovered).dwg"), + "plan (recovered).dwg" + ); + // Idempotent even for names that already stacked (old cycles). + assert_eq!( + recovered_name("plan (recovered) (recovered).dwg"), + "plan (recovered).dwg" + ); + // Extensionless stacked names de-stack too, then gain ".dxf". + assert_eq!( + recovered_name("plan (recovered)"), + "plan (recovered).dxf" + ); + // A marker embedded in the middle (not a suffix) is left alone. + assert_eq!( + recovered_name("(recovered) plan.dwg"), + "(recovered) plan (recovered).dwg" + ); } #[test] diff --git a/src/io/web_drop.rs b/src/io/web_drop.rs index f8083cf49..8f5ec248c 100644 --- a/src/io/web_drop.rs +++ b/src/io/web_drop.rs @@ -12,7 +12,8 @@ use wasm_bindgen_futures::JsFuture; /// A stray multi-drop must not grow the queue without bound; extras are /// rejected with a console warning instead of being silently dropped. -const QUEUE_CAP: usize = 8; +/// Entries are just name+bytes; 32 drawings is a realistic folder drop. +const QUEUE_CAP: usize = 32; /// One queued open. `restore_key` tags an autosave-restore file so its /// snapshot is deleted only when THIS open succeeds — matching by identity, @@ -41,7 +42,9 @@ fn lock_queue() -> std::sync::MutexGuard<'static, VecDeque> { .unwrap_or_else(|poisoned| poisoned.into_inner()) } -fn push(entry: QueuedOpen) { +/// Returns `false` (after a console warning) when the queue is full and the +/// entry was NOT stored — the caller decides how to surface the loss. +fn push(entry: QueuedOpen) -> bool { let name = entry.name.clone(); let mut queue = lock_queue(); if queue.len() >= QUEUE_CAP { @@ -49,35 +52,40 @@ fn push(entry: QueuedOpen) { warn(&format!( "ocs: dropped-file queue full; ignoring \"{name}\"" )); - return; + return false; } queue.push_back(entry); + true } /// Queue already-read drawing bytes (drag-and-drop) for the 50 ms poll to -/// open through the guarded web-picker flow. -pub fn enqueue_open(name: String, bytes: Vec) { +/// open through the guarded web-picker flow. Returns `false` when the queue +/// is full and the bytes were dropped. +pub fn enqueue_open(name: String, bytes: Vec) -> bool { push(QueuedOpen { name, bytes, restore_key: None, - }); + }) } -/// Queue an autosave-restore file tagged with its snapshot key. -pub fn enqueue_restore(name: String, bytes: Vec, restore_key: String) { +/// Queue an autosave-restore file tagged with its snapshot key. Returns +/// `false` when the queue is full and the bytes were dropped (the OPFS +/// snapshot survives; the caller must not wait for an open that will +/// never start). +pub fn enqueue_restore(name: String, bytes: Vec, restore_key: String) -> bool { push(QueuedOpen { name, bytes, restore_key: Some(restore_key), - }); + }) } /// JS-callable entry (same shape as `ocs_control_submit`) for hosts that /// forward drops themselves. #[wasm_bindgen::prelude::wasm_bindgen] pub fn ocs_web_drop_file(name: String, bytes: js_sys::Uint8Array) { - enqueue_open(name, bytes.to_vec()); + let _ = enqueue_open(name, bytes.to_vec()); } /// Pop the oldest dropped file that the poll has not started opening yet. @@ -179,7 +187,10 @@ pub fn install() { for (name, read) in reads { match JsFuture::from(read).await { Ok(buffer) => { - enqueue_open(name, js_sys::Uint8Array::new(&buffer).to_vec()) + // A full queue warns on the console; the drop + // handler has no command line to talk to. + let _ = + enqueue_open(name, js_sys::Uint8Array::new(&buffer).to_vec()); } Err(error) => { web_sys::console::error_1(&wasm_bindgen::JsValue::from_str(&format!( @@ -203,7 +214,11 @@ pub fn install() { } fn is_drawing_name(name: &str) -> bool { - name.rsplit_once('.').is_some_and(|(_, extension)| { - matches!(extension.to_ascii_lowercase().as_str(), "dwg" | "dxf") - }) + // Same convention as the worker's reader dispatch (`rsplit('.').next()`): + // a dot-less name is its own extension, so a file literally named + // "DWG"/"dxf" parses and must not be filtered out here. + matches!( + name.rsplit('.').next().unwrap_or_default().to_ascii_lowercase().as_str(), + "dwg" | "dxf" + ) } diff --git a/src/locale_catalog.rs b/src/locale_catalog.rs index a5bb5ba75..fec5e1025 100644 --- a/src/locale_catalog.rs +++ b/src/locale_catalog.rs @@ -5811,6 +5811,8 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "Autosave copy could not be stored: {error}" => Some(("common", "autosave-copy-could-not-be-stored")), "Restored \"{}\" from the autosave copy" => Some(("common", "restored-from-the-autosave-copy")), "Autosave copy could not be read: {error}" => Some(("common", "autosave-copy-could-not-be-read")), + "An open is already in progress." => Some(("common", "an-open-is-already-in-progress")), + "Autosave copy could not be queued: too many pending opens" => Some(("common", "autosave-copy-could-not-be-queued-too-many-pending-opens")), _ => None, } } diff --git a/src/sys.rs b/src/sys.rs index 82e130220..71364ce2a 100644 --- a/src/sys.rs +++ b/src/sys.rs @@ -454,9 +454,11 @@ pub fn show_pdf_preview(name: &str, pdf_bytes: &[u8], page_pngs: &[Vec], tot let Some(document) = window.document() else { return; }; - // One preview at a time: a replotted preview replaces its predecessor and - // revokes its blob URLs, so repeated previews cannot stack overlays or - // leak the images underneath. + // One preview at a time: a replotted preview replaces its predecessor, + // revokes its blob URLs and DROPS its Download closure (freeing the full + // PDF copy the closure captured — a `forget()` would leak one per + // preview), so repeated previews cannot stack overlays or leak the + // images underneath. const OVERLAY_ID: &str = "ocs-plot-preview"; if let Some(previous) = document.get_element_by_id(OVERLAY_ID) { previous.remove(); @@ -468,6 +470,11 @@ pub fn show_pdf_preview(name: &str, pdf_bytes: &[u8], page_pngs: &[Vec], tot { let _ = web_sys::Url::revoke_object_url(&url); } + // Take the previous closure out so it destructs BEFORE the new preview + // stores its own: removing the overlay element already released the JS + // handler reference, so this drop is the last one and actually frees + // the captured bytes. + drop(PREVIEW_DOWNLOAD_CLOSURE.with(|slot| slot.borrow_mut().take())); if page_pngs.is_empty() { // Rasterisation produced nothing: deliver the PDF itself rather than // show an empty frame. @@ -570,13 +577,16 @@ pub fn show_pdf_preview(name: &str, pdf_bytes: &[u8], page_pngs: &[Vec], tot let name = name.to_string(); let bytes = pdf_bytes.to_vec(); // A plain FnMut stays usable for every click; the one-shot variant - // throws on the second click, and users click Download twice. + // throws on the second click, and users click Download twice. The + // closure is HELD in a registry (not `forget()`ed): replacing or + // closing the overlay drops it and frees the captured PDF copy, + // while the cloned JS function keeps the live handler working. let closure: wasm_bindgen::closure::Closure = wasm_bindgen::closure::Closure::new(move || { download_bytes(&name, &bytes); }); let click: js_sys::Function = closure.as_ref().unchecked_ref::().clone(); - closure.forget(); + PREVIEW_DOWNLOAD_CLOSURE.with(|slot| *slot.borrow_mut() = Some(closure)); dl.set_onclick(Some(&click)); bar.append_child(&dl).ok(); } @@ -612,6 +622,20 @@ fn preview_urls() -> &'static std::sync::Mutex> { URLS.get_or_init(|| std::sync::Mutex::new(Vec::new())) } +// The live preview overlay's Download-button closure. The closure captures +// a full copy of the PDF bytes, so `forget()`-ing it would leak one PDF per +// preview; holding it here lets the next `show_pdf_preview` drop and free +// the previous one (the overlay element's removal already released the JS +// handler reference). A thread-local `RefCell` rather than a static +// `Mutex` like `preview_urls`: `Closure` is not `Send`, and the overlay +// only ever lives on the one UI thread. +#[cfg(target_arch = "wasm32")] +thread_local! { + static PREVIEW_DOWNLOAD_CLOSURE: + std::cell::RefCell>> = + std::cell::RefCell::new(None); +} + /// Short platform string for bug reports: OS + architecture on the desktop, /// the browser user-agent on the web. #[cfg(not(target_arch = "wasm32"))]