- 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>
199 lines
7.1 KiB
Dart
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'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|