From 8cc16bc365fbdd46ad1ca8d3129816fa8592c433 Mon Sep 17 00:00:00 2001 From: DariusIII Date: Thu, 22 Jan 2026 16:44:47 +0100 Subject: [PATCH] Update sorting --- .../Releases/ReleaseBrowseService.php | 3 + .../Releases/ReleaseSearchService.php | 1 + app/View/Components/SortDropdown.php | 94 +++++++++++ resources/js/csp-safe.js | 60 +++++++ resources/views/browse/index.blade.php | 11 +- .../views/components/sort-dropdown.blade.php | 27 ++++ resources/views/search/index.blade.php | 123 +++++++++++--- tests/Unit/ReleaseBrowseServiceTest.php | 151 ++++++++++++++++++ .../Unit/ReleaseSearchServiceOrderingTest.php | 110 +++++++++++++ 9 files changed, 547 insertions(+), 33 deletions(-) create mode 100644 app/View/Components/SortDropdown.php create mode 100644 resources/views/components/sort-dropdown.blade.php create mode 100644 tests/Unit/ReleaseBrowseServiceTest.php create mode 100644 tests/Unit/ReleaseSearchServiceOrderingTest.php diff --git a/app/Services/Releases/ReleaseBrowseService.php b/app/Services/Releases/ReleaseBrowseService.php index 4fab57765..3b3b90598 100644 --- a/app/Services/Releases/ReleaseBrowseService.php +++ b/app/Services/Releases/ReleaseBrowseService.php @@ -252,6 +252,7 @@ class ReleaseBrowseService 'size' => 'size', 'files' => 'totalpart', 'stats' => 'grabs', + 'added' => 'adddate', default => 'postdate', }; @@ -272,6 +273,8 @@ class ReleaseBrowseService 'cat_desc', 'posted_asc', 'posted_desc', + 'added_asc', + 'added_desc', 'size_asc', 'size_desc', 'files_asc', diff --git a/app/Services/Releases/ReleaseSearchService.php b/app/Services/Releases/ReleaseSearchService.php index eb998990f..a0446c5d4 100644 --- a/app/Services/Releases/ReleaseSearchService.php +++ b/app/Services/Releases/ReleaseSearchService.php @@ -1167,6 +1167,7 @@ class ReleaseSearchService 'size' => 'size', 'files' => 'totalpart', 'stats' => 'grabs', + 'added' => 'adddate', default => 'postdate', }; diff --git a/app/View/Components/SortDropdown.php b/app/View/Components/SortDropdown.php new file mode 100644 index 000000000..d1eec636c --- /dev/null +++ b/app/View/Components/SortDropdown.php @@ -0,0 +1,94 @@ +currentSort = $currentSort ?? request('ob', 'posted_desc'); + $this->baseUrl = $baseUrl ?? request()->url(); + $this->queryParams = $queryParams ?? request()->except(['ob', 'page']); + + // Default sort options for releases + $this->sortOptions = $options ?? [ + 'posted_desc' => ['label' => 'Posted (Newest)', 'icon' => 'fa-calendar-alt'], + 'posted_asc' => ['label' => 'Posted (Oldest)', 'icon' => 'fa-calendar-alt'], + 'added_desc' => ['label' => 'Added (Newest)', 'icon' => 'fa-clock'], + 'added_asc' => ['label' => 'Added (Oldest)', 'icon' => 'fa-clock'], + 'name_asc' => ['label' => 'Name (A-Z)', 'icon' => 'fa-font'], + 'name_desc' => ['label' => 'Name (Z-A)', 'icon' => 'fa-font'], + 'size_desc' => ['label' => 'Size (Largest)', 'icon' => 'fa-hdd'], + 'size_asc' => ['label' => 'Size (Smallest)', 'icon' => 'fa-hdd'], + 'files_desc' => ['label' => 'Files (Most)', 'icon' => 'fa-file'], + 'files_asc' => ['label' => 'Files (Least)', 'icon' => 'fa-file'], + 'stats_desc' => ['label' => 'Grabs (Most)', 'icon' => 'fa-download'], + 'stats_asc' => ['label' => 'Grabs (Least)', 'icon' => 'fa-download'], + ]; + + // Pre-compute current label and icon + $this->currentLabel = $this->sortOptions[$this->currentSort]['label'] ?? 'Posted (Newest)'; + $this->currentIcon = $this->sortOptions[$this->currentSort]['icon'] ?? 'fa-calendar-alt'; + + // Pre-compute all sort URLs + $this->sortUrls = []; + foreach ($this->sortOptions as $sortKey => $sortData) { + $params = array_merge($this->queryParams, ['ob' => $sortKey]); + $this->sortUrls[$sortKey] = $this->baseUrl.'?'.http_build_query($params); + } + } + + /** + * Get the view / contents that represent the component. + */ + public function render(): View|Closure|string + { + return view('components.sort-dropdown'); + } +} diff --git a/resources/js/csp-safe.js b/resources/js/csp-safe.js index 8ad1af0f4..09885f841 100644 --- a/resources/js/csp-safe.js +++ b/resources/js/csp-safe.js @@ -5667,3 +5667,63 @@ function initAutocompleteInput(inputId, dropdownId, formId, itemClass) { } } +// ============================================================================ +// Sort Dropdown Component +// ============================================================================ + +/** + * Initialize sort dropdown functionality + * Handles the custom dropdown for sorting releases on browse/search pages + */ +function initSortDropdowns() { + document.querySelectorAll('.sort-dropdown').forEach(function(dropdown) { + var toggle = dropdown.querySelector('.sort-dropdown-toggle'); + var menu = dropdown.querySelector('.sort-dropdown-menu'); + var chevron = dropdown.querySelector('.sort-dropdown-chevron'); + + if (!toggle || !menu) return; + + // Skip if already initialized + if (toggle.hasAttribute('data-sort-initialized')) return; + toggle.setAttribute('data-sort-initialized', 'true'); + + toggle.addEventListener('click', function(e) { + e.preventDefault(); + e.stopPropagation(); + var isOpen = !menu.classList.contains('hidden'); + + // Close all other dropdowns first + document.querySelectorAll('.sort-dropdown-menu').forEach(function(m) { + m.classList.add('hidden'); + }); + document.querySelectorAll('.sort-dropdown-chevron').forEach(function(c) { + c.classList.remove('rotate-180'); + }); + + if (!isOpen) { + menu.classList.remove('hidden'); + if (chevron) chevron.classList.add('rotate-180'); + } + }); + }); + + // Close dropdowns when clicking outside (single global listener) + if (!window._sortDropdownOutsideListenerAdded) { + window._sortDropdownOutsideListenerAdded = true; + document.addEventListener('click', function(e) { + var isInsideDropdown = e.target.closest('.sort-dropdown'); + if (!isInsideDropdown) { + document.querySelectorAll('.sort-dropdown-menu').forEach(function(m) { + m.classList.add('hidden'); + }); + document.querySelectorAll('.sort-dropdown-chevron').forEach(function(c) { + c.classList.remove('rotate-180'); + }); + } + }); + } +} + +// Initialize sort dropdowns on DOMContentLoaded +document.addEventListener('DOMContentLoaded', initSortDropdowns); + diff --git a/resources/views/browse/index.blade.php b/resources/views/browse/index.blade.php index 92a64f092..ba701526e 100644 --- a/resources/views/browse/index.blade.php +++ b/resources/views/browse/index.blade.php @@ -93,16 +93,7 @@
-
- - -
+
diff --git a/resources/views/components/sort-dropdown.blade.php b/resources/views/components/sort-dropdown.blade.php new file mode 100644 index 000000000..8f8d86210 --- /dev/null +++ b/resources/views/components/sort-dropdown.blade.php @@ -0,0 +1,27 @@ +
+ + + +
diff --git a/resources/views/search/index.blade.php b/resources/views/search/index.blade.php index 46f628e0a..6635bdf3f 100644 --- a/resources/views/search/index.blade.php +++ b/resources/views/search/index.blade.php @@ -157,6 +157,11 @@ + @if(auth()->check() && auth()->user()->hasRole('Admin')) + + @endif
@@ -173,28 +178,9 @@
-
- Sort by: -
- @php - $currentSort = request('ob', 'posted_desc'); - $sortOptions = [ - 'posted_desc' => ['label' => 'Posted Date ↓', 'icon' => 'fa-calendar'], - 'posted_asc' => ['label' => 'Posted Date ↑', 'icon' => 'fa-calendar'], - 'size_desc' => ['label' => 'Size ↓', 'icon' => 'fa-hdd'], - 'size_asc' => ['label' => 'Size ↑', 'icon' => 'fa-hdd'], - 'name_asc' => ['label' => 'Name ↑', 'icon' => 'fa-font'], - 'name_desc' => ['label' => 'Name ↓', 'icon' => 'fa-font'], - ]; - $queryParams = request()->except('ob'); - @endphp - @foreach($sortOptions as $sortKey => $sortData) - - {{ $sortData['label'] }} - - @endforeach -
+
+ Sort results: +
@@ -224,6 +210,12 @@
{{ $result->searchname }} + @if(!empty($result->failed) && $result->failed > 0) + + Failed ({{ $result->failed }}) + + @endif @if(isset($result->haspreview) && $result->haspreview == 1) @endif + @if(isset($result->reid) && $result->reid != null) + + @endif @if(isset($result->nfostatus) && $result->nfostatus == 1) + @else + {{ $result->totalpart ?? 0 }} + @endif
@@ -306,6 +315,13 @@ + @if(!empty($result->imdbid) && $result->imdbid != '0' && $result->imdbid != 0 && $result->imdbid != '0000000') + + + + @endif
@@ -325,6 +341,12 @@ {{ $result->searchname }} + @if(!empty($result->failed) && $result->failed > 0) + + Failed ({{ $result->failed }}) + + @endif @if(isset($result->haspreview) && $result->haspreview == 1) @endif + @if(isset($result->reid) && $result->reid != null) + + @endif @if(isset($result->nfostatus) && $result->nfostatus == 1)
@@ -436,6 +473,46 @@ + + + + + + @include('partials.nfo-modal') diff --git a/tests/Unit/ReleaseBrowseServiceTest.php b/tests/Unit/ReleaseBrowseServiceTest.php new file mode 100644 index 000000000..81898a116 --- /dev/null +++ b/tests/Unit/ReleaseBrowseServiceTest.php @@ -0,0 +1,151 @@ +service = new ReleaseBrowseService; + } + + /** + * Test that getBrowseOrdering returns all expected ordering options. + */ + public function test_get_browse_ordering_returns_all_options(): void + { + $ordering = $this->service->getBrowseOrdering(); + + $this->assertIsArray($ordering); + $this->assertContains('name_asc', $ordering); + $this->assertContains('name_desc', $ordering); + $this->assertContains('cat_asc', $ordering); + $this->assertContains('cat_desc', $ordering); + $this->assertContains('posted_asc', $ordering); + $this->assertContains('posted_desc', $ordering); + $this->assertContains('added_asc', $ordering); + $this->assertContains('added_desc', $ordering); + $this->assertContains('size_asc', $ordering); + $this->assertContains('size_desc', $ordering); + $this->assertContains('files_asc', $ordering); + $this->assertContains('files_desc', $ordering); + $this->assertContains('stats_asc', $ordering); + $this->assertContains('stats_desc', $ordering); + } + + /** + * Test that getBrowseOrder correctly maps 'added' to 'adddate' column. + */ + public function test_get_browse_order_maps_added_to_adddate(): void + { + $result = $this->service->getBrowseOrder('added_desc'); + + $this->assertEquals(['adddate', 'desc'], $result); + } + + /** + * Test that getBrowseOrder correctly handles added ascending. + */ + public function test_get_browse_order_handles_added_ascending(): void + { + $result = $this->service->getBrowseOrder('added_asc'); + + $this->assertEquals(['adddate', 'asc'], $result); + } + + /** + * Test that getBrowseOrder correctly maps 'posted' to 'postdate' column. + */ + public function test_get_browse_order_maps_posted_to_postdate(): void + { + $result = $this->service->getBrowseOrder('posted_desc'); + + $this->assertEquals(['postdate', 'desc'], $result); + } + + /** + * Test that getBrowseOrder defaults to postdate desc for empty string. + */ + public function test_get_browse_order_defaults_to_postdate_desc(): void + { + $result = $this->service->getBrowseOrder(''); + + $this->assertEquals(['postdate', 'desc'], $result); + } + + /** + * Test that getBrowseOrder correctly maps 'name' to 'searchname' column. + */ + public function test_get_browse_order_maps_name_to_searchname(): void + { + $result = $this->service->getBrowseOrder('name_asc'); + + $this->assertEquals(['searchname', 'asc'], $result); + } + + /** + * Test that getBrowseOrder correctly maps 'size' to 'size' column. + */ + public function test_get_browse_order_maps_size_correctly(): void + { + $result = $this->service->getBrowseOrder('size_desc'); + + $this->assertEquals(['size', 'desc'], $result); + } + + /** + * Test that getBrowseOrder correctly maps 'files' to 'totalpart' column. + */ + public function test_get_browse_order_maps_files_to_totalpart(): void + { + $result = $this->service->getBrowseOrder('files_asc'); + + $this->assertEquals(['totalpart', 'asc'], $result); + } + + /** + * Test that getBrowseOrder correctly maps 'stats' to 'grabs' column. + */ + public function test_get_browse_order_maps_stats_to_grabs(): void + { + $result = $this->service->getBrowseOrder('stats_desc'); + + $this->assertEquals(['grabs', 'desc'], $result); + } + + /** + * Test that getBrowseOrder correctly maps 'cat' to 'categories_id' column. + */ + public function test_get_browse_order_maps_cat_to_categories_id(): void + { + $result = $this->service->getBrowseOrder('cat_asc'); + + $this->assertEquals(['categories_id', 'asc'], $result); + } + + /** + * Test that getBrowseOrder handles unknown order type by defaulting to postdate. + */ + public function test_get_browse_order_defaults_unknown_to_postdate(): void + { + $result = $this->service->getBrowseOrder('unknown_desc'); + + $this->assertEquals(['postdate', 'desc'], $result); + } + + /** + * Test that getBrowseOrder handles invalid direction by defaulting to desc. + */ + public function test_get_browse_order_defaults_invalid_direction_to_desc(): void + { + $result = $this->service->getBrowseOrder('name_invalid'); + + $this->assertEquals(['searchname', 'desc'], $result); + } +} diff --git a/tests/Unit/ReleaseSearchServiceOrderingTest.php b/tests/Unit/ReleaseSearchServiceOrderingTest.php new file mode 100644 index 000000000..8a4a54c77 --- /dev/null +++ b/tests/Unit/ReleaseSearchServiceOrderingTest.php @@ -0,0 +1,110 @@ +service = $reflection->newInstanceWithoutConstructor(); + } + + /** + * Test that getBrowseOrder correctly maps 'added' to 'adddate' column. + */ + public function test_get_browse_order_maps_added_to_adddate(): void + { + $result = $this->service->getBrowseOrder('added_desc'); + + $this->assertEquals(['adddate', 'desc'], $result); + } + + /** + * Test that getBrowseOrder correctly handles added ascending. + */ + public function test_get_browse_order_handles_added_ascending(): void + { + $result = $this->service->getBrowseOrder('added_asc'); + + $this->assertEquals(['adddate', 'asc'], $result); + } + + /** + * Test that getBrowseOrder correctly maps 'posted' to 'postdate' column. + */ + public function test_get_browse_order_maps_posted_to_postdate(): void + { + $result = $this->service->getBrowseOrder('posted_desc'); + + $this->assertEquals(['postdate', 'desc'], $result); + } + + /** + * Test that getBrowseOrder defaults to postdate desc for empty string. + */ + public function test_get_browse_order_defaults_to_postdate_desc(): void + { + $result = $this->service->getBrowseOrder(''); + + $this->assertEquals(['postdate', 'desc'], $result); + } + + /** + * Test that getBrowseOrder correctly maps 'name' to 'searchname' column. + */ + public function test_get_browse_order_maps_name_to_searchname(): void + { + $result = $this->service->getBrowseOrder('name_asc'); + + $this->assertEquals(['searchname', 'asc'], $result); + } + + /** + * Test that getBrowseOrder correctly maps 'size' to 'size' column. + */ + public function test_get_browse_order_maps_size_correctly(): void + { + $result = $this->service->getBrowseOrder('size_desc'); + + $this->assertEquals(['size', 'desc'], $result); + } + + /** + * Test that getBrowseOrder correctly maps 'files' to 'totalpart' column. + */ + public function test_get_browse_order_maps_files_to_totalpart(): void + { + $result = $this->service->getBrowseOrder('files_asc'); + + $this->assertEquals(['totalpart', 'asc'], $result); + } + + /** + * Test that getBrowseOrder correctly maps 'stats' to 'grabs' column. + */ + public function test_get_browse_order_maps_stats_to_grabs(): void + { + $result = $this->service->getBrowseOrder('stats_desc'); + + $this->assertEquals(['grabs', 'desc'], $result); + } + + /** + * Test that getBrowseOrder correctly maps 'cat' to 'categories_id' column. + */ + public function test_get_browse_order_maps_cat_to_categories_id(): void + { + $result = $this->service->getBrowseOrder('cat_asc'); + + $this->assertEquals(['categories_id', 'asc'], $result); + } +}