daf4cf3dad
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>
202 lines
6.7 KiB
Dart
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'&'), '&')
|
|
.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),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|