mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 19:46:16 -08:00
* feat: add strip country prefix setting
* feat: scope country-prefix stripping to live content and cover missing surfaces
- narrow the heuristic: pipes always strip, dash/colon separators only
when the prefix is a short uppercase tag ("UK - BBC One" strips,
"Sky - Sports F1" and "Mission: Impossible - Fallout" stay intact)
- fall back to the original name when stripping would leave nothing
- scope stripping to live content only: grid type (live/itv/radio),
playback isLive, external sessions without contentInfo, dashboard
cards with contentType 'live'
- cover previously missed surfaces: M3U player EPG timeline header,
M3U inline player title, radio player, dashboard live rails
- replace hardcoded settings strings with translate keys and add
SETTINGS.STRIP_COUNTRY_PREFIX(_DESCRIPTION) to all 18 locales
- add unit specs for the utility plus regression specs for
channel-list-item and external-playback-dock
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* test: cover strip-country-prefix call sites for codecov
- dashboard-rail: new spec for cardTitle live/movie/series scoping
- grid-list: strip enabled/disabled, VOD passthrough, 'No name' fallback
- portal-inline-player: live strip vs VOD passthrough
- unified-live-tab: timeline channel name strip + M3U name precedence
- video-player: timeline/radio/inline titles with the setting on and off
- settings-store: default false + persisted true round-trip
- settings-form.utils: new spec for form default and ?? false fallback
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: 4gray <serega05@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
124 lines
3.7 KiB
TypeScript
124 lines
3.7 KiB
TypeScript
import {
|
|
ChangeDetectionStrategy,
|
|
Component,
|
|
computed,
|
|
effect,
|
|
inject,
|
|
input,
|
|
output,
|
|
signal,
|
|
} from '@angular/core';
|
|
import { MatButtonModule } from '@angular/material/button';
|
|
import { MatIcon } from '@angular/material/icon';
|
|
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
|
import { TranslatePipe } from '@ngx-translate/core';
|
|
import { ExternalPlayerSession } from '@iptvnator/shared/interfaces';
|
|
import { SettingsStore } from '@iptvnator/services';
|
|
import { applyChannelNameStrip } from '@iptvnator/shared/m3u-utils';
|
|
|
|
@Component({
|
|
selector: 'app-external-playback-dock',
|
|
imports: [
|
|
MatButtonModule,
|
|
MatIcon,
|
|
MatProgressSpinnerModule,
|
|
TranslatePipe,
|
|
],
|
|
templateUrl: './external-playback-dock.component.html',
|
|
styleUrl: './external-playback-dock.component.scss',
|
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
})
|
|
export class ExternalPlaybackDockComponent {
|
|
private readonly settingsStore = inject(SettingsStore);
|
|
|
|
readonly session = input.required<ExternalPlayerSession>();
|
|
readonly compact = input(false);
|
|
|
|
readonly closeClicked = output<void>();
|
|
readonly artworkClicked = output<void>();
|
|
private readonly artworkFailed = signal(false);
|
|
|
|
/**
|
|
* Sessions with contentInfo are tracked VOD/episode playback — their
|
|
* titles are movie/episode names, so the channel-prefix strip is
|
|
* applied only to sessions without it (live channels).
|
|
*/
|
|
readonly displayTitle = computed(() =>
|
|
applyChannelNameStrip(
|
|
this.session().title,
|
|
!this.session().contentInfo &&
|
|
this.settingsStore.stripCountryPrefix?.()
|
|
)
|
|
);
|
|
|
|
readonly playerLabel = computed(() => this.session().player.toUpperCase());
|
|
readonly artworkUrl = computed(
|
|
() => this.session().thumbnail?.trim() ?? ''
|
|
);
|
|
readonly statusLabel = computed(() => {
|
|
const session = this.session();
|
|
|
|
switch (session.status) {
|
|
case 'launching':
|
|
return 'Launching…';
|
|
case 'opened':
|
|
case 'playing':
|
|
return 'Opened';
|
|
case 'error':
|
|
return session.error || 'Playback failed';
|
|
default:
|
|
return 'Closed';
|
|
}
|
|
});
|
|
|
|
readonly statusIcon = computed(() => {
|
|
const status = this.session().status;
|
|
if (status === 'error') return 'error_outline';
|
|
return 'open_in_new';
|
|
});
|
|
|
|
readonly showSpinner = computed(
|
|
() => this.session().status === 'launching'
|
|
);
|
|
readonly showStatusIcon = computed(
|
|
() => this.session().status !== 'launching'
|
|
);
|
|
readonly showArtwork = computed(
|
|
() => !!this.artworkUrl() && !this.artworkFailed()
|
|
);
|
|
readonly artworkPlaceholderIcon = computed(() => {
|
|
const contentType = this.session().contentInfo?.contentType;
|
|
|
|
switch (contentType) {
|
|
case 'vod':
|
|
return 'movie';
|
|
case 'episode':
|
|
return 'video_library';
|
|
default:
|
|
return 'live_tv';
|
|
}
|
|
});
|
|
readonly artworkInteractive = computed(
|
|
() => !!this.session().contentInfo?.playlistId
|
|
);
|
|
readonly showCloseButton = computed(
|
|
() => this.session().canClose && this.session().status !== 'error'
|
|
);
|
|
|
|
constructor() {
|
|
effect(() => {
|
|
this.artworkUrl();
|
|
this.artworkFailed.set(false);
|
|
});
|
|
}
|
|
|
|
onArtworkError(): void {
|
|
this.artworkFailed.set(true);
|
|
}
|
|
|
|
onArtworkClick(): void {
|
|
if (!this.artworkInteractive()) return;
|
|
this.artworkClicked.emit();
|
|
}
|
|
}
|