mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
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>
This commit is contained in:
48 files changed
+773
-22
No files matched your search
@@ -0,0 +1,36 @@
|
||||
import { FormBuilder } from '@angular/forms';
|
||||
import { Settings } from '@iptvnator/shared/interfaces';
|
||||
import {
|
||||
createSettingsForm,
|
||||
createSettingsFromFormValue,
|
||||
} from './settings-form.utils';
|
||||
|
||||
describe('settings form utils — strip country prefix', () => {
|
||||
const formBuilder = new FormBuilder();
|
||||
|
||||
it('defaults the form control to false', () => {
|
||||
const form = createSettingsForm(formBuilder, true);
|
||||
|
||||
expect(form.getRawValue().stripCountryPrefix).toBe(false);
|
||||
});
|
||||
|
||||
it('carries an enabled toggle into the settings object', () => {
|
||||
const form = createSettingsForm(formBuilder, true);
|
||||
form.patchValue({ stripCountryPrefix: true });
|
||||
|
||||
const settings = createSettingsFromFormValue(form, {} as Settings);
|
||||
|
||||
expect(settings.stripCountryPrefix).toBe(true);
|
||||
});
|
||||
|
||||
it('falls back to false when the form value is missing', () => {
|
||||
const form = createSettingsForm(formBuilder, true);
|
||||
form.patchValue({
|
||||
stripCountryPrefix: null as unknown as boolean,
|
||||
});
|
||||
|
||||
const settings = createSettingsFromFormValue(form, {} as Settings);
|
||||
|
||||
expect(settings.stripCountryPrefix).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -55,6 +55,7 @@ export function createSettingsForm(
|
||||
}),
|
||||
startupBehavior: StartupBehavior.FirstView,
|
||||
showExternalPlaybackBar: true,
|
||||
stripCountryPrefix: false,
|
||||
theme: Theme.SystemTheme,
|
||||
mpvPlayerPath: '',
|
||||
mpvPlayerArguments: '',
|
||||
@@ -124,6 +125,7 @@ export function createSettingsFromFormValue(
|
||||
dashboardRails: normalizeDashboardRailsSettings(value.dashboardRails),
|
||||
startupBehavior: value.startupBehavior ?? StartupBehavior.FirstView,
|
||||
showExternalPlaybackBar: value.showExternalPlaybackBar ?? true,
|
||||
stripCountryPrefix: value.stripCountryPrefix ?? false,
|
||||
theme: value.theme ?? Theme.SystemTheme,
|
||||
mpvPlayerPath: normalizeExternalPlayerPath(value.mpvPlayerPath),
|
||||
mpvPlayerArguments: normalizeExternalPlayerArguments(
|
||||
|
||||
@@ -156,4 +156,16 @@
|
||||
<mat-checkbox formControlName="showCaptions"></mat-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-item">
|
||||
<div class="setting-item__meta">
|
||||
<h4>{{ 'SETTINGS.STRIP_COUNTRY_PREFIX' | translate }}</h4>
|
||||
<p>
|
||||
{{ 'SETTINGS.STRIP_COUNTRY_PREFIX_DESCRIPTION' | translate }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="setting-item__control setting-item__toggle">
|
||||
<mat-checkbox formControlName="stripCountryPrefix"></mat-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -92,6 +92,7 @@ const DEFAULT_SETTINGS = {
|
||||
showDashboard: true,
|
||||
startupBehavior: StartupBehavior.FirstView,
|
||||
showExternalPlaybackBar: true,
|
||||
stripCountryPrefix: false,
|
||||
theme: Theme.SystemTheme,
|
||||
mpvPlayerPath: '',
|
||||
mpvPlayerArguments: '',
|
||||
|
||||
@@ -317,6 +317,8 @@
|
||||
"SHOW_CAPTIONS_DESCRIPTION": "عرض أو إخفاء الترجمات افتراضيًا",
|
||||
"SHOW_EXTERNAL_PLAYBACK_BAR": "عرض شريط التشغيل الحالي",
|
||||
"SHOW_EXTERNAL_PLAYBACK_BAR_DESCRIPTION": "إبقاء شريط حالة المشغل الخارجي مرئيًا في أسفل مساحة العمل على سطح المكتب.",
|
||||
"STRIP_COUNTRY_PREFIX": "إزالة بادئات البلد من أسماء القنوات",
|
||||
"STRIP_COUNTRY_PREFIX_DESCRIPTION": "يزيل بادئات البلد/المجموعة مثل \"US | \" أو \"UK - \" من أسماء قنوات البث المباشر في جميع أنحاء التطبيق.",
|
||||
"VERSION_DESCRIPTION": "الإصدار الحالي للتطبيق",
|
||||
"ABOUT_SUBTITLE": "الإصدار وروابط المشروع وطرق دعم IPTVnator.",
|
||||
"EMBEDDED_MPV_NOTICE_TITLE": "بيئة تشغيل MPV المضمّنة",
|
||||
|
||||
@@ -317,6 +317,8 @@
|
||||
"SHOW_CAPTIONS_DESCRIPTION": "وري ولا خفي الترجمة افتراضياً",
|
||||
"SHOW_EXTERNAL_PLAYBACK_BAR": "وري شريط التشغيل الحالي",
|
||||
"SHOW_EXTERNAL_PLAYBACK_BAR_DESCRIPTION": "خلي شريط حالة المشغل الخارجي ظاهر في الأسفل ديال مساحة العمل على سطح المكتب.",
|
||||
"STRIP_COUNTRY_PREFIX": "حيد بادئات البلد من أسماء القنوات",
|
||||
"STRIP_COUNTRY_PREFIX_DESCRIPTION": "كيحيد بادئات البلد/المجموعة بحال \"US | \" ولا \"UK - \" من أسماء قنوات البث المباشر فكامل التطبيق.",
|
||||
"VERSION_DESCRIPTION": "الإصدار الحالي ديال التطبيق",
|
||||
"ABOUT_SUBTITLE": "الإصدار، روابط المشروع، وطرق دعم IPTVnator.",
|
||||
"EMBEDDED_MPV_NOTICE_TITLE": "وقت تشغيل MPV المدمج",
|
||||
|
||||
@@ -317,6 +317,8 @@
|
||||
"SHOW_CAPTIONS_DESCRIPTION": "Паказаць або схаваць субтытры па змаўчанні.",
|
||||
"SHOW_EXTERNAL_PLAYBACK_BAR": "Паказаць ніжнюю панэль прайгравання",
|
||||
"SHOW_EXTERNAL_PLAYBACK_BAR_DESCRIPTION": "Пакідаць панэль стану знешняга прайгравальніка бачнай унізе працоўнай прасторы на дэсктопе.",
|
||||
"STRIP_COUNTRY_PREFIX": "Выдаляць прэфіксы краін з назваў каналаў",
|
||||
"STRIP_COUNTRY_PREFIX_DESCRIPTION": "Выдаляе прэфіксы краін/груп, такія як \"US | \" або \"UK - \", з назваў каналаў прамых трансляцый ва ўсёй праграме.",
|
||||
"VERSION_DESCRIPTION": "Бягучая версія праграмы",
|
||||
"ABOUT_SUBTITLE": "Версія, спасылкі праекта і спосабы падтрымаць IPTVnator.",
|
||||
"EMBEDDED_MPV_NOTICE_TITLE": "Асяроддзе ўбудаванага MPV",
|
||||
|
||||
@@ -317,6 +317,8 @@
|
||||
"SHOW_CAPTIONS_DESCRIPTION": "Untertitel standardmäßig ein- oder ausblenden",
|
||||
"SHOW_EXTERNAL_PLAYBACK_BAR": "Now-Playing-Fußleiste anzeigen",
|
||||
"SHOW_EXTERNAL_PLAYBACK_BAR_DESCRIPTION": "Zeigt die Statusleiste für externe Player unten im Arbeitsbereich auf dem Desktop an.",
|
||||
"STRIP_COUNTRY_PREFIX": "Länderpräfixe aus Sendernamen entfernen",
|
||||
"STRIP_COUNTRY_PREFIX_DESCRIPTION": "Entfernt Länder-/Gruppenpräfixe wie \"US | \" oder \"UK - \" aus den Namen von Live-Sendern in der gesamten App.",
|
||||
"VERSION_DESCRIPTION": "Aktuelle Version der Anwendung",
|
||||
"ABOUT_SUBTITLE": "Version, Projekt-Links und Möglichkeiten, IPTVnator zu unterstützen.",
|
||||
"EMBEDDED_MPV_NOTICE_TITLE": "Eingebettete MPV-Laufzeit",
|
||||
|
||||
@@ -317,6 +317,8 @@
|
||||
"SHOW_CAPTIONS_DESCRIPTION": "Εμφάνιση ή απόκρυψη υποτίτλων από προεπιλογή",
|
||||
"SHOW_EXTERNAL_PLAYBACK_BAR": "Εμφάνιση υποσέλιδου αναπαραγωγής",
|
||||
"SHOW_EXTERNAL_PLAYBACK_BAR_DESCRIPTION": "Διατηρήστε ορατή τη γραμμή κατάστασης του εξωτερικού προγράμματος αναπαραγωγής στο κάτω μέρος του χώρου εργασίας στον υπολογιστή.",
|
||||
"STRIP_COUNTRY_PREFIX": "Αφαίρεση προθεμάτων χώρας από τα ονόματα καναλιών",
|
||||
"STRIP_COUNTRY_PREFIX_DESCRIPTION": "Αφαιρεί προθέματα χώρας/ομάδας όπως \"US | \" ή \"UK - \" από τα ονόματα καναλιών ζωντανής ροής σε όλη την εφαρμογή.",
|
||||
"VERSION_DESCRIPTION": "Τρέχουσα έκδοση της εφαρμογής",
|
||||
"ABOUT_SUBTITLE": "Έκδοση, σύνδεσμοι έργου και τρόποι υποστήριξης του IPTVnator.",
|
||||
"EMBEDDED_MPV_NOTICE_TITLE": "Ενσωματωμένος χρόνος εκτέλεσης MPV",
|
||||
|
||||
@@ -317,6 +317,8 @@
|
||||
"SHOW_CAPTIONS_DESCRIPTION": "Show or hide subtitles by default",
|
||||
"SHOW_EXTERNAL_PLAYBACK_BAR": "Show now playing footer",
|
||||
"SHOW_EXTERNAL_PLAYBACK_BAR_DESCRIPTION": "Keep the external-player status bar visible at the bottom of the workspace on desktop.",
|
||||
"STRIP_COUNTRY_PREFIX": "Strip country prefixes from channel names",
|
||||
"STRIP_COUNTRY_PREFIX_DESCRIPTION": "Removes country/group prefixes like \"US | \" or \"UK - \" from live channel names throughout the app.",
|
||||
"VERSION_DESCRIPTION": "Current version of the application",
|
||||
"ABOUT_SUBTITLE": "Version, project links and ways to support IPTVnator.",
|
||||
"EMBEDDED_MPV_NOTICE_TITLE": "Embedded MPV runtime",
|
||||
|
||||
@@ -317,6 +317,8 @@
|
||||
"SHOW_CAPTIONS_DESCRIPTION": "Mostrar u ocultar subtítulos por defecto",
|
||||
"SHOW_EXTERNAL_PLAYBACK_BAR": "Mostrar pie de reproducción actual",
|
||||
"SHOW_EXTERNAL_PLAYBACK_BAR_DESCRIPTION": "Mantén visible la barra de estado del reproductor externo en la parte inferior del espacio de trabajo en escritorio.",
|
||||
"STRIP_COUNTRY_PREFIX": "Quitar prefijos de país de los nombres de los canales",
|
||||
"STRIP_COUNTRY_PREFIX_DESCRIPTION": "Elimina los prefijos de país o grupo, como \"US | \" o \"UK - \", de los nombres de los canales en vivo en toda la app.",
|
||||
"VERSION_DESCRIPTION": "Versión actual de la aplicación.",
|
||||
"ABOUT_SUBTITLE": "Versión, enlaces del proyecto y formas de apoyar a IPTVnator.",
|
||||
"EMBEDDED_MPV_NOTICE_TITLE": "Runtime de MPV integrado",
|
||||
|
||||
@@ -317,6 +317,8 @@
|
||||
"SHOW_CAPTIONS_DESCRIPTION": "Afficher ou masquer les sous-titres par défaut",
|
||||
"SHOW_EXTERNAL_PLAYBACK_BAR": "Afficher le pied de page de lecture en cours",
|
||||
"SHOW_EXTERNAL_PLAYBACK_BAR_DESCRIPTION": "Garde la barre d'état du lecteur externe visible en bas de l'espace de travail sur ordinateur.",
|
||||
"STRIP_COUNTRY_PREFIX": "Retirer les préfixes de pays des noms de chaînes",
|
||||
"STRIP_COUNTRY_PREFIX_DESCRIPTION": "Supprime les préfixes de pays ou de groupe comme \"US | \" ou \"UK - \" des noms des chaînes en direct dans toute l'application.",
|
||||
"VERSION_DESCRIPTION": "Version actuelle de l'application",
|
||||
"ABOUT_SUBTITLE": "Version, liens du projet et façons de soutenir IPTVnator.",
|
||||
"EMBEDDED_MPV_NOTICE_TITLE": "Runtime MPV intégré",
|
||||
|
||||
@@ -317,6 +317,8 @@
|
||||
"SHOW_CAPTIONS_DESCRIPTION": "Visualizza/nascondi i sottotitoli per impostazione predefinita",
|
||||
"SHOW_EXTERNAL_PLAYBACK_BAR": "Mostra footer In riproduzione",
|
||||
"SHOW_EXTERNAL_PLAYBACK_BAR_DESCRIPTION": "Mantieni visibile la barra di stato del lettore esterno in fondo all'area di lavoro su desktop.",
|
||||
"STRIP_COUNTRY_PREFIX": "Rimuovi i prefissi del paese dai nomi dei canali",
|
||||
"STRIP_COUNTRY_PREFIX_DESCRIPTION": "Rimuove i prefissi di paese/gruppo come \"US | \" o \"UK - \" dai nomi dei canali live in tutta l'app.",
|
||||
"VERSION_DESCRIPTION": "Versione attuale dell'applicazione",
|
||||
"ABOUT_SUBTITLE": "Versione, link al progetto e modi per supportare IPTVnator.",
|
||||
"EMBEDDED_MPV_NOTICE_TITLE": "Runtime MPV integrato",
|
||||
|
||||
@@ -317,6 +317,8 @@
|
||||
"SHOW_CAPTIONS_DESCRIPTION": "デフォルトで字幕を表示/非表示",
|
||||
"SHOW_EXTERNAL_PLAYBACK_BAR": "再生中フッターを表示",
|
||||
"SHOW_EXTERNAL_PLAYBACK_BAR_DESCRIPTION": "デスクトップでワークスペース下部に外部プレーヤーのステータスバーを表示し続けます。",
|
||||
"STRIP_COUNTRY_PREFIX": "チャンネル名から国名プレフィックスを削除",
|
||||
"STRIP_COUNTRY_PREFIX_DESCRIPTION": "アプリ全体で、ライブチャンネル名から \"US | \" や \"UK - \" のような国/グループのプレフィックスを削除します。",
|
||||
"VERSION_DESCRIPTION": "アプリケーションの現在のバージョン",
|
||||
"ABOUT_SUBTITLE": "バージョン、プロジェクトのリンク、IPTVnatorをサポートする方法。",
|
||||
"EMBEDDED_MPV_NOTICE_TITLE": "組み込みMPVランタイム",
|
||||
|
||||
@@ -317,6 +317,8 @@
|
||||
"SHOW_CAPTIONS_DESCRIPTION": "기본적으로 자막 표시 또는 숨기기",
|
||||
"SHOW_EXTERNAL_PLAYBACK_BAR": "지금 재생 중 표시줄 표시",
|
||||
"SHOW_EXTERNAL_PLAYBACK_BAR_DESCRIPTION": "데스크톱의 작업 공간 하단에 외부 플레이어 상태 표시줄을 계속 표시합니다.",
|
||||
"STRIP_COUNTRY_PREFIX": "채널 이름에서 국가 접두사 제거",
|
||||
"STRIP_COUNTRY_PREFIX_DESCRIPTION": "라이브 채널 이름에서 \"US | \" 또는 \"UK - \"와 같은 국가/그룹 접두사를 앱 전체에서 제거합니다.",
|
||||
"VERSION_DESCRIPTION": "애플리케이션의 현재 버전",
|
||||
"ABOUT_SUBTITLE": "버전, 프로젝트 링크 및 IPTVnator를 후원하는 방법.",
|
||||
"EMBEDDED_MPV_NOTICE_TITLE": "내장 MPV 런타임",
|
||||
|
||||
@@ -317,6 +317,8 @@
|
||||
"SHOW_CAPTIONS_DESCRIPTION": "Standaard ondertiteling weergeven of verbergen",
|
||||
"SHOW_EXTERNAL_PLAYBACK_BAR": "Voettekst 'nu spelend' tonen",
|
||||
"SHOW_EXTERNAL_PLAYBACK_BAR_DESCRIPTION": "Houd de statusbalk van de externe speler zichtbaar onderaan de werkruimte op desktop.",
|
||||
"STRIP_COUNTRY_PREFIX": "Landprefixen uit kanaalnamen verwijderen",
|
||||
"STRIP_COUNTRY_PREFIX_DESCRIPTION": "Verwijdert land-/groepsprefixen zoals \"US | \" of \"UK - \" uit de namen van live-kanalen, overal in de app.",
|
||||
"VERSION_DESCRIPTION": "Huidige versie van de applicatie",
|
||||
"ABOUT_SUBTITLE": "Versie, projectlinks en manieren om IPTVnator te ondersteunen.",
|
||||
"EMBEDDED_MPV_NOTICE_TITLE": "Ingebedde MPV-runtime",
|
||||
|
||||
@@ -317,6 +317,8 @@
|
||||
"SHOW_CAPTIONS_DESCRIPTION": "Domyślnie pokaż lub ukryj napisy",
|
||||
"SHOW_EXTERNAL_PLAYBACK_BAR": "Pokaż stopkę „teraz odtwarzane”",
|
||||
"SHOW_EXTERNAL_PLAYBACK_BAR_DESCRIPTION": "Pozostaw pasek statusu zewnętrznego odtwarzacza widoczny u dołu obszaru roboczego na komputerze.",
|
||||
"STRIP_COUNTRY_PREFIX": "Usuń prefiksy krajów z nazw kanałów",
|
||||
"STRIP_COUNTRY_PREFIX_DESCRIPTION": "Usuwa prefiksy kraju/grupy, takie jak \"US | \" lub \"UK - \", z nazw kanałów na żywo w całej aplikacji.",
|
||||
"VERSION_DESCRIPTION": "Aktualna wersja aplikacji",
|
||||
"ABOUT_SUBTITLE": "Wersja, linki do projektu i sposoby wsparcia IPTVnator.",
|
||||
"EMBEDDED_MPV_NOTICE_TITLE": "Środowisko wbudowanego MPV",
|
||||
|
||||
@@ -317,6 +317,8 @@
|
||||
"SHOW_CAPTIONS_DESCRIPTION": "Mostrar ou ocultar legendas por padrão",
|
||||
"SHOW_EXTERNAL_PLAYBACK_BAR": "Mostrar rodapé de reprodução atual",
|
||||
"SHOW_EXTERNAL_PLAYBACK_BAR_DESCRIPTION": "Mantém a barra de status do reprodutor externo visível na parte inferior do espaço de trabalho no desktop.",
|
||||
"STRIP_COUNTRY_PREFIX": "Remover prefixos de país dos nomes dos canais",
|
||||
"STRIP_COUNTRY_PREFIX_DESCRIPTION": "Remove prefixos de país/grupo como \"US | \" ou \"UK - \" dos nomes dos canais ao vivo em todo o aplicativo.",
|
||||
"VERSION_DESCRIPTION": "Versão atual do aplicativo",
|
||||
"ABOUT_SUBTITLE": "Versão, links do projeto e formas de apoiar o IPTVnator.",
|
||||
"EMBEDDED_MPV_NOTICE_TITLE": "Runtime MPV integrado",
|
||||
|
||||
@@ -317,6 +317,8 @@
|
||||
"SHOW_CAPTIONS_DESCRIPTION": "Показать или скрыть субтитры по умолчанию.",
|
||||
"SHOW_EXTERNAL_PLAYBACK_BAR": "Показывать панель «Сейчас воспроизводится»",
|
||||
"SHOW_EXTERNAL_PLAYBACK_BAR_DESCRIPTION": "Оставлять панель состояния внешнего проигрывателя видимой внизу рабочей области в настольном приложении.",
|
||||
"STRIP_COUNTRY_PREFIX": "Удалять префиксы стран из названий каналов",
|
||||
"STRIP_COUNTRY_PREFIX_DESCRIPTION": "Удаляет префиксы стран/групп, такие как \"US | \" или \"UK - \", из названий каналов прямого эфира во всём приложении.",
|
||||
"VERSION_DESCRIPTION": "Текущая версия приложения",
|
||||
"ABOUT_SUBTITLE": "Версия, ссылки на проект и способы поддержать IPTVnator.",
|
||||
"EMBEDDED_MPV_NOTICE_TITLE": "Среда выполнения встроенного MPV",
|
||||
|
||||
@@ -317,6 +317,8 @@
|
||||
"SHOW_CAPTIONS_DESCRIPTION": "Varsayılan olarak altyazıları göster veya gizle",
|
||||
"SHOW_EXTERNAL_PLAYBACK_BAR": "Şu an oynatılıyor altbilgisini göster",
|
||||
"SHOW_EXTERNAL_PLAYBACK_BAR_DESCRIPTION": "Masaüstünde harici oynatıcı durum çubuğunu çalışma alanının altında görünür tutun.",
|
||||
"STRIP_COUNTRY_PREFIX": "Kanal adlarından ülke ön eklerini kaldır",
|
||||
"STRIP_COUNTRY_PREFIX_DESCRIPTION": "Uygulama genelinde canlı kanal adlarından \"US | \" veya \"UK - \" gibi ülke/grup ön eklerini kaldırır.",
|
||||
"VERSION_DESCRIPTION": "Uygulamanın geçerli sürümü",
|
||||
"ABOUT_SUBTITLE": "Sürüm, proje bağlantıları ve IPTVnator'u desteklemenin yolları.",
|
||||
"EMBEDDED_MPV_NOTICE_TITLE": "Gömülü MPV çalışma zamanı",
|
||||
|
||||
@@ -317,6 +317,8 @@
|
||||
"SHOW_CAPTIONS_DESCRIPTION": "默认显示或隐藏字幕",
|
||||
"SHOW_EXTERNAL_PLAYBACK_BAR": "显示「正在播放」底栏",
|
||||
"SHOW_EXTERNAL_PLAYBACK_BAR_DESCRIPTION": "在桌面端工作区底部保持外部播放器状态栏可见。",
|
||||
"STRIP_COUNTRY_PREFIX": "移除频道名称中的国家前缀",
|
||||
"STRIP_COUNTRY_PREFIX_DESCRIPTION": "从整个应用中的直播频道名称里移除国家/分组前缀,例如 \"US | \" 或 \"UK - \"。",
|
||||
"VERSION_DESCRIPTION": "应用程序的当前版本",
|
||||
"ABOUT_SUBTITLE": "版本、项目链接以及支持 IPTVnator 的方式。",
|
||||
"EMBEDDED_MPV_NOTICE_TITLE": "嵌入式 MPV 运行时",
|
||||
|
||||
@@ -317,6 +317,8 @@
|
||||
"SHOW_CAPTIONS_DESCRIPTION": "預設顯示或隱藏字幕",
|
||||
"SHOW_EXTERNAL_PLAYBACK_BAR": "顯示「正在播放」列",
|
||||
"SHOW_EXTERNAL_PLAYBACK_BAR_DESCRIPTION": "在桌面工作區底部保留外部播放器狀態列可見。",
|
||||
"STRIP_COUNTRY_PREFIX": "從頻道名稱中移除國家前綴",
|
||||
"STRIP_COUNTRY_PREFIX_DESCRIPTION": "移除應用程式中直播頻道名稱裡的國家/群組前綴,例如「US | 」或「UK - 」。",
|
||||
"VERSION_DESCRIPTION": "應用程式目前版本",
|
||||
"ABOUT_SUBTITLE": "版本、專案連結,以及支持 IPTVnator 的方式。",
|
||||
"EMBEDDED_MPV_NOTICE_TITLE": "內嵌 MPV 執行環境",
|
||||
|
||||
@@ -67,16 +67,14 @@
|
||||
<app-audio-player
|
||||
[url]="activeChannel.url"
|
||||
[icon]="activeChannel?.tvg?.logo!"
|
||||
[channelName]="
|
||||
activeChannel.name || activeChannel?.tvg?.name || ''
|
||||
"
|
||||
[channelName]="displayChannelName()"
|
||||
[volume]="volume()"
|
||||
(volumeChange)="onInlineVolumeChange($event)"
|
||||
/>
|
||||
} @else {
|
||||
<app-web-player-view
|
||||
[streamUrl]="embeddedPlayback()?.streamUrl ?? ''"
|
||||
[title]="embeddedPlayback()?.title ?? ''"
|
||||
[title]="inlinePlayerTitle()"
|
||||
[playback]="embeddedPlayback()"
|
||||
[playerOverride]="playerSettings.player ?? null"
|
||||
[volume]="volume()"
|
||||
|
||||
@@ -191,6 +191,7 @@ describe('VideoPlayerComponent', () => {
|
||||
|
||||
const player = signal<VideoPlayer>(VideoPlayer.VideoJs);
|
||||
const showCaptions = signal(false);
|
||||
const stripCountryPrefix = signal(false);
|
||||
const epgViewMode = signal<'timeline' | 'list'>('timeline');
|
||||
const originalElectron = window.electron;
|
||||
|
||||
@@ -314,6 +315,7 @@ describe('VideoPlayerComponent', () => {
|
||||
localStorage.removeItem(LIVE_EPG_PANEL_STATE_STORAGE_KEY);
|
||||
player.set(VideoPlayer.VideoJs);
|
||||
showCaptions.set(false);
|
||||
stripCountryPrefix.set(false);
|
||||
activePlaybackUrl.set(null);
|
||||
channelsLoading.set(false);
|
||||
currentEpgProgram.set(null);
|
||||
@@ -389,6 +391,7 @@ describe('VideoPlayerComponent', () => {
|
||||
useValue: {
|
||||
player,
|
||||
showCaptions,
|
||||
stripCountryPrefix,
|
||||
resolvedEpgViewMode: epgViewMode,
|
||||
},
|
||||
},
|
||||
@@ -456,6 +459,26 @@ describe('VideoPlayerComponent', () => {
|
||||
expect(headerContext.action()).toBeNull();
|
||||
});
|
||||
|
||||
it('strips country prefixes from the timeline and player titles when enabled', () => {
|
||||
stripCountryPrefix.set(true);
|
||||
syncStoreState({ ...sampleChannel, name: 'US | CNN' } as Channel);
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.timelineChannelName()).toBe('CNN');
|
||||
expect(component.displayChannelName()).toBe('CNN');
|
||||
expect(component.inlinePlayerTitle()).toBe('CNN');
|
||||
});
|
||||
|
||||
it('keeps raw channel names while prefix stripping is disabled', () => {
|
||||
syncStoreState({ ...sampleChannel, name: 'US | CNN' } as Channel);
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.timelineChannelName()).toBe('US | CNN');
|
||||
expect(component.displayChannelName()).toBe('US | CNN');
|
||||
});
|
||||
|
||||
it('renders the inline player with the embedded EPG panel', () => {
|
||||
syncStoreState(sampleChannel);
|
||||
player.set(VideoPlayer.VideoJs);
|
||||
|
||||
@@ -22,6 +22,7 @@ import { StorageMap } from '@ngx-pwa/local-storage';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { ResizableDirective } from '@iptvnator/ui/components';
|
||||
import {
|
||||
applyChannelNameStrip,
|
||||
getM3uArchiveDays,
|
||||
isM3uCatchupPlaybackSupported,
|
||||
} from '@iptvnator/shared/m3u-utils';
|
||||
@@ -182,8 +183,26 @@ export class VideoPlayerComponent implements OnInit, OnDestroy {
|
||||
readonly epgArchiveDays = computed(() =>
|
||||
getM3uArchiveDays(this.activeChannel())
|
||||
);
|
||||
readonly timelineChannelName = computed(
|
||||
() => this.activeChannel()?.name ?? ''
|
||||
readonly timelineChannelName = computed(() =>
|
||||
applyChannelNameStrip(
|
||||
this.activeChannel()?.name,
|
||||
this.settingsStore.stripCountryPrefix?.()
|
||||
)
|
||||
);
|
||||
/** Channel name for the radio player header. */
|
||||
readonly displayChannelName = computed(() => {
|
||||
const channel = this.activeChannel();
|
||||
return applyChannelNameStrip(
|
||||
channel?.name || channel?.tvg?.name,
|
||||
this.settingsStore.stripCountryPrefix?.()
|
||||
);
|
||||
});
|
||||
/** Display title for the inline web player header. */
|
||||
readonly inlinePlayerTitle = computed(() =>
|
||||
applyChannelNameStrip(
|
||||
this.embeddedPlayback()?.title,
|
||||
this.settingsStore.stripCountryPrefix?.()
|
||||
)
|
||||
);
|
||||
/** Channel logo from the EPG feed (M3U playlists often lack tvg-logo). */
|
||||
private readonly epgChannelLogo = toSignal(
|
||||
|
||||
+1
@@ -211,6 +211,7 @@
|
||||
[pageSizeOptions]="pageSizeOptions"
|
||||
[showPaginator]="false"
|
||||
[searchTerm]="searchTerm()"
|
||||
[type]="contentType() ?? ''"
|
||||
(pageChange)="onPageChange($event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
+1
@@ -31,6 +31,7 @@ class MockGridListComponent {
|
||||
readonly pageSizeOptions = input<number[]>();
|
||||
readonly showPaginator = input(true);
|
||||
readonly searchTerm = input<string>('');
|
||||
readonly type = input<string>('');
|
||||
readonly itemClicked = output<unknown>();
|
||||
readonly pageChange = output<unknown>();
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { signal } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { MockPipe } from 'ng-mocks';
|
||||
import { SettingsStore } from '@iptvnator/services';
|
||||
import {
|
||||
formatGridRating,
|
||||
GridListComponent,
|
||||
@@ -159,4 +161,78 @@ describe('GridListComponent', () => {
|
||||
'live_tv'
|
||||
);
|
||||
});
|
||||
|
||||
it('renders raw titles while prefix stripping is disabled', () => {
|
||||
fixture.componentRef.setInput('items', [{ name: 'US | CNN' }]);
|
||||
fixture.componentRef.setInput('type', 'live');
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
const title = fixture.debugElement.query(By.css('.title'));
|
||||
expect(title.nativeElement.textContent.trim()).toBe('US | CNN');
|
||||
});
|
||||
|
||||
it('falls back to a placeholder title for items without a name', () => {
|
||||
fixture.componentRef.setInput('items', [{}]);
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
const title = fixture.debugElement.query(By.css('.title'));
|
||||
expect(title.nativeElement.textContent.trim()).toBe('No name');
|
||||
});
|
||||
});
|
||||
|
||||
describe('GridListComponent with strip country prefix enabled', () => {
|
||||
let fixture: ComponentFixture<GridListComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [GridListComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: SettingsStore,
|
||||
useValue: { stripCountryPrefix: signal(true) },
|
||||
},
|
||||
],
|
||||
})
|
||||
.overrideComponent(GridListComponent, {
|
||||
remove: { imports: [TranslatePipe] },
|
||||
add: {
|
||||
imports: [
|
||||
MockPipe(
|
||||
TranslatePipe,
|
||||
(value: string | null | undefined) => value ?? ''
|
||||
),
|
||||
],
|
||||
},
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(GridListComponent);
|
||||
});
|
||||
|
||||
const renderedTitle = () =>
|
||||
fixture.debugElement
|
||||
.query(By.css('.title'))
|
||||
.nativeElement.textContent.trim();
|
||||
|
||||
it('strips prefixes from live grid titles', () => {
|
||||
fixture.componentRef.setInput('items', [{ name: 'US | CNN' }]);
|
||||
fixture.componentRef.setInput('type', 'live');
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(renderedTitle()).toBe('CNN');
|
||||
});
|
||||
|
||||
it('keeps VOD titles untouched', () => {
|
||||
fixture.componentRef.setInput('items', [
|
||||
{ title: 'US | Some Movie' },
|
||||
]);
|
||||
fixture.componentRef.setInput('type', 'vod');
|
||||
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(renderedTitle()).toBe('US | Some Movie');
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,7 @@ import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
input,
|
||||
output,
|
||||
signal,
|
||||
@@ -11,6 +12,8 @@ import { MatIcon } from '@angular/material/icon';
|
||||
import { MatPaginatorModule, PageEvent } from '@angular/material/paginator';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { applyChannelNameStrip } from '@iptvnator/shared/m3u-utils';
|
||||
import { SettingsStore } from '@iptvnator/services';
|
||||
import {
|
||||
ProgressCapsuleComponent,
|
||||
WatchedBadgeComponent,
|
||||
@@ -164,10 +167,9 @@ function normalizeArtworkUrl(value: string | undefined): string | undefined {
|
||||
<mat-icon>star</mat-icon>{{ rating }}
|
||||
</div>
|
||||
}
|
||||
@let title = i.title ?? i.o_name ?? i.name;
|
||||
<mat-card-actions>
|
||||
<div class="title">
|
||||
{{ title || 'No name' }}
|
||||
{{ channelTitle(i) }}
|
||||
</div>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
@@ -229,6 +231,7 @@ function normalizeArtworkUrl(value: string | undefined): string | undefined {
|
||||
})
|
||||
export class GridListComponent {
|
||||
private readonly failedArtworkUrls = signal<ReadonlySet<string>>(new Set());
|
||||
private readonly settingsStore = inject(SettingsStore);
|
||||
|
||||
readonly items = input<GridListItem[]>([]);
|
||||
readonly isLoading = input<boolean>(false);
|
||||
@@ -253,6 +256,18 @@ export class GridListComponent {
|
||||
protected readonly hasActiveSearch = computed(
|
||||
() => (this.searchTerm() ?? '').trim().length > 0
|
||||
);
|
||||
/** Prefix stripping applies to channel grids only, never VOD/series. */
|
||||
private readonly isLiveGrid = computed(() =>
|
||||
['live', 'itv', 'radio'].includes(this.type())
|
||||
);
|
||||
protected readonly channelTitle = (item: GridListItem): string => {
|
||||
const raw = item.title ?? item.o_name ?? item.name ?? '';
|
||||
const stripped = applyChannelNameStrip(
|
||||
raw,
|
||||
this.isLiveGrid() && this.settingsStore.stripCountryPrefix?.()
|
||||
);
|
||||
return stripped || 'No name';
|
||||
};
|
||||
|
||||
readonly skeletonRows = computed(() => {
|
||||
const preferredCount = this.limit() ?? 12;
|
||||
|
||||
+32
@@ -136,6 +136,7 @@ describe('UnifiedLiveTabComponent', () => {
|
||||
let component: UnifiedLiveTabComponent;
|
||||
let player: ReturnType<typeof signal<VideoPlayer>>;
|
||||
let epgViewMode: ReturnType<typeof signal<'timeline' | 'list'>>;
|
||||
let stripCountryPrefix: ReturnType<typeof signal<boolean>>;
|
||||
let streamResolver: {
|
||||
resolveLiveDetail: jest.Mock;
|
||||
resolveM3uPlaybackDetail: jest.Mock;
|
||||
@@ -171,6 +172,7 @@ describe('UnifiedLiveTabComponent', () => {
|
||||
};
|
||||
player = signal(VideoPlayer.VideoJs);
|
||||
epgViewMode = signal<'timeline' | 'list'>('timeline');
|
||||
stripCountryPrefix = signal(false);
|
||||
portalPlayer = {
|
||||
isEmbeddedPlayer: jest.fn().mockReturnValue(false),
|
||||
openResolvedPlayback: jest.fn(),
|
||||
@@ -196,6 +198,7 @@ describe('UnifiedLiveTabComponent', () => {
|
||||
useValue: {
|
||||
openStreamOnDoubleClick: signal(false),
|
||||
player,
|
||||
stripCountryPrefix,
|
||||
resolvedEpgViewMode: epgViewMode,
|
||||
},
|
||||
},
|
||||
@@ -1061,6 +1064,35 @@ describe('UnifiedLiveTabComponent', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('timeline channel name', () => {
|
||||
it('strips the country prefix when the setting is enabled', () => {
|
||||
stripCountryPrefix.set(true);
|
||||
component.activeDetail.set({
|
||||
epgMode: 'portal',
|
||||
playback: {
|
||||
streamUrl: 'https://example.com/live.m3u8',
|
||||
title: 'US | CNN',
|
||||
},
|
||||
epgItems: [],
|
||||
} as never);
|
||||
|
||||
expect(component.timelineChannelName()).toBe('CNN');
|
||||
});
|
||||
|
||||
it('prefers the M3U channel name and keeps it raw while disabled', () => {
|
||||
component.activeDetail.set({
|
||||
epgMode: 'm3u',
|
||||
channel: { name: 'US | CNN' },
|
||||
playback: {
|
||||
streamUrl: 'https://example.com/live.m3u8',
|
||||
title: 'Fallback Title',
|
||||
},
|
||||
} as never);
|
||||
|
||||
expect(component.timelineChannelName()).toBe('US | CNN');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function buildLiveItem(
|
||||
|
||||
+6
-4
@@ -15,6 +15,7 @@ import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
|
||||
import {
|
||||
applyChannelNameStrip,
|
||||
getM3uArchiveDays,
|
||||
isM3uCatchupPlaybackSupported,
|
||||
resolveM3uCatchupUrl,
|
||||
@@ -183,11 +184,12 @@ export class UnifiedLiveTabComponent {
|
||||
? this.currentM3uPrograms()
|
||||
: this.currentPortalEpgPrograms()
|
||||
);
|
||||
readonly timelineChannelName = computed(
|
||||
() =>
|
||||
readonly timelineChannelName = computed(() =>
|
||||
applyChannelNameStrip(
|
||||
this.currentM3uChannel()?.name ??
|
||||
this.activeDetail()?.playback?.title ??
|
||||
''
|
||||
this.activeDetail()?.playback?.title,
|
||||
this.settingsStore.stripCountryPrefix?.()
|
||||
)
|
||||
);
|
||||
readonly timelineChannelLogo = computed(
|
||||
() =>
|
||||
|
||||
@@ -136,6 +136,26 @@ describe('SettingsStore dashboard rail settings', () => {
|
||||
expect(store.getSettings().webPlayerSharedControls).toBe(true);
|
||||
});
|
||||
|
||||
it('defaults strip country prefix to false when the stored field is missing', async () => {
|
||||
storedSettings = {};
|
||||
const store = injector.get(SettingsStore);
|
||||
|
||||
await store.loadSettings();
|
||||
|
||||
expect(store.getSettings().stripCountryPrefix).toBe(false);
|
||||
});
|
||||
|
||||
it('restores a persisted true strip country prefix preference', async () => {
|
||||
storedSettings = {
|
||||
stripCountryPrefix: true,
|
||||
};
|
||||
const store = injector.get(SettingsStore);
|
||||
|
||||
await store.loadSettings();
|
||||
|
||||
expect(store.getSettings().stripCountryPrefix).toBe(true);
|
||||
});
|
||||
|
||||
it('normalizes a persisted string "true" shared web controls preference to false', async () => {
|
||||
storedSettings = {
|
||||
webPlayerSharedControls: 'true' as unknown as boolean,
|
||||
|
||||
@@ -34,6 +34,7 @@ const DEFAULT_SETTINGS: Settings = {
|
||||
showDashboard: true,
|
||||
startupBehavior: StartupBehavior.FirstView,
|
||||
showExternalPlaybackBar: true,
|
||||
stripCountryPrefix: false,
|
||||
theme: Theme.SystemTheme,
|
||||
mpvPlayerPath: '',
|
||||
mpvPlayerArguments: '',
|
||||
@@ -196,6 +197,9 @@ export const SettingsStore = signalStore(
|
||||
showExternalPlaybackBar:
|
||||
store.showExternalPlaybackBar?.() ??
|
||||
DEFAULT_SETTINGS.showExternalPlaybackBar,
|
||||
stripCountryPrefix:
|
||||
store.stripCountryPrefix?.() ??
|
||||
DEFAULT_SETTINGS.stripCountryPrefix,
|
||||
theme: store.theme(),
|
||||
mpvPlayerPath: store.mpvPlayerPath(),
|
||||
mpvPlayerArguments: store.mpvPlayerArguments(),
|
||||
|
||||
@@ -92,6 +92,8 @@ export interface Settings {
|
||||
startupBehavior: StartupBehavior;
|
||||
/** Show the desktop footer bar for external playback status */
|
||||
showExternalPlaybackBar?: boolean;
|
||||
/** Strip country/group prefixes like "US | " or "UK - " from channel names */
|
||||
stripCountryPrefix?: boolean;
|
||||
theme: Theme;
|
||||
mpvPlayerPath: string;
|
||||
/**
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './lib/playlist.utils';
|
||||
export * from './lib/catchup.utils';
|
||||
export * from './lib/strip-country-prefix.util';
|
||||
@@ -0,0 +1,107 @@
|
||||
import {
|
||||
applyChannelNameStrip,
|
||||
stripCountryPrefix,
|
||||
} from './strip-country-prefix.util';
|
||||
|
||||
describe('stripCountryPrefix', () => {
|
||||
describe('pipe separators', () => {
|
||||
it('strips a "XX | " prefix', () => {
|
||||
expect(stripCountryPrefix('US | CNN')).toBe('CNN');
|
||||
});
|
||||
|
||||
it('strips a bare-pipe prefix', () => {
|
||||
expect(stripCountryPrefix('FR|TF1')).toBe('TF1');
|
||||
});
|
||||
|
||||
it('strips a wrapped tag like "|DE| ARD"', () => {
|
||||
expect(stripCountryPrefix('|DE| ARD')).toBe('ARD');
|
||||
});
|
||||
|
||||
it('strips long prefixes before a pipe', () => {
|
||||
expect(stripCountryPrefix('US East | CNN')).toBe('CNN');
|
||||
});
|
||||
|
||||
it('only strips up to the first pipe', () => {
|
||||
expect(stripCountryPrefix('US | CNN | HD')).toBe('CNN | HD');
|
||||
});
|
||||
});
|
||||
|
||||
describe('dash and colon separators (short-tag prefixes only)', () => {
|
||||
it('strips a two-letter country tag', () => {
|
||||
expect(stripCountryPrefix('UK - BBC One')).toBe('BBC One');
|
||||
});
|
||||
|
||||
it('strips a three-letter country tag', () => {
|
||||
expect(stripCountryPrefix('USA - CNN')).toBe('CNN');
|
||||
});
|
||||
|
||||
it('strips alphanumeric tags like 4K', () => {
|
||||
expect(stripCountryPrefix('4K - Discovery')).toBe('Discovery');
|
||||
});
|
||||
|
||||
it('strips a colon-separated tag', () => {
|
||||
expect(stripCountryPrefix('US: CNN')).toBe('CNN');
|
||||
});
|
||||
|
||||
it('only strips the first tag segment', () => {
|
||||
expect(stripCountryPrefix('ES - A3 - Sports')).toBe('A3 - Sports');
|
||||
});
|
||||
|
||||
it('keeps names whose prefix is not a short uppercase tag', () => {
|
||||
expect(stripCountryPrefix('Sky - Sports F1')).toBe(
|
||||
'Sky - Sports F1'
|
||||
);
|
||||
expect(stripCountryPrefix('Discovery - Science')).toBe(
|
||||
'Discovery - Science'
|
||||
);
|
||||
expect(stripCountryPrefix('Mission: Impossible - Fallout')).toBe(
|
||||
'Mission: Impossible - Fallout'
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps hyphenated names without separator spacing', () => {
|
||||
expect(stripCountryPrefix('T-Mobile TV')).toBe('T-Mobile TV');
|
||||
expect(stripCountryPrefix('US-CNN')).toBe('US-CNN');
|
||||
});
|
||||
|
||||
it('keeps single-character prefixes', () => {
|
||||
expect(stripCountryPrefix('A - Team')).toBe('A - Team');
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('returns names without separators unchanged', () => {
|
||||
expect(stripCountryPrefix('BBC One')).toBe('BBC One');
|
||||
});
|
||||
|
||||
it('trims surrounding whitespace', () => {
|
||||
expect(stripCountryPrefix(' BBC One ')).toBe('BBC One');
|
||||
});
|
||||
|
||||
it('returns the original name when stripping would leave nothing', () => {
|
||||
expect(stripCountryPrefix('US | ')).toBe('US |');
|
||||
});
|
||||
|
||||
it('handles empty input', () => {
|
||||
expect(stripCountryPrefix('')).toBe('');
|
||||
expect(stripCountryPrefix(' ')).toBe('');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('applyChannelNameStrip', () => {
|
||||
it('strips when enabled', () => {
|
||||
expect(applyChannelNameStrip('US | CNN', true)).toBe('CNN');
|
||||
});
|
||||
|
||||
it('returns the raw name when disabled', () => {
|
||||
expect(applyChannelNameStrip('US | CNN', false)).toBe('US | CNN');
|
||||
expect(applyChannelNameStrip('US | CNN', undefined)).toBe('US | CNN');
|
||||
expect(applyChannelNameStrip('US | CNN', null)).toBe('US | CNN');
|
||||
});
|
||||
|
||||
it('normalizes nullish names to an empty string', () => {
|
||||
expect(applyChannelNameStrip(null, true)).toBe('');
|
||||
expect(applyChannelNameStrip(undefined, false)).toBe('');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* Dash/colon separators only count when the prefix looks like a short
|
||||
* country/group tag — they routinely appear inside real channel names
|
||||
* ("Sky - Sports F1", "Discovery - Science"). Pipes are conventionally
|
||||
* used only as tag separators, so they always count.
|
||||
*/
|
||||
const DASH_OR_COLON_SEPARATORS = [' - ', '- ', ' -', ': '];
|
||||
const TAG_PREFIX_PATTERN = /^[A-Z0-9]{2,3}$/;
|
||||
|
||||
interface SeparatorMatch {
|
||||
index: number;
|
||||
length: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip country / group prefixes from channel names.
|
||||
*
|
||||
* Finds the first tag separator and returns everything to its right.
|
||||
* Only the prefix is removed — if the separator appears multiple times,
|
||||
* only the first match counts. If stripping would leave an empty name,
|
||||
* the original (trimmed) name is returned instead.
|
||||
*
|
||||
* Examples:
|
||||
* "US | CNN" → "CNN"
|
||||
* "UK - BBC One" → "BBC One"
|
||||
* "FR|TF1" → "TF1"
|
||||
* "|DE| ARD" → "ARD"
|
||||
* "US: CNN" → "CNN"
|
||||
* "ES - A3 - Sports" → "A3 - Sports"
|
||||
* "Sky - Sports F1" → "Sky - Sports F1" (prefix is not a short tag)
|
||||
* "BBC One" → "BBC One" (no separator)
|
||||
* "US | " → "US |" (stripping would leave nothing)
|
||||
*/
|
||||
export function stripCountryPrefix(name: string): string {
|
||||
const trimmed = name.trim();
|
||||
if (!trimmed) return trimmed;
|
||||
|
||||
const match = findTagSeparator(trimmed);
|
||||
if (!match) return trimmed;
|
||||
|
||||
const prefix = trimmed.slice(0, match.index).trim();
|
||||
const stripped = trimmed.slice(match.index + match.length).trim();
|
||||
if (!stripped) return trimmed;
|
||||
|
||||
// A leading separator ("|DE| ARD") strips nothing by itself — retry on
|
||||
// the remainder so the actual tag segment is removed too.
|
||||
return prefix ? stripped : stripCountryPrefix(stripped);
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings-aware wrapper for display bindings: strips the prefix only when
|
||||
* the `stripCountryPrefix` setting is enabled, and normalizes nullish names
|
||||
* to an empty string.
|
||||
*/
|
||||
export function applyChannelNameStrip(
|
||||
name: string | null | undefined,
|
||||
enabled: boolean | null | undefined
|
||||
): string {
|
||||
const raw = name ?? '';
|
||||
return enabled ? stripCountryPrefix(raw) : raw;
|
||||
}
|
||||
|
||||
function findTagSeparator(name: string): SeparatorMatch | null {
|
||||
let best: SeparatorMatch | null = null;
|
||||
|
||||
const pipeIndex = name.indexOf('|');
|
||||
if (pipeIndex !== -1) {
|
||||
best = { index: pipeIndex, length: 1 };
|
||||
}
|
||||
|
||||
for (const separator of DASH_OR_COLON_SEPARATORS) {
|
||||
const index = name.indexOf(separator);
|
||||
if (index === -1 || (best && best.index <= index)) continue;
|
||||
if (!TAG_PREFIX_PATTERN.test(name.slice(0, index).trim())) continue;
|
||||
best = { index, length: separator.length };
|
||||
}
|
||||
|
||||
return best;
|
||||
}
|
||||
+1
-1
@@ -28,7 +28,7 @@
|
||||
}
|
||||
</div>
|
||||
<div class="channel-details">
|
||||
<div class="channel-name">{{ name() }}</div>
|
||||
<div class="channel-name">{{ displayName() }}</div>
|
||||
@if (showEpg()) {
|
||||
@if (epgProgram(); as program) {
|
||||
<div class="epg-title">
|
||||
|
||||
+46
@@ -1,8 +1,10 @@
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { signal } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { SettingsStore } from '@iptvnator/services';
|
||||
import { EpgProgram } from '@iptvnator/shared/interfaces';
|
||||
import { ChannelListItemComponent } from './channel-list-item.component';
|
||||
|
||||
@@ -141,4 +143,48 @@ describe('ChannelListItemComponent', () => {
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('renders the raw channel name while prefix stripping is disabled', () => {
|
||||
fixture.componentRef.setInput('name', 'US | CNN');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(
|
||||
fixture.nativeElement
|
||||
.querySelector('.channel-name')
|
||||
.textContent.trim()
|
||||
).toBe('US | CNN');
|
||||
});
|
||||
|
||||
describe('with strip country prefix enabled', () => {
|
||||
beforeEach(async () => {
|
||||
TestBed.resetTestingModule();
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [
|
||||
ChannelListItemComponent,
|
||||
NoopAnimationsModule,
|
||||
TranslateModule.forRoot(),
|
||||
],
|
||||
providers: [
|
||||
{ provide: MatDialog, useValue: dialog },
|
||||
{
|
||||
provide: SettingsStore,
|
||||
useValue: { stripCountryPrefix: signal(true) },
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ChannelListItemComponent);
|
||||
});
|
||||
|
||||
it('strips the country prefix from the rendered channel name', () => {
|
||||
fixture.componentRef.setInput('name', 'US | CNN');
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(
|
||||
fixture.nativeElement
|
||||
.querySelector('.channel-name')
|
||||
.textContent.trim()
|
||||
).toBe('CNN');
|
||||
});
|
||||
});
|
||||
});
|
||||
+10
@@ -3,6 +3,7 @@ import { DatePipe, NgStyle } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
effect,
|
||||
inject,
|
||||
input,
|
||||
@@ -16,6 +17,8 @@ import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { EpgItemDescriptionComponent } from '@iptvnator/ui/epg';
|
||||
import { EpgProgram } from '@iptvnator/shared/interfaces';
|
||||
import { SettingsStore } from '@iptvnator/services';
|
||||
import { applyChannelNameStrip } from '@iptvnator/shared/m3u-utils';
|
||||
|
||||
@Component({
|
||||
selector: 'app-channel-list-item',
|
||||
@@ -35,10 +38,17 @@ import { EpgProgram } from '@iptvnator/shared/interfaces';
|
||||
export class ChannelListItemComponent {
|
||||
private readonly dialog = inject(MatDialog);
|
||||
private readonly logoFailed = signal(false);
|
||||
private readonly settingsStore = inject(SettingsStore);
|
||||
|
||||
readonly isDraggable = input(false);
|
||||
readonly logo = input<string | null | undefined>('');
|
||||
readonly name = input('');
|
||||
readonly displayName = computed(() =>
|
||||
applyChannelNameStrip(
|
||||
this.name(),
|
||||
this.settingsStore.stripCountryPrefix?.()
|
||||
)
|
||||
);
|
||||
readonly showFavoriteButton = input(false);
|
||||
readonly showAuxActionButton = input(false);
|
||||
readonly showProgramInfoButton = input(true);
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@
|
||||
</button>
|
||||
|
||||
<div class="external-playback-dock__meta">
|
||||
<div class="external-playback-dock__title">{{ session().title }}</div>
|
||||
<div class="external-playback-dock__title">{{ displayTitle() }}</div>
|
||||
<div class="external-playback-dock__status">
|
||||
<span class="external-playback-dock__chip">{{ playerLabel() }}</span>
|
||||
<span
|
||||
|
||||
+60
@@ -1,3 +1,4 @@
|
||||
import { signal } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import {
|
||||
@@ -6,6 +7,7 @@ import {
|
||||
TranslateService,
|
||||
} from '@ngx-translate/core';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { SettingsStore } from '@iptvnator/services';
|
||||
import { ExternalPlayerSession } from '@iptvnator/shared/interfaces';
|
||||
import { ExternalPlaybackDockComponent } from './external-playback-dock.component';
|
||||
|
||||
@@ -146,4 +148,62 @@ describe('ExternalPlaybackDockComponent', () => {
|
||||
.nativeElement.textContent.trim()
|
||||
).toBe('movie');
|
||||
});
|
||||
|
||||
describe('with strip country prefix enabled', () => {
|
||||
beforeEach(async () => {
|
||||
TestBed.resetTestingModule();
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [
|
||||
ExternalPlaybackDockComponent,
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: FakeTranslateLoader,
|
||||
},
|
||||
}),
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: SettingsStore,
|
||||
useValue: { stripCountryPrefix: signal(true) },
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ExternalPlaybackDockComponent);
|
||||
});
|
||||
|
||||
it('strips the prefix from live session titles', () => {
|
||||
fixture.componentRef.setInput('session', {
|
||||
...session,
|
||||
title: 'US | CNN',
|
||||
});
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(
|
||||
fixture.nativeElement
|
||||
.querySelector('.external-playback-dock__title')
|
||||
.textContent.trim()
|
||||
).toBe('CNN');
|
||||
});
|
||||
|
||||
it('keeps VOD/episode titles untouched', () => {
|
||||
fixture.componentRef.setInput('session', {
|
||||
...session,
|
||||
title: 'US | Some Movie',
|
||||
contentInfo: {
|
||||
playlistId: 'playlist-1',
|
||||
contentXtreamId: 42,
|
||||
contentType: 'vod',
|
||||
},
|
||||
});
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(
|
||||
fixture.nativeElement
|
||||
.querySelector('.external-playback-dock__title')
|
||||
.textContent.trim()
|
||||
).toBe('US | Some Movie');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
Component,
|
||||
computed,
|
||||
effect,
|
||||
inject,
|
||||
input,
|
||||
output,
|
||||
signal,
|
||||
@@ -12,6 +13,8 @@ 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',
|
||||
@@ -26,6 +29,8 @@ import { ExternalPlayerSession } from '@iptvnator/shared/interfaces';
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ExternalPlaybackDockComponent {
|
||||
private readonly settingsStore = inject(SettingsStore);
|
||||
|
||||
readonly session = input.required<ExternalPlayerSession>();
|
||||
readonly compact = input(false);
|
||||
|
||||
@@ -33,6 +38,19 @@ export class ExternalPlaybackDockComponent {
|
||||
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() ?? ''
|
||||
|
||||
+53
-1
@@ -1,7 +1,8 @@
|
||||
import { Component, input, output } from '@angular/core';
|
||||
import { Component, input, output, signal } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { SettingsStore } from '@iptvnator/services';
|
||||
import type { PortalInlinePlayerComponent as PortalInlinePlayerComponentInstance } from './portal-inline-player.component';
|
||||
|
||||
jest.unstable_mockModule('video.js', () => ({
|
||||
@@ -150,4 +151,55 @@ describe('PortalInlinePlayerComponent', () => {
|
||||
expect(backCount).toBe(1);
|
||||
expect(closedCount).toBe(0);
|
||||
});
|
||||
|
||||
describe('with strip country prefix enabled', () => {
|
||||
beforeEach(async () => {
|
||||
TestBed.resetTestingModule();
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [
|
||||
PortalInlinePlayerComponent,
|
||||
TranslateModule.forRoot(),
|
||||
],
|
||||
providers: [
|
||||
{
|
||||
provide: SettingsStore,
|
||||
useValue: { stripCountryPrefix: signal(true) },
|
||||
},
|
||||
],
|
||||
})
|
||||
.overrideComponent(PortalInlinePlayerComponent, {
|
||||
remove: {
|
||||
imports: [WebPlayerViewComponent],
|
||||
},
|
||||
add: {
|
||||
imports: [StubWebPlayerViewComponent],
|
||||
},
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(PortalInlinePlayerComponent);
|
||||
component = fixture.componentInstance;
|
||||
});
|
||||
|
||||
it('strips the prefix from live playback titles', () => {
|
||||
fixture.componentRef.setInput('playback', {
|
||||
streamUrl: 'https://example.com/live.m3u8',
|
||||
title: 'US | CNN',
|
||||
isLive: true,
|
||||
});
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.title()).toBe('CNN');
|
||||
});
|
||||
|
||||
it('keeps VOD titles untouched', () => {
|
||||
fixture.componentRef.setInput('playback', {
|
||||
streamUrl: 'https://example.com/movie.mp4',
|
||||
title: 'US | Some Movie',
|
||||
});
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(component.title()).toBe('US | Some Movie');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
input,
|
||||
output,
|
||||
} from '@angular/core';
|
||||
@@ -15,6 +16,8 @@ import {
|
||||
ResolvedPortalPlayback,
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
import type { PlaybackFallbackRequest } from '../playback-diagnostics/playback-diagnostics.util';
|
||||
import { SettingsStore } from '@iptvnator/services';
|
||||
import { applyChannelNameStrip } from '@iptvnator/shared/m3u-utils';
|
||||
import { WebPlayerViewComponent } from '../web-player-view/web-player-view.component';
|
||||
import type {
|
||||
SeriesEpisodeMetadata,
|
||||
@@ -43,7 +46,16 @@ export class PortalInlinePlayerComponent {
|
||||
readonly playback = input<ResolvedPortalPlayback | null>(null);
|
||||
readonly episodeMetadata = input<SeriesEpisodeMetadata | null>(null);
|
||||
readonly seriesNavigation = input<SeriesPlaybackNavigation | null>(null);
|
||||
readonly title = computed(() => this.playback()?.title ?? '');
|
||||
private readonly settingsStore = inject(SettingsStore);
|
||||
// Strip only live-channel titles — VOD/series titles ("Mission:
|
||||
// Impossible - Fallout") must never lose their leading segment.
|
||||
readonly title = computed(() =>
|
||||
applyChannelNameStrip(
|
||||
this.playback()?.title,
|
||||
this.playback()?.isLive &&
|
||||
this.settingsStore.stripCountryPrefix?.()
|
||||
)
|
||||
);
|
||||
readonly streamUrl = computed(() => this.playback()?.streamUrl ?? '');
|
||||
readonly startTime = computed(() => this.playback()?.startTime ?? 0);
|
||||
readonly contentInfo = computed<PlayerContentInfo | undefined>(
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
<div
|
||||
class="rail__card rail__card--channel"
|
||||
role="listitem"
|
||||
[attr.title]="card.title"
|
||||
[attr.title]="cardTitle(card)"
|
||||
[attr.data-test-id]="
|
||||
testId() ? testId() + '-card' : null
|
||||
"
|
||||
@@ -83,7 +83,7 @@
|
||||
) {
|
||||
<img
|
||||
[src]="card.imageUrl"
|
||||
[alt]="card.title"
|
||||
[alt]="cardTitle(card)"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
(error)="markFailed(card.id)"
|
||||
@@ -94,7 +94,7 @@
|
||||
</div>
|
||||
<div class="rail__channel-meta">
|
||||
<span class="rail__channel-name">{{
|
||||
card.title
|
||||
cardTitle(card)
|
||||
}}</span>
|
||||
<span class="rail__channel-now">
|
||||
@if (card.nowPlayingTitle) {
|
||||
@@ -134,7 +134,7 @@
|
||||
<div
|
||||
class="rail__card"
|
||||
role="listitem"
|
||||
[attr.title]="card.title"
|
||||
[attr.title]="cardTitle(card)"
|
||||
[attr.data-test-id]="
|
||||
testId() ? testId() + '-card' : null
|
||||
"
|
||||
@@ -166,7 +166,7 @@
|
||||
}
|
||||
<img
|
||||
[src]="card.imageUrl"
|
||||
[alt]="card.title"
|
||||
[alt]="cardTitle(card)"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
(error)="markFailed(card.id)"
|
||||
@@ -191,7 +191,7 @@
|
||||
|
||||
<div class="rail__meta">
|
||||
<span class="rail__card-title">{{
|
||||
card.title
|
||||
cardTitle(card)
|
||||
}}</span>
|
||||
<span class="rail__card-meta-row">
|
||||
@if (card.episodeBadge) {
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { signal } from '@angular/core';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { SettingsStore } from '@iptvnator/services';
|
||||
import {
|
||||
DashboardRailCard,
|
||||
DashboardRailComponent,
|
||||
} from './dashboard-rail.component';
|
||||
|
||||
describe('DashboardRailComponent', () => {
|
||||
const createComponent = async (stripCountryPrefix: boolean) => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [DashboardRailComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: SettingsStore,
|
||||
useValue: {
|
||||
stripCountryPrefix: signal(stripCountryPrefix),
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
const fixture = TestBed.createComponent(DashboardRailComponent);
|
||||
fixture.componentRef.setInput('label', 'Rail');
|
||||
fixture.componentRef.setInput('items', []);
|
||||
return fixture.componentInstance as unknown as {
|
||||
cardTitle(card: DashboardRailCard): string;
|
||||
};
|
||||
};
|
||||
|
||||
const card = (overrides: Partial<DashboardRailCard>): DashboardRailCard =>
|
||||
({
|
||||
id: 'card-1',
|
||||
title: 'US | CNN',
|
||||
icon: 'live_tv',
|
||||
link: ['/workspace'],
|
||||
...overrides,
|
||||
}) as DashboardRailCard;
|
||||
|
||||
afterEach(() => {
|
||||
TestBed.resetTestingModule();
|
||||
});
|
||||
|
||||
it('strips the prefix from live card titles when the setting is enabled', async () => {
|
||||
const component = await createComponent(true);
|
||||
|
||||
expect(component.cardTitle(card({ contentType: 'live' }))).toBe('CNN');
|
||||
});
|
||||
|
||||
it('keeps movie and series card titles untouched', async () => {
|
||||
const component = await createComponent(true);
|
||||
|
||||
expect(
|
||||
component.cardTitle(
|
||||
card({ title: 'US | Some Movie', contentType: 'movie' })
|
||||
)
|
||||
).toBe('US | Some Movie');
|
||||
expect(
|
||||
component.cardTitle(
|
||||
card({ title: 'US | Some Show', contentType: 'series' })
|
||||
)
|
||||
).toBe('US | Some Show');
|
||||
});
|
||||
|
||||
it('keeps live card titles untouched while the setting is disabled', async () => {
|
||||
const component = await createComponent(false);
|
||||
|
||||
expect(component.cardTitle(card({ contentType: 'live' }))).toBe(
|
||||
'US | CNN'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
ElementRef,
|
||||
OnDestroy,
|
||||
effect,
|
||||
inject,
|
||||
input,
|
||||
output,
|
||||
signal,
|
||||
@@ -15,6 +16,8 @@ import { MatIcon } from '@angular/material/icon';
|
||||
import { MatMenuModule } from '@angular/material/menu';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { SettingsStore } from '@iptvnator/services';
|
||||
import { applyChannelNameStrip } from '@iptvnator/shared/m3u-utils';
|
||||
|
||||
export interface DashboardRailAction {
|
||||
id: string;
|
||||
@@ -94,6 +97,8 @@ export interface DashboardRailActionSelection {
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class DashboardRailComponent implements AfterViewInit, OnDestroy {
|
||||
private readonly settingsStore = inject(SettingsStore);
|
||||
|
||||
readonly label = input.required<string>();
|
||||
readonly items = input.required<DashboardRailCard[]>();
|
||||
readonly seeAllLink = input<string[] | null>(null);
|
||||
@@ -164,6 +169,15 @@ export class DashboardRailComponent implements AfterViewInit, OnDestroy {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
/** Prefix stripping applies to live-channel cards only, never VOD/series. */
|
||||
protected cardTitle(card: DashboardRailCard): string {
|
||||
return applyChannelNameStrip(
|
||||
card.title,
|
||||
card.contentType === 'live' &&
|
||||
this.settingsStore.stripCountryPrefix?.()
|
||||
);
|
||||
}
|
||||
|
||||
selectAction(
|
||||
card: DashboardRailCard,
|
||||
action: DashboardRailAction,
|
||||
|
||||
Reference in new issue
Block a user