diff --git a/CLAUDE.md b/CLAUDE.md index 33ea68e1a..1cbbac8e4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -697,7 +697,6 @@ No formal migration system yet. Schema changes are applied via raw SQL in `conne **State Management**: - Use NgRx for global application state (playlists) -- Use component stores (`@ngrx/component-store`) for feature-specific state - Use NgRx signals for reactive data streams diff --git a/apps/web/src/assets/i18n/ar.json b/apps/web/src/assets/i18n/ar.json index 8a35c14a5..3186464eb 100644 --- a/apps/web/src/assets/i18n/ar.json +++ b/apps/web/src/assets/i18n/ar.json @@ -192,10 +192,8 @@ "VALIDATING": "جارٍ التحقق من البوابة..." }, "FILTER_BY_NAME": "التصفية حسب الاسم", - "FILTER_BY_TYPE": "التصفية حسب النوع", "FILTER_AND_SORT": "تصفية وفرز", "SORT_PLAYLISTS": "فرز القوائم", - "SORT_BY": "ترتيب حسب", "SORT_OPTIONS": { "NAME_ASC": "الاسم (أ-ي)", "NAME_DESC": "الاسم (ي-أ)", @@ -483,7 +481,6 @@ "TOGGLE_SIDEBAR_TOOLTIP": "تبديل قائمة القنوات (⌘/Ctrl+B)" }, "CHANNELS": { - "SEARCH_CHANNEL": "البحث عن قناة", "UPLOAD_OR_SELECT_OTHER_PLAYLIST": "تحميل أو اختيار قائمة أخرى", "ALL_CHANNELS": "جميع القنوات", "GROUPS": "المجموعات", @@ -557,14 +554,9 @@ } }, "TOP_MENU": { - "OPEN_CHANNELS_LIST": "فتح قائمة القنوات", - "TOGGLE_FAVORITE_FLAG": "تبديل حالة المفضلة", - "OPEN_EPG_LIST": "فتح قائمة EPG", - "OPEN_MULTI_EPG": "فتح عرض Multi-EPG", - "SHOW_INFO": "عرض معلومات البرنامج (اضغط I)" + "OPEN_MULTI_EPG": "فتح عرض Multi-EPG" }, "EPG": { - "TITLE": "الدليل الإلكتروني للبرامج", "NEXT_DAY": "اليوم التالي", "PREVIOUS_DAY": "اليوم السابق", "ZOOM_IN": "تكبير", @@ -573,14 +565,9 @@ "CLOSE_FILTER": "إغلاق التصفية", "SEARCH_PROGRAMS": "البحث في البرامج", "CLOSE_PROGRAM_SEARCH": "إغلاق بحث البرامج", - "LIVE_NOW": "يُبث الآن", "LIVE_STREAM": "بث مباشر", - "TIMESHIFT_AVAILABLE": "التحريك الزمني متاح، انقر للتشغيل", "ARCHIVE_PLAYBACK_UNAVAILABLE_TITLE": "تشغيل الأرشيف غير متاح", "ARCHIVE_PLAYBACK_UNAVAILABLE_DESCRIPTION": "يكشف هذا المزود عن سجل الجدول لهذه القناة، لكنه لا يوفر تشغيلًا قابلًا للأرشيف أو Catchup.", - "EPG_NOT_AVAILABLE_DATE": "عذراً، الدليل غير متاح لهذا التاريخ", - "NO_GUIDE_HINT": "جرب تاريخاً آخر أو حدّث قائمة التشغيل.", - "EPG_NOT_AVAILABLE_CHANNEL_TITLE": "عذراً، الدليل غير متاح لهذه القناة", "EPG_NOT_AVAILABLE_CHANNEL_DESCRIPTION": "يرجى إضافة/تعديل رابط EPG في إعدادات التطبيق.", "FETCH_EPG": "جارٍ جلب بيانات EPG...", "ERROR": "عذراً، فشل تحميل الدليل.", @@ -615,7 +602,6 @@ "TRUST_TLS_HOST_WARNING": "Only continue if you trust this host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates.", "CURRENT_PROGRAM": "Current program", "ARCHIVE_PLAYBACK": "تشغيل الأرشيف", - "RETURN_TO_LIVE": "العودة إلى البث المباشر", "COLLAPSE_PANEL": "Collapse EPG panel", "EXPAND_PANEL": "Expand EPG panel", "TIMELINE": { @@ -679,7 +665,6 @@ "MENU": { "OPEN": "فتح القائمة", "SUPPORT_ARIA": "دعم IPTVnator على GitHub", - "SETTINGS": "الإعدادات", "SETTINGS_ARIA": "فتح الإعدادات", "BUG_REPORT": "ابلاغ عن خلل", "WHAT_IS_NEW": "ما الجديد", @@ -804,10 +789,7 @@ "SEARCH": "بحث", "RECENT": "الأخيرة", "RECENTLY_ADDED": "أُضيفت مؤخرًا", - "FAVORITES": "المفضلة", - "HOME": "الرئيسية", - "PLAYLIST_INFO": "معلومات قائمة التشغيل", - "ACCOUNT_INFO": "معلومات الحساب" + "FAVORITES": "المفضلة" }, "ERROR_VIEW": { "IMPORT_CANCELLED": { diff --git a/apps/web/src/assets/i18n/ary.json b/apps/web/src/assets/i18n/ary.json index c4a702eac..cb5e7d321 100644 --- a/apps/web/src/assets/i18n/ary.json +++ b/apps/web/src/assets/i18n/ary.json @@ -192,10 +192,8 @@ "VALIDATING": "جاري التحقق من البوابة..." }, "FILTER_BY_NAME": "فلتر بالاسم", - "FILTER_BY_TYPE": "فلتر بالنوع", "FILTER_AND_SORT": "فلتر وترتيب", "SORT_PLAYLISTS": "رتب قوائم التشغيل", - "SORT_BY": "رتب بـ", "SORT_OPTIONS": { "NAME_ASC": "الاسم (أ-ي)", "NAME_DESC": "الاسم (ي-أ)", @@ -483,7 +481,6 @@ "TOGGLE_SIDEBAR_TOOLTIP": "تبديل قائمة القنوات (⌘/Ctrl+B)" }, "CHANNELS": { - "SEARCH_CHANNEL": "قلب على قناة", "UPLOAD_OR_SELECT_OTHER_PLAYLIST": "رفع ولا اختار قائمة تشغيل أخرى", "ALL_CHANNELS": "جميع القنوات", "GROUPS": "المجموعات", @@ -557,14 +554,9 @@ } }, "TOP_MENU": { - "OPEN_CHANNELS_LIST": "حل قائمة القنوات", - "TOGGLE_FAVORITE_FLAG": "بدل علامة المفضلة", - "OPEN_EPG_LIST": "حل قائمة EPG", - "OPEN_MULTI_EPG": "حل عرض EPG المتعدد", - "SHOW_INFO": "وري معلومات البرنامج (ضغط على I)" + "OPEN_MULTI_EPG": "حل عرض EPG المتعدد" }, "EPG": { - "TITLE": "دليل البرامج الإلكتروني", "NEXT_DAY": "اليوم التالي", "PREVIOUS_DAY": "اليوم السابق", "ZOOM_IN": "كبر", @@ -573,14 +565,9 @@ "CLOSE_FILTER": "سد الفلتر", "SEARCH_PROGRAMS": "قلب على البرامج", "CLOSE_PROGRAM_SEARCH": "سد البحث على البرامج", - "LIVE_NOW": "مباشر الآن", "LIVE_STREAM": "بث مباشر", - "TIMESHIFT_AVAILABLE": "التأجيل الزمني متوفر، ضغط باش تلعب", "ARCHIVE_PLAYBACK_UNAVAILABLE_TITLE": "تشغيل الأرشيف ما متوفرش", "ARCHIVE_PLAYBACK_UNAVAILABLE_DESCRIPTION": "هاد المزود كيوفر تاريخ جدول البرامج للقناة، ولكن ما كيوفرش تشغيل أرشيف/Catchup.", - "EPG_NOT_AVAILABLE_DATE": "أوه، EPG ما متوفرش للتاريخ اللي اختاريتيه", - "NO_GUIDE_HINT": "جرب تاريخ آخر أو حدّث اللائحة.", - "EPG_NOT_AVAILABLE_CHANNEL_TITLE": "أوه، EPG ما متوفرش لهاد القناة.", "EPG_NOT_AVAILABLE_CHANNEL_DESCRIPTION": "عفاك زيد/بدل رابط قائمة EPG في إعدادات التطبيق.", "FETCH_EPG": "جاري جلب بيانات EPG...", "ERROR": "أوه، EPG ما قدرش يتشارجا.", @@ -615,7 +602,6 @@ "TRUST_TLS_HOST_WARNING": "Only continue if you trust this host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates.", "CURRENT_PROGRAM": "Current program", "ARCHIVE_PLAYBACK": "تشغيل الأرشيف", - "RETURN_TO_LIVE": "رجع للبث المباشر", "COLLAPSE_PANEL": "Collapse EPG panel", "EXPAND_PANEL": "Expand EPG panel", "TIMELINE": { @@ -679,7 +665,6 @@ "MENU": { "OPEN": "حل القائمة", "SUPPORT_ARIA": "دعم IPTVnator على GitHub", - "SETTINGS": "الإعدادات", "SETTINGS_ARIA": "حل الإعدادات", "BUG_REPORT": "بلغ على شي مشكل", "WHAT_IS_NEW": "شنو الجديد", @@ -804,10 +789,7 @@ "SEARCH": "بحث", "RECENT": "أخير", "RECENTLY_ADDED": "تزادو مؤخراً", - "FAVORITES": "المفضلة", - "HOME": "الرئيسية", - "PLAYLIST_INFO": "معلومات قائمة التشغيل", - "ACCOUNT_INFO": "معلومات الحساب" + "FAVORITES": "المفضلة" }, "ERROR_VIEW": { "IMPORT_CANCELLED": { diff --git a/apps/web/src/assets/i18n/by.json b/apps/web/src/assets/i18n/by.json index e2142f5e7..0ce8e448d 100644 --- a/apps/web/src/assets/i18n/by.json +++ b/apps/web/src/assets/i18n/by.json @@ -192,10 +192,8 @@ "VALIDATING": "Праверка партала..." }, "FILTER_BY_NAME": "Фільтраваць па імені", - "FILTER_BY_TYPE": "Фільтраваць па тыпу", "FILTER_AND_SORT": "Фільтр і сартаванне", "SORT_PLAYLISTS": "Сартаваць плэйлісты", - "SORT_BY": "Сартаваць па", "SORT_OPTIONS": { "NAME_ASC": "Назва (А-Я)", "NAME_DESC": "Назва (Я-А)", @@ -483,7 +481,6 @@ "TOGGLE_SIDEBAR_TOOLTIP": "Пераключыць спіс каналаў (⌘/Ctrl+B)" }, "CHANNELS": { - "SEARCH_CHANNEL": "Пошук каналаў", "UPLOAD_OR_SELECT_OTHER_PLAYLIST": "Загрузіць або выбраць іншы плэйліст", "ALL_CHANNELS": "Усе каналы", "GROUPS": "Групы", @@ -557,14 +554,9 @@ } }, "TOP_MENU": { - "OPEN_CHANNELS_LIST": "Адкрыць спіс каналаў", - "TOGGLE_FAVORITE_FLAG": "Выбраны", - "OPEN_EPG_LIST": "Адкрыць праграму (EPG)", - "OPEN_MULTI_EPG": "Адкрыць мульты-EPG прагляд", - "SHOW_INFO": "Паказаць інфармацыю аб праграме (націсніце I)" + "OPEN_MULTI_EPG": "Адкрыць мульты-EPG прагляд" }, "EPG": { - "TITLE": "Электронная праграма перадач", "NEXT_DAY": "Наступны дзень", "PREVIOUS_DAY": "Папярэдні дзень", "ZOOM_IN": "Павялічыць маштаб", @@ -573,14 +565,9 @@ "CLOSE_FILTER": "Закрыць фільтр", "SEARCH_PROGRAMS": "Пошук перадач", "CLOSE_PROGRAM_SEARCH": "Закрыць пошук перадач", - "LIVE_NOW": "Цяпер", "LIVE_STREAM": "Прамая трансляцыя", - "TIMESHIFT_AVAILABLE": "Даступная функцыя архіва (timeshift). Націсніце на перадачу для прайгравання.", "ARCHIVE_PLAYBACK_UNAVAILABLE_TITLE": "Прайграванне архіва недаступна", "ARCHIVE_PLAYBACK_UNAVAILABLE_DESCRIPTION": "Гэты правайдэр прадастаўляе гісторыю перадач для канала, але не падтрымлівае прайграванне архіва/catch-up.", - "EPG_NOT_AVAILABLE_DATE": "Ой, праграма на гэты дзень, на жаль, адсутнічае.", - "NO_GUIDE_HINT": "Паспрабуйце іншую дату або абнавіце плэйліст.", - "EPG_NOT_AVAILABLE_CHANNEL_TITLE": "Праграма для гэтага канала недаступная.", "EPG_NOT_AVAILABLE_CHANNEL_DESCRIPTION": "Паспрабуйце змяніць або абнавіць спасылку на EPG у наладах праграмы.", "FETCH_EPG": "Загружаю праграму ТБ-перадач (EPG)...", "ERROR": "Ой, не ўдалося загрузіць праграму каналаў (EPG)", @@ -615,7 +602,6 @@ "TRUST_TLS_HOST_WARNING": "Only continue if you trust this host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates.", "CURRENT_PROGRAM": "Current program", "ARCHIVE_PLAYBACK": "Прайграванне архіва", - "RETURN_TO_LIVE": "Вярнуцца да эфіру", "COLLAPSE_PANEL": "Collapse EPG panel", "EXPAND_PANEL": "Expand EPG panel", "TIMELINE": { @@ -679,7 +665,6 @@ "MENU": { "OPEN": "Адкрыць меню", "SUPPORT_ARIA": "Падтрымаць IPTVnator на GitHub", - "SETTINGS": "Налады", "SETTINGS_ARIA": "Адкрыць налады", "BUG_REPORT": "Паведаміць аб памылцы", "WHAT_IS_NEW": "Што новага?", @@ -804,10 +789,7 @@ "SEARCH": "Пошук", "RECENT": "Нядаўнія", "RECENTLY_ADDED": "Нядаўна дададзеныя", - "FAVORITES": "Выбранае", - "HOME": "Галоўная", - "PLAYLIST_INFO": "Інфармацыя аб плэйлісце", - "ACCOUNT_INFO": "Інфармацыя аб уліковым запісе" + "FAVORITES": "Выбранае" }, "ERROR_VIEW": { "IMPORT_CANCELLED": { diff --git a/apps/web/src/assets/i18n/de.json b/apps/web/src/assets/i18n/de.json index 25438869f..caf9b9bb8 100644 --- a/apps/web/src/assets/i18n/de.json +++ b/apps/web/src/assets/i18n/de.json @@ -192,10 +192,8 @@ "VALIDATING": "Portal wird überprüft …" }, "FILTER_BY_NAME": "Nach Namen filtern", - "FILTER_BY_TYPE": "Playlist-Typen", "FILTER_AND_SORT": "Filtern & Sortieren", "SORT_PLAYLISTS": "Playlisten sortieren", - "SORT_BY": "Sortieren nach", "SORT_OPTIONS": { "NAME_ASC": "Name (A-Z)", "NAME_DESC": "Name (Z-A)", @@ -483,7 +481,6 @@ "TOGGLE_SIDEBAR_TOOLTIP": "Senderliste umschalten (⌘/Strg+B)" }, "CHANNELS": { - "SEARCH_CHANNEL": "Nach Sender suchen", "UPLOAD_OR_SELECT_OTHER_PLAYLIST": "Neue Playlist hochladen oder eine andere aussuchen", "ALL_CHANNELS": "Alle Sender", "GROUPS": "Gruppen", @@ -557,14 +554,9 @@ } }, "TOP_MENU": { - "OPEN_CHANNELS_LIST": "Senderliste öffnen", - "TOGGLE_FAVORITE_FLAG": "Als Favoriten hinzufügen/entfernen", - "OPEN_EPG_LIST": "TV-Guide (EPG) öffnen", - "OPEN_MULTI_EPG": "Open Multi-EPG view", - "SHOW_INFO": "Programminfo anzeigen (Taste I)" + "OPEN_MULTI_EPG": "Open Multi-EPG view" }, "EPG": { - "TITLE": "Elektronischer Programmführer", "NEXT_DAY": "Zum nächsten Tag", "PREVIOUS_DAY": "Zum vorherigen Tag", "ZOOM_IN": "Vergrößern", @@ -573,14 +565,9 @@ "CLOSE_FILTER": "Filter schließen", "SEARCH_PROGRAMS": "Sendungen suchen", "CLOSE_PROGRAM_SEARCH": "Sendungssuche schließen", - "LIVE_NOW": "Live jetzt", "LIVE_STREAM": "Live stream", - "TIMESHIFT_AVAILABLE": "Die Timeshift-Funktion (Archiv) ist verfügbar, klicken Sie an um abzuspielen", "ARCHIVE_PLAYBACK_UNAVAILABLE_TITLE": "Archivwiedergabe nicht verfügbar", "ARCHIVE_PLAYBACK_UNAVAILABLE_DESCRIPTION": "Dieser Anbieter stellt zwar den Programmverlauf für den Sender bereit, jedoch keine abspielbare Archiv-/Catchup-Wiedergabe.", - "EPG_NOT_AVAILABLE_DATE": "Ups, leider ist das TV-Programm für das ausgewählte Datum nicht verfügbar", - "NO_GUIDE_HINT": "Versuchen Sie ein anderes Datum oder aktualisieren Sie die Playlist.", - "EPG_NOT_AVAILABLE_CHANNEL_TITLE": "Ups, leider ist das TV-Programm nicht verfügbar.", "EPG_NOT_AVAILABLE_CHANNEL_DESCRIPTION": "Versuchen Sie bitte die TV-Guide Einstellungen zu ändern.", "FETCH_EPG": "EPG-Daten werden geladen...", "ERROR": "Ooops, EPG konnte nicht geladen werden", @@ -615,7 +602,6 @@ "TRUST_TLS_HOST_WARNING": "Only continue if you trust this host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates.", "CURRENT_PROGRAM": "Current program", "ARCHIVE_PLAYBACK": "Archivwiedergabe", - "RETURN_TO_LIVE": "Zurück zum Live-TV", "COLLAPSE_PANEL": "Collapse EPG panel", "EXPAND_PANEL": "Expand EPG panel", "TIMELINE": { @@ -679,7 +665,6 @@ "MENU": { "OPEN": "Menü öffnen", "SUPPORT_ARIA": "IPTVnator auf GitHub unterstützen", - "SETTINGS": "Einstellungen", "SETTINGS_ARIA": "Einstellungen öffnen", "BUG_REPORT": "Fehler melden", "WHAT_IS_NEW": "Was ist neu", @@ -804,10 +789,7 @@ "SEARCH": "Suche", "RECENT": "Zuletzt angesehen", "RECENTLY_ADDED": "Kürzlich hinzugefügt", - "FAVORITES": "Favoriten", - "HOME": "Startseite", - "PLAYLIST_INFO": "Playlist-Info", - "ACCOUNT_INFO": "Account-Info" + "FAVORITES": "Favoriten" }, "ERROR_VIEW": { "IMPORT_CANCELLED": { diff --git a/apps/web/src/assets/i18n/el.json b/apps/web/src/assets/i18n/el.json index c552f9991..1eb16f157 100644 --- a/apps/web/src/assets/i18n/el.json +++ b/apps/web/src/assets/i18n/el.json @@ -192,10 +192,8 @@ "VALIDATING": "Επικύρωση πύλης..." }, "FILTER_BY_NAME": "Φιλτράρισμα κατά όνομα", - "FILTER_BY_TYPE": "Τύποι λιστών αναπαραγωγής", "FILTER_AND_SORT": "Φιλτράρισμα & Ταξινόμηση", "SORT_PLAYLISTS": "Ταξινόμηση λιστών αναπαραγωγής", - "SORT_BY": "Ταξινόμηση κατά", "SORT_OPTIONS": { "NAME_ASC": "Όνομα (A-Z)", "NAME_DESC": "Όνομα (Z-A)", @@ -483,7 +481,6 @@ "TOGGLE_SIDEBAR_TOOLTIP": "Εναλλαγή λίστας καναλιών (⌘/Ctrl+B)" }, "CHANNELS": { - "SEARCH_CHANNEL": "Αναζήτηση καναλιού", "UPLOAD_OR_SELECT_OTHER_PLAYLIST": "Μεταφορτώστε ή επιλέξτε άλλη λίστα αναπαραγωγής", "ALL_CHANNELS": "Όλα τα κανάλια", "GROUPS": "Ομάδες", @@ -557,14 +554,9 @@ } }, "TOP_MENU": { - "OPEN_CHANNELS_LIST": "Άνοιγμα λίστας καναλιών", - "TOGGLE_FAVORITE_FLAG": "Εναλλαγή σημαίας αγαπημένων", - "OPEN_EPG_LIST": "Άνοιγμα λίστας EPG", - "OPEN_MULTI_EPG": "Άνοιγμα προβολής Πολλαπλού EPG", - "SHOW_INFO": "Εμφάνιση πληροφοριών προγράμματος (Πατήστε το I)" + "OPEN_MULTI_EPG": "Άνοιγμα προβολής Πολλαπλού EPG" }, "EPG": { - "TITLE": "Ηλεκτρονικός Οδηγός Προγράμματος", "NEXT_DAY": "Επόμενη μέρα", "PREVIOUS_DAY": "Προηγούμενη ημέρα", "ZOOM_IN": "Μεγέθυνση", @@ -573,14 +565,9 @@ "CLOSE_FILTER": "Κλείσιμο φίλτρου", "SEARCH_PROGRAMS": "Αναζήτηση προγραμμάτων", "CLOSE_PROGRAM_SEARCH": "Κλείσιμο αναζήτησης προγραμμάτων", - "LIVE_NOW": "Ζωντανά τώρα", "LIVE_STREAM": "Ζωντανή ροή", - "TIMESHIFT_AVAILABLE": "Η χρονική μετατόπιση είναι διαθέσιμη, κάντε κλικ για αναπαραγωγή.", "ARCHIVE_PLAYBACK_UNAVAILABLE_TITLE": "Η αναπαραγωγή αρχείου δεν είναι διαθέσιμη", "ARCHIVE_PLAYBACK_UNAVAILABLE_DESCRIPTION": "Αυτός ο πάροχος εκθέτει ιστορικό προγράμματος για το κανάλι, αλλά δεν παρέχει αναπαραγωγή αρχείου/catch-up.", - "EPG_NOT_AVAILABLE_DATE": "Ωχ, το EPG δεν είναι διαθέσιμο για την επιλεγμένη ημερομηνία", - "NO_GUIDE_HINT": "Δοκιμάστε άλλη ημερομηνία ή ανανεώστε τη λίστα αναπαραγωγής.", - "EPG_NOT_AVAILABLE_CHANNEL_TITLE": "Ωχ, το EPG δεν είναι διαθέσιμο για αυτό το κανάλι.", "EPG_NOT_AVAILABLE_CHANNEL_DESCRIPTION": "Προσθέστε/αλλάξτε τη διεύθυνση URL της λίστας EPG στις ρυθμίσεις της εφαρμογής.", "FETCH_EPG": "Ανάκτηση δεδομένων EPG...", "ERROR": "Ωχ, δεν ήταν δυνατή η φόρτωση του EPG.", @@ -615,7 +602,6 @@ "TRUST_TLS_HOST_WARNING": "Only continue if you trust this host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates.", "CURRENT_PROGRAM": "Current program", "ARCHIVE_PLAYBACK": "Αναπαραγωγή αρχείου", - "RETURN_TO_LIVE": "Επιστροφή στη ζωντανή ροή", "COLLAPSE_PANEL": "Collapse EPG panel", "EXPAND_PANEL": "Expand EPG panel", "TIMELINE": { @@ -679,7 +665,6 @@ "MENU": { "OPEN": "Άνοιγμα μενού", "SUPPORT_ARIA": "Υποστήριξη του IPTVnator στο GitHub", - "SETTINGS": "Ρυθμίσεις", "SETTINGS_ARIA": "Άνοιγμα ρυθμίσεων", "BUG_REPORT": "Αναφορά σφάλματος", "WHAT_IS_NEW": "Τι νέο υπάρχει", @@ -804,10 +789,7 @@ "SEARCH": "Αναζήτηση", "RECENT": "Πρόσφατα", "RECENTLY_ADDED": "Πρόσφατα προστέθηκε", - "FAVORITES": "Αγαπημένα", - "HOME": "Αρχικό", - "PLAYLIST_INFO": "Πληροφορίες λίστας αναπαραγωγής", - "ACCOUNT_INFO": "Πληροφορίες λογαριασμού" + "FAVORITES": "Αγαπημένα" }, "ERROR_VIEW": { "IMPORT_CANCELLED": { diff --git a/apps/web/src/assets/i18n/en.json b/apps/web/src/assets/i18n/en.json index fcf8eedfa..a8820c9b5 100644 --- a/apps/web/src/assets/i18n/en.json +++ b/apps/web/src/assets/i18n/en.json @@ -192,10 +192,8 @@ "VALIDATING": "Validating portal..." }, "FILTER_BY_NAME": "Filter by name", - "FILTER_BY_TYPE": "Playlist Types", "FILTER_AND_SORT": "Filter & Sort", "SORT_PLAYLISTS": "Sort playlists", - "SORT_BY": "Sort By", "SORT_OPTIONS": { "NAME_ASC": "Name (A-Z)", "NAME_DESC": "Name (Z-A)", @@ -483,7 +481,6 @@ "TOGGLE_SIDEBAR_TOOLTIP": "Toggle channels list (⌘/Ctrl+B)" }, "CHANNELS": { - "SEARCH_CHANNEL": "Search channel", "UPLOAD_OR_SELECT_OTHER_PLAYLIST": "Upload or select other playlist", "ALL_CHANNELS": "All channels", "GROUPS": "Groups", @@ -557,14 +554,9 @@ } }, "TOP_MENU": { - "OPEN_CHANNELS_LIST": "Open channel list", - "TOGGLE_FAVORITE_FLAG": "Toggle favorite flag", - "OPEN_EPG_LIST": "Open EPG list", - "OPEN_MULTI_EPG": "Open Multi-EPG view", - "SHOW_INFO": "Show program info (Press I)" + "OPEN_MULTI_EPG": "Open Multi-EPG view" }, "EPG": { - "TITLE": "Electronic Program Guide", "NEXT_DAY": "Next day", "PREVIOUS_DAY": "Previous day", "ZOOM_IN": "Zoom in", @@ -573,14 +565,9 @@ "CLOSE_FILTER": "Close filter", "SEARCH_PROGRAMS": "Search programs", "CLOSE_PROGRAM_SEARCH": "Close program search", - "LIVE_NOW": "Live now", "LIVE_STREAM": "Live stream", - "TIMESHIFT_AVAILABLE": "Timeshift is available, click to play", "ARCHIVE_PLAYBACK_UNAVAILABLE_TITLE": "Archive playback is unavailable", "ARCHIVE_PLAYBACK_UNAVAILABLE_DESCRIPTION": "This provider exposes schedule history for the channel, but does not expose playable archive/catch-up playback.", - "EPG_NOT_AVAILABLE_DATE": "No program guide", - "NO_GUIDE_HINT": "Try a different date or refresh the playlist.", - "EPG_NOT_AVAILABLE_CHANNEL_TITLE": "Ooops, EPG is not available for this channel.", "EPG_NOT_AVAILABLE_CHANNEL_DESCRIPTION": "Please add/change the URL of the EPG list in the settings of the app.", "FETCH_EPG": "Fetching EPG data...", "ERROR": "Ooops, EPG could not be loaded.", @@ -615,7 +602,6 @@ "TRUST_TLS_HOST_WARNING": "Only continue if you trust this host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates.", "CURRENT_PROGRAM": "Current program", "ARCHIVE_PLAYBACK": "Archive playback", - "RETURN_TO_LIVE": "Return to live", "COLLAPSE_PANEL": "Collapse EPG panel", "EXPAND_PANEL": "Expand EPG panel", "TIMELINE": { @@ -679,7 +665,6 @@ "MENU": { "OPEN": "Open menu", "SUPPORT_ARIA": "Support IPTVnator on GitHub", - "SETTINGS": "Settings", "SETTINGS_ARIA": "Open settings", "BUG_REPORT": "Report a bug", "WHAT_IS_NEW": "What is new", @@ -804,10 +789,7 @@ "SEARCH": "Search", "RECENT": "Recent", "RECENTLY_ADDED": "Recently added", - "FAVORITES": "Favorites", - "HOME": "Home", - "PLAYLIST_INFO": "Playlist info", - "ACCOUNT_INFO": "Account info" + "FAVORITES": "Favorites" }, "ERROR_VIEW": { "IMPORT_CANCELLED": { diff --git a/apps/web/src/assets/i18n/es.json b/apps/web/src/assets/i18n/es.json index ef4d64e29..05620730c 100644 --- a/apps/web/src/assets/i18n/es.json +++ b/apps/web/src/assets/i18n/es.json @@ -192,10 +192,8 @@ "VALIDATING": "Validando portal…" }, "FILTER_BY_NAME": "Filtrar por nombre", - "FILTER_BY_TYPE": "Filtrar por tipo", "FILTER_AND_SORT": "Filtrar y ordenar", "SORT_PLAYLISTS": "Ordenar listas", - "SORT_BY": "Ordenar por", "SORT_OPTIONS": { "NAME_ASC": "Nombre (A-Z)", "NAME_DESC": "Nombre (Z-A)", @@ -483,7 +481,6 @@ "TOGGLE_SIDEBAR_TOOLTIP": "Alternar lista de canales (⌘/Ctrl+B)" }, "CHANNELS": { - "SEARCH_CHANNEL": "Buscar canal", "UPLOAD_OR_SELECT_OTHER_PLAYLIST": "Carga o selecciona una lista de reproducción", "ALL_CHANNELS": "Todos los canales", "GROUPS": "Grupos", @@ -557,14 +554,9 @@ } }, "TOP_MENU": { - "OPEN_CHANNELS_LIST": "Abrir lista de canales", - "TOGGLE_FAVORITE_FLAG": "Alternar bandera favorita", - "OPEN_EPG_LIST": "Abrir lista EPG", - "OPEN_MULTI_EPG": "Abrir vista Multi-EPG", - "SHOW_INFO": "Mostrar información del programa (presiona I)" + "OPEN_MULTI_EPG": "Abrir vista Multi-EPG" }, "EPG": { - "TITLE": "Guía electrónica de programación", "NEXT_DAY": "Día siguiente", "PREVIOUS_DAY": "Día anterior", "ZOOM_IN": "Acercar", @@ -573,14 +565,9 @@ "CLOSE_FILTER": "Cerrar filtro", "SEARCH_PROGRAMS": "Buscar programas", "CLOSE_PROGRAM_SEARCH": "Cerrar búsqueda de programas", - "LIVE_NOW": "Ahora en vivo", "LIVE_STREAM": "Transmisión en vivo", - "TIMESHIFT_AVAILABLE": "Timeshift está disponible, clic para reproducir", "ARCHIVE_PLAYBACK_UNAVAILABLE_TITLE": "La reproducción del archivo no está disponible", "ARCHIVE_PLAYBACK_UNAVAILABLE_DESCRIPTION": "Este proveedor expone el historial de programación del canal, pero no permite la reproducción del archivo/Catchup.", - "EPG_NOT_AVAILABLE_DATE": "Vaya, no está disponible la EPG de esa fecha", - "NO_GUIDE_HINT": "Prueba otra fecha o actualiza la lista de reproducción.", - "EPG_NOT_AVAILABLE_CHANNEL_TITLE": "Vaya, no está disponible la EPG del canal.", "EPG_NOT_AVAILABLE_CHANNEL_DESCRIPTION": "Por favor añada/cargue la url de la lista EPG en los ajustes de la app.", "FETCH_EPG": "Obteniendo datos de EPG...", "ERROR": "¡Vaya! Esta EPG no se pudo cargar.", @@ -615,7 +602,6 @@ "TRUST_TLS_HOST_WARNING": "Only continue if you trust this host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates.", "CURRENT_PROGRAM": "Current program", "ARCHIVE_PLAYBACK": "Reproducción de archivo", - "RETURN_TO_LIVE": "Volver al directo", "COLLAPSE_PANEL": "Collapse EPG panel", "EXPAND_PANEL": "Expand EPG panel", "TIMELINE": { @@ -679,7 +665,6 @@ "MENU": { "OPEN": "Abrir menú", "SUPPORT_ARIA": "Apoyar IPTVnator en GitHub", - "SETTINGS": "Ajustes", "SETTINGS_ARIA": "Abrir ajustes", "BUG_REPORT": "Reportar un bug", "WHAT_IS_NEW": "Novedades", @@ -804,10 +789,7 @@ "SEARCH": "Buscar", "RECENT": "Reciente", "RECENTLY_ADDED": "Añadidos recientemente", - "FAVORITES": "Favoritos", - "HOME": "Inicio", - "PLAYLIST_INFO": "Información de la lista", - "ACCOUNT_INFO": "Información de la cuenta" + "FAVORITES": "Favoritos" }, "ERROR_VIEW": { "IMPORT_CANCELLED": { diff --git a/apps/web/src/assets/i18n/fr.json b/apps/web/src/assets/i18n/fr.json index 16be1d351..ec727b4d0 100644 --- a/apps/web/src/assets/i18n/fr.json +++ b/apps/web/src/assets/i18n/fr.json @@ -192,10 +192,8 @@ "VALIDATING": "Validation du portail…" }, "FILTER_BY_NAME": "Filtrer par nom", - "FILTER_BY_TYPE": "Filtrer par type", "FILTER_AND_SORT": "Filtrer et trier", "SORT_PLAYLISTS": "Trier les listes", - "SORT_BY": "Trier par", "SORT_OPTIONS": { "NAME_ASC": "Nom (A-Z)", "NAME_DESC": "Nom (Z-A)", @@ -483,7 +481,6 @@ "TOGGLE_SIDEBAR_TOOLTIP": "Basculer la liste des chaînes (⌘/Ctrl+B)" }, "CHANNELS": { - "SEARCH_CHANNEL": "Rechercher une chaîne", "UPLOAD_OR_SELECT_OTHER_PLAYLIST": "Transférer ou choisir une autre liste de lecture", "ALL_CHANNELS": "Toutes les chaînes", "GROUPS": "Groupes", @@ -557,14 +554,9 @@ } }, "TOP_MENU": { - "OPEN_CHANNELS_LIST": "Ouvrir la liste des chaînes", - "TOGGLE_FAVORITE_FLAG": "Basculer le statut de favoris", - "OPEN_EPG_LIST": "Ouvrir la liste EPG", - "OPEN_MULTI_EPG": "Ouvrir la vue Multi-EPG", - "SHOW_INFO": "Afficher les infos du programme (Appuyez sur I)" + "OPEN_MULTI_EPG": "Ouvrir la vue Multi-EPG" }, "EPG": { - "TITLE": "Guide électronique des programmes", "NEXT_DAY": "Jour suivant", "PREVIOUS_DAY": "Jour précédent", "ZOOM_IN": "Zoom avant", @@ -573,14 +565,9 @@ "CLOSE_FILTER": "Fermer le filtre", "SEARCH_PROGRAMS": "Rechercher des programmes", "CLOSE_PROGRAM_SEARCH": "Fermer la recherche de programmes", - "LIVE_NOW": "En direct", "LIVE_STREAM": "Diffusion en direct", - "TIMESHIFT_AVAILABLE": "Contrôle du direct disponible, cliquez pour lire", "ARCHIVE_PLAYBACK_UNAVAILABLE_TITLE": "Lecture des archives indisponible", "ARCHIVE_PLAYBACK_UNAVAILABLE_DESCRIPTION": "Ce fournisseur expose l'historique des programmes pour la chaîne, mais n'expose pas la lecture archive/catch-up.", - "EPG_NOT_AVAILABLE_DATE": "L'EPG n'est pas disponible pour la date sélectionnée", - "NO_GUIDE_HINT": "Essayez une autre date ou actualisez la playlist.", - "EPG_NOT_AVAILABLE_CHANNEL_TITLE": "L'EPG n'est pas disponible pour cette chaîne.", "EPG_NOT_AVAILABLE_CHANNEL_DESCRIPTION": "Veuillez ajouter/modifier l'URL de la liste EPG dans les réglages de l'application.", "FETCH_EPG": "Récupération des données EPG...", "ERROR": "L'EPG n'a pas pu être chargé.", @@ -615,7 +602,6 @@ "TRUST_TLS_HOST_WARNING": "Only continue if you trust this host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates.", "CURRENT_PROGRAM": "Current program", "ARCHIVE_PLAYBACK": "Lecture archive", - "RETURN_TO_LIVE": "Retour au direct", "COLLAPSE_PANEL": "Collapse EPG panel", "EXPAND_PANEL": "Expand EPG panel", "TIMELINE": { @@ -679,7 +665,6 @@ "MENU": { "OPEN": "Ouvrir le menu", "SUPPORT_ARIA": "Soutenir IPTVnator sur GitHub", - "SETTINGS": "Paramètres", "SETTINGS_ARIA": "Ouvrir les paramètres", "BUG_REPORT": "Signaler un bug", "WHAT_IS_NEW": "Nouveautés", @@ -804,10 +789,7 @@ "SEARCH": "Rechercher", "RECENT": "Récent", "RECENTLY_ADDED": "Récemment ajoutés", - "FAVORITES": "Favoris", - "HOME": "Accueil", - "PLAYLIST_INFO": "Infos de la liste", - "ACCOUNT_INFO": "Infos du compte" + "FAVORITES": "Favoris" }, "ERROR_VIEW": { "IMPORT_CANCELLED": { diff --git a/apps/web/src/assets/i18n/it.json b/apps/web/src/assets/i18n/it.json index 77ba6bd57..89bdf6099 100644 --- a/apps/web/src/assets/i18n/it.json +++ b/apps/web/src/assets/i18n/it.json @@ -192,10 +192,8 @@ "VALIDATING": "Validazione del portale..." }, "FILTER_BY_NAME": "Filtra per nome", - "FILTER_BY_TYPE": "Filtra per tipo", "FILTER_AND_SORT": "Filtra e ordina", "SORT_PLAYLISTS": "Ordina playlist", - "SORT_BY": "Ordina per", "SORT_OPTIONS": { "NAME_ASC": "Nome (A-Z)", "NAME_DESC": "Nome (Z-A)", @@ -483,7 +481,6 @@ "TOGGLE_SIDEBAR_TOOLTIP": "Attiva/disattiva elenco canali (⌘/Ctrl+B)" }, "CHANNELS": { - "SEARCH_CHANNEL": "Cerca canale", "UPLOAD_OR_SELECT_OTHER_PLAYLIST": "Carica o seleziona un'altra playlist", "ALL_CHANNELS": "Tutti i canali", "GROUPS": "Gruppi", @@ -557,14 +554,9 @@ } }, "TOP_MENU": { - "OPEN_CHANNELS_LIST": "Apri elenco canali", - "TOGGLE_FAVORITE_FLAG": "Attiva/disattiva flag preferiti", - "OPEN_EPG_LIST": "Apri elenco EPG", - "OPEN_MULTI_EPG": "Apri vista Multi-EPG", - "SHOW_INFO": "Mostra info programma (premi I)" + "OPEN_MULTI_EPG": "Apri vista Multi-EPG" }, "EPG": { - "TITLE": "Guida elettronica programmi", "NEXT_DAY": "Giorno successivo", "PREVIOUS_DAY": "Giorno precedente", "ZOOM_IN": "Ingrandisci", @@ -573,14 +565,9 @@ "CLOSE_FILTER": "Chiudi filtro", "SEARCH_PROGRAMS": "Cerca programmi", "CLOSE_PROGRAM_SEARCH": "Chiudi ricerca programmi", - "LIVE_NOW": "Live attuale", "LIVE_STREAM": "Stream live", - "TIMESHIFT_AVAILABLE": "Disponibile il timeshift, fai clic per riprodurre", "ARCHIVE_PLAYBACK_UNAVAILABLE_TITLE": "Riproduzione archivio non disponibile", "ARCHIVE_PLAYBACK_UNAVAILABLE_DESCRIPTION": "Questo fornitore espone la cronologia della programmazione del canale, ma non la riproduzione di archivio/catch-up.", - "EPG_NOT_AVAILABLE_DATE": "Non è disponibile l'EPG per la data selezionata", - "NO_GUIDE_HINT": "Prova una data diversa o aggiorna la playlist.", - "EPG_NOT_AVAILABLE_CHANNEL_TITLE": "Non è disponibile l'EPG per questo canale.", "EPG_NOT_AVAILABLE_CHANNEL_DESCRIPTION": "Aggiungi/modifica l'URL dell'elenco EPG nelle impostazioni dell'app.", "FETCH_EPG": "Recupero dati EPG...", "ERROR": "Non è stato possibile caricare l'EPG.", @@ -615,7 +602,6 @@ "TRUST_TLS_HOST_WARNING": "Only continue if you trust this host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates.", "CURRENT_PROGRAM": "Current program", "ARCHIVE_PLAYBACK": "Riproduzione archivio", - "RETURN_TO_LIVE": "Torna alla diretta", "COLLAPSE_PANEL": "Collapse EPG panel", "EXPAND_PANEL": "Expand EPG panel", "TIMELINE": { @@ -679,7 +665,6 @@ "MENU": { "OPEN": "Apri menu", "SUPPORT_ARIA": "Supporta IPTVnator su GitHub", - "SETTINGS": "Impostazioni", "SETTINGS_ARIA": "Apri impostazioni", "BUG_REPORT": "Segnala un bug", "WHAT_IS_NEW": "Novità del programma", @@ -804,10 +789,7 @@ "SEARCH": "Cerca", "RECENT": "Recenti", "RECENTLY_ADDED": "Aggiunti di recente", - "FAVORITES": "Preferiti", - "HOME": "Home", - "PLAYLIST_INFO": "Info playlist", - "ACCOUNT_INFO": "Info account" + "FAVORITES": "Preferiti" }, "ERROR_VIEW": { "IMPORT_CANCELLED": { diff --git a/apps/web/src/assets/i18n/ja.json b/apps/web/src/assets/i18n/ja.json index 05abf58c3..7d5da5f74 100644 --- a/apps/web/src/assets/i18n/ja.json +++ b/apps/web/src/assets/i18n/ja.json @@ -192,10 +192,8 @@ "VALIDATING": "ポータルを検証中..." }, "FILTER_BY_NAME": "名前でフィルター", - "FILTER_BY_TYPE": "種類でフィルター", "FILTER_AND_SORT": "フィルターと並び替え", "SORT_PLAYLISTS": "プレイリストを並び替え", - "SORT_BY": "並び替え", "SORT_OPTIONS": { "NAME_ASC": "名前(A-Z)", "NAME_DESC": "名前(Z-A)", @@ -483,7 +481,6 @@ "TOGGLE_SIDEBAR_TOOLTIP": "チャンネル一覧の切り替え (⌘/Ctrl+B)" }, "CHANNELS": { - "SEARCH_CHANNEL": "チャンネルを検索", "UPLOAD_OR_SELECT_OTHER_PLAYLIST": "アップロードまたは他のプレイリストを選択", "ALL_CHANNELS": "すべてのチャンネル", "GROUPS": "グループ", @@ -557,14 +554,9 @@ } }, "TOP_MENU": { - "OPEN_CHANNELS_LIST": "チャンネルリストを開く", - "TOGGLE_FAVORITE_FLAG": "お気に入りフラグを切り替え", - "OPEN_EPG_LIST": "EPGリストを開く", - "OPEN_MULTI_EPG": "マルチEPGビューを開く", - "SHOW_INFO": "番組情報を表示(Iキー)" + "OPEN_MULTI_EPG": "マルチEPGビューを開く" }, "EPG": { - "TITLE": "電子番組表", "NEXT_DAY": "翌日", "PREVIOUS_DAY": "前日", "ZOOM_IN": "拡大", @@ -573,14 +565,9 @@ "CLOSE_FILTER": "フィルターを閉じる", "SEARCH_PROGRAMS": "番組を検索", "CLOSE_PROGRAM_SEARCH": "番組検索を閉じる", - "LIVE_NOW": "現在放送中", "LIVE_STREAM": "ライブストリーム", - "TIMESHIFT_AVAILABLE": "タイムシフトが利用可能です。クリックして再生", "ARCHIVE_PLAYBACK_UNAVAILABLE_TITLE": "アーカイブ再生は利用できません", "ARCHIVE_PLAYBACK_UNAVAILABLE_DESCRIPTION": "このプロバイダーはチャンネルの番組履歴を提供していますが、アーカイブ/Catchup再生には対応していません。", - "EPG_NOT_AVAILABLE_DATE": "申し訳ありません。選択した日付のEPGは利用できません", - "NO_GUIDE_HINT": "別の日付を試すか、プレイリストを更新してください。", - "EPG_NOT_AVAILABLE_CHANNEL_TITLE": "申し訳ありません。このチャンネルのEPGは利用できません。", "EPG_NOT_AVAILABLE_CHANNEL_DESCRIPTION": "アプリの設定でEPGリストのURLを追加/変更してください。", "FETCH_EPG": "EPGデータを取得中...", "ERROR": "申し訳ありません。EPGを読み込めませんでした。", @@ -615,7 +602,6 @@ "TRUST_TLS_HOST_WARNING": "Only continue if you trust this host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates.", "CURRENT_PROGRAM": "Current program", "ARCHIVE_PLAYBACK": "アーカイブ再生", - "RETURN_TO_LIVE": "ライブに戻る", "COLLAPSE_PANEL": "Collapse EPG panel", "EXPAND_PANEL": "Expand EPG panel", "TIMELINE": { @@ -679,7 +665,6 @@ "MENU": { "OPEN": "メニューを開く", "SUPPORT_ARIA": "GitHubでIPTVnatorをサポート", - "SETTINGS": "設定", "SETTINGS_ARIA": "設定を開く", "BUG_REPORT": "バグを報告", "WHAT_IS_NEW": "新着情報", @@ -804,10 +789,7 @@ "SEARCH": "検索", "RECENT": "最近", "RECENTLY_ADDED": "最近追加した項目", - "FAVORITES": "お気に入り", - "HOME": "ホーム", - "PLAYLIST_INFO": "プレイリスト情報", - "ACCOUNT_INFO": "アカウント情報" + "FAVORITES": "お気に入り" }, "ERROR_VIEW": { "IMPORT_CANCELLED": { diff --git a/apps/web/src/assets/i18n/ko.json b/apps/web/src/assets/i18n/ko.json index d8f4ddab9..6042470f0 100644 --- a/apps/web/src/assets/i18n/ko.json +++ b/apps/web/src/assets/i18n/ko.json @@ -192,10 +192,8 @@ "VALIDATING": "포털을 검증하는 중..." }, "FILTER_BY_NAME": "이름으로 필터", - "FILTER_BY_TYPE": "재생목록 유형", "FILTER_AND_SORT": "필터 및 정렬", "SORT_PLAYLISTS": "재생목록 정렬", - "SORT_BY": "정렬 기준", "SORT_OPTIONS": { "NAME_ASC": "이름 (A-Z)", "NAME_DESC": "이름 (Z-A)", @@ -483,7 +481,6 @@ "TOGGLE_SIDEBAR_TOOLTIP": "채널 목록 전환 (⌘/Ctrl+B)" }, "CHANNELS": { - "SEARCH_CHANNEL": "검색 채널", "UPLOAD_OR_SELECT_OTHER_PLAYLIST": "다른 재생 목록을 업로드하거나 선택하십시오", "ALL_CHANNELS": "모든 채널", "GROUPS": "그룹", @@ -557,14 +554,9 @@ } }, "TOP_MENU": { - "OPEN_CHANNELS_LIST": "채널 열기 목록", - "TOGGLE_FAVORITE_FLAG": "즐겨찾기 보이기/숨기기", - "OPEN_EPG_LIST": "EPG 목록 열기", - "OPEN_MULTI_EPG": "Open Multi-EPG view", - "SHOW_INFO": "프로그램 정보 표시 (I 키)" + "OPEN_MULTI_EPG": "Open Multi-EPG view" }, "EPG": { - "TITLE": "전자 프로그램 가이드", "NEXT_DAY": "다음날", "PREVIOUS_DAY": "전날", "ZOOM_IN": "확대", @@ -573,14 +565,9 @@ "CLOSE_FILTER": "필터 닫기", "SEARCH_PROGRAMS": "프로그램 검색", "CLOSE_PROGRAM_SEARCH": "프로그램 검색 닫기", - "LIVE_NOW": "Live now", "LIVE_STREAM": "Live stream", - "TIMESHIFT_AVAILABLE": "Timeshift를 사용할 수 있습니다, 클릭하여 재생합니다", "ARCHIVE_PLAYBACK_UNAVAILABLE_TITLE": "아카이브 재생을 사용할 수 없습니다", "ARCHIVE_PLAYBACK_UNAVAILABLE_DESCRIPTION": "이 공급자는 채널의 편성 이력을 제공하지만 재생 가능한 아카이브/Catchup 재생은 제공하지 않습니다.", - "EPG_NOT_AVAILABLE_DATE": "선택한 날짜에는 EPG를 사용할 수 없습니다.", - "NO_GUIDE_HINT": "다른 날짜를 시도하거나 재생 목록을 새로 고치세요.", - "EPG_NOT_AVAILABLE_CHANNEL_TITLE": "이 채널에는 EPG를 사용할 수 없습니다.", "EPG_NOT_AVAILABLE_CHANNEL_DESCRIPTION": "설정에서 EPG의 URL을 추가/변경하십시오.", "FETCH_EPG": "EPG 데이터 가져 오기...", "ERROR": "Ooops, EPG could not be loaded.", @@ -615,7 +602,6 @@ "TRUST_TLS_HOST_WARNING": "Only continue if you trust this host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates.", "CURRENT_PROGRAM": "Current program", "ARCHIVE_PLAYBACK": "아카이브 재생", - "RETURN_TO_LIVE": "라이브로 돌아가기", "COLLAPSE_PANEL": "Collapse EPG panel", "EXPAND_PANEL": "Expand EPG panel", "TIMELINE": { @@ -679,7 +665,6 @@ "MENU": { "OPEN": "Open menu", "SUPPORT_ARIA": "Support IPTVnator on GitHub", - "SETTINGS": "Settings", "SETTINGS_ARIA": "Open settings", "BUG_REPORT": "Report a bug", "WHAT_IS_NEW": "What is new", @@ -804,10 +789,7 @@ "SEARCH": "검색", "RECENT": "최근", "RECENTLY_ADDED": "최근 추가됨", - "FAVORITES": "즐겨찾기", - "HOME": "홈", - "PLAYLIST_INFO": "재생목록 정보", - "ACCOUNT_INFO": "계정 정보" + "FAVORITES": "즐겨찾기" }, "ERROR_VIEW": { "IMPORT_CANCELLED": { diff --git a/apps/web/src/assets/i18n/nl.json b/apps/web/src/assets/i18n/nl.json index 820747178..3245e41a3 100644 --- a/apps/web/src/assets/i18n/nl.json +++ b/apps/web/src/assets/i18n/nl.json @@ -192,10 +192,8 @@ "VALIDATING": "Portaal valideren..." }, "FILTER_BY_NAME": "Filter op naam", - "FILTER_BY_TYPE": "Filter op type", "FILTER_AND_SORT": "Filteren & sorteren", "SORT_PLAYLISTS": "Sorteer afspeellijsten", - "SORT_BY": "Sorteer op", "SORT_OPTIONS": { "NAME_ASC": "Naam (A-Z)", "NAME_DESC": "Naam (Z-A)", @@ -483,7 +481,6 @@ "TOGGLE_SIDEBAR_TOOLTIP": "Zenderlijst aan-/uitzetten (⌘/Ctrl+B)" }, "CHANNELS": { - "SEARCH_CHANNEL": "Zoek een kanaal", "UPLOAD_OR_SELECT_OTHER_PLAYLIST": "Upload of selecteer een andere afspeellijst", "ALL_CHANNELS": "Alle kanalen", "GROUPS": "Groepen", @@ -557,14 +554,9 @@ } }, "TOP_MENU": { - "OPEN_CHANNELS_LIST": "Open kanalenlijst", - "TOGGLE_FAVORITE_FLAG": "Wissel favorietenstatus", - "OPEN_EPG_LIST": "Open TV-gids lijst", - "OPEN_MULTI_EPG": "Open Multi TV-Gids weergave", - "SHOW_INFO": "Programma-info tonen (druk op I)" + "OPEN_MULTI_EPG": "Open Multi TV-Gids weergave" }, "EPG": { - "TITLE": "TV-Gids", "NEXT_DAY": "Volgende dag", "PREVIOUS_DAY": "Vorige dag", "ZOOM_IN": "Inzoomen", @@ -573,14 +565,9 @@ "CLOSE_FILTER": "Filter sluiten", "SEARCH_PROGRAMS": "Programma's zoeken", "CLOSE_PROGRAM_SEARCH": "Programmazoekopdracht sluiten", - "LIVE_NOW": "Nu live", "LIVE_STREAM": "Live stream", - "TIMESHIFT_AVAILABLE": "Doorspoelen is beschikbaar, klik om af te spelen", "ARCHIVE_PLAYBACK_UNAVAILABLE_TITLE": "Archiefweergave is niet beschikbaar", "ARCHIVE_PLAYBACK_UNAVAILABLE_DESCRIPTION": "Deze provider toont de programmageschiedenis voor het kanaal, maar biedt geen afspeelbare archief-/catch-up-weergave.", - "EPG_NOT_AVAILABLE_DATE": "De TV-Gids is niet beschikbaar voor de geselecteerde datum", - "NO_GUIDE_HINT": "Probeer een andere datum of vernieuw de afspeellijst.", - "EPG_NOT_AVAILABLE_CHANNEL_TITLE": "De TV-Gids is niet beschikbaar voor dit kanaal.", "EPG_NOT_AVAILABLE_CHANNEL_DESCRIPTION": "Voeg een TV-Gids URL toe of wijzig deze in de instellingen van de app.", "FETCH_EPG": "Ophalen TV-Gids...", "ERROR": "TV-Gids kan niet geladen worden.", @@ -615,7 +602,6 @@ "TRUST_TLS_HOST_WARNING": "Only continue if you trust this host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates.", "CURRENT_PROGRAM": "Current program", "ARCHIVE_PLAYBACK": "Archief afspelen", - "RETURN_TO_LIVE": "Terug naar live", "COLLAPSE_PANEL": "Collapse EPG panel", "EXPAND_PANEL": "Expand EPG panel", "TIMELINE": { @@ -679,7 +665,6 @@ "MENU": { "OPEN": "Open menu", "SUPPORT_ARIA": "Ondersteun IPTVnator op GitHub", - "SETTINGS": "Instellingen", "SETTINGS_ARIA": "Open instellingen", "BUG_REPORT": "Meld een bug", "WHAT_IS_NEW": "Wat is er nieuw", @@ -804,10 +789,7 @@ "SEARCH": "Zoeken", "RECENT": "Recent", "RECENTLY_ADDED": "Recent toegevoegd", - "FAVORITES": "Favorieten", - "HOME": "Home", - "PLAYLIST_INFO": "Afspeellijstinfo", - "ACCOUNT_INFO": "Accountinfo" + "FAVORITES": "Favorieten" }, "ERROR_VIEW": { "IMPORT_CANCELLED": { diff --git a/apps/web/src/assets/i18n/pl.json b/apps/web/src/assets/i18n/pl.json index 536538da8..f81d459ee 100644 --- a/apps/web/src/assets/i18n/pl.json +++ b/apps/web/src/assets/i18n/pl.json @@ -192,10 +192,8 @@ "VALIDATING": "Walidacja portalu..." }, "FILTER_BY_NAME": "Filtruj według nazwy", - "FILTER_BY_TYPE": "Filtruj według typu", "FILTER_AND_SORT": "Filtruj i sortuj", "SORT_PLAYLISTS": "Sortuj listy odtwarzania", - "SORT_BY": "Sortuj według", "SORT_OPTIONS": { "NAME_ASC": "Nazwa (A-Z)", "NAME_DESC": "Nazwa (Z-A)", @@ -483,7 +481,6 @@ "TOGGLE_SIDEBAR_TOOLTIP": "Przełącz listę kanałów (⌘/Ctrl+B)" }, "CHANNELS": { - "SEARCH_CHANNEL": "Szukaj kanału", "UPLOAD_OR_SELECT_OTHER_PLAYLIST": "Prześlij lub wybierz inną listę odtwarzania", "ALL_CHANNELS": "Wszystkie kanały", "GROUPS": "Grupy", @@ -557,14 +554,9 @@ } }, "TOP_MENU": { - "OPEN_CHANNELS_LIST": "Otwórz listę kanałów", - "TOGGLE_FAVORITE_FLAG": "Przełącz flagę ulubionych", - "OPEN_EPG_LIST": "Otwórz listę EPG", - "OPEN_MULTI_EPG": "Otwórz widok Multi-EPG", - "SHOW_INFO": "Pokaż informacje o programie (naciśnij I)" + "OPEN_MULTI_EPG": "Otwórz widok Multi-EPG" }, "EPG": { - "TITLE": "Elektroniczny przewodnik po programach", "NEXT_DAY": "Następny dzień", "PREVIOUS_DAY": "Poprzedni dzień", "ZOOM_IN": "Powiększ", @@ -573,14 +565,9 @@ "CLOSE_FILTER": "Zamknij filtr", "SEARCH_PROGRAMS": "Szukaj programów", "CLOSE_PROGRAM_SEARCH": "Zamknij wyszukiwanie programów", - "LIVE_NOW": "Na żywo teraz", "LIVE_STREAM": "Transmisja na żywo", - "TIMESHIFT_AVAILABLE": "Dostępne jest przesunięcie czasowe, kliknij, aby odtworzyć", "ARCHIVE_PLAYBACK_UNAVAILABLE_TITLE": "Odtwarzanie archiwum niedostępne", "ARCHIVE_PLAYBACK_UNAVAILABLE_DESCRIPTION": "Ten dostawca udostępnia historię programu dla kanału, ale nie udostępnia odtwarzalnego archiwum/catch-up.", - "EPG_NOT_AVAILABLE_DATE": "Ups, EPG nie jest dostępne dla wybranej daty", - "NO_GUIDE_HINT": "Spróbuj innej daty lub odśwież playlistę.", - "EPG_NOT_AVAILABLE_CHANNEL_TITLE": "Ups, EPG nie jest dostępne dla tego kanału.", "EPG_NOT_AVAILABLE_CHANNEL_DESCRIPTION": "Dodaj/zmień URL listy EPG w ustawieniach aplikacji.", "FETCH_EPG": "Pobieranie danych EPG...", "ERROR": "Ups, nie można załadować EPG.", @@ -615,7 +602,6 @@ "TRUST_TLS_HOST_WARNING": "Only continue if you trust this host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates.", "CURRENT_PROGRAM": "Current program", "ARCHIVE_PLAYBACK": "Odtwarzanie archiwum", - "RETURN_TO_LIVE": "Wróć do transmisji na żywo", "COLLAPSE_PANEL": "Collapse EPG panel", "EXPAND_PANEL": "Expand EPG panel", "TIMELINE": { @@ -679,7 +665,6 @@ "MENU": { "OPEN": "Otwórz menu", "SUPPORT_ARIA": "Wsparcie IPTVnator na GitHubie", - "SETTINGS": "Ustawienia", "SETTINGS_ARIA": "Otwórz ustawienia", "BUG_REPORT": "Zgłoś błąd", "WHAT_IS_NEW": "Co nowego", @@ -804,10 +789,7 @@ "SEARCH": "Szukaj", "RECENT": "Ostatnie", "RECENTLY_ADDED": "Ostatnio dodane", - "FAVORITES": "Ulubione", - "HOME": "Strona główna", - "PLAYLIST_INFO": "Informacje o liście odtwarzania", - "ACCOUNT_INFO": "Informacje o koncie" + "FAVORITES": "Ulubione" }, "ERROR_VIEW": { "IMPORT_CANCELLED": { diff --git a/apps/web/src/assets/i18n/pt.json b/apps/web/src/assets/i18n/pt.json index 6a17c720b..e5e2b2415 100644 --- a/apps/web/src/assets/i18n/pt.json +++ b/apps/web/src/assets/i18n/pt.json @@ -192,10 +192,8 @@ "VALIDATING": "Validando portal..." }, "FILTER_BY_NAME": "Filtrar por nome", - "FILTER_BY_TYPE": "Filtrar por tipo", "FILTER_AND_SORT": "Filtrar e ordenar", "SORT_PLAYLISTS": "Ordenar playlists", - "SORT_BY": "Ordenar por", "SORT_OPTIONS": { "NAME_ASC": "Nome (A-Z)", "NAME_DESC": "Nome (Z-A)", @@ -483,7 +481,6 @@ "TOGGLE_SIDEBAR_TOOLTIP": "Alternar lista de canais (⌘/Ctrl+B)" }, "CHANNELS": { - "SEARCH_CHANNEL": "Pesquisar canal", "UPLOAD_OR_SELECT_OTHER_PLAYLIST": "Enviar ou selecionar outra playlist", "ALL_CHANNELS": "Todos os canais", "GROUPS": "Grupos", @@ -557,14 +554,9 @@ } }, "TOP_MENU": { - "OPEN_CHANNELS_LIST": "Abrir lista de canais", - "TOGGLE_FAVORITE_FLAG": "Alternar sinalizador de favorito", - "OPEN_EPG_LIST": "Abrir lista de EPG", - "OPEN_MULTI_EPG": "Abrir visualização Multi-EPG", - "SHOW_INFO": "Mostrar informações do programa (pressione I)" + "OPEN_MULTI_EPG": "Abrir visualização Multi-EPG" }, "EPG": { - "TITLE": "Guia Eletrônico de Programação", "NEXT_DAY": "Próximo dia", "PREVIOUS_DAY": "Dia anterior", "ZOOM_IN": "Mais zoom", @@ -573,14 +565,9 @@ "CLOSE_FILTER": "Fechar filtro", "SEARCH_PROGRAMS": "Pesquisar programas", "CLOSE_PROGRAM_SEARCH": "Fechar pesquisa de programas", - "LIVE_NOW": "Ao vivo agora", "LIVE_STREAM": "Transmissão ao vivo", - "TIMESHIFT_AVAILABLE": "Timeshift está disponível, clique para reproduzir", "ARCHIVE_PLAYBACK_UNAVAILABLE_TITLE": "Reprodução de arquivo indisponível", "ARCHIVE_PLAYBACK_UNAVAILABLE_DESCRIPTION": "Este provedor expõe o histórico de programação do canal, mas não disponibiliza reprodução de arquivo/Catchup.", - "EPG_NOT_AVAILABLE_DATE": "Ooops, EPG não está disponível para a data selecionada", - "NO_GUIDE_HINT": "Tente uma data diferente ou atualize a lista de reprodução.", - "EPG_NOT_AVAILABLE_CHANNEL_TITLE": "Ooops, EPG não está disponível para este canal.", "EPG_NOT_AVAILABLE_CHANNEL_DESCRIPTION": "Adicione/altere a URL da lista de EPG nas configurações do aplicativo.", "FETCH_EPG": "Buscando dados do EPG...", "ERROR": "Ooops, EPG não pôde ser carregado.", @@ -615,7 +602,6 @@ "TRUST_TLS_HOST_WARNING": "Only continue if you trust this host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates.", "CURRENT_PROGRAM": "Current program", "ARCHIVE_PLAYBACK": "Reprodução do arquivo", - "RETURN_TO_LIVE": "Voltar ao vivo", "COLLAPSE_PANEL": "Collapse EPG panel", "EXPAND_PANEL": "Expand EPG panel", "TIMELINE": { @@ -679,7 +665,6 @@ "MENU": { "OPEN": "Abrir menu", "SUPPORT_ARIA": "Apoie o IPTVnator no GitHub", - "SETTINGS": "Configurações", "SETTINGS_ARIA": "Abrir configurações", "BUG_REPORT": "Reportar um bug", "WHAT_IS_NEW": "O que há de novo", @@ -804,10 +789,7 @@ "SEARCH": "Pesquisar", "RECENT": "Recente", "RECENTLY_ADDED": "Adicionados recentemente", - "FAVORITES": "Favoritos", - "HOME": "Início", - "PLAYLIST_INFO": "Informações da playlist", - "ACCOUNT_INFO": "Informações da conta" + "FAVORITES": "Favoritos" }, "ERROR_VIEW": { "IMPORT_CANCELLED": { diff --git a/apps/web/src/assets/i18n/ru.json b/apps/web/src/assets/i18n/ru.json index 8e33f5544..865c8ee6c 100644 --- a/apps/web/src/assets/i18n/ru.json +++ b/apps/web/src/assets/i18n/ru.json @@ -192,10 +192,8 @@ "VALIDATING": "Проверка портала…" }, "FILTER_BY_NAME": "Фильтровать по имени", - "FILTER_BY_TYPE": "Типы плейлистов", "FILTER_AND_SORT": "Фильтр и сортировка", "SORT_PLAYLISTS": "Сортировать плейлисты", - "SORT_BY": "Сортировать по", "SORT_OPTIONS": { "NAME_ASC": "Имени (А-Я)", "NAME_DESC": "Имени (Я-А)", @@ -483,7 +481,6 @@ "TOGGLE_SIDEBAR_TOOLTIP": "Переключить список каналов (⌘/Ctrl+B)" }, "CHANNELS": { - "SEARCH_CHANNEL": "Поиск каналов", "UPLOAD_OR_SELECT_OTHER_PLAYLIST": "Загрузить или выбрать другой плейлист", "ALL_CHANNELS": "Все каналы", "GROUPS": "Группы", @@ -557,14 +554,9 @@ } }, "TOP_MENU": { - "OPEN_CHANNELS_LIST": "Открыть список каналов", - "TOGGLE_FAVORITE_FLAG": "Избранный", - "OPEN_EPG_LIST": "Открыть программу (EPG)", - "OPEN_MULTI_EPG": "Открыть Multi-EPG вид", - "SHOW_INFO": "Показать информацию о программе (нажмите I)" + "OPEN_MULTI_EPG": "Открыть Multi-EPG вид" }, "EPG": { - "TITLE": "Электронный программный гид", "NEXT_DAY": "Следующий день", "PREVIOUS_DAY": "Предыдущий день", "ZOOM_IN": "Увеличить", @@ -573,14 +565,9 @@ "CLOSE_FILTER": "Закрыть фильтр", "SEARCH_PROGRAMS": "Поиск программ", "CLOSE_PROGRAM_SEARCH": "Закрыть поиск программ", - "LIVE_NOW": "Сейчас", "LIVE_STREAM": "Прямая трансляция", - "TIMESHIFT_AVAILABLE": "Доступна функция архива (timeshift). Нажмите на передачу для воспроизведения.", "ARCHIVE_PLAYBACK_UNAVAILABLE_TITLE": "Воспроизведение архива недоступно", "ARCHIVE_PLAYBACK_UNAVAILABLE_DESCRIPTION": "Этот провайдер предоставляет историю программы для канала, но не предоставляет воспроизводимый архив/catch-up.", - "EPG_NOT_AVAILABLE_DATE": "Ой, программа на этот день, к сожалению, отсутствует.", - "NO_GUIDE_HINT": "Попробуйте другую дату или обновите плейлист.", - "EPG_NOT_AVAILABLE_CHANNEL_TITLE": "Программа для этого канала недоступна.", "EPG_NOT_AVAILABLE_CHANNEL_DESCRIPTION": "Попробуйте изменить или обновить ссылку на EPG в настройках приложения.", "FETCH_EPG": "Загружаю программу ТВ-передач (EPG)...", "ERROR": "Ой, не удалось загрузить программу каналов (EPG)", @@ -615,7 +602,6 @@ "TRUST_TLS_HOST_WARNING": "Only continue if you trust this host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates.", "CURRENT_PROGRAM": "Current program", "ARCHIVE_PLAYBACK": "Архивная запись", - "RETURN_TO_LIVE": "Вернуться к эфиру", "COLLAPSE_PANEL": "Collapse EPG panel", "EXPAND_PANEL": "Expand EPG panel", "TIMELINE": { @@ -679,7 +665,6 @@ "MENU": { "OPEN": "Открыть меню", "SUPPORT_ARIA": "Поддержать IPTVnator на GitHub", - "SETTINGS": "Настройки", "SETTINGS_ARIA": "Открыть настройки", "BUG_REPORT": "Сообщить об ошибке", "WHAT_IS_NEW": "Что нового?", @@ -804,10 +789,7 @@ "SEARCH": "Поиск", "RECENT": "Недавние", "RECENTLY_ADDED": "Недавно добавленные", - "FAVORITES": "Избранное", - "HOME": "Домой", - "PLAYLIST_INFO": "Информация о плейлисте", - "ACCOUNT_INFO": "Информация об аккаунте" + "FAVORITES": "Избранное" }, "ERROR_VIEW": { "IMPORT_CANCELLED": { diff --git a/apps/web/src/assets/i18n/tr.json b/apps/web/src/assets/i18n/tr.json index 983481f44..8c7c27e90 100644 --- a/apps/web/src/assets/i18n/tr.json +++ b/apps/web/src/assets/i18n/tr.json @@ -192,10 +192,8 @@ "VALIDATING": "Portal doğrulanıyor..." }, "FILTER_BY_NAME": "İsime göre filtrele", - "FILTER_BY_TYPE": "Türe göre filtrele", "FILTER_AND_SORT": "Filtrele ve Sırala", "SORT_PLAYLISTS": "Oynatma listelerini sırala", - "SORT_BY": "Sıralama ölçütü", "SORT_OPTIONS": { "NAME_ASC": "İsim (A-Z)", "NAME_DESC": "İsim (Z-A)", @@ -483,7 +481,6 @@ "TOGGLE_SIDEBAR_TOOLTIP": "Kanal listesini aç/kapat (⌘/Ctrl+B)" }, "CHANNELS": { - "SEARCH_CHANNEL": "Kanal ara", "UPLOAD_OR_SELECT_OTHER_PLAYLIST": "Dosya yükle veya başka bir oynatma listesi seçin", "ALL_CHANNELS": "Tüm kanallar", "GROUPS": "Gruplar", @@ -557,14 +554,9 @@ } }, "TOP_MENU": { - "OPEN_CHANNELS_LIST": "Kanal listesini aç", - "TOGGLE_FAVORITE_FLAG": "Favori işaretini değiştir", - "OPEN_EPG_LIST": "EPG listesini aç", - "OPEN_MULTI_EPG": "Çoklu EPG görünümünü aç", - "SHOW_INFO": "Program bilgisini göster (I tuşuna basın)" + "OPEN_MULTI_EPG": "Çoklu EPG görünümünü aç" }, "EPG": { - "TITLE": "Elektronik Program Rehberi", "NEXT_DAY": "Sonraki gün", "PREVIOUS_DAY": "Önceki gün", "ZOOM_IN": "Yakınlaştır", @@ -573,14 +565,9 @@ "CLOSE_FILTER": "Filtreyi kapat", "SEARCH_PROGRAMS": "Programları ara", "CLOSE_PROGRAM_SEARCH": "Program aramayı kapat", - "LIVE_NOW": "Şu an canlı", "LIVE_STREAM": "Canlı yayın", - "TIMESHIFT_AVAILABLE": "Zaman kaydırma mevcut, tıklayarak oynat", "ARCHIVE_PLAYBACK_UNAVAILABLE_TITLE": "Arşiv oynatma kullanılamıyor", "ARCHIVE_PLAYBACK_UNAVAILABLE_DESCRIPTION": "Bu sağlayıcı kanal için yayın akışı geçmişini sunar, ancak oynatılabilir arşiv/Catchup oynatma sunmaz.", - "EPG_NOT_AVAILABLE_DATE": "Üzgünüz, seçilen tarih için EPG mevcut değil", - "NO_GUIDE_HINT": "Başka bir tarih deneyin veya oynatma listesini yenileyin.", - "EPG_NOT_AVAILABLE_CHANNEL_TITLE": "Üzgünüz, bu kanal için EPG mevcut değil.", "EPG_NOT_AVAILABLE_CHANNEL_DESCRIPTION": "Lütfen uygulamanın ayarlarında EPG listesi URL'sini ekleyin veya değiştirin.", "FETCH_EPG": "EPG verileri alınıyor...", "ERROR": "Üzgünüz, EPG yüklenemedi.", @@ -615,7 +602,6 @@ "TRUST_TLS_HOST_WARNING": "Only continue if you trust this host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates.", "CURRENT_PROGRAM": "Current program", "ARCHIVE_PLAYBACK": "Arşiv oynatma", - "RETURN_TO_LIVE": "Canlı yayına dön", "COLLAPSE_PANEL": "Collapse EPG panel", "EXPAND_PANEL": "Expand EPG panel", "TIMELINE": { @@ -679,7 +665,6 @@ "MENU": { "OPEN": "Menüyü aç", "SUPPORT_ARIA": "IPTVnator'u GitHub'da destekleyin", - "SETTINGS": "Ayarlar", "SETTINGS_ARIA": "Ayarları aç", "BUG_REPORT": "Hata bildir", "WHAT_IS_NEW": "Yeni ne var?", @@ -804,10 +789,7 @@ "SEARCH": "Ara", "RECENT": "Son İzlenenler", "RECENTLY_ADDED": "Yeni Eklenenler", - "FAVORITES": "Favoriler", - "HOME": "Ana Sayfa", - "PLAYLIST_INFO": "Oynatma Listesi Bilgisi", - "ACCOUNT_INFO": "Hesap Bilgisi" + "FAVORITES": "Favoriler" }, "ERROR_VIEW": { "IMPORT_CANCELLED": { diff --git a/apps/web/src/assets/i18n/zh.json b/apps/web/src/assets/i18n/zh.json index c867c4c64..764fad70e 100644 --- a/apps/web/src/assets/i18n/zh.json +++ b/apps/web/src/assets/i18n/zh.json @@ -192,10 +192,8 @@ "VALIDATING": "正在验证门户…" }, "FILTER_BY_NAME": "按名称筛选", - "FILTER_BY_TYPE": "播放列表类型", "FILTER_AND_SORT": "筛选与排序", "SORT_PLAYLISTS": "排序播放列表", - "SORT_BY": "排序方式", "SORT_OPTIONS": { "NAME_ASC": "名称(A-Z)", "NAME_DESC": "名称(Z-A)", @@ -483,7 +481,6 @@ "TOGGLE_SIDEBAR_TOOLTIP": "切换频道列表 (⌘/Ctrl+B)" }, "CHANNELS": { - "SEARCH_CHANNEL": "搜索频道", "UPLOAD_OR_SELECT_OTHER_PLAYLIST": "上传或选择其他播放列表", "ALL_CHANNELS": "所有频道", "GROUPS": "分组", @@ -557,14 +554,9 @@ } }, "TOP_MENU": { - "OPEN_CHANNELS_LIST": "打开频道列表", - "TOGGLE_FAVORITE_FLAG": "切换收藏夹标志", - "OPEN_EPG_LIST": "打开电子节目单EPG列表", - "OPEN_MULTI_EPG": "打开多EPG显示", - "SHOW_INFO": "显示节目信息(按 I 键)" + "OPEN_MULTI_EPG": "打开多EPG显示" }, "EPG": { - "TITLE": "电子节目指南", "NEXT_DAY": "明天", "PREVIOUS_DAY": "昨天", "ZOOM_IN": "放大", @@ -573,14 +565,9 @@ "CLOSE_FILTER": "关闭筛选", "SEARCH_PROGRAMS": "搜索节目", "CLOSE_PROGRAM_SEARCH": "关闭节目搜索", - "LIVE_NOW": "正在播放", "LIVE_STREAM": "正在直播", - "TIMESHIFT_AVAILABLE": "时移功能可用,点击播放", "ARCHIVE_PLAYBACK_UNAVAILABLE_TITLE": "归档播放不可用", "ARCHIVE_PLAYBACK_UNAVAILABLE_DESCRIPTION": "此提供商提供了该频道的节目历史,但不提供可播放的归档/Catchup 回看。", - "EPG_NOT_AVAILABLE_DATE": "错误:电子节目单EPG在所选日期不可用", - "NO_GUIDE_HINT": "尝试其他日期或刷新播放列表。", - "EPG_NOT_AVAILABLE_CHANNEL_TITLE": "错误:电子节目单EPG不适用于此频道", "EPG_NOT_AVAILABLE_CHANNEL_DESCRIPTION": "请添加/更改设置项中电子节目单的URL", "FETCH_EPG": "获取 EPG 数据...", "ERROR": "错误: 获取 EPG 数据失败", @@ -615,7 +602,6 @@ "TRUST_TLS_HOST_WARNING": "Only continue if you trust this host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates.", "CURRENT_PROGRAM": "Current program", "ARCHIVE_PLAYBACK": "回看播放", - "RETURN_TO_LIVE": "返回直播", "COLLAPSE_PANEL": "Collapse EPG panel", "EXPAND_PANEL": "Expand EPG panel", "TIMELINE": { @@ -679,7 +665,6 @@ "MENU": { "OPEN": "打开菜单", "SUPPORT_ARIA": "在 GitHub 上支持 IPTVnator", - "SETTINGS": "设置", "SETTINGS_ARIA": "打开设置", "BUG_REPORT": "问题反馈", "WHAT_IS_NEW": "更新日志", @@ -804,10 +789,7 @@ "SEARCH": "搜索", "RECENT": "最近", "RECENTLY_ADDED": "最近添加", - "FAVORITES": "收藏", - "HOME": "首页", - "PLAYLIST_INFO": "播放列表信息", - "ACCOUNT_INFO": "账户信息" + "FAVORITES": "收藏" }, "ERROR_VIEW": { "IMPORT_CANCELLED": { diff --git a/apps/web/src/assets/i18n/zhtw.json b/apps/web/src/assets/i18n/zhtw.json index a88dcda85..3d3fe0467 100644 --- a/apps/web/src/assets/i18n/zhtw.json +++ b/apps/web/src/assets/i18n/zhtw.json @@ -192,10 +192,8 @@ "VALIDATING": "正在驗證入口網站..." }, "FILTER_BY_NAME": "依名稱篩選", - "FILTER_BY_TYPE": "播放清單類型", "FILTER_AND_SORT": "篩選與排序", "SORT_PLAYLISTS": "排序播放清單", - "SORT_BY": "排序依據", "SORT_OPTIONS": { "NAME_ASC": "名稱(A-Z)", "NAME_DESC": "名稱(Z-A)", @@ -483,7 +481,6 @@ "TOGGLE_SIDEBAR_TOOLTIP": "切換頻道列表 (⌘/Ctrl+B)" }, "CHANNELS": { - "SEARCH_CHANNEL": "搜尋頻道", "UPLOAD_OR_SELECT_OTHER_PLAYLIST": "上傳或選擇其他播放清單", "ALL_CHANNELS": "所有頻道", "GROUPS": "群組", @@ -557,14 +554,9 @@ } }, "TOP_MENU": { - "OPEN_CHANNELS_LIST": "打開頻道列表", - "TOGGLE_FAVORITE_FLAG": "切換我的最愛標記", - "OPEN_EPG_LIST": "開啟 EPG 列表", - "OPEN_MULTI_EPG": "開啟多 EPG 視圖", - "SHOW_INFO": "顯示節目資訊(按 I 鍵)" + "OPEN_MULTI_EPG": "開啟多 EPG 視圖" }, "EPG": { - "TITLE": "電子節目指南", "NEXT_DAY": "下一天", "PREVIOUS_DAY": "上一天", "ZOOM_IN": "放大", @@ -573,14 +565,9 @@ "CLOSE_FILTER": "關閉篩選", "SEARCH_PROGRAMS": "搜尋節目", "CLOSE_PROGRAM_SEARCH": "關閉節目搜尋", - "LIVE_NOW": "現在播放", "LIVE_STREAM": "現場直播", - "TIMESHIFT_AVAILABLE": "時間平移功能可用,點選以播放", "ARCHIVE_PLAYBACK_UNAVAILABLE_TITLE": "無法播放存檔內容", "ARCHIVE_PLAYBACK_UNAVAILABLE_DESCRIPTION": "此供應商有提供本頻道的歷史節目表,但未提供可播放的存檔/Catchup 播放功能。", - "EPG_NOT_AVAILABLE_DATE": "抱歉,選擇的日期上 EPG 無法使用", - "NO_GUIDE_HINT": "嘗試其他日期或重新整理播放清單。", - "EPG_NOT_AVAILABLE_CHANNEL_TITLE": "抱歉,此頻道上的 EPG 無法使用。", "EPG_NOT_AVAILABLE_CHANNEL_DESCRIPTION": "請在應用程式設定中增加/更改 EPG 列表的URL。", "FETCH_EPG": "取得 EPG 資料...", "ERROR": "抱歉,無法載入 EPG。", @@ -615,7 +602,6 @@ "TRUST_TLS_HOST_WARNING": "Only continue if you trust this host. IPTVnator will allow invalid TLS certificates for this host, but other hosts still require valid certificates.", "CURRENT_PROGRAM": "Current program", "ARCHIVE_PLAYBACK": "回看播放", - "RETURN_TO_LIVE": "返回直播", "COLLAPSE_PANEL": "Collapse EPG panel", "EXPAND_PANEL": "Expand EPG panel", "TIMELINE": { @@ -679,7 +665,6 @@ "MENU": { "OPEN": "開啟選單", "SUPPORT_ARIA": "在 GitHub 上支持 IPTVnator", - "SETTINGS": "設定", "SETTINGS_ARIA": "開啟設定", "BUG_REPORT": "回報問題", "WHAT_IS_NEW": "更新內容", @@ -804,10 +789,7 @@ "SEARCH": "搜尋", "RECENT": "最近觀看", "RECENTLY_ADDED": "最近新增", - "FAVORITES": "我的最愛", - "HOME": "首頁", - "PLAYLIST_INFO": "播放清單資訊", - "ACCOUNT_INFO": "帳戶資訊" + "FAVORITES": "我的最愛" }, "ERROR_VIEW": { "IMPORT_CANCELLED": { diff --git a/apps/web/src/styles.scss b/apps/web/src/styles.scss index c4d00706e..60e038639 100644 --- a/apps/web/src/styles.scss +++ b/apps/web/src/styles.scss @@ -96,10 +96,6 @@ app-root.macos-platform .current-playlist { padding-top: 30px; } -app-root.macos-platform app-navigation .portal-status-container { - padding-top: 30px; -} - // Windows/Linux frameless title bar: reserve room in top-aligned drag // regions for the renderer-drawn window controls (3 × 46px buttons). // Body-level class so it also reaches cdk-overlay content (multi-EPG). diff --git a/docs/architecture/iptvnator-ui-guidelines.md b/docs/architecture/iptvnator-ui-guidelines.md index 810785905..eec478e0d 100644 --- a/docs/architecture/iptvnator-ui-guidelines.md +++ b/docs/architecture/iptvnator-ui-guidelines.md @@ -141,12 +141,11 @@ The shared row should be reused instead of rebuilding channel markup per view. ### Collapsible Live EPG -- Live TV layouts with an internal player use `app-live-epg-panel` around the +- Live TV layouts with an internal player render `app-epg-timeline` as the EPG content, including playlist-specific live pages and the global favorites/recent live tabs. -- The live panel toolbar owns the current-program summary and live date - navigation together, so `app-epg-list` hides its internal date navigator when - projected inside that panel. +- The timeline's own panel bar owns the current-program summary and live date + navigation together; there is no separate wrapper component around it. - Collapsed state is shared across M3U, Xtream, and Stalker with `live-epg-panel-state`; missing or invalid values restore to expanded. - The collapsed panel is a slim current-program strip with a trailing progress @@ -289,7 +288,7 @@ Settings use the same system but are flatter than content-heavy views. Before creating new markup or CSS: 1. Check whether `app-channel-list-item` can be reused. -2. Check whether `app-epg-view` already provides the correct structure. +2. Check whether `app-epg-timeline` already provides the correct structure. 3. Check whether `nav-list.scss` already solves the list-selection problem. 4. Extend tokens first, duplicate styles last. diff --git a/docs/architecture/m3u-playlist-module.md b/docs/architecture/m3u-playlist-module.md index be56e4055..da6ef65a7 100644 --- a/docs/architecture/m3u-playlist-module.md +++ b/docs/architecture/m3u-playlist-module.md @@ -564,8 +564,7 @@ class EpgService { | Component | Purpose | | ----------------------------- | ------------------------------------ | -| `EpgListComponent` | Timeline view for single channel | -| `EpgListItemComponent` | Individual program in timeline | +| `EpgTimelineComponent` | Horizontal timeline for one channel | | `EpgItemDescriptionComponent` | Program details dialog | | `MultiEpgContainerComponent` | Grid view of all channels' schedules | diff --git a/libs/playlist/shared/ui/src/index.ts b/libs/playlist/shared/ui/src/index.ts index 0ec759655..112ec10dc 100644 --- a/libs/playlist/shared/ui/src/index.ts +++ b/libs/playlist/shared/ui/src/index.ts @@ -1,5 +1,4 @@ export * from './lib/add-playlist-menu/playlist-type'; -export * from './lib/filter-sort-menu/filter-sort-menu.component'; export * from './lib/playlist-switcher/playlist-switcher.component'; export * from './lib/recent-playlists/playlist-info/playlist-info.component'; export * from './lib/recent-playlists/recent-playlists.component'; diff --git a/libs/playlist/shared/ui/src/lib/filter-sort-menu/filter-sort-menu.component.html b/libs/playlist/shared/ui/src/lib/filter-sort-menu/filter-sort-menu.component.html deleted file mode 100644 index fb7056325..000000000 --- a/libs/playlist/shared/ui/src/lib/filter-sort-menu/filter-sort-menu.component.html +++ /dev/null @@ -1,140 +0,0 @@ - -
- - @for (type of playlistTypes; track type.id) { - - } -
- -
- - - - - - -
- - -
diff --git a/libs/playlist/shared/ui/src/lib/filter-sort-menu/filter-sort-menu.component.scss b/libs/playlist/shared/ui/src/lib/filter-sort-menu/filter-sort-menu.component.scss deleted file mode 100644 index 3bbf88383..000000000 --- a/libs/playlist/shared/ui/src/lib/filter-sort-menu/filter-sort-menu.component.scss +++ /dev/null @@ -1,87 +0,0 @@ -.filter-section, -.sort-section { - padding: 4px 0; -} - -.menu-section-header { - display: flex; - align-items: center; - gap: 8px; - padding: 8px 16px 4px; - font-size: 11px; - font-weight: 600; - letter-spacing: 0.05em; - text-transform: uppercase; - pointer-events: none; - - .section-icon { - font-size: 16px; - width: 16px; - height: 16px; - color: rgba(139, 92, 246, 0.6); - } -} - -::ng-deep .filter-sort-menu { - .mat-mdc-menu-item { - display: flex !important; - align-items: center !important; - gap: 12px !important; - min-height: 40px !important; - padding: 0 16px !important; - - .mat-icon:first-child { - margin-right: 0 !important; - flex-shrink: 0; - } - - .menu-item-text { - flex: 1; - text-align: left; - } - - .check-icon { - margin-left: auto !important; - margin-right: 0 !important; - flex-shrink: 0; - opacity: 0; - transform: scale(0.8); - transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1); - - &.check-visible { - opacity: 1; - transform: scale(1); - } - } - } -} - -.active-option { - background-color: rgba(139, 92, 246, 0.12) !important; - position: relative; - - &::before { - content: ''; - position: absolute; - left: 0; - top: 50%; - transform: translateY(-50%); - width: 3px; - height: 60%; - background: rgba(139, 92, 246, 0.8); - border-radius: 0 2px 2px 0; - } -} - -.check-icon { - font-size: 18px !important; - width: 18px !important; - height: 18px !important; - color: rgba(139, 92, 246, 1) !important; -} - -.menu-footer { - padding: 4px 8px 4px; - display: flex; - justify-content: center; -} diff --git a/libs/playlist/shared/ui/src/lib/filter-sort-menu/filter-sort-menu.component.ts b/libs/playlist/shared/ui/src/lib/filter-sort-menu/filter-sort-menu.component.ts deleted file mode 100644 index 91c76e1da..000000000 --- a/libs/playlist/shared/ui/src/lib/filter-sort-menu/filter-sort-menu.component.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { - Component, - effect, - inject, - input, - output, - viewChild, -} from '@angular/core'; -import { toSignal } from '@angular/core/rxjs-interop'; -import { MatButtonModule } from '@angular/material/button'; -import { MatIconModule } from '@angular/material/icon'; -import { MatMenu, MatMenuModule, MatMenuTrigger } from '@angular/material/menu'; -import { Store } from '@ngrx/store'; -import { TranslateModule } from '@ngx-translate/core'; -import { FilterActions, selectActiveTypeFilters } from '@iptvnator/m3u-state'; -import { SortBy, SortOrder, SortService } from '@iptvnator/services'; - -@Component({ - selector: 'app-filter-sort-menu', - templateUrl: './filter-sort-menu.component.html', - styleUrls: ['./filter-sort-menu.component.scss'], - imports: [MatButtonModule, MatIconModule, MatMenuModule, TranslateModule], -}) -export class FilterSortMenuComponent { - private readonly store = inject(Store); - private readonly sortService = inject(SortService); - - readonly menu = viewChild.required('filterSortMenu'); - readonly menuTrigger = input(); - readonly filterChanged = output(); - - playlistTypes = [ - { - id: 'm3u', - icon: 'playlist_play', - translationKey: 'HOME.PLAYLIST_TYPES.M3U', - checked: true, - }, - { - id: 'xtream', - icon: 'cloud', - translationKey: 'HOME.PLAYLIST_TYPES.XTREAM', - checked: true, - }, - { - id: 'stalker', - icon: 'router', - translationKey: 'HOME.PLAYLIST_TYPES.STALKER', - checked: true, - }, - ]; - - private readonly selectedTypeFilters = this.store.selectSignal( - selectActiveTypeFilters - ); - - readonly SortBy = SortBy; - readonly SortOrder = SortOrder; - private readonly currentSortOptions = toSignal( - this.sortService.getSortOptions(), - { - requireSync: true, - } - ); - - constructor() { - effect(() => { - if (this.selectedTypeFilters) { - this.playlistTypes = this.playlistTypes.map((type) => { - type.checked = this.selectedTypeFilters().includes(type.id); - return type; - }); - } - }); - } - - togglePlaylistType(type: { - id: string; - icon: string; - translationKey: string; - checked: boolean; - }) { - const currentlySelectedCount = this.playlistTypes.filter( - (item) => item.checked - ).length; - - if (type.checked && currentlySelectedCount === 1) { - return; - } - - type.checked = !type.checked; - this.store.dispatch( - FilterActions.setSelectedFilters({ - selectedFilters: this.playlistTypes - .filter((item) => item.checked) - .map((item) => item.id), - }) - ); - this.filterChanged.emit(); - } - - setSortOptions(by: SortBy, order: SortOrder): void { - this.sortService.setSortOptions({ by, order }); - } - - isSortActive(by: SortBy, order: SortOrder): boolean { - const options = this.currentSortOptions(); - return options?.by === by && options?.order === order; - } - - get activeFiltersCount(): number { - return this.playlistTypes.filter((type) => !type.checked).length; - } - - closeMenu(): void { - this.menuTrigger()?.closeMenu(); - } -} diff --git a/libs/portal/shared/ui/src/index.ts b/libs/portal/shared/ui/src/index.ts index a95b5fa20..4b6df6e72 100644 --- a/libs/portal/shared/ui/src/index.ts +++ b/libs/portal/shared/ui/src/index.ts @@ -6,7 +6,6 @@ export * from './lib/components/global-favorites-list/global-favorites-list.comp export * from './lib/components/grid-list/grid-list.component'; export * from './lib/components/playlist-error-view/playlist-error-view.component'; export { ProgressCapsuleComponent } from '@iptvnator/ui/components'; -export * from './lib/components/portal-collection-shell/portal-collection-shell.component'; export * from './lib/components/portal-empty-state/portal-empty-state.component'; export * from './lib/components/search-form/search-form.component'; export * from './lib/components/search-layout/search-layout.component'; @@ -14,5 +13,4 @@ export { WatchedBadgeComponent } from '@iptvnator/ui/components'; export * from './lib/components/unified-collection/unified-collection-page.component'; export * from './lib/components/unified-collection/unified-collection-detail.directive'; export * from './lib/components/unified-collection/unified-live-tab.component'; -export * from './lib/navigation/navigation.component'; export * from './lib/navigation/portal-rail-links.component'; diff --git a/libs/portal/shared/ui/src/lib/components/portal-collection-shell/portal-collection-shell.component.html b/libs/portal/shared/ui/src/lib/components/portal-collection-shell/portal-collection-shell.component.html deleted file mode 100644 index 81f2d5a7d..000000000 --- a/libs/portal/shared/ui/src/lib/components/portal-collection-shell/portal-collection-shell.component.html +++ /dev/null @@ -1,29 +0,0 @@ -@switch (mode()) { - @case ('detail') { - - } - @case ('live') { - - } - @default { - - } -} diff --git a/libs/portal/shared/ui/src/lib/components/portal-collection-shell/portal-collection-shell.component.ts b/libs/portal/shared/ui/src/lib/components/portal-collection-shell/portal-collection-shell.component.ts deleted file mode 100644 index d468194f5..000000000 --- a/libs/portal/shared/ui/src/lib/components/portal-collection-shell/portal-collection-shell.component.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { ChangeDetectionStrategy, Component, input, output } from '@angular/core'; -import { XtreamCategory } from '@iptvnator/shared/interfaces'; -import { - FavoriteLayoutItem, - FavoritesLayoutComponent, -} from '../favorites-layout/favorites-layout.component'; - -interface PortalCategorySelection { - readonly category_id?: string | number; - readonly id?: string | number; -} - -export interface PortalCollectionShellLayout { - titleTranslationKey?: string; - removeTooltip?: string; - emptyIcon?: string; - showHeaderAction?: boolean; - headerActionIcon?: string; - headerActionTooltip?: string; -} - -export type PortalCollectionMode = 'grid' | 'detail' | 'live'; - -@Component({ - selector: 'app-portal-collection-shell', - imports: [FavoritesLayoutComponent], - templateUrl: './portal-collection-shell.component.html', - changeDetection: ChangeDetectionStrategy.OnPush, - styles: [ - ` - :host { - display: block; - height: 100%; - width: 100%; - } - `, - ], -}) -export class PortalCollectionShellComponent { - readonly categories = input([]); - readonly items = input([]); - readonly playlistSubtitle = input(''); - readonly playlistTitle = input('Playlist'); - readonly selectedCategoryId = input('all'); - readonly layout = input({}); - readonly mode = input('grid'); - - readonly categoryClicked = output(); - readonly removeItem = output(); - readonly openItem = output(); - readonly headerActionClicked = output(); - - onCategoryClicked(event: PortalCategorySelection): void { - const categoryId = event.category_id ?? event.id; - if (categoryId == null) { - return; - } - - this.categoryClicked.emit(String(categoryId)); - } -} diff --git a/libs/portal/shared/ui/src/lib/components/unified-collection/unified-live-tab.component.scss b/libs/portal/shared/ui/src/lib/components/unified-collection/unified-live-tab.component.scss index 829904eab..dc6bacbf0 100644 --- a/libs/portal/shared/ui/src/lib/components/unified-collection/unified-live-tab.component.scss +++ b/libs/portal/shared/ui/src/lib/components/unified-collection/unified-live-tab.component.scss @@ -67,8 +67,7 @@ display: flex; flex-direction: column; - > app-epg-view, - > app-epg-list { + > app-epg-timeline { flex: 1; min-height: 0; } diff --git a/libs/portal/shared/ui/src/lib/navigation/navigation.component.html b/libs/portal/shared/ui/src/lib/navigation/navigation.component.html deleted file mode 100644 index 76dcf67fc..000000000 --- a/libs/portal/shared/ui/src/lib/navigation/navigation.component.html +++ /dev/null @@ -1,78 +0,0 @@ -
- {{ getStatusIcon() }} -
- - - - - - - - diff --git a/libs/portal/shared/ui/src/lib/navigation/navigation.component.scss b/libs/portal/shared/ui/src/lib/navigation/navigation.component.scss deleted file mode 100644 index d12c17c3e..000000000 --- a/libs/portal/shared/ui/src/lib/navigation/navigation.component.scss +++ /dev/null @@ -1,116 +0,0 @@ -@use '../../../../../../../apps/web/src/nav-list.scss'; - -:host { - padding: 4px; - display: flex; - flex-direction: column; - height: 100%; -} - -.mdc-list { - padding: 2px 0; -} - -.mat-nav-list { - padding: 0; - margin: 0; - - .portal-nav-link.mdc-list-item { - min-height: 28px; - height: 28px; - padding: 0 !important; - border-radius: 10px; - margin: 2px 0; - display: flex; - justify-content: center; - color: var(--mat-sys-on-surface-variant); - - .mdc-list-item__start { - margin-inline-end: 0 !important; - margin-inline-start: 0 !important; - } - - .mdc-list-item__content { - display: none; - } - } - - mat-icon.mat-mdc-list-item-icon, - mat-icon { - font-size: 18px !important; - width: 18px !important; - height: 18px !important; - } -} - -.navigation-bottom { - margin-top: auto; - padding-bottom: 2px; -} - -.active { - color: var(--app-selection-color); - - mat-icon { - color: var(--app-selection-color); - } -} - -mat-divider { - margin: 1px 0; -} - -mat-icon { - margin-right: 0; - font-size: 18px !important; - width: 18px !important; - height: 18px !important; -} - -a.mat-mdc-list-item.mdc-list-item { - padding-right: 0; -} - -.portal-status-container { - app-region: drag; - margin: -4px -4px 0; - height: 42px; - display: flex; - align-items: center; - justify-content: center; - border-radius: 8px; - background: var(--mat-sys-surface-container); - color: var(--mat-sys-on-surface-variant); - border: 1px solid var(--mat-sys-outline-variant); -} - -.portal-status-container.status-active { - background: var(--mat-sys-primary-container); - color: var(--mat-sys-on-primary-container); -} - -.portal-status-container.status-inactive, -.portal-status-container.status-unavailable { - background: var(--mat-sys-error-container); - color: var(--mat-sys-on-error-container); -} - -.portal-status-container.status-expired { - background: var(--mat-sys-tertiary-container); - color: var(--mat-sys-on-tertiary-container); -} - -.portal-status-container .status-watermark { - font-size: 24px !important; - width: 24px !important; - height: 24px !important; - margin: 0; -} - -.nav-section { - app-portal-rail-links { - display: block; - padding: 0; - margin: 0; - } -} diff --git a/libs/portal/shared/ui/src/lib/navigation/navigation.component.spec.ts b/libs/portal/shared/ui/src/lib/navigation/navigation.component.spec.ts deleted file mode 100644 index 320625478..000000000 --- a/libs/portal/shared/ui/src/lib/navigation/navigation.component.spec.ts +++ /dev/null @@ -1,121 +0,0 @@ -import { signal } from '@angular/core'; -import { TestBed } from '@angular/core/testing'; -import { ActivatedRoute } from '@angular/router'; -import { Store } from '@ngrx/store'; -import { TranslateModule } from '@ngx-translate/core'; -import { PORTAL_NAVIGATION_ACTIONS } from '@iptvnator/portal/shared/util'; -import { RuntimeCapabilitiesService } from '@iptvnator/services'; -import { Playlist } from '@iptvnator/shared/interfaces'; -import { NavigationComponent } from './navigation.component'; - -describe('NavigationComponent', () => { - let playlist: ReturnType | undefined>>; - let runtime: { - supportsDownloads: boolean; - }; - - beforeEach(async () => { - playlist = signal | undefined>({ - _id: 'xtream-1', - }); - runtime = { - supportsDownloads: true, - }; - - await TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot(), NavigationComponent], - providers: [ - { - provide: ActivatedRoute, - useValue: { - snapshot: { - params: { - id: 'xtream-1', - }, - }, - }, - }, - { - provide: Store, - useValue: { - selectSignal: jest.fn(() => playlist), - }, - }, - { - provide: PORTAL_NAVIGATION_ACTIONS, - useValue: { - openAccountInfo: jest.fn(), - openPlaylistInfo: jest.fn(), - openSettings: jest.fn(), - }, - }, - { - provide: RuntimeCapabilitiesService, - useValue: runtime, - }, - ], - }).compileComponents(); - }); - - function createComponent(): NavigationComponent { - return TestBed.createComponent(NavigationComponent).componentInstance; - } - - it('includes downloads in portal rail links when downloads are supported', () => { - runtime.supportsDownloads = true; - - const component = createComponent(); - - expect(component.secondaryLinks().map((link) => link.section)).toEqual([ - 'recently-added', - 'search', - 'downloads', - ]); - }); - - it('hides downloads in portal rail links when downloads are unsupported', () => { - runtime.supportsDownloads = false; - - const component = createComponent(); - - expect(component.secondaryLinks().map((link) => link.section)).toEqual([ - 'recently-added', - 'search', - ]); - }); - - it('uses the Stalker rail shape for Stalker playlists', () => { - runtime.supportsDownloads = true; - playlist.set({ - _id: 'stalker-1', - macAddress: '00:1A:79:00:00:01', - }); - - const component = createComponent(); - - expect(component.primaryLinks().map((link) => link.section)).toEqual([ - 'vod', - 'itv', - 'radio', - 'series', - ]); - expect(component.secondaryLinks().map((link) => link.section)).toEqual([ - 'search', - 'downloads', - ]); - }); - - it('hides Stalker downloads when downloads are unsupported', () => { - runtime.supportsDownloads = false; - playlist.set({ - _id: 'stalker-1', - macAddress: '00:1A:79:00:00:01', - }); - - const component = createComponent(); - - expect(component.secondaryLinks().map((link) => link.section)).toEqual([ - 'search', - ]); - }); -}); diff --git a/libs/portal/shared/ui/src/lib/navigation/navigation.component.ts b/libs/portal/shared/ui/src/lib/navigation/navigation.component.ts deleted file mode 100644 index fae267ff1..000000000 --- a/libs/portal/shared/ui/src/lib/navigation/navigation.component.ts +++ /dev/null @@ -1,116 +0,0 @@ -import { Component, computed, inject, input } from '@angular/core'; -import { MatDividerModule } from '@angular/material/divider'; -import { MatIcon } from '@angular/material/icon'; -import { MatListModule } from '@angular/material/list'; -import { MatTooltip } from '@angular/material/tooltip'; -import { ActivatedRoute, RouterLink, RouterLinkActive } from '@angular/router'; -import { Store } from '@ngrx/store'; -import { TranslatePipe } from '@ngx-translate/core'; -import { - buildPortalRailLinks, - PORTAL_NAVIGATION_ACTIONS, - PortalRailSection, -} from '@iptvnator/portal/shared/util'; -import { selectPlaylistById } from '@iptvnator/m3u-state'; -import { RuntimeCapabilitiesService } from '@iptvnator/services'; -import { Playlist } from '@iptvnator/shared/interfaces'; -import { PortalRailLinksComponent } from './portal-rail-links.component'; - -@Component({ - selector: 'app-navigation', - imports: [ - MatDividerModule, - MatIcon, - MatListModule, - MatTooltip, - RouterLink, - RouterLinkActive, - TranslatePipe, - PortalRailLinksComponent, - ], - templateUrl: './navigation.component.html', - styleUrl: './navigation.component.scss', -}) -export class NavigationComponent { - private readonly activatedRoute = inject(ActivatedRoute); - private readonly navigationActions = inject(PORTAL_NAVIGATION_ACTIONS); - private readonly runtime = inject(RuntimeCapabilitiesService); - private readonly store = inject(Store); - - readonly portalStatus = input< - 'active' | 'inactive' | 'expired' | 'unavailable' - >(); - readonly selectedContentType = input(); - - readonly currentPlaylist = this.store.selectSignal( - selectPlaylistById(this.activatedRoute.snapshot.params.id) - ); - - readonly isStalkerPlaylist = computed( - () => !!(this.currentPlaylist() as Playlist | undefined)?.macAddress - ); - - get supportsDownloads(): boolean { - return this.runtime.supportsDownloads; - } - - readonly railLinks = computed(() => { - const playlistId = - (this.currentPlaylist() as Playlist | undefined)?._id ?? - this.activatedRoute.snapshot.params['id']; - if (!playlistId) { - return { primary: [], secondary: [] }; - } - - return buildPortalRailLinks({ - provider: this.isStalkerPlaylist() ? 'stalker' : 'xtreams', - playlistId, - supportsDownloads: this.supportsDownloads, - workspace: false, - }); - }); - readonly primaryLinks = computed(() => this.railLinks().primary); - readonly secondaryLinks = computed(() => this.railLinks().secondary); - - getStatusColor(): string { - if (this.isStalkerPlaylist()) return 'status-active'; - - switch (this.portalStatus()) { - case 'active': - return 'status-active'; - case 'inactive': - return 'status-inactive'; - case 'expired': - return 'status-expired'; - default: - return 'status-unavailable'; - } - } - - getStatusIcon(): string { - if (this.isStalkerPlaylist()) return 'play_circle'; - - switch (this.portalStatus()) { - case 'active': - return 'check_circle'; - case 'inactive': - return 'cancel'; - case 'expired': - return 'warning'; - default: - return 'error'; - } - } - - openAccountInfo(): void { - this.navigationActions.openAccountInfo(); - } - - openSettings(): void { - this.navigationActions.openSettings(); - } - - openPlaylistInfo(): void { - this.navigationActions.openPlaylistInfo(this.currentPlaylist()); - } -} diff --git a/libs/portal/stalker/feature/src/index.ts b/libs/portal/stalker/feature/src/index.ts index 5aae4db16..d5600ef48 100644 --- a/libs/portal/stalker/feature/src/index.ts +++ b/libs/portal/stalker/feature/src/index.ts @@ -1,6 +1,5 @@ export * from './lib/stalker-inline-detail/stalker-inline-detail.component'; export * from './lib/stalker-collection-detail.component'; -export * from './lib/stalker-collection-channels-list/stalker-collection-channels-list.component'; export * from './lib/stalker-catalog-detail/stalker-catalog-detail.component'; export * from './lib/stalker-favorites-button/stalker-favorites-button.component'; export * from './lib/stalker-catalog-facade.service'; diff --git a/libs/portal/stalker/feature/src/lib/stalker-collection-channels-list/stalker-collection-channels-list.component.html b/libs/portal/stalker/feature/src/lib/stalker-collection-channels-list/stalker-collection-channels-list.component.html deleted file mode 100644 index 4fd89ca7d..000000000 --- a/libs/portal/stalker/feature/src/lib/stalker-collection-channels-list/stalker-collection-channels-list.component.html +++ /dev/null @@ -1,44 +0,0 @@ -
- -
- -
- @if (filteredItems().length > 0) { - @for (item of filteredItems(); track item.id) { - - } - } @else { -
- search_off -

- {{ 'CHANNELS.NO_CHANNELS_FOUND' | translate }} -

-

- {{ 'CHANNELS.TRY_DIFFERENT_SEARCH' | translate }} -

-
- } -
diff --git a/libs/portal/stalker/feature/src/lib/stalker-collection-channels-list/stalker-collection-channels-list.component.scss b/libs/portal/stalker/feature/src/lib/stalker-collection-channels-list/stalker-collection-channels-list.component.scss deleted file mode 100644 index 150929661..000000000 --- a/libs/portal/stalker/feature/src/lib/stalker-collection-channels-list/stalker-collection-channels-list.component.scss +++ /dev/null @@ -1,115 +0,0 @@ -:host { - display: block; - height: 100%; - width: 100%; - overflow: hidden; - container-type: inline-size; -} - -.channels-list { - display: flex; - flex-direction: column; - height: calc(100% - 56px); - overflow-y: auto; - overflow-x: hidden; - width: 100%; - padding: 4px 8px 12px; - box-sizing: border-box; - gap: 2px; -} - -.search-panel { - padding: 10px 12px; - flex-shrink: 0; - height: 56px; - box-sizing: border-box; - display: flex; - align-items: center; -} - -.search-field { - flex: 1; - width: 100%; - height: 36px; - border-radius: 10px; - border: 1px solid rgba(127, 127, 127, 0.22); - background: - linear-gradient(180deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0)), - rgba(127, 127, 127, 0.08); - display: flex; - align-items: center; - gap: 8px; - padding: 0 12px; - box-sizing: border-box; - cursor: text; - transition: - border-color 0.15s ease, - background-color 0.15s ease, - box-shadow 0.15s ease; - - &:focus-within { - border-color: var(--app-selection-border); - box-shadow: 0 0 0 3px color-mix(in srgb, var(--app-selection-color) 16%, transparent); - } - - mat-icon { - width: 18px; - height: 18px; - font-size: 18px; - opacity: 0.68; - flex-shrink: 0; - } - - input { - flex: 1; - border: none; - outline: none; - background: transparent; - color: inherit; - font: inherit; - font-size: 0.875rem; - } -} - -.empty-search-state { - flex: 1; - min-height: 0; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 40px 24px; - text-align: center; - color: var(--app-muted-color); - - .empty-state-icon { - width: 48px; - height: 48px; - font-size: 48px; - opacity: 0.5; - margin-bottom: 16px; - } - - .empty-state-title { - font-size: 1.05rem; - font-weight: 520; - letter-spacing: -0.01em; - margin: 0 0 8px; - text-wrap: balance; - } - - .empty-state-hint { - font-size: 0.85rem; - margin: 0; - opacity: 0.65; - line-height: 1.4; - max-width: 240px; - text-wrap: pretty; - } -} - -@container (max-width: 350px) { - .channels-list { - padding-inline: 6px; - } -} diff --git a/libs/portal/stalker/feature/src/lib/stalker-collection-channels-list/stalker-collection-channels-list.component.spec.ts b/libs/portal/stalker/feature/src/lib/stalker-collection-channels-list/stalker-collection-channels-list.component.spec.ts deleted file mode 100644 index 625cac098..000000000 --- a/libs/portal/stalker/feature/src/lib/stalker-collection-channels-list/stalker-collection-channels-list.component.spec.ts +++ /dev/null @@ -1,118 +0,0 @@ -import { Component, input, output, signal } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { MockPipe } from 'ng-mocks'; -import { TranslatePipe } from '@ngx-translate/core'; -import { ChannelListItemComponent } from '@iptvnator/ui/components'; -import { EpgProgram } from '@iptvnator/shared/interfaces'; -import { StalkerStore } from '@iptvnator/portal/stalker/data-access'; -import { StalkerCollectionChannelsListComponent } from './stalker-collection-channels-list.component'; - -@Component({ - selector: 'app-channel-list-item', - standalone: true, - template: '', -}) -class StubChannelListItemComponent { - readonly name = input(''); - readonly logo = input(null); - readonly selected = input(false); - readonly epgProgram = input(null); - readonly progressPercentage = input(0); - readonly showFavoriteButton = input(false); - readonly showProgramInfoButton = input(false); - readonly isFavorite = input(false); - readonly clicked = output(); - readonly favoriteToggled = output(); -} - -describe('StalkerCollectionChannelsListComponent', () => { - let fixture: ComponentFixture; - let component: StalkerCollectionChannelsListComponent; - - const bulkItvEpgByChannel = signal>({}); - const stalkerStore = { - bulkItvEpgByChannel, - }; - - beforeEach(async () => { - bulkItvEpgByChannel.set({}); - - await TestBed.configureTestingModule({ - imports: [StalkerCollectionChannelsListComponent], - providers: [ - { provide: StalkerStore, useValue: stalkerStore }, - ], - }) - .overrideComponent(StalkerCollectionChannelsListComponent, { - remove: { - imports: [ChannelListItemComponent, TranslatePipe], - }, - add: { - imports: [ - StubChannelListItemComponent, - MockPipe( - TranslatePipe, - (value: string | null | undefined) => value ?? '' - ), - ], - }, - }) - .compileComponents(); - - fixture = TestBed.createComponent(StalkerCollectionChannelsListComponent); - component = fixture.componentInstance; - fixture.componentRef.setInput('items', [ - { - id: '10001', - name: 'Alpha TV', - o_name: 'Alpha TV', - logo: 'alpha.png', - }, - { - id: '10002', - name: 'Beta TV', - o_name: 'Beta TV', - logo: 'beta.png', - }, - ]); - }); - - afterEach(() => { - fixture?.destroy(); - }); - - it('keeps row previews empty until bulk epg has been loaded', () => { - fixture.detectChanges(); - - expect(component.epgPrograms.size).toBe(0); - expect(component.currentProgramsProgress.size).toBe(0); - }); - - it('derives row previews from cached bulk epg', () => { - bulkItvEpgByChannel.set({ - '10001': [buildProgram('10001', 'Current Show')], - '10002': [buildProgram('10002', 'Other Show')], - }); - - fixture.detectChanges(); - - expect(component.epgPrograms.get('10001')?.title).toBe('Current Show'); - expect(component.epgPrograms.get('10002')?.title).toBe('Other Show'); - }); -}); - -function buildProgram(channelId: string, title: string): EpgProgram { - const startTimestamp = Math.floor((Date.now() - 10 * 60 * 1000) / 1000); - const stopTimestamp = startTimestamp + 30 * 60; - - return { - start: new Date(startTimestamp * 1000).toISOString(), - stop: new Date(stopTimestamp * 1000).toISOString(), - channel: channelId, - title, - desc: `${title} description`, - category: null, - startTimestamp, - stopTimestamp, - }; -} diff --git a/libs/portal/stalker/feature/src/lib/stalker-collection-channels-list/stalker-collection-channels-list.component.ts b/libs/portal/stalker/feature/src/lib/stalker-collection-channels-list/stalker-collection-channels-list.component.ts deleted file mode 100644 index 58b52492e..000000000 --- a/libs/portal/stalker/feature/src/lib/stalker-collection-channels-list/stalker-collection-channels-list.component.ts +++ /dev/null @@ -1,171 +0,0 @@ -import { - ChangeDetectorRef, - ChangeDetectionStrategy, - Component, - computed, - effect, - inject, - input, - output, - signal, -} from '@angular/core'; -import { FormsModule } from '@angular/forms'; -import { MatIconModule } from '@angular/material/icon'; -import { TranslatePipe } from '@ngx-translate/core'; -import { ChannelListItemComponent } from '@iptvnator/ui/components'; -import { EpgProgram } from '@iptvnator/shared/interfaces'; -import { StalkerVodSource } from '@iptvnator/portal/stalker/data-access'; -import { normalizeStalkerEntityId } from '@iptvnator/portal/stalker/data-access'; -import { StalkerStore } from '@iptvnator/portal/stalker/data-access'; - -@Component({ - selector: 'app-stalker-collection-channels-list', - imports: [ChannelListItemComponent, FormsModule, MatIconModule, TranslatePipe], - templateUrl: './stalker-collection-channels-list.component.html', - styleUrl: './stalker-collection-channels-list.component.scss', - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class StalkerCollectionChannelsListComponent { - readonly items = input([]); - readonly selectedItemId = input(null); - readonly favoriteIds = input>(new Map()); - - readonly playClicked = output(); - readonly favoriteToggled = output(); - - private readonly stalkerStore = inject(StalkerStore); - private readonly cdr = inject(ChangeDetectorRef); - protected readonly normalizeStalkerEntityId = normalizeStalkerEntityId; - readonly searchString = signal(''); - readonly filteredItems = computed(() => { - const search = this.searchString().trim().toLowerCase(); - if (!search) { - return this.items(); - } - - return this.items().filter((item) => - `${item.o_name ?? ''} ${item.name ?? ''}` - .toLowerCase() - .includes(search) - ); - }); - - readonly epgPrograms = new Map(); - readonly currentProgramsProgress = new Map(); - - constructor() { - effect(() => { - const items = this.items(); - const bulkProgramsByChannel = this.stalkerStore.bulkItvEpgByChannel(); - this.syncBulkEpgPreviews(items, bulkProgramsByChannel); - }); - } - - onPlay(item: StalkerVodSource): void { - this.playClicked.emit(item); - } - - onFavoriteToggle(item: StalkerVodSource): void { - this.favoriteToggled.emit(item); - } - - isSelected(item: StalkerVodSource): boolean { - return String(this.selectedItemId() ?? '') === normalizeStalkerEntityId(item.id); - } - - isFavorite(item: StalkerVodSource): boolean { - return this.favoriteIds().get(normalizeStalkerEntityId(item.id)) ?? false; - } - - private syncBulkEpgPreviews( - items: StalkerVodSource[], - bulkProgramsByChannel: Record - ): void { - this.epgPrograms.clear(); - this.currentProgramsProgress.clear(); - - if (items.length === 0 || Object.keys(bulkProgramsByChannel).length === 0) { - this.cdr.markForCheck(); - return; - } - - for (const item of items) { - const channelId = normalizeStalkerEntityId(item.id); - const currentProgram = this.findCurrentProgram( - bulkProgramsByChannel[channelId] ?? [] - ); - - if (!currentProgram) { - continue; - } - - this.epgPrograms.set(channelId, currentProgram); - this.updateProgramProgress(channelId, currentProgram); - } - - this.cdr.markForCheck(); - } - - private updateProgramProgress( - channelId: string | number, - program: EpgProgram - ): void { - const startMs = this.getProgramTimestampMs( - program.start, - program.startTimestamp - ); - const stopMs = this.getProgramTimestampMs( - program.stop, - program.stopTimestamp - ); - const nowMs = Date.now(); - - if ( - Number.isFinite(startMs) && - Number.isFinite(stopMs) && - nowMs >= startMs && - nowMs <= stopMs && - stopMs > startMs - ) { - this.currentProgramsProgress.set( - channelId, - ((nowMs - startMs) / (stopMs - startMs)) * 100 - ); - return; - } - - this.currentProgramsProgress.delete(channelId); - } - - private findCurrentProgram(programs: EpgProgram[]): EpgProgram | null { - const nowMs = Date.now(); - - return ( - programs.find((program) => { - const startMs = this.getProgramTimestampMs( - program.start, - program.startTimestamp - ); - const stopMs = this.getProgramTimestampMs( - program.stop, - program.stopTimestamp - ); - - return nowMs >= startMs && nowMs <= stopMs; - }) ?? null - ); - } - - private getProgramTimestampMs( - rawDate: string, - timestamp?: number | null - ): number { - const parsedTimestamp = Number(timestamp); - if (Number.isFinite(parsedTimestamp) && parsedTimestamp > 0) { - return parsedTimestamp * 1000; - } - - const parsedDate = Date.parse(rawDate); - return Number.isFinite(parsedDate) ? parsedDate : Number.POSITIVE_INFINITY; - } -} diff --git a/libs/portal/xtream/feature/src/index.ts b/libs/portal/xtream/feature/src/index.ts index 7de311c3d..c0dc55e9e 100644 --- a/libs/portal/xtream/feature/src/index.ts +++ b/libs/portal/xtream/feature/src/index.ts @@ -2,7 +2,6 @@ export * from './lib/account-info/account-info.component'; export * from './lib/category-management-dialog/category-management-dialog.component'; export * from './lib/global-search-results/global-search-results.component'; export * from './lib/live-stream-layout/live-stream-layout.component'; -export * from './lib/loading-overlay.component'; export * from './lib/portal-channels-list/portal-channels-list.component'; export * from './lib/recently-added/recently-added.component'; export * from './lib/search-results/search-results.component'; diff --git a/libs/portal/xtream/feature/src/lib/loading-overlay.component.ts b/libs/portal/xtream/feature/src/lib/loading-overlay.component.ts deleted file mode 100644 index 9f5192bb3..000000000 --- a/libs/portal/xtream/feature/src/lib/loading-overlay.component.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { Component, input } from '@angular/core'; -import { MatProgressBarModule } from '@angular/material/progress-bar'; - -@Component({ - selector: 'app-loading-overlay', - imports: [MatProgressBarModule], - template: ` -
-
-

Loading playlist...

- @if (current() !== 0 && total() !== 0) { - -

{{ current() }} / {{ total() }}

- } @else { - - } -
-
- `, - styles: [ - ` - .overlay { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.7); - display: flex; - justify-content: center; - align-items: center; - z-index: 1000; - } - - .progress-container { - padding: 2rem; - border-radius: 8px; - min-width: 300px; - text-align: center; - - h3 { - margin-top: 0; - } - } - `, - ], -}) -export class LoadingOverlayComponent { - current = input(0); - total = input(0); -} diff --git a/libs/ui/epg/src/index.ts b/libs/ui/epg/src/index.ts index 41ef73a48..e335ef443 100644 --- a/libs/ui/epg/src/index.ts +++ b/libs/ui/epg/src/index.ts @@ -1,5 +1,5 @@ -export * from './lib/epg-list/epg-list.component'; -export * from './lib/epg-list/epg-item-description/epg-item-description.component'; +export * from './lib/epg-item-description/epg-item-description.component'; +export * from './lib/epg-program-activation-event'; export * from './lib/epg-date'; export * from './lib/epg-timeline/epg-timeline.component'; export * from './lib/epg-timeline/epg-timeline-empty-state.component'; diff --git a/libs/ui/epg/src/lib/epg-list/epg-item-description/epg-item-description.component.html b/libs/ui/epg/src/lib/epg-item-description/epg-item-description.component.html similarity index 100% rename from libs/ui/epg/src/lib/epg-list/epg-item-description/epg-item-description.component.html rename to libs/ui/epg/src/lib/epg-item-description/epg-item-description.component.html diff --git a/libs/ui/epg/src/lib/epg-list/epg-item-description/epg-item-description.component.scss b/libs/ui/epg/src/lib/epg-item-description/epg-item-description.component.scss similarity index 100% rename from libs/ui/epg/src/lib/epg-list/epg-item-description/epg-item-description.component.scss rename to libs/ui/epg/src/lib/epg-item-description/epg-item-description.component.scss diff --git a/libs/ui/epg/src/lib/epg-list/epg-item-description/epg-item-description.component.spec.ts b/libs/ui/epg/src/lib/epg-item-description/epg-item-description.component.spec.ts similarity index 100% rename from libs/ui/epg/src/lib/epg-list/epg-item-description/epg-item-description.component.spec.ts rename to libs/ui/epg/src/lib/epg-item-description/epg-item-description.component.spec.ts diff --git a/libs/ui/epg/src/lib/epg-list/epg-item-description/epg-item-description.component.ts b/libs/ui/epg/src/lib/epg-item-description/epg-item-description.component.ts similarity index 100% rename from libs/ui/epg/src/lib/epg-list/epg-item-description/epg-item-description.component.ts rename to libs/ui/epg/src/lib/epg-item-description/epg-item-description.component.ts diff --git a/libs/ui/epg/src/lib/epg-list/epg-list-item/epg-list-item.component.html b/libs/ui/epg/src/lib/epg-list/epg-list-item/epg-list-item.component.html deleted file mode 100644 index 147733ab4..000000000 --- a/libs/ui/epg/src/lib/epg-list/epg-list-item/epg-list-item.component.html +++ /dev/null @@ -1,33 +0,0 @@ -{{ (item.startTimestamp ? item.startTimestamp * 1000 : item.start) | date: 'HH:mm' }}–{{ (item.stopTimestamp ? item.stopTimestamp * 1000 : item.stop) | date: 'HH:mm' }} -@if (isLive) { - live -} - -@if (showArchiveBadge) { - - - {{ 'XTREAM.PLAY' | translate }} - -} - -@if (item.desc) { - info -} diff --git a/libs/ui/epg/src/lib/epg-list/epg-list-item/epg-list-item.component.scss b/libs/ui/epg/src/lib/epg-list/epg-list-item/epg-list-item.component.scss deleted file mode 100644 index 4b5787491..000000000 --- a/libs/ui/epg/src/lib/epg-list/epg-list-item/epg-list-item.component.scss +++ /dev/null @@ -1,114 +0,0 @@ -:host { - display: flex; - align-items: center; - gap: 8px; - width: 100%; - min-width: 0; -} - -.time { - font-size: 0.78rem; - line-height: 1; - font-weight: 600; - letter-spacing: 0.02em; - font-variant-numeric: tabular-nums; - white-space: nowrap; - color: var(--app-muted-color); -} - -.badge { - display: inline-flex; - align-items: center; - justify-content: center; - gap: 6px; - min-width: 42px; - padding: 2px 8px; - border-radius: 999px; - font-size: 0.68rem; - line-height: 1.35; - font-weight: 700; - letter-spacing: 0.02em; - border: 1px solid transparent; - white-space: nowrap; -} - -.live { - color: var(--app-live-color); - background: color-mix(in srgb, var(--app-live-color) 14%, transparent); - border-color: color-mix(in srgb, var(--app-live-color) 32%, transparent); - text-transform: uppercase; - letter-spacing: 0.08em; -} - -.archive-playback { - color: color-mix(in srgb, var(--app-selection-color) 92%, white); - background: - linear-gradient( - 135deg, - color-mix(in srgb, var(--app-selection-color) 16%, transparent), - color-mix(in srgb, var(--app-selection-color) 9%, transparent) - ), - color-mix(in srgb, var(--app-selection-surface) 42%, transparent); - border-color: color-mix( - in srgb, - var(--app-selection-border) 78%, - transparent - ); - box-shadow: - inset 0 1px 0 rgba(255, 255, 255, 0.08), - 0 6px 14px -12px var(--app-selection-glow); - padding-inline: 7px 10px; -} - -.archive-playback--active { - color: var(--app-selection-on-color); - background: var(--app-selection-color); - border-color: color-mix( - in srgb, - var(--app-selection-color) 88%, - transparent - ); - box-shadow: - inset 0 1px 0 rgba(255, 255, 255, 0.18), - 0 10px 18px -14px var(--app-selection-glow); -} - -.archive-playback__icon { - width: 14px; - height: 14px; - font-size: 14px; - color: inherit; - opacity: 0.92; -} - -.archive-playback__label { - font-size: 0.69rem; - line-height: 1; - font-weight: 700; -} - -.description-icon { - margin-inline-start: auto; - width: 32px; - height: 32px; - display: inline-flex; - align-items: center; - justify-content: center; - border-radius: 10px; - cursor: pointer; - opacity: 0.7; - transition: - opacity 0.15s ease, - background-color 0.15s ease; - - mat-icon { - width: 18px; - height: 18px; - font-size: 18px; - } - - &:hover { - opacity: 1; - background: color-mix(in srgb, currentColor 10%, transparent); - } -} diff --git a/libs/ui/epg/src/lib/epg-list/epg-list-item/epg-list-item.component.spec.ts b/libs/ui/epg/src/lib/epg-list/epg-list-item/epg-list-item.component.spec.ts deleted file mode 100644 index f9c6da839..000000000 --- a/libs/ui/epg/src/lib/epg-list/epg-list-item/epg-list-item.component.spec.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; -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 { TranslateModule, TranslateService } from '@ngx-translate/core'; -import { MockModule } from 'ng-mocks'; -import { EpgProgram } from '@iptvnator/shared/interfaces'; -import { EpgItemDescriptionComponent } from '../epg-item-description/epg-item-description.component'; -import { EpgListItemComponent } from './epg-list-item.component'; - -const EPG_PROGRAM_ITEM: EpgProgram = { - start: '2026-04-05T11:30:00.000Z', - stop: '2026-04-05T12:30:00.000Z', - channel: '12345', - 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', -}; - -describe('EpgListItemComponent', () => { - let component: EpgListItemComponent; - let fixture: ComponentFixture; - let dialog: MatDialog; - let translate: TranslateService; - - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - imports: [ - EpgListItemComponent, - MockModule(MatDialogModule), - MockModule(MatListModule), - MockModule(MatIconModule), - MockModule(MatTooltipModule), - TranslateModule.forRoot(), - ], - }).compileComponents(); - })); - - beforeEach(() => { - fixture = TestBed.createComponent(EpgListItemComponent); - dialog = TestBed.inject(MatDialog); - translate = TestBed.inject(TranslateService); - translate.setTranslation( - 'en', - { - EPG: { - TIMESHIFT_AVAILABLE: 'Archive playback is available', - }, - XTREAM: { - PLAY: 'Play', - }, - }, - true - ); - translate.use('en'); - component = fixture.componentInstance; - component.item = EPG_PROGRAM_ITEM; - fixture.detectChanges(); - }); - - it('should create', () => { - expect(component).toBeTruthy(); - }); - - it('should trigger the function to open the details dialog', () => { - jest.spyOn(dialog, 'open'); - component.showDescription({} as EpgProgram); - expect(dialog.open).toHaveBeenCalledTimes(1); - expect(dialog.open).toHaveBeenCalledWith(EpgItemDescriptionComponent, { - data: {}, - width: '800px', - }); - }); - - it('renders an archive playback chip with replay icon and label', () => { - fixture = TestBed.createComponent(EpgListItemComponent); - component = fixture.componentInstance; - component.item = EPG_PROGRAM_ITEM; - component.showArchiveBadge = true; - fixture.detectChanges(); - - const chip = fixture.nativeElement.querySelector('.archive-playback'); - const icon = fixture.nativeElement.querySelector( - '.archive-playback__icon' - ); - - expect(chip).not.toBeNull(); - expect(chip.textContent).toContain('Play'); - expect(icon.textContent.trim()).toBe('replay'); - }); - - it('marks the archive playback chip as active when the program is active', () => { - fixture = TestBed.createComponent(EpgListItemComponent); - component = fixture.componentInstance; - component.item = EPG_PROGRAM_ITEM; - component.showArchiveBadge = true; - component.isActive = true; - fixture.detectChanges(); - - const chip = fixture.nativeElement.querySelector('.archive-playback'); - - expect(chip.classList).toContain('archive-playback--active'); - }); -}); diff --git a/libs/ui/epg/src/lib/epg-list/epg-list-item/epg-list-item.component.ts b/libs/ui/epg/src/lib/epg-list/epg-list-item/epg-list-item.component.ts deleted file mode 100644 index 2fe12b722..000000000 --- a/libs/ui/epg/src/lib/epg-list/epg-list-item/epg-list-item.component.ts +++ /dev/null @@ -1,41 +0,0 @@ -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 { TranslatePipe } from '@ngx-translate/core'; -import { EpgProgram } from '@iptvnator/shared/interfaces'; -import { EpgItemDescriptionComponent } from '../epg-item-description/epg-item-description.component'; - -@Component({ - imports: [DatePipe, MatIcon, MatTooltip, TranslatePipe], - selector: 'app-epg-list-item', - templateUrl: './epg-list-item.component.html', - styleUrls: ['./epg-list-item.component.scss'], -}) -export class EpgListItemComponent { - private dialog = inject(MatDialog); - - /** EPG Program to render */ - @Input() item!: EpgProgram; - - /** Whether the program is currently live */ - @Input() isLive = false; - - /** Whether the program is the active playback target */ - @Input() isActive = false; - - /** Whether archive playback is available for the program */ - @Input() showArchiveBadge = false; - - /** - * Opens the dialog with details about the selected program - * @param program selected epg program - */ - showDescription(program: EpgProgram): void { - this.dialog.open(EpgItemDescriptionComponent, { - width: '800px', - data: program, - }); - } -} diff --git a/libs/ui/epg/src/lib/epg-list/epg-list.component.html b/libs/ui/epg/src/lib/epg-list/epg-list.component.html deleted file mode 100644 index 6f56ef7eb..000000000 --- a/libs/ui/epg/src/lib/epg-list/epg-list.component.html +++ /dev/null @@ -1,127 +0,0 @@ -@let items = filteredItems(); -@let timeshiftLimit = timeshiftUntil(); - -@if (showDateNavigator()) { - -
-
- - - {{ - selectedDateKey() - | date: 'MMMM d, EEEE' : undefined : currentLocale() - }} - -
-
-} - -@if (timeshiftLimit) { - -
- @if (items.length > 0) { - @for (program of items; track trackProgram($index, program)) { - @if (!canActivateProgram(program)) { -
-
- -
-
- {{ program?.title }} -
-
- {{ program?.desc }} -
- @if (isProgramPlaying(program)) { -
-
-
-
-
- } -
- } @else { -
-
- -
-
{{ program.title }}
- @if (isProgramPlaying(program)) { -
-
-
-
-
- } -
- } - } - } @else { -
- event_busy -
- {{ 'EPG.EPG_NOT_AVAILABLE_DATE' | translate }} -
-
- {{ 'EPG.NO_GUIDE_HINT' | translate }} -
-
- } -
-} diff --git a/libs/ui/epg/src/lib/epg-list/epg-list.component.scss b/libs/ui/epg/src/lib/epg-list/epg-list.component.scss deleted file mode 100644 index 80928cf8c..000000000 --- a/libs/ui/epg/src/lib/epg-list/epg-list.component.scss +++ /dev/null @@ -1,287 +0,0 @@ -:host { - display: flex; - flex-direction: column; - width: 100%; - height: 100%; - min-height: 0; - overflow-x: hidden; - background: transparent; -} - -#date-navigator { - display: flex; - justify-content: space-between; - align-items: center; - padding: 8px 10px 10px; - gap: 8px; - - button { - border-radius: 10px; - opacity: 0.84; - transition: - opacity 0.15s ease, - background-color 0.15s ease, - transform 0.15s ease; - - &:hover:not([disabled]) { - opacity: 1; - background: color-mix(in srgb, currentColor 10%, transparent); - transform: translateY(-1px); - } - - &[disabled] { - opacity: 0.3; - } - } -} - -.selected-date { - flex: 1; - text-align: center; - font-size: 0.83rem; - line-height: 1.2; - font-weight: 600; - letter-spacing: 0.01em; - opacity: 0.8; -} - -#program-list { - flex: 1; - overflow-y: scroll; - overflow-x: hidden; - padding: 4px 8px 12px; - display: flex; - flex-direction: column; - gap: 0; -} - -.program-item { - position: relative; - display: flex; - flex-direction: column; - gap: 5px; - padding: 10px 12px; - border: 1px solid transparent; - border-radius: 12px; - background: transparent; - transition: - background 0.15s ease, - border-color 0.15s ease, - box-shadow 0.18s ease, - color 0.15s ease; - cursor: default; - - & + .program-item { - position: relative; - - &::before { - content: ''; - position: absolute; - top: 0; - left: 12px; - right: 12px; - height: 1px; - background: var(--app-separator); - opacity: 0.5; - } - } - - &.current-program { - background: color-mix( - in srgb, - var(--app-selection-color) 6%, - transparent - ); - - &::before, - & + .program-item::before { - display: none; - } - } - - &.clickable { - cursor: pointer; - - &:hover { - background: color-mix(in srgb, currentColor 6%, transparent); - - &::before, - & + .program-item::before { - display: none; - } - } - - &.current-program:hover { - background: color-mix( - in srgb, - var(--app-selection-color) 10%, - transparent - ); - } - - &:focus-visible { - outline: 2px solid var(--app-selection-border); - outline-offset: 2px; - background: color-mix( - in srgb, - var(--app-selection-color) 8%, - transparent - ); - - &::before, - & + .program-item::before { - display: none; - } - } - } - - &.archive-program { - &::after { - content: ''; - position: absolute; - inset-block: 12px; - inset-inline-start: 0; - width: 3px; - border-radius: 999px; - background: var(--app-selection-color); - opacity: 0.48; - transform: scaleY(0.68); - transition: - opacity 0.15s ease, - transform 0.15s ease, - box-shadow 0.15s ease; - } - - &:hover::after, - &:focus-visible::after, - &.active::after { - opacity: 1; - transform: scaleY(1); - box-shadow: 0 0 12px -4px var(--app-selection-glow); - } - } - - &.active { - background: linear-gradient( - 135deg, - var(--app-selection-surface-strong), - var(--app-selection-surface) - ); - border-color: var(--app-selection-border); - box-shadow: - inset 0 0 0 1px var(--app-selection-border), - 0 12px 20px -18px var(--app-selection-glow); - - .program-title { - color: var(--app-selection-color); - } - - &::before, - & + .program-item::before { - display: none; - } - } -} - -.program-meta { - display: flex; - align-items: center; - gap: 8px; -} - -.program-title { - font-size: 0.94rem; - line-height: 1.3; - font-weight: 600; - letter-spacing: -0.01em; - text-wrap: balance; - overflow: hidden; - text-overflow: ellipsis; - display: -webkit-box; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; -} - -.epg-item__description { - font-size: 0.82rem; - line-height: 1.4; - color: var(--app-body-color); - text-wrap: pretty; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.program-progress { - padding-top: 2px; -} - -.epg-progress-track { - position: relative; - width: 100%; - height: 6px; - overflow: hidden; - border-radius: 999px; - background-color: rgba(127, 127, 127, 0.28); - background-image: linear-gradient( - 180deg, - rgba(255, 255, 255, 0.12), - rgba(255, 255, 255, 0) - ); - box-shadow: - inset 0 0 0 1px rgba(255, 255, 255, 0.12), - inset 0 1px 1px rgba(0, 0, 0, 0.18); -} - -.epg-progress-fill { - position: absolute; - inset-block: 0; - inset-inline-start: 0; - border-radius: inherit; - background-color: var(--app-selection-color); - background-image: linear-gradient( - 90deg, - rgba(255, 255, 255, 0.18), - rgba(255, 255, 255, 0) - ); - box-shadow: - 0 0 0 1px color-mix(in srgb, var(--app-selection-border) 82%, transparent), - 0 0 10px -2px var(--app-selection-glow); - transition: width 0.35s ease; -} - -.epg-empty-state { - flex: 1; - min-height: 0; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - padding: 40px 24px; - text-align: center; - color: var(--app-muted-color); - - .empty-state-icon { - width: 48px; - height: 48px; - font-size: 48px; - opacity: 0.5; - margin-bottom: 16px; - } - - .empty-state-title { - font-size: 1.05rem; - font-weight: 520; - letter-spacing: -0.01em; - margin-bottom: 8px; - text-wrap: balance; - } - - .empty-state-hint { - opacity: 0.65; - font-size: 0.85rem; - max-width: 240px; - line-height: 1.4; - text-wrap: pretty; - } -} diff --git a/libs/ui/epg/src/lib/epg-list/epg-list.component.spec.ts b/libs/ui/epg/src/lib/epg-list/epg-list.component.spec.ts deleted file mode 100644 index 9db0c2721..000000000 --- a/libs/ui/epg/src/lib/epg-list/epg-list.component.spec.ts +++ /dev/null @@ -1,508 +0,0 @@ -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 { Store } from '@ngrx/store'; -import { TranslatePipe, TranslateService } from '@ngx-translate/core'; -import { Channel, EpgProgram } from '@iptvnator/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, - template: '', -}) -class StubEpgListItemComponent { - readonly item = input(); - readonly isLive = input(false); - readonly isActive = input(false); - readonly showArchiveBadge = input(false); -} - -describe('EpgListComponent', () => { - let fixture: ComponentFixture; - let component: EpgListComponent; - let store: { select: jest.Mock; dispatch: jest.Mock }; - let epgService: { currentEpgPrograms$: BehaviorSubject }; - let translateService: { - currentLang: string; - defaultLang: string; - onLangChange: BehaviorSubject; - }; - let originalRequestAnimationFrame: typeof window.requestAnimationFrame; - - const fixedNow = new Date('2026-04-05T12:00:00.000Z'); - const controlledChannel: Channel = { - id: 'channel-101', - name: 'Channel 101', - url: 'https://example.com/live.m3u8', - group: { title: 'News' }, - tvg: { - id: 'channel-101', - name: 'Channel 101', - url: '', - logo: 'channel-101.png', - rec: '3', - }, - http: { referrer: '', 'user-agent': '', origin: '' }, - radio: 'false', - epgParams: '', - }; - - beforeEach(async () => { - jest.useFakeTimers(); - jest.setSystemTime(fixedNow); - - originalRequestAnimationFrame = window.requestAnimationFrame; - requestAnimationFrameQueue = []; - window.requestAnimationFrame = ((callback: FrameRequestCallback) => { - requestAnimationFrameQueue.push(callback); - return requestAnimationFrameQueue.length; - }) as typeof window.requestAnimationFrame; - - store = { - select: jest.fn().mockReturnValue(of(null)), - dispatch: jest.fn(), - }; - epgService = { - currentEpgPrograms$: new BehaviorSubject([]), - }; - translateService = { - currentLang: 'en', - defaultLang: 'en', - onLangChange: new BehaviorSubject(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, TranslatePipe], - }, - add: { - imports: [ - StubEpgListItemComponent, - MockPipe( - TranslatePipe, - (value: string | null | undefined) => value ?? '' - ), - ], - }, - }) - .compileComponents(); - - fixture = TestBed.createComponent(EpgListComponent); - component = fixture.componentInstance; - fixture.componentRef.setInput('controlledChannel', controlledChannel); - fixture.componentRef.setInput('controlledArchiveDays', 3); - }); - - afterEach(() => { - fixture.destroy(); - window.requestAnimationFrame = originalRequestAnimationFrame; - jest.useRealTimers(); - }); - - it('auto-scrolls once on the initial current-day load', () => { - fixture.componentRef.setInput('controlledPrograms', buildPrograms()); - - fixture.detectChanges(); - const scrollToSpy = configureScrollableProgramList(fixture); - - flushAnimationFrames(); - - expect(scrollToSpy).toHaveBeenCalledTimes(1); - expect(scrollToSpy).toHaveBeenCalledWith({ top: 172 }); - }); - - it('does not re-trigger auto-scroll on timer-driven updates', () => { - fixture.componentRef.setInput('controlledPrograms', buildPrograms()); - - fixture.detectChanges(); - const scrollToSpy = configureScrollableProgramList(fixture); - flushAnimationFrames(); - - scrollToSpy.mockClear(); - jest.advanceTimersByTime(30_000); - fixture.detectChanges(); - flushAnimationFrames(); - - expect(scrollToSpy).not.toHaveBeenCalled(); - }); - - it('keeps older same-day rows reachable after the initial scroll', () => { - fixture.componentRef.setInput('controlledPrograms', buildPrograms()); - - fixture.detectChanges(); - const scrollToSpy = configureScrollableProgramList(fixture); - const programList = fixture.nativeElement.querySelector( - '#program-list' - ) as HTMLElement; - flushAnimationFrames(); - - scrollToSpy.mockClear(); - programList.scrollTop = 0; - jest.advanceTimersByTime(30_000); - fixture.detectChanges(); - flushAnimationFrames(); - - expect(programList.scrollTop).toBe(0); - expect(scrollToSpy).not.toHaveBeenCalled(); - }); - - it('does not re-arm auto-scroll when changing dates for the same selection', () => { - fixture.componentRef.setInput('controlledPrograms', buildPrograms()); - - fixture.detectChanges(); - const scrollToSpy = configureScrollableProgramList(fixture); - flushAnimationFrames(); - - scrollToSpy.mockClear(); - component.changeDate('prev'); - fixture.detectChanges(); - component.changeDate('next'); - fixture.detectChanges(); - flushAnimationFrames(); - - expect(scrollToSpy).not.toHaveBeenCalled(); - }); - - it('re-arms auto-scroll when the effective selection changes', () => { - fixture.componentRef.setInput('controlledPrograms', buildPrograms()); - - fixture.detectChanges(); - let scrollToSpy = configureScrollableProgramList(fixture); - flushAnimationFrames(); - scrollToSpy.mockClear(); - - fixture.componentRef.setInput( - 'controlledPrograms', - buildPrograms({ - currentStart: '2026-04-05T11:45:00.000Z', - currentStop: '2026-04-05T12:45:00.000Z', - futureStart: '2026-04-05T12:45:00.000Z', - futureStop: '2026-04-05T13:45:00.000Z', - }) - ); - fixture.detectChanges(); - scrollToSpy = configureScrollableProgramList(fixture); - flushAnimationFrames(); - - expect(scrollToSpy).toHaveBeenCalledTimes(1); - }); - - it('shows archived rows as clickable only when archive playback is supported', () => { - const programs = buildPrograms(); - fixture.componentRef.setInput('controlledPrograms', programs); - fixture.componentRef.setInput('archivePlaybackAvailable', false); - - fixture.detectChanges(); - - expect(component.canActivateProgram(programs[0])).toBe(false); - expect(component.canActivateProgram(programs[1])).toBe(true); - expect( - fixture.nativeElement.querySelectorAll('.program-item.clickable') - .length - ).toBe(1); - - fixture.componentRef.setInput('archivePlaybackAvailable', true); - fixture.detectChanges(); - - expect(component.canActivateProgram(programs[0])).toBe(true); - expect( - fixture.nativeElement.querySelectorAll('.program-item.clickable') - .length - ).toBe(2); - }); - - it('highlights an externally active archived program without marking the live program active', () => { - const programs = buildPrograms(); - fixture.componentRef.setInput('controlledPrograms', programs); - fixture.componentRef.setInput('archivePlaybackAvailable', true); - fixture.componentRef.setInput('activeProgram', programs[0]); - - fixture.detectChanges(); - - const rows = fixture.nativeElement.querySelectorAll('.program-item'); - - expect(rows[0].classList).toContain('active'); - expect(rows[1].classList).toContain('current-program'); - expect(rows[1].classList).not.toContain('active'); - }); - - it('exposes playable rows as keyboard-activatable buttons', () => { - const programs = buildPrograms(); - const emitted: unknown[] = []; - component.programActivated.subscribe((event) => emitted.push(event)); - fixture.componentRef.setInput('controlledPrograms', programs); - fixture.componentRef.setInput('archivePlaybackAvailable', true); - - fixture.detectChanges(); - - const archivedRow = fixture.nativeElement.querySelector( - '.program-item.clickable' - ) as HTMLElement; - archivedRow.dispatchEvent( - new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }) - ); - - expect(archivedRow.getAttribute('role')).toBe('button'); - expect(archivedRow.getAttribute('tabindex')).toBe('0'); - expect(emitted).toEqual([{ program: programs[0], type: 'timeshift' }]); - }); - - it('renders duplicate-start programs without duplicate track-key errors and keeps clicks bound to each row', () => { - const programs = [ - buildProgram( - 'duplicate-a', - 'Duplicate Start A', - '2026-04-05T09:00:00.000Z', - '2026-04-05T09:30:00.000Z' - ), - buildProgram( - 'duplicate-b', - 'Duplicate Start B', - '2026-04-05T09:00:00.000Z', - '2026-04-05T10:00:00.000Z' - ), - buildProgram( - 'current', - 'Current Show', - '2026-04-05T11:30:00.000Z', - '2026-04-05T12:30:00.000Z' - ), - ]; - const emitted: unknown[] = []; - const consoleErrorSpy = jest - .spyOn(console, 'error') - .mockImplementation(() => undefined); - component.programActivated.subscribe((event) => emitted.push(event)); - fixture.componentRef.setInput('controlledPrograms', programs); - fixture.componentRef.setInput('archivePlaybackAvailable', true); - - try { - expect(component.trackProgram(0, programs[0])).not.toBe( - component.trackProgram(1, programs[1]) - ); - - fixture.detectChanges(); - - const duplicateTrackKeyError = consoleErrorSpy.mock.calls.some( - (args) => args.some((arg) => String(arg).includes('NG0955')) - ); - const rows = - fixture.nativeElement.querySelectorAll( - '.program-item.clickable' - ); - rows[1].click(); - - expect(duplicateTrackKeyError).toBe(false); - expect(rows).toHaveLength(3); - expect(emitted).toEqual([ - { program: programs[1], type: 'timeshift' }, - ]); - } finally { - consoleErrorSpy.mockRestore(); - } - }); - - it('shows one row per duplicated time slot and keeps the richer program entry', () => { - const sparseProgram = buildProgram( - 'duplicate-a', - 'World Cup Breakfast', - '2026-04-05T09:00:00.000Z', - '2026-04-05T10:00:00.000Z', - { - channel: 'channel-101', - desc: null, - } - ); - const richProgram = buildProgram( - 'duplicate-b', - 'World Cup Breakfast', - '2026-04-05T09:00:00.000Z', - '2026-04-05T10:00:00.000Z', - { - channel: 'channel-101', - desc: 'A preview of the day ahead.', - category: 'Sports', - } - ); - const nextProgram = buildProgram( - 'next', - 'World Cup Matchday', - '2026-04-05T10:00:00.000Z', - '2026-04-05T11:00:00.000Z', - { - channel: 'channel-101', - } - ); - const emitted: unknown[] = []; - component.programActivated.subscribe((event) => emitted.push(event)); - fixture.componentRef.setInput('controlledPrograms', [ - sparseProgram, - richProgram, - nextProgram, - ]); - fixture.componentRef.setInput('archivePlaybackAvailable', true); - - fixture.detectChanges(); - - const rows = - fixture.nativeElement.querySelectorAll( - '.program-item' - ); - rows[0].click(); - - expect(component.filteredItems()).toEqual([ - richProgram, - nextProgram, - ]); - expect(rows).toHaveLength(2); - expect(emitted).toEqual([{ program: richProgram, type: 'timeshift' }]); - }); - - 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'); - }); - - it('can hide its internal date navigator for a parent-owned toolbar', () => { - fixture.componentRef.setInput('controlledPrograms', buildPrograms()); - fixture.componentRef.setInput('showDateNavigator', false); - fixture.detectChanges(); - - expect( - fixture.nativeElement.querySelector('#channel-header') - ).toBeNull(); - expect( - fixture.nativeElement.querySelector('#program-list') - ).not.toBeNull(); - }); - - it('emits date changes when the selected date is controlled', () => { - const emittedDates: string[] = []; - component.selectedDateChange.subscribe((date) => - emittedDates.push(date) - ); - fixture.componentRef.setInput('selectedDate', '2026-04-04'); - fixture.componentRef.setInput('controlledPrograms', buildPrograms()); - fixture.detectChanges(); - - component.changeDate('next'); - - expect(emittedDates).toEqual(['2026-04-05']); - expect(component.selectedDateKey()).toBe('2026-04-04'); - }); -}); - -function buildPrograms(overrides?: { - currentStart?: string; - currentStop?: string; - futureStart?: string; - futureStop?: string; -}): EpgProgram[] { - const pastStart = '2026-04-05T09:00:00.000Z'; - const pastStop = '2026-04-05T10:00:00.000Z'; - const currentStart = overrides?.currentStart ?? '2026-04-05T11:30:00.000Z'; - const currentStop = overrides?.currentStop ?? '2026-04-05T12:30:00.000Z'; - const futureStart = overrides?.futureStart ?? '2026-04-05T12:30:00.000Z'; - const futureStop = overrides?.futureStop ?? '2026-04-05T13:30:00.000Z'; - - return [ - buildProgram('past', 'Past Show', pastStart, pastStop), - buildProgram('current', 'Current Show', currentStart, currentStop), - buildProgram('future', 'Future Show', futureStart, futureStop), - ]; -} - -function buildProgram( - channelSuffix: string, - title: string, - start: string, - stop: string, - overrides: Partial = {} -): EpgProgram { - return { - start, - stop, - channel: `channel-${channelSuffix}`, - title, - desc: null, - category: null, - startTimestamp: Math.floor(Date.parse(start) / 1000), - stopTimestamp: Math.floor(Date.parse(stop) / 1000), - ...overrides, - }; -} - -function configureScrollableProgramList( - fixture: ComponentFixture -): jest.Mock { - const programList = fixture.nativeElement.querySelector( - '#program-list' - ) as HTMLElement; - const currentProgram = fixture.nativeElement.querySelector( - '.program-item.current-program' - ) as HTMLElement; - - let scrollTop = 0; - Object.defineProperty(programList, 'scrollTop', { - configurable: true, - get: () => scrollTop, - set: (value: number) => { - scrollTop = value; - }, - }); - Object.defineProperty(programList, 'clientHeight', { - configurable: true, - value: 120, - }); - Object.defineProperty(currentProgram, 'offsetTop', { - configurable: true, - value: 240, - }); - Object.defineProperty(currentProgram, 'offsetHeight', { - configurable: true, - value: 36, - }); - - const scrollToSpy = jest.fn(({ top }: { top: number }) => { - scrollTop = top; - }); - programList.scrollTo = scrollToSpy as typeof programList.scrollTo; - - return scrollToSpy; -} - -function flushAnimationFrames(): void { - while (requestAnimationFrameQueue.length > 0) { - const callback = requestAnimationFrameQueue.shift(); - callback?.(0); - } -} diff --git a/libs/ui/epg/src/lib/epg-list/epg-list.component.ts b/libs/ui/epg/src/lib/epg-list/epg-list.component.ts deleted file mode 100644 index 739093f0f..000000000 --- a/libs/ui/epg/src/lib/epg-list/epg-list.component.ts +++ /dev/null @@ -1,383 +0,0 @@ -import { DatePipe } from '@angular/common'; -import { - ChangeDetectionStrategy, - Component, - ElementRef, - computed, - effect, - inject, - input, - output, - signal, - viewChild, -} 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 { EpgService } from '@iptvnator/epg/data-access'; -import { normalizeDateLocale } from '@iptvnator/pipes'; -import { Store } from '@ngrx/store'; -import { TranslatePipe, TranslateService } from '@ngx-translate/core'; -import { EpgActions, selectActive } from '@iptvnator/m3u-state'; -import { subDays } from 'date-fns'; -import { startWith } from 'rxjs'; -import { Channel, EpgChannel, EpgProgram } from '@iptvnator/shared/interfaces'; -import { - EpgDateNavigationDirection, - getTodayEpgDateKey, - shiftEpgDateKey, -} from '../epg-date'; -import { EpgListItemComponent } from './epg-list-item/epg-list-item.component'; -import { - areProgramsSame, - buildScrollContextKey, - deduplicateProgramsByTimeSlot, - getProgramDateKey, - getProgramTimeMs, - trackProgram, -} from './epg-list.utils'; - -export interface EpgProgramActivationEvent { - program: EpgProgram; - type: 'live' | 'timeshift'; -} - -@Component({ - imports: [ - DatePipe, - EpgListItemComponent, - MatIcon, - MatIconButton, - MatTooltip, - TranslatePipe, - ], - selector: 'app-epg-list', - templateUrl: './epg-list.component.html', - styleUrls: ['./epg-list.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class EpgListComponent { - readonly controlledChannel = input(null); - readonly controlledPrograms = input(null); - readonly controlledArchiveDays = input(null); - readonly archivePlaybackAvailable = input(null); - readonly activeProgram = input(null); - readonly selectedDateInput = input(null, { - alias: 'selectedDate', - }); - readonly showDateNavigator = input(true); - readonly programActivated = output(); - readonly selectedDateChange = output(); - readonly trackProgram = trackProgram; - - 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, - }); - private readonly servicePrograms = toSignal( - this.epgService.currentEpgPrograms$, - { initialValue: [] as EpgProgram[] } - ); - private readonly languageTick = toSignal( - this.translate.onLangChange.pipe(startWith(null)), - { initialValue: null } - ); - - readonly programList = viewChild>('programList'); - readonly internalSelectedDate = signal(getTodayEpgDateKey()); - readonly selectedDateKey = computed( - () => this.selectedDateInput() ?? this.internalSelectedDate() - ); - 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( - () => - this.controlledChannel() !== null || - this.controlledPrograms() !== null - ); - readonly displayChannel = computed( - () => this.controlledChannel() ?? this.activeChannel() ?? null - ); - readonly channel = computed(() => { - const channel = this.displayChannel(); - if (!channel) { - return null; - } - - return { - id: channel.tvg?.id || '', - displayName: channel.name - ? [{ lang: '', value: channel.name }] - : [], - url: channel.url ? [channel.url] : [], - icon: channel.tvg?.logo ? [{ src: channel.tvg.logo }] : [], - }; - }); - readonly items = computed( - () => this.controlledPrograms() ?? this.servicePrograms() ?? [] - ); - readonly archiveDays = computed(() => { - const controlledArchiveDays = this.controlledArchiveDays(); - if (controlledArchiveDays !== null) { - return Math.max(0, controlledArchiveDays); - } - - const channel = this.displayChannel(); - const value = - channel?.tvg?.rec || channel?.timeshift || channel?.catchup?.days; - return Math.max(0, Number(value ?? 0) || 0); - }); - readonly timeshiftUntil = computed(() => - subDays(new Date(), this.archiveDays()).toISOString() - ); - readonly archivePlaybackEnabled = computed( - () => this.archivePlaybackAvailable() ?? this.archiveDays() > 0 - ); - readonly filteredItems = computed(() => - deduplicateProgramsByTimeSlot( - [...this.items()] - .filter( - (item) => - getProgramDateKey(item.start, item.startTimestamp) === - this.selectedDateKey() - ) - .sort( - (left, right) => - getProgramTimeMs(left.start, left.startTimestamp) - - getProgramTimeMs(right.start, right.startTimestamp) - ) - ) - ); - - private scrollScheduled = false; - private activeScrollContextKey: string | null = null; - private lastAutoScrolledContextKey: string | null = null; - - constructor() { - effect((onCleanup) => { - const intervalId = window.setInterval(() => { - this.timeNow.set(new Date().toISOString()); - }, 30_000); - - onCleanup(() => clearInterval(intervalId)); - }); - - effect(() => { - const programs = this.items(); - const channel = this.displayChannel(); - this.currentTimeMs(); - - if (!this.isControlled()) { - this.store.dispatch( - EpgActions.setEpgAvailableFlag({ - value: programs.length > 0, - }) - ); - - if (programs.length > 0) { - const currentProgram = this.findCurrentProgram(programs); - if (currentProgram) { - this.store.dispatch( - EpgActions.setCurrentEpgProgram({ - program: currentProgram, - }) - ); - } else { - this.store.dispatch(EpgActions.resetActiveEpgProgram()); - } - } else { - this.store.dispatch(EpgActions.resetActiveEpgProgram()); - } - } - - if (!channel && !this.isControlled()) { - this.store.dispatch(EpgActions.resetActiveEpgProgram()); - } - - this.updateScrollContext(channel, programs); - this.scheduleScrollToCurrentProgram(); - }); - } - - changeDate(direction: EpgDateNavigationDirection): void { - const nextDate = shiftEpgDateKey(this.selectedDateKey(), direction); - - if (this.selectedDateInput() == null) { - this.internalSelectedDate.set(nextDate); - } - - this.selectedDateChange.emit(nextDate); - } - - activateProgram(program: EpgProgram): void { - const isLive = this.isProgramPlaying(program); - const isTimeshift = this.canPlayArchivedProgram(program); - - if (!isLive && !isTimeshift) { - return; - } - - if (this.isControlled()) { - this.programActivated.emit({ - program, - type: isLive ? 'live' : 'timeshift', - }); - this.timeNow.set(new Date().toISOString()); - return; - } - - if (isLive) { - this.store.dispatch(EpgActions.returnToLivePlayback()); - } else { - this.store.dispatch(EpgActions.setActiveEpgProgram({ program })); - } - - this.timeNow.set(new Date().toISOString()); - } - - activateProgramFromKeyboard(event: Event, program: EpgProgram): void { - event.preventDefault(); - this.activateProgram(program); - } - - canActivateProgram(program: EpgProgram): boolean { - return ( - this.isProgramPlaying(program) || - this.canPlayArchivedProgram(program) - ); - } - - canPlayArchivedProgram(program: EpgProgram): boolean { - return this.archivePlaybackEnabled() && this.isProgramArchived(program); - } - - calculateProgress(program: EpgProgram): number { - const now = this.currentTimeMs(); - const start = getProgramTimeMs(program.start, program.startTimestamp); - const stop = getProgramTimeMs(program.stop, program.stopTimestamp); - const total = stop - start; - const elapsed = now - start; - - return total > 0 - ? Math.min(100, Math.max(0, (elapsed / total) * 100)) - : 0; - } - - isProgramPlaying(program: EpgProgram): boolean { - const now = this.currentTimeMs(); - const start = getProgramTimeMs(program.start, program.startTimestamp); - const stop = getProgramTimeMs(program.stop, program.stopTimestamp); - return now >= start && now <= stop; - } - - isProgramArchived(program: EpgProgram): boolean { - if (this.archiveDays() <= 0) { - return false; - } - - const now = this.currentTimeMs(); - const start = getProgramTimeMs(program.start, program.startTimestamp); - const stop = getProgramTimeMs(program.stop, program.stopTimestamp); - const archiveLimit = Date.parse(this.timeshiftUntil()); - - return stop < now && start >= archiveLimit; - } - - isProgramActive(program: EpgProgram): boolean { - const activeProgram = this.activeProgram(); - return activeProgram - ? areProgramsSame(program, activeProgram) - : this.isProgramPlaying(program); - } - - private findCurrentProgram(programs: EpgProgram[]): EpgProgram | undefined { - const now = this.currentTimeMs(); - return programs.find((program) => { - const start = getProgramTimeMs( - program.start, - program.startTimestamp - ); - const stop = getProgramTimeMs(program.stop, program.stopTimestamp); - return now >= start && now <= stop; - }); - } - - private updateScrollContext( - channel: Channel | null, - programs: EpgProgram[] - ): void { - const nextContextKey = buildScrollContextKey(channel, programs); - if (nextContextKey === this.activeScrollContextKey) { - return; - } - - this.activeScrollContextKey = nextContextKey; - this.scrollScheduled = false; - } - - private scheduleScrollToCurrentProgram(): void { - if (this.selectedDateKey() !== getTodayEpgDateKey()) { - return; - } - - const scrollContextKey = this.activeScrollContextKey; - if ( - !scrollContextKey || - this.lastAutoScrolledContextKey === scrollContextKey - ) { - return; - } - - if (this.scrollScheduled) { - return; - } - - this.scrollScheduled = true; - requestAnimationFrame(() => { - requestAnimationFrame(() => { - this.scrollScheduled = false; - this.scrollCurrentProgramIntoView(scrollContextKey); - }); - }); - } - - private scrollCurrentProgramIntoView(scrollContextKey: string): void { - const container = this.programList()?.nativeElement; - const currentProgram = container?.querySelector( - '.program-item.current-program' - ); - - if (!container || !currentProgram) { - return; - } - - this.lastAutoScrolledContextKey = scrollContextKey; - - const viewTop = container.scrollTop; - const viewBottom = viewTop + container.clientHeight; - const elementTop = currentProgram.offsetTop; - const elementBottom = elementTop + currentProgram.offsetHeight; - - if (elementTop < viewTop) { - container.scrollTo({ top: Math.max(elementTop - 16, 0) }); - return; - } - - if (elementBottom > viewBottom) { - container.scrollTo({ - top: elementBottom - container.clientHeight + 16, - }); - } - } -} diff --git a/libs/ui/epg/src/lib/epg-list/epg-list.utils.ts b/libs/ui/epg/src/lib/epg-list/epg-list.utils.ts deleted file mode 100644 index 463181137..000000000 --- a/libs/ui/epg/src/lib/epg-list/epg-list.utils.ts +++ /dev/null @@ -1,125 +0,0 @@ -import { format } from 'date-fns'; -import type { Channel, EpgProgram } from '@iptvnator/shared/interfaces'; -import { EPG_DATE_KEY_FORMAT } from '../epg-date'; - -export function trackProgram(index: number, program: EpgProgram): string { - const start = getProgramTimeMs(program.start, program.startTimestamp); - const stop = getProgramTimeMs(program.stop, program.stopTimestamp); - - return [ - program.channel ?? '', - Number.isFinite(start) ? start : program.start, - Number.isFinite(stop) ? stop : program.stop, - program.title ?? '', - index, - ].join('|'); -} - -export function deduplicateProgramsByTimeSlot( - programs: EpgProgram[] -): EpgProgram[] { - const programsByTimeSlot = new Map(); - - for (const program of programs) { - const timeSlotKey = buildProgramTimeSlotKey(program); - const existingProgram = programsByTimeSlot.get(timeSlotKey); - - programsByTimeSlot.set( - timeSlotKey, - existingProgram - ? selectMoreInformativeProgram(existingProgram, program) - : program - ); - } - - return Array.from(programsByTimeSlot.values()); -} - -export function buildScrollContextKey( - channel: Channel | null, - programs: EpgProgram[] -): string | null { - if (!channel && programs.length === 0) { - return null; - } - - const channelKey = - channel?.tvg?.id || channel?.name || channel?.url || 'unknown-channel'; - const programKey = programs - .map( - (program) => - `${getProgramTimeMs(program.start, program.startTimestamp)}-${getProgramTimeMs(program.stop, program.stopTimestamp)}` - ) - .join('|'); - - return `${channelKey}:${programKey}`; -} - -export function getProgramTimeMs( - isoValue: string, - timestampValue?: number | null -): number { - if (Number.isFinite(timestampValue) && Number(timestampValue) > 0) { - return Number(timestampValue) * 1000; - } - - return Date.parse(isoValue); -} - -export function getProgramDateKey( - isoValue: string, - timestampValue?: number | null -): string { - const programTimeMs = getProgramTimeMs(isoValue, timestampValue); - - if (!Number.isFinite(programTimeMs)) { - return ''; - } - - return format(new Date(programTimeMs), EPG_DATE_KEY_FORMAT); -} - -export function areProgramsSame( - left: EpgProgram, - right: EpgProgram -): boolean { - return ( - (left.channel ?? '') === (right.channel ?? '') && - getProgramTimeMs(left.start, left.startTimestamp) === - getProgramTimeMs(right.start, right.startTimestamp) && - getProgramTimeMs(left.stop, left.stopTimestamp) === - getProgramTimeMs(right.stop, right.stopTimestamp) - ); -} - -function buildProgramTimeSlotKey(program: EpgProgram): string { - return [ - getProgramTimeMs(program.start, program.startTimestamp), - getProgramTimeMs(program.stop, program.stopTimestamp), - ].join('|'); -} - -function selectMoreInformativeProgram( - existingProgram: EpgProgram, - candidateProgram: EpgProgram -): EpgProgram { - return getProgramMetadataScore(candidateProgram) > - getProgramMetadataScore(existingProgram) - ? candidateProgram - : existingProgram; -} - -function getProgramMetadataScore(program: EpgProgram): number { - return ( - getTextScore(program.desc) * 8 + - getTextScore(program.category) * 4 + - getTextScore(program.iconUrl) * 2 + - getTextScore(program.rating) * 2 + - getTextScore(program.episodeNum) * 2 + - getTextScore(program.title) - ); -} - -function getTextScore(value: string | null | undefined): number { - return value?.trim() ? 1 : 0; -} diff --git a/libs/ui/epg/src/lib/epg-program-activation-event.ts b/libs/ui/epg/src/lib/epg-program-activation-event.ts new file mode 100644 index 000000000..9eef35b22 --- /dev/null +++ b/libs/ui/epg/src/lib/epg-program-activation-event.ts @@ -0,0 +1,6 @@ +import type { EpgProgram } from '@iptvnator/shared/interfaces'; + +export interface EpgProgramActivationEvent { + program: EpgProgram; + type: 'live' | 'timeshift'; +} diff --git a/libs/ui/epg/src/lib/epg-program.utils.ts b/libs/ui/epg/src/lib/epg-program.utils.ts new file mode 100644 index 000000000..2a94cf275 --- /dev/null +++ b/libs/ui/epg/src/lib/epg-program.utils.ts @@ -0,0 +1,40 @@ +import { format } from 'date-fns'; +import type { EpgProgram } from '@iptvnator/shared/interfaces'; +import { EPG_DATE_KEY_FORMAT } from './epg-date'; + +export function getProgramTimeMs( + isoValue: string, + timestampValue?: number | null +): number { + if (Number.isFinite(timestampValue) && Number(timestampValue) > 0) { + return Number(timestampValue) * 1000; + } + + return Date.parse(isoValue); +} + +export function getProgramDateKey( + isoValue: string, + timestampValue?: number | null +): string { + const programTimeMs = getProgramTimeMs(isoValue, timestampValue); + + if (!Number.isFinite(programTimeMs)) { + return ''; + } + + return format(new Date(programTimeMs), EPG_DATE_KEY_FORMAT); +} + +export function areProgramsSame( + left: EpgProgram, + right: EpgProgram +): boolean { + return ( + (left.channel ?? '') === (right.channel ?? '') && + getProgramTimeMs(left.start, left.startTimestamp) === + getProgramTimeMs(right.start, right.startTimestamp) && + getProgramTimeMs(left.stop, left.stopTimestamp) === + getProgramTimeMs(right.stop, right.stopTimestamp) + ); +} diff --git a/libs/ui/epg/src/lib/epg-programme-dialog.service.ts b/libs/ui/epg/src/lib/epg-programme-dialog.service.ts index 14deeda08..8c396de5b 100644 --- a/libs/ui/epg/src/lib/epg-programme-dialog.service.ts +++ b/libs/ui/epg/src/lib/epg-programme-dialog.service.ts @@ -5,7 +5,7 @@ import { EpgItemDescriptionComponent, EpgItemDialogAction, EpgItemDialogData, -} from './epg-list/epg-item-description/epg-item-description.component'; +} from './epg-item-description/epg-item-description.component'; /** * Opens the shared programme-details dialog and returns the chosen action. diff --git a/libs/ui/epg/src/lib/epg-timeline/epg-archive.util.ts b/libs/ui/epg/src/lib/epg-timeline/epg-archive.util.ts index 17f0ce75b..a529ce6d2 100644 --- a/libs/ui/epg/src/lib/epg-timeline/epg-archive.util.ts +++ b/libs/ui/epg/src/lib/epg-timeline/epg-archive.util.ts @@ -1,4 +1,4 @@ -import { EpgItemDialogAction } from '../epg-list/epg-item-description/epg-item-description.component'; +import { EpgItemDialogAction } from '../epg-item-description/epg-item-description.component'; import { TimelineWhen } from './epg-timeline.utils'; const DAY_MS = 24 * 60 * 60_000; diff --git a/libs/ui/epg/src/lib/epg-timeline/epg-timeline-track.component.ts b/libs/ui/epg/src/lib/epg-timeline/epg-timeline-track.component.ts index ef5bd719b..efd599e9b 100644 --- a/libs/ui/epg/src/lib/epg-timeline/epg-timeline-track.component.ts +++ b/libs/ui/epg/src/lib/epg-timeline/epg-timeline-track.component.ts @@ -11,7 +11,7 @@ import { import { MatIcon } from '@angular/material/icon'; import { EpgProgram } from '@iptvnator/shared/interfaces'; import { TranslatePipe } from '@ngx-translate/core'; -import { areProgramsSame } from '../epg-list/epg-list.utils'; +import { areProgramsSame } from '../epg-program.utils'; import { TimelineBlock, TimelineDayDivider, diff --git a/libs/ui/epg/src/lib/epg-timeline/epg-timeline.component.ts b/libs/ui/epg/src/lib/epg-timeline/epg-timeline.component.ts index 6f70851ae..9ecc95204 100644 --- a/libs/ui/epg/src/lib/epg-timeline/epg-timeline.component.ts +++ b/libs/ui/epg/src/lib/epg-timeline/epg-timeline.component.ts @@ -26,8 +26,8 @@ import { parseEpgDateKey, shiftEpgDateKey, } from '../epg-date'; -import { EpgItemDialogAction } from '../epg-list/epg-item-description/epg-item-description.component'; -import type { EpgProgramActivationEvent } from '../epg-list/epg-list.component'; +import { EpgItemDialogAction } from '../epg-item-description/epg-item-description.component'; +import type { EpgProgramActivationEvent } from '../epg-program-activation-event'; import { EpgProgrammeDialogService } from '../epg-programme-dialog.service'; import { EpgTimelineSummary, diff --git a/libs/ui/epg/src/lib/epg-timeline/epg-timeline.utils.ts b/libs/ui/epg/src/lib/epg-timeline/epg-timeline.utils.ts index 0b7411e36..e5565e8cf 100644 --- a/libs/ui/epg/src/lib/epg-timeline/epg-timeline.utils.ts +++ b/libs/ui/epg/src/lib/epg-timeline/epg-timeline.utils.ts @@ -1,7 +1,7 @@ import { EpgProgram } from '@iptvnator/shared/interfaces'; import { addDays, addMinutes, startOfDay } from 'date-fns'; import { parseEpgDateKey } from '../epg-date'; -import { getProgramDateKey, getProgramTimeMs } from '../epg-list/epg-list.utils'; +import { getProgramDateKey, getProgramTimeMs } from '../epg-program.utils'; export const TIMELINE_MINUTE_MS = 60_000; /** Pixels per minute on the ribbon (matches the prototype's SCALE). */ diff --git a/libs/ui/epg/src/lib/multi-epg/multi-epg-container.component.ts b/libs/ui/epg/src/lib/multi-epg/multi-epg-container.component.ts index 5c363c18f..796fde481 100644 --- a/libs/ui/epg/src/lib/multi-epg/multi-epg-container.component.ts +++ b/libs/ui/epg/src/lib/multi-epg/multi-epg-container.component.ts @@ -30,7 +30,7 @@ import { EpgProgram, } from '@iptvnator/shared/interfaces'; import { EpgRuntimeBridgeService } from '@iptvnator/epg/data-access'; -import { EpgItemDescriptionComponent } from '../epg-list/epg-item-description/epg-item-description.component'; +import { EpgItemDescriptionComponent } from '../epg-item-description/epg-item-description.component'; import { COMPONENT_OVERLAY_REF } from './overlay-ref.token'; type MultiEpgChannel = ElectronBridgeEpgChannelWithPrograms; diff --git a/libs/ui/playback/src/index.ts b/libs/ui/playback/src/index.ts index 42d2800c3..f3a4b4e7b 100644 --- a/libs/ui/playback/src/index.ts +++ b/libs/ui/playback/src/index.ts @@ -10,6 +10,5 @@ export * from './lib/portal-inline-player/portal-inline-player.component'; export * from './lib/portal-inline-player/series-playback-navigation'; export * from './lib/vjs-player/vjs-player.component'; export * from './lib/video-player/sidebar/sidebar.component'; -export * from './lib/video-player/toolbar/toolbar.component'; export * from './lib/vod-details/vod-details.component'; export * from './lib/web-player-view/web-player-view.component'; diff --git a/libs/ui/playback/src/lib/video-player/toolbar/toolbar.component.html b/libs/ui/playback/src/lib/video-player/toolbar/toolbar.component.html deleted file mode 100644 index 16009270a..000000000 --- a/libs/ui/playback/src/lib/video-player/toolbar/toolbar.component.html +++ /dev/null @@ -1,58 +0,0 @@ - - - - - {{ activeChannel.name }} -
- - - @let isEpgAvailable = isEpgAvailable$ | async; - @if (isEpgAvailable) { - - } -
diff --git a/libs/ui/playback/src/lib/video-player/toolbar/toolbar.component.scss b/libs/ui/playback/src/lib/video-player/toolbar/toolbar.component.scss deleted file mode 100644 index 58fec352e..000000000 --- a/libs/ui/playback/src/lib/video-player/toolbar/toolbar.component.scss +++ /dev/null @@ -1,26 +0,0 @@ -mat-toolbar { - app-region: drag; - - button { - app-region: no-drag; - } -} - -.spacer { - flex: 1 1 auto; -} - -.mat-toolbar.mat-primary { - background: var(--mdc-primary-100); - color: #ccc; - - button { - color: #ccc; - } -} - -@media only screen and (max-width: 599px) { - .sidenav-toggle-button { - display: none; - } -} diff --git a/libs/ui/playback/src/lib/video-player/toolbar/toolbar.component.ts b/libs/ui/playback/src/lib/video-player/toolbar/toolbar.component.ts deleted file mode 100644 index 4614afd37..000000000 --- a/libs/ui/playback/src/lib/video-player/toolbar/toolbar.component.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { AsyncPipe } from '@angular/common'; -import { Component, inject, Input, output } from '@angular/core'; -import { MatButton, MatIconButton } from '@angular/material/button'; -import { MatDialogModule } from '@angular/material/dialog'; -import { MatIcon } from '@angular/material/icon'; -import { MatToolbarModule } from '@angular/material/toolbar'; -import { MatTooltip } from '@angular/material/tooltip'; -import { Store } from '@ngrx/store'; -import { TranslatePipe } from '@ngx-translate/core'; -import { - FavoritesActions, - selectFavorites, - selectIsEpgAvailable, -} from '@iptvnator/m3u-state'; -import { Channel } from '@iptvnator/shared/interfaces'; - -@Component({ - imports: [ - AsyncPipe, - MatButton, - MatDialogModule, - MatIcon, - MatIconButton, - MatToolbarModule, - MatTooltip, - TranslatePipe, - ], - selector: 'app-toolbar', - templateUrl: './toolbar.component.html', - styleUrls: ['./toolbar.component.scss'], -}) -export class ToolbarComponent { - @Input() activeChannel!: Channel; - @Input() isLeftDrawerOpened = true; - readonly infoOverlayClicked = output(); - readonly multiEpgClicked = output(); - readonly settingsClicked = output(); - readonly toggleLeftDrawerClicked = output(); - readonly toggleRightDrawerClicked = output(); - - private readonly store = inject(Store); - - readonly favorites$ = this.store.select(selectFavorites); - readonly isEpgAvailable$ = this.store.select(selectIsEpgAvailable); - - updateFavoriteStatus(channel: Channel) { - this.store.dispatch(FavoritesActions.updateFavorites({ channel })); - } -} diff --git a/libs/ui/shared-portals/src/index.ts b/libs/ui/shared-portals/src/index.ts index a59872e5a..9b08824e3 100644 --- a/libs/ui/shared-portals/src/index.ts +++ b/libs/ui/shared-portals/src/index.ts @@ -1,2 +1 @@ -export * from './lib/epg-view/epg-view.component'; -export * from './lib/live-epg-panel/live-epg-panel.component'; +export * from './lib/live-epg-panel-summary'; diff --git a/libs/ui/shared-portals/src/lib/epg-view/epg-view.component.html b/libs/ui/shared-portals/src/lib/epg-view/epg-view.component.html deleted file mode 100644 index bac4881c4..000000000 --- a/libs/ui/shared-portals/src/lib/epg-view/epg-view.component.html +++ /dev/null @@ -1,52 +0,0 @@ -
{{ 'EPG.TITLE' | translate }}
- -
- @for (item of epgItems; track $index) { -
-
-
- {{ item.start | date: 'HH:mm' }} - - {{ getProgramEnd(item) | date: 'HH:mm' }} -
- @if (isCurrentProgram(item)) { - Live - } - -
- -
{{ item.title }}
- - @if (item.description) { -
- {{ item.description }} -
- } - - @if (isCurrentProgram(item)) { -
-
-
-
-
- } -
- } - - @if (epgItems.length === 0) { -
- {{ 'EPG.EPG_NOT_AVAILABLE_CHANNEL_TITLE' | translate }} -
- } -
diff --git a/libs/ui/shared-portals/src/lib/epg-view/epg-view.component.scss b/libs/ui/shared-portals/src/lib/epg-view/epg-view.component.scss deleted file mode 100644 index df8a8aa08..000000000 --- a/libs/ui/shared-portals/src/lib/epg-view/epg-view.component.scss +++ /dev/null @@ -1,188 +0,0 @@ -:host { - display: flex; - flex-direction: column; - height: 100%; - min-height: 0; - overflow: hidden; -} - -.epg-title { - margin: 0; - padding: 14px 16px 10px; - font-size: 1.02rem; - font-weight: 620; - letter-spacing: 0.01em; - flex-shrink: 0; - position: sticky; - top: 0; - z-index: 2; -} - -.epg-list { - flex: 1; - min-height: 0; - overflow-y: auto; - display: flex; - flex-direction: column; - gap: 6px; - padding: 10px 10px 22px; - box-sizing: border-box; - scrollbar-gutter: stable; -} - -.epg-item { - display: flex; - flex-direction: column; - gap: 10px; - padding: 10px 14px 16px; - border: 1px solid rgba(255, 255, 255, 0.06); - border-radius: 14px; - background: - linear-gradient(180deg, rgba(255, 255, 255, 0.035), rgba(255, 255, 255, 0)), - rgba(127, 127, 127, 0.045); - box-shadow: - inset 0 1px 0 rgba(255, 255, 255, 0.04), - 0 10px 20px -20px rgba(0, 0, 0, 0.35); - transition: - background 0.15s ease, - border-color 0.15s ease, - box-shadow 0.18s ease, - transform 0.18s ease; - - &:hover { - background: - linear-gradient(180deg, rgba(255, 255, 255, 0.05), rgba(255, 255, 255, 0)), - rgba(127, 127, 127, 0.065); - transform: translateY(-1px); - } - - &.current-program { - background: linear-gradient( - 135deg, - var(--app-selection-surface-strong), - var(--app-selection-surface) - ); - border-color: var(--app-selection-border); - box-shadow: - inset 0 0 0 1px var(--app-selection-border), - 0 12px 20px -18px var(--app-selection-glow); - - .epg-item__title, - .epg-item__time, - .epg-item__description, - .epg-item__info { - color: var(--app-selection-color); - } - } -} - -.epg-item__meta { - display: flex; - align-items: center; - gap: 8px; -} - -.epg-item__time { - font-size: 0.78rem; - line-height: 1; - font-weight: 600; - letter-spacing: 0.02em; - font-variant-numeric: tabular-nums; - opacity: 0.76; -} - -.epg-item__badge { - padding: 2px 8px; - border-radius: 999px; - font-size: 0.68rem; - line-height: 1.4; - font-weight: 700; - text-transform: uppercase; - letter-spacing: 0.08em; - background: color-mix(in srgb, var(--app-selection-color) 18%, transparent); - color: var(--app-selection-color); - border: 1px solid color-mix(in srgb, var(--app-selection-border) 86%, transparent); -} - -.epg-item__info { - margin-inline-start: auto; - width: 34px; - height: 34px; - border-radius: 10px; - color: inherit; - opacity: 0.76; - transition: - opacity 0.15s ease, - background-color 0.15s ease, - transform 0.15s ease; - - &:hover { - opacity: 1; - background: color-mix(in srgb, currentColor 10%, transparent); - transform: translateY(-1px); - } -} - -.epg-item__title { - font-size: 0.94rem; - line-height: 1.35; - font-weight: 560; - letter-spacing: 0.005em; -} - -.epg-item__description { - font-size: 0.82rem; - line-height: 1.4; - opacity: 0.74; - display: -webkit-box; - overflow: hidden; - -webkit-line-clamp: 2; - -webkit-box-orient: vertical; -} - -.epg-item__progress { - padding-top: 4px; -} - -.epg-progress-track { - position: relative; - width: 100%; - height: 6px; - overflow: hidden; - border-radius: 999px; - background-color: rgba(127, 127, 127, 0.28); - background-image: linear-gradient( - 180deg, - rgba(255, 255, 255, 0.12), - rgba(255, 255, 255, 0) - ); - box-shadow: - inset 0 0 0 1px rgba(255, 255, 255, 0.12), - inset 0 1px 1px rgba(0, 0, 0, 0.18); -} - -.epg-progress-fill { - position: absolute; - inset-block: 0; - inset-inline-start: 0; - border-radius: inherit; - background-color: var(--app-selection-color); - background-image: linear-gradient( - 90deg, - rgba(255, 255, 255, 0.18), - rgba(255, 255, 255, 0) - ); - box-shadow: - 0 0 0 1px color-mix(in srgb, var(--app-selection-border) 82%, transparent), - 0 0 10px -2px var(--app-selection-glow); - transition: width 0.35s ease; -} - -.epg-empty { - padding: 22px; - border: 1px dashed rgba(255, 255, 255, 0.12); - border-radius: 14px; - text-align: center; - font-size: 0.88rem; - opacity: 0.66; -} diff --git a/libs/ui/shared-portals/src/lib/epg-view/epg-view.component.spec.ts b/libs/ui/shared-portals/src/lib/epg-view/epg-view.component.spec.ts deleted file mode 100644 index 86ee7eec1..000000000 --- a/libs/ui/shared-portals/src/lib/epg-view/epg-view.component.spec.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { MatDialog } from '@angular/material/dialog'; -import { TranslateModule } from '@ngx-translate/core'; -import { EpgItem } from '@iptvnator/shared/interfaces'; -import { EpgViewComponent } from './epg-view.component'; - -describe('EpgViewComponent', () => { - let fixture: ComponentFixture; - - beforeEach(async () => { - await TestBed.configureTestingModule({ - imports: [EpgViewComponent, TranslateModule.forRoot()], - providers: [ - { - provide: MatDialog, - useValue: { - open: jest.fn(), - }, - }, - ], - }).compileComponents(); - - fixture = TestBed.createComponent(EpgViewComponent); - }); - - afterEach(() => { - fixture.destroy(); - }); - - it('renders the end timestamp when stop is absent at runtime', () => { - fixture.componentInstance.epgItems = [ - { - id: 'epg-1', - epg_id: 'channel-1', - title: 'Fallback program', - lang: 'en', - start: '2026-04-05T11:00:00', - end: '2026-04-05T12:30:00', - description: 'Program description', - channel_id: 'channel-1', - start_timestamp: '1775386800', - stop_timestamp: '1775392200', - } as EpgItem, - ]; - - fixture.detectChanges(); - - expect(fixture.nativeElement.textContent).toContain('11:00 - 12:30'); - }); -}); diff --git a/libs/ui/shared-portals/src/lib/epg-view/epg-view.component.ts b/libs/ui/shared-portals/src/lib/epg-view/epg-view.component.ts deleted file mode 100644 index fe260272f..000000000 --- a/libs/ui/shared-portals/src/lib/epg-view/epg-view.component.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { DatePipe } from '@angular/common'; -import { Component, Input, inject } from '@angular/core'; -import { MatIconButton } from '@angular/material/button'; -import { MatDialog } from '@angular/material/dialog'; -import { MatIcon } from '@angular/material/icon'; -import { MatListModule } from '@angular/material/list'; -import { TranslateModule } from '@ngx-translate/core'; -import { EpgItemDescriptionComponent } from '@iptvnator/ui/epg'; -import { EpgItem } from '@iptvnator/shared/interfaces'; - -@Component({ - selector: 'app-epg-view', - templateUrl: './epg-view.component.html', - imports: [DatePipe, MatIconButton, MatIcon, MatListModule, TranslateModule], - styleUrls: ['./epg-view.component.scss'], -}) -export class EpgViewComponent { - @Input() epgItems: EpgItem[] = []; - - dialog = inject(MatDialog); - - isCurrentProgram(item: EpgItem): boolean { - const end = this.getProgramEnd(item); - const now = new Date().getTime(); - const start = new Date(item.start).getTime(); - const stop = new Date(end).getTime(); - return now >= start && now <= stop; - } - - getProgress(item: EpgItem): number { - const now = new Date().getTime(); - const start = new Date(item.start).getTime(); - const end = new Date(this.getProgramEnd(item)).getTime(); - - const total = end - start; - const current = now - start; - - return Math.min(Math.max((current / total) * 100, 0), 100); - } - - getProgramEnd(item: EpgItem): string { - return item.stop ?? item.end; - } - - showDetails(item: EpgItem) { - this.dialog.open(EpgItemDescriptionComponent, { - width: '600px', - data: { - title: item.title ?? 'No title', - desc: item.description ?? 'No description', - }, - }); - } -} diff --git a/libs/ui/shared-portals/src/lib/live-epg-panel-summary.ts b/libs/ui/shared-portals/src/lib/live-epg-panel-summary.ts new file mode 100644 index 000000000..c5b4e613e --- /dev/null +++ b/libs/ui/shared-portals/src/lib/live-epg-panel-summary.ts @@ -0,0 +1,6 @@ +export interface LiveEpgPanelSummary { + readonly title?: string | null; + readonly start?: string | number | Date | null; + readonly stop?: string | number | Date | null; + readonly progress?: number | null; +} diff --git a/libs/ui/shared-portals/src/lib/live-epg-panel/live-epg-panel.component.html b/libs/ui/shared-portals/src/lib/live-epg-panel/live-epg-panel.component.html deleted file mode 100644 index 768fe12b4..000000000 --- a/libs/ui/shared-portals/src/lib/live-epg-panel/live-epg-panel.component.html +++ /dev/null @@ -1,169 +0,0 @@ -
-
- - -
- - {{ summaryLabelKey() | translate }} - - -
- @if (loading()) { - - {{ 'EPG.LOADING' | translate }} - - } @else if (hasSummary()) { - - {{ summary()?.title }} - - - @if (hasTimeRange()) { - - @if (summary()?.start) { - {{ summary()?.start | date: 'HH:mm' }} - } - @if (summary()?.start && summary()?.stop) { - - - } - @if (summary()?.stop) { - {{ summary()?.stop | date: 'HH:mm' }} - } - - } - } @else { - - {{ 'EPG.NO_PROGRAM_INFO' | translate }} - - } -
-
- - @if (showReturnToLive()) { - - } - - @if (showDateNavigator()) { - @if (!collapsed()) { -
- - - {{ - selectedDateValue() - | date - : 'MMMM d, EEEE' - : undefined - : currentLocale() - }} - - -
- - - - - - - - } - } - - @if (progress() !== null) { - - } -
- -
- -
-
diff --git a/libs/ui/shared-portals/src/lib/live-epg-panel/live-epg-panel.component.scss b/libs/ui/shared-portals/src/lib/live-epg-panel/live-epg-panel.component.scss deleted file mode 100644 index 42fa6a207..000000000 --- a/libs/ui/shared-portals/src/lib/live-epg-panel/live-epg-panel.component.scss +++ /dev/null @@ -1,291 +0,0 @@ -:host { - display: block; - height: 100%; - min-height: 0; -} - -.live-epg-panel { - display: flex; - flex-direction: column; - height: 100%; - min-height: 0; - overflow: hidden; - background: var(--mat-sys-surface); -} - -.live-epg-panel__summary-bar { - position: relative; - display: grid; - grid-template-columns: auto minmax(0, 1fr) auto auto; - align-items: center; - column-gap: 8px; - min-height: 56px; - padding: 7px 12px 9px 8px; - flex-shrink: 0; - border-bottom: 1px solid var(--app-separator); - background: var( - --app-widget-header-bg, - var(--mat-sys-surface-container-high) - ); - box-sizing: border-box; -} - -.live-epg-panel__toggle { - width: 40px; - height: 40px; - border-radius: 10px; - color: var(--mat-sys-on-surface-variant); - app-region: no-drag; - - &:hover { - color: var(--mat-sys-on-surface); - background: color-mix(in srgb, currentColor 10%, transparent); - } -} - -.live-epg-panel__summary { - min-width: 0; - display: grid; - align-content: center; - row-gap: 3px; -} - -.live-epg-panel__program-line { - min-width: 0; - display: flex; - align-items: baseline; - gap: 8px; -} - -.live-epg-panel__label { - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - font-size: 0.61rem; - line-height: 1; - font-weight: 700; - letter-spacing: 0.09em; - text-transform: uppercase; - color: var(--app-selection-color); -} - -.live-epg-panel__title { - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - font-size: 0.94rem; - line-height: 1.25; - font-weight: 600; - color: var(--mat-sys-on-surface); -} - -.live-epg-panel__title--muted { - font-weight: 500; - color: var(--mat-sys-on-surface-variant); -} - -.live-epg-panel__time { - flex-shrink: 0; - font-size: 0.78rem; - line-height: 1; - font-weight: 600; - font-variant-numeric: tabular-nums; - color: var(--mat-sys-on-surface-variant); -} - -.live-epg-panel__return-live { - min-width: 0; - height: 36px; - padding-inline: 10px 12px; - border-radius: 10px; - color: var(--app-selection-color); - background: color-mix( - in srgb, - var(--app-selection-color) 10%, - transparent - ); - app-region: no-drag; - - mat-icon { - flex-shrink: 0; - width: 18px; - height: 18px; - font-size: 18px; - } - - .live-epg-panel__return-live-text { - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - font-size: 0.78rem; - font-weight: 700; - } - - &:hover { - color: var(--mat-sys-on-primary-container); - background: color-mix( - in srgb, - var(--app-selection-color) 16%, - transparent - ); - } -} - -.live-epg-panel__date-nav { - display: flex; - align-items: center; - gap: 6px; - min-width: 0; - app-region: no-drag; - - button { - width: 40px; - height: 40px; - border-radius: 10px; - color: var(--mat-sys-on-surface-variant); - - &:hover:not([disabled]) { - color: var(--mat-sys-on-surface); - background: color-mix(in srgb, currentColor 10%, transparent); - } - } -} - -.live-epg-panel__selected-date { - min-width: 150px; - max-width: 220px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - text-align: center; - font-size: 0.83rem; - line-height: 1.2; - font-weight: 600; - color: var(--mat-sys-on-surface-variant); -} - -.live-epg-panel__date-chip { - display: none; - min-width: 0; - max-width: 150px; - height: 36px; - padding-inline: 10px; - border-radius: 10px; - color: var(--mat-sys-on-surface-variant); - background: color-mix(in srgb, currentColor 7%, transparent); - app-region: no-drag; - - mat-icon { - flex-shrink: 0; - width: 18px; - height: 18px; - font-size: 18px; - } - - span { - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - font-size: 0.78rem; - font-weight: 600; - } -} - -.live-epg-panel__progress { - position: absolute; - inset-inline: 8px 12px; - bottom: 5px; - height: 4px; - overflow: hidden; - border-radius: 999px; - background: color-mix(in srgb, var(--mat-sys-on-surface) 14%, transparent); -} - -.live-epg-panel__progress-fill { - height: 100%; - border-radius: inherit; - background: var(--app-selection-color); - box-shadow: 0 0 8px -2px var(--app-selection-glow); - transition: width 0.35s ease; -} - -.live-epg-panel__body { - flex: 1 1 auto; - min-height: 0; - display: flex; - flex-direction: column; - overflow: hidden; - transition: - opacity 0.16s ease, - block-size 0.16s ease; -} - -.live-epg-panel--collapsed { - .live-epg-panel__summary-bar { - border-bottom-color: transparent; - } - - .live-epg-panel__date-nav { - display: none; - } - - .live-epg-panel__summary { - display: flex; - align-items: baseline; - gap: 8px; - } - - .live-epg-panel__label { - flex-shrink: 0; - font-size: 0.7rem; - letter-spacing: 0.08em; - } - - .live-epg-panel__program-line { - gap: 8px; - } - - .live-epg-panel__body { - flex-basis: 0; - block-size: 0; - min-block-size: 0; - opacity: 0; - visibility: hidden; - pointer-events: none; - } -} - -@media only screen and (max-width: 720px) { - .live-epg-panel__date-nav { - display: none; - } - - .live-epg-panel__date-chip { - display: inline-flex; - } -} - -@media only screen and (max-width: 640px) { - .live-epg-panel__return-live { - width: 36px; - padding-inline: 0; - - .live-epg-panel__return-live-text { - display: none; - } - } -} - -@media only screen and (max-width: 560px) { - .live-epg-panel__label { - display: none; - } - - .live-epg-panel--collapsed .live-epg-panel__label { - display: none; - } -} diff --git a/libs/ui/shared-portals/src/lib/live-epg-panel/live-epg-panel.component.spec.ts b/libs/ui/shared-portals/src/lib/live-epg-panel/live-epg-panel.component.spec.ts deleted file mode 100644 index 13b012812..000000000 --- a/libs/ui/shared-portals/src/lib/live-epg-panel/live-epg-panel.component.spec.ts +++ /dev/null @@ -1,173 +0,0 @@ -import { Component } from '@angular/core'; -import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { TranslateModule } from '@ngx-translate/core'; -import { - LiveEpgPanelComponent, - LiveEpgPanelSummary, -} from './live-epg-panel.component'; - -@Component({ - standalone: true, - imports: [LiveEpgPanelComponent], - template: ` - -
Projected EPG
-
- `, -}) -class HostComponent { - collapsed = false; - loading = false; - summaryLabelKey = 'EPG.CURRENT_PROGRAM'; - showDateNavigator = false; - showReturnToLive = false; - selectedDate = '2026-04-05'; - dateDirection: 'next' | 'prev' | null = null; - returnToLiveCount = 0; - summary: LiveEpgPanelSummary | null = { - title: 'Current Show', - start: '2026-04-05T11:30:00.000Z', - stop: '2026-04-05T12:30:00.000Z', - }; -} - -describe('LiveEpgPanelComponent', () => { - let fixture: ComponentFixture; - - beforeEach(async () => { - jest.useFakeTimers(); - jest.setSystemTime(new Date('2026-04-05T12:00:00.000Z')); - - await TestBed.configureTestingModule({ - imports: [ - HostComponent, - NoopAnimationsModule, - TranslateModule.forRoot(), - ], - }).compileComponents(); - - fixture = TestBed.createComponent(HostComponent); - }); - - afterEach(() => { - fixture.destroy(); - jest.useRealTimers(); - }); - - it('renders the current program summary and progress', () => { - fixture.detectChanges(); - - expect( - fixture.nativeElement.querySelector('.live-epg-panel__title') - .textContent - ).toContain('Current Show'); - expect( - fixture.nativeElement.querySelector('.live-epg-panel__label') - .textContent - ).toContain('EPG.CURRENT_PROGRAM'); - expect( - fixture.nativeElement.querySelector( - '.live-epg-panel__progress-fill' - ).style.width - ).toBe('50%'); - }); - - it('emits and applies collapsed state from the toggle button', () => { - fixture.detectChanges(); - - fixture.nativeElement.querySelector('button').click(); - fixture.detectChanges(); - - expect(fixture.componentInstance.collapsed).toBe(true); - expect( - fixture.nativeElement - .querySelector('.live-epg-panel') - .classList.contains('live-epg-panel--collapsed') - ).toBe(true); - }); - - it('keeps projected EPG content mounted while collapsed and makes it inert', () => { - fixture.componentInstance.collapsed = true; - fixture.detectChanges(); - - const body = fixture.nativeElement.querySelector( - '.live-epg-panel__body' - ); - - expect( - fixture.nativeElement.querySelector('.projected-content') - .textContent - ).toContain('Projected EPG'); - expect(body.getAttribute('aria-hidden')).toBe('true'); - expect(body.hasAttribute('inert')).toBe(true); - }); - - it('renders the fallback text when there is no current program', () => { - fixture.componentInstance.summary = null; - fixture.detectChanges(); - - expect( - fixture.nativeElement.querySelector('.live-epg-panel__title') - .textContent - ).toContain('EPG.NO_PROGRAM_INFO'); - }); - - it('renders date navigation in the unified toolbar and emits day changes', () => { - fixture.componentInstance.showDateNavigator = true; - fixture.detectChanges(); - - expect( - fixture.nativeElement.querySelector('.selected-date').textContent - ).toContain('April 5, Sunday'); - - fixture.nativeElement.querySelector('.next-day').click(); - - expect(fixture.componentInstance.dateDirection).toBe('next'); - }); - - it('hides date navigation controls while collapsed', () => { - fixture.componentInstance.collapsed = true; - fixture.componentInstance.showDateNavigator = true; - fixture.detectChanges(); - - expect( - fixture.nativeElement.querySelector('.live-epg-panel__date-chip') - ).toBeNull(); - expect( - fixture.nativeElement.querySelector('.live-epg-panel__date-nav') - ).toBeNull(); - }); - - it('renders archive playback state and emits return-to-live requests', () => { - fixture.componentInstance.summaryLabelKey = 'EPG.ARCHIVE_PLAYBACK'; - fixture.componentInstance.showReturnToLive = true; - fixture.detectChanges(); - - expect( - fixture.nativeElement.querySelector('.live-epg-panel__label') - .textContent - ).toContain('EPG.ARCHIVE_PLAYBACK'); - - const returnButton = fixture.nativeElement.querySelector( - '.live-epg-panel__return-live' - ) as HTMLButtonElement | null; - expect(returnButton).not.toBeNull(); - expect(returnButton?.textContent).toContain('EPG.RETURN_TO_LIVE'); - - returnButton?.click(); - - expect(fixture.componentInstance.returnToLiveCount).toBe(1); - }); -}); diff --git a/libs/ui/shared-portals/src/lib/live-epg-panel/live-epg-panel.component.ts b/libs/ui/shared-portals/src/lib/live-epg-panel/live-epg-panel.component.ts deleted file mode 100644 index 8053dded3..000000000 --- a/libs/ui/shared-portals/src/lib/live-epg-panel/live-epg-panel.component.ts +++ /dev/null @@ -1,143 +0,0 @@ -import { DatePipe } from '@angular/common'; -import { - ChangeDetectionStrategy, - Component, - computed, - effect, - inject, - input, - output, - signal, -} from '@angular/core'; -import { toSignal } from '@angular/core/rxjs-interop'; -import { MatButton, MatIconButton } from '@angular/material/button'; -import { MatIcon } from '@angular/material/icon'; -import { MatMenuModule } from '@angular/material/menu'; -import { MatTooltip } from '@angular/material/tooltip'; -import { normalizeDateLocale } from '@iptvnator/pipes'; -import { - EpgDateNavigationDirection, - parseEpgDateKey, -} from '@iptvnator/ui/epg/date'; -import { TranslatePipe, TranslateService } from '@ngx-translate/core'; -import { startWith } from 'rxjs'; - -export interface LiveEpgPanelSummary { - readonly title?: string | null; - readonly start?: string | number | Date | null; - readonly stop?: string | number | Date | null; - readonly progress?: number | null; -} - -@Component({ - selector: 'app-live-epg-panel', - imports: [ - DatePipe, - MatButton, - MatIcon, - MatIconButton, - MatMenuModule, - MatTooltip, - TranslatePipe, - ], - templateUrl: './live-epg-panel.component.html', - styleUrl: './live-epg-panel.component.scss', - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class LiveEpgPanelComponent { - readonly collapsed = input(false); - readonly summary = input(null); - readonly loading = input(false); - readonly summaryLabelKey = input('EPG.CURRENT_PROGRAM'); - readonly showDateNavigator = input(false); - readonly selectedDate = input(null); - readonly showReturnToLive = input(false); - readonly collapsedChange = output(); - readonly dateNavigation = output(); - readonly returnToLive = output(); - - private readonly translate = inject(TranslateService); - private readonly currentTimeMs = signal(Date.now()); - private readonly languageTick = toSignal( - this.translate.onLangChange.pipe(startWith(null)), - { initialValue: null } - ); - - readonly hasSummary = computed(() => { - const title = this.summary()?.title; - return typeof title === 'string' && title.trim().length > 0; - }); - - readonly hasTimeRange = computed(() => { - const summary = this.summary(); - return !!summary?.start || !!summary?.stop; - }); - - readonly progress = computed(() => { - const summary = this.summary(); - if (!summary) { - return null; - } - - const explicitProgress = Number(summary.progress); - if (Number.isFinite(explicitProgress)) { - return clampProgress(explicitProgress); - } - - const startMs = toTimeMs(summary.start); - const stopMs = toTimeMs(summary.stop); - if (startMs === null || stopMs === null || stopMs <= startMs) { - return null; - } - - const elapsed = this.currentTimeMs() - startMs; - return clampProgress((elapsed / (stopMs - startMs)) * 100); - }); - readonly selectedDateValue = computed(() => - parseEpgDateKey(this.selectedDate()) - ); - readonly currentLocale = computed(() => { - this.languageTick(); - return normalizeDateLocale( - this.translate.currentLang || this.translate.defaultLang - ); - }); - - constructor() { - effect((onCleanup) => { - const intervalId = window.setInterval(() => { - this.currentTimeMs.set(Date.now()); - }, 30_000); - - onCleanup(() => clearInterval(intervalId)); - }); - } - - toggleCollapsed(): void { - this.collapsedChange.emit(!this.collapsed()); - } - - navigateDate(direction: EpgDateNavigationDirection): void { - this.dateNavigation.emit(direction); - } -} - -function toTimeMs( - value: string | number | Date | null | undefined -): number | null { - if (value === null || value === undefined || value === '') { - return null; - } - - const parsed = - value instanceof Date - ? value.getTime() - : typeof value === 'number' - ? value - : Date.parse(value); - return Number.isFinite(parsed) ? parsed : null; -} - -function clampProgress(value: number): number { - return Math.min(100, Math.max(0, value)); -} diff --git a/libs/ui/styles/_portal-layout.scss b/libs/ui/styles/_portal-layout.scss index 7293d319d..a7dd4839c 100644 --- a/libs/ui/styles/_portal-layout.scss +++ b/libs/ui/styles/_portal-layout.scss @@ -138,7 +138,6 @@ min-height: 56px; } - > app-live-epg-panel, > app-epg-timeline { flex: 1; min-height: 0; @@ -152,8 +151,6 @@ display: flex; flex-direction: column; - > app-epg-view, - > app-live-epg-panel, > app-epg-timeline { flex: 1; min-height: 0; diff --git a/package.json b/package.json index b342bcca3..b9c78205d 100644 --- a/package.json +++ b/package.json @@ -77,7 +77,6 @@ "@angular/platform-browser": "21.2.9", "@angular/platform-browser-dynamic": "21.2.9", "@angular/router": "21.2.9", - "@ngrx/component-store": "21.0.1", "@ngrx/effects": "21.0.1", "@ngrx/entity": "21.0.1", "@ngrx/router-store": "21.0.1", @@ -85,7 +84,6 @@ "@ngrx/store": "21.0.1", "@ngrx/store-devtools": "21.0.1", "@ngx-pwa/local-storage": "21.0.0", - "@videojs/http-streaming": "2.15.0", "@yangkghjh/videojs-aspect-ratio-panel": "0.0.1", "angularx-qrcode": "21.0.4", "artplayer": "5.3.0", @@ -100,7 +98,6 @@ "fix-path": "5.0.0", "hls.js": "1.6.13", "iptv-playlist-parser": "github:4gray/iptv-playlist-parser", - "lodash": "4.18.1", "marked": "18.0.5", "mpegts.js": "1.8.0", "ms": "2.1.3", @@ -108,7 +105,6 @@ "ngx-skeleton-loader": "11.3.0", "rxjs": "7.8.2", "saxes": "6.0.0", - "semver": "7.7.3", "uuid": "9.0.0", "video.js": "8.23.4", "videojs-contrib-quality-levels": "4.1.0", @@ -172,7 +168,6 @@ "@types/cors": "2.8.19", "@types/express": "5.0.6", "@types/jest": "^30.0.0", - "@types/lodash": "4.17.20", "@types/mocha": "9.0.0", "@types/node": "20.19.9", "@types/uuid": "^10.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5fcf722a4..81ef8fde5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -65,9 +65,6 @@ importers: '@angular/router': specifier: 21.2.9 version: 21.2.9(@angular/common@21.2.9(@angular/core@21.2.9(@angular/compiler@21.2.9)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@21.2.9(@angular/compiler@21.2.9)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@21.2.9(@angular/animations@21.2.9(@angular/core@21.2.9(@angular/compiler@21.2.9)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@21.2.9(@angular/core@21.2.9(@angular/compiler@21.2.9)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@21.2.9(@angular/compiler@21.2.9)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2) - '@ngrx/component-store': - specifier: 21.0.1 - version: 21.0.1(@angular/core@21.2.9(@angular/compiler@21.2.9)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2) '@ngrx/effects': specifier: 21.0.1 version: 21.0.1(@angular/core@21.2.9(@angular/compiler@21.2.9)(rxjs@7.8.2)(zone.js@0.15.1))(@ngrx/store@21.0.1(@angular/core@21.2.9(@angular/compiler@21.2.9)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(rxjs@7.8.2) @@ -89,9 +86,6 @@ importers: '@ngx-pwa/local-storage': specifier: 21.0.0 version: 21.0.0(@angular/common@21.2.9(@angular/core@21.2.9(@angular/compiler@21.2.9)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@21.2.9(@angular/compiler@21.2.9)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2) - '@videojs/http-streaming': - specifier: 2.15.0 - version: 2.15.0(video.js@8.23.4) '@yangkghjh/videojs-aspect-ratio-panel': specifier: 0.0.1 version: 0.0.1 @@ -134,9 +128,6 @@ importers: iptv-playlist-parser: specifier: github:4gray/iptv-playlist-parser version: https://codeload.github.com/4gray/iptv-playlist-parser/tar.gz/38a62d0b7c98f442bfa102ee8e1fe14169ae9386 - lodash: - specifier: 4.18.1 - version: 4.18.1 marked: specifier: 18.0.5 version: 18.0.5 @@ -158,9 +149,6 @@ importers: saxes: specifier: 6.0.0 version: 6.0.0 - semver: - specifier: 7.7.3 - version: 7.7.3 uuid: specifier: 9.0.0 version: 9.0.0 @@ -324,9 +312,6 @@ importers: '@types/jest': specifier: ^30.0.0 version: 30.0.0 - '@types/lodash': - specifier: 4.17.20 - version: 4.17.20 '@types/mocha': specifier: 9.0.0 version: 9.0.0 @@ -3357,12 +3342,6 @@ packages: '@napi-rs/wasm-runtime@1.1.1': resolution: {integrity: sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==} - '@ngrx/component-store@21.0.1': - resolution: {integrity: sha512-aOfudK8MqCZQUuBjMnXAXRxRcr9Qu/+9/2iIh4aLhmGXC5JCpkDi1a82A/39EUFZy2xBbI4lPI7P817b1PQRvQ==} - peerDependencies: - '@angular/core': ^21.0.0 - rxjs: ^6.5.3 || ^7.5.0 - '@ngrx/effects@21.0.1': resolution: {integrity: sha512-hSdpToAiSYa5FJ/CAygQHpnCaF2S1HO7q/57ob3XvNTWmkofa0VqI/IIe4W57bojh2YOWCJ91SCn3kAjymaV3g==} peerDependencies: @@ -4631,9 +4610,6 @@ packages: '@types/keyv@3.1.4': resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} - '@types/lodash@4.17.20': - resolution: {integrity: sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==} - '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} @@ -4908,12 +4884,6 @@ packages: cpu: [x64] os: [win32] - '@videojs/http-streaming@2.15.0': - resolution: {integrity: sha512-uRW62ybZOP4Osw24/yT79vjRkxqAyoB/wtVV6lroAaPHYUXCz9//VUQAC6xZn2z6JeCoBz0pQHGlxYb8JRFj6Q==} - engines: {node: '>=8', npm: '>=5'} - peerDependencies: - video.js: ^6 || ^7 - '@videojs/http-streaming@2.16.3': resolution: {integrity: sha512-91CJv5PnFBzNBvyEjt+9cPzTK/xoVixARj2g7ZAvItA+5bx8VKdk5RxCz/PP2kdzz9W+NiDUMPkdmTsosmy69Q==} engines: {node: '>=8', npm: '>=5'} @@ -4991,11 +4961,6 @@ packages: '@webassemblyjs/wast-printer@1.14.1': resolution: {integrity: sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==} - '@xmldom/xmldom@0.7.13': - resolution: {integrity: sha512-lm2GW5PkosIzccsaZIz7tp8cPADSIlIHWDFTR1N0SzfinhhYgeIQjFMz4rYzanCScr3DqQLeomUDArp6MWKm+g==} - engines: {node: '>=10.0.0'} - deprecated: this version has critical issues, please update to the latest version - '@xmldom/xmldom@0.8.11': resolution: {integrity: sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==} engines: {node: '>=10.0.0'} @@ -8908,10 +8873,6 @@ packages: engines: {node: '>=10'} hasBin: true - mpd-parser@0.22.0: - resolution: {integrity: sha512-P2GSZGTlud9zGr1IU/wd+dwK5v+cdeHd9ys+8y0Yo4kADfkKyYET37oIvOoQiLGp6k7CTNO7navdyV2mfG0yzw==} - hasBin: true - mpd-parser@0.22.1: resolution: {integrity: sha512-fwBebvpyPUU8bOzvhX0VQZgSohncbgYwUyJJoTSNpmy7ccD2ryiCvM7oRkn/xQH5cv73/xU7rJSNCLjdGFor0Q==} hasBin: true @@ -13760,7 +13721,7 @@ snapshots: make-fetch-happen: 10.2.1 nopt: 6.0.0 proc-log: 2.0.1 - semver: 7.7.3 + semver: 7.7.4 tar: 6.2.1 which: 2.0.2 transitivePeerDependencies: @@ -15388,12 +15349,6 @@ snapshots: '@tybys/wasm-util': 0.10.1 optional: true - '@ngrx/component-store@21.0.1(@angular/core@21.2.9(@angular/compiler@21.2.9)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2)': - dependencies: - '@angular/core': 21.2.9(@angular/compiler@21.2.9)(rxjs@7.8.2)(zone.js@0.15.1) - rxjs: 7.8.2 - tslib: 2.8.1 - '@ngrx/effects@21.0.1(@angular/core@21.2.9(@angular/compiler@21.2.9)(rxjs@7.8.2)(zone.js@0.15.1))(@ngrx/store@21.0.1(@angular/core@21.2.9(@angular/compiler@21.2.9)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(rxjs@7.8.2)': dependencies: '@angular/core': 21.2.9(@angular/compiler@21.2.9)(rxjs@7.8.2)(zone.js@0.15.1) @@ -15489,7 +15444,7 @@ snapshots: '@npmcli/fs@2.1.2': dependencies: '@gar/promisify': 1.1.3 - semver: 7.7.3 + semver: 7.7.4 '@npmcli/fs@5.0.0': dependencies: @@ -16921,8 +16876,6 @@ snapshots: dependencies: '@types/node': 20.19.9 - '@types/lodash@4.17.20': {} - '@types/mdast@4.0.4': dependencies: '@types/unist': 3.0.3 @@ -17186,17 +17139,6 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true - '@videojs/http-streaming@2.15.0(video.js@8.23.4)': - dependencies: - '@babel/runtime': 7.28.4 - '@videojs/vhs-utils': 3.0.5 - aes-decrypter: 3.1.3 - global: 4.4.0 - m3u8-parser: 4.8.0 - mpd-parser: 0.22.0 - mux.js: 6.0.1 - video.js: 8.23.4 - '@videojs/http-streaming@2.16.3(video.js@7.21.7)': dependencies: '@babel/runtime': 7.28.4 @@ -17322,8 +17264,6 @@ snapshots: '@webassemblyjs/ast': 1.14.1 '@xtuc/long': 4.2.2 - '@xmldom/xmldom@0.7.13': {} - '@xmldom/xmldom@0.8.11': {} '@xtuc/ieee754@1.2.0': {} @@ -22438,13 +22378,6 @@ snapshots: mkdirp@1.0.4: {} - mpd-parser@0.22.0: - dependencies: - '@babel/runtime': 7.28.4 - '@videojs/vhs-utils': 3.0.5 - '@xmldom/xmldom': 0.7.13 - global: 4.4.0 - mpd-parser@0.22.1: dependencies: '@babel/runtime': 7.28.4