mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-14 21:26:15 -08:00
style: enhance grid list component with loading skeletons and type safety
- Updated GridListComponent to use a skeleton loading UI during data fetching. - Introduced GridListItem interface for better type safety on items input. - Improved paginator integration with proper type definitions. - Refactored loading state handling to utilize computed properties. feat: Improve portal rail links with accessibility features - Added aria-current attribute to indicate active links. - Enhanced styling for active links with new CSS variables for selection colors. style: Refactor styles for portal rail links and context panels - Updated SCSS for portal rail links to improve hover and active states. - Introduced new styles for context skeletons in workspace context panel. feat: Implement skeleton loaders in stalker main container and workspace context panel - Replaced ngx-skeleton-loader with custom skeleton components for better control. - Added skeleton loading states for categories in stalker main container. style: Refine recently added component styles and interactions - Enhanced section styling with active state indicators and transitions. - Improved layout and visual feedback for recently added items. fix: Correctly handle section activation in recently added component - Added logic to determine active sections based on selected content type. refactor: Update XtreamShellComponent to improve import overlay handling - Changed isImporting check to showImportOverlay for better clarity. - Refactored section handling to use signals for state management. style: Introduce new selection colors and effects across the application - Added new CSS variables for selection colors and effects to enhance UI consistency. - Updated various components to utilize new selection styles for better user experience.
This commit is contained in:
1 parent
ad3f6fc206
commit
aa6a753aa5
27 files changed
+719
-122
No files matched your search
@@ -14,7 +14,11 @@
|
||||
novalidate
|
||||
>
|
||||
<div class="settings-content">
|
||||
<section class="settings-group" id="general">
|
||||
<section
|
||||
class="settings-group"
|
||||
[class.settings-group--active]="activeSection() === 'general'"
|
||||
id="general"
|
||||
>
|
||||
<header class="settings-group__header">
|
||||
<div class="settings-group__header-icon">
|
||||
<mat-icon>settings</mat-icon>
|
||||
@@ -105,7 +109,11 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="settings-group" id="playback">
|
||||
<section
|
||||
class="settings-group"
|
||||
[class.settings-group--active]="activeSection() === 'playback'"
|
||||
id="playback"
|
||||
>
|
||||
<header class="settings-group__header">
|
||||
<div class="settings-group__header-icon">
|
||||
<mat-icon>play_circle</mat-icon>
|
||||
@@ -269,7 +277,11 @@
|
||||
</section>
|
||||
|
||||
@if (isDesktop) {
|
||||
<section class="settings-group" id="epg">
|
||||
<section
|
||||
class="settings-group"
|
||||
[class.settings-group--active]="activeSection() === 'epg'"
|
||||
id="epg"
|
||||
>
|
||||
<header class="settings-group__header">
|
||||
<div class="settings-group__header-icon">
|
||||
<mat-icon>list_alt</mat-icon>
|
||||
@@ -369,7 +381,13 @@
|
||||
}
|
||||
|
||||
@if (isDesktop) {
|
||||
<section class="settings-group" id="remote-control">
|
||||
<section
|
||||
class="settings-group"
|
||||
[class.settings-group--active]="
|
||||
activeSection() === 'remote-control'
|
||||
"
|
||||
id="remote-control"
|
||||
>
|
||||
<header class="settings-group__header">
|
||||
<div class="settings-group__header-icon">
|
||||
<mat-icon>settings_remote</mat-icon>
|
||||
@@ -546,7 +564,11 @@
|
||||
</section>
|
||||
}
|
||||
|
||||
<section class="settings-group" id="data">
|
||||
<section
|
||||
class="settings-group"
|
||||
[class.settings-group--active]="activeSection() === 'data'"
|
||||
id="data"
|
||||
>
|
||||
<header class="settings-group__header">
|
||||
<div class="settings-group__header-icon">
|
||||
<mat-icon>sync_alt</mat-icon>
|
||||
@@ -631,7 +653,11 @@
|
||||
}
|
||||
</section>
|
||||
|
||||
<section class="settings-group" id="about">
|
||||
<section
|
||||
class="settings-group"
|
||||
[class.settings-group--active]="activeSection() === 'about'"
|
||||
id="about"
|
||||
>
|
||||
<header class="settings-group__header">
|
||||
<div class="settings-group__header-icon">
|
||||
<mat-icon>info</mat-icon>
|
||||
|
||||
@@ -33,17 +33,45 @@
|
||||
}
|
||||
|
||||
.settings-group {
|
||||
position: relative;
|
||||
background: transparent;
|
||||
overflow: hidden;
|
||||
scroll-margin-top: 24px;
|
||||
padding-bottom: 24px;
|
||||
border-bottom: 1px solid var(--mat-sys-outline-variant);
|
||||
border-radius: 18px;
|
||||
transition:
|
||||
background 0.2s ease,
|
||||
box-shadow 0.2s ease,
|
||||
border-color 0.2s ease;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.settings-group--active {
|
||||
background: linear-gradient(
|
||||
145deg,
|
||||
var(--app-selection-surface),
|
||||
color-mix(in srgb, var(--app-selection-color) 6%, transparent)
|
||||
);
|
||||
border-color: var(--app-selection-border);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px var(--app-selection-border),
|
||||
0 16px 28px -24px var(--app-selection-glow);
|
||||
|
||||
.settings-group__header-icon {
|
||||
background: var(--app-selection-surface-strong);
|
||||
color: var(--app-selection-color);
|
||||
box-shadow: inset 0 0 0 1px var(--app-selection-border);
|
||||
}
|
||||
|
||||
.settings-group__header-text h3 {
|
||||
color: var(--app-selection-color);
|
||||
}
|
||||
}
|
||||
|
||||
.settings-group__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -25,12 +25,8 @@
|
||||
<div class="main-content">
|
||||
<div class="content-header">
|
||||
<h2>
|
||||
{{
|
||||
(titleTranslationString() | translate) +
|
||||
' (' +
|
||||
selectedCategoryId() +
|
||||
')'
|
||||
}}
|
||||
{{ titleTranslationString() | translate }}
|
||||
<span class="selected-category-pill">{{ selectedCategoryId() }}</span>
|
||||
</h2>
|
||||
@if (showHeaderAction() && !isWorkspaceLayout) {
|
||||
<button
|
||||
|
||||
@@ -33,6 +33,24 @@
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.selected-category-pill {
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 0.76rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.02em;
|
||||
text-transform: uppercase;
|
||||
color: var(--app-selection-color);
|
||||
background: linear-gradient(
|
||||
145deg,
|
||||
var(--app-selection-surface-strong),
|
||||
var(--app-selection-surface)
|
||||
);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px var(--app-selection-border),
|
||||
0 10px 16px -18px var(--app-selection-glow);
|
||||
}
|
||||
}
|
||||
|
||||
.scrollable-content {
|
||||
|
||||
@@ -17,7 +17,6 @@ mat-card:hover {
|
||||
|
||||
// ─── Grid layout ─────────────────────────────────────────────────────────────
|
||||
.grid {
|
||||
|
||||
mat-card {
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
@@ -108,3 +107,97 @@ mat-card:hover {
|
||||
width: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.grid-skeleton-card {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.grid-skeleton-thumb {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
aspect-ratio: 2 / 3;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--app-widget-header-bg, var(--mat-sys-surface-container-high)) 84%,
|
||||
var(--app-widget-bg, var(--mat-sys-surface-container)) 16%
|
||||
);
|
||||
border: 1px solid
|
||||
color-mix(
|
||||
in srgb,
|
||||
var(--app-widget-header-border, var(--mat-sys-outline-variant)) 94%,
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
.grid-skeleton-badge {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
width: 34px;
|
||||
height: 14px;
|
||||
border-radius: 999px;
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--app-widget-header-bg, var(--mat-sys-surface-container-high)) 56%,
|
||||
var(--app-body-color, var(--mat-sys-on-surface-variant)) 44%
|
||||
);
|
||||
}
|
||||
|
||||
.grid-skeleton-title {
|
||||
margin-top: 10px;
|
||||
padding: 0 3px;
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.grid-skeleton-line {
|
||||
height: 9px;
|
||||
border-radius: 999px;
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--app-widget-header-bg, var(--mat-sys-surface-container-high)) 52%,
|
||||
var(--app-body-color, var(--mat-sys-on-surface-variant)) 48%
|
||||
);
|
||||
|
||||
&--primary {
|
||||
width: 92%;
|
||||
}
|
||||
|
||||
&--secondary {
|
||||
width: 68%;
|
||||
}
|
||||
}
|
||||
|
||||
.grid-skeleton-thumb,
|
||||
.grid-skeleton-badge,
|
||||
.grid-skeleton-line {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(
|
||||
100deg,
|
||||
transparent 10%,
|
||||
color-mix(
|
||||
in srgb,
|
||||
var(--app-heading-color, var(--mat-sys-on-surface)) 20%,
|
||||
transparent
|
||||
)
|
||||
46%,
|
||||
transparent 76%
|
||||
);
|
||||
transform: translateX(-120%);
|
||||
animation: grid-skeleton-shimmer 1.2s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes grid-skeleton-shimmer {
|
||||
100% {
|
||||
transform: translateX(120%);
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,54 @@
|
||||
import { Component, input, output } from '@angular/core';
|
||||
import { Component, computed, input, output } from '@angular/core';
|
||||
import { MatCardModule } from '@angular/material/card';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { MatPaginatorModule } from '@angular/material/paginator';
|
||||
import { MatProgressSpinner } from '@angular/material/progress-spinner';
|
||||
import { MatPaginatorModule, PageEvent } from '@angular/material/paginator';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { PlaylistErrorViewComponent } from '../../../xtream-electron/playlist-error-view/playlist-error-view.component';
|
||||
import { ProgressCapsuleComponent } from '../../../xtream-electron/shared/progress-capsule/progress-capsule.component';
|
||||
import { WatchedBadgeComponent } from '../../../xtream-electron/shared/watched-badge/watched-badge.component';
|
||||
|
||||
interface GridListItem {
|
||||
id?: number | string;
|
||||
is_series?: number | string | boolean;
|
||||
xtream_id?: number | string;
|
||||
series_id?: number | string;
|
||||
stream_id?: number | string;
|
||||
category_id?: number | string;
|
||||
poster_url?: string;
|
||||
cover?: string;
|
||||
title?: string;
|
||||
o_name?: string;
|
||||
name?: string;
|
||||
rating?: string | number;
|
||||
rating_imdb?: string | number;
|
||||
progress?: number;
|
||||
isWatched?: boolean;
|
||||
hasSeriesProgress?: boolean;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'app-grid-list',
|
||||
template: `<div
|
||||
class="grid grid-cols-[repeat(auto-fill,minmax(148px,1fr))] gap-4"
|
||||
>
|
||||
@if (isLoading()) {
|
||||
<div class="flex justify-center items-center p-8">
|
||||
<mat-spinner diameter="50"></mat-spinner>
|
||||
</div>
|
||||
@for (row of skeletonRows(); track row) {
|
||||
<div class="grid-skeleton-card" aria-hidden="true">
|
||||
<div class="grid-skeleton-thumb">
|
||||
<span class="grid-skeleton-badge"></span>
|
||||
</div>
|
||||
<div class="grid-skeleton-title">
|
||||
<span
|
||||
class="grid-skeleton-line grid-skeleton-line--primary"
|
||||
></span>
|
||||
<span
|
||||
class="grid-skeleton-line grid-skeleton-line--secondary"
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
} @else {
|
||||
@for (item of items(); track $index) {
|
||||
@let i = $any(item);
|
||||
@@ -100,7 +131,6 @@ import { WatchedBadgeComponent } from '../../../xtream-electron/shared/watched-b
|
||||
PlaylistErrorViewComponent,
|
||||
MatCardModule,
|
||||
MatIcon,
|
||||
MatProgressSpinner,
|
||||
MatTooltip,
|
||||
MatPaginatorModule,
|
||||
ProgressCapsuleComponent,
|
||||
@@ -108,14 +138,20 @@ import { WatchedBadgeComponent } from '../../../xtream-electron/shared/watched-b
|
||||
],
|
||||
})
|
||||
export class GridListComponent {
|
||||
readonly items = input<any[]>();
|
||||
readonly items = input<GridListItem[]>();
|
||||
readonly isLoading = input<boolean>();
|
||||
readonly showPaginator = input(true);
|
||||
readonly itemClicked = output<any>();
|
||||
readonly pageChange = output<any>();
|
||||
readonly itemClicked = output<GridListItem>();
|
||||
readonly pageChange = output<PageEvent>();
|
||||
|
||||
readonly pageIndex = input<number>();
|
||||
readonly totalPages = input<number>();
|
||||
readonly limit = input<number>();
|
||||
readonly pageSizeOptions = input<number[]>();
|
||||
|
||||
readonly skeletonRows = computed(() => {
|
||||
const preferredCount = this.limit() ?? 12;
|
||||
const count = Math.max(8, Math.min(18, preferredCount));
|
||||
return Array.from({ length: count }, (_, index) => index);
|
||||
});
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
<mat-nav-list class="p-0 m-0">
|
||||
@for (link of links(); track link.path.join('/')) {
|
||||
<a
|
||||
class="portal-rail-link"
|
||||
mat-list-item
|
||||
[routerLink]="link.path"
|
||||
[routerLinkActive]="activeClass()"
|
||||
@@ -9,6 +10,7 @@
|
||||
[matTooltip]="link.tooltip"
|
||||
matTooltipPosition="right"
|
||||
[attr.aria-label]="link.tooltip"
|
||||
[attr.aria-current]="isActive(link) ? 'page' : null"
|
||||
[class.active]="activeClass() === 'active' && isActive(link)"
|
||||
[class.is-active]="
|
||||
activeClass() === 'is-active' && isActive(link)
|
||||
@@ -22,12 +24,14 @@
|
||||
<nav class="flex flex-col items-center gap-2 w-full">
|
||||
@for (link of links(); track link.path.join('/')) {
|
||||
<a
|
||||
class="portal-rail-link"
|
||||
[routerLink]="link.path"
|
||||
[routerLinkActive]="activeClass()"
|
||||
[routerLinkActiveOptions]="{ exact: link.exact ?? false }"
|
||||
[matTooltip]="link.tooltip"
|
||||
[matTooltipPosition]="'right'"
|
||||
[attr.aria-label]="link.tooltip"
|
||||
[attr.aria-current]="isActive(link) ? 'page' : null"
|
||||
[class.active]="activeClass() === 'active' && isActive(link)"
|
||||
[class.is-active]="
|
||||
activeClass() === 'is-active' && isActive(link)
|
||||
|
||||
@@ -4,25 +4,29 @@
|
||||
--rail-link-color: var(--mat-sys-on-surface-variant);
|
||||
--rail-link-hover-color: var(--mat-sys-on-surface);
|
||||
--rail-link-hover-bg: var(--mat-sys-surface-container);
|
||||
/* Keep consistent with category-item.selected tone */
|
||||
--rail-link-active-bg: color-mix(in srgb, var(--mat-sys-primary) 12%, transparent);
|
||||
--rail-link-active-color: var(--mat-sys-primary);
|
||||
--rail-link-active-icon: var(--mat-sys-primary);
|
||||
--rail-link-active-border: color-mix(in srgb, var(--mat-sys-primary) 42%, transparent);
|
||||
--rail-link-active-glow: color-mix(in srgb, var(--mat-sys-primary) 24%, transparent);
|
||||
--rail-link-active-bg: var(--app-selection-surface);
|
||||
--rail-link-active-bg-strong: var(--app-selection-surface-strong);
|
||||
--rail-link-active-color: var(--app-selection-color);
|
||||
--rail-link-active-icon: var(--app-selection-color);
|
||||
--rail-link-active-border: var(--app-selection-border);
|
||||
--rail-link-active-glow: var(--app-selection-glow);
|
||||
}
|
||||
|
||||
.p-0.m-0 {
|
||||
|
||||
.mdc-list-item {
|
||||
min-height: 28px;
|
||||
height: 28px;
|
||||
padding: 0 !important;
|
||||
border-radius: 4px;
|
||||
border-radius: 10px;
|
||||
margin: 2px 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
color: var(--rail-link-color);
|
||||
transition:
|
||||
background 0.2s ease,
|
||||
color 0.2s ease,
|
||||
box-shadow 0.2s ease,
|
||||
transform 0.2s ease;
|
||||
|
||||
.mdc-list-item__start {
|
||||
margin-inline-end: 0 !important;
|
||||
@@ -43,11 +47,16 @@
|
||||
|
||||
.mdc-list-item.active,
|
||||
.mdc-list-item.is-active {
|
||||
background: var(--rail-link-active-bg);
|
||||
background: linear-gradient(
|
||||
145deg,
|
||||
var(--rail-link-active-bg-strong),
|
||||
var(--rail-link-active-bg)
|
||||
);
|
||||
color: var(--rail-link-active-color);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px var(--rail-link-active-border),
|
||||
0 8px 18px -14px var(--rail-link-active-glow);
|
||||
transform: translateY(-1px);
|
||||
|
||||
mat-icon {
|
||||
color: var(--rail-link-active-icon);
|
||||
@@ -55,7 +64,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
nav.flex a {
|
||||
nav.flex .portal-rail-link {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 14px;
|
||||
@@ -65,38 +74,47 @@ nav.flex a {
|
||||
justify-content: center;
|
||||
color: var(--rail-link-color);
|
||||
text-decoration: none;
|
||||
transition: background-color 0.2s ease, color 0.2s ease, box-shadow 0.2s ease;
|
||||
transition:
|
||||
background-color 0.2s ease,
|
||||
color 0.2s ease,
|
||||
box-shadow 0.2s ease,
|
||||
transform 0.2s ease;
|
||||
app-region: no-drag;
|
||||
}
|
||||
|
||||
nav.flex a:hover {
|
||||
nav.flex .portal-rail-link:hover {
|
||||
color: var(--rail-link-hover-color);
|
||||
background: var(--rail-link-hover-bg);
|
||||
}
|
||||
|
||||
nav.flex a.is-active,
|
||||
nav.flex a.active {
|
||||
nav.flex .portal-rail-link.is-active,
|
||||
nav.flex .portal-rail-link.active {
|
||||
color: var(--rail-link-active-color);
|
||||
background: var(--rail-link-active-bg);
|
||||
background: linear-gradient(
|
||||
145deg,
|
||||
var(--rail-link-active-bg-strong),
|
||||
var(--rail-link-active-bg)
|
||||
);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px var(--rail-link-active-border),
|
||||
0 10px 20px -16px var(--rail-link-active-glow);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
nav.flex a.is-active mat-icon,
|
||||
nav.flex a.active mat-icon {
|
||||
nav.flex .portal-rail-link.is-active mat-icon,
|
||||
nav.flex .portal-rail-link.active mat-icon {
|
||||
color: var(--rail-link-active-icon);
|
||||
}
|
||||
|
||||
nav.flex a.is-active::after,
|
||||
nav.flex a.active::after {
|
||||
nav.flex .portal-rail-link.is-active::after,
|
||||
nav.flex .portal-rail-link.active::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: -4px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 3px;
|
||||
height: 18px;
|
||||
width: 4px;
|
||||
height: 20px;
|
||||
border-radius: 999px;
|
||||
background: var(--rail-link-active-icon);
|
||||
box-shadow: 0 0 10px var(--rail-link-active-glow);
|
||||
|
||||
@@ -17,14 +17,18 @@
|
||||
</div>
|
||||
}
|
||||
@if (isCategoryLoading()) {
|
||||
<ngx-skeleton-loader
|
||||
count="20"
|
||||
[theme]="{
|
||||
width: '95%',
|
||||
height: '48px',
|
||||
borderRadius: '40px',
|
||||
}"
|
||||
/>
|
||||
<div class="category-skeleton" aria-hidden="true">
|
||||
@for (row of skeletonRows; track row) {
|
||||
<div class="category-skeleton__row">
|
||||
<span class="category-skeleton__dot"></span>
|
||||
<span
|
||||
class="category-skeleton__label"
|
||||
[style.width.%]="skeletonLabelWidths[row]"
|
||||
></span>
|
||||
<span class="category-skeleton__count"></span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
} @else if (isCategoryFailed()) {
|
||||
<app-playlist-error-view
|
||||
[title]="'PORTALS.ERROR_VIEW.UNKNOWN_ERROR.TITLE' | translate"
|
||||
|
||||
@@ -45,3 +45,85 @@
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.category-skeleton {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
padding: 12px 8px 10px;
|
||||
}
|
||||
|
||||
.category-skeleton__row {
|
||||
display: grid;
|
||||
grid-template-columns: 14px minmax(0, 1fr) 34px;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-height: 40px;
|
||||
padding: 0 12px;
|
||||
border-radius: 14px;
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--app-widget-header-bg, var(--mat-sys-surface-container-high)) 84%,
|
||||
var(--app-widget-bg, var(--mat-sys-surface-container)) 16%
|
||||
);
|
||||
border: 1px solid
|
||||
color-mix(
|
||||
in srgb,
|
||||
var(--app-widget-header-border, var(--mat-sys-outline-variant)) 92%,
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
.category-skeleton__dot,
|
||||
.category-skeleton__label,
|
||||
.category-skeleton__count {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--app-widget-header-bg, var(--mat-sys-surface-container-high)) 55%,
|
||||
var(--app-body-color, var(--mat-sys-on-surface-variant)) 45%
|
||||
);
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(
|
||||
100deg,
|
||||
transparent 8%,
|
||||
color-mix(
|
||||
in srgb,
|
||||
var(--app-heading-color, var(--mat-sys-on-surface)) 22%,
|
||||
transparent
|
||||
)
|
||||
44%,
|
||||
transparent 74%
|
||||
);
|
||||
transform: translateX(-120%);
|
||||
animation: category-skeleton-shimmer 1.25s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
|
||||
.category-skeleton__dot {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.category-skeleton__label {
|
||||
height: 10px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.category-skeleton__count {
|
||||
justify-self: end;
|
||||
width: 26px;
|
||||
height: 10px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
@keyframes category-skeleton-shimmer {
|
||||
100% {
|
||||
transform: translateX(120%);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import { MatIcon } from '@angular/material/icon';
|
||||
import { MatPaginatorModule } from '@angular/material/paginator';
|
||||
import { ActivatedRoute, Router, RouterOutlet } from '@angular/router';
|
||||
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
|
||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||
import { PlaylistSwitcherComponent, ResizableDirective } from 'components';
|
||||
import { CategoryViewComponent } from '../xtream-electron/category-view/category-view.component';
|
||||
import { PlaylistErrorViewComponent } from '../xtream-electron/playlist-error-view/playlist-error-view.component';
|
||||
@@ -23,7 +22,6 @@ import { StalkerStore } from './stalker.store';
|
||||
MatIcon,
|
||||
MatIconButton,
|
||||
MatPaginatorModule,
|
||||
NgxSkeletonLoaderModule,
|
||||
PlaylistErrorViewComponent,
|
||||
PlaylistSwitcherComponent,
|
||||
ResizableDirective,
|
||||
@@ -56,6 +54,10 @@ export class StalkerMainContainerComponent {
|
||||
/** content items */
|
||||
readonly contentItems = this.stalkerStore.getPaginatedContent;
|
||||
readonly isContentLoading = this.stalkerStore.isPaginatedContentLoading;
|
||||
readonly skeletonRows = Array.from({ length: 12 }, (_, index) => index);
|
||||
readonly skeletonLabelWidths = [
|
||||
82, 70, 77, 66, 86, 73, 79, 62, 84, 68, 76, 71,
|
||||
];
|
||||
|
||||
constructor() {
|
||||
// reset category title after changing content type
|
||||
@@ -98,7 +100,8 @@ export class StalkerMainContainerComponent {
|
||||
const selectedTypeLabel =
|
||||
typeLabelByContentType[selectedContentType] ?? '';
|
||||
|
||||
const selectedCategoryName = this.stalkerStore.getSelectedCategoryName();
|
||||
const selectedCategoryName =
|
||||
this.stalkerStore.getSelectedCategoryName();
|
||||
let categoryName = selectedCategoryName;
|
||||
|
||||
// For implicit "All" view, show a type-specific title so switching
|
||||
|
||||
@@ -30,14 +30,18 @@
|
||||
/>
|
||||
} @else if (isStalkerCategories()) {
|
||||
@if (isStalkerCategoryLoading()) {
|
||||
<ngx-skeleton-loader
|
||||
count="18"
|
||||
[theme]="{
|
||||
width: '95%',
|
||||
height: '36px',
|
||||
borderRadius: '8px',
|
||||
}"
|
||||
/>
|
||||
<div class="context-skeleton" aria-hidden="true">
|
||||
@for (row of skeletonRows; track row) {
|
||||
<div class="context-skeleton__row">
|
||||
<span class="context-skeleton__dot"></span>
|
||||
<span
|
||||
class="context-skeleton__label"
|
||||
[style.width.%]="skeletonLabelWidths[row]"
|
||||
></span>
|
||||
<span class="context-skeleton__count"></span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
} @else if (isStalkerCategoryFailed()) {
|
||||
<app-playlist-error-view
|
||||
[title]="'Unable to load categories'"
|
||||
|
||||
@@ -41,7 +41,9 @@
|
||||
line-height: 32px;
|
||||
border-radius: 8px;
|
||||
color: var(--mat-sys-on-surface-variant);
|
||||
transition: background 0.15s ease, color 0.15s ease;
|
||||
transition:
|
||||
background 0.15s ease,
|
||||
color 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
background: var(--mat-sys-surface-container);
|
||||
@@ -61,7 +63,7 @@
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
margin-top: 5px;
|
||||
padding: 2px 8px;
|
||||
padding: 2px 0;
|
||||
border-radius: 999px;
|
||||
background: var(--mat-sys-surface-container);
|
||||
color: var(--mat-sys-on-surface-variant);
|
||||
@@ -98,3 +100,86 @@ app-category-view {
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--mat-sys-outline-variant) transparent;
|
||||
}
|
||||
|
||||
.context-skeleton {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
padding: 4px 6px 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.context-skeleton__row {
|
||||
display: grid;
|
||||
grid-template-columns: 12px minmax(0, 1fr) 28px;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-height: 36px;
|
||||
padding: 0 10px;
|
||||
border-radius: 12px;
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--app-widget-header-bg, var(--mat-sys-surface-container-high)) 88%,
|
||||
var(--app-widget-bg, var(--mat-sys-surface-container)) 12%
|
||||
);
|
||||
border: 1px solid
|
||||
color-mix(
|
||||
in srgb,
|
||||
var(--app-widget-header-border, var(--mat-sys-outline-variant)) 96%,
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
.context-skeleton__dot,
|
||||
.context-skeleton__label,
|
||||
.context-skeleton__count {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--app-widget-header-bg, var(--mat-sys-surface-container-high)) 52%,
|
||||
var(--app-body-color, var(--mat-sys-on-surface-variant)) 48%
|
||||
);
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(
|
||||
100deg,
|
||||
transparent 8%,
|
||||
color-mix(
|
||||
in srgb,
|
||||
var(--app-heading-color, var(--mat-sys-on-surface)) 18%,
|
||||
transparent
|
||||
)
|
||||
44%,
|
||||
transparent 74%
|
||||
);
|
||||
transform: translateX(-120%);
|
||||
animation: context-skeleton-shimmer 1.15s ease-in-out infinite;
|
||||
}
|
||||
}
|
||||
|
||||
.context-skeleton__dot {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.context-skeleton__label {
|
||||
height: 9px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.context-skeleton__count {
|
||||
justify-self: end;
|
||||
width: 22px;
|
||||
height: 9px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
@keyframes context-skeleton-shimmer {
|
||||
100% {
|
||||
transform: translateX(120%);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import { MatIconButton } from '@angular/material/button';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { Router } from '@angular/router';
|
||||
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
|
||||
import { PlaylistErrorViewComponent } from '../xtream-electron/playlist-error-view/playlist-error-view.component';
|
||||
import {
|
||||
CategoryManagementDialogComponent,
|
||||
@@ -33,7 +32,6 @@ interface XtreamCategoryLike {
|
||||
MatIcon,
|
||||
MatIconButton,
|
||||
MatTooltip,
|
||||
NgxSkeletonLoaderModule,
|
||||
PlaylistErrorViewComponent,
|
||||
],
|
||||
templateUrl: './workspace-context-panel.component.html',
|
||||
@@ -69,8 +67,14 @@ export class WorkspaceContextPanelComponent {
|
||||
|
||||
readonly stalkerCategories = this.stalkerStore.getCategoryResource;
|
||||
readonly stalkerSelectedCategoryId = this.stalkerStore.selectedCategoryId;
|
||||
readonly isStalkerCategoryLoading = this.stalkerStore.isCategoryResourceLoading;
|
||||
readonly isStalkerCategoryFailed = this.stalkerStore.isCategoryResourceFailed;
|
||||
readonly isStalkerCategoryLoading =
|
||||
this.stalkerStore.isCategoryResourceLoading;
|
||||
readonly isStalkerCategoryFailed =
|
||||
this.stalkerStore.isCategoryResourceFailed;
|
||||
readonly skeletonRows = Array.from({ length: 14 }, (_, index) => index);
|
||||
readonly skeletonLabelWidths = [
|
||||
78, 66, 74, 59, 83, 69, 76, 62, 81, 64, 72, 67, 79, 61,
|
||||
];
|
||||
|
||||
readonly title = computed(() => {
|
||||
if (this.isXtreamCategories()) {
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
|
||||
<div class="rail-footer">
|
||||
<a
|
||||
class="rail-shortcut"
|
||||
routerLink="/workspace/settings"
|
||||
[class.is-active]="isSettingsRoute()"
|
||||
matTooltip="Settings"
|
||||
@@ -108,6 +109,7 @@
|
||||
@if (downloadsContext()) {
|
||||
<button
|
||||
type="button"
|
||||
class="header-shortcut"
|
||||
mat-icon-button
|
||||
[class.is-active]="isDownloadsView()"
|
||||
(click)="openDownloadsShortcut()"
|
||||
@@ -131,6 +133,7 @@
|
||||
}
|
||||
<button
|
||||
type="button"
|
||||
class="header-shortcut"
|
||||
mat-icon-button
|
||||
[class.is-active]="isFavoritesView()"
|
||||
(click)="toggleFavoritesShortcut()"
|
||||
|
||||
@@ -47,11 +47,12 @@
|
||||
--rail-link-color: var(--mat-sys-on-surface-variant);
|
||||
--rail-link-hover-color: var(--mat-sys-on-surface);
|
||||
--rail-link-hover-bg: var(--mat-sys-surface-container);
|
||||
--rail-link-active-bg: color-mix(in srgb, var(--mat-sys-primary) 12%, transparent);
|
||||
--rail-link-active-color: var(--mat-sys-primary);
|
||||
--rail-link-active-icon: var(--mat-sys-primary);
|
||||
--rail-link-active-border: color-mix(in srgb, var(--mat-sys-primary) 42%, transparent);
|
||||
--rail-link-active-glow: color-mix(in srgb, var(--mat-sys-primary) 24%, transparent);
|
||||
--rail-link-active-bg: var(--app-selection-surface);
|
||||
--rail-link-active-bg-strong: var(--app-selection-surface-strong);
|
||||
--rail-link-active-color: var(--app-selection-color);
|
||||
--rail-link-active-icon: var(--app-selection-color);
|
||||
--rail-link-active-border: var(--app-selection-border);
|
||||
--rail-link-active-glow: var(--app-selection-glow);
|
||||
}
|
||||
|
||||
.app-rail a {
|
||||
@@ -64,7 +65,11 @@
|
||||
justify-content: center;
|
||||
color: var(--rail-link-color);
|
||||
text-decoration: none;
|
||||
transition: background-color 0.2s ease, color 0.2s ease, box-shadow 0.2s ease;
|
||||
transition:
|
||||
background-color 0.2s ease,
|
||||
color 0.2s ease,
|
||||
box-shadow 0.2s ease,
|
||||
transform 0.2s ease;
|
||||
app-region: no-drag;
|
||||
|
||||
mat-icon {
|
||||
@@ -82,10 +87,15 @@
|
||||
.app-rail a.is-active,
|
||||
.app-rail a.active {
|
||||
color: var(--rail-link-active-color);
|
||||
background: var(--rail-link-active-bg);
|
||||
background: linear-gradient(
|
||||
145deg,
|
||||
var(--rail-link-active-bg-strong),
|
||||
var(--rail-link-active-bg)
|
||||
);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px var(--rail-link-active-border),
|
||||
0 10px 20px -16px var(--rail-link-active-glow);
|
||||
transform: translateY(-1px);
|
||||
|
||||
mat-icon {
|
||||
color: var(--rail-link-active-icon);
|
||||
@@ -237,8 +247,15 @@
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
color: var(--mat-sys-on-primary-container);
|
||||
background: var(--mat-sys-primary-container);
|
||||
color: var(--app-selection-color);
|
||||
background: linear-gradient(
|
||||
145deg,
|
||||
var(--app-selection-surface-strong),
|
||||
var(--app-selection-surface)
|
||||
);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px var(--app-selection-border),
|
||||
0 10px 20px -16px var(--app-selection-glow);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -336,9 +353,14 @@
|
||||
}
|
||||
|
||||
&.is-active {
|
||||
border-color: transparent;
|
||||
color: var(--mat-sys-on-primary-container);
|
||||
background: var(--mat-sys-primary-container);
|
||||
border-color: var(--app-selection-border);
|
||||
color: var(--app-selection-color);
|
||||
background: linear-gradient(
|
||||
145deg,
|
||||
var(--app-selection-surface-strong),
|
||||
var(--app-selection-surface)
|
||||
);
|
||||
box-shadow: 0 10px 20px -18px var(--app-selection-glow);
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,7 +209,9 @@ export class WorkspaceShellComponent {
|
||||
return (
|
||||
(context.provider === 'xtreams' ||
|
||||
context.provider === 'stalker') &&
|
||||
(section === 'favorites' || section === 'recent')
|
||||
(section === 'favorites' ||
|
||||
section === 'recent' ||
|
||||
section === 'downloads')
|
||||
);
|
||||
});
|
||||
readonly showContextPanel = computed(
|
||||
|
||||
@@ -57,12 +57,39 @@
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
|
||||
.option-list {
|
||||
overflow-y: auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.option-row {
|
||||
border-radius: 12px;
|
||||
transition:
|
||||
box-shadow 0.18s ease,
|
||||
transform 0.18s ease;
|
||||
|
||||
.option-label,
|
||||
.option-meta,
|
||||
mat-icon {
|
||||
transition: color 0.18s ease, opacity 0.18s ease;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
box-shadow: 0 10px 20px -18px var(--app-selection-glow);
|
||||
transform: translateY(-1px);
|
||||
|
||||
.option-label,
|
||||
.option-meta,
|
||||
mat-icon {
|
||||
color: var(--app-selection-color) !important;
|
||||
}
|
||||
|
||||
.option-meta {
|
||||
opacity: 0.95;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.panel-section-title {
|
||||
margin: 0;
|
||||
padding: 10px 10px 6px;
|
||||
|
||||
@@ -5,6 +5,8 @@
|
||||
class="nav-item category-item"
|
||||
[attr.data-category-id]="$any(item).category_id ?? item.id"
|
||||
[class.selected]="isSelected(item)"
|
||||
[class.category-item--selected]="isSelected(item)"
|
||||
[attr.aria-current]="isSelected(item) ? 'true' : null"
|
||||
(click)="categoryClicked.emit(item)"
|
||||
>
|
||||
<span class="nav-item-label">
|
||||
|
||||
@@ -20,6 +20,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
.category-item--selected .item-count {
|
||||
color: var(--app-selection-color) !important;
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
mat-card-content {
|
||||
overflow-wrap: break-word;
|
||||
padding: 12px;
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<h2>{{ 'PORTALS.SIDEBAR.RECENTLY_ADDED' | translate }}</h2>
|
||||
|
||||
@if (recentlyAddedVod().length > 0) {
|
||||
<div class="section">
|
||||
<div class="section" [class.section--active]="isSectionActive('vod')">
|
||||
<h3>{{ 'PORTALS.SIDEBAR.MOVIES' | translate }}</h3>
|
||||
<div class="carousel">
|
||||
@for (item of recentlyAddedVod(); track item.xtream_id) {
|
||||
@@ -20,7 +20,10 @@
|
||||
}
|
||||
|
||||
@if (recentlyAddedSeries().length > 0) {
|
||||
<div class="section">
|
||||
<div
|
||||
class="section"
|
||||
[class.section--active]="isSectionActive('series')"
|
||||
>
|
||||
<h3>{{ 'PORTALS.SIDEBAR.SERIES' | translate }}</h3>
|
||||
<div class="carousel">
|
||||
@for (item of recentlyAddedSeries(); track item.xtream_id) {
|
||||
@@ -38,7 +41,7 @@
|
||||
}
|
||||
|
||||
@if (recentlyAddedLive().length > 0) {
|
||||
<div class="section">
|
||||
<div class="section" [class.section--active]="isSectionActive('live')">
|
||||
<h3>{{ 'PORTALS.SIDEBAR.LIVE_TV' | translate }}</h3>
|
||||
<div class="carousel">
|
||||
@for (item of recentlyAddedLive(); track item.xtream_id) {
|
||||
|
||||
@@ -13,22 +13,58 @@ h2 {
|
||||
}
|
||||
|
||||
.section {
|
||||
padding: 12px 14px 0;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 16px;
|
||||
margin-bottom: 32px;
|
||||
transition:
|
||||
background 0.2s ease,
|
||||
border-color 0.2s ease,
|
||||
box-shadow 0.2s ease;
|
||||
|
||||
h3 {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.section.section--active {
|
||||
border-color: var(--app-selection-border);
|
||||
background: linear-gradient(
|
||||
145deg,
|
||||
var(--app-selection-surface),
|
||||
color-mix(in srgb, var(--app-selection-color) 6%, transparent)
|
||||
);
|
||||
box-shadow:
|
||||
inset 0 0 0 1px var(--app-selection-border),
|
||||
0 14px 24px -22px var(--app-selection-glow);
|
||||
|
||||
h3 {
|
||||
color: var(--app-selection-color);
|
||||
opacity: 1;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
width: 7px;
|
||||
height: 7px;
|
||||
border-radius: 999px;
|
||||
background: var(--app-selection-color);
|
||||
box-shadow: 0 0 10px var(--app-selection-glow);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.carousel {
|
||||
display: flex;
|
||||
overflow-x: auto;
|
||||
gap: 16px;
|
||||
padding-bottom: 16px;
|
||||
|
||||
|
||||
scrollbar-width: thin;
|
||||
|
||||
|
||||
.carousel-item {
|
||||
min-width: 160px;
|
||||
width: 160px;
|
||||
|
||||
@@ -25,6 +25,7 @@ export class RecentlyAddedComponent {
|
||||
readonly recentlyAddedSeries = computed(() =>
|
||||
this.getRecentlyAdded(this.xtreamStore.serialStreams(), true)
|
||||
);
|
||||
readonly selectedContentType = this.xtreamStore.selectedContentType;
|
||||
|
||||
private getRecentlyAdded(items: any[], isSeries = false) {
|
||||
return [...items]
|
||||
@@ -43,6 +44,10 @@ export class RecentlyAddedComponent {
|
||||
return parseInt(timestamp) * 1000;
|
||||
}
|
||||
|
||||
isSectionActive(type: ContentType): boolean {
|
||||
return this.selectedContentType() === type;
|
||||
}
|
||||
|
||||
openItem(item: any, type: ContentType) {
|
||||
this.xtreamStore.setSelectedContentType(type);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@if (isImporting()) {
|
||||
@if (showImportOverlay()) {
|
||||
<app-loading-overlay
|
||||
[current]="getImportCount()"
|
||||
[total]="itemsToImport()"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Component, effect, inject } from '@angular/core';
|
||||
import { Component, computed, effect, inject, signal } from '@angular/core';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import {
|
||||
ActivatedRoute,
|
||||
@@ -36,40 +36,49 @@ export class XtreamShellComponent {
|
||||
readonly portalStatus = this.xtreamStore.portalStatus;
|
||||
readonly isWorkspaceLayout =
|
||||
this.route.snapshot.data['layout'] === 'workspace';
|
||||
readonly showImportOverlay = computed(() => {
|
||||
const section = this.currentSection();
|
||||
return this.isImporting() && this.isContentSection(section);
|
||||
});
|
||||
|
||||
private currentPlaylistId: string | null = null;
|
||||
private currentSection: string | null = null;
|
||||
private readonly currentSection = signal<string | null>(null);
|
||||
|
||||
constructor() {
|
||||
// Subscribe to route params to handle switching between playlists
|
||||
this.route.params.pipe(takeUntilDestroyed()).subscribe(async (params) => {
|
||||
const newPlaylistId = params['id'];
|
||||
this.route.params
|
||||
.pipe(takeUntilDestroyed())
|
||||
.subscribe(async (params) => {
|
||||
const newPlaylistId = params['id'];
|
||||
|
||||
// Skip if playlist ID hasn't changed
|
||||
if (this.currentPlaylistId === newPlaylistId) {
|
||||
return;
|
||||
}
|
||||
// Skip if playlist ID hasn't changed
|
||||
if (this.currentPlaylistId === newPlaylistId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Always reset the store when playlist changes to prevent stale data
|
||||
// from previous sessions showing up (the store is a singleton)
|
||||
this.xtreamStore.resetStore(newPlaylistId);
|
||||
this.currentPlaylistId = newPlaylistId;
|
||||
// Always reset the store when playlist changes to prevent stale data
|
||||
// from previous sessions showing up (the store is a singleton)
|
||||
this.xtreamStore.resetStore(newPlaylistId);
|
||||
this.currentPlaylistId = newPlaylistId;
|
||||
|
||||
this.store.dispatch(
|
||||
PlaylistActions.setActivePlaylist({
|
||||
playlistId: newPlaylistId,
|
||||
})
|
||||
);
|
||||
this.store.dispatch(
|
||||
PlaylistActions.setActivePlaylist({
|
||||
playlistId: newPlaylistId,
|
||||
})
|
||||
);
|
||||
|
||||
// Must await fetchXtreamPlaylist before checking status
|
||||
// because checkPortalStatus needs currentPlaylist to be set
|
||||
await this.xtreamStore.fetchXtreamPlaylist();
|
||||
await this.xtreamStore.checkPortalStatus();
|
||||
});
|
||||
// Must await fetchXtreamPlaylist before checking status
|
||||
// because checkPortalStatus needs currentPlaylist to be set
|
||||
await this.xtreamStore.fetchXtreamPlaylist();
|
||||
await this.xtreamStore.checkPortalStatus();
|
||||
});
|
||||
|
||||
this.router.events
|
||||
.pipe(
|
||||
filter((event): event is NavigationEnd => event instanceof NavigationEnd),
|
||||
filter(
|
||||
(event): event is NavigationEnd =>
|
||||
event instanceof NavigationEnd
|
||||
),
|
||||
takeUntilDestroyed()
|
||||
)
|
||||
.subscribe(() => {
|
||||
@@ -81,11 +90,16 @@ export class XtreamShellComponent {
|
||||
effect(() => {
|
||||
const playlist = this.xtreamStore.currentPlaylist();
|
||||
const playlistId = this.xtreamStore.playlistId();
|
||||
const section = this.currentSection();
|
||||
|
||||
if (!this.isContentSection(section)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only initialize content when playlist is loaded AND matches the current playlistId
|
||||
// This prevents stale data from a previous session from being used
|
||||
if (playlist !== null && playlist.id === playlistId) {
|
||||
this.xtreamStore.initializeContent();
|
||||
void this.xtreamStore.initializeContent();
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -96,11 +110,11 @@ export class XtreamShellComponent {
|
||||
const sectionFromUrl = this.getSectionFromUrl(this.router.url);
|
||||
const section = sectionFromSnapshot ?? sectionFromUrl;
|
||||
|
||||
if (!section || section === this.currentSection) {
|
||||
if (!section || section === this.currentSection()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.currentSection = section;
|
||||
this.currentSection.set(section);
|
||||
|
||||
if (section === 'vod' || section === 'live' || section === 'series') {
|
||||
this.xtreamStore.setSelectedContentType(section);
|
||||
@@ -116,4 +130,14 @@ export class XtreamShellComponent {
|
||||
);
|
||||
return match?.[1] ?? null;
|
||||
}
|
||||
|
||||
private isContentSection(section: string | null): boolean {
|
||||
return (
|
||||
section === 'vod' ||
|
||||
section === 'live' ||
|
||||
section === 'series' ||
|
||||
section === 'search' ||
|
||||
section === 'recently-added'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -52,6 +52,28 @@ html {
|
||||
--app-rail-border: rgba(0, 0, 0, 0.08);
|
||||
--app-search-bg: rgba(255, 255, 255, 0.7);
|
||||
--app-search-border: rgba(0, 0, 0, 0.1);
|
||||
--app-selection-color: #2f7bff;
|
||||
--app-selection-on-color: #eef4ff;
|
||||
--app-selection-surface: color-mix(
|
||||
in srgb,
|
||||
var(--app-selection-color) 14%,
|
||||
transparent
|
||||
);
|
||||
--app-selection-surface-strong: color-mix(
|
||||
in srgb,
|
||||
var(--app-selection-color) 22%,
|
||||
transparent
|
||||
);
|
||||
--app-selection-border: color-mix(
|
||||
in srgb,
|
||||
var(--app-selection-color) 52%,
|
||||
transparent
|
||||
);
|
||||
--app-selection-glow: color-mix(
|
||||
in srgb,
|
||||
var(--app-selection-color) 34%,
|
||||
transparent
|
||||
);
|
||||
|
||||
// Widget surfaces — no borders, layered backgrounds
|
||||
--app-widget-bg: #ffffff;
|
||||
@@ -96,6 +118,28 @@ html {
|
||||
--app-rail-border: rgba(255, 255, 255, 0.05);
|
||||
--app-search-bg: rgba(255, 255, 255, 0.05);
|
||||
--app-search-border: rgba(255, 255, 255, 0.09);
|
||||
--app-selection-color: #78adff;
|
||||
--app-selection-on-color: #071225;
|
||||
--app-selection-surface: color-mix(
|
||||
in srgb,
|
||||
var(--app-selection-color) 18%,
|
||||
transparent
|
||||
);
|
||||
--app-selection-surface-strong: color-mix(
|
||||
in srgb,
|
||||
var(--app-selection-color) 28%,
|
||||
transparent
|
||||
);
|
||||
--app-selection-border: color-mix(
|
||||
in srgb,
|
||||
var(--app-selection-color) 62%,
|
||||
transparent
|
||||
);
|
||||
--app-selection-glow: color-mix(
|
||||
in srgb,
|
||||
var(--app-selection-color) 44%,
|
||||
transparent
|
||||
);
|
||||
|
||||
// Widget surfaces — NO border, surface contrast does the work
|
||||
// Widgets sit on content-bg (#161a22), so widget-bg is notably lighter
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -27,7 +28,9 @@
|
||||
transition:
|
||||
background 0.15s ease,
|
||||
color 0.15s ease,
|
||||
border-color 0.15s ease;
|
||||
border-color 0.15s ease,
|
||||
box-shadow 0.18s ease,
|
||||
transform 0.18s ease;
|
||||
|
||||
mat-icon {
|
||||
font-size: 18px;
|
||||
@@ -62,18 +65,38 @@
|
||||
|
||||
&.selected,
|
||||
&.active {
|
||||
background: var(--mat-sys-primary-container) !important;
|
||||
color: var(--mat-sys-on-primary-container) !important;
|
||||
border-color: transparent !important;
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
var(--app-selection-surface-strong),
|
||||
var(--app-selection-surface)
|
||||
) !important;
|
||||
color: var(--app-selection-color) !important;
|
||||
border-color: var(--app-selection-border) !important;
|
||||
font-weight: 600;
|
||||
box-shadow:
|
||||
inset 0 0 0 1px var(--app-selection-border),
|
||||
0 12px 20px -18px var(--app-selection-glow);
|
||||
transform: translateY(-1px);
|
||||
|
||||
* {
|
||||
color: var(--mat-sys-on-primary-container) !important;
|
||||
color: var(--app-selection-color) !important;
|
||||
}
|
||||
|
||||
.nav-item-meta {
|
||||
color: var(--mat-sys-on-primary-container) !important;
|
||||
opacity: 0.85;
|
||||
color: var(--app-selection-color) !important;
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset-inline-start: 6px;
|
||||
top: 7px;
|
||||
bottom: 7px;
|
||||
width: 3px;
|
||||
border-radius: 999px;
|
||||
background: var(--app-selection-color);
|
||||
box-shadow: 0 0 10px var(--app-selection-glow);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user