diff --git a/.env.example b/.env.example index 536d5299e..6dc6e770b 100644 --- a/.env.example +++ b/.env.example @@ -103,10 +103,15 @@ PUSHER_APP_CLUSTER=mt1 MIX_PUSHER_APP_KEY="${PUSHER_APP_KEY}" MIX_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" -NOCAPTCHA_ENABLED= +CAPTCHA_PROVIDER=recaptcha +NOCAPTCHA_ENABLED=false NOCAPTCHA_SITEKEY= NOCAPTCHA_SECRET= +TURNSTILE_SECRET= +TURNSTILE_SITEKEY= +TURNSTILE_ENABLED=false + SCOUT_DRIVER=database SCOUT_QUEUE=true diff --git a/CAPTCHA_CONFIGURATION.md b/CAPTCHA_CONFIGURATION.md new file mode 100644 index 000000000..dd247bd4a --- /dev/null +++ b/CAPTCHA_CONFIGURATION.md @@ -0,0 +1,157 @@ +# CAPTCHA Configuration Guide + +This application supports two CAPTCHA providers: +- **Google reCAPTCHA v2** +- **Cloudflare Turnstile** + +## Important: Only One Provider at a Time + +**You can only enable ONE captcha provider at a time.** The system will use the provider specified in `CAPTCHA_PROVIDER` environment variable. + +## Configuration + +### 1. Choose Your Provider + +In your `.env` file, set the provider: + +```env +CAPTCHA_PROVIDER=recaptcha # or 'turnstile' +``` + +### 2. Google reCAPTCHA Configuration + +To use Google reCAPTCHA: + +```env +CAPTCHA_PROVIDER=recaptcha +NOCAPTCHA_ENABLED=true +NOCAPTCHA_SITEKEY=your_recaptcha_sitekey +NOCAPTCHA_SECRET=your_recaptcha_secret + +# Disable Turnstile +TURNSTILE_ENABLED=false +``` + +**How to get reCAPTCHA keys:** +1. Visit [Google reCAPTCHA Admin](https://www.google.com/recaptcha/admin) +2. Register a new site with reCAPTCHA v2 (Checkbox) +3. Copy the Site Key and Secret Key + +### 3. Cloudflare Turnstile Configuration + +To use Cloudflare Turnstile: + +```env +CAPTCHA_PROVIDER=turnstile +TURNSTILE_ENABLED=true +TURNSTILE_SITEKEY=your_turnstile_sitekey +TURNSTILE_SECRET=your_turnstile_secret + +# Disable reCAPTCHA +NOCAPTCHA_ENABLED=false +``` + +**How to get Turnstile keys:** +1. Visit [Cloudflare Dashboard](https://dash.cloudflare.com/) +2. Go to Turnstile section +3. Create a new site +4. Copy the Site Key and Secret Key + +## Features + +### Where CAPTCHA is Used + +CAPTCHA verification is applied to: +- **User Registration** (`/register`) +- **User Login** (`/login`) +- **Password Reset** (`/password/reset`) +- **Contact Form** (`/contact`) + +### Switching Between Providers + +To switch from one provider to another: + +1. Update `CAPTCHA_PROVIDER` in `.env` +2. Enable the new provider and disable the old one +3. Clear config cache: `php artisan config:clear` + +Example - Switching from reCAPTCHA to Turnstile: + +```env +# Change this +CAPTCHA_PROVIDER=turnstile + +# Enable Turnstile +TURNSTILE_ENABLED=true +TURNSTILE_SITEKEY=your_turnstile_sitekey +TURNSTILE_SECRET=your_turnstile_secret + +# Disable reCAPTCHA +NOCAPTCHA_ENABLED=false +``` + +## Disabling CAPTCHA + +To completely disable CAPTCHA protection: + +```env +NOCAPTCHA_ENABLED=false +TURNSTILE_ENABLED=false +``` + +## Testing + +### Test Keys + +**Google reCAPTCHA Test Keys:** +- Site Key: `6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI` +- Secret Key: `6LeIxAcTAAAAAGG-vFI1TnRWxMZNFuojJ4WifJWe` + +**Cloudflare Turnstile Test Keys:** +- Site Key: `1x00000000000000000000AA` (Always passes) +- Secret Key: `1x0000000000000000000000000000000AA` + +## Troubleshooting + +### CAPTCHA Not Showing + +1. Check if CAPTCHA is enabled in `.env` +2. Verify you have both sitekey and secret configured +3. Clear config cache: `php artisan config:clear` +4. Check browser console for JavaScript errors + +### Validation Failing + +1. Verify the secret key is correct +2. Check if the domain is allowed in your CAPTCHA provider settings +3. Check application logs for detailed error messages + +### Both Providers Enabled Warning + +If you see a warning in logs about both providers being enabled, ensure you have: +- Set `CAPTCHA_PROVIDER` to either `recaptcha` or `turnstile` +- Disabled the provider you're not using by setting its `ENABLED` flag to `false` + +## Technical Details + +### Files Modified + +- **Config:** `config/captcha.php` - Main configuration +- **Service:** `app/Services/TurnstileService.php` - Turnstile API integration +- **Helper:** `app/Support/CaptchaHelper.php` - Unified captcha interface +- **Rule:** `app/Rules/TurnstileRule.php` - Turnstile validation rule +- **Views:** Updated to use dynamic captcha provider +- **Requests:** Updated validation to support both providers + +### API Endpoints + +- **reCAPTCHA:** `https://www.google.com/recaptcha/api/siteverify` +- **Turnstile:** `https://challenges.cloudflare.com/turnstile/v0/siteverify` + +## Security Notes + +- Never commit your production keys to version control +- Use environment-specific `.env` files +- Regularly rotate your secret keys +- Monitor failed CAPTCHA attempts in logs + diff --git a/app/Extensions/helper/helpers.php b/app/Extensions/helper/helpers.php index aa2982cd6..d36185e78 100644 --- a/app/Extensions/helper/helpers.php +++ b/app/Extensions/helper/helpers.php @@ -530,3 +530,20 @@ if (! function_exists('formatBytes')) { return round($bytes, 2).' '.$units[$pow]; } } + +if (! function_exists('csp_nonce')) { + /** + * Generate a CSP nonce for inline scripts + * This should be stored in the request and reused across the request lifecycle + */ + function csp_nonce(): string + { + static $nonce = null; + + if ($nonce === null) { + $nonce = base64_encode(random_bytes(16)); + } + + return $nonce; + } +} diff --git a/app/Http/Controllers/Auth/ForgotPasswordController.php b/app/Http/Controllers/Auth/ForgotPasswordController.php index 6bd422acb..15fba19aa 100644 --- a/app/Http/Controllers/Auth/ForgotPasswordController.php +++ b/app/Http/Controllers/Auth/ForgotPasswordController.php @@ -37,6 +37,12 @@ class ForgotPasswordController extends Controller */ public function showLinkRequestForm(Request $request) { + // If it's a GET request, just show the form + if ($request->isMethod('get')) { + return view('auth.passwords.email'); + } + + // Handle POST request $sent = ''; $email = $request->input('email') ?? ''; $rssToken = $request->input('apikey') ?? ''; @@ -45,10 +51,8 @@ class ForgotPasswordController extends Controller return view('auth.passwords.email')->withErrors(['error' => 'Missing parameter (email and/or apikey) to send password reset']); } - if (config('captcha.enabled') === true && (! empty(config('captcha.secret')) && ! empty(config('captcha.sitekey')))) { - $validate = Validator::make($request->all(), [ - 'g-recaptcha-response' => 'required|captcha', - ]); + if (\App\Support\CaptchaHelper::isEnabled()) { + $validate = Validator::make($request->all(), \App\Support\CaptchaHelper::getValidationRules()); if ($validate->fails()) { return view('auth.passwords.email')->withErrors(['error' => 'Captcha validation failed.']); } diff --git a/app/Http/Controllers/Auth/LoginController.php b/app/Http/Controllers/Auth/LoginController.php index dab2b8790..4ec6c7e92 100644 --- a/app/Http/Controllers/Auth/LoginController.php +++ b/app/Http/Controllers/Auth/LoginController.php @@ -13,6 +13,7 @@ use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Support\Arr; use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Validator; class LoginController extends Controller @@ -73,7 +74,13 @@ class LoginController extends Controller } if ($validator->passes()) { - $user = User::query()->orWhere(['username' => $request->input('username'), 'email' => $request->input('username')])->withTrashed()->first(); + $user = User::query() + ->where(function ($query) use ($request) { + $query->where('username', $request->input('username')) + ->orWhere('email', $request->input('username')); + }) + ->withTrashed() + ->first(); if ($user !== null) { // Check if user is soft deleted if ($user->trashed()) { @@ -147,9 +154,11 @@ class LoginController extends Controller } $this->incrementLoginAttempts($request); + Log::channel('failed_login')->error('Failed login attempt by user: '.$request->input('username').' from IP address: '.$request->ip()); $request->session()->flash('message', 'Username or email and password combination used does not match our records!'); } else { $this->incrementLoginAttempts($request); + Log::channel('failed_login')->error('Failed login attempt by user: '.$request->input('username').' from IP address: '.$request->ip()); $request->session()->flash('message', 'Username or email used do not match our records!'); } @@ -158,6 +167,7 @@ class LoginController extends Controller $this->incrementLoginAttempts($request); $request->session()->flash('message', implode('', Arr::collapse($validator->errors()->toArray()))); + Log::channel('failed_login')->error('Failed login attempt by user: '.$request->input('username').' from IP address: '.$request->ip()); return redirect()->to('login'); } diff --git a/app/Http/Controllers/Auth/RegisterController.php b/app/Http/Controllers/Auth/RegisterController.php index e03c7fe8c..b0e805cb7 100644 --- a/app/Http/Controllers/Auth/RegisterController.php +++ b/app/Http/Controllers/Auth/RegisterController.php @@ -11,7 +11,6 @@ use Illuminate\Foundation\Auth\RegistersUsers; use Illuminate\Http\RedirectResponse; use Illuminate\Http\Request; use Illuminate\Routing\Redirector; -use Illuminate\Support\Arr; use Illuminate\Support\Facades\Password as PasswordFacade; use Illuminate\Support\Facades\Validator; use Illuminate\Validation\Rules\Password; @@ -130,12 +129,19 @@ class RegisterController extends Controller 'username' => ['required', 'string', 'min:5', 'max:255', 'unique:users'], 'email' => ['required', 'string', 'email', 'max:255', 'unique:users', 'indisposable'], 'password' => ['required', 'confirmed', Password::min(8)->letters()->mixedCase()->numbers()->symbols()->uncompromised()], + ], [ + 'password.min' => 'The password must be at least 8 characters.', + 'password.letters' => 'The password must contain letters.', + 'password.mixed_case' => 'The password must contain both uppercase and lowercase letters.', + 'password.numbers' => 'The password must contain at least one number.', + 'password.symbols' => 'The password must contain at least one symbol.', + 'password.uncompromised' => 'The password appears in a data breach and should not be used.', ]); if ($validator->fails()) { - $error = implode('', Arr::collapse($validator->errors()->toArray())); - - return $this->showRegistrationForm($request, $error); + return redirect()->back() + ->withErrors($validator) + ->withInput($request->except('password', 'password_confirmation')); } $action = $request->input('action') ?? 'view'; @@ -164,7 +170,7 @@ class RegisterController extends Controller // Check invitation validity using custom system $invitationValid = $this->isInvitationValid($inviteCode, $email); - $registrationOpen = Settings::settingValue('registerstatus') !== Settings::REGISTER_STATUS_INVITE; + $registrationOpen = Settings::settingValue('registerstatus') === Settings::REGISTER_STATUS_OPEN; if ($invitationValid || $registrationOpen) { // Get invited_by from invitation if available @@ -229,12 +235,10 @@ class RegisterController extends Controller break; case 'view': - // See if it is a valid invite. + // Don't set showRegister here - let showRegistrationForm handle it + // Only validate invite code if present if (($inviteCode !== null) && ! $this->isInvitationTokenValid($inviteCode)) { $error = 'Invalid invitation token!'; - $showRegister = 0; - } else { - $showRegister = 1; } break; } diff --git a/app/Http/Middleware/ContentSecurityPolicy.php b/app/Http/Middleware/ContentSecurityPolicy.php new file mode 100644 index 000000000..2f89a9e2c --- /dev/null +++ b/app/Http/Middleware/ContentSecurityPolicy.php @@ -0,0 +1,70 @@ +headers->set('X-Frame-Options', 'SAMEORIGIN'); + $response->headers->set('X-Content-Type-Options', 'nosniff'); + $response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin'); + + return $response; + } + + // Generate nonce for inline scripts + $nonce = function_exists('csp_nonce') ? csp_nonce() : base64_encode(random_bytes(16)); + + // Build CSP directives for non-Turnstile pages + $directives = [ + "default-src 'self'", + "script-src 'self' 'nonce-{$nonce}' 'unsafe-inline' 'unsafe-eval' https://www.google.com https://www.gstatic.com https://cdn.jsdelivr.net https://unpkg.com blob:", + "script-src-elem 'self' 'nonce-{$nonce}' 'unsafe-inline' https://www.google.com https://www.gstatic.com https://cdn.jsdelivr.net https://unpkg.com", + "script-src-attr 'unsafe-inline'", + "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com", + "font-src 'self' https://fonts.gstatic.com data:", + "img-src 'self' data: https: blob:", + "connect-src 'self' https://www.google.com", + "frame-src 'self' https://www.google.com https://www.gstatic.com data: blob:", + "child-src 'self' https://www.google.com blob:", + "worker-src 'self' blob:", + "object-src 'none'", + "base-uri 'self'", + "form-action 'self'", + "frame-ancestors 'self'", + ]; + + $csp = implode('; ', $directives); + + $response->headers->set('Content-Security-Policy', $csp); + + // Also set X-Frame-Options for older browsers + $response->headers->set('X-Frame-Options', 'SAMEORIGIN'); + + // Set X-Content-Type-Options + $response->headers->set('X-Content-Type-Options', 'nosniff'); + + // Set Referrer-Policy + $response->headers->set('Referrer-Policy', 'strict-origin-when-cross-origin'); + + return $response; + } +} diff --git a/app/Models/Settings.php b/app/Models/Settings.php index 31411e0c9..4df00ebe4 100644 --- a/app/Models/Settings.php +++ b/app/Models/Settings.php @@ -27,18 +27,10 @@ use Illuminate\Database\Eloquent\Model; /** * Settings - model for settings table. * - * @property string $section - * @property string $subsection * @property string $name * @property string $value - * @property string $hint - * @property string $setting * - * @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Settings whereHint($value) * @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Settings whereName($value) - * @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Settings whereSection($value) - * @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Settings whereSetting($value) - * @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Settings whereSubsection($value) * @method static \Illuminate\Database\Eloquent\Builder|\App\Models\Settings whereValue($value) * * @mixin \Eloquent @@ -67,26 +59,19 @@ class Settings extends Model public const ERR_BADTMPUNRARPATH = -6; - public const ERR_BADNZBPATH_UNREADABLE = -7; - - public const ERR_BADNZBPATH_UNSET = -8; - - public const ERR_BAD_COVERS_PATH = -9; - - public const ERR_BAD_YYDECODER_PATH = -10; - public const ERR_BADLAMEPATH = -11; public const ERR_SABCOMPLETEPATH = -12; /** - * @var array + * @var string */ - protected $primaryKey = ['section', 'subsection', 'name']; + protected $primaryKey = 'name'; /** * @var string */ + protected $keyType = 'string'; /** * @var bool @@ -110,6 +95,16 @@ class Settings extends Model protected static $settingsCollection; + /** + * Get the value attribute and convert empty strings to null and numeric strings to numbers. + * + * @param string $value + */ + public function getValueAttribute($value): mixed + { + return self::convertValue($value); + } + /** * Adapted from https://laravel.io/forum/01-15-2016-overriding-eloquent-attributes. * @@ -130,41 +125,60 @@ class Settings extends Model } /** - * Return a tree-like array of all or selected settings. - * - * @param bool $excludeUnsectioned If rows with empty 'section' field should be excluded. - * Note this doesn't prevent empty 'subsection' fields. + * Return a simple key-value array of all settings. * * @throws \RuntimeException */ public static function toTree(bool $excludeUnsectioned = true): array { - $results = self::cursor()->remember(); + $results = self::query()->pluck('value', 'name')->toArray(); - $tree = []; - if (! empty($results)) { - foreach ($results as $result) { - if (! $excludeUnsectioned || ! empty($result->section)) { - $tree[$result->section][$result->subsection][$result->name] = - ['value' => $result->value, 'hint' => $result->hint]; - } - } - } else { + if (empty($results)) { throw new \RuntimeException( 'No results from Settings table! Check your table has been created and populated.' ); } - return $tree; + return $results; } public static function settingValue($setting): mixed { - return self::query()->where( - [ - 'name' => $setting, - ] - )->value('value'); + $value = self::query()->where('name', $setting)->value('value'); + + // Apply the same conversion logic as the accessor + return self::convertValue($value); + } + + /** + * Convert setting value: numeric strings to numbers, preserve empty strings. + * + * @param string|null $value + */ + public static function convertValue($value): mixed + { + // Handle null + if ($value === null) { + return null; + } + + // Keep empty strings as empty strings (don't convert to null) + // Many settings expect empty strings, not null + if ($value === '') { + return ''; + } + + // Convert numeric strings to actual numbers + if (is_numeric($value)) { + // Check if it's an integer or float + if (strpos($value, '.') !== false) { + return (float) $value; + } + + return (int) $value; + } + + return $value; } public static function settingsUpdate(array $data = []): void diff --git a/app/Models/User.php b/app/Models/User.php index 2d403c704..24f05953c 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -690,4 +690,10 @@ class User extends Authenticatable { return $this->hasOne(PasswordSecurity::class); } + + public static function canPost($user_id): bool + { + // return true if can_post column is true and false if can_post column is false + return self::where('id', $user_id)->value('can_post'); + } } diff --git a/app/Services/TurnstileService.php b/app/Services/TurnstileService.php new file mode 100644 index 000000000..0d28c8db9 --- /dev/null +++ b/app/Services/TurnstileService.php @@ -0,0 +1,72 @@ +post('https://challenges.cloudflare.com/turnstile/v0/siteverify', [ + 'secret' => $secret, + 'response' => $token, + 'remoteip' => $remoteIp ?? request()->ip(), + ]); + + $result = $response->json(); + + return isset($result['success']) && $result['success'] === true; + } catch (\Exception $e) { + \Log::error('Turnstile verification failed: '.$e->getMessage()); + + return false; + } + } + + /** + * Get the Turnstile HTML widget + */ + public static function display(array $attributes = []): string + { + $sitekey = config('captcha.turnstile.sitekey'); + + if (empty($sitekey)) { + return ''; + } + + $defaultAttributes = [ + 'class' => 'cf-turnstile', + 'data-sitekey' => $sitekey, + 'data-theme' => 'auto', + 'data-size' => 'normal', + ]; + + $attributes = array_merge($defaultAttributes, $attributes); + + $attributesString = ''; + foreach ($attributes as $key => $value) { + $attributesString .= sprintf('%s="%s" ', $key, htmlspecialchars($value)); + } + + return sprintf('
', trim($attributesString)); + } + + /** + * Get the Turnstile JavaScript + */ + public static function renderJs(): string + { + return ''; + } +} diff --git a/app/Support/CaptchaHelper.php b/app/Support/CaptchaHelper.php new file mode 100644 index 000000000..97779d4c8 --- /dev/null +++ b/app/Support/CaptchaHelper.php @@ -0,0 +1,129 @@ +display($attributes); + } + + /** + * Render the captcha JavaScript + */ + public static function renderJs(): string + { + if (! self::isEnabled()) { + return ''; + } + + $provider = self::getProvider(); + + if ($provider === 'turnstile') { + return TurnstileService::renderJs(); + } + + // Default to reCAPTCHA + return app('captcha')->renderJs(); + } + + /** + * Get the captcha response field name + */ + public static function getResponseFieldName(): string + { + $provider = self::getProvider(); + + if ($provider === 'turnstile') { + return 'cf-turnstile-response'; + } + + return 'g-recaptcha-response'; + } + + /** + * Get validation rules for captcha + */ + public static function getValidationRules(): array + { + if (! self::isEnabled()) { + return []; + } + + $provider = self::getProvider(); + $fieldName = self::getResponseFieldName(); + + if ($provider === 'turnstile') { + return [ + $fieldName => [ + 'required', + new \App\Rules\TurnstileRule, + ], + ]; + } + + // Default to reCAPTCHA + return [ + $fieldName => [ + 'required', + 'captcha', + ], + ]; + } +} diff --git a/config/captcha.php b/config/captcha.php index b8cdc943a..9da6e618e 100644 --- a/config/captcha.php +++ b/config/captcha.php @@ -1,6 +1,24 @@ env('CAPTCHA_PROVIDER', 'recaptcha'), + + // Google reCAPTCHA settings + 'recaptcha' => [ + 'secret' => env('NOCAPTCHA_SECRET', ''), + 'sitekey' => env('NOCAPTCHA_SITEKEY', ''), + 'enabled' => env('NOCAPTCHA_ENABLED', false), + ], + + // Cloudflare Turnstile settings + 'turnstile' => [ + 'secret' => env('TURNSTILE_SECRET', ''), + 'sitekey' => env('TURNSTILE_SITEKEY', ''), + 'enabled' => env('TURNSTILE_ENABLED', false), + ], + + // Legacy support - keep for backward compatibility 'secret' => env('NOCAPTCHA_SECRET', ''), 'sitekey' => env('NOCAPTCHA_SITEKEY', ''), 'enabled' => env('NOCAPTCHA_ENABLED', false), diff --git a/config/database.php b/config/database.php index ffd63705b..a49542e2d 100644 --- a/config/database.php +++ b/config/database.php @@ -14,8 +14,8 @@ return [ 'username' => env('DB_USERNAME', 'nntmux'), 'password' => env('DB_PASSWORD', ''), 'unix_socket' => env('DB_SOCKET', ''), - 'charset' => 'utf8', - 'collation' => 'utf8_unicode_ci', + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', 'strict' => false, 'engine' => 'InnoDB ROW_FORMAT=DYNAMIC', 'options' => extension_loaded('pdo_mysql') ? array_filter([ @@ -56,8 +56,8 @@ return [ 'username' => env('DB_USERNAME', 'nntmux'), 'password' => env('DB_PASSWORD', ''), 'unix_socket' => env('DB_SOCKET', ''), - 'charset' => 'utf8', - 'collation' => 'utf8_unicode_ci', + 'charset' => 'utf8mb4', + 'collation' => 'utf8mb4_unicode_ci', 'strict' => false, 'engine' => 'InnoDB ROW_FORMAT=DYNAMIC', 'options' => extension_loaded('pdo_mysql') ? array_filter([ diff --git a/database/migrations/2025_10_20_132950_convert_content_table_to_utf8mb4.php b/database/migrations/2025_10_20_132950_convert_content_table_to_utf8mb4.php new file mode 100644 index 000000000..497a89ec8 --- /dev/null +++ b/database/migrations/2025_10_20_132950_convert_content_table_to_utf8mb4.php @@ -0,0 +1,25 @@ + + + + + + + + + + + + NZB + + + + + + diff --git a/resources/assets/js/functions.js b/resources/assets/js/functions.js deleted file mode 100755 index cb95f532c..000000000 --- a/resources/assets/js/functions.js +++ /dev/null @@ -1,914 +0,0 @@ -//enable bootstrap tooltips -let tooltipTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="tooltip"]')); -let tooltipList = tooltipTriggerList.map(function (tooltipTriggerEl) { - return new bootstrap.Tooltip(tooltipTriggerEl); -}); -// event bindings -jQuery(function ($) { - const base_url = window.location.origin; - $('.cartadd').click(function (e) { - if ($(this).hasClass('icon_cart_clicked')) return false; - let guid = $('.guid').attr('id').substring(4); - //alert(guid); - $.ajaxSetup({ - headers: { - 'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content'), - }, - }); - $.post(base_url + '/cart/add?id=' + guid, function (resp) { - $(e.target).addClass('icon_cart_clicked').attr('title', 'Added to Cart'); - PNotify.defaults.icons = 'fontawesome5'; - PNotify.success({ - title: 'Release added to your Download Basket!', - type: 'success', - icon: 'fa fa-info fa-3x', - Animate: { - animate: true, - in_class: 'bounceInLeft', - out_class: 'bounceOutRight', - }, - desktop: { - desktop: true, - fallback: true, - }, - }); - }); - return false; - }); - - // browse.tpl, search.tpl -- show icons on hover - let orig_opac = $('table.data tr').children('td.icons').children('div.icon').css('opacity'); - $('table.data tr').hover( - function () { - $(this).children('td.icons').children('div.icon').css('opacity', 1); - }, - function () { - $(this).children('td.icons').children('div.icon').css('opacity', orig_opac); - } - ); - - $('.forumpostsubmit').click(function (e) { - if ($.trim($('#addMessage').val()) == '' || $.trim($('#addSubject').val()) == '') { - alert('Please enter a subject and message.'); - return false; - } - }); - - $('.forumreplysubmit').click(function (e) { - if ($.trim($('#addMessage').val()) == '') { - alert('Please enter a message.'); - return false; - } - }); - - $('.check').click(function (e) { - if (!$(e.target).is('input')) - $(this) - .children('.nzb_check') - .attr('checked', !$(this).children('.nzb_check').attr('checked')); - }); - - $('.descmore').click(function (e) { - $(this).prev('.descinitial').hide(); - $(this).next('.descfull').show(); - $(this).hide(); - return false; - }); - - $('.nzb_check_all').change(function () { - if ($(this).attr('checked')) { - $('table.data tr td input:checkbox').attr('checked', $(this).attr('checked')); - } else { - $('table.data tr td input:checkbox').removeAttr('checked'); - } - }); - - $('.nzb_check_all_season').change(function () { - let season = $(this).attr('name'); - $('table.data tr td input:checkbox').each(function (i, row) { - if ($(row).attr('name') == season) { - $(row).attr('checked', !$(row).attr('checked')); - } - }); - }); - - // browse.tpl, search.tpl - $('.icon_cart').click(function (e) { - if ($(this).hasClass('icon_cart_clicked')) return false; - let guid = $(this).attr('id').substring(4); - //alert(guid); - //alert(base_url + "/cart/add?id=" + guid); - $.ajaxSetup({ - headers: { - 'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content'), - }, - }); - $.post(base_url + '/cart/add?id=' + guid, function (resp) { - $(e.target).addClass('icon_cart_clicked').attr('title', ' Release added to Cart'); - PNotify.defaults.icons = 'fontawesome5'; - PNotify.success({ - title: 'Release added to your download basket!', - type: 'success', - icon: 'fa fa-info fa-3x', - Animate: { - animate: true, - in_class: 'bounceInLeft', - out_class: 'bounceOutRight', - }, - desktop: { - desktop: true, - fallback: true, - }, - }); - }); - return false; - }); - - $('table.data a.modal_nfo').colorbox({ - // NFO modal - href: function () { - return $(this).attr('href') + '&modal'; - }, - title: function () { - return $(this).parent().parent().children('a.title').text(); - }, - innerWidth: '800px', - innerHeight: '90%', - initialWidth: '800px', - initialHeight: '90%', - speed: 0, - opacity: 0.7, - }); - // Screenshot modal - $('table.data a.modal_prev').colorbox({ - scrolling: false, - maxWidth: '800px', - maxHeight: '450px', - }); - - $('table.data a.modal_imdb') - .colorbox({ - // IMDB modal - href: function () { - return base_url + 'movie/' + $(this).attr('name').substring(4) + '&modal'; - }, - title: function () { - return $(this).parent().parent().children('a.title').text(); - }, - innerWidth: '800px', - innerHeight: '450px', - initialWidth: '800px', - initialHeight: '450px', - speed: 0, - opacity: 0.7, - }) - .click(function () { - $('#colorbox').removeClass().addClass('cboxMovie'); - }); - - $('a.modal_imdbtrailer') - .colorbox({ - // IMDB trailer modal - href: function () { - return base_url + 'movietrailer/' + $(this).attr('name').substring(4) + '&modal'; - }, - title: function () { - return $(this).parent().parent().children('a.title').text(); - }, - innerWidth: '800px', - innerHeight: '450px', - initialWidth: '800px', - initialHeight: '450px', - speed: 0, - opacity: 0.7, - }) - .click(function () { - $('#colorbox').removeClass().addClass('cboxMovie'); - }); - - $('table.data a.modal_music') - .colorbox({ - // Music modal - href: function () { - return base_url + 'musicmodal/' + $(this).attr('name').substring(4) + '&modal'; - }, - title: function () { - return $(this).parent().parent().children('a.title').text(); - }, - innerWidth: '800px', - innerHeight: '450px', - initialWidth: '800px', - initialHeight: '450px', - speed: 0, - opacity: 0.7, - }) - .click(function () { - $('#colorbox').removeClass().addClass('cboxMusic'); - }); - $('table.data a.modal_console') - .colorbox({ - // Console modal - href: function () { - return base_url + 'consolemodal/' + $(this).attr('name').substring(4) + '&modal'; - }, - title: function () { - return $(this).parent().parent().children('a.title').text(); - }, - innerWidth: '800px', - innerHeight: '450px', - initialWidth: '800px', - initialHeight: '450px', - speed: 0, - opacity: 0.7, - }) - .click(function () { - $('#colorbox').removeClass().addClass('cboxConsole'); - }); - $('table.data a.modal_book') - .colorbox({ - // Book modal - href: function () { - return base_url + 'bookmodal/' + $(this).attr('name').substring(4) + '&modal'; - }, - title: function () { - return $(this).parent().parent().children('a.title').text(); - }, - innerWidth: '800px', - innerHeight: '450px', - initialWidth: '800px', - initialHeight: '450px', - speed: 0, - opacity: 0.7, - }) - .click(function () { - $('#colorbox').removeClass().addClass('cboxBook'); - }); - - $('#nzb_multi_operations_form').submit(function () { - return false; - }); - - // Reusable function to get selected item IDs - function getSelectedItemIds() { - const selectedIds = []; - - // Try multiple common container selectors to find checkboxes - const checkboxSelectors = [ - "INPUT[type='checkbox']:checked", // Any checked checkbox - ".table INPUT[type='checkbox']:checked", // Table checkboxes - ".card INPUT[type='checkbox']:checked", // Card layout checkboxes - "[data-guid] INPUT[type='checkbox']:checked", // Elements with data-guid attribute - "form INPUT[type='checkbox']:checked" // Form checkboxes - ]; - - // Try each selector until we find checked boxes - for (const selector of checkboxSelectors) { - $(selector).each(function() { - const value = $(this).val(); - if (value && value !== 'on') { - selectedIds.push(value); - } - }); - - // If we found any items, stop searching - if (selectedIds.length > 0) { - break; - } - } - - console.log('Selected IDs:', selectedIds); // Debugging - return selectedIds; - } - - // Download button handler - $('button.nzb_multi_operations_download').on('click', function() { - const ids = getSelectedItemIds().join(','); - if (ids) { - window.location = base_url + '/getnzb?zip=1&id=' + ids; - } else { - PNotify.alert({ - title: 'No items selected', - text: 'Please select at least one item to download.', - type: 'info' - }); - } - }); - - // Cart button handler - $('button.nzb_multi_operations_cart').on('click', function() { - const selectedIds = getSelectedItemIds(); - if (selectedIds.length === 0) { - PNotify.alert({ - title: 'No items selected', - text: 'Please select at least one item to add to your cart.', - type: 'info' - }); - return; - } - - $.ajaxSetup({ - headers: { - 'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content'), - }, - }); - - // Find and update cart icons using data attributes - selectedIds.forEach(guid => { - // Look for cart icon with matching data attribute - const $cartIcon = $(`[data-guid="${guid}"] .icon_cart, #guid${guid}`); - if ($cartIcon.length && !$cartIcon.hasClass('icon_cart_clicked')) { - $cartIcon.addClass('icon_cart_clicked').attr('title', 'Added to Cart'); - } - }); - - // Uncheck all selected checkboxes - $('input[type="checkbox"]:checked').prop('checked', false); - - // Send to cart - if (selectedIds.length > 0) { - const guidString = selectedIds.join(','); - $.post(base_url + '/cart/add?id=' + guidString, function() { - PNotify.defaults.icons = 'fontawesome5'; - PNotify.success({ - title: 'Items added to your Download Basket!', - type: 'success', - icon: 'fa fa-info fa-3x', - Animate: { - animate: true, - in_class: 'bounceInLeft', - out_class: 'bounceOutRight', - }, - desktop: { - desktop: true, - fallback: true, - }, - }); - }); - } - }); - $('button.nzb_multi_operations_sab').on('click', function () { - $("table.data INPUT[type='checkbox']:checked").each(function (i, row) { - let $sabIcon = $(row).parent().parent().children('td.icons').children('.icon_sab'); - let guid = $(row).val(); - //alert(guid); - if (guid && !$sabIcon.hasClass('icon_sab_clicked')) { - let nzburl = base_url + '/sendtoqueue/' + guid; - // alert(nzburl); - $.post(nzburl, function (resp) { - $sabIcon.addClass('icon_sab_clicked').attr('title', 'Added to Queue'); - PNotify.defaults.icons = 'fontawesome5'; - PNotify.success({ - title: 'Release added to your download queue!', - type: 'success', - icon: 'fa fa-info fa-3x', - Animate: { - animate: true, - in_class: 'bounceInLeft', - out_class: 'bounceOutRight', - }, - desktop: { - desktop: true, - fallback: true, - }, - }); - }); - } - $(this).attr('checked', false); - }); - }); - $('input.nzb_multi_operations_nzbget').on('click', function () { - $("table.data INPUT[type='checkbox']:checked").each(function (i, row) { - let $nzbgetIcon = $(row) - .parent() - .parent() - .children('td.icons') - .children('.icon_nzbget'); - let guid = $(row).val(); - if (guid && !$nzbgetIcon.hasClass('icon_nzbget_clicked')) { - let nzburl = base_url + '/sendtoqueue/' + guid; - $.post(nzburl, function (resp) { - $nzbgetIcon.addClass('icon_nzbget_clicked').attr('title', 'Added to Queue'); - PNotify.defaults.icons = 'fontawesome5'; - PNotify.success({ - title: 'Release added to your download queue!', - type: 'success', - icon: 'fa fa-info fa-3x', - Animate: { - animate: true, - in_class: 'bounceInLeft', - out_class: 'bounceOutRight', - }, - desktop: { - desktop: true, - fallback: true, - }, - }); - }); - } - $(this).attr('checked', false); - }); - }); - - //front end admin functions - $('input.nzb_multi_operations_edit').click(function () { - let ids = ''; - $("table.data INPUT[type='checkbox']:checked").each(function (i, row) { - if ($(row).val() != 'on') ids += '&id[]=' + $(row).val(); - }); - if (ids) - $('input.nzb_multi_operations_edit').colorbox({ - href: function () { - return ( - base_url + - 'ajax_release-admin?action=edit' + - ids + - '&from=' + - encodeURIComponent(window.location) - ); - }, - title: 'Edit Release', - innerWidth: '400px', - innerHeight: '250px', - initialWidth: '400px', - initialHeight: '250px', - speed: 0, - opacity: 0.7, - }); - }); - $('input.nzb_multi_operations_delete').click(function () { - let ids = ''; - $("table.data INPUT[type='checkbox']:checked").each(function (i, row) { - if ($(row).val() != 'on') ids += '&id[]=' + $(row).val(); - }); - if (ids) { - PNotify.defaults.icons = 'fontawesome5'; - const notice = PNotify.alert({ - title: 'Confirmation Needed', - text: 'Are you sure you want to delete the selected releases?', - icon: 'glyphicon glyphicon-question-sign', - hide: false, - modules: { - Confirm: { - confirm: true, - }, - }, - Buttons: { - closer: false, - sticker: false, - }, - History: { - history: false, - }, - }); - notice.on('pnotify.confirm', function () { - $.post(base_url + 'ajax_release-admin?action=dodelete' + ids, function (resp) { - location.reload(true); - }); - }); - notice.on('pnotify.cancel', function () { - alert('Cancelled'); - }); - } - }); - $('input.nzb_multi_operations_rebuild').click(function () { - let ids = ''; - $("table.data INPUT[type='checkbox']:checked").each(function (i, row) { - if ($(row).val() != 'on') ids += '&id[]=' + $(row).val(); - }); - if (ids) - if (confirm('Are you sure you want to rebuild the selected releases?')) { - $.post(base_url + 'ajax_release-admin?action=dorebuild' + ids, function (resp) { - location.reload(true); - }); - } - }); - //cart functions - $('input.nzb_multi_operations_cartdelete').click(function () { - let ids = new Array(); - $("table.data INPUT[type='checkbox']:checked").each(function (i, row) { - if ($(row).val() != 'on') ids.push($(row).val()); - }); - $.ajaxSetup({ - headers: { - 'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content'), - }, - }); - - //alert(base_url + "/cart/delete/" + ids); - if (ids) { - PNotify.defaults.icons = 'fontawesome5'; - const notice = PNotify.alert({ - title: 'Confirmation Needed', - text: 'Are you sure you want to delete the selected releases from your cart?', - hide: false, - modules: { - Confirm: { - confirm: true, - }, - }, - Buttons: { - closer: false, - sticker: false, - }, - History: { - history: false, - }, - }); - notice.on('pnotify.confirm', function () { - $.post(base_url + '/cart/delete/' + ids); - }); - notice.on('pnotify.cancel', function () { - alert('Cancelled'); - }); - } - }); - $('input.nzb_multi_operations_cartsab').click(function () { - let ids = new Array(); - $("table.data INPUT[type='checkbox']:checked").each(function (i, row) { - let guid = $(row).val(); - let nzburl = base_url + '/sendtoqueue/' + guid; - $.post(nzburl, function () { - PNotify.defaults.icons = 'fontawesome5'; - PNotify.success({ - title: 'Releases sent to queue!', - type: 'success', - icon: 'fa fa-info fa-3x', - Animate: { - animate: true, - in_class: 'bounceInLeft', - out_class: 'bounceOutRight', - }, - desktop: { - desktop: true, - fallback: true, - }, - }); - }); - }); - }); - - // headermenu.tpl - $('#headsearch') - .focus(function () { - if (this.value === 'Search...') this.value = ''; - else this.select(); - }) - .blur(function () { - if (this.value === '') this.value = 'Search...'; - }); - $('#headsearch_form').submit(function () { - $('#headsearch_go').trigger('click'); - return false; - }); - document.getElementById('headsearch_go').addEventListener('click', function () { - let searchInput = document.getElementById('headsearch'); - let categoryInput = document.getElementById('headcat'); - if (searchInput.value && searchInput.value != 'Search...') { - let sText = searchInput.value; - let sCat = categoryInput.value !== '-1' ? '&t=' + categoryInput.value : 't=-1'; - window.location.href = base_url + '/search?' + sCat + '&search=' + sText; - } else { - PNotify.defaults.icons = 'fontawesome5'; - PNotify.alert({ - title: 'You need to enter a search term!', - type: 'error', - icon: 'fa fa-info fa-3x', - Animate: { - animate: true, - in_class: 'bounceInLeft', - out_class: 'bounceOutRight', - }, - desktop: { - desktop: true, - fallback: true, - }, - }); - } - }); - - // search.tpl - $('#search_search_button').click(function () { - if ($('#search').val()) - document.location = - base_url + - '/search?search=' + - $('#search').val() + - ($('#search_cat').val() != -1 ? '&t=' + $('#search_cat').val() : '&t=-1'); - return false; - }); - - $('#search').focus(function () { - this.select(); - }); - - // searchraw.tpl - $('#searchraw_search_button').click(function () { - if ($('#search').val()) document.location = base_url + '/searchraw/' + $('#search').val(); - return false; - }); - $('#searchraw_download_selected').click(function () { - if ($('#dl input:checked').length) $('#dl').trigger('submit'); - return false; - }); - - // login.tpl, register.tpl, search.tpl, searchraw.tpl - if ($('#username').length) $('#username').focus(); - if ($('#search').length) $('#search').focus(); - - // viewfilelist.tpl - $('#viewfilelist_download_selected').click(function () { - if ($('#fileform input:checked').length) $('#fileform').trigger('submit'); - return false; - }); - - // misc - $('.confirm_action').click(function () { - return confirm('Are you sure?'); - }); - - // play audio preview - $('.audioprev').click(function () { - let a = document.getElementById($(this).next('audio').attr('ID')); - if (a != null) { - if ($(this).text() == 'Listen') { - a.play(); - $(this).text('Stop'); - } else { - a.pause(); - a.currentTime = 0; - $(this).text('Listen'); - } - } - - a.addEventListener('ended', function () { - $(this).prev().text('Listen'); - }); - - return false; - }); - - // mmenu - $('.mmenu').click(function () { - document.location = $(this).children('a').attr('href'); - return false; - }); - - // mmenu_new - $('.mmenu_new').click(function () { - window.open($(this).children('a').attr('href')); - return false; - }); - - // searchraw.tpl, viewfilelist.tpl -- checkbox operations - // selections - let last1, last2; - $('.checkbox_operations .select_all').click(function () { - $("table.data INPUT[type='checkbox']").attr('checked', true).trigger('change'); - return false; - }); - $('.checkbox_operations .select_none').click(function () { - $("table.data INPUT[type='checkbox']").attr('checked', false).trigger('change'); - return false; - }); - $('.checkbox_operations .select_invert').click(function () { - $("table.data INPUT[type='checkbox']").each(function () { - $(this).attr('checked', !$(this).attr('checked')).trigger('change'); - }); - return false; - }); - $('.checkbox_operations .select_range').click(function () { - if (last1 && last2 && last1 < last2) - $("table.data INPUT[type='checkbox']") - .slice(last1, last2) - .attr('checked', true) - .trigger('change'); - else if (last1 && last2) - $("table.data INPUT[type='checkbox']") - .slice(last2, last1) - .attr('checked', true) - .trigger('change'); - return false; - }); - $('table.data td.check INPUT[type="checkbox"]').click(function (e) { - // range event interaction -- see further above - let rowNum = $(e.target).parent().parent()[0].rowIndex; - if (last1) last2 = last1; - last1 = rowNum; - - // perform range selection - if (e.shiftKey && last1 && last2) { - if (last1 < last2) - $("table.data INPUT[type='checkbox']") - .slice(last1, last2) - .attr('checked', true) - .trigger('change'); - else - $("table.data INPUT[type='checkbox']") - .slice(last2, last1) - .attr('checked', true) - .trigger('change'); - } - }); - $('table.data a.data_filename').click(function (e) { - // click filenames to select - // range event interaction -- see further above - let rowNum = $(e.target).parent().parent()[0].rowIndex; - if (last1) last2 = last1; - last1 = rowNum; - - let $checkbox = $( - 'table.data tr:nth-child(' + (rowNum + 1) + ') td.selection INPUT[type="checkbox"]' - ); - $checkbox.attr('checked', !$checkbox.attr('checked')); - - return false; - }); - - // show/hide previews - $('#showmoviepreviews').click(function () { - $('#moviepreviews').next('form').toggle('fast'); - $('#moviepreviews').toggle('fast', function () { - $('#showmoviepreviews').text( - ($('#moviepreviews').is(':visible') ? 'Hide' : 'Show') + ' previews' - ); - }); - return false; - }); - - // show/hide invite form - $('#lnkSendInvite').click(function () { - $('#divInvite').slideToggle('fast'); - }); - - // send an invite - $('#frmSendInvite').submit(function () { - let inputEmailto = $('#txtInvite').val(); - if (isValidEmailAddress(inputEmailto)) { - // no caching of results - $.ajax({ - url: base_url + '/ajax_profile?action=1&rand=' + $.now(), - data: { emailto: inputEmailto }, - dataType: 'html', - success: function (data) { - $('#txtInvite').val(''); - $('#divInvite').slideToggle('fast'); - $('#divInviteSuccess').text(data).show(); - $('#divInviteError').hide(); - }, - error: function (xhr, err, e) { - alert('Error in ajax_profile: ' + err); - }, - }); - } else { - $('#divInviteSuccess').hide(); - $('#divInviteError').text('Invalid email').show(); - } - return false; - }); - - // movie.tpl - $('.mlmore').click(function () { - // show more movies - $(this).parent().parent().hide(); - $(this).parent().parent().parent().children('.mlextra').show(); - return false; - }); - - // lookup tmdb for a movie - $('#frmMyMovieLookup').submit(function () { - let movSearchText = $('#txtsearch').val(); - // no caching of results - $.ajax({ - url: base_url + '/ajax_mymovies?rand=' + $.now(), - data: { id: movSearchText }, - dataType: 'html', - success: function (data) { - $('#divMovResults').html(data); - }, - error: function (xhr, err, e) { - alert('Error in ajax_mymovies: ' + err); - }, - }); - - return false; - }); - - // season selector in TV - $('a[id^="seas_"]').click(function () { - seas = $(this).attr('ID').replace('seas_', ''); - - $('table[class^="tb_"]').hide(); - $('.tb_' + seas).show(); - - $('li', $(this).closest('ul')).removeClass('active'); - $(this).closest('li').addClass('active'); - - return false; - }); -}); - -$.extend({ - // http://plugins.jquery.com/project/URLEncode - URLEncode: function (c) { - let o = ''; - let x = 0; - c = c.toString(); - let r = /(^[a-zA-Z0-9_.]*)/; - while (x < c.length) { - let m = r.exec(c.substr(x)); - if (m != null && m.length > 1 && m[1] != '') { - o += m[1]; - x += m[1].length; - } else { - if (c[x] == ' ') o += '+'; - else { - let d = c.charCodeAt(x); - let h = d.toString(16); - o += '%' + (h.length < 2 ? '0' : '') + h.toUpperCase(); - } - x++; - } - } - return o; - }, - URLDecode: function (s) { - let o = s; - let binVal, t; - let r = /(%[^%]{2})/; - while ((m = r.exec(o)) != null && m.length > 1 && m[1] != '') { - b = parseInt(m[1].substr(1), 16); - t = String.fromCharCode(b); - o = o.replace(m[1], t); - } - return o; - }, -}); - -function isValidEmailAddress(emailAddress) { - let pattern = new RegExp( - /^(("[\w-\s]+")|([\w-]+(?:\.[\w-]+)*)|("[\w-\s]+")([\w-]+(?:\.[\w-]+)*))(@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$)|(@\[?((25[0-5]\.|2[0-4][0-9]\.|1[0-9]{2}\.|[0-9]{1,2}\.))((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[0-9]{1,2})\.){2}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[0-9]{1,2})\]?$)/i - ); - return pattern.test(emailAddress); -} - -function getNzbGetQueue() { - $.ajax({ - url: 'queuedata?type=nzbget&id=' + $.now(), - cache: false, - success: function (html) { - $('.nzbget_queue').html(html); - setTimeout('getNzbGetQueue()', 2500); - }, - error: function () { - $('.nzbget_queue').html( - 'Could not contact your queue. Refresh' - ); - }, - timeout: 5000, - }); -} - -function getHistory() { - $.ajax({ - url: 'queuedata?type=history&id=' + $.now(), - cache: false, - success: function (html) { - $('.sab_history').html(html); - setTimeout('getHistory()', 10000); - }, - error: function () { - //$(".sab_history").html("Could not contact your queue. Refresh"); - }, - timeout: 5000, - }); -} - -/** ****** iswitch *********************** **/ - -$(function () { - let checkAll = $('input.flat-all'); - let checkboxes = $('input.flat'); - - $('input').iCheck({ - checkboxClass: 'icheckbox_flat-green', - radioClass: 'iradio_flat-green', - }); - - checkAll.on('ifChecked ifUnchecked', function (event) { - if (event.type === 'ifChecked') { - checkboxes.iCheck('check'); - } else { - checkboxes.iCheck('uncheck'); - } - }); - - checkboxes.on('ifChanged', function (event) { - if (checkboxes.filter(':checked').length === checkboxes.length) { - checkAll.prop('checked', 'checked'); - } else { - checkAll.prop('checked', false); - } - checkAll.iCheck('update'); - }); -}); -/** ****** /iswitch *********************** **/ diff --git a/resources/assets/js/utils-admin.js b/resources/assets/js/utils-admin.js deleted file mode 100644 index 05f17e34c..000000000 --- a/resources/assets/js/utils-admin.js +++ /dev/null @@ -1,374 +0,0 @@ -/** - * ajax_group_status() - * - * @param id group id - * @param status 0 = deactive, 1 = activate - */ - -const base_url = window.location.origin; -function ajax_group_status(id, what) { - // no caching of results - let rand_no = Math.random(); - if (what != undefined) { - $.ajax({ - url: base_url + '/admin/ajax?action=toggle_group_active_status&rand=' + rand_no, - data: { group_id: id, group_status: what }, - dataType: 'html', - success: function (data) { - $('div#message').html(data); - $('div#message').show('fast', function () {}); - - // switch some links around - if (what == 0) { - $('td#group-' + id).html( - `` - ); - } else { - $('td#group-' + id).html( - `` - ); - } - - // fade.. mm - $('#message').fadeOut(5000); - }, - error: function (xhr, err, e) { - alert('Error in ajax_group_status: ' + err); - }, - }); - } else { - alert('Weird.. what group id are looking for?'); - } -} - -/** - * ajax_backfill_status() - * - * @param id group id - * @param status 0 = deactive, 1 = activate - */ -function ajax_backfill_status(id, what) { - // no caching of results - let rand_no = Math.random(); - if (what != undefined) { - $.ajax({ - url: base_url + '/admin/ajax?action=toggle_group_backfill_status&rand=' + rand_no, - data: { group_id: id, backfill_status: what }, - dataType: 'html', - success: function (data) { - $('div#message').html(data); - $('div#message').show('fast', function () {}); - - // switch some links around - if (what == 0) { - $('td#backfill-' + id).html( - `` - ); - } else { - $('td#backfill-' + id).html( - `` - ); - } - - // fade.. mm - $('#message').fadeOut(5000); - }, - error: function (xhr, err, e) { - alert('Error in ajax_backfill_status: ' + err); - }, - }); - } else { - alert('Weird.. what group id are looking for?'); - } -} - -/** - * ajax_group_delete() - * - * @param id group id - */ -function ajax_group_delete(id) { - // no caching of results - let rand_no = Math.random(); - $.ajax({ - url: base_url + '/admin/ajax?action=group_edit_delete_single&rand=' + rand_no, - data: { group_id: id }, - dataType: 'html', - success: function (data) { - $('div#message').html(data); - $('div#message').show('fast', function () {}); - $('#grouprow-' + id).fadeOut(2000); - $('#message').fadeOut(5000); - }, - error: function (xhr, err, e) { - alert('Error in ajax_group_delete: ' + err); - }, - }); -} - -/** - * ajax_group_reset() - * - * @param id group id - */ -function ajax_group_reset(id) { - // no caching of results - let rand_no = Math.random(); - $.ajax({ - url: base_url + '/admin/ajax?action=group_edit_reset_single&rand=' + rand_no, - data: { group_id: id }, - dataType: 'html', - success: function (data) { - $('div#message').html(data); - $('div#message').show('fast', function () {}); - $('#grouprow-' + id).fadeTo(2000, 0.5); - $('#message').fadeOut(5000); - }, - error: function (xhr, err, e) { - alert('Error in ajax_group_reset: ' + err); - }, - }); -} - -/** - * ajax_group_purge() - * - * @param id group id - */ -function ajax_group_purge(id) { - // no caching of results - let rand_no = Math.random(); - $.ajax({ - url: base_url + '/admin/ajax?action=group_edit_purge_single&rand=' + rand_no, - data: { group_id: id }, - dataType: 'html', - success: function (data) { - $('div#message').html(data); - $('div#message').show('fast', function () {}); - $('#grouprow-' + id).fadeTo(2000, 0.5); - $('#message').fadeOut(5000); - }, - error: function (xhr, err, e) { - alert('Error in ajax_group_purge: ' + err); - }, - }); -} - -/** - * ajax_all_reset() - * - * - */ -function ajax_all_reset() { - // no caching of results - let rand_no = Math.random(); - $.ajax({ - url: base_url + '/admin/ajax?action=group_edit_reset_all&rand=' + rand_no, - data: 'All groups reset.', - dataType: 'html', - success: function (data) { - $('div#message').html(data); - $('div#message').show('fast', function () {}); - $('#grouprow-' + id).fadeTo(2000, 0.5); - $('#message').fadeOut(5000); - }, - error: function (xhr, err, e) { - alert('Error in ajax_all_reset: ' + err); - }, - }); -} - -/** - * ajax_all_purge() - */ -function ajax_all_purge() { - // no caching of results - let rand_no = Math.random(); - $.ajax({ - url: base_url + '/admin/ajax?action=group_edit_purge_all&rand=' + rand_no, - data: 'All groups purged', - dataType: 'html', - success: function (data) { - $('div#message').html(data); - $('div#message').show('fast', function () {}); - $('#grouprow-' + id).fadeTo(2000, 0.5); - $('#message').fadeOut(5000); - }, - error: function (xhr, err, e) { - alert('Error in ajax_all_purge: ' + err); - }, - }); -} - -/** - * ajax_binaryblacklist_delete() - * - * @param id binary id - */ -function ajax_binaryblacklist_delete(id) { - // no caching of results - let rand_no = Math.random(); - $.ajax({ - url: base_url + '/admin/ajax?action=binary_blacklist_delete&rand=' + rand_no, - data: { row_id: id }, - dataType: 'html', - success: function (data) { - $('div#message').html(data); - $('div#message').show('fast', function () {}); - $('#row-' + id).fadeOut(2000); - $('#message').fadeOut(5000); - }, - error: function (xhr, err, e) { - alert('Error in ajax_binaryblacklist_delete: ' + err); - }, - }); -} - -/** - * ajax_category_regex_delete() - * - * @param id binary id - */ -function ajax_category_regex_delete(id) { - // no caching of results - let rand_no = Math.random(); - $.ajax({ - url: base_url + '/admin/ajax?action=category_regex_delete&rand=' + rand_no, - data: { row_id: id }, - dataType: 'html', - success: function (data) { - $('div#message').html(data); - $('div#message').show('fast', function () {}); - $('#row-' + id).fadeOut(2000); - $('#message').fadeOut(5000); - }, - error: function (xhr, err, e) { - alert('Error in ajax_category_regex_delete: ' + err); - }, - }); -} - -/** - * ajax_collection_regex_delete() - * - * @param id binary id - */ -function ajax_collection_regex_delete(id) { - // no caching of results - let rand_no = Math.random(); - $.ajax({ - url: base_url + '/admin/ajax?action=collection_regex_delete&rand=' + rand_no, - data: { row_id: id }, - dataType: 'html', - success: function (data) { - $('div#message').html(data); - $('div#message').show('fast', function () {}); - $('#row-' + id).fadeOut(2000); - $('#message').fadeOut(5000); - }, - error: function (xhr, err, e) { - alert('Error in ajax_collection_regex_delete: ' + err); - }, - }); -} - -/** - * ajax_release_naming_regex_delete() - * - * @param id binary id - */ -function ajax_release_naming_regex_delete(id) { - // no caching of results - let rand_no = Math.random(); - $.ajax({ - url: base_url + '/admin/ajax?action=release_naming_regex_delete&rand=' + rand_no, - data: { row_id: id }, - dataType: 'html', - success: function (data) { - $('div#message').html(data); - $('div#message').show('fast', function () {}); - $('#row-' + id).fadeOut(2000); - $('#message').fadeOut(5000); - }, - error: function (xhr, err, e) { - alert('Error in ajax_release_naming_regex_delete: ' + err); - }, - }); -} - -jQuery(function ($) { - $('#regexGroupSelect').change(function () { - document.location = '?group=' + $('#regexGroupSelect option:selected').attr('value'); - }); - - // misc - $('.confirm_action').click(function () { - return confirm('Are you sure?'); - }); -}); - -/** ****** tinyMCE ***************************/ -tinyMCE.init({ - selector: 'textarea#body', - theme: 'silver', - plugins: [ - 'advlist autolink link image lists charmap print preview hr anchor pagebreak spellchecker', - 'searchreplace wordcount visualblocks visualchars code fullscreen insertdatetime media nonbreaking', - 'save table directionality emoticons template paste code', - ], - theme_advanced_toolbar_location: 'top', - theme_advanced_toolbar_align: 'left', - toolbar: - 'insertfile undo redo | styleselect | fontselect |sizeselect | fontsizeselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link image | print preview media fullpage | forecolor backcolor emoticons | code', - fontsize_formats: '8pt 9pt 10pt 11pt 12pt 13pt 14pt 15pt 16pt 17pt 18pt 24pt 36pt', - mode: 'exact', - relative_urls: false, - remove_script_host: false, - convert_urls: true, -}); - -tinyMCE.init({ - selector: 'textarea#metadescription', - theme: 'silver', - plugins: [ - 'advlist autolink link image lists charmap print preview hr anchor pagebreak spellchecker', - 'searchreplace wordcount visualblocks visualchars code fullscreen insertdatetime media nonbreaking', - 'save table contextmenu directionality emoticons template paste textcolor code', - ], - theme_advanced_toolbar_location: 'top', - theme_advanced_toolbar_align: 'left', - toolbar: - 'insertfile undo redo | styleselect | fontselect |sizeselect | fontsizeselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link image | print preview media fullpage | forecolor backcolor emoticons | code', - fontsize_formats: '8pt 9pt 10pt 11pt 12pt 13pt 14pt 15pt 16pt 17pt 18pt 24pt 36pt', - mode: 'exact', - relative_urls: false, - remove_script_host: false, - convert_urls: true, -}); - -tinyMCE.init({ - selector: 'textarea#metakeywords', - theme: 'silver', - plugins: [ - 'advlist autolink link image lists charmap print preview hr anchor pagebreak spellchecker', - 'searchreplace wordcount visualblocks visualchars code fullscreen insertdatetime media nonbreaking', - 'save table contextmenu directionality emoticons template paste textcolor code', - ], - theme_advanced_toolbar_location: 'top', - theme_advanced_toolbar_align: 'left', - toolbar: - 'insertfile undo redo | styleselect | fontselect |sizeselect | fontsizeselect | bold italic | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | link image | print preview media fullpage | forecolor backcolor emoticons | code', - fontsize_formats: '8pt 9pt 10pt 11pt 12pt 13pt 14pt 15pt 16pt 17pt 18pt 24pt 36pt', - mode: 'exact', - relative_urls: false, - remove_script_host: false, - convert_urls: true, -}); diff --git a/resources/css/app.css b/resources/css/app.css index 192ae1792..56169dba4 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -1,9 +1,9 @@ /* Import FontAwesome */ @import '@fortawesome/fontawesome-free/css/all.min.css'; -/* Import other necessary libraries */ -@import 'animate.css/animate.min.css'; -@import 'datatables/media/css/jquery.dataTables.min.css'; + +/* Import custom CSP-safe styles */ +@import './csp-safe.css'; @tailwind base; @tailwind components; @@ -124,3 +124,234 @@ @apply flex-shrink-0; } +/* Mobile Responsive Improvements */ +@layer utilities { + /* Mobile-friendly tables */ + @media (max-width: 768px) { + .table-responsive { + @apply overflow-x-auto -mx-4 px-4; + } + + .table-responsive table { + @apply min-w-max; + } + + /* Card-style table rows for mobile */ + .table-mobile-cards tbody tr { + @apply block border border-gray-200 dark:border-gray-700 rounded-lg mb-4 p-4; + } + + .table-mobile-cards tbody td { + @apply block text-left; + padding-left: 50% !important; + position: relative; + } + + .table-mobile-cards tbody td::before { + content: attr(data-label); + position: absolute; + left: 1rem; + font-weight: 600; + @apply text-gray-700 dark:text-gray-300; + } + + .table-mobile-cards thead { + @apply hidden; + } + + /* Stack buttons vertically on mobile */ + .btn-group { + @apply flex-col space-x-0 space-y-2; + } + + /* Full width buttons on mobile */ + .btn-mobile-full { + @apply w-full justify-center; + } + + /* Mobile search forms */ + .search-form-mobile { + @apply flex-col space-y-2; + } + + .search-form-mobile select, + .search-form-mobile input { + @apply w-full rounded; + } + + /* Reduce padding on small screens */ + .container { + @apply px-2; + } + + /* Stack navigation items */ + .nav-mobile-stack { + @apply flex-col space-x-0 space-y-2; + } + + /* Hide non-essential columns on mobile */ + .hide-mobile { + @apply hidden; + } + + /* Smaller text on mobile */ + .text-mobile-sm { + @apply text-xs; + } + + /* Mobile dropdown menus should be full width */ + .dropdown-menu { + @apply left-0 right-0 w-auto mx-2; + } + + /* Mobile modal adjustments */ + .modal-content { + @apply mx-2 max-h-[90vh] overflow-y-auto; + } + + /* Smaller font sizes for headings on mobile */ + h1 { + @apply text-2xl; + } + + h2 { + @apply text-xl; + } + + h3 { + @apply text-lg; + } + + /* Mobile grid adjustments */ + .grid-mobile-1 { + @apply grid-cols-1; + } + + /* Touch-friendly sizing */ + .touch-target { + min-height: 44px; + min-width: 44px; + } + } + + /* Small mobile devices */ + @media (max-width: 480px) { + /* Even smaller text on very small screens */ + body { + @apply text-sm; + } + + .btn { + @apply text-xs px-3 py-2; + } + + /* Stack form fields */ + .form-grid { + @apply grid-cols-1 gap-2; + } + + /* Compact headers */ + .page-header { + @apply text-xl py-2; + } + + /* Reduce card padding */ + .card-mobile-compact { + @apply p-3; + } + } + + /* Tablet improvements */ + @media (min-width: 768px) and (max-width: 1024px) { + /* 2-column layout on tablets where appropriate */ + .tablet-cols-2 { + @apply grid-cols-2; + } + + /* Tablet-optimized navigation */ + .nav-tablet { + @apply space-x-2; + } + } + + /* Landscape mobile optimization */ + @media (max-height: 600px) and (orientation: landscape) { + /* Reduce vertical padding in landscape */ + .py-6 { + @apply py-2; + } + + /* Compact modals in landscape */ + .modal-content { + max-height: 85vh; + } + + /* Smaller fixed buttons */ + #mobile-sidebar-toggle, + #theme-toggle { + @apply p-2 text-sm; + } + } + + /* Mobile-optimized badges and labels */ + @media (max-width: 768px) { + .badge { + @apply text-xs px-2 py-1; + } + + /* Improve readability of release titles */ + .release-title { + @apply text-sm leading-tight; + } + + /* Stack action buttons in cards */ + .card-actions { + @apply flex-col space-y-2 space-x-0; + } + + /* Make pagination more compact */ + .pagination { + @apply text-sm; + } + + .pagination .page-link { + @apply px-2 py-1 min-w-[2rem]; + } + + /* Optimize data tables for mobile */ + table.dataTable { + font-size: 0.875rem; + } + + table.dataTable td, + table.dataTable th { + padding: 0.5rem 0.25rem; + } + + /* Improve form labels */ + label { + @apply text-sm font-medium; + } + + /* Better spacing for form groups */ + .form-group { + @apply mb-3; + } + + /* Mobile-friendly alerts */ + .alert { + @apply text-sm px-3 py-2; + } + + /* Compact breadcrumbs */ + .breadcrumb { + @apply text-xs; + } + + /* Stack filter options vertically */ + .filter-options { + @apply flex-col space-y-2; + } + } +} + diff --git a/resources/css/csp-safe.css b/resources/css/csp-safe.css new file mode 100644 index 000000000..9231d3117 --- /dev/null +++ b/resources/css/csp-safe.css @@ -0,0 +1,583 @@ +/* Sidebar Menu Animations */ +.rotate-180 { + transform: rotate(180deg); +} + +.sidebar-toggle .fa-chevron-down, +[data-toggle-submenu] .fa-chevron-down { + transition: transform 0.3s ease; +} + +.sidebar-submenu { + transition: all 0.3s ease; +} + +/* Mobile Sidebar */ +@media (max-width: 768px) { + #sidebar { + position: fixed; + top: 0; + left: -100%; + height: 100vh; + width: 280px; + z-index: 999; + transition: left 0.3s ease-in-out; + overflow-y: auto; + } + + #sidebar.mobile-open { + left: 0; + display: flex !important; + } + + /* Mobile overlay */ + .mobile-sidebar-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 998; + display: none; + } + + .mobile-sidebar-overlay.active { + display: block; + } +} + +/* Admin Dashboard Charts */ +.chart-container { + position: relative; + height: 300px; + width: 100%; +} + +@media (max-width: 768px) { + .chart-container { + height: 250px; + } +} + +/* Toast Notifications */ +#toast-container { + position: fixed; + top: 1rem; + right: 1rem; + z-index: 9999; + display: flex; + flex-direction: column; + gap: 0.5rem; + pointer-events: none; +} + +/* Mobile Toast Notifications */ +@media (max-width: 640px) { + #toast-container { + top: 0.5rem; + right: 0.5rem; + left: 0.5rem; + max-width: 100%; + } +} + +.toast-notification { + display: flex; + align-items: center; + gap: 0.75rem; + min-width: 300px; + max-width: 500px; + padding: 1rem 1.25rem; + border-radius: 0.5rem; + box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); + animation: slideIn 0.3s ease-out; + pointer-events: auto; + transition: all 0.3s ease; +} + +.toast-notification.removing { + animation: slideOut 0.3s ease-in; + opacity: 0; + transform: translateX(100%); +} + +/* Success Toast */ +.toast-notification.success { + background-color: #10b981; + color: white; +} + +.dark .toast-notification.success { + background-color: #059669; +} + +/* Error Toast */ +.toast-notification.error { + background-color: #ef4444; + color: white; +} + +.dark .toast-notification.error { + background-color: #dc2626; +} + +/* Info Toast */ +.toast-notification.info { + background-color: #3b82f6; + color: white; +} + +.dark .toast-notification.info { + background-color: #2563eb; +} + +/* Warning Toast */ +.toast-notification.warning { + background-color: #f59e0b; + color: white; +} + +.dark .toast-notification.warning { + background-color: #d97706; +} + +.toast-icon { + flex-shrink: 0; + font-size: 1.25rem; +} + +.toast-message { + flex: 1; + font-size: 0.875rem; + font-weight: 500; + line-height: 1.5; +} + +.toast-close { + flex-shrink: 0; + background: transparent; + border: none; + color: inherit; + font-size: 1.5rem; + line-height: 1; + cursor: pointer; + padding: 0; + margin-left: 0.5rem; + opacity: 0.8; + transition: opacity 0.2s; +} + +.toast-close:hover { + opacity: 1; +} + +@keyframes slideIn { + from { + transform: translateX(100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +@keyframes slideOut { + from { + transform: translateX(0); + opacity: 1; + } + to { + transform: translateX(100%); + opacity: 0; + } +} + +/* Inline form display */ +.inline-form { + display: inline; +} + +/* Line clamp utility for text truncation */ +.line-clamp-2 { + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +/* Admin Tmux - Select color states - Light mode */ +#tmuxForm select.select-yes { + background-color: #d1e7dd !important; + border-color: #badbcc !important; + color: #0f5132 !important; +} + +#tmuxForm select.select-no { + background-color: #f8d7da !important; + border-color: #f5c2c7 !important; + color: #842029 !important; +} + +#tmuxForm select.select-other { + background-color: #e2e3e5 !important; + border-color: #d3d6d8 !important; + color: #41464b !important; +} + +#tmuxForm select:focus { + box-shadow: 0 0 0 .25rem rgba(13,110,253,.25); +} + +/* Admin Tmux - Select color states - Dark mode */ +@media (prefers-color-scheme: dark) { + #tmuxForm select.select-yes { + background-color: #1a3a2a !important; + border-color: #2d5a3d !important; + color: #7fc99b !important; + } + + #tmuxForm select.select-no { + background-color: #3a1a1d !important; + border-color: #5a2d31 !important; + color: #f5a9b0 !important; + } + + #tmuxForm select.select-other { + background-color: #2a2a2a !important; + border-color: #3d3d3d !important; + color: #b0b0b0 !important; + } +} + +/* Also support dark mode class on html element */ +html.dark #tmuxForm select.select-yes { + background-color: #1a3a2a !important; + border-color: #2d5a3d !important; + color: #7fc99b !important; +} + +html.dark #tmuxForm select.select-no { + background-color: #3a1a1d !important; + border-color: #5a2d31 !important; + color: #f5a9b0 !important; +} + +html.dark #tmuxForm select.select-other { + background-color: #2a2a2a !important; + border-color: #3d3d3d !important; + color: #b0b0b0 !important; +} +/* Dropdown menu hidden by default */ +.dropdown-menu { + display: none; +} + +.dropdown-menu.show { + display: block; +} + +/* Chart container heights */ +.chart-container { + height: 250px; + max-height: 250px; + position: relative; +} + +/* Fixed widths for table columns */ +.col-width-180 { + width: 180px; +} + +.col-width-160 { + width: 160px; +} + +.col-width-170 { + width: 170px; +} + +.col-width-200 { + width: 200px; +} + +/* Image max heights */ +.img-max-h-400 { + max-height: 400px; +} + +/* Forum quote styling */ +.forum-quote { + border-color: #eee; +} + +.dark .forum-quote { + border-color: #374151; +} + +/* Email styles (for email templates) */ +.email-text-center { + text-align: center; +} + +.email-code-block { + word-break: break-all; + background-color: #f8f9fa; + padding: 10px; + border-radius: 4px; +} + +/* Profile Page Tab Content */ +.tab-content { + display: none; +} + +.tab-content:first-of-type, +.tab-content.active { + display: block; +} + +.dark .email-code-block { + background-color: #1f2937; +} + +/* Modal z-index and display */ +.modal-hidden { + display: none; + z-index: 9999 !important; +} + +.modal-visible { + display: flex; + z-index: 9999 !important; +} + +/* Modal content max height */ +.modal-content-scroll { + max-height: calc(90vh - 80px); +} + +/* Cart table column width */ +.cart-checkbox-col { + width: 30px; +} + +/* Admin Groups Management - Fade Out Animation */ +.fade-out { + transition: opacity 0.3s ease; + opacity: 0; +} + +/* My Movies/Shows Page Styles */ +.header-gradient { + background: linear-gradient(135deg, #2563eb 0%, #1e40af 100%); +} + +.info-card-gradient { + background: linear-gradient(135deg, #eff6ff 0%, #e0e7ff 100%); +} + +.dark .info-card-gradient { + background: linear-gradient(135deg, #1e3a8a 0%, #1e40af 100%); +} + +.table-header-gradient { + background: linear-gradient(135deg, #f9fafb 0%, #f3f4f6 100%); +} + +.dark .table-header-gradient { + background: linear-gradient(135deg, #1f2937 0%, #374151 100%); +} + +.movie-poster-shadow { + box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06); +} + +.category-badge { + background: linear-gradient(135deg, #dbeafe 0%, #bfdbfe 100%); +} + +.dark .category-badge { + background: linear-gradient(135deg, #1e40af 0%, #1e3a8a 100%); + color: #93c5fd !important; + border-color: #2563eb !important; +} + +.empty-state-bg { + background: linear-gradient(135deg, #dbeafe 0%, #e0e7ff 100%); +} + +.dark .empty-state-bg { + background: linear-gradient(135deg, #1e3a8a 0%, #1e40af 100%); +} + +.show-avatar { + background: linear-gradient(135deg, #3b82f6 0%, #6366f1 100%); +} + +/* My Shows Table Borders */ +table { + border-collapse: collapse; +} + +table thead tr { + border-bottom: 2px solid #e5e7eb; +} + +.dark table thead tr { + border-bottom-color: #4b5563; +} + +table tbody tr { + border-bottom: 1px solid #e5e7eb; +} + +.dark table tbody tr { + border-bottom-color: #374151; +} + +table tbody tr:last-child { + border-bottom: none; +} + +/* Checkbox :has() fallback for older browsers */ +@supports not selector(:has(*)) { + label:has(input[type="checkbox"]:checked) { + background-color: #eff6ff !important; + border-color: #3b82f6 !important; + color: #1e40af !important; + } +} + +/* Content Page Prose Styles */ +.prose h1, .prose h2, .prose h3, .prose h4, .prose h5, .prose h6 { + margin-top: 1.5em; + margin-bottom: 0.75em; + font-weight: 600; +} + +.prose p { + margin-bottom: 1em; +} + +.prose ul, .prose ol { + margin-bottom: 1em; + padding-left: 1.5em; +} + +.prose a { + color: #2563eb; + text-decoration: underline; +} + +.prose a:hover { + color: #1d4ed8; +} + +.prose img { + max-width: 100%; + height: auto; + border-radius: 0.5rem; + margin: 1.5em 0; +} + +.prose blockquote { + border-left: 4px solid #e5e7eb; + padding-left: 1em; + color: #6b7280; + font-style: italic; + margin: 1.5em 0; +} + +.prose code { + background-color: #f3f4f6; + padding: 0.2em 0.4em; + border-radius: 0.25rem; + font-size: 0.875em; +} + +/* Cart Confirmation Modal Styles */ +.confirmation-modal { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 99999; + animation: fadeIn 0.2s ease-out; +} + +.confirmation-modal-content { + background: white; + border-radius: 12px; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2); + max-width: 500px; + width: 90%; + padding: 24px; + animation: slideUp 0.3s ease-out; +} + +@media (prefers-color-scheme: dark) { + .confirmation-modal-content { + background: #1f2937; + box-shadow: 0 10px 40px rgba(0, 0, 0, 0.5); + } +} + +.confirmation-modal-header { + display: flex; + align-items: center; + margin-bottom: 16px; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +@keyframes slideUp { + from { + transform: translateY(20px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} + +/* Contact Page Container */ +.contact-page-container { + max-width: 75% !important; + width: 75% !important; +} + +@media (min-width: 1536px) { + .contact-page-container { + max-width: 1200px !important; + } +} + +/* Toast Container Override */ +#toast-container { + position: fixed; + top: 20px; + right: 20px; + z-index: 99999; + max-width: 350px; + pointer-events: none; +} + +/* Preview/Sample Image Modal */ +#previewModal { + z-index: 9999 !important; +} diff --git a/resources/js/app.js b/resources/js/app.js index e1aa76900..2c3d78a0d 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -1,28 +1,23 @@ import './bootstrap'; -// Import jQuery and make it globally available -import $ from 'jquery'; -window.$ = window.jQuery = $; +// Import CSP-safe styles and scripts +import '../css/csp-safe.css'; +import './csp-safe.js'; -// Import Bootstrap JS -import 'bootstrap'; +// Theme initialization - must run early to prevent flash +(function() { + const themePreference = document.querySelector('meta[name="theme-preference"]')?.content || 'light'; + + if (themePreference === 'system') { + // Use OS preference + if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { + document.documentElement.classList.add('dark'); + } + } else if (themePreference === 'dark') { + document.documentElement.classList.add('dark'); + } +})(); // Import FontAwesome import '@fortawesome/fontawesome-free/js/all.min.js'; -// Import DataTables -import 'datatables'; - -// Import other necessary libraries -import autosize from 'autosize'; -window.autosize = autosize; - -// Initialize autosize on page load -document.addEventListener('DOMContentLoaded', function() { - // Auto-resize textareas - const textareas = document.querySelectorAll('textarea[data-autosize]'); - if (textareas.length > 0) { - autosize(textareas); - } -}); - diff --git a/resources/js/csp-safe.js b/resources/js/csp-safe.js new file mode 100644 index 000000000..3124e7bc5 --- /dev/null +++ b/resources/js/csp-safe.js @@ -0,0 +1,3831 @@ +// CSP-Safe JavaScript - All inline event handlers moved here +// This file handles all onclick, onchange, and other inline event handlers + +document.addEventListener('DOMContentLoaded', function() { + initEventDelegation(); + initToastNotifications(); + initNfoModal(); + initConfirmDialogs(); + initSelectRedirects(); + initLogoutForms(); + initSeasonSwitcher(); + initBinaryBlacklist(); + initImageModal(); + initTabSwitcher(); + initPreviewModal(); + initRegexManagement(); + initMediainfoAndFilelist(); + initCartFunctionality(); + initAdminMenu(); + initSidebarToggle(); + initDropdownMenus(); + initImageFallbacks(); + initProfileTabs(); + initAdminUserEdit(); + initMobileEnhancements(); + initAdminDashboardCharts(); + initAdminGroups(); + initTinyMCE(); + initAdminSpecificFeatures(); + + // Initialize page-specific functionality from inline scripts + initMyMovies(); + initAuthPages(); + initProfileEdit(); + initDetailsPageImageModal(); + initAddToCart(); +}); + +// Event delegation for dynamically added elements +function initEventDelegation() { + document.addEventListener('click', function(e) { + // Handle toast close buttons + if (e.target.classList.contains('toast-close') || e.target.closest('.toast-close')) { + const toast = e.target.closest('.toast-notification'); + if (toast) { + toast.remove(); + } + } + + // Handle NFO modal close + if (e.target.hasAttribute('data-close-nfo-modal') || e.target.closest('[data-close-nfo-modal]')) { + e.preventDefault(); + closeNfoModal(); + } + + // Handle NFO modal open + if (e.target.hasAttribute('data-open-nfo') || e.target.closest('[data-open-nfo]')) { + e.preventDefault(); + const guid = e.target.getAttribute('data-open-nfo') || e.target.closest('[data-open-nfo]').getAttribute('data-open-nfo'); + if (guid) { + openNfoModal(guid); + } + } + + // Handle logout + if (e.target.hasAttribute('data-logout') || e.target.closest('[data-logout]')) { + e.preventDefault(); + const logoutForm = document.getElementById('logout-form'); + if (logoutForm) { + logoutForm.submit(); + } + } + + // Handle confirm delete + if (e.target.hasAttribute('data-confirm-delete') || e.target.closest('[data-confirm-delete]')) { + const confirmed = confirm('Are you sure you want to delete this item?'); + if (!confirmed) { + e.preventDefault(); + } + } + + // Handle confirm action + if (e.target.hasAttribute('data-confirm') || e.target.closest('[data-confirm]')) { + const message = e.target.getAttribute('data-confirm') || e.target.closest('[data-confirm]').getAttribute('data-confirm'); + const confirmed = confirm(message); + if (!confirmed) { + e.preventDefault(); + } + } + + // Handle download NZB buttons + if (e.target.classList.contains('download-nzb') || e.target.closest('.download-nzb')) { + if (typeof showToast === 'function') { + showToast('Downloading NZB...', 'success'); + } + } + + // Handle season switcher + if (e.target.hasAttribute('data-season') || e.target.closest('[data-season]')) { + e.preventDefault(); + const seasonNumber = e.target.getAttribute('data-season') || e.target.closest('[data-season]').getAttribute('data-season'); + switchSeason(seasonNumber); + } + + // Handle binary blacklist delete + if (e.target.hasAttribute('data-delete-blacklist') || e.target.closest('[data-delete-blacklist]')) { + const id = e.target.getAttribute('data-delete-blacklist') || e.target.closest('[data-delete-blacklist]').getAttribute('data-delete-blacklist'); + const confirmed = confirm('Are you sure? This will delete the blacklist from this list.'); + if (confirmed && typeof ajax_binaryblacklist_delete === 'function') { + ajax_binaryblacklist_delete(id); + } + e.preventDefault(); + } + + // Handle admin groups management actions + const actionTarget = e.target.closest('[data-action]'); + if (actionTarget) { + const action = actionTarget.dataset.action; + const groupId = actionTarget.dataset.groupId; + const status = actionTarget.dataset.status; + + switch(action) { + case 'show-reset-modal': + showResetAllModal(); + break; + case 'hide-reset-modal': + hideResetAllModal(); + break; + case 'show-purge-modal': + showPurgeAllModal(); + break; + case 'hide-purge-modal': + hidePurgeAllModal(); + break; + case 'toggle-group-status': + ajax_group_status(groupId, status); + break; + case 'toggle-backfill': + ajax_backfill_status(groupId, status); + break; + case 'reset-group': + ajax_group_reset(groupId); + break; + case 'delete-group': + confirmGroupDelete(groupId); + break; + case 'purge-group': + confirmGroupPurge(groupId); + break; + case 'reset-all': + ajax_group_reset_all(); + break; + case 'purge-all': + ajax_group_purge_all(); + break; + } + } + }); + + // Handle select redirects + document.addEventListener('change', function(e) { + if (e.target.hasAttribute('data-redirect-on-change')) { + const url = e.target.value; + if (url && url !== '#') { + window.location.href = url; + } + } + }); +} + +// Toast Notifications +function initToastNotifications() { + // showToast is defined globally for backward compatibility + window.showToast = function(message, type) { + type = type || 'success'; + + let container = document.getElementById('toast-container'); + if (!container) { + // Create container if it doesn't exist + container = document.createElement('div'); + container.id = 'toast-container'; + container.className = 'fixed top-4 right-4 z-50'; + document.body.appendChild(container); + } + + const toast = document.createElement('div'); + toast.className = 'toast-notification ' + type; + + const iconClass = type === 'success' ? 'fa-check-circle' : + type === 'error' ? 'fa-exclamation-circle' : + 'fa-info-circle'; + + toast.innerHTML = + '' + + '' + escapeHtml(message) + '' + + ''; + + container.appendChild(toast); + + // Auto-remove after 5 seconds + setTimeout(function() { + toast.classList.add('removing'); + setTimeout(function() { + toast.remove(); + }, 300); + }, 5000); + }; +} + +// NFO Modal +function initNfoModal() { + window.openNfoModal = function(guid) { + const modal = document.getElementById('nfoModal'); + const content = document.getElementById('nfoContent'); + + if (!modal || !content) return; + + // Show modal + modal.classList.remove('hidden'); + + // Reset content with loading message + content.innerHTML = '
Loading NFO...
'; + + // Fetch NFO content + const baseUrl = document.querySelector('meta[name="app-url"]')?.content || ''; + fetch(baseUrl + '/nfo/' + guid + '?modal=1') + .then(response => { + if (!response.ok) { + throw new Error('NFO not found'); + } + return response.text(); + }) + .then(html => { + // Extract the NFO content from the response + const parser = new DOMParser(); + const doc = parser.parseFromString(html, 'text/html'); + const nfoText = doc.querySelector('pre')?.textContent || doc.body.textContent; + content.textContent = nfoText; + }) + .catch(error => { + content.innerHTML = '
Error loading NFO file
'; + console.error('Error loading NFO:', error); + }); + }; + + window.closeNfoModal = function() { + const modal = document.getElementById('nfoModal'); + if (modal) { + modal.classList.add('hidden'); + } + }; + + // Close modal on Escape key + document.addEventListener('keydown', function(event) { + if (event.key === 'Escape') { + const modal = document.getElementById('nfoModal'); + if (modal && !modal.classList.contains('hidden')) { + closeNfoModal(); + } + } + }); + + // Add click handlers for NFO badges + document.addEventListener('click', function(event) { + if (event.target.closest('.nfo-badge')) { + event.preventDefault(); + const badge = event.target.closest('.nfo-badge'); + const guid = badge.getAttribute('data-guid'); + if (guid) { + openNfoModal(guid); + } + } + }); +} + +// Confirm dialogs +function initConfirmDialogs() { + window.confirmDelete = function(id) { + if (confirm('Are you sure you want to delete this item?')) { + // If there's a form with this ID, submit it + const form = document.getElementById('delete-form-' + id); + if (form) { + form.submit(); + } + return true; + } + return false; + }; +} + +// Select redirects +function initSelectRedirects() { + // Already handled in event delegation +} + +// Logout forms +function initLogoutForms() { + // Already handled in event delegation +} + +// Season switcher for series +function initSeasonSwitcher() { + window.switchSeason = function(seasonNumber) { + // Hide all season containers + document.querySelectorAll('[data-season-container]').forEach(function(container) { + container.style.display = 'none'; + }); + + // Show selected season + const selectedSeason = document.querySelector('[data-season-container="' + seasonNumber + '"]'); + if (selectedSeason) { + selectedSeason.style.display = 'block'; + } + + // Update button states + document.querySelectorAll('[data-season]').forEach(function(btn) { + btn.classList.remove('active', 'bg-blue-600', 'text-white'); + btn.classList.add('bg-gray-200', 'text-gray-700'); + }); + + const activeBtn = document.querySelector('[data-season="' + seasonNumber + '"]'); + if (activeBtn) { + activeBtn.classList.remove('bg-gray-200', 'text-gray-700'); + activeBtn.classList.add('active', 'bg-blue-600', 'text-white'); + } + }; +} + +// Binary blacklist +function initBinaryBlacklist() { + // Placeholder - actual implementation depends on existing ajax function + window.ajax_binaryblacklist_delete = window.ajax_binaryblacklist_delete || function(id) { + console.log('Delete blacklist:', id); + }; +} + +// Image modal +function initImageModal() { + // Handle image modal if needed + document.querySelectorAll('[data-open-image-modal]').forEach(function(element) { + element.addEventListener('click', function(e) { + e.preventDefault(); + const imageUrl = this.getAttribute('data-open-image-modal'); + openImageModal(imageUrl); + }); + }); + + window.openImageModal = function(imageUrl) { + const modal = document.getElementById('imageModal'); + if (modal) { + const img = modal.querySelector('img'); + if (img) { + img.src = imageUrl; + } + modal.classList.remove('hidden'); + modal.classList.add('flex'); + } + }; + + window.closeImageModal = function() { + const modal = document.getElementById('imageModal'); + if (modal) { + modal.classList.add('hidden'); + modal.classList.remove('flex'); + } + }; +} + +// Tab switcher for profile and other pages +function initTabSwitcher() { + document.querySelectorAll('[data-tab-trigger]').forEach(function(trigger) { + trigger.addEventListener('click', function(e) { + e.preventDefault(); + const tabId = this.getAttribute('data-tab-trigger'); + + // Hide all tabs + document.querySelectorAll('.tab-content').forEach(function(tab) { + tab.style.display = 'none'; + }); + + // Show selected tab + const selectedTab = document.getElementById(tabId); + if (selectedTab) { + selectedTab.style.display = 'block'; + } + + // Update active state + document.querySelectorAll('[data-tab-trigger]').forEach(function(t) { + t.classList.remove('active', 'border-blue-500', 'text-blue-600'); + t.classList.add('border-transparent', 'text-gray-500'); + }); + + this.classList.remove('border-transparent', 'text-gray-500'); + this.classList.add('active', 'border-blue-500', 'text-blue-600'); + }); + }); +} + +// Preview Modal for Browse Page +function initPreviewModal() { + window.closePreviewModal = function() { + const modal = document.getElementById('previewModal'); + if (modal) { + modal.style.display = 'none'; + modal.classList.add('hidden'); + const img = document.getElementById('previewImage'); + if (img) img.src = ''; + } + }; + + window.showPreviewImage = function(guid, type = 'preview') { + const modal = document.getElementById('previewModal'); + const img = document.getElementById('previewImage'); + const error = document.getElementById('previewError'); + const title = document.getElementById('previewTitle'); + + if (!modal || !img || !error || !title) return; + + modal.style.display = 'flex'; + modal.classList.remove('hidden'); + title.textContent = type === 'sample' ? 'Sample Image' : 'Preview Image'; + + const imageUrl = '/covers/' + type + '/' + guid + '_thumb.jpg'; + img.src = imageUrl; + error.classList.add('hidden'); + img.style.display = 'block'; + + img.onerror = function() { + error.textContent = (type === 'sample' ? 'Sample' : 'Preview') + ' image not available'; + error.classList.remove('hidden'); + img.style.display = 'none'; + }; + + img.onload = function() { + img.style.display = 'block'; + }; + }; + + // Event listeners for preview badges + document.querySelectorAll('.preview-badge').forEach(function(badge) { + badge.addEventListener('click', function() { + const guid = this.getAttribute('data-guid'); + if (guid) { + window.showPreviewImage(guid, 'preview'); + } + }); + }); + + // Event listeners for sample badges + document.querySelectorAll('.sample-badge').forEach(function(badge) { + badge.addEventListener('click', function() { + const guid = this.getAttribute('data-guid'); + if (guid) { + window.showPreviewImage(guid, 'sample'); + } + }); + }); + + // Event listeners for NFO badges + document.querySelectorAll('.nfo-badge').forEach(function(badge) { + badge.addEventListener('click', function() { + const guid = this.getAttribute('data-guid'); + if (guid) { + fetch('/getnfo/' + guid) + .then(response => response.text()) + .then(data => { + const nfoContent = document.getElementById('nfo-content'); + if (nfoContent) { + nfoContent.textContent = data; + const nfoModal = document.getElementById('nfo-modal'); + if (nfoModal) { + nfoModal.classList.remove('hidden'); + nfoModal.classList.add('flex'); + } + } + }) + .catch(error => { + console.error('Error fetching NFO:', error); + }); + } + }); + }); + + // Close preview modal on background click + const previewModal = document.getElementById('previewModal'); + if (previewModal) { + previewModal.addEventListener('click', function(e) { + if (e.target === this) { + window.closePreviewModal(); + } + }); + + // Event listener for close button (using onclick attribute) + const closeButton = previewModal.querySelector('button[onclick*="closePreviewModal"]'); + if (closeButton) { + closeButton.addEventListener('click', function(e) { + e.stopPropagation(); + window.closePreviewModal(); + }); + } + + // Also handle any data-close-preview buttons + const dataCloseButton = previewModal.querySelector('[data-close-preview]'); + if (dataCloseButton) { + dataCloseButton.addEventListener('click', function(e) { + e.stopPropagation(); + window.closePreviewModal(); + }); + } + } + + // Close modal on Escape key + document.addEventListener('keydown', function(e) { + if (e.key === 'Escape') { + window.closePreviewModal(); + } + }); +} + +// Regex Management Functions +function initRegexManagement() { + window.deleteRegex = function(id, deleteUrl) { + const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content; + + fetch(deleteUrl + '?id=' + id, { + method: 'GET', + headers: { + 'X-Requested-With': 'XMLHttpRequest', + 'X-CSRF-TOKEN': csrfToken + } + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + const row = document.getElementById('row-' + id); + if (row) { + row.style.transition = 'opacity 0.3s'; + row.style.opacity = '0'; + setTimeout(() => row.remove(), 300); + } + showMessage('Regex deleted successfully', 'success'); + } else { + showMessage('Error deleting regex', 'error'); + } + }) + .catch(error => { + showMessage('Error deleting regex', 'error'); + console.error('Error:', error); + }); + }; + + window.showMessage = function(message, type = 'success') { + const messageDiv = document.getElementById('message'); + if (!messageDiv) return; + + const bgColor = type === 'success' ? 'bg-green-50 dark:bg-green-900 border-green-200 dark:border-green-700' : 'bg-red-50 dark:bg-red-900 border-red-200 dark:border-red-700'; + const textColor = type === 'success' ? 'text-green-800 dark:text-green-200' : 'text-red-800 dark:text-red-200'; + const icon = type === 'success' ? 'check-circle' : 'exclamation-circle'; + + const messageEl = document.createElement('div'); + messageEl.className = 'mt-4 p-4 border rounded-lg ' + bgColor; + messageEl.innerHTML = ` +
+

+ ${escapeHtml(message)} +

+ +
+ `; + + messageDiv.appendChild(messageEl); + + // Add close handler + messageEl.querySelector('.close-message').addEventListener('click', function() { + messageEl.remove(); + }); + + // Auto-remove after 5 seconds + setTimeout(() => { + messageEl.remove(); + }, 5000); + }; + + // Handle delete buttons with proper confirmation + document.addEventListener('click', function(e) { + if (e.target.hasAttribute('data-delete-regex') || e.target.closest('[data-delete-regex]')) { + const element = e.target.hasAttribute('data-delete-regex') ? e.target : e.target.closest('[data-delete-regex]'); + const id = element.getAttribute('data-delete-regex'); + const deleteUrl = element.getAttribute('data-delete-url'); + + if (confirm('Are you sure you want to delete this regex? This action cannot be undone.')) { + deleteRegex(id, deleteUrl); + } + e.preventDefault(); + } + }); +} + +// Mediainfo and Filelist Modals +function initMediainfoAndFilelist() { + window.closeMediainfoModal = function() { + const modal = document.getElementById('mediainfoModal'); + if (modal) { + modal.style.display = 'none'; + } + }; + + window.closeFilelistModal = function() { + const modal = document.getElementById('filelistModal'); + if (modal) { + modal.style.display = 'none'; + } + }; + + window.formatFileSize = function(bytes) { + if (bytes === 0) return '0 Bytes'; + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i]; + }; + + window.showFilelist = function(guid) { + let modal = document.getElementById('filelistModal'); + if (modal && modal.parentElement !== document.body) { + document.body.appendChild(modal); + } + + const content = document.getElementById('filelistContent'); + modal.style.display = 'flex'; + modal.style.position = 'fixed'; + modal.style.top = '0'; + modal.style.left = '0'; + modal.style.right = '0'; + modal.style.bottom = '0'; + modal.style.zIndex = '99999'; + modal.style.backgroundColor = 'rgba(0, 0, 0, 0.75)'; + + content.innerHTML = ` +
+ +

Loading file list...

+
+ `; + + const apiUrl = '/api/release/' + guid + '/filelist'; + + fetch(apiUrl) + .then(response => { + if (!response.ok) { + throw new Error('Failed to load file list'); + } + return response.json(); + }) + .then(data => { + if (!data.files || data.files.length === 0) { + content.innerHTML = '

No files available

'; + return; + } + + let html = '
'; + html += ` +
+

${escapeHtml(data.release.searchname)}

+

Total Files: ${data.total}

+
+ `; + + html += ` +
+ + + + + + + + + `; + + data.files.forEach((file, index) => { + const rowClass = index % 2 === 0 ? 'bg-white dark:bg-gray-800' : 'bg-gray-50 dark:bg-gray-900'; + const fileName = file.title || file.name || 'Unknown'; + const fileSize = file.size ? formatFileSize(file.size) : 'N/A'; + + html += ` + + + + + `; + }); + + html += ` + +
File NameSize
${escapeHtml(fileName)}${fileSize}
+
+ `; + + html += '
'; + content.innerHTML = html; + }) + .catch(error => { + content.innerHTML = ` +
+ +

${escapeHtml(error.message)}

+
+ `; + }); + }; + + window.showMediainfo = function(releaseId) { + let modal = document.getElementById('mediainfoModal'); + + if (modal && modal.parentElement !== document.body) { + document.body.appendChild(modal); + } + + const content = document.getElementById('mediainfoContent'); + + modal.style.display = 'flex'; + modal.style.position = 'fixed'; + modal.style.top = '0'; + modal.style.left = '0'; + modal.style.right = '0'; + modal.style.bottom = '0'; + modal.style.zIndex = '99999'; + modal.style.backgroundColor = 'rgba(0, 0, 0, 0.75)'; + + content.innerHTML = ` +
+ +

Loading media information...

+
+ `; + + const apiUrl = '/api/release/' + releaseId + '/mediainfo'; + + fetch(apiUrl) + .then(response => { + if (!response.ok) { + throw new Error('Failed to load media information'); + } + return response.json(); + }) + .then(data => { + if (!data.video && !data.audio && !data.subs) { + content.innerHTML = '

No media information available

'; + return; + } + + let html = '
'; + + // Video information + if (data.video) { + html += ` +
+

+ Video Information +

+
+ `; + + if (data.video.containerformat) { + html += ` +
+
Container
+
${escapeHtml(data.video.containerformat)}
+
+ `; + } + if (data.video.videocodec) { + html += ` +
+
Codec
+
${escapeHtml(data.video.videocodec)}
+
+ `; + } + if (data.video.videowidth && data.video.videoheight) { + html += ` +
+
Resolution
+
${data.video.videowidth}x${data.video.videoheight}
+
+ `; + } + if (data.video.videoaspect) { + html += ` +
+
Aspect Ratio
+
${escapeHtml(data.video.videoaspect)}
+
+ `; + } + if (data.video.videoframerate) { + html += ` +
+
Frame Rate
+
${escapeHtml(data.video.videoframerate)} fps
+
+ `; + } + if (data.video.videoduration) { + const durationMs = parseInt(data.video.videoduration); + if (!isNaN(durationMs) && durationMs > 0) { + const minutes = Math.round(durationMs / 1000 / 60); + html += ` +
+
Duration
+
${minutes} minutes
+
+ `; + } + } + + html += '
'; + } + + // Audio information + if (data.audio && data.audio.length > 0) { + html += ` +
+

+ Audio Information +

+ `; + + data.audio.forEach((audio, index) => { + if (index > 0) html += '
'; + html += '
'; + + if (audio.audioformat) { + html += ` +
+
Format
+
${escapeHtml(audio.audioformat)}
+
+ `; + } + if (audio.audiochannels) { + html += ` +
+
Channels
+
${escapeHtml(audio.audiochannels)}
+
+ `; + } + if (audio.audiobitrate) { + html += ` +
+
Bit Rate
+
${escapeHtml(audio.audiobitrate)}
+
+ `; + } + if (audio.audiolanguage) { + html += ` +
+
Language
+
${escapeHtml(audio.audiolanguage)}
+
+ `; + } + if (audio.audiosamplerate) { + html += ` +
+
Sample Rate
+
${escapeHtml(audio.audiosamplerate)}
+
+ `; + } + + html += '
'; + }); + + html += '
'; + } + + // Subtitle information + if (data.subs) { + html += ` +
+

+ Subtitles +

+

${escapeHtml(data.subs)}

+
+ `; + } + + html += '
'; + content.innerHTML = html; + }) + .catch(error => { + content.innerHTML = ` +
+ +

${escapeHtml(error.message)}

+
+ `; + }); + }; + + // Event handlers for badges and modals + document.addEventListener('click', function(e) { + // Preview badge + const previewBadge = e.target.closest('.preview-badge'); + if (previewBadge) { + e.preventDefault(); + const guid = previewBadge.dataset.guid; + if (typeof showPreviewImage === 'function') { + showPreviewImage(guid, 'preview'); + } + } + + // Sample badge + const sampleBadge = e.target.closest('.sample-badge'); + if (sampleBadge) { + e.preventDefault(); + const guid = sampleBadge.dataset.guid; + if (typeof showPreviewImage === 'function') { + showPreviewImage(guid, 'sample'); + } + } + + // Mediainfo badge + const mediainfoBadge = e.target.closest('.mediainfo-badge'); + if (mediainfoBadge) { + e.preventDefault(); + const releaseId = mediainfoBadge.dataset.releaseId; + showMediainfo(releaseId); + } + + // File list badge + const filelistBadge = e.target.closest('.filelist-badge'); + if (filelistBadge) { + e.preventDefault(); + const guid = filelistBadge.dataset.guid; + showFilelist(guid); + } + }); + + // Close modals on background click + const previewModal = document.getElementById('previewModal'); + if (previewModal) { + previewModal.addEventListener('click', function(e) { + if (e.target === this && typeof closePreviewModal === 'function') { + closePreviewModal(); + } + }); + } + + const mediainfoModal = document.getElementById('mediainfoModal'); + if (mediainfoModal) { + mediainfoModal.addEventListener('click', function(e) { + // Close if clicking the backdrop OR the close button + if (e.target === this || e.target.closest('[onclick*="closeMediainfoModal"]')) { + closeMediainfoModal(); + } + }); + } + + const filelistModal = document.getElementById('filelistModal'); + if (filelistModal) { + filelistModal.addEventListener('click', function(e) { + // Close if clicking the backdrop OR the close button + if (e.target === this || e.target.closest('[onclick*="closeFilelistModal"]')) { + closeFilelistModal(); + } + }); + } + + // Close modals on ESC key + document.addEventListener('keydown', function(e) { + if (e.key === 'Escape') { + if (typeof closePreviewModal === 'function') closePreviewModal(); + closeMediainfoModal(); + closeFilelistModal(); + } + }); +} + +// Cart and Multi-select functionality +function initCartFunctionality() { + // Select all checkbox functionality + const selectAllCheckbox = document.getElementById('chkSelectAll'); + if (selectAllCheckbox) { + selectAllCheckbox.addEventListener('change', function() { + const checkboxes = document.querySelectorAll('.chkRelease'); + checkboxes.forEach(checkbox => checkbox.checked = this.checked); + }); + } + + // Multi-operations download + const multiDownloadBtn = document.querySelector('.nzb_multi_operations_download'); + if (multiDownloadBtn) { + multiDownloadBtn.addEventListener('click', function() { + const selected = Array.from(document.querySelectorAll('.chkRelease:checked')).map(cb => cb.value); + if (selected.length === 0) { + if (typeof showToast === 'function') { + showToast('Please select at least one release', 'error'); + } + return; + } + + // If only one release is selected, download it directly + if (selected.length === 1) { + window.location.href = '/getnzb/' + selected[0]; + if (typeof showToast === 'function') { + showToast('Downloading NZB...', 'success'); + } + } else { + // For multiple releases, download as zip + const guids = selected.join(','); + window.location.href = '/getnzb?id=' + encodeURIComponent(guids) + '&zip=1'; + if (typeof showToast === 'function') { + showToast(`Downloading ${selected.length} NZBs as zip file...`, 'success'); + } + } + }); + } + + // Multi-operations cart + const multiCartBtn = document.querySelector('.nzb_multi_operations_cart'); + if (multiCartBtn) { + multiCartBtn.addEventListener('click', function() { + const selected = Array.from(document.querySelectorAll('.chkRelease:checked')).map(cb => cb.value); + if (selected.length === 0) { + if (typeof showToast === 'function') { + showToast('Please select at least one release', 'error'); + } + return; + } + + const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content; + + fetch('/cart/add', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': csrfToken, + 'X-Requested-With': 'XMLHttpRequest' + }, + body: JSON.stringify({ id: selected.join(',') }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + if (typeof showToast === 'function') { + showToast(data.message || `Added ${selected.length} item${selected.length > 1 ? 's' : ''} to cart`, 'success'); + } + } else { + if (typeof showToast === 'function') { + showToast('Failed to add items to cart', 'error'); + } + } + }) + .catch(error => { + console.error('Error adding to cart:', error); + if (typeof showToast === 'function') { + showToast('An error occurred', 'error'); + } + }); + }); + } + + // Individual add to cart buttons + document.addEventListener('click', function(e) { + const cartBtn = e.target.closest('.add-to-cart'); + + if (cartBtn) { + e.preventDefault(); + + const guid = cartBtn.dataset.guid; + const iconElement = cartBtn.querySelector('.icon_cart'); + + if (!guid) { + console.error('No GUID found for cart item'); + return; + } + + // Prevent double-clicking + if (iconElement && iconElement.classList.contains('icon_cart_clicked')) { + return; + } + + const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content; + + // Send AJAX request to add item to cart + fetch('/cart/add', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': csrfToken, + 'X-Requested-With': 'XMLHttpRequest' + }, + body: JSON.stringify({ id: guid }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + // Visual feedback + if (iconElement) { + iconElement.classList.remove('fa-shopping-basket'); + iconElement.classList.add('fa-check', 'icon_cart_clicked'); + + // Reset icon after 2 seconds + setTimeout(() => { + iconElement.classList.remove('fa-check', 'icon_cart_clicked'); + iconElement.classList.add('fa-shopping-basket'); + }, 2000); + } + + // Show success notification + if (typeof showToast === 'function') { + showToast('Added to cart successfully!', 'success'); + } + } else { + if (typeof showToast === 'function') { + showToast('Failed to add item to cart', 'error'); + } + } + }) + .catch(error => { + console.error('Error adding to cart:', error); + if (typeof showToast === 'function') { + showToast('An error occurred', 'error'); + } + }); + } + }); +} + +// Admin Menu Toggle +function initAdminMenu() { + window.toggleAdminSubmenu = function(id) { + const submenu = document.getElementById(id); + const icon = document.getElementById(id + '-icon'); + if (submenu) { + submenu.classList.toggle('hidden'); + } + if (icon) { + icon.classList.toggle('rotate-180'); + } + }; + + // Add click handlers for admin menu buttons + document.addEventListener('click', function(e) { + const menuButton = e.target.closest('[data-toggle-submenu]'); + if (menuButton) { + const menuId = menuButton.getAttribute('data-toggle-submenu'); + if (menuId) { + toggleAdminSubmenu(menuId); + } + } + }); + + // Theme management with system preference support for admin panel + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + const themeToggle = document.getElementById('theme-toggle'); + + if (themeToggle) { + // Get initial theme preference + const metaTheme = document.querySelector('meta[name="theme-preference"]'); + const isAuthenticated = document.querySelector('meta[name="user-authenticated"]'); + let currentTheme = metaTheme ? metaTheme.content : 'light'; + + if (!isAuthenticated || isAuthenticated.content !== 'true') { + currentTheme = localStorage.getItem('theme') || 'light'; + } + + // Function to apply theme + function applyTheme(themePreference) { + const html = document.documentElement; + + if (themePreference === 'system') { + if (mediaQuery.matches) { + html.classList.add('dark'); + } else { + html.classList.remove('dark'); + } + } else if (themePreference === 'dark') { + html.classList.add('dark'); + } else { + html.classList.remove('dark'); + } + } + + // Apply initial theme + applyTheme(currentTheme); + + // Listen for OS theme changes + mediaQuery.addEventListener('change', () => { + const userThemePreference = metaTheme ? metaTheme.content : localStorage.getItem('theme') || 'light'; + if (userThemePreference === 'system') { + applyTheme('system'); + } + }); + + // Theme toggle click handler - cycles through light -> dark -> system + themeToggle.addEventListener('click', function() { + let nextTheme; + + // Cycle through: light -> dark -> system -> light + if (currentTheme === 'light') { + nextTheme = 'dark'; + } else if (currentTheme === 'dark') { + nextTheme = 'system'; + } else { + nextTheme = 'light'; + } + + applyTheme(nextTheme); + + // Update button title + const titles = { + 'light': 'Theme: Light', + 'dark': 'Theme: Dark', + 'system': 'Theme: System (Auto)' + }; + this.setAttribute('title', titles[nextTheme]); + + // Save theme preference + const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content; + const updateThemeUrl = document.querySelector('meta[name="update-theme-url"]')?.content; + + if (isAuthenticated && isAuthenticated.content === 'true' && updateThemeUrl && csrfToken) { + // Save to backend for authenticated users + fetch(updateThemeUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': csrfToken + }, + body: JSON.stringify({ theme_preference: nextTheme }) + }).then(response => response.json()) + .then(data => { + if (data.success) { + currentTheme = nextTheme; + if (metaTheme) { + metaTheme.content = nextTheme; + } + } + }) + .catch(error => console.error('Error updating theme:', error)); + } else { + // Save to localStorage for guests + localStorage.setItem('theme', nextTheme); + currentTheme = nextTheme; + } + }); + } +} + +// Sidebar Toggle functionality for regular user sidebar +function initSidebarToggle() { + const sidebarToggles = document.querySelectorAll('.sidebar-toggle'); + + sidebarToggles.forEach(toggle => { + toggle.addEventListener('click', function() { + const targetId = this.getAttribute('data-target'); + const submenu = document.getElementById(targetId); + const chevron = this.querySelector('.fa-chevron-down'); + + if (submenu) { + submenu.classList.toggle('hidden'); + if (chevron) { + chevron.classList.toggle('rotate-180'); + } + } + }); + }); + + // Handle logout link in sidebar + const sidebarLogoutLink = document.querySelector('[data-logout]'); + const sidebarLogoutForm = document.getElementById('sidebar-logout-form'); + + if (sidebarLogoutLink && sidebarLogoutForm) { + sidebarLogoutLink.addEventListener('click', function(e) { + e.preventDefault(); + sidebarLogoutForm.submit(); + }); + } +} + +// Dropdown Menus for Header Navigation +function initDropdownMenus() { + document.querySelectorAll('.dropdown-toggle').forEach(function(toggle) { + toggle.addEventListener('click', function(e) { + e.preventDefault(); + const menu = this.nextElementSibling; + if (menu && menu.classList.contains('dropdown-menu')) { + // Close all other dropdowns + document.querySelectorAll('.dropdown-menu').forEach(function(m) { + if (m !== menu) { + m.classList.remove('show'); + m.style.display = 'none'; + } + }); + + // Toggle this dropdown + menu.classList.toggle('show'); + menu.style.display = menu.classList.contains('show') ? 'block' : 'none'; + } + }); + }); + + // Close dropdowns when clicking outside + document.addEventListener('click', function(e) { + if (!e.target.closest('.dropdown-toggle') && !e.target.closest('.dropdown-menu')) { + document.querySelectorAll('.dropdown-menu').forEach(function(menu) { + menu.classList.remove('show'); + menu.style.display = 'none'; + }); + } + }); +} + +// Image Fallbacks +function initImageFallbacks() { + // Handle images with data-fallback-src attribute + document.querySelectorAll('img[data-fallback-src]').forEach(function(img) { + img.addEventListener('error', function() { + const fallbackSrc = this.getAttribute('data-fallback-src'); + if (fallbackSrc && this.src !== fallbackSrc) { + this.src = fallbackSrc; + } + }); + }); +} + +// Profile Page Tab Switching +function initProfileTabs() { + const tabLinks = document.querySelectorAll('.tab-link'); + const tabContents = document.querySelectorAll('.tab-content'); + + if (tabLinks.length === 0 || tabContents.length === 0) { + return; // Not on a page with tabs + } + + tabLinks.forEach(link => { + link.addEventListener('click', function(e) { + e.preventDefault(); + const targetId = this.getAttribute('href').substring(1); + + // Update active states on tab links + tabLinks.forEach(l => { + l.classList.remove('bg-blue-50', 'text-blue-700', 'font-medium'); + l.classList.add('text-gray-700'); + l.classList.add('dark:text-gray-300'); + }); + this.classList.add('bg-blue-50', 'text-blue-700', 'font-medium'); + this.classList.remove('text-gray-700', 'dark:text-gray-300'); + + // Hide all tab contents + tabContents.forEach(content => { + content.style.display = 'none'; + }); + + // Show selected tab content + const targetContent = document.getElementById(targetId); + if (targetContent) { + targetContent.style.display = 'block'; + } + + // Update URL hash without scrolling + history.pushState(null, null, '#' + targetId); + }); + }); + + // Handle initial hash + const hash = window.location.hash.substring(1); + if (hash && document.getElementById(hash)) { + const link = document.querySelector(`a[href="#${hash}"]`); + if (link) { + link.click(); + } + } +} + +// Add to Cart function (standalone for backward compatibility) +window.addToCart = function(guid) { + if (!guid) { + console.error('No GUID provided to addToCart'); + return; + } + + const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content; + + fetch('/cart/add', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': csrfToken, + 'X-Requested-With': 'XMLHttpRequest' + }, + body: JSON.stringify({ id: guid }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + if (typeof showToast === 'function') { + showToast('Added to cart successfully!', 'success'); + } + } else { + if (typeof showToast === 'function') { + showToast('Failed to add item to cart', 'error'); + } + } + }) + .catch(error => { + console.error('Error adding to cart:', error); + if (typeof showToast === 'function') { + showToast('An error occurred', 'error'); + } + }); +}; + +// Admin User Edit - Role Expiry DateTime Picker +function initAdminUserEdit() { + // Only run if we're on the admin user edit page + if (!document.getElementById('rolechangedate')) { + return; + } + + initializeDateTimePicker(); + setupTypeToSelect(); + setupExpiryDateHandlers(); +} + +// Initialize the datetime picker with existing values +function initializeDateTimePicker() { + const hiddenInput = document.getElementById('rolechangedate'); + if (!hiddenInput) return; + + if (hiddenInput.value) { + const date = new Date(hiddenInput.value); + if (!isNaN(date.getTime())) { + document.getElementById('expiry_year').value = date.getFullYear().toString(); + document.getElementById('expiry_month').value = String(date.getMonth() + 1).padStart(2, '0'); + document.getElementById('expiry_day').value = String(date.getDate()).padStart(2, '0'); + document.getElementById('expiry_hour').value = String(date.getHours()).padStart(2, '0'); + document.getElementById('expiry_minute').value = String(date.getMinutes()).padStart(2, '0'); + updateDateTimePreview(); + } + } + + // Add change listeners to all selectors + ['expiry_year', 'expiry_month', 'expiry_day', 'expiry_hour', 'expiry_minute'].forEach(id => { + const element = document.getElementById(id); + if (element) { + element.addEventListener('change', updateDateTime); + } + }); +} + +// Setup type-to-select functionality for all dropdowns +function setupTypeToSelect() { + const selects = ['expiry_year', 'expiry_month', 'expiry_day', 'expiry_hour', 'expiry_minute']; + + selects.forEach(selectId => { + const select = document.getElementById(selectId); + if (!select) return; + + let typedValue = ''; + let typingTimer; + + select.addEventListener('keypress', function(e) { + clearTimeout(typingTimer); + + // Add typed character + typedValue += e.key; + + // Find matching option + const options = Array.from(this.options); + const match = options.find(opt => + opt.value.startsWith(typedValue) || + opt.text.toLowerCase().startsWith(typedValue.toLowerCase()) + ); + + if (match) { + this.value = match.value; + updateDateTime(); + + // Visual feedback + this.classList.add('ring-2', 'ring-green-500', 'dark:ring-green-400'); + setTimeout(() => { + this.classList.remove('ring-2', 'ring-green-500', 'dark:ring-green-400'); + }, 300); + } + + // Clear typed value after 1 second + typingTimer = setTimeout(() => { + typedValue = ''; + }, 1000); + }); + }); +} + +// Update the hidden input and preview when any selector changes +function updateDateTime() { + const year = document.getElementById('expiry_year')?.value; + const month = document.getElementById('expiry_month')?.value; + const day = document.getElementById('expiry_day')?.value; + const hour = document.getElementById('expiry_hour')?.value; + const minute = document.getElementById('expiry_minute')?.value; + + if (year && month && day && hour && minute) { + const dateTimeStr = `${year}-${month}-${day}T${hour}:${minute}:00`; + document.getElementById('rolechangedate').value = dateTimeStr; + updateDateTimePreview(); + + // Show success flash on all filled selectors + ['expiry_year', 'expiry_month', 'expiry_day', 'expiry_hour', 'expiry_minute'].forEach(id => { + const el = document.getElementById(id); + if (el && el.value) { + el.classList.add('border-green-500', 'dark:border-green-400'); + setTimeout(() => { + el.classList.remove('border-green-500', 'dark:border-green-400'); + }, 500); + } + }); + } else { + const preview = document.getElementById('datetime_preview'); + if (preview) { + preview.classList.add('hidden'); + } + } +} + +// Update the datetime preview display +function updateDateTimePreview() { + const hiddenInput = document.getElementById('rolechangedate'); + const preview = document.getElementById('datetime_preview'); + const display = document.getElementById('datetime_display'); + + if (!hiddenInput || !preview || !display) return; + + if (hiddenInput.value) { + const date = new Date(hiddenInput.value); + const options = { + weekday: 'short', + year: 'numeric', + month: 'long', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }; + display.textContent = date.toLocaleDateString('en-US', options); + preview.classList.remove('hidden'); + + // Check if date is in past or expiring soon + const now = new Date(); + const diff = date - now; + if (diff < 0) { + display.classList.add('text-red-600', 'dark:text-red-400'); + display.classList.remove('text-blue-600', 'dark:text-blue-400', 'text-yellow-600', 'dark:text-yellow-400'); + } else if (diff < 7 * 24 * 60 * 60 * 1000) { + display.classList.add('text-yellow-600', 'dark:text-yellow-400'); + display.classList.remove('text-blue-600', 'dark:text-blue-400', 'text-red-600', 'dark:text-red-400'); + } else { + display.classList.add('text-blue-600', 'dark:text-blue-400'); + display.classList.remove('text-red-600', 'dark:text-red-400', 'text-yellow-600', 'dark:text-yellow-400'); + } + } else { + preview.classList.add('hidden'); + } +} + +// Setup event handlers for expiry date quick actions +function setupExpiryDateHandlers() { + // Event delegation for all expiry date buttons + document.addEventListener('click', function(e) { + const target = e.target.closest('[data-expiry-action]'); + if (!target) return; + + e.preventDefault(); + const action = target.getAttribute('data-expiry-action'); + const days = parseInt(target.getAttribute('data-days') || '0', 10); + const hours = parseInt(target.getAttribute('data-hours') || '0', 10); + + if (action === 'set') { + setExpiryDateTime(days, hours); + } else if (action === 'end-of-day') { + setEndOfDay(); + } else if (action === 'clear') { + clearExpiryDate(); + } + }); +} + +// Function to set expiry date and time by adding days and hours (stackable) +function setExpiryDateTime(days, hours) { + let baseDate; + + // Check if there's already a selected datetime + const year = document.getElementById('expiry_year')?.value; + const month = document.getElementById('expiry_month')?.value; + const day = document.getElementById('expiry_day')?.value; + const hour = document.getElementById('expiry_hour')?.value; + const minute = document.getElementById('expiry_minute')?.value; + + if (year && month && day && hour && minute) { + // Use existing selected datetime as base + baseDate = new Date(year, parseInt(month) - 1, day, hour, minute); + showExpiryToast('Added ' + (days > 0 ? days + ' day' + (days !== 1 ? 's' : '') : '') + (days > 0 && hours > 0 ? ' and ' : '') + (hours > 0 ? hours + ' hour' + (hours !== 1 ? 's' : '') : ''), 'info'); + } else { + // Start from current time + baseDate = new Date(); + showExpiryToast('Expiry date set to ' + formatDateTimeForDisplay(baseDate), 'success'); + } + + // Add the days and hours + baseDate.setDate(baseDate.getDate() + days); + baseDate.setHours(baseDate.getHours() + hours); + + // Update all selectors + document.getElementById('expiry_year').value = baseDate.getFullYear().toString(); + document.getElementById('expiry_month').value = String(baseDate.getMonth() + 1).padStart(2, '0'); + document.getElementById('expiry_day').value = String(baseDate.getDate()).padStart(2, '0'); + document.getElementById('expiry_hour').value = String(baseDate.getHours()).padStart(2, '0'); + document.getElementById('expiry_minute').value = String(baseDate.getMinutes()).padStart(2, '0'); + + updateDateTime(); + + // Add animated visual feedback + ['expiry_year', 'expiry_month', 'expiry_day', 'expiry_hour', 'expiry_minute'].forEach(id => { + const el = document.getElementById(id); + if (el) { + el.classList.add('ring-2', 'ring-green-500', 'dark:ring-green-400', 'scale-105'); + setTimeout(() => { + el.classList.remove('ring-2', 'ring-green-500', 'dark:ring-green-400', 'scale-105'); + }, 600); + } + }); +} + +// Function to set expiry to end of current day (23:59) +function setEndOfDay() { + const endOfDay = new Date(); + endOfDay.setHours(23, 59, 0, 0); + + document.getElementById('expiry_year').value = endOfDay.getFullYear().toString(); + document.getElementById('expiry_month').value = String(endOfDay.getMonth() + 1).padStart(2, '0'); + document.getElementById('expiry_day').value = String(endOfDay.getDate()).padStart(2, '0'); + document.getElementById('expiry_hour').value = '23'; + document.getElementById('expiry_minute').value = '55'; + + updateDateTime(); + + ['expiry_year', 'expiry_month', 'expiry_day', 'expiry_hour', 'expiry_minute'].forEach(id => { + const el = document.getElementById(id); + if (el) { + el.classList.add('ring-2', 'ring-indigo-500', 'dark:ring-indigo-400', 'scale-105'); + setTimeout(() => { + el.classList.remove('ring-2', 'ring-indigo-500', 'dark:ring-indigo-400', 'scale-105'); + }, 600); + } + }); + + showExpiryToast('Expiry set to end of today (23:59)', 'info'); +} + +// Function to clear expiry date +function clearExpiryDate() { + document.getElementById('expiry_year').value = ''; + document.getElementById('expiry_month').value = ''; + document.getElementById('expiry_day').value = ''; + document.getElementById('expiry_hour').value = ''; + document.getElementById('expiry_minute').value = ''; + document.getElementById('rolechangedate').value = ''; + const preview = document.getElementById('datetime_preview'); + if (preview) { + preview.classList.add('hidden'); + } + + // Add a visual feedback with gray pulse + ['expiry_year', 'expiry_month', 'expiry_day', 'expiry_hour', 'expiry_minute'].forEach(id => { + const el = document.getElementById(id); + if (el) { + el.classList.add('ring-2', 'ring-gray-500', 'dark:ring-gray-400', 'scale-105'); + setTimeout(() => { + el.classList.remove('ring-2', 'ring-gray-500', 'dark:ring-gray-400', 'scale-105'); + }, 600); + } + }); + + showExpiryToast('Expiry date cleared - role is now permanent', 'info'); +} + +// Format date for display +function formatDateTimeForDisplay(date) { + const options = { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit' + }; + return date.toLocaleDateString('en-US', options); +} + +// Show toast notification specific to expiry date changes +function showExpiryToast(message, type) { + type = type || 'success'; + + // Remove existing toast if any + const existingToast = document.getElementById('expiryToast'); + if (existingToast) { + existingToast.remove(); + } + + // Create toast + const toast = document.createElement('div'); + toast.id = 'expiryToast'; + toast.className = 'fixed bottom-4 right-4 px-4 py-3 rounded-lg shadow-lg flex items-center gap-2 z-50 transform transition-all duration-300 translate-y-0 opacity-100'; + + if (type === 'success') { + toast.classList.add('bg-green-500', 'dark:bg-green-600', 'text-white'); + toast.innerHTML = '' + escapeHtml(message) + ''; + } else if (type === 'info') { + toast.classList.add('bg-blue-500', 'dark:bg-blue-600', 'text-white'); + toast.innerHTML = '' + escapeHtml(message) + ''; + } + + document.body.appendChild(toast); + + // Animate in + setTimeout(() => { + toast.style.transform = 'translateY(0)'; + toast.style.opacity = '1'; + }, 10); + + // Remove after 3 seconds with fade out + setTimeout(() => { + toast.style.transform = 'translateY(20px)'; + toast.style.opacity = '0'; + setTimeout(() => toast.remove(), 300); + }, 3000); +} + +// Utility function to escape HTML +function escapeHtml(text) { + // Handle non-string values + if (text === null || text === undefined) { + return ''; + } + // Convert to string if it's not already + if (typeof text !== 'string') { + text = String(text); + } + + const map = { + '&': '&', + '<': '<', + '>': '>', + '"': '"', + "'": ''' + }; + return text.replace(/[&<>"']/g, function(m) { return map[m]; }); +} + +// TinyMCE Initialization for Admin Content Pages +function initTinyMCE() { + // Only initialize if TinyMCE editor element exists + if (!document.getElementById('body')) { + return; + } + + // Function to dynamically load TinyMCE from CDN + function loadTinyMCEScript() { + return new Promise(function(resolve, reject) { + // Check if already loaded + if (typeof tinymce !== 'undefined') { + resolve(); + return; + } + + // Get API key from textarea data attribute or create meta tag + const bodyTextarea = document.getElementById('body'); + let apiKey = 'no-api-key'; + + // Try to get from existing meta tag first + let apiKeyMeta = document.querySelector('meta[name="tinymce-api-key"]'); + + if (!apiKeyMeta) { + // Create meta tag dynamically if it doesn't exist + // Get API key from window config or use default + if (window.NNTmuxConfig && window.NNTmuxConfig.tinymceApiKey) { + apiKey = window.NNTmuxConfig.tinymceApiKey; + } else if (bodyTextarea && bodyTextarea.dataset.tinymceApiKey) { + apiKey = bodyTextarea.dataset.tinymceApiKey; + } + + // Create and append meta tag + apiKeyMeta = document.createElement('meta'); + apiKeyMeta.setAttribute('name', 'tinymce-api-key'); + apiKeyMeta.setAttribute('content', apiKey); + document.head.appendChild(apiKeyMeta); + } else { + apiKey = apiKeyMeta.content; + } + + // Create script element + const script = document.createElement('script'); + script.src = 'https://cdn.tiny.cloud/1/' + apiKey + '/tinymce/8/tinymce.min.js'; + script.referrerPolicy = 'origin'; + + script.onload = function() { + console.log('TinyMCE script loaded from CDN'); + resolve(); + }; + + script.onerror = function() { + console.error('Failed to load TinyMCE script from CDN'); + reject(new Error('Failed to load TinyMCE')); + }; + + // Append to head + document.head.appendChild(script); + }); + } + + // Function to detect if dark mode is active + function isDarkMode() { + return document.documentElement.classList.contains('dark') || + (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches); + } + + // Function to get TinyMCE config + function getTinyMCEConfig() { + const darkMode = isDarkMode(); + const apiKeyMeta = document.querySelector('meta[name="tinymce-api-key"]'); + const apiKey = apiKeyMeta ? apiKeyMeta.content : 'no-api-key'; + + return { + selector: '#body', + height: 500, + menubar: true, + skin: darkMode ? 'oxide-dark' : 'oxide', + content_css: darkMode ? 'dark' : 'default', + plugins: [ + 'advlist', 'autolink', 'lists', 'link', 'image', 'charmap', 'preview', + 'anchor', 'searchreplace', 'visualblocks', 'code', 'fullscreen', + 'insertdatetime', 'media', 'table', 'help', 'wordcount', 'emoticons' + ], + toolbar: 'undo redo | blocks fontfamily fontsize | ' + + 'bold italic underline strikethrough | forecolor backcolor | ' + + 'alignleft aligncenter alignright alignjustify | ' + + 'bullist numlist outdent indent | ' + + 'link image media table emoticons | ' + + 'removeformat code fullscreen | help', + toolbar_mode: 'sliding', + content_style: 'body { font-family: Helvetica, Arial, sans-serif; font-size: 14px; line-height: 1.6; }', + branding: false, + promotion: false, + resize: true, + statusbar: true, + elementpath: true, + automatic_uploads: false, + file_picker_types: 'image', + font_family_formats: 'Arial=arial,helvetica,sans-serif; Courier New=courier new,courier,monospace; Georgia=georgia,palatino,serif; Tahoma=tahoma,arial,helvetica,sans-serif; Times New Roman=times new roman,times,serif; Verdana=verdana,geneva,sans-serif', + font_size_formats: '8pt 10pt 12pt 14pt 16pt 18pt 24pt 36pt 48pt', + autolink_pattern: /^(https?:\/\/|www\.|(?!www\.)[a-z0-9\-]+\.[a-z]{2,13})/i, + link_default_protocol: 'https', + link_assume_external_targets: true, + link_target_list: [ + {title: 'None', value: ''}, + {title: 'New window', value: '_blank'}, + {title: 'Same window', value: '_self'} + ], + block_formats: 'Paragraph=p; Heading 1=h1; Heading 2=h2; Heading 3=h3; Heading 4=h4; Heading 5=h5; Heading 6=h6; Preformatted=pre; Blockquote=blockquote', + valid_elements: '*[*]', + extended_valid_elements: '*[*]', + valid_children: '+body[style]', + paste_as_text: false, + paste_block_drop: false, + paste_data_images: true, + image_advtab: true, + image_caption: true, + image_description: true, + image_dimensions: true, + image_title: true, + table_default_attributes: { + border: '1' + }, + table_default_styles: { + 'border-collapse': 'collapse', + 'width': '100%' + }, + emoticons_database: 'emojis', + setup: function(editor) { + // Auto-save on content change + editor.on('change', function() { + editor.save(); + }); + // Auto-save on blur (when editor loses focus) + editor.on('blur', function() { + editor.save(); + }); + // Auto-save on keyup (for continuous saving) + editor.on('keyup', function() { + editor.save(); + }); + // Save before form submission + editor.on('submit', function() { + editor.save(); + }); + } + }; + } + + // Function to actually initialize TinyMCE once it's loaded + function doInitTinyMCE() { + // Initialize TinyMCE + tinymce.init(getTinyMCEConfig()).then(function(editors) { + if (editors && editors[0]) { + console.log('TinyMCE initialized successfully'); + + // Add form submission handler to sync content + const textarea = document.getElementById('body'); + if (textarea && textarea.form) { + textarea.form.addEventListener('submit', function(e) { + // Sync all TinyMCE editors before form submission + tinymce.triggerSave(); + console.log('TinyMCE content synced to textarea before form submission'); + }); + } + } + }).catch(function(error) { + console.error('TinyMCE initialization failed:', error); + }); + + // Watch for theme changes and reinitialize TinyMCE + const observer = new MutationObserver(function(mutations) { + mutations.forEach(function(mutation) { + if (mutation.attributeName === 'class') { + const editor = tinymce.get('body'); + if (editor) { + // Save current content + const content = editor.getContent(); + // Remove the editor + tinymce.remove('#body'); + // Reinitialize with new theme + tinymce.init(getTinyMCEConfig()).then(function(editors) { + // Restore content + if (editors && editors[0]) { + editors[0].setContent(content); + } + }); + } + } + }); + }); + + // Start observing theme changes on the html element + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ['class'] + }); + } + + // Load TinyMCE script dynamically, then initialize + loadTinyMCEScript() + .then(function() { + console.log('TinyMCE available, initializing editor...'); + doInitTinyMCE(); + }) + .catch(function(error) { + console.error('Failed to load TinyMCE:', error); + // Show error message to user + const bodyTextarea = document.getElementById('body'); + if (bodyTextarea && bodyTextarea.parentElement) { + const errorDiv = document.createElement('div'); + errorDiv.className = 'mt-2 p-3 bg-red-50 dark:bg-red-900 border border-red-200 dark:border-red-700 text-red-800 dark:text-red-200 rounded'; + errorDiv.innerHTML = 'TinyMCE editor failed to load. Please refresh the page or check your internet connection.'; + bodyTextarea.parentElement.insertBefore(errorDiv, bodyTextarea.nextSibling); + } + }); +} + +// Mobile Enhancements +function initMobileEnhancements() { + // Create mobile sidebar overlay + createMobileSidebarOverlay(); + + // Enhanced mobile sidebar toggle + initMobileSidebarToggle(); + + // Mobile menu toggle in header + initMobileHeaderMenu(); + + // Touch-friendly improvements + initTouchEnhancements(); + + // Mobile table responsiveness + initMobileTableEnhancements(); + + // Prevent zoom on double tap for buttons + preventDoubleTapZoom(); + + // Optimize modals for mobile + optimizeModalsForMobile(); +} + +// Create overlay for mobile sidebar +function createMobileSidebarOverlay() { + if (document.querySelector('.mobile-sidebar-overlay')) return; + + const overlay = document.createElement('div'); + overlay.className = 'mobile-sidebar-overlay'; + overlay.addEventListener('click', function() { + closeMobileSidebar(); + }); + document.body.appendChild(overlay); +} + +// Mobile sidebar toggle functionality +function initMobileSidebarToggle() { + const mobileToggle = document.getElementById('mobile-sidebar-toggle'); + const sidebar = document.getElementById('sidebar'); + + if (mobileToggle && sidebar) { + mobileToggle.addEventListener('click', function(e) { + e.stopPropagation(); + toggleMobileSidebar(); + }); + } +} + +function toggleMobileSidebar() { + const sidebar = document.getElementById('sidebar'); + const overlay = document.querySelector('.mobile-sidebar-overlay'); + + if (sidebar && overlay) { + sidebar.classList.toggle('mobile-open'); + overlay.classList.toggle('active'); + + // Prevent body scroll when sidebar is open + if (sidebar.classList.contains('mobile-open')) { + document.body.style.overflow = 'hidden'; + } else { + document.body.style.overflow = ''; + } + } +} + +function closeMobileSidebar() { + const sidebar = document.getElementById('sidebar'); + const overlay = document.querySelector('.mobile-sidebar-overlay'); + + if (sidebar && overlay) { + sidebar.classList.remove('mobile-open'); + overlay.classList.remove('active'); + document.body.style.overflow = ''; + } +} + +// Mobile header menu toggle +function initMobileHeaderMenu() { + // Mobile search toggle + const mobileSearchToggle = document.getElementById('mobile-search-toggle'); + const mobileSearchForm = document.getElementById('mobile-search-form'); + + if (mobileSearchToggle && mobileSearchForm) { + mobileSearchToggle.addEventListener('click', function(e) { + e.stopPropagation(); + + if (mobileSearchForm.classList.contains('hidden')) { + mobileSearchForm.classList.remove('hidden'); + // Close mobile menu if open + const mobileMenu = document.getElementById('mobile-menu'); + if (mobileMenu) { + mobileMenu.style.display = 'none'; + } + } else { + mobileSearchForm.classList.add('hidden'); + } + }); + + // Close mobile search when clicking outside + document.addEventListener('click', function(e) { + if (!mobileSearchForm.contains(e.target) && + !mobileSearchToggle.contains(e.target) && + !mobileSearchForm.classList.contains('hidden')) { + mobileSearchForm.classList.add('hidden'); + } + }); + } + + const mobileMenuToggle = document.getElementById('mobile-menu-toggle'); + + if (mobileMenuToggle) { + mobileMenuToggle.addEventListener('click', function(e) { + e.stopPropagation(); + + // Close mobile search if open + if (mobileSearchForm && !mobileSearchForm.classList.contains('hidden')) { + mobileSearchForm.classList.add('hidden'); + } + + const desktopNav = document.querySelector('.hidden.md\\:flex'); + + if (desktopNav) { + // Create mobile menu if it doesn't exist + let mobileMenu = document.getElementById('mobile-menu'); + + if (!mobileMenu) { + mobileMenu = document.createElement('div'); + mobileMenu.id = 'mobile-menu'; + mobileMenu.className = 'md:hidden absolute top-16 left-0 right-0 bg-gray-800 dark:bg-gray-950 shadow-lg z-50 max-h-[80vh] overflow-y-auto'; + mobileMenu.style.display = 'none'; + + // Clone navigation content + const navClone = desktopNav.cloneNode(true); + navClone.classList.remove('hidden', 'md:flex', 'md:items-center', 'md:space-x-1', 'flex-1'); + navClone.classList.add('flex', 'flex-col', 'space-y-2', 'p-4'); + + // Update dropdown styles for mobile + navClone.querySelectorAll('.dropdown-container').forEach(dropdown => { + dropdown.classList.remove('relative'); + dropdown.classList.add('w-full'); + + const button = dropdown.querySelector('.dropdown-toggle'); + if (button) { + button.classList.add('w-full', 'justify-between'); + } + + const menu = dropdown.querySelector('.dropdown-menu'); + if (menu) { + menu.classList.remove('absolute', 'left-0', 'top-full', 'w-48'); + menu.classList.add('relative', 'w-full', 'mt-2'); + } + }); + + mobileMenu.appendChild(navClone); + mobileMenuToggle.parentElement.parentElement.appendChild(mobileMenu); + } + + // Toggle menu visibility + if (mobileMenu.style.display === 'none') { + mobileMenu.style.display = 'block'; + mobileMenuToggle.querySelector('i').classList.replace('fa-bars', 'fa-times'); + } else { + mobileMenu.style.display = 'none'; + mobileMenuToggle.querySelector('i').classList.replace('fa-times', 'fa-bars'); + } + } + }); + } + + // Close mobile menu when clicking outside + document.addEventListener('click', function(e) { + const mobileMenu = document.getElementById('mobile-menu'); + const mobileMenuToggle = document.getElementById('mobile-menu-toggle'); + + if (mobileMenu && mobileMenuToggle && + mobileMenu.style.display === 'block' && + !mobileMenu.contains(e.target) && + !mobileMenuToggle.contains(e.target)) { + mobileMenu.style.display = 'none'; + mobileMenuToggle.querySelector('i').classList.replace('fa-times', 'fa-bars'); + } + }); +} + +// Touch enhancements for better mobile interaction +function initTouchEnhancements() { + // Add touch feedback to all buttons + document.addEventListener('touchstart', function(e) { + const btn = e.target.closest('.btn, button, a[class*="btn"]'); + if (btn) { + btn.style.opacity = '0.7'; + } + }, { passive: true }); + + document.addEventListener('touchend', function(e) { + const btn = e.target.closest('.btn, button, a[class*="btn"]'); + if (btn) { + setTimeout(() => { + btn.style.opacity = ''; + }, 100); + } + }, { passive: true }); + + // Improve dropdown touch handling + document.querySelectorAll('.dropdown-toggle').forEach(toggle => { + let touchStartY = 0; + + toggle.addEventListener('touchstart', function(e) { + touchStartY = e.touches[0].clientY; + }, { passive: true }); + + toggle.addEventListener('touchend', function(e) { + const touchEndY = e.changedTouches[0].clientY; + const touchDiff = Math.abs(touchEndY - touchStartY); + + // Only trigger if it's a tap, not a scroll + if (touchDiff < 10) { + e.preventDefault(); + this.click(); + } + }); + }); +} + +// Mobile table enhancements +function initMobileTableEnhancements() { + // Add data labels to table cells for mobile view + const tables = document.querySelectorAll('table'); + + tables.forEach(table => { + const headers = Array.from(table.querySelectorAll('thead th')).map(th => th.textContent.trim()); + + table.querySelectorAll('tbody tr').forEach(row => { + Array.from(row.querySelectorAll('td')).forEach((cell, index) => { + if (headers[index] && !cell.hasAttribute('data-label')) { + cell.setAttribute('data-label', headers[index]); + } + }); + }); + + // Add responsive wrapper if not present + if (!table.parentElement.classList.contains('table-responsive')) { + const wrapper = document.createElement('div'); + wrapper.className = 'table-responsive'; + table.parentNode.insertBefore(wrapper, table); + wrapper.appendChild(table); + } + }); +} + +// Prevent double-tap zoom on buttons +function preventDoubleTapZoom() { + let lastTouchEnd = 0; + + document.addEventListener('touchend', function(e) { + const now = Date.now(); + if (now - lastTouchEnd <= 300) { + const target = e.target.closest('button, a, .btn, input[type="submit"]'); + if (target) { + e.preventDefault(); + } + } + lastTouchEnd = now; + }, { passive: false }); +} + +// Optimize modals for mobile devices +function optimizeModalsForMobile() { + // Ensure modals are properly sized on mobile + const observer = new MutationObserver(function(mutations) { + mutations.forEach(function(mutation) { + mutation.addedNodes.forEach(function(node) { + if (node.nodeType === 1) { + const modal = node.querySelector('.modal-content') || + (node.classList && node.classList.contains('modal-content') ? node : null); + + if (modal && window.innerWidth < 768) { + modal.style.maxHeight = '90vh'; + modal.style.overflowY = 'auto'; + modal.style.margin = '0.5rem'; + modal.style.width = 'calc(100% - 1rem)'; + } + } + }); + }); + }); + + observer.observe(document.body, { childList: true, subtree: true }); + + // Handle existing modals + document.querySelectorAll('.modal-content').forEach(modal => { + if (window.innerWidth < 768) { + modal.style.maxHeight = '90vh'; + modal.style.overflowY = 'auto'; + modal.style.margin = '0.5rem'; + modal.style.width = 'calc(100% - 1rem)'; + } + }); + + // Close sidebar when opening modal + document.addEventListener('click', function(e) { + if (e.target.closest('[data-open-nfo], [data-open-image-modal], .mediainfo-badge, .filelist-badge')) { + closeMobileSidebar(); + } + }); +} + +// Admin Dashboard Charts Initialization +function initAdminDashboardCharts() { + // Check if Chart.js is loaded and if we're on the admin dashboard + if (typeof Chart === 'undefined') { + return; + } + + // Check for dark mode + const isDarkMode = document.documentElement.classList.contains('dark'); + const textColor = isDarkMode ? '#e5e7eb' : '#374151'; + const gridColor = isDarkMode ? '#374151' : '#e5e7eb'; + + // Store chart instances for updates + window.adminCharts = { + cpu24h: null, + ram24h: null, + cpu30d: null, + ram30d: null + }; + + // Downloads Chart + const downloadsCtx = document.getElementById('downloadsChart'); + if (downloadsCtx) { + const downloadsData = JSON.parse(downloadsCtx.getAttribute('data-chart-data') || '[]'); + new Chart(downloadsCtx, { + type: 'bar', + data: { + labels: downloadsData.map(d => d.date), + datasets: [{ + label: 'Downloads', + data: downloadsData.map(d => d.count), + backgroundColor: 'rgba(34, 197, 94, 0.7)', + borderColor: 'rgba(34, 197, 94, 1)', + borderWidth: 2, + borderRadius: 6, + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + display: false + }, + tooltip: { + backgroundColor: isDarkMode ? '#1f2937' : '#ffffff', + titleColor: textColor, + bodyColor: textColor, + borderColor: gridColor, + borderWidth: 1, + padding: 12, + displayColors: false, + callbacks: { + label: function(context) { + return 'Downloads: ' + context.parsed.y.toLocaleString(); + } + } + } + }, + scales: { + y: { + beginAtZero: true, + ticks: { + color: textColor, + precision: 0 + }, + grid: { + color: gridColor, + drawBorder: false + } + }, + x: { + ticks: { + color: textColor + }, + grid: { + display: false + } + } + } + } + }); + } + + // API Hits Chart + const apiHitsCtx = document.getElementById('apiHitsChart'); + if (apiHitsCtx) { + const apiHitsData = JSON.parse(apiHitsCtx.getAttribute('data-chart-data') || '[]'); + new Chart(apiHitsCtx, { + type: 'line', + data: { + labels: apiHitsData.map(d => d.date), + datasets: [{ + label: 'API Hits', + data: apiHitsData.map(d => d.count), + backgroundColor: 'rgba(147, 51, 234, 0.1)', + borderColor: 'rgba(147, 51, 234, 1)', + borderWidth: 3, + fill: true, + tension: 0.4, + pointRadius: 4, + pointHoverRadius: 6, + pointBackgroundColor: 'rgba(147, 51, 234, 1)', + pointBorderColor: '#ffffff', + pointBorderWidth: 2, + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + display: false + }, + tooltip: { + backgroundColor: isDarkMode ? '#1f2937' : '#ffffff', + titleColor: textColor, + bodyColor: textColor, + borderColor: gridColor, + borderWidth: 1, + padding: 12, + displayColors: false, + callbacks: { + label: function(context) { + return 'API Hits: ' + context.parsed.y.toLocaleString(); + } + } + } + }, + scales: { + y: { + beginAtZero: true, + ticks: { + color: textColor, + precision: 0 + }, + grid: { + color: gridColor, + drawBorder: false + } + }, + x: { + ticks: { + color: textColor + }, + grid: { + color: gridColor, + drawBorder: false + } + } + } + } + }); + } + + // CPU Usage 24 Hour Historical Chart (Line) + const cpuHistory24hCtx = document.getElementById('cpuHistory24hChart'); + if (cpuHistory24hCtx) { + const cpuHistory = JSON.parse(cpuHistory24hCtx.getAttribute('data-history') || '[]'); + const cpuLabel = cpuHistory24hCtx.getAttribute('data-chart-label') || 'CPU Usage %'; + + window.adminCharts.cpu24h = new Chart(cpuHistory24hCtx, { + type: 'line', + data: { + labels: cpuHistory.map(d => d.time), + datasets: [{ + label: cpuLabel, + data: cpuHistory.map(d => d.value), + backgroundColor: 'rgba(249, 115, 22, 0.1)', + borderColor: 'rgba(249, 115, 22, 1)', + borderWidth: 3, + fill: true, + tension: 0.4, + pointRadius: 4, + pointHoverRadius: 6, + pointBackgroundColor: 'rgba(249, 115, 22, 1)', + pointBorderColor: '#ffffff', + pointBorderWidth: 2, + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + display: false + }, + tooltip: { + backgroundColor: isDarkMode ? '#1f2937' : '#ffffff', + titleColor: textColor, + bodyColor: textColor, + borderColor: gridColor, + borderWidth: 1, + padding: 12, + displayColors: false, + callbacks: { + label: function(context) { + return 'CPU: ' + context.parsed.y.toFixed(1) + '%'; + } + } + } + }, + scales: { + y: { + beginAtZero: true, + max: 100, + ticks: { + color: textColor, + callback: function(value) { + return value + '%'; + } + }, + grid: { + color: gridColor, + drawBorder: false + } + }, + x: { + ticks: { + color: textColor, + maxRotation: 45, + minRotation: 45 + }, + grid: { + color: gridColor, + drawBorder: false + } + } + } + } + }); + } + + // RAM Usage 24 Hour Historical Chart (Line) + const ramHistory24hCtx = document.getElementById('ramHistory24hChart'); + if (ramHistory24hCtx) { + const ramHistory = JSON.parse(ramHistory24hCtx.getAttribute('data-history') || '[]'); + const ramLabel = ramHistory24hCtx.getAttribute('data-chart-label') || 'RAM Usage %'; + + window.adminCharts.ram24h = new Chart(ramHistory24hCtx, { + type: 'line', + data: { + labels: ramHistory.map(d => d.time), + datasets: [{ + label: ramLabel, + data: ramHistory.map(d => d.value), + backgroundColor: 'rgba(6, 182, 212, 0.1)', + borderColor: 'rgba(6, 182, 212, 1)', + borderWidth: 3, + fill: true, + tension: 0.4, + pointRadius: 4, + pointHoverRadius: 6, + pointBackgroundColor: 'rgba(6, 182, 212, 1)', + pointBorderColor: '#ffffff', + pointBorderWidth: 2, + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + display: false + }, + tooltip: { + backgroundColor: isDarkMode ? '#1f2937' : '#ffffff', + titleColor: textColor, + bodyColor: textColor, + borderColor: gridColor, + borderWidth: 1, + padding: 12, + displayColors: false, + callbacks: { + label: function(context) { + return 'RAM: ' + context.parsed.y.toFixed(1) + '%'; + } + } + } + }, + scales: { + y: { + beginAtZero: true, + max: 100, + ticks: { + color: textColor, + callback: function(value) { + return value + '%'; + } + }, + grid: { + color: gridColor, + drawBorder: false + } + }, + x: { + ticks: { + color: textColor, + maxRotation: 45, + minRotation: 45 + }, + grid: { + color: gridColor, + drawBorder: false + } + } + } + } + }); + } + + // CPU Usage 30 Day Historical Chart (Line) + const cpuHistory30dCtx = document.getElementById('cpuHistory30dChart'); + if (cpuHistory30dCtx) { + const cpuHistory = JSON.parse(cpuHistory30dCtx.getAttribute('data-history') || '[]'); + const cpuLabel = cpuHistory30dCtx.getAttribute('data-chart-label') || 'CPU Usage %'; + + window.adminCharts.cpu30d = new Chart(cpuHistory30dCtx, { + type: 'line', + data: { + labels: cpuHistory.map(d => d.time), + datasets: [{ + label: cpuLabel, + data: cpuHistory.map(d => d.value), + backgroundColor: 'rgba(249, 115, 22, 0.1)', + borderColor: 'rgba(249, 115, 22, 1)', + borderWidth: 3, + fill: true, + tension: 0.4, + pointRadius: 3, + pointHoverRadius: 5, + pointBackgroundColor: 'rgba(249, 115, 22, 1)', + pointBorderColor: '#ffffff', + pointBorderWidth: 2, + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + display: false + }, + tooltip: { + backgroundColor: isDarkMode ? '#1f2937' : '#ffffff', + titleColor: textColor, + bodyColor: textColor, + borderColor: gridColor, + borderWidth: 1, + padding: 12, + displayColors: false, + callbacks: { + label: function(context) { + return 'CPU: ' + context.parsed.y.toFixed(1) + '%'; + } + } + } + }, + scales: { + y: { + beginAtZero: true, + max: 100, + ticks: { + color: textColor, + callback: function(value) { + return value + '%'; + } + }, + grid: { + color: gridColor, + drawBorder: false + } + }, + x: { + ticks: { + color: textColor, + maxRotation: 45, + minRotation: 45 + }, + grid: { + color: gridColor, + drawBorder: false + } + } + } + } + }); + } + + // RAM Usage 30 Day Historical Chart (Line) + const ramHistory30dCtx = document.getElementById('ramHistory30dChart'); + if (ramHistory30dCtx) { + const ramHistory = JSON.parse(ramHistory30dCtx.getAttribute('data-history') || '[]'); + const ramLabel = ramHistory30dCtx.getAttribute('data-chart-label') || 'RAM Usage %'; + + window.adminCharts.ram30d = new Chart(ramHistory30dCtx, { + type: 'line', + data: { + labels: ramHistory.map(d => d.time), + datasets: [{ + label: ramLabel, + data: ramHistory.map(d => d.value), + backgroundColor: 'rgba(6, 182, 212, 0.1)', + borderColor: 'rgba(6, 182, 212, 1)', + borderWidth: 3, + fill: true, + tension: 0.4, + pointRadius: 3, + pointHoverRadius: 5, + pointBackgroundColor: 'rgba(6, 182, 212, 1)', + pointBorderColor: '#ffffff', + pointBorderWidth: 2, + }] + }, + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { + display: false + }, + tooltip: { + backgroundColor: isDarkMode ? '#1f2937' : '#ffffff', + titleColor: textColor, + bodyColor: textColor, + borderColor: gridColor, + borderWidth: 1, + padding: 12, + displayColors: false, + callbacks: { + label: function(context) { + return 'RAM: ' + context.parsed.y.toFixed(1) + '%'; + } + } + } + }, + scales: { + y: { + beginAtZero: true, + max: 100, + ticks: { + color: textColor, + callback: function(value) { + return value + '%'; + } + }, + grid: { + color: gridColor, + drawBorder: false + } + }, + x: { + ticks: { + color: textColor, + maxRotation: 45, + minRotation: 45 + }, + grid: { + color: gridColor, + drawBorder: false + } + } + } + } + }); + } + + // Start auto-refresh for system metrics (every 60 seconds) + startSystemMetricsAutoRefresh(); +} + +// Auto-refresh system metrics every minute +function startSystemMetricsAutoRefresh() { + // Check if we're on the admin dashboard with system metrics + if (!document.getElementById('cpuHistory24hChart')) { + return; + } + + // Update current metrics display + function updateCurrentMetrics() { + fetch('/admin/api/system-metrics/current') + .then(response => response.json()) + .then(data => { + // Update CPU current usage + const cpuCurrentElem = document.querySelector('[data-metric="cpu-current"]'); + if (cpuCurrentElem) { + cpuCurrentElem.textContent = data.cpu.current.toFixed(1) + '%'; + } + + // Update CPU load averages + const load1minElem = document.querySelector('[data-metric="cpu-load-1min"]'); + const load5minElem = document.querySelector('[data-metric="cpu-load-5min"]'); + const load15minElem = document.querySelector('[data-metric="cpu-load-15min"]'); + + if (load1minElem) load1minElem.textContent = data.cpu.load_average['1min']; + if (load5minElem) load5minElem.textContent = data.cpu.load_average['5min']; + if (load15minElem) load15minElem.textContent = data.cpu.load_average['15min']; + + // Update RAM current usage + const ramCurrentElem = document.querySelector('[data-metric="ram-current"]'); + if (ramCurrentElem) { + ramCurrentElem.textContent = data.ram.percentage.toFixed(1) + '%'; + } + + const ramDetailsElem = document.querySelector('[data-metric="ram-details"]'); + if (ramDetailsElem) { + ramDetailsElem.textContent = `${data.ram.used.toFixed(2)} GB / ${data.ram.total.toFixed(2)} GB`; + } + + // Update last updated timestamp + const timestampElem = document.querySelector('[data-metric="last-updated"]'); + if (timestampElem) { + const now = new Date(); + timestampElem.textContent = now.toLocaleTimeString(); + } + }) + .catch(error => { + console.error('Error fetching current metrics:', error); + }); + } + + // Update historical charts + function updateHistoricalCharts() { + fetch('/admin/api/system-metrics/historical') + .then(response => response.json()) + .then(data => { + // Update CPU 24h chart + if (window.adminCharts && window.adminCharts.cpu24h) { + window.adminCharts.cpu24h.data.labels = data.cpu.history_24h.map(d => d.time); + window.adminCharts.cpu24h.data.datasets[0].data = data.cpu.history_24h.map(d => d.value); + window.adminCharts.cpu24h.update('none'); // 'none' for no animation + } + + // Update RAM 24h chart + if (window.adminCharts && window.adminCharts.ram24h) { + window.adminCharts.ram24h.data.labels = data.ram.history_24h.map(d => d.time); + window.adminCharts.ram24h.data.datasets[0].data = data.ram.history_24h.map(d => d.value); + window.adminCharts.ram24h.update('none'); + } + + // Update CPU 30d chart + if (window.adminCharts && window.adminCharts.cpu30d) { + window.adminCharts.cpu30d.data.labels = data.cpu.history_30d.map(d => d.time); + window.adminCharts.cpu30d.data.datasets[0].data = data.cpu.history_30d.map(d => d.value); + window.adminCharts.cpu30d.update('none'); + } + + // Update RAM 30d chart + if (window.adminCharts && window.adminCharts.ram30d) { + window.adminCharts.ram30d.data.labels = data.ram.history_30d.map(d => d.time); + window.adminCharts.ram30d.data.datasets[0].data = data.ram.history_30d.map(d => d.value); + window.adminCharts.ram30d.update('none'); + } + }) + .catch(error => { + console.error('Error fetching historical metrics:', error); + }); + } + + // Initial update + updateCurrentMetrics(); + + // Update current metrics every 60 seconds (1 minute) + setInterval(updateCurrentMetrics, 60000); + + // Update historical charts every 5 minutes (to reduce load) + setInterval(updateHistoricalCharts, 300000); +} + +// Admin Groups Management +function initAdminGroups() { + // Group Edit Form Validation + const groupEditForm = document.getElementById('groupForm'); + if (groupEditForm) { + groupEditForm.addEventListener('submit', function(event) { + const firstRecord = document.getElementById('first_record'); + const lastRecord = document.getElementById('last_record'); + + if (!firstRecord || !lastRecord) return; + + const firstVal = parseInt(firstRecord.value); + const lastVal = parseInt(lastRecord.value); + + if (firstVal > lastVal && lastVal > 0) { + event.preventDefault(); + alert('First record ID cannot be greater than last record ID'); + return false; + } + }); + } + + // Group Bulk Form Validation + const groupBulkForm = document.getElementById('groupBulkForm'); + if (groupBulkForm) { + groupBulkForm.addEventListener('submit', function(event) { + const groupfilter = document.getElementById('groupfilter'); + if (!groupfilter) return; + + const filterValue = groupfilter.value.trim(); + + if (filterValue === '') { + event.preventDefault(); + alert('Please enter a group pattern'); + return false; + } + + // Simple regex validation + try { + new RegExp(filterValue); + } catch(e) { + event.preventDefault(); + alert('Invalid regex pattern: ' + e.message); + return false; + } + }); + } +} + +function showResetAllModal() { + const modal = document.getElementById('resetAllModal'); + if (modal) { + modal.classList.remove('hidden'); + } +} + +function hideResetAllModal() { + const modal = document.getElementById('resetAllModal'); + if (modal) { + modal.classList.add('hidden'); + } +} + +function showPurgeAllModal() { + const modal = document.getElementById('purgeAllModal'); + if (modal) { + modal.classList.remove('hidden'); + } +} + +function hidePurgeAllModal() { + const modal = document.getElementById('purgeAllModal'); + if (modal) { + modal.classList.add('hidden'); + } +} + +function ajax_group_status(id, status) { + const ajaxUrl = document.querySelector('[data-ajax-url]')?.dataset.ajaxUrl; + const csrfToken = document.querySelector('[data-csrf-token]')?.dataset.csrfToken; + + if (!ajaxUrl || !csrfToken) return; + + // Ensure status is an integer + const statusInt = parseInt(status); + + fetch(ajaxUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': csrfToken + }, + body: JSON.stringify({ + action: 'toggle_group_active_status', + group_id: parseInt(id), + group_status: statusInt + }) + }) + .then(response => response.text()) + .then(data => { + // Update succeeded, regenerate the button with the new status + const groupCell = document.getElementById('group-' + id); + if (groupCell) { + // The status parameter is what we're setting it TO (not toggling) + // status = 1 means we're activating, status = 0 means we're deactivating + const isActive = statusInt === 1; + + // Generate new button HTML with proper data attributes + const buttonHTML = isActive + ? `` + : ``; + + groupCell.innerHTML = buttonHTML; + + // Show success message + if (typeof showToast === 'function') { + showToast(isActive ? 'Group activated successfully' : 'Group deactivated successfully', 'success'); + } + } + }) + .catch(error => { + console.error('Error toggling group status:', error); + if (typeof showToast === 'function') { + showToast('Error updating group status', 'error'); + } + }); +} + +function ajax_backfill_status(id, status) { + const ajaxUrl = document.querySelector('[data-ajax-url]')?.dataset.ajaxUrl; + const csrfToken = document.querySelector('[data-csrf-token]')?.dataset.csrfToken; + + if (!ajaxUrl || !csrfToken) return; + + // Ensure status is an integer + const statusInt = parseInt(status); + + fetch(ajaxUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': csrfToken + }, + body: JSON.stringify({ + action: 'toggle_group_backfill', + group_id: parseInt(id), + backfill: statusInt + }) + }) + .then(response => response.text()) + .then(data => { + // Update succeeded, regenerate the button with the new status + const backfillCell = document.getElementById('backfill-' + id); + if (backfillCell) { + // The status parameter is what we're setting it TO (not toggling) + // status = 1 means we're enabling, status = 0 means we're disabling + const isEnabled = statusInt === 1; + + // Generate new button HTML with proper data attributes + const buttonHTML = isEnabled + ? `` + : ``; + + backfillCell.innerHTML = buttonHTML; + + // Show success message + if (typeof showToast === 'function') { + showToast(isEnabled ? 'Backfill enabled successfully' : 'Backfill disabled successfully', 'success'); + } + } + }) + .catch(error => { + console.error('Error toggling backfill status:', error); + if (typeof showToast === 'function') { + showToast('Error updating backfill status', 'error'); + } + }); +} + +function ajax_group_reset(id) { + if (!confirm('Are you sure you want to reset this group?')) { + return; + } + + const ajaxUrl = document.querySelector('[data-ajax-url]')?.dataset.ajaxUrl; + const csrfToken = document.querySelector('[data-csrf-token]')?.dataset.csrfToken; + + if (!ajaxUrl || !csrfToken) return; + + fetch(ajaxUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': csrfToken + }, + body: JSON.stringify({ + action: 'reset_group', + group_id: id + }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + location.reload(); + } + }) + .catch(error => console.error('Error resetting group:', error)); +} + +function confirmGroupDelete(id) { + if (!confirm('Are you sure you want to delete this group?')) { + return; + } + + const ajaxUrl = document.querySelector('[data-ajax-url]')?.dataset.ajaxUrl; + const csrfToken = document.querySelector('[data-csrf-token]')?.dataset.csrfToken; + + if (!ajaxUrl || !csrfToken) return; + + fetch(ajaxUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': csrfToken + }, + body: JSON.stringify({ + action: 'delete_group', + group_id: id + }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + const row = document.getElementById('grouprow-' + id); + if (row) { + row.classList.add('fade-out'); + setTimeout(() => row.remove(), 300); + } + } + }) + .catch(error => console.error('Error deleting group:', error)); +} + +function confirmGroupPurge(id) { + if (!confirm('Are you sure you want to purge this group? This will delete all releases and binaries!')) { + return; + } + + const ajaxUrl = document.querySelector('[data-ajax-url]')?.dataset.ajaxUrl; + const csrfToken = document.querySelector('[data-csrf-token]')?.dataset.csrfToken; + + if (!ajaxUrl || !csrfToken) return; + + fetch(ajaxUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': csrfToken + }, + body: JSON.stringify({ + action: 'purge_group', + group_id: id + }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + location.reload(); + } + }) + .catch(error => console.error('Error purging group:', error)); +} + +function ajax_group_reset_all() { + const ajaxUrl = document.querySelector('[data-ajax-url]')?.dataset.ajaxUrl; + const csrfToken = document.querySelector('[data-csrf-token]')?.dataset.csrfToken; + + if (!ajaxUrl || !csrfToken) return; + + fetch(ajaxUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': csrfToken + }, + body: JSON.stringify({ + action: 'reset_all_groups' + }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + hideResetAllModal(); + location.reload(); + } + }) + .catch(error => console.error('Error resetting all groups:', error)); +} + +function ajax_group_purge_all() { + const ajaxUrl = document.querySelector('[data-ajax-url]')?.dataset.ajaxUrl; + const csrfToken = document.querySelector('[data-csrf-token]')?.dataset.csrfToken; + + if (!ajaxUrl || !csrfToken) return; + + fetch(ajaxUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': csrfToken + }, + body: JSON.stringify({ + action: 'purge_all_groups' + }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + hidePurgeAllModal(); + location.reload(); + } + }) + .catch(error => console.error('Error purging all groups:', error)); +} + +// ======================================== +// Admin-specific functions +// ======================================== + +// Admin Categories - Delete confirmation +window.confirmDelete = window.confirmDelete || function(id) { + const modal = document.getElementById('deleteModal'); + const deleteLink = document.getElementById('confirmDeleteLink'); + if (modal && deleteLink) { + deleteLink.href = window.location.origin + '/admin/category-delete?id=' + id; + modal.classList.remove('hidden'); + } +}; + +window.closeDeleteModal = function() { + const modal = document.getElementById('deleteModal'); + if (modal) { + modal.classList.add('hidden'); + } +}; + +// Admin Console/Books - Image preview on file upload +function initAdminImagePreview() { + // Console cover preview + const consoleCoverInput = document.getElementById('cover'); + if (consoleCoverInput) { + consoleCoverInput.addEventListener('change', function(e) { + if (e.target.files && e.target.files[0]) { + const reader = new FileReader(); + reader.onload = function(event) { + const imgs = document.querySelectorAll('img[alt]'); + imgs.forEach(img => { + if (img.closest('form')?.contains(consoleCoverInput)) { + img.src = event.target.result; + } + }); + }; + reader.readAsDataURL(e.target.files[0]); + } + }); + } +} + +// Admin Users - Verify user modal +window.showVerifyModal = function(event, form) { + event.preventDefault(); + window.currentVerifyForm = form; + const modal = document.getElementById('verifyUserModal'); + if (modal) { + modal.classList.remove('hidden'); + } +}; + +window.hideVerifyModal = function() { + const modal = document.getElementById('verifyUserModal'); + if (modal) { + modal.classList.add('hidden'); + } + window.currentVerifyForm = null; +}; + +window.submitVerifyForm = function() { + if (window.currentVerifyForm) { + window.currentVerifyForm.submit(); + } +}; + +// Admin Invitations - Select all checkboxes +function initAdminInvitationsSelectAll() { + const selectAllCheckbox = document.getElementById('select_all'); + if (selectAllCheckbox) { + selectAllCheckbox.addEventListener('change', function(e) { + document.querySelectorAll('.invitation-checkbox').forEach(cb => { + cb.checked = e.target.checked; + }); + }); + } +} + +// Admin Invitations - Copy to clipboard +window.copyToClipboard = function(text) { + navigator.clipboard.writeText(text).then(function() { + if (typeof showToast === 'function') { + showToast('Link copied to clipboard!', 'success'); + } else { + alert('Link copied to clipboard!'); + } + }, function(err) { + console.error('Could not copy text: ', err); + alert('Failed to copy to clipboard'); + }); +}; + +// Admin Blacklist - Delete via AJAX +window.ajax_binaryblacklist_delete = function(id) { + const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content; + + fetch(window.location.origin + '/admin/binaryblacklist-delete?id=' + id, { + method: 'GET', + headers: { + 'X-Requested-With': 'XMLHttpRequest', + 'X-CSRF-TOKEN': csrfToken + } + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + const row = document.getElementById('row-' + id); + if (row) { + row.style.transition = 'opacity 0.3s'; + row.style.opacity = '0'; + setTimeout(() => row.remove(), 300); + } + if (typeof showToast === 'function') { + showToast('Blacklist entry deleted successfully', 'success'); + } + } else { + if (typeof showToast === 'function') { + showToast('Error deleting blacklist entry', 'error'); + } + } + }) + .catch(error => { + console.error('Error:', error); + if (typeof showToast === 'function') { + showToast('Error deleting blacklist entry', 'error'); + } + }); +}; + +// Admin Comments - Delete modal +window.openDeleteModal = function(commentId, commentText) { + const modal = document.getElementById('deleteModal'); + const form = document.getElementById('deleteForm'); + const preview = document.getElementById('deleteCommentPreview'); + + if (modal && form && preview) { + // Set the form action + form.action = window.location.origin + '/admin/comments-delete/' + commentId; + + // Set the comment preview + preview.textContent = '"' + commentText.substring(0, 100) + (commentText.length >= 100 ? '..."' : '"'); + + // Show the modal + modal.classList.remove('hidden'); + modal.classList.add('flex'); + + // Prevent body scroll + document.body.style.overflow = 'hidden'; + } +}; + +window.closeDeleteModal = window.closeDeleteModal || function() { + const modal = document.getElementById('deleteModal'); + if (modal) { + // Hide the modal + modal.classList.add('hidden'); + modal.classList.remove('flex'); + + // Restore body scroll + document.body.style.overflow = ''; + } +}; + +// Admin Regex Forms - Validation +function initAdminRegexFormValidation() { + // Release naming regex form + const releaseNamingForm = document.getElementById('regexForm'); + if (releaseNamingForm && window.location.pathname.includes('release-naming')) { + releaseNamingForm.addEventListener('submit', function(event) { + const groupRegex = document.getElementById('group_regex'); + const regex = document.getElementById('regex'); + const ordinal = document.getElementById('ordinal'); + + if (!groupRegex?.value.trim() || !regex?.value.trim() || !ordinal?.value) { + event.preventDefault(); + alert('Please fill in all required fields.'); + return false; + } + }); + } + + // Category regex form + if (releaseNamingForm && window.location.pathname.includes('category')) { + releaseNamingForm.addEventListener('submit', function(event) { + const groupRegex = document.getElementById('group_regex'); + const regex = document.getElementById('regex'); + const ordinal = document.getElementById('ordinal'); + + if (!groupRegex?.value.trim() || !regex?.value.trim() || !ordinal?.value) { + event.preventDefault(); + alert('Please fill in all required fields.'); + return false; + } + }); + } + + // Collection regex form + if (releaseNamingForm && window.location.pathname.includes('collection')) { + releaseNamingForm.addEventListener('submit', function(event) { + const groupRegex = document.getElementById('group_regex'); + const regex = document.getElementById('regex'); + const ordinal = document.getElementById('ordinal'); + + if (!groupRegex?.value.trim() || !regex?.value.trim() || !ordinal?.value) { + event.preventDefault(); + alert('Please fill in all required fields.'); + return false; + } + }); + } + + // Blacklist form + const blacklistForm = document.getElementById('blacklistForm'); + if (blacklistForm) { + blacklistForm.addEventListener('submit', function(event) { + const groupname = document.getElementById('groupname'); + const regex = document.getElementById('regex'); + + if (!groupname?.value.trim() || !regex?.value.trim()) { + event.preventDefault(); + alert('Please fill in all required fields.'); + return false; + } + }); + } +} + +// Admin Tmux - Select color highlighting +function initAdminTmuxSelectColors() { + const tmuxForm = document.getElementById('tmuxForm'); + if (!tmuxForm) return; + + // Function to update select colors based on value + function updateSelectColor(select) { + select.classList.remove('select-yes', 'select-no', 'select-other'); + + const value = select.value.toLowerCase(); + if (value === '1' || value === 'true' || value === 'yes') { + select.classList.add('select-yes'); + } else if (value === '0' || value === 'false' || value === 'no') { + select.classList.add('select-no'); + } else { + select.classList.add('select-other'); + } + } + + // Apply to all select elements in the form + const selects = tmuxForm.querySelectorAll('select'); + selects.forEach(select => { + // Initial color + updateSelectColor(select); + + // Update on change + select.addEventListener('change', function() { + updateSelectColor(this); + }); + }); +} + +// Initialize all admin-specific features +function initAdminSpecificFeatures() { + initAdminImagePreview(); + initAdminInvitationsSelectAll(); + initAdminRegexFormValidation(); + initAdminTmuxSelectColors(); + initAdminDeletedUsers(); + + // Close delete modal when clicking outside + const deleteModal = document.getElementById('deleteModal'); + if (deleteModal) { + deleteModal.addEventListener('click', function(e) { + if (e.target === this) { + closeDeleteModal(); + } + }); + } + + // Close verify modal when clicking outside + const verifyUserModal = document.getElementById('verifyUserModal'); + if (verifyUserModal) { + verifyUserModal.addEventListener('click', function(event) { + if (event.target === this) { + hideVerifyModal(); + } + }); + } +} + +// Admin Deleted Users page functionality +function initAdminDeletedUsers() { + // Select all checkbox + const selectAllCheckbox = document.getElementById('selectAll'); + if (selectAllCheckbox) { + selectAllCheckbox.addEventListener('change', function() { + const checkboxes = document.querySelectorAll('.user-checkbox'); + checkboxes.forEach(checkbox => { + checkbox.checked = this.checked; + }); + }); + } + + // Bulk action confirmation + window.confirmBulkAction = function() { + const action = document.getElementById('bulkAction')?.value; + const checkedBoxes = document.querySelectorAll('.user-checkbox:checked'); + const validationError = document.getElementById('validationError'); + const validationErrorMessage = document.getElementById('validationErrorMessage'); + + if (!action) { + if (validationError && validationErrorMessage) { + validationErrorMessage.textContent = 'Please select an action from the dropdown.'; + validationError.classList.remove('hidden'); + } + return false; + } + + if (checkedBoxes.length === 0) { + if (validationError && validationErrorMessage) { + validationErrorMessage.textContent = 'Please select at least one user.'; + validationError.classList.remove('hidden'); + } + return false; + } + + // Hide validation error if showing + if (validationError) { + validationError.classList.add('hidden'); + } + + const count = checkedBoxes.length; + const actionText = action === 'restore' ? 'restore' : 'permanently delete'; + + return confirm(`Are you sure you want to ${actionText} ${count} user${count > 1 ? 's' : ''}?`); + }; + + // Individual action confirmations + document.querySelectorAll('.inline-form button[data-confirm]').forEach(button => { + button.addEventListener('click', function(e) { + const message = this.getAttribute('data-confirm'); + if (!confirm(message)) { + e.preventDefault(); + } + }); + }); +} + +// My Movies page functionality +function initMyMovies() { + // Confirm before removing movies + document.querySelectorAll('.confirm_action').forEach(element => { + element.addEventListener('click', function(e) { + if (!confirm('Are you sure you want to remove this movie from your watchlist?')) { + e.preventDefault(); + } + }); + }); + + // Image fallback for movie covers + document.querySelectorAll('img[data-fallback-src]').forEach(img => { + img.addEventListener('error', function() { + const fallback = this.getAttribute('data-fallback-src'); + if (fallback && this.src !== fallback) { + this.src = fallback; + } + }); + }); + + // Initialize Bootstrap tooltips if they exist + if (typeof bootstrap !== 'undefined' && bootstrap.Tooltip) { + const tooltipTriggerList = document.querySelectorAll('[data-bs-toggle="tooltip"]'); + const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl)); + } +} + +// Auth pages functionality +function initAuthPages() { + // Auto-hide success messages after 5 seconds on login page + if (window.location.pathname.includes('/login')) { + setTimeout(function() { + const alerts = document.querySelectorAll('.bg-green-50, .bg-blue-50'); + alerts.forEach(function(alert) { + alert.style.transition = 'opacity 0.5s ease-out'; + alert.style.opacity = '0'; + setTimeout(() => alert.remove(), 500); + }); + }, 5000); + } + + // 2FA verification - Auto-submit when 6 digits are entered + const otpInput = document.getElementById('one_time_password'); + if (otpInput) { + otpInput.addEventListener('input', function(e) { + // Remove non-numeric characters + this.value = this.value.replace(/[^0-9]/g, ''); + + // Auto-submit when 6 digits are entered + if (this.value.length === 6) { + // Small delay to show the complete code before submitting + setTimeout(() => { + this.form.submit(); + }, 300); + } + }); + + // Focus on input when page loads + otpInput.focus(); + } +} + +// Profile edit page functionality +function initProfileEdit() { + // 2FA Disable Form Toggle + const toggleBtn = document.getElementById('toggle-disable-2fa-btn'); + const cancelBtn = document.getElementById('cancel-disable-2fa-btn'); + const formContainer = document.getElementById('disable-2fa-form-container'); + + if (toggleBtn && formContainer) { + toggleBtn.addEventListener('click', function() { + formContainer.style.display = formContainer.style.display === 'none' ? 'block' : 'none'; + }); + } + + if (cancelBtn && formContainer) { + cancelBtn.addEventListener('click', function() { + formContainer.style.display = 'none'; + // Clear password field + const passwordInput = document.getElementById('disable_2fa_password'); + if (passwordInput) { + passwordInput.value = ''; + } + }); + } + + // Theme preference instant preview + const themeRadios = document.querySelectorAll('input[name="theme_preference"]'); + const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); + + function applyTheme(themeValue) { + const html = document.documentElement; + + if (themeValue === 'system') { + // Use system preference + if (mediaQuery.matches) { + html.classList.add('dark'); + } else { + html.classList.remove('dark'); + } + } else if (themeValue === 'dark') { + html.classList.add('dark'); + } else { + html.classList.remove('dark'); + } + } + + themeRadios.forEach(radio => { + radio.addEventListener('change', function() { + applyTheme(this.value); + }); + }); + + // Listen to system preference changes if 'system' is selected + mediaQuery.addEventListener('change', function(e) { + const selectedTheme = document.querySelector('input[name="theme_preference"]:checked')?.value; + if (selectedTheme === 'system') { + applyTheme('system'); + } + }); +} + +// Details page image modal functionality +function initDetailsPageImageModal() { + window.openImageModal = function(imageUrl, title) { + const modal = document.getElementById('imageModal'); + const modalImage = document.getElementById('imageModalImage'); + const modalTitle = document.getElementById('imageModalTitle'); + + if (modal && modalImage) { + modalImage.src = imageUrl; + if (modalTitle) { + modalTitle.textContent = title || ''; + } + modal.classList.remove('hidden'); + modal.classList.add('flex'); + + // Prevent body scroll when modal is open + document.body.style.overflow = 'hidden'; + } + }; + + window.closeImageModal = function() { + const modal = document.getElementById('imageModal'); + if (modal) { + modal.classList.add('hidden'); + modal.classList.remove('flex'); + + // Restore body scroll + document.body.style.overflow = ''; + } + }; + + // Close modal when clicking outside the image + const imageModal = document.getElementById('imageModal'); + if (imageModal) { + imageModal.addEventListener('click', function(e) { + if (e.target === this) { + closeImageModal(); + } + }); + } + + // Close modal with Escape key + document.addEventListener('keydown', function(e) { + if (e.key === 'Escape') { + const modal = document.getElementById('imageModal'); + if (modal && !modal.classList.contains('hidden')) { + closeImageModal(); + } + } + }); +} + +// Add to cart functionality for movie/browse pages +function initAddToCart() { + window.addToCart = function(guid) { + const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content; + const baseUrl = document.querySelector('meta[name="app-url"]')?.content || ''; + + fetch(baseUrl + '/cart/add', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-TOKEN': csrfToken, + 'X-Requested-With': 'XMLHttpRequest' + }, + body: JSON.stringify({ id: guid }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + if (typeof showToast === 'function') { + showToast('Added to cart successfully!', 'success'); + } + } else { + if (typeof showToast === 'function') { + showToast('Failed to add to cart', 'error'); + } + } + }) + .catch(error => { + console.error('Error:', error); + if (typeof showToast === 'function') { + showToast('An error occurred', 'error'); + } + }); + }; +} + +// Note: Main DOMContentLoaded event listener is already defined at the top of the file +// Just adding the new initializations to the existing initialization list above + +// Show validation error message for deleted users page +window.showValidationError = function(message) { + const errorDiv = document.getElementById('validationError'); + const errorMessage = document.getElementById('validationErrorMessage'); + if (errorDiv && errorMessage) { + errorMessage.textContent = message; + errorDiv.classList.remove('hidden'); + + // Scroll to the error message + errorDiv.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); + + // Auto-hide after 5 seconds + setTimeout(() => { + errorDiv.classList.add('hidden'); + }, 5000); + } +}; + +// Hide validation error +window.hideValidationError = function() { + const errorDiv = document.getElementById('validationError'); + if (errorDiv) { + errorDiv.classList.add('hidden'); + } +}; + +// Confirm bulk action for deleted users +window.confirmBulkAction = function(event) { + const action = document.getElementById('bulkAction')?.value; + const checkedBoxes = document.querySelectorAll('.user-checkbox:checked'); + + if (!action) { + event.preventDefault(); + showValidationError('Please select an action from the dropdown.'); + return false; + } + + if (checkedBoxes.length === 0) { + event.preventDefault(); + showValidationError('Please select at least one user to perform this action.'); + return false; + } + + // Show confirmation dialog with appropriate message + let confirmMessage = ''; + if (action === 'delete') { + confirmMessage = `Are you sure you want to PERMANENTLY delete ${checkedBoxes.length} user(s)?\n\nThis action cannot be undone!`; + } else if (action === 'restore') { + confirmMessage = `Are you sure you want to restore ${checkedBoxes.length} user(s)?`; + } + + if (!confirm(confirmMessage)) { + event.preventDefault(); + return false; + } + + return true; +}; + diff --git a/resources/smarty/plugins/block.php.php b/resources/smarty/plugins/block.php.php deleted file mode 100755 index e40ead937..000000000 --- a/resources/smarty/plugins/block.php.php +++ /dev/null @@ -1,24 +0,0 @@ -allow_php_tag) { - throw new SmartyException('{php} is deprecated, set allow_php_tag = true to enable'); - } - eval($content); - - return ''; -} diff --git a/resources/smarty/plugins/function.getcatval.php b/resources/smarty/plugins/function.getcatval.php deleted file mode 100755 index 3c7fee49a..000000000 --- a/resources/smarty/plugins/function.getcatval.php +++ /dev/null @@ -1,34 +0,0 @@ -. - * - * @author niel - * @copyright 2016 nZEDb - */ -use App\Models\Category; - -/** - * Returns the value of the specified Category constant. - * - * @usage {getcatval category=BOOKS_COMICS} - * - * @param string $params Name of constant whose value to return. - * @return Value of the specified Category constant. - */ -function smarty_function_getcatval($params) -{ - return Category::getCategoryValue($params['category']); -} diff --git a/resources/smarty/plugins/function.html_options_multiple.php b/resources/smarty/plugins/function.html_options_multiple.php deleted file mode 100755 index b04cf61ec..000000000 --- a/resources/smarty/plugins/function.html_options_multiple.php +++ /dev/null @@ -1,185 +0,0 @@ - - * Name: html_options_multiple
- * Purpose: Prints the list of