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:
DariusIII
2025-10-31 16:01:01 +01:00
parent 4b847c04dd
commit 71abf520bf
46 changed files with 40318 additions and 199 deletions
+33137
View File
File diff suppressed because it is too large Load Diff
+130
View File
@@ -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
+5867
View File
File diff suppressed because it is too large Load Diff
+32
View File
@@ -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.
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
+1
View File
@@ -0,0 +1 @@
../../storage/covers/anime
+1
View File
@@ -0,0 +1 @@
../../storage/covers/audio
+1
View File
@@ -0,0 +1 @@
../../storage/covers/audiosample
+1
View File
@@ -0,0 +1 @@
../../storage/covers/book
+1
View File
@@ -0,0 +1 @@
../../storage/covers/console
+1
View File
@@ -0,0 +1 @@
../../storage/covers/games
+1
View File
@@ -0,0 +1 @@
../../storage/covers/music
+1
View File
@@ -0,0 +1 @@
../../storage/covers/preview
+1
View File
@@ -0,0 +1 @@
../../storage/covers/sample
+1
View File
@@ -0,0 +1 @@
../../storage/covers/tvrage
+1
View File
@@ -0,0 +1 @@
../../storage/covers/tvshows
+1
View File
@@ -0,0 +1 @@
../../storage/covers/video
+1
View File
@@ -0,0 +1 @@
../../storage/covers/xxx
+1
View File
@@ -0,0 +1 @@
../../storage/covers
+24 -7
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -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>
+5 -4
View File
@@ -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>
+3 -3
View File
@@ -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>
+1 -1
View File
@@ -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">
+1 -1
View File
@@ -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">
+1 -1
View File
@@ -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
+1 -76
View File
@@ -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
+1 -1
View File
@@ -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">
+1 -41
View File
@@ -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']));
}
}