Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 9 additions & 18 deletions lib/core/data/models/report_request.dart
Original file line number Diff line number Diff line change
@@ -1,31 +1,22 @@
class ReportRequest {
final String contentId;
final String resourceId;
final String resourceType;
final String reason;
final String details;
final String name;
final String email;
final String url;
final List<String> violations;
final String description;

ReportRequest({
required this.contentId,
required this.resourceId,
this.resourceType = 'track',
required this.reason,
required this.details,
required this.name,
required this.email,
required this.url,
required this.violations,
required this.description,
});

Map<String, dynamic> toJson() {
return {
"contentId": contentId,
"resource_id": resourceId,
"resource_type": resourceType,
"reason": reason,
"details": details,
"name": name,
"email": email,
"url": url,
"violations": violations,
"description": description,
};
}
}
5 changes: 5 additions & 0 deletions lib/core/data/models/track_dto.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ class TrackDto {
data['artists'] as String? ??
data['user']?['display_name'] as String? ??
'',
artistUsername:
data['username'] as String? ??
data['artist_username'] as String? ??
data['user']?['username'] as String?,

artistPfp:
data['profile_picture'] as String? ??
Expand Down Expand Up @@ -101,6 +105,7 @@ class TrackDto {
'user_id': track.userId,
'title': track.title,
'artist': track.artist,
'artist_username': track.artistUsername,
'artist_pfp': track.artistPfp,
'artist_city': track.artistCity,
'artist_country': track.artistCountry,
Expand Down
4 changes: 4 additions & 0 deletions lib/core/domain/entities/track.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ class Track {
final String userId;
final String title;
final String artist;
final String? artistUsername;
final String? artistPfp;
final String? artistCity;
final String? artistCountry;
Expand Down Expand Up @@ -46,6 +47,7 @@ class Track {
required this.userId,
required this.title,
required this.artist,
this.artistUsername,
this.artistPfp,
this.artistCity,
this.artistCountry,
Expand Down Expand Up @@ -88,6 +90,7 @@ class Track {
String? userId,
String? title,
String? artist,
String? artistUsername,
String? artistPfp,
String? artistCity,
String? artistCountry,
Expand Down Expand Up @@ -126,6 +129,7 @@ class Track {
userId: userId ?? this.userId,
title: title ?? this.title,
artist: artist ?? this.artist,
artistUsername: artistUsername ?? this.artistUsername,
artistPfp: artistPfp ?? this.artistPfp,
artistCity: artistCity ?? this.artistCity,
artistCountry: artistCountry ?? this.artistCountry,
Expand Down
116 changes: 82 additions & 34 deletions lib/core/presentation/pages/report_page.dart
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import 'package:flutter/material.dart';
import 'package:dio/dio.dart';

import '../../theme/app_theme.dart';
import '../../data/models/report_request.dart';
import '../../data/models/report_repository.dart';
import '../../network/api_client.dart';

class ReportPage extends StatefulWidget {
final String reportedContentId;
final String resourceType;

const ReportPage({super.key, required this.reportedContentId});
const ReportPage({
super.key,
required this.reportedContentId,
this.resourceType = 'track',
});

@override
State<ReportPage> createState() => _ReportPageState();
Expand All @@ -23,27 +28,27 @@ class _ReportPageState extends State<ReportPage> {

final _detailsController = TextEditingController();
final _nameController = TextEditingController();
final _emailController = TextEditingController(
text: 'basseialaa33@gmail.com',
);
final _emailController = TextEditingController(text: '');
final _urlController = TextEditingController();
final _resourceTypeController = TextEditingController();
final _resourceIdController = TextEditingController();

late final ReportRepository _repository;

final List<String> _reasons = [
"It's hate speech",
"It's harassing or abusive content",
"It contains sexual content or nudity",
"It contains graphic violence",
"It promotes self-harm",
"It's infringement of intellectual property",
"It's spam or misleading content",
"It's private or confidential information",
"It's selling illegal goods",
"Something else",
"I just don't like it",
"Copyright",
"Inappropriate content",
"Spam",
"Impersonation",
];

final Map<String, String> _reasonMapping = {
"Copyright": "copyright",
"Inappropriate content": "inappropriate",
"Spam": "spam",
"Impersonation": "impersonation",
};

final Map<String, bool> _violations = {
'Audio': false,
'Title/Description': false,
Expand All @@ -57,11 +62,19 @@ class _ReportPageState extends State<ReportPage> {
void initState() {
super.initState();

_repository = ReportRepository(Dio());
_repository = ReportRepository(apiClient.dio);

// ✅ Auto-generate URL
_urlController.text =
'https://rythmify.com/tracks/${widget.reportedContentId}';
_resourceIdController.text = widget.reportedContentId;
_resourceTypeController.text = widget.resourceType;

// Adjust URL generation based on type
if (widget.resourceType == 'track') {
_urlController.text =
'https://rythmify.com/tracks/${widget.reportedContentId}';
} else if (widget.resourceType == 'user') {
_urlController.text =
'https://rythmify.com/profile/${widget.reportedContentId}';
}
}

@override
Expand All @@ -70,6 +83,8 @@ class _ReportPageState extends State<ReportPage> {
_nameController.dispose();
_emailController.dispose();
_urlController.dispose();
_resourceIdController.dispose();
_resourceTypeController.dispose();
super.dispose();
}

Expand All @@ -80,19 +95,13 @@ class _ReportPageState extends State<ReportPage> {
return;
}

final selectedViolations = _violations.entries
.where((e) => e.value)
.map((e) => e.key)
.toList();
final reasonSlug = _reasonMapping[_selectedReason!] ?? 'inappropriate';

final request = ReportRequest(
contentId: widget.reportedContentId,
reason: _selectedReason!,
details: _detailsController.text,
name: _nameController.text,
email: _emailController.text,
url: _urlController.text,
violations: selectedViolations,
resourceId: widget.reportedContentId,
resourceType: widget.resourceType,
reason: reasonSlug,
description: _detailsController.text,
);

setState(() => _isLoading = true);
Expand Down Expand Up @@ -121,6 +130,12 @@ class _ReportPageState extends State<ReportPage> {

@override
Widget build(BuildContext context) {
// Determine which reasons to show based on resource type
final filteredReasons = _reasons.where((r) {
if (widget.resourceType == 'user' && r == 'Copyright') return false;
return true;
}).toList();

final bool isFormValid =
_selectedReason != null && _isConsentChecked && !_isLoading;

Expand All @@ -144,6 +159,40 @@ class _ReportPageState extends State<ReportPage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/// RESOURCE PREVIEW
Text(
'Report Details',
style: AppTheme.titleMedium.copyWith(color: AppTheme.babyBlue),
),
const SizedBox(height: 12),
TextFormField(
controller: _resourceTypeController,
readOnly: true,
decoration: InputDecoration(
labelText: 'Resource Type',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
filled: true,
fillColor: Colors.white10,
),
),
const SizedBox(height: 16),
TextFormField(
controller: _resourceIdController,
readOnly: true,
decoration: InputDecoration(
labelText: 'Resource ID',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
),
filled: true,
fillColor: Colors.white10,
),
),

const SizedBox(height: 32),

/// TITLE
Text(
'Reason for Reporting',
Expand All @@ -152,9 +201,8 @@ class _ReportPageState extends State<ReportPage> {

const SizedBox(height: 12),

/// ✅ FIXED RADIO LIST (NO NEGATIVE SPACING)
Column(
children: _reasons.map((reason) {
children: filteredReasons.map((reason) {
return RadioListTile<String>(
title: Text(reason, style: AppTheme.bodyNormal),
value: reason,
Expand All @@ -165,7 +213,7 @@ class _ReportPageState extends State<ReportPage> {
setState(() => _selectedReason = value);
},
contentPadding: EdgeInsets.zero,
dense: true, // ✅ cleaner spacing
dense: true,
activeColor: AppTheme.primaryBrand,
);
}).toList(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,12 @@ class _CommentsScreenState extends ConsumerState<CommentsScreen> {
});

WidgetsBinding.instance.addPostFrameCallback((_) {
ref
.read(trackCommentsProvider(widget.track.id).notifier)
.setInitialTrack(widget.track);
final notifier = ref.read(
trackCommentsProvider(widget.track.id).notifier,
);
notifier.setInitialTrack(widget.track);
// Force a fresh fetch from the server every time the screen opens
notifier.fetchComments(refresh: true);
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,12 @@ class CommentActionBottomSheet extends ConsumerWidget {
}
}

void _reportUser(BuildContext context) {
void _reportComment(BuildContext context) {
Navigator.pop(context);
Navigator.of(context, rootNavigator: true).push(
MaterialPageRoute(
builder: (context) => ReportPage(reportedContentId: comment.id),
builder: (context) =>
ReportPage(reportedContentId: comment.userId, resourceType: 'user'),
),
);
}
Expand Down Expand Up @@ -159,8 +160,8 @@ class CommentActionBottomSheet extends ConsumerWidget {
_buildActionRow(
key: const Key('comment_action_report_inkwell'),
icon: Icons.flag_outlined,
label: 'Report user',
onTap: () => _reportUser(context),
label: 'Report comment',
onTap: () => _reportComment(context),
),

// Row 6 (Other): Block/Unblock
Expand Down
48 changes: 46 additions & 2 deletions lib/features/library/presentation/pages/downloads_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,40 @@ class _DownloadsPageState extends ConsumerState<DownloadsPage> {
);
}

Future<void> showDeleteConfirmation(Track track) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (context) => AlertDialog(
backgroundColor: AppTheme.surface,
title: const Text('Delete Download'),
content: Text(
'Do you really want to delete "${track.title}" from your downloads? It cannot be restored.',
style: const TextStyle(color: Colors.white70),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
child: const Text(
'Cancel',
style: TextStyle(color: Colors.white54),
),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
child: const Text(
'Delete',
style: TextStyle(color: Colors.redAccent),
),
),
],
),
);

if (confirmed == true) {
await ref.read(downloadsProvider.notifier).removeTrack(track.id);
}
}

return CustomScrollView(
key: const Key('downloads_scroll_view'),
controller: _scrollController,
Expand Down Expand Up @@ -214,12 +248,22 @@ class _DownloadsPageState extends ConsumerState<DownloadsPage> {
sliver: SliverList.builder(
itemCount: filtered.length,
itemBuilder: (context, index) {
final track = filteredTracks[index];
return TrackCard(
key: Key('downloads_item_${filteredTracks[index].id}_$index'),
track: filteredTracks[index],
key: Key('downloads_item_${track.id}_$index'),
track: track,
onTap: () => playAt(index),
showOptions: false,
showStats: false,
trailing: IconButton(
key: Key('downloads_delete_button_${track.id}'),
icon: const Icon(
Icons.delete_outline_rounded,
color: Colors.white54,
size: 22,
),
onPressed: () => showDeleteConfirmation(track),
),
);
},
),
Expand Down
Loading
Loading