Files
pyramid/lib/features/chat/document/document_viewer_text.dart
T
Bernd Steckmeister daf4cf3dad refactor: Gott-Datei document_viewer.dart in 5 Dateien aufgeteilt
Sechster Durchgang derselben part/part-of-Technik wie bei den 5 vorigen
Umbauten - reine Verschiebung, keine Logikänderung. document_viewer.dart
schrumpft von 1773 auf 20 Zeilen (nur noch Bibliothekskopf), 34
Top-Level-Bloecke (31 Klassen/Enums + 3 lose Funktionen) auf 5 thematische
Dateien unter lib/features/chat/document/ verteilt. Verifiziert per
automatisiertem Blockvergleich gegen das Original und flutter analyze
(weiterhin nur 1 bekannter Hinweis).

voice_channel.dart und app_shell.dart bewusst nicht angefasst - haengen an
der noch unentschiedenen Call-Fassade bzw. dem Bootstrap/Login-Pfad.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-03 12:59:03 +02:00

202 lines
6.7 KiB
Dart

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 = <ArchiveFile>[];
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<int>, allowMalformed: true);
// Strip XML tags, collapse whitespace
final stripped = xml
.replaceAll(RegExp(r'<[^>]+>'), ' ')
.replaceAll(RegExp(r'&amp;'), '&')
.replaceAll(RegExp(r'&lt;'), '<')
.replaceAll(RegExp(r'&gt;'), '>')
.replaceAll(RegExp(r'&nbsp;'), ' ')
.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),
),
),
),
],
);
}
}