diff --git a/libs/portal/shared/ui/src/lib/components/unified-collection/unified-collection-page.component.html b/libs/portal/shared/ui/src/lib/components/unified-collection/unified-collection-page.component.html
index fb21f7d15..a7807219c 100644
--- a/libs/portal/shared/ui/src/lib/components/unified-collection/unified-collection-page.component.html
+++ b/libs/portal/shared/ui/src/lib/components/unified-collection/unified-collection-page.component.html
@@ -19,21 +19,11 @@
@if (selectedContentType() === 'live') {
- @for (row of skeletonRows; track row) {
-
- }
+
} @else {
diff --git a/libs/portal/shared/ui/src/lib/components/unified-collection/unified-collection-page.component.ts b/libs/portal/shared/ui/src/lib/components/unified-collection/unified-collection-page.component.ts
index 28c7df6e3..d76077915 100644
--- a/libs/portal/shared/ui/src/lib/components/unified-collection/unified-collection-page.component.ts
+++ b/libs/portal/shared/ui/src/lib/components/unified-collection/unified-collection-page.component.ts
@@ -21,7 +21,7 @@ import { MatTooltip } from '@angular/material/tooltip';
import { ActivatedRoute, Router } from '@angular/router';
import { Store } from '@ngrx/store';
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
-import { DialogService } from 'components';
+import { ChannelListSkeletonComponent, DialogService } from 'components';
import {
buildGlobalCollectionDetailNavigationTarget,
buildCollectionViewState,
@@ -66,6 +66,7 @@ import {
styleUrl: './unified-collection-page.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
+ ChannelListSkeletonComponent,
EmptyStateComponent,
NgTemplateOutlet,
MatButtonToggleModule,
diff --git a/libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.html b/libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.html
index e1732252a..31841ad4f 100644
--- a/libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.html
+++ b/libs/portal/stalker/feature/src/lib/stalker-live-stream-layout/stalker-live-stream-layout.component.html
@@ -26,7 +26,13 @@
- @if (searchTerm() && visibleChannels().length === 0) {
+ @if (isInitialChannelsLoading()) {
+
+ } @else if (searchTerm() && visibleChannels().length === 0) {
+ !!this.stalkerStore.selectedCategoryId() &&
+ this.itvChannels().length === 0 &&
+ !this.searchTerm()
+ );
readonly selectedChannelId = this.stalkerStore.selectedItvId;
protected readonly normalizeStalkerEntityId = normalizeStalkerEntityId;
diff --git a/libs/portal/xtream/feature/src/lib/portal-channels-list/portal-channels-list.component.html b/libs/portal/xtream/feature/src/lib/portal-channels-list/portal-channels-list.component.html
index 3b1e0dc9a..ef08b5a5b 100644
--- a/libs/portal/xtream/feature/src/lib/portal-channels-list/portal-channels-list.component.html
+++ b/libs/portal/xtream/feature/src/lib/portal-channels-list/portal-channels-list.component.html
@@ -7,14 +7,12 @@
-
- @for (row of loadingRows; track row) {
-
-
-
-
-
- }
+
} @else if (filteredChannels().length > 0) {
diff --git a/libs/portal/xtream/feature/src/lib/portal-channels-list/portal-channels-list.component.scss b/libs/portal/xtream/feature/src/lib/portal-channels-list/portal-channels-list.component.scss
index 3e0183e83..0df50214b 100644
--- a/libs/portal/xtream/feature/src/lib/portal-channels-list/portal-channels-list.component.scss
+++ b/libs/portal/xtream/feature/src/lib/portal-channels-list/portal-channels-list.component.scss
@@ -150,83 +150,10 @@
}
.channels-loading-state__list {
- display: grid;
- gap: 8px;
+ min-height: 0;
overflow: hidden;
}
-.channel-skeleton {
- display: grid;
- grid-template-columns: 44px minmax(0, 1fr);
- grid-template-rows: 14px 10px;
- align-items: center;
- gap: 8px 12px;
- min-height: 68px;
- padding: 10px 12px;
- border-radius: 14px;
- background: color-mix(
- in srgb,
- var(--app-widget-header-bg, var(--mat-sys-surface-container-high)) 88%,
- var(--app-widget-bg, var(--mat-sys-surface-container)) 12%
- );
- border: 1px solid
- color-mix(
- in srgb,
- var(--app-widget-header-border, var(--mat-sys-outline-variant)) 96%,
- transparent
- );
-}
-
-.channel-skeleton__logo,
-.channel-skeleton__title,
-.channel-skeleton__meta {
- position: relative;
- overflow: hidden;
- background: color-mix(
- in srgb,
- var(--app-widget-header-bg, var(--mat-sys-surface-container-high)) 52%,
- var(--app-body-color, var(--mat-sys-on-surface-variant)) 48%
- );
-
- &::after {
- content: '';
- position: absolute;
- inset: 0;
- background: linear-gradient(
- 100deg,
- transparent 8%,
- color-mix(
- in srgb,
- var(--app-heading-color, var(--mat-sys-on-surface)) 18%,
- transparent
- )
- 44%,
- transparent 74%
- );
- transform: translateX(-120%);
- animation: channel-skeleton-shimmer 1.15s ease-in-out infinite;
- }
-}
-
-.channel-skeleton__logo {
- grid-row: 1 / span 2;
- width: 44px;
- height: 44px;
- border-radius: 12px;
-}
-
-.channel-skeleton__title {
- height: 11px;
- width: min(68%, 180px);
- border-radius: 999px;
-}
-
-.channel-skeleton__meta {
- height: 9px;
- width: min(42%, 120px);
- border-radius: 999px;
-}
-
@container (max-width: 350px) {
.scroll-viewport-portals {
padding-inline: 6px;
@@ -244,9 +171,3 @@
transform: translateY(0);
}
}
-
-@keyframes channel-skeleton-shimmer {
- 100% {
- transform: translateX(120%);
- }
-}
diff --git a/libs/portal/xtream/feature/src/lib/portal-channels-list/portal-channels-list.component.ts b/libs/portal/xtream/feature/src/lib/portal-channels-list/portal-channels-list.component.ts
index 9639af3b8..fabface7c 100644
--- a/libs/portal/xtream/feature/src/lib/portal-channels-list/portal-channels-list.component.ts
+++ b/libs/portal/xtream/feature/src/lib/portal-channels-list/portal-channels-list.component.ts
@@ -26,7 +26,10 @@ import {
XtreamCategory,
XtreamItem,
} from 'shared-interfaces';
-import { ChannelListItemComponent } from 'components';
+import {
+ ChannelListItemComponent,
+ ChannelListSkeletonComponent,
+} from 'components';
import {
PortalChannelSortMode,
sortPortalChannelItems,
@@ -59,6 +62,7 @@ interface XtreamCategoryLike {
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
ChannelListItemComponent,
+ ChannelListSkeletonComponent,
MatIcon,
ScrollingModule,
TranslatePipe,
@@ -76,7 +80,6 @@ export class PortalChannelsListComponent implements AfterViewInit, OnDestroy {
private readonly route = inject(ActivatedRoute);
readonly isSelectedTypeContentLoading =
this.xtreamStore.selectedTypeContentLoading;
- readonly loadingRows = Array.from({ length: 9 }, (_, index) => index);
readonly channels = computed(() => {
const override = this.channelsOverride();
if (Array.isArray(override)) {
diff --git a/libs/ui/components/src/index.ts b/libs/ui/components/src/index.ts
index e13958e6e..4aacc7da0 100644
--- a/libs/ui/components/src/index.ts
+++ b/libs/ui/components/src/index.ts
@@ -1,5 +1,7 @@
export * from './lib/channel-list-container/channel-list-container.component';
export * from './lib/channel-list-container/channel-list-item/channel-list-item.component';
+export * from './lib/channel-list-container/channel-list-item-skeleton/channel-list-item-skeleton.component';
+export * from './lib/channel-list-container/channel-list-skeleton/channel-list-skeleton.component';
export * from './lib/channel-list-loading-state/channel-list-loading-state.component';
export * from './lib/confirm-dialog/confirm-dialog.component';
export * from './lib/confirm-dialog/dialog.service';
diff --git a/libs/ui/components/src/lib/channel-list-container/channel-list-item-skeleton/channel-list-item-skeleton.component.html b/libs/ui/components/src/lib/channel-list-container/channel-list-item-skeleton/channel-list-item-skeleton.component.html
new file mode 100644
index 000000000..7b3bafb9d
--- /dev/null
+++ b/libs/ui/components/src/lib/channel-list-container/channel-list-item-skeleton/channel-list-item-skeleton.component.html
@@ -0,0 +1,33 @@
+
+
+
+
+
+ @if (showEpg()) {
+
+
+ }
+
+
+ @if (actionCount() > 0) {
+
+ @for (slot of actionSlots; track slot) {
+ @if (slot < actionCount()) {
+
+ }
+ }
+
+ }
+
diff --git a/libs/ui/components/src/lib/channel-list-container/channel-list-item-skeleton/channel-list-item-skeleton.component.scss b/libs/ui/components/src/lib/channel-list-container/channel-list-item-skeleton/channel-list-item-skeleton.component.scss
new file mode 100644
index 000000000..8deb39f91
--- /dev/null
+++ b/libs/ui/components/src/lib/channel-list-container/channel-list-item-skeleton/channel-list-item-skeleton.component.scss
@@ -0,0 +1,132 @@
+@use '../../../../../../portal/shared/ui/src/lib/styles/skeleton';
+
+:host {
+ display: block;
+ width: 100%;
+ container-type: inline-size;
+}
+
+.channel-list-item-skeleton {
+ width: 100%;
+ min-height: 68px;
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 8px 10px 8px 12px;
+ border: 1px solid transparent;
+ border-radius: 12px;
+ background: transparent;
+ box-sizing: border-box;
+ overflow: hidden;
+
+ &.compact {
+ min-height: 52px;
+ padding-block: 7px;
+ }
+}
+
+.channel-content-skeleton {
+ min-width: 0;
+ flex: 1;
+ display: flex;
+ align-items: center;
+ gap: 12px;
+}
+
+.channel-logo-skeleton {
+ width: 44px;
+ height: 44px;
+ flex-shrink: 0;
+ border-radius: 10px;
+
+ .compact & {
+ width: 38px;
+ height: 38px;
+ }
+}
+
+.channel-details-skeleton {
+ min-width: 0;
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ gap: 6px;
+}
+
+.channel-name-skeleton {
+ height: 11px;
+ border-radius: 999px;
+ max-width: 100%;
+}
+
+.epg-title-skeleton {
+ height: 9px;
+ border-radius: 999px;
+ max-width: 100%;
+}
+
+.epg-progress-skeleton {
+ height: 6px;
+ width: 100%;
+ border-radius: 999px;
+}
+
+.action-buttons-skeleton {
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ gap: 2px;
+ margin-inline-start: auto;
+}
+
+.action-button-skeleton {
+ width: 34px;
+ height: 34px;
+ border-radius: 10px;
+}
+
+@container (max-width: 360px) {
+ .channel-list-item-skeleton {
+ gap: 10px;
+ padding-inline: 10px 8px;
+ }
+
+ .channel-logo-skeleton {
+ width: 40px;
+ height: 40px;
+ }
+
+ .action-buttons-skeleton {
+ gap: 0;
+ }
+}
+
+@container (max-width: 270px) {
+ .channel-list-item-skeleton {
+ min-height: 52px;
+ gap: 8px;
+ padding-inline: 8px 6px;
+ }
+
+ .channel-logo-skeleton {
+ width: 34px;
+ height: 34px;
+ }
+
+ .epg-title-skeleton,
+ .epg-progress-skeleton {
+ display: none;
+ }
+}
+
+@container (max-width: 220px) {
+ .channel-logo-skeleton,
+ .action-buttons-skeleton {
+ display: none;
+ }
+
+ .channel-content-skeleton {
+ gap: 0;
+ }
+}
diff --git a/libs/ui/components/src/lib/channel-list-container/channel-list-item-skeleton/channel-list-item-skeleton.component.ts b/libs/ui/components/src/lib/channel-list-container/channel-list-item-skeleton/channel-list-item-skeleton.component.ts
new file mode 100644
index 000000000..e972f46da
--- /dev/null
+++ b/libs/ui/components/src/lib/channel-list-container/channel-list-item-skeleton/channel-list-item-skeleton.component.ts
@@ -0,0 +1,16 @@
+import { ChangeDetectionStrategy, Component, input } from '@angular/core';
+
+@Component({
+ selector: 'app-channel-list-item-skeleton',
+ templateUrl: './channel-list-item-skeleton.component.html',
+ styleUrl: './channel-list-item-skeleton.component.scss',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+})
+export class ChannelListItemSkeletonComponent {
+ readonly showEpg = input(true);
+ readonly actionCount = input<0 | 1 | 2 | 3>(1);
+ readonly titleWidth = input(70);
+ readonly metaWidth = input(40);
+
+ readonly actionSlots = Array.from({ length: 3 }, (_, i) => i);
+}
diff --git a/libs/ui/components/src/lib/channel-list-container/channel-list-skeleton/channel-list-skeleton.component.html b/libs/ui/components/src/lib/channel-list-container/channel-list-skeleton/channel-list-skeleton.component.html
new file mode 100644
index 000000000..7efc8da0c
--- /dev/null
+++ b/libs/ui/components/src/lib/channel-list-container/channel-list-skeleton/channel-list-skeleton.component.html
@@ -0,0 +1,10 @@
+
+ @for (row of rows(); track row.index) {
+
+ }
+
diff --git a/libs/ui/components/src/lib/channel-list-container/channel-list-skeleton/channel-list-skeleton.component.scss b/libs/ui/components/src/lib/channel-list-container/channel-list-skeleton/channel-list-skeleton.component.scss
new file mode 100644
index 000000000..d0690e66d
--- /dev/null
+++ b/libs/ui/components/src/lib/channel-list-container/channel-list-skeleton/channel-list-skeleton.component.scss
@@ -0,0 +1,11 @@
+:host {
+ display: block;
+ width: 100%;
+}
+
+.channel-list-skeleton {
+ display: flex;
+ flex-direction: column;
+ gap: 0;
+ width: 100%;
+}
diff --git a/libs/ui/components/src/lib/channel-list-container/channel-list-skeleton/channel-list-skeleton.component.ts b/libs/ui/components/src/lib/channel-list-container/channel-list-skeleton/channel-list-skeleton.component.ts
new file mode 100644
index 000000000..261baf268
--- /dev/null
+++ b/libs/ui/components/src/lib/channel-list-container/channel-list-skeleton/channel-list-skeleton.component.ts
@@ -0,0 +1,31 @@
+import {
+ ChangeDetectionStrategy,
+ Component,
+ computed,
+ input,
+} from '@angular/core';
+import { ChannelListItemSkeletonComponent } from '../channel-list-item-skeleton/channel-list-item-skeleton.component';
+
+const TITLE_WIDTHS = [72, 61, 68, 54, 78, 64, 70, 57, 75, 66, 73, 59];
+const META_WIDTHS = [38, 34, 42, 36, 44, 35, 40, 32, 39, 37, 43, 33];
+
+@Component({
+ selector: 'app-channel-list-skeleton',
+ templateUrl: './channel-list-skeleton.component.html',
+ styleUrl: './channel-list-skeleton.component.scss',
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ imports: [ChannelListItemSkeletonComponent],
+})
+export class ChannelListSkeletonComponent {
+ readonly count = input(9);
+ readonly showEpg = input(true);
+ readonly actionCount = input<0 | 1 | 2 | 3>(1);
+
+ readonly rows = computed(() =>
+ Array.from({ length: this.count() }, (_, index) => ({
+ index,
+ title: TITLE_WIDTHS[index % TITLE_WIDTHS.length],
+ meta: META_WIDTHS[index % META_WIDTHS.length],
+ }))
+ );
+}
diff --git a/libs/ui/components/src/lib/channel-list-loading-state/channel-list-loading-state.component.html b/libs/ui/components/src/lib/channel-list-loading-state/channel-list-loading-state.component.html
index 54defc1a4..1ce0fe8c3 100644
--- a/libs/ui/components/src/lib/channel-list-loading-state/channel-list-loading-state.component.html
+++ b/libs/ui/components/src/lib/channel-list-loading-state/channel-list-loading-state.component.html
@@ -40,26 +40,11 @@
- @for (row of channelRows; track row) {
-
-
-
-
-
-
-
-
- }
+
@@ -71,27 +56,12 @@
-
- @for (row of channelRows; track row) {
-
-
-
-
-
-
-
-
- }
+
}
diff --git a/libs/ui/components/src/lib/channel-list-loading-state/channel-list-loading-state.component.scss b/libs/ui/components/src/lib/channel-list-loading-state/channel-list-loading-state.component.scss
index a1732a8cc..3292c762c 100644
--- a/libs/ui/components/src/lib/channel-list-loading-state/channel-list-loading-state.component.scss
+++ b/libs/ui/components/src/lib/channel-list-loading-state/channel-list-loading-state.component.scss
@@ -33,6 +33,7 @@
height: 100%;
min-height: 0;
padding: 16px 12px 18px;
+ grid-template-rows: auto 1fr;
}
.channels-loading-state__copy {
@@ -50,61 +51,10 @@
.channels-loading-state__list,
.groups-loading-content__list {
- display: grid;
- gap: 8px;
+ min-height: 0;
overflow: hidden;
}
-.channel-loading-row {
- display: grid;
- grid-template-columns: 44px minmax(0, 1fr) 18px;
- align-items: center;
- gap: 12px;
- min-height: 68px;
- padding: 10px 12px;
- border-radius: 14px;
- background: color-mix(
- in srgb,
- var(--app-widget-header-bg, var(--mat-sys-surface-container-high)) 88%,
- var(--app-widget-bg, var(--mat-sys-surface-container)) 12%
- );
- border: 1px solid
- color-mix(
- in srgb,
- var(--app-widget-header-border, var(--mat-sys-outline-variant)) 96%,
- transparent
- );
-}
-
-.channel-loading-row__logo {
- width: 44px;
- height: 44px;
- border-radius: 12px;
-}
-
-.channel-loading-row__text {
- min-width: 0;
- display: grid;
- gap: 8px;
-}
-
-.channel-loading-row__title {
- height: 11px;
- border-radius: 999px;
-}
-
-.channel-loading-row__meta {
- height: 9px;
- border-radius: 999px;
-}
-
-.channel-loading-row__favorite {
- justify-self: end;
- width: 18px;
- height: 18px;
- border-radius: 999px;
-}
-
.groups-loading-layout {
display: flex;
width: 100%;
@@ -225,18 +175,6 @@
padding-inline: 8px;
}
- .channel-loading-row {
- grid-template-columns: 40px minmax(0, 1fr) 16px;
- gap: 10px;
- padding-inline: 10px;
- }
-
- .channel-loading-row__logo {
- width: 40px;
- height: 40px;
- border-radius: 10px;
- }
-
.groups-loading-nav {
min-width: 136px;
width: 42%;
diff --git a/libs/ui/components/src/lib/channel-list-loading-state/channel-list-loading-state.component.spec.ts b/libs/ui/components/src/lib/channel-list-loading-state/channel-list-loading-state.component.spec.ts
index 1e088688e..b223d283b 100644
--- a/libs/ui/components/src/lib/channel-list-loading-state/channel-list-loading-state.component.spec.ts
+++ b/libs/ui/components/src/lib/channel-list-loading-state/channel-list-loading-state.component.spec.ts
@@ -18,15 +18,14 @@ describe('ChannelListLoadingStateComponent', () => {
component = fixture.componentInstance;
});
- it('renders channel rows for non-group views', () => {
+ it('renders the channel-list skeleton wrapper for non-group views', () => {
fixture.componentRef.setInput('view', 'all');
fixture.detectChanges();
expect(component.isGroupsView()).toBe(false);
expect(
- fixture.nativeElement.querySelectorAll('.channel-loading-row')
- .length
- ).toBe(component.channelRows.length);
+ fixture.nativeElement.querySelector('app-channel-list-skeleton')
+ ).not.toBeNull();
expect(
fixture.nativeElement.querySelector('.groups-loading-layout')
).toBeNull();
diff --git a/libs/ui/components/src/lib/channel-list-loading-state/channel-list-loading-state.component.ts b/libs/ui/components/src/lib/channel-list-loading-state/channel-list-loading-state.component.ts
index 0750d232d..a65429ae4 100644
--- a/libs/ui/components/src/lib/channel-list-loading-state/channel-list-loading-state.component.ts
+++ b/libs/ui/components/src/lib/channel-list-loading-state/channel-list-loading-state.component.ts
@@ -5,21 +5,19 @@ import {
input,
} from '@angular/core';
import { TranslatePipe } from '@ngx-translate/core';
+import { ChannelListSkeletonComponent } from '../channel-list-container/channel-list-skeleton/channel-list-skeleton.component';
@Component({
selector: 'app-channel-list-loading-state',
templateUrl: './channel-list-loading-state.component.html',
styleUrl: './channel-list-loading-state.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
- imports: [TranslatePipe],
+ imports: [TranslatePipe, ChannelListSkeletonComponent],
})
export class ChannelListLoadingStateComponent {
readonly view = input('all');
readonly isGroupsView = computed(() => this.view() === 'groups');
- readonly channelRows = Array.from({ length: 9 }, (_, index) => index);
readonly groupRows = Array.from({ length: 10 }, (_, index) => index);
- readonly channelMetaWidths = [38, 34, 42, 36, 44, 35, 40, 32, 39];
- readonly channelTitleWidths = [72, 61, 68, 54, 78, 64, 70, 57, 75];
readonly groupLabelWidths = [78, 66, 84, 58, 73, 69, 81, 62, 76, 71];
}