@if (!xtreamStore.selectedItem() && !xtreamStore.isLoadingDetails()) {
-

- - {{ getContentLabel() }} -

+
+

+ + {{ getContentLabel() }} +

+
+ @if (canShowSortMenu()) { +
+ + search + + @if (categorySearchTerm()) { + + } + + +
+ + + + + + + }
}
{ + const hasCategorySelected = this.selectedCategoryId() != null; + const type = this.xtreamStore.selectedContentType(); + return hasCategorySelected && (type === 'vod' || type === 'series'); + }); + readonly contentSortLabel = computed(() => { + const mode = this.contentSortMode(); + if (mode === 'date-asc') return 'Date Added (Oldest First)'; + if (mode === 'name-asc') return 'Name A-Z'; + if (mode === 'name-desc') return 'Name Z-A'; + return 'Date Added (Latest First)'; + }); ngOnInit(): void { + const savedSortMode = localStorage.getItem(XTREAM_CATEGORY_SORT_STORAGE_KEY); + if ( + savedSortMode === 'date-desc' || + savedSortMode === 'date-asc' || + savedSortMode === 'name-asc' || + savedSortMode === 'name-desc' + ) { + this.xtreamStore.setContentSortMode(savedSortMode); + } + const { categoryId } = this.route.snapshot.params; if (categoryId) this.xtreamStore.setSelectedCategory(Number(categoryId)); @@ -132,4 +167,17 @@ export class XtreamMainContainerComponent implements OnInit { } }); } + + setContentSortMode(mode: XtreamCategorySortMode): void { + this.xtreamStore.setContentSortMode(mode); + localStorage.setItem(XTREAM_CATEGORY_SORT_STORAGE_KEY, mode); + } + + onCategorySearchChange(term: string): void { + this.xtreamStore.setCategorySearchTerm(term); + } + + clearCategorySearch(): void { + this.xtreamStore.setCategorySearchTerm(''); + } } diff --git a/libs/ui/components/src/lib/channel-list-container/all-channels-tab/all-channels-tab.component.scss b/libs/ui/components/src/lib/channel-list-container/all-channels-tab/all-channels-tab.component.scss index 3cb935f40..1f2156e4e 100644 --- a/libs/ui/components/src/lib/channel-list-container/all-channels-tab/all-channels-tab.component.scss +++ b/libs/ui/components/src/lib/channel-list-container/all-channels-tab/all-channels-tab.component.scss @@ -3,6 +3,12 @@ .scroll-viewport { min-height: calc(100vh - 190px); overflow-x: hidden; + width: 100%; + + ::ng-deep .cdk-virtual-scroll-content-wrapper { + width: 100%; + max-width: 100%; + } } .search-panel { diff --git a/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.scss b/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.scss index d5e003a78..7615cfc41 100644 --- a/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.scss +++ b/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.scss @@ -1,3 +1,10 @@ +:host { + display: block; + width: 100%; + height: 100%; + container-type: inline-size; +} + ::ng-deep { .mat-mdc-tab.mdc-tab { padding: 0 5px; diff --git a/libs/ui/components/src/lib/channel-list-container/channel-list-item/channel-list-item.component.scss b/libs/ui/components/src/lib/channel-list-container/channel-list-item/channel-list-item.component.scss index 187e74762..8f131b5ae 100644 --- a/libs/ui/components/src/lib/channel-list-container/channel-list-item/channel-list-item.component.scss +++ b/libs/ui/components/src/lib/channel-list-container/channel-list-item/channel-list-item.component.scss @@ -1,6 +1,6 @@ // Main channel list item container .channel-list-item { - width: 360px; + width: 100%; display: flex; align-items: center; height: 68px; @@ -9,6 +9,9 @@ cursor: pointer; transition: background-color 0.2s ease; gap: 12px; + box-sizing: border-box; + min-width: 0; + overflow: hidden; &:hover { background-color: rgba(0, 0, 0, 0.04); @@ -224,3 +227,69 @@ } } } + +// ============================================================================ +// Container queries for responsive sidebar resizing +// ============================================================================ + +@container (max-width: 350px) { + .channel-list-item { + gap: 8px; + padding: 6px 8px; + } + + .channel-logo { + width: 36px; + height: 36px; + } + + // Hide EPG timeline (progress bar + times) but keep program title + .epg-timeline { + display: none; + } + + .epg-placeholder { + display: none; + } + + // Hide info button, keep only favorite + .program-info-button { + display: none; + } +} + +@container (max-width: 280px) { + .channel-logo { + width: 28px; + height: 28px; + } + + // Hide all EPG content + .epg-container { + display: none; + } + + .channel-list-item { + height: 48px; + + &.compact { + height: 40px; + } + } + + // Hide action buttons + .action-buttons { + display: none; + } +} + +@container (max-width: 200px) { + .channel-logo { + display: none; + } + + .channel-list-item { + gap: 4px; + padding: 4px 6px; + } +} 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 872e143ed..9c3bb8e1e 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 @@ -4,7 +4,13 @@ - @if (isLoading) { + @if (errorMessage()) { +
+ cloud_off +

{{ errorMessage() }}

+ +
+ } @else if (isLoading()) {
@@ -85,12 +91,12 @@
@@ -102,10 +108,10 @@
- @if (posterUrl) { + @if (posterUrl()) { -

{{ title }}

+

{{ title() }}

@@ -127,9 +133,9 @@
- @if (description) { + @if (description()) {

- {{ description }} + {{ description() }}

} @@ -148,4 +154,4 @@
} - \ No newline at end of file + 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 d1fe1cb28..93d725ce6 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 @@ -179,6 +179,29 @@ display: none; } } + + // Error state + &__error { + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + gap: 16px; + + .error-icon { + font-size: 64px; + width: 64px; + height: 64px; + color: var(--text-muted); + } + + .error-title { + font-size: 1.25rem; + font-weight: 500; + color: var(--text-secondary); + margin: 0; + } + } } // ============================================================================ 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 2dcfff209..75fb970c2 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,5 @@ import { CommonModule } from '@angular/common'; -import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { Component, input, output } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { NgxSkeletonLoaderComponent } from 'ngx-skeleton-loader'; @@ -17,13 +17,14 @@ import { NgxSkeletonLoaderComponent } from 'ngx-skeleton-loader'; styleUrls: ['./content-hero.component.scss'], }) export class ContentHeroComponent { - @Input() title: string | undefined; - @Input() description: string | undefined; - @Input() posterUrl: string | undefined; - @Input() backdropUrl: string | undefined; - @Input() isLoading = false; + readonly title = input(); + readonly description = input(); + readonly posterUrl = input(); + readonly backdropUrl = input(); + readonly isLoading = input(false); + readonly errorMessage = input(); - @Output() backClicked = new EventEmitter(); + readonly backClicked = output(); onBack(): void { this.backClicked.emit();