forked from 1b-agent/opencadstudio-upstream
web: fix red-team breaks in restore, drop-open, and plot paths
Three attack agents (state-machine tracing, executable input fuzzing,
browser-API review) found two breaks and several degradations; all are
fixed here, with the fuzzer's 22 tests kept as regression suites.
Breaks:
- Untitled tabs (and dot-names like ".dxf") autosave as DXF, but the
restore name defaulted to .dwg — the worker dispatches its reader by
the name's extension, so the only copy of unsaved work could never
be restored and was re-offered forever. recovered_name now uses the
worker's rsplit('.') convention and defaults extensionless names to
dxf, matching new-document serialization.
- " (recovered)" stacked one suffix per crash-restore cycle; the
marker is now stripped idempotently before appending.
- Opening a file (Ctrl+O / OPEN / automation) while a web open was in
flight clobbered it: the in-flight open's bytes were lost and every
queued restore prompt stalled for the session. on_open_file now
refuses while an open is running (web only).
Hardening:
- Escape on the Recover Drawing modal defers instead of deleting the
only snapshot; the Discard button still removes it.
- A cap-rejected restore enqueue warns in-app and re-offers the next
snapshot; the drop queue cap rises to 32.
- Exiting mid-round no longer races snapshot removals (deferrals are
drained, the sweep skips while a round is in flight); closing a tab
with "Don't save" now also removes its web snapshot; the snapshot
key derivation is shared via web_autosave_key.
- The plot-preview overlay no longer leaks a full PDF copy per preview
(the Download closure lives in a replaceable registry).
- A corrupt autosave index.json fails the round/boot instead of
silently resetting every row; plot pages with non-finite or non-
positive sheet dimensions error instead of emitting a NaN MediaBox.
- The drop filter accepts dot-less "DWG"/"dxf" names the worker
parses (same rsplit convention on both gates).
(cherry picked from commit 3eab00bfbf)
Ported onto main (#1717 review): the five standalone tests/*_redteam.rs
files stay out — no test target builds them and they exercised copies of
the functions rather than the code itself.
This commit is contained in:
1 parent
9611f40400
commit
69fa7cae4f
30 files changed
+331
-76
No files matched your search
@@ -290,6 +290,8 @@ common =
|
|||||||
.autosave-copy-could-not-be-stored = تعذر تخزين نسخة الحفظ التلقائي: __ocs_fmt_0__
|
.autosave-copy-could-not-be-stored = تعذر تخزين نسخة الحفظ التلقائي: __ocs_fmt_0__
|
||||||
.restored-from-the-autosave-copy = تم استرداد "__ocs_fmt_0__" من نسخة الحفظ التلقائي
|
.restored-from-the-autosave-copy = تم استرداد "__ocs_fmt_0__" من نسخة الحفظ التلقائي
|
||||||
.autosave-copy-could-not-be-read = تعذر قراءة نسخة الحفظ التلقائي: __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 = نصف قطر منكسر
|
.jogged-radius = نصف قطر منكسر
|
||||||
.none-use-shortcuts-set-key-command = (لا شيء — استخدم: SHORTCUTS SET <key> <command>)
|
.none-use-shortcuts-set-key-command = (لا شيء — استخدم: SHORTCUTS SET <key> <command>)
|
||||||
.alignment-indented-label-title-case = المحاذاة:
|
.alignment-indented-label-title-case = المحاذاة:
|
||||||
|
|||||||
@@ -277,6 +277,8 @@ common =
|
|||||||
.autosave-copy-could-not-be-stored = Копието от автоматичното записване не можа да бъде съхранено: __ocs_fmt_0__
|
.autosave-copy-could-not-be-stored = Копието от автоматичното записване не можа да бъде съхранено: __ocs_fmt_0__
|
||||||
.restored-from-the-autosave-copy = "__ocs_fmt_0__" беше възстановен от копието от автоматичното записване
|
.restored-from-the-autosave-copy = "__ocs_fmt_0__" беше възстановен от копието от автоматичното записване
|
||||||
.autosave-copy-could-not-be-read = Копието от автоматичното записване не можа да бъде прочетено: __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 = Радиус с чупка
|
.jogged-radius = Радиус с чупка
|
||||||
.none-use-shortcuts-set-key-command = (няма — използвай: SHORTCUTS SET <клавиш> <команда>)
|
.none-use-shortcuts-set-key-command = (няма — използвай: SHORTCUTS SET <клавиш> <команда>)
|
||||||
.alignment-indented-label-title-case = Подравняване:
|
.alignment-indented-label-title-case = Подравняване:
|
||||||
|
|||||||
@@ -277,6 +277,8 @@ common =
|
|||||||
.autosave-copy-could-not-be-stored = Kopii automatického uložení nelze uložit: __ocs_fmt_0__
|
.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í
|
.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__
|
.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
|
.jogged-radius = Zalomený poloměr
|
||||||
.none-use-shortcuts-set-key-command =(žádné — použijte: SHORTCUTS SET <key> <command>)
|
.none-use-shortcuts-set-key-command =(žádné — použijte: SHORTCUTS SET <key> <command>)
|
||||||
.alignment-indented-label-title-case = Zarovnání:
|
.alignment-indented-label-title-case = Zarovnání:
|
||||||
|
|||||||
@@ -278,6 +278,8 @@ common =
|
|||||||
.autosave-copy-could-not-be-stored = Automatische Sicherungskopie konnte nicht gespeichert werden: __ocs_fmt_0__
|
.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
|
.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__
|
.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
|
.jogged-radius = Geknickter Radius
|
||||||
.none-use-shortcuts-set-key-command = (keine — verwenden: SHORTCUTS SET <key> <command>)
|
.none-use-shortcuts-set-key-command = (keine — verwenden: SHORTCUTS SET <key> <command>)
|
||||||
.alignment-indented-label-title-case = Ausrichtung:
|
.alignment-indented-label-title-case = Ausrichtung:
|
||||||
|
|||||||
@@ -277,6 +277,8 @@ common =
|
|||||||
.autosave-copy-could-not-be-stored = Δεν ήταν δυνατή η αποθήκευση του αντιγράφου αυτόματης αποθήκευσης: __ocs_fmt_0__
|
.autosave-copy-could-not-be-stored = Δεν ήταν δυνατή η αποθήκευση του αντιγράφου αυτόματης αποθήκευσης: __ocs_fmt_0__
|
||||||
.restored-from-the-autosave-copy = Το "__ocs_fmt_0__" επαναφέρθηκε από το αντίγραφο αυτόματης αποθήκευσης
|
.restored-from-the-autosave-copy = Το "__ocs_fmt_0__" επαναφέρθηκε από το αντίγραφο αυτόματης αποθήκευσης
|
||||||
.autosave-copy-could-not-be-read = Δεν ήταν δυνατή η ανάγνωση του αντιγράφου αυτόματης αποθήκευσης: __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-created-objects-failed-sources = REVOLVE: δημιουργήθηκαν __ocs_arg_created__ αντικείμενα· __ocs_arg_failed__ αρχικά αντικείμενα δεν μπόρεσαν να περιστραφούν.
|
||||||
.revolve-no-selected-object-could-be-revolved = REVOLVE: κανένα επιλεγμένο αντικείμενο δεν μπόρεσε να περιστραφεί με τις ζητούμενες επιλογές.
|
.revolve-no-selected-object-could-be-revolved = REVOLVE: κανένα επιλεγμένο αντικείμενο δεν μπόρεσε να περιστραφεί με τις ζητούμενες επιλογές.
|
||||||
.none-use-shortcuts-set-key-command = (κανένα — χρησιμοποιήστε: SHORTCUTS SET <πλήκτρο> <εντολή>)
|
.none-use-shortcuts-set-key-command = (κανένα — χρησιμοποιήστε: SHORTCUTS SET <πλήκτρο> <εντολή>)
|
||||||
|
|||||||
@@ -277,6 +277,8 @@ common =
|
|||||||
.autosave-copy-could-not-be-stored = Autosave copy could not be stored: __ocs_fmt_0__
|
.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
|
.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__
|
.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 <key> <command>)
|
.none-use-shortcuts-set-key-command = (none — use: SHORTCUTS SET <key> <command>)
|
||||||
.alignment-indented-label-title-case = Alignment:
|
.alignment-indented-label-title-case = Alignment:
|
||||||
.background-fill-enabled = Background fill enabled
|
.background-fill-enabled = Background fill enabled
|
||||||
|
|||||||
@@ -278,6 +278,8 @@ common =
|
|||||||
.autosave-copy-could-not-be-stored = No se pudo guardar la copia de autoguardado: __ocs_fmt_0__
|
.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
|
.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__
|
.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
|
.jogged-radius = Radio con recodo
|
||||||
.none-use-shortcuts-set-key-command = (ninguno — use: SHORTCUTS SET <tecla> <comando>)
|
.none-use-shortcuts-set-key-command = (ninguno — use: SHORTCUTS SET <tecla> <comando>)
|
||||||
.alignment-indented-label-title-case = Alineación:
|
.alignment-indented-label-title-case = Alineación:
|
||||||
|
|||||||
@@ -277,6 +277,8 @@ common =
|
|||||||
.autosave-copy-could-not-be-stored = Automaattisen tallennuksen kopiota ei voitu tallentaa: __ocs_fmt_0__
|
.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
|
.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__
|
.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
|
.jogged-radius = Katkaistu säde
|
||||||
.none-use-shortcuts-set-key-command =(ei mitään — käytä: SHORTCUTS SET <key> <command>)
|
.none-use-shortcuts-set-key-command =(ei mitään — käytä: SHORTCUTS SET <key> <command>)
|
||||||
.alignment-indented-label-title-case = Tasaus:
|
.alignment-indented-label-title-case = Tasaus:
|
||||||
|
|||||||
@@ -278,6 +278,8 @@ common =
|
|||||||
.autosave-copy-could-not-be-stored = Impossible de stocker la copie de sauvegarde automatique : __ocs_fmt_0__
|
.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
|
.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__
|
.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
|
.jogged-radius = Rayon raccourci
|
||||||
.none-use-shortcuts-set-key-command = (aucune utilisation: SHORTCUTS SET <key> <command>)
|
.none-use-shortcuts-set-key-command = (aucune utilisation: SHORTCUTS SET <key> <command>)
|
||||||
.alignment-indented-label-title-case = Alignement :
|
.alignment-indented-label-title-case = Alignement :
|
||||||
|
|||||||
@@ -266,6 +266,8 @@ common =
|
|||||||
.autosave-copy-could-not-be-stored = ऑटोसेव प्रतिलिपि संग्रहित नहीं की जा सकी: __ocs_fmt_0__
|
.autosave-copy-could-not-be-stored = ऑटोसेव प्रतिलिपि संग्रहित नहीं की जा सकी: __ocs_fmt_0__
|
||||||
.restored-from-the-autosave-copy = "__ocs_fmt_0__" को ऑटोसेव प्रतिलिपि से पुनर्स्थापित किया गया
|
.restored-from-the-autosave-copy = "__ocs_fmt_0__" को ऑटोसेव प्रतिलिपि से पुनर्स्थापित किया गया
|
||||||
.autosave-copy-could-not-be-read = ऑटोसेव प्रतिलिपि पढ़ी नहीं जा सकी: __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 = मोड़ वाला त्रिज्या माप
|
.jogged-radius = मोड़ वाला त्रिज्या माप
|
||||||
.none-use-shortcuts-set-key-command = (कोई नहीं — उपयोग: SHORTCUTS SET <key> <command>)
|
.none-use-shortcuts-set-key-command = (कोई नहीं — उपयोग: SHORTCUTS SET <key> <command>)
|
||||||
.alignment-indented-label-title-case = संरेखण:
|
.alignment-indented-label-title-case = संरेखण:
|
||||||
|
|||||||
@@ -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__
|
.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
|
.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__
|
.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 <key> <command>)
|
||||||
|
.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
|
.name-already-in-use = A „__ocs_fmt_0__” név már használatban van
|
||||||
.rrggbb-or-blank = #RRGGBB vagy üres
|
.rrggbb-or-blank = #RRGGBB vagy üres
|
||||||
.default-parenthesized-lowercase = (alapértelmezett)
|
.default-parenthesized-lowercase = (alapértelmezett)
|
||||||
|
|||||||
@@ -277,6 +277,8 @@ common =
|
|||||||
.autosave-copy-could-not-be-stored = Impossibile salvare la copia di salvataggio automatico: __ocs_fmt_0__
|
.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
|
.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__
|
.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
|
.jogged-radius = Raggio spezzato
|
||||||
.none-use-shortcuts-set-key-command =(nessuno — utilizzare: SHORTCUTS SET <key> <command>)
|
.none-use-shortcuts-set-key-command =(nessuno — utilizzare: SHORTCUTS SET <key> <command>)
|
||||||
.alignment-indented-label-title-case = Allineamento:
|
.alignment-indented-label-title-case = Allineamento:
|
||||||
|
|||||||
@@ -268,6 +268,8 @@ common =
|
|||||||
.autosave-copy-could-not-be-stored = 自動保存コピーを保存できませんでした: __ocs_fmt_0__
|
.autosave-copy-could-not-be-stored = 自動保存コピーを保存できませんでした: __ocs_fmt_0__
|
||||||
.restored-from-the-autosave-copy = "__ocs_fmt_0__" を自動保存コピーから復元しました
|
.restored-from-the-autosave-copy = "__ocs_fmt_0__" を自動保存コピーから復元しました
|
||||||
.autosave-copy-could-not-be-read = 自動保存コピーを読み込めませんでした: __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 = 折れ曲がり半径
|
.jogged-radius = 折れ曲がり半径
|
||||||
.none-use-shortcuts-set-key-command = (なし — SHORTCUTS SET <key> <command> を使用)
|
.none-use-shortcuts-set-key-command = (なし — SHORTCUTS SET <key> <command> を使用)
|
||||||
.alignment-indented-label-title-case = 配置:
|
.alignment-indented-label-title-case = 配置:
|
||||||
|
|||||||
@@ -277,6 +277,8 @@ common =
|
|||||||
.autosave-copy-could-not-be-stored = 자동 저장 사본을 저장할 수 없습니다: __ocs_fmt_0__
|
.autosave-copy-could-not-be-stored = 자동 저장 사본을 저장할 수 없습니다: __ocs_fmt_0__
|
||||||
.restored-from-the-autosave-copy = "__ocs_fmt_0__"을(를) 자동 저장 사본에서 복원했습니다
|
.restored-from-the-autosave-copy = "__ocs_fmt_0__"을(를) 자동 저장 사본에서 복원했습니다
|
||||||
.autosave-copy-could-not-be-read = 자동 저장 사본을 읽을 수 없습니다: __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 = 꺾인 반지름
|
.jogged-radius = 꺾인 반지름
|
||||||
.none-use-shortcuts-set-key-command = (없음 — 사용: SHORTCUTS SET <key> <command>)
|
.none-use-shortcuts-set-key-command = (없음 — 사용: SHORTCUTS SET <key> <command>)
|
||||||
.alignment-indented-label-title-case =정렬:
|
.alignment-indented-label-title-case =정렬:
|
||||||
|
|||||||
@@ -278,6 +278,8 @@ common =
|
|||||||
.autosave-copy-could-not-be-stored = Automatische opslagkopie kon niet worden bewaard: __ocs_fmt_0__
|
.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
|
.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__
|
.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
|
.jogged-radius = Geknikte straal
|
||||||
.none-use-shortcuts-set-key-command = (geen gebruik: SHORTCUTS SET <toets> <opdracht>)
|
.none-use-shortcuts-set-key-command = (geen gebruik: SHORTCUTS SET <toets> <opdracht>)
|
||||||
.alignment-indented-label-title-case = Uitlijning:
|
.alignment-indented-label-title-case = Uitlijning:
|
||||||
|
|||||||
@@ -277,6 +277,8 @@ common =
|
|||||||
.autosave-copy-could-not-be-stored = Nie można zapisać kopii autozapisu: __ocs_fmt_0__
|
.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
|
.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__
|
.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
|
.jogged-radius = Promień łamany
|
||||||
.none-use-shortcuts-set-key-command =(brak — użyj: SHORTCUTS SET <key> <command>)
|
.none-use-shortcuts-set-key-command =(brak — użyj: SHORTCUTS SET <key> <command>)
|
||||||
.alignment-indented-label-title-case = Wyrównanie:
|
.alignment-indented-label-title-case = Wyrównanie:
|
||||||
|
|||||||
@@ -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__
|
.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
|
.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__
|
.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
|
.jogged-radius = Raio com desvio
|
||||||
.none-use-shortcuts-set-key-command = (nenhum — use: SHORTCUTS SET <tecla> <comando>)
|
.none-use-shortcuts-set-key-command = (nenhum — use: SHORTCUTS SET <tecla> <comando>)
|
||||||
.alignment-indented-label-title-case = Alinhamento:
|
.alignment-indented-label-title-case = Alinhamento:
|
||||||
|
|||||||
@@ -281,6 +281,8 @@ common =
|
|||||||
.autosave-copy-could-not-be-stored = Не удалось сохранить копию автосохранения: __ocs_fmt_0__
|
.autosave-copy-could-not-be-stored = Не удалось сохранить копию автосохранения: __ocs_fmt_0__
|
||||||
.restored-from-the-autosave-copy = «__ocs_fmt_0__» восстановлен из копии автосохранения
|
.restored-from-the-autosave-copy = «__ocs_fmt_0__» восстановлен из копии автосохранения
|
||||||
.autosave-copy-could-not-be-read = Не удалось прочитать копию автосохранения: __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 = Радиус с изломом
|
.jogged-radius = Радиус с изломом
|
||||||
.none-use-shortcuts-set-key-command = (нет — используйте: SHORTCUTS SET <key> <command>)
|
.none-use-shortcuts-set-key-command = (нет — используйте: SHORTCUTS SET <key> <command>)
|
||||||
.alignment-indented-label-title-case = Выравнивание:
|
.alignment-indented-label-title-case = Выравнивание:
|
||||||
|
|||||||
@@ -263,6 +263,8 @@ common =
|
|||||||
.autosave-copy-could-not-be-stored = Otomatik kayıt kopyası depolanamadı: __ocs_fmt_0__
|
.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
|
.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__
|
.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 <key> <command>)
|
.none-use-shortcuts-set-key-command = (none – use: SHORTCUTS SET <key> <command>)
|
||||||
.alignment-indented-label-title-case = Adapt:
|
.alignment-indented-label-title-case = Adapt:
|
||||||
.background-fill-enabled = Arka plan doldurma etkinleştir etkinleştirildi
|
.background-fill-enabled = Arka plan doldurma etkinleştir etkinleştirildi
|
||||||
|
|||||||
@@ -265,6 +265,8 @@ common =
|
|||||||
.autosave-copy-could-not-be-stored = 无法存储自动保存副本: __ocs_fmt_0__
|
.autosave-copy-could-not-be-stored = 无法存储自动保存副本: __ocs_fmt_0__
|
||||||
.restored-from-the-autosave-copy = 已从自动保存副本恢复 "__ocs_fmt_0__"
|
.restored-from-the-autosave-copy = 已从自动保存副本恢复 "__ocs_fmt_0__"
|
||||||
.autosave-copy-could-not-be-read = 无法读取自动保存副本: __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 = 折弯半径
|
.jogged-radius = 折弯半径
|
||||||
.none-use-shortcuts-set-key-command = (无 — 使用:SHORTCUTS SET <key> <command>)
|
.none-use-shortcuts-set-key-command = (无 — 使用:SHORTCUTS SET <key> <command>)
|
||||||
.alignment-indented-label-title-case = 对齐:
|
.alignment-indented-label-title-case = 对齐:
|
||||||
|
|||||||
@@ -277,6 +277,8 @@ common =
|
|||||||
.autosave-copy-could-not-be-stored = 無法儲存自動儲存副本: __ocs_fmt_0__
|
.autosave-copy-could-not-be-stored = 無法儲存自動儲存副本: __ocs_fmt_0__
|
||||||
.restored-from-the-autosave-copy = 已從自動儲存副本恢復 "__ocs_fmt_0__"
|
.restored-from-the-autosave-copy = 已從自動儲存副本恢復 "__ocs_fmt_0__"
|
||||||
.autosave-copy-could-not-be-read = 無法讀取自動儲存副本: __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 = 折彎半徑
|
.jogged-radius = 折彎半徑
|
||||||
.none-use-shortcuts-set-key-command = (無 — 使用:SHORTCUTS SET <key> <command>)
|
.none-use-shortcuts-set-key-command = (無 — 使用:SHORTCUTS SET <key> <command>)
|
||||||
.alignment-indented-label-title-case = 對齊:
|
.alignment-indented-label-title-case = 對齊:
|
||||||
|
|||||||
@@ -2337,6 +2337,11 @@ pub enum Message {
|
|||||||
/// Web: delete the offered autosave snapshot (and offer the next, if any).
|
/// Web: delete the offered autosave snapshot (and offer the next, if any).
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
WebAutosaveDiscard,
|
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
|
/// 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.
|
/// open poll. The OPFS copy is consumed only once that open succeeds.
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
|||||||
@@ -191,6 +191,22 @@ impl OpenCADStudio {
|
|||||||
// Discarded — drop this tab's autosave recovery copy.
|
// Discarded — drop this tab's autosave recovery copy.
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
let _ = std::fs::remove_file(self.autosave_target(idx));
|
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 {
|
if self.tabs.len() == 1 {
|
||||||
self.tab_counter += 1;
|
self.tab_counter += 1;
|
||||||
self.tabs[0] = crate::app::document::DocumentTab::new_drawing(self.tab_counter);
|
self.tabs[0] = crate::app::document::DocumentTab::new_drawing(self.tab_counter);
|
||||||
|
|||||||
+80
-26
@@ -1471,6 +1471,19 @@ impl OpenCADStudio {
|
|||||||
// Native: pick a path, then load on a worker thread. Web: the
|
// Native: pick a path, then load on a worker thread. Web: the
|
||||||
// browser hands back bytes, so pick + parse in one step and feed
|
// browser hands back bytes, so pick + parse in one step and feed
|
||||||
// the shared `FileOpened` handler directly.
|
// 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"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
{
|
{
|
||||||
Task::perform(crate::io::pick_open_path(), Message::OpenPathPicked)
|
Task::perform(crate::io::pick_open_path(), Message::OpenPathPicked)
|
||||||
@@ -2496,12 +2509,7 @@ impl OpenCADStudio {
|
|||||||
let mut recent_task = Task::none();
|
let mut recent_task = Task::none();
|
||||||
// The name the autosave snapshot for this tab is keyed by — capture it
|
// The name the autosave snapshot for this tab is keyed by — capture it
|
||||||
// before the save re-points `current_path` at the download name.
|
// before the save re-points `current_path` at the download name.
|
||||||
let autosave_key = self.tabs[i]
|
let autosave_key = Self::web_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 document = self.tabs[i].scene.document_for_save();
|
let document = self.tabs[i].scene.document_for_save();
|
||||||
let saved = match crate::io::save_to_bytes(&document, &ext, version) {
|
let saved = match crate::io::save_to_bytes(&document, &ext, version) {
|
||||||
Ok(bytes) => {
|
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
|
/// 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
|
/// 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
|
/// 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
|
// 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
|
// the boot scan can compare the two; untitled tabs use the tab
|
||||||
// title, which is what Save would download as.
|
// title, which is what Save would download as.
|
||||||
let name = tab
|
let name = Self::web_autosave_key(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 is_dxf = crate::io::source_is_dxf(tab.current_path.as_deref(), &tab.scene.document);
|
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 ext = if is_dxf { "dxf" } else { "dwg" }.to_string();
|
||||||
let version = tab.scene.document.version;
|
let version = tab.scene.document.version;
|
||||||
@@ -3725,6 +3743,21 @@ impl OpenCADStudio {
|
|||||||
Task::none()
|
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<Message> {
|
||||||
|
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
|
/// 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
|
/// 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
|
/// 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
|
// (`web_autosave_opened` / the failure hook), not here — the
|
||||||
// poll has not started the open yet, so queueing now would
|
// poll has not started the open yet, so queueing now would
|
||||||
// show the modal before the recovered drawing loads.
|
// 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()
|
Task::none()
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
@@ -3772,7 +3817,7 @@ impl OpenCADStudio {
|
|||||||
|
|
||||||
/// Delete the `.sv$` autosave recovery files for all open drawings. They
|
/// 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.
|
/// 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"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
for i in 0..self.tabs.len() {
|
for i in 0..self.tabs.len() {
|
||||||
let _ = std::fs::remove_file(self.autosave_target(i));
|
let _ = std::fs::remove_file(self.autosave_target(i));
|
||||||
@@ -3781,25 +3826,34 @@ impl OpenCADStudio {
|
|||||||
for path in &self.sheet_set.locks {
|
for path in &self.sheet_set.locks {
|
||||||
crate::app::commands::sheet_set::release_lock(path);
|
crate::app::commands::sheet_set::release_lock(path);
|
||||||
}
|
}
|
||||||
// Web counterpart: drop each open tab's OPFS snapshot. A tab close is
|
// Web counterpart: drop each open tab's OPFS snapshot, plus any
|
||||||
// not an exit, so this only runs on the explicit exit path; anything
|
// removes a save deferred while a snapshot round was still writing.
|
||||||
// the teardown race leaves behind is caught by the next boot scan.
|
// 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")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
for tab in &self.tabs {
|
{
|
||||||
let name = tab
|
let mut keys = std::mem::take(&mut self.web_autosave_clear_after);
|
||||||
.current_path
|
// An in-flight round may still be writing snapshots right now;
|
||||||
.as_ref()
|
// its late writes would land AFTER these removes and resurrect
|
||||||
.and_then(|path| path.file_name())
|
// exactly what was removed, so the per-tab sweep is pointless in
|
||||||
.map(|name| name.to_string_lossy().into_owned())
|
// that race — skip it entirely (the boot scan bounds leftovers:
|
||||||
.unwrap_or_else(|| tab.tab_display_name());
|
// `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 {
|
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.
|
/// Remove the autosave recovery files, then quit the application.
|
||||||
pub(in crate::app) fn exit_app(&self) -> Task<Message> {
|
pub(in crate::app) fn exit_app(&mut self) -> Task<Message> {
|
||||||
self.cleanup_autosaves();
|
self.cleanup_autosaves();
|
||||||
iced::exit()
|
iced::exit()
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-5
@@ -1136,12 +1136,15 @@ impl OpenCADStudio {
|
|||||||
// not be clobbered (its bytes are gone once dequeued), so the
|
// not be clobbered (its bytes are gone once dequeued), so the
|
||||||
// entry goes back to the queue for the next poll tick.
|
// entry goes back to the queue for the next poll tick.
|
||||||
if self.opening.is_some() {
|
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) => {
|
Some(key) => {
|
||||||
crate::io::web_drop::enqueue_restore(name, bytes, key)
|
crate::io::web_drop::enqueue_restore(name, bytes, key)
|
||||||
}
|
}
|
||||||
None => crate::io::web_drop::enqueue_open(name, bytes),
|
None => crate::io::web_drop::enqueue_open(name, bytes),
|
||||||
}
|
};
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
let state = std::sync::Arc::new(crate::io::OpenProgressState::new(
|
let state = std::sync::Arc::new(crate::io::OpenProgressState::new(
|
||||||
@@ -1312,6 +1315,8 @@ impl OpenCADStudio {
|
|||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
Message::WebAutosaveDiscard => self.on_web_autosave_discard(),
|
Message::WebAutosaveDiscard => self.on_web_autosave_discard(),
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
Message::WebAutosaveDefer => self.on_web_autosave_defer(),
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
Message::WebAutosaveBytes(name, key, result) => {
|
Message::WebAutosaveBytes(name, key, result) => {
|
||||||
self.on_web_autosave_bytes(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) {
|
if self.active_modal == Some(super::ModalKind::Options) {
|
||||||
return self.update(Message::OptionsClose);
|
return self.update(Message::OptionsClose);
|
||||||
}
|
}
|
||||||
// Closing the crash-recovery prompt (✕ or Escape) discards
|
// Closing the crash-recovery prompt (✕ or Escape) DEFERS:
|
||||||
// the offered snapshot — otherwise it would requeue itself.
|
// 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")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
if self.active_modal == Some(super::ModalKind::AutosaveRestore) {
|
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
|
// Closing the shortcut editor with un-applied rows needs an
|
||||||
// explicit discard; a second close attempt (or the overlay's
|
// explicit discard; a second close attempt (or the overlay's
|
||||||
|
|||||||
@@ -472,6 +472,17 @@ fn append_pdf_page(
|
|||||||
fallback_plot_style: Option<&PlotStyleTable>,
|
fallback_plot_style: Option<&PlotStyleTable>,
|
||||||
fonts: &SearchableFontSet,
|
fonts: &SearchableFontSet,
|
||||||
) -> Result<(), String> {
|
) -> 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 {
|
let PlotContent {
|
||||||
wires,
|
wires,
|
||||||
hatches,
|
hatches,
|
||||||
|
|||||||
+93
-26
@@ -83,24 +83,44 @@ pub(crate) fn cap_evictions(
|
|||||||
(entries, evicted)
|
(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
|
/// 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
|
/// stem plus " (recovered)" and its own extension. The extension split
|
||||||
/// has none, matching how autosave serializes untitled tabs). The extension
|
/// follows the WORKER convention (`name.rsplit('.').next()`, the whole name
|
||||||
/// must survive intact because the web parser picks the format from it.
|
/// 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
|
#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))] // unit-tested on native
|
||||||
pub(crate) fn recovered_name(drawing: &str) -> String {
|
pub(crate) fn recovered_name(drawing: &str) -> String {
|
||||||
let path = std::path::Path::new(drawing);
|
let (head, ext) = match drawing.rsplit_once('.') {
|
||||||
let stem = path
|
Some((head, ext)) if !ext.is_empty() => (head, ext.to_string()),
|
||||||
.file_stem()
|
// No dot (or a trailing one): the whole name is the stem.
|
||||||
.map(|stem| stem.to_string_lossy().into_owned())
|
_ => (drawing, "dxf".to_string()),
|
||||||
.filter(|stem| !stem.is_empty())
|
};
|
||||||
.unwrap_or_else(|| drawing.to_string());
|
// A hostile stored name must not smuggle path separators into the tab
|
||||||
let ext = path
|
// title / download name: keep only the final component of the head.
|
||||||
.extension()
|
let head = head.rsplit(['/', '\\']).next().unwrap_or_default();
|
||||||
.map(|ext| ext.to_string_lossy().into_owned())
|
// When the split consumed everything (".dxf") fall back to the whole
|
||||||
.filter(|ext| !ext.is_empty())
|
// stored name, like `Path::file_stem`'s empty-stem fallback did.
|
||||||
.unwrap_or_else(|| "dwg".to_string());
|
let mut stem = if head.is_empty() {
|
||||||
format!("{stem} (recovered).{ext}")
|
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
|
/// 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")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
pub async fn snapshot(name: &str, bytes: &[u8], thumbnail: Option<&[u8]>) -> Result<(), String> {
|
pub async fn snapshot(name: &str, bytes: &[u8], thumbnail: Option<&[u8]>) -> Result<(), String> {
|
||||||
let directory = autosave_directory(true).await?;
|
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) {
|
if !names.iter().any(|stored| stored == name) {
|
||||||
names.push(name.to_string());
|
names.push(name.to_string());
|
||||||
write_index(&directory, &names).await?;
|
write_index(&directory, &names).await?;
|
||||||
@@ -140,7 +160,9 @@ pub async fn remove(name: &str) -> Result<(), String> {
|
|||||||
.await
|
.await
|
||||||
.map_err(super::web_recent::js_error);
|
.map_err(super::web_recent::js_error);
|
||||||
let _ = JsFuture::from(directory.remove_entry(&thumbnail_key(name))).await;
|
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();
|
let before = names.len();
|
||||||
names.retain(|stored| stored != name);
|
names.retain(|stored| stored != name);
|
||||||
if names.len() != before {
|
if names.len() != before {
|
||||||
@@ -161,7 +183,9 @@ pub async fn scan() -> Result<Vec<WebAutosaveEntry>, String> {
|
|||||||
let directory = autosave_directory(false).await?;
|
let directory = autosave_directory(false).await?;
|
||||||
let mut entries = Vec::new();
|
let mut entries = Vec::new();
|
||||||
let mut dead: Vec<String> = Vec::new();
|
let mut dead: Vec<String> = 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 {
|
let file = match super::web_recent::get_file(&directory, &snapshot_key(&name)).await {
|
||||||
Ok(file) => file,
|
Ok(file) => file,
|
||||||
// Crash between the index and snapshot writes: the row points
|
// Crash between the index and snapshot writes: the row points
|
||||||
@@ -212,12 +236,23 @@ fn thumbnail_key(name: &str) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(target_arch = "wasm32")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
async fn read_index(directory: &web_sys::FileSystemDirectoryHandle) -> Vec<String> {
|
async fn read_index(
|
||||||
super::web_recent::read_entry(directory, INDEX_KEY)
|
directory: &web_sys::FileSystemDirectoryHandle,
|
||||||
.await
|
) -> Result<Vec<String>, String> {
|
||||||
.ok()
|
let bytes = match super::web_recent::read_entry(directory, INDEX_KEY).await {
|
||||||
.and_then(|bytes| serde_json::from_slice(&bytes).ok())
|
Ok(bytes) => bytes,
|
||||||
.unwrap_or_default()
|
// 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")]
|
#[cfg(target_arch = "wasm32")]
|
||||||
@@ -266,10 +301,42 @@ mod tests {
|
|||||||
fn recovered_names_keep_a_parseable_extension() {
|
fn recovered_names_keep_a_parseable_extension() {
|
||||||
assert_eq!(recovered_name("plan.dwg"), "plan (recovered).dwg");
|
assert_eq!(recovered_name("plan.dwg"), "plan (recovered).dwg");
|
||||||
assert_eq!(recovered_name("sketch.dxf"), "sketch (recovered).dxf");
|
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.
|
// 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("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]
|
#[test]
|
||||||
|
|||||||
+29
-14
@@ -12,7 +12,8 @@ use wasm_bindgen_futures::JsFuture;
|
|||||||
|
|
||||||
/// A stray multi-drop must not grow the queue without bound; extras are
|
/// A stray multi-drop must not grow the queue without bound; extras are
|
||||||
/// rejected with a console warning instead of being silently dropped.
|
/// 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
|
/// One queued open. `restore_key` tags an autosave-restore file so its
|
||||||
/// snapshot is deleted only when THIS open succeeds — matching by identity,
|
/// snapshot is deleted only when THIS open succeeds — matching by identity,
|
||||||
@@ -41,7 +42,9 @@ fn lock_queue() -> std::sync::MutexGuard<'static, VecDeque<QueuedOpen>> {
|
|||||||
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
.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 name = entry.name.clone();
|
||||||
let mut queue = lock_queue();
|
let mut queue = lock_queue();
|
||||||
if queue.len() >= QUEUE_CAP {
|
if queue.len() >= QUEUE_CAP {
|
||||||
@@ -49,35 +52,40 @@ fn push(entry: QueuedOpen) {
|
|||||||
warn(&format!(
|
warn(&format!(
|
||||||
"ocs: dropped-file queue full; ignoring \"{name}\""
|
"ocs: dropped-file queue full; ignoring \"{name}\""
|
||||||
));
|
));
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
queue.push_back(entry);
|
queue.push_back(entry);
|
||||||
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Queue already-read drawing bytes (drag-and-drop) for the 50 ms poll to
|
/// Queue already-read drawing bytes (drag-and-drop) for the 50 ms poll to
|
||||||
/// open through the guarded web-picker flow.
|
/// open through the guarded web-picker flow. Returns `false` when the queue
|
||||||
pub fn enqueue_open(name: String, bytes: Vec<u8>) {
|
/// is full and the bytes were dropped.
|
||||||
|
pub fn enqueue_open(name: String, bytes: Vec<u8>) -> bool {
|
||||||
push(QueuedOpen {
|
push(QueuedOpen {
|
||||||
name,
|
name,
|
||||||
bytes,
|
bytes,
|
||||||
restore_key: None,
|
restore_key: None,
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Queue an autosave-restore file tagged with its snapshot key.
|
/// Queue an autosave-restore file tagged with its snapshot key. Returns
|
||||||
pub fn enqueue_restore(name: String, bytes: Vec<u8>, restore_key: String) {
|
/// `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<u8>, restore_key: String) -> bool {
|
||||||
push(QueuedOpen {
|
push(QueuedOpen {
|
||||||
name,
|
name,
|
||||||
bytes,
|
bytes,
|
||||||
restore_key: Some(restore_key),
|
restore_key: Some(restore_key),
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// JS-callable entry (same shape as `ocs_control_submit`) for hosts that
|
/// JS-callable entry (same shape as `ocs_control_submit`) for hosts that
|
||||||
/// forward drops themselves.
|
/// forward drops themselves.
|
||||||
#[wasm_bindgen::prelude::wasm_bindgen]
|
#[wasm_bindgen::prelude::wasm_bindgen]
|
||||||
pub fn ocs_web_drop_file(name: String, bytes: js_sys::Uint8Array) {
|
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.
|
/// 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 {
|
for (name, read) in reads {
|
||||||
match JsFuture::from(read).await {
|
match JsFuture::from(read).await {
|
||||||
Ok(buffer) => {
|
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) => {
|
Err(error) => {
|
||||||
web_sys::console::error_1(&wasm_bindgen::JsValue::from_str(&format!(
|
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 {
|
fn is_drawing_name(name: &str) -> bool {
|
||||||
name.rsplit_once('.').is_some_and(|(_, extension)| {
|
// Same convention as the worker's reader dispatch (`rsplit('.').next()`):
|
||||||
matches!(extension.to_ascii_lowercase().as_str(), "dwg" | "dxf")
|
// 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"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
@@ -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")),
|
"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")),
|
"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")),
|
"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,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+29
-5
@@ -454,9 +454,11 @@ pub fn show_pdf_preview(name: &str, pdf_bytes: &[u8], page_pngs: &[Vec<u8>], tot
|
|||||||
let Some(document) = window.document() else {
|
let Some(document) = window.document() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
// One preview at a time: a replotted preview replaces its predecessor and
|
// One preview at a time: a replotted preview replaces its predecessor,
|
||||||
// revokes its blob URLs, so repeated previews cannot stack overlays or
|
// revokes its blob URLs and DROPS its Download closure (freeing the full
|
||||||
// leak the images underneath.
|
// 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";
|
const OVERLAY_ID: &str = "ocs-plot-preview";
|
||||||
if let Some(previous) = document.get_element_by_id(OVERLAY_ID) {
|
if let Some(previous) = document.get_element_by_id(OVERLAY_ID) {
|
||||||
previous.remove();
|
previous.remove();
|
||||||
@@ -468,6 +470,11 @@ pub fn show_pdf_preview(name: &str, pdf_bytes: &[u8], page_pngs: &[Vec<u8>], tot
|
|||||||
{
|
{
|
||||||
let _ = web_sys::Url::revoke_object_url(&url);
|
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() {
|
if page_pngs.is_empty() {
|
||||||
// Rasterisation produced nothing: deliver the PDF itself rather than
|
// Rasterisation produced nothing: deliver the PDF itself rather than
|
||||||
// show an empty frame.
|
// show an empty frame.
|
||||||
@@ -570,13 +577,16 @@ pub fn show_pdf_preview(name: &str, pdf_bytes: &[u8], page_pngs: &[Vec<u8>], tot
|
|||||||
let name = name.to_string();
|
let name = name.to_string();
|
||||||
let bytes = pdf_bytes.to_vec();
|
let bytes = pdf_bytes.to_vec();
|
||||||
// A plain FnMut stays usable for every click; the one-shot variant
|
// 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<dyn FnMut()> =
|
let closure: wasm_bindgen::closure::Closure<dyn FnMut()> =
|
||||||
wasm_bindgen::closure::Closure::new(move || {
|
wasm_bindgen::closure::Closure::new(move || {
|
||||||
download_bytes(&name, &bytes);
|
download_bytes(&name, &bytes);
|
||||||
});
|
});
|
||||||
let click: js_sys::Function = closure.as_ref().unchecked_ref::<js_sys::Function>().clone();
|
let click: js_sys::Function = closure.as_ref().unchecked_ref::<js_sys::Function>().clone();
|
||||||
closure.forget();
|
PREVIEW_DOWNLOAD_CLOSURE.with(|slot| *slot.borrow_mut() = Some(closure));
|
||||||
dl.set_onclick(Some(&click));
|
dl.set_onclick(Some(&click));
|
||||||
bar.append_child(&dl).ok();
|
bar.append_child(&dl).ok();
|
||||||
}
|
}
|
||||||
@@ -612,6 +622,20 @@ fn preview_urls() -> &'static std::sync::Mutex<Vec<String>> {
|
|||||||
URLS.get_or_init(|| std::sync::Mutex::new(Vec::new()))
|
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<Option<wasm_bindgen::closure::Closure<dyn FnMut()>>> =
|
||||||
|
std::cell::RefCell::new(None);
|
||||||
|
}
|
||||||
|
|
||||||
/// Short platform string for bug reports: OS + architecture on the desktop,
|
/// Short platform string for bug reports: OS + architecture on the desktop,
|
||||||
/// the browser user-agent on the web.
|
/// the browser user-agent on the web.
|
||||||
#[cfg(not(target_arch = "wasm32"))]
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
|||||||
Reference in new issue
Block a user