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>
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
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'&'), '&')
|
||||
.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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user