From 51ce39e2b4ff08898a58cb8a4fc297740a213004 Mon Sep 17 00:00:00 2001 From: 4gray Date: Mon, 12 Jan 2026 00:43:21 +0100 Subject: [PATCH 01/17] fix(ui): check portal statuses when menu opens --- .../lib/playlist-switcher/playlist-switcher.component.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/libs/ui/components/src/lib/playlist-switcher/playlist-switcher.component.ts b/libs/ui/components/src/lib/playlist-switcher/playlist-switcher.component.ts index 3c2d8ee15..7d5bc29b0 100644 --- a/libs/ui/components/src/lib/playlist-switcher/playlist-switcher.component.ts +++ b/libs/ui/components/src/lib/playlist-switcher/playlist-switcher.component.ts @@ -96,12 +96,6 @@ export class PlaylistSwitcherComponent { .subscribe((event: NavigationEnd) => { this.updateActivePlaylistFromRoute(event.urlAfterRedirects); }); - - // Check portal status for Xtream playlists when playlists load - effect(() => { - const allPlaylists = this.playlists(); - this.checkPortalStatuses(allPlaylists); - }); } private updateActivePlaylistFromRoute(url: string) { @@ -137,6 +131,7 @@ export class PlaylistSwitcherComponent { onMenuOpened() { this.isMenuOpen.set(true); + this.checkPortalStatuses(this.playlists()); } onMenuClosed() { @@ -186,4 +181,4 @@ export class PlaylistSwitcherComponent { isSelected(playlist: PlaylistMeta): boolean { return this.activePlaylistId() === playlist._id; } -} +} \ No newline at end of file From fb1104f6b7652e6658c861984086ee8f3ddd9c50 Mon Sep 17 00:00:00 2001 From: 4gray Date: Mon, 12 Jan 2026 00:52:42 +0100 Subject: [PATCH 02/17] feat(category-management): show category item counts --- .../category-management-dialog.component.html | 1 + .../category-management-dialog.component.scss | 6 +++ .../category-management-dialog.component.ts | 7 +++ .../category-view/category-view.component.ts | 4 +- .../live-stream-layout.component.ts | 1 + .../stores/features/with-selection.feature.ts | 52 +++++++++++++------ 6 files changed, 54 insertions(+), 17 deletions(-) diff --git a/apps/web/src/app/xtream-tauri/category-management-dialog/category-management-dialog.component.html b/apps/web/src/app/xtream-tauri/category-management-dialog/category-management-dialog.component.html index 781b0e08b..589d8f41e 100644 --- a/apps/web/src/app/xtream-tauri/category-management-dialog/category-management-dialog.component.html +++ b/apps/web/src/app/xtream-tauri/category-management-dialog/category-management-dialog.component.html @@ -59,6 +59,7 @@ > {{ category.name }} + ({{ getItemCount(category) }}) } @empty {
diff --git a/apps/web/src/app/xtream-tauri/category-management-dialog/category-management-dialog.component.scss b/apps/web/src/app/xtream-tauri/category-management-dialog/category-management-dialog.component.scss index 079ba4d9f..f5026434d 100644 --- a/apps/web/src/app/xtream-tauri/category-management-dialog/category-management-dialog.component.scss +++ b/apps/web/src/app/xtream-tauri/category-management-dialog/category-management-dialog.component.scss @@ -79,6 +79,12 @@ text-overflow: ellipsis; } +.item-count { + opacity: 0.6; + font-size: 12px; + color: var(--mat-sys-on-surface-variant); +} + .empty-message { padding: 24px; text-align: center; diff --git a/apps/web/src/app/xtream-tauri/category-management-dialog/category-management-dialog.component.ts b/apps/web/src/app/xtream-tauri/category-management-dialog/category-management-dialog.component.ts index 38ff9f639..373ca23e3 100644 --- a/apps/web/src/app/xtream-tauri/category-management-dialog/category-management-dialog.component.ts +++ b/apps/web/src/app/xtream-tauri/category-management-dialog/category-management-dialog.component.ts @@ -18,6 +18,7 @@ import { DatabaseService, XCategoryFromDb } from 'services'; export interface CategoryManagementDialogData { playlistId: string; contentType: 'live' | 'vod' | 'series'; + itemCounts: Map; } interface CategoryWithSelection extends XCategoryFromDb { @@ -104,6 +105,12 @@ export class CategoryManagementDialogComponent implements OnInit { this.searchTerm.set(''); } + getItemCount(category: CategoryWithSelection): number { + // content.category_id references categories.id (internal DB id), not xtream_id + const categoryId = Number(category.id); + return this.data.itemCounts.get(categoryId) ?? 0; + } + private getDbType(): 'live' | 'movies' | 'series' { switch (this.data.contentType) { case 'live': diff --git a/apps/web/src/app/xtream-tauri/category-view/category-view.component.ts b/apps/web/src/app/xtream-tauri/category-view/category-view.component.ts index 35e73073f..ed9f9eb32 100644 --- a/apps/web/src/app/xtream-tauri/category-view/category-view.component.ts +++ b/apps/web/src/app/xtream-tauri/category-view/category-view.component.ts @@ -32,7 +32,9 @@ export class CategoryViewComponent { } getItemCount(item: XtreamCategory): number { - const itemId = Number((item as any).category_id ?? item.id); + // content.category_id references categories.id (internal DB id) + // For DB format categories, use id; for API format, use category_id + const itemId = Number(item.id ?? (item as any).category_id); return this.itemCounts().get(itemId) ?? 0; } } diff --git a/apps/web/src/app/xtream-tauri/live-stream-layout/live-stream-layout.component.ts b/apps/web/src/app/xtream-tauri/live-stream-layout/live-stream-layout.component.ts index b8ff9855c..f86d29464 100644 --- a/apps/web/src/app/xtream-tauri/live-stream-layout/live-stream-layout.component.ts +++ b/apps/web/src/app/xtream-tauri/live-stream-layout/live-stream-layout.component.ts @@ -118,6 +118,7 @@ export class LiveStreamLayoutComponent implements OnInit { data: { playlistId, contentType, + itemCounts: this.categoryItemCounts(), }, width: '500px', maxHeight: '80vh', diff --git a/apps/web/src/app/xtream-tauri/stores/features/with-selection.feature.ts b/apps/web/src/app/xtream-tauri/stores/features/with-selection.feature.ts index d86e80c13..1dc659b59 100644 --- a/apps/web/src/app/xtream-tauri/stores/features/with-selection.feature.ts +++ b/apps/web/src/app/xtream-tauri/stores/features/with-selection.feature.ts @@ -50,7 +50,9 @@ export function withSelection() { */ getSelectedCategory: computed(() => { const categoryId = store.selectedCategoryId(); - if (!categoryId) return null; + if (!categoryId) { + return { id: 0, name: 'All Items', type: store.selectedContentType() }; + } // Access parent store categories (from withContent) const storeAny = store as any; @@ -98,9 +100,6 @@ export function withSelection() { const startIndex = store.page() * store.limit(); const endIndex = startIndex + store.limit(); const categoryId = store.selectedCategoryId(); - - if (!categoryId) return []; - const categoryType = store.selectedContentType(); // Access parent store content (from withContent) @@ -112,9 +111,20 @@ export function withSelection() { ? storeAny.vodStreams?.() || [] : storeAny.serialStreams?.() || []; - return content - .filter((item: any) => Number(item.category_id) === categoryId) - .slice(startIndex, endIndex); + let filteredContent = content; + if (categoryId) { + filteredContent = content.filter((item: any) => Number(item.category_id) === categoryId); + } else { + // Filter nothing (all items), sort by added + // Clone to avoid mutation + filteredContent = [...content].sort((a: any, b: any) => { + const dateA = parseInt(categoryType === 'series' ? a.last_modified : a.added) || 0; + const dateB = parseInt(categoryType === 'series' ? b.last_modified : b.added) || 0; + return dateB - dateA; + }); + } + + return filteredContent.slice(startIndex, endIndex); }), /** @@ -124,8 +134,6 @@ export function withSelection() { const categoryId = store.selectedCategoryId(); const categoryType = store.selectedContentType(); - if (!categoryId) return []; - // Access parent store content (from withContent) const storeAny = store as any; const content = @@ -135,6 +143,15 @@ export function withSelection() { ? storeAny.vodStreams?.() || [] : storeAny.serialStreams?.() || []; + if (!categoryId) { + // Return all items sorted by added + return [...content].sort((a: any, b: any) => { + const dateA = parseInt(categoryType === 'series' ? a.last_modified : a.added) || 0; + const dateB = parseInt(categoryType === 'series' ? b.last_modified : b.added) || 0; + return dateB - dateA; + }); + } + return content.filter( (item: any) => Number(item.category_id) === categoryId ); @@ -145,8 +162,6 @@ export function withSelection() { */ getTotalPages: computed(() => { const categoryId = store.selectedCategoryId(); - if (!categoryId) return 0; - const categoryType = store.selectedContentType(); // Access parent store content (from withContent) @@ -158,9 +173,14 @@ export function withSelection() { ? storeAny.vodStreams?.() || [] : storeAny.serialStreams?.() || []; - const totalItems = content.filter( - (item: any) => Number(item.category_id) === categoryId - ).length; + let totalItems = 0; + if (categoryId) { + totalItems = content.filter( + (item: any) => Number(item.category_id) === categoryId + ).length; + } else { + totalItems = content.length; + } return Math.ceil(totalItems / store.limit()); }), @@ -230,9 +250,9 @@ export function withSelection() { /** * Set the selected category */ - setSelectedCategory(categoryId: number): void { + setSelectedCategory(categoryId: number | null): void { patchState(store, { - selectedCategoryId: Number(categoryId), + selectedCategoryId: categoryId !== null ? Number(categoryId) : null, page: 0, }); }, From d31605da9fc00bf756d90855134f35dd4eff4251 Mon Sep 17 00:00:00 2001 From: 4gray Date: Mon, 12 Jan 2026 00:46:20 +0100 Subject: [PATCH 03/17] style(stalker-search): move details view out of search page --- .../stalker-search.component.html | 61 ++++++++----------- .../stalker-search.component.ts | 5 +- 2 files changed, 28 insertions(+), 38 deletions(-) diff --git a/apps/web/src/app/stalker/stalker-search/stalker-search.component.html b/apps/web/src/app/stalker/stalker-search/stalker-search.component.html index 56701404a..e9a606599 100644 --- a/apps/web/src/app/stalker/stalker-search/stalker-search.component.html +++ b/apps/web/src/app/stalker/stalker-search/stalker-search.component.html @@ -1,38 +1,31 @@ -
-
-

{{ 'PORTALS.SIDEBAR.SEARCH' | translate }}

-
- - @if (itemDetails) { - - @switch (selectedFilterType()) { - @case ('vod') { - - } - @case ('series') { - - } +@if (itemDetails) { + @switch (selectedFilterType()) { + @case ('vod') { + } - } @else { + @case ('series') { + + } + } +} + +
+ @if (!itemDetails) { +
+

{{ 'PORTALS.SIDEBAR.SEARCH' | translate }}

+
Date: Mon, 12 Jan 2026 00:50:23 +0100 Subject: [PATCH 04/17] fix(xtream): switch to XtreamApiService and sendRequest --- apps/web/src/app/services/electron.service.ts | 19 +------ apps/web/src/app/services/pwa.service.ts | 4 -- .../account-info/account-info.component.ts | 19 +++---- .../services/xtream-api.service.ts | 49 +++++++++++++------ .../app/xtream-tauri/stores/xtream.store.ts | 39 +++++++-------- libs/services/src/lib/data.service.ts | 1 - 6 files changed, 60 insertions(+), 71 deletions(-) diff --git a/apps/web/src/app/services/electron.service.ts b/apps/web/src/app/services/electron.service.ts index 730bba602..976dcc7a3 100644 --- a/apps/web/src/app/services/electron.service.ts +++ b/apps/web/src/app/services/electron.service.ts @@ -12,6 +12,7 @@ import { PLAYLIST_PARSE_BY_URL, PLAYLIST_UPDATE, XTREAM_RESPONSE, + XTREAM_REQUEST, } from 'shared-interfaces'; import { AppConfig } from '../../environments/environment'; @@ -69,7 +70,7 @@ export class ElectronService extends DataService { title: string; } ); - } else if (type === 'XTREAM_REQUEST') { + } else if (type === XTREAM_REQUEST) { return await this.forwardXtreamRequest( payload as { url: string; params: Record } ); @@ -322,20 +323,4 @@ export class ElectronService extends DataService { getAppEnvironment(): string { return 'electron'; } - - async fetchData(url: string, queryParams: Params) { - const urlObject = new URL(url); - Object.entries(queryParams).forEach(([key, value]) => { - urlObject.searchParams.append(key, value); - }); - const response = await fetch(urlObject.toString()); - if (!response.ok) { - throw new Error( - `Error: ${response.statusText} (Status: ${response.status})` - ); - } - - const result = await response.json(); - return result; - } } diff --git a/apps/web/src/app/services/pwa.service.ts b/apps/web/src/app/services/pwa.service.ts index 643246807..3f4c4d040 100644 --- a/apps/web/src/app/services/pwa.service.ts +++ b/apps/web/src/app/services/pwa.service.ts @@ -285,8 +285,4 @@ export class PwaService extends DataService { getAppEnvironment(): string { return 'pwa'; } - - fetchData(url: string, queryParams: Params) { - // not implemented - } } diff --git a/apps/web/src/app/xtream-tauri/account-info/account-info.component.ts b/apps/web/src/app/xtream-tauri/account-info/account-info.component.ts index 2e1f168c3..94cae7146 100644 --- a/apps/web/src/app/xtream-tauri/account-info/account-info.component.ts +++ b/apps/web/src/app/xtream-tauri/account-info/account-info.component.ts @@ -5,7 +5,7 @@ import { MatListModule } from '@angular/material/list'; import { Store } from '@ngrx/store'; import { TranslatePipe } from '@ngx-translate/core'; import { selectActivePlaylist } from 'm3u-state'; -import { DataService } from 'services'; +import { XtreamApiService } from '../services/xtream-api.service'; import { XtreamAccountInfo } from './account-info.interface'; @Component({ @@ -20,7 +20,7 @@ export class AccountInfoComponent { liveStreamsCount: number; seriesCount: number; }>(MAT_DIALOG_DATA); - private readonly dataService = inject(DataService); + private readonly xtreamApiService = inject(XtreamApiService); private readonly store = inject(Store); accountInfo: XtreamAccountInfo; @@ -44,14 +44,11 @@ export class AccountInfoComponent { if (!playlist) return; try { - this.accountInfo = await this.dataService.fetchData( - `${playlist.serverUrl}/player_api.php`, - { - username: playlist.username, - password: playlist.password, - action: 'get_account_info', - } - ); + this.accountInfo = await this.xtreamApiService.getAccountInfo({ + serverUrl: playlist.serverUrl, + username: playlist.username, + password: playlist.password, + }); if (this.accountInfo) { this.formattedExpDate = new Date( @@ -73,4 +70,4 @@ export class AccountInfoComponent { get isTrial(): boolean { return this.accountInfo?.user_info?.is_trial === '1'; } -} +} \ No newline at end of file diff --git a/apps/web/src/app/xtream-tauri/services/xtream-api.service.ts b/apps/web/src/app/xtream-tauri/services/xtream-api.service.ts index 0c5a0bd14..b2f5274c9 100644 --- a/apps/web/src/app/xtream-tauri/services/xtream-api.service.ts +++ b/apps/web/src/app/xtream-tauri/services/xtream-api.service.ts @@ -9,6 +9,7 @@ import { XtreamSerieItem, XtreamVodDetails, XtreamVodStream, + XTREAM_REQUEST, } from 'shared-interfaces'; import { XtreamAccountInfo } from '../account-info/account-info.interface'; @@ -65,8 +66,8 @@ export class XtreamApiService { async getAccountInfo( credentials: XtreamCredentials ): Promise { - return this.dataService.fetchData( - `${credentials.serverUrl}/player_api.php`, + return this.sendRequest( + credentials.serverUrl, { username: credentials.username, password: credentials.password, @@ -88,8 +89,8 @@ export class XtreamApiService { series: XtreamCodeActions.GetSeriesCategories, }; - const response = await this.dataService.fetchData( - `${credentials.serverUrl}/player_api.php`, + const response = await this.sendRequest( + credentials.serverUrl, { action: actionMap[type], username: credentials.username, @@ -106,8 +107,8 @@ export class XtreamApiService { async getLiveStreams( credentials: XtreamCredentials ): Promise { - const response = await this.dataService.fetchData( - `${credentials.serverUrl}/player_api.php`, + const response = await this.sendRequest( + credentials.serverUrl, { action: XtreamCodeActions.GetLiveStreams, username: credentials.username, @@ -124,8 +125,8 @@ export class XtreamApiService { async getVodStreams( credentials: XtreamCredentials ): Promise { - const response = await this.dataService.fetchData( - `${credentials.serverUrl}/player_api.php`, + const response = await this.sendRequest( + credentials.serverUrl, { action: XtreamCodeActions.GetVodStreams, username: credentials.username, @@ -142,8 +143,8 @@ export class XtreamApiService { async getSeriesStreams( credentials: XtreamCredentials ): Promise { - const response = await this.dataService.fetchData( - `${credentials.serverUrl}/player_api.php`, + const response = await this.sendRequest( + credentials.serverUrl, { action: XtreamCodeActions.GetSeries, username: credentials.username, @@ -178,8 +179,8 @@ export class XtreamApiService { credentials: XtreamCredentials, vodId: string | number ): Promise { - return this.dataService.fetchData( - `${credentials.serverUrl}/player_api.php`, + return this.sendRequest( + credentials.serverUrl, { action: XtreamCodeActions.GetVodInfo, username: credentials.username, @@ -196,8 +197,8 @@ export class XtreamApiService { credentials: XtreamCredentials, seriesId: string | number ): Promise { - return this.dataService.fetchData( - `${credentials.serverUrl}/player_api.php`, + return this.sendRequest( + credentials.serverUrl, { action: XtreamCodeActions.GetSeriesInfo, username: credentials.username, @@ -216,8 +217,8 @@ export class XtreamApiService { streamId: number, limit: number = 10 ): Promise { - const response: EpgResponse = await this.dataService.fetchData( - `${credentials.serverUrl}/player_api.php`, + const response: EpgResponse = await this.sendRequest( + credentials.serverUrl, { action: XtreamCodeActions.GetShortEpg, username: credentials.username, @@ -256,4 +257,20 @@ export class XtreamApiService { return str; } } + + /** + * Send request via IPC to avoid CORS issues + */ + private async sendRequest(url: string, params: Record): Promise { + const serializedParams: Record = {}; + Object.entries(params).forEach(([key, value]) => { + serializedParams[key] = String(value); + }); + + const response = await this.dataService.sendIpcEvent(XTREAM_REQUEST, { + url, + params: serializedParams, + }); + return response?.payload; + } } diff --git a/apps/web/src/app/xtream-tauri/stores/xtream.store.ts b/apps/web/src/app/xtream-tauri/stores/xtream.store.ts index 25ed8d442..8f380482f 100644 --- a/apps/web/src/app/xtream-tauri/stores/xtream.store.ts +++ b/apps/web/src/app/xtream-tauri/stores/xtream.store.ts @@ -6,9 +6,7 @@ import { } from '@ngrx/signals'; import { Store } from '@ngrx/store'; import { selectActivePlaylist } from 'm3u-state'; -import { DataService } from 'services'; import { - XtreamCodeActions, XtreamSerieDetails, XtreamVodDetails, } from 'shared-interfaces'; @@ -17,6 +15,9 @@ import { import { withFavorites } from '../with-favorites.feature'; import { withRecentItems } from '../with-recent-items'; +// Import service +import { XtreamApiService } from '../services/xtream-api.service'; + // Import new feature stores import { withPortal, @@ -75,8 +76,10 @@ export const XtreamStore = signalStore( // Cross-feature methods & orchestration withMethods((store) => { - const dataService = inject(DataService); + const xtreamApiService = inject(XtreamApiService); const ngrxStore = inject(Store); + const searchContent = (store as any) + .searchContent as (term: string, types: string[]) => Promise; return { /** @@ -122,15 +125,11 @@ export const XtreamStore = signalStore( if (!playlist) return; store.setIsLoadingDetails(true); - dataService.fetchData( - `${playlist.serverUrl}/player_api.php`, - { - action: XtreamCodeActions.GetVodInfo, - username: playlist.username, - password: playlist.password, - vod_id: params.vodId, - } - ).then((vodDetails: XtreamVodDetails) => { + xtreamApiService.getVodInfo({ + serverUrl: playlist.serverUrl, + username: playlist.username, + password: playlist.password, + }, params.vodId).then((vodDetails: XtreamVodDetails) => { store.setSelectedCategory(params.categoryId); store.setSelectedItem({ ...vodDetails, @@ -154,15 +153,11 @@ export const XtreamStore = signalStore( if (!playlist) return; store.setIsLoadingDetails(true); - dataService.fetchData( - `${playlist.serverUrl}/player_api.php`, - { - action: XtreamCodeActions.GetSeriesInfo, - username: playlist.username, - password: playlist.password, - series_id: params.serialId, - } - ).then((serialDetails: XtreamSerieDetails) => { + xtreamApiService.getSeriesInfo({ + serverUrl: playlist.serverUrl, + username: playlist.username, + password: playlist.password, + }, params.serialId).then((serialDetails: XtreamSerieDetails) => { store.setSelectedCategory(params.categoryId); store.setSelectedItem({ ...serialDetails, @@ -221,7 +216,7 @@ export const XtreamStore = signalStore( */ searchContent(params: { term: string; types: string[] }): void { // Call the underlying search method from withSearch - (store as any).searchContent(params.term, params.types); + void searchContent(params.term, params.types); }, }; }) diff --git a/libs/services/src/lib/data.service.ts b/libs/services/src/lib/data.service.ts index 17aff072e..af3b73113 100644 --- a/libs/services/src/lib/data.service.ts +++ b/libs/services/src/lib/data.service.ts @@ -17,7 +17,6 @@ export abstract class DataService { abstract getAppVersion(): string; abstract sendIpcEvent(type: string, payload?: unknown): any; abstract removeAllListeners(type: string): void; - abstract fetchData(url: string, queryParams: Params): any; abstract listenOn( command: string, callback: (...args: any[]) => void From 16469256df8d304580ff040c7ca0ae6746be4671 Mon Sep 17 00:00:00 2001 From: 4gray Date: Mon, 12 Jan 2026 00:53:07 +0100 Subject: [PATCH 05/17] refactor: wrap search & recent in global components --- apps/web/src/app/app.component.ts | 8 ++++---- .../global-recently-viewed.component.ts | 11 +++++++++++ .../recently-viewed/recently-viewed.component.ts | 4 +--- .../search-results/global-search-results.component.ts | 11 +++++++++++ 4 files changed, 27 insertions(+), 7 deletions(-) create mode 100644 apps/web/src/app/xtream-tauri/recently-viewed/global-recently-viewed.component.ts create mode 100644 apps/web/src/app/xtream-tauri/search-results/global-search-results.component.ts diff --git a/apps/web/src/app/app.component.ts b/apps/web/src/app/app.component.ts index daf3619e0..62e546cd7 100644 --- a/apps/web/src/app/app.component.ts +++ b/apps/web/src/app/app.component.ts @@ -18,8 +18,8 @@ import { } from 'shared-interfaces'; import { SettingsService } from './services/settings.service'; import { EpgProgressPanelComponent } from './shared/epg-progress-panel/epg-progress-panel.component'; -import { RecentlyViewedComponent } from './xtream-tauri/recently-viewed/recently-viewed.component'; -import { SearchResultsComponent } from './xtream-tauri/search-results/search-results.component'; +import { GlobalRecentlyViewedComponent } from './xtream-tauri/recently-viewed/global-recently-viewed.component'; +import { GlobalSearchResultsComponent } from './xtream-tauri/search-results/global-search-results.component'; @Component({ selector: 'app-root', @@ -147,7 +147,7 @@ export class AppComponent implements OnInit { } openGlobalSearch(): void { - this.dialog.open(SearchResultsComponent, { + this.dialog.open(GlobalSearchResultsComponent, { width: '100%', height: '100%', maxWidth: '100%', @@ -157,7 +157,7 @@ export class AppComponent implements OnInit { } openGlobalRecent(): void { - this.dialog.open(RecentlyViewedComponent, { + this.dialog.open(GlobalRecentlyViewedComponent, { width: '100%', height: '100%', maxWidth: '100%', diff --git a/apps/web/src/app/xtream-tauri/recently-viewed/global-recently-viewed.component.ts b/apps/web/src/app/xtream-tauri/recently-viewed/global-recently-viewed.component.ts new file mode 100644 index 000000000..f204311cc --- /dev/null +++ b/apps/web/src/app/xtream-tauri/recently-viewed/global-recently-viewed.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; +import { RecentlyViewedComponent } from './recently-viewed.component'; +import { XtreamStore } from '../stores/xtream.store'; + +@Component({ + selector: 'app-global-recently-viewed', + imports: [RecentlyViewedComponent], + providers: [XtreamStore], + template: '' +}) +export class GlobalRecentlyViewedComponent {} diff --git a/apps/web/src/app/xtream-tauri/recently-viewed/recently-viewed.component.ts b/apps/web/src/app/xtream-tauri/recently-viewed/recently-viewed.component.ts index 5c24fea5a..3a336e64d 100644 --- a/apps/web/src/app/xtream-tauri/recently-viewed/recently-viewed.component.ts +++ b/apps/web/src/app/xtream-tauri/recently-viewed/recently-viewed.component.ts @@ -20,9 +20,7 @@ import { XtreamStore } from '../stores/xtream.store'; MatIconButton, TitleCasePipe, ], - providers: [ - XtreamStore - ], + providers: [], templateUrl: './recently-viewed.component.html', styleUrl: './recently-viewed.component.scss', }) diff --git a/apps/web/src/app/xtream-tauri/search-results/global-search-results.component.ts b/apps/web/src/app/xtream-tauri/search-results/global-search-results.component.ts new file mode 100644 index 000000000..49da78548 --- /dev/null +++ b/apps/web/src/app/xtream-tauri/search-results/global-search-results.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; +import { SearchResultsComponent } from './search-results.component'; +import { XtreamStore } from '../stores/xtream.store'; + +@Component({ + selector: 'app-global-search-results', + imports: [SearchResultsComponent], + providers: [XtreamStore], + template: '' +}) +export class GlobalSearchResultsComponent {} From c173553d78bf936d4cc374cd81a2fa83d7b84039 Mon Sep 17 00:00:00 2001 From: 4gray Date: Mon, 12 Jan 2026 00:54:00 +0100 Subject: [PATCH 06/17] feat(xtream): add recently added views --- .../events/database/recently-viewed.events.ts | 6 +- .../category-content-view.component.ts | 28 ++++++--- .../navigation/navigation.component.html | 12 ++++ .../navigation/navigation.component.ts | 4 +- .../recently-added.component.html | 57 ++++++++++++++++++ .../recently-added.component.scss | 37 ++++++++++++ .../recently-added.component.ts | 59 +++++++++++++++++++ .../xtream-main-container.component.ts | 42 ++++++++----- .../xtream-shell/xtream-shell.component.ts | 12 ++-- .../web/src/app/xtream-tauri/xtream.routes.ts | 21 +++++++ 10 files changed, 245 insertions(+), 33 deletions(-) create mode 100644 apps/web/src/app/xtream-tauri/recently-added/recently-added.component.html create mode 100644 apps/web/src/app/xtream-tauri/recently-added/recently-added.component.scss create mode 100644 apps/web/src/app/xtream-tauri/recently-added/recently-added.component.ts diff --git a/apps/electron-backend/src/app/events/database/recently-viewed.events.ts b/apps/electron-backend/src/app/events/database/recently-viewed.events.ts index 9fdf2232e..38d47b769 100644 --- a/apps/electron-backend/src/app/events/database/recently-viewed.events.ts +++ b/apps/electron-backend/src/app/events/database/recently-viewed.events.ts @@ -87,11 +87,7 @@ ipcMain.handle('DB_GET_RECENT_ITEMS', async (event, playlistId: string) => { schema.content, eq(schema.recentlyViewed.contentId, schema.content.id) ) - .innerJoin( - schema.categories, - eq(schema.content.categoryId, schema.categories.id) - ) - .where(eq(schema.categories.playlistId, playlistId)) + .where(eq(schema.recentlyViewed.playlistId, playlistId)) .orderBy(desc(schema.recentlyViewed.viewedAt)) .limit(100); return result; diff --git a/apps/web/src/app/xtream-tauri/category-content-view/category-content-view.component.ts b/apps/web/src/app/xtream-tauri/category-content-view/category-content-view.component.ts index 116d891aa..79f550092 100644 --- a/apps/web/src/app/xtream-tauri/category-content-view/category-content-view.component.ts +++ b/apps/web/src/app/xtream-tauri/category-content-view/category-content-view.component.ts @@ -53,11 +53,15 @@ export class CategoryContentViewComponent implements OnInit { // Only set category if it's different from the currently selected one // This preserves the page state when navigating back from detail view - if ( - categoryId && - this.store.selectedCategoryId() !== Number(categoryId) - ) { - this.store.setSelectedCategory(categoryId); + if (categoryId) { + if (this.store.selectedCategoryId() !== Number(categoryId)) { + this.store.setSelectedCategory(categoryId); + } + } else { + // No categoryId in route means "All Items" + if (this.store.selectedCategoryId() !== null) { + this.store.setSelectedCategory(null); + } } } @@ -99,9 +103,17 @@ export class CategoryContentViewComponent implements OnInit { if (this.isStalker) { this.store.setSelectedItem(selectedItem); } else { - this.router.navigate([item.xtream_id], { - relativeTo: this.activatedRoute, - }); + // When viewing "Recently Added" (no category selected), include category_id in path + const categoryId = this.store.selectedCategoryId(); + if (categoryId) { + this.router.navigate([item.xtream_id], { + relativeTo: this.activatedRoute, + }); + } else { + this.router.navigate([item.category_id, item.xtream_id], { + relativeTo: this.activatedRoute, + }); + } } } diff --git a/apps/web/src/app/xtream-tauri/navigation/navigation.component.html b/apps/web/src/app/xtream-tauri/navigation/navigation.component.html index f611d27b4..a27f7f634 100644 --- a/apps/web/src/app/xtream-tauri/navigation/navigation.component.html +++ b/apps/web/src/app/xtream-tauri/navigation/navigation.component.html @@ -61,6 +61,18 @@ {{ 'PORTALS.SIDEBAR.LIBRARY' | translate }}
--> + @if (!isStalkerPlaylist()) { + + new_releases + + } (); readonly categoryClick = output(); - readonly pageClicked = output<'search' | 'recent' | 'favorites'>(); + readonly pageClicked = output<'search' | 'recent' | 'favorites' | 'recently-added'>(); readonly currentPlaylist = this.store.selectSignal( selectPlaylistById(this.activatedRoute.snapshot.params.id) @@ -113,7 +113,7 @@ export class NavigationComponent { }); } - pageSwitch(page: 'search' | 'recent' | 'favorites') { + pageSwitch(page: 'search' | 'recent' | 'favorites' | 'recently-added') { this.pageClicked.emit(page); this.xtreamStore.setSelectedContentType(undefined); } diff --git a/apps/web/src/app/xtream-tauri/recently-added/recently-added.component.html b/apps/web/src/app/xtream-tauri/recently-added/recently-added.component.html new file mode 100644 index 000000000..906c9983c --- /dev/null +++ b/apps/web/src/app/xtream-tauri/recently-added/recently-added.component.html @@ -0,0 +1,57 @@ +
+

{{ 'PORTALS.SIDEBAR.RECENTLY_ADDED' | translate }}

+ + @if (recentlyAddedVod().length > 0) { +
+

{{ 'PORTALS.SIDEBAR.MOVIES' | translate }}

+ +
+ } + + @if (recentlyAddedSeries().length > 0) { +
+

{{ 'PORTALS.SIDEBAR.SERIES' | translate }}

+ +
+ } + + @if (recentlyAddedLive().length > 0) { +
+

{{ 'PORTALS.SIDEBAR.LIVE_TV' | translate }}

+ +
+ } +
diff --git a/apps/web/src/app/xtream-tauri/recently-added/recently-added.component.scss b/apps/web/src/app/xtream-tauri/recently-added/recently-added.component.scss new file mode 100644 index 000000000..a1e88cdfe --- /dev/null +++ b/apps/web/src/app/xtream-tauri/recently-added/recently-added.component.scss @@ -0,0 +1,37 @@ +:host { + display: block; + height: 100%; + overflow-y: auto; +} + +.recently-added-page { + padding: 20px; +} + +h2 { + margin-bottom: 24px; +} + +.section { + margin-bottom: 32px; + + h3 { + margin-bottom: 16px; + opacity: 0.8; + } +} + +.carousel { + display: flex; + overflow-x: auto; + gap: 16px; + padding-bottom: 16px; + + scrollbar-width: thin; + + .carousel-item { + min-width: 160px; + width: 160px; + flex-shrink: 0; + } +} diff --git a/apps/web/src/app/xtream-tauri/recently-added/recently-added.component.ts b/apps/web/src/app/xtream-tauri/recently-added/recently-added.component.ts new file mode 100644 index 000000000..d7dd99abf --- /dev/null +++ b/apps/web/src/app/xtream-tauri/recently-added/recently-added.component.ts @@ -0,0 +1,59 @@ +import { DatePipe } from '@angular/common'; +import { Component, computed, inject } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; +import { TranslatePipe } from '@ngx-translate/core'; +import { SearchResultItemComponent } from '../../shared/components/search-result-item/search-result-item.component'; +import { ContentType } from '../xtream-state'; +import { XtreamStore } from '../stores/xtream.store'; + +@Component({ + selector: 'app-recently-added', + templateUrl: './recently-added.component.html', + styleUrls: ['./recently-added.component.scss'], + imports: [DatePipe, SearchResultItemComponent, TranslatePipe], +}) +export class RecentlyAddedComponent { + private readonly xtreamStore = inject(XtreamStore); + private readonly router = inject(Router); + private readonly activatedRoute = inject(ActivatedRoute); + + readonly recentlyAddedLive = computed(() => + this.getRecentlyAdded(this.xtreamStore.liveStreams()) + ); + readonly recentlyAddedVod = computed(() => + this.getRecentlyAdded(this.xtreamStore.vodStreams()) + ); + readonly recentlyAddedSeries = computed(() => + this.getRecentlyAdded(this.xtreamStore.serialStreams(), true) + ); + + private getRecentlyAdded(items: any[], isSeries = false) { + return [...items] + .sort((a, b) => { + const dateA = parseInt(isSeries ? a.last_modified : a.added) || 0; + const dateB = parseInt(isSeries ? b.last_modified : b.added) || 0; + return dateB - dateA; + }) + .slice(0, 20); + } + + getDate(item: any): number { + const timestamp = item.added || item.last_modified; + return parseInt(timestamp) * 1000; + } + + openItem(item: any, type: ContentType) { + this.xtreamStore.setSelectedContentType(type); + + if (type === 'live') { + this.router.navigate(['..', type, item.category_id], { + relativeTo: this.activatedRoute, + }); + } else { + const itemId = item.xtream_id || (type === 'series' ? item.series_id : item.stream_id); + this.router.navigate(['..', type, item.category_id, itemId], { + relativeTo: this.activatedRoute, + }); + } + } +} diff --git a/apps/web/src/app/xtream-tauri/xtream-main-container.component.ts b/apps/web/src/app/xtream-tauri/xtream-main-container.component.ts index 197fee986..de8ad55e9 100644 --- a/apps/web/src/app/xtream-tauri/xtream-main-container.component.ts +++ b/apps/web/src/app/xtream-tauri/xtream-main-container.component.ts @@ -60,30 +60,43 @@ export class XtreamMainContainerComponent implements OnInit { } getContentLabel(): string { - if ( - this.xtreamStore.getSelectedCategory() === null || - this.xtreamStore.getSelectedCategory() === undefined - ) { - return this.translateService.instant('PORTALS.SELECT_CATEGORY'); - } else { - const selectedCategory = this.xtreamStore.getSelectedCategory(); - const categoryName = selectedCategory - ? (selectedCategory as any).name - : 'Category Content'; + const selectedCategoryId = this.xtreamStore.selectedCategoryId(); - // Show page number when viewing category content (not detail view) + // When no category is selected, show "Recently Added" + if (selectedCategoryId === null || selectedCategoryId === undefined) { + const recentlyAddedLabel = this.translateService.instant('PORTALS.SIDEBAR.RECENTLY_ADDED'); + + // Show page number when viewing recently added (not detail view) if ( !this.xtreamStore.selectedItem() && this.xtreamStore.getTotalPages() > 1 ) { - const currentPage = this.xtreamStore.page() + 1; // +1 because page is 0-indexed + const currentPage = this.xtreamStore.page() + 1; const totalPages = this.xtreamStore.getTotalPages(); const pageLabel = this.translateService.instant('PORTALS.PAGE'); - return `${categoryName} (${pageLabel} ${currentPage}/${totalPages})`; + return `${recentlyAddedLabel} (${pageLabel} ${currentPage}/${totalPages})`; } - return `Content for ${categoryName}`; + return recentlyAddedLabel; } + + const selectedCategory = this.xtreamStore.getSelectedCategory(); + const categoryName = selectedCategory + ? (selectedCategory as any).name + : 'Category Content'; + + // Show page number when viewing category content (not detail view) + if ( + !this.xtreamStore.selectedItem() && + this.xtreamStore.getTotalPages() > 1 + ) { + const currentPage = this.xtreamStore.page() + 1; // +1 because page is 0-indexed + const totalPages = this.xtreamStore.getTotalPages(); + const pageLabel = this.translateService.instant('PORTALS.PAGE'); + return `${categoryName} (${pageLabel} ${currentPage}/${totalPages})`; + } + + return categoryName; } historyBack() { @@ -105,6 +118,7 @@ export class XtreamMainContainerComponent implements OnInit { data: { playlistId, contentType, + itemCounts: this.categoryItemCounts(), }, width: '500px', maxHeight: '80vh', diff --git a/apps/web/src/app/xtream-tauri/xtream-shell/xtream-shell.component.ts b/apps/web/src/app/xtream-tauri/xtream-shell/xtream-shell.component.ts index 9583e1d5d..9d2fc9433 100644 --- a/apps/web/src/app/xtream-tauri/xtream-shell/xtream-shell.component.ts +++ b/apps/web/src/app/xtream-tauri/xtream-shell/xtream-shell.component.ts @@ -57,11 +57,16 @@ export class XtreamShellComponent { this.route.params.pipe(takeUntilDestroyed()).subscribe(async (params) => { const newPlaylistId = params['id']; + // Skip if playlist ID hasn't changed (after first load) + if (!this.isFirstLoad && this.currentPlaylistId === newPlaylistId) { + return; + } + if (this.isFirstLoad) { // First load - just set the playlist ID without resetting this.xtreamStore.setPlaylistId(newPlaylistId); this.isFirstLoad = false; - } else if (this.currentPlaylistId !== newPlaylistId) { + } else { // Switching to a different playlist - reset and set new ID this.xtreamStore.resetStore(newPlaylistId); } @@ -85,7 +90,6 @@ export class XtreamShellComponent { this.xtreamStore.initializeContent(); } }, - { allowSignalWrites: true } ); } @@ -96,10 +100,10 @@ export class XtreamShellComponent { }); } - handlePageClick(page: 'search' | 'recent' | 'favorites') { + handlePageClick(page: 'search' | 'recent' | 'favorites' | 'recently-added') { this.xtreamStore.setSelectedContentType(undefined); this.router.navigate([page], { relativeTo: this.route, }); } -} +} \ No newline at end of file diff --git a/apps/web/src/app/xtream-tauri/xtream.routes.ts b/apps/web/src/app/xtream-tauri/xtream.routes.ts index 591297b57..df8db0228 100644 --- a/apps/web/src/app/xtream-tauri/xtream.routes.ts +++ b/apps/web/src/app/xtream-tauri/xtream.routes.ts @@ -36,6 +36,13 @@ export const xtreamRoutes: Route[] = [ (c) => c.XtreamMainContainerComponent ), children: [ + { + path: '', + loadComponent: () => + import( + './category-content-view/category-content-view.component' + ).then((c) => c.CategoryContentViewComponent), + }, { path: ':categoryId', loadComponent: () => @@ -59,6 +66,13 @@ export const xtreamRoutes: Route[] = [ (c) => c.XtreamMainContainerComponent ), children: [ + { + path: '', + loadComponent: () => + import( + './category-content-view/category-content-view.component' + ).then((c) => c.CategoryContentViewComponent), + }, { path: ':categoryId', loadComponent: () => @@ -96,6 +110,13 @@ export const xtreamRoutes: Route[] = [ (c) => c.SearchResultsComponent ), }, + { + path: 'recently-added', + loadComponent: () => + import('./recently-added/recently-added.component').then( + (c) => c.RecentlyAddedComponent + ), + }, ], }, ]; From 54f4abb48bc12918889c54632c66dc81daa58c4a Mon Sep 17 00:00:00 2001 From: 4gray Date: Mon, 12 Jan 2026 00:54:55 +0100 Subject: [PATCH 07/17] fix(xtream): track and show import progress of playlist --- .../src/app/api/main.preload.ts | 10 ++++++ .../src/app/events/database/content.events.ts | 4 +-- .../electron-xtream-data-source.ts | 14 ++++++-- .../data-sources/pwa-xtream-data-source.ts | 12 ++++++- .../xtream-data-source.interface.ts | 6 +++- .../stores/features/with-content.feature.ts | 32 +++++++++++++++---- global.d.ts | 3 ++ .../src/lib/database-electron.service.ts | 25 ++++++++------- 8 files changed, 82 insertions(+), 24 deletions(-) diff --git a/apps/electron-backend/src/app/api/main.preload.ts b/apps/electron-backend/src/app/api/main.preload.ts index db4ee78c8..5da932f3b 100644 --- a/apps/electron-backend/src/app/api/main.preload.ts +++ b/apps/electron-backend/src/app/api/main.preload.ts @@ -28,6 +28,16 @@ contextBridge.exposeInMainWorld('electron', { ) => { ipcRenderer.on('EPG_PROGRESS_UPDATE', (_event, data) => callback(data)); }, + // DB save content progress listener + onDbSaveContentProgress: (callback: (count: number) => void) => { + ipcRenderer.on('DB_SAVE_CONTENT_PROGRESS', (_event, count) => + callback(count) + ); + }, + // Remove DB save content progress listener + removeDbSaveContentProgress: () => { + ipcRenderer.removeAllListeners('DB_SAVE_CONTENT_PROGRESS'); + }, getAppVersion: () => ipcRenderer.invoke('get-app-version'), platform: process.platform, fetchPlaylistByUrl: (url: string, title?: string) => diff --git a/apps/electron-backend/src/app/events/database/content.events.ts b/apps/electron-backend/src/app/events/database/content.events.ts index 53baa54d2..571b4392f 100644 --- a/apps/electron-backend/src/app/events/database/content.events.ts +++ b/apps/electron-backend/src/app/events/database/content.events.ts @@ -184,8 +184,8 @@ ipcMain.handle( ); totalInserted += chunk.length; - // Send progress update - event.sender.send('DB_SAVE_CONTENT_PROGRESS', totalInserted); + // Send incremental progress update (chunk size, not cumulative total) + event.sender.send('DB_SAVE_CONTENT_PROGRESS', chunk.length); } return { success: true, count: totalInserted }; diff --git a/apps/web/src/app/xtream-tauri/data-sources/electron-xtream-data-source.ts b/apps/web/src/app/xtream-tauri/data-sources/electron-xtream-data-source.ts index 18b022ef8..521d0d8f3 100644 --- a/apps/web/src/app/xtream-tauri/data-sources/electron-xtream-data-source.ts +++ b/apps/web/src/app/xtream-tauri/data-sources/electron-xtream-data-source.ts @@ -184,7 +184,9 @@ export class ElectronXtreamDataSource implements IXtreamDataSource { async getContent( playlistId: string, credentials: XtreamCredentials, - type: StreamType + type: StreamType, + onProgress?: (count: number) => void, + onTotal?: (total: number) => void ): Promise { // Check if we have cached data const exists = await this.dbService.hasXtreamContent(playlistId, type); @@ -198,11 +200,17 @@ export class ElectronXtreamDataSource implements IXtreamDataSource { const remoteData = await this.apiService.getStreams(credentials, type); if (remoteData && Array.isArray(remoteData) && remoteData.length > 0) { - // Save to cache + // Report total items to import + if (onTotal) { + onTotal(remoteData.length); + } + + // Save to cache with progress tracking await this.dbService.saveXtreamContent( playlistId, remoteData as any[], - type + type, + onProgress ); } diff --git a/apps/web/src/app/xtream-tauri/data-sources/pwa-xtream-data-source.ts b/apps/web/src/app/xtream-tauri/data-sources/pwa-xtream-data-source.ts index 20f82bae1..dc8b37136 100644 --- a/apps/web/src/app/xtream-tauri/data-sources/pwa-xtream-data-source.ts +++ b/apps/web/src/app/xtream-tauri/data-sources/pwa-xtream-data-source.ts @@ -167,7 +167,9 @@ export class PwaXtreamDataSource implements IXtreamDataSource { async getContent( playlistId: string, credentials: XtreamCredentials, - type: StreamType + type: StreamType, + onProgress?: (count: number) => void, + onTotal?: (total: number) => void ): Promise { const cacheKey = `${playlistId}-${type}-content`; @@ -179,6 +181,14 @@ export class PwaXtreamDataSource implements IXtreamDataSource { // Fetch from API const content = await this.apiService.getStreams(credentials, type); + // Report total and progress (PWA doesn't have incremental save, so report all at once) + if (onTotal) { + onTotal(content.length); + } + if (onProgress) { + onProgress(content.length); + } + // Cache in memory this.contentCache.set(cacheKey, content); diff --git a/apps/web/src/app/xtream-tauri/data-sources/xtream-data-source.interface.ts b/apps/web/src/app/xtream-tauri/data-sources/xtream-data-source.interface.ts index 9cfd4afb8..9a01c97f8 100644 --- a/apps/web/src/app/xtream-tauri/data-sources/xtream-data-source.interface.ts +++ b/apps/web/src/app/xtream-tauri/data-sources/xtream-data-source.interface.ts @@ -199,11 +199,15 @@ export interface IXtreamDataSource { /** * Get content/streams for a playlist and type + * @param onProgress - Optional callback for import progress (count) + * @param onTotal - Optional callback for total items to import */ getContent( playlistId: string, credentials: XtreamCredentials, - type: StreamType + type: StreamType, + onProgress?: ProgressCallback, + onTotal?: (total: number) => void ): Promise; /** diff --git a/apps/web/src/app/xtream-tauri/stores/features/with-content.feature.ts b/apps/web/src/app/xtream-tauri/stores/features/with-content.feature.ts index 6170b033d..6e5921d01 100644 --- a/apps/web/src/app/xtream-tauri/stores/features/with-content.feature.ts +++ b/apps/web/src/app/xtream-tauri/stores/features/with-content.feature.ts @@ -181,7 +181,7 @@ export function withContent() { }, /** - * Fetch all content/streams in parallel + * Fetch all content/streams in parallel with progress tracking */ async fetchAllContent(): Promise { const ctx = getCredentialsFromStore(); @@ -189,22 +189,42 @@ export function withContent() { patchState(store, { isLoadingContent: true }); + // Track combined progress across all content types + let totalItems = 0; + let importedItems = 0; + + const onTotal = (count: number) => { + totalItems += count; + patchState(store, { itemsToImport: totalItems }); + }; + + const onProgress = (count: number) => { + importedItems += count; + patchState(store, { importCount: importedItems }); + }; + try { const [live, vod, series] = await Promise.all([ dataSource.getContent( ctx.playlistId, ctx.credentials, - 'live' + 'live', + onProgress, + onTotal ), dataSource.getContent( ctx.playlistId, ctx.credentials, - 'movie' + 'movie', + onProgress, + onTotal ), dataSource.getContent( ctx.playlistId, ctx.credentials, - 'series' + 'series', + onProgress, + onTotal ), ]); @@ -227,13 +247,13 @@ export function withContent() { const ctx = getCredentialsFromStore(); if (!ctx) return; - patchState(store, { isImporting: true, importCount: 0 }); + patchState(store, { isImporting: true, importCount: 0, itemsToImport: 0 }); try { // Fetch categories first await this.fetchAllCategories(); - // Then fetch content + // Then fetch content (with progress tracking) await this.fetchAllContent(); // Restore user data if needed diff --git a/global.d.ts b/global.d.ts index 5efe28ee2..793ef6ce8 100644 --- a/global.d.ts +++ b/global.d.ts @@ -210,6 +210,9 @@ declare global { error?: string; }) => void ) => void; + // DB save content progress listener + onDbSaveContentProgress: (callback: (count: number) => void) => void; + removeDbSaveContentProgress: () => void; dbDeleteAllPlaylists: () => Promise<{ success: boolean }>; }; process: NodeJS.Process; diff --git a/libs/services/src/lib/database-electron.service.ts b/libs/services/src/lib/database-electron.service.ts index e51f8498d..929ef648b 100644 --- a/libs/services/src/lib/database-electron.service.ts +++ b/libs/services/src/lib/database-electron.service.ts @@ -237,19 +237,22 @@ export class DatabaseService { ): Promise { // Setup progress listener if callback provided if (onProgress) { - window.addEventListener('message', (event) => { - if (event.data.type === 'DB_SAVE_CONTENT_PROGRESS') { - onProgress(event.data.count); - } - }); + window.electron.onDbSaveContentProgress(onProgress); } - const result = await window.electron.dbSaveContent( - playlistId, - streams, - type - ); - return result.count; + try { + const result = await window.electron.dbSaveContent( + playlistId, + streams, + type + ); + return result.count; + } finally { + // Clean up the listener + if (onProgress) { + window.electron.removeDbSaveContentProgress(); + } + } } /** From 76191b415f0e7f3bac69c4973e2272aaabf352fe Mon Sep 17 00:00:00 2001 From: 4gray Date: Mon, 12 Jan 2026 00:55:18 +0100 Subject: [PATCH 08/17] refactor(xtream): refine search result card layout --- .../search-result-item.component.ts | 83 +++++-- .../portal-channels-list.component.ts | 3 - .../search-results.component.html | 69 ++++-- .../search-results.component.scss | 209 +++++++++++------- .../search-results.component.ts | 6 +- apps/web/src/assets/i18n/en.json | 5 + 6 files changed, 250 insertions(+), 125 deletions(-) diff --git a/apps/web/src/app/shared/components/search-result-item/search-result-item.component.ts b/apps/web/src/app/shared/components/search-result-item/search-result-item.component.ts index cae49b44e..9a914738c 100644 --- a/apps/web/src/app/shared/components/search-result-item/search-result-item.component.ts +++ b/apps/web/src/app/shared/components/search-result-item/search-result-item.component.ts @@ -25,9 +25,6 @@ import { MatIconModule } from '@angular/material/icon'; @if (description) {

{{ description }}

} - @if (type) { -
{{ type }}
- } @if (showPlaylistInfo && playlistName) {
playlist_play @@ -40,46 +37,92 @@ import { MatIconModule } from '@angular/material/icon'; ` :host { display: block; + height: 100%; } mat-card { cursor: pointer; - } - - h4 { - margin: 0; - padding: 8px; + height: 100%; + display: flex; + flex-direction: column; + position: relative; + overflow: hidden; + background: transparent !important; + box-shadow: none !important; + border: none !important; + padding: 0 !important; } .poster { width: 100%; + aspect-ratio: 2/3; object-fit: cover; + display: block; + border-radius: 8px; } .poster-placeholder { width: 100%; - height: 200px; + aspect-ratio: 2/3; display: flex; align-items: center; justify-content: center; + background: rgba(0, 0, 0, 0.05); + border-radius: 8px; + + mat-icon { + font-size: 48px; + width: 48px; + height: 48px; + opacity: 0.3; + } } - .type-badge { - position: absolute; - top: 8px; - right: 8px; - padding: 4px 8px; - border-radius: 4px; - font-size: 12px; - text-transform: uppercase; + h4 { + margin: 0; + padding: 10px 0 2px 0; + font-size: 0.9rem; + font-weight: 500; + line-height: 1.3em; + height: 2.6em; /* Fixed height for 2 lines */ + color: var(--text-color, #fff); + + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + text-overflow: ellipsis; + } + + p { + margin: 0; + padding: 0 0 10px 0; + font-size: 0.75rem; + line-height: 1.2em; + height: 1.2em; /* Fixed height for 1 line */ + color: var(--text-color-secondary, rgba(255, 255, 255, 0.7)); + opacity: 0.9; /* Improved readability */ + + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .playlist-badge { display: flex; align-items: center; gap: 4px; - margin-top: 8px; - font-size: 12px; + margin-top: auto; + padding: 0 0 10px 0; + font-size: 11px; + opacity: 0.8; + color: var(--text-color-secondary, inherit); + + mat-icon { + font-size: 14px; + width: 14px; + height: 14px; + } } `, ], @@ -93,4 +136,4 @@ export class SearchResultItemComponent { @Input() showPlaylistInfo = false; @Output() itemClick = new EventEmitter(); -} +} \ No newline at end of file diff --git a/apps/web/src/app/xtream-tauri/portal-channels-list/portal-channels-list.component.ts b/apps/web/src/app/xtream-tauri/portal-channels-list/portal-channels-list.component.ts index 2e3ac8e41..0729ff577 100644 --- a/apps/web/src/app/xtream-tauri/portal-channels-list/portal-channels-list.component.ts +++ b/apps/web/src/app/xtream-tauri/portal-channels-list/portal-channels-list.component.ts @@ -89,14 +89,11 @@ export class PortalChannelsListComponent implements AfterViewInit { this.favoritesService .getFavorites(playlist.id) .subscribe((favorites) => { - // Map using content.id instead of xtream_id favorites.forEach((fav: any) => { this.favorites.set(fav.xtream_id, true); }); - console.log(this.favorites); }); } - // Removed loadCurrentEpgData() call since we're using virtual scroll } ngAfterViewInit() { diff --git a/apps/web/src/app/xtream-tauri/search-results/search-results.component.html b/apps/web/src/app/xtream-tauri/search-results/search-results.component.html index acb1ede0d..8aa520953 100644 --- a/apps/web/src/app/xtream-tauri/search-results/search-results.component.html +++ b/apps/web/src/app/xtream-tauri/search-results/search-results.component.html @@ -1,24 +1,43 @@
-
-

{{ 'PORTALS.SIDEBAR.SEARCH' | translate }}

- @if (isGlobalSearch) { - - } -
+ diff --git a/apps/web/src/app/xtream-tauri/search-results/search-results.component.scss b/apps/web/src/app/xtream-tauri/search-results/search-results.component.scss index 13d6fc1b6..532921c11 100644 --- a/apps/web/src/app/xtream-tauri/search-results/search-results.component.scss +++ b/apps/web/src/app/xtream-tauri/search-results/search-results.component.scss @@ -7,31 +7,75 @@ } .search-page { - padding: 20px; + padding: 0 20px 20px 20px; } .header { - display: flex; - justify-content: space-between; - align-items: center; + position: sticky; + top: 0; + z-index: 100; + // Use theme background variable or fallback + background: var(--background, var(--mat-sidenav-content-background-color, #fff)); + padding: 20px 0 10px 0; margin-bottom: 20px; -} + // Subtle shadow or border for separation + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.02); + + display: flex; + flex-direction: column; + align-items: stretch; + gap: 16px; -.search-container { - flex-shrink: 0; - - .search-field { - width: 100%; + .header-top { + display: flex; + justify-content: space-between; + align-items: center; + gap: 20px; } - .type-filters { - mat-checkbox { - margin-right: 1rem; + .header-left { + display: flex; + align-items: center; + gap: 32px; + flex: 1; + flex-wrap: wrap; + + h2 { + margin: 0; + white-space: nowrap; + } + + .type-filters { + display: flex; + gap: 20px; + align-items: center; + + mat-checkbox { + margin: 0; + } } } - .results-count { - font-size: 0.9rem; + .header-right { + display: flex; + align-items: center; + gap: 16px; + + .results-count { + font-size: 0.85rem; + color: var(--text-color-secondary, rgba(0,0,0,0.7)); + background: rgba(0, 0, 0, 0.06); + padding: 6px 12px; + border-radius: 16px; + white-space: nowrap; + font-weight: 500; + display: flex; + align-items: center; + } + } + + .search-input-container { + margin-bottom: 0; } } @@ -50,54 +94,72 @@ mat-card { cursor: pointer; + transition: transform 0.2s, box-shadow 0.2s; + + &:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0,0,0,0.1); + } + .poster, .poster-placeholder { - border-radius: 13px; + border-radius: 8px; width: 100%; aspect-ratio: 2/3; object-fit: cover; - margin-bottom: 0.5rem; + margin-bottom: 0.75rem; } .poster-placeholder { display: flex; align-items: center; justify-content: center; + background: rgba(0,0,0,0.05); + mat-icon { font-size: 48px; width: 48px; height: 48px; + opacity: 0.3; } } h3 { margin: 0.5rem 0; font-size: 1rem; - font-weight: normal; + font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } p { - font-size: 0.8rem; - color: rgba(0, 0, 0, 0.6); - margin: 0.5rem 0; + font-size: 0.85rem; + color: var(--text-color-secondary, rgba(0, 0, 0, 0.6)); + margin: 0 0 0.75rem 0; + line-height: 1.4; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; } .type-badge { display: inline-block; padding: 0.25rem 0.5rem; border-radius: 4px; - font-size: 0.8rem; + font-size: 0.75rem; + font-weight: 600; text-transform: uppercase; + letter-spacing: 0.5px; + &.live { - background: #e53935; - color: white; + background: rgba(229, 57, 53, 0.1); + color: #e53935; } &.movie { - background: #1e88e5; - color: white; + background: rgba(30, 136, 229, 0.1); + color: #1e88e5; } &.series { - background: #43a047; - color: white; + background: rgba(67, 160, 71, 0.1); + color: #43a047; } } } @@ -109,18 +171,45 @@ flex-direction: column; align-items: center; justify-content: center; - padding: 2rem; + padding: 4rem 2rem; + text-align: center; mat-icon { - font-size: 48px; - width: 48px; - height: 48px; - margin-bottom: 1rem; + font-size: 64px; + width: 64px; + height: 64px; + margin-bottom: 1.5rem; + opacity: 0.5; } - p { - font-size: 1.1rem; - margin: 0; + .no-items-content { + h3 { + margin: 0 0 0.5rem; + font-size: 1.5rem; + font-weight: 500; + color: var(--text-color); + } + + p { + font-size: 1.1rem; + margin: 0; + opacity: 0.8; + } + + .search-term-note { + margin-top: 1rem; + font-size: 0.9rem; + font-style: italic; + opacity: 0.6; + } + } + + &.initial-state { + opacity: 0.8; + mat-icon { + color: var(--primary-color); + opacity: 0.6; + } } } @@ -130,6 +219,11 @@ height: 100vh; } + /* Ensure sticky header background matches overlay background */ + .header { + background: var(--background); + } + .results-grid { mat-card { position: relative; @@ -200,49 +294,6 @@ } :host-context(.global-search-overlay) { - .search-page { - background: var(--background); - height: 100vh; - } - - .results-grid { - mat-card { - position: relative; - - .poster, - .poster-placeholder { - margin-top: 24px; - } - - .playlist-badge { - position: absolute; - top: 0; - left: 0; - right: 0; - background: rgba(0, 0, 0, 0.7); - padding: 6px 8px; - font-size: 12px; - color: white; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - border-top-left-radius: inherit; - border-top-right-radius: inherit; - backdrop-filter: blur(5px); - display: flex; - align-items: center; - gap: 4px; - - mat-icon { - font-size: 14px; - width: 14px; - height: 14px; - flex-shrink: 0; - } - } - } - } - .playlist-group { .playlist-title { background: var(--surface); diff --git a/apps/web/src/app/xtream-tauri/search-results/search-results.component.ts b/apps/web/src/app/xtream-tauri/search-results/search-results.component.ts index b2a9e35e2..b24d965e8 100644 --- a/apps/web/src/app/xtream-tauri/search-results/search-results.component.ts +++ b/apps/web/src/app/xtream-tauri/search-results/search-results.component.ts @@ -51,9 +51,7 @@ interface SearchResultsData { SearchResultItemComponent, TranslatePipe, ], - providers: [ - XtreamStore - ], + providers: [], templateUrl: './search-results.component.html', styleUrls: ['./search-results.component.scss'], }) @@ -112,7 +110,7 @@ export class SearchResultsComponent implements AfterViewInit { }); } - private executeSearch() { + executeSearch() { const types = Object.entries(this.filters) .filter(([_, enabled]) => enabled) .map(([type]) => type); diff --git a/apps/web/src/assets/i18n/en.json b/apps/web/src/assets/i18n/en.json index 0a5b99de1..a593b21a1 100644 --- a/apps/web/src/assets/i18n/en.json +++ b/apps/web/src/assets/i18n/en.json @@ -384,6 +384,7 @@ "LIBRARY": "Library", "SEARCH": "Search", "RECENT": "Recent", + "RECENTLY_ADDED": "Recently added", "FAVORITES": "Favorites", "HOME": "Home", "PLAYLIST_INFO": "Playlist info", @@ -422,6 +423,10 @@ "TITLE": "No results found", "NO_SEARCH_RESULTS": "Nothing found, try to change you search request" }, + "SEARCH_VIEW": { + "INITIAL_TITLE": "Search for content", + "INITIAL_DESCRIPTION": "Enter at least 3 characters to search across live TV, movies and series." + }, "STREAM_URL_COPIED": "Stream URL copied to clipboard", "COPY_STREAM_URL": "Copy Stream URL", "SELECT_CATEGORY": "Select a category", From 225e9866efa9ce3f0d639df56b919d16bcb240b2 Mon Sep 17 00:00:00 2001 From: 4gray Date: Mon, 12 Jan 2026 00:55:25 +0100 Subject: [PATCH 09/17] chore: bump TS lib for newer ES features --- apps/web/tsconfig.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index 87c0bc490..1d8d0b1d3 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -11,7 +11,7 @@ "experimentalDecorators": true, "target": "ES2022", "typeRoots": ["node_modules/@types"], - "lib": ["es2017", "es2016", "es2015", "dom"], + "lib": ["es2018", "es2017", "es2016", "es2015", "dom"], "resolveJsonModule": true, "strict": false, "skipLibCheck": true, From 0a8c890df5c82847bef36a33a2439bce5397cb67 Mon Sep 17 00:00:00 2001 From: 4gray Date: Mon, 12 Jan 2026 17:50:37 +0100 Subject: [PATCH 10/17] fix: restore ContentType import and optimize sorting logic in selection feature --- .../recently-added.component.ts | 17 +- .../stores/features/with-selection.feature.ts | 390 ++++++++++-------- 2 files changed, 222 insertions(+), 185 deletions(-) diff --git a/apps/web/src/app/xtream-tauri/recently-added/recently-added.component.ts b/apps/web/src/app/xtream-tauri/recently-added/recently-added.component.ts index d7dd99abf..da5c30ae4 100644 --- a/apps/web/src/app/xtream-tauri/recently-added/recently-added.component.ts +++ b/apps/web/src/app/xtream-tauri/recently-added/recently-added.component.ts @@ -3,8 +3,8 @@ import { Component, computed, inject } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { TranslatePipe } from '@ngx-translate/core'; import { SearchResultItemComponent } from '../../shared/components/search-result-item/search-result-item.component'; -import { ContentType } from '../xtream-state'; import { XtreamStore } from '../stores/xtream.store'; +import { ContentType } from '../xtream-state'; @Component({ selector: 'app-recently-added', @@ -30,8 +30,10 @@ export class RecentlyAddedComponent { private getRecentlyAdded(items: any[], isSeries = false) { return [...items] .sort((a, b) => { - const dateA = parseInt(isSeries ? a.last_modified : a.added) || 0; - const dateB = parseInt(isSeries ? b.last_modified : b.added) || 0; + const dateA = + parseInt(isSeries ? a.last_modified : a.added) || 0; + const dateB = + parseInt(isSeries ? b.last_modified : b.added) || 0; return dateB - dateA; }) .slice(0, 20); @@ -44,13 +46,16 @@ export class RecentlyAddedComponent { openItem(item: any, type: ContentType) { this.xtreamStore.setSelectedContentType(type); - + if (type === 'live') { - this.router.navigate(['..', type, item.category_id], { + this.router.navigate(['..', type, item.category_id], { relativeTo: this.activatedRoute, }); } else { - const itemId = item.xtream_id || (type === 'series' ? item.series_id : item.stream_id); + const itemId = + item.xtream_id || + item.id || + (type === 'series' ? item.series_id : item.stream_id); this.router.navigate(['..', type, item.category_id, itemId], { relativeTo: this.activatedRoute, }); diff --git a/apps/web/src/app/xtream-tauri/stores/features/with-selection.feature.ts b/apps/web/src/app/xtream-tauri/stores/features/with-selection.feature.ts index 1dc659b59..760e7105b 100644 --- a/apps/web/src/app/xtream-tauri/stores/features/with-selection.feature.ts +++ b/apps/web/src/app/xtream-tauri/stores/features/with-selection.feature.ts @@ -44,39 +44,10 @@ export function withSelection() { return signalStoreFeature( withState(initialSelectionState), - withComputed((store) => ({ - /** - * Get the selected category from the parent store's categories - */ - getSelectedCategory: computed(() => { - const categoryId = store.selectedCategoryId(); - if (!categoryId) { - return { id: 0, name: 'All Items', type: store.selectedContentType() }; - } - - // Access parent store categories (from withContent) - const storeAny = store as any; - const allCategories = [ - ...(storeAny.vodCategories?.() || []), - ...(storeAny.liveCategories?.() || []), - ...(storeAny.serialCategories?.() || []), - ]; - - return allCategories.find( - (c: any) => c.id === categoryId || c.category_id === String(categoryId) - ); - }), - - /** - * Get the selected item by ID from content - */ - getSelectedItemById: computed(() => { + withComputed((store) => { + // Memoized sorted content - only recalculates when content/type changes + const sortedContent = computed(() => { const categoryType = store.selectedContentType(); - const selectedItem = store.selectedItem(); - - if (!selectedItem) return null; - - // Access parent store content (from withContent) const storeAny = store as any; const content = categoryType === 'live' @@ -85,155 +56,213 @@ export function withSelection() { ? storeAny.vodStreams?.() || [] : storeAny.serialStreams?.() || []; - return content.find( - (item: any) => - item.stream_id === selectedItem.stream_id || - item.id === selectedItem.id || - item.series_id === selectedItem.series_id - ); - }), + return [...content].sort((a: any, b: any) => { + const dateA = + parseInt( + categoryType === 'series' + ? a.last_modified + : a.added + ) || 0; + const dateB = + parseInt( + categoryType === 'series' + ? b.last_modified + : b.added + ) || 0; + return dateB - dateA; + }); + }); - /** - * Get paginated content for the selected category - */ - getPaginatedContent: computed(() => { - const startIndex = store.page() * store.limit(); - const endIndex = startIndex + store.limit(); - const categoryId = store.selectedCategoryId(); - const categoryType = store.selectedContentType(); - - // Access parent store content (from withContent) - const storeAny = store as any; - const content = - categoryType === 'live' - ? storeAny.liveStreams?.() || [] - : categoryType === 'vod' - ? storeAny.vodStreams?.() || [] - : storeAny.serialStreams?.() || []; - - let filteredContent = content; - if (categoryId) { - filteredContent = content.filter((item: any) => Number(item.category_id) === categoryId); - } else { - // Filter nothing (all items), sort by added - // Clone to avoid mutation - filteredContent = [...content].sort((a: any, b: any) => { - const dateA = parseInt(categoryType === 'series' ? a.last_modified : a.added) || 0; - const dateB = parseInt(categoryType === 'series' ? b.last_modified : b.added) || 0; - return dateB - dateA; - }); - } - - return filteredContent.slice(startIndex, endIndex); - }), - - /** - * Get all items from the selected category (without pagination) - */ - selectItemsFromSelectedCategory: computed(() => { - const categoryId = store.selectedCategoryId(); - const categoryType = store.selectedContentType(); - - // Access parent store content (from withContent) - const storeAny = store as any; - const content = - categoryType === 'live' - ? storeAny.liveStreams?.() || [] - : categoryType === 'vod' - ? storeAny.vodStreams?.() || [] - : storeAny.serialStreams?.() || []; - - if (!categoryId) { - // Return all items sorted by added - return [...content].sort((a: any, b: any) => { - const dateA = parseInt(categoryType === 'series' ? a.last_modified : a.added) || 0; - const dateB = parseInt(categoryType === 'series' ? b.last_modified : b.added) || 0; - return dateB - dateA; - }); - } - - return content.filter( - (item: any) => Number(item.category_id) === categoryId - ); - }), - - /** - * Get total pages for the selected category - */ - getTotalPages: computed(() => { - const categoryId = store.selectedCategoryId(); - const categoryType = store.selectedContentType(); - - // Access parent store content (from withContent) - const storeAny = store as any; - const content = - categoryType === 'live' - ? storeAny.liveStreams?.() || [] - : categoryType === 'vod' - ? storeAny.vodStreams?.() || [] - : storeAny.serialStreams?.() || []; - - let totalItems = 0; - if (categoryId) { - totalItems = content.filter( - (item: any) => Number(item.category_id) === categoryId - ).length; - } else { - totalItems = content.length; - } - - return Math.ceil(totalItems / store.limit()); - }), - - /** - * Check if paginated content is loading - */ - isPaginatedContentLoading: computed(() => { - // Access parent store loading state (from withContent) - const storeAny = store as any; - return storeAny.isLoadingContent?.() || false; - }), - - /** - * Memoized category item counts map - */ - getCategoryItemCounts: computed(() => { - const type = store.selectedContentType(); - - // Access parent store content (from withContent) - const storeAny = store as any; - const streams = - type === 'live' - ? storeAny.liveStreams?.() || [] - : type === 'vod' - ? storeAny.vodStreams?.() || [] - : storeAny.serialStreams?.() || []; - - const countMap = new Map(); - for (const item of streams) { - const catId = Number(item.category_id); - if (!isNaN(catId)) { - countMap.set(catId, (countMap.get(catId) || 0) + 1); + return { + /** + * Get the selected category from the parent store's categories + */ + getSelectedCategory: computed(() => { + const categoryId = store.selectedCategoryId(); + if (!categoryId) { + return { + id: 0, + name: 'All Items', + type: store.selectedContentType(), + }; } - } - return countMap; - }), - /** - * Get categories for the currently selected content type - */ - getCategoriesBySelectedType: computed(() => { - const type = store.selectedContentType(); + // Access parent store categories (from withContent) + const storeAny = store as any; + const allCategories = [ + ...(storeAny.vodCategories?.() || []), + ...(storeAny.liveCategories?.() || []), + ...(storeAny.serialCategories?.() || []), + ]; - // Access parent store categories (from withContent) - const storeAny = store as any; - return type === 'live' - ? storeAny.liveCategories?.() || [] - : type === 'vod' - ? storeAny.vodCategories?.() || [] - : storeAny.serialCategories?.() || []; - }), - })), + return allCategories.find( + (c: any) => + c.id === categoryId || + c.category_id === String(categoryId) + ); + }), + + /** + * Get the selected item by ID from content + */ + getSelectedItemById: computed(() => { + const categoryType = store.selectedContentType(); + const selectedItem = store.selectedItem(); + + if (!selectedItem) return null; + + // Access parent store content (from withContent) + const storeAny = store as any; + const content = + categoryType === 'live' + ? storeAny.liveStreams?.() || [] + : categoryType === 'vod' + ? storeAny.vodStreams?.() || [] + : storeAny.serialStreams?.() || []; + + return content.find( + (item: any) => + item.stream_id === selectedItem.stream_id || + item.id === selectedItem.id || + item.series_id === selectedItem.series_id + ); + }), + + /** + * Get paginated content for the selected category + */ + getPaginatedContent: computed(() => { + const startIndex = store.page() * store.limit(); + const endIndex = startIndex + store.limit(); + const categoryId = store.selectedCategoryId(); + const categoryType = store.selectedContentType(); + + // Access parent store content (from withContent) + const storeAny = store as any; + const content = + categoryType === 'live' + ? storeAny.liveStreams?.() || [] + : categoryType === 'vod' + ? storeAny.vodStreams?.() || [] + : storeAny.serialStreams?.() || []; + + let filteredContent = content; + if (categoryId) { + filteredContent = content.filter( + (item: any) => + Number(item.category_id) === categoryId + ); + } else { + filteredContent = sortedContent(); + } + + return filteredContent.slice(startIndex, endIndex); + }), + + /** + * Get all items from the selected category (without pagination) + */ + selectItemsFromSelectedCategory: computed(() => { + const categoryId = store.selectedCategoryId(); + const categoryType = store.selectedContentType(); + + // Access parent store content (from withContent) + const storeAny = store as any; + const content = + categoryType === 'live' + ? storeAny.liveStreams?.() || [] + : categoryType === 'vod' + ? storeAny.vodStreams?.() || [] + : storeAny.serialStreams?.() || []; + + if (!categoryId) { + return sortedContent(); + } + + return content.filter( + (item: any) => Number(item.category_id) === categoryId + ); + }), + + /** + * Get total pages for the selected category + */ + getTotalPages: computed(() => { + const categoryId = store.selectedCategoryId(); + const categoryType = store.selectedContentType(); + + // Access parent store content (from withContent) + const storeAny = store as any; + const content = + categoryType === 'live' + ? storeAny.liveStreams?.() || [] + : categoryType === 'vod' + ? storeAny.vodStreams?.() || [] + : storeAny.serialStreams?.() || []; + + let totalItems = 0; + if (categoryId) { + totalItems = content.filter( + (item: any) => + Number(item.category_id) === categoryId + ).length; + } else { + totalItems = content.length; + } + + return Math.ceil(totalItems / store.limit()); + }), + + /** + * Check if paginated content is loading + */ + isPaginatedContentLoading: computed(() => { + // Access parent store loading state (from withContent) + const storeAny = store as any; + return storeAny.isLoadingContent?.() || false; + }), + + /** + * Memoized category item counts map + */ + getCategoryItemCounts: computed(() => { + const type = store.selectedContentType(); + + // Access parent store content (from withContent) + const storeAny = store as any; + const streams = + type === 'live' + ? storeAny.liveStreams?.() || [] + : type === 'vod' + ? storeAny.vodStreams?.() || [] + : storeAny.serialStreams?.() || []; + + const countMap = new Map(); + for (const item of streams) { + const catId = Number(item.category_id); + if (!isNaN(catId)) { + countMap.set(catId, (countMap.get(catId) || 0) + 1); + } + } + return countMap; + }), + + /** + * Get categories for the currently selected content type + */ + getCategoriesBySelectedType: computed(() => { + const type = store.selectedContentType(); + + // Access parent store categories (from withContent) + const storeAny = store as any; + return type === 'live' + ? storeAny.liveCategories?.() || [] + : type === 'vod' + ? storeAny.vodCategories?.() || [] + : storeAny.serialCategories?.() || []; + }), + }; + }), withMethods((store) => ({ /** @@ -252,7 +281,8 @@ export function withSelection() { */ setSelectedCategory(categoryId: number | null): void { patchState(store, { - selectedCategoryId: categoryId !== null ? Number(categoryId) : null, + selectedCategoryId: + categoryId !== null ? Number(categoryId) : null, page: 0, }); }, @@ -292,7 +322,9 @@ export function withSelection() { resetSelection(): void { patchState(store, { ...initialSelectionState, - limit: Number(localStorage.getItem('xtream-page-size') ?? 25), + limit: Number( + localStorage.getItem('xtream-page-size') ?? 25 + ), }); }, })) From aec69196bc12ed71c7492ae77b0f5b6a6b1a59ad Mon Sep 17 00:00:00 2001 From: 4gray Date: Mon, 12 Jan 2026 17:51:02 +0100 Subject: [PATCH 11/17] fix(playlist-switcher): adjust menu dimensions and clean up unused styles --- .../playlist-switcher.component.html | 14 ++++++++++---- .../playlist-switcher.component.scss | 9 ++++----- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/libs/ui/components/src/lib/playlist-switcher/playlist-switcher.component.html b/libs/ui/components/src/lib/playlist-switcher/playlist-switcher.component.html index faa72cfc3..d52ba7dd2 100644 --- a/libs/ui/components/src/lib/playlist-switcher/playlist-switcher.component.html +++ b/libs/ui/components/src/lib/playlist-switcher/playlist-switcher.component.html @@ -54,11 +54,11 @@ [class.selected]="isSelected(playlist)" (click)="selectPlaylist(playlist)" > - @if (isSelected(playlist)) { +
@@ -70,8 +70,14 @@ [class]="getStatusClass(playlist._id)" > } - @if (playlist.autoRefresh && !playlist.serverUrl && !playlist.macAddress) { - autorenew + @if ( + playlist.autoRefresh && + !playlist.serverUrl && + !playlist.macAddress + ) { + autorenew }
diff --git a/libs/ui/components/src/lib/playlist-switcher/playlist-switcher.component.scss b/libs/ui/components/src/lib/playlist-switcher/playlist-switcher.component.scss index cb3a5cc1a..bac983c1f 100644 --- a/libs/ui/components/src/lib/playlist-switcher/playlist-switcher.component.scss +++ b/libs/ui/components/src/lib/playlist-switcher/playlist-switcher.component.scss @@ -67,7 +67,7 @@ // Menu styles (global) ::ng-deep .playlist-switcher-menu { min-width: 320px !important; - max-width: 400px !important; + max-width: 320px !important; border-radius: 12px !important; overflow: hidden !important; @@ -81,7 +81,6 @@ align-items: center; gap: 10px; padding: 14px 16px 10px; - border-bottom: 1px solid rgba(255, 255, 255, 0.08); font-size: 0.85em; font-weight: 600; text-transform: uppercase; @@ -201,7 +200,7 @@ flex-direction: row; align-items: center; gap: 10px; - padding: 10px 16px; + padding: 4px 12px; min-height: 52px; transition: all 0.15s ease; position: relative; @@ -220,7 +219,7 @@ } } -.selected-indicator { +/* .selected-indicator { color: #8b5cf6; font-size: 18px; width: 18px; @@ -232,7 +231,7 @@ width: 18px; height: 18px; flex-shrink: 0; -} +} */ @media (prefers-color-scheme: light) { .playlist-item { From aced6209aa85edcbcc2518a595ab1c40e0ed846e Mon Sep 17 00:00:00 2001 From: 4gray Date: Mon, 12 Jan 2026 17:53:58 +0100 Subject: [PATCH 12/17] fix(stalker-search): remove unused MatButton and MatIcon imports --- .../app/stalker/stalker-search/stalker-search.component.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/apps/web/src/app/stalker/stalker-search/stalker-search.component.ts b/apps/web/src/app/stalker/stalker-search/stalker-search.component.ts index 12b2a6edb..f24ad7edc 100644 --- a/apps/web/src/app/stalker/stalker-search/stalker-search.component.ts +++ b/apps/web/src/app/stalker/stalker-search/stalker-search.component.ts @@ -1,7 +1,5 @@ import { Component, inject, resource, signal } from '@angular/core'; -import { MatButton } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; -import { MatIcon } from '@angular/material/icon'; import { MatProgressSpinner } from '@angular/material/progress-spinner'; import { ActivatedRoute } from '@angular/router'; import { Store } from '@ngrx/store'; @@ -25,8 +23,6 @@ import { StalkerStore } from '../stalker.store'; selector: 'app-stalker-search', imports: [ MatCardModule, - MatButton, - MatIcon, MatProgressSpinner, PlaylistErrorViewComponent, SearchFormComponent, From b422cad26068c62e992464570b0751401654893d Mon Sep 17 00:00:00 2001 From: 4gray Date: Mon, 12 Jan 2026 17:54:06 +0100 Subject: [PATCH 13/17] fix(playlist-switcher): enhance filtered playlists logic to include sorting by import date --- .../playlist-switcher.component.ts | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/libs/ui/components/src/lib/playlist-switcher/playlist-switcher.component.ts b/libs/ui/components/src/lib/playlist-switcher/playlist-switcher.component.ts index 7d5bc29b0..30c7639a5 100644 --- a/libs/ui/components/src/lib/playlist-switcher/playlist-switcher.component.ts +++ b/libs/ui/components/src/lib/playlist-switcher/playlist-switcher.component.ts @@ -63,17 +63,22 @@ export class PlaylistSwitcherComponent { /** All playlists from store */ readonly playlists = this.store.selectSignal(selectAllPlaylistsMeta); - /** Filtered playlists based on search query */ + /** Filtered playlists based on search query, sorted by importDate (newest first) */ readonly filteredPlaylists = computed(() => { const query = this.searchQuery().toLowerCase().trim(); const allPlaylists = this.playlists(); - if (!query) { - return allPlaylists; - } - return allPlaylists.filter( - (p) => - p.title?.toLowerCase().includes(query) || - p.filename?.toLowerCase().includes(query) + const filtered = query + ? allPlaylists.filter( + (p) => + p.title?.toLowerCase().includes(query) || + p.filename?.toLowerCase().includes(query) + ) + : allPlaylists; + + return [...filtered].sort( + (a, b) => + new Date(b.importDate ?? 0).getTime() - + new Date(a.importDate ?? 0).getTime() ); }); From 6cc9000a61a238fd83eeca002572cd900ae200ec Mon Sep 17 00:00:00 2001 From: 4gray Date: Mon, 12 Jan 2026 18:02:54 +0100 Subject: [PATCH 14/17] refactor: code style and formatting --- .../app/xtream-tauri/stores/features/index.ts | 6 ++-- .../stores/features/with-content.feature.ts | 26 +++++++++++------ .../stores/features/with-epg.feature.ts | 18 ++++++++++-- .../stores/features/with-player.feature.ts | 28 ++++++++++++++----- .../stores/features/with-portal.feature.ts | 25 ++++++++++++----- .../stores/features/with-search.feature.ts | 8 ++++-- .../playlist-switcher.component.ts | 28 +++++++++++-------- 7 files changed, 98 insertions(+), 41 deletions(-) diff --git a/apps/web/src/app/xtream-tauri/stores/features/index.ts b/apps/web/src/app/xtream-tauri/stores/features/index.ts index cfa95752e..5e7df6cf9 100644 --- a/apps/web/src/app/xtream-tauri/stores/features/index.ts +++ b/apps/web/src/app/xtream-tauri/stores/features/index.ts @@ -1,6 +1,6 @@ -export * from './with-portal.feature'; export * from './with-content.feature'; -export * from './with-selection.feature'; -export * from './with-search.feature'; export * from './with-epg.feature'; export * from './with-player.feature'; +export * from './with-portal.feature'; +export * from './with-search.feature'; +export * from './with-selection.feature'; diff --git a/apps/web/src/app/xtream-tauri/stores/features/with-content.feature.ts b/apps/web/src/app/xtream-tauri/stores/features/with-content.feature.ts index 6e5921d01..97d2981f4 100644 --- a/apps/web/src/app/xtream-tauri/stores/features/with-content.feature.ts +++ b/apps/web/src/app/xtream-tauri/stores/features/with-content.feature.ts @@ -12,13 +12,12 @@ import { XtreamSerieItem, XtreamVodStream, } from 'shared-interfaces'; -import { ContentType } from '../../xtream-state'; -import { XtreamCredentials } from '../../services/xtream-api.service'; import { - IXtreamDataSource, XTREAM_DATA_SOURCE, XtreamCategoryFromDb, } from '../../data-sources/xtream-data-source.interface'; +import { XtreamCredentials } from '../../services/xtream-api.service'; +import { ContentType } from '../../xtream-state'; /** * Content state for managing categories and streams @@ -247,7 +246,11 @@ export function withContent() { const ctx = getCredentialsFromStore(); if (!ctx) return; - patchState(store, { isImporting: true, importCount: 0, itemsToImport: 0 }); + patchState(store, { + isImporting: true, + importCount: 0, + itemsToImport: 0, + }); try { // Fetch categories first @@ -261,8 +264,10 @@ export function withContent() { const restoreData = localStorage.getItem(restoreKey); if (restoreData) { try { - const { favoritedXtreamIds, recentlyViewedXtreamIds } = - JSON.parse(restoreData); + const { + favoritedXtreamIds, + recentlyViewedXtreamIds, + } = JSON.parse(restoreData); await dataSource.restoreUserData( ctx.playlistId, favoritedXtreamIds, @@ -270,7 +275,10 @@ export function withContent() { ); localStorage.removeItem(restoreKey); } catch (err) { - console.error('Error restoring user data:', err); + console.error( + 'Error restoring user data:', + err + ); } } } finally { @@ -318,7 +326,9 @@ export function withContent() { * Update import progress */ setImportProgress(count: number, total?: number): void { - const updates: Partial = { importCount: count }; + const updates: Partial = { + importCount: count, + }; if (total !== undefined) { updates.itemsToImport = total; } diff --git a/apps/web/src/app/xtream-tauri/stores/features/with-epg.feature.ts b/apps/web/src/app/xtream-tauri/stores/features/with-epg.feature.ts index 2fcc85fd7..acebcb1df 100644 --- a/apps/web/src/app/xtream-tauri/stores/features/with-epg.feature.ts +++ b/apps/web/src/app/xtream-tauri/stores/features/with-epg.feature.ts @@ -1,7 +1,15 @@ import { inject } from '@angular/core'; -import { patchState, signalStoreFeature, withMethods, withState } from '@ngrx/signals'; +import { + patchState, + signalStoreFeature, + withMethods, + withState, +} from '@ngrx/signals'; import { EpgItem } from 'shared-interfaces'; -import { XtreamApiService, XtreamCredentials } from '../../services/xtream-api.service'; +import { + XtreamApiService, + XtreamCredentials, +} from '../../services/xtream-api.service'; /** * EPG state for managing Electronic Program Guide data @@ -105,7 +113,11 @@ export function withEpg() { } try { - return await apiService.getShortEpg(credentials, streamId, 1); + return await apiService.getShortEpg( + credentials, + streamId, + 1 + ); } catch (error) { console.error('Error loading channel EPG:', error); return []; diff --git a/apps/web/src/app/xtream-tauri/stores/features/with-player.feature.ts b/apps/web/src/app/xtream-tauri/stores/features/with-player.feature.ts index 93cf17d00..38bb78d89 100644 --- a/apps/web/src/app/xtream-tauri/stores/features/with-player.feature.ts +++ b/apps/web/src/app/xtream-tauri/stores/features/with-player.feature.ts @@ -1,9 +1,14 @@ import { inject } from '@angular/core'; -import { patchState, signalStoreFeature, withMethods, withState } from '@ngrx/signals'; +import { + patchState, + signalStoreFeature, + withMethods, + withState, +} from '@ngrx/signals'; import { XtreamSerieEpisode, XtreamVodDetails } from 'shared-interfaces'; import { PlayerService } from '../../../services/player.service'; -import { XtreamUrlService } from '../../services/xtream-url.service'; import { XtreamCredentials } from '../../services/xtream-api.service'; +import { XtreamUrlService } from '../../services/xtream-url.service'; /** * Player state for managing stream URLs and player settings @@ -100,7 +105,10 @@ export function withPlayer() { return ''; } - const streamUrl = urlService.constructVodUrl(credentials, vodItem); + const streamUrl = urlService.constructVodUrl( + credentials, + vodItem + ); patchState(store, { streamUrl }); return streamUrl; }, @@ -132,13 +140,15 @@ export function withPlayer() { ): void { const playlist = getPlaylistFromStore(); const storeAny = store as any; - const contentType = storeAny.selectedContentType?.() || 'vod'; + const contentType = + storeAny.selectedContentType?.() || 'vod'; playerService.openPlayer( streamUrl, title, thumbnail, - localStorage.getItem('hideExternalInfoDialog') === 'true', + localStorage.getItem('hideExternalInfoDialog') === + 'true', contentType === 'live', playlist?.userAgent, playlist?.referrer, @@ -150,7 +160,10 @@ export function withPlayer() { * Set hide external info dialog preference */ setHideExternalInfoDialog(hide: boolean): void { - localStorage.setItem('hideExternalInfoDialog', String(hide)); + localStorage.setItem( + 'hideExternalInfoDialog', + String(hide) + ); patchState(store, { hideExternalInfoDialog: hide }); }, @@ -161,7 +174,8 @@ export function withPlayer() { patchState(store, { streamUrl: null, hideExternalInfoDialog: - localStorage.getItem('hideExternalInfoDialog') === 'true', + localStorage.getItem('hideExternalInfoDialog') === + 'true', }); }, }; diff --git a/apps/web/src/app/xtream-tauri/stores/features/with-portal.feature.ts b/apps/web/src/app/xtream-tauri/stores/features/with-portal.feature.ts index a4a07005e..032f35973 100644 --- a/apps/web/src/app/xtream-tauri/stores/features/with-portal.feature.ts +++ b/apps/web/src/app/xtream-tauri/stores/features/with-portal.feature.ts @@ -1,15 +1,22 @@ import { inject } from '@angular/core'; -import { patchState, signalStoreFeature, withMethods, withState } from '@ngrx/signals'; +import { + patchState, + signalStoreFeature, + withMethods, + withState, +} from '@ngrx/signals'; import { Store } from '@ngrx/store'; import { selectActivePlaylist } from 'm3u-state'; import { Playlist } from 'shared-interfaces'; -import { PortalStatusType } from '../../xtream-state'; -import { XtreamApiService, XtreamCredentials } from '../../services/xtream-api.service'; import { - IXtreamDataSource, XTREAM_DATA_SOURCE, XtreamPlaylistData, } from '../../data-sources/xtream-data-source.interface'; +import { + XtreamApiService, + XtreamCredentials, +} from '../../services/xtream-api.service'; +import { PortalStatusType } from '../../xtream-state'; /** * Portal state for managing playlist and portal status @@ -63,7 +70,8 @@ export function withPortal() { } try { - const playlist = await dataSource.getPlaylist(playlistId); + const playlist = + await dataSource.getPlaylist(playlistId); if (playlist) { patchState(store, { currentPlaylist: playlist }); @@ -84,7 +92,9 @@ export function withPortal() { }; await dataSource.createPlaylist(newPlaylist); - patchState(store, { currentPlaylist: newPlaylist }); + patchState(store, { + currentPlaylist: newPlaylist, + }); } } } catch (error) { @@ -109,7 +119,8 @@ export function withPortal() { }; try { - const response = await apiService.getAccountInfo(credentials); + const response = + await apiService.getAccountInfo(credentials); if (!response?.user_info?.status) { patchState(store, { portalStatus: 'unavailable' }); diff --git a/apps/web/src/app/xtream-tauri/stores/features/with-search.feature.ts b/apps/web/src/app/xtream-tauri/stores/features/with-search.feature.ts index b0305f643..fcc009566 100644 --- a/apps/web/src/app/xtream-tauri/stores/features/with-search.feature.ts +++ b/apps/web/src/app/xtream-tauri/stores/features/with-search.feature.ts @@ -1,8 +1,12 @@ import { inject } from '@angular/core'; -import { patchState, signalStoreFeature, withMethods, withState } from '@ngrx/signals'; +import { + patchState, + signalStoreFeature, + withMethods, + withState, +} from '@ngrx/signals'; import { GlobalSearchResult } from 'services'; import { - IXtreamDataSource, XTREAM_DATA_SOURCE, XtreamContentItem, } from '../../data-sources/xtream-data-source.interface'; diff --git a/libs/ui/components/src/lib/playlist-switcher/playlist-switcher.component.ts b/libs/ui/components/src/lib/playlist-switcher/playlist-switcher.component.ts index 30c7639a5..bdf34c139 100644 --- a/libs/ui/components/src/lib/playlist-switcher/playlist-switcher.component.ts +++ b/libs/ui/components/src/lib/playlist-switcher/playlist-switcher.component.ts @@ -2,7 +2,6 @@ import { DatePipe } from '@angular/common'; import { Component, computed, - effect, inject, input, output, @@ -12,10 +11,10 @@ import { import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FormsModule } from '@angular/forms'; import { MatIconButton } from '@angular/material/button'; +import { MatRippleModule } from '@angular/material/core'; import { MatIcon } from '@angular/material/icon'; import { MatInputModule } from '@angular/material/input'; import { MatMenu, MatMenuModule, MatMenuTrigger } from '@angular/material/menu'; -import { MatRippleModule } from '@angular/material/core'; import { NavigationEnd, Router } from '@angular/router'; import { Store } from '@ngrx/store'; import { TranslatePipe } from '@ngx-translate/core'; @@ -115,22 +114,29 @@ export class PlaylistSwitcherComponent { private async checkPortalStatuses(playlists: PlaylistMeta[]) { const statusPromises = playlists - .filter(playlist => playlist.serverUrl && playlist.username && playlist.password) + .filter( + (playlist) => + playlist.serverUrl && playlist.username && playlist.password + ) .map(async (playlist) => { try { - const status = await this.portalStatusService.checkPortalStatus( - playlist.serverUrl, - playlist.username, - playlist.password - ); + const status = + await this.portalStatusService.checkPortalStatus( + playlist.serverUrl, + playlist.username, + playlist.password + ); return { id: playlist._id, status }; } catch { - return { id: playlist._id, status: 'unavailable' as PortalStatus }; + return { + id: playlist._id, + status: 'unavailable' as PortalStatus, + }; } }); const results = await Promise.all(statusPromises); - const statusMap = new Map(results.map(r => [r.id, r.status])); + const statusMap = new Map(results.map((r) => [r.id, r.status])); this.portalStatuses.set(statusMap); } @@ -186,4 +192,4 @@ export class PlaylistSwitcherComponent { isSelected(playlist: PlaylistMeta): boolean { return this.activePlaylistId() === playlist._id; } -} \ No newline at end of file +} From 4491880e815247c56bbd0cb17a8872f9c4342ad2 Mon Sep 17 00:00:00 2001 From: 4gray Date: Mon, 12 Jan 2026 18:05:58 +0100 Subject: [PATCH 15/17] fix(stalker-search): remove redundant width style from search page --- .../src/app/stalker/stalker-search/stalker-search.component.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/web/src/app/stalker/stalker-search/stalker-search.component.ts b/apps/web/src/app/stalker/stalker-search/stalker-search.component.ts index f24ad7edc..b000851b0 100644 --- a/apps/web/src/app/stalker/stalker-search/stalker-search.component.ts +++ b/apps/web/src/app/stalker/stalker-search/stalker-search.component.ts @@ -39,7 +39,6 @@ import { StalkerStore } from '../stalker.store'; .search-page { padding: 20px; - width: 100%; } .results-grid { From 155a7d120965246a82920b91f499bc1437b9711d Mon Sep 17 00:00:00 2001 From: 4gray Date: Mon, 12 Jan 2026 18:17:38 +0100 Subject: [PATCH 16/17] style(category-management): improve layout and styling of dialog components --- .../category-management-dialog.component.html | 10 ++++++++-- .../category-management-dialog.component.scss | 19 +++++++++++-------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/apps/web/src/app/xtream-tauri/category-management-dialog/category-management-dialog.component.html b/apps/web/src/app/xtream-tauri/category-management-dialog/category-management-dialog.component.html index 589d8f41e..6ea7fc584 100644 --- a/apps/web/src/app/xtream-tauri/category-management-dialog/category-management-dialog.component.html +++ b/apps/web/src/app/xtream-tauri/category-management-dialog/category-management-dialog.component.html @@ -32,7 +32,11 @@
- + search {{ category.name }} - ({{ getItemCount(category) }}) + ({{ getItemCount(category) }})
} @empty {
diff --git a/apps/web/src/app/xtream-tauri/category-management-dialog/category-management-dialog.component.scss b/apps/web/src/app/xtream-tauri/category-management-dialog/category-management-dialog.component.scss index f5026434d..ac07e7ddf 100644 --- a/apps/web/src/app/xtream-tauri/category-management-dialog/category-management-dialog.component.scss +++ b/apps/web/src/app/xtream-tauri/category-management-dialog/category-management-dialog.component.scss @@ -1,3 +1,13 @@ +::ng-deep .mat-mdc-dialog-content { + overflow: hidden !important; +} + +.mat-mdc-dialog-content { + display: flex; + gap: 10px; + flex-direction: column; +} + .loading-container { display: flex; flex-direction: column; @@ -11,7 +21,6 @@ display: flex; justify-content: space-between; align-items: center; - margin-bottom: 16px; flex-wrap: wrap; gap: 12px; } @@ -53,7 +62,7 @@ .category-item { display: flex; align-items: center; - padding: 6px 12px; + padding: 2px 10px; cursor: pointer; gap: 8px; transition: background-color 0.12s ease; @@ -79,12 +88,6 @@ text-overflow: ellipsis; } -.item-count { - opacity: 0.6; - font-size: 12px; - color: var(--mat-sys-on-surface-variant); -} - .empty-message { padding: 24px; text-align: center; From 2961c35ec1c102ab5fc8631d03ac75e3639f0256 Mon Sep 17 00:00:00 2001 From: 4gray Date: Mon, 12 Jan 2026 18:20:15 +0100 Subject: [PATCH 17/17] style(channel-list-container): adjust min-height for scroll viewport and clean up search-bar styles --- .../channel-list-container.component.scss | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) 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 548f167a1..396684de6 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,7 +1,7 @@ @use '@angular/material' as mat; .scroll-viewport { - min-height: calc(100vh - 159px); + min-height: calc(100vh - 190px); overflow-x: hidden; } @@ -13,7 +13,7 @@ .search-bar { @include mat.form-field-density(-5); - + & { width: 96%; } @@ -36,7 +36,8 @@ } } - div[matformfieldlineripple], .mat-mdc-form-field-subscript-wrapper { + div[matformfieldlineripple], + .mat-mdc-form-field-subscript-wrapper { display: none; }