import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:url_launcher/url_launcher.dart'; import '../data/models.dart'; import '../theme/reclaim_tokens.dart'; import 'reclaim_card.dart'; /// Renders the actual paragraphs of a norm right next to the /// evaluation. Solves the "I want to read the law in place" /// requirement so a reviewer never has to context-switch to a /// browser tab just to confirm what the score is about. /// /// Two side-buttons at the top: /// - "Im Browser öffnen" — pop the canonical source URL /// - "URL kopieren" — clipboard, useful for citations /// /// On macOS the browser-open uses the system `open` command — /// no extra package, no permission prompt. iOS/Linux/Windows /// branches would need `url_launcher` later; Phase 0 ships /// macOS first. class NormTextCard extends StatelessWidget { const NormTextCard({super.key, required this.norm}); final Norm norm; @override Widget build(BuildContext context) { final t = Theme.of(context).textTheme; final paragraphs = norm.paragraphs; return ReclaimCard( child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('Gesetzestext', style: t.headlineSmall), const SizedBox(height: 4), Text( '${norm.jurabk} ${norm.paragraph} — Stand ' '${_isoDate(norm.standDate)}', style: t.labelSmall?.copyWith( color: ReclaimColors.mute, ), ), ], ), ), IconButton( tooltip: 'URL kopieren', onPressed: () => Clipboard.setData(ClipboardData(text: norm.sourceUrl)), icon: const Icon(Icons.link), ), FilledButton.icon( onPressed: () => _openInBrowser(context, norm.sourceUrl), icon: const Icon(Icons.open_in_new), label: const Text('Im Browser öffnen'), ), ], ), const SizedBox(height: ReclaimSpace.lg), if (paragraphs.isEmpty) Text( 'Für diese Norm ist noch kein normalisierter Text ' 'eingebunden. Wenn Ch∆In mit `text.akoma-normalize@^0` ' 'gegen die Quell-URL läuft, erscheinen die Absätze hier.', style: t.bodyLarge?.copyWith(color: ReclaimColors.mute), ) else for (final p in paragraphs) Padding( padding: const EdgeInsets.only(bottom: ReclaimSpace.md), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ SizedBox( width: 64, child: Text( p.number, style: t.labelLarge?.copyWith( color: ReclaimColors.signal, fontFamily: ReclaimTypography.mono, ), ), ), Expanded( child: SelectableText( p.text, style: t.bodyLarge?.copyWith(height: 1.55), ), ), ], ), ), const SizedBox(height: ReclaimSpace.sm), SelectableText( norm.sourceUrl, style: t.labelSmall?.copyWith( color: ReclaimColors.mute, fontFamily: ReclaimTypography.mono, ), ), ], ), ); } String _isoDate(DateTime d) => '${d.year.toString().padLeft(4, "0")}-' '${d.month.toString().padLeft(2, "0")}-' '${d.day.toString().padLeft(2, "0")}'; Future _openInBrowser(BuildContext context, String url) async { final messenger = ScaffoldMessenger.of(context); final uri = Uri.tryParse(url); if (uri == null) { messenger.showSnackBar( SnackBar(content: Text('Ungültige URL: $url')), ); return; } try { final ok = await launchUrl(uri, mode: LaunchMode.externalApplication); if (!ok) { messenger.showSnackBar( const SnackBar(content: Text('Browser-Aufruf abgelehnt.')), ); } } catch (e) { messenger.showSnackBar( SnackBar(content: Text('Konnte Browser nicht öffnen: $e')), ); } } }