feat: Lokale Dateien öffnen und als Datei speichern (D72, Stufe 1)

Zwei Einträge im Dokumenten-Menü: „Datei öffnen…" (Datei-Input, neues
Dokument, Dateiname als Name) und „Als Datei speichern" (Blob-Download,
Dateiname aus dem Dokumentnamen via localfile.js). Läuft in jedem Browser;
Stufe 2 (File System Access, Chromium) folgt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
mhoennig
2026-08-25 14:26:31 +02:00
co-authored by Claude Fable 5
parent ced3507678
commit defeaf84d0
7 changed files with 182 additions and 3 deletions
+49
View File
@@ -7,6 +7,7 @@ import { padUrls } from './remote.js';
import { depFragment, collectIds, matchIds, depIdAt, idLine } from './autocomplete.js';
import { LS_SNAPS, SNAP_EVERY, parseSnaps, addSnapshot, persistSnaps, snapLabel }
from './snapshots.js';
import { FILE_ACCEPT, saveFileName } from './localfile.js';
/* Neuigkeiten (D58): die git-Historie, zur BAUZEIT eingelesen (Vite-Plugin in
vite.config.js). Zur Laufzeit gibt es kein git und keinen Server, der
nachliefern könnte (D11/D19). Leer, wo git nicht erreichbar war. */
@@ -2191,6 +2192,7 @@ const I18N = {
docDeleteConfirm:"Dokument „{name}“ löschen?",
docRestore:"Original wiederherstellen",
docRestoreConfirm:"„{name}“ auf den mitgelieferten Stand zurücksetzen? Eigene Änderungen gehen verloren.",
docOpenFile:"Datei öffnen…", docSaveFile:"Als Datei speichern",
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)",
@@ -2296,6 +2298,7 @@ const I18N = {
docDeleteConfirm:"Delete document “{name}”?",
docRestore:"Restore original",
docRestoreConfirm:"Reset “{name}” to the shipped version? Your changes will be lost.",
docOpenFile:"Open file…", docSaveFile:"Save as file",
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)",
@@ -2401,6 +2404,7 @@ const I18N = {
docDeleteConfirm:"¿Eliminar el documento «{name}»?",
docRestore:"Restaurar original",
docRestoreConfirm:"¿Restablecer «{name}» a la versión incluida? Tus cambios se perderán.",
docOpenFile:"Abrir archivo…", docSaveFile:"Guardar como archivo",
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)",
@@ -2506,6 +2510,7 @@ const I18N = {
docDeleteConfirm:"Supprimer le document « {name} » ?",
docRestore:"Restaurer loriginal",
docRestoreConfirm:"Réinitialiser « {name} » à la version livrée ? Vos modifications seront perdues.",
docOpenFile:"Ouvrir un fichier…", docSaveFile:"Enregistrer comme fichier",
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)",
@@ -2611,6 +2616,7 @@ const I18N = {
docDeleteConfirm:"Usunąć dokument „{name}”?",
docRestore:"Przywróć oryginał",
docRestoreConfirm:"Przywrócić „{name}” do dostarczonej wersji? Twoje zmiany zostaną utracone.",
docOpenFile:"Otwórz plik…", docSaveFile:"Zapisz jako plik",
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)",
@@ -2716,6 +2722,7 @@ const I18N = {
docDeleteConfirm:"Удалить документ «{name}»?",
docRestore:"Восстановить оригинал",
docRestoreConfirm:"Вернуть «{name}» к поставляемой версии? Ваши изменения будут потеряны.",
docOpenFile:"Открыть файл…", docSaveFile:"Сохранить как файл",
copy:"копировать", copyDone:"скопировано ✓", copyTooltip:"Скопировать текст в буфер обмена",
copyDiagramTooltip:"Скопировать диаграмму как изображение PNG в буфер обмена",
downloadDiagramTooltip:"Скачать диаграмму как файл SVG (напр. для LibreOffice: Вставка → Изображение)",
@@ -2821,6 +2828,7 @@ const I18N = {
docDeleteConfirm:"दस्तावेज़ „{name}“ हटाएँ?",
docRestore:"मूल पुनर्स्थापित करें",
docRestoreConfirm:"„{name}“ को मूल संस्करण पर लौटाएँ? आपके परिवर्तन खो जाएँगे।",
docOpenFile:"फ़ाइल खोलें…", docSaveFile:"फ़ाइल के रूप में सहेजें",
copy:"कॉपी करें", copyDone:"कॉपी हो गया ✓", copyTooltip:"टेक्स्ट को क्लिपबोर्ड पर कॉपी करें",
copyDiagramTooltip:"आरेख को PNG छवि के रूप में क्लिपबोर्ड पर कॉपी करें",
downloadDiagramTooltip:"आरेख को SVG फ़ाइल के रूप में डाउनलोड करें (जैसे LibreOffice: सम्मिलित करें → छवि)",
@@ -2926,6 +2934,7 @@ const I18N = {
docDeleteConfirm:"删除文档“{name}”?",
docRestore:"恢复原始版本",
docRestoreConfirm:"将“{name}”重置为随附版本?您的更改将丢失。",
docOpenFile:"打开文件…", docSaveFile:"另存为文件",
copy:"复制", copyDone:"已复制 ✓", copyTooltip:"将文本复制到剪贴板",
copyDiagramTooltip:"将图表作为 PNG 图片复制到剪贴板",
downloadDiagramTooltip:"将图表下载为 SVG 文件(例如用于 LibreOffice:插入 → 图像)",
@@ -3031,6 +3040,7 @@ const I18N = {
docDeleteConfirm:"ドキュメント「{name}」を削除しますか?",
docRestore:"オリジナルを復元",
docRestoreConfirm:"「{name}」を同梱版に戻しますか?変更内容は失われます。",
docOpenFile:"ファイルを開く…", docSaveFile:"ファイルとして保存",
copy:"コピー", copyDone:"コピーしました ✓", copyTooltip:"テキストをクリップボードにコピー",
copyDiagramTooltip:"ダイアグラムを PNG 画像としてクリップボードにコピー",
downloadDiagramTooltip:"ダイアグラムを SVG ファイルとしてダウンロード(例:LibreOffice の 挿入 → 画像)",
@@ -3845,6 +3855,43 @@ function deleteDoc(){
persistDocs();
closeDocMenu();
}
/* ---------- Lokale Dateien öffnen und speichern (D72, Stufe 1) ----------
Der klassische Weg, der in jedem Browser läuft: Datei-Input zum Öffnen,
Blob-Download zum Speichern. Geöffnet wird als NEUES Dokument (D22) eine
Identität gleicher Dateiname = gleiches Dokument" wäre eine Vermutung, und
zwei verschiedene Dateien gleichen Namens überschrieben sich still. Der
Dateiname wird der Dokumentname; beim Speichern entsteht er daraus zurück
(saveFileName, localfile.js). */
const fileOpenInput = document.createElement('input');
fileOpenInput.type = 'file';
fileOpenInput.accept = FILE_ACCEPT;
fileOpenInput.hidden = true;
document.body.appendChild(fileOpenInput);
fileOpenInput.addEventListener('change', async () => {
const f = fileOpenInput.files && fileOpenInput.files[0];
fileOpenInput.value = ''; /* dieselbe Datei soll erneut wählbar sein */
if(!f) return;
let text;
try{ text = await f.text(); }catch(_){ return; }
flushActive();
const d = { id: uid(), name: uniqueName(f.name), text };
docs.push(d);
activeId = d.id;
foldOverrides.clear();
loadActiveIntoEditor();
persistDocs();
closeDocMenu();
});
function openLocalFile(){ fileOpenInput.click(); }
/* Speichern über den vorhandenen Blob-Download (saveBlob, Grafikexport):
UTF-8, LF das Textfeld normalisiert Zeilenenden ohnehin auf \n (D24). */
function saveLocalFile(){
flushActive();
const d = activeDoc();
if(!d) return;
saveBlob(new Blob([d.text], {type:'text/plain;charset=utf-8'}), saveFileName(d.name));
closeDocMenu();
}
/* Dokumente laden + aktiven Text in den Editor holen (nach applyLang). */
function initDocs(){
restoring = true;
@@ -4176,6 +4223,8 @@ docList.addEventListener('click', e => {
/* `newDoc()` zeichnet das Menü selbst neu es geht direkt ins Umbenennen
(D51), und ein zweiter Durchlauf baute das Eingabefeld nur noch einmal auf. */
document.getElementById('docNew').addEventListener('click', e => { e.stopPropagation(); newDoc(); });
document.getElementById('docOpenFile').addEventListener('click', e => { e.stopPropagation(); openLocalFile(); });
document.getElementById('docSaveFile').addEventListener('click', e => { e.stopPropagation(); saveLocalFile(); });
document.getElementById('docRename').addEventListener('click', e => { e.stopPropagation(); renameDoc(); });
document.getElementById('docDelete').addEventListener('click', e => { e.stopPropagation(); deleteDoc(); });
document.getElementById('docRestore').addEventListener('click', e => { e.stopPropagation(); restoreDoc(); });