Proyecto App Linux
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
# shared-portals This library was generated with [Nx](https://nx.dev). ##
|
||||
Running unit tests Run `nx test shared-portals` to execute the unit tests.
|
||||
@@ -0,0 +1,34 @@
|
||||
import nx from '@nx/eslint-plugin';
|
||||
import baseConfig from '../../../eslint.config.mjs';
|
||||
|
||||
export default [
|
||||
...baseConfig,
|
||||
...nx.configs['flat/angular'],
|
||||
...nx.configs['flat/angular-template'],
|
||||
{
|
||||
files: ['**/*.ts'],
|
||||
rules: {
|
||||
'@angular-eslint/directive-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'attribute',
|
||||
prefix: 'app',
|
||||
style: 'camelCase',
|
||||
},
|
||||
],
|
||||
'@angular-eslint/component-selector': [
|
||||
'error',
|
||||
{
|
||||
type: 'element',
|
||||
prefix: 'app',
|
||||
style: 'kebab-case',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['**/*.html'],
|
||||
// Override or add rules here
|
||||
rules: {},
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,21 @@
|
||||
export default {
|
||||
displayName: 'shared-portals',
|
||||
preset: '../../../jest.preset.js',
|
||||
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
|
||||
coverageDirectory: '../../../coverage/libs/ui/shared-portals',
|
||||
transform: {
|
||||
'^.+\\.(ts|mjs|js|html)$': [
|
||||
'jest-preset-angular',
|
||||
{
|
||||
tsconfig: '<rootDir>/tsconfig.spec.json',
|
||||
stringifyContentPathRegex: '\\.(html|svg)$',
|
||||
},
|
||||
],
|
||||
},
|
||||
transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
|
||||
snapshotSerializers: [
|
||||
'jest-preset-angular/build/serializers/no-ng-attributes',
|
||||
'jest-preset-angular/build/serializers/ng-snapshot',
|
||||
'jest-preset-angular/build/serializers/html-comment',
|
||||
]
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "shared-portals",
|
||||
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "libs/ui/shared-portals/src",
|
||||
"prefix": "lib",
|
||||
"projectType": "library",
|
||||
"tags": [
|
||||
"scope:portal",
|
||||
"domain:portal-shared",
|
||||
"type:ui"
|
||||
],
|
||||
"targets": {
|
||||
"test": {
|
||||
"executor": "@nx/jest:jest",
|
||||
"outputs": [
|
||||
"{workspaceRoot}/coverage/{projectRoot}"
|
||||
],
|
||||
"options": {
|
||||
"jestConfig": "libs/ui/shared-portals/jest.config.ts",
|
||||
"tsConfig": "libs/ui/shared-portals/tsconfig.spec.json"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nx/eslint:lint"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './lib/epg-view/epg-view.component';
|
||||
export * from './lib/live-stream-layout/live-stream-layout.component';
|
||||
export { WebPlayerViewComponent } from '@iptvnator/ui/playback';
|
||||
@@ -0,0 +1,49 @@
|
||||
<div class="epg-title">{{ 'EPG.TITLE' | translate }}</div>
|
||||
|
||||
<div class="epg-list">
|
||||
@for (item of epgItems; track $index) {
|
||||
<article class="epg-item" [class.current-program]="isCurrentProgram(item)">
|
||||
<div class="epg-item__meta">
|
||||
<div class="epg-item__time">
|
||||
{{ item.start | date: 'HH:mm' }} -
|
||||
{{ item.stop ?? item.end | date: 'HH:mm' }}
|
||||
</div>
|
||||
@if (isCurrentProgram(item)) {
|
||||
<span class="epg-item__badge">Live</span>
|
||||
}
|
||||
<button
|
||||
mat-icon-button
|
||||
class="epg-item__info"
|
||||
(click)="showDetails(item)"
|
||||
>
|
||||
<mat-icon>info</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="epg-item__title">{{ item.title }}</div>
|
||||
|
||||
@if (item.description) {
|
||||
<div class="epg-item__description">
|
||||
{{ item.description }}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (isCurrentProgram(item)) {
|
||||
<div class="epg-item__progress">
|
||||
<div class="epg-progress-track">
|
||||
<div
|
||||
class="epg-progress-fill"
|
||||
[style.width.%]="getProgress(item)"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</article>
|
||||
}
|
||||
|
||||
@if (epgItems?.length === 0) {
|
||||
<div class="epg-empty">
|
||||
{{ 'EPG.EPG_NOT_AVAILABLE_CHANNEL_TITLE' | translate }}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -0,0 +1,188 @@
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.epg-title {
|
||||
margin: 0;
|
||||
padding: 14px 16px 10px;
|
||||
font-size: 1.02rem;
|
||||
font-weight: 620;
|
||||
letter-spacing: 0.01em;
|
||||
flex-shrink: 0;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.epg-list {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 10px 10px 22px;
|
||||
box-sizing: border-box;
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
|
||||
.epg-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
padding: 10px 14px 16px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
border-radius: 14px;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.035), rgba(255, 255, 255, 0)),
|
||||
rgba(127, 127, 127, 0.045);
|
||||
box-shadow:
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.04),
|
||||
0 10px 20px -20px rgba(0, 0, 0, 0.35);
|
||||
transition:
|
||||
background 0.15s ease,
|
||||
border-color 0.15s ease,
|
||||
box-shadow 0.18s ease,
|
||||
transform 0.18s ease;
|
||||
|
||||
&:hover {
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0)),
|
||||
rgba(127, 127, 127, 0.065);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
&.current-program {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--app-selection-surface-strong),
|
||||
var(--app-selection-surface)
|
||||
);
|
||||
border-color: var(--app-selection-border);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px var(--app-selection-border),
|
||||
0 12px 20px -18px var(--app-selection-glow);
|
||||
|
||||
.epg-item__title,
|
||||
.epg-item__time,
|
||||
.epg-item__description,
|
||||
.epg-item__info {
|
||||
color: var(--app-selection-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.epg-item__meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.epg-item__time {
|
||||
font-size: 0.78rem;
|
||||
line-height: 1;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
font-variant-numeric: tabular-nums;
|
||||
opacity: 0.76;
|
||||
}
|
||||
|
||||
.epg-item__badge {
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
font-size: 0.68rem;
|
||||
line-height: 1.4;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
background: color-mix(in srgb, var(--app-selection-color) 18%, transparent);
|
||||
color: var(--app-selection-color);
|
||||
border: 1px solid color-mix(in srgb, var(--app-selection-border) 86%, transparent);
|
||||
}
|
||||
|
||||
.epg-item__info {
|
||||
margin-inline-start: auto;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 10px;
|
||||
color: inherit;
|
||||
opacity: 0.76;
|
||||
transition:
|
||||
opacity 0.15s ease,
|
||||
background-color 0.15s ease,
|
||||
transform 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
background: color-mix(in srgb, currentColor 10%, transparent);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
|
||||
.epg-item__title {
|
||||
font-size: 0.94rem;
|
||||
line-height: 1.35;
|
||||
font-weight: 560;
|
||||
letter-spacing: 0.005em;
|
||||
}
|
||||
|
||||
.epg-item__description {
|
||||
font-size: 0.82rem;
|
||||
line-height: 1.4;
|
||||
opacity: 0.74;
|
||||
display: -webkit-box;
|
||||
overflow: hidden;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
.epg-item__progress {
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
.epg-progress-track {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
overflow: hidden;
|
||||
border-radius: 999px;
|
||||
background-color: rgba(127, 127, 127, 0.28);
|
||||
background-image: linear-gradient(
|
||||
180deg,
|
||||
rgba(255, 255, 255, 0.12),
|
||||
rgba(255, 255, 255, 0)
|
||||
);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px rgba(255, 255, 255, 0.12),
|
||||
inset 0 1px 1px rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
.epg-progress-fill {
|
||||
position: absolute;
|
||||
inset-block: 0;
|
||||
inset-inline-start: 0;
|
||||
border-radius: inherit;
|
||||
background-color: var(--app-selection-color);
|
||||
background-image: linear-gradient(
|
||||
90deg,
|
||||
rgba(255, 255, 255, 0.18),
|
||||
rgba(255, 255, 255, 0)
|
||||
);
|
||||
box-shadow:
|
||||
0 0 0 1px color-mix(in srgb, var(--app-selection-border) 82%, transparent),
|
||||
0 0 10px -2px var(--app-selection-glow);
|
||||
transition: width 0.35s ease;
|
||||
}
|
||||
|
||||
.epg-empty {
|
||||
padding: 22px;
|
||||
border: 1px dashed rgba(255, 255, 255, 0.12);
|
||||
border-radius: 14px;
|
||||
text-align: center;
|
||||
font-size: 0.88rem;
|
||||
opacity: 0.66;
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { Component, Input, inject } from '@angular/core';
|
||||
import { MatIconButton } from '@angular/material/button';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { MatListModule } from '@angular/material/list';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { EpgItemDescriptionComponent } from '@iptvnator/ui/epg';
|
||||
import { EpgItem } from 'shared-interfaces';
|
||||
|
||||
@Component({
|
||||
selector: 'app-epg-view',
|
||||
templateUrl: './epg-view.component.html',
|
||||
imports: [DatePipe, MatIconButton, MatIcon, MatListModule, TranslateModule],
|
||||
styleUrls: ['./epg-view.component.scss'],
|
||||
})
|
||||
export class EpgViewComponent {
|
||||
@Input() epgItems: EpgItem[] = [];
|
||||
|
||||
dialog = inject(MatDialog);
|
||||
|
||||
isCurrentProgram(item: EpgItem): boolean {
|
||||
const end = item.stop ?? item.end;
|
||||
const now = new Date().getTime();
|
||||
const start = new Date(item.start).getTime();
|
||||
const stop = new Date(end).getTime();
|
||||
return now >= start && now <= stop;
|
||||
}
|
||||
|
||||
getProgress(item: EpgItem): number {
|
||||
const now = new Date().getTime();
|
||||
const start = new Date(item.start).getTime();
|
||||
const end = new Date(item.stop ?? item.end).getTime();
|
||||
|
||||
const total = end - start;
|
||||
const current = now - start;
|
||||
|
||||
return Math.min(Math.max((current / total) * 100, 0), 100);
|
||||
}
|
||||
|
||||
showDetails(item: EpgItem) {
|
||||
this.dialog.open(EpgItemDescriptionComponent, {
|
||||
width: '600px',
|
||||
data: {
|
||||
title: item.title ?? 'No title',
|
||||
desc: item.description ?? 'No description',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<div
|
||||
[class]="
|
||||
player === 'videojs' || player === 'html5'
|
||||
? 'itv-container'
|
||||
: 'itv-container-epg'
|
||||
"
|
||||
>
|
||||
<div
|
||||
class="sidebar"
|
||||
appResizable
|
||||
[minWidth]="250"
|
||||
[maxWidth]="600"
|
||||
[defaultWidth]="400"
|
||||
storageKey="sidebar-width"
|
||||
>
|
||||
<div class="sidebar-content">
|
||||
<div class="search-panel">
|
||||
<mat-form-field class="search-bar">
|
||||
<mat-icon matPrefix>search</mat-icon>
|
||||
<input
|
||||
matInput
|
||||
type="search"
|
||||
[placeholder]="'CHANNELS.SEARCH_CHANNEL' | translate"
|
||||
[(ngModel)]="searchString"
|
||||
/>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="nav-list" style="height: calc(100% - 70px)">
|
||||
<cdk-virtual-scroll-viewport
|
||||
[itemSize]="56"
|
||||
class="scroll-viewport-portals"
|
||||
>
|
||||
<div
|
||||
class="nav-item"
|
||||
*cdkVirtualFor="
|
||||
let item of channels
|
||||
| filterBy: searchString() : 'name';
|
||||
trackBy: trackBy;
|
||||
templateCacheSize: 0
|
||||
"
|
||||
[class.selected]="
|
||||
item.stream_id === activeLiveStream?.stream_id
|
||||
"
|
||||
(click)="itemClicked.emit(item)"
|
||||
>
|
||||
@if (item.stream_icon) {
|
||||
<img
|
||||
class="stream-icon"
|
||||
[src]="item.stream_icon"
|
||||
alt="channel icon"
|
||||
/>
|
||||
}
|
||||
<div class="title">
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</div>
|
||||
</cdk-virtual-scroll-viewport>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@if (player === 'videojs' || player === 'html5') {
|
||||
@if (streamUrl) {
|
||||
<div class="video-player">
|
||||
<app-web-player-view [streamUrl]="streamUrl" />
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@if (streamUrl) {
|
||||
<div class="epg">
|
||||
<div class="epg-content">
|
||||
<app-epg-view [epgItems]="epgItems" />
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="no-channel-selected">
|
||||
{{ 'PORTALS.SELECT_CATEGORY' | translate }}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -0,0 +1,201 @@
|
||||
@use '@angular/material' as mat;
|
||||
|
||||
:host {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.itv-container {
|
||||
display: grid;
|
||||
// Use auto for sidebar to respect the directive's width
|
||||
grid-template-columns: auto 1fr 1fr;
|
||||
grid-template-rows: minmax(0, 1fr) minmax(0, 1fr);
|
||||
gap: 0 0;
|
||||
grid-auto-flow: row;
|
||||
grid-template-areas:
|
||||
'sidebar video-player video-player'
|
||||
'sidebar epg epg';
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.itv-container-epg {
|
||||
display: grid;
|
||||
// Use auto for sidebar to respect the directive's width
|
||||
grid-template-columns: auto 1fr 1fr;
|
||||
grid-template-rows: repeat(3, minmax(0, 1fr));
|
||||
gap: 0 0;
|
||||
grid-auto-flow: row;
|
||||
grid-template-areas:
|
||||
'sidebar epg epg'
|
||||
'sidebar epg epg'
|
||||
'sidebar epg epg';
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
grid-area: sidebar;
|
||||
// Default width (controlled by resizable directive)
|
||||
width: 400px;
|
||||
min-width: 250px;
|
||||
max-width: 600px;
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
border-right: 1px solid #333;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.video-player {
|
||||
grid-area: video-player;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.video-player > app-web-player-view {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.epg {
|
||||
grid-area: epg;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.search-panel {
|
||||
margin-top: 10px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: -15px;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
width: 90%;
|
||||
@include mat.form-field-density(-5);
|
||||
}
|
||||
|
||||
.epg-content {
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-right: #111 1px solid;
|
||||
}
|
||||
|
||||
.epg-content > app-epg-view {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.sidebar-content {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
border-right: #111 1px solid;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.no-channel-selected {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
::ng-deep {
|
||||
.search-bar {
|
||||
div {
|
||||
border-radius: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nav-list {
|
||||
flex: 1;
|
||||
overflow-y: hidden;
|
||||
padding: 0 8px 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 10px;
|
||||
background: transparent;
|
||||
color: var(--mat-sys-on-surface-variant);
|
||||
text-align: left;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
box-sizing: border-box;
|
||||
transition:
|
||||
background 0.15s ease,
|
||||
color 0.15s ease,
|
||||
border-color 0.15s ease,
|
||||
box-shadow 0.18s ease,
|
||||
transform 0.18s ease;
|
||||
|
||||
.stream-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 4px;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
background: color-mix(in srgb, var(--mat-sys-on-surface) 5%, transparent);
|
||||
}
|
||||
|
||||
span,
|
||||
.title {
|
||||
line-height: 1.2;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--mat-sys-surface-container);
|
||||
color: var(--mat-sys-on-surface);
|
||||
}
|
||||
|
||||
&.selected,
|
||||
&.active {
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--app-selection-surface-strong),
|
||||
var(--app-selection-surface)
|
||||
) !important;
|
||||
color: var(--app-selection-color) !important;
|
||||
border-color: var(--app-selection-border) !important;
|
||||
font-weight: 600;
|
||||
box-shadow:
|
||||
inset 0 0 0 1px var(--app-selection-border),
|
||||
0 12px 20px -18px var(--app-selection-glow);
|
||||
transform: translateY(-1px);
|
||||
|
||||
* {
|
||||
color: var(--app-selection-color) !important;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset-inline-start: 6px;
|
||||
top: 7px;
|
||||
bottom: 7px;
|
||||
width: 3px;
|
||||
border-radius: 999px;
|
||||
background: var(--app-selection-color);
|
||||
box-shadow: 0 0 10px var(--app-selection-glow);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import { ScrollingModule } from '@angular/cdk/scrolling';
|
||||
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
Output,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatListModule } from '@angular/material/list';
|
||||
import { FilterPipe } from '@iptvnator/pipes';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { WebPlayerViewComponent } from '@iptvnator/ui/playback';
|
||||
import { ResizableDirective } from 'components';
|
||||
import { EpgItem, VideoPlayer, XtreamItem } from 'shared-interfaces';
|
||||
import { EpgViewComponent } from '../epg-view/epg-view.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-live-stream-layout',
|
||||
templateUrl: './live-stream-layout.component.html',
|
||||
styleUrls: ['./live-stream-layout.component.scss'],
|
||||
imports: [
|
||||
EpgViewComponent,
|
||||
FilterPipe,
|
||||
FormsModule,
|
||||
MatListModule,
|
||||
MatIconModule,
|
||||
MatInputModule,
|
||||
MatFormFieldModule,
|
||||
ResizableDirective,
|
||||
ScrollingModule,
|
||||
WebPlayerViewComponent,
|
||||
TranslateModule
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class LiveStreamLayoutComponent {
|
||||
@Input({ required: true }) channels!: XtreamItem[];
|
||||
@Input({ required: true }) player: VideoPlayer = VideoPlayer.VideoJs;
|
||||
@Input() epgItems!: EpgItem[];
|
||||
@Input() streamUrl!: string;
|
||||
@Input() activeLiveStream!: XtreamItem;
|
||||
|
||||
@Output() itemClicked = new EventEmitter<XtreamItem>();
|
||||
|
||||
searchString = signal<string>('');
|
||||
|
||||
trackBy(_index: number, item: XtreamItem) {
|
||||
return item.stream_id;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';
|
||||
|
||||
setupZoneTestEnv({
|
||||
errorOnUnknownElements: true,
|
||||
errorOnUnknownProperties: true
|
||||
});
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"target": "es2022",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"module": "preserve"
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"typeCheckHostBindings": true,
|
||||
"strictTemplates": true
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../dist/out-tsc",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"inlineSources": true,
|
||||
"types": []
|
||||
},
|
||||
"exclude": [
|
||||
"src/**/*.spec.ts",
|
||||
"src/test-setup.ts",
|
||||
"jest.config.ts",
|
||||
"src/**/*.test.ts"
|
||||
],
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../dist/out-tsc",
|
||||
"module": "commonjs",
|
||||
"target": "es2016",
|
||||
"types": [
|
||||
"jest",
|
||||
"node"
|
||||
],
|
||||
"moduleResolution": "node10"
|
||||
},
|
||||
"files": [
|
||||
"src/test-setup.ts"
|
||||
],
|
||||
"include": [
|
||||
"jest.config.ts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user