feat: general development, update core application components, and enhance build scripts.

This commit is contained in:
4gray committed 2026-03-01 22:52:33 +01:00
1 parent 9a57eaff92
commit 992b996ecd
37 files changed
+500 -341

No files matched your search

@@ -1,7 +1,14 @@
@use '@angular/material' as mat;
:host {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
}
.scroll-viewport {
min-height: calc(100vh - 190px);
height: 100%;
overflow-x: hidden;
width: 100%;
@@ -26,9 +33,9 @@
}
#all-channels {
flex: 1;
min-height: 0;
overflow: hidden;
overflow-x: hidden;
max-height: calc(100vh - 167px);
}
.channel-list {
@@ -89,7 +96,5 @@
}
@media only screen and (max-width: 599px) {
.scroll-viewport {
min-height: calc(50vh - 159px);
}
/* Mobile responsive tweaks if needed */
}
@@ -26,20 +26,20 @@ export interface EnrichedChannel extends Channel {
}
@Component({
selector: 'app-all-channels-tab',
templateUrl: './all-channels-tab.component.html',
styleUrls: ['./all-channels-tab.component.scss'],
selector: 'app-all-channels-view',
templateUrl: './all-channels-view.component.html',
styleUrls: ['./all-channels-view.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
ChannelListItemComponent,
MatFormFieldModule,
MatIconModule,
MatInputModule,
ScrollingModule,
TranslatePipe
],
ChannelListItemComponent,
MatFormFieldModule,
MatIconModule,
MatInputModule,
ScrollingModule,
TranslatePipe,
],
})
export class AllChannelsTabComponent implements OnDestroy {
export class AllChannelsViewComponent implements OnDestroy {
/** All channels (will be filtered by search) */
readonly channels = input.required<Channel[]>();
@@ -1,14 +1,16 @@
@if (displayedChannels().length) {
<mat-tab-group color="primary" backgroundColor="primary">
<!-- All Channels Tab -->
<mat-tab>
<ng-template mat-tab-label>
<mat-icon class="tab-icon">list</mat-icon>
<div class="tab-title">
{{ 'CHANNELS.ALL_CHANNELS' | translate }}
</div>
</ng-template>
<app-all-channels-tab
@if (viewTitle()) {
<header class="context-header">
<div class="context-header__top">
<h2>{{ viewTitle() | translate }}</h2>
</div>
</header>
<div class="context-divider"></div>
}
@switch (activeView()) {
@case ('all') {
<app-all-channels-view
[channels]="displayedChannels()"
[channelEpgMap]="channelEpgMap()"
[progressTick]="progressTick()"
@@ -19,17 +21,9 @@
(channelSelected)="onChannelSelected($event)"
(favoriteToggled)="onFavoriteToggled($event)"
/>
</mat-tab>
<!-- Groups Tab -->
<mat-tab>
<ng-template mat-tab-label>
<mat-icon class="tab-icon">category</mat-icon>
<div class="tab-title">
{{ 'CHANNELS.GROUPS' | translate }}
</div>
</ng-template>
<app-groups-tab
}
@case ('groups') {
<app-groups-view
[groupedChannels]="groupedChannels()"
[channelEpgMap]="channelEpgMap()"
[progressTick]="progressTick()"
@@ -39,34 +33,23 @@
(channelSelected)="onChannelSelected($event)"
(favoriteToggled)="onFavoriteToggled($event)"
/>
</mat-tab>
<!-- Favorites Tab -->
@let showFavorites = (playlistId$ | async) !== 'GLOBAL_FAVORITES';
@if (showFavorites) {
<mat-tab>
<ng-template mat-tab-label>
<mat-icon class="tab-icon">star</mat-icon>
<div class="tab-title">
{{ 'CHANNELS.FAVORITES' | translate }}
</div>
</ng-template>
@let favorites = favorites$ | async;
@if (favorites) {
<app-favorites-tab
[favorites]="favorites"
[channelEpgMap]="channelEpgMap()"
[progressTick]="progressTick()"
[shouldShowEpg]="shouldShowEpg()"
[activeChannelUrl]="activeChannelUrl()"
(channelSelected)="onChannelSelected($event)"
(favoriteToggled)="onFavoriteToggled($event)"
(favoritesReordered)="onFavoritesReordered($event)"
/>
}
</mat-tab>
}
</mat-tab-group>
@case ('favorites') {
@let favorites = favorites$ | async;
@if (favorites) {
<app-favorites-view
[favorites]="favorites"
[channelEpgMap]="channelEpgMap()"
[progressTick]="progressTick()"
[shouldShowEpg]="shouldShowEpg()"
[activeChannelUrl]="activeChannelUrl()"
(channelSelected)="onChannelSelected($event)"
(favoriteToggled)="onFavoriteToggled($event)"
(favoritesReordered)="onFavoritesReordered($event)"
/>
}
}
}
} @else {
<div class="loading-container">
<mat-icon class="loading-spinner">sync</mat-icon>
@@ -1,5 +1,6 @@
:host {
display: block;
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
container-type: inline-size;
@@ -52,3 +53,33 @@
display: block;
}
}
// ─── Header ───────────────────────────────────────────────────────────────────
.context-header {
padding: 12px 16px 8px;
flex-shrink: 0;
}
.context-header__top {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
h2 {
margin: 0;
font-size: 0.94rem;
font-weight: 600;
letter-spacing: -0.01em;
color: var(--mat-sys-on-surface);
}
}
// ─── Divider ──────────────────────────────────────────────────────────────────
.context-divider {
height: 1px;
background: var(--mat-sys-outline-variant);
margin: 0 0 6px;
opacity: 0.6;
flex-shrink: 0;
}
@@ -5,12 +5,14 @@ import {
computed,
inject,
Input,
input,
OnDestroy,
OnInit,
signal,
} from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { MatIconModule } from '@angular/material/icon';
import { MatTabsModule } from '@angular/material/tabs';
import { NavigationEnd, Router } from '@angular/router';
import { Store } from '@ngrx/store';
import { StorageMap } from '@ngx-pwa/local-storage';
import { TranslatePipe } from '@ngx-translate/core';
@@ -22,18 +24,12 @@ import {
selectActivePlaylistId,
selectFavorites,
} from 'm3u-state';
import { BehaviorSubject, combineLatest, map, skipWhile } from 'rxjs';
import { BehaviorSubject, combineLatest, filter, map, skipWhile } from 'rxjs';
import { EpgService } from 'services';
import {
Channel,
EpgProgram,
GLOBAL_FAVORITES_PLAYLIST_ID,
Settings,
STORE_KEY,
} from 'shared-interfaces';
import { AllChannelsTabComponent } from './all-channels-tab/all-channels-tab.component';
import { FavoritesTabComponent } from './favorites-tab/favorites-tab.component';
import { GroupsTabComponent } from './groups-tab/groups-tab.component';
import { Channel, EpgProgram, Settings, STORE_KEY } from 'shared-interfaces';
import { AllChannelsViewComponent } from './all-channels-view/all-channels-view.component';
import { FavoritesViewComponent } from './favorites-view/favorites-view.component';
import { GroupsViewComponent } from './groups-view/groups-view.component';
@Component({
selector: 'app-channel-list-container',
@@ -41,12 +37,11 @@ import { GroupsTabComponent } from './groups-tab/groups-tab.component';
styleUrls: ['./channel-list-container.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
AllChannelsTabComponent,
AllChannelsViewComponent,
CommonModule,
FavoritesTabComponent,
GroupsTabComponent,
FavoritesViewComponent,
GroupsViewComponent,
MatIconModule,
MatTabsModule,
TranslatePipe,
],
})
@@ -54,6 +49,7 @@ export class ChannelListContainerComponent implements OnInit, OnDestroy {
private readonly epgService = inject(EpgService);
private readonly storage = inject(StorageMap);
private readonly store = inject(Store);
private readonly router = inject(Router);
/** Map of channel ID to current EPG program */
readonly channelEpgMap = signal(new Map<string, EpgProgram | null>());
@@ -73,6 +69,30 @@ export class ChannelListContainerComponent implements OnInit, OnDestroy {
/** Item size for virtual scroll - compact when no EPG */
readonly itemSize = computed(() => (this.shouldShowEpg() ? 68 : 48));
/** Active view (all, groups, favorites) */
readonly activeView = input<string>('all');
readonly currentUrl = toSignal(
this.router.events.pipe(
filter((event) => event instanceof NavigationEnd),
map((event) => (event as NavigationEnd).urlAfterRedirects)
),
{ initialValue: this.router.url }
);
readonly viewTitle = computed(() => {
const view = this.activeView();
const url = this.currentUrl();
if (view === 'all') return 'CHANNELS.ALL_CHANNELS';
if (view === 'groups') return 'CHANNELS.GROUPS';
if (view === 'favorites') {
return url.includes('/workspace/global-favorites')
? 'HOME.PLAYLISTS.GLOBAL_FAVORITES'
: 'CHANNELS.FAVORITES';
}
return '';
});
/** Channels array */
_channelList: Channel[] = [];
private readonly channelListSignal = signal<Channel[]>([]);
@@ -98,11 +118,7 @@ export class ChannelListContainerComponent implements OnInit, OnDestroy {
/** Displayed channels - filters out unfavorited channels in global favorites view */
readonly displayedChannels = computed(() => {
const channels = this.channelListSignal();
if (this.activePlaylistIdSignal() === GLOBAL_FAVORITES_PLAYLIST_ID) {
return channels.filter((ch) => this.favoriteIds().has(ch.url));
}
return channels;
return this.channelListSignal();
});
/** Object with channels sorted by groups */
@@ -1,6 +1,15 @@
:host {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
}
#favorites-list {
height: calc(100vh - 105px);
flex: 1;
min-height: 0;
overflow-x: hidden;
overflow-y: auto;
}
.channel-list {
@@ -12,17 +12,17 @@ import {
} from '@angular/core';
import { TranslatePipe } from '@ngx-translate/core';
import { Channel, EpgProgram } from 'shared-interfaces';
import { EnrichedChannel } from '../all-channels-tab/all-channels-tab.component';
import { EnrichedChannel } from '../all-channels-view/all-channels-view.component';
import { ChannelListItemComponent } from '../channel-list-item/channel-list-item.component';
@Component({
selector: 'app-favorites-tab',
templateUrl: './favorites-tab.component.html',
styleUrls: ['./favorites-tab.component.scss'],
selector: 'app-favorites-view',
templateUrl: './favorites-view.component.html',
styleUrls: ['./favorites-view.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [ChannelListItemComponent, DragDropModule, TranslatePipe],
})
export class FavoritesTabComponent {
export class FavoritesViewComponent {
/** Favorite channels */
readonly favorites = input.required<Channel[]>();
@@ -1,6 +1,15 @@
:host {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
}
#groups-list {
height: calc(100vh - 105px);
flex: 1;
min-height: 0;
overflow-x: hidden;
overflow-y: auto;
}
.channel-list {
@@ -16,13 +16,13 @@ import { MatExpansionModule } from '@angular/material/expansion';
import { MatIconModule } from '@angular/material/icon';
import { TranslatePipe } from '@ngx-translate/core';
import { Channel, EpgProgram } from 'shared-interfaces';
import { EnrichedChannel } from '../all-channels-tab/all-channels-tab.component';
import { EnrichedChannel } from '../all-channels-view/all-channels-view.component';
import { ChannelListItemComponent } from '../channel-list-item/channel-list-item.component';
@Component({
selector: 'app-groups-tab',
templateUrl: './groups-tab.component.html',
styleUrls: ['./groups-tab.component.scss'],
selector: 'app-groups-view',
templateUrl: './groups-view.component.html',
styleUrls: ['./groups-view.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
ChannelListItemComponent,
@@ -33,7 +33,7 @@ import { ChannelListItemComponent } from '../channel-list-item/channel-list-item
TranslatePipe,
],
})
export class GroupsTabComponent implements AfterViewInit, OnDestroy {
export class GroupsViewComponent implements AfterViewInit, OnDestroy {
private readonly cdr = inject(ChangeDetectorRef);
private readonly ngZone = inject(NgZone);
@@ -17,4 +17,7 @@
</div>
}
<app-channel-list-container [channelList]="channels()" />
<app-channel-list-container
[channelList]="channels()"
[activeView]="activeView()"
/>
@@ -27,6 +27,7 @@ import { ChannelListContainerComponent } from './../../channel-list-container/ch
export class SidebarComponent {
readonly channels = input<Channel[]>([]);
readonly showPlaylistHeader = input(true);
readonly activeView = input<string>('all');
private readonly store = inject(Store);
private readonly translate = inject(TranslateService);
@@ -7,7 +7,7 @@
>
<mat-icon>{{ isLeftDrawerOpened ? 'menu' : 'menu_open' }}</mat-icon>
</button>
@if ((playlistId$ | async) !== 'GLOBAL_FAVORITES') {
<!-- @if (playlistId$ | async) {
<button
mat-icon-button
(click)="updateFavoriteStatus(activeChannel)"
@@ -21,7 +21,7 @@
}}</mat-icon
>
</button>
}
} -->
<button
mat-icon-button
(click)="infoOverlayClicked.emit()"
@@ -31,13 +31,13 @@
</button>
{{ activeChannel?.name }}
<div class="spacer"></div>
<button
<!-- <button
mat-icon-button
(click)="settingsClicked.emit()"
[matTooltip]="'MENU.SETTINGS' | translate"
>
<mat-icon>settings</mat-icon>
</button>
</button> -->
<button
mat-icon-button
(click)="multiEpgClicked.emit()"
@@ -8,10 +8,10 @@ import { MatTooltip } from '@angular/material/tooltip';
import { Store } from '@ngrx/store';
import { TranslatePipe } from '@ngx-translate/core';
import {
FavoritesActions,
selectActivePlaylistId,
selectFavorites,
selectIsEpgAvailable,
FavoritesActions,
} from 'm3u-state';
import { Channel } from 'shared-interfaces';