From 09efead104b1079cf61fc555a1c7c937d5c49532 Mon Sep 17 00:00:00 2001 From: 4gray Date: Thu, 26 Feb 2026 00:45:58 +0100 Subject: [PATCH] style: enhance UI components with improved styles, search functionality, and error handling --- .../grid-list/grid-list.component.scss | 5 +- .../src/app/shared/styles/_content-grid.scss | 4 +- .../workspace/workspace-shell.component.html | 4 +- .../live-stream-layout.component.html | 43 +++-- .../portal-channels-list.component.html | 165 +++++++++------- .../portal-channels-list.component.scss | 179 +++++++++++++++--- .../search-results.component.scss | 3 +- .../season-container.component.scss | 6 +- apps/web/src/assets/i18n/en.json | 1 + apps/web/src/nav-list.scss | 1 + .../content-hero/content-hero.component.html | 44 +++-- .../content-hero/content-hero.component.scss | 30 ++- .../content-hero/content-hero.component.ts | 43 ++++- .../live-stream-layout.component.html | 15 +- .../live-stream-layout.component.scss | 95 +++++++++- 15 files changed, 482 insertions(+), 156 deletions(-) diff --git a/apps/web/src/app/shared/components/grid-list/grid-list.component.scss b/apps/web/src/app/shared/components/grid-list/grid-list.component.scss index 8548abcc9..7a8844163 100644 --- a/apps/web/src/app/shared/components/grid-list/grid-list.component.scss +++ b/apps/web/src/app/shared/components/grid-list/grid-list.component.scss @@ -88,8 +88,8 @@ mat-card:hover { position: absolute; top: 6px; right: 6px; - // Use a semi-transparent dark pill, not hardcoded green - background: rgba(0, 0, 0, 0.65); + // Use a solid or highly opaque dark pill to avoid expensive backdrop-filter + background: rgba(0, 0, 0, 0.85); color: #4ade80; // green-400, readable across themes font-size: 0.72rem; font-weight: 600; @@ -99,7 +99,6 @@ mat-card:hover { align-items: center; padding: 2px 5px; gap: 2px; - backdrop-filter: blur(4px); mat-icon { font-size: 13px; diff --git a/apps/web/src/app/shared/styles/_content-grid.scss b/apps/web/src/app/shared/styles/_content-grid.scss index eff0a43ba..4f1e7388c 100644 --- a/apps/web/src/app/shared/styles/_content-grid.scss +++ b/apps/web/src/app/shared/styles/_content-grid.scss @@ -60,9 +60,7 @@ font-weight: 700; text-transform: uppercase; letter-spacing: 0.5px; - backdrop-filter: blur(8px); - -webkit-backdrop-filter: blur(8px); - background: rgba(0, 0, 0, 0.6); + background: rgba(0, 0, 0, 0.85); color: #fff; &.live { diff --git a/apps/web/src/app/workspace/workspace-shell.component.html b/apps/web/src/app/workspace/workspace-shell.component.html index 4f1374610..0857b569b 100644 --- a/apps/web/src/app/workspace/workspace-shell.component.html +++ b/apps/web/src/app/workspace/workspace-shell.component.html @@ -131,7 +131,7 @@ {{ action.icon }} } - + --> @if (canSort()) { - - - @@ -95,10 +112,12 @@ } @else {
- - {{ 'PORTALS.SELECT_CATEGORY' | translate }} + @if (!selectedCategoryId()) { + + {{ 'PORTALS.SELECT_CATEGORY' | translate }} + }
} diff --git a/apps/web/src/app/xtream-electron/portal-channels-list/portal-channels-list.component.html b/apps/web/src/app/xtream-electron/portal-channels-list/portal-channels-list.component.html index 9534e50fd..ba5e509c1 100644 --- a/apps/web/src/app/xtream-electron/portal-channels-list/portal-channels-list.component.html +++ b/apps/web/src/app/xtream-electron/portal-channels-list/portal-channels-list.component.html @@ -1,84 +1,111 @@
- - search + +
- -
- channel icon -
-
{{ item.title }}
- @if (currentPrograms.get(item.xtream_id)) { -
-
- {{ currentPrograms.get(item.xtream_id) }} -
- @if (currentProgramsProgress.has(item.xtream_id)) { -
- {{ - programTimings.get(item.xtream_id)?.start - | date: 'HH:mm' - }} -
-
+ @if ( + sortedChannels() | filterBy: searchString() : 'title'; + as filteredChannels + ) { + @if (filteredChannels.length > 0) { + +
+ channel icon +
+
{{ item.title }}
+ @if (currentPrograms.get(item.xtream_id)) { +
+
+ {{ currentPrograms.get(item.xtream_id) }} +
+ @if ( + currentProgramsProgress.has(item.xtream_id) + ) { +
+ {{ + programTimings.get(item.xtream_id) + ?.start | date: 'HH:mm' + }} +
+
+
+ {{ + programTimings.get(item.xtream_id) + ?.end | date: 'HH:mm' + }} +
+ } +
+ } @else { +
+
+
+ --:-- +
+ --:--
- {{ - programTimings.get(item.xtream_id)?.end - | date: 'HH:mm' - }}
}
- } @else { -
-
-
- --:-- -
- --:-- -
-
- } + +
+
+ } @else { +
+
+ search_off +
+

{{ 'CHANNELS.NO_CHANNELS_FOUND' | translate }}

+

We couldn't find anything matching "{{ searchString() }}"

- -
- + } + }
diff --git a/apps/web/src/app/xtream-electron/portal-channels-list/portal-channels-list.component.scss b/apps/web/src/app/xtream-electron/portal-channels-list/portal-channels-list.component.scss index 2eb44479b..839daf87d 100644 --- a/apps/web/src/app/xtream-electron/portal-channels-list/portal-channels-list.component.scss +++ b/apps/web/src/app/xtream-electron/portal-channels-list/portal-channels-list.component.scss @@ -32,32 +32,68 @@ .channel-item { display: flex; align-items: center; - padding: 8px; + position: relative; + padding: 8px 8px 8px 12px; margin: 2px 4px; - border-radius: 4px; + border-radius: 10px; + border: 1px solid transparent; cursor: pointer; - transition: background-color 0.2s ease; + background: transparent; + color: var(--mat-sys-on-surface-variant); height: 64px; box-sizing: border-box; gap: 8px; - // Critical: use margin-based calc and ensure it doesn't overflow width: calc(100% - 8px); - min-width: 0; // Allow shrinking below content size - overflow: hidden; + min-width: 0; + transition: + background 0.15s ease, + color 0.15s ease, + border-color 0.15s ease, + box-shadow 0.18s ease, + transform 0.18s ease; &:hover { - background-color: color-mix(in srgb, var(--mat-sys-on-surface) 8%, transparent); + background: var(--mat-sys-surface-container); + color: var(--mat-sys-on-surface); .title { - color: var(--mat-sys-primary); + color: var(--mat-sys-on-surface); } } &.active { - background-color: color-mix(in srgb, var(--mat-sys-primary) 16%, transparent); + background: linear-gradient( + 135deg, + var(--app-selection-surface-strong), + var(--app-selection-surface) + ) !important; + color: var(--app-selection-color) !important; + border-color: var(--app-selection-border) !important; + font-weight: 600; + box-shadow: + inset 0 0 0 1px var(--app-selection-border), + 0 12px 20px -18px var(--app-selection-glow); + transform: translateY(-1px); - .title { - color: var(--mat-sys-primary); + * { + color: var(--app-selection-color) !important; + } + + .current-program, + .progress-container { + opacity: 0.92; + } + + &::before { + content: ''; + position: absolute; + inset-inline-start: 6px; + top: 7px; + bottom: 7px; + width: 3px; + border-radius: 999px; + background: var(--app-selection-color); + box-shadow: 0 0 10px var(--app-selection-glow); } } } @@ -162,26 +198,63 @@ } .search-panel { - padding: 8px 12px; + padding: 10px 12px; flex-shrink: 0; height: 56px; box-sizing: border-box; + display: flex; + align-items: center; } -.search-bar { +.search-field { + flex: 1; width: 100%; - @include mat.form-field-density(-5); -} + height: 36px; + border-radius: 10px; + border: 1px solid var(--app-search-border, var(--mat-sys-outline-variant)); + background: var(--app-search-bg, var(--mat-sys-surface-container)); + display: flex; + align-items: center; + gap: 8px; + padding: 0 12px; + box-sizing: border-box; + cursor: text; + transition: + border-color 0.15s ease, + background-color 0.15s ease, + box-shadow 0.15s ease; -::ng-deep { - .search-bar { - div { - border-radius: 20px; - } + &:focus-within { + border-color: var(--mat-sys-primary); + box-shadow: 0 0 0 3px rgba(var(--mat-sys-primary), 0.12); } - div[matformfieldlineripple] { - display: none; + mat-icon { + font-size: 18px; + width: 18px; + height: 18px; + color: var(--mat-sys-on-surface-variant); + flex-shrink: 0; + } + + input { + flex: 1; + border: none; + outline: none; + background: transparent; + color: var(--mat-sys-on-surface); + font: inherit; + font-size: 0.875rem; + + &:disabled { + cursor: not-allowed; + opacity: 0.5; + } + + &::placeholder { + color: var(--mat-sys-on-surface-variant); + opacity: 0.7; + } } } @@ -236,3 +309,65 @@ height: 48px; } } + +// ─── Empty State ────────────────────────────────────────────────────────────── +.empty-search-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + padding: 32px 24px; + text-align: center; + color: var(--mat-sys-on-surface-variant); + animation: fadeIn 0.3s ease-out; + + .empty-icon-wrapper { + width: 64px; + height: 64px; + border-radius: 20px; + background: var(--mat-sys-surface-container); + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 20px; + box-shadow: + 0 8px 24px -8px rgba(0, 0, 0, 0.1), + inset 0 1px 1px rgba(255, 255, 255, 0.05); + + mat-icon { + font-size: 32px; + width: 32px; + height: 32px; + color: var(--mat-sys-on-surface-variant); + opacity: 0.8; + } + } + + h3 { + font-size: 1.125rem; + font-weight: 600; + margin: 0 0 8px 0; + color: var(--mat-sys-on-surface); + letter-spacing: -0.01em; + } + + p { + font-size: 0.875rem; + margin: 0; + opacity: 0.7; + line-height: 1.4; + max-width: 260px; + } +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} diff --git a/apps/web/src/app/xtream-electron/search-results/search-results.component.scss b/apps/web/src/app/xtream-electron/search-results/search-results.component.scss index 149ec9c03..3ac08fca5 100644 --- a/apps/web/src/app/xtream-electron/search-results/search-results.component.scss +++ b/apps/web/src/app/xtream-electron/search-results/search-results.component.scss @@ -30,7 +30,7 @@ top: 0; left: 0; right: 0; - background: rgba(0, 0, 0, 0.7); + background: rgba(0, 0, 0, 0.85); padding: 6px 8px; font-size: 12px; color: white; @@ -39,7 +39,6 @@ text-overflow: ellipsis; border-top-left-radius: inherit; border-top-right-radius: inherit; - backdrop-filter: blur(5px); display: flex; align-items: center; gap: 4px; diff --git a/apps/web/src/app/xtream-electron/season-container/season-container.component.scss b/apps/web/src/app/xtream-electron/season-container/season-container.component.scss index 1ec8ae384..1348c34ba 100644 --- a/apps/web/src/app/xtream-electron/season-container/season-container.component.scss +++ b/apps/web/src/app/xtream-electron/season-container/season-container.component.scss @@ -324,13 +324,12 @@ position: absolute; bottom: 8px; left: 8px; - background: rgba(0, 0, 0, 0.75); + background: rgba(0, 0, 0, 0.85); padding: 2px 6px; border-radius: 3px; font-size: 0.7rem; font-weight: 600; color: white; - backdrop-filter: blur(4px); z-index: 2; } @@ -440,13 +439,12 @@ position: absolute; bottom: 6px; right: 6px; - background: rgba(0, 0, 0, 0.7); + background: rgba(0, 0, 0, 0.85); padding: 2px 6px; border-radius: 3px; font-size: 0.7rem; font-weight: 600; color: white; - backdrop-filter: blur(4px); } &__info { diff --git a/apps/web/src/assets/i18n/en.json b/apps/web/src/assets/i18n/en.json index f6e456317..66787f59f 100644 --- a/apps/web/src/assets/i18n/en.json +++ b/apps/web/src/assets/i18n/en.json @@ -236,6 +236,7 @@ "LOADING": "Loading channels...", "LOADING_MORE": "Loading more...", "NO_SEARCH_RESULTS": "No channels found", + "NO_CHANNELS_FOUND": "No channels found", "TRY_DIFFERENT_SEARCH": "Try a different search term" }, "TOP_MENU": { diff --git a/apps/web/src/nav-list.scss b/apps/web/src/nav-list.scss index 5b0fba1ba..34c2d9ebe 100644 --- a/apps/web/src/nav-list.scss +++ b/apps/web/src/nav-list.scss @@ -47,6 +47,7 @@ text-overflow: ellipsis; flex: 1; min-width: 0; + padding-left: 10px; } .nav-item-meta { diff --git a/libs/ui/components/src/lib/content-hero/content-hero.component.html b/libs/ui/components/src/lib/content-hero/content-hero.component.html index 9c3bb8e1e..cd5899744 100644 --- a/libs/ui/components/src/lib/content-hero/content-hero.component.html +++ b/libs/ui/components/src/lib/content-hero/content-hero.component.html @@ -21,7 +21,7 @@ width: '100%', 'border-radius': '8px', 'aspect-ratio': '2/3', - display: 'block' + display: 'block', }" />
@@ -35,7 +35,7 @@ height: '40px', width: '60%', 'margin-bottom': '16px', - display: 'block' + display: 'block', }" /> @@ -46,7 +46,7 @@ [theme]="{ height: '24px', width: '60px', - display: 'block' + display: 'block', }" />
@@ -58,7 +58,7 @@ height: '16px', 'margin-bottom': '8px', width: '100%', - display: 'block' + display: 'block', }" /> @@ -69,7 +69,7 @@ [theme]="{ height: '40px', width: '100px', - display: 'block' + display: 'block', }" />
@@ -81,7 +81,7 @@ [theme]="{ height: '48px', width: '140px', - display: 'block' + display: 'block', }" />
@@ -91,13 +91,15 @@
@@ -108,24 +110,30 @@
- @if (posterUrl()) { + @if (posterUrl() && !posterError()) { Poster } @else { -
No Poster
+
+ movie + {{ + formattedTitle() + }} +
}
-
-

{{ title() }}

+

{{ formattedTitle() }}

diff --git a/libs/ui/components/src/lib/content-hero/content-hero.component.scss b/libs/ui/components/src/lib/content-hero/content-hero.component.scss index 93d725ce6..48dedce1e 100644 --- a/libs/ui/components/src/lib/content-hero/content-hero.component.scss +++ b/libs/ui/components/src/lib/content-hero/content-hero.component.scss @@ -233,11 +233,32 @@ border-radius: 8px; background: linear-gradient(135deg, #3a3a3a 0%, #2a2a2a 100%); display: flex; + flex-direction: column; align-items: center; justify-content: center; - color: var(--text-muted); - font-size: 0.875rem; + color: rgba(255, 255, 255, 0.8); box-shadow: var(--poster-shadow); + overflow: hidden; + + &-icon { + font-size: 48px; + width: 48px; + height: 48px; + opacity: 0.5; + margin-bottom: 12px; + } + + &-text { + font-size: 1rem; + font-weight: 500; + text-align: center; + display: -webkit-box; + -webkit-line-clamp: 4; + -webkit-box-orient: vertical; + overflow: hidden; + overflow-wrap: anywhere; + padding: 16px; + } } } @@ -257,6 +278,11 @@ margin: 0 0 16px 0; letter-spacing: -0.02em; text-shadow: 0 2px 8px rgba(0, 0, 0, 0.3); + overflow-wrap: anywhere; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; } &__tags { diff --git a/libs/ui/components/src/lib/content-hero/content-hero.component.ts b/libs/ui/components/src/lib/content-hero/content-hero.component.ts index f8500a62e..a04f8610b 100644 --- a/libs/ui/components/src/lib/content-hero/content-hero.component.ts +++ b/libs/ui/components/src/lib/content-hero/content-hero.component.ts @@ -1,5 +1,4 @@ - -import { Component, input, output } from '@angular/core'; +import { Component, computed, input, output, signal } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { NgxSkeletonLoaderComponent } from 'ngx-skeleton-loader'; @@ -7,11 +6,7 @@ import { NgxSkeletonLoaderComponent } from 'ngx-skeleton-loader'; @Component({ selector: 'app-content-hero', standalone: true, - imports: [ - MatIconModule, - MatButtonModule, - NgxSkeletonLoaderComponent -], + imports: [MatIconModule, MatButtonModule, NgxSkeletonLoaderComponent], templateUrl: './content-hero.component.html', styleUrls: ['./content-hero.component.scss'], }) @@ -24,6 +19,40 @@ export class ContentHeroComponent { readonly errorMessage = input(); readonly backClicked = output(); + readonly posterError = signal(false); + + onPosterError(): void { + this.posterError.set(true); + } + + readonly formattedTitle = computed(() => { + const t = this.title(); + if (!t) return ''; + // Replace underscores with spaces for cleaner UX on slug/filename style titles + return t.replace(/_/g, ' ').replace(/\s+/g, ' ').trim(); + }); + + private calculateHue(text: string): number { + if (!text) return 0; + let hash = 0; + for (let i = 0; i < text.length; i++) { + hash = text.charCodeAt(i) + ((hash << 5) - hash); + hash = hash & hash; + } + return Math.abs(hash) % 360; + } + + readonly fallbackPosterBackground = computed(() => { + const hue = this.calculateHue(this.title() || 'placeholder'); + const h2 = (hue + 40) % 360; + return `linear-gradient(135deg, hsl(${hue}, 40%, 25%) 0%, hsl(${h2}, 50%, 15%) 100%)`; + }); + + readonly fallbackBackdropBackground = computed(() => { + const hue = this.calculateHue(this.title() || 'placeholder'); + const h2 = (hue + 60) % 360; + return `linear-gradient(135deg, hsl(${hue}, 50%, 15%) 0%, hsl(${h2}, 80%, 5%) 100%)`; + }); onBack(): void { this.backClicked.emit(); diff --git a/libs/ui/shared-portals/src/lib/live-stream-layout/live-stream-layout.component.html b/libs/ui/shared-portals/src/lib/live-stream-layout/live-stream-layout.component.html index 5c0b62040..71982145d 100644 --- a/libs/ui/shared-portals/src/lib/live-stream-layout/live-stream-layout.component.html +++ b/libs/ui/shared-portals/src/lib/live-stream-layout/live-stream-layout.component.html @@ -25,38 +25,37 @@ />
- +
@if (player === 'videojs' || player === 'html5') { diff --git a/libs/ui/shared-portals/src/lib/live-stream-layout/live-stream-layout.component.scss b/libs/ui/shared-portals/src/lib/live-stream-layout/live-stream-layout.component.scss index a7f7d51c4..2158f469b 100644 --- a/libs/ui/shared-portals/src/lib/live-stream-layout/live-stream-layout.component.scss +++ b/libs/ui/shared-portals/src/lib/live-stream-layout/live-stream-layout.component.scss @@ -78,10 +78,6 @@ margin: 10px; } -mat-list-option { - width: 100%; -} - ::ng-deep { .search-bar { div { @@ -89,3 +85,94 @@ mat-list-option { } } } + +.nav-list { + flex: 1; + overflow-y: hidden; + padding: 0 8px 12px; + display: flex; + flex-direction: column; + gap: 2px; +} + +.nav-item { + position: relative; + width: 100%; + display: flex; + align-items: center; + gap: 10px; + padding: 6px 10px; + border: 1px solid transparent; + border-radius: 10px; + background: transparent; + color: var(--mat-sys-on-surface-variant); + text-align: left; + text-decoration: none; + cursor: pointer; + font-size: 0.875rem; + font-weight: 500; + box-sizing: border-box; + transition: + background 0.15s ease, + color 0.15s ease, + border-color 0.15s ease, + box-shadow 0.18s ease, + transform 0.18s ease; + + .stream-icon { + width: 36px; + height: 36px; + border-radius: 4px; + object-fit: cover; + flex-shrink: 0; + background: color-mix(in srgb, var(--mat-sys-on-surface) 5%, transparent); + } + + span, + .title { + line-height: 1.2; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + flex: 1; + min-width: 0; + } + + &:hover { + background: var(--mat-sys-surface-container); + color: var(--mat-sys-on-surface); + } + + &.selected, + &.active { + background: linear-gradient( + 135deg, + var(--app-selection-surface-strong), + var(--app-selection-surface) + ) !important; + color: var(--app-selection-color) !important; + border-color: var(--app-selection-border) !important; + font-weight: 600; + box-shadow: + inset 0 0 0 1px var(--app-selection-border), + 0 12px 20px -18px var(--app-selection-glow); + transform: translateY(-1px); + + * { + color: var(--app-selection-color) !important; + } + + &::before { + content: ''; + position: absolute; + inset-inline-start: 6px; + top: 7px; + bottom: 7px; + width: 3px; + border-radius: 999px; + background: var(--app-selection-color); + box-shadow: 0 0 10px var(--app-selection-glow); + } + } +} +