nkbmusic/lib/features/settings/report_screen.dart
metatroncubeswdev ad8c4f1c6b App: speaker/Bluetooth picker, Settings & help, Privacy Policy, Terms, FAQ, song reports
- Now Playing shows where music plays and opens the phone's output switcher
- Settings hub: Wi-Fi-only downloads (default on), storage, sound, help, legal
- Privacy Policy and Terms bundled as Markdown (assets/legal), shown in-app
- 'Is this your song?' report form feeding takedown_requests (+ kind, user_id)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 12:00:16 -04:00

199 lines
7.1 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:supabase_flutter/supabase_flutter.dart';
import '../../app/theme.dart';
import '../../core/env.dart';
import '../../data/catalog_repository.dart';
import '../profile/profile_screen.dart';
enum _Kind {
claim('claim', 'This is my song', 'Credit me (or my family member) as the singer, writer or composer.'),
takedown('takedown', 'Please remove this song', 'I am the artist, a family member or the rights holder.'),
wrongInfo('wrong_info', 'The details are wrong', 'Title, singer, album or occasion needs correcting.'),
other('other', 'Something else', 'Sound problem, offensive content or anything else.');
const _Kind(this.id, this.title, this.subtitle);
final String id;
final String title;
final String subtitle;
}
/// "Is this your song?" / report a problem. Goes to the admin team's queue.
class ReportScreen extends ConsumerStatefulWidget {
const ReportScreen({super.key, this.trackId});
final String? trackId;
@override
ConsumerState<ReportScreen> createState() => _ReportScreenState();
}
class _ReportScreenState extends ConsumerState<ReportScreen> {
_Kind _kind = _Kind.claim;
final _name = TextEditingController();
final _contact = TextEditingController();
final _song = TextEditingController();
final _message = TextEditingController();
bool _sending = false;
bool _sent = false;
@override
void initState() {
super.initState();
if (!Env.hasSupabase) return;
_contact.text = Supabase.instance.client.auth.currentUser?.email ?? '';
ref.read(profileProvider.future).then((p) {
if (mounted && p?.name != null && _name.text.isEmpty) setState(() => _name.text = p!.name!);
}).ignore();
}
@override
void dispose() {
for (final c in [_name, _contact, _song, _message]) {
c.dispose();
}
super.dispose();
}
bool get _valid =>
_name.text.trim().isNotEmpty &&
_contact.text.trim().length >= 6 &&
_message.text.trim().length >= 5 &&
(widget.trackId != null || _song.text.trim().isNotEmpty);
Future<void> _send() async {
setState(() => _sending = true);
try {
await Supabase.instance.client.from('takedown_requests').insert({
'track_id': widget.trackId,
'kind': _kind.id,
'requester': _name.text.trim(),
'contact': _contact.text.trim(),
'reason': widget.trackId == null
? 'Song: ${_song.text.trim()}\n\n${_message.text.trim()}'
: _message.text.trim(),
});
setState(() => _sent = true);
} catch (_) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Could not send. Check your connection, or email support@nkbmusic.com.')),
);
}
} finally {
if (mounted) setState(() => _sending = false);
}
}
@override
Widget build(BuildContext context) {
final text = Theme.of(context).textTheme;
final catalog = ref.watch(catalogProvider).value;
final track = widget.trackId == null ? null : catalog?.trackById(widget.trackId!);
if (_sent) {
return Scaffold(
appBar: AppBar(),
body: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Icon(Icons.favorite_rounded, color: NkColors.sun, size: 56),
const SizedBox(height: 16),
Text('Thank you', textAlign: TextAlign.center, style: text.headlineMedium),
const SizedBox(height: 8),
const Text(
'We have received your message and will get back to you, usually within 7 days. '
'Genuine removal requests are acted on as soon as we have checked them.',
textAlign: TextAlign.center,
style: TextStyle(color: NkColors.muted, height: 1.5),
),
const SizedBox(height: 24),
FilledButton(onPressed: () => context.pop(), child: const Text('Done')),
],
),
),
);
}
return Scaffold(
appBar: AppBar(title: const Text('Is this your song?')),
body: ListView(
padding: const EdgeInsets.fromLTRB(20, 0, 20, 32),
children: [
if (track != null)
Text('About "${track.titleLatin}"', style: text.titleMedium?.copyWith(color: NkColors.sun)),
const SizedBox(height: 8),
const Text(
'We want every artist credited properly. Tell us what is going on and our team will review it.',
style: TextStyle(color: NkColors.muted, height: 1.5),
),
const SizedBox(height: 12),
RadioGroup<_Kind>(
groupValue: _kind,
onChanged: (k) => setState(() => _kind = k ?? _kind),
child: Column(
children: [
for (final k in _Kind.values)
RadioListTile<_Kind>(
value: k,
contentPadding: EdgeInsets.zero,
title: Text(k.title, style: const TextStyle(fontWeight: FontWeight.w700)),
subtitle: Text(k.subtitle, style: const TextStyle(color: NkColors.muted)),
),
],
),
),
const SizedBox(height: 8),
if (track == null) ...[
TextField(
controller: _song,
onChanged: (_) => setState(() {}),
decoration: const InputDecoration(labelText: 'Which song?', hintText: 'Song title and album'),
),
const SizedBox(height: 12),
],
TextField(
controller: _name,
textCapitalization: TextCapitalization.words,
onChanged: (_) => setState(() {}),
decoration: const InputDecoration(labelText: 'Your name'),
),
const SizedBox(height: 12),
TextField(
controller: _contact,
onChanged: (_) => setState(() {}),
decoration: const InputDecoration(
labelText: 'Email or WhatsApp number',
helperText: 'Only our team sees this, to reply to you.',
),
),
const SizedBox(height: 12),
TextField(
controller: _message,
minLines: 4,
maxLines: 8,
onChanged: (_) => setState(() {}),
decoration: const InputDecoration(
labelText: 'Tell us more',
hintText: 'For example: who sang it, who wrote it, the correct title, or why it should be removed.',
alignLabelWithHint: true,
),
),
const SizedBox(height: 20),
FilledButton(
onPressed: _valid && !_sending && Env.hasSupabase ? _send : null,
child: _sending
? const SizedBox.square(dimension: 22, child: CircularProgressIndicator(strokeWidth: 2.5))
: const Text('Send to the Nanga Kathai team'),
),
],
),
);
}
}