mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-16 22:26:16 -08:00
refactor(m3u): add loading state for channel list and improve playlist handling
- Implemented a loading state for channel lists in various components, enhancing user experience during data fetching. - Added `ChannelListLoadingStateComponent` to display loading skeletons for channels and groups. - Updated `M3uWorkspaceRouteSession` to manage loading states more effectively when fetching playlists. - Modified `VideoPlayerComponent`, `SidebarComponent`, and `ChannelListContainerComponent` to utilize the new loading state. - Enhanced tests to cover new loading behaviors and ensure proper dispatching of loading actions.
This commit is contained in:
1 parent
7cc25380dd
commit
0f72efb749
33 files changed
+1104
-41
No files matched your search
@@ -1,5 +1,6 @@
|
||||
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-loading-state/channel-list-loading-state.component';
|
||||
export * from './lib/confirm-dialog/confirm-dialog.component';
|
||||
export * from './lib/confirm-dialog/dialog.service';
|
||||
export * from './lib/content-hero/content-hero.component';
|
||||
|
||||
+10
-4
@@ -1,4 +1,6 @@
|
||||
@if (displayedChannels().length) {
|
||||
@if (channelsLoading()) {
|
||||
<app-channel-list-loading-state [view]="activeView()" />
|
||||
} @else if (displayedChannels().length) {
|
||||
@if (viewTitle() && activeView() !== 'groups') {
|
||||
<header class="context-header">
|
||||
<div class="context-header__top">
|
||||
@@ -77,8 +79,12 @@
|
||||
}
|
||||
}
|
||||
} @else {
|
||||
<div class="loading-container">
|
||||
<mat-icon class="loading-spinner">sync</mat-icon>
|
||||
<div class="loading-text">{{ 'CHANNELS.LOADING' | translate }}</div>
|
||||
<div class="empty-source-state">
|
||||
<div class="empty-source-state__icon-wrapper">
|
||||
<mat-icon class="empty-source-state__icon">folder_off</mat-icon>
|
||||
</div>
|
||||
<h3 class="empty-source-state__title">
|
||||
{{ 'CHANNELS.NO_CHANNELS_FOUND' | translate }}
|
||||
</h3>
|
||||
</div>
|
||||
}
|
||||
+33
-19
@@ -6,42 +6,56 @@
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
:host > app-channel-list-loading-state {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
::ng-deep {
|
||||
.mat-mdc-tab.mdc-tab {
|
||||
padding: 0 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
.empty-source-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: calc(100vh - 120px);
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
padding: 32px 24px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
font-size: 48px;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
animation: spin 1.5s linear infinite;
|
||||
opacity: 0.7;
|
||||
margin-bottom: 16px;
|
||||
.empty-source-state__icon-wrapper {
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
border-radius: 20px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0)),
|
||||
rgba(127, 127, 127, 0.1);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 20px;
|
||||
box-shadow:
|
||||
0 8px 24px -8px rgba(0, 0, 0, 0.1),
|
||||
inset 0 1px 1px rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.loading-text {
|
||||
font-size: 1rem;
|
||||
opacity: 0.7;
|
||||
.empty-source-state__icon {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
font-size: 32px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
.empty-source-state__title {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 520;
|
||||
margin: 0;
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 599px) {
|
||||
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
import { signal } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { convertToParamMap, ActivatedRoute, NavigationEnd, Router } from '@angular/router';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { StorageMap } from '@ngx-pwa/local-storage';
|
||||
import { of, Subject } from 'rxjs';
|
||||
import { EpgService } from '@iptvnator/epg/data-access';
|
||||
import { PlaylistContextFacade } from '@iptvnator/playlist/shared/util';
|
||||
import { ChannelActions } from 'm3u-state';
|
||||
import { PlaylistsService } from 'services';
|
||||
import { ChannelListContainerComponent } from './channel-list-container.component';
|
||||
|
||||
describe('ChannelListContainerComponent', () => {
|
||||
let fixture: ComponentFixture<ChannelListContainerComponent>;
|
||||
let dispatch: jest.Mock;
|
||||
|
||||
beforeEach(async () => {
|
||||
const routerEvents$ = new Subject<NavigationEnd>();
|
||||
dispatch = jest.fn();
|
||||
|
||||
const route = {
|
||||
snapshot: {
|
||||
data: { layout: 'workspace' },
|
||||
paramMap: convertToParamMap({}),
|
||||
queryParamMap: convertToParamMap({}),
|
||||
params: {},
|
||||
queryParams: {},
|
||||
},
|
||||
pathFromRoot: [
|
||||
{
|
||||
snapshot: {
|
||||
data: { layout: 'workspace' },
|
||||
paramMap: convertToParamMap({}),
|
||||
params: {},
|
||||
},
|
||||
paramMap: of(convertToParamMap({})),
|
||||
},
|
||||
],
|
||||
paramMap: of(convertToParamMap({})),
|
||||
queryParamMap: of(convertToParamMap({})),
|
||||
} as unknown as ActivatedRoute;
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ChannelListContainerComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: EpgService,
|
||||
useValue: {
|
||||
getCurrentProgramsForChannels: jest
|
||||
.fn()
|
||||
.mockReturnValue(of(new Map())),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: PlaylistsService,
|
||||
useValue: {},
|
||||
},
|
||||
{
|
||||
provide: StorageMap,
|
||||
useValue: {
|
||||
get: jest.fn().mockReturnValue(of({})),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: Store,
|
||||
useValue: {
|
||||
dispatch,
|
||||
select: jest.fn().mockReturnValue(of([])),
|
||||
selectSignal: jest.fn(() => signal(undefined)),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: Router,
|
||||
useValue: {
|
||||
url: '/workspace/playlists/demo/all',
|
||||
events: routerEvents$.asObservable(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
useValue: route,
|
||||
},
|
||||
{
|
||||
provide: PlaylistContextFacade,
|
||||
useValue: {
|
||||
resolvedPlaylistId: signal(null),
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
.overrideComponent(ChannelListContainerComponent, {
|
||||
set: {
|
||||
template: '',
|
||||
imports: [],
|
||||
},
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ChannelListContainerComponent);
|
||||
});
|
||||
|
||||
it('does not clear the shared channel list on destroy', () => {
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.destroy();
|
||||
|
||||
expect(dispatch).toHaveBeenCalledTimes(1);
|
||||
expect(dispatch).toHaveBeenCalledWith(
|
||||
ChannelActions.resetActiveChannel()
|
||||
);
|
||||
});
|
||||
});
|
||||
+3
-1
@@ -56,6 +56,7 @@ import {
|
||||
RecentViewComponent,
|
||||
RecentViewItem,
|
||||
} from './recent-view/recent-view.component';
|
||||
import { ChannelListLoadingStateComponent } from '../channel-list-loading-state/channel-list-loading-state.component';
|
||||
|
||||
function groupChannelsByTitle(channels: Channel[]): Record<string, Channel[]> {
|
||||
return channels.reduce<Record<string, Channel[]>>((groups, channel) => {
|
||||
@@ -75,6 +76,7 @@ function groupChannelsByTitle(channels: Channel[]): Record<string, Channel[]> {
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [
|
||||
AllChannelsViewComponent,
|
||||
ChannelListLoadingStateComponent,
|
||||
CommonModule,
|
||||
FavoritesViewComponent,
|
||||
GroupsViewComponent,
|
||||
@@ -113,6 +115,7 @@ export class ChannelListContainerComponent implements OnInit, OnDestroy {
|
||||
|
||||
/** Active view (all, groups, favorites, recent) */
|
||||
readonly activeView = input<string>('all');
|
||||
readonly channelsLoading = input(false);
|
||||
readonly recentItems = input<PlaylistRecentlyViewedItem[]>([]);
|
||||
readonly sidebarWidth = input<number | null>(null);
|
||||
readonly sidebarWidthRequested = output<number>();
|
||||
@@ -281,7 +284,6 @@ export class ChannelListContainerComponent implements OnInit, OnDestroy {
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.store.dispatch(ChannelActions.resetActiveChannel());
|
||||
this.store.dispatch(ChannelActions.setChannels({ channels: [] }));
|
||||
|
||||
if (this.epgRefreshInterval) {
|
||||
clearInterval(this.epgRefreshInterval);
|
||||
|
||||
+98
@@ -0,0 +1,98 @@
|
||||
<div class="channel-list-loading-state" aria-live="polite" role="status">
|
||||
<span class="visually-hidden">
|
||||
{{ 'CHANNELS.LOADING' | translate }}
|
||||
</span>
|
||||
|
||||
@if (isGroupsView()) {
|
||||
<div class="groups-loading-layout" aria-hidden="true">
|
||||
<aside class="groups-loading-nav">
|
||||
<header class="groups-loading-nav__header">
|
||||
<h2 class="groups-loading-nav__title">
|
||||
{{ 'CHANNELS.GROUPS' | translate }}
|
||||
</h2>
|
||||
</header>
|
||||
|
||||
<div class="groups-loading-nav__list">
|
||||
@for (row of groupRows; track row) {
|
||||
<div class="groups-loading-nav__item">
|
||||
<span
|
||||
class="grid-skeleton-line groups-loading-nav__label"
|
||||
[style.width.%]="groupLabelWidths[row]"
|
||||
></span>
|
||||
<span
|
||||
class="grid-skeleton-line groups-loading-nav__count"
|
||||
></span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<section class="groups-loading-content">
|
||||
<header class="groups-loading-content__header">
|
||||
<div class="groups-loading-content__meta">
|
||||
<span
|
||||
class="grid-skeleton-line groups-loading-content__title"
|
||||
></span>
|
||||
<span
|
||||
class="grid-skeleton-line groups-loading-content__subtitle"
|
||||
></span>
|
||||
</div>
|
||||
</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>
|
||||
}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="channels-loading-state">
|
||||
<div class="channels-loading-state__copy">
|
||||
<div class="channels-loading-state__title">
|
||||
{{ 'CHANNELS.LOADING' | translate }}
|
||||
</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>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
+253
@@ -0,0 +1,253 @@
|
||||
@use '../../../../../portal/shared/ui/src/lib/styles/skeleton';
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
min-height: 0;
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
.channel-list-loading-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.visually-hidden {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.channels-loading-state {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
padding: 16px 12px 18px;
|
||||
}
|
||||
|
||||
.channels-loading-state__copy {
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.channels-loading-state__title,
|
||||
.groups-loading-nav__title {
|
||||
margin: 0;
|
||||
font-size: 0.86rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.01em;
|
||||
color: var(--mat-sys-on-surface);
|
||||
}
|
||||
|
||||
.channels-loading-state__list,
|
||||
.groups-loading-content__list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
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%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
background: var(--app-content-bg, var(--mat-sys-surface));
|
||||
}
|
||||
|
||||
.groups-loading-nav {
|
||||
width: 208px;
|
||||
min-width: 164px;
|
||||
max-width: 320px;
|
||||
flex-shrink: 0;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background: var(--app-rail-bg, var(--mat-sys-surface-container-low));
|
||||
padding: 12px 10px 10px 12px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.groups-loading-nav__header {
|
||||
padding: 4px 6px 12px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.groups-loading-nav__list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.groups-loading-nav__item {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) 24px;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-height: 36px;
|
||||
padding: 0 10px;
|
||||
border-radius: 12px;
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
.groups-loading-nav__label,
|
||||
.groups-loading-nav__count,
|
||||
.groups-loading-content__title,
|
||||
.groups-loading-content__subtitle {
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.groups-loading-nav__label {
|
||||
height: 9px;
|
||||
}
|
||||
|
||||
.groups-loading-nav__count {
|
||||
width: 22px;
|
||||
height: 9px;
|
||||
justify-self: end;
|
||||
}
|
||||
|
||||
.groups-loading-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
background: var(--app-content-bg, var(--mat-sys-surface));
|
||||
border-top-left-radius: 16px;
|
||||
}
|
||||
|
||||
.groups-loading-content__header {
|
||||
flex-shrink: 0;
|
||||
min-height: 56px;
|
||||
padding: 12px 16px 10px;
|
||||
border-bottom: 1px solid var(--app-separator);
|
||||
}
|
||||
|
||||
.groups-loading-content__meta {
|
||||
max-width: 180px;
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.groups-loading-content__title {
|
||||
width: 100%;
|
||||
max-width: 156px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.groups-loading-content__subtitle {
|
||||
width: 82px;
|
||||
height: 9px;
|
||||
}
|
||||
|
||||
.groups-loading-content__list {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
align-content: start;
|
||||
padding: 12px 8px 12px;
|
||||
}
|
||||
|
||||
@container (max-width: 380px) {
|
||||
.channels-loading-state {
|
||||
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%;
|
||||
padding-inline: 8px;
|
||||
}
|
||||
|
||||
.groups-loading-content__header {
|
||||
padding-inline: 12px;
|
||||
}
|
||||
|
||||
.groups-loading-content__list {
|
||||
padding-inline: 6px;
|
||||
}
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { ChannelListLoadingStateComponent } from './channel-list-loading-state.component';
|
||||
|
||||
describe('ChannelListLoadingStateComponent', () => {
|
||||
let fixture: ComponentFixture<ChannelListLoadingStateComponent>;
|
||||
let component: ChannelListLoadingStateComponent;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [
|
||||
ChannelListLoadingStateComponent,
|
||||
TranslateModule.forRoot(),
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ChannelListLoadingStateComponent);
|
||||
component = fixture.componentInstance;
|
||||
});
|
||||
|
||||
it('renders channel rows 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);
|
||||
expect(
|
||||
fixture.nativeElement.querySelector('.groups-loading-layout')
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('renders a two-column group loading layout for the groups view', () => {
|
||||
fixture.componentRef.setInput('view', 'groups');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.isGroupsView()).toBe(true);
|
||||
expect(
|
||||
fixture.nativeElement.querySelectorAll('.groups-loading-nav__item')
|
||||
.length
|
||||
).toBe(component.groupRows.length);
|
||||
expect(
|
||||
fixture.nativeElement.querySelector('.groups-loading-content')
|
||||
).not.toBeNull();
|
||||
});
|
||||
});
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
input,
|
||||
} from '@angular/core';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
|
||||
@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],
|
||||
})
|
||||
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