From c76cf357b7b5453212f9a9dbaeeaa4ea743dc777 Mon Sep 17 00:00:00 2001 From: 4gray <4gray@users.noreply.github.com> Date: Sun, 11 Oct 2026 11:07:57 +0200 Subject: [PATCH] refactor(workspace): remove standalone-layout branches no route can reach (#1889) * refactor(workspace): remove standalone-layout branches no route can reach Every routed view sits under the /workspace route (the other top-level paths are redirects), so isWorkspaceLayoutRoute() was true for every caller. Global search became a route on 2026-01-12 and the last route outside the workspace went with the home component on 2026-03-02; nothing opens SearchResultsComponent in a MatDialog. Removed as unreachable: - isWorkspaceLayoutRoute() and the route's data.layout marker; the channel list, Xtream live layout and unified collection read the q query param directly, the clear-view command no longer checks a flag that was always true, and the M3U EPG guide header action keeps only its supportsEpg gate. - The playback sidebar's playlist header (Home button and playlist switcher, showPlaylistHeader, its stylesheet and the macOS padding rule). The switcher itself stays: the workspace header uses it. - Xtream search's dialog mode (MAT_DIALOG_DATA initialQuery, dialogRef, close buttons) and inline search input; Back is offered whenever the search is not the global one, as before. - Stalker search's inline-input flag (Back is always offered, as before). - SearchLayoutComponent's showSearchInput, showCloseButton, closeClick, searchTermChange and focusSearchInput, and SearchFormComponent, which only it rendered. - SETTINGS.BACK_TO_HOME and PORTALS.SEARCH in all 19 locales, the zoneless checklist entry for the deleted form, the specs that only covered these paths, and two Electron E2E assertions that the deleted form is absent. Behaviour of every reachable path is unchanged. Co-Authored-By: Claude Opus 5.5 * refactor(ui): drop the macOS host class and switcher rule nothing uses The app root's `macos-platform` host class only fed the removed sidebar header's padding rule, so no stylesheet reads it any more. The shared portal-sidebar partial's `.sidebar-header .switcher` rule matched no element: the only `switcher` class lived in that removed header, which used `.current-playlist`, not `.sidebar-header`. Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Claude Opus 5.5 --- apps/electron-backend-e2e/src/search.e2e.ts | 6 - apps/web/src/app/app.component.spec.ts | 1 - apps/web/src/app/app.component.ts | 4 - apps/web/src/app/app.routes.ts | 3 - apps/web/src/assets/i18n/ar.json | 2 - apps/web/src/assets/i18n/ary.json | 2 - apps/web/src/assets/i18n/by.json | 2 - apps/web/src/assets/i18n/de.json | 2 - apps/web/src/assets/i18n/el.json | 2 - apps/web/src/assets/i18n/en.json | 2 - apps/web/src/assets/i18n/es.json | 2 - apps/web/src/assets/i18n/fr.json | 2 - apps/web/src/assets/i18n/hu.json | 2 - apps/web/src/assets/i18n/it.json | 2 - apps/web/src/assets/i18n/ja.json | 2 - apps/web/src/assets/i18n/ko.json | 2 - apps/web/src/assets/i18n/nl.json | 2 - apps/web/src/assets/i18n/pl.json | 2 - apps/web/src/assets/i18n/pt.json | 2 - apps/web/src/assets/i18n/ru.json | 2 - apps/web/src/assets/i18n/tr.json | 2 - apps/web/src/assets/i18n/zh.json | 2 - apps/web/src/assets/i18n/zhtw.json | 2 - apps/web/src/styles.scss | 4 - docs/architecture/workspace-shell.md | 14 +- docs/architecture/zoneless-migration.md | 1 - .../video-player-catchup-timeline.spec.ts | 1 - .../video-player-guide-gating.spec.ts | 1 - .../video-player-movie-gate.spec.ts | 1 - .../video-player-recent-history.spec.ts | 1 - .../video-player/video-player-sidebar.spec.ts | 1 - ...-player.component.fullscreen-panel.spec.ts | 1 - .../video-player/video-player.component.html | 1 - .../video-player.component.spec.ts | 1 - .../video-player/video-player.component.ts | 4 +- libs/portal/shared/ui/src/index.ts | 1 - .../search-form/search-form.component.html | 42 ------ .../search-form/search-form.component.ts | 129 ------------------ .../search-layout.component.html | 19 --- .../search-layout.component.scss | 4 - .../search-layout/search-layout.component.ts | 30 ---- .../unified-collection-clear-action.ts | 4 +- .../unified-collection-page.component.spec.ts | 2 - .../unified-collection-page.component.ts | 12 +- .../lib/navigation/portal-route.utils.spec.ts | 43 ------ .../src/lib/navigation/portal-route.utils.ts | 11 -- .../stalker-search.component.html | 4 +- .../stalker-search.component.ts | 7 - ...e-stream-auto-open-queue.component.spec.ts | 3 - ...ive-stream-layout.catchup-timeline.spec.ts | 3 - .../live-stream-layout.component.spec.ts | 7 - .../live-stream-layout.component.ts | 11 +- .../live-stream-layout.parental-lock.spec.ts | 3 - .../live-stream-layout.sidebar-levels.spec.ts | 3 - .../search-results.component.html | 14 -- .../search-results.component.spec.ts | 107 ++------------- .../search-results.component.ts | 80 +---------- .../channel-list-container.component.spec.ts | 2 - .../channel-list-container.component.ts | 8 +- .../sidebar/sidebar.component.html | 20 --- .../sidebar/sidebar.component.scss | 22 --- .../sidebar/sidebar.component.spec.ts | 37 ----- .../video-player/sidebar/sidebar.component.ts | 44 +----- libs/ui/styles/_portal-sidebar.scss | 5 - 64 files changed, 37 insertions(+), 723 deletions(-) delete mode 100644 libs/portal/shared/ui/src/lib/components/search-form/search-form.component.html delete mode 100644 libs/portal/shared/ui/src/lib/components/search-form/search-form.component.ts delete mode 100644 libs/ui/playback/src/lib/video-player/sidebar/sidebar.component.scss delete mode 100644 libs/ui/playback/src/lib/video-player/sidebar/sidebar.component.spec.ts diff --git a/apps/electron-backend-e2e/src/search.e2e.ts b/apps/electron-backend-e2e/src/search.e2e.ts index 381068e75..285ef9e05 100644 --- a/apps/electron-backend-e2e/src/search.e2e.ts +++ b/apps/electron-backend-e2e/src/search.e2e.ts @@ -580,9 +580,6 @@ test.describe('Electron Workspace Search', () => { xtreamQueries ); await expectWorkspaceSearchScope(app.mainWindow, 'Advanced search'); - await expect( - app.mainWindow.locator('app-search-layout app-search-form') - ).toHaveCount(0); await expect( xtreamSearchResultCards(app.mainWindow).first() ).toBeVisible({ timeout: 20000 }); @@ -901,9 +898,6 @@ test.describe('Electron Workspace Search', () => { /\/workspace\/stalker\/[^/]+\/search$/ ); await expectWorkspaceSearchScope(app.mainWindow, 'Advanced search'); - await expect( - app.mainWindow.locator('app-search-layout app-search-form') - ).toHaveCount(0); await fillWorkspaceSearch(app.mainWindow, sample.targetTitle); diff --git a/apps/web/src/app/app.component.spec.ts b/apps/web/src/app/app.component.spec.ts index 3dc225196..e8e80fb61 100644 --- a/apps/web/src/app/app.component.spec.ts +++ b/apps/web/src/app/app.component.spec.ts @@ -94,7 +94,6 @@ describe('AppComponent', () => { beforeEach(waitForAsync(() => { runtimeCapabilities = { isElectron: true, - isMacOS: false, }; epgBridge = { checkFreshness: jest.fn().mockResolvedValue({ diff --git a/apps/web/src/app/app.component.ts b/apps/web/src/app/app.component.ts index 804899aa4..5332fcc7a 100644 --- a/apps/web/src/app/app.component.ts +++ b/apps/web/src/app/app.component.ts @@ -1,7 +1,6 @@ import { Component, effect, - HostBinding, inject, OnInit, signal, @@ -66,9 +65,6 @@ const debugAppComponent = createDevLogger('AppComponent'); }) export class AppComponent implements OnInit { readonly routeReady = signal(false); - @HostBinding('class.macos-platform') get isMacOS() { - return this.runtime.isMacOS; - } get usesCustomWindowControls() { return this.runtime.usesCustomWindowControls; } diff --git a/apps/web/src/app/app.routes.ts b/apps/web/src/app/app.routes.ts index 123f51372..6a2f87f5d 100644 --- a/apps/web/src/app/app.routes.ts +++ b/apps/web/src/app/app.routes.ts @@ -77,9 +77,6 @@ export const routes: Routes = [ }, { path: 'workspace', - data: { - layout: 'workspace', - }, resolve: { settingsReady: settingsReadyResolver, }, diff --git a/apps/web/src/assets/i18n/ar.json b/apps/web/src/assets/i18n/ar.json index 6b6519765..95df4bbd4 100644 --- a/apps/web/src/assets/i18n/ar.json +++ b/apps/web/src/assets/i18n/ar.json @@ -394,7 +394,6 @@ "UNSAVED_DIALOG_SAVE": "حفظ", "UNSAVED_DIALOG_DISCARD": "تجاهل", "UNSAVED_DIALOG_INVALID_HINT": "بعض الحقول غير صالحة، لذلك لا يمكن حفظ التغييرات بعد.", - "BACK_TO_HOME": "العودة للرئيسية", "NEW_VERSION_AVAILABLE": "إصدار جديد متاح", "LATEST_VERSION": "أنت تستخدم الإصدار الأحدث", "STREAM_FORMAT": "تنسيق البث", @@ -1278,7 +1277,6 @@ "LIVE_TV": "البث المباشر", "MOVIES": "الأفلام", "SERIES": "المسلسلات", - "SEARCH": "بحث", "NO_FAVORITES": "لا توجد مفضلة بعد", "NO_RECENT_ITEMS": "لا توجد عناصر تمت مشاهدتها مؤخرًا", "REMOVE_FROM_RECENT": "إزالة من العناصر التي تمت مشاهدتها مؤخرًا", diff --git a/apps/web/src/assets/i18n/ary.json b/apps/web/src/assets/i18n/ary.json index b5e819ad5..67212700c 100644 --- a/apps/web/src/assets/i18n/ary.json +++ b/apps/web/src/assets/i18n/ary.json @@ -394,7 +394,6 @@ "UNSAVED_DIALOG_SAVE": "حفظ", "UNSAVED_DIALOG_DISCARD": "تجاهل", "UNSAVED_DIALOG_INVALID_HINT": "شي خانات ماشي صحيحين، علاش مايمكنش تسجل التغييرات دابا.", - "BACK_TO_HOME": "رجوع", "NEW_VERSION_AVAILABLE": "كاين إصدار جديد متوفر", "LATEST_VERSION": "راك تستعمل أحدث إصدار", "STREAM_FORMAT": "صيغة البث", @@ -1278,7 +1277,6 @@ "LIVE_TV": "البث المباشر", "MOVIES": "الأفلام", "SERIES": "السلاسل", - "SEARCH": "بحث", "NO_FAVORITES": "مازال ما كاينش مفضلة", "NO_RECENT_ITEMS": "ما كاينش عناصر متشافة مؤخراً", "REMOVE_FROM_RECENT": "حيد من المتشاف مؤخراً", diff --git a/apps/web/src/assets/i18n/by.json b/apps/web/src/assets/i18n/by.json index 6540fbd6f..daadab7d4 100644 --- a/apps/web/src/assets/i18n/by.json +++ b/apps/web/src/assets/i18n/by.json @@ -394,7 +394,6 @@ "UNSAVED_DIALOG_SAVE": "Захаваць", "UNSAVED_DIALOG_DISCARD": "Не захоўваць", "UNSAVED_DIALOG_INVALID_HINT": "Некаторыя палі запоўнены няправільна, таму змены пакуль нельга захаваць.", - "BACK_TO_HOME": "Назад", "NEW_VERSION_AVAILABLE": "Даступная новая версія праграмы", "LATEST_VERSION": "Вы выкарыстоўваеце апошнюю версію праграмы", "STREAM_FORMAT": "Фармат патоку", @@ -1278,7 +1277,6 @@ "LIVE_TV": "Прамы эфір", "MOVIES": "Фільмы", "SERIES": "Серыялы", - "SEARCH": "Пошук", "NO_FAVORITES": "Пакуль няма выбранага", "NO_RECENT_ITEMS": "Няма нядаўна праглянутых элементаў", "REMOVE_FROM_RECENT": "Выдаліць з нядаўна праглянутага", diff --git a/apps/web/src/assets/i18n/de.json b/apps/web/src/assets/i18n/de.json index d12a5ec18..11962d897 100644 --- a/apps/web/src/assets/i18n/de.json +++ b/apps/web/src/assets/i18n/de.json @@ -394,7 +394,6 @@ "UNSAVED_DIALOG_SAVE": "Speichern", "UNSAVED_DIALOG_DISCARD": "Verwerfen", "UNSAVED_DIALOG_INVALID_HINT": "Einige Felder sind ungültig, daher können die Änderungen noch nicht gespeichert werden.", - "BACK_TO_HOME": "Zurück", "NEW_VERSION_AVAILABLE": "EIne neue Version der Anwendung ist verfügbar", "LATEST_VERSION": "Sie nutzen die neueste Version der Anwendung", "STREAM_FORMAT": "Stream-Format", @@ -1278,7 +1277,6 @@ "LIVE_TV": "Live-TV", "MOVIES": "Filme", "SERIES": "Serien", - "SEARCH": "Suche", "NO_FAVORITES": "Noch keine Favoriten", "NO_RECENT_ITEMS": "Keine zuletzt angesehenen Inhalte", "REMOVE_FROM_RECENT": "Aus „Zuletzt angesehen“ entfernen", diff --git a/apps/web/src/assets/i18n/el.json b/apps/web/src/assets/i18n/el.json index 559940e19..f898d92ed 100644 --- a/apps/web/src/assets/i18n/el.json +++ b/apps/web/src/assets/i18n/el.json @@ -394,7 +394,6 @@ "UNSAVED_DIALOG_SAVE": "Αποθήκευση", "UNSAVED_DIALOG_DISCARD": "Απόρριψη", "UNSAVED_DIALOG_INVALID_HINT": "Ορισμένα πεδία δεν είναι έγκυρα, επομένως οι αλλαγές δεν μπορούν να αποθηκευτούν ακόμη.", - "BACK_TO_HOME": "Πίσω", "NEW_VERSION_AVAILABLE": "Υπάρχει διαθέσιμη μια νέα έκδοση", "LATEST_VERSION": "Χρησιμοποιείτε την πιο πρόσφατη έκδοση", "STREAM_FORMAT": "Μορφή Stream", @@ -1278,7 +1277,6 @@ "LIVE_TV": "Ζωντανή TV", "MOVIES": "Ταινίες", "SERIES": "Σειρές", - "SEARCH": "Αναζήτηση", "NO_FAVORITES": "Δεν υπάρχουν ακόμα αγαπημένα", "NO_RECENT_ITEMS": "Δεν υπάρχουν πρόσφατα προβληθέντα στοιχεία", "REMOVE_FROM_RECENT": "Αφαίρεση από τα πρόσφατα προβληθέντα", diff --git a/apps/web/src/assets/i18n/en.json b/apps/web/src/assets/i18n/en.json index 100427948..084229dc6 100644 --- a/apps/web/src/assets/i18n/en.json +++ b/apps/web/src/assets/i18n/en.json @@ -394,7 +394,6 @@ "UNSAVED_DIALOG_SAVE": "Save", "UNSAVED_DIALOG_DISCARD": "Discard", "UNSAVED_DIALOG_INVALID_HINT": "Some fields are invalid, so the changes cannot be saved yet.", - "BACK_TO_HOME": "Back", "NEW_VERSION_AVAILABLE": "There is a new version available", "LATEST_VERSION": "You are using the latest version", "STREAM_FORMAT": "Stream Format", @@ -1278,7 +1277,6 @@ "LIVE_TV": "Live TV", "MOVIES": "Movies", "SERIES": "Series", - "SEARCH": "Search", "NO_FAVORITES": "No favorites yet", "NO_RECENT_ITEMS": "No recently viewed items", "REMOVE_FROM_RECENT": "Remove from recently viewed", diff --git a/apps/web/src/assets/i18n/es.json b/apps/web/src/assets/i18n/es.json index faac6c3c7..53da10327 100644 --- a/apps/web/src/assets/i18n/es.json +++ b/apps/web/src/assets/i18n/es.json @@ -394,7 +394,6 @@ "UNSAVED_DIALOG_SAVE": "Guardar", "UNSAVED_DIALOG_DISCARD": "Descartar", "UNSAVED_DIALOG_INVALID_HINT": "Algunos campos no son válidos, por lo que los cambios aún no se pueden guardar.", - "BACK_TO_HOME": "Volver al inicio", "NEW_VERSION_AVAILABLE": "Hay una nueva versión disponible", "LATEST_VERSION": "Estás usando la versión reciente", "STREAM_FORMAT": "Formato de transmision", @@ -1278,7 +1277,6 @@ "LIVE_TV": "TV en vivo", "MOVIES": "Películas", "SERIES": "Series", - "SEARCH": "Buscar", "NO_FAVORITES": "Aún no hay favoritos", "NO_RECENT_ITEMS": "No hay elementos vistos recientemente", "REMOVE_FROM_RECENT": "Quitar de vistos recientemente", diff --git a/apps/web/src/assets/i18n/fr.json b/apps/web/src/assets/i18n/fr.json index f4e6eb630..6d5697727 100644 --- a/apps/web/src/assets/i18n/fr.json +++ b/apps/web/src/assets/i18n/fr.json @@ -394,7 +394,6 @@ "UNSAVED_DIALOG_SAVE": "Enregistrer", "UNSAVED_DIALOG_DISCARD": "Ne pas enregistrer", "UNSAVED_DIALOG_INVALID_HINT": "Certains champs sont invalides, les modifications ne peuvent donc pas encore être enregistrées.", - "BACK_TO_HOME": "Retourner à l'accueil", "NEW_VERSION_AVAILABLE": "Une nouvelle version est disponible", "LATEST_VERSION": "Vous utilisez la dernière version", "STREAM_FORMAT": "Format du flux", @@ -1278,7 +1277,6 @@ "LIVE_TV": "TV en direct", "MOVIES": "Films", "SERIES": "Séries", - "SEARCH": "Rechercher", "NO_FAVORITES": "Aucun favori pour le moment", "NO_RECENT_ITEMS": "Aucun élément récemment consulté", "REMOVE_FROM_RECENT": "Retirer des éléments récemment consultés", diff --git a/apps/web/src/assets/i18n/hu.json b/apps/web/src/assets/i18n/hu.json index 0e10bd5cf..303057267 100644 --- a/apps/web/src/assets/i18n/hu.json +++ b/apps/web/src/assets/i18n/hu.json @@ -394,7 +394,6 @@ "UNSAVED_DIALOG_SAVE": "Mentés", "UNSAVED_DIALOG_DISCARD": "Elvetés", "UNSAVED_DIALOG_INVALID_HINT": "Néhány mező érvénytelen, ezért a módosítások még nem menthetők.", - "BACK_TO_HOME": "Vissza", "NEW_VERSION_AVAILABLE": "Új verzió érhető el", "LATEST_VERSION": "A legújabb verziót használja", "STREAM_FORMAT": "Adatfolyam formátuma", @@ -1278,7 +1277,6 @@ "LIVE_TV": "Élő tévéadás", "MOVIES": "Filmek", "SERIES": "Sorozatok", - "SEARCH": "Keresés", "NO_FAVORITES": "Még nincs kedvenc tartalom", "NO_RECENT_ITEMS": "Még nincs megtekintési előzmény", "REMOVE_FROM_RECENT": "Eltávolítás a megtekintési előzményekből", diff --git a/apps/web/src/assets/i18n/it.json b/apps/web/src/assets/i18n/it.json index b989b1ff9..665fd12c8 100644 --- a/apps/web/src/assets/i18n/it.json +++ b/apps/web/src/assets/i18n/it.json @@ -394,7 +394,6 @@ "UNSAVED_DIALOG_SAVE": "Salva", "UNSAVED_DIALOG_DISCARD": "Non salvare", "UNSAVED_DIALOG_INVALID_HINT": "Alcuni campi non sono validi, quindi le modifiche non possono ancora essere salvate.", - "BACK_TO_HOME": "Indietro", "NEW_VERSION_AVAILABLE": "È disponibile una nuova versione", "LATEST_VERSION": "Questa versione è aggiornata", "STREAM_FORMAT": "Formato streaming", @@ -1278,7 +1277,6 @@ "LIVE_TV": "TV in diretta", "MOVIES": "Film", "SERIES": "Serie", - "SEARCH": "Cerca", "NO_FAVORITES": "Ancora nessun preferito", "NO_RECENT_ITEMS": "Nessun elemento visto di recente", "REMOVE_FROM_RECENT": "Rimuovi dai visti di recente", diff --git a/apps/web/src/assets/i18n/ja.json b/apps/web/src/assets/i18n/ja.json index 1999c648d..9ed6e3341 100644 --- a/apps/web/src/assets/i18n/ja.json +++ b/apps/web/src/assets/i18n/ja.json @@ -394,7 +394,6 @@ "UNSAVED_DIALOG_SAVE": "保存", "UNSAVED_DIALOG_DISCARD": "破棄", "UNSAVED_DIALOG_INVALID_HINT": "一部の項目が無効なため、変更をまだ保存できません。", - "BACK_TO_HOME": "戻る", "NEW_VERSION_AVAILABLE": "新しいバージョンが利用可能です", "LATEST_VERSION": "最新バージョンを使用中です", "STREAM_FORMAT": "ストリーム形式", @@ -1278,7 +1277,6 @@ "LIVE_TV": "ライブTV", "MOVIES": "映画", "SERIES": "シリーズ", - "SEARCH": "検索", "NO_FAVORITES": "まだお気に入りがありません", "NO_RECENT_ITEMS": "最近表示した項目はありません", "REMOVE_FROM_RECENT": "最近表示した項目から削除", diff --git a/apps/web/src/assets/i18n/ko.json b/apps/web/src/assets/i18n/ko.json index 03e6288b1..ea0e77435 100644 --- a/apps/web/src/assets/i18n/ko.json +++ b/apps/web/src/assets/i18n/ko.json @@ -394,7 +394,6 @@ "UNSAVED_DIALOG_SAVE": "저장", "UNSAVED_DIALOG_DISCARD": "저장 안 함", "UNSAVED_DIALOG_INVALID_HINT": "일부 항목이 유효하지 않아 변경 사항을 아직 저장할 수 없습니다.", - "BACK_TO_HOME": "홈으로", "NEW_VERSION_AVAILABLE": "새로운 버전이 있습니다", "LATEST_VERSION": "최신 버전을 사용하고 있습니다", "STREAM_FORMAT": "스트림 형식", @@ -1278,7 +1277,6 @@ "LIVE_TV": "라이브 TV", "MOVIES": "영화", "SERIES": "시리즈", - "SEARCH": "검색", "NO_FAVORITES": "아직 즐겨찾기가 없습니다", "NO_RECENT_ITEMS": "최근 본 항목이 없습니다", "REMOVE_FROM_RECENT": "최근 본 항목에서 제거", diff --git a/apps/web/src/assets/i18n/nl.json b/apps/web/src/assets/i18n/nl.json index 69f8d27c5..3b8fc4959 100644 --- a/apps/web/src/assets/i18n/nl.json +++ b/apps/web/src/assets/i18n/nl.json @@ -394,7 +394,6 @@ "UNSAVED_DIALOG_SAVE": "Opslaan", "UNSAVED_DIALOG_DISCARD": "Verwerpen", "UNSAVED_DIALOG_INVALID_HINT": "Sommige velden zijn ongeldig, dus de wijzigingen kunnen nog niet worden opgeslagen.", - "BACK_TO_HOME": "Terug", "NEW_VERSION_AVAILABLE": "Een nieuwe versie is beschikbaar", "LATEST_VERSION": "Je gebruikt de nieuwste versie", "STREAM_FORMAT": "Streamformaat", @@ -1278,7 +1277,6 @@ "LIVE_TV": "Live tv", "MOVIES": "Films", "SERIES": "Series", - "SEARCH": "Zoeken", "NO_FAVORITES": "Nog geen favorieten", "NO_RECENT_ITEMS": "Geen recent bekeken items", "REMOVE_FROM_RECENT": "Verwijderen uit recent bekeken", diff --git a/apps/web/src/assets/i18n/pl.json b/apps/web/src/assets/i18n/pl.json index c3eaf6069..f05786109 100644 --- a/apps/web/src/assets/i18n/pl.json +++ b/apps/web/src/assets/i18n/pl.json @@ -394,7 +394,6 @@ "UNSAVED_DIALOG_SAVE": "Zapisz", "UNSAVED_DIALOG_DISCARD": "Odrzuć", "UNSAVED_DIALOG_INVALID_HINT": "Niektóre pola są nieprawidłowe, więc zmian nie można jeszcze zapisać.", - "BACK_TO_HOME": "Wróć", "NEW_VERSION_AVAILABLE": "Dostępna jest nowa wersja", "LATEST_VERSION": "Używasz najnowszej wersji", "STREAM_FORMAT": "Format strumienia", @@ -1278,7 +1277,6 @@ "LIVE_TV": "Telewizja na żywo", "MOVIES": "Filmy", "SERIES": "Seriale", - "SEARCH": "Szukaj", "NO_FAVORITES": "Brak ulubionych", "NO_RECENT_ITEMS": "Brak ostatnio oglądanych", "REMOVE_FROM_RECENT": "Usuń z ostatnio oglądanych", diff --git a/apps/web/src/assets/i18n/pt.json b/apps/web/src/assets/i18n/pt.json index ab15f4548..e0e2e43a4 100644 --- a/apps/web/src/assets/i18n/pt.json +++ b/apps/web/src/assets/i18n/pt.json @@ -394,7 +394,6 @@ "UNSAVED_DIALOG_SAVE": "Salvar", "UNSAVED_DIALOG_DISCARD": "Descartar", "UNSAVED_DIALOG_INVALID_HINT": "Alguns campos são inválidos, então as alterações ainda não podem ser salvas.", - "BACK_TO_HOME": "Voltar", "NEW_VERSION_AVAILABLE": "Há uma nova versão disponível", "LATEST_VERSION": "Você está usando a versão mais recente", "STREAM_FORMAT": "Formato de stream", @@ -1278,7 +1277,6 @@ "LIVE_TV": "TV ao vivo", "MOVIES": "Filmes", "SERIES": "Séries", - "SEARCH": "Pesquisar", "NO_FAVORITES": "Nenhum favorito ainda", "NO_RECENT_ITEMS": "Nenhum item visualizado recentemente", "REMOVE_FROM_RECENT": "Remover dos visualizados recentemente", diff --git a/apps/web/src/assets/i18n/ru.json b/apps/web/src/assets/i18n/ru.json index 91867c5de..9da7e722b 100644 --- a/apps/web/src/assets/i18n/ru.json +++ b/apps/web/src/assets/i18n/ru.json @@ -394,7 +394,6 @@ "UNSAVED_DIALOG_SAVE": "Сохранить", "UNSAVED_DIALOG_DISCARD": "Не сохранять", "UNSAVED_DIALOG_INVALID_HINT": "Некоторые поля заполнены неверно, поэтому изменения пока нельзя сохранить.", - "BACK_TO_HOME": "Назад", "NEW_VERSION_AVAILABLE": "Доступна новая версия приложения", "LATEST_VERSION": "Вы используете последнюю версию приложения", "STREAM_FORMAT": "Формат потока", @@ -1278,7 +1277,6 @@ "LIVE_TV": "Live TV", "MOVIES": "Фильмы", "SERIES": "Сериалы", - "SEARCH": "Поиск", "NO_FAVORITES": "Пока нет избранного", "NO_RECENT_ITEMS": "Нет недавно просмотренных элементов", "REMOVE_FROM_RECENT": "Удалить из недавно просмотренных", diff --git a/apps/web/src/assets/i18n/tr.json b/apps/web/src/assets/i18n/tr.json index f54f32a7d..f207fa817 100644 --- a/apps/web/src/assets/i18n/tr.json +++ b/apps/web/src/assets/i18n/tr.json @@ -394,7 +394,6 @@ "UNSAVED_DIALOG_SAVE": "Kaydet", "UNSAVED_DIALOG_DISCARD": "Kaydetme", "UNSAVED_DIALOG_INVALID_HINT": "Bazı alanlar geçersiz olduğundan değişiklikler henüz kaydedilemiyor.", - "BACK_TO_HOME": "Ana sayfaya dön", "NEW_VERSION_AVAILABLE": "Yeni bir sürüm mevcut", "LATEST_VERSION": "En son sürümü kullanıyorsunuz", "STREAM_FORMAT": "Yayın Formatı", @@ -1278,7 +1277,6 @@ "LIVE_TV": "Canlı TV", "MOVIES": "Filmler", "SERIES": "Diziler", - "SEARCH": "Ara", "NO_FAVORITES": "Henüz favori yok", "NO_RECENT_ITEMS": "Son izlenen öğe yok", "REMOVE_FROM_RECENT": "Son izlenenlerden kaldır", diff --git a/apps/web/src/assets/i18n/zh.json b/apps/web/src/assets/i18n/zh.json index 9318a271e..5391ff5d0 100644 --- a/apps/web/src/assets/i18n/zh.json +++ b/apps/web/src/assets/i18n/zh.json @@ -394,7 +394,6 @@ "UNSAVED_DIALOG_SAVE": "保存", "UNSAVED_DIALOG_DISCARD": "放弃", "UNSAVED_DIALOG_INVALID_HINT": "部分字段无效,因此暂时无法保存更改。", - "BACK_TO_HOME": "回到主页", "NEW_VERSION_AVAILABLE": "有新版本可用 ", "LATEST_VERSION": "您使用的是最新版本", "STREAM_FORMAT": "流格式", @@ -1278,7 +1277,6 @@ "LIVE_TV": "直播电视", "MOVIES": "电影", "SERIES": "剧集", - "SEARCH": "搜索", "NO_FAVORITES": "暂无收藏", "NO_RECENT_ITEMS": "没有最近观看的项目", "REMOVE_FROM_RECENT": "从最近观看中移除", diff --git a/apps/web/src/assets/i18n/zhtw.json b/apps/web/src/assets/i18n/zhtw.json index 4e34981d6..b09af6277 100644 --- a/apps/web/src/assets/i18n/zhtw.json +++ b/apps/web/src/assets/i18n/zhtw.json @@ -394,7 +394,6 @@ "UNSAVED_DIALOG_SAVE": "儲存", "UNSAVED_DIALOG_DISCARD": "捨棄", "UNSAVED_DIALOG_INVALID_HINT": "部分欄位無效,因此暫時無法儲存變更。", - "BACK_TO_HOME": "返回主頁", "NEW_VERSION_AVAILABLE": "有新版本可用", "LATEST_VERSION": "您正在使用最新版本", "STREAM_FORMAT": "串流格式", @@ -1278,7 +1277,6 @@ "LIVE_TV": "直播電視", "MOVIES": "電影", "SERIES": "影集", - "SEARCH": "搜尋", "NO_FAVORITES": "尚無最愛項目", "NO_RECENT_ITEMS": "沒有最近觀看的項目", "REMOVE_FROM_RECENT": "從最近觀看中移除", diff --git a/apps/web/src/styles.scss b/apps/web/src/styles.scss index ffe72b68c..e2a6d22dc 100644 --- a/apps/web/src/styles.scss +++ b/apps/web/src/styles.scss @@ -102,10 +102,6 @@ letter-spacing: 0; } -app-root.macos-platform .current-playlist { - 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 (dialogs, menus). diff --git a/docs/architecture/workspace-shell.md b/docs/architecture/workspace-shell.md index 5566f9659..a161a3eee 100644 --- a/docs/architecture/workspace-shell.md +++ b/docs/architecture/workspace-shell.md @@ -65,19 +65,19 @@ Compatibility redirect: Provider route integration: -1. `apps/web/src/app/app.routes.ts` marks the `/workspace` root route with - `data.layout = 'workspace'`. -2. `isWorkspaceLayoutRoute(...)` treats that layout marker as inherited route - state for all descendants. -3. Xtream and Stalker parent routes attach route-scoped session providers that +1. Every routed view renders inside the workspace shell (the only top-level + paths outside `/workspace` are redirects), so views keep no standalone + layout branches: the shell header owns search, Back and the playlist + switcher. +2. Xtream and Stalker parent routes attach route-scoped session providers that bootstrap the active playlist, sync provider section state, and clean up provider-local state when the route is destroyed. -4. Xtream route bootstrap is DB-first for already imported Electron playlists: +3. Xtream route bootstrap is DB-first for already imported Electron playlists: if the requested section has persisted categories and content, the route hydrates from SQLite even when the portal status probe reports unavailable, expired, or inactive. Fresh/no-cache Xtream routes still use the status probe to block remote imports before the loading overlay starts. -5. Workspace routes no longer rely on nested provider shell components for +4. Workspace routes no longer rely on nested provider shell components for hidden local chrome. ## Shell Structure diff --git a/docs/architecture/zoneless-migration.md b/docs/architecture/zoneless-migration.md index 7af3e090c..a5eddb7c6 100644 --- a/docs/architecture/zoneless-migration.md +++ b/docs/architecture/zoneless-migration.md @@ -149,7 +149,6 @@ picker), so each marks itself on the form's `events` through ### libs/portal (8) - [x] `libs/portal/shared/ui/src/lib/components/playlist-error-view/playlist-error-view.component.ts` -- [x] `libs/portal/shared/ui/src/lib/components/search-form/search-form.component.ts` - [x] `libs/portal/shared/ui/src/lib/navigation/portal-rail-links.component.ts` - [x] `libs/portal/stalker/feature/src/lib/stalker-catalog-detail/stalker-catalog-detail.component.ts` - [x] `libs/portal/stalker/feature/src/lib/stalker-favorites-button/stalker-favorites-button.component.ts` diff --git a/libs/playlist/m3u/feature-player/src/lib/video-player/video-player-catchup-timeline.spec.ts b/libs/playlist/m3u/feature-player/src/lib/video-player/video-player-catchup-timeline.spec.ts index c169b5924..a4f9fc4c2 100644 --- a/libs/playlist/m3u/feature-player/src/lib/video-player/video-player-catchup-timeline.spec.ts +++ b/libs/playlist/m3u/feature-player/src/lib/video-player/video-player-catchup-timeline.spec.ts @@ -116,7 +116,6 @@ describe('VideoPlayerComponent — catch-up timeline segments', () => { params: of({ id: playlistId(), view: 'all' }), queryParams: of({}), snapshot: { - data: { layout: 'workspace' }, queryParams: {}, }, }, diff --git a/libs/playlist/m3u/feature-player/src/lib/video-player/video-player-guide-gating.spec.ts b/libs/playlist/m3u/feature-player/src/lib/video-player/video-player-guide-gating.spec.ts index 8cab98b70..95d93298c 100644 --- a/libs/playlist/m3u/feature-player/src/lib/video-player/video-player-guide-gating.spec.ts +++ b/libs/playlist/m3u/feature-player/src/lib/video-player/video-player-guide-gating.spec.ts @@ -100,7 +100,6 @@ describe('VideoPlayerComponent — guide gating', () => { params: of({ id: playlistId(), view: 'all' }), queryParams: of({}), snapshot: { - data: { layout: 'workspace' }, queryParams: {}, }, }, diff --git a/libs/playlist/m3u/feature-player/src/lib/video-player/video-player-movie-gate.spec.ts b/libs/playlist/m3u/feature-player/src/lib/video-player/video-player-movie-gate.spec.ts index 0e3e900c3..2ad11b09f 100644 --- a/libs/playlist/m3u/feature-player/src/lib/video-player/video-player-movie-gate.spec.ts +++ b/libs/playlist/m3u/feature-player/src/lib/video-player/video-player-movie-gate.spec.ts @@ -153,7 +153,6 @@ describe('VideoPlayerComponent — M3U movie recognition gate', () => { params: of({ id: playlistId(), view: 'all' }), queryParams: of({}), snapshot: { - data: { layout: 'workspace' }, queryParams: {}, }, }, diff --git a/libs/playlist/m3u/feature-player/src/lib/video-player/video-player-recent-history.spec.ts b/libs/playlist/m3u/feature-player/src/lib/video-player/video-player-recent-history.spec.ts index 889b9f0ed..8cccecc35 100644 --- a/libs/playlist/m3u/feature-player/src/lib/video-player/video-player-recent-history.spec.ts +++ b/libs/playlist/m3u/feature-player/src/lib/video-player/video-player-recent-history.spec.ts @@ -91,7 +91,6 @@ describe('VideoPlayerComponent — recently viewed history', () => { params: of({ id: playlistId(), view: 'all' }), queryParams: of({}), snapshot: { - data: { layout: 'workspace' }, queryParams: {}, }, }, diff --git a/libs/playlist/m3u/feature-player/src/lib/video-player/video-player-sidebar.spec.ts b/libs/playlist/m3u/feature-player/src/lib/video-player/video-player-sidebar.spec.ts index 8743ea02a..a93e9acea 100644 --- a/libs/playlist/m3u/feature-player/src/lib/video-player/video-player-sidebar.spec.ts +++ b/libs/playlist/m3u/feature-player/src/lib/video-player/video-player-sidebar.spec.ts @@ -171,7 +171,6 @@ describe('VideoPlayerComponent — collapsible channels rail', () => { params: routeParams$.asObservable(), queryParams: of({}), snapshot: { - data: { layout: 'workspace' }, queryParams: {}, }, }, diff --git a/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.fullscreen-panel.spec.ts b/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.fullscreen-panel.spec.ts index 72af02b4e..440cd0e54 100644 --- a/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.fullscreen-panel.spec.ts +++ b/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.fullscreen-panel.spec.ts @@ -144,7 +144,6 @@ describe('VideoPlayerComponent fullscreen channel panel + zapping', () => { params: of({ id: 'playlist-1', view: 'all' }), queryParams: of({}), snapshot: { - data: { layout: 'workspace' }, queryParams: {}, }, }, diff --git a/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.html b/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.html index 274f29958..0a136aac4 100644 --- a/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.html +++ b/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.html @@ -36,7 +36,6 @@ { params: of({ id: playlistId(), view: 'all' }), queryParams: of({}), snapshot: { - data: { layout: 'workspace' }, queryParams: {}, }, }, diff --git a/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.ts b/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.ts index 592f5bd55..c90ed40c0 100644 --- a/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.ts +++ b/libs/playlist/m3u/feature-player/src/lib/video-player/video-player.component.ts @@ -87,7 +87,6 @@ import { getAdjacentChannelItem, getChannelItemByNumber, isTypingInInput, - isWorkspaceLayoutRoute, LiveEpgPanelState, LiveLayoutSidebarStateService, persistLiveEpgPanelState, @@ -719,7 +718,6 @@ export class VideoPlayerComponent readonly isDesktop = this.runtime.isElectron; readonly supportsEpg = this.runtime.supportsEpg; - readonly isWorkspaceLayout = isWorkspaceLayoutRoute(this.activatedRoute); private unsubscribeRemoteChannelChange?: () => void; private unsubscribeRemoteCommand?: () => void; @@ -1750,7 +1748,7 @@ export class VideoPlayerComponent } private registerHeaderShortcut(): void { - if (!this.isWorkspaceLayout || !this.supportsEpg) { + if (!this.supportsEpg) { return; } diff --git a/libs/portal/shared/ui/src/index.ts b/libs/portal/shared/ui/src/index.ts index f99f0849d..d7fda96ed 100644 --- a/libs/portal/shared/ui/src/index.ts +++ b/libs/portal/shared/ui/src/index.ts @@ -9,7 +9,6 @@ export * from './lib/components/playlist-error-view/playlist-error-view.componen export { ProgressCapsuleComponent } from '@iptvnator/ui/components'; export * from './lib/components/portal-empty-state/portal-empty-state.component'; export * from './lib/components/channel-list-hidden-state/channel-list-hidden-state.component'; -export * from './lib/components/search-form/search-form.component'; export * from './lib/components/search-layout/search-layout.component'; export { WatchedBadgeComponent } from '@iptvnator/ui/components'; export * from './lib/components/unified-collection/unified-collection-page.component'; diff --git a/libs/portal/shared/ui/src/lib/components/search-form/search-form.component.html b/libs/portal/shared/ui/src/lib/components/search-form/search-form.component.html deleted file mode 100644 index 460295174..000000000 --- a/libs/portal/shared/ui/src/lib/components/search-form/search-form.component.html +++ /dev/null @@ -1,42 +0,0 @@ -
- - - @if (filterConfig.length > 0) { -
- @for (filter of filterConfig; track filter.key) { - - @if (filter.translationKey) { - {{ filter.translationKey | translate }} - } @else { - {{ filter.label }} - } - - } -
- } -
diff --git a/libs/portal/shared/ui/src/lib/components/search-form/search-form.component.ts b/libs/portal/shared/ui/src/lib/components/search-form/search-form.component.ts deleted file mode 100644 index d9c9f114f..000000000 --- a/libs/portal/shared/ui/src/lib/components/search-form/search-form.component.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { - Component, - ElementRef, - EventEmitter, - Input, - model, - Output, - ViewChild, - ChangeDetectionStrategy, -} from '@angular/core'; -import { FormsModule } from '@angular/forms'; -import { MatCheckboxModule } from '@angular/material/checkbox'; -import { MatButtonModule } from '@angular/material/button'; -import { MatFormFieldModule } from '@angular/material/form-field'; -import { MatIconModule } from '@angular/material/icon'; -import { MatInputModule } from '@angular/material/input'; -import { TranslatePipe } from '@ngx-translate/core'; - -export interface SearchFilters { - [key: string]: boolean; -} - -export interface SearchFilter { - key: string; - label: string; - translationKey?: string; -} - -@Component({ - selector: 'app-search-form', - standalone: true, - imports: [ - FormsModule, - MatFormFieldModule, - MatInputModule, - MatIconModule, - MatCheckboxModule, - MatButtonModule, - TranslatePipe, - ], - templateUrl: './search-form.component.html', - changeDetection: ChangeDetectionStrategy.OnPush, - styles: [ - ` - .search-container { - display: flex; - flex-direction: column; - gap: 16px; - } - - .recent-search { - display: flex; - align-items: center; - gap: 10px; - padding: 0 12px; - height: 42px; - border-radius: 12px; - border: 1px solid rgba(255, 255, 255, 0.08); - background: rgba(255, 255, 255, 0.03); - } - - .recent-search mat-icon { - opacity: 0.6; - font-size: 18px; - width: 18px; - height: 18px; - } - - .recent-search input { - width: 100%; - border: 0; - background: transparent; - color: inherit; - font: inherit; - opacity: 0.95; - } - - .recent-search input::placeholder { - opacity: 0.5; - } - - .clear-btn { - width: 30px; - height: 30px; - min-width: 30px; - } - `, - ], -}) -export class SearchFormComponent { - @ViewChild('searchInput') searchInput!: ElementRef; - /** Empty means the generic "Search" label */ - @Input() placeholder = ''; - @Input() filters: SearchFilters = {}; - @Input() filterConfig: SearchFilter[] = []; - @Input() singleSelection = false; // Add this new input - searchTerm = model(''); - - @Output() filtersChange = new EventEmitter(); - @Output() search = new EventEmitter(); - - onFilterChange(changedKey: string) { - if (this.singleSelection) { - // Reset all other filters when in single selection mode - Object.keys(this.filters).forEach((key) => { - if (key !== changedKey) { - this.filters[key] = false; - } - }); - } - this.filtersChange.emit(this.filters); - this.search.emit(); - } - - onSearchTermChange() { - setTimeout(() => { - this.searchTerm.set(this.searchInput.nativeElement.value); - }, 500); - } - - clearSearch() { - this.searchInput.nativeElement.value = ''; - this.searchTerm.set(''); - } - - focusSearchInput() { - this.searchInput.nativeElement.focus(); - } -} diff --git a/libs/portal/shared/ui/src/lib/components/search-layout/search-layout.component.html b/libs/portal/shared/ui/src/lib/components/search-layout/search-layout.component.html index 55efad6b2..51bc15fe3 100644 --- a/libs/portal/shared/ui/src/lib/components/search-layout/search-layout.component.html +++ b/libs/portal/shared/ui/src/lib/components/search-layout/search-layout.component.html @@ -24,30 +24,11 @@ }} } - @if (showCloseButton()) { - - } - @if (showSearchInput()) { -
- -
- } - diff --git a/libs/portal/shared/ui/src/lib/components/search-layout/search-layout.component.scss b/libs/portal/shared/ui/src/lib/components/search-layout/search-layout.component.scss index 5d7d7600f..5dcc22b74 100644 --- a/libs/portal/shared/ui/src/lib/components/search-layout/search-layout.component.scss +++ b/libs/portal/shared/ui/src/lib/components/search-layout/search-layout.component.scss @@ -84,10 +84,6 @@ align-items: center; } } - - .search-input-container { - margin-bottom: 0; - } } .results-container { diff --git a/libs/portal/shared/ui/src/lib/components/search-layout/search-layout.component.ts b/libs/portal/shared/ui/src/lib/components/search-layout/search-layout.component.ts index da6e2d23e..9e3152459 100644 --- a/libs/portal/shared/ui/src/lib/components/search-layout/search-layout.component.ts +++ b/libs/portal/shared/ui/src/lib/components/search-layout/search-layout.component.ts @@ -6,13 +6,11 @@ import { output, viewChild, } from '@angular/core'; -import { MatIconButton } from '@angular/material/button'; import { MatIcon } from '@angular/material/icon'; import { MatProgressSpinner } from '@angular/material/progress-spinner'; import { TranslatePipe } from '@ngx-translate/core'; import { registerWorkspaceBack } from '@iptvnator/portal/shared/data-access'; import { InfiniteScrollDirective } from '../../directives/infinite-scroll.directive'; -import { SearchFormComponent } from '../search-form/search-form.component'; @Component({ selector: 'app-search-layout', @@ -20,9 +18,7 @@ import { SearchFormComponent } from '../search-form/search-form.component'; imports: [ InfiniteScrollDirective, MatIcon, - MatIconButton, MatProgressSpinner, - SearchFormComponent, TranslatePipe, ], templateUrl: './search-layout.component.html', @@ -30,7 +26,6 @@ import { SearchFormComponent } from '../search-form/search-form.component'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class SearchLayoutComponent { - private readonly searchFormComponent = viewChild(SearchFormComponent); private readonly resultsContainer = viewChild>('resultsContainer'); @@ -46,9 +41,6 @@ export class SearchLayoutComponent { /** Whether search is in progress */ readonly isLoading = input(false); - /** Whether to show the close button (for dialog mode) */ - readonly showCloseButton = input(false); - /** * Whether the page offers Back (a nested search reached via navigation). * It is the workspace header's Back, registered while the results show; @@ -59,9 +51,6 @@ export class SearchLayoutComponent { /** Whether to show results count */ readonly showResultsCount = input(true); - /** Whether to show the inline search input */ - readonly showSearchInput = input(true); - /** Whether inline details are active and should replace the search body */ readonly showDetails = input(false); @@ -105,12 +94,6 @@ export class SearchLayoutComponent { 'PORTALS.SEARCH_VIEW.INITIAL_DESCRIPTION' ); - /** Emitted when search term changes */ - readonly searchTermChange = output(); - - /** Emitted when close button is clicked */ - readonly closeClick = output(); - /** Emitted by the header's Back while `backAvailable` is set */ readonly backClick = output(); @@ -128,11 +111,6 @@ export class SearchLayoutComponent { }); } - /** Focus the search input */ - focusSearchInput(): void { - this.searchFormComponent()?.focusSearchInput(); - } - /** * Scroll handoff for hosts whose inline detail replaces the results * (`showDetails`): the container is destroyed with the detail open and @@ -146,14 +124,6 @@ export class SearchLayoutComponent { this.resultsContainer()?.nativeElement.scrollTo?.({ top: scrollTop }); } - onSearchTermChange(term: string): void { - this.searchTermChange.emit(term); - } - - onCloseClick(): void { - this.closeClick.emit(); - } - /** Check if we should show the "no results" state */ get showNoResults(): boolean { return ( diff --git a/libs/portal/shared/ui/src/lib/components/unified-collection/unified-collection-clear-action.ts b/libs/portal/shared/ui/src/lib/components/unified-collection/unified-collection-clear-action.ts index d32f8ce18..1075dc705 100644 --- a/libs/portal/shared/ui/src/lib/components/unified-collection/unified-collection-clear-action.ts +++ b/libs/portal/shared/ui/src/lib/components/unified-collection/unified-collection-clear-action.ts @@ -84,8 +84,6 @@ export function createClearCollectionAction(options: { } export interface ClearCollectionViewCommandHost { - /** Only the workspace shell renders a command palette to register into. */ - isWorkspaceLayout: boolean; mode: Signal; selectedContentType: Signal; /** Rows the command would clear; with none there is nothing to offer. */ @@ -110,7 +108,7 @@ export function setupClearCollectionViewCommand( }); effect((onCleanup) => { - if (!host.isWorkspaceLayout || host.currentTypeItems().length === 0) { + if (host.currentTypeItems().length === 0) { return; } diff --git a/libs/portal/shared/ui/src/lib/components/unified-collection/unified-collection-page.component.spec.ts b/libs/portal/shared/ui/src/lib/components/unified-collection/unified-collection-page.component.spec.ts index 7892ed0f2..efdd4c2a8 100644 --- a/libs/portal/shared/ui/src/lib/components/unified-collection/unified-collection-page.component.spec.ts +++ b/libs/portal/shared/ui/src/lib/components/unified-collection/unified-collection-page.component.spec.ts @@ -203,7 +203,6 @@ describe('UnifiedCollectionPageComponent', () => { const workspaceRoute = { snapshot: { - data: { layout: 'workspace' }, paramMap: convertToParamMap({}), params: {}, }, @@ -672,7 +671,6 @@ describe('UnifiedCollectionPageComponent', () => { it('registers a workspace command when the current view has actionable items', async () => { fixture.detectChanges(); await fixture.whenStable(); - expect(fixture.componentInstance.isWorkspaceLayout).toBe(true); fixture.componentInstance.allItems.set([ { diff --git a/libs/portal/shared/ui/src/lib/components/unified-collection/unified-collection-page.component.ts b/libs/portal/shared/ui/src/lib/components/unified-collection/unified-collection-page.component.ts index 919f42b0d..cbbbdc259 100644 --- a/libs/portal/shared/ui/src/lib/components/unified-collection/unified-collection-page.component.ts +++ b/libs/portal/shared/ui/src/lib/components/unified-collection/unified-collection-page.component.ts @@ -31,7 +31,6 @@ import { CollectionScope, FavoritesChannelSortMode, getOpenLiveCollectionItemState, - isWorkspaceLayoutRoute, isTypingInInput, LiveLayoutSidebarStateService, OPEN_LIVE_COLLECTION_ITEM_STATE_KEY, @@ -110,11 +109,8 @@ export class UnifiedCollectionPageComponent implements AfterContentInit { LiveLayoutSidebarStateService ); readonly detailTemplate = contentChild(UnifiedCollectionDetailDirective); - readonly isWorkspaceLayout = isWorkspaceLayoutRoute(this.route); - private readonly routeSearchTerm = queryParamSignal( - this.route, - 'q', - (value) => (value ?? '').trim() + readonly workspaceSearchTerm = queryParamSignal(this.route, 'q', (value) => + (value ?? '').trim() ); private readonly routePlaylistId = routeParamSignal( this.route, @@ -124,9 +120,6 @@ export class UnifiedCollectionPageComponent implements AfterContentInit { readonly playlistId = computed( () => this.playlistIdInput() ?? this.routePlaylistId() ); - readonly workspaceSearchTerm = computed(() => - this.isWorkspaceLayout ? this.routeSearchTerm() : '' - ); private readonly viewStateHistory = new CollectionViewStateHistory(); readonly isLoading = this.data.isLoading; @@ -247,7 +240,6 @@ export class UnifiedCollectionPageComponent implements AfterContentInit { constructor() { setupClearCollectionViewCommand({ - isWorkspaceLayout: this.isWorkspaceLayout, mode: this.mode, selectedContentType: this.selectedContentType, currentTypeItems: this.currentTypeItems, diff --git a/libs/portal/shared/util/src/lib/navigation/portal-route.utils.spec.ts b/libs/portal/shared/util/src/lib/navigation/portal-route.utils.spec.ts index 4ff19f141..17166b18a 100644 --- a/libs/portal/shared/util/src/lib/navigation/portal-route.utils.spec.ts +++ b/libs/portal/shared/util/src/lib/navigation/portal-route.utils.spec.ts @@ -17,7 +17,6 @@ import { BehaviorSubject } from 'rxjs'; import { extractPortalPlaylistId, extractPortalSection, - isWorkspaceLayoutRoute, queryParamSignal, routeParamSignal, resolveCurrentPortalPlaylistId, @@ -26,48 +25,6 @@ import { } from './portal-route.utils'; describe('portal-route.utils', () => { - it('detects workspace layout from route data', () => { - const route = { - snapshot: { - data: { - layout: 'workspace', - }, - }, - } as unknown as ActivatedRoute; - - expect(isWorkspaceLayoutRoute(route)).toBe(true); - }); - - it('detects workspace layout from ancestor route data', () => { - const parentRoute = { - snapshot: { - data: { - layout: 'workspace', - }, - }, - } as unknown as ActivatedRoute; - const childRoute = { - snapshot: { - data: {}, - }, - pathFromRoot: [ - { - snapshot: { - data: {}, - }, - }, - parentRoute, - { - snapshot: { - data: {}, - }, - }, - ], - } as unknown as ActivatedRoute; - - expect(isWorkspaceLayoutRoute(childRoute)).toBe(true); - }); - it('extracts Xtream sections from workspace and local URLs', () => { expect(extractPortalSection('/xtreams/123/search', 'xtreams')).toBe( 'search' diff --git a/libs/portal/shared/util/src/lib/navigation/portal-route.utils.ts b/libs/portal/shared/util/src/lib/navigation/portal-route.utils.ts index 050baee42..c679d1eb2 100644 --- a/libs/portal/shared/util/src/lib/navigation/portal-route.utils.ts +++ b/libs/portal/shared/util/src/lib/navigation/portal-route.utils.ts @@ -25,17 +25,6 @@ const PORTAL_SECTIONS = new Set([ 'vod', ]); -export function isWorkspaceLayoutRoute(route: ActivatedRoute): boolean { - const routeChain = - Array.isArray(route.pathFromRoot) && route.pathFromRoot.length > 0 - ? route.pathFromRoot - : [route]; - - return routeChain.some( - (currentRoute) => currentRoute.snapshot.data['layout'] === 'workspace' - ); -} - export function queryParamSignal( route: ActivatedRoute, key: string, diff --git a/libs/portal/stalker/feature/src/lib/stalker-search/stalker-search.component.html b/libs/portal/stalker/feature/src/lib/stalker-search/stalker-search.component.html index 45b3fe7bd..0c3db9387 100644 --- a/libs/portal/stalker/feature/src/lib/stalker-search/stalker-search.component.html +++ b/libs/portal/stalker/feature/src/lib/stalker-search/stalker-search.component.html @@ -3,14 +3,12 @@ [resultsCount]="resultsCount" [isLoading]="isInitialSearchLoading()" [showResultsCount]="true" - [showSearchInput]="!isWorkspaceLayout" - [backAvailable]="isWorkspaceLayout" + [backAvailable]="true" [showDetails]="showingDetails" [nearEndHasMore]="searchHasMore()" [nearEndAppending]="isAppendingSearchResults()" [nearEndRenderedCount]="resultsCount" [nearEndResetKey]="searchScrollResetKey()" - (searchTermChange)="updateSearchTerm($event)" (backClick)="goBack()" (nearEnd)="loadMoreSearchResults()" > diff --git a/libs/portal/stalker/feature/src/lib/stalker-search/stalker-search.component.ts b/libs/portal/stalker/feature/src/lib/stalker-search/stalker-search.component.ts index ef4c207a6..b78fdaa5c 100644 --- a/libs/portal/stalker/feature/src/lib/stalker-search/stalker-search.component.ts +++ b/libs/portal/stalker/feature/src/lib/stalker-search/stalker-search.component.ts @@ -37,7 +37,6 @@ import { StalkerStore } from '@iptvnator/portal/stalker/data-access'; import { PlaylistContextFacade } from '@iptvnator/playlist/shared/util'; import { WorkspaceBackNavigationService } from '@iptvnator/portal/shared/data-access'; import { - isWorkspaceLayoutRoute, PORTAL_EXTERNAL_PLAYBACK, PORTAL_PLAYBACK_POSITIONS, PORTAL_PLAYER, @@ -117,8 +116,6 @@ export class StalkerSearchComponent { series: false, vod: true, }); - readonly isWorkspaceLayout = isWorkspaceLayoutRoute(this.activatedRoute); - readonly filterConfig: StalkerFilter[] = [ { key: 'vod', @@ -279,10 +276,6 @@ export class StalkerSearchComponent { return this.searchResults().length; } - updateSearchTerm(term: string) { - this.searchTerm.set(term); - } - updateFilter(key: StalkerSearchContentType, value: boolean) { if (value) { // Single selection mode - set clicked filter, disable others diff --git a/libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-auto-open-queue.component.spec.ts b/libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-auto-open-queue.component.spec.ts index de016edda..15544dcc7 100644 --- a/libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-auto-open-queue.component.spec.ts +++ b/libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-auto-open-queue.component.spec.ts @@ -114,9 +114,6 @@ describe('Xtream live auto-open playback queue', () => { queryParamMap: convertToParamMap({}), }, queryParamMap: query, - pathFromRoot: [ - { snapshot: { data: { layout: 'workspace' } } }, - ], }, }, { provide: Router, useValue: { events, navigate: jest.fn() } }, diff --git a/libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.catchup-timeline.spec.ts b/libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.catchup-timeline.spec.ts index 12c69cdcf..1ca35d793 100644 --- a/libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.catchup-timeline.spec.ts +++ b/libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.catchup-timeline.spec.ts @@ -128,9 +128,6 @@ describe('LiveStreamLayoutComponent catch-up timeline segments', () => { queryParamMap: convertToParamMap({}), }, queryParamMap: of(convertToParamMap({})), - pathFromRoot: [ - { snapshot: { data: { layout: 'workspace' } } }, - ], }, }, { diff --git a/libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.component.spec.ts b/libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.component.spec.ts index c65ae9ba5..036c281a7 100644 --- a/libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.component.spec.ts +++ b/libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.component.spec.ts @@ -206,13 +206,6 @@ describe('LiveStreamLayoutComponent', () => { queryParamMap: convertToParamMap({}), }, queryParamMap: routeQueryParamMap.asObservable(), - pathFromRoot: [ - { - snapshot: { - data: { layout: 'workspace' }, - }, - }, - ], }, }, { diff --git a/libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.component.ts b/libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.component.ts index 00431fcac..e007cc59f 100644 --- a/libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.component.ts +++ b/libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.component.ts @@ -47,7 +47,6 @@ import { getAdjacentChannelItem, getChannelItemByNumber, isTypingInInput, - isWorkspaceLayoutRoute, LiveEpgPanelState, persistLiveEpgPanelState, persistPortalChannelSortMode, @@ -216,14 +215,8 @@ export class LiveStreamLayoutComponent ); readonly isElectron = this.runtime.isElectron; readonly supportsEpg = this.runtime.supportsEpg; - readonly isWorkspaceLayout = isWorkspaceLayoutRoute(this.route); - private readonly routeSearchTerm = queryParamSignal( - this.route, - 'q', - (value) => (value ?? '').trim() - ); - readonly workspaceSearchTerm = computed(() => - this.isWorkspaceLayout ? this.routeSearchTerm() : '' + readonly workspaceSearchTerm = queryParamSignal(this.route, 'q', (value) => + (value ?? '').trim() ); readonly showLiveChannelSidebar = computed( () => !!this.selectedCategoryId() || !!this.workspaceSearchTerm() diff --git a/libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.parental-lock.spec.ts b/libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.parental-lock.spec.ts index 2c10375fc..a58a2b13f 100644 --- a/libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.parental-lock.spec.ts +++ b/libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.parental-lock.spec.ts @@ -113,9 +113,6 @@ describe('LiveStreamLayoutComponent parental lock', () => { queryParamMap: convertToParamMap({}), }, queryParamMap: of(convertToParamMap({})), - pathFromRoot: [ - { snapshot: { data: { layout: 'workspace' } } }, - ], }, }, { diff --git a/libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.sidebar-levels.spec.ts b/libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.sidebar-levels.spec.ts index 26f3b060c..55690b6ca 100644 --- a/libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.sidebar-levels.spec.ts +++ b/libs/portal/xtream/feature/src/lib/live-stream-layout/live-stream-layout.sidebar-levels.spec.ts @@ -159,9 +159,6 @@ describe('LiveStreamLayoutComponent sidebar levels', () => { queryParamMap: convertToParamMap({}), }, queryParamMap: of(convertToParamMap({})), - pathFromRoot: [ - { snapshot: { data: { layout: 'workspace' } } }, - ], }, }, { diff --git a/libs/portal/xtream/feature/src/lib/search-results/search-results.component.html b/libs/portal/xtream/feature/src/lib/search-results/search-results.component.html index 24b0bcb1b..e7bc86f8f 100644 --- a/libs/portal/xtream/feature/src/lib/search-results/search-results.component.html +++ b/libs/portal/xtream/feature/src/lib/search-results/search-results.component.html @@ -2,18 +2,14 @@ [searchTerm]="searchTerm()" [resultsCount]="xtreamStore.searchResults().length" [isLoading]="xtreamStore.isSearching()" - [showCloseButton]="false" [backAvailable]="backAvailable" [showResultsCount]="!isGlobalSearch || !groupByPlaylist()" - [showSearchInput]="showInlineSearchInput" [minSearchLength]="minSearchLength" [initialDescriptionKey]="initialDescriptionKey" [nearEndHasMore]="hasMoreSearchResults()" [nearEndAppending]="isAppendingSearchResults()" [nearEndRenderedCount]="renderedSearchResultsCount()" [nearEndResetKey]="searchResetIdentity()" - (searchTermChange)="updateSearchTerm($event)" - (closeClick)="onCloseDialog()" (backClick)="goBack()" (nearEnd)="onResultsNearEnd()" > @@ -40,16 +36,6 @@ {{ 'PORTALS.SEARCH_VIEW.VISIBLE_CATEGORIES_ONLY' | translate }} } - @if (showGlobalCloseButton) { - - } diff --git a/libs/portal/xtream/feature/src/lib/search-results/search-results.component.spec.ts b/libs/portal/xtream/feature/src/lib/search-results/search-results.component.spec.ts index 0b08aacc2..5cd30c85a 100644 --- a/libs/portal/xtream/feature/src/lib/search-results/search-results.component.spec.ts +++ b/libs/portal/xtream/feature/src/lib/search-results/search-results.component.spec.ts @@ -1,6 +1,5 @@ import { signal } from '@angular/core'; import { TestBed } from '@angular/core/testing'; -import { MatDialogRef } from '@angular/material/dialog'; import { ActivatedRoute, Router, convertToParamMap } from '@angular/router'; import { of } from 'rxjs'; import { DatabaseService } from '@iptvnator/services'; @@ -17,10 +16,6 @@ jest.mock('@iptvnator/portal/shared/ui', () => ({ SearchLayoutComponent: class {}, })); -/** The component is opened as a route, not a dialog: `@Optional()` resolves nothing. */ -const NO_DIALOG_REF = - undefined as unknown as MatDialogRef; - const DEFAULT_SEARCH_FILTERS: SearchFilters = { live: true, movie: true, @@ -78,7 +73,7 @@ class MockXtreamStore { setSelectedContentType = jest.fn(); } -describe('SearchResultsComponent initialQuery contract', () => { +describe('SearchResultsComponent global search', () => { let routerNavigateMock: jest.Mock; beforeEach(() => { @@ -102,7 +97,6 @@ describe('SearchResultsComponent initialQuery contract', () => { snapshot: { data: { isGlobalSearch: true, - layout: 'workspace', }, queryParamMap: convertToParamMap({ q: '' }), }, @@ -119,60 +113,24 @@ describe('SearchResultsComponent initialQuery contract', () => { }); }); - it('applies initialQuery when opened as global search', () => { - const component = TestBed.runInInjectionContext( - () => - new SearchResultsComponent( - { - isGlobalSearch: true, - initialQuery: 'matrix', - }, - NO_DIALOG_REF - ) - ); - - const store = TestBed.inject(XtreamStore) as unknown as MockXtreamStore; - expect(store.setSearchTerm).toHaveBeenCalledWith('matrix'); - expect(component.searchTerm()).toBe('matrix'); - }); - - it('does not force search term when initialQuery is not provided', () => { - TestBed.runInInjectionContext( - () => - new SearchResultsComponent( - { - isGlobalSearch: true, - }, - NO_DIALOG_REF - ) - ); - - const store = TestBed.inject(XtreamStore) as unknown as MockXtreamStore; - expect(store.setSearchTerm).not.toHaveBeenCalled(); - }); - it('clears the workspace global search term when the route q param is empty', () => { const store = TestBed.inject(XtreamStore) as unknown as MockXtreamStore; store.searchTerm.set('matrix'); const component = TestBed.runInInjectionContext( - () => new SearchResultsComponent(null, NO_DIALOG_REF) + () => new SearchResultsComponent() ); TestBed.flushEffects(); expect(store.setSearchTerm).toHaveBeenCalledWith(''); expect(component.searchTerm()).toBe(''); + expect(component.isGlobalSearch).toBe(true); + expect(component.backAvailable).toBe(false); }); it('navigates global search selection to workspace xtream route', () => { const component = TestBed.runInInjectionContext( - () => - new SearchResultsComponent( - { - isGlobalSearch: true, - }, - NO_DIALOG_REF - ) + () => new SearchResultsComponent() ); component.selectItem( @@ -191,13 +149,7 @@ describe('SearchResultsComponent initialQuery contract', () => { it('navigates M3U global search selection to the M3U player with route state', () => { const component = TestBed.runInInjectionContext( - () => - new SearchResultsComponent( - { - isGlobalSearch: true, - }, - NO_DIALOG_REF - ) + () => new SearchResultsComponent() ); const channel = { id: 'channel-news', @@ -262,13 +214,7 @@ describe('SearchResultsComponent initialQuery contract', () => { .mockResolvedValueOnce(freshResults); const component = TestBed.runInInjectionContext( - () => - new SearchResultsComponent( - { - isGlobalSearch: true, - }, - NO_DIALOG_REF - ) + () => new SearchResultsComponent() ); const store = TestBed.inject(XtreamStore) as unknown as MockXtreamStore; @@ -309,13 +255,7 @@ describe('SearchResultsComponent initialQuery contract', () => { .mockResolvedValueOnce(secondPage); const component = TestBed.runInInjectionContext( - () => - new SearchResultsComponent( - { - isGlobalSearch: true, - }, - NO_DIALOG_REF - ) + () => new SearchResultsComponent() ); const store = TestBed.inject(XtreamStore) as unknown as MockXtreamStore; @@ -362,13 +302,7 @@ describe('SearchResultsComponent initialQuery contract', () => { .mockReturnValueOnce(freshSearch.promise); const component = TestBed.runInInjectionContext( - () => - new SearchResultsComponent( - { - isGlobalSearch: true, - }, - NO_DIALOG_REF - ) + () => new SearchResultsComponent() ); const store = TestBed.inject(XtreamStore) as unknown as MockXtreamStore; store.searchResults.set([createSearchItem()]); @@ -409,13 +343,7 @@ describe('SearchResultsComponent initialQuery contract', () => { databaseService.globalSearchContent.mockResolvedValueOnce(nextPage); const component = TestBed.runInInjectionContext( - () => - new SearchResultsComponent( - { - isGlobalSearch: true, - }, - NO_DIALOG_REF - ) + () => new SearchResultsComponent() ); const store = TestBed.inject(XtreamStore) as unknown as MockXtreamStore; store.searchResults.set([createSearchItem({ id: 1, xtream_id: 1 })]); @@ -465,13 +393,7 @@ describe('SearchResultsComponent initialQuery contract', () => { databaseService.globalSearchContent.mockResolvedValueOnce(firstPage); const component = TestBed.runInInjectionContext( - () => - new SearchResultsComponent( - { - isGlobalSearch: true, - }, - NO_DIALOG_REF - ) + () => new SearchResultsComponent() ); const store = TestBed.inject(XtreamStore) as unknown as MockXtreamStore; @@ -505,7 +427,7 @@ describe('SearchResultsComponent in-portal result window', () => { provide: ActivatedRoute, useValue: { snapshot: { - data: { layout: 'workspace' }, + data: {}, queryParamMap: convertToParamMap({ q: '' }), }, queryParamMap: of(convertToParamMap({ q: '' })), @@ -524,7 +446,7 @@ describe('SearchResultsComponent in-portal result window', () => { it('windows the full result set, reveals further chunks, and resets on a new result set', () => { const store = TestBed.inject(XtreamStore) as unknown as MockXtreamStore; const component = TestBed.runInInjectionContext( - () => new SearchResultsComponent(null, NO_DIALOG_REF) + () => new SearchResultsComponent() ); const items = Array.from({ length: 130 }, (_, index) => createSearchItem({ @@ -536,6 +458,7 @@ describe('SearchResultsComponent in-portal result window', () => { store.searchResults.set(items); expect(component.isGlobalSearch).toBe(false); + expect(component.backAvailable).toBe(true); expect(component.visibleInPortalResults()).toHaveLength(60); expect(component.renderedSearchResultsCount()).toBe(60); expect(component.hasMoreSearchResults()).toBe(true); @@ -563,7 +486,7 @@ describe('SearchResultsComponent in-portal result window', () => { it('changes the reset identity on filter-only transitions', () => { const store = TestBed.inject(XtreamStore) as unknown as MockXtreamStore; const component = TestBed.runInInjectionContext( - () => new SearchResultsComponent(null, NO_DIALOG_REF) + () => new SearchResultsComponent() ); const initialIdentity = component.searchResetIdentity(); diff --git a/libs/portal/xtream/feature/src/lib/search-results/search-results.component.ts b/libs/portal/xtream/feature/src/lib/search-results/search-results.component.ts index 737eee026..23299dfe5 100644 --- a/libs/portal/xtream/feature/src/lib/search-results/search-results.component.ts +++ b/libs/portal/xtream/feature/src/lib/search-results/search-results.component.ts @@ -5,20 +5,11 @@ import { computed, effect, inject, - Inject, linkedSignal, - Optional, signal, - viewChild, } from '@angular/core'; import { FormsModule } from '@angular/forms'; -import { MatIconButton } from '@angular/material/button'; import { MatCheckboxModule } from '@angular/material/checkbox'; -import { - MAT_DIALOG_DATA, - MatDialogModule, - MatDialogRef, -} from '@angular/material/dialog'; import { MatIcon } from '@angular/material/icon'; import { MatProgressSpinner } from '@angular/material/progress-spinner'; import { ActivatedRoute, Router } from '@angular/router'; @@ -29,7 +20,6 @@ import { SearchLayoutComponent } from '@iptvnator/portal/shared/ui'; import { WorkspaceBackNavigationService } from '@iptvnator/portal/shared/data-access'; import { buildXtreamNavigationTarget, - isWorkspaceLayoutRoute, queryParamSignal, workspacePortalCommands, } from '@iptvnator/portal/shared/util'; @@ -45,11 +35,6 @@ import { isM3uGlobalSearchResult, } from '@iptvnator/shared/interfaces'; -interface SearchResultsData { - isGlobalSearch: boolean; - initialQuery?: string; -} - interface GlobalSearchResultGroup { playlistId: string; playlistName: string; @@ -92,9 +77,7 @@ function groupResultsByPlaylistId( ContentCardComponent, FormsModule, MatCheckboxModule, - MatDialogModule, MatIcon, - MatIconButton, MatProgressSpinner, SearchLayoutComponent, TranslatePipe, @@ -104,14 +87,12 @@ function groupResultsByPlaylistId( styleUrls: ['./search-results.component.scss'], }) export class SearchResultsComponent implements AfterViewInit { - readonly searchLayoutComponent = viewChild(SearchLayoutComponent); readonly xtreamStore = inject(XtreamStore); readonly router = inject(Router); readonly activatedRoute = inject(ActivatedRoute); readonly databaseService = inject(DatabaseService); private readonly backNavigation = inject(WorkspaceBackNavigationService); private readonly logger = createLogger('XtreamSearchResults'); - readonly isWorkspaceLayout = isWorkspaceLayoutRoute(this.activatedRoute); readonly routeSearchTerm = queryParamSignal( this.activatedRoute, 'q', @@ -137,7 +118,8 @@ export class SearchResultsComponent implements AfterViewInit { private static readonly TYPE_FILTERS_STORAGE_KEY = 'global-search-type-filters'; - isGlobalSearch = false; + readonly isGlobalSearch = + this.activatedRoute.snapshot.data?.['isGlobalSearch'] === true; /** Whether to group global search results by playlist */ readonly groupByPlaylist = signal( @@ -246,15 +228,7 @@ export class SearchResultsComponent implements AfterViewInit { } } - constructor( - @Optional() @Inject(MAT_DIALOG_DATA) data: SearchResultsData | null, - @Optional() public dialogRef: MatDialogRef - ) { - this.isGlobalSearch = - data?.isGlobalSearch || - this.activatedRoute.snapshot.data?.['isGlobalSearch'] === true; - const initialQuery = (data?.initialQuery ?? '').trim(); - + constructor() { if (this.isGlobalSearch) { const savedFilters = localStorage.getItem( SearchResultsComponent.TYPE_FILTERS_STORAGE_KEY @@ -273,10 +247,6 @@ export class SearchResultsComponent implements AfterViewInit { // Ignore malformed storage value and keep defaults. } } - - if (initialQuery) { - this.xtreamStore.setSearchTerm(initialQuery); - } } effect((onCleanup) => { @@ -290,10 +260,6 @@ export class SearchResultsComponent implements AfterViewInit { }); effect(() => { - if (!this.isWorkspaceLayout) { - return; - } - const queryTerm = this.routeSearchTerm(); if (queryTerm === this.searchTerm()) { return; @@ -305,11 +271,6 @@ export class SearchResultsComponent implements AfterViewInit { ngAfterViewInit() { this.xtreamStore.setSelectedContentType('vod'); - if (this.showInlineSearchInput) { - setTimeout(() => { - this.searchLayoutComponent()?.focusSearchInput(); - }); - } } executeSearch() { @@ -330,13 +291,6 @@ export class SearchResultsComponent implements AfterViewInit { } } - /** - * Update search term in the store - */ - updateSearchTerm(term: string) { - this.xtreamStore.setSearchTerm(term); - } - /** * Update a single filter in the store */ @@ -526,19 +480,11 @@ export class SearchResultsComponent implements AfterViewInit { imageUrl: item.poster_url, }); - if (this.isGlobalSearch) { - this.dialogRef?.close(); - } - void this.router.navigate(navigation.link, { state: navigation.state, }); } - onCloseDialog() { - this.dialogRef?.close(); - } - /** History Back; the portal's default section when it opened the session. */ goBack(): void { this.backNavigation.back(() => @@ -569,25 +515,13 @@ export class SearchResultsComponent implements AfterViewInit { return this.groupedResults(); } - get showInlineSearchInput(): boolean { - return !this.isWorkspaceLayout; - } - - get showGlobalCloseButton(): boolean { - return this.isGlobalSearch && !this.isWorkspaceLayout; - } - /** * The in-portal search is a nested view (reached from the toolbar * search box or an actor page), so it offers the header's Back; the - * global search is a top-level sidebar destination, and the dialog has - * its own close button — a Back there would navigate the page behind - * the open dialog. + * global search is a top-level sidebar destination. */ get backAvailable(): boolean { - return ( - this.isWorkspaceLayout && !this.isGlobalSearch && !this.dialogRef - ); + return !this.isGlobalSearch; } get minSearchLength(): number { @@ -618,10 +552,6 @@ export class SearchResultsComponent implements AfterViewInit { return; } - if (this.isGlobalSearch) { - this.dialogRef?.close(); - } - void this.router.navigate( ['/workspace', 'playlists', item.playlist_id, 'all'], { diff --git a/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.spec.ts b/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.spec.ts index 83f4d2096..adedc6619 100644 --- a/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.spec.ts +++ b/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.spec.ts @@ -106,7 +106,6 @@ describe('ChannelListContainerComponent', () => { const route = { snapshot: { - data: { layout: 'workspace' }, paramMap: convertToParamMap({}), queryParamMap: convertToParamMap({}), params: {}, @@ -115,7 +114,6 @@ describe('ChannelListContainerComponent', () => { pathFromRoot: [ { snapshot: { - data: { layout: 'workspace' }, paramMap: convertToParamMap({}), params: {}, }, diff --git a/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.ts b/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.ts index 8e94db1f7..f02b7efb2 100644 --- a/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.ts +++ b/libs/ui/components/src/lib/channel-list-container/channel-list-container.component.ts @@ -22,10 +22,7 @@ import { MatSnackBar } from '@angular/material/snack-bar'; import { TranslatePipe, TranslateService } from '@ngx-translate/core'; import { EpgService } from '@iptvnator/epg/data-access'; import { PlaylistContextFacade } from '@iptvnator/playlist/shared/util'; -import { - isWorkspaceLayoutRoute, - queryParamSignal, -} from '@iptvnator/portal/shared/util'; +import { queryParamSignal } from '@iptvnator/portal/shared/util'; import { ChannelActions, FavoritesActions, @@ -219,7 +216,6 @@ export class ChannelListContainerComponent implements OnInit, OnDestroy { readonly sidebarToggleRequested = output(); /** Groups view only: the group whose channels are currently listed. */ readonly selectedGroupChange = output(); - readonly isWorkspaceLayout = isWorkspaceLayoutRoute(this.route); private readonly routeSearchTerm = queryParamSignal( this.route, 'q', @@ -230,7 +226,7 @@ export class ChannelListContainerComponent implements OnInit, OnDestroy { if (hostTerm !== null) { return foldSearchText(hostTerm.trim()); } - return this.isWorkspaceLayout ? this.routeSearchTerm() : ''; + return this.routeSearchTerm(); }); readonly currentUrl = toSignal( diff --git a/libs/ui/playback/src/lib/video-player/sidebar/sidebar.component.html b/libs/ui/playback/src/lib/video-player/sidebar/sidebar.component.html index 6707bb45e..5259b84b0 100644 --- a/libs/ui/playback/src/lib/video-player/sidebar/sidebar.component.html +++ b/libs/ui/playback/src/lib/video-player/sidebar/sidebar.component.html @@ -1,23 +1,3 @@ -@if (showPlaylistHeader()) { -
- - - -
-} - { - it('re-words the channel count after a runtime language switch', () => { - TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot(), SidebarComponent], - providers: [ - { - provide: PlaylistContextFacade, - useValue: { activePlaylist: signal(null) }, - }, - ], - }).overrideComponent(SidebarComponent, { - set: { template: '', imports: [] }, - }); - const translate = TestBed.inject(TranslateService); - translate.setTranslation('en', { - HOME: { PLAYLISTS: { CHANNELS: 'channels' } }, - }); - translate.setTranslation('ru', { - HOME: { PLAYLISTS: { CHANNELS: 'каналов' } }, - }); - translate.use('en'); - const fixture = TestBed.createComponent(SidebarComponent); - fixture.componentRef.setInput('channels', [{}, {}]); - - expect(fixture.componentInstance.subtitle()).toBe('2 channels'); - - translate.use('ru'); - - expect(fixture.componentInstance.subtitle()).toBe('2 каналов'); - }); -}); diff --git a/libs/ui/playback/src/lib/video-player/sidebar/sidebar.component.ts b/libs/ui/playback/src/lib/video-player/sidebar/sidebar.component.ts index a1f0eaa2e..82044b18a 100644 --- a/libs/ui/playback/src/lib/video-player/sidebar/sidebar.component.ts +++ b/libs/ui/playback/src/lib/video-player/sidebar/sidebar.component.ts @@ -1,18 +1,10 @@ import { Component, - computed, inject, input, output, ChangeDetectionStrategy, } from '@angular/core'; -import { MatIconButton } from '@angular/material/button'; -import { MatIcon } from '@angular/material/icon'; -import { MatTooltip } from '@angular/material/tooltip'; -import { RouterLink } from '@angular/router'; -import { TranslatePipe, TranslateService } from '@ngx-translate/core'; -import { injectTranslationTick } from '@iptvnator/services'; -import { PlaylistSwitcherComponent } from '@iptvnator/playlist/shared/ui'; import { PlaylistContextFacade } from '@iptvnator/playlist/shared/util'; import { Channel } from '@iptvnator/shared/interfaces'; import { ChannelListContainerComponent } from '@iptvnator/ui/components'; @@ -20,22 +12,12 @@ import { ChannelListContainerComponent } from '@iptvnator/ui/components'; @Component({ selector: 'app-sidebar', templateUrl: './sidebar.component.html', - styleUrls: ['./sidebar.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, - imports: [ - ChannelListContainerComponent, - MatIcon, - MatIconButton, - MatTooltip, - PlaylistSwitcherComponent, - RouterLink, - TranslatePipe, - ], + imports: [ChannelListContainerComponent], }) export class SidebarComponent { readonly channels = input([]); readonly channelsLoading = input(false); - readonly showPlaylistHeader = input(true); readonly activeView = input('all'); readonly sidebarWidth = input(null); readonly sidebarWidthRequested = output(); @@ -45,32 +27,8 @@ export class SidebarComponent { readonly selectedGroupChange = output(); private readonly playlistContext = inject(PlaylistContextFacade); - private readonly translate = inject(TranslateService); - private readonly languageTick = injectTranslationTick(); readonly activePlaylist = this.playlistContext.activePlaylist; - readonly playlistTitle = computed(() => { - const playlist = this.activePlaylist(); - - return ( - playlist?.title || - playlist?.filename || - playlist?.url || - playlist?.portalUrl || - 'Untitled playlist' - ); - }); - - /** Worded per language: the tick re-runs this after a switch. */ - readonly subtitle = computed(() => { - this.languageTick(); - if (this.channelsLoading()) { - return this.translate.instant('CHANNELS.LOADING'); - } - - const count = this.channels()?.length ?? 0; - return `${count} ${this.translate.instant('HOME.PLAYLISTS.CHANNELS')}`; - }); onSidebarWidthRequested(width: number): void { this.sidebarWidthRequested.emit(width); diff --git a/libs/ui/styles/_portal-sidebar.scss b/libs/ui/styles/_portal-sidebar.scss index aedd5c499..98da1c56f 100644 --- a/libs/ui/styles/_portal-sidebar.scss +++ b/libs/ui/styles/_portal-sidebar.scss @@ -42,11 +42,6 @@ app-region: no-drag; } - .switcher { - flex: 1; - min-width: 0; - } - .manage-categories-btn { margin-left: auto; }