fix(ui): correct imports, guard checks, logging and settings handling

This commit is contained in:
4gray committed 2025-10-27 08:20:29 +01:00
1 parent 516a376dc0
commit ccbb08cfb4
21 files changed
+240 -216

No files matched your search

@@ -8,6 +8,10 @@ contextBridge.exposeInMainWorld('electron', {
updatePlaylistFromFilePath: (filePath: string, title: string) =>
ipcRenderer.invoke('update-playlist-from-file-path', filePath, title),
openPlaylistFromFile: () => ipcRenderer.invoke('open-playlist-from-file'),
saveFileDialog: (defaultPath: string, filters?: { name: string; extensions: string[] }[]) =>
ipcRenderer.invoke('save-file-dialog', defaultPath, filters),
writeFile: (filePath: string, content: string) =>
ipcRenderer.invoke('write-file', filePath, content),
setUserAgent: (userAgent: string, referer?: string) =>
ipcRenderer.invoke('set-user-agent', userAgent, referer),
openInMpv: (
@@ -7,7 +7,7 @@ import axios from 'axios';
import { dialog, ipcMain } from 'electron';
import { parse } from 'iptv-playlist-parser';
import { createPlaylistObject, getFilenameFromUrl } from 'm3u-utils';
import { readFile } from 'node:fs/promises';
import { readFile, writeFile } from 'node:fs/promises';
import { AUTO_UPDATE_PLAYLISTS } from 'shared-interfaces';
export default class PlaylistEvents {
@@ -102,3 +102,33 @@ ipcMain.handle(AUTO_UPDATE_PLAYLISTS, async (event, playlistUrls) => {
console.log(`Auto-updating playlist from ${url}`);
}
});
ipcMain.handle('save-file-dialog', async (event, defaultPath, filters) => {
try {
const { canceled, filePath } = await dialog.showSaveDialog({
defaultPath,
filters: filters || [
{ name: 'All Files', extensions: ['*'] },
],
});
if (canceled || !filePath) {
return null;
}
return filePath;
} catch (error) {
console.error('Error showing save dialog:', error);
throw error;
}
});
ipcMain.handle('write-file', async (event, filePath, content) => {
try {
await writeFile(filePath, content, 'utf-8');
return { success: true };
} catch (error) {
console.error('Error writing file:', error);
throw error;
}
});
@@ -1,6 +1,5 @@
import { ipcMain } from 'electron';
import {
MPV_PLAYER_PATH,
MPV_REUSE_INSTANCE,
store,
} from '../services/store.service';
@@ -13,5 +12,9 @@ export default class SettingsEvents {
ipcMain.handle('SETTINGS_UPDATE', (_event, arg) => {
console.log('Received SETTINGS_UPDATE with data:', arg);
store.set(MPV_REUSE_INSTANCE, arg.mpvReuseInstance);
// Only set values that are defined
if (arg.mpvReuseInstance !== undefined) {
store.set(MPV_REUSE_INSTANCE, arg.mpvReuseInstance);
}
});
+5 -7
View File
@@ -13,7 +13,6 @@ import {
Language,
OPEN_FILE,
Settings,
SETTINGS_UPDATE,
STORE_KEY,
Theme,
VIEW_ADD_PLAYLIST,
@@ -120,13 +119,12 @@ export class AppComponent implements OnInit, OnDestroy {
.getValueFromLocalStorage(STORE_KEY.Settings)
.subscribe((settings: Settings) => {
if (settings && Object.keys(settings).length > 0) {
// Send settings to Electron main process
if (window.electron) {
window.electron.updateSettings(settings);
}
// No need to send settings to Electron on init
// Settings are stored in IndexedDB and loaded by the settings store
// Only specific Electron settings (MPV/VLC paths) are sent when changed in settings component
this.translate.use(settings.language ?? this.DEFAULT_LANG);
// Fetch EPG if URLs are configured
if (
window.electron &&
@@ -252,16 +252,20 @@ export class SettingsComponent implements OnInit {
onSubmit(): void {
this.settingsStore.updateSettings(this.settingsForm.value).then(() => {
this.applyChangedSettings();
if (window.electron) {
window.electron.updateSettings(this.settingsForm.value);
// Set player paths if using external players
if (this.settingsForm.value.mpvPlayerPath) {
window.electron.setMpvPlayerPath(this.settingsForm.value.mpvPlayerPath);
window.electron.setMpvPlayerPath(
this.settingsForm.value.mpvPlayerPath
);
}
if (this.settingsForm.value.vlcPlayerPath) {
window.electron.setVlcPlayerPath(this.settingsForm.value.vlcPlayerPath);
window.electron.setVlcPlayerPath(
this.settingsForm.value.vlcPlayerPath
);
}
}
});
@@ -1,4 +1,4 @@
@if ((activeProcesses$ | async)?.length) {
<!-- @if ((activeProcesses$ | async)?.length) {
<div class="mpv-player-bar">
@for (process of activeProcesses$ | async; track process.id) {
<div class="player-item">
@@ -24,20 +24,6 @@
</div>
</div>
<div class="player-controls">
<!-- <button
mat-icon-button
(click)="pauseStream(process.id)"
title="Pause"
>
<mat-icon>pause</mat-icon>
</button>
<button
mat-icon-button
(click)="playStream(process.id)"
title="Play"
>
<mat-icon>play_arrow</mat-icon>
</button> -->
<button
mat-icon-button
(click)="closeStream(process.id)"
@@ -50,3 +36,4 @@
}
</div>
}
-->
@@ -1,30 +1,19 @@
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { Observable } from 'rxjs';
import {
MpvPlayerService,
MpvProcess,
} from '../../services/mpv-player.service';
@Component({
imports: [CommonModule, MatIconModule, MatButtonModule],
/* imports: [MatIcon, MatIconButton], */
selector: 'app-mpv-player-bar',
templateUrl: './mpv-player-bar.component.html',
styleUrls: ['./mpv-player-bar.component.scss']
styleUrls: ['./mpv-player-bar.component.scss'],
})
export class MpvPlayerBarComponent {
activeProcesses$: Observable<MpvProcess[]>;
constructor(private mpvPlayerService: MpvPlayerService) {
this.activeProcesses$ = this.mpvPlayerService.activeProcesses$;
}
/* private mpvPlayerService = inject(MpvPlayerService);
activeProcesses$ = this.mpvPlayerService.activeProcesses$;
async closeStream(processId: number): Promise<void> {
await this.mpvPlayerService.closeStream(processId);
}
} */
/* async playStream(processId: number): Promise<void> {
await this.mpvPlayerService.playStream(processId);
}
@@ -1,4 +1,4 @@
import { Injectable } from '@angular/core';
/* import { Injectable } from '@angular/core';
import { invoke } from '@tauri-apps/api/core';
import { listen } from '@tauri-apps/api/event';
import { BehaviorSubject } from 'rxjs';
@@ -75,3 +75,4 @@ export class MpvPlayerService {
await invoke('close_mpv_process', { processId });
}
}
*/
Vendored
+8
View File
@@ -21,6 +21,14 @@ declare global {
title: string
) => Promise<Playlist>;
openPlaylistFromFile: () => Promise<Playlist>;
saveFileDialog: (
defaultPath: string,
filters?: { name: string; extensions: string[] }[]
) => Promise<string | null>;
writeFile: (
filePath: string,
content: string
) => Promise<{ success: boolean }>;
setUserAgent: (userAgent: string, referer?: string) => void;
openInMpv: (
url: string,
+2 -2
View File
@@ -125,13 +125,13 @@ export class PlaylistEffects {
ofType(PlaylistActions.setActiveChannel),
map((action) => {
const { channel } = action;
// Use modern EPG service to get channel programs
const channelId = channel.tvg?.id || channel.name;
if (channelId) {
this.epgService.getChannelPrograms(channelId);
}
// Set user agent if specified on channel
if (channel.http['user-agent']) {
window.electron?.setUserAgent(
@@ -20,10 +20,7 @@ import moment from 'moment';
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { DataService, EpgService } from 'services';
import {
EpgChannel,
EpgProgram,
} from 'shared-interfaces';
import { EpgChannel, EpgProgram } from 'shared-interfaces';
import { EpgListItemComponent } from './epg-list-item/epg-list-item.component';
export interface EpgData {
@@ -164,12 +161,12 @@ export class EpgListComponent implements OnInit {
handleEpgData(programs: EpgProgram[]): void {
this.timeNow = new Date().toISOString();
this.dateToday = moment().format(DATE_FORMAT);
// Dispatch EPG availability flag
this.store.dispatch(
setEpgAvailableFlag({ value: programs.length > 0 })
);
if (programs.length > 0) {
this.setPlayingNow();
} else {
@@ -88,11 +88,7 @@ export class HtmlVideoPlayerComponent implements OnInit, OnChanges, OnDestroy {
Hls &&
Hls.isSupported()
) {
console.log(
'... switching channel to ',
channel.name,
url
);
console.log('... switching channel to ', channel.name, url);
this.hls = new Hls();
this.hls.attachMedia(this.videoPlayer.nativeElement);
this.hls.loadSource(url);
@@ -33,7 +33,7 @@ export class InfoOverlayComponent implements OnChanges {
stop: any | undefined;
/** Timeout for the overlay visibility */
currentTimeout: any;
private currentTimeout: any;
/**
* Calculates the necessary information for the visualization in the overview popup
@@ -159,17 +159,12 @@ export class MultiEpgContainerComponent
}
try {
console.log('Requesting EPG data:');
console.log('- Skip:', this.channelsLowerRange);
console.log('- Limit:', this.visibleChannels);
const response = await window.electron.getEpgChannelsByRange(
this.channelsLowerRange,
this.visibleChannels
);
if (response && Array.isArray(response)) {
console.log('Received channels:', response.length);
this.originalEpgData = response;
this.channels$.next(this.enrichProgramData());
@@ -82,7 +82,7 @@
<input matInput formControlName="count" />
</mat-form-field>
}
@if (isTauri) {
@if (isDesktop) {
<mat-form-field class="full-width mb-10">
<mat-label>{{
'HOME.PLAYLISTS.INFO_DIALOG.USER_AGENT' | translate
@@ -109,18 +109,16 @@
}
</mat-form-field>
}
@if (isTauri && (playlist.url || playlist.filePath)) {
<ng-container>
<mat-checkbox class="full-width" formControlName="autoRefresh">
{{ 'HOME.PLAYLISTS.INFO_DIALOG.AUTO_UPDATE' | translate }}
</mat-checkbox>
<p>
{{
'HOME.PLAYLISTS.INFO_DIALOG.AUTO_UPDATE_DESCRIPTION'
| translate
}}
</p>
</ng-container>
@if (isDesktop && (playlist.url || playlist.filePath)) {
<mat-checkbox class="full-width" formControlName="autoRefresh">
{{ 'HOME.PLAYLISTS.INFO_DIALOG.AUTO_UPDATE' | translate }}
</mat-checkbox>
<p>
{{
'HOME.PLAYLISTS.INFO_DIALOG.AUTO_UPDATE_DESCRIPTION'
| translate
}}
</p>
}
</mat-dialog-content>
<mat-dialog-actions align="end">
@@ -1,5 +1,4 @@
/* eslint-disable @typescript-eslint/unbound-method */
import { CommonModule, DatePipe } from '@angular/common';
import { DatePipe } from '@angular/common';
import { Component, inject } from '@angular/core';
import {
FormControl,
@@ -8,16 +7,14 @@ import {
UntypedFormGroup,
Validators,
} from '@angular/forms';
import { MatButtonModule } from '@angular/material/button';
import { MatButton } from '@angular/material/button';
import { MatCheckboxModule } from '@angular/material/checkbox';
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
import { MatIconModule } from '@angular/material/icon';
import { MatIcon } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatSnackBar } from '@angular/material/snack-bar';
import { Store } from '@ngrx/store';
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
import { save } from '@tauri-apps/plugin-dialog';
import { writeTextFile } from '@tauri-apps/plugin-fs';
import * as PlaylistActions from 'm3u-state';
import { firstValueFrom } from 'rxjs';
import { DatabaseService, PlaylistsService } from 'services';
@@ -35,11 +32,10 @@ import { Playlist, PlaylistMeta } from 'shared-interfaces';
],
providers: [DatePipe],
imports: [
CommonModule,
MatButtonModule,
MatButton,
MatCheckboxModule,
MatDialogModule,
MatIconModule,
MatIcon,
MatInputModule,
ReactiveFormsModule,
TranslatePipe,
@@ -55,7 +51,7 @@ export class PlaylistInfoComponent {
private translate = inject(TranslateService);
public playlistData = inject<Playlist & { id: string }>(MAT_DIALOG_DATA);
isTauri = !!window.electron;
readonly isDesktop = !!window.electron;
/** Playlist object */
playlist: Playlist & { id: string };
@@ -168,20 +164,20 @@ export class PlaylistInfoComponent {
this.playlistsService.getRawPlaylistById(this.playlist._id)
);
if (this.isTauri) {
if (this.isDesktop) {
try {
const savePath = await save({
filters: [
const savePath = await window.electron.saveFileDialog(
`${this.playlist.title || 'exported'}.m3u8`,
[
{
name: 'Playlist',
extensions: ['m3u8'],
extensions: ['m3u8', 'm3u'],
},
],
defaultPath: `${this.playlist.title || 'exported'}.m3u8`,
});
]
);
if (savePath) {
await writeTextFile(savePath, playlistAsString);
await window.electron.writeFile(savePath, playlistAsString);
this.snackBar.open(
this.translate.instant(
'HOME.PLAYLISTS.INFO_DIALOG.PLAYLIST_EXPORT_SUCCESS'
@@ -1,56 +1,60 @@
<mat-list-item (click)="playlistClicked.emit(item._id)">
<mat-icon
matListItemIcon
class="upload-type-icon"
*ngIf="item?.url"
[matTooltip]="
('HOME.PLAYLISTS.ADDED_VIA_URL' | translate) + ' ' + item?.url
"
>cloud</mat-icon
>
<mat-icon
matListItemIcon
class="upload-type-icon"
*ngIf="!item?.url && !item.serverUrl && !item.macAddress"
[matTooltip]="'HOME.PLAYLISTS.ADDED_VIA_FILE' | translate"
>folder</mat-icon
>
<div class="icon-container" *ngIf="item?.serverUrl" matListItemIcon>
@if (item.url) {
<mat-icon
matListItemIcon
class="upload-type-icon"
[matTooltip]="'HOME.PLAYLISTS.XTREAM_PLAYLIST' | translate"
>public</mat-icon
[matTooltip]="
('HOME.PLAYLISTS.ADDED_VIA_URL' | translate) + ' ' + item.url
"
>cloud</mat-icon
>
<span
class="status-dot"
[class]="getStatusClass()"
[matTooltip]="'Portal status: ' + portalStatus"
></span>
</div>
<mat-icon
matListItemIcon
class="upload-type-icon"
*ngIf="item?.macAddress"
[matTooltip]="'HOME.PLAYLISTS.STALKER_PORTAL' | translate"
>dashboard</mat-icon
>
}
@if (!item.url && !item.serverUrl && !item.macAddress) {
<mat-icon
matListItemIcon
class="upload-type-icon"
[matTooltip]="'HOME.PLAYLISTS.ADDED_VIA_FILE' | translate"
>folder</mat-icon
>
}
@if (item.serverUrl) {
<div class="icon-container" matListItemIcon>
<mat-icon
class="upload-type-icon"
[matTooltip]="'HOME.PLAYLISTS.XTREAM_PLAYLIST' | translate"
>public</mat-icon
>
<span
class="status-dot"
[class]="getStatusClass()"
[matTooltip]="'Portal status: ' + portalStatus"
[attr.aria-label]="'Portal status: ' + portalStatus"
></span>
</div>
}
@if (item.macAddress) {
<mat-icon
matListItemIcon
class="upload-type-icon"
[matTooltip]="'HOME.PLAYLISTS.STALKER_PORTAL' | translate"
>dashboard</mat-icon
>
}
<div matListItemTitle>
{{ item.title || item.filename }}
</div>
<div matListItemLine class="meta">
<ng-container
*ngIf="!item.serverUrl && !item.portalUrl; else xtreamPlaylist"
>
@if (!item.serverUrl && !item.portalUrl) {
{{ 'HOME.PLAYLISTS.CHANNELS' | translate }}: {{ item.count }}
<ng-container *ngIf="item.importDate?.length > 3">
@if (item.importDate.length > 3) {
| {{ 'HOME.PLAYLISTS.ADDED' | translate }}:
{{ item.importDate | date }}
</ng-container>
<ng-container *ngIf="item.updateDate">
}
@if (item.updateDate) {
| {{ 'HOME.PLAYLISTS.UPDATED' | translate }}:
{{ item.updateDate | date: 'MMMM d, yyyy, HH:mm' }}
</ng-container>
<ng-container *ngIf="item.updateState === 2">
}
@if (item.updateState === 2) {
<mat-icon
[matTooltip]="
'HOME.PLAYLISTS.INFO_DIALOG.UPDATE_FAILED' | translate
@@ -58,64 +62,74 @@
[inline]="true"
>warning</mat-icon
>
</ng-container>
</ng-container>
<ng-template #xtreamPlaylist>
@if (item?.macAddress) {
}
} @else {
@if (item.macAddress) {
Stalker Portal
} @else {
Xtream Code Playlist
}
<ng-container *ngIf="item.importDate?.length > 3">
@if (item.importDate.length > 3) {
| {{ 'HOME.PLAYLISTS.ADDED' | translate }}:
{{ item.importDate | date }}
</ng-container>
<ng-container *ngIf="item.updateDate">
}
@if (item.updateDate) {
| {{ 'HOME.PLAYLISTS.UPDATED' | translate }}:
{{ item.updateDate | date: 'MMMM d, yyyy, HH:mm' }}
</ng-container>
</ng-template>
}
}
</div>
<span matListItemMeta class="action-buttons" *ngIf="showActions">
<button
*ngIf="item.url || item.filePath"
mat-icon-button
class="refresh-btn"
color="accent"
(click)="$event.stopPropagation(); refreshClicked.emit(item)"
[matTooltip]="'HOME.PLAYLISTS.REFRESH' | translate"
>
<mat-icon>sync</mat-icon>
</button>
<button
*ngIf="item.serverUrl"
mat-icon-button
class="refresh-btn"
color="accent"
(click)="$event.stopPropagation(); refreshClicked.emit(item)"
[matTooltip]="'HOME.PLAYLISTS.REFRESH_XTREAM' | translate"
>
<mat-icon>sync</mat-icon>
</button>
<button
mat-icon-button
color="accent"
class="edit-btn"
(click)="$event.stopPropagation(); editPlaylistClicked.emit(item)"
[matTooltip]="'HOME.PLAYLISTS.SHOW_DETAILS' | translate"
>
<mat-icon>edit</mat-icon>
</button>
@if (showActions()) {
<span matListItemMeta class="action-buttons">
@if (item.url || item.filePath) {
<button
mat-icon-button
class="refresh-btn"
color="accent"
(click)="
$event.stopPropagation(); refreshClicked.emit(item)
"
[matTooltip]="'HOME.PLAYLISTS.REFRESH' | translate"
>
<mat-icon>sync</mat-icon>
</button>
}
@if (item.serverUrl) {
<button
mat-icon-button
class="refresh-btn"
color="accent"
(click)="
$event.stopPropagation(); refreshClicked.emit(item)
"
[matTooltip]="'HOME.PLAYLISTS.REFRESH_XTREAM' | translate"
>
<mat-icon>sync</mat-icon>
</button>
}
<button
mat-icon-button
color="accent"
class="delete-btn"
(click)="$event.stopPropagation(); removeClicked.emit(item._id)"
[matTooltip]="'HOME.PLAYLISTS.REMOVE' | translate"
>
<mat-icon>delete</mat-icon>
</button>
</span>
<button
mat-icon-button
color="accent"
class="edit-btn"
(click)="
$event.stopPropagation(); editPlaylistClicked.emit(item)
"
[matTooltip]="'HOME.PLAYLISTS.SHOW_DETAILS' | translate"
>
<mat-icon>edit</mat-icon>
</button>
<button
mat-icon-button
color="accent"
class="delete-btn"
(click)="$event.stopPropagation(); removeClicked.emit(item._id)"
[matTooltip]="'HOME.PLAYLISTS.REMOVE' | translate"
>
<mat-icon>delete</mat-icon>
</button>
</span>
}
</mat-list-item>
@@ -4,7 +4,7 @@ import { MatListModule } from '@angular/material/list';
import { MatTooltipModule } from '@angular/material/tooltip';
import { TranslateModule } from '@ngx-translate/core';
import { MockModule } from 'ng-mocks';
import { DataService } from '../../../../../../services/src/lib/data.service';
import { DataService } from 'services';
import { PlaylistItemComponent } from './playlist-item.component';
describe('PlaylistItemComponent', () => {
@@ -27,7 +27,13 @@ describe('PlaylistItemComponent', () => {
beforeEach(() => {
fixture = TestBed.createComponent(PlaylistItemComponent);
component = fixture.componentInstance;
component.item = { title: 'Playlist', id: '1' } as any;
component.item = {
title: 'Playlist',
_id: '1',
count: 10,
importDate: Date.now().toString(),
autoRefresh: false,
};
fixture.detectChanges();
});
@@ -1,9 +1,9 @@
import { DatePipe, NgIf } from '@angular/common';
import { Component, Input, OnInit, inject, output } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
import { DatePipe } from '@angular/common';
import { Component, Input, OnInit, inject, input, output } from '@angular/core';
import { MatIconButton } from '@angular/material/button';
import { MatIcon } from '@angular/material/icon';
import { MatListModule } from '@angular/material/list';
import { MatTooltipModule } from '@angular/material/tooltip';
import { MatTooltip } from '@angular/material/tooltip';
import { TranslatePipe } from '@ngx-translate/core';
import { PortalStatus, PortalStatusService } from 'services';
import { PlaylistMeta } from 'shared-interfaces';
@@ -14,17 +14,16 @@ import { PlaylistMeta } from 'shared-interfaces';
styleUrls: ['./playlist-item.component.scss'],
imports: [
DatePipe,
MatButtonModule,
MatIconModule,
MatIconButton,
MatIcon,
MatListModule,
MatTooltipModule,
NgIf,
MatTooltip,
TranslatePipe,
],
})
export class PlaylistItemComponent implements OnInit {
@Input() item!: PlaylistMeta;
@Input() showActions = true;
readonly showActions = input(true);
readonly editPlaylistClicked = output<PlaylistMeta>();
readonly playlistClicked = output<string>();
@@ -34,21 +33,24 @@ export class PlaylistItemComponent implements OnInit {
portalStatus: PortalStatus = 'unavailable';
private readonly portalStatusService = inject(PortalStatusService);
// eslint-disable-next-line @typescript-eslint/no-misused-promises
async ngOnInit() {
if (this.item?.serverUrl) {
await this.checkPortalStatus();
}
await this.checkPortalStatus();
}
private async checkPortalStatus() {
try {
this.portalStatus =
await this.portalStatusService.checkPortalStatus(
this.item.serverUrl,
this.item.username,
this.item.password
);
if (
this.item.serverUrl &&
this.item.username &&
this.item.password
) {
this.portalStatus =
await this.portalStatusService.checkPortalStatus(
this.item.serverUrl,
this.item.username,
this.item.password
);
}
} catch (error) {
console.error('Error checking portal status:', error);
this.portalStatus = 'unavailable';
@@ -10,10 +10,8 @@ import {
Output,
viewChild,
} from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatDialog } from '@angular/material/dialog';
import { MatDividerModule } from '@angular/material/divider';
import { MatIconModule } from '@angular/material/icon';
import { MatIcon } from '@angular/material/icon';
import { MatInputModule } from '@angular/material/input';
import { MatListModule } from '@angular/material/list';
import { MatSnackBar } from '@angular/material/snack-bar';
@@ -47,9 +45,7 @@ import { PlaylistItemComponent } from './playlist-item/playlist-item.component';
styleUrls: ['./recent-playlists.component.scss'],
imports: [
AsyncPipe,
MatButtonModule,
MatDividerModule,
MatIconModule,
MatIcon,
MatInputModule,
MatListModule,
NgxSkeletonLoaderComponent,
@@ -75,7 +71,7 @@ export class RecentPlaylistsComponent implements OnInit {
readonly ghostElements = new Array(10);
playlists$ = combineLatest([
readonly playlists$ = combineLatest([
this.store.select(selectAllPlaylistsMeta),
this.searchQuery,
this.store.select(selectActiveTypeFilters),
+1 -1
View File
@@ -13,7 +13,7 @@ export class MomentDatePipe implements PipeTransform {
value: string,
formatToParse: string,
formatToReturn = 'MMMM Do, dddd'
): any {
): string {
return moment(value, formatToParse).format(formatToReturn);
}
}