Files
iptvnator/libs/ui/components/src/lib/vod-sources/vod-sources-chip.component.scss
T
4grayandClaude Fable 5 0010dd7351 feat(portals): rework the VOD sources popup and action row (#1359)
* feat(portals): rework the VOD sources popup and action row

The Sources popup now anchors above its button and always fits on screen:
the overlay caps it to the space beside the chip, only the source list
scrolls so the header, search, filter chips and footer stay visible, and
it flips below the button when the space above is too small. Filter chips
(All / Available / HD+ / language) compose with the playlist search, and
"Available" runs check-all itself when nothing has been checked yet.

Expanded copy rows no longer repeat the playlist domain: each copy shows
its parsed language chip, the provider's raw stream title, and only the
tags that differ from the parent copy.

Availability checks run at most four at a time and settled verdicts are
remembered per movie and source for ten minutes, so reopening a movie no
longer re-contacts every foreign portal.

Favorites and Download become icon-only buttons with real state: a filled
heart when favorited, and a download icon that turns into a progress ring
and then a checkmark that reveals the finished file.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(portals): match the movie detail's icon-only favorite button

The Xtream movie detail's favorite control is now an icon-only button, so
the shared "add this detail to favorites" helpers no longer found it by
class. They are used against series and Stalker details too, which still
render the labeled variant, so they now select by accessible name — the
icon button carries the same label in aria-label.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(portals): keep provider playback for a downloaded movie

Once a movie is downloaded the primary button plays the local file, and
the sources popover only exists when another playlist carries the same
film. The icon-only rework left those as the only two paths, so a
downloaded movie in a single-playlist library had no way at all to stream
the provider's copy — the labeled action that used to do it was gone.

Restores it as an icon button beside the downloaded checkmark, under the
same condition the old one used.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 22:27:24 +02:00

135 lines
4.0 KiB
SCSS

$font-mono: var(--font-mono, ui-monospace, 'SF Mono', Menlo, monospace);
:host {
display: inline-block;
}
.sources-chip {
// Layout plus a full reset of the UA button appearance. The surface itself
// is supplied by whichever bar hosts the chip, because "looks like its
// neighbours" means something different in the hero than in the player.
// Without the reset, a host that sets no background gets Chromium's grey
// `buttonface` default instead of nothing.
appearance: none;
background: transparent;
border: 0;
color: inherit;
padding: 0;
display: inline-flex;
align-items: center;
gap: 8px;
box-sizing: border-box;
cursor: pointer;
font: inherit;
// Tactile press, matching the rest of the action row.
&:active {
transform: scale(0.985);
}
}
// Pressed while the popover is open, so the button and the panel read as one
// control. Doubled class beats the host surface rule the hero applies.
.sources-chip.sources-chip--open {
background: color-mix(in srgb, var(--app-selection-color) 16%, transparent);
border-color: var(--app-selection-color);
color: var(--app-selection-color);
.sources-chip__icon,
.sources-chip__caret {
color: inherit;
}
}
.sources-chip__icon {
font-size: 18px;
width: 18px;
height: 18px;
line-height: 18px;
color: var(--app-eyebrow-color);
}
.sources-chip__count {
font-family: $font-mono;
font-weight: 700;
font-size: 11px;
line-height: 1.3;
border-radius: 999px;
padding: 2px 8px;
color: var(--app-selection-color);
background: color-mix(in srgb, var(--app-selection-color) 12%, transparent);
}
.sources-chip__caret {
font-size: 18px;
width: 18px;
height: 18px;
line-height: 18px;
color: var(--app-eyebrow-color);
transition: transform 0.14s ease-out;
&.is-open {
transform: rotate(180deg);
}
}
// The popover surface. Width lives here, on an element we fully own, instead
// of being fought out with Material's 280px menu cap.
.sources-chip__panel {
position: relative;
display: flex;
flex-direction: column;
// The overlay's flexible bounding box is the height authority; the panel
// takes what it grants and the menu's list scrolls inside it.
max-height: 100%;
min-height: 0;
width: 660px;
max-width: calc(100vw - 32px);
box-shadow:
0 24px 70px rgba(0, 0, 0, 0.45),
0 4px 18px rgba(0, 0, 0, 0.3);
border-radius: 14px;
// Opaque here as well as on the inner surface: a CDK overlay floats over
// arbitrary page content, so a missing background is not a cosmetic bug —
// it makes the whole panel unreadable.
background: var(--app-widget-bg, #161a22);
}
// The pane carries these classes (per-position `panelClass`), so they must
// escape this component's scoping. The overlay's `minHeight` exists for the
// FLIP calculation — as an inline style on the pane it would also stretch a
// short panel, so it is neutralized visually.
::ng-deep .vod-sources-pop--above,
::ng-deep .vod-sources-pop--below {
min-height: 0 !important;
}
// Pointer tail: an 11px rotated square on the edge facing the chip, wearing
// the panel's own surface and border so it reads as part of it.
::ng-deep .vod-sources-pop--above .sources-chip__panel::after,
::ng-deep .vod-sources-pop--below .sources-chip__panel::after {
content: '';
position: absolute;
right: 60px;
width: 11px;
height: 11px;
background: var(--app-widget-bg, #161a22);
transform: rotate(45deg);
pointer-events: none;
}
::ng-deep .vod-sources-pop--above .sources-chip__panel::after {
bottom: -6px;
border-right: 1px solid var(--app-widget-border, rgba(255, 255, 255, 0.1));
border-bottom: 1px solid var(--app-widget-border, rgba(255, 255, 255, 0.1));
}
::ng-deep .vod-sources-pop--below .sources-chip__panel::after {
top: -6px;
border-left: 1px solid var(--app-widget-border, rgba(255, 255, 255, 0.1));
border-top: 1px solid var(--app-widget-border, rgba(255, 255, 255, 0.1));
}