fix(portals): hydrate the offline catalog and hide results while matching

- `toCachedContentScope` returned null for the actor and discover routes,
  so an expired, inactive or offline portal skipped hydration entirely
  and both pages answered "not in your library" from an empty catalog
  even though a full imported catalog sat in SQLite. Both map to the
  aggregate `search` scope now — neither reads a single content type.

- The results grid stayed rendered under the matching spinner. Until the
  matches land every card reads as unavailable, so a click during the
  worker request opened the portal search for a title the next tick would
  have resolved in another playlist. The grid is hidden while matching.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5 committed 2026-08-16 09:13:28 +02:00
1 parent 8a7a7ba8b0
commit 11744dd389
2 files changed
+78 -65

No files matched your search

@@ -92,7 +92,7 @@ function toContentInitBlockReason(
}
function toCachedContentScope(
section: PortalRailSection | null
section: string | null
): XtreamCachedContentScope | null {
switch (section) {
case 'live':
@@ -101,6 +101,13 @@ function toCachedContentScope(
case 'search':
case 'recently-added':
return section;
// Neither reads one content type: both match TMDB titles against
// the whole catalog, so they take the aggregate scope search uses.
// Without it an expired or offline portal skips hydration and the
// page answers "not in your library" from an empty catalog.
case 'actor':
case 'discover':
return 'search';
default:
return null;
}
@@ -59,69 +59,75 @@
<mat-icon>search_off</mat-icon>
<p>{{ emptyMessageKey() | translate }}</p>
</div>
} @else {
<!-- Hidden while matching on purpose: until the matches land every
card still reads as unavailable, and clicking one would open
the portal search for a title the next tick will resolve. -->
<div class="title-results__grid">
@for (
item of visibleItems();
track item.mediaType + '-' + item.tmdbId
) {
<button
type="button"
class="title-results__card"
[class.title-results__card--unavailable]="!item.available"
[title]="
item.character
? item.title + ' — ' + item.character
: item.title
"
(click)="itemClicked.emit(item)"
>
@if (item.posterUrl) {
<img
class="title-results__poster"
[src]="item.posterUrl"
[alt]="item.title"
loading="lazy"
/>
} @else {
<span
class="title-results__poster title-results__poster--fallback"
>
{{ item.title.charAt(0) }}
</span>
}
@if (item.available && item.availableIn) {
<span class="title-results__badge">
<mat-icon>check_circle</mat-icon>
{{ item.availableIn }}
</span>
} @else if (
item.available && filterMode() !== 'available'
) {
<span class="title-results__badge">
<mat-icon>check_circle</mat-icon>
{{ 'XTREAM.ACTOR_IN_LIBRARY' | translate }}
</span>
} @else if (!item.available) {
<span
class="title-results__badge title-results__badge--search"
>
<mat-icon>search</mat-icon>
{{ 'XTREAM.ACTOR_SEARCH_IN_PORTAL' | translate }}
</span>
}
<span class="title-results__card-title">
{{ item.title }}
</span>
<span class="title-results__card-sub">
@if (item.year) {
{{ item.year }}
}
@if (item.character) {
· {{ item.character }}
} @else if (item.crewJob) {
· {{ crewJobKey(item.crewJob) | translate }}
}
</span>
</button>
}
</div>
}
<div class="title-results__grid">
@for (
item of visibleItems();
track item.mediaType + '-' + item.tmdbId
) {
<button
type="button"
class="title-results__card"
[class.title-results__card--unavailable]="!item.available"
[title]="
item.character
? item.title + ' — ' + item.character
: item.title
"
(click)="itemClicked.emit(item)"
>
@if (item.posterUrl) {
<img
class="title-results__poster"
[src]="item.posterUrl"
[alt]="item.title"
loading="lazy"
/>
} @else {
<span
class="title-results__poster title-results__poster--fallback"
>
{{ item.title.charAt(0) }}
</span>
}
@if (item.available && item.availableIn) {
<span class="title-results__badge">
<mat-icon>check_circle</mat-icon>
{{ item.availableIn }}
</span>
} @else if (item.available && filterMode() !== 'available') {
<span class="title-results__badge">
<mat-icon>check_circle</mat-icon>
{{ 'XTREAM.ACTOR_IN_LIBRARY' | translate }}
</span>
} @else if (!item.available) {
<span
class="title-results__badge title-results__badge--search"
>
<mat-icon>search</mat-icon>
{{ 'XTREAM.ACTOR_SEARCH_IN_PORTAL' | translate }}
</span>
}
<span class="title-results__card-title">
{{ item.title }}
</span>
<span class="title-results__card-sub">
@if (item.year) {
{{ item.year }}
}
@if (item.character) {
· {{ item.character }}
} @else if (item.crewJob) {
· {{ crewJobKey(item.crewJob) | translate }}
}
</span>
</button>
}
</div>
</section>