Files
iptvnator/libs/ui/components/src/lib/external-playback-dock/external-playback-dock.component.ts
T
d61fd5db19 feat: add strip country prefix setting (#1162)
* 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>
2026-07-18 15:43:37 +02:00

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();
}
}