From 37fc5fb1485b1540556d83cb75d36f6d57dab673 Mon Sep 17 00:00:00 2001 From: 4gray Date: Tue, 6 Jan 2026 01:10:27 +0100 Subject: [PATCH] feat: enhance favorites button and series view with loading states and improved styling --- .../favorites-button.component.html | 8 +- .../favorites-button.component.scss | 32 ++ .../favorites-button.component.ts | 4 +- .../stalker-series-view.component.html | 103 +--- .../stalker-series-view.component.ts | 211 +++++--- .../stalker-shell/stalker-shell.component.ts | 8 +- .../season-container.component.html | 6 +- .../season-container.component.scss | 464 +++++------------- .../season-container.component.ts | 6 +- 9 files changed, 353 insertions(+), 489 deletions(-) create mode 100644 apps/web/src/app/stalker/favorites-button/favorites-button.component.scss diff --git a/apps/web/src/app/stalker/favorites-button/favorites-button.component.html b/apps/web/src/app/stalker/favorites-button/favorites-button.component.html index 5bd3760e1..ef7ead09a 100644 --- a/apps/web/src/app/stalker/favorites-button/favorites-button.component.html +++ b/apps/web/src/app/stalker/favorites-button/favorites-button.component.html @@ -1,12 +1,12 @@ @let isFavorite = isFavorite$ | async; @if (isFavorite === false) { - } @else { - } diff --git a/apps/web/src/app/stalker/favorites-button/favorites-button.component.scss b/apps/web/src/app/stalker/favorites-button/favorites-button.component.scss new file mode 100644 index 000000000..679630b10 --- /dev/null +++ b/apps/web/src/app/stalker/favorites-button/favorites-button.component.scss @@ -0,0 +1,32 @@ +.favorite-btn { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 12px 20px; + font-size: 0.9375rem; + font-weight: 500; + background: rgba(109, 109, 110, 0.7); + color: #fff; + border: none; + border-radius: 4px; + cursor: pointer; + transition: all 0.2s ease; + + &:hover { + background: rgba(109, 109, 110, 0.5); + } + + &--active { + background: rgba(229, 9, 20, 0.8); + + &:hover { + background: rgba(229, 9, 20, 0.6); + } + } + + mat-icon { + font-size: 20px; + width: 20px; + height: 20px; + } +} diff --git a/apps/web/src/app/stalker/favorites-button/favorites-button.component.ts b/apps/web/src/app/stalker/favorites-button/favorites-button.component.ts index 478848891..9e4213dc0 100644 --- a/apps/web/src/app/stalker/favorites-button/favorites-button.component.ts +++ b/apps/web/src/app/stalker/favorites-button/favorites-button.component.ts @@ -1,6 +1,5 @@ import { AsyncPipe } from '@angular/common'; import { Component, inject, input, output } from '@angular/core'; -import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; import { TranslateModule } from '@ngx-translate/core'; import { BehaviorSubject, map, switchMap } from 'rxjs'; @@ -10,7 +9,8 @@ import { StalkerStore } from '../stalker.store'; @Component({ selector: 'app-favorites-button', templateUrl: './favorites-button.component.html', - imports: [AsyncPipe, MatButtonModule, MatIconModule, TranslateModule], + styleUrls: ['./favorites-button.component.scss'], + imports: [AsyncPipe, MatIconModule, TranslateModule], }) export class FavoritesButtonComponent { private playlistService = inject(PlaylistsService); diff --git a/apps/web/src/app/stalker/stalker-series-view/stalker-series-view.component.html b/apps/web/src/app/stalker/stalker-series-view/stalker-series-view.component.html index 2f5e65d35..8e160cc0e 100644 --- a/apps/web/src/app/stalker/stalker-series-view/stalker-series-view.component.html +++ b/apps/web/src/app/stalker/stalker-series-view/stalker-series-view.component.html @@ -1,5 +1,3 @@ -@let seasons = regularSeasons(); - @if (displayItem(); as serial) { - - @if (isVodSeries() && isVodSeriesSeasonsLoading()) { -
- - {{ 'PORTALS.LOADING_SEASONS' | translate }} -
+ + @if (mappedSeasons()) { + } - - - @if (!isVodSeries() && isSerialSeasonsLoading()) { -
- - {{ 'PORTALS.LOADING_SEASONS' | translate }} -
- } - -
- @if (isVodSeries()) { - - @for (item of vodSeriesSeasons(); track item.id) { - -
-

{{ item.name }}

- {{ - item.isExpanded ? '▼' : '►' - }} -
- - @if (item.isExpanded) { - @if (item.isLoading) { -
- -
- } @else { -
- @for ( - episode of item.episodes; - track episode.id ?? $index - ) { - - } - @if ( - !item.episodes || item.episodes.length === 0 - ) { - {{ - 'PORTALS.NO_EPISODES' | translate - }} - } -
- } - } - } - } @else { - - @for (item of seasons; track item.id ?? $index) { - -
-

{{ item.name }}

-
-
- @for (episode of item.series; track $index) { - - } -
- } - } -
} diff --git a/apps/web/src/app/stalker/stalker-series-view/stalker-series-view.component.ts b/apps/web/src/app/stalker/stalker-series-view/stalker-series-view.component.ts index 2a9ac29f1..83a7cbc5e 100644 --- a/apps/web/src/app/stalker/stalker-series-view/stalker-series-view.component.ts +++ b/apps/web/src/app/stalker/stalker-series-view/stalker-series-view.component.ts @@ -6,13 +6,12 @@ import { input, signal, } from '@angular/core'; -import { MatButton } from '@angular/material/button'; -import { MatDivider } from '@angular/material/divider'; -import { MatProgressSpinner } from '@angular/material/progress-spinner'; import { TranslatePipe } from '@ngx-translate/core'; import { FavoritesButtonComponent } from '../favorites-button/favorites-button.component'; import { ContentHeroComponent } from 'components'; import { StalkerStore } from '../stalker.store'; +import { SeasonContainerComponent } from '../../xtream-tauri/season-container/season-container.component'; +import { XtreamSerieEpisode } from 'shared-interfaces'; /** * VOD series season with episodes loaded dynamically @@ -51,10 +50,8 @@ interface StalkerSeriesSeason { imports: [ FavoritesButtonComponent, ContentHeroComponent, - MatButton, - MatDivider, - MatProgressSpinner, TranslatePipe, + SeasonContainerComponent, ], }) export class StalkerSeriesViewComponent { @@ -151,89 +148,180 @@ export class StalkerSeriesViewComponent { }); /** - * Toggle season expansion and load episodes if needed (for VOD series) + * Adapts both Regular and VOD series data into the format expected by SeasonContainerComponent. + * Record where string is season number/name. */ - async toggleSeasonExpanded(season: VodSeriesSeason) { + readonly mappedSeasons = computed>( + () => { + const mapped: Record = {}; + + if (this.isVodSeries()) { + // Map VOD Series (async loaded) + this.vodSeriesSeasons().forEach((season) => { + const seasonKey = + season.season_number || season.name || season.id; + // Map existing episodes or empty array + mapped[seasonKey] = (season.episodes || []).map( + (ep) => + ({ + episode_num: + ep.series_number || ep.episode_num || 0, + title: + ep.name || + `Episode ${ep.series_number || ep.episode_num}`, + container_extension: 'mpg', + info: { + movie_image: + ep.cover || + this.displayItem().info.movie_image, + plot: ep.description || '', + duration: ep.duration + ? `${ep.duration} min` + : '', + }, + id: ep.id, + custom_sid: 'vod-series', // marker + }) as unknown as XtreamSerieEpisode + ); + }); + } else { + // Map Regular Series (just numbers) + (this.regularSeasons() || []).forEach((season, index) => { + const seasonKey = (index + 1).toString(); + mapped[seasonKey] = (season.series || []).map( + (epNum) => + ({ + episode_num: epNum, + title: `Episode ${epNum}`, + container_extension: '', + info: { + movie_image: + this.displayItem().info.movie_image, + }, + custom_sid: 'regular-series', // marker + id: season.cmd, // Store cmd in id for playEpisodeClicked + }) as unknown as XtreamSerieEpisode + ); + }); + } + + return mapped; + } + ); + + /** + * Handles season selection from the container. + * For VOD Series, triggers lazy loading of episodes. + */ + onSeasonSelected(seasonKey: string) { + if (!this.isVodSeries()) return; + + const seasons = this.vodSeriesSeasons(); + const season = seasons.find( + (s) => (s.season_number || s.name || s.id) == seasonKey + ); + + if (season && (!season.episodes || season.episodes.length === 0)) { + this.loadEpisodesForSeason(season); + } + } + + /** + * Loads episodes for a specific VOD season + */ + async loadEpisodesForSeason(season: VodSeriesSeason) { + // Set loading state in local signal const seasons = this.vodSeriesSeasons(); const index = seasons.findIndex((s) => s.id === season.id); if (index === -1) return; const updatedSeasons = [...seasons]; - const currentSeason = { ...updatedSeasons[index] }; + updatedSeasons[index] = { ...updatedSeasons[index], isLoading: true }; + this.vodSeriesSeasons.set(updatedSeasons); - // Toggle expansion - currentSeason.isExpanded = !currentSeason.isExpanded; + try { + const episodes = await this.stalkerStore.fetchVodSeriesEpisodes( + season.video_id, + season.id + ); - // Load episodes if expanding and not already loaded - if ( - currentSeason.isExpanded && - (!currentSeason.episodes || currentSeason.episodes.length === 0) - ) { - currentSeason.isLoading = true; - updatedSeasons[index] = currentSeason; - this.vodSeriesSeasons.set(updatedSeasons); - - try { - const episodes = await this.stalkerStore.fetchVodSeriesEpisodes( - currentSeason.video_id, - currentSeason.id - ); - // Update season with loaded episodes - const newSeasons = [...this.vodSeriesSeasons()]; - const seasonIndex = newSeasons.findIndex( - (s) => s.id === season.id - ); - if (seasonIndex !== -1) { - newSeasons[seasonIndex] = { - ...newSeasons[seasonIndex], - episodes: episodes, - isLoading: false, - }; - this.vodSeriesSeasons.set(newSeasons); - } - } catch (error) { - console.error( - '[StalkerSeriesViewComponent] Failed to load episodes:', - error - ); - const newSeasons = [...this.vodSeriesSeasons()]; - const seasonIndex = newSeasons.findIndex( - (s) => s.id === season.id - ); - if (seasonIndex !== -1) { - newSeasons[seasonIndex] = { - ...newSeasons[seasonIndex], - isLoading: false, - }; - this.vodSeriesSeasons.set(newSeasons); - } + // Update with loaded episodes + const newSeasons = [...this.vodSeriesSeasons()]; + const newIndex = newSeasons.findIndex((s) => s.id === season.id); + if (newIndex !== -1) { + newSeasons[newIndex] = { + ...newSeasons[newIndex], + episodes: episodes, + isLoading: false, + }; + this.vodSeriesSeasons.set(newSeasons); } + } catch (error) { + console.error( + '[StalkerSeriesViewComponent] Failed to load episodes:', + error + ); + const newSeasons = [...this.vodSeriesSeasons()]; + const newIndex = newSeasons.findIndex((s) => s.id === season.id); + if (newIndex !== -1) { + newSeasons[newIndex] = { + ...newSeasons[newIndex], + isLoading: false, + }; + this.vodSeriesSeasons.set(newSeasons); + } + } + } + + /** + * Determines if the current selected season is loading + */ + isCurrentSeasonLoading(seasonKey: string): boolean { + if (!this.isVodSeries()) return false; + const season = this.vodSeriesSeasons().find( + (s) => (s.season_number || s.name || s.id) == seasonKey + ); + return season?.isLoading || false; + } + + /** + * Handles episode click from the container + */ + onEpisodeClicked(episode: XtreamSerieEpisode) { + if (episode.custom_sid === 'vod-series') { + // It's a VOD series episode + // We reconstructed the episode object, but playVodSeriesEpisode expects the raw object + // or at least { id, name, series_number } + this.playVodSeriesEpisode({ + id: episode.id, + name: episode.title, + series_number: episode.episode_num, + }); } else { - updatedSeasons[index] = currentSeason; - this.vodSeriesSeasons.set(updatedSeasons); + // Regular series + // episode.id contains the 'cmd' from mapping + this.playEpisodeClicked(episode.episode_num, episode.id); } } /** * Play episode - handles both regular series and VOD series */ - playEpisodeClicked(episode: any, cmd?: string) { + playEpisodeClicked(episodeNum: any, cmd?: string) { const item = this.displayItem(); this.stalkerStore.createLinkToPlayVod( cmd, item.info.name, item.info.movie_image, - episode + episodeNum ); } /** * Play VOD series episode - * For VOD series, we need to use a different cmd format */ playVodSeriesEpisode(episode: any) { const item = this.displayItem(); - // For VOD series episodes, the cmd is: /media/file_{episode.id}.mpg const cmd = `/media/file_${episode.id}.mpg`; const episodeName = episode.name || `Episode ${episode.series_number}`; @@ -261,7 +349,6 @@ export class StalkerSeriesViewComponent { } goBack() { - // Clear selectedItem to return to category view this.stalkerStore.clearSelectedItem(); } -} +} \ No newline at end of file diff --git a/apps/web/src/app/stalker/stalker-shell/stalker-shell.component.ts b/apps/web/src/app/stalker/stalker-shell/stalker-shell.component.ts index caee8a551..ca990df87 100644 --- a/apps/web/src/app/stalker/stalker-shell/stalker-shell.component.ts +++ b/apps/web/src/app/stalker/stalker-shell/stalker-shell.component.ts @@ -48,7 +48,13 @@ export class StalkerShellComponent implements OnDestroy { playlistId: this.route.snapshot.params['id'], }) ); - this.stalkerStore.setSelectedContentType('vod'); + + const childRoute = this.route.snapshot.firstChild; + const path = childRoute?.url[0]?.path; + const validTypes = ['vod', 'series', 'itv']; + const initialType = validTypes.includes(path) ? (path as any) : 'vod'; + + this.stalkerStore.setSelectedContentType(initialType); this.stalkerStore.setSelectedCategory(null); effect(() => { this.stalkerStore.setCurrentPlaylist(this.currentPlaylist()); diff --git a/apps/web/src/app/xtream-tauri/season-container/season-container.component.html b/apps/web/src/app/xtream-tauri/season-container/season-container.component.html index 33b5d2be5..8200fc753 100644 --- a/apps/web/src/app/xtream-tauri/season-container/season-container.component.html +++ b/apps/web/src/app/xtream-tauri/season-container/season-container.component.html @@ -10,7 +10,11 @@
- @if (!selectedSeason) { + @if (isLoading()) { +
+ +
+ } @else if (!selectedSeason) {
@for ( diff --git a/apps/web/src/app/xtream-tauri/season-container/season-container.component.scss b/apps/web/src/app/xtream-tauri/season-container/season-container.component.scss index 87198a36d..24f115df4 100644 --- a/apps/web/src/app/xtream-tauri/season-container/season-container.component.scss +++ b/apps/web/src/app/xtream-tauri/season-container/season-container.component.scss @@ -1,143 +1,97 @@ -// ============================================================================ -// Netflix-style Seasons & Episodes Component -// ============================================================================ - -:host { - display: block; - width: 100%; - - // Default dark theme colors (matches detail-view) - --card-bg: rgba(50, 50, 50, 0.8); - --card-bg-hover: rgba(70, 70, 70, 0.9); - --card-border: rgba(255, 255, 255, 0.15); - --text-primary: #ffffff; - --text-secondary: rgba(255, 255, 255, 0.75); - --text-muted: rgba(255, 255, 255, 0.55); - --accent-color: #e50914; - --accent-hover: #f40612; -} - -// Light theme overrides - only when body does NOT have .dark-theme -:host-context(body:not(.dark-theme)) { - --card-bg: rgba(0, 0, 0, 0.06); - --card-bg-hover: rgba(0, 0, 0, 0.1); - --card-border: rgba(0, 0, 0, 0.12); - --text-primary: #1a1a1a; - --text-secondary: rgba(26, 26, 26, 0.75); - --text-muted: rgba(26, 26, 26, 0.55); -} - -// ============================================================================ -// Section Layout -// ============================================================================ - .seasons-section { - margin-top: 32px; + width: 100%; + margin-top: 24px; } .section-header { display: flex; align-items: center; - gap: 16px; + justify-content: space-between; margin-bottom: 20px; - flex-wrap: wrap; -} + min-height: 32px; -.section-title { - font-size: 1.125rem; - font-weight: 600; - color: #fff; // Force white for dark mode - margin: 0; - letter-spacing: -0.01em; -} - -.back-btn { - display: inline-flex; - align-items: center; - gap: 6px; - padding: 6px 12px; - background: rgba(255, 255, 255, 0.1); - border: 1px solid rgba(255, 255, 255, 0.2); - border-radius: 4px; - color: #fff; // Force white for dark mode - font-size: 0.8125rem; - font-weight: 500; - cursor: pointer; - transition: all 0.2s ease; - - &:hover { - background: rgba(255, 255, 255, 0.2); - border-color: rgba(255, 255, 255, 0.3); - } - - mat-icon { - font-size: 16px; - width: 16px; - height: 16px; - } -} - -// Light theme overrides for header -:host-context(body:not(.dark-theme)) { .section-title { - color: #1a1a1a; + font-size: 1.25rem; + font-weight: 600; + margin: 0; + color: var(--text-primary); + letter-spacing: -0.01em; } + .back-btn { - background: rgba(0, 0, 0, 0.06); - border-color: rgba(0, 0, 0, 0.12); - color: #1a1a1a; + display: flex; + align-items: center; + gap: 6px; + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + color: var(--text-secondary); + font-size: 0.8125rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + padding: 6px 12px; + border-radius: 4px; &:hover { - background: rgba(0, 0, 0, 0.1); + color: var(--text-primary); + background: rgba(255, 255, 255, 0.1); + border-color: rgba(255, 255, 255, 0.2); + } + + mat-icon { + font-size: 18px; + width: 18px; + height: 18px; } } } -// ============================================================================ -// Seasons Grid -// ============================================================================ +.loading-container { + display: flex; + justify-content: center; + align-items: center; + padding: 30px 0; + width: 100%; +} + +// Grid Layouts +.seasons-grid, +.episodes-grid { + display: grid; + gap: 12px; + width: 100%; +} .seasons-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); - gap: 10px; + grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); } +.episodes-grid { + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); +} + +// Season Card .season-card { - position: relative; display: flex; - flex-direction: column; align-items: center; - justify-content: center; - padding: 16px 12px; - min-height: 80px; - background: rgba(50, 50, 50, 0.8); - border: 1px solid rgba(255, 255, 255, 0.15); + gap: 12px; + padding: 16px; + background: rgba(255, 255, 255, 0.04); + border: 1px solid rgba(255, 255, 255, 0.08); border-radius: 6px; cursor: pointer; - transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); + transition: all 0.2s ease; + text-align: left; + position: relative; overflow: hidden; - - &::before { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - height: 3px; - background: var(--accent-color); - transform: scaleX(0); - transition: transform 0.25s ease; - } + color: var(--text-primary); + font-size: 1rem; + font-family: inherit; &:hover { - background: var(--card-bg-hover); + background: rgba(255, 255, 255, 0.1); transform: translateY(-2px); - box-shadow: 0 8px 25px rgba(0, 0, 0, 0.3); - - &::before { - transform: scaleX(1); - } + border-color: rgba(255, 255, 255, 0.15); .season-card__arrow { opacity: 1; @@ -146,83 +100,59 @@ } &__number { - font-size: 1.75rem; - font-weight: 700; - color: #fff; // Force white + font-size: 1.75rem !important; + font-weight: 800; + color: rgba(255, 255, 255, 0.05); line-height: 1; - margin-bottom: 4px; + transition: color 0.3s ease; } &__label { - font-size: 0.6875rem; - font-weight: 500; - color: rgba(255, 255, 255, 0.6); // Force light - text-transform: uppercase; - letter-spacing: 0.05em; + font-size: 0.9375rem !important; + font-weight: 600; + color: var(--text-primary); + flex: 1; } &__episodes { - font-size: 0.6875rem; - color: rgba(255, 255, 255, 0.7); // Force light - margin-top: 6px; + font-size: 0.75rem; + color: var(--text-muted); } &__arrow { - position: absolute; - right: 10px; - top: 50%; - transform: translateX(8px) translateY(-50%); opacity: 0; - color: var(--accent-color); - transition: all 0.25s ease; + transform: translateX(-5px); + transition: all 0.2s ease; + color: var(--text-muted); mat-icon { - font-size: 20px; - width: 20px; - height: 20px; + font-size: 18px; + width: 18px; + height: 18px; + } + } + + &--has-episodes { + .season-card__number { + color: var(--accent-color); + opacity: 0.4; // More subtle } } } -// Light theme overrides for season card -:host-context(body:not(.dark-theme)) { - .season-card { - &__number { - color: #1a1a1a; - } - &__label { - color: rgba(26, 26, 26, 0.6); - } - &__episodes { - color: rgba(26, 26, 26, 0.7); - } - } -} - -// ============================================================================ -// Episodes Grid -// ============================================================================ - -.episodes-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); - gap: 12px; -} - +// Episode Card .episode-card { - display: flex; - flex-direction: column; - background: var(--card-bg); - border: 1px solid var(--card-border); - border-radius: 8px; + background: rgba(255, 255, 255, 0.03); + border-radius: 6px; overflow: hidden; cursor: pointer; - transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1); + transition: all 0.2s ease; + border: 1px solid rgba(255, 255, 255, 0.05); &:hover { - background: var(--card-bg-hover); - transform: scale(1.02); - box-shadow: 0 12px 40px rgba(0, 0, 0, 0.4); + transform: translateY(-2px); + background: rgba(255, 255, 255, 0.07); + border-color: rgba(255, 255, 255, 0.1); .episode-card__overlay { opacity: 1; @@ -231,24 +161,18 @@ .episode-card__play { transform: scale(1); } - - .episode-card__thumbnail img { - transform: scale(1.05); - } } &__thumbnail { position: relative; - width: 100%; aspect-ratio: 16 / 9; + background: #000; overflow: hidden; - background: linear-gradient(135deg, #2a2a2a 0%, #1a1a1a 100%); img { width: 100%; height: 100%; object-fit: cover; - transition: transform 0.4s ease; } } @@ -258,208 +182,92 @@ display: flex; align-items: center; justify-content: center; - background: linear-gradient(135deg, #3a3a3a 0%, #2a2a2a 100%); + background: linear-gradient(135deg, #2a2a2a 0%, #1a1a1a 100%); + color: var(--text-muted); mat-icon { - font-size: 48px; - width: 48px; - height: 48px; - color: var(--text-muted); + font-size: 32px; + width: 32px; + height: 32px; + opacity: 0.3; } } &__overlay { position: absolute; inset: 0; - background: rgba(0, 0, 0, 0.5); + background: rgba(0, 0, 0, 0.4); display: flex; align-items: center; justify-content: center; opacity: 0; - transition: opacity 0.25s ease; + transition: opacity 0.2s ease; } &__play { - width: 56px; - height: 56px; + width: 40px; + height: 40px; border-radius: 50%; background: var(--accent-color); display: flex; align-items: center; justify-content: center; + color: white; transform: scale(0.8); - transition: transform 0.25s ease; - box-shadow: 0 4px 20px rgba(229, 9, 20, 0.4); + transition: transform 0.2s cubic-bezier(0.175, 0.885, 0.32, 1.275); mat-icon { - font-size: 32px; - width: 32px; - height: 32px; - color: #fff; - margin-left: 3px; // Optical centering for play icon + font-size: 24px; + width: 24px; + height: 24px; } } &__number { position: absolute; - bottom: 8px; - left: 8px; - padding: 4px 10px; - background: rgba(0, 0, 0, 0.75); - border-radius: 4px; - font-size: 0.75rem; + bottom: 6px; + right: 6px; + background: rgba(0, 0, 0, 0.7); + padding: 2px 6px; + border-radius: 3px; + font-size: 0.7rem; font-weight: 600; - color: #fff; // Always white on dark badge + color: white; backdrop-filter: blur(4px); } &__info { - padding: 10px 12px; - flex: 1; - display: flex; - flex-direction: column; - gap: 4px; - background: var(--card-bg); // Ensure background for contrast + padding: 12px; } &__title { - font-size: 0.8125rem; + font-size: 0.875rem; font-weight: 600; + margin: 0 0 4px 0; color: var(--text-primary); - color: #fff; // Force white in dark mode for readability - margin: 0; - line-height: 1.3; + line-height: 1.4; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } - // Truncate long titles + &__plot { + font-size: 0.75rem; + color: var(--text-secondary); + margin: 0 0 8px 0; + line-height: 1.4; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } - &__plot { - font-size: 0.75rem; - color: rgba(255, 255, 255, 0.7); // Force light color - margin: 0; - line-height: 1.4; - - // Truncate plot - display: -webkit-box; - -webkit-line-clamp: 1; - -webkit-box-orient: vertical; - overflow: hidden; - } - &__duration { font-size: 0.6875rem; - color: rgba(255, 255, 255, 0.55); // Force light color - margin-top: auto; - } -} - -// Light theme text overrides -:host-context(body:not(.dark-theme)) { - .episode-card__title { - color: #1a1a1a; - } - .episode-card__plot { - color: rgba(26, 26, 26, 0.75); - } - .episode-card__duration { - color: rgba(26, 26, 26, 0.55); - } -} - -// ============================================================================ -// Mobile Responsive -// ============================================================================ - -@media (max-width: 600px) { - .seasons-section { - margin-top: 24px; - } - - .section-header { - flex-direction: column; - align-items: flex-start; - gap: 12px; - } - - .seasons-grid { - grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); - gap: 8px; - } - - .season-card { - padding: 16px 12px; - min-height: 80px; - - &__number { - font-size: 1.5rem; - } - - &__arrow { - display: none; - } - } - - .episodes-grid { - grid-template-columns: 1fr; - gap: 12px; - } - - .episode-card { - flex-direction: row; - - &__thumbnail { - width: 140px; - flex-shrink: 0; - aspect-ratio: 16 / 10; - } - - &__info { - padding: 12px; - } - - &__title { - font-size: 0.875rem; - } - - &__plot { - -webkit-line-clamp: 1; - } - - &__number { - bottom: 4px; - left: 4px; - padding: 2px 6px; - font-size: 0.625rem; - } - } -} - -// ============================================================================ -// Animations -// ============================================================================ - -@keyframes slideInUp { - from { - opacity: 0; - transform: translateY(16px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -.season-card, -.episode-card { - animation: slideInUp 0.4s ease backwards; - - @for $i from 1 through 12 { - &:nth-child(#{$i}) { - animation-delay: #{$i * 0.05}s; - } + color: var(--text-muted); + display: inline-block; + padding: 1px 6px; + background: rgba(255, 255, 255, 0.08); + border-radius: 10px; } } diff --git a/apps/web/src/app/xtream-tauri/season-container/season-container.component.ts b/apps/web/src/app/xtream-tauri/season-container/season-container.component.ts index 706e5be32..b7555eb8e 100644 --- a/apps/web/src/app/xtream-tauri/season-container/season-container.component.ts +++ b/apps/web/src/app/xtream-tauri/season-container/season-container.component.ts @@ -1,18 +1,21 @@ import { KeyValuePipe } from '@angular/common'; import { Component, EventEmitter, Output, input } from '@angular/core'; import { MatIcon } from '@angular/material/icon'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { XtreamSerieEpisode, XtreamSerieEpisodeInfo } from 'shared-interfaces'; @Component({ selector: 'app-season-container', templateUrl: './season-container.component.html', styleUrls: ['./season-container.component.scss'], - imports: [KeyValuePipe, MatIcon], + imports: [KeyValuePipe, MatIcon, MatProgressSpinnerModule], }) export class SeasonContainerComponent { readonly seasons = input.required>(); + readonly isLoading = input(false); @Output() episodeClicked = new EventEmitter(); + @Output() seasonSelected = new EventEmitter(); selectedSeason: string; @@ -22,6 +25,7 @@ export class SeasonContainerComponent { selectSeason(seasonKey: string) { this.selectedSeason = seasonKey; + this.seasonSelected.emit(seasonKey); } selectEpisode(episode: XtreamSerieEpisode) {