mirror of
https://github.com/NNTmux/newznab-tmux.git
synced 2026-08-28 23:01:29 +00:00
Improve page load speed
This commit is contained in:
@@ -27,8 +27,14 @@ class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
Paginator::useTailwind();
|
||||
|
||||
// Share global data with all views using View Composer
|
||||
view()->composer('*', \App\View\Composers\GlobalDataComposer::class);
|
||||
// Share global data only with layouts and partials that need it
|
||||
// (avoids running queries for emails, components, and other minimal views)
|
||||
view()->composer([
|
||||
'layouts.main',
|
||||
'layouts.admin',
|
||||
'layouts.guest',
|
||||
'layouts.app',
|
||||
], \App\View\Composers\GlobalDataComposer::class);
|
||||
|
||||
Gate::define('viewPulse', function (User $user) {
|
||||
return $user->hasRole('Admin');
|
||||
|
||||
@@ -8,50 +8,62 @@ use App\Models\Content;
|
||||
use App\Models\Settings;
|
||||
use App\Models\User;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class GlobalDataComposer
|
||||
{
|
||||
/**
|
||||
* Cache TTL in seconds (5 minutes).
|
||||
*/
|
||||
private const CACHE_TTL = 300;
|
||||
|
||||
/**
|
||||
* Bind data to the view.
|
||||
*/
|
||||
public function compose(View $view): void
|
||||
{
|
||||
$viewName = $view->getName() ?? '';
|
||||
$isEmailView = str_starts_with($viewName, 'emails.');
|
||||
|
||||
// Load settings as array with type conversions (empty strings -> null, numeric strings -> numbers)
|
||||
$siteArray = Settings::query()
|
||||
->pluck('value', 'name')
|
||||
->map(fn ($value) => Settings::convertValue($value))
|
||||
->all();
|
||||
// Cached site settings (shared across all requests)
|
||||
$siteArray = Cache::remember('site_settings_array', self::CACHE_TTL, function () {
|
||||
return Settings::query()
|
||||
->pluck('value', 'name')
|
||||
->map(fn ($value) => Settings::convertValue($value))
|
||||
->all();
|
||||
});
|
||||
|
||||
$viewData = [
|
||||
'serverroot' => url('/'),
|
||||
'site' => $siteArray,
|
||||
];
|
||||
|
||||
// Email views expect $site to be the string provided by the mailable
|
||||
if (! $isEmailView) {
|
||||
$viewData['site'] = $siteArray; // Now it's a proper array, not a Settings model
|
||||
}
|
||||
|
||||
// Load useful links for sidebar
|
||||
$usefulLinks = Content::active()
|
||||
->ofType(Content::TYPE_USEFUL)
|
||||
->orderBy('ordinal')
|
||||
->get();
|
||||
$viewData['usefulLinks'] = $usefulLinks;
|
||||
// Cached useful links for sidebar
|
||||
$viewData['usefulLinks'] = Cache::remember('content_useful_links', self::CACHE_TTL, function () {
|
||||
return Content::active()
|
||||
->ofType(Content::TYPE_USEFUL)
|
||||
->orderBy('ordinal')
|
||||
->get();
|
||||
});
|
||||
|
||||
if (Auth::check()) {
|
||||
$userdata = User::find(Auth::id());
|
||||
$userdata->categoryexclusions = User::getCategoryExclusionById(Auth::id());
|
||||
$userId = Auth::id();
|
||||
|
||||
// Update last login every 15 mins.
|
||||
// User data with category exclusions (short cache per user)
|
||||
$userdata = Cache::remember('composer_user_'.$userId, self::CACHE_TTL, function () use ($userId) {
|
||||
$user = User::find($userId);
|
||||
$user->categoryexclusions = User::getCategoryExclusionById($userId);
|
||||
|
||||
return $user;
|
||||
});
|
||||
|
||||
// Update last login every 3 hours (outside of cache to avoid stale checks)
|
||||
if (now()->subHours(3) > $userdata->lastlogin) {
|
||||
event(new UserLoggedIn($userdata));
|
||||
}
|
||||
|
||||
$parentcatlist = Category::getForMenu($userdata->categoryexclusions);
|
||||
// Cached menu categories per user (depends on their exclusions)
|
||||
$parentcatlist = Cache::remember('menu_user_'.$userId, self::CACHE_TTL, function () use ($userdata) {
|
||||
return Category::getForMenu($userdata->categoryexclusions);
|
||||
});
|
||||
|
||||
$viewData = array_merge($viewData, [
|
||||
'userdata' => $userdata,
|
||||
|
||||
+13
-1
@@ -185,6 +185,18 @@
|
||||
"post-create-project-cmd": [
|
||||
"@php artisan key:generate --ansi"
|
||||
],
|
||||
"fix-style": "pint"
|
||||
"fix-style": "pint",
|
||||
"production-optimize": [
|
||||
"@php artisan config:cache",
|
||||
"@php artisan route:cache",
|
||||
"@php artisan view:cache",
|
||||
"@php artisan event:cache"
|
||||
],
|
||||
"production-clear": [
|
||||
"@php artisan config:clear",
|
||||
"@php artisan route:clear",
|
||||
"@php artisan view:clear",
|
||||
"@php artisan event:clear"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,8 +15,11 @@
|
||||
--radius-card: 0.75rem;
|
||||
}
|
||||
|
||||
/* Import FontAwesome */
|
||||
@import '@fortawesome/fontawesome-free/css/all.min.css';
|
||||
/* Import FontAwesome — only the weights we use (skips v4-compat font + shims) */
|
||||
@import '@fortawesome/fontawesome-free/css/fontawesome.min.css';
|
||||
@import '@fortawesome/fontawesome-free/css/solid.min.css';
|
||||
@import '@fortawesome/fontawesome-free/css/regular.min.css';
|
||||
@import '@fortawesome/fontawesome-free/css/brands.min.css';
|
||||
|
||||
/* Import custom CSP-safe styles */
|
||||
@import './csp-safe.css';
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
/**
|
||||
* Alpine.js CSP-safe initialization
|
||||
* Registers all stores and components, then starts Alpine.
|
||||
*
|
||||
* Core components are imported eagerly (used on every/most pages).
|
||||
* Page-specific and heavy components are lazy-loaded: only the modules
|
||||
* whose x-data names appear in the current page's DOM are fetched.
|
||||
* This keeps the initial JS bundle small and improves TTI.
|
||||
*/
|
||||
import Alpine from '@alpinejs/csp';
|
||||
|
||||
@@ -12,7 +16,7 @@ import './stores/theme.js';
|
||||
import './stores/toast.js';
|
||||
import './stores/cart.js';
|
||||
|
||||
// --- Core UI components ---
|
||||
// --- Core UI components (used on every/most pages) ---
|
||||
import './components/theme-toggle.js';
|
||||
import './components/back-to-top.js';
|
||||
import './components/dropdown.js';
|
||||
@@ -20,36 +24,12 @@ import './components/sidebar-toggle.js';
|
||||
import './components/admin-submenu.js';
|
||||
import './components/mobile-nav.js';
|
||||
import './components/password-toggle.js';
|
||||
|
||||
// --- Modal components ---
|
||||
import './components/confirm-modal.js';
|
||||
import './components/nfo-modal.js';
|
||||
import './components/image-modal.js';
|
||||
import './components/preview-modal.js';
|
||||
import './components/mediainfo-modal.js';
|
||||
import './components/filelist-modal.js';
|
||||
|
||||
// --- Feature components ---
|
||||
import './components/confirm-link.js';
|
||||
import './components/toast-notification.js';
|
||||
import './components/tab-switcher.js';
|
||||
import './components/cart-button.js';
|
||||
import './components/cart-page.js';
|
||||
import './components/search-autocomplete.js';
|
||||
import './components/quality-filter.js';
|
||||
import './components/content-toggle.js';
|
||||
import './components/movies-layout.js';
|
||||
import './components/movies-page.js';
|
||||
import './components/confirm-link.js';
|
||||
|
||||
// --- Page-specific components ---
|
||||
import './components/release-report.js';
|
||||
import './components/profile-edit.js';
|
||||
import './components/auth-page.js';
|
||||
import './components/toast-notification.js';
|
||||
|
||||
// --- Admin components ---
|
||||
import './components/admin/dashboard.js';
|
||||
import './components/admin/groups.js';
|
||||
import './components/admin/features.js';
|
||||
|
||||
// --- Shared utility (backward compat) ---
|
||||
window.escapeHtml = function(text) {
|
||||
@@ -57,5 +37,6 @@ window.escapeHtml = function(text) {
|
||||
return String(text).replace(/[&<>"']/g, m => map[m]);
|
||||
};
|
||||
|
||||
// Start Alpine
|
||||
Alpine.start();
|
||||
// --- Lazy-load page-specific components, then start Alpine ---
|
||||
import { loadAndStart } from './lazy-loader.js';
|
||||
loadAndStart();
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* Alpine.js CSP-safe lazy component loader.
|
||||
*
|
||||
* Scans the DOM for x-data attributes that match known lazy components.
|
||||
* Only imports the JS modules for components actually present on the page.
|
||||
* After all needed modules are loaded, calls Alpine.start().
|
||||
*
|
||||
* Dynamic import() is CSP-compliant (no eval / new Function).
|
||||
*/
|
||||
import Alpine from '@alpinejs/csp';
|
||||
|
||||
/**
|
||||
* Map of Alpine component name → dynamic import function.
|
||||
* Each module registers itself via Alpine.data() as a side effect.
|
||||
*/
|
||||
const lazyComponentMap = {
|
||||
// --- Modal components (only on release pages) ---
|
||||
'nfoModal': () => import('./components/nfo-modal.js'),
|
||||
'filelistModal': () => import('./components/filelist-modal.js'),
|
||||
'previewModal': () => import('./components/preview-modal.js'),
|
||||
'mediainfoModal': () => import('./components/mediainfo-modal.js'),
|
||||
'imageModal': () => import('./components/image-modal.js'),
|
||||
|
||||
// --- Page-specific components ---
|
||||
'moviesPage': () => import('./components/movies-page.js'),
|
||||
'moviesLayout': () => import('./components/movies-layout.js'),
|
||||
'qualityFilter': () => import('./components/quality-filter.js'),
|
||||
'contentToggle': () => import('./components/content-toggle.js'),
|
||||
'contentDelete': () => import('./components/content-toggle.js'), // same file
|
||||
'releaseReport': () => import('./components/release-report.js'),
|
||||
'adminReleaseReports': () => import('./components/release-report.js'), // same file
|
||||
'profileEdit': () => import('./components/profile-edit.js'),
|
||||
'profilePage': () => import('./components/profile-edit.js'), // same file
|
||||
'copyToClipboard': () => import('./components/profile-edit.js'), // same file
|
||||
'cartPage': () => import('./components/cart-page.js'),
|
||||
'authPage': () => import('./components/auth-page.js'),
|
||||
'otpInput': () => import('./components/auth-page.js'), // same file
|
||||
|
||||
// --- Admin components (includes Chart.js — heavy) ---
|
||||
'adminDashboard': () => import('./components/admin/dashboard.js'),
|
||||
'recentActivity': () => import('./components/admin/dashboard.js'), // same file
|
||||
'adminGroups': () => import('./components/admin/groups.js'),
|
||||
'adminFeatures': () => import('./components/admin/features.js'),
|
||||
};
|
||||
|
||||
/**
|
||||
* Extract Alpine component names from all x-data attributes in the DOM.
|
||||
* Handles: x-data="componentName", x-data="componentName()", x-data="{ ... }" (skipped).
|
||||
*/
|
||||
function getUsedComponentNames() {
|
||||
const names = new Set();
|
||||
document.querySelectorAll('[x-data]').forEach(el => {
|
||||
const raw = (el.getAttribute('x-data') || '').trim();
|
||||
// Skip inline objects like { show: true }
|
||||
if (!raw || raw.startsWith('{')) return;
|
||||
// Extract identifier before ( or whitespace
|
||||
const match = raw.match(/^([a-zA-Z_$][a-zA-Z0-9_$]*)/);
|
||||
if (match) names.add(match[1]);
|
||||
});
|
||||
return names;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load only the lazy components found on the current page, then start Alpine.
|
||||
*/
|
||||
export function loadAndStart() {
|
||||
const usedNames = getUsedComponentNames();
|
||||
const imports = new Set(); // deduplicate (multiple names → same file)
|
||||
|
||||
for (const name of usedNames) {
|
||||
if (lazyComponentMap[name]) {
|
||||
imports.add(lazyComponentMap[name]);
|
||||
}
|
||||
}
|
||||
|
||||
if (imports.size === 0) {
|
||||
Alpine.start();
|
||||
return;
|
||||
}
|
||||
|
||||
// Load all needed modules in parallel, then start Alpine
|
||||
Promise.all([...imports].map(fn => fn()))
|
||||
.then(() => Alpine.start())
|
||||
.catch(err => {
|
||||
console.error('[lazy-loader] Failed to load component:', err);
|
||||
// Start Alpine anyway so the page is usable
|
||||
Alpine.start();
|
||||
});
|
||||
}
|
||||
@@ -1,5 +1,9 @@
|
||||
@extends('layouts.main')
|
||||
|
||||
@push('modals')
|
||||
@include('partials.release-modals')
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm transition-colors duration-200">
|
||||
<!-- Breadcrumb -->
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
@extends('layouts.main')
|
||||
|
||||
@push('modals')
|
||||
@include('partials.release-modals')
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm">
|
||||
<!-- Breadcrumb -->
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
@extends('layouts.main')
|
||||
|
||||
@push('modals')
|
||||
@include('partials.release-modals')
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6">
|
||||
<div class="mb-6">
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
@extends('layouts.main')
|
||||
|
||||
@push('modals')
|
||||
@include('partials.release-modals')
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm">
|
||||
<!-- Breadcrumb -->
|
||||
|
||||
@@ -100,59 +100,27 @@
|
||||
@include('partials.back-to-top')
|
||||
|
||||
<!-- Theme Toggle -->
|
||||
@php $themePreference = auth()->check() ? (auth()->user()->theme_preference ?? 'light') : 'light'; @endphp
|
||||
<button id="theme-toggle" class="fixed z-50 bg-gray-200 dark:bg-gray-700 text-gray-800 dark:text-gray-200 px-4 py-3 rounded-full shadow-lg hover:bg-gray-300 dark:hover:bg-gray-600 transition-all duration-200 flex items-center gap-2 touch-target bottom-[max(1rem,env(safe-area-inset-bottom))] left-[max(1rem,env(safe-area-inset-left))]"
|
||||
title="{{ ucfirst(auth()->check() ? (auth()->user()->theme_preference ?? 'light') : 'light') }} Mode">
|
||||
<i id="theme-icon" class="fas
|
||||
@if(auth()->check())
|
||||
@if((auth()->user()->theme_preference ?? 'light') === 'dark')
|
||||
fa-moon
|
||||
@elseif((auth()->user()->theme_preference ?? 'light') === 'system')
|
||||
fa-desktop
|
||||
@else
|
||||
fa-sun
|
||||
@endif
|
||||
@else
|
||||
fa-sun
|
||||
@endif
|
||||
"></i>
|
||||
<span id="theme-label" class="text-xs font-medium hidden sm:inline">
|
||||
@if(auth()->check())
|
||||
{{ ucfirst(auth()->user()->theme_preference ?? 'light') }}
|
||||
@else
|
||||
Light
|
||||
@endif
|
||||
</span>
|
||||
title="{{ ucfirst($themePreference) }} Mode">
|
||||
<i id="theme-icon" class="fas {{ $themePreference === 'dark' ? 'fa-moon' : ($themePreference === 'system' ? 'fa-desktop' : 'fa-sun') }}"></i>
|
||||
<span id="theme-label" class="text-xs font-medium hidden sm:inline">{{ ucfirst($themePreference) }}</span>
|
||||
</button>
|
||||
|
||||
<!-- Confirmation Modal -->
|
||||
<!-- Confirmation Modal (used on many pages) -->
|
||||
@include('partials.confirmation-modal')
|
||||
|
||||
<!-- File List Modal -->
|
||||
@include('partials.filelist-modal')
|
||||
|
||||
<!-- NFO Modal -->
|
||||
@include('partials.nfo-modal')
|
||||
|
||||
<!-- Preview/Sample Image Modal -->
|
||||
@include('partials.preview-modal')
|
||||
|
||||
<!-- Media Info Modal -->
|
||||
@include('partials.mediainfo-modal')
|
||||
|
||||
<!-- Image Modal -->
|
||||
@include('partials.image-modal')
|
||||
|
||||
<!-- Shared Report Modal (single instance for all report-trigger buttons) -->
|
||||
@include('partials.report-modal')
|
||||
|
||||
<!-- Toast Notifications (Alpine.js CSP Safe) -->
|
||||
@include('partials.toast-notifications')
|
||||
|
||||
{{-- Release-specific modals: pushed by pages that show releases --}}
|
||||
@stack('modals')
|
||||
|
||||
@stack('scripts')
|
||||
|
||||
<!-- Theme Management Data (moved to csp-safe.js) -->
|
||||
<div id="current-theme-data"
|
||||
data-theme="{{ auth()->check() ? (auth()->user()->theme_preference ?? 'light') : 'light' }}"
|
||||
data-theme="{{ $themePreference }}"
|
||||
data-authenticated="{{ auth()->check() ? 'true' : 'false' }}"
|
||||
data-update-url="{{ route('profile.update-theme') }}"
|
||||
style="display: none;">
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
@extends('layouts.main')
|
||||
|
||||
@push('modals')
|
||||
@include('partials.release-modals')
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm" x-data="moviesPage" data-movie-layout="{{ $movie_layout ?? 2 }}">
|
||||
{{-- Breadcrumb --}}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
@extends('layouts.main')
|
||||
|
||||
@push('modals')
|
||||
@include('partials.release-modals')
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm">
|
||||
<!-- Breadcrumb -->
|
||||
|
||||
@@ -254,7 +254,7 @@
|
||||
<i class="fa fa-user fa-fw mr-2"></i>Profile
|
||||
</a>
|
||||
<a href="{{ route('logout') }}" data-logout class="block px-4 py-2 text-sm text-gray-300 hover:bg-gray-800 hover:text-white">
|
||||
<i class="fa fa-sign-out fa-fw mr-2"></i>Sign Out
|
||||
<i class="fas fa-sign-out-alt fa-fw mr-2"></i>Sign Out
|
||||
</a>
|
||||
<form id="logout-form" action="{{ route('logout') }}" method="POST" class="hidden">
|
||||
@csrf
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
{{-- Release-related modals: only include on pages that show releases --}}
|
||||
@include('partials.filelist-modal')
|
||||
@include('partials.nfo-modal')
|
||||
@include('partials.preview-modal')
|
||||
@include('partials.mediainfo-modal')
|
||||
@include('partials.image-modal')
|
||||
@include('partials.report-modal')
|
||||
@@ -3,7 +3,7 @@
|
||||
<div class="sidebar-section">
|
||||
<button class="sidebar-toggle w-full flex items-center justify-between px-4 py-3 text-white hover:bg-gray-800 rounded-lg transition" data-target="submenu1">
|
||||
<div class="flex items-center">
|
||||
<i class="fa fa-dashboard fa-fw mr-3"></i>
|
||||
<i class="fas fa-tachometer-alt fa-fw mr-3"></i>
|
||||
<span>Browse</span>
|
||||
</div>
|
||||
<i class="fas fa-chevron-down text-xs transition-transform"></i>
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
@extends('layouts.main')
|
||||
|
||||
@push('modals')
|
||||
@include('partials.release-modals')
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm p-6">
|
||||
<div class="mb-6">
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
@extends('layouts.main')
|
||||
|
||||
@push('modals')
|
||||
@include('partials.release-modals')
|
||||
@endpush
|
||||
|
||||
@section('content')
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm">
|
||||
<!-- Breadcrumb -->
|
||||
|
||||
Reference in New Issue
Block a user