Update views and related controllers

This commit is contained in:
DariusIII
2026-07-10 21:31:19 +02:00
parent 81d60db201
commit f6c18cdeac
40 changed files with 1303 additions and 1178 deletions
+10 -8
View File
@@ -32,24 +32,25 @@ class AdultController extends BasePageController
'title' => $mcat->title,
];
}
$category = $request->has('t') ? $request->input('t') : Category::XXX_ROOT;
$category = $this->integerInput($request, 't', Category::XXX_ROOT);
if ($id && \in_array($id, Arr::pluck($mtmp, 'title'), true)) {
$cat = Category::query()
->where('title', $id)
->where('root_categories_id', '=', Category::XXX_ROOT)
->first(['id']);
$category = $cat !== null ? $cat['id'] : Category::XXX_ROOT;
$category = $cat !== null ? (int) $cat['id'] : Category::XXX_ROOT;
}
$catarray = [];
$catarray[] = $category;
$ordering = $this->releaseBrowseService->getBrowseOrdering();
$orderby = $request->has('ob') && \in_array($request->input('ob'), $ordering, true) ? $request->input('ob') : '';
$orderby = $this->resolveOrderBy($request, $ordering);
$page = $request->has('page') && is_numeric($request->input('page')) ? $request->input('page') : 1;
$offset = ($page - 1) * config('nntmux.items_per_page');
$rslt = $this->releaseBrowseService->getBrowseRange($page, $catarray, $offset, config('nntmux.items_per_page'), $orderby, -1, (array) ($this->userdata->categoryexclusions ?? []), -1);
$results = $this->paginate($rslt ?? [], isset($rslt[0]->_totalcount) ? $rslt[0]->_totalcount : 0, config('nntmux.items_per_page'), $page, $request->url(), $request->query());
$page = $this->resolvePage($request);
$perPage = (int) config('nntmux.items_per_page');
$offset = $this->paginationOffset($page, $perPage);
$rslt = $this->releaseBrowseService->getBrowseRange($page, $catarray, $offset, $perPage, $orderby, -1, (array) ($this->userdata->categoryexclusions ?? []), -1);
$results = $this->paginate($rslt ?? [], isset($rslt[0]->_totalcount) ? $rslt[0]->_totalcount : 0, $perPage, $page, $request->url(), $request->query());
if ((int) $category === -1) {
$catname = 'All';
@@ -60,7 +61,7 @@ class AdultController extends BasePageController
$orderByUrls = [];
foreach ($ordering as $ordertype) {
$orderByUrls['orderby'.$ordertype] = url('/XXX/'.($id ?: 'All').'?t='.$category.'&ob='.$ordertype.'&offset=0');
$orderByUrls['orderby'.$ordertype] = url('/XXX/'.($id ?: 'All').'?t='.$category.'&ob='.$ordertype);
}
$this->viewData = array_merge($this->viewData, [
@@ -69,6 +70,7 @@ class AdultController extends BasePageController
'categorytitle' => $id,
'catname' => $catname,
'ordering' => $ordering,
'orderByUrls' => $orderByUrls,
'results' => $results,
'lastvisit' => $this->userdata['lastlogin'] ?? null,
'meta_title' => 'Browse XXX',
@@ -124,6 +124,89 @@ class BasePageController extends Controller
return new LengthAwarePaginator($query, $totalCount, $items, $page, ['path' => $path, 'query' => $reqQuery]);
}
protected function resolvePage(Request $request, string $key = 'page'): int
{
$page = $request->input($key, 1);
if (! is_scalar($page)) {
return 1;
}
$page = (string) $page;
if ($page === '' || preg_match('/^\d+$/', $page) !== 1) {
return 1;
}
return max(1, (int) $page);
}
/**
* @param array<int, string> $ordering
*/
protected function resolveOrderBy(Request $request, array $ordering, string $key = 'ob'): string
{
$orderByInput = $request->input($key, '');
$orderBy = is_scalar($orderByInput) ? (string) $orderByInput : '';
return \in_array($orderBy, $ordering, true) ? $orderBy : '';
}
protected function paginationOffset(int $page, int $perPage): int
{
return ($page - 1) * $perPage;
}
/**
* @param array<int, string> $ordering
* @return array<string, string>
*/
protected function buildOrderByUrls(array $ordering, string $basePath, string $prefix = 'orderby'): array
{
$orderByUrls = [];
foreach ($ordering as $orderType) {
$orderByUrls[$prefix.$orderType] = url($basePath.'?ob='.$orderType);
}
return $orderByUrls;
}
protected function scalarInput(Request $request, string $key, string $default = ''): string
{
$value = $request->input($key, $default);
return is_scalar($value) ? (string) $value : $default;
}
protected function integerInput(Request $request, string $key, int $default = 0): int
{
$value = $this->scalarInput($request, $key, (string) $default);
return preg_match('/^-?\d+$/', $value) === 1 ? (int) $value : $default;
}
/**
* @return array<int|string, mixed>
*/
protected function arrayInput(Request $request, string $key): array
{
$value = $request->input($key, []);
return \is_array($value) ? $value : [];
}
protected function localReturnUrl(Request $request, string $fallback, string $key = 'from'): string
{
$from = $this->scalarInput($request, $key);
if ($from === '') {
return url($fallback);
}
if (filter_var($from, FILTER_VALIDATE_URL) !== false) {
return parse_url($from, PHP_URL_HOST) === $request->getHost() ? $from : url($fallback);
}
return url($from);
}
/**
* Check if request is POST.
*/
+13 -11
View File
@@ -34,27 +34,27 @@ class BooksController extends BasePageController
'title' => $bcat->title,
];
}
$category = $request->has('t') ? $request->input('t') : Category::BOOKS_ROOT;
$category = $request->has('t') ? $this->scalarInput($request, 't', (string) Category::BOOKS_ROOT) : Category::BOOKS_ROOT;
if ($id && \in_array($id, Arr::pluck($btmp, 'title'), false)) {
$cat = Category::query()
->where('title', $id)
->where('root_categories_id', '=', Category::BOOKS_ROOT)
->first(['id']);
$category = $cat !== null ? $cat['id'] : Category::BOOKS_ROOT;
$category = $cat !== null ? (int) $cat['id'] : Category::BOOKS_ROOT;
}
$catarray = [];
$catarray[] = $category;
$catarray[] = (int) $category;
$ordering = $this->bookService->getBookOrdering();
$orderby = $request->has('ob') && \in_array($request->input('ob'), $ordering, false) ? $request->input('ob') : '';
$orderby = $this->resolveOrderBy($request, $ordering);
$books = [];
$pageInput = $request->input('page');
$page = is_scalar($pageInput) && preg_match('/^\d+$/', (string) $pageInput) === 1 ? max(1, (int) $pageInput) : 1;
$offset = ($page - 1) * (int) config('nntmux.items_per_cover_page');
$rslt = $this->bookService->getBookRange($page, $catarray, $offset, (int) config('nntmux.items_per_cover_page'), $orderby, (array) $this->userdata->categoryexclusions);
$results = $this->paginate($rslt, $rslt[0]->_totalcount ?? 0, (int) config('nntmux.items_per_cover_page'), $page, $request->url(), $request->query());
$page = $this->resolvePage($request);
$perPage = (int) config('nntmux.items_per_cover_page');
$offset = $this->paginationOffset($page, $perPage);
$rslt = $this->bookService->getBookRange($page, $catarray, $offset, $perPage, $orderby, (array) $this->userdata->categoryexclusions);
$results = $this->paginate($rslt, $rslt[0]->_totalcount ?? 0, $perPage, $page, $request->url(), $request->query());
$maxwords = 50;
foreach ($results as $result) {
if (! empty($result->overview)) {
@@ -67,9 +67,11 @@ class BooksController extends BasePageController
$books[] = $result;
}
$author = ($request->has('author') && ! empty($request->input('author'))) ? stripslashes($request->input('author')) : '';
$authorInput = $this->scalarInput($request, 'author');
$author = $authorInput !== '' ? stripslashes($authorInput) : '';
$title = ($request->has('title') && ! empty($request->input('title'))) ? stripslashes($request->input('title')) : '';
$titleInput = $this->scalarInput($request, 'title');
$title = $titleInput !== '' ? stripslashes($titleInput) : '';
$browseby_link = '&title='.$title.'&author='.$author;
+34 -29
View File
@@ -8,6 +8,7 @@ use App\Models\Category;
use App\Models\RootCategory;
use App\Services\Releases\ReleaseBrowseService;
use Illuminate\Http\Request;
use Illuminate\Pagination\LengthAwarePaginator;
class BrowseController extends BasePageController
{
@@ -25,18 +26,12 @@ class BrowseController extends BasePageController
public function index(Request $request): mixed
{
$ordering = $this->releaseBrowseService->getBrowseOrdering();
$orderBy = $request->has('ob') && ! empty($request->input('ob')) ? $request->input('ob') : '';
$page = $request->has('page') && is_numeric($request->input('page')) ? $request->input('page') : 1;
$offset = ($page - 1) * config('nntmux.items_per_page');
$rslt = $this->releaseBrowseService->getBrowseRange($page, [-1], $offset, config('nntmux.items_per_page'), $orderBy, -1, (array) $this->userdata->categoryexclusions, -1);
$results = $this->paginate($rslt ?? [], $rslt[0]->_totalcount ?? 0, config('nntmux.items_per_page'), $page, $request->url(), $request->query());
$orderBy = $this->resolveOrderBy($request, $ordering);
$page = $this->resolvePage($request);
$results = $this->getBrowsePaginator($request, $page, [-1], $orderBy);
// Build order by URLs
$orderByUrls = [];
foreach ($ordering as $orderType) {
$orderByUrls['orderby'.$orderType] = url('browse/All?ob='.$orderType);
}
$orderByUrls = $this->buildOrderByUrls($ordering, 'browse/All');
$this->viewData = array_merge($this->viewData, [
'category' => -1,
@@ -57,12 +52,13 @@ class BrowseController extends BasePageController
public function show(Request $request, string $parentCategory, string $id = 'All'): mixed
{
$parentId = RootCategory::query()->where('title', $parentCategory)->value('id');
$parentIdValue = RootCategory::query()->where('title', $parentCategory)->value('id');
$parentId = $parentIdValue === null ? null : (int) $parentIdValue;
$query = Category::query()->where('title', $id)->where('root_categories_id', $parentId);
if ($id !== 'All') {
$cat = $query->first();
$category = $cat !== null ? $cat->id : -1;
$category = $cat !== null ? (int) $cat->id : -1;
} else {
$category = $parentId ?? -1;
}
@@ -73,12 +69,9 @@ class BrowseController extends BasePageController
$catarray[] = $category;
$ordering = $this->releaseBrowseService->getBrowseOrdering();
$orderBy = $request->has('ob') && ! empty($request->input('ob')) ? $request->input('ob') : '';
$page = $request->has('page') && is_numeric($request->input('page')) ? $request->input('page') : 1;
$offset = ($page - 1) * config('nntmux.items_per_page');
$rslt = $this->releaseBrowseService->getBrowseRange($page, $catarray, $offset, config('nntmux.items_per_page'), $orderBy, -1, (array) $this->userdata->categoryexclusions, $grp);
$results = $this->paginate($rslt ?? [], $rslt[0]->_totalcount ?? 0, config('nntmux.items_per_page'), $page, $request->url(), $request->query());
$orderBy = $this->resolveOrderBy($request, $ordering);
$page = $this->resolvePage($request);
$results = $this->getBrowsePaginator($request, $page, $catarray, $orderBy, $grp);
$covgroup = '';
$shows = false;
@@ -126,14 +119,10 @@ class BrowseController extends BasePageController
$orderByUrls = [];
if ($id === 'All' && $parentCategory === 'All') {
$meta_title = 'Browse '.$parentCategory.' releases';
foreach ($ordering as $orderType) {
$orderByUrls['orderby'.$orderType] = url('browse/'.$parentCategory.'?ob='.$orderType);
}
$orderByUrls = $this->buildOrderByUrls($ordering, 'browse/'.$parentCategory);
} else {
$meta_title = 'Browse '.$parentCategory.' / '.$id.' releases';
foreach ($ordering as $orderType) {
$orderByUrls['orderby'.$orderType] = url('browse/'.$parentCategory.'/'.$id.'?ob='.$orderType);
}
$orderByUrls = $this->buildOrderByUrls($ordering, 'browse/'.$parentCategory.'/'.$id);
}
$viewData = [
@@ -163,11 +152,14 @@ class BrowseController extends BasePageController
public function group(Request $request): mixed
{
if ($request->has('g')) {
$group = $request->input('g');
$page = $request->has('page') && is_numeric($request->input('page')) ? $request->input('page') : 1;
$offset = ($page - 1) * config('nntmux.items_per_page');
$rslt = $this->releaseBrowseService->getBrowseRange($page, [-1], $offset, config('nntmux.items_per_page'), '', -1, (array) $this->userdata->categoryexclusions, $group);
$results = $this->paginate($rslt ?? [], $rslt[0]->_totalcount ?? 0, config('nntmux.items_per_page'), $page, $request->url(), $request->query());
$groupInput = $request->input('g');
if (! is_scalar($groupInput)) {
return redirect()->back()->with('error', 'Group parameter is invalid');
}
$group = (string) $groupInput;
$page = $this->resolvePage($request);
$results = $this->getBrowsePaginator($request, $page, [-1], '', $group);
$this->viewData = array_merge($this->viewData, [
'results' => $results,
@@ -184,4 +176,17 @@ class BrowseController extends BasePageController
return redirect()->back()->with('error', 'Group parameter is required');
}
/**
* @param array<int> $categories
* @return LengthAwarePaginator<int, mixed>
*/
private function getBrowsePaginator(Request $request, int $page, array $categories, string $orderBy, int|string $group = -1): LengthAwarePaginator
{
$perPage = (int) config('nntmux.items_per_page');
$offset = $this->paginationOffset($page, $perPage);
$rslt = $this->releaseBrowseService->getBrowseRange($page, $categories, $offset, $perPage, $orderBy, -1, (array) $this->userdata->categoryexclusions, $group);
return $this->paginate($rslt ?? [], $rslt[0]->_totalcount ?? 0, $perPage, $page, $request->url(), $request->query());
}
}
+1 -1
View File
@@ -36,7 +36,7 @@ class CartController extends BasePageController
*/
public function store(Request $request): RedirectResponse|JsonResponse
{
$guids = collect(explode(',', (string) $request->input('id')))
$guids = collect(explode(',', $this->scalarInput($request, 'id')))
->map(static fn (string $guid): string => trim($guid))
->filter()
->unique()
+15 -11
View File
@@ -40,26 +40,27 @@ class ConsoleController extends BasePageController
'title' => $ccat->title,
];
}
$category = $request->has('t') ? $request->input('t') : Category::GAME_ROOT;
$category = $request->has('t') ? $this->scalarInput($request, 't', (string) Category::GAME_ROOT) : Category::GAME_ROOT;
if ($id && \in_array($id, Arr::pluck($ctmp, 'title'), false)) {
$cat = Category::query()
->where('title', $id)
->where('root_categories_id', '=', Category::GAME_ROOT)
->first(['id']);
$category = $cat !== null ? $cat['id'] : Category::GAME_ROOT;
$category = $cat !== null ? (int) $cat['id'] : Category::GAME_ROOT;
}
$catarray = [];
$catarray[] = $category;
$catarray[] = (int) $category;
$ordering = $this->consoleService->getConsoleOrdering();
$orderby = $request->has('ob') && \in_array($request->input('ob'), $ordering, false) ? $request->input('ob') : '';
$page = $request->has('page') && is_numeric($request->input('page')) ? $request->input('page') : 1;
$offset = ($page - 1) * (int) config('nntmux.items_per_cover_page');
$orderby = $this->resolveOrderBy($request, $ordering);
$page = $this->resolvePage($request);
$perPage = (int) config('nntmux.items_per_cover_page');
$offset = $this->paginationOffset($page, $perPage);
$consoles = [];
$rslt = $this->consoleService->getConsoleRange($page, $catarray, $offset, (int) config('nntmux.items_per_cover_page'), $orderby, (array) $this->userdata->categoryexclusions);
$results = $this->paginate($rslt, $rslt[0]->_totalcount ?? 0, (int) config('nntmux.items_per_cover_page'), $page, $request->url(), $request->query());
$rslt = $this->consoleService->getConsoleRange($page, $catarray, $offset, $perPage, $orderby, (array) $this->userdata->categoryexclusions);
$results = $this->paginate($rslt, $rslt[0]->_totalcount ?? 0, $perPage, $page, $request->url(), $request->query());
$maxwords = 50;
foreach ($results as $result) {
@@ -73,8 +74,10 @@ class ConsoleController extends BasePageController
$consoles[] = $result;
}
$platform = ($request->has('platform') && ! empty($request->input('platform'))) ? stripslashes($request->input('platform')) : '';
$title = ($request->has('title') && ! empty($request->input('title'))) ? stripslashes($request->input('title')) : '';
$platformInput = $this->scalarInput($request, 'platform');
$platform = $platformInput !== '' ? stripslashes($platformInput) : '';
$titleInput = $this->scalarInput($request, 'title');
$title = $titleInput !== '' ? stripslashes($titleInput) : '';
$genres = $gen->getGenres((string) GenreService::CONSOLE_TYPE, true);
$tmpgnr = [];
@@ -82,7 +85,8 @@ class ConsoleController extends BasePageController
/** @var Genre $gn */
$tmpgnr[$gn->id] = $gn->title;
}
$genre = ($request->has('genre') && isset($tmpgnr[$request->input('genre')])) ? $request->input('genre') : '';
$genreInput = $this->scalarInput($request, 'genre');
$genre = isset($tmpgnr[$genreInput]) ? $genreInput : '';
if ((int) $category === -1) {
$catname = 'All';
+21 -4
View File
@@ -22,7 +22,7 @@ class ContentController extends BasePageController
*/
public function show(Request $request)
{
$role = $this->userdata->role ?? 0;
$role = $this->contentRole();
/* The role column in the content table values are:
* 1 = logged in users
@@ -38,8 +38,8 @@ class ContentController extends BasePageController
*/
$isAdmin = \in_array($role, [UserRole::ADMIN->value, UserRole::MODERATOR->value], true);
$contentId = $request->input('id', 0);
$contentPage = $request->input('page', false);
$contentId = $this->integerInput($request, 'id');
$contentPage = $this->scalarInput($request, 'page');
if ($contentId === 0 && $contentPage === 'content') {
// Show all content except front page
@@ -48,7 +48,7 @@ class ContentController extends BasePageController
$meta_title = 'Contents page';
$meta_keywords = 'contents';
$meta_description = 'This is the contents page.';
} elseif ($contentId !== 0 && $contentPage !== false) {
} elseif ($contentId !== 0 && $contentPage !== '') {
// Show specific content by ID
$contentItem = $this->getContentById($contentId, $role);
$content = $contentItem ? [$contentItem] : [];
@@ -82,6 +82,23 @@ class ContentController extends BasePageController
return view('content.index', $this->viewData);
}
private function contentRole(): int
{
if (! isset($this->userdata)) {
return Content::ROLE_EVERYONE;
}
if ($this->userdata->hasRole('Admin')) {
return UserRole::ADMIN->value;
}
if ($this->userdata->hasRole('Moderator')) {
return UserRole::MODERATOR->value;
}
return Content::ROLE_LOGGED_IN;
}
/**
* Get all active content ordered by type and ordinal.
*
@@ -8,12 +8,9 @@ use App\Models\DnzbFailure;
use App\Models\Predb;
use App\Models\Release;
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;
use App\Services\AnidbService;
use App\Services\BookService;
@@ -63,9 +60,6 @@ class DetailsController extends BasePageController
return redirect()->route('details', ['guid' => $guid])->with('success', 'Comment posted successfully!');
}
$nfoData = ReleaseNfo::getReleaseNfo($data['id']);
/** @var ReleaseNfo|null $nfoData */
$nfo = $nfoData ? $nfoData->nfo : null;
$reVideo = $this->releaseExtraService->getVideo($data['id']);
$reAudio = $this->releaseExtraService->getAudio($data['id']);
$reSubs = $this->releaseExtraService->getSubs($data['id']);
@@ -93,8 +87,6 @@ class DetailsController extends BasePageController
->where('response', '!=', '')
->orderByDesc('responded_at')
->get();
$downloadedBy = UserDownload::query()->with('user')->where('releases_id', $data['id'])->get(['users_id']);
$showInfo = '';
if ($data['videos_id'] > 0) {
$showInfo = Video::getByVideoID($data['videos_id']);
@@ -166,15 +158,11 @@ class DetailsController extends BasePageController
$pre = Predb::getForRelease($data['predb_id']);
$releasefiles = ReleaseFile::getReleaseFiles($data['id']);
$this->viewData = array_merge($this->viewData, [
'releasefiles' => $releasefiles,
'release' => $data,
'reVideo' => $reVideo,
'reAudio' => $reAudio,
'reSubs' => $reSubs,
'nfo' => $nfo,
'show' => $showInfo,
'movie' => $mov,
'anidb' => $AniDBAPIArray,
@@ -184,7 +172,6 @@ class DetailsController extends BasePageController
'book' => $book,
'predb' => $pre,
'comments' => $comments,
'files' => $releasefiles,
'searchname' => getSimilarName($data['searchname']),
'similars' => $similars !== false ? $similars : [],
'privateprofiles' => config('nntmux_settings.private_profiles'),
@@ -196,7 +183,6 @@ class DetailsController extends BasePageController
'allReportReasons' => $allReportReasons,
'publicReportResponses' => $publicReportResponses,
'regex' => $releaseRegex,
'downloadedby' => $downloadedBy,
'meta_title' => 'View NZB',
'meta_keywords' => 'view,nzb,description,details',
'meta_description' => 'View NZB for '.$data['searchname'],
+16 -10
View File
@@ -26,21 +26,24 @@ class GamesController extends BasePageController
$ctmp[$ccat['id']] = $ccat;
}
$category = Category::PC_GAMES;
if ($request->has('t') && isset($ctmp[$request->input('t')])) {
$category = $request->input('t') + 0;
$categoryInput = $this->scalarInput($request, 't');
if ($categoryInput !== '' && isset($ctmp[$categoryInput])) {
$category = (int) $categoryInput;
}
$catarray = [];
$catarray[] = $category;
$page = $request->has('page') && is_numeric($request->input('page')) ? $request->input('page') : 1;
$page = $this->resolvePage($request);
$ordering = $games->getGamesOrdering();
$orderby = $request->has('ob') && \in_array($request->input('ob'), $ordering, true) ? $request->input('ob') : '';
$offset = ($page - 1) * (int) config('nntmux.items_per_cover_page');
$rslt = $games->getGamesRange($page, $catarray, $offset, (int) config('nntmux.items_per_cover_page'), $orderby, '', (array) $this->userdata->categoryexclusions);
$results = $this->paginate($rslt, $rslt[0]->_totalcount ?? 0, (int) config('nntmux.items_per_cover_page'), $page, $request->url(), $request->query());
$orderby = $this->resolveOrderBy($request, $ordering);
$perPage = (int) config('nntmux.items_per_cover_page');
$offset = $this->paginationOffset($page, $perPage);
$rslt = $games->getGamesRange($page, $catarray, $offset, $perPage, $orderby, '', (array) $this->userdata->categoryexclusions);
$results = $this->paginate($rslt, $rslt[0]->_totalcount ?? 0, $perPage, $page, $request->url(), $request->query());
$title = ($request->has('title') && ! empty($request->input('title'))) ? stripslashes($request->input('title')) : '';
$titleInput = $this->scalarInput($request, 'title');
$title = $titleInput !== '' ? stripslashes($titleInput) : '';
$genres = $gen->getGenres((string) GenreService::GAME_TYPE, true);
$tmpgnr = [];
@@ -51,9 +54,12 @@ class GamesController extends BasePageController
$years = range(1903, date('Y') + 1);
rsort($years);
$year = ($request->has('year') && \in_array($request->input('year'), $years, true)) ? $request->input('year') : '';
$yearInput = $this->scalarInput($request, 'year');
$yearValue = is_numeric($yearInput) ? (int) $yearInput : null;
$year = $yearValue !== null && \in_array($yearValue, $years, true) ? $yearValue : '';
$genre = ($request->has('genre') && isset($tmpgnr[$request->input('genre')])) ? $request->input('genre') : '';
$genreInput = $this->scalarInput($request, 'genre');
$genre = isset($tmpgnr[$genreInput]) ? $genreInput : '';
if ((int) $category === -1) {
$catname = 'All';
+2 -2
View File
@@ -195,9 +195,9 @@ class GetNzbController extends BasePageController
*/
private function validateAndSanitizeId(Request $request): string|Response
{
$id = $request->input('id');
$id = $this->scalarInput($request, 'id');
if (empty($id)) {
if ($id === '') {
return showApiError(200, 'Parameter id is required');
}
+23 -20
View File
@@ -36,26 +36,23 @@ class MovieController extends BasePageController
return ['id' => $mcat->id, 'title' => $mcat->title];
});
$category = $request->has('imdb') ? -1 : ($request->input('t', Category::MOVIE_ROOT));
$category = $request->has('imdb') ? -1 : $this->integerInput($request, 't', Category::MOVIE_ROOT);
if ($id && $moviecats->pluck('title')->contains($id)) {
$cat = Category::where(['title' => $id, 'root_categories_id' => Category::MOVIE_ROOT])->first(['id']);
$category = $cat->id ?? Category::MOVIE_ROOT;
$category = $cat !== null ? (int) $cat->id : Category::MOVIE_ROOT;
}
$catarray = $category !== -1 ? [$category] : [];
$page = (int) $request->input('page', 1);
$offset = ($page - 1) * (int) config('nntmux.items_per_cover_page');
$orderby = $request->input('ob', '');
$page = $this->resolvePage($request);
$perPage = (int) config('nntmux.items_per_cover_page');
$offset = $this->paginationOffset($page, $perPage);
$ordering = $this->movieBrowseService->getMovieOrdering();
if (! in_array($orderby, $ordering, true)) {
$orderby = '';
}
$orderby = $this->resolveOrderBy($request, $ordering);
$rslt = $this->movieBrowseService->getMovieRange($page, $catarray, $offset, (int) config('nntmux.items_per_cover_page'), $orderby, -1, (array) $this->userdata->categoryexclusions);
$rslt = $this->movieBrowseService->getMovieRange($page, $catarray, $offset, $perPage, $orderby, -1, (array) $this->userdata->categoryexclusions);
$totalCount = $rslt->isNotEmpty() ? ($rslt[0]->_totalcount ?? 0) : 0;
$results = $this->paginate($rslt ?? [], $totalCount, (int) config('nntmux.items_per_cover_page'), $page, $request->url(), $request->query());
$results = $this->paginate($rslt ?? [], $totalCount, $perPage, $page, $request->url(), $request->query());
$movies = $results->map(function ($result) {
$result->genre = makeFieldLinks($result, 'genre', 'movies');
@@ -71,6 +68,11 @@ class MovieController extends BasePageController
$years = range(1903, now()->addYear()->year);
rsort($years);
$genres = $this->movieBrowseService->getGenres();
$ratingInput = $this->integerInput($request, 'rating', 0);
$genreInput = $this->scalarInput($request, 'genre');
$yearInput = $this->integerInput($request, 'year', 0);
$catname = $category === -1 ? 'All' : Category::find($category) ?? 'All';
$this->viewData = array_merge($this->viewData, [
@@ -79,15 +81,15 @@ class MovieController extends BasePageController
'catlist' => $moviecats,
'category' => $category,
'categorytitle' => $id,
'title' => stripslashes((string) ($request->input('title') ?? '')),
'actors' => stripslashes((string) ($request->input('actors') ?? '')),
'director' => stripslashes((string) ($request->input('director') ?? '')),
'title' => stripslashes($this->scalarInput($request, 'title')),
'actors' => stripslashes($this->scalarInput($request, 'actors')),
'director' => stripslashes($this->scalarInput($request, 'director')),
'ratings' => range(1, 9),
'rating' => $request->input('rating', ''),
'genres' => $this->movieBrowseService->getGenres(),
'genre' => $request->input('genre', ''),
'rating' => \in_array($ratingInput, range(1, 9), true) ? $ratingInput : '',
'genres' => $genres,
'genre' => \in_array($genreInput, $genres, true) ? $genreInput : '',
'years' => $years,
'year' => $request->input('year', ''),
'year' => \in_array($yearInput, $years, true) ? $yearInput : '',
'catname' => $catname,
'resultsadd' => $movies,
'results' => $results,
@@ -162,8 +164,9 @@ class MovieController extends BasePageController
*/
public function showTrailer(Request $request)
{
if ($request->has('id') && ctype_digit($request->input('id'))) {
$mov = $this->movieService->getMovieInfo($request->input('id'));
$movieId = $this->scalarInput($request, 'id');
if ($movieId !== '' && ctype_digit($movieId)) {
$mov = $this->movieService->getMovieInfo($movieId);
if (! $mov) {
return response()->json(['message' => 'There is no trailer for this movie.'], 404);
+19 -14
View File
@@ -39,31 +39,33 @@ class MusicController extends BasePageController
];
}
$category = $request->has('t') ? $request->input('t') : Category::MUSIC_ROOT;
$category = $request->has('t') ? $this->scalarInput($request, 't', (string) Category::MUSIC_ROOT) : Category::MUSIC_ROOT;
if ($id && \in_array($id, Arr::pluck($mtmp, 'title'), true)) {
$cat = Category::query()
->where('title', $id)
->where('root_categories_id', '=', Category::MUSIC_ROOT)
->first(['id']);
$category = $cat !== null ? $cat['id'] : Category::MUSIC_ROOT;
$category = $cat !== null ? (int) $cat['id'] : Category::MUSIC_ROOT;
}
$catarray = [];
$catarray[] = $category;
$catarray[] = (int) $category;
$pageInput = $request->input('page');
$page = is_scalar($pageInput) && preg_match('/^\d+$/', (string) $pageInput) === 1 ? max(1, (int) $pageInput) : 1;
$offset = ($page - 1) * (int) config('nntmux.items_per_cover_page');
$page = $this->resolvePage($request);
$perPage = (int) config('nntmux.items_per_cover_page');
$offset = $this->paginationOffset($page, $perPage);
$ordering = $this->musicService->getMusicOrdering();
$orderby = $request->has('ob') && \in_array($request->input('ob'), $ordering, true) ? $request->input('ob') : '';
$orderby = $this->resolveOrderBy($request, $ordering);
$musics = [];
$rslt = $this->musicService->getMusicRange($page, $catarray, $offset, (int) config('nntmux.items_per_cover_page'), $orderby, (array) $this->userdata->categoryexclusions);
$results = $this->paginate($rslt ?? [], $rslt[0]->_totalcount ?? 0, (int) config('nntmux.items_per_cover_page'), $page, $request->url(), $request->query());
$rslt = $this->musicService->getMusicRange($page, $catarray, $offset, $perPage, $orderby, (array) $this->userdata->categoryexclusions);
$results = $this->paginate($rslt ?? [], $rslt[0]->_totalcount ?? 0, $perPage, $page, $request->url(), $request->query());
$artist = ($request->has('artist') && ! empty($request->input('artist'))) ? stripslashes($request->input('artist')) : '';
$artistInput = $this->scalarInput($request, 'artist');
$artist = $artistInput !== '' ? stripslashes($artistInput) : '';
$title = ($request->has('title') && ! empty($request->input('title'))) ? stripslashes($request->input('title')) : '';
$titleInput = $this->scalarInput($request, 'title');
$title = $titleInput !== '' ? stripslashes($titleInput) : '';
$genres = $this->genreService->getGenres((string) GenreService::MUSIC_TYPE, true);
$tmpgnr = [];
@@ -74,15 +76,18 @@ class MusicController extends BasePageController
foreach ($results as $result) {
$res = $result;
$result->genre = $tmpgnr[$res->genres_id];
$result->genre = $tmpgnr[$res->genres_id] ?? '';
$musics[] = $result;
}
$genre = ($request->has('genre') && isset($tmpgnr[$request->input('genre')])) ? $request->input('genre') : '';
$genreInput = $this->scalarInput($request, 'genre');
$genre = isset($tmpgnr[$genreInput]) ? $genreInput : '';
$years = range(1950, date('Y') + 1);
rsort($years);
$year = ($request->has('year') && \in_array($request->input('year'), $years, true)) ? $request->input('year') : '';
$yearInput = $this->scalarInput($request, 'year');
$yearValue = is_numeric($yearInput) ? (int) $yearInput : null;
$year = $yearValue !== null && \in_array($yearValue, $years, true) ? $yearValue : '';
if ((int) $category === -1) {
$catname = 'All';
+57 -54
View File
@@ -28,14 +28,11 @@ class MyMoviesController extends BasePageController
public function show(Request $request): mixed
{
$action = $request->input('id') ?? '';
$imdbid = $request->input('imdb') ?? '';
$action = $this->scalarInput($request, 'id');
$imdbid = $this->scalarInput($request, 'imdb');
$from = $this->localReturnUrl($request, '/mymovies');
if ($request->has('from')) {
$this->viewData['from'] = url($request->input('from'));
} else {
$this->viewData['from'] = url('/mymovies');
}
$this->viewData['from'] = $from;
switch ($action) {
case 'delete':
@@ -44,13 +41,8 @@ class MyMoviesController extends BasePageController
return redirect()->to('/mymovies');
}
UserMovie::delMovie($this->userdata->id, $imdbid);
if ($request->has('from')) {
header('Location:'.url($request->input('from')));
} else {
return redirect()->to('/mymovies');
}
break;
return redirect()->to($from);
case 'add':
case 'doadd':
$movie = UserMovie::getMovie($this->userdata->id, $imdbid);
@@ -64,24 +56,13 @@ class MyMoviesController extends BasePageController
}
if ($action === 'doadd') {
$category = ($request->has('category') && \is_array($request->input('category')) && ! empty($request->input('category'))) ? $request->input('category') : [];
$category = $this->selectedCategoryIds($request, $this->movieCategories(true));
UserMovie::addMovie($this->userdata->id, $imdbid, $category);
if ($request->has('from')) {
return redirect()->to($request->input('from'));
}
return redirect()->to('/mymovies');
return redirect()->to($from);
}
$tmpcats = Category::getChildren(Category::MOVIE_ROOT);
$categories = [];
foreach ($tmpcats as $c) {
// If MOVIE WEB-DL categorization is disabled, don't include it as an option
if ((int) $c['id'] === Category::MOVIE_WEBDL && (int) Settings::settingValue('catwebdl') === 0) {
continue;
}
$categories[$c['id']] = $c['title'];
}
$categories = $this->movieCategories(true);
$this->viewData['type'] = 'add';
$this->viewData['cat_ids'] = array_keys($categories);
$this->viewData['cat_names'] = $categories;
@@ -102,20 +83,13 @@ class MyMoviesController extends BasePageController
}
if ($action === 'doedit') {
$category = ($request->has('category') && \is_array($request->input('category')) && ! empty($request->input('category'))) ? $request->input('category') : [];
$category = $this->selectedCategoryIds($request, $this->movieCategories(false));
UserMovie::updateMovie($this->userdata->id, $imdbid, $category);
if ($request->has('from')) {
return redirect()->to($request->input('from'));
}
return redirect()->to('mymovies');
return redirect()->to($from);
}
$tmpcats = Category::getChildren(Category::MOVIE_ROOT);
$categories = [];
foreach ($tmpcats as $c) {
$categories[$c['id']] = $c['title'];
}
$categories = $this->movieCategories(false);
$this->viewData['type'] = 'edit';
$this->viewData['cat_ids'] = array_keys($categories);
@@ -135,19 +109,19 @@ class MyMoviesController extends BasePageController
$meta_keywords = 'search,add,to,cart,nzb,description,details';
$meta_description = 'Browse Your Movies';
$page = $request->has('page') && is_numeric($request->input('page')) ? $request->input('page') : 1;
$offset = ($page - 1) * (int) config('nntmux.items_per_cover_page');
$page = $this->resolvePage($request);
$perPage = (int) config('nntmux.items_per_cover_page');
$offset = $this->paginationOffset($page, $perPage);
$movies = UserMovie::getMovies($this->userdata->id);
/** @var array<string, string> $categories */
$categories = $movie = [];
$categories = $this->movieCategories(false);
foreach ($movies as $moviek => $movie) {
$showcats = explode('|', $movie['categories']);
if (\count($showcats) > 0) {
$catarr = [];
foreach ($showcats as $scat) {
if (! empty($scat)) {
if (! empty($scat) && isset($categories[$scat])) {
$catarr[] = $categories[$scat];
}
}
@@ -155,13 +129,14 @@ class MyMoviesController extends BasePageController
} else {
$movie['categoryNames'] = '';
}
$movies[$moviek] = $movie;
}
$ordering = $request->input('ob', '');
$ordering = $this->movieBrowseService->getMovieOrdering();
$orderby = $this->resolveOrderBy($request, $ordering);
$page = $request->has('page') && is_numeric($request->input('page')) ? $request->input('page') : 1;
$results = $this->movieBrowseService->getMovieRange($page, [], $offset, (int) config('nntmux.items_per_cover_page'), $ordering, -1, (array) $this->userdata->categoryexclusions);
$results = $this->movieBrowseService->getMovieRange($page, [], $offset, $perPage, $orderby, -1, (array) $this->userdata->categoryexclusions);
$this->viewData['covgroup'] = '';
@@ -171,9 +146,10 @@ class MyMoviesController extends BasePageController
$this->viewData['lastvisit'] = $this->userdata->lastlogin;
$this->viewData['results'] = $results;
$this->viewData['resultsadd'] = $results;
$this->viewData['movies'] = true;
/** @var view-string $browseView */
$browseView = 'browse';
$browseView = 'browse.index';
$this->viewData['content'] = view($browseView, $this->viewData)->render();
$this->viewData = array_merge($this->viewData, compact('title', 'meta_title', 'meta_keywords', 'meta_description'));
@@ -186,11 +162,7 @@ class MyMoviesController extends BasePageController
$meta_keywords = 'search,add,to,cart,nzb,description,details';
$meta_description = 'Manage Your Movies';
$tmpcats = Category::getChildren(Category::MOVIE_ROOT);
$categories = [];
foreach ($tmpcats as $c) {
$categories[$c['id']] = $c['title'];
}
$categories = $this->movieCategories(false);
$movies = UserMovie::getMovies($this->userdata->id);
$results = [];
@@ -218,7 +190,38 @@ class MyMoviesController extends BasePageController
return $this->pagerender();
}
// Fallback return in case no case matches
return redirect()->to('/mymovies');
}
/**
* @return array<int, string>
*/
private function movieCategories(bool $excludeDisabledWebdl): array
{
$categories = [];
foreach (Category::getChildren(Category::MOVIE_ROOT) as $category) {
if ($excludeDisabledWebdl && (int) $category['id'] === Category::MOVIE_WEBDL && (int) Settings::settingValue('catwebdl') === 0) {
continue;
}
$categories[(int) $category['id']] = (string) $category['title'];
}
return $categories;
}
/**
* @param array<int, string> $allowedCategories
* @return array<int, int>
*/
private function selectedCategoryIds(Request $request, array $allowedCategories): array
{
$selected = [];
foreach ($this->arrayInput($request, 'category') as $categoryId) {
if (is_scalar($categoryId) && preg_match('/^\d+$/', (string) $categoryId) === 1 && isset($allowedCategories[(int) $categoryId])) {
$selected[] = (int) $categoryId;
}
}
return array_values(array_unique($selected));
}
}
+51 -49
View File
@@ -23,14 +23,11 @@ class MyShowsController extends BasePageController
public function show(Request $request): mixed
{
$action = $request->input('action') ?? '';
$videoId = $request->input('id') ?? '';
$action = $this->scalarInput($request, 'action');
$videoId = $this->scalarInput($request, 'id');
$from = $this->localReturnUrl($request, '/myshows');
if ($request->has('from')) {
$this->viewData['from'] = url($request->input('from'));
} else {
$this->viewData['from'] = url('/myshows');
}
$this->viewData['from'] = $from;
switch ($action) {
case 'delete':
@@ -40,13 +37,8 @@ class MyShowsController extends BasePageController
}
UserSerie::delShow($this->userdata->id, $videoId);
if ($request->has('from')) {
header('Location:'.url($request->input('from')));
} else {
return redirect()->to('myshows');
}
break;
return redirect()->to($from);
case 'add':
case 'doadd':
$show = UserSerie::getShow($this->userdata->id, $videoId);
@@ -60,24 +52,13 @@ class MyShowsController extends BasePageController
}
if ($action === 'doadd') {
$category = ($request->has('category') && \is_array($request->input('category')) && ! empty($request->input('category'))) ? $request->input('category') : [];
$category = $this->selectedCategoryIds($request, $this->showCategories(true));
UserSerie::addShow($this->userdata->id, $videoId, $category);
if ($request->has('from')) {
return redirect()->to($request->input('from'));
}
return redirect()->to('myshows');
return redirect()->to($from);
}
$tmpcats = Category::getChildren(Category::TV_ROOT);
$categories = [];
foreach ($tmpcats as $c) {
// If TV WEB-DL categorization is disabled, don't include it as an option
if ((int) $c['id'] === Category::TV_WEBDL && (int) Settings::settingValue('catwebdl') === 0) {
continue;
}
$categories[$c['id']] = $c['title'];
}
$categories = $this->showCategories(true);
$this->viewData['type'] = 'add';
$this->viewData['cat_ids'] = array_keys($categories);
$this->viewData['cat_names'] = $categories;
@@ -98,20 +79,13 @@ class MyShowsController extends BasePageController
}
if ($action === 'doedit') {
$category = ($request->has('category') && \is_array($request->input('category')) && ! empty($request->input('category'))) ? $request->input('category') : [];
$category = $this->selectedCategoryIds($request, $this->showCategories(false));
UserSerie::updateShow($this->userdata->id, $videoId, $category);
if ($request->has('from')) {
return redirect()->to($request->input('from'));
}
return redirect()->to('myshows');
return redirect()->to($from);
}
$tmpcats = Category::getChildren(Category::TV_ROOT);
$categories = [];
foreach ($tmpcats as $c) {
$categories[$c['id']] = $c['title'];
}
$categories = $this->showCategories(false);
$this->viewData['type'] = 'edit';
$this->viewData['cat_ids'] = array_keys($categories);
@@ -131,11 +105,7 @@ class MyShowsController extends BasePageController
$meta_keywords = 'search,add,to,cart,nzb,description,details';
$meta_description = 'Manage Your Shows';
$tmpcats = Category::getChildren(Category::TV_ROOT);
$categories = [];
foreach ($tmpcats as $c) {
$categories[$c['id']] = $c['title'];
}
$categories = $this->showCategories(false);
$shows = UserSerie::getShows($this->userdata->id);
$results = [];
@@ -165,8 +135,6 @@ class MyShowsController extends BasePageController
return $this->pagerender();
}
// Fallback return in case no case matches
return redirect()->to('/myshows');
}
/**
@@ -181,14 +149,15 @@ class MyShowsController extends BasePageController
$shows = UserSerie::getShows($this->userdata->id);
$page = $request->has('page') && is_numeric($request->input('page')) ? $request->input('page') : 1;
$offset = ($page - 1) * config('nntmux.items_per_page');
$page = $this->resolvePage($request);
$perPage = (int) config('nntmux.items_per_page');
$offset = $this->paginationOffset($page, $perPage);
$ordering = $this->releaseBrowseService->getBrowseOrdering();
$orderby = $request->has('ob') && \in_array($request->input('ob'), $ordering, true) ? $request->input('ob') : '';
$orderby = $this->resolveOrderBy($request, $ordering);
$browseCount = $shows ? $shows->count() : 0;
$rslt = $this->releaseBrowseService->getShowsRange($shows ?? [], $offset, config('nntmux.items_per_page'), $orderby, -1, (array) $this->userdata->categoryexclusions);
$results = $this->paginate($rslt ?? [], $browseCount, config('nntmux.items_per_page'), $page, $request->url(), $request->query());
$rslt = $this->releaseBrowseService->getShowsRange($shows ?? [], $offset, $perPage, $orderby, -1, (array) $this->userdata->categoryexclusions);
$results = $this->paginate($rslt ?? [], $browseCount, $perPage, $page, $request->url(), $request->query());
$this->viewData['covgroup'] = '';
@@ -205,4 +174,37 @@ class MyShowsController extends BasePageController
return $this->pagerender();
}
/**
* @return array<int, string>
*/
private function showCategories(bool $excludeDisabledWebdl): array
{
$categories = [];
foreach (Category::getChildren(Category::TV_ROOT) as $category) {
if ($excludeDisabledWebdl && (int) $category['id'] === Category::TV_WEBDL && (int) Settings::settingValue('catwebdl') === 0) {
continue;
}
$categories[(int) $category['id']] = (string) $category['title'];
}
return $categories;
}
/**
* @param array<int, string> $allowedCategories
* @return array<int, int>
*/
private function selectedCategoryIds(Request $request, array $allowedCategories): array
{
$selected = [];
foreach ($this->arrayInput($request, 'category') as $categoryId) {
if (is_scalar($categoryId) && preg_match('/^\d+$/', (string) $categoryId) === 1 && isset($allowedCategories[(int) $categoryId])) {
$selected[] = (int) $categoryId;
}
}
return array_values(array_unique($selected));
}
}
+6 -4
View File
@@ -41,8 +41,10 @@ class ProfileController extends BasePageController
$publicView = false;
if ($privileged || ! $privateProfiles) {
$altID = ($request->has('id') && (int) $request->input('id') >= 0) ? (int) $request->input('id') : false;
$altUsername = ($request->has('name') && $request->input('name') !== '') ? $request->input('name') : false;
$altIdInput = $this->scalarInput($request, 'id');
$altID = $altIdInput !== '' && preg_match('/^\d+$/', $altIdInput) === 1 ? (int) $altIdInput : false;
$altUsername = $this->scalarInput($request, 'name');
$altUsername = $altUsername !== '' ? $altUsername : false;
// If both 'id' and 'name' are specified, 'id' should take precedence.
if ($altID === false && $altUsername !== false) {
@@ -252,9 +254,9 @@ class ProfileController extends BasePageController
*/
public function destroy(Request $request, GdprErasureService $erasureService): Application|View|Factory|RedirectResponse|\Illuminate\Contracts\Foundation\Application
{
$userId = $request->input('id');
$userId = $this->integerInput($request, 'id');
if ($userId !== null && (int) $userId === $this->userdata->id && ! $this->userdata->hasRole('Admin')) {
if ($userId > 0 && $userId === $this->userdata->id && ! $this->userdata->hasRole('Admin')) {
$user = User::find($userId);
if ($user === null) {
return redirect()->to('profile');
+10 -2
View File
@@ -156,7 +156,8 @@ class RssController extends BasePageController
*/
public function categoryFeedRss(Request $request)
{
if ($request->missing('id')) {
$categoryInput = $this->scalarInput($request, 'id');
if ($categoryInput === '') {
return response()->json(['error' => 'Category ID is missing'], 403);
}
@@ -165,7 +166,14 @@ class RssController extends BasePageController
return $user;
}
$categoryId = explode(',', $request->input('id'));
$categoryId = array_values(array_filter(
array_map('trim', explode(',', $categoryInput)),
static fn (string $categoryId): bool => preg_match('/^-?\d+$/', $categoryId) === 1
));
if ($categoryId === []) {
return response()->json(['error' => 'Category ID is missing'], 403);
}
[$userShow, $userAnidb, $userAirDate, $userNum, $userLimit, $outputXML] = $this->parseCommonRssParams($request);
$relData = $this->rss->getRss($categoryId, $userShow, $userAnidb, $user['user_id'], $userAirDate, $userLimit, $userNum);
+81 -46
View File
@@ -5,11 +5,12 @@ declare(strict_types=1);
namespace App\Http\Controllers;
use App\Models\Category;
use App\Models\UsenetGroup;
use App\Services\Releases\ReleaseBrowseService;
use App\Services\Releases\ReleaseSearchService;
use App\Services\Search\Contracts\SearchServiceInterface;
use Illuminate\Http\Request;
use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\Cache;
class SearchController extends BasePageController
{
@@ -44,9 +45,10 @@ class SearchController extends BasePageController
}
$ordering = $this->releaseBrowseService->getBrowseOrdering();
$orderBy = ($request->has('ob') && \in_array($request->input('ob'), $ordering, true) ? $request->input('ob') : '');
$page = $request->has('page') && is_numeric($request->input('page')) ? $request->input('page') : 1;
$offset = ($page - 1) * config('nntmux.items_per_page');
$orderBy = $this->resolveOrderBy($request, $ordering);
$page = $this->resolvePage($request);
$perPage = (int) config('nntmux.items_per_page');
$offset = $this->paginationOffset($page, $perPage);
$subject = '';
$search = '';
@@ -58,15 +60,15 @@ class SearchController extends BasePageController
$searchString = [];
switch (true) {
case $request->filled('subject'):
$searchString['searchname'] = (string) $request->input('subject');
$searchString['searchname'] = $this->scalarInput($request, 'subject');
$subject = $searchString['searchname'];
break;
case $request->filled('id'):
$searchString['searchname'] = (string) $request->input('id');
$searchString['searchname'] = $this->scalarInput($request, 'id');
$id = $searchString['searchname'];
break;
case $request->filled('search'):
$searchString['searchname'] = (string) $request->input('search');
$searchString['searchname'] = $this->scalarInput($request, 'search');
$search = $searchString['searchname'];
break;
default:
@@ -88,14 +90,14 @@ class SearchController extends BasePageController
-1,
-1,
$offset,
(int) config('nntmux.items_per_page'),
$perPage,
$orderBy,
-1,
$this->userdata->categoryexclusions ?? [],
'basic',
$categoryID);
$results = $this->paginate($rslt ?? [], $rslt[0]->_totalrows ?? 0, config('nntmux.items_per_page'), $page, $request->url(), $request->query());
$results = $this->paginate($rslt ?? [], $rslt[0]->_totalrows ?? 0, $perPage, $page, $request->url(), $request->query());
$category = $categoryID;
} else {
$orderByUrls = [];
@@ -117,32 +119,32 @@ class SearchController extends BasePageController
];
foreach ($searchVars as $searchVarKey => $searchVar) {
$searchVars[$searchVarKey] = ($request->has($searchVarKey) ? (string) $request->input($searchVarKey) : '');
$searchVars[$searchVarKey] = $this->scalarInput($request, $searchVarKey);
}
// Map new form field names to old internal names
if ($request->has('minage')) {
$searchVars['searchadvdaysnew'] = (string) $request->input('minage');
$searchVars['searchadvdaysnew'] = $this->scalarInput($request, 'minage');
}
if ($request->has('maxage')) {
$searchVars['searchadvdaysold'] = (string) $request->input('maxage');
$searchVars['searchadvdaysold'] = $this->scalarInput($request, 'maxage');
}
if ($request->has('group')) {
$searchVars['searchadvgroups'] = (string) $request->input('group');
$searchVars['searchadvgroups'] = $this->scalarInput($request, 'group');
}
if ($request->has('minsize')) {
$searchVars['searchadvsizefrom'] = (string) $request->input('minsize');
$searchVars['searchadvsizefrom'] = $this->scalarInput($request, 'minsize');
}
if ($request->has('maxsize')) {
$searchVars['searchadvsizeto'] = (string) $request->input('maxsize');
$searchVars['searchadvsizeto'] = $this->scalarInput($request, 'maxsize');
}
// Map basic search field to advanced search when in advanced mode
if ($request->has('search') && $searchType === 'advanced') {
$searchVars['searchadvr'] = (string) $request->input('search');
$searchVars['searchadvr'] = $this->scalarInput($request, 'search');
}
// Map basic category field to advanced category when in advanced mode
if ($request->has('t') && $searchType === 'advanced') {
$searchVars['searchadvcat'] = (string) $request->input('t');
$searchVars['searchadvcat'] = implode(',', $this->resolveCategoryIdsFromRequest($request));
}
$searchVars['selectedgroup'] = $searchVars['searchadvgroups'];
@@ -150,22 +152,6 @@ class SearchController extends BasePageController
$searchVars['selectedsizefrom'] = $searchVars['searchadvsizefrom'];
$searchVars['selectedsizeto'] = $searchVars['searchadvsizeto'];
// Get spell correction suggestions if we have a search query but few/no results
$spellSuggestion = null;
$searchQuery = $search ?: $searchVars['searchadvr'];
if (! empty($searchQuery) && $this->searchService->isSuggestEnabled()) {
// Get suggestions from search service
$suggestions = $this->searchService->suggest($searchQuery);
if (! empty($suggestions)) {
// Sort by doc count descending to get best suggestion
usort($suggestions, fn ($a, $b) => $b['docs'] - $a['docs']);
// Only show suggestion if it's different from the query
if ($suggestions[0]['suggest'] !== $searchQuery) {
$spellSuggestion = $suggestions[0]['suggest'];
}
}
}
if ($searchType !== 'basic' && $request->missing('id') && $request->missing('subject') && $request->anyFilled(['searchadvr', 'searchadvsubject', 'searchadvfilename', 'searchadvposter', 'search'])) {
$orderByString = '';
foreach ($searchVars as $searchVarKey => $searchVar) {
@@ -173,7 +159,7 @@ class SearchController extends BasePageController
}
$orderByString = ltrim($orderByString, '&');
if ($request->filled('t')) {
$orderByString .= '&t='.urlencode((string) $request->input('t'));
$orderByString .= '&t='.urlencode(implode(',', $this->resolveCategoryIdsFromRequest($request)));
}
$orderByUrls = [];
@@ -196,7 +182,7 @@ class SearchController extends BasePageController
($searchVars['searchadvdaysnew'] === '' ? -1 : $searchVars['searchadvdaysnew']),
($searchVars['searchadvdaysold'] === '' ? -1 : $searchVars['searchadvdaysold']),
$offset,
(int) config('nntmux.items_per_page'),
$perPage,
$orderBy,
-1,
$this->userdata->categoryexclusions ?? [],
@@ -204,9 +190,12 @@ class SearchController extends BasePageController
$this->resolveCategoryIdsFromRequest($request)
);
$results = $this->paginate($rslt ?? [], $rslt[0]->_totalrows ?? 0, config('nntmux.items_per_page'), $page, $request->url(), $request->query());
$results = $this->paginate($rslt ?? [], $rslt[0]->_totalrows ?? 0, $perPage, $page, $request->url(), $request->query());
}
$suggestEnabled = $this->searchService->isSuggestEnabled();
$spellSuggestion = $this->resolveSpellSuggestion($search ?: $searchVars['searchadvr'], $results, $suggestEnabled);
$this->viewData = array_merge($this->viewData, $searchVars, $orderByUrls, [
'subject' => $subject,
'search' => $search,
@@ -214,21 +203,16 @@ class SearchController extends BasePageController
'category' => $category,
'covgroup' => '',
'lastvisit' => $lastvisit,
'sizelist' => [
-1 => '--Select--', 1 => '100MB', 2 => '250MB', 3 => '500MB', 4 => '1GB', 5 => '2GB',
6 => '3GB', 7 => '4GB', 8 => '8GB', 9 => '16GB', 10 => '32GB', 11 => '64GB',
],
'results' => $results,
'sadvanced' => $searchType !== 'basic',
'grouplist' => UsenetGroup::getGroupsForSelect(),
'catlist' => Category::getForSelect(),
'catlist' => $this->getCategorySelectOptions(),
'meta_title' => 'Search Nzbs',
'meta_keywords' => 'search,nzb,description,details',
'meta_description' => 'Search for Nzbs',
// Search enhanced features
'spellSuggestion' => $spellSuggestion,
'autocompleteEnabled' => $this->searchService->isAutocompleteEnabled(),
'suggestEnabled' => $this->searchService->isSuggestEnabled(),
'suggestEnabled' => $suggestEnabled,
]);
return view('search.index', $this->viewData);
@@ -239,12 +223,63 @@ class SearchController extends BasePageController
*/
private function resolveCategoryIdsFromRequest(Request $request): array
{
$raw = $request->input('t', $request->input('searchadvcat', ''));
$raw = $this->scalarInput($request, 't', $this->scalarInput($request, 'searchadvcat'));
if ($raw === null || $raw === '' || $raw === '-1') {
if ($raw === '' || $raw === '-1') {
return [-1];
}
return array_map('intval', explode(',', (string) $raw));
$categoryIds = [];
foreach (explode(',', $raw) as $categoryId) {
$categoryId = trim($categoryId);
if (preg_match('/^-?\d+$/', $categoryId) === 1) {
$categoryIds[] = (int) $categoryId;
}
}
return $categoryIds === [] ? [-1] : array_values(array_unique($categoryIds));
}
/**
* @param array<int, mixed>|LengthAwarePaginator<int, mixed> $results
*/
private function resolveSpellSuggestion(string $searchQuery, array|LengthAwarePaginator $results, bool $suggestEnabled): ?string
{
if ($searchQuery === '' || ! $suggestEnabled || $this->resultCount($results) > 3) {
return null;
}
$suggestions = $this->searchService->suggest($searchQuery);
if ($suggestions === []) {
return null;
}
usort($suggestions, static fn (array $a, array $b): int => (int) $b['docs'] <=> (int) $a['docs']);
return $suggestions[0]['suggest'] !== $searchQuery ? $suggestions[0]['suggest'] : null;
}
/**
* @param array<int, mixed>|LengthAwarePaginator<int, mixed> $results
*/
private function resultCount(array|LengthAwarePaginator $results): int
{
if ($results instanceof LengthAwarePaginator) {
return $results->total();
}
return count($results);
}
/**
* @return array<int, string>
*/
private function getCategorySelectOptions(): array
{
try {
return Cache::remember('search:category-select-options', now()->addMinutes(10), fn (): array => Category::getForSelect());
} catch (\Throwable) {
return Category::getForSelect();
}
}
}
+6 -6
View File
@@ -37,17 +37,17 @@ class SeriesController extends BasePageController
if ($id && ctype_digit($id)) {
$category = -1;
if ($request->has('t') && ctype_digit($request->input('t'))) {
$category = $request->input('t');
$categoryInput = $this->scalarInput($request, 't');
if ($categoryInput !== '' && ctype_digit($categoryInput)) {
$category = (int) $categoryInput;
}
$catarray = [];
$catarray[] = $category;
$seriesLimit = (int) config('nntmux.series_view_limit', 200);
$page = $request->has('page') && is_numeric($request->input('page')) ? (int) $request->input('page') : 1;
$page = max($page, 1);
$offset = $seriesLimit > 0 ? ($page - 1) * $seriesLimit : 0;
$page = $this->resolvePage($request);
$offset = $seriesLimit > 0 ? $this->paginationOffset($page, $seriesLimit) : 0;
$rel = $this->releaseSearchService->tvSearch(['id' => $id], '', '', '', $offset, $seriesLimit, '', $catarray, -1);
@@ -175,7 +175,7 @@ class SeriesController extends BasePageController
} else {
$letter = ($id && preg_match('/^(0-9|[A-Z])$/i', $id)) ? $id : '0-9';
$showname = ($request->has('title') && ! empty($request->input('title'))) ? $request->input('title') : '';
$showname = $this->scalarInput($request, 'title');
if ($showname !== '' && ! $id) {
$letter = '';
+2 -2
View File
@@ -41,7 +41,7 @@ class UserMovie extends Model
protected $dateFormat = false;
/**
* @param array<string, mixed> $catID
* @param array<int|string, mixed> $catID
* @return int|Builder
*/
public static function addMovie(mixed $uid, mixed $imdbid, array $catID = []) // @phpstan-ignore missingType.generics
@@ -97,7 +97,7 @@ class UserMovie extends Model
}
/**
* @param array<string, mixed> $catID
* @param array<int|string, mixed> $catID
*/
public static function updateMovie(mixed $uid, mixed $imdbid, array $catID = []): void
{
+2 -2
View File
@@ -54,7 +54,7 @@ class UserSerie extends Model
* When a user wants to add a show to "my shows" insert it into the user series table.
*
*
* @param array<string, mixed> $catID
* @param array<int|string, mixed> $catID
* @return int|Builder
*/
public static function addShow(mixed $userId, mixed $videoId, array $catID = []) // @phpstan-ignore missingType.generics
@@ -125,7 +125,7 @@ class UserSerie extends Model
/**
* Update a TV show category ID for a user's "my show" TV show.
*
* @param array<string, mixed> $catID List of category ID's.
* @param array<int|string, mixed> $catID List of category ID's.
*/
public static function updateShow(mixed $users_id, mixed $videos_id, array $catID = []): void
{
+11 -11
View File
@@ -164,25 +164,25 @@ class ReleaseBrowseService
// Browse only needs columns used in browse/index.blade.php and search/index.blade.php
$outerSelect = "SELECT r.id, r.searchname, r.guid, r.postdate, r.categories_id, r.size, r.totalpart, r.fromname, r.grabs, r.comments, r.adddate, r.videos_id, r.haspreview, r.jpgstatus, r.nfostatus, r.group_name,
CONCAT(cp.title, ' > ', c.title) AS category_name,
df.failed AS failed_count,
rr.total_report_count AS total_report_count,
rr.report_count AS report_count,
rr.report_reasons AS report_reasons,
rr.all_report_reasons AS all_report_reasons,
rr.latest_report_reason AS latest_report_reason,
rr.latest_report_description AS latest_report_description,
rr.response_count AS report_response_count,
rr.latest_response_at AS latest_report_response_at,
MAX(df.failed) AS failed_count,
COUNT(DISTINCT rr.id) AS total_report_count,
COUNT(DISTINCT CASE WHEN rr.status IN ('pending', 'reviewed', 'resolved') THEN rr.id ELSE NULL END) AS report_count,
GROUP_CONCAT(DISTINCT CASE WHEN rr.status IN ('pending', 'reviewed', 'resolved') THEN rr.reason ELSE NULL END SEPARATOR ', ') AS report_reasons,
GROUP_CONCAT(DISTINCT rr.reason SEPARATOR ', ') AS all_report_reasons,
SUBSTRING_INDEX(GROUP_CONCAT(rr.reason ORDER BY rr.created_at DESC SEPARATOR ','), ',', 1) AS latest_report_reason,
SUBSTRING_INDEX(GROUP_CONCAT(COALESCE(NULLIF(rr.description, ''), 'No additional report details were provided.') ORDER BY rr.created_at DESC SEPARATOR ' || '), ' || ', 1) AS latest_report_description,
COUNT(DISTINCT CASE WHEN rr.response IS NOT NULL AND rr.response != '' AND rr.response_is_public = 1 THEN rr.id ELSE NULL END) AS report_response_count,
MAX(CASE WHEN rr.response IS NOT NULL AND rr.response != '' AND rr.response_is_public = 1 THEN rr.responded_at ELSE NULL END) AS latest_report_response_at,
rn.releases_id AS nfoid,
re.releases_id AS reid,
m.imdbid";
$outerJoins = "LEFT JOIN categories c ON c.id = r.categories_id
$outerJoins = '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 movieinfo m ON m.id = r.movieinfo_id
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 total_report_count, SUM(CASE WHEN status IN ('pending', 'reviewed', 'resolved') THEN 1 ELSE 0 END) AS report_count, GROUP_CONCAT(DISTINCT CASE WHEN status IN ('pending', 'reviewed', 'resolved') THEN reason ELSE NULL END SEPARATOR ', ') AS report_reasons, GROUP_CONCAT(DISTINCT reason SEPARATOR ', ') AS all_report_reasons, SUBSTRING_INDEX(GROUP_CONCAT(reason ORDER BY created_at DESC SEPARATOR ','), ',', 1) AS latest_report_reason, SUBSTRING_INDEX(GROUP_CONCAT(COALESCE(NULLIF(description, ''), 'No additional report details were provided.') ORDER BY created_at DESC SEPARATOR ' || '), ' || ', 1) AS latest_report_description, SUM(CASE WHEN response IS NOT NULL AND response != '' AND response_is_public = 1 THEN 1 ELSE 0 END) AS response_count, MAX(CASE WHEN response IS NOT NULL AND response != '' AND response_is_public = 1 THEN responded_at ELSE NULL END) AS latest_response_at FROM release_reports GROUP BY releases_id) rr ON rr.releases_id = r.id";
LEFT OUTER JOIN release_reports rr ON rr.releases_id = r.id';
$innerSelect = 'SELECT r.id, r.searchname, r.guid, r.postdate, r.groups_id, r.categories_id, r.size, r.totalpart, r.fromname, r.passwordstatus, r.grabs, r.comments, r.adddate, r.videos_id, r.haspreview, r.jpgstatus, r.nfostatus, g.name AS group_name, r.movieinfo_id';
}
+15 -12
View File
@@ -1900,15 +1900,15 @@ class ReleaseSearchService
r.totalpart, r.fromname, r.grabs, r.comments, r.adddate,
r.videos_id, r.haspreview, r.jpgstatus, r.nfostatus,
CONCAT(cp.title, ' > ', c.title) AS category_name,
df.failed AS failed_count,
rr.total_report_count AS total_report_count,
rr.report_count AS report_count,
rr.report_reasons AS report_reasons,
rr.all_report_reasons AS all_report_reasons,
rr.latest_report_reason AS latest_report_reason,
rr.latest_report_description AS latest_report_description,
rr.response_count AS report_response_count,
rr.latest_response_at AS latest_report_response_at,
MAX(df.failed) AS failed_count,
COUNT(DISTINCT rr.id) AS total_report_count,
COUNT(DISTINCT CASE WHEN rr.status IN ('pending', 'reviewed', 'resolved') THEN rr.id ELSE NULL END) AS report_count,
GROUP_CONCAT(DISTINCT CASE WHEN rr.status IN ('pending', 'reviewed', 'resolved') THEN rr.reason ELSE NULL END SEPARATOR ', ') AS report_reasons,
GROUP_CONCAT(DISTINCT rr.reason SEPARATOR ', ') AS all_report_reasons,
SUBSTRING_INDEX(GROUP_CONCAT(rr.reason ORDER BY rr.created_at DESC SEPARATOR ','), ',', 1) AS latest_report_reason,
SUBSTRING_INDEX(GROUP_CONCAT(COALESCE(NULLIF(rr.description, ''), 'No additional report details were provided.') ORDER BY rr.created_at DESC SEPARATOR ' || '), ' || ', 1) AS latest_report_description,
COUNT(DISTINCT CASE WHEN rr.response IS NOT NULL AND rr.response != '' AND rr.response_is_public = 1 THEN rr.id ELSE NULL END) AS report_response_count,
MAX(CASE WHEN rr.response IS NOT NULL AND rr.response != '' AND rr.response_is_public = 1 THEN rr.responded_at ELSE NULL END) AS latest_report_response_at,
g.name AS group_name,
rn.releases_id AS nfoid,
re.releases_id AS reid,
@@ -1921,8 +1921,9 @@ 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 total_report_count, SUM(CASE WHEN status IN ('pending', 'reviewed', 'resolved') THEN 1 ELSE 0 END) AS report_count, GROUP_CONCAT(DISTINCT CASE WHEN status IN ('pending', 'reviewed', 'resolved') THEN reason ELSE NULL END SEPARATOR ', ') AS report_reasons, GROUP_CONCAT(DISTINCT reason SEPARATOR ', ') AS all_report_reasons, SUBSTRING_INDEX(GROUP_CONCAT(reason ORDER BY created_at DESC SEPARATOR ','), ',', 1) AS latest_report_reason, SUBSTRING_INDEX(GROUP_CONCAT(COALESCE(NULLIF(description, ''), 'No additional report details were provided.') ORDER BY created_at DESC SEPARATOR ' || '), ' || ', 1) AS latest_report_description, SUM(CASE WHEN response IS NOT NULL AND response != '' AND response_is_public = 1 THEN 1 ELSE 0 END) AS response_count, MAX(CASE WHEN response IS NOT NULL AND response != '' AND response_is_public = 1 THEN responded_at ELSE NULL END) AS latest_response_at FROM release_reports GROUP BY releases_id) rr ON rr.releases_id = r.id
%s",
LEFT OUTER JOIN release_reports rr ON rr.releases_id = r.id
%s
GROUP BY r.id",
$whereSql
);
}
@@ -2073,6 +2074,8 @@ class ReleaseSearchService
// Remove ORDER BY clause (not needed for COUNT)
$countQuery = preg_replace('/ORDER\s+BY\s+[^)]+$/is', '', $countQuery);
$hadGroupBy = preg_match('/GROUP\s+BY/is', $countQuery);
// Remove GROUP BY if it's only grouping by r.id
$countQuery = preg_replace('/GROUP\s+BY\s+r\.id\s*$/is', '', $countQuery);
@@ -2080,7 +2083,7 @@ class ReleaseSearchService
$hasDistinct = preg_match('/SELECT\s+DISTINCT/is', $countQuery);
// Replace SELECT clause with COUNT
if ($hasDistinct || preg_match('/GROUP\s+BY/is', $countQuery)) {
if ($hasDistinct || $hadGroupBy || preg_match('/GROUP\s+BY/is', $countQuery)) {
// For queries with DISTINCT or GROUP BY, count distinct r.id
$countQuery = preg_replace(
'/SELECT\s+.+?\s+FROM/is',
+10 -22
View File
@@ -73,27 +73,16 @@
<!-- Results -->
@if(count($results) > 0)
<div class="mb-4 flex flex-wrap justify-between items-center gap-4">
<div class="flex items-center gap-4">
<h2 class="text-xl font-semibold text-gray-800 dark:text-gray-200">
<i class="fa fa-book mr-2 text-blue-600"></i>
{{ $catname ?? 'All' }} Books
</h2>
<x-view-toggle
current-view="covers"
covgroup="books"
:category="$categorytitle ?? 'All'"
parentcat="Books"
:shows="false"
/>
</div>
<div class="flex items-center gap-4">
<x-inline-search placeholder="Search in Books..." :category="$category ?? null" />
<span class="text-sm text-gray-600 dark:text-gray-400">
{{ $results->total() }} results found
</span>
</div>
</div>
<x-cover-results-toolbar
:results="$results"
icon="fa fa-book"
:title="($catname ?? 'All') . ' Books'"
covgroup="books"
:category="$categorytitle ?? 'All'"
parentcat="Books"
search-placeholder="Search in Books..."
:search-category="$category ?? null"
/>
<!-- Books Grid -->
<div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 xl:grid-cols-6 gap-4 mb-6">
@@ -173,4 +162,3 @@
</div>
</div>
@endsection
+36 -308
View File
@@ -24,317 +24,46 @@
<x-breadcrumb :items="$crumbs" />
@if($results->count() > 0)
<form id="nzb_multi_operations_form" method="get" x-data="releaseMultiOps" data-show-thumbs="{{ request()->query('thumbs', '0') === '1' ? '1' : '0' }}">
<div class="px-6 py-4 surface-panel-alt border-b">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
<!-- Left Section -->
<div class="space-y-3">
@if(isset($shows))
<div class="flex flex-wrap gap-2 text-sm">
<a href="{{ route('series') }}" class="text-primary-600 dark:text-primary-400 hover:text-primary-800 dark:hover:text-primary-300" title="View available TV series">Series List</a>
<span class="text-gray-400 dark:text-gray-500">|</span>
<a href="{{ route('trending-tv') }}" class="text-orange-600 dark:text-orange-400 hover:text-orange-800 dark:hover:text-orange-300" title="View trending TV shows"><i class="fas fa-fire mr-1"></i>Trending TV</a>
<span class="text-gray-400 dark:text-gray-500">|</span>
<a href="{{ route('myshows') }}" class="text-primary-600 dark:text-primary-400 hover:text-primary-800 dark:hover:text-primary-300" title="Manage your shows">Manage My Shows</a>
<span class="text-gray-400 dark:text-gray-500">|</span>
<a href="{{ url('/rss/myshows?dl=1&i=' . auth()->id() . '&api_token=' . auth()->user()->api_token) }}" class="text-primary-600 dark:text-primary-400 hover:text-primary-800 dark:hover:text-primary-300" title="RSS Feed">RSS Feed</a>
</div>
@endif
@php
$searchPlaceholder = 'Search';
if (!empty($parentcat) && $parentcat !== 'All') {
$searchPlaceholder .= ' in ' . $parentcat;
if (!empty($catname) && $catname !== 'All' && $catname !== 'all') {
$searchPlaceholder .= ' ' . $catname;
}
}
$searchPlaceholder .= '...';
@endphp
@if(isset($covgroup) && $covgroup != '' || isset($shows) && $shows)
<x-view-toggle
current-view="list"
:covgroup="$covgroup ?? null"
:category="$catname ?? 'All'"
:parentcat="$parentcat ?? null"
:shows="$shows ?? false"
/>
@endif
<div class="flex flex-wrap items-center gap-2">
<small class="text-gray-600 dark:text-gray-400">With Selected:</small>
<div class="flex gap-1">
<button type="button" class="nzb_multi_operations_download px-3 py-1 bg-green-600 dark:bg-green-700 text-white rounded-lg hover:bg-green-700 dark:hover:bg-green-800 transition text-sm" title="Download NZBs">
<i class="fa fa-cloud-download"></i>
</button>
<button type="button" class="nzb_multi_operations_cart px-3 py-1 bg-primary-600 dark:bg-primary-700 text-white rounded-lg hover:bg-primary-700 dark:hover:bg-primary-800 transition text-sm" title="Send to Download Basket">
<i class="fa fa-shopping-basket"></i>
</button>
@if(auth()->check() && auth()->user()->hasRole('Admin'))
<button type="button" class="nzb_multi_operations_delete px-3 py-1 bg-red-600 dark:bg-red-700 text-white rounded-lg hover:bg-red-700 dark:hover:bg-red-800 transition text-sm" title="Delete">
<i class="fa fa-trash"></i>
</button>
@endif
</div>
</div>
<x-release-results-panel :results="$results" :show-thumbs="true" date-field="adddate" :show-top-pagination="true">
<x-slot:beforeActions>
@if(isset($shows))
<div class="flex flex-wrap gap-2 text-sm">
<a href="{{ route('series') }}" class="text-primary-600 dark:text-primary-400 hover:text-primary-800 dark:hover:text-primary-300" title="View available TV series">Series List</a>
<span class="text-gray-400 dark:text-gray-500">|</span>
<a href="{{ route('trending-tv') }}" class="text-orange-600 dark:text-orange-400 hover:text-orange-800 dark:hover:text-orange-300" title="View trending TV shows"><i class="fas fa-fire mr-1"></i>Trending TV</a>
<span class="text-gray-400 dark:text-gray-500">|</span>
<a href="{{ route('myshows') }}" class="text-primary-600 dark:text-primary-400 hover:text-primary-800 dark:hover:text-primary-300" title="Manage your shows">Manage My Shows</a>
<span class="text-gray-400 dark:text-gray-500">|</span>
<a href="{{ url('/rss/myshows?dl=1&i=' . auth()->id() . '&api_token=' . auth()->user()->api_token) }}" class="text-primary-600 dark:text-primary-400 hover:text-primary-800 dark:hover:text-primary-300" title="RSS Feed">RSS Feed</a>
</div>
@endif
<!-- Center Section - Pagination Info -->
<div class="flex items-center justify-center">
<div class="text-sm text-gray-600 dark:text-gray-400">
Showing {{ $results->firstItem() }} to {{ $results->lastItem() }} of {{ $results->total() }} results
</div>
</div>
@if(isset($covgroup) && $covgroup != '' || isset($shows) && $shows)
<x-view-toggle
current-view="list"
:covgroup="$covgroup ?? null"
:category="$catname ?? 'All'"
:parentcat="$parentcat ?? null"
:shows="$shows ?? false"
/>
@endif
</x-slot:beforeActions>
<!-- Right Section - Sort & Search -->
<div class="flex items-center justify-end gap-3">
@php
$searchPlaceholder = 'Search';
if (!empty($parentcat) && $parentcat !== 'All') {
$searchPlaceholder .= ' in ' . $parentcat;
if (!empty($catname) && $catname !== 'All' && $catname !== 'all') {
$searchPlaceholder .= ' ' . $catname;
}
}
$searchPlaceholder .= '...';
@endphp
<x-inline-search :placeholder="$searchPlaceholder" :category="$category ?? null" />
<x-sort-dropdown />
</div>
</div>
</div>
<!-- Top Pagination -->
<div class="px-6 py-3 surface-panel-alt border-b">
{{ $results->links() }}
</div>
<!-- Results Table (Desktop) -->
<div class="hidden md:block overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-100 dark:bg-gray-800 dark:bg-gray-900">
<tr>
<th class="px-3 py-3 text-left">
<input type="checkbox" class="rounded border-gray-300 dark:border-gray-600 text-primary-600 dark:text-primary-500 focus:ring-primary-500 dark:focus:ring-primary-400 dark:bg-gray-700" id="chkSelectAll" x-model="allChecked" @change="toggleAll()">
</th>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">Name</th>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">Category</th>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">Added</th>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">Size</th>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">Files</th>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">Stats</th>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">Action</th>
</tr>
</thead>
<tbody class="surface-panel divide-y divide-gray-200 dark:divide-gray-700">
@foreach($results as $result)
<tr class="hover:bg-gray-50 dark:bg-gray-900 dark:hover:bg-gray-700 transition">
<td class="px-3 py-4 whitespace-nowrap">
<input type="checkbox" class="chkRelease rounded border-gray-300 dark:border-gray-600 text-primary-600 dark:text-primary-500 focus:ring-primary-500 dark:focus:ring-primary-400 dark:bg-gray-700" name="release[]" value="{{ $result->guid }}" @change="onCheckboxChange()">
</td>
<td class="px-3 py-4">
<div class="flex items-start">
@php
$coverUrl = ($result->cover ?? false) ? $result->cover : getReleaseCover($result);
$hasValidCover = $coverUrl && !str_contains($coverUrl, 'no-cover.png');
@endphp
@if($hasValidCover)
<a href="{{ url('/details/' . $result->guid) }}" class="shrink-0 bg-gray-100 dark:bg-gray-700 rounded mr-3" x-show="showThumbs" @unless(request()->query('thumbs') === '1') style="display:none" @endunless>
<img src="{{ request()->query('thumbs') === '1' ? $coverUrl : '' }}" x-bind:src="showThumbs ? '{{ $coverUrl }}' : ''" class="w-12 h-16 object-cover rounded shadow-sm hover:shadow-md transition" alt="Cover" loading="lazy">
</a>
@endif
<div class="flex-1">
<div class="flex items-center gap-2 flex-wrap">
<a href="{{ url('/details/' . $result->guid) }}" class="text-primary-600 dark:text-primary-400 hover:text-primary-800 dark:hover:text-primary-300 font-medium wrap-break-word break-all">{{ $result->searchname }}</a>
@if(!empty($result->total_report_count) && $result->total_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: {{ $result->all_report_reasons ?? 'Unknown' }} | Original report: {{ $result->latest_report_reason ?? 'Unknown' }} - {{ $result->latest_report_description ?? 'No additional report details were provided.' }}">
<i class="fas fa-flag mr-1"></i> Reported ({{ $result->total_report_count }})
</span>
@endif
@if(!empty($result->report_response_count) && $result->report_response_count > 0)
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200"
title="Staff response available on release details">
<i class="fas fa-reply mr-1"></i> Response
</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">
<i class="fas fa-exclamation-triangle mr-1"></i> Failed ({{ $result->failed_count }})
</span>
@endif
@if(isset($result->haspreview) && $result->haspreview == 1)
<button type="button"
class="preview-badge inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200 hover:bg-purple-200 dark:hover:bg-purple-800 transition cursor-pointer"
data-guid="{{ $result->guid }}"
title="View preview image">
<i class="fas fa-image mr-1"></i> Preview
</button>
@endif
@if(isset($result->jpgstatus) && $result->jpgstatus == 1)
<button type="button"
class="sample-badge inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 hover:bg-green-200 dark:hover:bg-green-800 transition cursor-pointer"
data-guid="{{ $result->guid }}"
title="View sample image">
<i class="fas fa-images mr-1"></i> Sample
</button>
@endif
@if(!empty($result->videos_id) && (int)$result->videos_id > 0)
<a href="{{ url('/series/' . $result->videos_id) }}"
class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-indigo-100 dark:bg-indigo-900 text-indigo-800 dark:text-indigo-200 hover:bg-indigo-200 dark:hover:bg-indigo-800 transition"
title="View full series">
<i class="fas fa-tv mr-1"></i> View Series
</a>
@endif
@if(isset($result->reid) && $result->reid != null)
<button type="button"
class="mediainfo-badge inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-primary-100 dark:bg-primary-900/50 text-primary-800 dark:text-primary-200 hover:bg-primary-200 dark:hover:bg-primary-800 transition cursor-pointer"
data-release-id="{{ $result->id }}"
title="View media info">
<i class="fas fa-info-circle mr-1"></i> Media Info
</button>
@endif
@if(isset($result->nfostatus) && $result->nfostatus == 1)
<button type="button"
class="nfo-badge inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200 hover:bg-yellow-200 dark:hover:bg-yellow-800 transition cursor-pointer"
data-guid="{{ $result->guid }}"
title="View NFO file">
<i class="fas fa-file-alt mr-1"></i> NFO
</button>
@endif
</div>
<div class="text-xs text-gray-500 dark:text-gray-400 mt-1 flex flex-wrap gap-2">
@if($result->group_name)
<span class="inline-flex items-center px-2 py-0.5 rounded bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300">
<i class="fas fa-users mr-1"></i> {{ $result->group_name }}
</span>
@endif
@if(!empty($result->postdate))
<span class="inline-flex items-center px-2 py-0.5 rounded bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200">
<i class="fas fa-calendar mr-1"></i> Posted: {{ userDate($result->postdate, 'M d, Y H:i') }}
</span>
@endif
@if(!empty($result->fromname))
<span class="inline-flex items-center px-2 py-0.5 rounded bg-indigo-100 dark:bg-indigo-900 text-indigo-800 dark:text-indigo-200 font-mono">
<i class="fas fa-user mr-1"></i> {{ $result->fromname }}
</span>
@endif
</div>
</div>
</div>
</td>
<td class="px-3 py-4 whitespace-nowrap">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-primary-100 dark:bg-primary-900/50 text-primary-800 dark:text-primary-200">
{{ $result->category_name ?? 'Other' }}
</span>
</td>
<td class="px-3 py-4 whitespace-nowrap text-sm text-gray-600 dark:text-gray-400">
{{ userDateDiffForHumans($result->adddate) }}
</td>
<td class="px-3 py-4 whitespace-nowrap text-sm text-gray-600 dark:text-gray-400">
{{ $result->size_formatted ?? number_format($result->size / 1073741824, 2) . ' GB' }}
</td>
<td class="px-3 py-4 whitespace-nowrap text-sm text-gray-600 dark:text-gray-400">
@if($result->totalpart > 0)
<button type="button"
class="filelist-badge text-primary-600 dark:text-primary-400 hover:text-primary-800 dark:hover:text-primary-300 font-medium cursor-pointer hover:underline"
data-guid="{{ $result->guid }}"
title="View file list">
{{ $result->totalpart ?? 0 }}
</button>
@else
{{ $result->totalpart ?? 0 }}
@endif
</td>
<td class="px-3 py-4 whitespace-nowrap text-sm text-gray-600 dark:text-gray-400">
<div class="flex items-center gap-2">
<span title="Grabs"><i class="fas fa-download text-green-600 dark:text-green-400"></i> {{ $result->grabs ?? 0 }}</span>
<span title="Comments"><i class="fas fa-comment text-primary-600 dark:text-primary-400"></i> {{ $result->comments ?? 0 }}</span>
</div>
</td>
<td class="px-3 py-4 whitespace-nowrap">
<div class="flex items-center gap-1 flex-wrap">
<a href="{{ url('/getnzb/' . $result->guid) }}" class="download-nzb px-2 py-1 bg-green-600 dark:bg-green-700 text-white rounded-lg hover:bg-green-700 dark:hover:bg-green-800 transition text-sm" title="Download NZB">
<i class="fa fa-download"></i>
</a>
<a href="{{ url('/details/' . $result->guid) }}" class="px-2 py-1 bg-primary-600 dark:bg-primary-700 text-white rounded-lg hover:bg-primary-700 dark:hover:bg-primary-800 transition text-sm" title="View Details">
<i class="fa fa-info"></i>
</a>
<a href="#" class="add-to-cart px-2 py-1 bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-300 dark:hover:bg-gray-500 transition text-sm" data-guid="{{ $result->guid }}" title="Add to Cart">
<i class="icon_cart fa fa-shopping-basket"></i>
</a>
@if(!empty($result->imdbid) && imdb_id_is_valid($result->imdbid))
<a href="{{ url('/mymovies?id=add&imdb=' . $result->imdbid) }}"
class="px-2 py-1 bg-purple-600 dark:bg-purple-700 text-white rounded-lg hover:bg-purple-700 dark:hover:bg-purple-800 transition text-sm"
title="Add to My Movies">
<i class="fa fa-film"></i>
</a>
@endif
<x-report-button :release-id="$result->id" :reported-count="$result->total_report_count ?? 0" variant="icon" />
</div>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
<!-- Mobile Card View -->
<div class="md:hidden space-y-3 px-4 py-4">
@foreach($results as $result)
<div class="surface-panel border rounded-xl p-4 hover:shadow-md transition">
<div class="flex items-start gap-3">
<input type="checkbox" class="chkRelease rounded border-gray-300 dark:border-gray-600 text-primary-600 dark:text-primary-500 focus:ring-primary-500 dark:bg-gray-700 mt-1" name="release[]" value="{{ $result->guid }}" @change="onCheckboxChange()">
<div class="flex-1 min-w-0">
@php
$mCoverUrl = ($result->cover ?? false) ? $result->cover : getReleaseCover($result);
$mHasCover = $mCoverUrl && !str_contains($mCoverUrl, 'no-cover.png');
@endphp
@if($mHasCover)
<a href="{{ url('/details/' . $result->guid) }}" class="block mb-2 bg-gray-100 dark:bg-gray-700 rounded-lg" x-show="showThumbs" @unless(request()->query('thumbs') === '1') style="display:none" @endunless>
<img src="{{ request()->query('thumbs') === '1' ? $mCoverUrl : '' }}" x-bind:src="showThumbs ? '{{ $mCoverUrl }}' : ''" class="w-16 h-20 object-cover rounded-lg shadow-sm" alt="Cover" loading="lazy">
</a>
@endif
<a href="{{ url('/details/' . $result->guid) }}" class="text-primary-600 dark:text-primary-400 hover:text-primary-800 dark:hover:text-primary-300 font-medium wrap-break-word text-base">
{{ $result->searchname }}
</a>
<div class="flex flex-wrap items-center gap-2 mt-2">
@if(!empty($result->total_report_count) && $result->total_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: {{ $result->all_report_reasons ?? 'Unknown' }} | Original report: {{ $result->latest_report_reason ?? 'Unknown' }} - {{ $result->latest_report_description ?? 'No additional report details were provided.' }}">
<i class="fas fa-flag mr-1"></i> Reported ({{ $result->total_report_count }})
</span>
@endif
@if(!empty($result->report_response_count) && $result->report_response_count > 0)
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200"
title="Staff response available on release details">
<i class="fas fa-reply mr-1"></i> Response
</span>
@endif
</div>
<div class="flex flex-wrap items-center gap-2 mt-2 text-sm text-gray-600 dark:text-gray-400">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-primary-100 dark:bg-primary-900/50 text-primary-800 dark:text-primary-200">
{{ $result->category_name ?? 'Other' }}
</span>
<span><i class="fas fa-clock mr-1"></i>{{ userDateDiffForHumans($result->adddate) }}</span>
<span><i class="fas fa-hdd mr-1"></i>{{ $result->size_formatted ?? number_format($result->size / 1073741824, 2) . ' GB' }}</span>
<span><i class="fas fa-file mr-1"></i>{{ $result->totalpart ?? 0 }} files</span>
<span title="Grabs"><i class="fas fa-download text-green-600 dark:text-green-400 mr-1"></i>{{ $result->grabs ?? 0 }}</span>
</div>
<div class="mt-3 flex gap-1 flex-wrap">
<a href="{{ url('/getnzb/' . $result->guid) }}" class="download-nzb px-3 py-1.5 bg-green-600 dark:bg-green-700 text-white rounded-lg hover:bg-green-700 dark:hover:bg-green-800 transition text-sm" title="Download NZB">
<i class="fa fa-download"></i>
</a>
<a href="{{ url('/details/' . $result->guid) }}" class="px-3 py-1.5 bg-primary-600 dark:bg-primary-700 text-white rounded-lg hover:bg-primary-700 dark:hover:bg-primary-800 transition text-sm" title="View Details">
<i class="fa fa-info"></i>
</a>
<a href="#" class="add-to-cart px-3 py-1.5 bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600 transition text-sm" data-guid="{{ $result->guid }}" title="Add to Cart">
<i class="icon_cart fa fa-shopping-basket"></i>
</a>
<x-report-button :release-id="$result->id" :reported-count="$result->total_report_count ?? 0" variant="icon" />
</div>
</div>
</div>
</div>
@endforeach
</div>
<!-- Bottom Pagination -->
<div class="px-6 py-3 surface-panel-alt border-t">
{{ $results->links() }}
</div>
</form>
<x-slot:toolbarRight>
<x-inline-search :placeholder="$searchPlaceholder" :category="$category ?? null" />
</x-slot:toolbarRight>
</x-release-results-panel>
@else
<x-empty-state
icon="fas fa-search"
@@ -346,4 +75,3 @@
{{-- All modals (preview, mediainfo, filelist, NFO) are included globally via layouts.main --}}
</div>
@endsection
@@ -0,0 +1,100 @@
@props([
'releases',
'totalReleases' => null,
'max' => 2,
'showCheckbox' => false,
'showAddDate' => false,
'showGroup' => false,
'showStats' => false,
])
@php
$releaseItems = $releases instanceof \Illuminate\Support\Collection ? $releases->all() : (array) $releases;
$maxReleases = (int) $max;
$displayReleases = array_slice($releaseItems, 0, $maxReleases);
$totalReleaseCount = $totalReleases ?? count($releaseItems);
$shouldShowCheckbox = filter_var($showCheckbox, FILTER_VALIDATE_BOOL);
$shouldShowAddDate = filter_var($showAddDate, FILTER_VALIDATE_BOOL);
$shouldShowGroup = filter_var($showGroup, FILTER_VALIDATE_BOOL);
$shouldShowStats = filter_var($showStats, FILTER_VALIDATE_BOOL);
@endphp
@if(!empty($displayReleases))
<div class="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">
Available Releases
@if($totalReleaseCount > $maxReleases)
<span class="text-xs font-normal text-gray-500">(Showing {{ $maxReleases }} of {{ $totalReleaseCount }})</span>
@endif
</h4>
<div class="space-y-2">
@foreach($displayReleases as $release)
@if(!empty($release->searchname))
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-2 border border-gray-200 dark:border-gray-700">
<div class="space-y-2">
<div class="{{ $shouldShowCheckbox ? 'flex items-start justify-between gap-2' : '' }}">
<a href="{{ url('/details/' . $release->guid) }}" class="text-sm text-gray-800 dark:text-gray-200 hover:text-blue-600 dark:hover:text-blue-400 font-medium block break-all {{ $shouldShowCheckbox ? 'flex-1' : '' }}" title="{{ $release->searchname }}">
{{ $release->searchname }}
</a>
@if($shouldShowCheckbox)
<label class="inline-flex items-center shrink-0">
<input type="checkbox" class="chkRelease form-checkbox h-4 w-4 text-blue-600" value="{{ $release->guid }}" name="release[]" @change="onCheckboxChange()">
</label>
@endif
</div>
<div class="flex flex-wrap items-center gap-1.5">
@if(isset($release->size))
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300">
<i class="fas fa-hdd mr-1"></i>{{ number_format($release->size / 1073741824, 2) }} GB
</span>
@endif
@if(isset($release->postdate))
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300">
<i class="fas fa-calendar-alt mr-1"></i>{{ date('M d, Y H:i', strtotime($release->postdate)) }}
</span>
@endif
@if($shouldShowAddDate && isset($release->adddate))
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300">
<i class="fas fa-plus-circle mr-1"></i>{{ userDateDiffForHumans($release->adddate) }}
</span>
@endif
@if((isset($release->nfoid) && !empty($release->nfoid)) || (isset($release->nfostatus) && (int) $release->nfostatus === 1))
<button type="button"
class="nfo-badge inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200 hover:bg-yellow-200 dark:hover:bg-yellow-800 transition cursor-pointer"
data-guid="{{ $release->guid }}"
title="View NFO file">
<i class="fas fa-file-alt mr-1"></i> NFO
</button>
@endif
@if($shouldShowGroup && isset($release->group_name) && !empty($release->group_name))
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-indigo-100 dark:bg-indigo-900 text-indigo-800 dark:text-indigo-200" title="Poster/Uploader">
<i class="fas fa-user mr-1"></i> {{ $release->group_name }}
</span>
@endif
</div>
<div class="flex flex-wrap items-center gap-1.5">
<a href="{{ url('/getnzb/' . $release->guid) }}" class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-600 dark:bg-green-700 text-white hover:bg-green-700 dark:hover:bg-green-800 transition">
<i class="fas fa-download mr-1"></i> Download
@if($shouldShowStats && isset($release->grabs) && $release->grabs > 0)
<span class="ml-1 px-1 bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200 text-xs rounded">{{ $release->grabs }}</span>
@endif
</a>
<button type="button" class="add-to-cart inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-600 dark:bg-blue-700 text-white hover:bg-blue-700 dark:hover:bg-blue-800 transition" data-guid="{{ $release->guid }}">
<i class="fas fa-shopping-cart mr-1"></i> Cart
</button>
<a href="{{ url('/details/' . $release->guid) }}" 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
@if($shouldShowStats && isset($release->comments) && $release->comments > 0)
<span class="ml-1 px-1 bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200 text-xs rounded">{{ $release->comments }}</span>
@endif
</a>
</div>
</div>
</div>
@endif
@endforeach
</div>
</div>
@endif
@@ -0,0 +1,37 @@
@props([
'results',
'icon',
'title',
'covgroup',
'category' => 'All',
'parentcat',
'searchPlaceholder',
'searchCategory' => null,
'totalLabel' => 'results found',
])
@php
$totalResults = is_object($results) && method_exists($results, 'total') ? $results->total() : count($results);
@endphp
<div class="mb-4 flex flex-wrap justify-between items-center gap-4">
<div class="flex items-center gap-4">
<h2 class="text-xl font-semibold text-gray-800 dark:text-gray-200">
<i class="{{ $icon }} mr-2 text-blue-600 dark:text-blue-400"></i>
{{ $title }}
</h2>
<x-view-toggle
current-view="covers"
:covgroup="$covgroup"
:category="$category"
:parentcat="$parentcat"
:shows="false"
/>
</div>
<div class="flex items-center gap-4">
<x-inline-search :placeholder="$searchPlaceholder" :category="$searchCategory" />
<span class="text-sm text-gray-600 dark:text-gray-400">
{{ $totalResults }} {{ $totalLabel }}
</span>
</div>
</div>
@@ -0,0 +1,81 @@
@props([
'results',
'showThumbs' => false,
'dateField' => 'adddate',
'showTopPagination' => false,
'showBottomPagination' => true,
'showSort' => true,
])
@php
$hasPaginatorLinks = is_object($results) && method_exists($results, 'links');
$shouldShowThumbs = filter_var($showThumbs, FILTER_VALIDATE_BOOL);
@endphp
<form
id="nzb_multi_operations_form"
method="get"
x-data="releaseMultiOps"
@if($shouldShowThumbs) data-show-thumbs="{{ request()->query('thumbs', '0') === '1' ? '1' : '0' }}" @endif
>
<div class="px-6 py-4 surface-panel-alt border-b">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
<div class="space-y-3">
@isset($beforeActions)
{{ $beforeActions }}
@endisset
<div class="flex flex-wrap items-center gap-2">
<small class="text-gray-600 dark:text-gray-400">With Selected:</small>
<div class="flex gap-1">
<button type="button" class="nzb_multi_operations_download px-3 py-1 bg-green-600 dark:bg-green-700 text-white rounded-lg hover:bg-green-700 dark:hover:bg-green-800 transition text-sm" title="Download NZBs">
<i class="fa fa-cloud-download"></i>
</button>
<button type="button" class="nzb_multi_operations_cart px-3 py-1 bg-primary-600 dark:bg-primary-700 text-white rounded-lg hover:bg-primary-700 dark:hover:bg-primary-800 transition text-sm" title="Send to Download Basket">
<i class="fa fa-shopping-basket"></i>
</button>
@if(auth()->check() && auth()->user()->hasRole('Admin'))
<button type="button" class="nzb_multi_operations_delete px-3 py-1 bg-red-600 dark:bg-red-700 text-white rounded-lg hover:bg-red-700 dark:hover:bg-red-800 transition text-sm" title="Delete">
<i class="fa fa-trash"></i>
</button>
@endif
</div>
</div>
</div>
<div class="flex items-center justify-center">
<div class="text-sm text-gray-600 dark:text-gray-400">
@isset($summary)
{{ $summary }}
@else
Showing {{ $results->firstItem() }} to {{ $results->lastItem() }} of {{ $results->total() }} results
@endisset
</div>
</div>
<div class="flex items-center justify-end gap-3">
@isset($toolbarRight)
{{ $toolbarRight }}
@endisset
@if($showSort)
<x-sort-dropdown />
@endif
</div>
</div>
</div>
@if($showTopPagination && $hasPaginatorLinks)
<div class="px-6 py-3 surface-panel-alt border-b">
{{ $results->links() }}
</div>
@endif
<x-release-results :results="$results" :show-thumbs="$shouldShowThumbs" :date-field="$dateField" />
@if($showBottomPagination && $hasPaginatorLinks)
<div class="px-6 py-3 surface-panel-alt border-t">
{{ $results->appends(request()->query())->links() }}
</div>
@endif
</form>
@@ -0,0 +1,271 @@
@props([
'results',
'showThumbs' => false,
'dateField' => 'adddate',
])
@php
$shouldShowThumbs = filter_var($showThumbs, FILTER_VALIDATE_BOOL);
$activeDateField = (string) $dateField;
@endphp
<!-- Results Table (Desktop) -->
<div class="hidden md:block overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-100 dark:bg-gray-900">
<tr>
<th class="px-3 py-3 text-left">
<input type="checkbox" class="rounded border-gray-300 dark:border-gray-600 text-primary-600 dark:text-primary-500 focus:ring-primary-500 dark:focus:ring-primary-400 dark:bg-gray-700" id="chkSelectAll" x-model="allChecked" @change="toggleAll()">
</th>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">Name</th>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">Category</th>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">Added</th>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">Size</th>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">Files</th>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">Stats</th>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">Action</th>
</tr>
</thead>
<tbody class="surface-panel divide-y divide-gray-200 dark:divide-gray-700">
@foreach($results as $result)
@php
$reportedCount = (int) ($result->total_report_count ?? $result->report_count ?? 0);
$responseCount = (int) ($result->report_response_count ?? 0);
$sizeLabel = $result->size_formatted ?? number_format(($result->size ?? 0) / 1073741824, 2) . ' GB';
$dateValue = $result->{$activeDateField} ?? $result->adddate ?? $result->postdate ?? null;
@endphp
<tr class="hover:bg-gray-50 dark:bg-gray-900 dark:hover:bg-gray-700 transition">
<td class="px-3 py-4 whitespace-nowrap">
<input type="checkbox" class="chkRelease rounded border-gray-300 dark:border-gray-600 text-primary-600 dark:text-primary-500 focus:ring-primary-500 dark:focus:ring-primary-400 dark:bg-gray-700" name="release[]" value="{{ $result->guid }}" @change="onCheckboxChange()">
</td>
<td class="px-3 py-4">
<div class="flex items-start">
@if($shouldShowThumbs)
@php
$coverUrl = ($result->cover ?? false) ? $result->cover : getReleaseCover($result);
$hasValidCover = $coverUrl && !str_contains($coverUrl, 'no-cover.png');
@endphp
@if($hasValidCover)
<a href="{{ url('/details/' . $result->guid) }}" class="shrink-0 bg-gray-100 dark:bg-gray-700 rounded mr-3" x-show="showThumbs" @unless(request()->query('thumbs') === '1') style="display:none" @endunless>
<img src="{{ request()->query('thumbs') === '1' ? $coverUrl : '' }}" x-bind:src="showThumbs ? '{{ $coverUrl }}' : ''" class="w-12 h-16 object-cover rounded shadow-sm hover:shadow-md transition" alt="Cover" loading="lazy">
</a>
@endif
@endif
<div class="flex-1">
<div class="flex items-center gap-2 flex-wrap">
<a href="{{ url('/details/' . $result->guid) }}" class="text-primary-600 dark:text-primary-400 hover:text-primary-800 dark:hover:text-primary-300 font-medium wrap-break-word break-all">{{ $result->searchname }}</a>
@if($reportedCount > 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: {{ $result->all_report_reasons ?? \App\Models\ReleaseReport::reasonKeysToLabels($result->report_reasons ?? '') }} | Original report: {{ $result->latest_report_reason ?? 'Unknown' }} - {{ $result->latest_report_description ?? 'No additional report details were provided.' }}">
<i class="fas fa-flag mr-1"></i> Reported ({{ $reportedCount }})
</span>
@endif
@if($responseCount > 0)
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200"
title="Staff response available on release details">
<i class="fas fa-reply mr-1"></i> Response
</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">
<i class="fas fa-exclamation-triangle mr-1"></i> Failed ({{ $result->failed_count }})
</span>
@endif
@if(isset($result->haspreview) && $result->haspreview == 1)
<button type="button"
class="preview-badge inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200 hover:bg-purple-200 dark:hover:bg-purple-800 transition cursor-pointer"
data-guid="{{ $result->guid }}"
title="View preview image">
<i class="fas fa-image mr-1"></i> Preview
</button>
@endif
@if(isset($result->jpgstatus) && $result->jpgstatus == 1)
<button type="button"
class="sample-badge inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 hover:bg-green-200 dark:hover:bg-green-800 transition cursor-pointer"
data-guid="{{ $result->guid }}"
title="View sample image">
<i class="fas fa-images mr-1"></i> Sample
</button>
@endif
@if(!empty($result->videos_id) && (int) $result->videos_id > 0)
<a href="{{ url('/series/' . $result->videos_id) }}"
class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-indigo-100 dark:bg-indigo-900 text-indigo-800 dark:text-indigo-200 hover:bg-indigo-200 dark:hover:bg-indigo-800 transition"
title="View full series">
<i class="fas fa-tv mr-1"></i> View Series
</a>
@endif
@if(isset($result->reid) && $result->reid != null)
<button type="button"
class="mediainfo-badge inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-primary-100 dark:bg-primary-900/50 text-primary-800 dark:text-primary-200 hover:bg-primary-200 dark:hover:bg-primary-800 transition cursor-pointer"
data-release-id="{{ $result->id }}"
title="View media info">
<i class="fas fa-info-circle mr-1"></i> Media Info
</button>
@endif
@if(isset($result->nfostatus) && $result->nfostatus == 1)
<button type="button"
class="nfo-badge inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200 hover:bg-yellow-200 dark:hover:bg-yellow-800 transition cursor-pointer"
data-guid="{{ $result->guid }}"
title="View NFO file">
<i class="fas fa-file-alt mr-1"></i> NFO
</button>
@endif
</div>
<div class="text-xs text-gray-500 dark:text-gray-400 mt-1 flex flex-wrap gap-2">
@if($result->group_name)
<span class="inline-flex items-center px-2 py-0.5 rounded bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300">
<i class="fas fa-users mr-1"></i> {{ $result->group_name }}
</span>
@endif
@if(!empty($result->postdate))
<span class="inline-flex items-center px-2 py-0.5 rounded bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200">
<i class="fas fa-calendar mr-1"></i> Posted: {{ userDate($result->postdate, 'M d, Y H:i') }}
</span>
@endif
@if(!empty($result->fromname))
<span class="inline-flex items-center px-2 py-0.5 rounded bg-indigo-100 dark:bg-indigo-900 text-indigo-800 dark:text-indigo-200 font-mono">
<i class="fas fa-user mr-1"></i> {{ $result->fromname }}
</span>
@endif
</div>
</div>
</div>
</td>
<td class="px-3 py-4 whitespace-nowrap">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-primary-100 dark:bg-primary-900/50 text-primary-800 dark:text-primary-200">
{{ $result->category_name ?? 'Other' }}
</span>
</td>
<td class="px-3 py-4 whitespace-nowrap text-sm text-gray-600 dark:text-gray-400">
{{ $dateValue ? userDateDiffForHumans($dateValue) : 'Unknown' }}
</td>
<td class="px-3 py-4 whitespace-nowrap text-sm text-gray-600 dark:text-gray-400">
{{ $sizeLabel }}
</td>
<td class="px-3 py-4 whitespace-nowrap text-sm text-gray-600 dark:text-gray-400">
@if(($result->totalpart ?? 0) > 0)
<button type="button"
class="filelist-badge text-primary-600 dark:text-primary-400 hover:text-primary-800 dark:hover:text-primary-300 font-medium cursor-pointer hover:underline"
data-guid="{{ $result->guid }}"
title="View file list">
{{ $result->totalpart ?? 0 }}
</button>
@else
{{ $result->totalpart ?? 0 }}
@endif
</td>
<td class="px-3 py-4 whitespace-nowrap text-sm text-gray-600 dark:text-gray-400">
<div class="flex items-center gap-2">
<span title="Grabs"><i class="fas fa-download text-green-600 dark:text-green-400"></i> {{ $result->grabs ?? 0 }}</span>
<span title="Comments"><i class="fas fa-comment text-primary-600 dark:text-primary-400"></i> {{ $result->comments ?? 0 }}</span>
</div>
</td>
<td class="px-3 py-4 whitespace-nowrap">
<div class="flex items-center gap-1 flex-wrap">
<a href="{{ url('/getnzb/' . $result->guid) }}" class="download-nzb px-2 py-1 bg-green-600 dark:bg-green-700 text-white rounded-lg hover:bg-green-700 dark:hover:bg-green-800 transition text-sm" title="Download NZB">
<i class="fa fa-download"></i>
</a>
<a href="{{ url('/details/' . $result->guid) }}" class="px-2 py-1 bg-primary-600 dark:bg-primary-700 text-white rounded-lg hover:bg-primary-700 dark:hover:bg-primary-800 transition text-sm" title="View Details">
<i class="fa fa-info"></i>
</a>
<a href="#" class="add-to-cart px-2 py-1 bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-300 dark:hover:bg-gray-500 transition text-sm" data-guid="{{ $result->guid }}" title="Add to Cart">
<i class="icon_cart fa fa-shopping-basket"></i>
</a>
@if(!empty($result->imdbid) && imdb_id_is_valid($result->imdbid))
<a href="{{ url('/mymovies?id=add&imdb=' . $result->imdbid) }}"
class="px-2 py-1 bg-purple-600 dark:bg-purple-700 text-white rounded-lg hover:bg-purple-700 dark:hover:bg-purple-800 transition text-sm"
title="Add to My Movies">
<i class="fa fa-film"></i>
</a>
@endif
<x-report-button :release-id="$result->id" :reported-count="$reportedCount" variant="icon" />
</div>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
<!-- Mobile Card View -->
<div class="md:hidden space-y-3 px-4 py-4">
@foreach($results as $result)
@php
$reportedCount = (int) ($result->total_report_count ?? $result->report_count ?? 0);
$responseCount = (int) ($result->report_response_count ?? 0);
$sizeLabel = $result->size_formatted ?? number_format(($result->size ?? 0) / 1073741824, 2) . ' GB';
$dateValue = $result->{$activeDateField} ?? $result->adddate ?? $result->postdate ?? null;
@endphp
<div class="surface-panel border rounded-xl p-4 hover:shadow-md transition">
<div class="flex items-start gap-3">
<input type="checkbox" class="chkRelease rounded border-gray-300 dark:border-gray-600 text-primary-600 dark:text-primary-500 focus:ring-primary-500 dark:focus:ring-primary-400 dark:bg-gray-700 mt-1" name="release[]" value="{{ $result->guid }}" @change="onCheckboxChange()">
<div class="flex-1 min-w-0">
@if($shouldShowThumbs)
@php
$mCoverUrl = ($result->cover ?? false) ? $result->cover : getReleaseCover($result);
$mHasCover = $mCoverUrl && !str_contains($mCoverUrl, 'no-cover.png');
@endphp
@if($mHasCover)
<a href="{{ url('/details/' . $result->guid) }}" class="block mb-2 bg-gray-100 dark:bg-gray-700 rounded-lg" x-show="showThumbs" @unless(request()->query('thumbs') === '1') style="display:none" @endunless>
<img src="{{ request()->query('thumbs') === '1' ? $mCoverUrl : '' }}" x-bind:src="showThumbs ? '{{ $mCoverUrl }}' : ''" class="w-16 h-20 object-cover rounded-lg shadow-sm" alt="Cover" loading="lazy">
</a>
@endif
@endif
<a href="{{ url('/details/' . $result->guid) }}" class="text-primary-600 dark:text-primary-400 hover:text-primary-800 dark:hover:text-primary-300 font-medium wrap-break-word text-base break-all">
{{ $result->searchname }}
</a>
<div class="flex flex-wrap items-center gap-2 mt-2">
@if($reportedCount > 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: {{ $result->all_report_reasons ?? \App\Models\ReleaseReport::reasonKeysToLabels($result->report_reasons ?? '') }} | Original report: {{ $result->latest_report_reason ?? 'Unknown' }} - {{ $result->latest_report_description ?? 'No additional report details were provided.' }}">
<i class="fas fa-flag mr-1"></i> Reported ({{ $reportedCount }})
</span>
@endif
@if($responseCount > 0)
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200"
title="Staff response available on release details">
<i class="fas fa-reply mr-1"></i> Response
</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">
<i class="fas fa-exclamation-triangle mr-1"></i> Failed ({{ $result->failed_count }})
</span>
@endif
</div>
<div class="flex flex-wrap items-center gap-2 mt-2 text-sm text-gray-600 dark:text-gray-400">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-primary-100 dark:bg-primary-900/50 text-primary-800 dark:text-primary-200">
{{ $result->category_name ?? 'Other' }}
</span>
<span><i class="fas fa-clock mr-1"></i>{{ $dateValue ? userDateDiffForHumans($dateValue) : 'Unknown' }}</span>
<span><i class="fas fa-hdd mr-1"></i>{{ $sizeLabel }}</span>
<span><i class="fas fa-file mr-1"></i>{{ $result->totalpart ?? 0 }} files</span>
<span title="Grabs"><i class="fas fa-download text-green-600 dark:text-green-400 mr-1"></i>{{ $result->grabs ?? 0 }}</span>
<span title="Comments"><i class="fas fa-comment text-primary-600 dark:text-primary-400 mr-1"></i>{{ $result->comments ?? 0 }}</span>
</div>
<div class="mt-3 flex gap-1 flex-wrap">
<a href="{{ url('/getnzb/' . $result->guid) }}" class="download-nzb px-3 py-1.5 bg-green-600 dark:bg-green-700 text-white rounded-lg hover:bg-green-700 dark:hover:bg-green-800 transition text-sm" title="Download NZB">
<i class="fa fa-download"></i>
</a>
<a href="{{ url('/details/' . $result->guid) }}" class="px-3 py-1.5 bg-primary-600 dark:bg-primary-700 text-white rounded-lg hover:bg-primary-700 dark:hover:bg-primary-800 transition text-sm" title="View Details">
<i class="fa fa-info"></i>
</a>
<a href="#" class="add-to-cart px-3 py-1.5 bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600 transition text-sm" data-guid="{{ $result->guid }}" title="Add to Cart">
<i class="icon_cart fa fa-shopping-basket"></i>
</a>
@if(!empty($result->imdbid) && imdb_id_is_valid($result->imdbid))
<a href="{{ url('/mymovies?id=add&imdb=' . $result->imdbid) }}"
class="px-3 py-1.5 bg-purple-600 dark:bg-purple-700 text-white rounded-lg hover:bg-purple-700 dark:hover:bg-purple-800 transition text-sm"
title="Add to My Movies">
<i class="fa fa-film"></i>
</a>
@endif
<x-report-button :release-id="$result->id" :reported-count="$reportedCount" variant="icon" />
</div>
</div>
</div>
</div>
@endforeach
</div>
+9 -84
View File
@@ -62,9 +62,7 @@
@php
$releases = $result->releases ?? [];
$totalReleases = $result->total_releases ?? count($releases);
$maxReleases = 2;
$displayReleases = array_slice($releases, 0, $maxReleases);
$guid = !empty($displayReleases) ? $displayReleases[0]->guid : null;
$guid = !empty($releases) ? $releases[0]->guid : null;
$totalFailed = collect($releases)->sum(fn($r) => (int)($r->failed_count ?? 0));
@endphp
@@ -137,87 +135,14 @@
</div>
</div>
<!-- Release Information -->
@if(!empty($displayReleases))
<div class="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">
Available Releases
@if($totalReleases > $maxReleases)
<span class="text-xs font-normal text-gray-500">(Showing {{ $maxReleases }} of {{ $totalReleases }})</span>
@endif
</h4>
<div class="space-y-2">
@foreach($displayReleases as $release)
@if($release->searchname)
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-2 border border-gray-200 dark:border-gray-700">
<div class="space-y-2">
<div class="flex items-start justify-between gap-2">
<!-- Release Name -->
<a href="{{ url('/details/' . $release->guid) }}" class="text-sm text-gray-800 dark:text-gray-200 hover:text-blue-600 dark:hover:text-blue-400 font-medium block break-all flex-1" title="{{ $release->searchname }}">
{{ $release->searchname }}
</a>
<label class="inline-flex items-center shrink-0">
<input type="checkbox" class="chkRelease form-checkbox h-4 w-4 text-blue-600" value="{{ $release->guid }}" name="release[]" @change="onCheckboxChange()"/>
</label>
</div>
<!-- Info Badges -->
<div class="flex flex-wrap items-center gap-1.5">
@if(isset($release->size))
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300">
<i class="fas fa-hdd mr-1"></i>{{ number_format($release->size / 1073741824, 2) }} GB
</span>
@endif
@if(isset($release->postdate))
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300">
<i class="fas fa-calendar-alt mr-1"></i>{{ date('M d, Y H:i', strtotime($release->postdate)) }}
</span>
@endif
@if(isset($release->adddate))
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300">
<i class="fas fa-plus-circle mr-1"></i>{{ userDateDiffForHumans($release->adddate) }}
</span>
@endif
@if(isset($release->nfoid) && !empty($release->nfoid))
<button type="button"
class="nfo-badge inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200 hover:bg-yellow-200 dark:hover:bg-yellow-800 transition cursor-pointer"
data-guid="{{ $release->guid }}"
title="View NFO file">
<i class="fas fa-file-alt mr-1"></i> NFO
</button>
@endif
@if(isset($release->group_name) && !empty($release->group_name))
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-indigo-100 dark:bg-indigo-900 text-indigo-800 dark:text-indigo-200" title="Poster/Uploader">
<i class="fas fa-user mr-1"></i> {{ $release->group_name }}
</span>
@endif
</div>
<!-- Action Buttons -->
<div class="flex flex-wrap items-center gap-1.5">
<a href="{{ url('/getnzb/' . $release->guid) }}" class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-600 dark:bg-green-700 text-white hover:bg-green-700 dark:hover:bg-green-800 transition">
<i class="fas fa-download mr-1"></i> Download
@if(isset($release->grabs) && $release->grabs > 0)
<span class="ml-1 px-1 bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200 text-xs rounded">{{ $release->grabs }}</span>
@endif
</a>
<button class="add-to-cart inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-600 dark:bg-blue-700 text-white hover:bg-blue-700 dark:hover:bg-blue-800 transition" data-guid="{{ $release->guid }}">
<i class="fas fa-shopping-cart mr-1"></i> Cart
</button>
<a href="{{ url('/details/' . $release->guid) }}" 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
@if(isset($release->comments) && $release->comments > 0)
<span class="ml-1 px-1 bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200 text-xs rounded">{{ $release->comments }}</span>
@endif
</a>
</div>
</div>
</div>
@endif
@endforeach
</div>
</div>
@endif
<x-cover-release-list
:releases="$releases"
:total-releases="$totalReleases"
:show-checkbox="true"
:show-add-date="true"
:show-group="true"
:show-stats="true"
/>
</div>
</div>
</div>
+5 -36
View File
@@ -61,6 +61,11 @@
<i class="fas fa-file-alt mr-2"></i> View NFO
</button>
@endif
@if(($release->totalpart ?? 0) > 0)
<button type="button" class="filelist-badge px-4 py-2 bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-100 rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600 transition inline-flex items-center" data-guid="{{ $release->guid }}" title="View file list">
<i class="fas fa-list mr-2"></i> View Files
</button>
@endif
@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-primary-600 text-white rounded-lg hover:bg-primary-700 transition inline-flex items-center" title="Edit Release">
@@ -947,41 +952,6 @@
</div>
@endif
<!-- NFO -->
@if($nfo ?? false)
<div>
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-3">NFO</h3>
<div class="bg-black dark:bg-gray-950 text-green-400 dark:text-green-300 p-4 rounded-lg overflow-x-auto font-mono text-sm border border-gray-700 dark:border-gray-600">
<pre>{{ $nfo }}</pre>
</div>
</div>
@endif
<!-- File List -->
@if(isset($files) && count($files) > 0)
<div>
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-3">Files ({{ count($files) }})</h3>
<div class="detail-file-list surface-panel-alt rounded-lg overflow-hidden border">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="surface-panel-alt">
<tr>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-700 dark:text-gray-300">Filename</th>
<th class="px-4 py-2 text-left text-xs font-medium text-gray-700 dark:text-gray-300">Size</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 dark:divide-gray-700">
@foreach($files as $file)
<tr class="hover:bg-gray-100 dark:hover:bg-gray-700 transition">
<td class="px-4 py-2 text-sm text-gray-800 dark:text-gray-200">{{ $file->name }}</td>
<td class="px-4 py-2 text-sm text-gray-600 dark:text-gray-400">{{ number_format($file->size / 1048576, 2) }} MB</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
@endif
<!-- Comments Section -->
<div>
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-4 flex items-center">
@@ -1156,4 +1126,3 @@
@push('scripts')
@include('partials.cart-script')
@endpush
+12 -85
View File
@@ -96,27 +96,16 @@
<!-- Results -->
@if(count($results) > 0)
<div class="mb-4 flex flex-wrap justify-between items-center gap-4">
<div class="flex items-center gap-4">
<h2 class="text-xl font-semibold text-gray-800 dark:text-gray-200">
<i class="fa fa-gamepad mr-2 text-blue-600"></i>
{{ $catname ?? 'All' }} Games
</h2>
<x-view-toggle
current-view="covers"
covgroup="games"
:category="$catname ?? 'All'"
parentcat="PC"
:shows="false"
/>
</div>
<div class="flex items-center gap-4">
<x-inline-search placeholder="Search in Games..." :category="$category ?? null" />
<span class="text-sm text-gray-600 dark:text-gray-400">
{{ $results->total() }} results found
</span>
</div>
</div>
<x-cover-results-toolbar
:results="$results"
icon="fa fa-gamepad"
:title="($catname ?? 'All') . ' Games'"
covgroup="games"
:category="$catname ?? 'All'"
parentcat="PC"
search-placeholder="Search in Games..."
:search-category="$category ?? null"
/>
<!-- Games Grid - Card Layout with Multiple Releases -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-6">
@@ -124,9 +113,7 @@
@php
$releases = $result->releases ?? [];
$totalReleases = $result->total_releases ?? count($releases);
$maxReleases = 2;
$displayReleases = array_slice($releases, 0, $maxReleases);
$guid = !empty($displayReleases) ? $displayReleases[0]->guid : null;
$guid = !empty($releases) ? $releases[0]->guid : null;
$totalFailed = collect($releases)->sum(fn($r) => (int)($r->failed_count ?? 0));
@endphp
@@ -189,66 +176,7 @@
</div>
</div>
<!-- Release Information -->
@if(!empty($displayReleases))
<div class="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">
Available Releases
@if($totalReleases > $maxReleases)
<span class="text-xs font-normal text-gray-500">(Showing {{ $maxReleases }} of {{ $totalReleases }})</span>
@endif
</h4>
<div class="space-y-2">
@foreach($displayReleases as $release)
@if($release->searchname)
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-2 border border-gray-200 dark:border-gray-700">
<div class="space-y-2">
<!-- Release Name -->
<a href="{{ url('/details/' . $release->guid) }}" class="text-sm text-gray-800 dark:text-gray-200 hover:text-blue-600 dark:hover:text-blue-400 font-medium block break-all" title="{{ $release->searchname }}">
{{ $release->searchname }}
</a>
<!-- Info Badges -->
<div class="flex flex-wrap items-center gap-1.5">
@if(isset($release->size))
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300">
<i class="fas fa-hdd mr-1"></i>{{ number_format($release->size / 1073741824, 2) }} GB
</span>
@endif
@if(isset($release->postdate))
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300">
<i class="fas fa-calendar-alt mr-1"></i>{{ date('M d, Y H:i', strtotime($release->postdate)) }}
</span>
@endif
@if(isset($release->nfoid) && !empty($release->nfoid))
<button type="button"
class="nfo-badge inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200 hover:bg-yellow-200 dark:hover:bg-yellow-800 transition cursor-pointer"
data-guid="{{ $release->guid }}"
title="View NFO file">
<i class="fas fa-file-alt mr-1"></i> NFO
</button>
@endif
</div>
<!-- Action Buttons -->
<div class="flex flex-wrap items-center gap-1.5">
<a href="{{ url('/getnzb/' . $release->guid) }}" class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-600 dark:bg-green-700 text-white hover:bg-green-700 dark:hover:bg-green-800 transition">
<i class="fas fa-download mr-1"></i> Download
</a>
<button class="add-to-cart inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-600 dark:bg-blue-700 text-white hover:bg-blue-700 dark:hover:bg-blue-800 transition" data-guid="{{ $release->guid }}">
<i class="fas fa-shopping-cart mr-1"></i> Cart
</button>
<a href="{{ url('/details/' . $release->guid) }}" 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>
</div>
</div>
</div>
@endif
@endforeach
</div>
</div>
@endif
<x-cover-release-list :releases="$releases" :total-releases="$totalReleases" />
</div>
</div>
</div>
@@ -275,4 +203,3 @@
{{-- NFO modal is included globally via layouts.main --}}
@endsection
+12 -85
View File
@@ -93,27 +93,16 @@
<!-- Results -->
@if(count($results) > 0)
<div class="mb-4 flex flex-wrap justify-between items-center gap-4">
<div class="flex items-center gap-4">
<h2 class="text-xl font-semibold text-gray-800 dark:text-gray-200">
<i class="fa fa-music mr-2 text-blue-600 dark:text-blue-400"></i>
{{ $catname ?? 'All' }} Albums
</h2>
<x-view-toggle
current-view="covers"
covgroup="music"
:category="$categorytitle ?? 'All'"
parentcat="Audio"
:shows="false"
/>
</div>
<div class="flex items-center gap-4">
<x-inline-search placeholder="Search in Audio..." :category="$category ?? null" />
<span class="text-sm text-gray-600 dark:text-gray-400">
{{ $results->total() }} results found
</span>
</div>
</div>
<x-cover-results-toolbar
:results="$results"
icon="fa fa-music"
:title="($catname ?? 'All') . ' Albums'"
covgroup="music"
:category="$categorytitle ?? 'All'"
parentcat="Audio"
search-placeholder="Search in Audio..."
:search-category="$category ?? null"
/>
<!-- Album Grid - Card Layout with Multiple Releases -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 mb-6">
@@ -121,9 +110,7 @@
@php
$releases = $result->releases ?? [];
$totalReleases = $result->total_releases ?? count($releases);
$maxReleases = 2;
$displayReleases = array_slice($releases, 0, $maxReleases);
$guid = !empty($displayReleases) ? $displayReleases[0]->guid : null;
$guid = !empty($releases) ? $releases[0]->guid : null;
$totalFailed = collect($releases)->sum(fn($r) => (int)($r->failed_count ?? 0));
@endphp
@@ -182,66 +169,7 @@
</div>
</div>
<!-- Release Information -->
@if(!empty($displayReleases))
<div class="mt-4 pt-4 border-t border-gray-200 dark:border-gray-700">
<h4 class="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">
Available Releases
@if($totalReleases > $maxReleases)
<span class="text-xs font-normal text-gray-500">(Showing {{ $maxReleases }} of {{ $totalReleases }})</span>
@endif
</h4>
<div class="space-y-2">
@foreach($displayReleases as $release)
@if($release->searchname)
<div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-2 border border-gray-200 dark:border-gray-700">
<div class="space-y-2">
<!-- Release Name -->
<a href="{{ url('/details/' . $release->guid) }}" class="text-sm text-gray-800 dark:text-gray-200 hover:text-blue-600 dark:hover:text-blue-400 font-medium block break-all" title="{{ $release->searchname }}">
{{ $release->searchname }}
</a>
<!-- Info Badges -->
<div class="flex flex-wrap items-center gap-1.5">
@if(isset($release->size))
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300">
<i class="fas fa-hdd mr-1"></i>{{ number_format($release->size / 1073741824, 2) }} GB
</span>
@endif
@if(isset($release->postdate))
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 dark:bg-gray-800 text-gray-700 dark:text-gray-300">
<i class="fas fa-calendar-alt mr-1"></i>{{ date('M d, Y H:i', strtotime($release->postdate)) }}
</span>
@endif
@if(isset($release->nfoid) && !empty($release->nfoid))
<button type="button"
class="nfo-badge inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200 hover:bg-yellow-200 dark:hover:bg-yellow-800 transition cursor-pointer"
data-guid="{{ $release->guid }}"
title="View NFO file">
<i class="fas fa-file-alt mr-1"></i> NFO
</button>
@endif
</div>
<!-- Action Buttons -->
<div class="flex flex-wrap items-center gap-1.5">
<a href="{{ url('/getnzb/' . $release->guid) }}" class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-600 dark:bg-green-700 text-white hover:bg-green-700 dark:hover:bg-green-800 transition">
<i class="fas fa-download mr-1"></i> Download
</a>
<button class="add-to-cart inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-600 dark:bg-blue-700 text-white hover:bg-blue-700 dark:hover:bg-blue-800 transition" data-guid="{{ $release->guid }}">
<i class="fas fa-shopping-cart mr-1"></i> Cart
</button>
<a href="{{ url('/details/' . $release->guid) }}" 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>
</div>
</div>
</div>
@endif
@endforeach
</div>
</div>
@endif
<x-cover-release-list :releases="$releases" :total-releases="$totalReleases" />
</div>
</div>
</div>
@@ -268,4 +196,3 @@
{{-- NFO modal is included globally via layouts.main --}}
@endsection
+8 -241
View File
@@ -158,246 +158,14 @@
<!-- Search Results -->
@if(isset($results) && ((is_array($results) && count($results) > 0) || (is_object($results) && $results->count() > 0)))
<form id="nzb_multi_operations_form" method="get" x-data="releaseMultiOps">
<!-- Multi-operations toolbar -->
<div class="px-6 py-4 surface-panel-alt border-y">
<div class="grid grid-cols-1 lg:grid-cols-3 gap-4">
<div class="flex flex-wrap items-center gap-2">
<small class="text-gray-600 dark:text-gray-400">With Selected:</small>
<div class="flex gap-1">
<button type="button" class="nzb_multi_operations_download px-3 py-1 bg-green-600 dark:bg-green-700 text-white rounded-lg hover:bg-green-700 dark:hover:bg-green-800 transition text-sm" title="Download NZBs">
<i class="fa fa-cloud-download"></i>
</button>
<button type="button" class="nzb_multi_operations_cart px-3 py-1 bg-primary-600 dark:bg-primary-700 text-white rounded-lg hover:bg-primary-700 dark:hover:bg-primary-800 transition text-sm" title="Send to Download Basket">
<i class="fa fa-shopping-basket"></i>
</button>
@if(auth()->check() && auth()->user()->hasRole('Admin'))
<button type="button" class="nzb_multi_operations_delete px-3 py-1 bg-red-600 dark:bg-red-700 text-white rounded-lg hover:bg-red-700 dark:hover:bg-red-800 transition text-sm" title="Delete">
<i class="fa fa-trash"></i>
</button>
@endif
</div>
</div>
<div class="flex items-center justify-center">
<div class="text-sm text-gray-600 dark:text-gray-400">
<span class="font-semibold">{{ is_object($results) ? $results->total() : count($results) }}</span> results found
@if(is_object($results))
Page {{ $results->currentPage() }} of {{ $results->lastPage() }}
@endif
</div>
</div>
<div class="flex items-center justify-end">
<x-sort-dropdown />
</div>
</div>
</div>
<!-- Desktop Table View (hidden on mobile) -->
<div class="hidden md:block overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-100 dark:bg-gray-900">
<tr>
<th class="px-3 py-3 text-left">
<input type="checkbox" class="rounded border-gray-300 dark:border-gray-600 text-primary-600 dark:text-primary-500 focus:ring-primary-500 dark:focus:ring-primary-400 dark:bg-gray-700" id="chkSelectAll" x-model="allChecked" @change="toggleAll()">
</th>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">Name</th>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">Category</th>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">Added</th>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">Size</th>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">Files</th>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">Stats</th>
<th class="px-3 py-3 text-left text-xs font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">Action</th>
</tr>
</thead>
<tbody class="surface-panel divide-y divide-gray-200 dark:divide-gray-700">
@foreach($results as $result)
<tr class="hover:bg-gray-50 dark:hover:bg-gray-700 transition">
<td class="px-3 py-4 whitespace-nowrap">
<input type="checkbox" class="chkRelease rounded border-gray-300 dark:border-gray-600 text-primary-600 dark:text-primary-500 focus:ring-primary-500 dark:focus:ring-primary-400 dark:bg-gray-700" name="release[]" value="{{ $result->guid }}" @change="onCheckboxChange()">
</td>
<td class="px-3 py-4">
<div class="flex-1">
<div class="flex items-center gap-2 flex-wrap">
<a href="{{ url('/details/' . $result->guid) }}" class="text-primary-600 dark:text-primary-400 hover:text-primary-800 dark:hover:text-primary-300 font-medium wrap-break-word 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">
<i class="fas fa-exclamation-triangle mr-1"></i> Failed ({{ $result->failed_count }})
</span>
@endif
@if(isset($result->haspreview) && $result->haspreview == 1)
<button type="button"
class="preview-badge inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200 hover:bg-purple-200 dark:hover:bg-purple-800 transition cursor-pointer"
data-guid="{{ $result->guid }}"
title="View preview image">
<i class="fas fa-image mr-1"></i> Preview
</button>
@endif
@if(isset($result->jpgstatus) && $result->jpgstatus == 1)
<button type="button"
class="sample-badge inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 hover:bg-green-200 dark:hover:bg-green-800 transition cursor-pointer"
data-guid="{{ $result->guid }}"
title="View sample image">
<i class="fas fa-images mr-1"></i> Sample
</button>
@endif
@if(isset($result->reid) && $result->reid != null)
<button type="button"
class="mediainfo-badge inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-primary-100 dark:bg-primary-900/50 text-primary-800 dark:text-primary-200 hover:bg-primary-200 dark:hover:bg-primary-800 transition cursor-pointer"
data-release-id="{{ $result->id }}"
title="View media info">
<i class="fas fa-info-circle mr-1"></i> Media Info
</button>
@endif
@if(isset($result->nfostatus) && $result->nfostatus == 1)
<button type="button"
class="nfo-badge inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200 hover:bg-yellow-200 dark:hover:bg-yellow-800 transition cursor-pointer"
data-guid="{{ $result->guid }}"
title="View NFO file">
<i class="fas fa-file-alt mr-1"></i> NFO
</button>
@endif
@if(!empty($result->videos_id) && (int)$result->videos_id > 0)
<a href="{{ url('/series/' . $result->videos_id) }}"
class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-indigo-100 dark:bg-indigo-900 text-indigo-800 dark:text-indigo-200 hover:bg-indigo-200 dark:hover:bg-indigo-800 transition"
title="View full series">
<i class="fas fa-tv mr-1"></i> View Series
</a>
@endif
</div>
<div class="text-xs text-gray-500 dark:text-gray-400 mt-1 flex flex-wrap gap-2">
@if($result->group_name)
<span class="inline-flex items-center px-2 py-0.5 rounded bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300">
<i class="fas fa-users mr-1"></i> {{ $result->group_name }}
</span>
@endif
@if(!empty($result->postdate))
<span class="inline-flex items-center px-2 py-0.5 rounded bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200">
<i class="fas fa-calendar mr-1"></i> Posted: {{ userDate($result->postdate, 'M d, Y H:i') }}
</span>
@endif
@if(!empty($result->fromname))
<span class="inline-flex items-center px-2 py-0.5 rounded bg-indigo-100 dark:bg-indigo-900 text-indigo-800 dark:text-indigo-200 font-mono">
<i class="fas fa-user mr-1"></i> {{ $result->fromname }}
</span>
@endif
</div>
</div>
</td>
<td class="px-3 py-4 whitespace-nowrap">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-primary-100 dark:bg-primary-900/50 text-primary-800 dark:text-primary-200">
{{ $result->category_name ?? 'Other' }}
</span>
</td>
<td class="px-3 py-4 whitespace-nowrap text-sm text-gray-600 dark:text-gray-400">
{{ userDateDiffForHumans($result->adddate) }}
</td>
<td class="px-3 py-4 whitespace-nowrap text-sm text-gray-600 dark:text-gray-400">
{{ number_format($result->size / 1073741824, 2) }} GB
</td>
<td class="px-3 py-4 whitespace-nowrap text-sm text-gray-600 dark:text-gray-400">
@if($result->totalpart > 0)
<button type="button"
class="filelist-badge text-primary-600 dark:text-primary-400 hover:text-primary-800 dark:hover:text-primary-300 font-medium cursor-pointer hover:underline"
data-guid="{{ $result->guid }}"
title="View file list">
{{ $result->totalpart ?? 0 }}
</button>
@else
{{ $result->totalpart ?? 0 }}
@endif
</td>
<td class="px-3 py-4 whitespace-nowrap text-sm text-gray-600 dark:text-gray-400">
<div class="flex items-center gap-2">
<span title="Grabs"><i class="fas fa-download text-green-600 dark:text-green-400"></i> {{ $result->grabs ?? 0 }}</span>
<span title="Comments"><i class="fas fa-comment text-primary-600 dark:text-primary-400"></i> {{ $result->comments ?? 0 }}</span>
</div>
</td>
<td class="px-3 py-4 whitespace-nowrap">
<div class="flex items-center gap-1 flex-wrap">
<a href="{{ url('/getnzb/' . $result->guid) }}" class="download-nzb px-2 py-1 bg-green-600 dark:bg-green-700 text-white rounded-lg hover:bg-green-700 dark:hover:bg-green-800 transition text-sm" title="Download NZB">
<i class="fa fa-download"></i>
</a>
<a href="{{ url('/details/' . $result->guid) }}" class="px-2 py-1 bg-primary-600 dark:bg-primary-700 text-white rounded-lg hover:bg-primary-700 dark:hover:bg-primary-800 transition text-sm" title="View Details">
<i class="fa fa-info"></i>
</a>
<a href="#" class="add-to-cart px-2 py-1 bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600 transition text-sm" data-guid="{{ $result->guid }}" title="Add to Cart">
<i class="icon_cart fa fa-shopping-basket"></i>
</a>
@if(!empty($result->imdbid) && imdb_id_is_valid($result->imdbid))
<a href="{{ url('/mymovies?id=add&imdb=' . $result->imdbid) }}"
class="px-2 py-1 bg-purple-600 dark:bg-purple-700 text-white rounded-lg hover:bg-purple-700 dark:hover:bg-purple-800 transition text-sm"
title="Add to My Movies">
<i class="fa fa-film"></i>
</a>
@endif
<x-report-button :release-id="$result->id" variant="icon" />
</div>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
<!-- Mobile Card View (visible on mobile only) -->
<div class="md:hidden space-y-3 px-4 py-4">
@foreach($results as $result)
<div class="surface-panel border rounded-xl p-4 hover:shadow-md transition">
<div class="flex items-start gap-3">
<input type="checkbox" class="chkRelease rounded border-gray-300 dark:border-gray-600 text-primary-600 dark:text-primary-500 focus:ring-primary-500 dark:focus:ring-primary-400 dark:bg-gray-700 mt-1" name="release[]" value="{{ $result->guid }}" @change="onCheckboxChange()">
<div class="flex-1 min-w-0">
<a href="{{ url('/details/' . $result->guid) }}" class="text-primary-600 dark:text-primary-400 hover:text-primary-800 dark:hover:text-primary-300 font-medium wrap-break-word text-base break-all">
{{ $result->searchname }}
</a>
<div class="flex flex-wrap items-center gap-2 mt-2 text-sm text-gray-600 dark:text-gray-400">
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-primary-100 dark:bg-primary-900/50 text-primary-800 dark:text-primary-200">
{{ $result->category_name ?? 'Other' }}
</span>
<span><i class="fas fa-clock mr-1"></i>{{ userDateDiffForHumans($result->postdate) }}</span>
<span><i class="fas fa-hdd mr-1"></i>{{ number_format($result->size / 1073741824, 2) }} GB</span>
<span><i class="fas fa-file mr-1"></i>{{ $result->totalpart ?? 0 }} files</span>
<span title="Grabs"><i class="fas fa-download text-green-600 dark:text-green-400 mr-1"></i>{{ $result->grabs ?? 0 }}</span>
<span title="Comments"><i class="fas fa-comment text-primary-600 dark:text-primary-400 mr-1"></i>{{ $result->comments ?? 0 }}</span>
</div>
<div class="mt-3 flex gap-1 flex-wrap">
<a href="{{ url('/getnzb/' . $result->guid) }}" class="download-nzb px-2 py-1 bg-green-600 dark:bg-green-700 text-white rounded-lg hover:bg-green-700 dark:hover:bg-green-800 transition text-sm" title="Download NZB">
<i class="fa fa-download"></i>
</a>
<a href="{{ url('/details/' . $result->guid) }}" class="px-2 py-1 bg-primary-600 dark:bg-primary-700 text-white rounded-lg hover:bg-primary-700 dark:hover:bg-primary-800 transition text-sm" title="View Details">
<i class="fa fa-info"></i>
</a>
<a href="#" class="add-to-cart px-2 py-1 bg-gray-200 dark:bg-gray-700 text-gray-700 dark:text-gray-300 rounded-lg hover:bg-gray-300 dark:hover:bg-gray-600 transition text-sm" data-guid="{{ $result->guid }}" title="Add to Cart">
<i class="icon_cart fa fa-shopping-basket"></i>
</a>
@if(!empty($result->imdbid) && imdb_id_is_valid($result->imdbid))
<a href="{{ url('/mymovies?id=add&imdb=' . $result->imdbid) }}"
class="px-2 py-1 bg-purple-600 dark:bg-purple-700 text-white rounded-lg hover:bg-purple-700 dark:hover:bg-purple-800 transition text-sm"
title="Add to My Movies">
<i class="fa fa-film"></i>
</a>
@endif
<x-report-button :release-id="$result->id" variant="icon" />
</div>
</div>
</div>
</div>
@endforeach
</div>
<!-- Pagination -->
@if(is_object($results) && method_exists($results, 'links'))
<div class="px-6 py-3 surface-panel-alt border-t">
{{ $results->appends(request()->query())->links() }}
</div>
@endif
</form>
<x-release-results-panel :results="$results" date-field="adddate">
<x-slot:summary>
<span class="font-semibold">{{ is_object($results) ? $results->total() : count($results) }}</span> results found
@if(is_object($results))
- Page {{ $results->currentPage() }} of {{ $results->lastPage() }}
@endif
</x-slot:summary>
</x-release-results-panel>
@elseif(request()->has('search'))
<x-empty-state
icon="fas fa-search"
@@ -415,4 +183,3 @@
{{-- All modals (preview, mediainfo, filelist, NFO) are included globally via layouts.main --}}
</div>
@endsection
@@ -141,6 +141,29 @@ class AdminContentControllerTest extends TestCase
$this->assertSame('/about/', $content->url);
}
public function test_content_page_accepts_string_content_id_from_query(): void
{
$user = $this->createUserWithRole('User');
/** @var Authenticatable $authenticatedUser */
$authenticatedUser = $user;
$content = $this->createContent([
'title' => 'Useful Content',
'body' => '<p>Visible body.</p>',
'contenttype' => Content::TYPE_USEFUL,
'status' => Content::STATUS_ENABLED,
'role' => Content::ROLE_LOGGED_IN,
]);
$response = $this->actingAs($authenticatedUser)->get(route('content', [
'id' => (string) $content->id,
'page' => 'content',
]));
$response->assertOk();
$response->assertSee('Useful Content');
}
public function test_create_form_marks_title_as_optional(): void
{
$admin = $this->createUserWithRole('Admin');
@@ -306,17 +306,17 @@ class AdminReleaseReportControllerTest extends TestCase
{
$detailsControllerPath = app_path('Http/Controllers/DetailsController.php');
$detailsViewPath = resource_path('views/details/index.blade.php');
$browseViewPath = resource_path('views/browse/index.blade.php');
$releaseResultsComponentPath = resource_path('views/components/release-results.blade.php');
$browseServicePath = app_path('Services/Releases/ReleaseBrowseService.php');
$this->assertFileExists($detailsControllerPath);
$this->assertFileExists($detailsViewPath);
$this->assertFileExists($browseViewPath);
$this->assertFileExists($releaseResultsComponentPath);
$this->assertFileExists($browseServicePath);
$detailsController = file_get_contents($detailsControllerPath);
$detailsView = file_get_contents($detailsViewPath);
$browseView = file_get_contents($browseViewPath);
$releaseResultsComponent = file_get_contents($releaseResultsComponentPath);
$browseService = file_get_contents($browseServicePath);
$this->assertStringContainsString('publicReportResponses', $detailsController);
@@ -330,7 +330,7 @@ class AdminReleaseReportControllerTest extends TestCase
$this->assertStringContainsString('all_report_reasons', $browseService);
$this->assertStringContainsString('report_response_count', $browseService);
$this->assertStringContainsString('response_is_public = 1', $browseService);
$this->assertStringContainsString('Original report:', $browseView);
$this->assertStringContainsString('Staff response available on release details', $browseView);
$this->assertStringContainsString('Original report:', $releaseResultsComponent);
$this->assertStringContainsString('Staff response available on release details', $releaseResultsComponent);
}
}
+39
View File
@@ -0,0 +1,39 @@
<?php
namespace Tests\Unit;
use PHPUnit\Framework\TestCase;
class CoverBrowseComponentsTest extends TestCase
{
public function test_cover_browse_pages_use_shared_toolbar_component(): void
{
$books = (string) file_get_contents(__DIR__.'/../../resources/views/books/index.blade.php');
$music = (string) file_get_contents(__DIR__.'/../../resources/views/music/index.blade.php');
$games = (string) file_get_contents(__DIR__.'/../../resources/views/games/index.blade.php');
$this->assertStringContainsString('<x-cover-results-toolbar', $books);
$this->assertStringContainsString('<x-cover-results-toolbar', $music);
$this->assertStringContainsString('<x-cover-results-toolbar', $games);
}
public function test_cover_browse_release_cards_use_shared_release_list_component(): void
{
$console = (string) file_get_contents(__DIR__.'/../../resources/views/console/index.blade.php');
$music = (string) file_get_contents(__DIR__.'/../../resources/views/music/index.blade.php');
$games = (string) file_get_contents(__DIR__.'/../../resources/views/games/index.blade.php');
$component = (string) file_get_contents(__DIR__.'/../../resources/views/components/cover-release-list.blade.php');
$this->assertStringContainsString('<x-cover-release-list', $console);
$this->assertStringContainsString('<x-cover-release-list', $music);
$this->assertStringContainsString('<x-cover-release-list', $games);
$this->assertStringNotContainsString('$displayReleases = array_slice', $console);
$this->assertStringNotContainsString('$displayReleases = array_slice', $music);
$this->assertStringNotContainsString('$displayReleases = array_slice', $games);
$this->assertStringContainsString('nfo-badge', $component);
$this->assertStringContainsString('add-to-cart', $component);
$this->assertStringContainsString('chkRelease', $component);
$this->assertStringContainsString('Available Releases', $component);
}
}
+13
View File
@@ -148,4 +148,17 @@ class ReleaseBrowseServiceTest extends TestCase
$this->assertEquals(['searchname', 'desc'], $result);
}
/**
* Test that browse result report badges use joined page rows, not a full-table report aggregate.
*/
public function test_browse_query_uses_page_scoped_report_join(): void
{
$servicePath = __DIR__.'/../../app/Services/Releases/ReleaseBrowseService.php';
$content = (string) file_get_contents($servicePath);
$this->assertStringContainsString('LEFT OUTER JOIN release_reports rr ON rr.releases_id = r.id', $content);
$this->assertStringContainsString('COUNT(DISTINCT rr.id) AS total_report_count', $content);
$this->assertStringNotContainsString('FROM release_reports GROUP BY releases_id', $content);
}
}
@@ -0,0 +1,47 @@
<?php
namespace Tests\Unit;
use PHPUnit\Framework\TestCase;
class ReleaseResultsComponentTest extends TestCase
{
public function test_browse_and_search_use_shared_release_results_component(): void
{
$browse = (string) file_get_contents(__DIR__.'/../../resources/views/browse/index.blade.php');
$search = (string) file_get_contents(__DIR__.'/../../resources/views/search/index.blade.php');
$panel = (string) file_get_contents(__DIR__.'/../../resources/views/components/release-results-panel.blade.php');
$this->assertStringContainsString('<x-release-results-panel :results="$results"', $browse);
$this->assertStringContainsString('<x-release-results-panel :results="$results"', $search);
$this->assertStringContainsString('<x-release-results :results="$results"', $panel);
$this->assertStringNotContainsString('@foreach($results as $result)', $browse);
$this->assertStringNotContainsString('@foreach($results as $result)', $search);
}
public function test_shared_release_results_panel_keeps_bulk_actions_and_toolbar_slots(): void
{
$panel = (string) file_get_contents(__DIR__.'/../../resources/views/components/release-results-panel.blade.php');
$this->assertStringContainsString('nzb_multi_operations_form', $panel);
$this->assertStringContainsString('nzb_multi_operations_download', $panel);
$this->assertStringContainsString('nzb_multi_operations_cart', $panel);
$this->assertStringContainsString('nzb_multi_operations_delete', $panel);
$this->assertStringContainsString('$beforeActions', $panel);
$this->assertStringContainsString('$toolbarRight', $panel);
$this->assertStringContainsString('$summary', $panel);
}
public function test_shared_release_results_component_keeps_expected_release_actions(): void
{
$component = (string) file_get_contents(__DIR__.'/../../resources/views/components/release-results.blade.php');
$this->assertStringContainsString('download-nzb', $component);
$this->assertStringContainsString('add-to-cart', $component);
$this->assertStringContainsString('filelist-badge', $component);
$this->assertStringContainsString('nfo-badge', $component);
$this->assertStringContainsString('preview-badge', $component);
$this->assertStringContainsString('mediainfo-badge', $component);
$this->assertStringContainsString('<x-report-button', $component);
}
}
@@ -187,6 +187,22 @@ class ReleaseSearchServiceOrderingTest extends TestCase
$this->assertStringNotContainsString('JOIN', $sql);
}
/**
* Test that web search report badge data is aggregated on joined result rows.
*/
public function test_build_search_base_sql_uses_page_scoped_report_join(): void
{
$reflection = new ReflectionClass(ReleaseSearchService::class);
$method = $reflection->getMethod('buildSearchBaseSql');
$sql = $method->invoke($this->service, 'WHERE r.id IN (1,2,3)');
$this->assertStringContainsString('LEFT OUTER JOIN release_reports rr ON rr.releases_id = r.id', $sql);
$this->assertStringContainsString('COUNT(DISTINCT rr.id) AS total_report_count', $sql);
$this->assertStringContainsString('GROUP BY r.id', $sql);
$this->assertStringNotContainsString('FROM release_reports GROUP BY releases_id', $sql);
}
/**
* Test that the lightweight count query is built directly against releases.
*/
+101
View File
@@ -0,0 +1,101 @@
<?php
namespace Tests\Unit;
use App\Http\Controllers\BasePageController;
use App\Http\Controllers\SearchController;
use Illuminate\Http\Request;
use PHPUnit\Framework\TestCase;
use ReflectionClass;
class UserViewRequestInputTest extends TestCase
{
public function test_page_resolution_rejects_non_scalar_and_negative_values(): void
{
$controller = $this->controller();
$this->assertSame(1, $controller->page(Request::create('/browse', 'GET', ['page' => ['2']])));
$this->assertSame(1, $controller->page(Request::create('/browse', 'GET', ['page' => -4])));
$this->assertSame(1, $controller->page(Request::create('/browse', 'GET', ['page' => '3.5'])));
$this->assertSame(3, $controller->page(Request::create('/browse', 'GET', ['page' => '3'])));
}
public function test_order_resolution_only_allows_known_sort_keys(): void
{
$controller = $this->controller();
$ordering = ['name_asc', 'postdate_desc'];
$this->assertSame('name_asc', $controller->order(Request::create('/browse', 'GET', ['ob' => 'name_asc']), $ordering));
$this->assertSame('', $controller->order(Request::create('/browse', 'GET', ['ob' => 'name_desc']), $ordering));
$this->assertSame('', $controller->order(Request::create('/browse', 'GET', ['ob' => ['name_asc']]), $ordering));
}
public function test_scalar_input_and_offset_helpers_normalize_user_query_values(): void
{
$controller = $this->controller();
$this->assertSame('', $controller->scalar(Request::create('/search', 'GET', ['title' => ['bad']]), 'title'));
$this->assertSame('ubuntu', $controller->scalar(Request::create('/search', 'GET', ['title' => 'ubuntu']), 'title'));
$this->assertSame(42, $controller->integer(Request::create('/browse', 'GET', ['t' => '42']), 't', 100));
$this->assertSame(100, $controller->integer(Request::create('/browse', 'GET', ['t' => '42.2']), 't', 100));
$this->assertSame(100, $controller->integer(Request::create('/browse', 'GET', ['t' => ['42']]), 't', 100));
$this->assertSame(['7010', '7020'], $controller->array(Request::create('/browse', 'GET', ['category' => ['7010', '7020']]), 'category'));
$this->assertSame([], $controller->array(Request::create('/browse', 'GET', ['category' => '7010']), 'category'));
$this->assertSame(80, $controller->offset(5, 20));
}
public function test_search_category_resolution_rejects_malformed_category_values(): void
{
$reflection = new ReflectionClass(SearchController::class);
$controller = $reflection->newInstanceWithoutConstructor();
$method = $reflection->getMethod('resolveCategoryIdsFromRequest');
$this->assertSame([1000, 2000], $method->invoke($controller, Request::create('/search', 'GET', ['t' => '1000,bad,2000,1000'])));
$this->assertSame([-1], $method->invoke($controller, Request::create('/search', 'GET', ['t' => ['1000']])));
$this->assertSame([-1], $method->invoke($controller, Request::create('/search', 'GET', ['searchadvcat' => ''])));
}
private function controller(): object
{
return new class extends BasePageController
{
public function __construct() {}
public function page(Request $request): int
{
return $this->resolvePage($request);
}
/**
* @param array<int, string> $ordering
*/
public function order(Request $request, array $ordering): string
{
return $this->resolveOrderBy($request, $ordering);
}
public function scalar(Request $request, string $key): string
{
return $this->scalarInput($request, $key);
}
public function integer(Request $request, string $key, int $default): int
{
return $this->integerInput($request, $key, $default);
}
/**
* @return array<int|string, mixed>
*/
public function array(Request $request, string $key): array
{
return $this->arrayInput($request, $key);
}
public function offset(int $page, int $perPage): int
{
return $this->paginationOffset($page, $perPage);
}
};
}
}