nkbmusic/lib/features/settings/document_screen.dart
metatroncubeswdev fc989b4c77 Store release setup: app icon, launch screen, iOS config, Android signing, Codemagic
- App icon from the NK emblem (iOS set, Android adaptive icon, Play 512 icon)
- Dark launch screen on both platforms; app locked to portrait; iPhone-only
- Info.plist: display name, photo/camera reasons, export compliance
- Android release signing from key.properties or Codemagic CM_KEYSTORE_*
- codemagic.yaml: iOS to TestFlight, Android to Play internal testing
- Account deletion web page; fix trailing dot in email links
- docs/app-store-release.md: step-by-step store guide

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 00:06:31 -04:00

100 lines
3.5 KiB
Dart

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<String>(
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<Widget> _render(BuildContext context, String md) {
final text = Theme.of(context).textTheme;
final body = text.bodyLarge?.copyWith(color: NkColors.mutedOnPhoto, height: 1.55);
final out = <Widget>[];
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 = <InlineSpan>[];
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);
}
}