mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
refactor: refactor date handling across components to use date-fns and Intl.DateTimeFormat
- Replaced moment.js usage with Intl.DateTimeFormat for date formatting in various components. - Introduced utility functions `normalizeDateLocale` and `formatWithIntl` for locale normalization and date formatting. - Updated PlaylistSwitcher, RecentPlaylists, ContentCard, EPG components, and MultiEpgContainer to utilize the new date formatting approach. - Removed moment-date pipe and related tests, simplifying date handling logic. - Ensured all date displays respect the current locale settings. Entire-Checkpoint: 271bd0162027
This commit is contained in:
1 parent
d3e30056f6
commit
c97673b928
29 files changed
+410
-247
No files matched your search
@@ -0,0 +1,46 @@
|
||||
import { registerLocaleData } from '@angular/common';
|
||||
import localeAr from '@angular/common/locales/ar';
|
||||
import localeArMa from '@angular/common/locales/ar-MA';
|
||||
import localeBe from '@angular/common/locales/be';
|
||||
import localeDe from '@angular/common/locales/de';
|
||||
import localeEl from '@angular/common/locales/el';
|
||||
import localeEs from '@angular/common/locales/es';
|
||||
import localeFr from '@angular/common/locales/fr';
|
||||
import localeIt from '@angular/common/locales/it';
|
||||
import localeJa from '@angular/common/locales/ja';
|
||||
import localeKo from '@angular/common/locales/ko';
|
||||
import localeNl from '@angular/common/locales/nl';
|
||||
import localePl from '@angular/common/locales/pl';
|
||||
import localePt from '@angular/common/locales/pt';
|
||||
import localeRu from '@angular/common/locales/ru';
|
||||
import localeTr from '@angular/common/locales/tr';
|
||||
import localeZh from '@angular/common/locales/zh';
|
||||
import localeZhHant from '@angular/common/locales/zh-Hant';
|
||||
|
||||
let localesRegistered = false;
|
||||
|
||||
export function registerAppDateLocales(): void {
|
||||
if (localesRegistered) {
|
||||
return;
|
||||
}
|
||||
|
||||
registerLocaleData(localeAr, 'ar');
|
||||
registerLocaleData(localeArMa, 'ar-MA');
|
||||
registerLocaleData(localeBe, 'be');
|
||||
registerLocaleData(localeDe, 'de');
|
||||
registerLocaleData(localeEl, 'el');
|
||||
registerLocaleData(localeEs, 'es');
|
||||
registerLocaleData(localeFr, 'fr');
|
||||
registerLocaleData(localeIt, 'it');
|
||||
registerLocaleData(localeJa, 'ja');
|
||||
registerLocaleData(localeKo, 'ko');
|
||||
registerLocaleData(localeNl, 'nl');
|
||||
registerLocaleData(localePl, 'pl');
|
||||
registerLocaleData(localePt, 'pt');
|
||||
registerLocaleData(localeRu, 'ru');
|
||||
registerLocaleData(localeTr, 'tr');
|
||||
registerLocaleData(localeZh, 'zh');
|
||||
registerLocaleData(localeZhHant, 'zh-Hant');
|
||||
|
||||
localesRegistered = true;
|
||||
}
|
||||
@@ -1,7 +1,10 @@
|
||||
import { bootstrapApplication } from '@angular/platform-browser';
|
||||
import { registerAppDateLocales } from './app/app-date-locales';
|
||||
import { AppComponent } from './app/app.component';
|
||||
import { appConfig } from './app/app.config';
|
||||
|
||||
registerAppDateLocales();
|
||||
|
||||
bootstrapApplication(AppComponent, appConfig).catch((err) =>
|
||||
console.error(err)
|
||||
);
|
||||
@@ -0,0 +1,25 @@
|
||||
# Date Handling
|
||||
|
||||
## Rules
|
||||
|
||||
- Use native `Date`, ISO strings, and epoch timestamps as the stored/runtime values.
|
||||
- Use `date-fns` for parsing, arithmetic, and normalization logic.
|
||||
- Use Angular `DatePipe` in templates when the value is already a `Date`, ISO string, or epoch timestamp.
|
||||
- Use cached `Intl.DateTimeFormat` helpers in TypeScript-only formatting paths where Angular pipes are not available.
|
||||
- Do not add new `moment` usage. The app no longer depends on it.
|
||||
|
||||
## Locale Strategy
|
||||
|
||||
- Date display should follow the user-selected app language from `TranslateService`.
|
||||
- When a template renders localized month or weekday names, pass the normalized app locale explicitly to `DatePipe`.
|
||||
- Angular locale data is registered in [apps/web/src/app/app-date-locales.ts](/Users/4gray/Code/iptvnator/apps/web/src/app/app-date-locales.ts).
|
||||
- App language aliases are normalized in [libs/ui/pipes/src/lib/date-format.util.ts](/Users/4gray/Code/iptvnator/libs/ui/pipes/src/lib/date-format.util.ts):
|
||||
- `ary` -> `ar-MA`
|
||||
- `by` -> `be`
|
||||
- `zhtw` -> `zh-Hant`
|
||||
|
||||
## Parsing Boundaries
|
||||
|
||||
- Normalize provider-specific or legacy date strings to ISO as early as possible.
|
||||
- Keep optional epoch fields such as `startTimestamp` and `stopTimestamp` when the provider already supplies them.
|
||||
- Avoid new non-standard `Date.parse(...)` usage for provider formats; prefer explicit `date-fns` parsing when the input is not ISO.
|
||||
@@ -125,7 +125,13 @@
|
||||
{{ getPlaylistTypeLabel(playlist) }}
|
||||
@if (playlist.updateDate) {
|
||||
<span class="separator">|</span>
|
||||
{{ playlist.updateDate | date: 'MMM d, HH:mm' }}
|
||||
{{
|
||||
playlist.updateDate
|
||||
| date
|
||||
: 'MMM d, HH:mm'
|
||||
: undefined
|
||||
: currentLocale()
|
||||
}}
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
signal,
|
||||
viewChild,
|
||||
} from '@angular/core';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MatIconButton } from '@angular/material/button';
|
||||
import { MatRippleModule } from '@angular/material/core';
|
||||
@@ -17,10 +18,12 @@ import { MatDivider } from '@angular/material/divider';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatMenu, MatMenuModule, MatMenuTrigger } from '@angular/material/menu';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { normalizeDateLocale } from '@iptvnator/pipes';
|
||||
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
|
||||
import { PlaylistContextFacade } from '@iptvnator/playlist/shared/util';
|
||||
import { PortalStatus, PortalStatusService } from 'services';
|
||||
import { PlaylistMeta } from 'shared-interfaces';
|
||||
import { startWith } from 'rxjs';
|
||||
|
||||
type PlaylistFilterType = 'm3u' | 'stalker' | 'xtream';
|
||||
|
||||
@@ -53,7 +56,12 @@ export class PlaylistSwitcherComponent {
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
private readonly playlistContext = inject(PlaylistContextFacade);
|
||||
private readonly portalStatusService = inject(PortalStatusService);
|
||||
private readonly translate = inject(TranslateService);
|
||||
private focusSearchTimeoutId: ReturnType<typeof setTimeout> | null = null;
|
||||
private readonly languageTick = toSignal(
|
||||
this.translate.onLangChange.pipe(startWith(null)),
|
||||
{ initialValue: null }
|
||||
);
|
||||
|
||||
readonly currentTitle = input.required<string>();
|
||||
readonly subtitle = input<string>('');
|
||||
@@ -116,6 +124,12 @@ export class PlaylistSwitcherComponent {
|
||||
});
|
||||
|
||||
readonly portalStatuses = signal<Map<string, PortalStatus>>(new Map());
|
||||
readonly currentLocale = computed(() => {
|
||||
this.languageTick();
|
||||
return normalizeDateLocale(
|
||||
this.translate.currentLang || this.translate.defaultLang
|
||||
);
|
||||
});
|
||||
|
||||
constructor() {
|
||||
this.destroyRef.onDestroy(() => {
|
||||
|
||||
+16
-4
@@ -83,11 +83,17 @@
|
||||
{{ 'HOME.PLAYLISTS.CHANNELS' | translate }}: {{ item.count }}
|
||||
@if (item.importDate.length > 3) {
|
||||
| {{ 'HOME.PLAYLISTS.ADDED' | translate }}:
|
||||
{{ item.importDate | date }}
|
||||
{{ item.importDate | date: undefined : undefined : currentLocale() }}
|
||||
}
|
||||
@if (item.updateDate) {
|
||||
| {{ 'HOME.PLAYLISTS.UPDATED' | translate }}:
|
||||
{{ item.updateDate | date: 'MMMM d, yyyy, HH:mm' }}
|
||||
{{
|
||||
item.updateDate
|
||||
| date
|
||||
: 'MMMM d, yyyy, HH:mm'
|
||||
: undefined
|
||||
: currentLocale()
|
||||
}}
|
||||
}
|
||||
@if (item.updateState === 2) {
|
||||
<mat-icon
|
||||
@@ -107,11 +113,17 @@
|
||||
}
|
||||
@if (item.importDate.length > 3) {
|
||||
| {{ 'HOME.PLAYLISTS.ADDED' | translate }}:
|
||||
{{ item.importDate | date }}
|
||||
{{ item.importDate | date: undefined : undefined : currentLocale() }}
|
||||
}
|
||||
@if (item.updateDate) {
|
||||
| {{ 'HOME.PLAYLISTS.UPDATED' | translate }}:
|
||||
{{ item.updateDate | date: 'MMMM d, yyyy, HH:mm' }}
|
||||
{{
|
||||
item.updateDate
|
||||
| date
|
||||
: 'MMMM d, yyyy, HH:mm'
|
||||
: undefined
|
||||
: currentLocale()
|
||||
}}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
+15
-1
@@ -9,12 +9,15 @@ import {
|
||||
input,
|
||||
output,
|
||||
} from '@angular/core';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { MatIconButton } from '@angular/material/button';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { normalizeDateLocale } from '@iptvnator/pipes';
|
||||
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
|
||||
import { startWith } from 'rxjs';
|
||||
import { PortalStatus, PortalStatusService } from 'services';
|
||||
import { PlaylistMeta } from 'shared-interfaces';
|
||||
|
||||
@@ -53,8 +56,19 @@ export class PlaylistItemComponent implements OnInit {
|
||||
|
||||
portalStatus: PortalStatus = 'unavailable';
|
||||
private readonly portalStatusService = inject(PortalStatusService);
|
||||
private readonly translate = inject(TranslateService);
|
||||
private readonly languageTick = toSignal(
|
||||
this.translate.onLangChange.pipe(startWith(null)),
|
||||
{ initialValue: null }
|
||||
);
|
||||
|
||||
readonly isElectron = !!window.electron;
|
||||
readonly currentLocale = computed(() => {
|
||||
this.languageTick();
|
||||
return normalizeDateLocale(
|
||||
this.translate.currentLang || this.translate.defaultLang
|
||||
);
|
||||
});
|
||||
|
||||
async ngOnInit() {
|
||||
await this.checkPortalStatus();
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
<h3>{{ title() }}</h3>
|
||||
@if (date()) {
|
||||
<p class="date-info">
|
||||
{{ date() | date: 'd MMM, HH:mm' }}
|
||||
{{ date() | date: 'd MMM, HH:mm' : undefined : currentLocale() }}
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -2,12 +2,18 @@ import { DatePipe } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
input,
|
||||
output,
|
||||
} from '@angular/core';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { MatIconButton } from '@angular/material/button';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { normalizeDateLocale } from '@iptvnator/pipes';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { startWith } from 'rxjs';
|
||||
|
||||
@Component({
|
||||
selector: 'app-content-card',
|
||||
@@ -18,6 +24,12 @@ import { MatTooltip } from '@angular/material/tooltip';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ContentCardComponent {
|
||||
private readonly translate = inject(TranslateService);
|
||||
private readonly languageTick = toSignal(
|
||||
this.translate.onLangChange.pipe(startWith(null)),
|
||||
{ initialValue: null }
|
||||
);
|
||||
|
||||
/** URL of the poster image */
|
||||
readonly posterUrl = input<string>();
|
||||
|
||||
@@ -44,6 +56,12 @@ export class ContentCardComponent {
|
||||
|
||||
/** Emitted when the remove button is clicked */
|
||||
readonly remove = output<void>();
|
||||
readonly currentLocale = computed(() => {
|
||||
this.languageTick();
|
||||
return normalizeDateLocale(
|
||||
this.translate.currentLang || this.translate.defaultLang
|
||||
);
|
||||
});
|
||||
|
||||
/** Get the icon for the placeholder based on type */
|
||||
getPlaceholderIcon(): string {
|
||||
|
||||
+8
-2
@@ -35,7 +35,10 @@
|
||||
</div>
|
||||
<div class="epg-timeline">
|
||||
<span class="epg-time">{{
|
||||
formatTime(program.start, program.startTimestamp)
|
||||
(program.startTimestamp
|
||||
? program.startTimestamp * 1000
|
||||
: program.start
|
||||
) | date: 'HH:mm'
|
||||
}}</span>
|
||||
<div class="epg-progress-track">
|
||||
<div
|
||||
@@ -46,7 +49,10 @@
|
||||
></div>
|
||||
</div>
|
||||
<span class="epg-time">{{
|
||||
formatTime(program.stop, program.stopTimestamp)
|
||||
(program.stopTimestamp
|
||||
? program.stopTimestamp * 1000
|
||||
: program.stop
|
||||
) | date: 'HH:mm'
|
||||
}}</span>
|
||||
</div>
|
||||
} @else if (!isRadio()) {
|
||||
|
||||
+4
-10
@@ -1,3 +1,4 @@
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
@@ -58,18 +59,11 @@ describe('ChannelListItemComponent', () => {
|
||||
fixture.nativeElement.querySelectorAll('.epg-time'),
|
||||
(element: Element) => element.textContent?.trim() ?? ''
|
||||
);
|
||||
const datePipe = new DatePipe('en-US');
|
||||
|
||||
expect(times).toEqual([
|
||||
new Date(startTimestamp * 1000).toLocaleTimeString('en-US', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
}),
|
||||
new Date(stopTimestamp * 1000).toLocaleTimeString('en-US', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
}),
|
||||
datePipe.transform(startTimestamp * 1000, 'HH:mm') ?? '',
|
||||
datePipe.transform(stopTimestamp * 1000, 'HH:mm') ?? '',
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
+2
-25
@@ -1,5 +1,5 @@
|
||||
import { DragDropModule } from '@angular/cdk/drag-drop';
|
||||
import { NgStyle } from '@angular/common';
|
||||
import { DatePipe, NgStyle } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
@@ -23,6 +23,7 @@ import { EpgProgram } from 'shared-interfaces';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
templateUrl: './channel-list-item.component.html',
|
||||
imports: [
|
||||
DatePipe,
|
||||
DragDropModule,
|
||||
MatIcon,
|
||||
MatIconButton,
|
||||
@@ -64,30 +65,6 @@ export class ChannelListItemComponent {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats time for display (HH:mm)
|
||||
*/
|
||||
formatTime(
|
||||
dateString: string | number,
|
||||
timestampSeconds?: number | null
|
||||
): string {
|
||||
const unixTimestamp = Number(timestampSeconds);
|
||||
const date =
|
||||
Number.isFinite(unixTimestamp) && unixTimestamp > 0
|
||||
? new Date(unixTimestamp * 1000)
|
||||
: new Date(dateString);
|
||||
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return date.toLocaleTimeString('en-US', {
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the dialog with details about the current EPG program
|
||||
* @param program EPG program to show details for
|
||||
|
||||
+18
-3
@@ -3,11 +3,26 @@
|
||||
<header class="program-header">
|
||||
<div class="time-block">
|
||||
<div class="time-display">
|
||||
<span class="time-start">{{ formatTime(epgProgram.start) }}</span>
|
||||
<span class="time-start">{{
|
||||
(epgProgram.startTimestamp
|
||||
? epgProgram.startTimestamp * 1000
|
||||
: epgProgram.start
|
||||
) | date: 'HH:mm'
|
||||
}}</span>
|
||||
<span class="time-separator">—</span>
|
||||
<span class="time-end">{{ formatTime(epgProgram.stop) }}</span>
|
||||
<span class="time-end">{{
|
||||
(epgProgram.stopTimestamp
|
||||
? epgProgram.stopTimestamp * 1000
|
||||
: epgProgram.stop
|
||||
) | date: 'HH:mm'
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="date-display">{{ formatDate(epgProgram.start) }}</div>
|
||||
<div class="date-display">{{
|
||||
(epgProgram.startTimestamp
|
||||
? epgProgram.startTimestamp * 1000
|
||||
: epgProgram.start
|
||||
) | date: 'EEEE, MMMM d' : undefined : currentLocale()
|
||||
}}</div>
|
||||
</div>
|
||||
@if (channelName) {
|
||||
<div class="channel-badge">
|
||||
|
||||
+19
-22
@@ -1,8 +1,12 @@
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { Component, computed, inject } from '@angular/core';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { format, differenceInMinutes } from 'date-fns';
|
||||
import { normalizeDateLocale } from '@iptvnator/pipes';
|
||||
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
|
||||
import { differenceInMinutes } from 'date-fns';
|
||||
import { startWith } from 'rxjs';
|
||||
import { EpgProgram } from 'shared-interfaces';
|
||||
|
||||
type EpgItemDialogData = EpgProgram & {
|
||||
@@ -15,14 +19,25 @@ type EpgItemDialogData = EpgProgram & {
|
||||
selector: 'app-epg-item-description',
|
||||
templateUrl: './epg-item-description.component.html',
|
||||
styleUrls: ['./epg-item-description.component.scss'],
|
||||
imports: [MatButtonModule, MatDialogModule, TranslatePipe],
|
||||
imports: [DatePipe, MatButtonModule, MatDialogModule, TranslatePipe],
|
||||
})
|
||||
export class EpgItemDescriptionComponent {
|
||||
dialogData = inject<EpgItemDialogData>(MAT_DIALOG_DATA);
|
||||
private readonly translate = inject(TranslateService);
|
||||
private readonly languageTick = toSignal(
|
||||
this.translate.onLangChange.pipe(startWith(null)),
|
||||
{ initialValue: null }
|
||||
);
|
||||
|
||||
epgProgram: EpgProgram;
|
||||
channelName: string | null = null;
|
||||
duration: string | null = null;
|
||||
readonly currentLocale = computed(() => {
|
||||
this.languageTick();
|
||||
return normalizeDateLocale(
|
||||
this.translate.currentLang || this.translate.defaultLang
|
||||
);
|
||||
});
|
||||
|
||||
constructor() {
|
||||
this.epgProgram = this.dialogData;
|
||||
@@ -34,24 +49,6 @@ export class EpgItemDescriptionComponent {
|
||||
this.duration = this.calculateDuration();
|
||||
}
|
||||
|
||||
formatTime(dateStr: string): string {
|
||||
if (!dateStr) return '';
|
||||
try {
|
||||
return format(new Date(dateStr), 'HH:mm');
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
formatDate(dateStr: string): string {
|
||||
if (!dateStr) return '';
|
||||
try {
|
||||
return format(new Date(dateStr), 'EEEE, MMMM d');
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
private calculateDuration(): string | null {
|
||||
if (!this.epgProgram.start || !this.epgProgram.stop) return null;
|
||||
try {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<span class="time"
|
||||
>{{ item.start | momentDate: 'HH:mm' }}
|
||||
>{{ (item.startTimestamp ? item.startTimestamp * 1000 : item.start) | date: 'HH:mm' }}
|
||||
-
|
||||
{{ item.stop | momentDate: 'HH:mm' }}</span
|
||||
{{ (item.stopTimestamp ? item.stopTimestamp * 1000 : item.stop) | date: 'HH:mm' }}</span
|
||||
>
|
||||
@if (isLive) {
|
||||
<span class="live badge" [matTooltip]="'EPG.LIVE_NOW' | translate"
|
||||
|
||||
@@ -3,56 +3,23 @@ import { MatDialog, MatDialogModule } from '@angular/material/dialog';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatListModule } from '@angular/material/list';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { MomentDatePipe } from '@iptvnator/pipes';
|
||||
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||
import moment from 'moment';
|
||||
import { MockModule, MockPipe } from 'ng-mocks';
|
||||
import { MockModule } from 'ng-mocks';
|
||||
import { EpgProgram } from 'shared-interfaces';
|
||||
import { EpgItemDescriptionComponent } from '../epg-item-description/epg-item-description.component';
|
||||
import { EpgListItemComponent } from './epg-list-item.component';
|
||||
|
||||
const EPG_PROGRAM_ITEM = {
|
||||
start: moment(Date.now()).format('YYYYMMDD'),
|
||||
stop: moment(Date.now()).format('YYYYMMDD'),
|
||||
const EPG_PROGRAM_ITEM: EpgProgram = {
|
||||
start: '2026-04-05T11:30:00.000Z',
|
||||
stop: '2026-04-05T12:30:00.000Z',
|
||||
channel: '12345',
|
||||
title: [{ lang: 'en', value: 'NOW on PBS' }],
|
||||
desc: [
|
||||
{
|
||||
lang: 'en',
|
||||
value: "Jordan's Queen Rania has made job creation a priority to help curb the staggering unemployment rates among youths in the Middle East.",
|
||||
},
|
||||
],
|
||||
date: ['20080711'],
|
||||
category: [
|
||||
{ lang: 'en', value: 'Newsmagazine' },
|
||||
{ lang: 'en', value: 'Interview' },
|
||||
],
|
||||
episodeNum: [
|
||||
{ system: 'dd_progid', value: 'EP01006886.0028' },
|
||||
{ system: 'onscreen', value: '427' },
|
||||
],
|
||||
previouslyShown: [{ start: '20080711000000' }],
|
||||
subtitles: [{ type: 'teletext' }],
|
||||
rating: [
|
||||
{
|
||||
system: 'VCHIP',
|
||||
value: 'TV-G',
|
||||
},
|
||||
],
|
||||
credits: [
|
||||
{
|
||||
role: 'actor',
|
||||
name: 'Peter Bergman',
|
||||
},
|
||||
],
|
||||
icon: [
|
||||
title: 'NOW on PBS',
|
||||
desc: "Jordan's Queen Rania has made job creation a priority to help curb the staggering unemployment rates among youths in the Middle East.",
|
||||
category: 'Newsmagazine',
|
||||
episodeNum: '427',
|
||||
rating: 'TV-G',
|
||||
iconUrl:
|
||||
'http://imageswoapi.whatsonindia.com/WhatsOnTV/images/ProgramImages/xlarge/38B4DE4E9A7132257749051B6C8B4F699DB264F4V.jpg',
|
||||
],
|
||||
audio: [],
|
||||
_attributes: {
|
||||
start: moment(Date.now()).format('YYYYMMDD'),
|
||||
stop: moment(Date.now()).format('YYYYMMDD'),
|
||||
},
|
||||
};
|
||||
|
||||
describe('EpgListItemComponent', () => {
|
||||
@@ -63,8 +30,8 @@ describe('EpgListItemComponent', () => {
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [EpgListItemComponent, MockPipe(MomentDatePipe)],
|
||||
imports: [
|
||||
EpgListItemComponent,
|
||||
MockModule(MatDialogModule),
|
||||
MockModule(MatListModule),
|
||||
MockModule(MatIconModule),
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { Component, inject, Input } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { MomentDatePipe } from '@iptvnator/pipes';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { EpgProgram } from 'shared-interfaces';
|
||||
import { EpgItemDescriptionComponent } from '../epg-item-description/epg-item-description.component';
|
||||
|
||||
@Component({
|
||||
imports: [MomentDatePipe, MatIcon, MatTooltip, TranslatePipe],
|
||||
imports: [DatePipe, MatIcon, MatTooltip, TranslatePipe],
|
||||
selector: 'app-epg-list-item',
|
||||
templateUrl: './epg-list-item.component.html',
|
||||
styleUrls: ['./epg-list-item.component.scss'],
|
||||
|
||||
@@ -14,7 +14,8 @@
|
||||
</button>
|
||||
<span class="selected-date">
|
||||
{{
|
||||
selectedDate() | momentDate: 'MMMM Do, dddd' : 'YYYY-MM-DD'
|
||||
selectedDate()
|
||||
| date: 'MMMM d, EEEE' : undefined : currentLocale()
|
||||
}}</span
|
||||
>
|
||||
<button
|
||||
|
||||
@@ -1,18 +1,21 @@
|
||||
import { registerLocaleData } from '@angular/common';
|
||||
import { Component, input } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import localeDe from '@angular/common/locales/de';
|
||||
import { MockPipe } from 'ng-mocks';
|
||||
import { BehaviorSubject, of } from 'rxjs';
|
||||
import { EpgService } from '@iptvnator/epg/data-access';
|
||||
import { MomentDatePipe } from '@iptvnator/pipes';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
|
||||
import { Channel, EpgProgram } from 'shared-interfaces';
|
||||
import { EpgListItemComponent } from './epg-list-item/epg-list-item.component';
|
||||
import { EpgListComponent } from './epg-list.component';
|
||||
|
||||
let requestAnimationFrameQueue: FrameRequestCallback[] = [];
|
||||
|
||||
registerLocaleData(localeDe, 'de');
|
||||
|
||||
@Component({
|
||||
selector: 'app-epg-list-item',
|
||||
standalone: true,
|
||||
@@ -29,6 +32,11 @@ describe('EpgListComponent', () => {
|
||||
let component: EpgListComponent;
|
||||
let store: { select: jest.Mock; dispatch: jest.Mock };
|
||||
let epgService: { currentEpgPrograms$: BehaviorSubject<EpgProgram[]> };
|
||||
let translateService: {
|
||||
currentLang: string;
|
||||
defaultLang: string;
|
||||
onLangChange: BehaviorSubject<unknown>;
|
||||
};
|
||||
let originalRequestAnimationFrame: typeof window.requestAnimationFrame;
|
||||
|
||||
const fixedNow = new Date('2026-04-05T12:00:00.000Z');
|
||||
@@ -67,45 +75,27 @@ describe('EpgListComponent', () => {
|
||||
epgService = {
|
||||
currentEpgPrograms$: new BehaviorSubject<EpgProgram[]>([]),
|
||||
};
|
||||
translateService = {
|
||||
currentLang: 'en',
|
||||
defaultLang: 'en',
|
||||
onLangChange: new BehaviorSubject<unknown>(null),
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [EpgListComponent, NoopAnimationsModule],
|
||||
providers: [
|
||||
{ provide: Store, useValue: store },
|
||||
{ provide: EpgService, useValue: epgService },
|
||||
{ provide: TranslateService, useValue: translateService },
|
||||
],
|
||||
})
|
||||
.overrideComponent(EpgListComponent, {
|
||||
remove: {
|
||||
imports: [
|
||||
EpgListItemComponent,
|
||||
MomentDatePipe,
|
||||
TranslatePipe,
|
||||
],
|
||||
imports: [EpgListItemComponent, TranslatePipe],
|
||||
},
|
||||
add: {
|
||||
imports: [
|
||||
StubEpgListItemComponent,
|
||||
MockPipe(
|
||||
MomentDatePipe,
|
||||
(
|
||||
value: string | null | undefined,
|
||||
format?: string
|
||||
) => {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const parsed = new Date(value);
|
||||
if (format === 'HH:mm') {
|
||||
return parsed
|
||||
.toISOString()
|
||||
.slice(11, 16);
|
||||
}
|
||||
|
||||
return value.slice(0, 10);
|
||||
}
|
||||
),
|
||||
MockPipe(
|
||||
TranslatePipe,
|
||||
(value: string | null | undefined) => value ?? ''
|
||||
@@ -238,6 +228,21 @@ describe('EpgListComponent', () => {
|
||||
.length
|
||||
).toBe(2);
|
||||
});
|
||||
|
||||
it('updates the selected-day header when the app language changes', () => {
|
||||
fixture.componentRef.setInput('controlledPrograms', buildPrograms());
|
||||
fixture.detectChanges();
|
||||
|
||||
translateService.currentLang = 'de';
|
||||
translateService.onLangChange.next({ lang: 'de' });
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(
|
||||
fixture.nativeElement
|
||||
.querySelector('.selected-date')
|
||||
.textContent.trim()
|
||||
).toContain('Sonntag');
|
||||
});
|
||||
});
|
||||
|
||||
function buildPrograms(overrides?: {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { DatePipe } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
@@ -15,15 +16,16 @@ import { MatIconButton } from '@angular/material/button';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { EpgService } from '@iptvnator/epg/data-access';
|
||||
import { MomentDatePipe } from '@iptvnator/pipes';
|
||||
import { normalizeDateLocale } from '@iptvnator/pipes';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
|
||||
import { EpgActions, selectActive } from 'm3u-state';
|
||||
import moment from 'moment';
|
||||
import { addDays, format, subDays } from 'date-fns';
|
||||
import { startWith } from 'rxjs';
|
||||
import { Channel, EpgChannel, EpgProgram } from 'shared-interfaces';
|
||||
import { EpgListItemComponent } from './epg-list-item/epg-list-item.component';
|
||||
|
||||
const DATE_FORMAT = 'YYYY-MM-DD';
|
||||
const DATE_FORMAT = 'yyyy-MM-dd';
|
||||
|
||||
export interface EpgProgramActivationEvent {
|
||||
program: EpgProgram;
|
||||
@@ -32,11 +34,11 @@ export interface EpgProgramActivationEvent {
|
||||
|
||||
@Component({
|
||||
imports: [
|
||||
DatePipe,
|
||||
EpgListItemComponent,
|
||||
MatIcon,
|
||||
MatIconButton,
|
||||
MatTooltip,
|
||||
MomentDatePipe,
|
||||
TranslatePipe,
|
||||
],
|
||||
selector: 'app-epg-list',
|
||||
@@ -53,6 +55,7 @@ export class EpgListComponent {
|
||||
|
||||
private readonly store = inject(Store);
|
||||
private readonly epgService = inject(EpgService);
|
||||
private readonly translate = inject(TranslateService);
|
||||
|
||||
private readonly activeChannel = toSignal(this.store.select(selectActive), {
|
||||
initialValue: null,
|
||||
@@ -61,11 +64,21 @@ export class EpgListComponent {
|
||||
this.epgService.currentEpgPrograms$,
|
||||
{ initialValue: [] as EpgProgram[] }
|
||||
);
|
||||
private readonly languageTick = toSignal(
|
||||
this.translate.onLangChange.pipe(startWith(null)),
|
||||
{ initialValue: null }
|
||||
);
|
||||
|
||||
readonly programList = viewChild<ElementRef<HTMLElement>>('programList');
|
||||
readonly selectedDate = signal(moment().format(DATE_FORMAT));
|
||||
readonly selectedDate = signal(format(new Date(), DATE_FORMAT));
|
||||
readonly timeNow = signal(new Date().toISOString());
|
||||
readonly currentTimeMs = computed(() => Date.parse(this.timeNow()));
|
||||
readonly currentLocale = computed(() => {
|
||||
this.languageTick();
|
||||
return normalizeDateLocale(
|
||||
this.translate.currentLang || this.translate.defaultLang
|
||||
);
|
||||
});
|
||||
|
||||
readonly isControlled = computed(
|
||||
() =>
|
||||
@@ -103,9 +116,7 @@ export class EpgListComponent {
|
||||
return Math.max(0, Number(value ?? 0) || 0);
|
||||
});
|
||||
readonly timeshiftUntil = computed(() =>
|
||||
moment()
|
||||
.subtract(this.archiveDays(), 'days')
|
||||
.toISOString()
|
||||
subDays(new Date(), this.archiveDays()).toISOString()
|
||||
);
|
||||
readonly archivePlaybackEnabled = computed(
|
||||
() => this.archivePlaybackAvailable() ?? (this.archiveDays() > 0)
|
||||
@@ -114,7 +125,7 @@ export class EpgListComponent {
|
||||
[...this.items()]
|
||||
.filter(
|
||||
(item) =>
|
||||
moment(item.start).format(DATE_FORMAT) ===
|
||||
getProgramDateKey(item.start, item.startTimestamp) ===
|
||||
this.selectedDate()
|
||||
)
|
||||
.sort(
|
||||
@@ -175,10 +186,14 @@ export class EpgListComponent {
|
||||
}
|
||||
|
||||
changeDate(direction: 'next' | 'prev'): void {
|
||||
const selectedDate = new Date(`${this.selectedDate()}T00:00:00`);
|
||||
this.selectedDate.set(
|
||||
moment(this.selectedDate())
|
||||
[direction === 'next' ? 'add' : 'subtract'](1, 'days')
|
||||
.format(DATE_FORMAT)
|
||||
format(
|
||||
direction === 'next'
|
||||
? addDays(selectedDate, 1)
|
||||
: subDays(selectedDate, 1),
|
||||
DATE_FORMAT
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -274,7 +289,7 @@ export class EpgListComponent {
|
||||
}
|
||||
|
||||
private scheduleScrollToCurrentProgram(): void {
|
||||
if (this.selectedDate() !== moment().format(DATE_FORMAT)) {
|
||||
if (this.selectedDate() !== format(new Date(), DATE_FORMAT)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -359,3 +374,16 @@ function getProgramTimeMs(
|
||||
|
||||
return Date.parse(isoValue);
|
||||
}
|
||||
|
||||
function getProgramDateKey(
|
||||
isoValue: string,
|
||||
timestampValue?: number | null
|
||||
): string {
|
||||
const programTimeMs = getProgramTimeMs(isoValue, timestampValue);
|
||||
|
||||
if (!Number.isFinite(programTimeMs)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return format(new Date(programTimeMs), DATE_FORMAT);
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
<mat-icon>chevron_left</mat-icon>
|
||||
</button>
|
||||
<div class="today-date">
|
||||
{{ today() | momentDate: 'MMMM Do, dddd' : 'YYYYMMDD' }}
|
||||
{{ selectedDayDate() | date: 'MMMM d, EEEE' : undefined : currentLocale() }}
|
||||
</div>
|
||||
<button
|
||||
[matTooltip]="'EPG.NEXT_DAY' | translate"
|
||||
@@ -88,8 +88,14 @@
|
||||
<div class="result-info">
|
||||
<span class="result-title">{{ program.title }}</span>
|
||||
<span class="result-meta">
|
||||
<span class="result-time">{{ formatProgramTime(program) }}</span>
|
||||
<span class="result-date">{{ formatProgramDate(program) }}</span>
|
||||
<span class="result-time">
|
||||
{{ (program.startTimestamp ? program.startTimestamp * 1000 : program.start) | date: 'HH:mm' }}
|
||||
-
|
||||
{{ (program.stopTimestamp ? program.stopTimestamp * 1000 : program.stop) | date: 'HH:mm' }}
|
||||
</span>
|
||||
<span class="result-date">
|
||||
{{ (program.startTimestamp ? program.startTimestamp * 1000 : program.start) | date: 'MMM d' : undefined : currentLocale() }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<mat-icon class="result-arrow">arrow_forward</mat-icon>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { OverlayRef } from '@angular/cdk/overlay';
|
||||
|
||||
import {
|
||||
@@ -13,14 +14,15 @@ import {
|
||||
signal,
|
||||
viewChild,
|
||||
} from '@angular/core';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { MomentDatePipe } from '@iptvnator/pipes';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { normalizeDateLocale } from '@iptvnator/pipes';
|
||||
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
|
||||
import { addDays, differenceInMinutes, format, parse, subDays } from 'date-fns';
|
||||
import { Observable, Subscription } from 'rxjs';
|
||||
import { Observable, Subscription, startWith } from 'rxjs';
|
||||
import {
|
||||
Channel,
|
||||
EpgChannel,
|
||||
@@ -54,10 +56,10 @@ interface ProgramSearchResult extends EpgProgram {
|
||||
|
||||
@Component({
|
||||
imports: [
|
||||
DatePipe,
|
||||
MatButtonModule,
|
||||
MatIcon,
|
||||
MatTooltip,
|
||||
MomentDatePipe,
|
||||
TranslatePipe,
|
||||
],
|
||||
selector: 'app-multi-epg-container',
|
||||
@@ -104,6 +106,20 @@ export class MultiEpgContainerComponent
|
||||
readonly isSearchingPrograms = signal(false);
|
||||
readonly highlightedProgramKey = signal<string | null>(null);
|
||||
private searchDebounceTimer: ReturnType<typeof setTimeout> | null = null;
|
||||
private readonly translate = inject(TranslateService);
|
||||
private readonly languageTick = toSignal(
|
||||
this.translate.onLangChange.pipe(startWith(null)),
|
||||
{ initialValue: null }
|
||||
);
|
||||
readonly currentLocale = computed(() => {
|
||||
this.languageTick();
|
||||
return normalizeDateLocale(
|
||||
this.translate.currentLang || this.translate.defaultLang
|
||||
);
|
||||
});
|
||||
readonly selectedDayDate = computed(() =>
|
||||
parse(this.today(), 'yyyyMMdd', new Date())
|
||||
);
|
||||
|
||||
// Computed signal for enriched channels - automatically updates when dependencies change
|
||||
readonly channels = computed(() => this.enrichProgramData());
|
||||
@@ -451,17 +467,6 @@ export class MultiEpgContainerComponent
|
||||
return `${channelId}|${program.start}`;
|
||||
}
|
||||
|
||||
formatProgramTime(program: EpgProgram): string {
|
||||
const start = new Date(program.start);
|
||||
const stop = new Date(program.stop);
|
||||
return `${format(start, 'HH:mm')} - ${format(stop, 'HH:mm')}`;
|
||||
}
|
||||
|
||||
formatProgramDate(program: EpgProgram): string {
|
||||
const start = new Date(program.start);
|
||||
return format(start, 'MMM d');
|
||||
}
|
||||
|
||||
close() {
|
||||
this.overlayRef.detach();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export * from './lib/filter.pipe';
|
||||
export * from './lib/moment-date.pipe';
|
||||
export * from './lib/date-format.util';
|
||||
export * from './lib/safe.pipe';
|
||||
export * from './lib/sort.pipe';
|
||||
@@ -0,0 +1,24 @@
|
||||
import { formatWithIntl, normalizeDateLocale } from './date-format.util';
|
||||
|
||||
describe('date-format.util', () => {
|
||||
it('normalizes app locale aliases to Angular/Intl locale ids', () => {
|
||||
expect(normalizeDateLocale('ary')).toBe('ar-MA');
|
||||
expect(normalizeDateLocale('by')).toBe('be');
|
||||
expect(normalizeDateLocale('zhtw')).toBe('zh-Hant');
|
||||
expect(normalizeDateLocale('de')).toBe('de');
|
||||
expect(normalizeDateLocale()).toBe('en');
|
||||
});
|
||||
|
||||
it('formats dates with cached Intl formatters', () => {
|
||||
expect(
|
||||
formatWithIntl('2026-04-05T12:30:00.000Z', {
|
||||
locale: 'de',
|
||||
timeZone: 'UTC',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
).toContain('12:30');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,52 @@
|
||||
const DEFAULT_DATE_LOCALE = 'en';
|
||||
|
||||
const DATE_LOCALE_ALIASES: Record<string, string> = {
|
||||
ary: 'ar-MA',
|
||||
by: 'be',
|
||||
zhtw: 'zh-Hant',
|
||||
};
|
||||
|
||||
const formatterCache = new Map<string, Intl.DateTimeFormat>();
|
||||
|
||||
export function normalizeDateLocale(locale?: string | null): string {
|
||||
const normalizedLocale = String(locale ?? '').trim();
|
||||
|
||||
if (!normalizedLocale) {
|
||||
return DEFAULT_DATE_LOCALE;
|
||||
}
|
||||
|
||||
return (
|
||||
DATE_LOCALE_ALIASES[normalizedLocale.toLowerCase()] ?? normalizedLocale
|
||||
);
|
||||
}
|
||||
|
||||
export function formatWithIntl(
|
||||
value: Date | number | string,
|
||||
options: Intl.DateTimeFormatOptions & {
|
||||
locale?: string | null;
|
||||
timeZone?: string;
|
||||
} = {}
|
||||
): string {
|
||||
const date =
|
||||
value instanceof Date
|
||||
? value
|
||||
: typeof value === 'number'
|
||||
? new Date(value)
|
||||
: new Date(value);
|
||||
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return '';
|
||||
}
|
||||
|
||||
const { locale, ...formatOptions } = options;
|
||||
const resolvedLocale = normalizeDateLocale(locale);
|
||||
const cacheKey = JSON.stringify([resolvedLocale, formatOptions]);
|
||||
|
||||
let formatter = formatterCache.get(cacheKey);
|
||||
if (!formatter) {
|
||||
formatter = new Intl.DateTimeFormat(resolvedLocale, formatOptions);
|
||||
formatterCache.set(cacheKey, formatter);
|
||||
}
|
||||
|
||||
return formatter.format(date);
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import { MomentDatePipe } from './moment-date.pipe';
|
||||
|
||||
describe('MomentDatePipe', () => {
|
||||
let pipe: MomentDatePipe;
|
||||
|
||||
beforeEach(() => {
|
||||
pipe = new MomentDatePipe();
|
||||
});
|
||||
|
||||
it('formats ISO strings using the requested output format', () => {
|
||||
expect(pipe.transform('2026-03-28T12:00:00.000Z', 'YYYY-MM-DD')).toBe(
|
||||
'2026-03-28'
|
||||
);
|
||||
});
|
||||
|
||||
it('supports explicit parse formats for non-ISO values', () => {
|
||||
expect(
|
||||
pipe.transform('28/03/2026', 'YYYY-MM-DD', 'DD/MM/YYYY')
|
||||
).toBe('2026-03-28');
|
||||
});
|
||||
});
|
||||
@@ -1,25 +0,0 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
import moment from 'moment';
|
||||
|
||||
/**
|
||||
* Moment.js based pipe to parse and return the provided date based on given params.
|
||||
* Auto-detects ISO 8601 format and converts to user's local timezone.
|
||||
* Optionally accepts a format string for non-ISO date strings.
|
||||
*/
|
||||
@Pipe({
|
||||
name: 'momentDate',
|
||||
standalone: true,
|
||||
})
|
||||
export class MomentDatePipe implements PipeTransform {
|
||||
transform(
|
||||
value: string,
|
||||
formatToReturn = 'MMMM Do, dddd',
|
||||
formatToParse?: string
|
||||
): string {
|
||||
// If formatToParse is provided, use it; otherwise auto-detect (works for ISO 8601)
|
||||
const parsed = formatToParse
|
||||
? moment(value, formatToParse)
|
||||
: moment(value);
|
||||
return parsed.format(formatToReturn);
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import { MatIcon } from '@angular/material/icon';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { formatWithIntl } from '@iptvnator/pipes';
|
||||
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
|
||||
import { startWith } from 'rxjs';
|
||||
import {
|
||||
@@ -213,16 +214,12 @@ export class GlobalFavoritesWidgetComponent implements OnInit {
|
||||
return this.translate.instant('WORKSPACE.DASHBOARD.RECENTLY');
|
||||
}
|
||||
|
||||
return date.toLocaleString(
|
||||
this.translate.currentLang ||
|
||||
this.translate.defaultLang ||
|
||||
undefined,
|
||||
{
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
}
|
||||
);
|
||||
return formatWithIntl(date, {
|
||||
locale: this.translate.currentLang || this.translate.defaultLang,
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import { MatIcon } from '@angular/material/icon';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { formatWithIntl } from '@iptvnator/pipes';
|
||||
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
|
||||
import { startWith } from 'rxjs';
|
||||
import {
|
||||
@@ -212,16 +213,12 @@ export class RecentlyWatchedWidgetComponent implements OnInit {
|
||||
return this.translate.instant('WORKSPACE.DASHBOARD.RECENTLY');
|
||||
}
|
||||
|
||||
return date.toLocaleString(
|
||||
this.translate.currentLang ||
|
||||
this.translate.defaultLang ||
|
||||
undefined,
|
||||
{
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
}
|
||||
);
|
||||
return formatWithIntl(date, {
|
||||
locale: this.translate.currentLang || this.translate.defaultLang,
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user