mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-09 01:16:15 -08:00
Merge pull request #694 from 4gray/refactor/playlist-filter
refactor/merge-playlist-filter-and-sort-into-one-menu
This commit is contained in:
39 files changed
+839
-679
No files matched your search
@@ -4,10 +4,8 @@ import { Router, RouterOutlet } from '@angular/router';
|
||||
import { Actions, ofType } from '@ngrx/effects';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { PlaylistActions, selectAllPlaylistsMeta } from 'm3u-state';
|
||||
import { filter, take } from 'rxjs';
|
||||
/* import * as semver from 'semver'; */
|
||||
import * as PlaylistActions from 'm3u-state';
|
||||
import { selectAllPlaylistsMeta } from 'm3u-state';
|
||||
import { DataService, EpgService } from 'services';
|
||||
import {
|
||||
AUTO_UPDATE_PLAYLISTS,
|
||||
@@ -28,7 +26,9 @@ import { SearchResultsComponent } from './xtream-tauri/search-results/search-res
|
||||
})
|
||||
export class AppComponent implements OnInit {
|
||||
@HostBinding('class.macos-platform') get isMacOS() {
|
||||
return window.electron && navigator.platform.toLowerCase().includes('mac');
|
||||
return (
|
||||
window.electron && navigator.platform.toLowerCase().includes('mac')
|
||||
);
|
||||
}
|
||||
private actions$ = inject(Actions);
|
||||
private dataService = inject(DataService);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Component, EventEmitter, inject, Output } from '@angular/core';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { addPlaylist } from 'm3u-state';
|
||||
import { PlaylistActions } from 'm3u-state';
|
||||
import { DragDropFileUploadDirective } from './drag-drop-file-upload.directive';
|
||||
|
||||
@Component({
|
||||
@@ -45,7 +45,7 @@ export class FileUploadComponent {
|
||||
playlistObject
|
||||
);
|
||||
this.store.dispatch(
|
||||
addPlaylist({ playlist: playlistObject })
|
||||
PlaylistActions.addPlaylist({ playlist: playlistObject })
|
||||
);
|
||||
} else {
|
||||
// User canceled the dialog
|
||||
|
||||
@@ -11,7 +11,7 @@ import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { addPlaylist } from 'm3u-state';
|
||||
import { PlaylistActions } from 'm3u-state';
|
||||
import { DataService } from 'services';
|
||||
import { Playlist } from 'shared-interfaces';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
@@ -67,7 +67,7 @@ export class StalkerPortalImportComponent {
|
||||
this.form.value.portalUrl
|
||||
);
|
||||
this.store.dispatch(
|
||||
addPlaylist({ playlist: this.form.value as Playlist })
|
||||
PlaylistActions.addPlaylist({ playlist: this.form.value as Playlist })
|
||||
);
|
||||
this.addClicked.emit();
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
ToolbarComponent,
|
||||
VjsPlayerComponent,
|
||||
} from 'components';
|
||||
import * as PlaylistActions from 'm3u-state';
|
||||
import { PlaylistActions, ChannelActions, FavoritesActions } from 'm3u-state';
|
||||
import {
|
||||
selectActive,
|
||||
selectChannels,
|
||||
@@ -169,7 +169,7 @@ export class VideoPlayerComponent implements OnInit {
|
||||
}
|
||||
|
||||
this.store.dispatch(
|
||||
PlaylistActions.setChannels({
|
||||
ChannelActions.setChannels({
|
||||
channels: playlist.playlist.items,
|
||||
})
|
||||
);
|
||||
@@ -180,14 +180,14 @@ export class VideoPlayerComponent implements OnInit {
|
||||
playlist.favorites.length > 0
|
||||
) {
|
||||
this.store.dispatch(
|
||||
PlaylistActions.setFavorites({
|
||||
FavoritesActions.setFavorites({
|
||||
channelIds: playlist.favorites,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
// Clear favorites if playlist has none
|
||||
this.store.dispatch(
|
||||
PlaylistActions.setFavorites({
|
||||
FavoritesActions.setFavorites({
|
||||
channelIds: [],
|
||||
})
|
||||
);
|
||||
@@ -255,7 +255,7 @@ export class VideoPlayerComponent implements OnInit {
|
||||
// Dispatch action to change channel
|
||||
const nextChannel = channels[nextIndex];
|
||||
this.store.dispatch(
|
||||
PlaylistActions.setActiveChannel({
|
||||
ChannelActions.setActiveChannel({
|
||||
channel: nextChannel,
|
||||
})
|
||||
);
|
||||
@@ -444,7 +444,7 @@ export class VideoPlayerComponent implements OnInit {
|
||||
.subscribe((channel) => {
|
||||
if (channel) {
|
||||
this.store.dispatch(
|
||||
PlaylistActions.setActiveChannel({ channel })
|
||||
ChannelActions.setActiveChannel({ channel })
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -12,7 +12,7 @@ import { MatIcon } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { addPlaylist } from 'm3u-state';
|
||||
import { PlaylistActions } from 'm3u-state';
|
||||
import { PortalStatus, PortalStatusService } from 'services';
|
||||
import { Playlist } from 'shared-interfaces';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
@@ -137,7 +137,7 @@ export class XtreamCodeImportComponent {
|
||||
url.port ? ':' + url.port : ''
|
||||
}`;
|
||||
this.store.dispatch(
|
||||
addPlaylist({
|
||||
PlaylistActions.addPlaylist({
|
||||
playlist: {
|
||||
...this.form.value,
|
||||
serverUrl,
|
||||
|
||||
@@ -3,7 +3,7 @@ import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { Params } from '@angular/router';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import * as PlaylistActions from 'm3u-state';
|
||||
import { PlaylistActions } from 'm3u-state';
|
||||
import { DataService } from 'services';
|
||||
import {
|
||||
AUTO_UPDATE_PLAYLISTS,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Params } from '@angular/router';
|
||||
import { SwUpdate } from '@angular/service-worker';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import * as PlaylistActions from 'm3u-state';
|
||||
import { PlaylistActions } from 'm3u-state';
|
||||
import { catchError, firstValueFrom, throwError } from 'rxjs';
|
||||
import { DataService } from 'services';
|
||||
import {
|
||||
|
||||
@@ -5,7 +5,7 @@ button {
|
||||
|
||||
.settings-container {
|
||||
overflow: auto;
|
||||
height: calc(100vh - 100px);
|
||||
height: calc(100vh - 70px);
|
||||
}
|
||||
|
||||
.row {
|
||||
|
||||
@@ -35,8 +35,7 @@ import { Store } from '@ngrx/store';
|
||||
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||
import { QRCodeComponent } from 'angularx-qrcode';
|
||||
import { DialogService } from 'components';
|
||||
import * as PlaylistActions from 'm3u-state';
|
||||
import { selectIsEpgAvailable } from 'm3u-state';
|
||||
import { PlaylistActions, selectIsEpgAvailable } from 'm3u-state';
|
||||
import { take } from 'rxjs';
|
||||
import { DataService, EpgService, PlaylistsService } from 'services';
|
||||
import {
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||
import { parsePlaylist } from 'm3u-state';
|
||||
import { PlaylistActions } from 'm3u-state';
|
||||
import { getFilenameFromUrl } from 'm3u-utils';
|
||||
import { DataService } from 'services';
|
||||
import { PLAYLIST_PARSE_BY_URL } from 'shared-interfaces';
|
||||
@@ -57,11 +57,11 @@ export class AddPlaylistDialogComponent {
|
||||
.result as string;
|
||||
|
||||
this.store.dispatch(
|
||||
parsePlaylist({
|
||||
PlaylistActions.parsePlaylist({
|
||||
uploadType: 'FILE',
|
||||
playlist,
|
||||
title: payload.file.name,
|
||||
path: (payload.file as any).path,
|
||||
path: (payload.file as File & { path?: string }).path,
|
||||
})
|
||||
);
|
||||
this.closeDialog();
|
||||
@@ -93,7 +93,7 @@ export class AddPlaylistDialogComponent {
|
||||
*/
|
||||
uploadAsText(playlist: string): void {
|
||||
this.store.dispatch(
|
||||
parsePlaylist({
|
||||
PlaylistActions.parsePlaylist({
|
||||
uploadType: 'TEXT',
|
||||
playlist,
|
||||
title: this.translateService.instant('HOME.IMPORTED_AS_TEXT'),
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
<mat-menu #filterSortMenu="matMenu" class="filter-sort-menu" xPosition="before">
|
||||
<!-- Filter section -->
|
||||
<section class="filter-section">
|
||||
<div class="menu-section-header">
|
||||
<mat-icon class="section-icon">filter_list</mat-icon>
|
||||
<span>{{ 'HOME.FILTER_BY_TYPE' | translate }}</span>
|
||||
</div>
|
||||
@for (type of playlistTypes; track type.id) {
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="togglePlaylistType(type); $event.stopPropagation()"
|
||||
[class.active-option]="type.checked"
|
||||
>
|
||||
<mat-icon>{{ type.icon }}</mat-icon>
|
||||
<span class="menu-item-text">{{
|
||||
type.translationKey | translate
|
||||
}}</span>
|
||||
<mat-icon
|
||||
class="check-icon"
|
||||
[class.check-visible]="type.checked"
|
||||
>check</mat-icon
|
||||
>
|
||||
</button>
|
||||
}
|
||||
</section>
|
||||
|
||||
<!-- Sort section -->
|
||||
<section class="sort-section">
|
||||
<div class="menu-section-header">
|
||||
<mat-icon class="section-icon">sort</mat-icon>
|
||||
<span>{{ 'HOME.SORT_BY' | translate }}</span>
|
||||
</div>
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="
|
||||
setSortOptions(SortBy.NAME, SortOrder.ASC);
|
||||
$event.stopPropagation()
|
||||
"
|
||||
[class.active-option]="isSortActive(SortBy.NAME, SortOrder.ASC)"
|
||||
>
|
||||
<mat-icon>sort_by_alpha</mat-icon>
|
||||
<span class="menu-item-text">{{
|
||||
'HOME.SORT_OPTIONS.NAME_ASC' | translate
|
||||
}}</span>
|
||||
<mat-icon
|
||||
class="check-icon"
|
||||
[class.check-visible]="isSortActive(SortBy.NAME, SortOrder.ASC)"
|
||||
>check</mat-icon
|
||||
>
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="
|
||||
setSortOptions(SortBy.NAME, SortOrder.DESC);
|
||||
$event.stopPropagation()
|
||||
"
|
||||
[class.active-option]="isSortActive(SortBy.NAME, SortOrder.DESC)"
|
||||
>
|
||||
<mat-icon>sort_by_alpha</mat-icon>
|
||||
<span class="menu-item-text">{{
|
||||
'HOME.SORT_OPTIONS.NAME_DESC' | translate
|
||||
}}</span>
|
||||
<mat-icon
|
||||
class="check-icon"
|
||||
[class.check-visible]="
|
||||
isSortActive(SortBy.NAME, SortOrder.DESC)
|
||||
"
|
||||
>check</mat-icon
|
||||
>
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="
|
||||
setSortOptions(SortBy.DATE_ADDED, SortOrder.DESC);
|
||||
$event.stopPropagation()
|
||||
"
|
||||
[class.active-option]="
|
||||
isSortActive(SortBy.DATE_ADDED, SortOrder.DESC)
|
||||
"
|
||||
>
|
||||
<mat-icon>date_range</mat-icon>
|
||||
<span class="menu-item-text">{{
|
||||
'HOME.SORT_OPTIONS.NEWEST' | translate
|
||||
}}</span>
|
||||
<mat-icon
|
||||
class="check-icon"
|
||||
[class.check-visible]="
|
||||
isSortActive(SortBy.DATE_ADDED, SortOrder.DESC)
|
||||
"
|
||||
>check</mat-icon
|
||||
>
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="
|
||||
setSortOptions(SortBy.DATE_ADDED, SortOrder.ASC);
|
||||
$event.stopPropagation()
|
||||
"
|
||||
[class.active-option]="
|
||||
isSortActive(SortBy.DATE_ADDED, SortOrder.ASC)
|
||||
"
|
||||
>
|
||||
<mat-icon>date_range</mat-icon>
|
||||
<span class="menu-item-text">{{
|
||||
'HOME.SORT_OPTIONS.OLDEST' | translate
|
||||
}}</span>
|
||||
<mat-icon
|
||||
class="check-icon"
|
||||
[class.check-visible]="
|
||||
isSortActive(SortBy.DATE_ADDED, SortOrder.ASC)
|
||||
"
|
||||
>check</mat-icon
|
||||
>
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<!-- Close button -->
|
||||
<div class="menu-footer">
|
||||
<button mat-button (click)="closeMenu()">
|
||||
{{ 'CLOSE' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</mat-menu>
|
||||
@@ -0,0 +1,90 @@
|
||||
// Combined Filter & Sort menu styles
|
||||
.filter-section,
|
||||
.sort-section {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.menu-section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
pointer-events: none;
|
||||
|
||||
.section-icon {
|
||||
font-size: 16px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: rgba(139, 92, 246, 0.6);
|
||||
}
|
||||
}
|
||||
|
||||
// Menu item layout with right-aligned checkmark
|
||||
::ng-deep .filter-sort-menu {
|
||||
.mat-mdc-menu-item {
|
||||
display: flex !important;
|
||||
align-items: center !important;
|
||||
gap: 12px !important;
|
||||
min-height: 40px !important;
|
||||
padding: 0 16px !important;
|
||||
|
||||
.mat-icon:first-child {
|
||||
margin-right: 0 !important;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.menu-item-text {
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
margin-left: auto !important;
|
||||
margin-right: 0 !important;
|
||||
flex-shrink: 0;
|
||||
opacity: 0;
|
||||
transform: scale(0.8);
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
&.check-visible {
|
||||
opacity: 1;
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.active-option {
|
||||
background-color: rgba(139, 92, 246, 0.12) !important;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 3px;
|
||||
height: 60%;
|
||||
background: rgba(139, 92, 246, 0.8);
|
||||
border-radius: 0 2px 2px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
font-size: 18px !important;
|
||||
width: 18px !important;
|
||||
height: 18px !important;
|
||||
color: rgba(139, 92, 246, 1) !important;
|
||||
}
|
||||
|
||||
// Menu footer
|
||||
.menu-footer {
|
||||
padding: 4px 8px 4px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import {
|
||||
Component,
|
||||
effect,
|
||||
inject,
|
||||
input,
|
||||
output,
|
||||
viewChild,
|
||||
} from '@angular/core';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatMenu, MatMenuModule, MatMenuTrigger } from '@angular/material/menu';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { FilterActions, selectActiveTypeFilters } from 'm3u-state';
|
||||
import { SortBy, SortOrder, SortService } from 'services';
|
||||
|
||||
@Component({
|
||||
selector: 'app-filter-sort-menu',
|
||||
templateUrl: './filter-sort-menu.component.html',
|
||||
styleUrls: ['./filter-sort-menu.component.scss'],
|
||||
imports: [MatButtonModule, MatIconModule, MatMenuModule, TranslateModule],
|
||||
})
|
||||
export class FilterSortMenuComponent {
|
||||
private store = inject(Store);
|
||||
private sortService = inject(SortService);
|
||||
|
||||
readonly menu = viewChild.required<MatMenu>('filterSortMenu');
|
||||
readonly menuTrigger = input<MatMenuTrigger>();
|
||||
readonly filterChanged = output<void>();
|
||||
|
||||
playlistTypes = [
|
||||
{
|
||||
id: 'm3u',
|
||||
icon: 'playlist_play',
|
||||
translationKey: 'HOME.PLAYLIST_TYPES.M3U',
|
||||
checked: true,
|
||||
},
|
||||
{
|
||||
id: 'xtream',
|
||||
icon: 'cloud',
|
||||
translationKey: 'HOME.PLAYLIST_TYPES.XTREAM',
|
||||
checked: true,
|
||||
},
|
||||
{
|
||||
id: 'stalker',
|
||||
icon: 'router',
|
||||
translationKey: 'HOME.PLAYLIST_TYPES.STALKER',
|
||||
checked: true,
|
||||
},
|
||||
];
|
||||
|
||||
private readonly selectedTypeFilters = this.store.selectSignal(
|
||||
selectActiveTypeFilters
|
||||
);
|
||||
|
||||
readonly SortBy = SortBy;
|
||||
readonly SortOrder = SortOrder;
|
||||
private readonly currentSortOptions = toSignal(
|
||||
this.sortService.getSortOptions(),
|
||||
{
|
||||
requireSync: true,
|
||||
}
|
||||
);
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
if (this.selectedTypeFilters) {
|
||||
this.playlistTypes = this.playlistTypes.map((type) => {
|
||||
type.checked = this.selectedTypeFilters().includes(type.id);
|
||||
return type;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
togglePlaylistType(type: {
|
||||
id: string;
|
||||
icon: string;
|
||||
translationKey: string;
|
||||
checked: boolean;
|
||||
}) {
|
||||
const currentlySelectedCount = this.playlistTypes.filter(
|
||||
(f) => f.checked
|
||||
).length;
|
||||
|
||||
// Prevent deselecting if it's the last selected option
|
||||
if (type.checked && currentlySelectedCount === 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
type.checked = !type.checked;
|
||||
this.store.dispatch(
|
||||
FilterActions.setSelectedFilters({
|
||||
selectedFilters: this.playlistTypes
|
||||
.filter((f) => f.checked)
|
||||
.map((f) => f.id),
|
||||
})
|
||||
);
|
||||
this.filterChanged.emit();
|
||||
}
|
||||
|
||||
setSortOptions(by: SortBy, order: SortOrder): void {
|
||||
this.sortService.setSortOptions({ by, order });
|
||||
}
|
||||
|
||||
isSortActive(by: SortBy, order: SortOrder): boolean {
|
||||
const options = this.currentSortOptions();
|
||||
return options?.by === by && options?.order === order;
|
||||
}
|
||||
|
||||
get activeFiltersCount(): number {
|
||||
return this.playlistTypes.filter((type) => !type.checked).length;
|
||||
}
|
||||
|
||||
closeMenu(): void {
|
||||
this.menuTrigger()?.closeMenu();
|
||||
}
|
||||
}
|
||||
@@ -34,23 +34,23 @@
|
||||
<app-add-playlist-menu
|
||||
(playlistTypeSelected)="openAddPlaylistDialog($event)"
|
||||
/>
|
||||
<!-- Sort button -->
|
||||
<!-- Combined Filter & Sort button -->
|
||||
<button
|
||||
mat-icon-button
|
||||
[matMenuTriggerFor]="sortMenu"
|
||||
[matTooltip]="'HOME.SORT_PLAYLISTS' | translate"
|
||||
data-test-id="sort-playlists"
|
||||
#filterMenuTrigger="matMenuTrigger"
|
||||
[matMenuTriggerFor]="filterSortMenuComponent().menu()"
|
||||
[matTooltip]="'HOME.FILTER_AND_SORT' | translate"
|
||||
[matBadge]="filterSortMenuComponent().activeFiltersCount"
|
||||
[matBadgeHidden]="
|
||||
filterSortMenuComponent().activeFiltersCount === 0
|
||||
"
|
||||
matBadgeSize="small"
|
||||
matBadgeColor="accent"
|
||||
data-test-id="filter-sort-playlists"
|
||||
>
|
||||
<mat-icon>sort</mat-icon>
|
||||
</button>
|
||||
<button
|
||||
mat-icon-button
|
||||
[matMenuTriggerFor]="filterPlaylistMenu"
|
||||
[matTooltip]="'HOME.FILTER_BY_TYPE' | translate"
|
||||
data-test-id="filter-playlist-by-type"
|
||||
>
|
||||
<mat-icon>filter_alt</mat-icon>
|
||||
<mat-icon>tune</mat-icon>
|
||||
</button>
|
||||
<app-filter-sort-menu [menuTrigger]="filterMenuTrigger" />
|
||||
@if (isDesktop) {
|
||||
<button
|
||||
mat-icon-button
|
||||
@@ -120,80 +120,6 @@
|
||||
</button>
|
||||
</mat-menu>
|
||||
}
|
||||
<!-- Sort menu -->
|
||||
<mat-menu #sortMenu="matMenu">
|
||||
<div
|
||||
class="dropdown-menu-header"
|
||||
(click)="$event.stopPropagation()"
|
||||
>
|
||||
{{ 'HOME.SORT_BY' | translate }}
|
||||
</div>
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="setSortOptions(SortBy.NAME, SortOrder.ASC)"
|
||||
[class.active-sort]="
|
||||
isSortActive(SortBy.NAME, SortOrder.ASC)
|
||||
"
|
||||
>
|
||||
<mat-icon>sort_by_alpha</mat-icon>
|
||||
<span>{{ 'HOME.SORT_OPTIONS.NAME_ASC' | translate }}</span>
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="setSortOptions(SortBy.NAME, SortOrder.DESC)"
|
||||
[class.active-sort]="
|
||||
isSortActive(SortBy.NAME, SortOrder.DESC)
|
||||
"
|
||||
>
|
||||
<mat-icon>sort_by_alpha</mat-icon>
|
||||
<span>{{ 'HOME.SORT_OPTIONS.NAME_DESC' | translate }}</span>
|
||||
</button>
|
||||
<mat-divider></mat-divider>
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="setSortOptions(SortBy.DATE_ADDED, SortOrder.DESC)"
|
||||
[class.active-sort]="
|
||||
isSortActive(SortBy.DATE_ADDED, SortOrder.DESC)
|
||||
"
|
||||
>
|
||||
<mat-icon>date_range</mat-icon>
|
||||
<span>{{ 'HOME.SORT_OPTIONS.NEWEST' | translate }}</span>
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="setSortOptions(SortBy.DATE_ADDED, SortOrder.ASC)"
|
||||
[class.active-sort]="
|
||||
isSortActive(SortBy.DATE_ADDED, SortOrder.ASC)
|
||||
"
|
||||
>
|
||||
<mat-icon>date_range</mat-icon>
|
||||
<span>{{ 'HOME.SORT_OPTIONS.OLDEST' | translate }}</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
<mat-menu #filterPlaylistMenu="matMenu">
|
||||
<section
|
||||
(click)="$event.stopPropagation()"
|
||||
class="filter-container"
|
||||
>
|
||||
<div
|
||||
class="dropdown-menu-header"
|
||||
(click)="$event.stopPropagation()"
|
||||
>
|
||||
{{ 'HOME.FILTER_BY_TYPE' | translate }}
|
||||
</div>
|
||||
@for (type of playlistTypes; track $index) {
|
||||
<p>
|
||||
<mat-checkbox
|
||||
(click)="$event.stopPropagation()"
|
||||
[checked]="true"
|
||||
(change)="onPlaylistFilterChange()"
|
||||
[(ngModel)]="type.checked"
|
||||
>{{ type.title }}</mat-checkbox
|
||||
>
|
||||
</p>
|
||||
}
|
||||
</section>
|
||||
</mat-menu>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -344,10 +344,6 @@
|
||||
app-region: no-drag; // Allow button interactions
|
||||
}
|
||||
|
||||
.filter-container {
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.add-playlist-btn {
|
||||
background-color: rgba(139, 92, 246, 0.12) !important;
|
||||
color: rgba(139, 92, 246, 1) !important;
|
||||
@@ -365,35 +361,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-menu-header {
|
||||
padding: 0 16px;
|
||||
margin: 8px 0;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.active-sort {
|
||||
background-color: rgba(139, 92, 246, 0.12) !important;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 3px;
|
||||
height: 60%;
|
||||
background: rgba(139, 92, 246, 0.8);
|
||||
border-radius: 0 2px 2px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.check-icon {
|
||||
margin-left: 8px;
|
||||
// Badge styling for filter count
|
||||
::ng-deep .mat-badge-accent .mat-badge-content {
|
||||
background: rgba(139, 92, 246, 1);
|
||||
color: white;
|
||||
}
|
||||
|
||||
// Refined Material button and icon overrides
|
||||
|
||||
@@ -2,30 +2,28 @@ import {
|
||||
Component,
|
||||
HostBinding,
|
||||
OnInit,
|
||||
effect,
|
||||
inject,
|
||||
input,
|
||||
output,
|
||||
viewChild,
|
||||
} from '@angular/core';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { MatBadgeModule } from '@angular/material/badge';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { DataService, SortBy, SortOrder, SortService } from 'services';
|
||||
import { DataService } from 'services';
|
||||
//import { shell } from 'electron';
|
||||
import { AddPlaylistMenuComponent, PlaylistType } from 'components';
|
||||
import { selectActiveTypeFilters, setSelectedFilters } from 'm3u-state';
|
||||
import { HomeComponent } from '../../../home/home.component';
|
||||
import { AboutDialogComponent } from '../about-dialog/about-dialog.component';
|
||||
import { AddPlaylistDialogComponent } from '../add-playlist/add-playlist-dialog.component';
|
||||
import { FilterSortMenuComponent } from '../filter-sort-menu/filter-sort-menu.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-header',
|
||||
@@ -33,9 +31,10 @@ import { AddPlaylistDialogComponent } from '../add-playlist/add-playlist-dialog.
|
||||
styleUrls: ['./header.component.scss'],
|
||||
imports: [
|
||||
AddPlaylistMenuComponent,
|
||||
FilterSortMenuComponent,
|
||||
FormsModule,
|
||||
MatBadgeModule,
|
||||
MatButtonModule,
|
||||
MatCheckboxModule,
|
||||
MatDividerModule,
|
||||
MatIconModule,
|
||||
MatMenuModule,
|
||||
@@ -53,12 +52,13 @@ export class HeaderComponent implements OnInit {
|
||||
private dialog = inject(MatDialog);
|
||||
private dataService = inject(DataService);
|
||||
private router = inject(Router);
|
||||
private store = inject(Store);
|
||||
private sortService = inject(SortService);
|
||||
|
||||
readonly addPlaylistMenuComponent = viewChild.required(
|
||||
AddPlaylistMenuComponent
|
||||
);
|
||||
readonly filterSortMenuComponent = viewChild.required(
|
||||
FilterSortMenuComponent
|
||||
);
|
||||
readonly isDesktop = !!window.electron;
|
||||
readonly title = input.required<string>();
|
||||
readonly subtitle = input.required<string>();
|
||||
@@ -66,47 +66,6 @@ export class HeaderComponent implements OnInit {
|
||||
|
||||
isHome = true;
|
||||
|
||||
playlistTypes = [
|
||||
{
|
||||
title: 'M3U (local, url, text)',
|
||||
id: 'm3u',
|
||||
checked: true,
|
||||
},
|
||||
{
|
||||
title: 'Xtream',
|
||||
id: 'xtream',
|
||||
checked: true,
|
||||
},
|
||||
{
|
||||
title: 'Stalker',
|
||||
id: 'stalker',
|
||||
checked: true,
|
||||
},
|
||||
];
|
||||
|
||||
private readonly selectedTypeFilters = this.store.selectSignal(
|
||||
selectActiveTypeFilters
|
||||
);
|
||||
|
||||
readonly SortBy = SortBy;
|
||||
readonly SortOrder = SortOrder;
|
||||
private currentSortOptions: { by: SortBy; order: SortOrder };
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
if (this.selectedTypeFilters) {
|
||||
this.playlistTypes = this.playlistTypes.map((type) => {
|
||||
type.checked = this.selectedTypeFilters().includes(type.id);
|
||||
return type;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
this.sortService.getSortOptions().subscribe((options) => {
|
||||
this.currentSortOptions = options;
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.isHome =
|
||||
this.activatedRoute.snapshot.component.name === HomeComponent.name;
|
||||
@@ -154,27 +113,6 @@ export class HeaderComponent implements OnInit {
|
||||
);
|
||||
}
|
||||
|
||||
onPlaylistFilterChange() {
|
||||
this.store.dispatch(
|
||||
setSelectedFilters({
|
||||
selectedFilters: this.playlistTypes
|
||||
.filter((f) => f.checked)
|
||||
.map((f) => f.id),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
setSortOptions(by: SortBy, order: SortOrder): void {
|
||||
this.sortService.setSortOptions({ by, order });
|
||||
}
|
||||
|
||||
isSortActive(by: SortBy, order: SortOrder): boolean {
|
||||
return (
|
||||
this.currentSortOptions?.by === by &&
|
||||
this.currentSortOptions?.order === order
|
||||
);
|
||||
}
|
||||
|
||||
onSearchQueryUpdate(query: string): void {
|
||||
this.searchQuery.emit(query);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component, effect, inject, OnDestroy } from '@angular/core';
|
||||
import { ActivatedRoute, Router, RouterOutlet } from '@angular/router';
|
||||
import { Store } from '@ngrx/store';
|
||||
import * as PlaylistActions from 'm3u-state';
|
||||
import { PlaylistActions } from 'm3u-state';
|
||||
import { selectPlaylistById } from 'm3u-state';
|
||||
import { NavigationComponent } from '../../xtream-tauri/navigation/navigation.component';
|
||||
import { NavigationItem } from '../../xtream-tauri/navigation/navigation.interface';
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Router, RouterLink } from '@angular/router';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||
import { DialogService, PlaylistInfoComponent } from 'components';
|
||||
import * as PlaylistActions from 'm3u-state';
|
||||
import { PlaylistActions } from 'm3u-state';
|
||||
import { selectCurrentPlaylist } from 'm3u-state';
|
||||
import { DatabaseService } from 'services';
|
||||
import { Playlist } from 'shared-interfaces';
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Component, effect, inject } from '@angular/core';
|
||||
import { ActivatedRoute, Router, RouterOutlet } from '@angular/router';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import * as PlaylistActions from 'm3u-state';
|
||||
import { PlaylistActions } from 'm3u-state';
|
||||
import { LoadingOverlayComponent } from '../loading-overlay/loading-overlay.component';
|
||||
import { NavigationComponent } from '../navigation/navigation.component';
|
||||
import { NavigationItem } from '../navigation/navigation.interface';
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Router, RouterLink } from '@angular/router';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||
import { DialogService, PlaylistInfoComponent } from 'components';
|
||||
import * as PlaylistActions from 'm3u-state';
|
||||
import { PlaylistActions } from 'm3u-state';
|
||||
import { selectCurrentPlaylist } from 'm3u-state';
|
||||
import { Playlist } from 'shared-interfaces';
|
||||
|
||||
|
||||
@@ -150,8 +150,9 @@
|
||||
"MAC_ADDRESS": "Mac-Adresse",
|
||||
"URL_VALIDATION_ERROR": "Sollte eine gültige URL mit Protokoll sein (z. B. http://example.com/c oder https://example.com/stalker_portal/c)"
|
||||
},
|
||||
"FILTER_BY_TYPE": "Nach Typ filtern",
|
||||
"FILTER_BY_TYPE": "Playlist-Typen",
|
||||
"FILTER_BY_NAME": "Nach Namen filtern",
|
||||
"FILTER_AND_SORT": "Filtern & Sortieren",
|
||||
"SORT_PLAYLISTS": "Playlisten sortieren",
|
||||
"SORT_BY": "Sortieren nach",
|
||||
"SORT_OPTIONS": {
|
||||
@@ -160,6 +161,11 @@
|
||||
"NEWEST": "Hinzugefügt (Neueste zuerst)",
|
||||
"OLDEST": "Hinzugefügt (Älteste zuerst)"
|
||||
},
|
||||
"PLAYLIST_TYPES": {
|
||||
"M3U": "M3U (lokal, URL, Text)",
|
||||
"XTREAM": "Xtream",
|
||||
"STALKER": "Stalker"
|
||||
},
|
||||
"IMPORTED_AS_TEXT": "Als Text importiert",
|
||||
"PARSING_ERROR": "Fehler: Parsen fehlgeschlagen, keine gültige Playlist:"
|
||||
},
|
||||
|
||||
@@ -112,15 +112,21 @@
|
||||
"ADD": "Add"
|
||||
},
|
||||
"FILTER_BY_NAME": "Filter by name",
|
||||
"FILTER_BY_TYPE": "Filter by type",
|
||||
"FILTER_BY_TYPE": "Playlist Types",
|
||||
"FILTER_AND_SORT": "Filter & Sort",
|
||||
"SORT_PLAYLISTS": "Sort playlists",
|
||||
"SORT_BY": "Sort by",
|
||||
"SORT_BY": "Sort By",
|
||||
"SORT_OPTIONS": {
|
||||
"NAME_ASC": "Name (A-Z)",
|
||||
"NAME_DESC": "Name (Z-A)",
|
||||
"NEWEST": "Date added (Newest first)",
|
||||
"OLDEST": "Date added (Oldest first)"
|
||||
},
|
||||
"PLAYLIST_TYPES": {
|
||||
"M3U": "M3U (local, url, text)",
|
||||
"XTREAM": "Xtream",
|
||||
"STALKER": "Stalker"
|
||||
},
|
||||
"IMPORTED_AS_TEXT": "Imported as text",
|
||||
"PARSING_ERROR": "Error: Parsing failed, not a valid playlist:"
|
||||
},
|
||||
|
||||
@@ -109,8 +109,9 @@
|
||||
"SERVER_URL": "URL сервера",
|
||||
"URL_VALIDATION_ERROR": "Должен быть действительный URL-адрес с протоколом (например, http://example.com/c или https://example.com/stalker_portal/c)."
|
||||
},
|
||||
"FILTER_BY_TYPE": "Фильтровать по типу",
|
||||
"FILTER_BY_TYPE": "Типы плейлистов",
|
||||
"FILTER_BY_NAME": "Фильтровать по имени",
|
||||
"FILTER_AND_SORT": "Фильтр и сортировка",
|
||||
"SORT_PLAYLISTS": "Сортировать плейлисты",
|
||||
"SORT_BY": "Сортировать по",
|
||||
"SORT_OPTIONS": {
|
||||
@@ -119,6 +120,11 @@
|
||||
"NEWEST": "Дате добавления (сначала новые)",
|
||||
"OLDEST": "Дате добавления (сначала старые)"
|
||||
},
|
||||
"PLAYLIST_TYPES": {
|
||||
"M3U": "M3U (локально, URL, текст)",
|
||||
"XTREAM": "Xtream",
|
||||
"STALKER": "Stalker"
|
||||
},
|
||||
"IMPORTED_AS_TEXT": "Импортировано как текст",
|
||||
"PARSING_ERROR": "Ошибка парсинга, недействительный плейлист:"
|
||||
},
|
||||
|
||||
@@ -1,140 +1,70 @@
|
||||
import { createAction, props } from '@ngrx/store';
|
||||
import { createActionGroup, emptyProps, props } from '@ngrx/store';
|
||||
import { Channel, EpgProgram, Playlist, PlaylistMeta } from 'shared-interfaces';
|
||||
|
||||
const STORE_KEY = '[GLOBAL STORE]';
|
||||
const PLAYLISTS_STORE_KEY = '[PLAYLISTS STORE]';
|
||||
export const PlaylistActions = createActionGroup({
|
||||
source: 'Playlists',
|
||||
events: {
|
||||
'Load Playlists': emptyProps(),
|
||||
'Load Playlists Success': props<{ playlists: PlaylistMeta[] }>(),
|
||||
'Add Playlist': props<{ playlist: Playlist }>(),
|
||||
'Add Many Playlists': props<{ playlists: Playlist[] }>(),
|
||||
'Remove Playlist': props<{ playlistId: string }>(),
|
||||
'Update Playlist Meta': props<{ playlist: PlaylistMeta }>(),
|
||||
'Update Playlist': props<{ playlist: Playlist; playlistId: string }>(),
|
||||
'Update Many Playlists': props<{ playlists: Playlist[] }>(),
|
||||
'Parse Playlist': props<{
|
||||
uploadType: 'FILE' | 'URL' | 'TEXT';
|
||||
playlist: string;
|
||||
title: string;
|
||||
path?: string;
|
||||
}>(),
|
||||
'Set Active Playlist': props<{ playlistId: string }>(),
|
||||
'Update Playlist Positions': props<{
|
||||
positionUpdates: { id: string; changes: { position: number } }[];
|
||||
}>(),
|
||||
'Remove All Playlists': emptyProps(),
|
||||
'Set Current Playlist Id': props<{ playlistId: string | undefined }>(),
|
||||
'Handle Adding Playlist By Url': props<{
|
||||
isTemporary: boolean;
|
||||
playlist: Playlist;
|
||||
}>(),
|
||||
},
|
||||
});
|
||||
|
||||
export const loadPlaylists = createAction(
|
||||
`${PLAYLISTS_STORE_KEY} Load playlists from db`
|
||||
);
|
||||
export const ChannelActions = createActionGroup({
|
||||
source: 'Channels',
|
||||
events: {
|
||||
'Set Channels': props<{ channels: Channel[] }>(),
|
||||
'Set Active Channel': props<{ channel: Channel }>(),
|
||||
'Set Active Channel Success': props<{ channel: Channel }>(),
|
||||
'Reset Active Channel': emptyProps(),
|
||||
'Set Adjacent Channel As Active': props<{
|
||||
direction: 'next' | 'previous';
|
||||
}>(),
|
||||
},
|
||||
});
|
||||
|
||||
export const loadPlaylistsSuccess = createAction(
|
||||
`${PLAYLISTS_STORE_KEY} Successfully loaded playlists from db`,
|
||||
props<{ playlists: PlaylistMeta[] }>()
|
||||
);
|
||||
export const EpgActions = createActionGroup({
|
||||
source: 'EPG',
|
||||
events: {
|
||||
'Set Active Epg Program': props<{ program: EpgProgram }>(),
|
||||
'Set Current Epg Program': props<{ program: EpgProgram }>(),
|
||||
'Reset Active Epg Program': emptyProps(),
|
||||
'Set Epg Available Flag': props<{ value: boolean }>(),
|
||||
},
|
||||
});
|
||||
|
||||
export const addPlaylist = createAction(
|
||||
`${PLAYLISTS_STORE_KEY} Add new playlist`,
|
||||
props<{ playlist: Playlist }>()
|
||||
);
|
||||
export const FavoritesActions = createActionGroup({
|
||||
source: 'Favorites',
|
||||
events: {
|
||||
'Update Favorites': props<{ channel: Channel }>(),
|
||||
'Set Favorites': props<{ channelIds: string[] }>(),
|
||||
},
|
||||
});
|
||||
|
||||
export const addManyPlaylists = createAction(
|
||||
`${PLAYLISTS_STORE_KEY} Add many playlists`,
|
||||
props<{ playlists: Playlist[] }>()
|
||||
);
|
||||
|
||||
export const removePlaylist = createAction(
|
||||
`${PLAYLISTS_STORE_KEY} Remove playlist by id`,
|
||||
props<{ playlistId: string }>()
|
||||
);
|
||||
|
||||
export const updatePlaylistMeta = createAction(
|
||||
`${PLAYLISTS_STORE_KEY} update playlist meta`,
|
||||
props<{ playlist: PlaylistMeta }>()
|
||||
);
|
||||
|
||||
export const updatePlaylist = createAction(
|
||||
`${PLAYLISTS_STORE_KEY} update playlist`,
|
||||
props<{ playlist: Playlist; playlistId: string }>()
|
||||
);
|
||||
|
||||
export const updateManyPlaylists = createAction(
|
||||
`${PLAYLISTS_STORE_KEY} Update many playlists (auto-update mechanism)`,
|
||||
props<{ playlists: Playlist[] }>()
|
||||
);
|
||||
|
||||
export const parsePlaylist = createAction(
|
||||
`${PLAYLISTS_STORE_KEY} parse playlist`,
|
||||
props<{
|
||||
uploadType: 'FILE' | 'URL' | 'TEXT';
|
||||
playlist: string;
|
||||
title: string;
|
||||
path?: string;
|
||||
}>()
|
||||
);
|
||||
|
||||
export const setActivePlaylist = createAction(
|
||||
`${PLAYLISTS_STORE_KEY} set active playlist`,
|
||||
props<{ playlistId: string }>()
|
||||
);
|
||||
|
||||
export const updateFavorites = createAction(
|
||||
`${STORE_KEY} Add/remove provided channel to the favorites`,
|
||||
props<{ channel: Channel }>()
|
||||
);
|
||||
|
||||
export const setFavorites = createAction(
|
||||
`${STORE_KEY} Set favorites`,
|
||||
props<{ channelIds: string[] }>()
|
||||
);
|
||||
|
||||
export const setActiveChannel = createAction(
|
||||
`${STORE_KEY} Set active channel`,
|
||||
props<{ channel: Channel }>()
|
||||
);
|
||||
|
||||
export const resetActiveChannel = createAction(
|
||||
`${STORE_KEY} Reset active channel`
|
||||
);
|
||||
|
||||
export const setActiveChannelSuccess = createAction(
|
||||
`${STORE_KEY} Set active channel success`,
|
||||
props<{ channel: Channel }>()
|
||||
);
|
||||
|
||||
export const setActiveEpgProgram = createAction(
|
||||
`${STORE_KEY} Sets the given timestamp for the epg program`,
|
||||
props<{ program: EpgProgram }>()
|
||||
);
|
||||
|
||||
export const setCurrentEpgProgram = createAction(
|
||||
`${STORE_KEY} Updates the active epg program for the active channel`,
|
||||
props<{ program: EpgProgram }>()
|
||||
);
|
||||
|
||||
export const resetActiveEpgProgram = createAction(
|
||||
`${STORE_KEY} Reset active epg program`
|
||||
);
|
||||
|
||||
export const setEpgAvailableFlag = createAction(
|
||||
`${STORE_KEY} Reset active epg program`,
|
||||
props<{ value: boolean }>()
|
||||
);
|
||||
|
||||
export const setChannels = createAction(
|
||||
`${STORE_KEY} Set channels`,
|
||||
props<{ channels: Channel[] }>()
|
||||
);
|
||||
|
||||
export const updatePlaylistPositions = createAction(
|
||||
`${STORE_KEY} Update playlist positions`,
|
||||
props<{
|
||||
positionUpdates: { id: string; changes: { position: number } }[];
|
||||
}>()
|
||||
);
|
||||
|
||||
export const removeAllPlaylists = createAction(
|
||||
`${STORE_KEY} Remove all playlists`
|
||||
);
|
||||
|
||||
export const setAdjacentChannelAsActive = createAction(
|
||||
`${STORE_KEY} Set adjacent channel as active`,
|
||||
props<{
|
||||
direction: 'next' | 'previous';
|
||||
}>()
|
||||
);
|
||||
|
||||
export const setSelectedFilters = createAction(
|
||||
`${STORE_KEY} Set selected filters`,
|
||||
props<{ selectedFilters: string[] }>()
|
||||
);
|
||||
|
||||
export const setCurrentPlaylistId = createAction(
|
||||
`${STORE_KEY} Set current playlist id`,
|
||||
props<{ playlistId: string | undefined }>()
|
||||
);
|
||||
|
||||
export const handleAddingPlaylistByUrl = createAction(
|
||||
`${PLAYLISTS_STORE_KEY} Handle adding playlist by URL`,
|
||||
props<{ isTemporary: boolean; playlist: Playlist }>()
|
||||
);
|
||||
export const FilterActions = createActionGroup({
|
||||
source: 'Filters',
|
||||
events: {
|
||||
'Set Selected Filters': props<{ selectedFilters: string[] }>(),
|
||||
},
|
||||
});
|
||||
@@ -24,7 +24,12 @@ import {
|
||||
STORE_KEY,
|
||||
VideoPlayer,
|
||||
} from 'shared-interfaces';
|
||||
import * as PlaylistActions from './actions';
|
||||
import {
|
||||
PlaylistActions,
|
||||
ChannelActions,
|
||||
EpgActions,
|
||||
FavoritesActions,
|
||||
} from './actions';
|
||||
import {
|
||||
selectActive,
|
||||
selectActivePlaylistId,
|
||||
@@ -47,7 +52,7 @@ export class PlaylistEffects {
|
||||
updateFavorites$ = createEffect(
|
||||
() => {
|
||||
return this.actions$.pipe(
|
||||
ofType(PlaylistActions.updateFavorites),
|
||||
ofType(FavoritesActions.updateFavorites),
|
||||
combineLatestWith(
|
||||
this.store.select(selectFavorites),
|
||||
this.store.select(selectActivePlaylistId)
|
||||
@@ -70,7 +75,7 @@ export class PlaylistEffects {
|
||||
setFavorites$ = createEffect(
|
||||
() => {
|
||||
return this.actions$.pipe(
|
||||
ofType(PlaylistActions.setFavorites),
|
||||
ofType(FavoritesActions.setFavorites),
|
||||
combineLatestWith(this.store.select(selectActivePlaylistId)),
|
||||
filter(
|
||||
([, playlistId]) =>
|
||||
@@ -92,7 +97,7 @@ export class PlaylistEffects {
|
||||
setActiveEpgProgram$ = createEffect(
|
||||
() => {
|
||||
return this.actions$.pipe(
|
||||
ofType(PlaylistActions.setActiveEpgProgram),
|
||||
ofType(EpgActions.setActiveEpgProgram),
|
||||
combineLatestWith(this.store.select(selectActive)),
|
||||
map(([, activeChannel]) => {
|
||||
firstValueFrom(this.storage.get(STORE_KEY.Settings)).then(
|
||||
@@ -135,7 +140,7 @@ export class PlaylistEffects {
|
||||
|
||||
setActiveChannel$ = createEffect(() => {
|
||||
return this.actions$.pipe(
|
||||
ofType(PlaylistActions.setActiveChannel),
|
||||
ofType(ChannelActions.setActiveChannel),
|
||||
// Skip the effect entirely when channel is falsy
|
||||
filter((action) => !!action.channel),
|
||||
map((action) => {
|
||||
@@ -186,7 +191,7 @@ export class PlaylistEffects {
|
||||
}
|
||||
);
|
||||
|
||||
return PlaylistActions.setActiveChannelSuccess({
|
||||
return ChannelActions.setActiveChannelSuccess({
|
||||
channel: action.channel,
|
||||
});
|
||||
})
|
||||
@@ -400,7 +405,7 @@ export class PlaylistEffects {
|
||||
|
||||
setAdjacentChannelAsActive$ = createEffect(() => {
|
||||
return this.actions$.pipe(
|
||||
ofType(PlaylistActions.setAdjacentChannelAsActive),
|
||||
ofType(ChannelActions.setAdjacentChannelAsActive),
|
||||
withLatestFrom(
|
||||
this.store.select(selectChannels),
|
||||
this.store.select(selectActive)
|
||||
@@ -419,7 +424,7 @@ export class PlaylistEffects {
|
||||
adjacentChannel = activeChannel;
|
||||
adjacentChannel = channels[index - 1];
|
||||
}
|
||||
return PlaylistActions.setActiveChannelSuccess({
|
||||
return ChannelActions.setActiveChannelSuccess({
|
||||
channel: adjacentChannel!,
|
||||
});
|
||||
})
|
||||
|
||||
@@ -1,292 +1,2 @@
|
||||
import { createReducer, on } from '@ngrx/store';
|
||||
import moment from 'moment';
|
||||
import { Channel } from 'shared-interfaces';
|
||||
import * as PlaylistActions from './actions';
|
||||
import { playlistsAdapter } from './playlists.state';
|
||||
import { PlaylistState, initialState } from './state';
|
||||
|
||||
export const playlistReducer = createReducer(
|
||||
initialState,
|
||||
on(PlaylistActions.setActiveEpgProgram, (state, action): PlaylistState => {
|
||||
const { program } = action;
|
||||
const from = moment(program.start, 'YYYYMMDDHHmm ZZ').unix();
|
||||
const now = moment(Date.now()).unix();
|
||||
const epgParams = `?utc=${from}&lutc=${now}`;
|
||||
return {
|
||||
...state,
|
||||
active: { ...state.active, epgParams } as Channel,
|
||||
};
|
||||
}),
|
||||
on(
|
||||
PlaylistActions.resetActiveEpgProgram,
|
||||
(state): PlaylistState => ({
|
||||
...state,
|
||||
active: { ...state.active, epgParams: '' } as Channel,
|
||||
})
|
||||
),
|
||||
on(
|
||||
PlaylistActions.setActiveChannelSuccess,
|
||||
(state, action): PlaylistState => {
|
||||
const { channel } = action;
|
||||
return {
|
||||
...state,
|
||||
active: { ...channel, epgParams: '' } as Channel,
|
||||
};
|
||||
}
|
||||
),
|
||||
on(PlaylistActions.resetActiveChannel, (state): PlaylistState => {
|
||||
return {
|
||||
...state,
|
||||
active: undefined,
|
||||
};
|
||||
}),
|
||||
on(
|
||||
PlaylistActions.setCurrentEpgProgram,
|
||||
(state, action): PlaylistState => ({
|
||||
...state,
|
||||
currentEpgProgram: action.program,
|
||||
})
|
||||
),
|
||||
on(
|
||||
PlaylistActions.setEpgAvailableFlag,
|
||||
(state, action): PlaylistState => ({
|
||||
...state,
|
||||
epgAvailable: action.value,
|
||||
})
|
||||
),
|
||||
on(PlaylistActions.updateFavorites, (state, action): PlaylistState => {
|
||||
let favorites;
|
||||
const { channel } = action;
|
||||
const playlistFavorites =
|
||||
state.playlists.entities[state.playlists.selectedId]?.favorites;
|
||||
if (playlistFavorites?.includes(channel.url)) {
|
||||
favorites = [
|
||||
...(playlistFavorites ?? []).filter(
|
||||
(url: string) => url !== channel.url
|
||||
),
|
||||
];
|
||||
} else {
|
||||
favorites = [...(playlistFavorites ?? []), channel.url];
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
playlists: {
|
||||
...state.playlists,
|
||||
entities: {
|
||||
...state.playlists.entities,
|
||||
[state.playlists.selectedId]: {
|
||||
...state.playlists.entities[state.playlists.selectedId],
|
||||
favorites,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}),
|
||||
on(PlaylistActions.setFavorites, (state, action): PlaylistState => {
|
||||
const { channelIds } = action;
|
||||
return {
|
||||
...state,
|
||||
playlists: {
|
||||
...state.playlists,
|
||||
entities: {
|
||||
...state.playlists.entities,
|
||||
[state.playlists.selectedId]: {
|
||||
...state.playlists.entities[state.playlists.selectedId],
|
||||
favorites: channelIds,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}),
|
||||
on(PlaylistActions.loadPlaylistsSuccess, (state, action): PlaylistState => {
|
||||
return {
|
||||
...state,
|
||||
playlists: playlistsAdapter.addMany(action.playlists, {
|
||||
...state.playlists,
|
||||
allPlaylistsLoaded: true,
|
||||
}),
|
||||
};
|
||||
}),
|
||||
on(PlaylistActions.removePlaylist, (state, action): PlaylistState => {
|
||||
return {
|
||||
...state,
|
||||
playlists: playlistsAdapter.removeOne(
|
||||
action.playlistId,
|
||||
state.playlists
|
||||
),
|
||||
};
|
||||
}),
|
||||
on(PlaylistActions.updatePlaylist, (state, action): PlaylistState => {
|
||||
return {
|
||||
...state,
|
||||
playlists: playlistsAdapter.updateOne(
|
||||
{
|
||||
id: action.playlistId,
|
||||
changes: {
|
||||
...action.playlist,
|
||||
_id: action.playlistId,
|
||||
updateDate: Date.now(),
|
||||
count: action.playlist.playlist.items.length,
|
||||
userAgent: action.playlist.userAgent,
|
||||
favorites:
|
||||
state.playlists.entities[action.playlistId]
|
||||
?.favorites ?? [],
|
||||
},
|
||||
},
|
||||
state.playlists
|
||||
),
|
||||
};
|
||||
}),
|
||||
on(PlaylistActions.addPlaylist, (state, action): PlaylistState => {
|
||||
return {
|
||||
...state,
|
||||
playlists: playlistsAdapter.addOne(
|
||||
action.playlist,
|
||||
state.playlists
|
||||
),
|
||||
};
|
||||
}),
|
||||
on(PlaylistActions.addManyPlaylists, (state, action): PlaylistState => {
|
||||
return {
|
||||
...state,
|
||||
playlists: playlistsAdapter.addMany(
|
||||
action.playlists,
|
||||
state.playlists
|
||||
),
|
||||
};
|
||||
}),
|
||||
on(PlaylistActions.setActivePlaylist, (state, action): PlaylistState => {
|
||||
return {
|
||||
...state,
|
||||
playlists: {
|
||||
...state.playlists,
|
||||
selectedId: action.playlistId,
|
||||
},
|
||||
};
|
||||
}),
|
||||
on(
|
||||
PlaylistActions.updatePlaylistPositions,
|
||||
(state, action): PlaylistState => {
|
||||
return {
|
||||
...state,
|
||||
playlists: playlistsAdapter.updateMany(
|
||||
action.positionUpdates,
|
||||
state.playlists
|
||||
),
|
||||
};
|
||||
}
|
||||
),
|
||||
on(PlaylistActions.updatePlaylistMeta, (state, action): PlaylistState => {
|
||||
return {
|
||||
...state,
|
||||
playlists: playlistsAdapter.updateOne(
|
||||
{
|
||||
id: action.playlist._id,
|
||||
changes: {
|
||||
title: action.playlist.title,
|
||||
autoRefresh: action.playlist.autoRefresh || false,
|
||||
userAgent: action.playlist.userAgent,
|
||||
...(action.playlist.serverUrl !== null
|
||||
? { serverUrl: action.playlist.serverUrl }
|
||||
: {}),
|
||||
...(action.playlist.username !== null
|
||||
? { username: action.playlist.username }
|
||||
: {}),
|
||||
...(action.playlist.password !== null
|
||||
? { password: action.playlist.password }
|
||||
: {}),
|
||||
...(action.playlist.macAddress !== null
|
||||
? { macAddress: action.playlist.macAddress }
|
||||
: {}),
|
||||
...(action.playlist.portalUrl !== null
|
||||
? { portalUrl: action.playlist.portalUrl }
|
||||
: {}),
|
||||
},
|
||||
},
|
||||
state.playlists
|
||||
),
|
||||
};
|
||||
}),
|
||||
on(PlaylistActions.setChannels, (state, action): PlaylistState => {
|
||||
return {
|
||||
...state,
|
||||
channels: action.channels,
|
||||
};
|
||||
}),
|
||||
on(PlaylistActions.updateManyPlaylists, (state, action): PlaylistState => {
|
||||
return {
|
||||
...state,
|
||||
playlists: playlistsAdapter.updateMany(
|
||||
action.playlists.map((updatedPlaylist) => ({
|
||||
id: updatedPlaylist._id,
|
||||
changes: {
|
||||
...updatedPlaylist,
|
||||
updateDate: Date.now(),
|
||||
},
|
||||
})),
|
||||
state.playlists
|
||||
),
|
||||
};
|
||||
}),
|
||||
on(PlaylistActions.removeAllPlaylists, (state): PlaylistState => {
|
||||
return {
|
||||
...state,
|
||||
playlists: playlistsAdapter.removeAll(state.playlists),
|
||||
};
|
||||
}),
|
||||
on(
|
||||
PlaylistActions.setSelectedFilters,
|
||||
(state, { selectedFilters }): PlaylistState => {
|
||||
return {
|
||||
...state,
|
||||
playlists: {
|
||||
...state.playlists,
|
||||
selectedFilters,
|
||||
},
|
||||
};
|
||||
}
|
||||
),
|
||||
on(
|
||||
PlaylistActions.setCurrentPlaylistId,
|
||||
(state, { playlistId }): PlaylistState => {
|
||||
return {
|
||||
...state,
|
||||
currentPlaylistId: playlistId,
|
||||
};
|
||||
}
|
||||
),
|
||||
on(
|
||||
PlaylistActions.handleAddingPlaylistByUrl,
|
||||
(state, action): PlaylistState => {
|
||||
if (action.isTemporary) {
|
||||
return {
|
||||
...state,
|
||||
channels: action.playlist.playlist.items as Channel[],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...state,
|
||||
playlists: playlistsAdapter.addOne(
|
||||
action.playlist,
|
||||
state.playlists
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
export const selectPlaylists = (state: PlaylistState) => state.playlists;
|
||||
export const selectPlaylistId = (state: PlaylistState) =>
|
||||
state.playlists?.selectedId;
|
||||
|
||||
export const selectIsEpgAvailableReducer = (state: PlaylistState) =>
|
||||
state.epgAvailable;
|
||||
export const selectActiveReducer = (state: PlaylistState) => state.active;
|
||||
export const selectCurrentEpgProgramReducer = (state: PlaylistState) =>
|
||||
state.currentEpgProgram;
|
||||
export const selectChannelsReducer = (state: PlaylistState) => state.channels;
|
||||
export const selectPlaylistsLoadingFlagReducer = (state: PlaylistState) =>
|
||||
state.playlists?.allPlaylistsLoaded;
|
||||
export const selectCurrentPlaylistIdReducer = (state: PlaylistState) =>
|
||||
state.currentPlaylistId;
|
||||
// Re-export from the new modular reducers structure
|
||||
export * from './reducers/index';
|
||||
@@ -0,0 +1,29 @@
|
||||
import { on } from '@ngrx/store';
|
||||
import { Channel } from 'shared-interfaces';
|
||||
import { ChannelActions } from '../actions';
|
||||
import { PlaylistState } from '../state';
|
||||
|
||||
export const channelReducers = [
|
||||
on(
|
||||
ChannelActions.setActiveChannelSuccess,
|
||||
(state, action): PlaylistState => {
|
||||
const { channel } = action;
|
||||
return {
|
||||
...state,
|
||||
active: { ...channel, epgParams: '' } as Channel,
|
||||
};
|
||||
}
|
||||
),
|
||||
on(ChannelActions.resetActiveChannel, (state): PlaylistState => {
|
||||
return {
|
||||
...state,
|
||||
active: undefined,
|
||||
};
|
||||
}),
|
||||
on(ChannelActions.setChannels, (state, action): PlaylistState => {
|
||||
return {
|
||||
...state,
|
||||
channels: action.channels,
|
||||
};
|
||||
}),
|
||||
];
|
||||
@@ -0,0 +1,39 @@
|
||||
import { on } from '@ngrx/store';
|
||||
import moment from 'moment';
|
||||
import { Channel } from 'shared-interfaces';
|
||||
import { EpgActions } from '../actions';
|
||||
import { PlaylistState } from '../state';
|
||||
|
||||
export const epgReducers = [
|
||||
on(EpgActions.setActiveEpgProgram, (state, action): PlaylistState => {
|
||||
const { program } = action;
|
||||
const from = moment(program.start, 'YYYYMMDDHHmm ZZ').unix();
|
||||
const now = moment(Date.now()).unix();
|
||||
const epgParams = `?utc=${from}&lutc=${now}`;
|
||||
return {
|
||||
...state,
|
||||
active: { ...state.active, epgParams } as Channel,
|
||||
};
|
||||
}),
|
||||
on(
|
||||
EpgActions.resetActiveEpgProgram,
|
||||
(state): PlaylistState => ({
|
||||
...state,
|
||||
active: { ...state.active, epgParams: '' } as Channel,
|
||||
})
|
||||
),
|
||||
on(
|
||||
EpgActions.setCurrentEpgProgram,
|
||||
(state, action): PlaylistState => ({
|
||||
...state,
|
||||
currentEpgProgram: action.program,
|
||||
})
|
||||
),
|
||||
on(
|
||||
EpgActions.setEpgAvailableFlag,
|
||||
(state, action): PlaylistState => ({
|
||||
...state,
|
||||
epgAvailable: action.value,
|
||||
})
|
||||
),
|
||||
];
|
||||
@@ -0,0 +1,50 @@
|
||||
import { on } from '@ngrx/store';
|
||||
import { FavoritesActions } from '../actions';
|
||||
import { PlaylistState } from '../state';
|
||||
|
||||
export const favoritesReducers = [
|
||||
on(FavoritesActions.updateFavorites, (state, action): PlaylistState => {
|
||||
let favorites;
|
||||
const { channel } = action;
|
||||
const playlistFavorites =
|
||||
state.playlists.entities[state.playlists.selectedId]?.favorites;
|
||||
if (playlistFavorites?.includes(channel.url)) {
|
||||
favorites = [
|
||||
...(playlistFavorites ?? []).filter(
|
||||
(url: string) => url !== channel.url
|
||||
),
|
||||
];
|
||||
} else {
|
||||
favorites = [...(playlistFavorites ?? []), channel.url];
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
playlists: {
|
||||
...state.playlists,
|
||||
entities: {
|
||||
...state.playlists.entities,
|
||||
[state.playlists.selectedId]: {
|
||||
...state.playlists.entities[state.playlists.selectedId],
|
||||
favorites,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}),
|
||||
on(FavoritesActions.setFavorites, (state, action): PlaylistState => {
|
||||
const { channelIds } = action;
|
||||
return {
|
||||
...state,
|
||||
playlists: {
|
||||
...state.playlists,
|
||||
entities: {
|
||||
...state.playlists.entities,
|
||||
[state.playlists.selectedId]: {
|
||||
...state.playlists.entities[state.playlists.selectedId],
|
||||
favorites: channelIds,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
}),
|
||||
];
|
||||
@@ -0,0 +1,18 @@
|
||||
import { on } from '@ngrx/store';
|
||||
import { FilterActions } from '../actions';
|
||||
import { PlaylistState } from '../state';
|
||||
|
||||
export const filterReducers = [
|
||||
on(
|
||||
FilterActions.setSelectedFilters,
|
||||
(state, { selectedFilters }): PlaylistState => {
|
||||
return {
|
||||
...state,
|
||||
playlists: {
|
||||
...state.playlists,
|
||||
selectedFilters,
|
||||
},
|
||||
};
|
||||
}
|
||||
),
|
||||
];
|
||||
@@ -0,0 +1,32 @@
|
||||
import { createReducer } from '@ngrx/store';
|
||||
import { initialState, PlaylistState } from '../state';
|
||||
import { channelReducers } from './channel.reducers';
|
||||
import { epgReducers } from './epg.reducers';
|
||||
import { favoritesReducers } from './favorites.reducers';
|
||||
import { filterReducers } from './filter.reducers';
|
||||
import { playlistReducers } from './playlist.reducers';
|
||||
|
||||
export const playlistReducer = createReducer(
|
||||
initialState,
|
||||
...epgReducers,
|
||||
...channelReducers,
|
||||
...playlistReducers,
|
||||
...favoritesReducers,
|
||||
...filterReducers
|
||||
);
|
||||
|
||||
// Selectors
|
||||
export const selectPlaylists = (state: PlaylistState) => state.playlists;
|
||||
export const selectPlaylistId = (state: PlaylistState) =>
|
||||
state.playlists?.selectedId;
|
||||
|
||||
export const selectIsEpgAvailableReducer = (state: PlaylistState) =>
|
||||
state.epgAvailable;
|
||||
export const selectActiveReducer = (state: PlaylistState) => state.active;
|
||||
export const selectCurrentEpgProgramReducer = (state: PlaylistState) =>
|
||||
state.currentEpgProgram;
|
||||
export const selectChannelsReducer = (state: PlaylistState) => state.channels;
|
||||
export const selectPlaylistsLoadingFlagReducer = (state: PlaylistState) =>
|
||||
state.playlists?.allPlaylistsLoaded;
|
||||
export const selectCurrentPlaylistIdReducer = (state: PlaylistState) =>
|
||||
state.currentPlaylistId;
|
||||
@@ -0,0 +1,166 @@
|
||||
import { on } from '@ngrx/store';
|
||||
import { Channel } from 'shared-interfaces';
|
||||
import { PlaylistActions } from '../actions';
|
||||
import { playlistsAdapter } from '../playlists.state';
|
||||
import { PlaylistState } from '../state';
|
||||
|
||||
export const playlistReducers = [
|
||||
on(PlaylistActions.loadPlaylistsSuccess, (state, action): PlaylistState => {
|
||||
return {
|
||||
...state,
|
||||
playlists: playlistsAdapter.addMany(action.playlists, {
|
||||
...state.playlists,
|
||||
allPlaylistsLoaded: true,
|
||||
}),
|
||||
};
|
||||
}),
|
||||
on(PlaylistActions.removePlaylist, (state, action): PlaylistState => {
|
||||
return {
|
||||
...state,
|
||||
playlists: playlistsAdapter.removeOne(
|
||||
action.playlistId,
|
||||
state.playlists
|
||||
),
|
||||
};
|
||||
}),
|
||||
on(PlaylistActions.updatePlaylist, (state, action): PlaylistState => {
|
||||
return {
|
||||
...state,
|
||||
playlists: playlistsAdapter.updateOne(
|
||||
{
|
||||
id: action.playlistId,
|
||||
changes: {
|
||||
...action.playlist,
|
||||
_id: action.playlistId,
|
||||
updateDate: Date.now(),
|
||||
count: action.playlist.playlist.items.length,
|
||||
userAgent: action.playlist.userAgent,
|
||||
favorites:
|
||||
state.playlists.entities[action.playlistId]
|
||||
?.favorites ?? [],
|
||||
},
|
||||
},
|
||||
state.playlists
|
||||
),
|
||||
};
|
||||
}),
|
||||
on(PlaylistActions.addPlaylist, (state, action): PlaylistState => {
|
||||
return {
|
||||
...state,
|
||||
playlists: playlistsAdapter.addOne(
|
||||
action.playlist,
|
||||
state.playlists
|
||||
),
|
||||
};
|
||||
}),
|
||||
on(PlaylistActions.addManyPlaylists, (state, action): PlaylistState => {
|
||||
return {
|
||||
...state,
|
||||
playlists: playlistsAdapter.addMany(
|
||||
action.playlists,
|
||||
state.playlists
|
||||
),
|
||||
};
|
||||
}),
|
||||
on(PlaylistActions.setActivePlaylist, (state, action): PlaylistState => {
|
||||
return {
|
||||
...state,
|
||||
playlists: {
|
||||
...state.playlists,
|
||||
selectedId: action.playlistId,
|
||||
},
|
||||
};
|
||||
}),
|
||||
on(
|
||||
PlaylistActions.updatePlaylistPositions,
|
||||
(state, action): PlaylistState => {
|
||||
return {
|
||||
...state,
|
||||
playlists: playlistsAdapter.updateMany(
|
||||
action.positionUpdates,
|
||||
state.playlists
|
||||
),
|
||||
};
|
||||
}
|
||||
),
|
||||
on(PlaylistActions.updatePlaylistMeta, (state, action): PlaylistState => {
|
||||
return {
|
||||
...state,
|
||||
playlists: playlistsAdapter.updateOne(
|
||||
{
|
||||
id: action.playlist._id,
|
||||
changes: {
|
||||
title: action.playlist.title,
|
||||
autoRefresh: action.playlist.autoRefresh || false,
|
||||
userAgent: action.playlist.userAgent,
|
||||
...(action.playlist.serverUrl !== null
|
||||
? { serverUrl: action.playlist.serverUrl }
|
||||
: {}),
|
||||
...(action.playlist.username !== null
|
||||
? { username: action.playlist.username }
|
||||
: {}),
|
||||
...(action.playlist.password !== null
|
||||
? { password: action.playlist.password }
|
||||
: {}),
|
||||
...(action.playlist.macAddress !== null
|
||||
? { macAddress: action.playlist.macAddress }
|
||||
: {}),
|
||||
...(action.playlist.portalUrl !== null
|
||||
? { portalUrl: action.playlist.portalUrl }
|
||||
: {}),
|
||||
},
|
||||
},
|
||||
state.playlists
|
||||
),
|
||||
};
|
||||
}),
|
||||
on(PlaylistActions.updateManyPlaylists, (state, action): PlaylistState => {
|
||||
return {
|
||||
...state,
|
||||
playlists: playlistsAdapter.updateMany(
|
||||
action.playlists.map((updatedPlaylist) => ({
|
||||
id: updatedPlaylist._id,
|
||||
changes: {
|
||||
...updatedPlaylist,
|
||||
updateDate: Date.now(),
|
||||
},
|
||||
})),
|
||||
state.playlists
|
||||
),
|
||||
};
|
||||
}),
|
||||
on(PlaylistActions.removeAllPlaylists, (state): PlaylistState => {
|
||||
return {
|
||||
...state,
|
||||
playlists: playlistsAdapter.removeAll(state.playlists),
|
||||
};
|
||||
}),
|
||||
on(
|
||||
PlaylistActions.setCurrentPlaylistId,
|
||||
(state, { playlistId }): PlaylistState => {
|
||||
return {
|
||||
...state,
|
||||
currentPlaylistId: playlistId,
|
||||
};
|
||||
}
|
||||
),
|
||||
on(
|
||||
PlaylistActions.handleAddingPlaylistByUrl,
|
||||
(state, action): PlaylistState => {
|
||||
if (action.isTemporary) {
|
||||
return {
|
||||
...state,
|
||||
channels: action.playlist.playlist.items as Channel[],
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
...state,
|
||||
playlists: playlistsAdapter.addOne(
|
||||
action.playlist,
|
||||
state.playlists
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
),
|
||||
];
|
||||
@@ -12,7 +12,7 @@ import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatSliderModule } from '@angular/material/slider';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { setAdjacentChannelAsActive } from 'm3u-state';
|
||||
import { ChannelActions } from 'm3u-state';
|
||||
|
||||
@Component({
|
||||
selector: 'app-audio-player',
|
||||
@@ -125,6 +125,6 @@ export class AudioPlayerComponent implements OnChanges {
|
||||
}
|
||||
|
||||
switchChannel(direction: 'next' | 'previous') {
|
||||
this.store.dispatch(setAdjacentChannelAsActive({ direction }));
|
||||
this.store.dispatch(ChannelActions.setAdjacentChannelAsActive({ direction }));
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -28,7 +28,7 @@ import { Store } from '@ngrx/store';
|
||||
import { StorageMap } from '@ngx-pwa/local-storage';
|
||||
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
|
||||
import * as _ from 'lodash';
|
||||
import * as PlaylistActions from 'm3u-state';
|
||||
import { ChannelActions, FavoritesActions } from 'm3u-state';
|
||||
import {
|
||||
selectActive,
|
||||
selectActivePlaylistId,
|
||||
@@ -165,7 +165,7 @@ export class ChannelListContainerComponent implements OnInit, OnDestroy {
|
||||
* @param channel selected channel
|
||||
*/
|
||||
selectChannel(channel: Channel): void {
|
||||
this.store.dispatch(PlaylistActions.setActiveChannel({ channel }));
|
||||
this.store.dispatch(ChannelActions.setActiveChannel({ channel }));
|
||||
|
||||
// Use tvg-id for EPG matching, fallback to channel name if not available
|
||||
const epgChannelId = channel?.tvg?.id?.trim() || channel?.name.trim();
|
||||
@@ -187,7 +187,7 @@ export class ChannelListContainerComponent implements OnInit, OnDestroy {
|
||||
undefined,
|
||||
{ duration: 2000 }
|
||||
);
|
||||
this.store.dispatch(PlaylistActions.updateFavorites({ channel }));
|
||||
this.store.dispatch(FavoritesActions.updateFavorites({ channel }));
|
||||
}
|
||||
|
||||
trackByFn(_: number, channel: Channel): string {
|
||||
@@ -197,7 +197,7 @@ export class ChannelListContainerComponent implements OnInit, OnDestroy {
|
||||
drop(event: CdkDragDrop<Channel[]>, favorites: Channel[]) {
|
||||
moveItemInArray(favorites, event.previousIndex, event.currentIndex);
|
||||
this.store.dispatch(
|
||||
PlaylistActions.setFavorites({
|
||||
FavoritesActions.setFavorites({
|
||||
channelIds: favorites.map((item) => item.url),
|
||||
})
|
||||
);
|
||||
@@ -230,7 +230,7 @@ export class ChannelListContainerComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.store.dispatch(PlaylistActions.setChannels({ channels: [] }));
|
||||
this.store.dispatch(ChannelActions.setChannels({ channels: [] }));
|
||||
|
||||
if (this.epgRefreshInterval) {
|
||||
clearInterval(this.epgRefreshInterval);
|
||||
|
||||
@@ -8,11 +8,8 @@ import { MomentDatePipe } from '@iptvnator/pipes';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import {
|
||||
resetActiveEpgProgram,
|
||||
selectActive,
|
||||
setActiveEpgProgram,
|
||||
setCurrentEpgProgram,
|
||||
setEpgAvailableFlag,
|
||||
EpgActions,
|
||||
} from 'm3u-state';
|
||||
import moment from 'moment';
|
||||
import { BehaviorSubject, combineLatest, map } from 'rxjs';
|
||||
@@ -157,7 +154,7 @@ export class EpgListComponent implements OnInit {
|
||||
|
||||
// Dispatch EPG availability flag
|
||||
this.store.dispatch(
|
||||
setEpgAvailableFlag({ value: programs.length > 0 })
|
||||
EpgActions.setEpgAvailableFlag({ value: programs.length > 0 })
|
||||
);
|
||||
|
||||
if (programs.length > 0) {
|
||||
@@ -165,7 +162,7 @@ export class EpgListComponent implements OnInit {
|
||||
} else {
|
||||
this.channel = {} as EpgChannel;
|
||||
// Clear the current EPG program when no programs available
|
||||
this.store.dispatch(resetActiveEpgProgram());
|
||||
this.store.dispatch(EpgActions.resetActiveEpgProgram());
|
||||
}
|
||||
|
||||
// Trigger change detection for OnPush strategy
|
||||
@@ -223,7 +220,7 @@ export class EpgListComponent implements OnInit {
|
||||
// Always dispatch the action, even when playingNow is null/undefined
|
||||
// This ensures the store is updated and old EPG data is cleared
|
||||
this.store.dispatch(
|
||||
setCurrentEpgProgram({ program: this.playingNow })
|
||||
EpgActions.setCurrentEpgProgram({ program: this.playingNow })
|
||||
);
|
||||
// Trigger change detection for OnPush strategy
|
||||
this.cdr.markForCheck();
|
||||
@@ -242,10 +239,10 @@ export class EpgListComponent implements OnInit {
|
||||
timeshift?: boolean
|
||||
): void {
|
||||
if (isLive) {
|
||||
this.store.dispatch(resetActiveEpgProgram());
|
||||
this.store.dispatch(EpgActions.resetActiveEpgProgram());
|
||||
} else {
|
||||
if (!timeshift) return;
|
||||
this.store.dispatch(setActiveEpgProgram({ program }));
|
||||
this.store.dispatch(EpgActions.setActiveEpgProgram({ program }));
|
||||
}
|
||||
this.playingNow = program;
|
||||
// Trigger change detection for OnPush strategy
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
|
||||
import * as PlaylistActions from 'm3u-state';
|
||||
import { PlaylistActions } from 'm3u-state';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { DatabaseService, PlaylistsService } from 'services';
|
||||
import { Playlist, PlaylistMeta } from 'shared-interfaces';
|
||||
|
||||
@@ -9,7 +9,7 @@ import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { Router } from '@angular/router';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
|
||||
import * as PlaylistActions from 'm3u-state';
|
||||
import { PlaylistActions } from 'm3u-state';
|
||||
import {
|
||||
selectActiveTypeFilters,
|
||||
selectAllPlaylistsMeta,
|
||||
|
||||
@@ -6,7 +6,7 @@ import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { Router, RouterLink } from '@angular/router';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import * as PlaylistActions from 'm3u-state';
|
||||
import { ChannelActions } from 'm3u-state';
|
||||
import { selectPlaylistTitle } from 'm3u-state';
|
||||
import { Channel, SidebarView } from 'shared-interfaces';
|
||||
import { RecentPlaylistsComponent } from '../../recent-playlists/recent-playlists.component';
|
||||
@@ -38,7 +38,7 @@ export class SidebarComponent {
|
||||
|
||||
goBack() {
|
||||
if (this.sidebarView === 'PLAYLISTS') {
|
||||
this.store.dispatch(PlaylistActions.resetActiveChannel());
|
||||
this.store.dispatch(ChannelActions.resetActiveChannel());
|
||||
this.router.navigate(['/']);
|
||||
} else {
|
||||
this.sidebarView = 'PLAYLISTS';
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
selectActivePlaylistId,
|
||||
selectFavorites,
|
||||
selectIsEpgAvailable,
|
||||
updateFavorites,
|
||||
FavoritesActions,
|
||||
} from 'm3u-state';
|
||||
import { Channel } from 'shared-interfaces';
|
||||
|
||||
@@ -46,6 +46,6 @@ export class ToolbarComponent {
|
||||
readonly playlistId$ = this.store.select(selectActivePlaylistId);
|
||||
|
||||
updateFavoriteStatus(channel: Channel) {
|
||||
this.store.dispatch(updateFavorites({ channel }));
|
||||
this.store.dispatch(FavoritesActions.updateFavorites({ channel }));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user