import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:url_launcher/url_launcher.dart'; import '../../app/theme.dart'; /// Shows a bundled Markdown document (privacy policy, terms). Supports the /// small subset those files use: #/## headings, "- " bullets, paragraphs, /// **bold** and email addresses (tappable). class DocumentScreen extends StatelessWidget { const DocumentScreen({super.key, required this.title, required this.asset}); final String title; final String asset; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text(title)), body: FutureBuilder( future: rootBundle.loadString(asset), builder: (context, snap) { if (!snap.hasData) return const Center(child: CircularProgressIndicator()); return SelectionArea( child: ListView( padding: const EdgeInsets.fromLTRB(20, 4, 20, 40), children: _render(context, snap.data!), ), ); }, ), ); } List _render(BuildContext context, String md) { final text = Theme.of(context).textTheme; final body = text.bodyLarge?.copyWith(color: NkColors.mutedOnPhoto, height: 1.55); final out = []; for (final block in md.split(RegExp(r'\n\s*\n'))) { final lines = block.trim().split('\n'); if (lines.first.isEmpty) continue; if (lines.first.startsWith('# ')) { continue; // the app bar already shows the title } else if (lines.first.startsWith('## ')) { out.add(Padding( padding: const EdgeInsets.only(top: 18, bottom: 6), child: Text(lines.first.substring(3), style: text.titleLarge), )); } else if (lines.every((l) => l.startsWith('- '))) { for (final l in lines) { out.add(Padding( padding: const EdgeInsets.only(bottom: 6), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('• ', style: body?.copyWith(color: NkColors.sun)), Expanded(child: Text.rich(_inline(l.substring(2), body), style: body)), ], ), )); } out.add(const SizedBox(height: 6)); } else { out.add(Padding( padding: const EdgeInsets.only(bottom: 12), child: Text.rich(_inline(lines.join('\n'), body), style: body), )); } } return out; } /// **bold** and tappable email addresses. static TextSpan _inline(String s, TextStyle? base) { final spans = []; final pattern = RegExp(r'\*\*(.+?)\*\*|([\w.+-]+@[\w-]+(?:\.[\w-]+)+)'); var last = 0; for (final m in pattern.allMatches(s)) { if (m.start > last) spans.add(TextSpan(text: s.substring(last, m.start))); if (m.group(1) != null) { spans.add(TextSpan( text: m.group(1), style: const TextStyle(fontWeight: FontWeight.w700, color: NkColors.cream), )); } else { final email = m.group(2)!; spans.add(TextSpan( text: email, style: const TextStyle(color: NkColors.sun), recognizer: TapGestureRecognizer()..onTap = () => launchUrl(Uri(scheme: 'mailto', path: email)), )); } last = m.end; } if (last < s.length) spans.add(TextSpan(text: s.substring(last))); return TextSpan(children: spans, style: base); } }