mirror of
https://github.com/NNTmux/newznab-tmux.git
synced 2026-08-29 02:01:33 +00:00
Update views and related controllers
This commit is contained in:
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 = '';
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user