part of '../document_viewer.dart'; class _SvgViewer extends StatelessWidget { final Uint8List bytes; final PyramidTheme pt; const _SvgViewer({required this.bytes, required this.pt}); @override Widget build(BuildContext context) { return Center( child: InteractiveViewer( minScale: 0.5, maxScale: 10, child: Padding( padding: const EdgeInsets.all(24), child: SvgPicture.memory(bytes), ), ), ); } } class _MarkdownViewer extends StatelessWidget { final Uint8List bytes; final PyramidTheme pt; const _MarkdownViewer({required this.bytes, required this.pt}); @override Widget build(BuildContext context) { final String text; try { text = utf8.decode(bytes, allowMalformed: true); } catch (e) { return _ErrorView(message: e.toString(), pt: pt); } return Markdown( data: text, selectable: true, padding: const EdgeInsets.all(24), styleSheet: MarkdownStyleSheet( p: TextStyle(color: pt.fg, fontSize: 14, height: 1.65), h1: TextStyle(color: pt.fg, fontSize: 22, fontWeight: FontWeight.w700), h2: TextStyle(color: pt.fg, fontSize: 18, fontWeight: FontWeight.w600), h3: TextStyle(color: pt.fg, fontSize: 16, fontWeight: FontWeight.w600), h4: TextStyle(color: pt.fg, fontSize: 14, fontWeight: FontWeight.w600), code: TextStyle(color: pt.accent, fontFamily: 'monospace', fontSize: 13, backgroundColor: pt.bg2), codeblockDecoration: BoxDecoration( color: pt.bg2, borderRadius: BorderRadius.circular(pt.rSm), border: Border.all(color: pt.border), ), blockquoteDecoration: BoxDecoration( border: Border(left: BorderSide(color: pt.accent, width: 3)), color: pt.accentSoft, ), blockquotePadding: const EdgeInsets.fromLTRB(12, 8, 12, 8), a: TextStyle(color: pt.accent, decoration: TextDecoration.underline, decorationColor: pt.accent), strong: TextStyle(color: pt.fg, fontWeight: FontWeight.w600), em: TextStyle(color: pt.fg, fontStyle: FontStyle.italic), listBullet: TextStyle(color: pt.fgMuted), horizontalRuleDecoration: BoxDecoration(border: Border(top: BorderSide(color: pt.border))), ), onTapLink: (text, href, title) { if (href != null) launchUrl(Uri.parse(href), mode: LaunchMode.externalApplication); }, ); } } class _TextViewer extends StatelessWidget { final Uint8List bytes; final PyramidTheme pt; final bool mono; static const _maxBytes = 512 * 1024; const _TextViewer({required this.bytes, required this.pt, required this.mono}); @override Widget build(BuildContext context) { final bool truncated = bytes.length > _maxBytes; final slice = truncated ? bytes.sublist(0, _maxBytes) : bytes; final String text; try { text = utf8.decode(slice, allowMalformed: true); } catch (e) { return _ErrorView(message: e.toString(), pt: pt); } return Column( children: [ if (truncated) Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 6), color: pt.bg2, child: Text('Zeige erste ${_maxBytes ~/ 1024} KB', style: TextStyle(color: pt.fgDim, fontSize: 11)), ), Expanded( child: SingleChildScrollView( padding: const EdgeInsets.all(20), child: SelectableText( text, style: mono ? TextStyle(color: pt.fg, fontFamily: 'monospace', fontSize: 13, height: 1.5) : TextStyle(color: pt.fg, fontSize: 14, height: 1.65), ), ), ), ], ); } } class _OfficeTextViewer extends StatefulWidget { final Uint8List bytes; final String filename; final PyramidTheme pt; const _OfficeTextViewer({required this.bytes, required this.filename, required this.pt}); @override State<_OfficeTextViewer> createState() => _OfficeTextViewerState(); } class _OfficeTextViewerState extends State<_OfficeTextViewer> { String? _text; String? _error; @override void initState() { super.initState(); _extract(); } void _extract() { try { final archive = ZipDecoder().decodeBytes(widget.bytes); final ext = widget.filename.contains('.') ? widget.filename.split('.').last.toLowerCase() : ''; final xmlFiles = []; for (final f in archive.files) { if (!f.isFile) continue; final n = f.name.toLowerCase(); if (ext == 'docx' && n == 'word/document.xml') { xmlFiles.add(f); break; } if (ext == 'xlsx' && n.startsWith('xl/worksheets/') && n.endsWith('.xml')) xmlFiles.add(f); if (ext == 'pptx' && n.startsWith('ppt/slides/slide') && n.endsWith('.xml')) xmlFiles.add(f); // ODF formats (Writer/Impress/Calc) all use content.xml if ((ext == 'odt' || ext == 'odp' || ext == 'ods') && n == 'content.xml') { xmlFiles.add(f); break; } } final buf = StringBuffer(); for (final f in xmlFiles) { final xml = utf8.decode(f.content as List, allowMalformed: true); // Strip XML tags, collapse whitespace final stripped = xml .replaceAll(RegExp(r'<[^>]+>'), ' ') .replaceAll(RegExp(r'&'), '&') .replaceAll(RegExp(r'<'), '<') .replaceAll(RegExp(r'>'), '>') .replaceAll(RegExp(r' '), ' ') .replaceAll(RegExp(r'\s+'), ' ') .trim(); buf.writeln(stripped); buf.writeln(); } _text = buf.toString().trim().isEmpty ? '(Kein lesbarer Text gefunden)' : buf.toString().trim(); } catch (e) { _error = e.toString().split('\n').first; } } @override Widget build(BuildContext context) { final pt = widget.pt; if (_error != null) return _ErrorView(message: _error!, pt: pt); return Column( children: [ Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), color: pt.bg2, child: Row( children: [ Icon(Icons.info_outline_rounded, size: 14, color: pt.fgDim), const SizedBox(width: 8), Text('Textvorschau (Formatierung nicht verfügbar)', style: TextStyle(color: pt.fgDim, fontSize: 12)), ], ), ), Expanded( child: SingleChildScrollView( padding: const EdgeInsets.all(20), child: SelectableText( _text ?? '', style: TextStyle(color: pt.fg, fontSize: 14, height: 1.65), ), ), ), ], ); } }