Add release reporting feature

This commit is contained in:
DariusIII
2026-02-01 22:16:40 +01:00
parent 239266fa04
commit b60d8b29e3
18 changed files with 1261 additions and 6 deletions
@@ -0,0 +1,163 @@
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\BasePageController;
use App\Models\Release;
use App\Models\ReleaseReport;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\View\View;
class AdminReleaseReportController extends BasePageController
{
/**
* Display a listing of release reports.
*/
public function index(Request $request): View
{
$this->setAdminPrefs();
$status = $request->input('status', 'pending');
$reportsList = ReleaseReport::getReportsRange($status, 50);
$statusCounts = ReleaseReport::getCountByStatus();
$meta_title = $title = 'Release Reports';
return view('admin.release-reports.index', compact(
'reportsList',
'statusCounts',
'status',
'title',
'meta_title'
));
}
/**
* Update report status.
*/
public function updateStatus(Request $request, int $id): RedirectResponse
{
$request->validate([
'status' => 'required|in:pending,reviewed,resolved,dismissed',
]);
$report = ReleaseReport::findOrFail($id);
$report->update([
'status' => $request->input('status'),
'reviewed_by' => Auth::id(),
'reviewed_at' => now(),
]);
return redirect()->back()->with('success', 'Report status updated successfully.');
}
/**
* Delete the reported release and resolve the report.
*/
public function deleteRelease(int $id): RedirectResponse
{
$report = ReleaseReport::with('release')->findOrFail($id);
if ($report->release) {
$releaseName = $report->release->searchname;
$releaseId = $report->releases_id;
// Delete the release
Release::where('id', $releaseId)->delete();
// Update all reports for this release to resolved
ReleaseReport::where('releases_id', $releaseId)
->update([
'status' => 'resolved',
'reviewed_by' => Auth::id(),
'reviewed_at' => now(),
]);
return redirect()->back()->with('success', "Release '{$releaseName}' deleted and all related reports resolved.");
}
return redirect()->back()->with('error', 'Release not found or already deleted.');
}
/**
* Dismiss a report.
*/
public function dismiss(int $id): RedirectResponse
{
$report = ReleaseReport::findOrFail($id);
$report->update([
'status' => 'dismissed',
'reviewed_by' => Auth::id(),
'reviewed_at' => now(),
]);
return redirect()->back()->with('success', 'Report dismissed successfully.');
}
/**
* Bulk update report statuses.
*/
public function bulkAction(Request $request): RedirectResponse
{
$request->validate([
'action' => 'required|in:dismiss,resolve,reviewed,delete',
'report_ids' => 'required|array',
'report_ids.*' => 'integer',
]);
$action = $request->input('action');
$reportIds = $request->input('report_ids');
$count = 0;
foreach ($reportIds as $reportId) {
$report = ReleaseReport::with('release')->find($reportId);
if (! $report) {
continue;
}
if ($action === 'delete' && $report->release) {
$releaseId = $report->releases_id;
Release::where('id', $releaseId)->delete();
ReleaseReport::where('releases_id', $releaseId)
->update([
'status' => 'resolved',
'reviewed_by' => Auth::id(),
'reviewed_at' => now(),
]);
} elseif ($action === 'dismiss') {
$report->update([
'status' => 'dismissed',
'reviewed_by' => Auth::id(),
'reviewed_at' => now(),
]);
} elseif ($action === 'resolve') {
$report->update([
'status' => 'resolved',
'reviewed_by' => Auth::id(),
'reviewed_at' => now(),
]);
} elseif ($action === 'reviewed') {
$report->update([
'status' => 'reviewed',
'reviewed_by' => Auth::id(),
'reviewed_at' => now(),
]);
}
$count++;
}
$actionLabel = match ($action) {
'delete' => 'deleted',
'dismiss' => 'dismissed',
'resolve' => 'resolved',
'reviewed' => 'marked as reviewed',
default => 'processed',
};
return redirect()->back()->with('success', "{$count} report(s) {$actionLabel} successfully.");
}
}
@@ -9,6 +9,7 @@ use App\Models\ReleaseComment;
use App\Models\ReleaseFile;
use App\Models\ReleaseNfo;
use App\Models\ReleaseRegex;
use App\Models\ReleaseReport;
use App\Models\Settings;
use App\Models\UserDownload;
use App\Models\Video;
@@ -74,6 +75,11 @@ class DetailsController extends BasePageController
$comments = ReleaseComment::getComments($data['id']);
$similars = $this->releaseSearchService->searchSimilar($data['id'], $data['searchname'], $this->userdata->categoryexclusions);
$failed = DnzbFailure::getFailedCount($data['id']);
$reportData = ReleaseReport::where('releases_id', $data['id'])
->whereIn('status', ['pending', 'reviewed'])
->get();
$reportCount = $reportData->count();
$reportReasons = ReleaseReport::reasonKeysToLabels($reportData->pluck('reason')->unique()->implode(', '));
$downloadedBy = UserDownload::query()->with('user')->where('releases_id', $data['id'])->get(['users_id']);
$showInfo = '';
@@ -183,6 +189,8 @@ class DetailsController extends BasePageController
'similars' => $similars !== false ? $similars : [],
'privateprofiles' => config('nntmux_settings.private_profiles'),
'failed' => $failed,
'reportCount' => $reportCount,
'reportReasons' => $reportReasons,
'regex' => $releaseRegex,
'downloadedby' => $downloadedBy,
'meta_title' => 'View NZB',
@@ -0,0 +1,83 @@
<?php
namespace App\Http\Controllers;
use App\Models\Release;
use App\Models\ReleaseReport;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class ReleaseReportController extends BasePageController
{
/**
* Submit a report for a release.
*/
public function store(Request $request): JsonResponse
{
$request->validate([
'release_id' => 'required|integer',
'reason' => 'required|string|max:255',
'description' => 'nullable|string|max:1000',
]);
$releaseId = $request->input('release_id');
$userId = Auth::id();
// Check if release exists
$release = Release::find($releaseId);
if (! $release) {
return response()->json([
'success' => false,
'message' => 'Release not found.',
], 404);
}
// Check if user already reported this release
if (ReleaseReport::hasUserReported($releaseId, $userId)) {
return response()->json([
'success' => false,
'message' => 'You have already reported this release.',
], 409);
}
// Create the report
ReleaseReport::create([
'releases_id' => $releaseId,
'users_id' => $userId,
'reason' => $request->input('reason'),
'description' => $request->input('description'),
'status' => 'pending',
]);
return response()->json([
'success' => true,
'message' => 'Report submitted successfully. Thank you for helping improve our content.',
]);
}
/**
* Get report reasons for the form.
*/
public function getReasons(): JsonResponse
{
return response()->json([
'reasons' => ReleaseReport::REASONS,
]);
}
/**
* Check if user has already reported a release.
*/
public function checkReported(Request $request): JsonResponse
{
$releaseId = $request->input('release_id');
$userId = Auth::id();
$hasReported = ReleaseReport::hasUserReported($releaseId, $userId);
return response()->json([
'has_reported' => $hasReported,
]);
}
}
+5
View File
@@ -76,6 +76,11 @@ class Release extends Model
return $this->hasMany(ReleaseComment::class, 'releases_id');
}
public function reports(): HasMany
{
return $this->hasMany(ReleaseReport::class, 'releases_id');
}
public function releaseGroup(): HasMany
{
return $this->hasMany(ReleasesGroups::class, 'releases_id');
+150
View File
@@ -0,0 +1,150 @@
<?php
namespace App\Models;
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class ReleaseReport extends Model
{
/**
* The table associated with the model.
*/
protected $table = 'release_reports';
/**
* The attributes that are mass assignable.
*/
protected $fillable = [
'releases_id',
'users_id',
'reason',
'description',
'status',
'reviewed_by',
'reviewed_at',
];
/**
* The attributes that should be cast.
*/
protected $casts = [
'reviewed_at' => 'datetime',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
/**
* Predefined report reasons
*/
public const REASONS = [
'duplicate' => 'Duplicate Release',
'fake' => 'Fake/Malicious Content',
'password' => 'Password Protected',
'incomplete' => 'Incomplete/Corrupted',
'wrong_category' => 'Wrong Category',
'spam' => 'Spam/Advertisement',
'other' => 'Other',
];
/**
* Convert a comma-separated list of reason keys to their labels.
*/
public static function reasonKeysToLabels(?string $reasonKeys): string
{
if (empty($reasonKeys)) {
return 'Unknown';
}
$keys = array_map('trim', explode(',', $reasonKeys));
$labels = [];
foreach ($keys as $key) {
$labels[] = self::REASONS[$key] ?? ucfirst($key);
}
return implode(', ', array_unique($labels));
}
/**
* Get the release that was reported.
*/
public function release(): BelongsTo
{
return $this->belongsTo(Release::class, 'releases_id');
}
/**
* Get the user who reported the release.
*/
public function user(): BelongsTo
{
return $this->belongsTo(User::class, 'users_id');
}
/**
* Get the admin who reviewed the report.
*/
public function reviewer(): BelongsTo
{
return $this->belongsTo(User::class, 'reviewed_by');
}
/**
* Get a paginated list of reports with optional filters.
*/
public static function getReportsRange(
?string $status = null,
int $perPage = 50
): LengthAwarePaginator {
$query = self::query()
->with(['release', 'user', 'reviewer'])
->orderBy('created_at', 'desc');
if ($status !== null && $status !== 'all') {
$query->where('status', $status);
}
return $query->paginate($perPage);
}
/**
* Get report count by status.
*/
public static function getCountByStatus(): array
{
$counts = self::query()
->selectRaw('status, COUNT(*) as count')
->groupBy('status')
->pluck('count', 'status')
->toArray();
return [
'pending' => $counts['pending'] ?? 0,
'reviewed' => $counts['reviewed'] ?? 0,
'resolved' => $counts['resolved'] ?? 0,
'dismissed' => $counts['dismissed'] ?? 0,
'total' => array_sum($counts),
];
}
/**
* Check if a user has already reported a specific release.
*/
public static function hasUserReported(int $releaseId, int $userId): bool
{
return self::query()
->where('releases_id', $releaseId)
->where('users_id', $userId)
->exists();
}
/**
* Get the human-readable reason label.
*/
public function getReasonLabelAttribute(): string
{
return self::REASONS[$this->reason] ?? $this->reason;
}
}
+3
View File
@@ -77,11 +77,14 @@ class MovieBrowseService
."GROUP_CONCAT(r.comments ORDER BY r.postdate DESC SEPARATOR ',' ) AS grp_release_comments, "
."GROUP_CONCAT(r.grabs ORDER BY r.postdate DESC SEPARATOR ',' ) AS grp_release_grabs, "
."GROUP_CONCAT(df.failed ORDER BY r.postdate DESC SEPARATOR ',' ) AS grp_release_failed, "
."GROUP_CONCAT(rr.report_count ORDER BY r.postdate DESC SEPARATOR ',' ) AS grp_release_reports, "
."GROUP_CONCAT(rr.report_reasons ORDER BY r.postdate DESC SEPARATOR '|' ) AS grp_release_report_reasons, "
."GROUP_CONCAT(cp.title, ' > ', c.title ORDER BY r.postdate DESC SEPARATOR ',' ) AS grp_release_catname, "
.'m.*, g.name AS group_name, rn.releases_id AS nfoid FROM releases r '
.'LEFT OUTER JOIN usenet_groups g ON g.id = r.groups_id '
.'LEFT OUTER JOIN release_nfos rn ON rn.releases_id = r.id '
.'LEFT OUTER JOIN dnzb_failures df ON df.release_id = r.id '
.'LEFT OUTER JOIN (SELECT releases_id, COUNT(*) AS report_count, GROUP_CONCAT(DISTINCT reason SEPARATOR \', \') AS report_reasons FROM release_reports WHERE status IN (\'pending\', \'reviewed\') GROUP BY releases_id) rr ON rr.releases_id = r.id '
.'LEFT OUTER JOIN categories c ON c.id = r.categories_id '
.'LEFT OUTER JOIN root_categories cp ON cp.id = c.root_categories_id '
.'INNER JOIN movieinfo m ON m.imdbid = r.imdbid '
@@ -63,6 +63,8 @@ class ReleaseBrowseService
CONCAT(cp.title, ' > ', c.title) AS category_name,
CONCAT(cp.id, ',', c.id) AS category_ids,
df.failed AS failed_count,
rr.report_count AS report_count,
rr.report_reasons AS report_reasons,
rn.releases_id AS nfoid,
re.releases_id AS reid,
v.tvdb, v.trakt, v.tvrage, v.tvmaze, v.imdb, v.tmdb,
@@ -85,6 +87,7 @@ class ReleaseBrowseService
LEFT OUTER JOIN video_data re ON re.releases_id = r.id
LEFT OUTER JOIN release_nfos rn ON rn.releases_id = r.id
LEFT OUTER JOIN dnzb_failures df ON df.release_id = r.id
LEFT OUTER JOIN (SELECT releases_id, COUNT(*) AS report_count, GROUP_CONCAT(DISTINCT reason SEPARATOR ', ') AS report_reasons FROM release_reports WHERE status IN ('pending', 'reviewed') GROUP BY releases_id) rr ON rr.releases_id = r.id
GROUP BY r.id
ORDER BY %8\$s %9\$s",
$this->showPasswords(),
@@ -1121,6 +1121,8 @@ class ReleaseSearchService
cp.title AS parent_category, c.title AS sub_category,
CONCAT(cp.title, ' > ', c.title) AS category_name,
df.failed AS failed_count,
rr.report_count AS report_count,
rr.report_reasons AS report_reasons,
g.name AS group_name,
rn.releases_id AS nfoid,
re.releases_id AS reid,
@@ -1136,6 +1138,7 @@ class ReleaseSearchService
LEFT JOIN categories c ON c.id = r.categories_id
LEFT JOIN root_categories cp ON cp.id = c.root_categories_id
LEFT OUTER JOIN dnzb_failures df ON df.release_id = r.id
LEFT OUTER JOIN (SELECT releases_id, COUNT(*) AS report_count, GROUP_CONCAT(DISTINCT reason SEPARATOR ', ') AS report_reasons FROM release_reports WHERE status IN ('pending', 'reviewed') GROUP BY releases_id) rr ON rr.releases_id = r.id
%s",
$whereSql
);
@@ -0,0 +1,41 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('release_reports', function (Blueprint $table) {
$table->id();
$table->unsignedInteger('releases_id');
$table->unsignedInteger('users_id');
$table->string('reason', 255);
$table->text('description')->nullable();
$table->enum('status', ['pending', 'reviewed', 'resolved', 'dismissed'])->default('pending');
$table->unsignedInteger('reviewed_by')->nullable();
$table->timestamp('reviewed_at')->nullable();
$table->timestamps();
$table->foreign('releases_id')->references('id')->on('releases')->onDelete('cascade');
$table->foreign('users_id')->references('id')->on('users')->onDelete('cascade');
$table->foreign('reviewed_by')->references('id')->on('users')->onDelete('set null');
$table->index(['releases_id', 'status']);
$table->index(['status', 'created_at']);
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('release_reports');
}
};
+290
View File
@@ -5727,3 +5727,293 @@ function initSortDropdowns() {
// Initialize sort dropdowns on DOMContentLoaded
document.addEventListener('DOMContentLoaded', initSortDropdowns);
// =============================================================================
// RELEASE REPORT FUNCTIONALITY
// =============================================================================
/**
* Initialize release report buttons and modals
* CSP-safe implementation for reporting bad releases
*/
function initReleaseReportButtons() {
// Find all report button containers
var containers = document.querySelectorAll('.report-button-container');
containers.forEach(function(container) {
var trigger = container.querySelector('.report-trigger');
var modal = container.querySelector('.report-modal');
if (!trigger || !modal) return;
// Skip if already initialized
if (trigger.hasAttribute('data-report-initialized')) return;
trigger.setAttribute('data-report-initialized', 'true');
var releaseId = trigger.getAttribute('data-report-release-id');
var form = modal.querySelector('.report-form');
var closeButtons = modal.querySelectorAll('.report-modal-close');
var backdrop = modal.querySelector('.report-modal-backdrop');
var reasonSelect = modal.querySelector('.report-reason');
var descriptionTextarea = modal.querySelector('.report-description');
var charCount = modal.querySelector('.report-char-count');
var submitButton = modal.querySelector('.report-submit');
var errorDiv = modal.querySelector('.report-error');
var successDiv = modal.querySelector('.report-success');
var reportLabel = trigger.querySelector('.report-label');
var flagIcon = trigger.querySelector('i');
var hasReported = false;
var isSubmitting = false;
// Open modal
trigger.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
if (hasReported) return;
modal.classList.remove('hidden');
// Reset form state
if (errorDiv) errorDiv.classList.add('hidden');
if (successDiv) successDiv.classList.add('hidden');
if (reasonSelect) reasonSelect.value = '';
if (descriptionTextarea) descriptionTextarea.value = '';
if (charCount) charCount.textContent = '0/1000 characters';
if (submitButton) submitButton.disabled = true;
});
// Close modal function
function closeModal() {
modal.classList.add('hidden');
if (hasReported) {
trigger.disabled = true;
trigger.classList.add('opacity-50', 'cursor-not-allowed');
if (flagIcon) flagIcon.classList.add('text-red-500');
if (reportLabel) reportLabel.textContent = 'Reported';
}
}
// Close button handlers
closeButtons.forEach(function(closeBtn) {
closeBtn.addEventListener('click', function(e) {
e.preventDefault();
closeModal();
});
});
// Backdrop click to close
if (backdrop) {
backdrop.addEventListener('click', function() {
closeModal();
});
}
// Character count for description
if (descriptionTextarea && charCount) {
descriptionTextarea.addEventListener('input', function() {
charCount.textContent = this.value.length + '/1000 characters';
});
}
// Enable/disable submit based on reason selection
if (reasonSelect && submitButton) {
reasonSelect.addEventListener('change', function() {
submitButton.disabled = !this.value;
});
}
// Submit button click handler
if (submitButton) {
submitButton.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
if (isSubmitting || !reasonSelect || !reasonSelect.value) return;
isSubmitting = true;
submitButton.disabled = true;
submitButton.innerHTML = '<i class="fas fa-spinner fa-spin mr-2"></i> Submitting...';
if (errorDiv) errorDiv.classList.add('hidden');
if (successDiv) successDiv.classList.add('hidden');
var csrfToken = document.querySelector('meta[name="csrf-token"]');
var csrfValue = csrfToken ? csrfToken.getAttribute('content') : '';
var requestBody = {
release_id: releaseId,
reason: reasonSelect.value,
description: descriptionTextarea ? descriptionTextarea.value : ''
};
fetch('/release-report', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfValue,
'Accept': 'application/json'
},
body: JSON.stringify(requestBody)
})
.then(function(response) {
return response.json().then(function(data) {
return { ok: response.ok, data: data };
});
})
.then(function(result) {
if (result.ok && result.data.success) {
if (successDiv) {
successDiv.querySelector('p').textContent = result.data.message;
successDiv.classList.remove('hidden');
}
hasReported = true;
setTimeout(closeModal, 2000);
} else {
if (errorDiv) {
errorDiv.querySelector('p').textContent = result.data.message || 'An error occurred. Please try again.';
errorDiv.classList.remove('hidden');
}
}
})
.catch(function(error) {
if (errorDiv) {
errorDiv.querySelector('p').textContent = 'Network error. Please try again.';
errorDiv.classList.remove('hidden');
}
})
.finally(function() {
isSubmitting = false;
if (submitButton) {
submitButton.disabled = !reasonSelect.value;
submitButton.innerHTML = 'Submit Report';
}
});
});
}
});
}
// Initialize report buttons on DOMContentLoaded
document.addEventListener('DOMContentLoaded', initReleaseReportButtons);
// Re-initialize on dynamic content load (for AJAX-loaded content)
document.addEventListener('contentLoaded', initReleaseReportButtons);
// =============================================================================
// ADMIN RELEASE REPORTS PAGE FUNCTIONALITY
// =============================================================================
/**
* Initialize admin release reports page functionality
* Handles description modal, select all, and bulk actions
*/
function initAdminReleaseReports() {
// Report Description Modal
var descModal = document.getElementById('reportDescriptionModal');
var descButtons = document.querySelectorAll('.report-description-btn');
var descContent = document.getElementById('reportDescContent');
var descReason = document.getElementById('reportDescReason');
var descReporter = document.getElementById('reportDescReporter');
var descCloseButtons = document.querySelectorAll('.report-desc-modal-close');
var descBackdrop = document.querySelector('.report-desc-modal-backdrop');
// Open description modal
descButtons.forEach(function(btn) {
if (btn.hasAttribute('data-desc-initialized')) return;
btn.setAttribute('data-desc-initialized', 'true');
btn.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
var description = this.getAttribute('data-description');
var reason = this.getAttribute('data-reason');
var reporter = this.getAttribute('data-reporter');
if (descContent) descContent.textContent = description || 'No additional details provided.';
if (descReason) descReason.textContent = reason || 'Unknown';
if (descReporter) descReporter.textContent = reporter || 'Unknown';
if (descModal) descModal.classList.remove('hidden');
});
});
// Close description modal
function closeDescModal() {
if (descModal) descModal.classList.add('hidden');
}
descCloseButtons.forEach(function(btn) {
btn.addEventListener('click', function(e) {
e.preventDefault();
closeDescModal();
});
});
if (descBackdrop) {
descBackdrop.addEventListener('click', closeDescModal);
}
// Select All functionality
var selectAll = document.getElementById('select-all');
var checkboxes = document.querySelectorAll('.report-checkbox');
if (selectAll && !selectAll.hasAttribute('data-selectall-initialized')) {
selectAll.setAttribute('data-selectall-initialized', 'true');
selectAll.addEventListener('change', function() {
checkboxes.forEach(function(checkbox) {
checkbox.checked = selectAll.checked;
});
});
}
// Update select all checkbox when individual checkboxes change
checkboxes.forEach(function(checkbox) {
if (checkbox.hasAttribute('data-checkbox-initialized')) return;
checkbox.setAttribute('data-checkbox-initialized', 'true');
checkbox.addEventListener('change', function() {
var allChecked = Array.from(checkboxes).every(function(cb) { return cb.checked; });
var someChecked = Array.from(checkboxes).some(function(cb) { return cb.checked; });
if (selectAll) {
selectAll.checked = allChecked;
selectAll.indeterminate = someChecked && !allChecked;
}
});
});
// Bulk action form validation
var bulkForm = document.getElementById('bulk-action-form');
if (bulkForm && !bulkForm.hasAttribute('data-bulk-initialized')) {
bulkForm.setAttribute('data-bulk-initialized', 'true');
bulkForm.addEventListener('submit', function(e) {
var actionSelect = this.querySelector('select[name="action"]');
var action = actionSelect ? actionSelect.value : '';
var checkedCount = document.querySelectorAll('.report-checkbox:checked').length;
if (!action) {
e.preventDefault();
alert('Please select an action.');
return;
}
if (checkedCount === 0) {
e.preventDefault();
alert('Please select at least one report.');
return;
}
if (action === 'delete') {
if (!confirm('Are you sure you want to DELETE ' + checkedCount + ' release(s)? This action cannot be undone.')) {
e.preventDefault();
}
}
});
}
}
// Initialize admin release reports on DOMContentLoaded
document.addEventListener('DOMContentLoaded', initAdminReleaseReports);
@@ -0,0 +1,323 @@
@extends('layouts.admin')
@section('content')
<div class="container mx-auto px-4 py-6">
<!-- Flash Messages -->
@if(session('success'))
<div class="mb-4 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg p-4 flex items-center justify-between">
<div class="flex items-center">
<i class="fas fa-check-circle text-green-600 dark:text-green-400 mr-3 text-xl"></i>
<p class="text-sm text-green-800 dark:text-green-200 font-medium">{{ session('success') }}</p>
</div>
<button onclick="this.parentElement.parentElement.remove()" class="text-green-600 dark:text-green-400 hover:text-green-800 dark:hover:text-green-200">
<i class="fas fa-times"></i>
</button>
</div>
@endif
@if(session('error'))
<div class="mb-4 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg p-4 flex items-center justify-between">
<div class="flex items-center">
<i class="fas fa-exclamation-circle text-red-600 dark:text-red-400 mr-3 text-xl"></i>
<p class="text-sm text-red-800 dark:text-red-200 font-medium">{{ session('error') }}</p>
</div>
<button onclick="this.parentElement.parentElement.remove()" class="text-red-600 dark:text-red-400 hover:text-red-800 dark:hover:text-red-200">
<i class="fas fa-times"></i>
</button>
</div>
@endif
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm">
<!-- Header -->
<div class="px-6 py-4 border-b border-gray-200 dark:border-gray-700">
<div class="flex flex-col md:flex-row md:justify-between md:items-center gap-4">
<div>
<h1 class="text-2xl font-semibold text-gray-800 dark:text-gray-200">
<i class="fas fa-flag mr-2 text-red-500"></i>{{ $title }}
</h1>
<p class="text-sm text-gray-600 dark:text-gray-400 mt-1">Manage release reports submitted by users</p>
</div>
<!-- Status Stats -->
<div class="flex flex-wrap gap-2">
<a href="{{ route('admin.release-reports', ['status' => 'pending']) }}"
class="px-3 py-1.5 rounded-full text-sm font-medium transition {{ $status === 'pending' ? 'bg-yellow-500 text-white' : 'bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200 hover:bg-yellow-200 dark:hover:bg-yellow-800' }}">
<i class="fas fa-clock mr-1"></i> Pending ({{ $statusCounts['pending'] }})
</a>
<a href="{{ route('admin.release-reports', ['status' => 'reviewed']) }}"
class="px-3 py-1.5 rounded-full text-sm font-medium transition {{ $status === 'reviewed' ? 'bg-blue-500 text-white' : 'bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200 hover:bg-blue-200 dark:hover:bg-blue-800' }}">
<i class="fas fa-eye mr-1"></i> Reviewed ({{ $statusCounts['reviewed'] }})
</a>
<a href="{{ route('admin.release-reports', ['status' => 'resolved']) }}"
class="px-3 py-1.5 rounded-full text-sm font-medium transition {{ $status === 'resolved' ? 'bg-green-500 text-white' : 'bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 hover:bg-green-200 dark:hover:bg-green-800' }}">
<i class="fas fa-check mr-1"></i> Resolved ({{ $statusCounts['resolved'] }})
</a>
<a href="{{ route('admin.release-reports', ['status' => 'dismissed']) }}"
class="px-3 py-1.5 rounded-full text-sm font-medium transition {{ $status === 'dismissed' ? 'bg-gray-500 text-white' : 'bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 hover:bg-gray-200 dark:hover:bg-gray-600' }}">
<i class="fas fa-ban mr-1"></i> Dismissed ({{ $statusCounts['dismissed'] }})
</a>
<a href="{{ route('admin.release-reports', ['status' => 'all']) }}"
class="px-3 py-1.5 rounded-full text-sm font-medium transition {{ $status === 'all' ? 'bg-purple-500 text-white' : 'bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200 hover:bg-purple-200 dark:hover:bg-purple-800' }}">
<i class="fas fa-list mr-1"></i> All ({{ $statusCounts['total'] }})
</a>
</div>
</div>
</div>
<!-- Reports Table -->
@if($reportsList->count() > 0)
<form id="bulk-action-form" method="POST" action="{{ route('admin.release-reports.bulk') }}">
@csrf
<div class="px-6 py-3 bg-gray-50 dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700 flex items-center gap-4">
<input type="checkbox" id="select-all" class="rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500 dark:bg-gray-700">
<select name="action" class="text-sm border-gray-300 dark:border-gray-600 rounded-lg focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-gray-200">
<option value="">Bulk Actions</option>
<option value="reviewed">Mark as Reviewed</option>
<option value="resolve">Mark as Resolved</option>
<option value="dismiss">Dismiss Selected</option>
<option value="delete">Delete Releases & Resolve</option>
</select>
<button type="submit" class="px-4 py-2 bg-blue-600 text-white text-sm rounded-lg hover:bg-blue-700 transition">
Apply
</button>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50 dark:bg-gray-900">
<tr>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-10"></th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">ID</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Release</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Reporter</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Reason</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Status</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Date</th>
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
@foreach($reportsList as $report)
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700">
<td class="px-4 py-4 whitespace-nowrap">
<input type="checkbox" name="report_ids[]" value="{{ $report->id }}" class="report-checkbox rounded border-gray-300 dark:border-gray-600 text-blue-600 focus:ring-blue-500 dark:bg-gray-700">
</td>
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-200 font-mono">
#{{ $report->id }}
</td>
<td class="px-4 py-4">
<div class="max-w-md">
@if($report->release)
<a href="{{ url('/details/' . $report->release->guid) }}"
target="_blank"
class="text-blue-600 dark:text-blue-400 hover:text-blue-900 dark:hover:text-blue-300 font-medium text-sm break-all">
{{ Str::limit($report->release->searchname, 80) }}
</a>
<div class="text-xs text-gray-500 dark:text-gray-400 mt-1">
ID: {{ $report->releases_id }} | Size: {{ number_format($report->release->size / 1073741824, 2) }} GB
</div>
@else
<span class="text-red-500 dark:text-red-400 text-sm">
<i class="fas fa-exclamation-triangle mr-1"></i> Release Deleted
</span>
<div class="text-xs text-gray-500 dark:text-gray-400 mt-1">
Original ID: {{ $report->releases_id }}
</div>
@endif
</div>
</td>
<td class="px-4 py-4 whitespace-nowrap">
<div class="text-sm text-gray-900 dark:text-gray-200">
@if($report->user)
{{ $report->user->username }}
@else
<span class="text-gray-500">Deleted User</span>
@endif
</div>
</td>
<td class="px-4 py-4">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-orange-100 dark:bg-orange-900 text-orange-800 dark:text-orange-200">
{{ $report->reason_label }}
</span>
@if($report->description)
<button type="button"
class="report-description-btn ml-2 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300"
data-description="{{ htmlspecialchars($report->description, ENT_QUOTES) }}"
data-reason="{{ $report->reason_label }}"
data-reporter="{{ $report->user ? $report->user->username : 'Unknown' }}"
title="View description">
<i class="fas fa-comment-dots"></i>
</button>
@endif
</td>
<td class="px-4 py-4 whitespace-nowrap">
@php
$statusColors = [
'pending' => 'bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200',
'reviewed' => 'bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200',
'resolved' => 'bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200',
'dismissed' => 'bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200',
];
$statusIcons = [
'pending' => 'fa-clock',
'reviewed' => 'fa-eye',
'resolved' => 'fa-check',
'dismissed' => 'fa-ban',
];
@endphp
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium {{ $statusColors[$report->status] ?? 'bg-gray-100 text-gray-800' }}">
<i class="fas {{ $statusIcons[$report->status] ?? 'fa-question' }} mr-1"></i>
{{ ucfirst($report->status) }}
</span>
@if($report->reviewer && $report->reviewed_at)
<div class="text-xs text-gray-500 dark:text-gray-400 mt-1">
by {{ $report->reviewer->username }} at {{ $report->reviewed_at->format('M d, Y H:i') }}
</div>
@endif
</td>
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-600 dark:text-gray-400">
{{ $report->created_at->format('M d, Y') }}
<div class="text-xs">{{ $report->created_at->format('H:i') }}</div>
</td>
<td class="px-4 py-4 whitespace-nowrap">
<div class="flex items-center gap-2 flex-wrap">
@if($report->release && in_array($report->status, ['pending', 'reviewed']))
<!-- Delete Release Button -->
<form method="POST" action="{{ route('admin.release-reports.delete-release', $report->id) }}" class="inline" onsubmit="return confirm('Are you sure you want to DELETE this release? This action cannot be undone.');">
@csrf
<button type="submit"
class="px-3 py-1.5 bg-red-600 text-white rounded-lg hover:bg-red-700 transition text-sm inline-flex items-center"
title="Delete Release">
<i class="fas fa-trash mr-1"></i> Delete
</button>
</form>
@endif
@if($report->status === 'pending')
<!-- Mark as Reviewed Button -->
<form method="POST" action="{{ route('admin.release-reports.update-status', $report->id) }}" class="inline">
@csrf
<input type="hidden" name="status" value="reviewed">
<button type="submit"
class="px-3 py-1.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition text-sm inline-flex items-center"
title="Mark as Reviewed">
<i class="fas fa-eye mr-1"></i> Reviewed
</button>
</form>
<!-- Dismiss Button -->
<form method="POST" action="{{ route('admin.release-reports.dismiss', $report->id) }}" class="inline">
@csrf
<button type="submit"
class="px-3 py-1.5 bg-gray-500 text-white rounded-lg hover:bg-gray-600 transition text-sm inline-flex items-center"
title="Dismiss Report">
<i class="fas fa-ban mr-1"></i> Dismiss
</button>
</form>
@elseif($report->status === 'reviewed')
<!-- Mark as Resolved Button -->
<form method="POST" action="{{ route('admin.release-reports.update-status', $report->id) }}" class="inline">
@csrf
<input type="hidden" name="status" value="resolved">
<button type="submit"
class="px-3 py-1.5 bg-green-600 text-white rounded-lg hover:bg-green-700 transition text-sm inline-flex items-center"
title="Mark as Resolved">
<i class="fas fa-check mr-1"></i> Resolve
</button>
</form>
<!-- Dismiss Button -->
<form method="POST" action="{{ route('admin.release-reports.dismiss', $report->id) }}" class="inline">
@csrf
<button type="submit"
class="px-3 py-1.5 bg-gray-500 text-white rounded-lg hover:bg-gray-600 transition text-sm inline-flex items-center"
title="Dismiss Report">
<i class="fas fa-ban mr-1"></i> Dismiss
</button>
</form>
@elseif(!$report->release)
<span class="text-xs text-gray-500 dark:text-gray-400">Release already deleted</span>
@else
<span class="text-xs text-gray-500 dark:text-gray-400">{{ ucfirst($report->status) }}</span>
@endif
@if($report->release)
<a href="{{ url('/details/' . $report->release->guid) }}"
target="_blank"
class="px-3 py-1.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition text-sm inline-flex items-center"
title="View Release">
<i class="fas fa-external-link-alt"></i>
</a>
@endif
</div>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</form>
<!-- Pagination -->
<div class="px-6 py-4 border-t border-gray-200 dark:border-gray-700">
{{ $reportsList->appends(['status' => $status])->links() }}
</div>
@else
<div class="px-6 py-12 text-center">
<i class="fas fa-flag text-gray-300 dark:text-gray-600 text-6xl mb-4"></i>
<h3 class="text-xl font-medium text-gray-700 dark:text-gray-300 mb-2">No Reports Found</h3>
<p class="text-gray-500 dark:text-gray-400">
@if($status !== 'all')
No {{ $status }} reports at this time.
<a href="{{ route('admin.release-reports', ['status' => 'all']) }}" class="text-blue-600 dark:text-blue-400 hover:underline">View all reports</a>
@else
No release reports have been submitted yet.
@endif
</p>
</div>
@endif
</div>
</div>
<!-- Report Description Modal -->
<div id="reportDescriptionModal" class="fixed inset-0 z-50 overflow-y-auto hidden">
<div class="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:p-0">
<!-- Backdrop -->
<div class="report-desc-modal-backdrop fixed inset-0 transition-opacity bg-gray-500 dark:bg-gray-900 bg-opacity-75 dark:bg-opacity-75"></div>
<!-- Modal Content -->
<div class="relative inline-block w-full max-w-lg p-6 my-8 overflow-hidden text-left align-middle transition-all transform bg-white dark:bg-gray-800 shadow-xl rounded-lg">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">
<i class="fas fa-comment-dots text-blue-500 mr-2"></i>Report Details
</h3>
<button type="button" class="report-desc-modal-close text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
<i class="fas fa-times"></i>
</button>
</div>
<div class="space-y-4">
<div>
<label class="block text-sm font-medium text-gray-500 dark:text-gray-400 mb-1">Reason</label>
<p id="reportDescReason" class="text-gray-900 dark:text-gray-100 font-medium"></p>
</div>
<div>
<label class="block text-sm font-medium text-gray-500 dark:text-gray-400 mb-1">Reported By</label>
<p id="reportDescReporter" class="text-gray-900 dark:text-gray-100"></p>
</div>
<div>
<label class="block text-sm font-medium text-gray-500 dark:text-gray-400 mb-1">Additional Details</label>
<div id="reportDescContent" class="p-4 bg-gray-50 dark:bg-gray-900 rounded-lg text-gray-700 dark:text-gray-300 whitespace-pre-wrap break-words max-h-64 overflow-y-auto"></div>
</div>
</div>
<div class="mt-6 flex justify-end">
<button type="button" class="report-desc-modal-close px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition">
Close
</button>
</div>
</div>
</div>
</div>
@endsection
+7
View File
@@ -141,6 +141,12 @@
<div class="flex-1">
<div class="flex items-center gap-2 flex-wrap">
<a href="{{ url('/details/' . $result->guid) }}" class="text-blue-600 dark:text-blue-400 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 dark:hover:text-blue-300 font-medium break-words break-all">{{ $result->searchname }}</a>
@if(!empty($result->report_count) && $result->report_count > 0)
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-orange-100 dark:bg-orange-900 text-orange-800 dark:text-orange-200"
title="Reported: {{ \App\Models\ReleaseReport::reasonKeysToLabels($result->report_reasons ?? '') }}">
<i class="fas fa-flag mr-1"></i> Reported ({{ $result->report_count }})
</span>
@endif
@if(!empty($result->failed_count) && $result->failed_count > 0)
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200"
title="{{ $result->failed_count }} user(s) reported download failure">
@@ -254,6 +260,7 @@
<i class="fa fa-film"></i>
</a>
@endif
<x-report-button :release-id="$result->id" variant="icon" />
</div>
</td>
</tr>
@@ -0,0 +1,113 @@
@props([
'releaseId' => null,
'releaseGuid' => null,
'size' => 'sm', // sm, md, lg
'variant' => 'icon', // icon, button, text
])
@auth
<div class="report-button-container inline-block">
<!-- Report Button -->
@if($variant === 'icon')
<button type="button"
data-report-release-id="{{ $releaseId }}"
class="report-trigger text-gray-400 dark:text-gray-500 hover:text-red-600 dark:hover:text-red-400 transition p-1"
title="Report this release">
<i class="fas fa-flag"></i>
</button>
@elseif($variant === 'button')
<button type="button"
data-report-release-id="{{ $releaseId }}"
class="report-trigger px-3 py-1.5 text-{{ $size }} bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded hover:bg-red-100 dark:hover:bg-red-900 hover:text-red-700 dark:hover:text-red-300 transition inline-flex items-center">
<i class="fas fa-flag mr-1.5"></i>
<span class="report-label">Report</span>
</button>
@else
<button type="button"
data-report-release-id="{{ $releaseId }}"
class="report-trigger text-gray-600 dark:text-gray-400 hover:text-red-600 dark:hover:text-red-400 text-{{ $size }} transition">
<i class="fas fa-flag mr-1"></i>
<span class="report-label">Report Issue</span>
</button>
@endif
<!-- Report Modal -->
<div class="report-modal fixed inset-0 z-50 overflow-y-auto hidden" data-release-id="{{ $releaseId }}">
<div class="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:p-0">
<!-- Backdrop -->
<div class="report-modal-backdrop fixed inset-0 transition-opacity bg-gray-500 dark:bg-gray-900 bg-opacity-75 dark:bg-opacity-75"></div>
<!-- Modal Content -->
<div class="relative inline-block w-full max-w-md p-6 my-8 overflow-hidden text-left align-middle transition-all transform bg-white dark:bg-gray-800 shadow-xl rounded-lg">
<div class="flex items-center justify-between mb-4">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">
<i class="fas fa-flag text-red-500 mr-2"></i>Report Release
</h3>
<button type="button" class="report-modal-close text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
<i class="fas fa-times"></i>
</button>
</div>
<form class="report-form" data-ajax-form="true">
<!-- Reason Select -->
<div class="mb-4">
<label for="report-reason-{{ $releaseId }}" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Reason for Report <span class="text-red-500">*</span>
</label>
<select name="reason"
id="report-reason-{{ $releaseId }}"
required
class="report-reason w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-red-500 focus:border-red-500 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100">
<option value="">Select a reason...</option>
<option value="duplicate">Duplicate Release</option>
<option value="fake">Fake/Malicious Content</option>
<option value="password">Password Protected</option>
<option value="incomplete">Incomplete/Corrupted</option>
<option value="wrong_category">Wrong Category</option>
<option value="spam">Spam/Advertisement</option>
<option value="other">Other</option>
</select>
</div>
<!-- Description Textarea -->
<div class="mb-4">
<label for="report-description-{{ $releaseId }}" class="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Additional Details (optional)
</label>
<textarea name="description"
id="report-description-{{ $releaseId }}"
rows="3"
maxlength="1000"
placeholder="Provide any additional details that might help us review this report..."
class="report-description w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-red-500 focus:border-red-500 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 resize-none"></textarea>
<p class="report-char-count text-xs text-gray-500 dark:text-gray-400 mt-1">0/1000 characters</p>
</div>
<!-- Error/Success Messages -->
<div class="report-error hidden mb-4 p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg">
<p class="text-sm text-red-600 dark:text-red-400"></p>
</div>
<div class="report-success hidden mb-4 p-3 bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 rounded-lg">
<p class="text-sm text-green-600 dark:text-green-400"></p>
</div>
<!-- Action Buttons -->
<div class="flex justify-end space-x-3">
<button type="button"
class="report-modal-close px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition">
Cancel
</button>
<button type="button"
disabled
class="report-submit px-4 py-2 text-sm font-medium text-white bg-red-600 rounded-lg hover:bg-red-700 transition disabled:opacity-50 disabled:cursor-not-allowed inline-flex items-center">
Submit Report
</button>
</div>
</form>
</div>
</div>
</div>
</div>
@endauth
+16 -6
View File
@@ -30,12 +30,21 @@
<div class="flex-1">
<div class="mb-3">
<h2 class="text-xl font-bold text-gray-800 dark:text-gray-200 mb-2 break-words break-all">{{ $release->searchname }}</h2>
@if(!empty($failed) && $failed > 0)
<div class="inline-flex items-center px-3 py-1.5 rounded-lg text-sm font-medium bg-red-100 text-red-800 border border-red-200">
<i class="fas fa-exclamation-triangle mr-2"></i>
<span>{{ $failed }} user{{ $failed > 1 ? 's' : '' }} reported download failure</span>
</div>
@endif
<div class="flex flex-wrap gap-2">
@if(!empty($reportCount) && $reportCount > 0)
<div class="inline-flex items-center px-3 py-1.5 rounded-lg text-sm font-medium bg-orange-100 dark:bg-orange-900 text-orange-800 dark:text-orange-200 border border-orange-200 dark:border-orange-800"
title="Reported: {{ $reportReasons ?? 'Unknown' }}">
<i class="fas fa-flag mr-2"></i>
<span>{{ $reportCount }} report{{ $reportCount > 1 ? 's' : '' }}: {{ $reportReasons ?? 'Unknown' }}</span>
</div>
@endif
@if(!empty($failed) && $failed > 0)
<div class="inline-flex items-center px-3 py-1.5 rounded-lg text-sm font-medium bg-red-100 text-red-800 border border-red-200">
<i class="fas fa-exclamation-triangle mr-2"></i>
<span>{{ $failed }} user{{ $failed > 1 ? 's' : '' }} reported download failure</span>
</div>
@endif
</div>
</div>
<div class="flex flex-wrap gap-2">
<a href="{{ url('/getnzb/' . $release->guid) }}" class="download-nzb px-4 py-2 bg-green-600 dark:bg-green-700 text-white rounded-lg hover:bg-green-700 dark:hover:bg-green-800 transition inline-flex items-center">
@@ -49,6 +58,7 @@
<i class="fas fa-file-alt mr-2"></i> View NFO
</button>
@endif
<x-report-button :release-id="$release->id" variant="button" />
@auth
@if(auth()->user()->hasRole('Admin') || auth()->user()->hasRole('Moderator'))
<a href="{{ route('admin.release-edit', ['id' => $release->guid]) }}" class="px-4 py-2 bg-orange-600 text-white rounded-lg hover:bg-orange-700 transition inline-flex items-center" title="Edit Release">
+23
View File
@@ -148,6 +148,24 @@
<h3 class="text-xl font-bold text-gray-900">{{ $result->title }}</h3>
@endif
@if(!empty($result->grp_release_reports))
@php
$reportCounts = array_filter(explode(',', $result->grp_release_reports));
$totalReports = array_sum($reportCounts);
$reportReasonsRaw = !empty($result->grp_release_report_reasons)
? implode(',', array_unique(array_filter(preg_split('/[|,]/', $result->grp_release_report_reasons))))
: '';
$reportReasons = \App\Models\ReleaseReport::reasonKeysToLabels($reportReasonsRaw);
@endphp
@if($totalReports > 0)
<div class="inline-flex items-center px-2 py-1 rounded text-xs font-medium bg-orange-100 dark:bg-orange-900 text-orange-800 dark:text-orange-200 border border-orange-200 dark:border-orange-800 mt-1"
title="Reported: {{ $reportReasons }}">
<i class="fas fa-flag mr-1"></i>
<span>{{ $totalReports }} report{{ $totalReports > 1 ? 's' : '' }}</span>
</div>
@endif
@endif
@if(!empty($result->grp_release_failed))
@php
$failedCounts = array_filter(explode(',', $result->grp_release_failed));
@@ -227,6 +245,7 @@
$releaseNames = isset($result->grp_release_name) ? explode('#', $result->grp_release_name) : [];
$releaseSizes = isset($result->grp_release_size) ? explode(',', $result->grp_release_size) : [];
$releaseGuids = isset($result->grp_release_guid) ? explode(',', $result->grp_release_guid) : [];
$releaseIds = isset($result->grp_release_id) ? explode(',', $result->grp_release_id) : [];
$releasePostDates = isset($result->grp_release_postdate) ? explode(',', $result->grp_release_postdate) : [];
$releaseAddDates = isset($result->grp_release_adddate) ? explode(',', $result->grp_release_adddate) : [];
$releaseHasPreview = isset($result->grp_haspreview) ? explode(',', $result->grp_haspreview) : [];
@@ -240,6 +259,7 @@
$releaseNames = array_slice($releaseNames, 0, $maxReleases);
$releaseSizes = array_slice($releaseSizes, 0, $maxReleases);
$releaseGuids = array_slice($releaseGuids, 0, $maxReleases);
$releaseIds = array_slice($releaseIds, 0, $maxReleases);
$releasePostDates = array_slice($releasePostDates, 0, $maxReleases);
$releaseAddDates = array_slice($releaseAddDates, 0, $maxReleases);
$releaseHasPreview = array_slice($releaseHasPreview, 0, $maxReleases);
@@ -327,6 +347,9 @@
<a href="{{ url('/details/' . $releaseGuids[$index]) }}" class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-600 dark:bg-gray-700 text-white hover:bg-gray-700 dark:hover:bg-gray-800 transition">
<i class="fas fa-info-circle mr-1"></i> Details
</a>
@if(isset($releaseIds[$index]))
<x-report-button :release-id="(int)$releaseIds[$index]" variant="icon" />
@endif
</div>
</div>
</div>
@@ -67,6 +67,9 @@
</button>
<div id="releases-menu" class="hidden mt-2 ml-6 space-y-1">
<a href="{{ url('/admin/release-list') }}" class="block py-2 px-3 text-gray-400 dark:text-gray-500 hover:text-white dark:hover:text-white hover:bg-gray-800 dark:hover:bg-gray-800 rounded transition">Release List</a>
<a href="{{ url('/admin/release-reports') }}" class="block py-2 px-3 text-gray-400 dark:text-gray-500 hover:text-white dark:hover:text-white hover:bg-gray-800 dark:hover:bg-gray-800 rounded transition">
<i class="fas fa-flag text-red-500 mr-1"></i> Release Reports
</a>
<a href="{{ url('/admin/failrel-list') }}" class="block py-2 px-3 text-gray-400 dark:text-gray-500 hover:text-white dark:hover:text-white hover:bg-gray-800 dark:hover:bg-gray-800 rounded transition">Failed Releases</a>
<a href="{{ url('/admin/category-list') }}" class="block py-2 px-3 text-gray-400 dark:text-gray-500 hover:text-white dark:hover:text-white hover:bg-gray-800 dark:hover:bg-gray-800 rounded transition">Categories</a>
</div>
+14
View File
@@ -210,6 +210,12 @@
<div class="flex-1">
<div class="flex items-center gap-2 flex-wrap">
<a href="{{ url('/details/' . $result->guid) }}" class="text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 font-medium break-words break-all">{{ $result->searchname }}</a>
@if(!empty($result->report_count) && $result->report_count > 0)
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-orange-100 dark:bg-orange-900 text-orange-800 dark:text-orange-200"
title="Reported: {{ \App\Models\ReleaseReport::reasonKeysToLabels($result->report_reasons ?? '') }}">
<i class="fas fa-flag mr-1"></i> Reported ({{ $result->report_count }})
</span>
@endif
@if(!empty($result->failed_count) && $result->failed_count > 0)
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200"
title="{{ $result->failed_count }} user(s) reported download failure">
@@ -322,6 +328,7 @@
<i class="fa fa-film"></i>
</a>
@endif
<x-report-button :release-id="$result->id" variant="icon" />
</div>
</td>
</tr>
@@ -341,6 +348,12 @@
<a href="{{ url('/details/' . $result->guid) }}" class="text-lg font-medium text-blue-600 dark:text-blue-400 hover:text-blue-800 break-words break-all">
{{ $result->searchname }}
</a>
@if(!empty($result->report_count) && $result->report_count > 0)
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-orange-100 dark:bg-orange-900 text-orange-800 dark:text-orange-200"
title="Reported: {{ \App\Models\ReleaseReport::reasonKeysToLabels($result->report_reasons ?? '') }}">
<i class="fas fa-flag mr-1"></i> Reported ({{ $result->report_count }})
</span>
@endif
@if(!empty($result->failed_count) && $result->failed_count > 0)
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200"
title="{{ $result->failed_count }} user(s) reported download failure">
@@ -429,6 +442,7 @@
<i class="fa fa-film"></i>
</a>
@endif
<x-report-button :release-id="$result->id" variant="icon" />
</div>
</div>
</div>
+13
View File
@@ -154,6 +154,11 @@ Route::middleware('isVerified')->group(function () {
Route::post('profile/update-theme', [ProfileController::class, 'updateTheme'])->name('profile.update-theme');
Route::match(['GET', 'POST'], 'search', [SearchController::class, 'search'])->name('search');
// Release Report routes
Route::post('release-report', [\App\Http\Controllers\ReleaseReportController::class, 'store'])->name('release-report.store');
Route::get('release-report/reasons', [\App\Http\Controllers\ReleaseReportController::class, 'getReasons'])->name('release-report.reasons');
Route::get('release-report/check', [\App\Http\Controllers\ReleaseReportController::class, 'checkReported'])->name('release-report.check');
Route::match(['GET', 'POST'], 'mymovies', [MyMoviesController::class, 'show'])->name('mymovies');
Route::match(['GET', 'POST'], 'myshows', [MyShowsController::class, 'show'])->name('myshows');
Route::match(['GET', 'POST'], 'myshows/browse', [MyShowsController::class, 'browse'])->name('myshows.browse');
@@ -236,6 +241,14 @@ Route::middleware('role:Admin', '2fa')->prefix('admin')->group(function () {
Route::match(['GET', 'POST'], 'tmux-edit', [AdminTmuxController::class, 'edit'])->name('admin.tmux-edit');
Route::get('release-list', [AdminReleasesController::class, 'index'])->name('admin.release-list');
Route::post('release-delete/{id}', [AdminReleasesController::class, 'destroy'])->name('admin.release-delete');
// Release Reports Management
Route::get('release-reports', [\App\Http\Controllers\Admin\AdminReleaseReportController::class, 'index'])->name('admin.release-reports');
Route::post('release-reports/{id}/status', [\App\Http\Controllers\Admin\AdminReleaseReportController::class, 'updateStatus'])->name('admin.release-reports.update-status');
Route::post('release-reports/{id}/delete-release', [\App\Http\Controllers\Admin\AdminReleaseReportController::class, 'deleteRelease'])->name('admin.release-reports.delete-release');
Route::post('release-reports/{id}/dismiss', [\App\Http\Controllers\Admin\AdminReleaseReportController::class, 'dismiss'])->name('admin.release-reports.dismiss');
Route::post('release-reports/bulk', [\App\Http\Controllers\Admin\AdminReleaseReportController::class, 'bulkAction'])->name('admin.release-reports.bulk');
Route::get('show-list', [AdminShowsController::class, 'index'])->name('admin.show-list');
Route::match(['GET', 'POST'], 'show-edit', [AdminShowsController::class, 'edit'])->name('admin.show-edit');
Route::get('show-remove/{id}', [AdminShowsController::class, 'destroy'])->name('admin.show-remove');