From ed67cea4cb5161d13f8ae1f84b140297a4c265e5 Mon Sep 17 00:00:00 2001 From: DariusIII Date: Tue, 3 Mar 2026 15:50:36 +0100 Subject: [PATCH] Add support for multiple color schemes --- app/Http/Controllers/ProfileController.php | 26 +- ...120000_add_color_scheme_to_users_table.php | 28 +++ resources/css/app.css | 228 +++++++++++++++++- .../js/alpine/components/theme-toggle.js | 16 ++ resources/js/alpine/stores/theme.js | 52 +++- resources/views/api/apidesc.blade.php | 60 ++--- resources/views/api/apiv2desc.blade.php | 70 +++--- resources/views/auth/login.blade.php | 4 +- resources/views/browse/index.blade.php | 42 ++-- resources/views/cart/index.blade.php | 32 +-- resources/views/contact/index.blade.php | 14 +- resources/views/content/index.blade.php | 10 +- resources/views/details/index.blade.php | 88 +++---- resources/views/invitations/create.blade.php | 2 +- resources/views/invitations/show.blade.php | 10 +- resources/views/layouts/main.blade.php | 7 +- resources/views/movies/index.blade.php | 18 +- .../movies/partials/movie-card.blade.php | 10 +- resources/views/movies/trending.blade.php | 30 +-- resources/views/partials/footer.blade.php | 2 +- .../views/partials/header-menu.blade.php | 66 +++-- resources/views/partials/theme-init.blade.php | 7 +- resources/views/profile/edit.blade.php | 108 ++++++--- resources/views/profile/index.blade.php | 62 ++--- resources/views/rss/rssdesc.blade.php | 110 ++++----- resources/views/search/index.blade.php | 60 ++--- resources/views/series/trending.blade.php | 30 +-- .../views/series/viewserieslist.blade.php | 32 +-- 28 files changed, 811 insertions(+), 413 deletions(-) create mode 100644 database/migrations/2026_03_03_120000_add_color_scheme_to_users_table.php diff --git a/app/Http/Controllers/ProfileController.php b/app/Http/Controllers/ProfileController.php index 25f7d3896..06b55a05e 100644 --- a/app/Http/Controllers/ProfileController.php +++ b/app/Http/Controllers/ProfileController.php @@ -165,6 +165,14 @@ class ProfileController extends BasePageController } } + // Update color scheme + if ($request->has('color_scheme')) { + $schemeValue = $request->input('color_scheme'); + if (in_array($schemeValue, ['blue', 'emerald', 'violet'], true)) { + User::where('id', $userid)->update(['color_scheme' => $schemeValue]); + } + } + // Update timezone preference if ($request->has('timezone')) { $timezoneValue = $request->input('timezone'); @@ -346,7 +354,7 @@ class ProfileController extends BasePageController } /** - * Update user's dark mode preference + * Update user's theme preference (light/dark/system) and/or color scheme (blue/emerald/violet). */ public function updateTheme(Request $request): mixed { @@ -357,16 +365,26 @@ class ProfileController extends BasePageController } $validator = Validator::make($request->all(), [ - 'theme_preference' => ['required', 'in:light,dark,system'], + 'theme_preference' => ['sometimes', 'in:light,dark,system'], + 'color_scheme' => ['sometimes', 'in:blue,emerald,violet'], ]); if ($validator->fails()) { return response()->json(['success' => false, 'message' => 'Invalid input'], 400); } - $user->theme_preference = $request->input('theme_preference'); + if ($request->has('theme_preference')) { + $user->theme_preference = $request->input('theme_preference'); + } + if ($request->has('color_scheme')) { + $user->color_scheme = $request->input('color_scheme'); + } $user->save(); - return response()->json(['success' => true, 'theme_preference' => $user->theme_preference]); + return response()->json([ + 'success' => true, + 'theme_preference' => $user->theme_preference, + 'color_scheme' => $user->color_scheme, + ]); } } diff --git a/database/migrations/2026_03_03_120000_add_color_scheme_to_users_table.php b/database/migrations/2026_03_03_120000_add_color_scheme_to_users_table.php new file mode 100644 index 000000000..bdd015f7f --- /dev/null +++ b/database/migrations/2026_03_03_120000_add_color_scheme_to_users_table.php @@ -0,0 +1,28 @@ +string('color_scheme', 20)->default('blue')->after('theme_preference'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('users', function (Blueprint $table) { + $table->dropColumn('color_scheme'); + }); + } +}; diff --git a/resources/css/app.css b/resources/css/app.css index 094f736d1..f77a26ba5 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -9,10 +9,121 @@ /* Dark mode: class-based strategy (replaces darkMode: 'class' in tailwind.config.js) */ @custom-variant dark (&:where(.dark, .dark *)); +/* Color scheme: surface + accent variables. data-color-scheme set on . */ +:root, +[data-color-scheme="blue"] { + /* Surface - Blue/Slate */ + --surface-body: #f8fafc; + --surface-body-dark: #0f172a; + --surface-sidebar: #1e293b; + --surface-sidebar-dark: #020617; + --surface-header: #1e293b; + --surface-header-dark: #020617; + --surface-card: #ffffff; + --surface-card-dark: #1e293b; + --surface-dropdown: #0f172a; + --surface-dropdown-dark: #020617; + --surface-footer: #f1f5f9; + --surface-footer-dark: #1e293b; + --surface-hover: #334155; + --surface-hover-dark: #1e293b; + --border-default: #e2e8f0; + --border-default-dark: #334155; + --text-muted: #64748b; + --text-muted-dark: #94a3b8; + /* Accent - Blue */ + --color-primary-50: #eff6ff; + --color-primary-100: #dbeafe; + --color-primary-200: #bfdbfe; + --color-primary-300: #93c5fd; + --color-primary-400: #60a5fa; + --color-primary-500: #3b82f6; + --color-primary-600: #2563eb; + --color-primary-700: #1d4ed8; + --color-primary-800: #1e40af; + --color-primary-900: #1e3a8a; + --color-primary-950: #172554; +} + +[data-color-scheme="emerald"] { + --surface-body: #f0fdf4; + --surface-body-dark: #071a12; + --surface-sidebar: #14332a; + --surface-sidebar-dark: #020c09; + --surface-header: #14332a; + --surface-header-dark: #020c09; + --surface-card: #ffffff; + --surface-card-dark: #0f2e24; + --surface-dropdown: #0d281f; + --surface-dropdown-dark: #020c09; + --surface-footer: #ecfdf5; + --surface-footer-dark: #14332a; + --surface-hover: #166534; + --surface-hover-dark: #14532d; + --border-default: #bbf7d0; + --border-default-dark: #14532d; + --text-muted: #047857; + --text-muted-dark: #6ee7b7; + --color-primary-50: #ecfdf5; + --color-primary-100: #d1fae5; + --color-primary-200: #a7f3d0; + --color-primary-300: #6ee7b7; + --color-primary-400: #34d399; + --color-primary-500: #10b981; + --color-primary-600: #059669; + --color-primary-700: #047857; + --color-primary-800: #065f46; + --color-primary-900: #064e3b; + --color-primary-950: #022c22; +} + +[data-color-scheme="violet"] { + --surface-body: #faf5ff; + --surface-body-dark: #120b20; + --surface-sidebar: #2d1f4e; + --surface-sidebar-dark: #0f0620; + --surface-header: #2d1f4e; + --surface-header-dark: #0f0620; + --surface-card: #ffffff; + --surface-card-dark: #1e1b4b; + --surface-dropdown: #1e1b4b; + --surface-dropdown-dark: #0f0620; + --surface-footer: #f5f3ff; + --surface-footer-dark: #2d1f4e; + --surface-hover: #5b21b6; + --surface-hover-dark: #3b0764; + --border-default: #e9d5ff; + --border-default-dark: #6b21a8; + --text-muted: #6d28d9; + --text-muted-dark: #c4b5fd; + --color-primary-50: #f5f3ff; + --color-primary-100: #ede9fe; + --color-primary-200: #ddd6fe; + --color-primary-300: #c4b5fd; + --color-primary-400: #a78bfa; + --color-primary-500: #8b5cf6; + --color-primary-600: #7c3aed; + --color-primary-700: #6d28d9; + --color-primary-800: #5b21b6; + --color-primary-900: #4c1d95; + --color-primary-950: #2e1065; +} + /* Theme customization (replaces theme.extend in tailwind.config.js) */ @theme { --font-sans: "Figtree", ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; --radius-card: 0.75rem; + --color-primary-50: var(--color-primary-50); + --color-primary-100: var(--color-primary-100); + --color-primary-200: var(--color-primary-200); + --color-primary-300: var(--color-primary-300); + --color-primary-400: var(--color-primary-400); + --color-primary-500: var(--color-primary-500); + --color-primary-600: var(--color-primary-600); + --color-primary-700: var(--color-primary-700); + --color-primary-800: var(--color-primary-800); + --color-primary-900: var(--color-primary-900); + --color-primary-950: var(--color-primary-950); } /* Import FontAwesome — only the weights we use (skips v4-compat font + shims) */ @@ -35,6 +146,43 @@ html, body { overflow: hidden; } +/* Structural surfaces: color scheme variables applied to main chrome */ +body { + background-color: var(--surface-body) !important; +} +.dark body { + background-color: var(--surface-body-dark) !important; +} +#sidebar { + background-color: var(--surface-sidebar) !important; +} +.dark #sidebar { + background-color: var(--surface-sidebar-dark) !important; +} +/* Header bar and the nav inside it - .surface-header raises specificity so we beat Tailwind bg utilities */ +header.surface-header, +header.surface-header nav { + background-color: var(--surface-header) !important; +} +.dark header.surface-header, +.dark header.surface-header nav { + background-color: var(--surface-header-dark) !important; +} +.dropdown-menu { + background-color: var(--surface-dropdown) !important; +} +.dark .dropdown-menu { + background-color: var(--surface-dropdown-dark) !important; +} +footer { + background-color: var(--surface-footer) !important; + border-color: var(--border-default) !important; +} +.dark footer { + background-color: var(--surface-footer-dark) !important; + border-color: var(--border-default-dark) !important; +} + /* Dark Mode Text Readability Fixes */ @layer base { /* Ensure dark backgrounds have light text */ @@ -88,9 +236,83 @@ html, body { } } -/* Page-level card container (rounded-xl, shadow) */ +/* Page-level card container (rounded-xl, shadow) - !important so scheme wins over Tailwind utilities */ .card { - @apply bg-white dark:bg-gray-800 rounded-xl shadow-md border border-gray-200 dark:border-gray-700 transition-colors duration-200; + background-color: var(--surface-card) !important; + border: 1px solid var(--border-default) !important; + @apply rounded-xl shadow-md transition-colors duration-200; +} +.dark .card { + background-color: var(--surface-card-dark) !important; + border-color: var(--border-default-dark) !important; +} + +/* Content panels (welcome card, movies wrapper, profile wrapper, etc.) - same surface as cards */ +.surface-panel { + background-color: var(--surface-card) !important; + border-color: var(--border-default); +} +.surface-panel.border { + border-width: 1px; + border-style: solid; +} +.dark .surface-panel { + background-color: var(--surface-card-dark) !important; + border-color: var(--border-default-dark); +} +/* Sub-sections (filter bars, toolbars) - use footer surface for slight contrast */ +.surface-panel-alt { + background-color: var(--surface-footer) !important; + border-color: var(--border-default); +} +.dark .surface-panel-alt { + background-color: var(--surface-footer-dark) !important; + border-color: var(--border-default-dark); +} +.surface-panel-alt.border-b, +.surface-panel-alt.border-t { + border-color: var(--border-default); +} +.dark .surface-panel-alt.border-b, +.dark .surface-panel-alt.border-t { + border-color: var(--border-default-dark); +} + +/* Tables in main content - use scheme surfaces so they match sidebar/header/cards */ +main table { + background-color: var(--surface-card) !important; +} +.dark main table { + background-color: var(--surface-card-dark) !important; +} +main thead th { + background-color: var(--surface-dropdown) !important; + border-bottom-color: var(--border-default) !important; +} +.dark main thead th { + background-color: var(--surface-dropdown-dark) !important; + border-bottom-color: var(--border-default-dark) !important; +} +/* Table body and every row use scheme surface (override Tailwind bg-white / dark:bg-gray-800) */ +main tbody { + background-color: var(--surface-card) !important; +} +.dark main tbody { + background-color: var(--surface-card-dark) !important; +} +main tbody tr { + background-color: var(--surface-card) !important; + border-bottom-color: var(--border-default); +} +.dark main tbody tr { + background-color: var(--surface-card-dark) !important; + border-bottom-color: var(--border-default-dark); +} +main tbody tr:hover { + background-color: var(--surface-hover) !important; +} +.dark main tbody tr:hover { + background-color: var(--surface-hover-dark) !important; } /* Custom styles for proper spacing and button alignment */ @@ -103,7 +325,7 @@ html, body { } .btn-primary { - @apply bg-blue-600 dark:bg-blue-700 text-white border-blue-600 dark:border-blue-700 hover:bg-blue-700 dark:hover:bg-blue-800; + @apply bg-primary-600 text-white border border-primary-600 hover:bg-primary-700 dark:bg-primary-700 dark:border-primary-700 dark:hover:bg-primary-800; } .btn-secondary { diff --git a/resources/js/alpine/components/theme-toggle.js b/resources/js/alpine/components/theme-toggle.js index 1e95705b0..79aa57470 100644 --- a/resources/js/alpine/components/theme-toggle.js +++ b/resources/js/alpine/components/theme-toggle.js @@ -61,4 +61,20 @@ Alpine.data('themeRadio', () => ({ } }); }); + + // Color scheme swatch buttons (dropdown + mobile) + document.body.addEventListener('click', function(e) { + var btn = e.target.closest('.dropdown-scheme-btn, .mobile-scheme-btn'); + if (!btn || !btn.dataset.scheme) return; + e.preventDefault(); + e.stopPropagation(); + Alpine.store('theme').setScheme(btn.dataset.scheme); + }); + + // Profile edit page: live preview when color scheme radio changes + document.body.addEventListener('change', function(e) { + var radio = e.target.closest('input.profile-scheme-radio'); + if (!radio || !radio.dataset.scheme) return; + Alpine.store('theme').setScheme(radio.dataset.scheme); + }); })(); diff --git a/resources/js/alpine/stores/theme.js b/resources/js/alpine/stores/theme.js index 01e17ed7f..606e19d92 100644 --- a/resources/js/alpine/stores/theme.js +++ b/resources/js/alpine/stores/theme.js @@ -8,6 +8,7 @@ const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); Alpine.store('theme', { current: 'light', + colorScheme: 'blue', init() { const meta = document.querySelector('meta[name="theme-preference"]'); @@ -16,10 +17,36 @@ Alpine.store('theme', { ? (meta ? meta.content : 'light') : (localStorage.getItem('theme') || 'light'); + const schemeMeta = document.querySelector('meta[name="color-scheme-preference"]'); + const schemeData = document.getElementById('current-theme-data'); + this.colorScheme = (isAuth && isAuth.content === 'true') + ? (schemeMeta ? schemeMeta.content : (schemeData?.dataset?.colorScheme || 'blue')) + : (localStorage.getItem('color_scheme') || 'blue'); + this.apply(); + this.applyScheme(); this._listenOS(); }, + /** Set and persist color scheme (blue, emerald, violet) */ + setScheme(scheme) { + if (!['blue', 'emerald', 'violet'].includes(scheme)) return; + this.colorScheme = scheme; + this.applyScheme(); + this._save(); + }, + + /** Apply color scheme to and meta */ + applyScheme() { + const html = document.documentElement; + html.setAttribute('data-color-scheme', this.colorScheme); + const meta = document.querySelector('meta[name="color-scheme-preference"]'); + if (meta) meta.content = this.colorScheme; + const dataEl = document.getElementById('current-theme-data'); + if (dataEl) dataEl.dataset.colorScheme = this.colorScheme; + this._updateUI(); + }, + /** Cycle light -> dark -> system -> light */ cycle() { const next = this.current === 'light' ? 'dark' @@ -31,7 +58,7 @@ Alpine.store('theme', { set(theme) { this.current = theme; this.apply(); - this._save(theme); + this._save(); }, /** Apply the current theme to */ @@ -71,9 +98,9 @@ Alpine.store('theme', { var self = this; document.querySelectorAll('.dropdown-theme-btn, .mobile-theme-btn').forEach(function(btn) { var isActive = btn.dataset.theme === self.current; - btn.classList.remove('bg-blue-600', 'text-white', 'text-gray-300', 'hover:bg-gray-800', 'hover:bg-gray-700', 'hover:text-white'); + btn.classList.remove('bg-primary-600', 'bg-blue-600', 'text-white', 'text-gray-300', 'hover:bg-gray-800', 'hover:bg-gray-700', 'hover:text-white'); if (isActive) { - btn.classList.add('bg-blue-600', 'text-white'); + btn.classList.add('bg-primary-600', 'text-white'); } else { btn.classList.add('text-gray-300', 'hover:text-white'); if (btn.classList.contains('mobile-theme-btn')) { @@ -83,6 +110,15 @@ Alpine.store('theme', { } } }); + + // Update color scheme swatch buttons + document.querySelectorAll('.dropdown-scheme-btn, .mobile-scheme-btn').forEach(function(btn) { + var isActive = btn.dataset.scheme === self.colorScheme; + btn.classList.remove('ring-2', 'ring-offset-2', 'ring-offset-gray-900', 'ring-primary-500', 'ring-white'); + if (isActive) { + btn.classList.add('ring-2', 'ring-offset-2', 'ring-offset-gray-900', 'ring-primary-500', 'dark:ring-offset-gray-950'); + } + }); }, icon() { @@ -107,20 +143,22 @@ Alpine.store('theme', { }); }, - /** Persist to server (authenticated) or localStorage (guest) */ - _save(theme) { + /** Persist theme and color scheme to server (authenticated) or localStorage (guest) */ + _save() { const csrf = document.querySelector('meta[name="csrf-token"]')?.content; const url = document.querySelector('meta[name="update-theme-url"]')?.content; const isAuth = document.querySelector('meta[name="user-authenticated"]'); + const payload = { theme_preference: this.current, color_scheme: this.colorScheme }; if (isAuth && isAuth.content === 'true' && url && csrf) { fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': csrf }, - body: JSON.stringify({ theme_preference: theme }) + body: JSON.stringify(payload) }).catch(err => console.error('Error saving theme:', err)); } else { - localStorage.setItem('theme', theme); + localStorage.setItem('theme', this.current); + localStorage.setItem('color_scheme', this.colorScheme); } } }); diff --git a/resources/views/api/apidesc.blade.php b/resources/views/api/apidesc.blade.php index b44bf0b04..5d1cf5146 100644 --- a/resources/views/api/apidesc.blade.php +++ b/resources/views/api/apidesc.blade.php @@ -1,9 +1,9 @@ @extends('layouts.main') @section('content') -
-
-

- {{ $title }} +
+
+

+ {{ $title }}

@@ -12,13 +12,13 @@ logged in users, or by providing an API key.

@auth -
+

Your API Credentials

- -
@@ -50,9 +50,9 @@
- + - ?t=caps + ?t=caps @@ -64,7 +64,7 @@ a comma separated list of categories.
- OPTIONS + OPTIONS
extended=1 - Return extended information in results
@@ -73,13 +73,13 @@
@auth - + - ?t=search&q=linux + ?t=search&q=linux - + - ?t=search&cat={{ $catClass::GAME_ROOT }},{{ $catClass::MOVIE_ROOT }} + ?t=search&cat={{ $catClass::GAME_ROOT }},{{ $catClass::MOVIE_ROOT }} @else @@ -99,7 +99,7 @@ Returns a list of NZBs matching a query, category, or TV ID. Filter by season, episode, or various database IDs.
- ID OPTIONS + ID OPTIONS
rid=25056 - TVRage
tvdbid=153021 - TVDB
@@ -112,9 +112,9 @@ @auth - + - ?t=tvsearch&q=law and order&season=7&ep=12 + ?t=tvsearch&q=law and order&season=7&ep=12 @else @@ -130,7 +130,7 @@ Returns a list of NZBs matching a query, an IMDB ID and optionally a category.
- OPTIONS + OPTIONS
extended=1 - Return extended information in results
@@ -138,9 +138,9 @@ @auth - + - ?t=movie&imdbid=1418646 + ?t=movie&imdbid=1418646 @else @@ -154,9 +154,9 @@ Returns detailed information about an NZB. @auth - + - ?t=details&id=9ca52909ba9b9e5e6758d815fef4ecda + ?t=details&id=9ca52909ba9b9e5e6758d815fef4ecda @else @@ -174,9 +174,9 @@ @auth - + - ?t=info&id=9ca52909ba9b9e5e6758d815fef4ecda + ?t=info&id=9ca52909ba9b9e5e6758d815fef4ecda @else @@ -190,9 +190,9 @@ Downloads the NZB file associated with an ID. @auth - + - ?t=get&id=9ca52909ba9b9e5e6758d815fef4ecda + ?t=get&id=9ca52909ba9b9e5e6758d815fef4ecda @else @@ -209,19 +209,19 @@

Select your preferred output format (not applicable to functions which return an NZB/NFO file).

-
+
- XML (default) + XML (default)

Returns the data in an XML document.

?t=search&q=linux&o=xml
-
+
- JSON + JSON

Returns the data in a JSON object.

?t=search&q=linux&o=json diff --git a/resources/views/api/apiv2desc.blade.php b/resources/views/api/apiv2desc.blade.php index a1c4feb29..4aea2190f 100644 --- a/resources/views/api/apiv2desc.blade.php +++ b/resources/views/api/apiv2desc.blade.php @@ -1,10 +1,10 @@ @extends('layouts.main') @section('content') -
-
-

- {{ $title }} +
+
+

+ {{ $title }}

@@ -13,20 +13,20 @@

@auth -
-

+
+

Your API Credentials

- -
@endauth -

+

Available Functions

@@ -52,9 +52,9 @@
- + - capabilities + capabilities @@ -69,13 +69,13 @@
@auth - + - search?id=linux + search?id=linux - + - search?cat={{ $catClass::GAME_ROOT }},{{ $catClass::MOVIE_ROOT }} + search?cat={{ $catClass::GAME_ROOT }},{{ $catClass::MOVIE_ROOT }} @else @@ -95,7 +95,7 @@ Returns a list of NZBs matching a query, category, TVRageID, season or episode.
- ID OPTIONS + ID OPTIONS
rid=25056 - TVRage
tvdbid=153021 - TVDB
@@ -109,13 +109,13 @@
@auth - + - tv?id=law and order&season=7&ep=12 + tv?id=law and order&season=7&ep=12 - + - tv?rid=2204&cat={{ $catClass::GAME_ROOT }},{{ $catClass::MOVIE_ROOT }} + tv?rid=2204&cat={{ $catClass::GAME_ROOT }},{{ $catClass::MOVIE_ROOT }} @else @@ -135,7 +135,7 @@ Returns a list of NZBs matching a query, an ID (IMDB, TMDB, or Trakt) and optionally a category.
- ID OPTIONS + ID OPTIONS
imdbid=1418646 - IMDB
tmdbid=43418 - TMDB
@@ -146,13 +146,13 @@ -

+

Output Format

-
+
- +
-
JSON Format
+
JSON Format

All information is returned in JSON format.

-
- +
+ Note: When using these API endpoints in your applications, always send your API token with each request.
diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php index f7079adb7..3c0e884ec 100644 --- a/resources/views/auth/login.blade.php +++ b/resources/views/auth/login.blade.php @@ -154,13 +154,13 @@
@if(Route::has('register')) - + Create an account @endif @if(Route::has('contact-us')) - + Contact Us @endif diff --git a/resources/views/browse/index.blade.php b/resources/views/browse/index.blade.php index a8e812eec..5df87b0b5 100644 --- a/resources/views/browse/index.blade.php +++ b/resources/views/browse/index.blade.php @@ -5,13 +5,13 @@ @endpush @section('content') -
+