feat: Office-Preview im neuen Tab + DOCX/XLSX/Text bearbeitbar
Preview-System komplett ueberarbeitet: - Neuer Tab: Doppelklick oder Auge-Icon oeffnet Vorschau im neuen Tab - Dedizierte PreviewView mit Toolbar (Zurueck, Bearbeiten, Speichern, Download) - Token wird als Query-Parameter an Preview/Download-URLs angehaengt (kein 404 mehr) Unterstuetzte Formate: - PDF: Inline-Anzeige im iFrame - Bilder: Zentrierte Anzeige mit Schatten - DOCX: HTML-Darstellung mit Formatierung (Headings, Bold, Italic, Tabellen) - XLSX: Tabellen-Ansicht mit Sheet-Tabs - PPTX: Folien-Navigation (vor/zurueck) - Text/Code: Monospace mit Syntax Bearbeitung (neu!): - DOCX: ContentEditable-Editor, Bold/Italic/Headings bleiben erhalten, Speichern schreibt zurueck als .docx (python-docx) - XLSX: Direkt in der Tabelle bearbeiten (Zellen anklicken), Speichern schreibt zurueck als .xlsx (openpyxl) - Text/Code: Textarea-Editor, Speichern als UTF-8 Backend: POST /files/<id>/save mit type-spezifischer Konvertierung - html -> DOCX (Headings, Bold/Italic/Underline erhalten) - spreadsheet -> XLSX (Zahlen werden automatisch konvertiert) - text -> direkt als Datei Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,7 @@
|
||||
import io
|
||||
import os
|
||||
import hashlib
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from flask import request, jsonify, current_app, send_file
|
||||
@@ -168,3 +171,146 @@ def _convert_pptx(filepath):
|
||||
'html': '\n'.join(content_parts) if content_parts else '<p>(Leere Folie)</p>',
|
||||
})
|
||||
return slides
|
||||
|
||||
|
||||
# ========== Save (write back edited documents) ==========
|
||||
|
||||
@api_bp.route('/files/<int:file_id>/save', methods=['POST'])
|
||||
@token_required
|
||||
def save_file(file_id):
|
||||
"""Save edited content back to the original file format."""
|
||||
user = request.current_user
|
||||
f, err = _get_file_or_403(file_id, user, 'write')
|
||||
if err:
|
||||
return err
|
||||
|
||||
if f.is_folder:
|
||||
return jsonify({'error': 'Ordner koennen nicht gespeichert werden'}), 400
|
||||
|
||||
data = request.get_json()
|
||||
save_type = data.get('type', '')
|
||||
filepath = Path(current_app.config['UPLOAD_PATH']) / str(f.owner_id) / f.storage_path
|
||||
|
||||
try:
|
||||
if save_type == 'html' and f.name.endswith('.docx'):
|
||||
_save_html_to_docx(filepath, data.get('content', ''))
|
||||
elif save_type == 'spreadsheet' and (f.name.endswith('.xlsx') or f.name.endswith('.xls')):
|
||||
_save_sheets_to_xlsx(filepath, data.get('sheets', []))
|
||||
elif save_type == 'text':
|
||||
filepath.write_text(data.get('content', ''), encoding='utf-8')
|
||||
else:
|
||||
return jsonify({'error': f'Speichern fuer diesen Typ nicht unterstuetzt'}), 400
|
||||
|
||||
# Update file metadata
|
||||
f.size = os.path.getsize(str(filepath))
|
||||
h = hashlib.sha256()
|
||||
with open(str(filepath), 'rb') as fh:
|
||||
for chunk in iter(lambda: fh.read(8192), b''):
|
||||
h.update(chunk)
|
||||
f.checksum = h.hexdigest()
|
||||
f.updated_at = datetime.now(timezone.utc)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({'message': 'Gespeichert', 'size': f.size}), 200
|
||||
except Exception as e:
|
||||
return jsonify({'error': f'Speichern fehlgeschlagen: {str(e)}'}), 500
|
||||
|
||||
|
||||
def _save_html_to_docx(filepath, html_content):
|
||||
"""Convert HTML content back to DOCX."""
|
||||
from docx import Document
|
||||
from docx.shared import Pt
|
||||
import re
|
||||
|
||||
doc = Document()
|
||||
|
||||
# Simple HTML to DOCX conversion
|
||||
# Strip tags and convert basic elements
|
||||
html = html_content.replace('\r\n', '\n').replace('\r', '\n')
|
||||
|
||||
# Process block elements
|
||||
blocks = re.split(r'<(?:p|h[1-3]|br\s*/?)(?:\s[^>]*)?>|</(?:p|h[1-3])>', html)
|
||||
|
||||
# Find tag types
|
||||
tags = re.findall(r'<(/?(?:p|h[1-3]|br\s*/?)(?:\s[^>]*)?)>', html)
|
||||
|
||||
current_tag = 'p'
|
||||
for i, block in enumerate(blocks):
|
||||
text = re.sub(r'<[^>]+>', '', block).strip()
|
||||
if not text:
|
||||
if i < len(tags):
|
||||
tag = tags[i] if i < len(tags) else ''
|
||||
if tag.startswith('h1'):
|
||||
current_tag = 'h1'
|
||||
elif tag.startswith('h2'):
|
||||
current_tag = 'h2'
|
||||
elif tag.startswith('h3'):
|
||||
current_tag = 'h3'
|
||||
else:
|
||||
current_tag = 'p'
|
||||
continue
|
||||
|
||||
if current_tag == 'h1':
|
||||
doc.add_heading(text, level=1)
|
||||
elif current_tag == 'h2':
|
||||
doc.add_heading(text, level=2)
|
||||
elif current_tag == 'h3':
|
||||
doc.add_heading(text, level=3)
|
||||
else:
|
||||
# Check for bold/italic in remaining inline tags
|
||||
para = doc.add_paragraph()
|
||||
# Simple inline parsing
|
||||
parts = re.split(r'(</?(?:strong|b|em|i|u)>)', block)
|
||||
bold = False
|
||||
italic = False
|
||||
underline = False
|
||||
for part in parts:
|
||||
if part in ('<strong>', '<b>'):
|
||||
bold = True
|
||||
elif part in ('</strong>', '</b>'):
|
||||
bold = False
|
||||
elif part in ('<em>', '<i>'):
|
||||
italic = True
|
||||
elif part in ('</em>', '</i>'):
|
||||
italic = False
|
||||
elif part in ('<u>',):
|
||||
underline = True
|
||||
elif part in ('</u>',):
|
||||
underline = False
|
||||
else:
|
||||
clean = re.sub(r'<[^>]+>', '', part)
|
||||
if clean:
|
||||
run = para.add_run(clean)
|
||||
run.bold = bold
|
||||
run.italic = italic
|
||||
run.underline = underline
|
||||
|
||||
current_tag = 'p'
|
||||
|
||||
doc.save(str(filepath))
|
||||
|
||||
|
||||
def _save_sheets_to_xlsx(filepath, sheets_data):
|
||||
"""Save spreadsheet data back to XLSX."""
|
||||
from openpyxl import Workbook
|
||||
|
||||
wb = Workbook()
|
||||
# Remove default sheet
|
||||
wb.remove(wb.active)
|
||||
|
||||
for sheet_data in sheets_data:
|
||||
ws = wb.create_sheet(title=sheet_data.get('name', 'Sheet'))
|
||||
for ri, row in enumerate(sheet_data.get('rows', []), 1):
|
||||
for ci, cell_value in enumerate(row, 1):
|
||||
val = cell_value
|
||||
# Try to convert to number
|
||||
try:
|
||||
if '.' in str(val):
|
||||
val = float(val)
|
||||
else:
|
||||
val = int(val)
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
ws.cell(row=ri, column=ci, value=val if val != '' else None)
|
||||
|
||||
wb.save(str(filepath))
|
||||
|
||||
Reference in New Issue
Block a user