feat: Einmaliger Hinweis in Browsern ohne File System Access (D72-Nachtrag 2)

Firefox/Safari laden Dateien als Kopie und speichern als Download — beim
ersten Öffnen/Speichern erklärt ein Banner die Grenze (i18n in neun
Sprachen, „Verstanden" merkt der localStorage, Reset räumt mit).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-08-25 15:53:42 +02:00
co-authored by Claude Fable 5
parent 731a383345
commit b16f9954a9
3 changed files with 78 additions and 1 deletions
+1
View File
@@ -19,6 +19,7 @@ reverse.
## 2026-08-25
- Browsers without the File System Access API (Firefox, Safari) explain themselves once: files open as copies and saving downloads a new file
- Fix: the save dialog suggested a "name (1)" neighbour in the wrong folder — it now points at the original file, and open/save dialogs remember the plan folder
- Ctrl+S saves the document as a file — with a remembered file it writes straight back in place, and the document name flashes as confirmation
- Fix: a file double-clicked right after the app starts could open as a duplicate document instead of updating its own
+36
View File
@@ -5587,3 +5587,39 @@ gelöscht (übrig: Example, Werkbaum). Ob der Nutzer zusätzlich noch das
den Strg+S-Handler noch nicht — die Taste ging an den Browser, dessen
„Seite speichern" hängt bei Wiederholung ebenfalls „ (1)" an), ließ sich
von hier nicht feststellen; ein Neustart der App stellt es klar.
## D72 — Nachtrag 2: Browser ohne File System Access erklären sich einmalig
Gewünscht vom Nutzer: In Firefox/Safari (und allem anderen ohne die API)
verhält sich die App bei lokalen Dateien „etwas seltsam" — eine geöffnete
Datei kommt als Kopie herein (beim erneuten Öffnen eine weitere, D72), und
Speichern legt eine neue Datei in den Downloads ab, statt zurückzuschreiben.
Wer die Chromium-Fassung kennt oder erwartet, hält das für einen Fehler.
Ein **einmaliger Hinweis** benennt die Grenze, bevor sie verwirrt.
- **Gezeigt beim ersten Öffnen oder Speichern, nicht beim App-Start.** Der
Hinweis erklärt das Verhalten der Datei-Funktionen — wer sie nie benutzt,
bekommt ihn nie zu sehen. Ein Banner beim Start hätte jeden
Firefox-Besucher mit einer Auskunft über etwas begrüßt, das ihn (noch)
nichts angeht.
- **Form: das Banner-Idiom der Update-Meldung** (fixiert oben, Tinte statt
Petrol — Information, keine Aufforderung), mit „Verstanden"-Knopf. Er darf
neben dem gerade aufgehenden Datei-Dialog erscheinen: Der Dialog liegt
darüber, und nach dessen Schließen steht die Erklärung da — genau dann,
wenn man das Ergebnis sieht.
- **„Verstanden" merkt der localStorage** (`werkbaum-fs-notice`); der
Debug-Reset räumt den Schlüssel mit weg. Kein erneutes Zeigen je Sitzung
oder je Dokument — die Auskunft ändert sich nicht, und ein wiederkehrender
Hinweis wäre Gängelung.
- **Text in allen neun Sprachen** (`fsNotice`/`fsNoticeOk`, Deutsch als
Quellsprache): was passiert (Kopie, Download) und dass Chromium-Browser
direkt zurückschreiben — die eine Zeile, die den Wechsel-Anreiz ehrlich
benennt, ohne zu werben.
**Nachgemessen** im Dev-Server mit erzwungenem `hasFsAccess = false` (das
Prüf-Pane ist Chromium — dieselbe Werkzeuggrenze wie bei den
Picker-Stubs; der Datei-Input war stummgeschaltet): Erstes „Datei öffnen…"
zeigt das Banner mit deutschem Text und Knopf, der Input-Klick läuft
trotzdem; „Verstanden" setzt den Merker und entfernt das Banner; ein
zweites Öffnen zeigt nichts mehr und öffnet weiter den Input. In Chromium
(`hasFsAccess` wahr) kehrt `maybeShowFsNotice()` in der ersten Zeile um —
dort existiert der Hinweis nicht.
+41 -1
View File
@@ -2193,6 +2193,7 @@ const I18N = {
docRestore:"Original wiederherstellen",
docRestoreConfirm:"„{name}“ auf den mitgelieferten Stand zurücksetzen? Eigene Änderungen gehen verloren.",
docOpenFile:"Datei öffnen…", docSaveFile:"Als Datei speichern",
fsNotice:"Dieser Browser kann lokale Dateien nicht direkt beschreiben: Eine geöffnete Datei wird als Kopie geladen, und Speichern legt eine neue Datei in den Downloads ab. Chromium-Browser schreiben direkt in die geöffnete Datei zurück.", fsNoticeOk:"Verstanden",
copy:"kopieren", copyDone:"kopiert ✓", copyTooltip:"Text in die Zwischenablage kopieren",
copyDiagramTooltip:"Diagramm als PNG-Bild in die Zwischenablage kopieren",
downloadDiagramTooltip:"Diagramm als SVG-Datei herunterladen (z. B. für LibreOffice: Einfügen → Bild)",
@@ -2300,6 +2301,7 @@ const I18N = {
docRestore:"Restore original",
docRestoreConfirm:"Reset “{name}” to the shipped version? Your changes will be lost.",
docOpenFile:"Open file…", docSaveFile:"Save as file",
fsNotice:"This browser cannot write to local files directly: an opened file is loaded as a copy, and saving puts a new file into your downloads. Chromium browsers write straight back into the opened file.", fsNoticeOk:"Got it",
copy:"copy", copyDone:"copied ✓", copyTooltip:"Copy text to clipboard",
copyDiagramTooltip:"Copy diagram as a PNG image to the clipboard",
downloadDiagramTooltip:"Download diagram as an SVG file (e.g. for LibreOffice: Insert → Image)",
@@ -2407,6 +2409,7 @@ const I18N = {
docRestore:"Restaurar original",
docRestoreConfirm:"¿Restablecer «{name}» a la versión incluida? Tus cambios se perderán.",
docOpenFile:"Abrir archivo…", docSaveFile:"Guardar como archivo",
fsNotice:"Este navegador no puede escribir directamente en archivos locales: un archivo abierto se carga como copia, y al guardar se crea un archivo nuevo en las descargas. Los navegadores Chromium escriben directamente en el archivo abierto.", fsNoticeOk:"Entendido",
copy:"copiar", copyDone:"copiado ✓", copyTooltip:"Copiar el texto al portapapeles",
copyDiagramTooltip:"Copiar el diagrama como imagen PNG al portapapeles",
downloadDiagramTooltip:"Descargar el diagrama como archivo SVG (p. ej. para LibreOffice: Insertar → Imagen)",
@@ -2514,6 +2517,7 @@ const I18N = {
docRestore:"Restaurer loriginal",
docRestoreConfirm:"Réinitialiser « {name} » à la version livrée ? Vos modifications seront perdues.",
docOpenFile:"Ouvrir un fichier…", docSaveFile:"Enregistrer comme fichier",
fsNotice:"Ce navigateur ne peut pas écrire directement dans les fichiers locaux : un fichier ouvert est chargé comme copie, et lenregistrement dépose un nouveau fichier dans les téléchargements. Les navigateurs Chromium réécrivent directement dans le fichier ouvert.", fsNoticeOk:"Compris",
copy:"copier", copyDone:"copié ✓", copyTooltip:"Copier le texte dans le presse-papiers",
copyDiagramTooltip:"Copier le diagramme comme image PNG dans le presse-papiers",
downloadDiagramTooltip:"Télécharger le diagramme en fichier SVG (p. ex. pour LibreOffice : Insertion → Image)",
@@ -2621,6 +2625,7 @@ const I18N = {
docRestore:"Przywróć oryginał",
docRestoreConfirm:"Przywrócić „{name}” do dostarczonej wersji? Twoje zmiany zostaną utracone.",
docOpenFile:"Otwórz plik…", docSaveFile:"Zapisz jako plik",
fsNotice:"Ta przeglądarka nie może bezpośrednio zapisywać plików lokalnych: otwarty plik jest wczytywany jako kopia, a zapis tworzy nowy plik w pobranych. Przeglądarki Chromium zapisują bezpośrednio do otwartego pliku.", fsNoticeOk:"Rozumiem",
copy:"kopiuj", copyDone:"skopiowano ✓", copyTooltip:"Kopiuj tekst do schowka",
copyDiagramTooltip:"Kopiuj diagram jako obraz PNG do schowka",
downloadDiagramTooltip:"Pobierz diagram jako plik SVG (np. dla LibreOffice: Wstaw → Obraz)",
@@ -2728,6 +2733,7 @@ const I18N = {
docRestore:"Восстановить оригинал",
docRestoreConfirm:"Вернуть «{name}» к поставляемой версии? Ваши изменения будут потеряны.",
docOpenFile:"Открыть файл…", docSaveFile:"Сохранить как файл",
fsNotice:"Этот браузер не может напрямую записывать локальные файлы: открытый файл загружается как копия, а сохранение создаёт новый файл в загрузках. Браузеры на Chromium записывают прямо в открытый файл.", fsNoticeOk:"Понятно",
copy:"копировать", copyDone:"скопировано ✓", copyTooltip:"Скопировать текст в буфер обмена",
copyDiagramTooltip:"Скопировать диаграмму как изображение PNG в буфер обмена",
downloadDiagramTooltip:"Скачать диаграмму как файл SVG (напр. для LibreOffice: Вставка → Изображение)",
@@ -2835,6 +2841,7 @@ const I18N = {
docRestore:"मूल पुनर्स्थापित करें",
docRestoreConfirm:"„{name}“ को मूल संस्करण पर लौटाएँ? आपके परिवर्तन खो जाएँगे।",
docOpenFile:"फ़ाइल खोलें…", docSaveFile:"फ़ाइल के रूप में सहेजें",
fsNotice:"यह ब्राउज़र स्थानीय फ़ाइलों में सीधे नहीं लिख सकता: खोली गई फ़ाइल एक प्रति के रूप में लोड होती है, और सहेजने पर डाउनलोड में एक नई फ़ाइल बनती है। Chromium ब्राउज़र सीधे खोली गई फ़ाइल में लिखते हैं।", fsNoticeOk:"समझ गया",
copy:"कॉपी करें", copyDone:"कॉपी हो गया ✓", copyTooltip:"टेक्स्ट को क्लिपबोर्ड पर कॉपी करें",
copyDiagramTooltip:"आरेख को PNG छवि के रूप में क्लिपबोर्ड पर कॉपी करें",
downloadDiagramTooltip:"आरेख को SVG फ़ाइल के रूप में डाउनलोड करें (जैसे LibreOffice: सम्मिलित करें → छवि)",
@@ -2942,6 +2949,7 @@ const I18N = {
docRestore:"恢复原始版本",
docRestoreConfirm:"将“{name}”重置为随附版本?您的更改将丢失。",
docOpenFile:"打开文件…", docSaveFile:"另存为文件",
fsNotice:"此浏览器无法直接写入本地文件:打开的文件会作为副本载入,保存时会在下载目录生成一个新文件。Chromium 浏览器则会直接写回打开的文件。", fsNoticeOk:"知道了",
copy:"复制", copyDone:"已复制 ✓", copyTooltip:"将文本复制到剪贴板",
copyDiagramTooltip:"将图表作为 PNG 图片复制到剪贴板",
downloadDiagramTooltip:"将图表下载为 SVG 文件(例如用于 LibreOffice:插入 → 图像)",
@@ -3049,6 +3057,7 @@ const I18N = {
docRestore:"オリジナルを復元",
docRestoreConfirm:"「{name}」を同梱版に戻しますか?変更内容は失われます。",
docOpenFile:"ファイルを開く…", docSaveFile:"ファイルとして保存",
fsNotice:"このブラウザーはローカルファイルへ直接書き込めません。開いたファイルはコピーとして読み込まれ、保存するとダウンロードに新しいファイルが作られます。Chromium 系ブラウザーは開いたファイルへ直接書き戻します。", fsNoticeOk:"わかりました",
copy:"コピー", copyDone:"コピーしました ✓", copyTooltip:"テキストをクリップボードにコピー",
copyDiagramTooltip:"ダイアグラムを PNG 画像としてクリップボードにコピー",
downloadDiagramTooltip:"ダイアグラムを SVG ファイルとしてダウンロード(例:LibreOffice の 挿入 → 画像)",
@@ -3986,7 +3995,37 @@ async function openWithPicker(){
try{ text = await (await handle.getFile()).text(); }catch(_){ return; }
await adoptFile(handle, handle.name, text);
}
/* Einmaliger Hinweis für Browser ohne File System Access (D72-Nachtrag 2):
Ohne die API wird eine geöffnete Datei als Kopie geladen, und Speichern
legt eine neue Datei in den Downloads ab wer das nicht weiß, hält es
für ein Fehlverhalten der App. Gezeigt beim ersten Öffnen/Speichern,
nicht beim App-Start: Wer die Datei-Funktionen nie benutzt, braucht ihn
nie. Verstanden" merkt sich der localStorage; der Reset räumt ihn mit. */
const LS_FS_NOTICE = 'werkbaum-fs-notice';
function maybeShowFsNotice(){
if(hasFsAccess) return;
try{ if(localStorage.getItem(LS_FS_NOTICE)) return; }catch(_){}
if(document.getElementById('fsNotice')) return;
const n = document.createElement('div');
n.id = 'fsNotice';
n.style.cssText = 'position:fixed;top:0;left:0;right:0;background:#243447;color:#fff;'
+ 'padding:12px 16px;display:flex;align-items:center;justify-content:space-between;'
+ "gap:16px;z-index:1000;font-size:14px;font-family:'IBM Plex Sans',system-ui,sans-serif;";
const span = document.createElement('span');
span.textContent = t('fsNotice');
const btn = document.createElement('button');
btn.textContent = t('fsNoticeOk');
btn.style.cssText = 'background:#fff;color:#243447;border:none;padding:4px 10px;'
+ 'border-radius:4px;cursor:pointer;font-weight:500;font-size:12px;flex:0 0 auto;';
btn.addEventListener('click', () => {
try{ localStorage.setItem(LS_FS_NOTICE, '1'); }catch(_){}
n.remove();
});
n.append(span, btn);
document.body.insertBefore(n, document.body.firstChild);
}
function openLocalFile(){
maybeShowFsNotice();
if(hasFsAccess) openWithPicker();
else fileOpenInput.click();
}
@@ -4037,6 +4076,7 @@ async function saveWithPicker(d){
Blob-Download (Stufe 1). UTF-8, LF das Textfeld normalisiert Zeilenenden
ohnehin auf \n (D24). */
async function saveLocalFile(){
maybeShowFsNotice();
flushActive();
const d = activeDoc();
if(!d) return;
@@ -4983,7 +5023,7 @@ function resetToDefaults(){
Dokumentenliste (werkbaum-docs) bleibt erhalten (D22). Die beiden
Update-Schlüssel schreibt niemand mehr (D45); sie werden nur noch
aufgeräumt, falls sie aus einer früheren Fassung herumliegen. */
['werkbaum-ui','werkbaum-lang','werkbaum-html-hash','werkbaum-update-available','werkbaum-update-log']
['werkbaum-ui','werkbaum-lang','werkbaum-html-hash','werkbaum-update-available','werkbaum-update-log','werkbaum-fs-notice']
.forEach(k => { try{ localStorage.removeItem(k); }catch(_){} });
/* Nur die MITGELIEFERTEN Dokumente auf ihren Ausgangstext zurücksetzen;