diff --git a/CLAUDE.md b/CLAUDE.md index 851eb439e..fdfb23910 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -618,6 +618,14 @@ This project uses modern Angular signal-based APIs and patterns. **ALWAYS** use - External players: MPV, VLC (via IPC to Electron backend) - Embedded MPV (experimental, macOS/Windows/Linux): renders mpv video inside the Electron window through a native addon. macOS uses the libmpv render API in an `NSOpenGLView`; Windows uses in-process libmpv with `--wid` against an app-owned child `HWND`; Linux spawns an out-of-process `mpv --wid=` controlled over a JSON IPC socket (X11/XWayland only, requires system `mpv` on PATH; subtitles/speed/aspect/recording are not exported there). mpv's own screensaver inhibition does not apply to any of these paths, so `EmbeddedMpvNativeService` holds an Electron `powerSaveBlocker` (`prevent-display-sleep`) whenever any session's status is `playing`, and releases it on pause, dispose, or shutdown. Service: `apps/electron-backend/src/app/services/embedded-mpv-native.service.ts`; full architecture: `docs/architecture/embedded-mpv-native.md`. +**VOD/Series Detail Pages (two-state layout)**: + +- Xtream and Stalker detail pages use the shared `PortalDetailShellComponent` (`libs/ui/components/src/lib/portal-detail-shell/`) with two states: **Browse** (hero with poster/metadata/actions, episodes below) and **Watch** (hero collapses with a ~300ms morph, the inline player takes the full content width, metadata moves to an About block below the episodes) +- Watch state derives from `inlinePlayback() !== null` only; external MPV/VLC playback keeps the browse layout. Esc and "Close player" exit to browse without navigation; the now-playing back arrow is route-level back (straight to the list via the host's `goBack()`) +- Hosts pass hero chips/meta/actions as `*appDetailTags`/`*appDetailMeta`/`*appDetailActions` templates; the shell stamps them into both the hero and the About block +- Seasons are tabs (`SeasonTabsComponent`, dropdown beyond 6 seasons) with auto-selection (playing episode's season → resume season → first) that fires the same `seasonSelected` lazy-load/enrichment hooks as manual clicks; grid/list episode view toggle persists to localStorage; season descriptions come from `get_series_info` (Xtream) or TMDB (Stalker) +- See `docs/architecture/embedded-inline-playback.md` ("Two-State Detail Layout") + **Radio Player**: - Dedicated audio player for channels with `radio="true"` M3U attribute diff --git a/apps/web/src/assets/i18n/ar.json b/apps/web/src/assets/i18n/ar.json index dc48580c1..ba2530487 100644 --- a/apps/web/src/assets/i18n/ar.json +++ b/apps/web/src/assets/i18n/ar.json @@ -2,6 +2,7 @@ "CANCEL": "إلغاء", "CLEAR": "مسح", "CLOSE": "إغلاق", + "BACK": "رجوع", "SHOW_MORE": "المزيد", "SHOW_LESS": "أقل", "PLAYLISTS": "قوائم التشغيل", @@ -918,6 +919,19 @@ }, "STREAM_URL_COPIED": "تم نسخ رابط البث إلى الحافظة", "COPY_STREAM_URL": "نسخ رابط البث", + "NOW_PLAYING": "قيد التشغيل", + "CLOSE_PLAYER": "إغلاق المشغل", + "ABOUT": "نبذة", + "SEASONS_AND_EPISODES": "المواسم والحلقات", + "SEASON_TAB": "الموسم {{number}}", + "BACK_TO_PLAYING_EPISODE": "العودة إلى الحلقة قيد التشغيل", + "GRID_VIEW": "عرض شبكي", + "LIST_VIEW": "عرض قائمة", + "NO_EPISODES_AVAILABLE": "لا توجد حلقات متاحة", + "NO_EPISODES_AVAILABLE_MESSAGE": "هذا المسلسل مدرج لدى مزودك، لكن لا توجد مواسم أو حلقات متاحة للتشغيل.", + "SEASON_EMPTY": "هذا الموسم فارغ", + "SEASON_EMPTY_MESSAGE": "هذا الموسم مدرج لدى مزودك، لكن لا توجد حلقات قابلة للتشغيل متاحة.", + "EPISODE_INFO": "تفاصيل الحلقة", "SELECT_CATEGORY": "اختر فئة", "PAGE": "الصفحة", "CONTENT_NOT_AVAILABLE": "هذا المحتوى غير متاح على هذا الخادم", diff --git a/apps/web/src/assets/i18n/ary.json b/apps/web/src/assets/i18n/ary.json index ab4d6be74..0c7280ba1 100644 --- a/apps/web/src/assets/i18n/ary.json +++ b/apps/web/src/assets/i18n/ary.json @@ -2,6 +2,7 @@ "CANCEL": "إلغاء", "CLEAR": "مسح", "CLOSE": "سد", + "BACK": "رجع", "SHOW_MORE": "وري كثر", "SHOW_LESS": "وري قل", "PLAYLISTS": "قوائم التشغيل", @@ -918,6 +919,19 @@ }, "STREAM_URL_COPIED": "رابط البث تنقل للحافظة", "COPY_STREAM_URL": "نسخ رابط البث", + "NOW_PLAYING": "كيتشغل دابا", + "CLOSE_PLAYER": "سد المشغل", + "ABOUT": "حول", + "SEASONS_AND_EPISODES": "المواسم والحلقات", + "SEASON_TAB": "الموسم {{number}}", + "BACK_TO_PLAYING_EPISODE": "رجوع للحلقة اللي كتشغل", + "GRID_VIEW": "عرض الشبكة", + "LIST_VIEW": "عرض اللائحة", + "NO_EPISODES_AVAILABLE": "ما كاينش حلقات متوفرة", + "NO_EPISODES_AVAILABLE_MESSAGE": "هاد السلسلة مذكورة عند المزود ديالك، ولكن ما كاينش مواسم ولا حلقات متوفرة للتشغيل.", + "SEASON_EMPTY": "هاد الموسم خاوي", + "SEASON_EMPTY_MESSAGE": "هاد الموسم مذكور عند المزود ديالك، ولكن ما كاينش حلقات متوفرة للتشغيل.", + "EPISODE_INFO": "تفاصيل الحلقة", "SELECT_CATEGORY": "اختار فئة", "PAGE": "الصفحة", "CONTENT_NOT_AVAILABLE": "هاد المحتوى ما متوفرش على هاد السيرفر", diff --git a/apps/web/src/assets/i18n/by.json b/apps/web/src/assets/i18n/by.json index 7ead61989..454e6c65f 100644 --- a/apps/web/src/assets/i18n/by.json +++ b/apps/web/src/assets/i18n/by.json @@ -2,6 +2,7 @@ "CANCEL": "Адмена", "CLEAR": "Ачысціць", "CLOSE": "Закрыць", + "BACK": "Назад", "SHOW_MORE": "Болей", "SHOW_LESS": "Меней", "PLAYLISTS": "Плэйлісты", @@ -918,6 +919,19 @@ }, "STREAM_URL_COPIED": "URL-адрас патоку быў скапіяваны ў буфер абмену.", "COPY_STREAM_URL": "Капіраваць URL-адрас патоку", + "NOW_PLAYING": "Зараз прайграецца", + "CLOSE_PLAYER": "Закрыць прайгравальнік", + "ABOUT": "Пра фільм", + "SEASONS_AND_EPISODES": "Сезоны і эпізоды", + "SEASON_TAB": "Сезон {{number}}", + "BACK_TO_PLAYING_EPISODE": "Назад да бягучага эпізоду", + "GRID_VIEW": "Выгляд сеткай", + "LIST_VIEW": "Выгляд спісам", + "NO_EPISODES_AVAILABLE": "Няма даступных эпізодаў", + "NO_EPISODES_AVAILABLE_MESSAGE": "Гэты серыял пазначаны вашым правайдэрам, але сезоны або эпізоды для прайгравання недаступныя.", + "SEASON_EMPTY": "Гэты сезон пусты", + "SEASON_EMPTY_MESSAGE": "Гэты сезон пазначаны вашым правайдэрам, але даступных для прайгравання эпізодаў няма.", + "EPISODE_INFO": "Падрабязнасці эпізода", "SELECT_CATEGORY": "Выберыце катэгорыю", "PAGE": "Старонка", "CONTENT_NOT_AVAILABLE": "Гэты кантэнт недаступны на дадзеным серверы", diff --git a/apps/web/src/assets/i18n/de.json b/apps/web/src/assets/i18n/de.json index 064431be4..015d8e617 100644 --- a/apps/web/src/assets/i18n/de.json +++ b/apps/web/src/assets/i18n/de.json @@ -2,6 +2,7 @@ "CANCEL": "Abbrechen", "CLEAR": "Leeren", "CLOSE": "Schließen", + "BACK": "Zurück", "SHOW_MORE": "Mehr", "SHOW_LESS": "Weniger", "PLAYLISTS": "Playlisten", @@ -918,6 +919,19 @@ }, "STREAM_URL_COPIED": "Stream-URL in die Zwischenablage kopiert", "COPY_STREAM_URL": "Stream-URL kopieren", + "NOW_PLAYING": "Wird wiedergegeben", + "CLOSE_PLAYER": "Player schließen", + "ABOUT": "Info", + "SEASONS_AND_EPISODES": "Staffeln und Folgen", + "SEASON_TAB": "Staffel {{number}}", + "BACK_TO_PLAYING_EPISODE": "Zurück zur laufenden Folge", + "GRID_VIEW": "Rasteransicht", + "LIST_VIEW": "Listenansicht", + "NO_EPISODES_AVAILABLE": "Keine Folgen verfügbar", + "NO_EPISODES_AVAILABLE_MESSAGE": "Diese Serie wird von Ihrem Anbieter gelistet, es sind jedoch keine Staffeln oder Folgen zum Abspielen verfügbar.", + "SEASON_EMPTY": "Diese Staffel ist leer", + "SEASON_EMPTY_MESSAGE": "Diese Staffel wird von Ihrem Anbieter gelistet, es sind jedoch keine abspielbaren Folgen verfügbar.", + "EPISODE_INFO": "Details zur Folge", "SELECT_CATEGORY": "Wählen Sie eine Kategorie aus", "PAGE": "Seite", "CONTENT_NOT_AVAILABLE": "Dieser Inhalt ist auf diesem Server nicht verfügbar", diff --git a/apps/web/src/assets/i18n/el.json b/apps/web/src/assets/i18n/el.json index bfbeb3b79..44eb43e3b 100644 --- a/apps/web/src/assets/i18n/el.json +++ b/apps/web/src/assets/i18n/el.json @@ -2,6 +2,7 @@ "CANCEL": "Ακύρωση", "CLEAR": "Εκκαθάριση", "CLOSE": "Κλείσιμο", + "BACK": "Πίσω", "SHOW_MORE": "Περισσότερα", "SHOW_LESS": "Λιγότερα", "PLAYLISTS": "Λίστα Αναπαραγωγής", @@ -918,6 +919,19 @@ }, "STREAM_URL_COPIED": "Η διεύθυνση URL ροής αντιγράφηκε στο πρόχειρο", "COPY_STREAM_URL": "Αντιγραφή διεύθηνσης URL ροής", + "NOW_PLAYING": "Τώρα παίζει", + "CLOSE_PLAYER": "Κλείσιμο προγράμματος αναπαραγωγής", + "ABOUT": "Σχετικά", + "SEASONS_AND_EPISODES": "Σεζόν και επεισόδια", + "SEASON_TAB": "Σεζόν {{number}}", + "BACK_TO_PLAYING_EPISODE": "Πίσω στο επεισόδιο που παίζει", + "GRID_VIEW": "Προβολή πλέγματος", + "LIST_VIEW": "Προβολή λίστας", + "NO_EPISODES_AVAILABLE": "Δεν υπάρχουν διαθέσιμα επεισόδια", + "NO_EPISODES_AVAILABLE_MESSAGE": "Αυτή η σειρά είναι καταχωρημένη από τον πάροχό σας, αλλά δεν υπάρχουν διαθέσιμες σεζόν ή επεισόδια για αναπαραγωγή.", + "SEASON_EMPTY": "Αυτή η σεζόν είναι κενή", + "SEASON_EMPTY_MESSAGE": "Αυτή η σεζόν είναι καταχωρημένη από τον πάροχό σας, αλλά δεν υπάρχουν διαθέσιμα επεισόδια για αναπαραγωγή.", + "EPISODE_INFO": "Λεπτομέρειες επεισοδίου", "SELECT_CATEGORY": "Επιλέξτε μια κατηγορία", "PAGE": "Σελίδα", "CONTENT_NOT_AVAILABLE": "Αυτό το περιεχόμενο δεν είναι διαθέσιμο σε αυτόν τον διακομιστή", diff --git a/apps/web/src/assets/i18n/en.json b/apps/web/src/assets/i18n/en.json index 54ccd5cd2..0c10f8cef 100644 --- a/apps/web/src/assets/i18n/en.json +++ b/apps/web/src/assets/i18n/en.json @@ -2,6 +2,7 @@ "CANCEL": "Cancel", "CLEAR": "Clear", "CLOSE": "Close", + "BACK": "Back", "SHOW_MORE": "More", "SHOW_LESS": "Less", "PLAYLISTS": "Playlists", @@ -918,6 +919,19 @@ }, "STREAM_URL_COPIED": "Stream URL copied to clipboard", "COPY_STREAM_URL": "Copy Stream URL", + "NOW_PLAYING": "Now playing", + "CLOSE_PLAYER": "Close player", + "ABOUT": "About", + "SEASONS_AND_EPISODES": "Seasons and Episodes", + "SEASON_TAB": "Season {{number}}", + "BACK_TO_PLAYING_EPISODE": "Back to playing episode", + "GRID_VIEW": "Grid view", + "LIST_VIEW": "List view", + "NO_EPISODES_AVAILABLE": "No episodes available", + "NO_EPISODES_AVAILABLE_MESSAGE": "This series is listed by your provider, but no seasons or episodes are available to play.", + "SEASON_EMPTY": "This season is empty", + "SEASON_EMPTY_MESSAGE": "This season is listed by your provider, but no playable episodes are available.", + "EPISODE_INFO": "Episode details", "SELECT_CATEGORY": "Select a category", "PAGE": "Page", "CONTENT_NOT_AVAILABLE": "This content is not available on this server", diff --git a/apps/web/src/assets/i18n/es.json b/apps/web/src/assets/i18n/es.json index c83e0207b..9fea237d8 100644 --- a/apps/web/src/assets/i18n/es.json +++ b/apps/web/src/assets/i18n/es.json @@ -2,6 +2,7 @@ "CANCEL": "Cancelar", "CLEAR": "Borrar", "CLOSE": "Cerrar", + "BACK": "Atrás", "SHOW_MORE": "Más", "SHOW_LESS": "Menos", "PLAYLISTS": "Listas de reproducción", @@ -918,6 +919,19 @@ }, "STREAM_URL_COPIED": "URL de transmisión copiada al portapapeles", "COPY_STREAM_URL": "Copiar URL de transmisión", + "NOW_PLAYING": "Reproduciendo ahora", + "CLOSE_PLAYER": "Cerrar reproductor", + "ABOUT": "Acerca de", + "SEASONS_AND_EPISODES": "Temporadas y episodios", + "SEASON_TAB": "Temporada {{number}}", + "BACK_TO_PLAYING_EPISODE": "Volver al episodio en reproducción", + "GRID_VIEW": "Vista de cuadrícula", + "LIST_VIEW": "Vista de lista", + "NO_EPISODES_AVAILABLE": "No hay episodios disponibles", + "NO_EPISODES_AVAILABLE_MESSAGE": "Tu proveedor incluye esta serie en el listado, pero no hay temporadas ni episodios disponibles para reproducir.", + "SEASON_EMPTY": "Esta temporada está vacía", + "SEASON_EMPTY_MESSAGE": "Tu proveedor incluye esta temporada en el listado, pero no hay episodios reproducibles disponibles.", + "EPISODE_INFO": "Detalles del episodio", "SELECT_CATEGORY": "Selecciona una categoría", "PAGE": "Página", "CONTENT_NOT_AVAILABLE": "Este contenido no está disponible en este servidor", diff --git a/apps/web/src/assets/i18n/fr.json b/apps/web/src/assets/i18n/fr.json index e62dc207d..153aed137 100644 --- a/apps/web/src/assets/i18n/fr.json +++ b/apps/web/src/assets/i18n/fr.json @@ -2,6 +2,7 @@ "CANCEL": "Annuler", "CLEAR": "Effacer", "CLOSE": "Fermer", + "BACK": "Retour", "SHOW_MORE": "Plus", "SHOW_LESS": "Moins", "PLAYLISTS": "Listes de lecture", @@ -918,6 +919,19 @@ }, "STREAM_URL_COPIED": "URL du flux copiée dans le presse-papiers", "COPY_STREAM_URL": "Copier l'URL du flux", + "NOW_PLAYING": "En cours de lecture", + "CLOSE_PLAYER": "Fermer le lecteur", + "ABOUT": "À propos", + "SEASONS_AND_EPISODES": "Saisons et épisodes", + "SEASON_TAB": "Saison {{number}}", + "BACK_TO_PLAYING_EPISODE": "Retour à l'épisode en cours", + "GRID_VIEW": "Vue en grille", + "LIST_VIEW": "Vue en liste", + "NO_EPISODES_AVAILABLE": "Aucun épisode disponible", + "NO_EPISODES_AVAILABLE_MESSAGE": "Cette série est répertoriée par votre fournisseur, mais aucune saison ni épisode n'est disponible à la lecture.", + "SEASON_EMPTY": "Cette saison est vide", + "SEASON_EMPTY_MESSAGE": "Cette saison est répertoriée par votre fournisseur, mais aucun épisode lisible n'est disponible.", + "EPISODE_INFO": "Détails de l'épisode", "SELECT_CATEGORY": "Sélectionner une catégorie", "PAGE": "Page", "CONTENT_NOT_AVAILABLE": "Ce contenu n'est pas disponible sur ce serveur", diff --git a/apps/web/src/assets/i18n/it.json b/apps/web/src/assets/i18n/it.json index 4879114b5..ae4fcf81d 100644 --- a/apps/web/src/assets/i18n/it.json +++ b/apps/web/src/assets/i18n/it.json @@ -2,6 +2,7 @@ "CANCEL": "Annulla", "CLEAR": "Cancella", "CLOSE": "Chiudi", + "BACK": "Indietro", "SHOW_MORE": "Altro", "SHOW_LESS": "Meno", "PLAYLISTS": "Playlist", @@ -918,6 +919,19 @@ }, "STREAM_URL_COPIED": "URL streaming copiato negli appunti", "COPY_STREAM_URL": "Copia URL streaming", + "NOW_PLAYING": "In riproduzione", + "CLOSE_PLAYER": "Chiudi lettore", + "ABOUT": "Info", + "SEASONS_AND_EPISODES": "Stagioni ed episodi", + "SEASON_TAB": "Stagione {{number}}", + "BACK_TO_PLAYING_EPISODE": "Torna all'episodio in riproduzione", + "GRID_VIEW": "Vista griglia", + "LIST_VIEW": "Vista elenco", + "NO_EPISODES_AVAILABLE": "Nessun episodio disponibile", + "NO_EPISODES_AVAILABLE_MESSAGE": "Questa serie è elencata dal tuo fornitore, ma non sono disponibili stagioni o episodi da riprodurre.", + "SEASON_EMPTY": "Questa stagione è vuota", + "SEASON_EMPTY_MESSAGE": "Questa stagione è elencata dal tuo fornitore, ma non sono disponibili episodi riproducibili.", + "EPISODE_INFO": "Dettagli episodio", "SELECT_CATEGORY": "Seleziona una categoria", "PAGE": "Pagina", "CONTENT_NOT_AVAILABLE": "Questo contenuto non è disponibile su questo server", diff --git a/apps/web/src/assets/i18n/ja.json b/apps/web/src/assets/i18n/ja.json index a3804e70b..1b4143563 100644 --- a/apps/web/src/assets/i18n/ja.json +++ b/apps/web/src/assets/i18n/ja.json @@ -2,6 +2,7 @@ "CANCEL": "キャンセル", "CLEAR": "クリア", "CLOSE": "閉じる", + "BACK": "戻る", "SHOW_MORE": "もっと見る", "SHOW_LESS": "折りたたむ", "PLAYLISTS": "プレイリスト", @@ -918,6 +919,19 @@ }, "STREAM_URL_COPIED": "ストリームURLがクリップボードにコピーされました", "COPY_STREAM_URL": "ストリームURLをコピー", + "NOW_PLAYING": "再生中", + "CLOSE_PLAYER": "プレーヤーを閉じる", + "ABOUT": "概要", + "SEASONS_AND_EPISODES": "シーズンとエピソード", + "SEASON_TAB": "シーズン{{number}}", + "BACK_TO_PLAYING_EPISODE": "再生中のエピソードに戻る", + "GRID_VIEW": "グリッド表示", + "LIST_VIEW": "リスト表示", + "NO_EPISODES_AVAILABLE": "利用可能なエピソードはありません", + "NO_EPISODES_AVAILABLE_MESSAGE": "このシリーズはプロバイダーに登録されていますが、再生可能なシーズンやエピソードはありません。", + "SEASON_EMPTY": "このシーズンにはエピソードがありません", + "SEASON_EMPTY_MESSAGE": "このシーズンはプロバイダーに登録されていますが、再生可能なエピソードはありません。", + "EPISODE_INFO": "エピソードの詳細", "SELECT_CATEGORY": "カテゴリーを選択", "PAGE": "ページ", "CONTENT_NOT_AVAILABLE": "このコンテンツはこのサーバーでは利用できません", diff --git a/apps/web/src/assets/i18n/ko.json b/apps/web/src/assets/i18n/ko.json index d36b11733..bb8a15cc4 100644 --- a/apps/web/src/assets/i18n/ko.json +++ b/apps/web/src/assets/i18n/ko.json @@ -2,6 +2,7 @@ "CANCEL": "취소", "CLEAR": "지우기", "CLOSE": "닫기", + "BACK": "뒤로", "SHOW_MORE": "더 보기", "SHOW_LESS": "간략히 보기", "PLAYLISTS": "재생목록", @@ -918,6 +919,19 @@ }, "STREAM_URL_COPIED": "스트림 URL이 클립보드에 복사되었습니다.", "COPY_STREAM_URL": "스트림 URL 복사", + "NOW_PLAYING": "지금 재생 중", + "CLOSE_PLAYER": "플레이어 닫기", + "ABOUT": "정보", + "SEASONS_AND_EPISODES": "시즌 및 에피소드", + "SEASON_TAB": "시즌 {{number}}", + "BACK_TO_PLAYING_EPISODE": "재생 중인 에피소드로 돌아가기", + "GRID_VIEW": "그리드 보기", + "LIST_VIEW": "목록 보기", + "NO_EPISODES_AVAILABLE": "사용 가능한 에피소드가 없습니다", + "NO_EPISODES_AVAILABLE_MESSAGE": "이 시리즈는 공급자 목록에 있지만 재생 가능한 시즌이나 에피소드가 없습니다.", + "SEASON_EMPTY": "이 시즌은 비어 있습니다", + "SEASON_EMPTY_MESSAGE": "이 시즌은 공급자 목록에 있지만 재생 가능한 에피소드가 없습니다.", + "EPISODE_INFO": "에피소드 정보", "SELECT_CATEGORY": "카테고리를 선택하세요", "PAGE": "페이지", "CONTENT_NOT_AVAILABLE": "이 서버에서는 이 콘텐츠를 사용할 수 없습니다", diff --git a/apps/web/src/assets/i18n/nl.json b/apps/web/src/assets/i18n/nl.json index d8a2b49e0..6feeaa613 100644 --- a/apps/web/src/assets/i18n/nl.json +++ b/apps/web/src/assets/i18n/nl.json @@ -2,6 +2,7 @@ "CANCEL": "Annuleren", "CLEAR": "Wissen", "CLOSE": "Afsluiten", + "BACK": "Terug", "SHOW_MORE": "Meer", "SHOW_LESS": "Minder", "PLAYLISTS": "Afspeellijst", @@ -918,6 +919,19 @@ }, "STREAM_URL_COPIED": "Stream URL Gekopieërd", "COPY_STREAM_URL": "Kopieër Stream URL", + "NOW_PLAYING": "Nu bezig", + "CLOSE_PLAYER": "Speler sluiten", + "ABOUT": "Over", + "SEASONS_AND_EPISODES": "Seizoenen en afleveringen", + "SEASON_TAB": "Seizoen {{number}}", + "BACK_TO_PLAYING_EPISODE": "Terug naar lopende aflevering", + "GRID_VIEW": "Rasterweergave", + "LIST_VIEW": "Lijstweergave", + "NO_EPISODES_AVAILABLE": "Geen afleveringen beschikbaar", + "NO_EPISODES_AVAILABLE_MESSAGE": "Deze serie staat in de lijst van je provider, maar er zijn geen seizoenen of afleveringen beschikbaar om af te spelen.", + "SEASON_EMPTY": "Dit seizoen is leeg", + "SEASON_EMPTY_MESSAGE": "Dit seizoen staat in de lijst van je provider, maar er zijn geen afspeelbare afleveringen beschikbaar.", + "EPISODE_INFO": "Afleveringsdetails", "SELECT_CATEGORY": "Selecteer een categorie", "PAGE": "Pagina", "CONTENT_NOT_AVAILABLE": "Deze inhoud is niet beschikbaar op deze server", diff --git a/apps/web/src/assets/i18n/pl.json b/apps/web/src/assets/i18n/pl.json index 2dde3f1e8..cb9af4de2 100644 --- a/apps/web/src/assets/i18n/pl.json +++ b/apps/web/src/assets/i18n/pl.json @@ -2,6 +2,7 @@ "CANCEL": "Anuluj", "CLEAR": "Wyczyść", "CLOSE": "Zamknij", + "BACK": "Wstecz", "SHOW_MORE": "Więcej", "SHOW_LESS": "Mniej", "PLAYLISTS": "Listy odtwarzania", @@ -918,6 +919,19 @@ }, "STREAM_URL_COPIED": "URL strumienia skopiowany do schowka", "COPY_STREAM_URL": "Kopiuj URL strumienia", + "NOW_PLAYING": "Teraz odtwarzane", + "CLOSE_PLAYER": "Zamknij odtwarzacz", + "ABOUT": "Informacje", + "SEASONS_AND_EPISODES": "Sezony i odcinki", + "SEASON_TAB": "Sezon {{number}}", + "BACK_TO_PLAYING_EPISODE": "Wróć do odtwarzanego odcinka", + "GRID_VIEW": "Widok siatki", + "LIST_VIEW": "Widok listy", + "NO_EPISODES_AVAILABLE": "Brak dostępnych odcinków", + "NO_EPISODES_AVAILABLE_MESSAGE": "Ten serial jest dostępny u Twojego dostawcy, ale nie ma sezonów ani odcinków do odtworzenia.", + "SEASON_EMPTY": "Ten sezon jest pusty", + "SEASON_EMPTY_MESSAGE": "Ten sezon jest dostępny u Twojego dostawcy, ale nie ma odcinków do odtworzenia.", + "EPISODE_INFO": "Szczegóły odcinka", "SELECT_CATEGORY": "Wybierz kategorię", "PAGE": "Strona", "CONTENT_NOT_AVAILABLE": "Ta treść jest niedostępna na tym serwerze", diff --git a/apps/web/src/assets/i18n/pt.json b/apps/web/src/assets/i18n/pt.json index 95399c668..ceddf223d 100644 --- a/apps/web/src/assets/i18n/pt.json +++ b/apps/web/src/assets/i18n/pt.json @@ -2,6 +2,7 @@ "CANCEL": "Cancelar", "CLEAR": "Limpar", "CLOSE": "Fechar", + "BACK": "Voltar", "SHOW_MORE": "Mais", "SHOW_LESS": "Menos", "PLAYLISTS": "Playlists", @@ -918,6 +919,19 @@ }, "STREAM_URL_COPIED": "URL do stream copiado para a área de transferência", "COPY_STREAM_URL": "Copiar URL do Stream", + "NOW_PLAYING": "Reproduzindo agora", + "CLOSE_PLAYER": "Fechar reprodutor", + "ABOUT": "Sobre", + "SEASONS_AND_EPISODES": "Temporadas e episódios", + "SEASON_TAB": "Temporada {{number}}", + "BACK_TO_PLAYING_EPISODE": "Voltar ao episódio em reprodução", + "GRID_VIEW": "Visualização em grade", + "LIST_VIEW": "Visualização em lista", + "NO_EPISODES_AVAILABLE": "Nenhum episódio disponível", + "NO_EPISODES_AVAILABLE_MESSAGE": "Esta série está listada pelo seu provedor, mas não há temporadas ou episódios disponíveis para reprodução.", + "SEASON_EMPTY": "Esta temporada está vazia", + "SEASON_EMPTY_MESSAGE": "Esta temporada está listada pelo seu provedor, mas não há episódios reproduzíveis disponíveis.", + "EPISODE_INFO": "Detalhes do episódio", "SELECT_CATEGORY": "Selecione uma categoria", "PAGE": "Página", "CONTENT_NOT_AVAILABLE": "Este conteúdo não está disponível neste servidor", diff --git a/apps/web/src/assets/i18n/ru.json b/apps/web/src/assets/i18n/ru.json index 09cc42b94..548ce29b2 100644 --- a/apps/web/src/assets/i18n/ru.json +++ b/apps/web/src/assets/i18n/ru.json @@ -2,6 +2,7 @@ "CANCEL": "Отмена", "CLEAR": "Очистить", "CLOSE": "Закрыть", + "BACK": "Назад", "SHOW_MORE": "Больше", "SHOW_LESS": "Меньше", "PLAYLISTS": "Плейлисты", @@ -918,6 +919,19 @@ }, "STREAM_URL_COPIED": "URL-адрес потока был скопирован в буфер обмена.", "COPY_STREAM_URL": "Копировать URL-адрес потока", + "NOW_PLAYING": "Сейчас воспроизводится", + "CLOSE_PLAYER": "Закрыть плеер", + "ABOUT": "Об этом", + "SEASONS_AND_EPISODES": "Сезоны и эпизоды", + "SEASON_TAB": "Сезон {{number}}", + "BACK_TO_PLAYING_EPISODE": "Вернуться к воспроизводимому эпизоду", + "GRID_VIEW": "Вид сеткой", + "LIST_VIEW": "Вид списком", + "NO_EPISODES_AVAILABLE": "Нет доступных эпизодов", + "NO_EPISODES_AVAILABLE_MESSAGE": "Этот сериал есть в списке вашего провайдера, но доступных для воспроизведения сезонов или эпизодов нет.", + "SEASON_EMPTY": "Этот сезон пуст", + "SEASON_EMPTY_MESSAGE": "Этот сезон есть в списке вашего провайдера, но доступных для воспроизведения эпизодов нет.", + "EPISODE_INFO": "Детали эпизода", "SELECT_CATEGORY": "Выберите категорию", "PAGE": "Страница", "CONTENT_NOT_AVAILABLE": "Этот контент недоступен на этом сервере", diff --git a/apps/web/src/assets/i18n/tr.json b/apps/web/src/assets/i18n/tr.json index 8640198ba..36bbb0ed9 100644 --- a/apps/web/src/assets/i18n/tr.json +++ b/apps/web/src/assets/i18n/tr.json @@ -2,6 +2,7 @@ "CANCEL": "İptal", "CLEAR": "Temizle", "CLOSE": "Kapat", + "BACK": "Geri", "SHOW_MORE": "Daha fazla", "SHOW_LESS": "Daha az", "PLAYLISTS": "Oynatma Listeleri", @@ -918,6 +919,19 @@ }, "STREAM_URL_COPIED": "Yayın URL'si panoya kopyalandı", "COPY_STREAM_URL": "Yayın URL'sini kopyala", + "NOW_PLAYING": "Şimdi oynatılıyor", + "CLOSE_PLAYER": "Oynatıcıyı kapat", + "ABOUT": "Hakkında", + "SEASONS_AND_EPISODES": "Sezonlar ve Bölümler", + "SEASON_TAB": "{{number}}. Sezon", + "BACK_TO_PLAYING_EPISODE": "Oynatılan bölüme dön", + "GRID_VIEW": "Izgara görünümü", + "LIST_VIEW": "Liste görünümü", + "NO_EPISODES_AVAILABLE": "Bölüm mevcut değil", + "NO_EPISODES_AVAILABLE_MESSAGE": "Bu dizi sağlayıcınız tarafından listeleniyor, ancak oynatılabilecek sezon veya bölüm bulunmuyor.", + "SEASON_EMPTY": "Bu sezon boş", + "SEASON_EMPTY_MESSAGE": "Bu sezon sağlayıcınız tarafından listeleniyor, ancak oynatılabilir bölüm bulunmuyor.", + "EPISODE_INFO": "Bölüm ayrıntıları", "SELECT_CATEGORY": "Bir kategori seçin", "PAGE": "Sayfa", "CONTENT_NOT_AVAILABLE": "Bu içerik bu sunucuda mevcut değil", diff --git a/apps/web/src/assets/i18n/zh.json b/apps/web/src/assets/i18n/zh.json index 38886c40a..777ae5d33 100644 --- a/apps/web/src/assets/i18n/zh.json +++ b/apps/web/src/assets/i18n/zh.json @@ -2,6 +2,7 @@ "CANCEL": "取消", "CLEAR": "清除", "CLOSE": "关闭", + "BACK": "返回", "SHOW_MORE": "更多", "SHOW_LESS": "收起", "PLAYLISTS": "播放列表", @@ -918,6 +919,19 @@ }, "STREAM_URL_COPIED": "流 URL 已复制到剪贴板", "COPY_STREAM_URL": "复制流 URL", + "NOW_PLAYING": "正在播放", + "CLOSE_PLAYER": "关闭播放器", + "ABOUT": "简介", + "SEASONS_AND_EPISODES": "季与集", + "SEASON_TAB": "第 {{number}} 季", + "BACK_TO_PLAYING_EPISODE": "返回正在播放的剧集", + "GRID_VIEW": "网格视图", + "LIST_VIEW": "列表视图", + "NO_EPISODES_AVAILABLE": "没有可用的剧集", + "NO_EPISODES_AVAILABLE_MESSAGE": "您的提供商列出了此剧集,但没有可播放的季或集。", + "SEASON_EMPTY": "本季暂无内容", + "SEASON_EMPTY_MESSAGE": "您的提供商列出了本季,但没有可播放的剧集。", + "EPISODE_INFO": "剧集详情", "SELECT_CATEGORY": "选择一个分类", "PAGE": "页", "CONTENT_NOT_AVAILABLE": "此服务器上没有此内容", diff --git a/apps/web/src/assets/i18n/zhtw.json b/apps/web/src/assets/i18n/zhtw.json index 275771b37..495789324 100644 --- a/apps/web/src/assets/i18n/zhtw.json +++ b/apps/web/src/assets/i18n/zhtw.json @@ -2,6 +2,7 @@ "CANCEL": "取消", "CLEAR": "清除", "CLOSE": "關閉", + "BACK": "返回", "SHOW_MORE": "更多", "SHOW_LESS": "收起", "PLAYLISTS": "播放清單", @@ -918,6 +919,19 @@ }, "STREAM_URL_COPIED": "串流網址已複製到剪貼簿", "COPY_STREAM_URL": "複製串流網址", + "NOW_PLAYING": "現正播放", + "CLOSE_PLAYER": "關閉播放器", + "ABOUT": "簡介", + "SEASONS_AND_EPISODES": "季數與集數", + "SEASON_TAB": "第 {{number}} 季", + "BACK_TO_PLAYING_EPISODE": "返回正在播放的集數", + "GRID_VIEW": "格狀檢視", + "LIST_VIEW": "清單檢視", + "NO_EPISODES_AVAILABLE": "沒有可用的集數", + "NO_EPISODES_AVAILABLE_MESSAGE": "此供應商列出了這部影集,但目前沒有可播放的季數或集數。", + "SEASON_EMPTY": "此季暫無內容", + "SEASON_EMPTY_MESSAGE": "此供應商列出了這一季,但目前沒有可播放的集數。", + "EPISODE_INFO": "集數詳情", "SELECT_CATEGORY": "選擇類別", "PAGE": "頁", "CONTENT_NOT_AVAILABLE": "此伺服器上未提供此內容", diff --git a/docs/architecture/embedded-inline-playback.md b/docs/architecture/embedded-inline-playback.md index 8d53a7051..69f7c642d 100644 --- a/docs/architecture/embedded-inline-playback.md +++ b/docs/architecture/embedded-inline-playback.md @@ -82,8 +82,63 @@ Current limitation: - because of that, the setting is auto-sanitized back to the default inline player unless support detection reports that the experimental runtime is available - this follows the rollout gate: keep the native work in-tree, but do not leave it user-facing until playback, resize, focus, and packaging are stable +## Two-State Detail Layout (Browse ↔ Watch) + +Portal VOD/series detail pages are hosted by the shared +`PortalDetailShellComponent` +(`libs/ui/components/src/lib/portal-detail-shell/portal-detail-shell.component.ts`), +which owns the page scroll container and two layout states: + +- **Browse** (`playbackActive=false`): the hero (`ContentHeroComponent`, + now a natural-height, non-scrolling block) renders poster, title, chips, + description, credits, and actions. Episodes render below. +- **Watch** (`playbackActive=true`, bound by hosts to + `inlinePlayback() !== null`): the hero collapses (~300ms CSS morph, + disabled under `prefers-reduced-motion`), the host-projected + `[detail-player]` slot renders the inline player at full content width, + and the shell renders an About block (`ContentAboutComponent`) below the + `[detail-episodes]` slot so the hero metadata stays reachable. + +Contracts: + +- Hosts provide hero chips/meta/actions as `*appDetailTags` / `*appDetailMeta` / + `*appDetailActions` templates; the shell stamps them into the hero and again + into the About block. Degradation stays "missing → not rendered". +- The shell never wraps the `[detail-player]` slot in a conditional; the + host's `@if (inlinePlayback())` is the only creator/destroyer of the + player subtree, so shell state changes cannot recreate the `