mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
refactor(portal): split collection data access (#953)
This commit is contained in:
1 parent
118d4fce32
commit
20e7322353
28 files changed
+185
-236
No files matched your search
@@ -76,3 +76,9 @@ buildable dependency paths to `dist/` during `@nx/js:tsc` builds.
|
||||
|
||||
If a change needs a dependency in the opposite direction, move the shared
|
||||
contract into a lower-level library instead of weakening boundaries.
|
||||
|
||||
Portal collection orchestration that reads/writes favorites, recent items, live
|
||||
playback, or EPG data belongs in `libs/portal/shared/data-access`, not
|
||||
`libs/portal/shared/util`. That keeps pure collection helpers importable by
|
||||
Xtream/Stalker data-access libraries while allowing shared UI to use
|
||||
provider-specific collection services without creating cycles.
|
||||
@@ -0,0 +1,21 @@
|
||||
export default {
|
||||
displayName: 'portal-shared-data-access',
|
||||
preset: '../../../../jest.preset.js',
|
||||
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
|
||||
coverageDirectory: '../../../../coverage/libs/portal/shared/data-access',
|
||||
transform: {
|
||||
'^.+\\.(ts|mjs|js|html)$': [
|
||||
'jest-preset-angular',
|
||||
{
|
||||
tsconfig: '<rootDir>/tsconfig.spec.json',
|
||||
stringifyContentPathRegex: '\\.(html|svg)$',
|
||||
},
|
||||
],
|
||||
},
|
||||
transformIgnorePatterns: ['node_modules/(?!.*\\.mjs$)'],
|
||||
snapshotSerializers: [
|
||||
'jest-preset-angular/build/serializers/no-ng-attributes',
|
||||
'jest-preset-angular/build/serializers/ng-snapshot',
|
||||
'jest-preset-angular/build/serializers/html-comment',
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "portal-shared-data-access",
|
||||
"$schema": "../../../../node_modules/nx/schemas/project-schema.json",
|
||||
"sourceRoot": "libs/portal/shared/data-access/src",
|
||||
"prefix": "lib",
|
||||
"projectType": "library",
|
||||
"tags": ["scope:portal", "domain:portal-shared", "type:data-access"],
|
||||
"targets": {
|
||||
"test": {
|
||||
"executor": "@nx/jest:jest",
|
||||
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
||||
"options": {
|
||||
"jestConfig": "libs/portal/shared/data-access/jest.config.ts",
|
||||
"tsConfig": "libs/portal/shared/data-access/tsconfig.spec.json"
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"executor": "@nx/eslint:lint"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * from './lib/collection';
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './unified-favorites-data.service';
|
||||
export * from './unified-recent-data.service';
|
||||
export * from './stream-resolver.service';
|
||||
+1
-1
@@ -7,7 +7,7 @@ import {
|
||||
import { StalkerSessionService } from '@iptvnator/portal/stalker/data-access';
|
||||
import { DataService, PlaylistsService } from '@iptvnator/services';
|
||||
import { Playlist } from '@iptvnator/shared/interfaces';
|
||||
import { UnifiedCollectionItem } from './unified-collection-item.interface';
|
||||
import { UnifiedCollectionItem } from '@iptvnator/portal/shared/util';
|
||||
import {
|
||||
ResolvedLiveCollectionDetail,
|
||||
StreamResolverService,
|
||||
+1
-1
@@ -15,7 +15,7 @@ import {
|
||||
XtreamUrlService,
|
||||
} from '@iptvnator/portal/xtream/data-access';
|
||||
import { StalkerSessionService } from '@iptvnator/portal/stalker/data-access';
|
||||
import { UnifiedCollectionItem } from './unified-collection-item.interface';
|
||||
import { UnifiedCollectionItem } from '@iptvnator/portal/shared/util';
|
||||
|
||||
type PlaylistWithChannels = Playlist & {
|
||||
readonly playlist?: { readonly items?: Channel[] };
|
||||
+1
-1
@@ -9,7 +9,7 @@ import {
|
||||
PlaylistMeta,
|
||||
StalkerPortalItem,
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
import { UnifiedCollectionItem } from './unified-collection-item.interface';
|
||||
import { UnifiedCollectionItem } from '@iptvnator/portal/shared/util';
|
||||
import { UnifiedFavoritesDataService } from './unified-favorites-data.service';
|
||||
|
||||
describe('UnifiedFavoritesDataService', () => {
|
||||
+30
-22
@@ -19,14 +19,12 @@ import {
|
||||
import {
|
||||
buildCollectionUid,
|
||||
buildXtreamCollectionUid,
|
||||
UnifiedCollectionItem,
|
||||
} from './unified-collection-item.interface';
|
||||
import { CollectionScope } from './scope-toggle.service';
|
||||
import {
|
||||
CollectionScope,
|
||||
isStalkerItem,
|
||||
UnifiedCollectionItem,
|
||||
xtreamContentType,
|
||||
XtreamFavoriteRow,
|
||||
} from './collection-helpers';
|
||||
} from '@iptvnator/portal/shared/util';
|
||||
|
||||
const GLOBAL_FAVORITES_ORDER_KEY = 'global-favorites-channel-order-v1';
|
||||
|
||||
@@ -346,18 +344,22 @@ export class UnifiedFavoritesDataService {
|
||||
private async clearXtreamFavorites(
|
||||
items: UnifiedCollectionItem[]
|
||||
): Promise<void> {
|
||||
if (!window.electron) {
|
||||
const electron = window.electron;
|
||||
if (!electron) {
|
||||
return;
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
items
|
||||
.filter((item) => item.contentId != null)
|
||||
.filter(
|
||||
(
|
||||
item
|
||||
): item is UnifiedCollectionItem & {
|
||||
readonly contentId: number;
|
||||
} => item.contentId != null
|
||||
)
|
||||
.map((item) =>
|
||||
window.electron!.dbRemoveFavorite(
|
||||
item.contentId!,
|
||||
item.playlistId
|
||||
)
|
||||
electron.dbRemoveFavorite(item.contentId, item.playlistId)
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -679,9 +681,11 @@ export class UnifiedFavoritesDataService {
|
||||
const unordered: UnifiedCollectionItem[] = [];
|
||||
for (const item of items) {
|
||||
const pos = orderMap.get(item.uid);
|
||||
pos != null
|
||||
? ordered.push({ ...item, position: pos })
|
||||
: unordered.push(item);
|
||||
if (pos != null) {
|
||||
ordered.push({ ...item, position: pos });
|
||||
} else {
|
||||
unordered.push(item);
|
||||
}
|
||||
}
|
||||
ordered.sort((a, b) => (a.position ?? 0) - (b.position ?? 0));
|
||||
unordered.sort(
|
||||
@@ -693,14 +697,18 @@ export class UnifiedFavoritesDataService {
|
||||
}
|
||||
|
||||
private buildXtreamPositionUpdates(items: UnifiedCollectionItem[]) {
|
||||
return items
|
||||
.filter(
|
||||
(item) => item.sourceType === 'xtream' && item.contentId != null
|
||||
)
|
||||
.map((item, index) => ({
|
||||
content_id: item.contentId!,
|
||||
position: index,
|
||||
}));
|
||||
const updates: { content_id: number; position: number }[] = [];
|
||||
|
||||
for (const item of items) {
|
||||
if (item.sourceType === 'xtream' && item.contentId != null) {
|
||||
updates.push({
|
||||
content_id: item.contentId,
|
||||
position: updates.length,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return updates;
|
||||
}
|
||||
|
||||
private groupItemsByPlaylist(
|
||||
+1
-1
@@ -8,7 +8,7 @@ import {
|
||||
Playlist,
|
||||
PlaylistMeta,
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
import { UnifiedCollectionItem } from './unified-collection-item.interface';
|
||||
import { UnifiedCollectionItem } from '@iptvnator/portal/shared/util';
|
||||
import { UnifiedRecentDataService } from './unified-recent-data.service';
|
||||
|
||||
describe('UnifiedRecentDataService', () => {
|
||||
+3
-3
@@ -22,10 +22,10 @@ import {
|
||||
import {
|
||||
buildCollectionUid,
|
||||
buildXtreamCollectionUid,
|
||||
CollectionScope,
|
||||
UnifiedCollectionItem,
|
||||
} from './unified-collection-item.interface';
|
||||
import { CollectionScope } from './scope-toggle.service';
|
||||
import { xtreamContentType } from './collection-helpers';
|
||||
xtreamContentType,
|
||||
} from '@iptvnator/portal/shared/util';
|
||||
|
||||
type PlaylistWithChannels = Playlist & {
|
||||
readonly playlist?: { readonly items?: Channel[] };
|
||||
@@ -0,0 +1,6 @@
|
||||
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';
|
||||
|
||||
setupZoneTestEnv({
|
||||
errorOnUnknownElements: true,
|
||||
errorOnUnknownProperties: true,
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"extends": "../../../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"isolatedModules": true,
|
||||
"target": "es2022",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"noImplicitOverride": true,
|
||||
"noPropertyAccessFromIndexSignature": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"emitDecoratorMetadata": false,
|
||||
"module": "preserve"
|
||||
},
|
||||
"angularCompilerOptions": {
|
||||
"enableI18nLegacyMessageIdFormat": false,
|
||||
"strictInjectionParameters": true,
|
||||
"strictInputAccessModifiers": true,
|
||||
"strictTemplates": true
|
||||
},
|
||||
"files": [],
|
||||
"include": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.lib.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.spec.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../../dist/out-tsc",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"inlineSources": true,
|
||||
"types": []
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"]
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"outDir": "../../../../dist/out-tsc",
|
||||
"module": "commonjs",
|
||||
"target": "es2016",
|
||||
"types": ["jest", "node"],
|
||||
"moduleResolution": "node10"
|
||||
},
|
||||
"files": ["src/test-setup.ts"],
|
||||
"include": [
|
||||
"jest.config.ts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
@@ -6,7 +6,6 @@ export * from './lib/components/global-favorites-list/global-favorites-list.comp
|
||||
export * from './lib/components/grid-list/grid-list.component';
|
||||
export * from './lib/components/playlist-error-view/playlist-error-view.component';
|
||||
export { ProgressCapsuleComponent } from '@iptvnator/ui/components';
|
||||
export * from './lib/components/portal-collection-live-shell/portal-collection-live-shell.component';
|
||||
export * from './lib/components/portal-collection-shell/portal-collection-shell.component';
|
||||
export * from './lib/components/portal-empty-state/portal-empty-state.component';
|
||||
export * from './lib/components/search-form/search-form.component';
|
||||
|
||||
-55
@@ -1,55 +0,0 @@
|
||||
<div
|
||||
class="sidebar"
|
||||
appResizable
|
||||
[minWidth]="250"
|
||||
[maxWidth]="600"
|
||||
[defaultWidth]="400"
|
||||
storageKey="sidebar-width"
|
||||
>
|
||||
<div class="sidebar-header category-content-header">
|
||||
<div class="category-meta">
|
||||
<h2 class="category-title">{{ title() }}</h2>
|
||||
<div class="category-subtitle">{{ subtitle() }}</div>
|
||||
</div>
|
||||
<ng-content select="[liveHeaderActions]" />
|
||||
</div>
|
||||
|
||||
<ng-content select="[liveChannels]" />
|
||||
</div>
|
||||
|
||||
<div class="content-container">
|
||||
@if (hasSelection()) {
|
||||
@if (streamUrl() && isEmbeddedPlayer()) {
|
||||
<div class="video-player">
|
||||
<app-web-player-view [streamUrl]="streamUrl()" />
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="epg">
|
||||
<div class="epg-content">
|
||||
@if (epgItems().length > 0) {
|
||||
<app-epg-view [epgItems]="epgItems()" />
|
||||
}
|
||||
|
||||
@if (isLoadingEpg()) {
|
||||
<div class="loading-indicator">
|
||||
<mat-spinner diameter="24" />
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (hasMoreEpg() && !isLoadingEpg()) {
|
||||
<div class="load-more-epg">
|
||||
<button mat-button (click)="onLoadMoreEpg()">
|
||||
{{ 'CHANNELS.LOAD_MORE' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<app-portal-empty-state
|
||||
icon="live_tv"
|
||||
[message]="'CHANNELS.SELECT_CHANNEL_PLAYBACK' | translate"
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
-81
@@ -1,81 +0,0 @@
|
||||
@use '@angular/material' as mat;
|
||||
@use '../../styles/portal-layout' as portal;
|
||||
|
||||
@include portal.live-layout;
|
||||
|
||||
// ─── Sidebar panel header ─────────────────────────────────────────────────────
|
||||
.category-content-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
min-height: 48px;
|
||||
flex-shrink: 0;
|
||||
background: var(--app-widget-header-bg, var(--mat-sys-surface-container-high));
|
||||
border-bottom: 1px solid var(--app-separator);
|
||||
app-region: drag;
|
||||
|
||||
// Own buttons (direct children scoped to this component)
|
||||
button {
|
||||
@include mat.form-field-density(-5);
|
||||
app-region: no-drag;
|
||||
}
|
||||
|
||||
// Projected buttons (liveHeaderActions slot) — view encapsulation
|
||||
// prevents the rule above from reaching them, so ng-deep is required.
|
||||
::ng-deep button {
|
||||
app-region: no-drag;
|
||||
}
|
||||
}
|
||||
|
||||
.category-meta {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
// ─── Title + count chip — now aligned with category-content-view ─────────────
|
||||
.category-title {
|
||||
margin: 0;
|
||||
font-size: 0.94rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.01em;
|
||||
color: var(--mat-sys-on-surface);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
// Informational badge — primary-tinted pill with definition ring
|
||||
.category-subtitle {
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
font-size: 0.70rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.05em;
|
||||
text-transform: capitalize;
|
||||
line-height: 1;
|
||||
color: var(--mat-sys-primary);
|
||||
background: color-mix(in srgb, var(--mat-sys-primary) 10%, var(--mat-sys-surface-container-high));
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
box-shadow:
|
||||
inset 0 0 0 1px color-mix(in srgb, var(--mat-sys-primary) 30%, transparent),
|
||||
0 1px 3px rgba(0, 0, 0, 0.18);
|
||||
}
|
||||
|
||||
// ─── EPG controls ─────────────────────────────────────────────────────────────
|
||||
.loading-indicator {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.load-more-epg {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 8px 0;
|
||||
}
|
||||
-47
@@ -1,47 +0,0 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
input,
|
||||
output,
|
||||
} from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { ResizableDirective } from '@iptvnator/ui/components';
|
||||
import { EpgItem } from '@iptvnator/shared/interfaces';
|
||||
import { WebPlayerViewComponent } from '@iptvnator/ui/playback';
|
||||
import { EpgViewComponent } from '@iptvnator/ui/shared-portals';
|
||||
import { PortalEmptyStateComponent } from '../portal-empty-state/portal-empty-state.component';
|
||||
|
||||
@Component({
|
||||
selector: 'app-portal-collection-live-shell',
|
||||
imports: [
|
||||
EpgViewComponent,
|
||||
MatButtonModule,
|
||||
MatProgressSpinnerModule,
|
||||
PortalEmptyStateComponent,
|
||||
ResizableDirective,
|
||||
TranslatePipe,
|
||||
WebPlayerViewComponent,
|
||||
],
|
||||
templateUrl: './portal-collection-live-shell.component.html',
|
||||
styleUrl: './portal-collection-live-shell.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class PortalCollectionLiveShellComponent {
|
||||
readonly title = input<string>('Favorites');
|
||||
readonly subtitle = input<string>('Live TV');
|
||||
readonly count = input<number>(0);
|
||||
readonly streamUrl = input<string>('');
|
||||
readonly isEmbeddedPlayer = input<boolean>(true);
|
||||
readonly hasSelection = input<boolean>(false);
|
||||
readonly epgItems = input<EpgItem[]>([]);
|
||||
readonly isLoadingEpg = input<boolean>(false);
|
||||
readonly hasMoreEpg = input<boolean>(false);
|
||||
|
||||
readonly loadMoreEpgClicked = output<void>();
|
||||
|
||||
onLoadMoreEpg(): void {
|
||||
this.loadMoreEpgClicked.emit();
|
||||
}
|
||||
}
|
||||
+4
-2
@@ -19,10 +19,12 @@ import {
|
||||
OPEN_COLLECTION_DETAIL_STATE_KEY,
|
||||
ScopeToggleService,
|
||||
UnifiedCollectionItem,
|
||||
UnifiedFavoritesDataService,
|
||||
UnifiedRecentDataService,
|
||||
WorkspaceViewCommandService,
|
||||
} from '@iptvnator/portal/shared/util';
|
||||
import {
|
||||
UnifiedFavoritesDataService,
|
||||
UnifiedRecentDataService,
|
||||
} from '@iptvnator/portal/shared/data-access';
|
||||
import { selectAllPlaylistsMeta, selectPlaylistsLoadingFlag } from '@iptvnator/m3u-state';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { PlaylistMeta } from '@iptvnator/shared/interfaces';
|
||||
|
||||
+4
-2
@@ -48,10 +48,12 @@ import {
|
||||
ScopeToggleService,
|
||||
STALKER_RETURN_TO_STATE_KEY,
|
||||
UnifiedCollectionItem,
|
||||
UnifiedFavoritesDataService,
|
||||
UnifiedRecentDataService,
|
||||
WorkspaceViewCommandService,
|
||||
} from '@iptvnator/portal/shared/util';
|
||||
import {
|
||||
UnifiedFavoritesDataService,
|
||||
UnifiedRecentDataService,
|
||||
} from '@iptvnator/portal/shared/data-access';
|
||||
import { selectAllPlaylistsMeta, selectPlaylistsLoadingFlag } from '@iptvnator/m3u-state';
|
||||
import { EmptyStateComponent } from '@iptvnator/playlist/shared/ui';
|
||||
import { UnifiedLiveTabComponent } from './unified-live-tab.component';
|
||||
|
||||
+4
-2
@@ -40,11 +40,13 @@ import {
|
||||
DEFAULT_FAVORITES_CHANNEL_SORT_MODE,
|
||||
PORTAL_PLAYER,
|
||||
FavoritesChannelSortMode,
|
||||
StreamResolverService,
|
||||
UnifiedCollectionItem,
|
||||
UnifiedFavoriteChannel,
|
||||
UnifiedRecentDataService,
|
||||
} from '@iptvnator/portal/shared/util';
|
||||
import {
|
||||
StreamResolverService,
|
||||
UnifiedRecentDataService,
|
||||
} from '@iptvnator/portal/shared/data-access';
|
||||
|
||||
@Directive({
|
||||
selector: '[appResizable]',
|
||||
|
||||
+5
-3
@@ -22,13 +22,15 @@ import {
|
||||
OpenLiveCollectionItemState,
|
||||
PORTAL_PLAYER,
|
||||
persistLiveEpgPanelState,
|
||||
ResolvedLiveCollectionDetail,
|
||||
restoreLiveEpgPanelState,
|
||||
StreamResolverService,
|
||||
UnifiedCollectionItem,
|
||||
UnifiedFavoriteChannel,
|
||||
UnifiedRecentDataService,
|
||||
} from '@iptvnator/portal/shared/util';
|
||||
import {
|
||||
ResolvedLiveCollectionDetail,
|
||||
StreamResolverService,
|
||||
UnifiedRecentDataService,
|
||||
} from '@iptvnator/portal/shared/data-access';
|
||||
import {
|
||||
EpgDateNavigationDirection,
|
||||
EpgListComponent,
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
export * from './unified-collection-item.interface';
|
||||
export * from './unified-favorite-channel.interface';
|
||||
export * from './collection-helpers';
|
||||
export * from './scope-toggle.service';
|
||||
export * from './unified-favorites-data.service';
|
||||
export * from './unified-recent-data.service';
|
||||
export * from './stream-resolver.service';
|
||||
@@ -1,19 +1,22 @@
|
||||
import { Injectable, signal } from '@angular/core';
|
||||
|
||||
export type CollectionScope = 'playlist' | 'all';
|
||||
type CollectionScopeSignal = ReturnType<typeof signal<CollectionScope>>;
|
||||
|
||||
const STORAGE_PREFIX = 'collection-scope-';
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class ScopeToggleService {
|
||||
private readonly scopes = new Map<string, ReturnType<typeof signal<CollectionScope>>>();
|
||||
private readonly scopes = new Map<string, CollectionScopeSignal>();
|
||||
|
||||
getScope(viewKey: string): ReturnType<typeof signal<CollectionScope>> {
|
||||
if (!this.scopes.has(viewKey)) {
|
||||
getScope(viewKey: string): CollectionScopeSignal {
|
||||
let scope = this.scopes.get(viewKey);
|
||||
if (!scope) {
|
||||
const persisted = this.readFromStorage(viewKey);
|
||||
this.scopes.set(viewKey, signal<CollectionScope>(persisted));
|
||||
scope = signal<CollectionScope>(persisted);
|
||||
this.scopes.set(viewKey, scope);
|
||||
}
|
||||
return this.scopes.get(viewKey)!;
|
||||
return scope;
|
||||
}
|
||||
|
||||
setScope(viewKey: string, value: CollectionScope): void {
|
||||
|
||||
@@ -7,15 +7,11 @@ import {
|
||||
getCollectionViewState,
|
||||
getOpenCollectionDetailItemState,
|
||||
getOpenLiveCollectionItemState,
|
||||
getOpenStalkerItemState,
|
||||
getRecentItemNavigation,
|
||||
getStalkerReturnToState,
|
||||
getUnifiedCollectionNavigation,
|
||||
OPEN_COLLECTION_DETAIL_STATE_KEY,
|
||||
matchesOpenLiveCollectionItem,
|
||||
OPEN_LIVE_COLLECTION_ITEM_STATE_KEY,
|
||||
OPEN_STALKER_ITEM_STATE_KEY,
|
||||
STALKER_RETURN_TO_STATE_KEY,
|
||||
} from './workspace-portal-navigation';
|
||||
|
||||
describe('workspace-portal-navigation', () => {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
// ─── Shared mixin for live-stream portal layouts ──────────────────────────────
|
||||
// Used by: xtream live-stream-layout, stalker live-stream-layout,
|
||||
// portal-collection-live-shell
|
||||
// Used by: xtream live-stream-layout and stalker live-stream-layout.
|
||||
//
|
||||
// Include this mixin in a component's SCSS to get the shared flex host+sidebar
|
||||
// skeleton, then add only the component-specific rules underneath.
|
||||
|
||||
@@ -24,6 +24,9 @@
|
||||
"@iptvnator/portal/shared/ui": [
|
||||
"libs/portal/shared/ui/src/index.ts"
|
||||
],
|
||||
"@iptvnator/portal/shared/data-access": [
|
||||
"libs/portal/shared/data-access/src/index.ts"
|
||||
],
|
||||
"@iptvnator/portal/shared/util": [
|
||||
"libs/portal/shared/util/src/index.ts"
|
||||
],
|
||||
|
||||
Reference in new issue
Block a user