mirror of
https://github.com/NNTmux/newznab-tmux.git
synced 2026-08-28 17:01:16 +00:00
Refactor JavaScript functionality for improved performance and maintainability. Optimized theme initialization, event delegation, and modal handling. Consolidated copy-to-clipboard functionality and added new features for user management in the admin panel. Updated confirmation prompts to use data attributes for better accessibility. Cleaned up unused scripts and improved code organization across various files.
This commit is contained in:
+33137
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,130 @@
|
||||
services:
|
||||
laravel.test:
|
||||
build:
|
||||
context: ./docker/8.4
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
WWWGROUP: '${WWWGROUP}'
|
||||
image: sail-8.4/app
|
||||
extra_hosts:
|
||||
- 'host.docker.internal:host-gateway'
|
||||
ports:
|
||||
- '${APP_PORT:-80}:80'
|
||||
- '${VITE_PORT:-5173}:${VITE_PORT:-5173}'
|
||||
environment:
|
||||
TZ: ${APP_TIMEZONE}
|
||||
COMPOSER_AUTH: ${COMPOSER_AUTH}
|
||||
WWWUSER: '${WWWUSER}'
|
||||
LARAVEL_SAIL: 1
|
||||
XDEBUG_MODE: '${SAIL_XDEBUG_MODE:-off}'
|
||||
XDEBUG_CONFIG: '${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}'
|
||||
volumes:
|
||||
- '.:/var/www/html'
|
||||
networks:
|
||||
- sail
|
||||
depends_on:
|
||||
- mariadb
|
||||
- redis
|
||||
- manticore
|
||||
- elasticsearch
|
||||
mariadb:
|
||||
image: 'mariadb:latest'
|
||||
ports:
|
||||
- '${FORWARD_DB_PORT:-3306}:3306'
|
||||
environment:
|
||||
TZ: ${APP_TIMEZONE}
|
||||
MYSQL_ROOT_PASSWORD: '${DB_PASSWORD}'
|
||||
MYSQL_ROOT_HOST: '%'
|
||||
MYSQL_DATABASE: '${DB_DATABASE}'
|
||||
MYSQL_USER: '${DB_USERNAME}'
|
||||
MYSQL_PASSWORD: '${DB_PASSWORD}'
|
||||
MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
|
||||
command:
|
||||
--max_allowed_packet=128M
|
||||
--group_concat_max_len=16384
|
||||
--max_connections=200
|
||||
volumes:
|
||||
- 'sail-mariadb:/var/lib/mysql'
|
||||
- './vendor/laravel/sail/database/mysql/create-testing-database.sh:/docker-entrypoint-initdb.d/10-create-testing-database.sh'
|
||||
networks:
|
||||
- sail
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD
|
||||
- mysqladmin
|
||||
- ping
|
||||
- '-p${DB_PASSWORD}'
|
||||
retries: 3
|
||||
timeout: 5s
|
||||
redis:
|
||||
image: 'redis:alpine'
|
||||
ports:
|
||||
- '${FORWARD_REDIS_PORT:-6379}:6379'
|
||||
environment:
|
||||
TZ: ${APP_TIMEZONE}
|
||||
volumes:
|
||||
- 'sail-redis:/data'
|
||||
networks:
|
||||
- sail
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD
|
||||
- redis-cli
|
||||
- ping
|
||||
retries: 3
|
||||
timeout: 5s
|
||||
mailpit:
|
||||
image: 'axllent/mailpit:latest'
|
||||
ports:
|
||||
- '${FORWARD_MAILPIT_PORT:-1025}:1025'
|
||||
- '${FORWARD_MAILPIT_DASHBOARD_PORT:-8025}:8025'
|
||||
networks:
|
||||
- sail
|
||||
manticore:
|
||||
image: manticoresearch/manticore:7.4.6
|
||||
environment:
|
||||
TZ: ${APP_TIMEZONE}
|
||||
EXTRA: 1 # Activates extra features
|
||||
restart: always
|
||||
ports:
|
||||
- 9306:9306
|
||||
- 9308:9308
|
||||
ulimits:
|
||||
nproc: 65535
|
||||
nofile:
|
||||
soft: 65535
|
||||
hard: 65535
|
||||
memlock:
|
||||
soft: -1
|
||||
hard: -1
|
||||
volumes:
|
||||
- 'sail-manticore:/var/lib/manticore/data'
|
||||
- ./misc/manticoresearch/manticore.conf:/etc/manticoresearch/manticore.conf # uncomment if you use a custom config
|
||||
networks:
|
||||
- sail
|
||||
elasticsearch:
|
||||
image: docker.elastic.co/elasticsearch/elasticsearch:8.17.0
|
||||
environment:
|
||||
- TZ=${APP_TIMEZONE}
|
||||
- discovery.type=single-node
|
||||
- xpack.security.enabled=false
|
||||
- "ES_JAVA_OPTS=-Xms512m -Xmx512m"
|
||||
ports:
|
||||
- 9200:9200
|
||||
- 9300:9300
|
||||
volumes:
|
||||
- sail-elasticsearch:/usr/share/elasticsearch/data
|
||||
networks:
|
||||
- sail
|
||||
networks:
|
||||
sail:
|
||||
driver: bridge
|
||||
volumes:
|
||||
sail-mariadb:
|
||||
driver: local
|
||||
sail-redis:
|
||||
driver: local
|
||||
sail-manticore:
|
||||
driver: local
|
||||
sail-elasticsearch:
|
||||
driver: local
|
||||
Generated
+5867
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,32 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" backupGlobals="false" bootstrap="./bootstrap/autoload.php" colors="true" processIsolation="false" stopOnFailure="false" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.0/phpunit.xsd" cacheDirectory=".phpunit.cache" backupStaticProperties="false">
|
||||
<coverage>
|
||||
<include>
|
||||
<directory suffix=".php">Blacklight</directory>
|
||||
<directory suffix=".php">app</directory>
|
||||
</include>
|
||||
<report>
|
||||
<clover outputFile="tests/build/logs/clover.xml"/>
|
||||
</report>
|
||||
</coverage>
|
||||
<testsuites>
|
||||
<testsuite name="Install">
|
||||
<directory suffix="Test.php">tests/Install</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Unit">
|
||||
<directory suffix="Test.php">tests/Unit</directory>
|
||||
</testsuite>
|
||||
<testsuite name="Feature">
|
||||
<directory suffix="Test.php">tests/Feature</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<php>
|
||||
<env name="APP_ENV" value="testing"/>
|
||||
<env name="BCRYPT_ROUNDS" value="4"/>
|
||||
<env name="CACHE_DRIVER" value="array"/>
|
||||
<env name="SESSION_DRIVER" value="array"/>
|
||||
<env name="QUEUE_DRIVER" value="sync"/>
|
||||
<env name="MAIL_DRIVER" value="array"/>
|
||||
</php>
|
||||
<logging/>
|
||||
</phpunit>
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../storage/covers/anime
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../storage/covers/audio
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../storage/covers/audiosample
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../storage/covers/book
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../storage/covers/console
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../storage/covers/games
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../storage/covers/music
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../storage/covers/preview
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../storage/covers/sample
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../storage/covers/tvrage
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../storage/covers/tvshows
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../storage/covers/video
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../storage/covers/xxx
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../../storage/covers
|
||||
+24
-7
@@ -4,17 +4,34 @@ import './bootstrap';
|
||||
import '../css/csp-safe.css';
|
||||
import './csp-safe.js';
|
||||
|
||||
// Theme initialization - must run early to prevent flash
|
||||
// Theme initialization - optimized to prevent flash of unstyled content
|
||||
(function() {
|
||||
const themePreference = document.querySelector('meta[name="theme-preference"]')?.content || 'light';
|
||||
'use strict';
|
||||
// Use requestAnimationFrame for better performance
|
||||
const applyTheme = function() {
|
||||
const metaTheme = document.querySelector('meta[name="theme-preference"]');
|
||||
const themePreference = metaTheme?.content || 'light';
|
||||
|
||||
if (themePreference === 'system') {
|
||||
// Use OS preference
|
||||
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||
if (themePreference === 'system') {
|
||||
// Use OS preference with proper media query listener
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
if (mediaQuery.matches) {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
} else if (themePreference === 'dark') {
|
||||
document.documentElement.classList.add('dark');
|
||||
} else {
|
||||
document.documentElement.classList.remove('dark');
|
||||
}
|
||||
} else if (themePreference === 'dark') {
|
||||
document.documentElement.classList.add('dark');
|
||||
};
|
||||
|
||||
// Apply theme immediately if DOM is ready, otherwise wait
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', applyTheme);
|
||||
} else {
|
||||
applyTheme();
|
||||
}
|
||||
})();
|
||||
|
||||
|
||||
+875
-61
File diff suppressed because it is too large
Load Diff
@@ -247,7 +247,7 @@
|
||||
</a>
|
||||
<a href="{{ url('admin/movie-edit?id=' . ($movie['imdbid'] ?? $movie->imdbid ?? '') . '&update=1') }}"
|
||||
class="ml-auto px-6 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700"
|
||||
onclick="return confirm('This will fetch and update movie data from TMDB. Continue?')">
|
||||
data-confirm="This will fetch and update movie data from TMDB. Continue?">
|
||||
<i class="fa fa-refresh mr-2"></i>Update from TMDB
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -146,7 +146,7 @@
|
||||
<a href="{{ url('/admin/release-delete/' . $release->guid) }}"
|
||||
class="text-red-600 dark:text-red-400 hover:text-red-900 dark:hover:text-red-300"
|
||||
title="Delete release"
|
||||
onclick="return confirm('Are you sure you want to delete this release?')">
|
||||
data-confirm="Are you sure you want to delete this release?">
|
||||
<i class="fa fa-trash"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
<a href="{{ url('admin/release-delete/' . $release->id) }}"
|
||||
class="text-red-600 dark:text-red-400 hover:text-red-900 dark:hover:text-red-300"
|
||||
title="Delete"
|
||||
onclick="return confirm('Are you sure you want to delete this release?')">
|
||||
data-confirm="Are you sure you want to delete this release?">
|
||||
<i class="fa fa-trash"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
@@ -278,7 +278,8 @@
|
||||
<button type="button"
|
||||
class="text-green-600 dark:text-green-400 hover:text-green-900 dark:hover:text-green-300 border-0 bg-transparent cursor-pointer p-0"
|
||||
title="Verify User"
|
||||
onclick="showVerifyModal(event, this.closest('form'))">
|
||||
data-show-verify-modal
|
||||
data-form-id="{{ $user->id }}">
|
||||
<i class="fa fa-check-circle"></i>
|
||||
</button>
|
||||
</form>
|
||||
@@ -330,7 +331,7 @@
|
||||
<i class="fa fa-check-circle text-green-600 dark:text-green-400 mr-2"></i>
|
||||
Verify User
|
||||
</h3>
|
||||
<button type="button" onclick="hideVerifyModal()" class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
|
||||
<button type="button" data-close-verify-modal class="text-gray-400 hover:text-gray-600 dark:hover:text-gray-300">
|
||||
<i class="fas fa-times text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -342,12 +343,12 @@
|
||||
</div>
|
||||
<div class="px-6 py-4 bg-gray-50 dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700 flex justify-end space-x-3">
|
||||
<button type="button"
|
||||
onclick="hideVerifyModal()"
|
||||
data-close-verify-modal
|
||||
class="px-4 py-2 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 font-medium">
|
||||
<i class="fas fa-times mr-2"></i>Cancel
|
||||
</button>
|
||||
<button type="button"
|
||||
onclick="submitVerifyForm()"
|
||||
data-submit-verify-form
|
||||
class="px-4 py-2 bg-green-600 dark:bg-green-700 text-white rounded-lg hover:bg-green-700 dark:hover:bg-green-800 transition font-medium">
|
||||
<i class="fa fa-check mr-2"></i>Verify
|
||||
</button>
|
||||
|
||||
@@ -279,7 +279,7 @@
|
||||
<!-- Preview/Sample Image Modal -->
|
||||
<div id="previewModal" class="hidden fixed inset-0 bg-black bg-opacity-75 items-center justify-center p-4" style="display: none; z-index: 9999 !important;">
|
||||
<div class="relative max-w-4xl w-full">
|
||||
<button type="button" onclick="closePreviewModal()" class="absolute top-4 right-4 text-white hover:text-gray-300 text-3xl font-bold z-10">
|
||||
<button type="button" data-close-preview-modal class="absolute top-4 right-4 text-white hover:text-gray-300 text-3xl font-bold z-10">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
<div class="text-center mb-2">
|
||||
@@ -299,7 +299,7 @@
|
||||
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200 dark:text-gray-200 flex items-center">
|
||||
<i class="fas fa-info-circle mr-2 text-blue-600 dark:text-blue-400 dark:text-blue-400"></i> Media Information
|
||||
</h3>
|
||||
<button type="button" onclick="closeMediainfoModal()" class="text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:text-gray-400 dark:hover:text-gray-300 text-2xl font-bold">
|
||||
<button type="button" data-close-mediainfo-modal class="text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:text-gray-400 dark:hover:text-gray-300 text-2xl font-bold">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
@@ -319,7 +319,7 @@
|
||||
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200 dark:text-gray-200 flex items-center">
|
||||
<i class="fas fa-file-archive mr-2 text-green-600 dark:text-green-400"></i> File List
|
||||
</h3>
|
||||
<button type="button" onclick="closeFilelistModal()" class="text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:text-gray-400 dark:hover:text-gray-300 text-2xl font-bold">
|
||||
<button type="button" data-close-filelist-modal class="text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:text-gray-400 dark:hover:text-gray-300 text-2xl font-bold">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -348,7 +348,7 @@
|
||||
<!-- Preview/Sample Image Modal -->
|
||||
<div id="previewModal" class="hidden fixed inset-0 bg-black bg-opacity-75 items-center justify-center p-4">
|
||||
<div class="relative max-w-4xl w-full">
|
||||
<button type="button" onclick="closePreviewModal()" class="absolute top-4 right-4 text-white hover:text-gray-300 text-3xl font-bold z-10">
|
||||
<button type="button" data-close-preview-modal class="absolute top-4 right-4 text-white hover:text-gray-300 text-3xl font-bold z-10">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
<div class="text-center mb-2">
|
||||
|
||||
@@ -236,7 +236,7 @@
|
||||
<div class="flex space-x-2">
|
||||
<a href="{{ route('profileedit', ['action' => 'newapikey']) }}"
|
||||
class="px-6 py-2 text-blue-700 bg-blue-100 rounded-lg hover:bg-blue-200 transition"
|
||||
onclick="return confirm('Are you sure you want to generate a new API key?')">
|
||||
data-confirm="Are you sure you want to generate a new API key?">
|
||||
<i class="fas fa-key mr-2"></i>New API Key
|
||||
</a>
|
||||
<button type="submit" class="px-6 py-2 bg-blue-600 dark:bg-blue-700 text-white rounded-lg hover:bg-blue-700 dark:hover:bg-blue-800 transition">
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
@if(!($isadmin ?? false) && !$publicview)
|
||||
<a href="{{ url('profile_delete?id=' . $user->id) }}"
|
||||
class="px-4 py-2 bg-red-600 dark:bg-red-700 text-white text-sm rounded hover:bg-red-700 dark:hover:bg-red-800 transition"
|
||||
onclick="return confirm('Are you sure you want to delete your account? This action cannot be undone.')">
|
||||
data-confirm="Are you sure you want to delete your account? This action cannot be undone.">
|
||||
<i class="fa fa-trash mr-1"></i>Delete Account
|
||||
</a>
|
||||
@endif
|
||||
|
||||
@@ -282,82 +282,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Copy to clipboard functionality using modern Clipboard API
|
||||
document.querySelectorAll('.copy-btn').forEach(function(btn) {
|
||||
btn.addEventListener('click', function() {
|
||||
const targetId = this.getAttribute('data-copy-target');
|
||||
const input = document.getElementById(targetId);
|
||||
if (input) {
|
||||
input.select();
|
||||
input.setSelectionRange(0, 99999);
|
||||
navigator.clipboard.writeText(input.value).then(() => {
|
||||
// Visual feedback
|
||||
const icon = this.querySelector('i');
|
||||
icon.classList.remove('fa-copy');
|
||||
icon.classList.add('fa-check');
|
||||
this.classList.add('text-green-600');
|
||||
setTimeout(() => {
|
||||
icon.classList.remove('fa-check');
|
||||
icon.classList.add('fa-copy');
|
||||
this.classList.remove('text-green-600');
|
||||
}, 2000);
|
||||
}).catch(() => {
|
||||
// Fallback for older browsers
|
||||
document.execCommand('copy');
|
||||
const icon = this.querySelector('i');
|
||||
icon.classList.remove('fa-copy');
|
||||
icon.classList.add('fa-check');
|
||||
this.classList.add('text-green-600');
|
||||
setTimeout(() => {
|
||||
icon.classList.remove('fa-check');
|
||||
icon.classList.add('fa-copy');
|
||||
this.classList.remove('text-green-600');
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// API Token copy button
|
||||
const copyApiTokenBtn = document.getElementById('copyApiToken');
|
||||
if (copyApiTokenBtn) {
|
||||
copyApiTokenBtn.addEventListener('click', function() {
|
||||
const input = document.getElementById('apiTokenInput');
|
||||
if (input) {
|
||||
input.select();
|
||||
input.setSelectionRange(0, 99999);
|
||||
navigator.clipboard.writeText(input.value).then(() => {
|
||||
// Visual feedback
|
||||
const icon = this.querySelector('i');
|
||||
icon.classList.remove('fa-copy');
|
||||
icon.classList.add('fa-check');
|
||||
this.classList.add('text-green-600');
|
||||
setTimeout(() => {
|
||||
icon.classList.remove('fa-check');
|
||||
icon.classList.add('fa-copy');
|
||||
this.classList.remove('text-green-600');
|
||||
}, 2000);
|
||||
}).catch(() => {
|
||||
// Fallback for older browsers
|
||||
document.execCommand('copy');
|
||||
const icon = this.querySelector('i');
|
||||
icon.classList.remove('fa-copy');
|
||||
icon.classList.add('fa-check');
|
||||
this.classList.add('text-green-600');
|
||||
setTimeout(() => {
|
||||
icon.classList.remove('fa-check');
|
||||
icon.classList.add('fa-copy');
|
||||
this.classList.remove('text-green-600');
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{{-- Copy to clipboard functionality moved to csp-safe.js --}}
|
||||
@endpush
|
||||
@endsection
|
||||
|
||||
|
||||
@@ -383,7 +383,7 @@
|
||||
<!-- Preview/Sample Image Modal -->
|
||||
<div id="previewModal" class="hidden fixed inset-0 bg-black bg-opacity-75 items-center justify-center p-4" style="display: none; z-index: 9999 !important;">
|
||||
<div class="relative max-w-4xl w-full">
|
||||
<button type="button" onclick="closePreviewModal()" class="absolute top-4 right-4 text-white hover:text-gray-300 text-3xl font-bold z-10">
|
||||
<button type="button" data-close-preview-modal class="absolute top-4 right-4 text-white hover:text-gray-300 text-3xl font-bold z-10">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
<div class="text-center mb-2">
|
||||
|
||||
@@ -286,47 +286,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function switchSeason(seasonNumber) {
|
||||
// Hide all season content
|
||||
document.querySelectorAll('.season-content').forEach(content => {
|
||||
content.classList.add('hidden');
|
||||
});
|
||||
|
||||
// Remove active styling from all tabs
|
||||
document.querySelectorAll('.season-tab').forEach(tab => {
|
||||
tab.classList.remove('border-blue-500', 'text-blue-600');
|
||||
tab.classList.add('border-transparent', 'text-gray-500');
|
||||
|
||||
// Update badge styling
|
||||
const badge = tab.querySelector('span');
|
||||
if (badge) {
|
||||
badge.classList.remove('bg-blue-100', 'text-blue-800');
|
||||
badge.classList.add('bg-gray-100', 'text-gray-600');
|
||||
}
|
||||
});
|
||||
|
||||
// Show selected season content
|
||||
const selectedContent = document.querySelector(`.season-content[data-season="${seasonNumber}"]`);
|
||||
if (selectedContent) {
|
||||
selectedContent.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Add active styling to selected tab
|
||||
const selectedTab = document.querySelector(`.season-tab[data-season="${seasonNumber}"]`);
|
||||
if (selectedTab) {
|
||||
selectedTab.classList.remove('border-transparent', 'text-gray-500');
|
||||
selectedTab.classList.add('border-blue-500', 'text-blue-600');
|
||||
|
||||
// Update badge styling
|
||||
const badge = selectedTab.querySelector('span');
|
||||
if (badge) {
|
||||
badge.classList.remove('bg-gray-100', 'text-gray-600');
|
||||
badge.classList.add('bg-blue-100', 'text-blue-800');
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{{-- Season switcher functionality moved to csp-safe.js --}}
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use App\Console\Commands\NntmuxResetPostProcessing;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use ReflectionClass;
|
||||
|
||||
class NntmuxResetPostProcessingTest extends TestCase
|
||||
{
|
||||
private function callNormalize(array $raw): array
|
||||
{
|
||||
$cmd = new NntmuxResetPostProcessing;
|
||||
$ref = new ReflectionClass($cmd);
|
||||
$method = $ref->getMethod('normalizeCategories');
|
||||
$method->setAccessible(true);
|
||||
/** @var array $res */
|
||||
$res = $method->invoke($cmd, $raw);
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
private function callInvalid(array $normalized): array
|
||||
{
|
||||
$cmd = new NntmuxResetPostProcessing;
|
||||
$ref = new ReflectionClass($cmd);
|
||||
$method = $ref->getMethod('invalidCategories');
|
||||
$method->setAccessible(true);
|
||||
/** @var array $res */
|
||||
$res = $method->invoke($cmd, $normalized);
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
public function test_normalize_handles_basic_and_commas_and_repeats_and_case_and_plural(): void
|
||||
{
|
||||
$this->assertSame([], $this->callNormalize([]));
|
||||
$this->assertSame(['movie'], $this->callNormalize(['movie']));
|
||||
$this->assertSame(['movie'], $this->callNormalize(['Movies']));
|
||||
$this->assertSame(['movie', 'tv'], $this->callNormalize(['movie,tv']));
|
||||
$this->assertSame(['movie', 'tv', 'music'], $this->callNormalize(['movie', 'TV', 'musics']));
|
||||
}
|
||||
|
||||
public function test_normalize_strips_value_from_equals_tokens(): void
|
||||
{
|
||||
// Simulate Symfony short option mapping: -category=misc => -c ategory=misc => option('category') contains [ 'ategory=misc' ]
|
||||
$this->assertSame(['misc'], $this->callNormalize(['ategory=misc']));
|
||||
// Also support explicit key=value inputs like category=music
|
||||
$this->assertSame(['music'], $this->callNormalize(['category=music']));
|
||||
}
|
||||
|
||||
public function test_invalid_detection_and_all_passthrough(): void
|
||||
{
|
||||
$this->assertSame(['foo'], $this->callInvalid(['foo']));
|
||||
$this->assertSame([], $this->callInvalid(['movie', 'tv']));
|
||||
$this->assertSame([], $this->callInvalid(['all']));
|
||||
$this->assertSame(['foo'], $this->callInvalid(['all', 'foo']));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user