mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
refactor(ui): unify live-tv channel skeleton across m3u, xtream, stalker
The loading placeholders for live channel rows were inconsistent and visibly heavier than the real channel-list-item: each module rendered its own skeleton with a colored card surface, larger padding/radius, mismatched logo border-radius, and only a single tiny right-side dot instead of the real 34×34 action buttons. Stalker had no initial-load skeleton at all. Extract a shared app-channel-list-item-skeleton that mirrors channel-list-item 1:1 (transparent surface, 8/10/8/12 padding, 12px radius, 44×44 logo with 10px radius, optional EPG progress placeholder, 0–3 button placeholders) plus a thin app-channel-list-skeleton wrapper that renders N rows with deterministic width variance, and route every live-tv loading state through it (m3u channel-list-loading-state, xtream portal-channels-list, stalker live-stream-layout via a derived isInitialChannelsLoading signal, and the unified favorites/recent collection page). Drops the bespoke channel-skeleton-shimmer keyframes in favor of the shared skeleton-shimmer animation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Entire-Checkpoint: 58a2d50756dd
This commit is contained in:
1 parent
888a2dd80a
commit
e888841b21
18 files changed
+291
-221
No files matched your search
+5
-15
@@ -19,21 +19,11 @@
|
||||
</div>
|
||||
@if (selectedContentType() === 'live') {
|
||||
<div class="skeleton-list">
|
||||
@for (row of skeletonRows; track row) {
|
||||
<div class="list-skeleton-row" aria-hidden="true">
|
||||
<span class="list-skeleton-handle"></span>
|
||||
<span class="list-skeleton-avatar"></span>
|
||||
<div class="list-skeleton-text">
|
||||
<span
|
||||
class="grid-skeleton-line grid-skeleton-line--primary"
|
||||
></span>
|
||||
<span
|
||||
class="grid-skeleton-line grid-skeleton-line--secondary"
|
||||
></span>
|
||||
</div>
|
||||
<span class="list-skeleton-star"></span>
|
||||
</div>
|
||||
}
|
||||
<app-channel-list-skeleton
|
||||
[count]="12"
|
||||
[showEpg]="true"
|
||||
[actionCount]="1"
|
||||
/>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="skeleton-grid">
|
||||
|
||||
+2
-1
@@ -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,
|
||||
|
||||
+7
-1
@@ -26,7 +26,13 @@
|
||||
</div>
|
||||
|
||||
<div #scrollContainer class="channels-list">
|
||||
@if (searchTerm() && visibleChannels().length === 0) {
|
||||
@if (isInitialChannelsLoading()) {
|
||||
<app-channel-list-skeleton
|
||||
[count]="9"
|
||||
[showEpg]="false"
|
||||
[actionCount]="1"
|
||||
/>
|
||||
} @else if (searchTerm() && visibleChannels().length === 0) {
|
||||
<div class="empty-search-state">
|
||||
<app-portal-empty-state
|
||||
icon="search_off"
|
||||
|
||||
+12
-1
@@ -16,7 +16,11 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
|
||||
import { ChannelListItemComponent, ResizableDirective } from 'components';
|
||||
import {
|
||||
ChannelListItemComponent,
|
||||
ChannelListSkeletonComponent,
|
||||
ResizableDirective,
|
||||
} from 'components';
|
||||
import { PlaylistsService } from 'services';
|
||||
import {
|
||||
Channel,
|
||||
@@ -58,6 +62,7 @@ import {
|
||||
styleUrls: ['./stalker-live-stream-layout.component.scss'],
|
||||
imports: [
|
||||
ChannelListItemComponent,
|
||||
ChannelListSkeletonComponent,
|
||||
EpgListComponent,
|
||||
MatProgressSpinnerModule,
|
||||
LiveEpgPanelComponent,
|
||||
@@ -99,6 +104,12 @@ export class StalkerLiveStreamLayoutComponent implements OnDestroy {
|
||||
});
|
||||
readonly hasMoreItems = this.stalkerStore.hasMoreChannels;
|
||||
readonly isLoadingMore = signal(false);
|
||||
readonly isInitialChannelsLoading = computed(
|
||||
() =>
|
||||
!!this.stalkerStore.selectedCategoryId() &&
|
||||
this.itvChannels().length === 0 &&
|
||||
!this.searchTerm()
|
||||
);
|
||||
|
||||
readonly selectedChannelId = this.stalkerStore.selectedItvId;
|
||||
protected readonly normalizeStalkerEntityId = normalizeStalkerEntityId;
|
||||
|
||||
+6
-8
@@ -7,14 +7,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="channels-loading-state__list" aria-hidden="true">
|
||||
@for (row of loadingRows; track row) {
|
||||
<div class="channel-skeleton">
|
||||
<span class="channel-skeleton__logo"></span>
|
||||
<span class="channel-skeleton__title"></span>
|
||||
<span class="channel-skeleton__meta"></span>
|
||||
</div>
|
||||
}
|
||||
<div class="channels-loading-state__list">
|
||||
<app-channel-list-skeleton
|
||||
[count]="9"
|
||||
[showEpg]="true"
|
||||
[actionCount]="1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
} @else if (filteredChannels().length > 0) {
|
||||
|
||||
+1
-80
@@ -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%);
|
||||
}
|
||||
}
|
||||
+5
-2
@@ -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)) {
|
||||
|
||||
@@ -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';
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
<div
|
||||
class="channel-list-item-skeleton"
|
||||
[class.compact]="!showEpg()"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div class="channel-content-skeleton">
|
||||
<span class="list-skeleton-avatar channel-logo-skeleton"></span>
|
||||
<div class="channel-details-skeleton">
|
||||
<span
|
||||
class="grid-skeleton-line channel-name-skeleton"
|
||||
[style.width.%]="titleWidth()"
|
||||
></span>
|
||||
@if (showEpg()) {
|
||||
<span
|
||||
class="grid-skeleton-line epg-title-skeleton"
|
||||
[style.width.%]="metaWidth()"
|
||||
></span>
|
||||
<span
|
||||
class="grid-skeleton-line epg-progress-skeleton"
|
||||
></span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@if (actionCount() > 0) {
|
||||
<div class="action-buttons-skeleton">
|
||||
@for (slot of actionSlots; track slot) {
|
||||
@if (slot < actionCount()) {
|
||||
<span class="list-skeleton-star action-button-skeleton"></span>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
+132
@@ -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;
|
||||
}
|
||||
}
|
||||
+16
@@ -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);
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
<div class="channel-list-skeleton" aria-hidden="true">
|
||||
@for (row of rows(); track row.index) {
|
||||
<app-channel-list-item-skeleton
|
||||
[showEpg]="showEpg()"
|
||||
[actionCount]="actionCount()"
|
||||
[titleWidth]="row.title"
|
||||
[metaWidth]="row.meta"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
:host {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.channel-list-skeleton {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
width: 100%;
|
||||
}
|
||||
+31
@@ -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],
|
||||
}))
|
||||
);
|
||||
}
|
||||
+11
-41
@@ -40,26 +40,11 @@
|
||||
</header>
|
||||
|
||||
<div class="groups-loading-content__list">
|
||||
@for (row of channelRows; track row) {
|
||||
<div class="channel-loading-row">
|
||||
<span
|
||||
class="list-skeleton-avatar channel-loading-row__logo"
|
||||
></span>
|
||||
<span class="channel-loading-row__text">
|
||||
<span
|
||||
class="grid-skeleton-line channel-loading-row__title"
|
||||
[style.width.%]="channelTitleWidths[row]"
|
||||
></span>
|
||||
<span
|
||||
class="grid-skeleton-line channel-loading-row__meta"
|
||||
[style.width.%]="channelMetaWidths[row]"
|
||||
></span>
|
||||
</span>
|
||||
<span
|
||||
class="list-skeleton-star channel-loading-row__favorite"
|
||||
></span>
|
||||
</div>
|
||||
}
|
||||
<app-channel-list-skeleton
|
||||
[count]="9"
|
||||
[showEpg]="true"
|
||||
[actionCount]="2"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
@@ -71,27 +56,12 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="channels-loading-state__list" aria-hidden="true">
|
||||
@for (row of channelRows; track row) {
|
||||
<div class="channel-loading-row">
|
||||
<span
|
||||
class="list-skeleton-avatar channel-loading-row__logo"
|
||||
></span>
|
||||
<span class="channel-loading-row__text">
|
||||
<span
|
||||
class="grid-skeleton-line channel-loading-row__title"
|
||||
[style.width.%]="channelTitleWidths[row]"
|
||||
></span>
|
||||
<span
|
||||
class="grid-skeleton-line channel-loading-row__meta"
|
||||
[style.width.%]="channelMetaWidths[row]"
|
||||
></span>
|
||||
</span>
|
||||
<span
|
||||
class="list-skeleton-star channel-loading-row__favorite"
|
||||
></span>
|
||||
</div>
|
||||
}
|
||||
<div class="channels-loading-state__list">
|
||||
<app-channel-list-skeleton
|
||||
[count]="9"
|
||||
[showEpg]="true"
|
||||
[actionCount]="2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
+2
-64
@@ -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%;
|
||||
|
||||
+3
-4
@@ -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();
|
||||
|
||||
+2
-4
@@ -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<string>('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];
|
||||
}
|
||||
Reference in new issue
Block a user