chain's catalog charset (naming.rs check_segment, capability-namespaces.yaml) allows only [a-z0-9-] per dot-segment — underscores are rejected by `chain validate`. Repack all 8 modules with the dash capability form (econ.skm-score, law.benefit-score, …) and align the store index, flows, app strings and docs to match. Bundles now use the .chain extension (content-addressed; legacy .fai would still install). - store/store.yaml: name: dash, wasm_url .chain, refreshed sha256 - store/SHA256.txt: 8 new .chain hashes - flows + app/lib + RUN.md + MACHBARKEITSSTUDIE.md: capability refs _ -> - - left untouched: text_akoma_normalize.wasm artifact, DB field skm_score, backlog module law.skm_score Signed-off-by: flemming-it <sf@flemming.it>
143 lines
4.8 KiB
Dart
143 lines
4.8 KiB
Dart
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<void> _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')),
|
|
);
|
|
}
|
|
}
|
|
}
|