mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 09:01:03 -08:00
refactor(workspace-shell): split 1666-line facade into helpers and sub-services
The WorkspaceShellFacade had grown to 1666 lines, well past the 300/400
LOC target in CLAUDE.md. The component template binds to 54 facade
members directly, so the split preserves the public API by reference
re-export and keeps the template untouched.
Phase 1 — extract pure helpers (no DI, no behavior change):
- helpers/workspace-shell-constants.ts: i18n keys, RAIL_TOOLTIP_KEYS,
XtreamImportPhaseTone, WorkspaceHeaderBulkAction
- helpers/workspace-shell-route-utils.ts: toQueryString,
getRouteQueryParam, syncSearchQueryParam, bumpRefreshQueryParam,
getProviderFromPlaylist
- helpers/workspace-shell-search-labels.ts: resolveSearchPlaceholderKey,
resolveSearchScopeLabel, resolveActiveCategoryLabel, getRailTooltipKey,
translateRailLinks (each takes TranslateFn + snapshots as params)
- helpers/workspace-shell-import-labels.ts: build* helpers for the 14
Xtream import overlay labels and formatLocalizedNumber
- helpers/workspace-shell-command-builders.ts: buildCommandPaletteItems
plus the three command-definition builders, navigation command
factory, resolveCommand, comparePaletteCommands, getCommandGroupOrder
Phase 2 — extract the two component-scoped sub-services with the
cleanest boundaries:
- WorkspaceShellXtreamImportService: read-only labels, cancel action,
isImportRunning. Reads only XtreamStore + TranslateService.
- WorkspaceShellCommandPaletteService: MatDialog lifecycle, recent
commands integration, eager WorkspacePlayerCommandsContributor
bootstrap. Receives a typed CommandBuilderContext via function
argument from the parent — no circular DI.
The parent facade re-exposes every member by reference assignment so
signal identity is preserved for change detection. workspace-shell.
component.html is unchanged. workspace-shell.component.ts gains only
two provider entries for the new sub-services.
Verification:
- workspace-shell.facade.spec.ts: 20/20 passing
- pnpm nx lint workspace-shell-feature: clean
- workspace-shell-feature suite: 53/60 passing — same 7 pre-existing
failures (header & shell component specs hit unrelated NG0301
on playlistDropZone export); no regressions introduced
- CDP smoke: command palette opens with Recently Used + Global
Actions groups, settings nav disables search bar correctly
Facade reduces from 1666 to 917 lines. Each new helper/service is
under 250 lines except command-builders.ts (462) which is mostly
declarative command definitions. The parent facade is still over the
400-line hard max because cross-cutting orchestration
(searchCapability, runHeaderBulkAction, rail link computeds) reads
from multiple clusters; further reduction would require moving logic
into child components, which is deferred per the plan.
Plan: ~/.claude/plans/workspace-shell-facade-ts-is-now-1668-crispy-planet.md
(also copied locally to .plans/2026-05-01-workspace-shell-facade-split.md;
the working-tree .gitignore change excludes .plans from tracking).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Entire-Checkpoint: c2cea9c530e6
This commit is contained in:
1 parent
2f47ed16cb
commit
d1cd805622
10 files changed
+1194
-861
No files matched your search
+462
@@ -0,0 +1,462 @@
|
||||
import { Router } from '@angular/router';
|
||||
import {
|
||||
PortalRailSection,
|
||||
WorkspaceCommandContribution,
|
||||
WorkspaceResolvedCommandItem,
|
||||
} from '@iptvnator/portal/shared/util';
|
||||
import {
|
||||
WorkspacePortalContext,
|
||||
WorkspaceShellRoute,
|
||||
} from '@iptvnator/workspace/shell/util';
|
||||
import { TranslateFn } from './workspace-shell-search-labels';
|
||||
|
||||
export interface CommandBuilderActions {
|
||||
openPlaylistSearch: (query: string) => void;
|
||||
refreshCurrentPlaylist: () => void;
|
||||
openPlaylistInfo: () => void;
|
||||
openAccountInfo: () => void;
|
||||
openGlobalSearch: (query: string) => void;
|
||||
navigateToGlobalFavorites: () => void;
|
||||
openGlobalRecent: () => void;
|
||||
openDownloadsShortcut: () => void;
|
||||
openAddPlaylistDialog: (kind?: 'url' | 'xtream' | 'stalker') => void;
|
||||
}
|
||||
|
||||
export interface CommandBuilderContext {
|
||||
route: WorkspaceShellRoute;
|
||||
context: WorkspacePortalContext | null;
|
||||
section: PortalRailSection | null;
|
||||
hasActivePlaylist: boolean;
|
||||
hasXtreamPlaylists: boolean;
|
||||
canRefreshPlaylist: boolean;
|
||||
isElectron: boolean;
|
||||
showDashboard: boolean;
|
||||
translate: TranslateFn;
|
||||
router: Router;
|
||||
actions: CommandBuilderActions;
|
||||
}
|
||||
|
||||
export function buildCommandPaletteItems(
|
||||
ctx: CommandBuilderContext,
|
||||
viewCommands: WorkspaceCommandContribution[]
|
||||
): WorkspaceResolvedCommandItem[] {
|
||||
return [
|
||||
...getViewCommandDefinitions(ctx),
|
||||
...getPlaylistCommandDefinitions(ctx),
|
||||
...getGlobalCommandDefinitions(ctx),
|
||||
...viewCommands,
|
||||
]
|
||||
.map((command) => resolveCommand(command, ctx.translate))
|
||||
.filter((command) => command.visible)
|
||||
.sort(comparePaletteCommands);
|
||||
}
|
||||
|
||||
export function getViewCommandDefinitions(
|
||||
ctx: CommandBuilderContext
|
||||
): WorkspaceCommandContribution[] {
|
||||
const { context, section } = ctx;
|
||||
|
||||
if (!context || !section) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (context.provider === 'playlists') {
|
||||
return getM3uNavigationCommands(ctx, context, section);
|
||||
}
|
||||
|
||||
if (context.provider === 'xtreams' || context.provider === 'stalker') {
|
||||
return getPortalNavigationCommands(ctx, context, section);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
export function getPlaylistCommandDefinitions(
|
||||
ctx: CommandBuilderContext
|
||||
): WorkspaceCommandContribution[] {
|
||||
const { context } = ctx.route;
|
||||
|
||||
if (
|
||||
!context ||
|
||||
(context.provider !== 'xtreams' &&
|
||||
context.provider !== 'stalker' &&
|
||||
context.provider !== 'playlists')
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const canOpenPlaylistSearch =
|
||||
context.provider === 'xtreams' || context.provider === 'stalker';
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'playlist-search',
|
||||
group: 'playlist',
|
||||
icon: 'playlist_play',
|
||||
labelKey: 'WORKSPACE.SHELL.COMMANDS.PLAYLIST_SEARCH_LABEL',
|
||||
descriptionKey:
|
||||
'WORKSPACE.SHELL.COMMANDS.PLAYLIST_SEARCH_DESCRIPTION',
|
||||
priority: 10,
|
||||
visible: canOpenPlaylistSearch,
|
||||
run: ({ query }) => ctx.actions.openPlaylistSearch(query),
|
||||
},
|
||||
{
|
||||
id: 'refresh-playlist',
|
||||
group: 'playlist',
|
||||
icon: 'sync',
|
||||
labelKey: 'WORKSPACE.SHELL.COMMANDS.REFRESH_PLAYLIST_LABEL',
|
||||
descriptionKey:
|
||||
'WORKSPACE.SHELL.COMMANDS.REFRESH_PLAYLIST_DESCRIPTION',
|
||||
priority: 20,
|
||||
visible: ctx.canRefreshPlaylist,
|
||||
run: () => ctx.actions.refreshCurrentPlaylist(),
|
||||
},
|
||||
{
|
||||
id: 'playlist-info',
|
||||
group: 'playlist',
|
||||
icon: 'info',
|
||||
labelKey: 'WORKSPACE.SHELL.COMMANDS.PLAYLIST_INFO_LABEL',
|
||||
descriptionKey:
|
||||
'WORKSPACE.SHELL.COMMANDS.PLAYLIST_INFO_DESCRIPTION',
|
||||
priority: 30,
|
||||
visible: ctx.hasActivePlaylist,
|
||||
run: () => ctx.actions.openPlaylistInfo(),
|
||||
},
|
||||
{
|
||||
id: 'account-info',
|
||||
group: 'playlist',
|
||||
icon: 'account_circle',
|
||||
labelKey: 'WORKSPACE.SHELL.COMMANDS.ACCOUNT_INFO_LABEL',
|
||||
descriptionKey:
|
||||
'WORKSPACE.SHELL.COMMANDS.ACCOUNT_INFO_DESCRIPTION',
|
||||
priority: 40,
|
||||
visible: context.provider === 'xtreams',
|
||||
run: () => ctx.actions.openAccountInfo(),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
export function getGlobalCommandDefinitions(
|
||||
ctx: CommandBuilderContext
|
||||
): WorkspaceCommandContribution[] {
|
||||
const { route, hasXtreamPlaylists, isElectron, showDashboard, actions } =
|
||||
ctx;
|
||||
|
||||
return [
|
||||
{
|
||||
id: 'global-search',
|
||||
group: 'global',
|
||||
icon: 'search',
|
||||
labelKey: 'WORKSPACE.SHELL.COMMANDS.GLOBAL_SEARCH_LABEL',
|
||||
descriptionKey:
|
||||
'WORKSPACE.SHELL.COMMANDS.GLOBAL_SEARCH_DESCRIPTION',
|
||||
priority: 10,
|
||||
visible: hasXtreamPlaylists,
|
||||
keywords: ['xtream'],
|
||||
run: ({ query }) => actions.openGlobalSearch(query),
|
||||
},
|
||||
{
|
||||
id: 'open-global-favorites',
|
||||
group: 'global',
|
||||
icon: 'star',
|
||||
labelKey: 'WORKSPACE.SHELL.COMMANDS.OPEN_GLOBAL_FAVORITES_LABEL',
|
||||
descriptionKey:
|
||||
'WORKSPACE.SHELL.COMMANDS.OPEN_GLOBAL_FAVORITES_DESCRIPTION',
|
||||
priority: 20,
|
||||
visible: route.kind !== 'global-favorites',
|
||||
run: () => actions.navigateToGlobalFavorites(),
|
||||
},
|
||||
{
|
||||
id: 'open-global-recent',
|
||||
group: 'global',
|
||||
icon: 'history',
|
||||
labelKey: 'WORKSPACE.SHELL.COMMANDS.OPEN_GLOBAL_RECENT_LABEL',
|
||||
descriptionKey:
|
||||
'WORKSPACE.SHELL.COMMANDS.OPEN_GLOBAL_RECENT_DESCRIPTION',
|
||||
priority: 30,
|
||||
visible: route.kind !== 'global-recent',
|
||||
run: () => actions.openGlobalRecent(),
|
||||
},
|
||||
{
|
||||
id: 'open-downloads',
|
||||
group: 'global',
|
||||
icon: 'download',
|
||||
labelKey: 'WORKSPACE.SHELL.COMMANDS.OPEN_DOWNLOADS_LABEL',
|
||||
descriptionKey:
|
||||
'WORKSPACE.SHELL.COMMANDS.OPEN_DOWNLOADS_DESCRIPTION',
|
||||
priority: 40,
|
||||
visible: isElectron && route.kind !== 'downloads',
|
||||
run: () => actions.openDownloadsShortcut(),
|
||||
},
|
||||
{
|
||||
id: 'open-settings',
|
||||
group: 'global',
|
||||
icon: 'settings',
|
||||
labelKey: 'WORKSPACE.SHELL.COMMANDS.OPEN_SETTINGS_LABEL',
|
||||
descriptionKey:
|
||||
'WORKSPACE.SHELL.COMMANDS.OPEN_SETTINGS_DESCRIPTION',
|
||||
priority: 50,
|
||||
visible: route.kind !== 'settings',
|
||||
run: () => {
|
||||
void ctx.router.navigate(['/workspace', 'settings']);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'open-sources',
|
||||
group: 'global',
|
||||
icon: 'library_books',
|
||||
labelKey: 'WORKSPACE.SHELL.COMMANDS.OPEN_SOURCES_LABEL',
|
||||
descriptionKey:
|
||||
'WORKSPACE.SHELL.COMMANDS.OPEN_SOURCES_DESCRIPTION',
|
||||
priority: 60,
|
||||
visible: route.kind !== 'sources',
|
||||
run: () => {
|
||||
void ctx.router.navigate(['/workspace', 'sources']);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'open-dashboard',
|
||||
group: 'global',
|
||||
icon: 'dashboard',
|
||||
labelKey: 'WORKSPACE.SHELL.COMMANDS.OPEN_DASHBOARD_LABEL',
|
||||
descriptionKey:
|
||||
'WORKSPACE.SHELL.COMMANDS.OPEN_DASHBOARD_DESCRIPTION',
|
||||
priority: 70,
|
||||
visible: showDashboard && route.kind !== 'dashboard',
|
||||
run: () => {
|
||||
void ctx.router.navigate(['/workspace', 'dashboard']);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'add-playlist',
|
||||
group: 'global',
|
||||
icon: 'add_circle_outline',
|
||||
labelKey: 'WORKSPACE.SHELL.COMMANDS.ADD_PLAYLIST_LABEL',
|
||||
descriptionKey:
|
||||
'WORKSPACE.SHELL.COMMANDS.ADD_PLAYLIST_DESCRIPTION',
|
||||
priority: 80,
|
||||
run: () => actions.openAddPlaylistDialog(),
|
||||
},
|
||||
{
|
||||
id: 'add-playlist-m3u',
|
||||
group: 'global',
|
||||
icon: 'folder_open',
|
||||
labelKey: 'WORKSPACE.SHELL.COMMANDS.ADD_PLAYLIST_M3U_LABEL',
|
||||
descriptionKey:
|
||||
'WORKSPACE.SHELL.COMMANDS.ADD_PLAYLIST_M3U_DESCRIPTION',
|
||||
keywords: ['m3u', 'm3u8', 'file', 'url', 'add', 'import'],
|
||||
priority: 79,
|
||||
run: () => actions.openAddPlaylistDialog('url'),
|
||||
},
|
||||
{
|
||||
id: 'add-playlist-xtream',
|
||||
group: 'global',
|
||||
icon: 'cloud',
|
||||
labelKey: 'WORKSPACE.SHELL.COMMANDS.ADD_PLAYLIST_XTREAM_LABEL',
|
||||
descriptionKey:
|
||||
'WORKSPACE.SHELL.COMMANDS.ADD_PLAYLIST_XTREAM_DESCRIPTION',
|
||||
keywords: ['xtream', 'codes', 'iptv', 'add', 'import'],
|
||||
priority: 78,
|
||||
run: () => actions.openAddPlaylistDialog('xtream'),
|
||||
},
|
||||
{
|
||||
id: 'add-playlist-stalker',
|
||||
group: 'global',
|
||||
icon: 'cast',
|
||||
labelKey: 'WORKSPACE.SHELL.COMMANDS.ADD_PLAYLIST_STALKER_LABEL',
|
||||
descriptionKey:
|
||||
'WORKSPACE.SHELL.COMMANDS.ADD_PLAYLIST_STALKER_DESCRIPTION',
|
||||
keywords: ['stalker', 'portal', 'mac', 'ministra', 'add'],
|
||||
priority: 77,
|
||||
run: () => actions.openAddPlaylistDialog('stalker'),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
function getPortalNavigationCommands(
|
||||
ctx: CommandBuilderContext,
|
||||
context: WorkspacePortalContext,
|
||||
section: PortalRailSection
|
||||
): WorkspaceCommandContribution[] {
|
||||
const liveSection = context.provider === 'stalker' ? 'itv' : 'live';
|
||||
|
||||
return [
|
||||
createNavigationCommand(ctx, {
|
||||
id: 'go-to-vod',
|
||||
context,
|
||||
targetSection: 'vod',
|
||||
currentSection: section,
|
||||
icon: 'movie',
|
||||
labelKey: 'WORKSPACE.SHELL.RAIL_MOVIES',
|
||||
priority: 100,
|
||||
}),
|
||||
createNavigationCommand(ctx, {
|
||||
id: 'go-to-live',
|
||||
context,
|
||||
targetSection: liveSection,
|
||||
currentSection: section,
|
||||
icon: 'live_tv',
|
||||
labelKey: 'WORKSPACE.SHELL.RAIL_LIVE',
|
||||
priority: 110,
|
||||
}),
|
||||
createNavigationCommand(ctx, {
|
||||
id: 'go-to-series',
|
||||
context,
|
||||
targetSection: 'series',
|
||||
currentSection: section,
|
||||
icon: 'video_library',
|
||||
labelKey: 'WORKSPACE.SHELL.RAIL_SERIES',
|
||||
priority: 120,
|
||||
}),
|
||||
].filter(
|
||||
(command): command is WorkspaceCommandContribution => command !== null
|
||||
);
|
||||
}
|
||||
|
||||
function getM3uNavigationCommands(
|
||||
ctx: CommandBuilderContext,
|
||||
context: WorkspacePortalContext,
|
||||
section: PortalRailSection
|
||||
): WorkspaceCommandContribution[] {
|
||||
return [
|
||||
createNavigationCommand(ctx, {
|
||||
id: 'go-to-all',
|
||||
context,
|
||||
targetSection: 'all',
|
||||
currentSection: section,
|
||||
icon: 'format_list_bulleted',
|
||||
labelKey: 'WORKSPACE.SHELL.RAIL_ALL_CHANNELS',
|
||||
priority: 100,
|
||||
}),
|
||||
createNavigationCommand(ctx, {
|
||||
id: 'go-to-groups',
|
||||
context,
|
||||
targetSection: 'groups',
|
||||
currentSection: section,
|
||||
icon: 'folder_open',
|
||||
labelKey: 'WORKSPACE.SHELL.RAIL_GROUPS',
|
||||
priority: 110,
|
||||
}),
|
||||
createNavigationCommand(ctx, {
|
||||
id: 'go-to-favorites',
|
||||
context,
|
||||
targetSection: 'favorites',
|
||||
currentSection: section,
|
||||
icon: 'star',
|
||||
labelKey: 'WORKSPACE.SHELL.RAIL_FAVORITES',
|
||||
priority: 120,
|
||||
}),
|
||||
createNavigationCommand(ctx, {
|
||||
id: 'go-to-recent',
|
||||
context,
|
||||
targetSection: 'recent',
|
||||
currentSection: section,
|
||||
icon: 'history',
|
||||
labelKey: 'WORKSPACE.SHELL.RAIL_RECENT',
|
||||
priority: 130,
|
||||
}),
|
||||
].filter(
|
||||
(command): command is WorkspaceCommandContribution => command !== null
|
||||
);
|
||||
}
|
||||
|
||||
function createNavigationCommand(
|
||||
ctx: CommandBuilderContext,
|
||||
config: {
|
||||
id: string;
|
||||
context: WorkspacePortalContext;
|
||||
targetSection: string;
|
||||
currentSection: PortalRailSection;
|
||||
icon: string;
|
||||
labelKey: string;
|
||||
priority: number;
|
||||
}
|
||||
): WorkspaceCommandContribution | null {
|
||||
if (config.currentSection === config.targetSection) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: config.id,
|
||||
group: 'view',
|
||||
icon: config.icon,
|
||||
labelKey: config.labelKey,
|
||||
descriptionKey: 'WORKSPACE.SHELL.COMMANDS.OPEN_VIEW_DESCRIPTION',
|
||||
descriptionParams: () => ({
|
||||
view: ctx.translate(config.labelKey),
|
||||
}),
|
||||
priority: config.priority,
|
||||
run: () => {
|
||||
void ctx.router.navigate([
|
||||
'/workspace',
|
||||
config.context.provider,
|
||||
config.context.playlistId,
|
||||
config.targetSection,
|
||||
]);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveCommand(
|
||||
command: WorkspaceCommandContribution,
|
||||
translate: TranslateFn
|
||||
): WorkspaceResolvedCommandItem {
|
||||
const labelParams = resolveCommandValue(command.labelParams);
|
||||
const descriptionParams = resolveCommandValue(command.descriptionParams);
|
||||
|
||||
return {
|
||||
id: command.id,
|
||||
group: command.group,
|
||||
icon: command.icon,
|
||||
label: translate(command.labelKey, labelParams),
|
||||
description: command.descriptionKey
|
||||
? translate(command.descriptionKey, descriptionParams)
|
||||
: '',
|
||||
keywords: resolveCommandValue(command.keywords) ?? [],
|
||||
priority: command.priority ?? 100,
|
||||
visible: resolveCommandValue(command.visible) ?? true,
|
||||
enabled: resolveCommandValue(command.enabled) ?? true,
|
||||
run: command.run,
|
||||
};
|
||||
}
|
||||
|
||||
export function comparePaletteCommands(
|
||||
left: WorkspaceResolvedCommandItem,
|
||||
right: WorkspaceResolvedCommandItem
|
||||
): number {
|
||||
const groupOrder =
|
||||
getCommandGroupOrder(left.group) - getCommandGroupOrder(right.group);
|
||||
|
||||
if (groupOrder !== 0) {
|
||||
return groupOrder;
|
||||
}
|
||||
|
||||
if (left.priority !== right.priority) {
|
||||
return left.priority - right.priority;
|
||||
}
|
||||
|
||||
return left.label.localeCompare(right.label);
|
||||
}
|
||||
|
||||
export function getCommandGroupOrder(
|
||||
group: WorkspaceResolvedCommandItem['group']
|
||||
): number {
|
||||
switch (group) {
|
||||
case 'view':
|
||||
return 0;
|
||||
case 'playlist':
|
||||
return 1;
|
||||
default:
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveCommandValue<T>(
|
||||
value: T | (() => T | undefined) | undefined
|
||||
): T | undefined {
|
||||
if (typeof value === 'function') {
|
||||
return (value as () => T | undefined)();
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
import { PortalRailSection } from '@iptvnator/portal/shared/util';
|
||||
|
||||
export const SEARCH_INPUT_DEBOUNCE_MS = 350;
|
||||
|
||||
export const SEARCH_PLAYLIST_PLACEHOLDER =
|
||||
'WORKSPACE.SHELL.SEARCH_PLAYLIST_PLACEHOLDER';
|
||||
export const SEARCH_SECTION_PLACEHOLDER =
|
||||
'WORKSPACE.SHELL.SEARCH_SECTION_PLACEHOLDER';
|
||||
export const FILTER_SECTION_PLACEHOLDER =
|
||||
'WORKSPACE.SHELL.FILTER_SECTION_PLACEHOLDER';
|
||||
export const SEARCH_SOURCES_PLACEHOLDER =
|
||||
'WORKSPACE.SHELL.SEARCH_SOURCES_PLACEHOLDER';
|
||||
export const SEARCH_LOADED_ONLY_STATUS =
|
||||
'WORKSPACE.SHELL.SEARCH_STATUS_LOADED_ONLY';
|
||||
|
||||
export const CLEAR_RECENTLY_VIEWED_TOOLTIP =
|
||||
'WORKSPACE.SHELL.CLEAR_RECENTLY_VIEWED_SECTION';
|
||||
export const CLEAR_RECENTLY_VIEWED_ARIA =
|
||||
'WORKSPACE.SHELL.CLEAR_RECENTLY_VIEWED_SECTION_ARIA';
|
||||
|
||||
export const RAIL_TOOLTIP_KEYS: Readonly<
|
||||
Partial<Record<PortalRailSection, string>>
|
||||
> = {
|
||||
vod: 'WORKSPACE.SHELL.RAIL_MOVIES',
|
||||
live: 'WORKSPACE.SHELL.RAIL_LIVE',
|
||||
itv: 'WORKSPACE.SHELL.RAIL_LIVE',
|
||||
series: 'WORKSPACE.SHELL.RAIL_SERIES',
|
||||
'recently-added': 'WORKSPACE.SHELL.RAIL_RECENTLY_ADDED',
|
||||
search: 'WORKSPACE.SHELL.RAIL_SEARCH',
|
||||
recent: 'WORKSPACE.SHELL.RAIL_RECENT',
|
||||
favorites: 'WORKSPACE.SHELL.RAIL_FAVORITES',
|
||||
downloads: 'WORKSPACE.SHELL.RAIL_DOWNLOADS',
|
||||
all: 'WORKSPACE.SHELL.RAIL_ALL_CHANNELS',
|
||||
groups: 'WORKSPACE.SHELL.RAIL_GROUPS',
|
||||
};
|
||||
|
||||
export type XtreamImportPhaseTone = 'remote' | 'local' | null;
|
||||
|
||||
export interface WorkspaceHeaderBulkAction {
|
||||
icon: string;
|
||||
tooltip: string;
|
||||
ariaLabel: string;
|
||||
disabled: boolean;
|
||||
}
|
||||
+119
@@ -0,0 +1,119 @@
|
||||
import { XtreamImportPhaseTone } from './workspace-shell-constants';
|
||||
import { TranslateFn } from './workspace-shell-search-labels';
|
||||
|
||||
export type XtreamImportContentType = 'live' | 'vod' | 'series' | null;
|
||||
|
||||
export function buildXtreamImportTypeLabel(
|
||||
contentType: XtreamImportContentType,
|
||||
translate: TranslateFn
|
||||
): string {
|
||||
switch (contentType) {
|
||||
case 'live':
|
||||
return translate('WORKSPACE.SHELL.RAIL_LIVE');
|
||||
case 'vod':
|
||||
return translate('WORKSPACE.SHELL.RAIL_MOVIES');
|
||||
case 'series':
|
||||
return translate('WORKSPACE.SHELL.RAIL_SERIES');
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export function buildXtreamImportPhaseTone(
|
||||
phase: string | null | undefined
|
||||
): XtreamImportPhaseTone {
|
||||
switch (phase) {
|
||||
case 'loading-categories':
|
||||
case 'loading-live':
|
||||
case 'loading-movies':
|
||||
case 'loading-series':
|
||||
return 'remote';
|
||||
case 'preparing-content':
|
||||
case 'saving-categories':
|
||||
case 'saving-content':
|
||||
case 'restoring-favorites':
|
||||
case 'restoring-recently-viewed':
|
||||
return 'local';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function buildXtreamImportSourceLabel(
|
||||
tone: XtreamImportPhaseTone,
|
||||
translate: TranslateFn
|
||||
): string {
|
||||
if (tone === 'remote') {
|
||||
return translate('WORKSPACE.SHELL.XTREAM_IMPORT_REMOTE_BADGE');
|
||||
}
|
||||
if (tone === 'local') {
|
||||
return translate('WORKSPACE.SHELL.XTREAM_IMPORT_LOCAL_BADGE');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
export function buildXtreamImportPhaseLabel(
|
||||
phase: string | null | undefined,
|
||||
translate: TranslateFn
|
||||
): string {
|
||||
switch (phase) {
|
||||
case 'preparing-content':
|
||||
return translate('WORKSPACE.SHELL.XTREAM_IMPORT_PREPARING');
|
||||
case 'loading-categories':
|
||||
case 'loading-live':
|
||||
case 'loading-movies':
|
||||
case 'loading-series':
|
||||
return translate('WORKSPACE.SHELL.XTREAM_IMPORT_LOADING');
|
||||
case 'saving-categories':
|
||||
case 'saving-content':
|
||||
return translate('WORKSPACE.SHELL.XTREAM_IMPORT_SAVING');
|
||||
case 'restoring-favorites':
|
||||
return translate(
|
||||
'WORKSPACE.SHELL.XTREAM_IMPORT_RESTORING_FAVORITES'
|
||||
);
|
||||
case 'restoring-recently-viewed':
|
||||
return translate('WORKSPACE.SHELL.XTREAM_IMPORT_RESTORING_RECENT');
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export function buildXtreamImportDetailLabel(
|
||||
tone: XtreamImportPhaseTone,
|
||||
translate: TranslateFn
|
||||
): string {
|
||||
if (tone === 'remote') {
|
||||
return translate('WORKSPACE.SHELL.XTREAM_IMPORT_DETAIL_REMOTE');
|
||||
}
|
||||
if (tone === 'local') {
|
||||
return translate('WORKSPACE.SHELL.XTREAM_IMPORT_DETAIL_LOCAL');
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
export function buildXtreamImportProgressLabel(
|
||||
typeLabel: string,
|
||||
currentCount: number,
|
||||
totalCount: number,
|
||||
translate: TranslateFn,
|
||||
formatNumber: (value: number) => string
|
||||
): string {
|
||||
if (!typeLabel || totalCount === 0) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return translate('WORKSPACE.SHELL.XTREAM_IMPORT_PROGRESS', {
|
||||
type: typeLabel,
|
||||
current: formatNumber(currentCount),
|
||||
total: formatNumber(totalCount),
|
||||
});
|
||||
}
|
||||
|
||||
export function formatLocalizedNumber(
|
||||
value: number,
|
||||
currentLang: string | null | undefined,
|
||||
defaultLang: string | null | undefined
|
||||
): string {
|
||||
const locale = currentLang || defaultLang || 'en';
|
||||
return new Intl.NumberFormat(locale).format(value);
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
import { Router } from '@angular/router';
|
||||
import { WorkspacePortalContext } from '@iptvnator/workspace/shell/util';
|
||||
|
||||
export function toQueryString(queryParams: Record<string, unknown>): string {
|
||||
const urlSearchParams = new URLSearchParams();
|
||||
|
||||
Object.entries(queryParams).forEach(([key, value]) => {
|
||||
if (value == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((item) => urlSearchParams.append(key, String(item)));
|
||||
return;
|
||||
}
|
||||
|
||||
urlSearchParams.set(key, String(value));
|
||||
});
|
||||
|
||||
return urlSearchParams.toString();
|
||||
}
|
||||
|
||||
export function getRouteQueryParam(
|
||||
router: Router,
|
||||
currentUrl: string,
|
||||
name: string
|
||||
): string {
|
||||
const value = router.parseUrl(currentUrl).queryParams[name];
|
||||
return typeof value === 'string' ? value : '';
|
||||
}
|
||||
|
||||
export function syncSearchQueryParam(
|
||||
router: Router,
|
||||
currentUrl: string,
|
||||
term: string
|
||||
): boolean {
|
||||
const nextTerm = term.trim();
|
||||
const currentTerm = getRouteQueryParam(router, currentUrl, 'q');
|
||||
if (nextTerm === currentTerm) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const routePath = currentUrl.split('?')[0];
|
||||
const queryParams = {
|
||||
...router.parseUrl(currentUrl).queryParams,
|
||||
};
|
||||
|
||||
if (nextTerm.length > 0) {
|
||||
queryParams['q'] = nextTerm;
|
||||
} else {
|
||||
delete queryParams['q'];
|
||||
}
|
||||
|
||||
const queryString = toQueryString(queryParams);
|
||||
const nextUrl = queryString ? `${routePath}?${queryString}` : routePath;
|
||||
void router.navigateByUrl(nextUrl, { replaceUrl: true });
|
||||
return true;
|
||||
}
|
||||
|
||||
export function bumpRefreshQueryParam(router: Router, currentUrl: string): void {
|
||||
const routePath = currentUrl.split('?')[0];
|
||||
const queryParams = {
|
||||
...router.parseUrl(currentUrl).queryParams,
|
||||
refresh: Date.now().toString(),
|
||||
};
|
||||
|
||||
const queryString = toQueryString(queryParams);
|
||||
const nextUrl = queryString ? `${routePath}?${queryString}` : routePath;
|
||||
void router.navigateByUrl(nextUrl, { replaceUrl: true });
|
||||
}
|
||||
|
||||
export function getProviderFromPlaylist(playlist: {
|
||||
serverUrl?: string;
|
||||
macAddress?: string;
|
||||
}): WorkspacePortalContext['provider'] {
|
||||
if (playlist.serverUrl) {
|
||||
return 'xtreams';
|
||||
}
|
||||
if (playlist.macAddress) {
|
||||
return 'stalker';
|
||||
}
|
||||
return 'playlists';
|
||||
}
|
||||
+159
@@ -0,0 +1,159 @@
|
||||
import {
|
||||
PortalRailLink,
|
||||
PortalRailSection,
|
||||
} from '@iptvnator/portal/shared/util';
|
||||
import {
|
||||
WorkspacePortalContext,
|
||||
WorkspaceShellPageKind,
|
||||
} from '@iptvnator/workspace/shell/util';
|
||||
import {
|
||||
FILTER_SECTION_PLACEHOLDER,
|
||||
RAIL_TOOLTIP_KEYS,
|
||||
SEARCH_PLAYLIST_PLACEHOLDER,
|
||||
SEARCH_SECTION_PLACEHOLDER,
|
||||
SEARCH_SOURCES_PLACEHOLDER,
|
||||
} from './workspace-shell-constants';
|
||||
|
||||
export type TranslateFn = (
|
||||
key: string,
|
||||
params?: Record<string, string | number>
|
||||
) => string;
|
||||
|
||||
export interface SearchScopeContext {
|
||||
kind: WorkspaceShellPageKind;
|
||||
context: WorkspacePortalContext | null;
|
||||
section: PortalRailSection | null;
|
||||
translate: TranslateFn;
|
||||
xtreamCategory:
|
||||
| { category_name?: string; name?: string }
|
||||
| null
|
||||
| undefined;
|
||||
stalkerCategoryName: string;
|
||||
}
|
||||
|
||||
export function getRailTooltipKey(
|
||||
provider: WorkspacePortalContext['provider'],
|
||||
section?: PortalRailSection
|
||||
): string {
|
||||
if (provider === 'xtreams' && section === 'library') {
|
||||
return 'WORKSPACE.SHELL.RAIL_LIBRARY';
|
||||
}
|
||||
|
||||
return (
|
||||
(section ? RAIL_TOOLTIP_KEYS[section] : null) ??
|
||||
'WORKSPACE.SHELL.RAIL_CONTEXT_ACTIONS'
|
||||
);
|
||||
}
|
||||
|
||||
export function translateRailSection(
|
||||
section: PortalRailSection,
|
||||
translate: TranslateFn
|
||||
): string {
|
||||
return translate(getRailTooltipKey('playlists', section));
|
||||
}
|
||||
|
||||
export function translateRailLinks(
|
||||
links: PortalRailLink[],
|
||||
provider: WorkspacePortalContext['provider'],
|
||||
translate: TranslateFn
|
||||
): PortalRailLink[] {
|
||||
return links.map((link) => ({
|
||||
...link,
|
||||
tooltip: translate(getRailTooltipKey(provider, link.section)),
|
||||
}));
|
||||
}
|
||||
|
||||
export function resolveSearchPlaceholderKey(
|
||||
kind: WorkspaceShellPageKind,
|
||||
context: WorkspacePortalContext | null,
|
||||
section: PortalRailSection | null
|
||||
): string {
|
||||
if (kind === 'sources') {
|
||||
return SEARCH_SOURCES_PLACEHOLDER;
|
||||
}
|
||||
|
||||
if (kind === 'dashboard' || section === 'search') {
|
||||
return SEARCH_PLAYLIST_PLACEHOLDER;
|
||||
}
|
||||
|
||||
if (
|
||||
context &&
|
||||
(section === 'vod' ||
|
||||
section === 'series' ||
|
||||
section === 'live' ||
|
||||
section === 'itv')
|
||||
) {
|
||||
return SEARCH_SECTION_PLACEHOLDER;
|
||||
}
|
||||
|
||||
return FILTER_SECTION_PLACEHOLDER;
|
||||
}
|
||||
|
||||
export function resolveActiveCategoryLabel(ctx: SearchScopeContext): string {
|
||||
const { context, section, translate, xtreamCategory, stalkerCategoryName } =
|
||||
ctx;
|
||||
if (!context || !section) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (context.provider === 'xtreams') {
|
||||
return (
|
||||
xtreamCategory?.category_name ??
|
||||
xtreamCategory?.name ??
|
||||
translateRailSection(section, translate)
|
||||
);
|
||||
}
|
||||
|
||||
if (context.provider === 'stalker') {
|
||||
return (
|
||||
stalkerCategoryName.trim() ||
|
||||
translateRailSection(section, translate)
|
||||
);
|
||||
}
|
||||
|
||||
return translateRailSection(section, translate);
|
||||
}
|
||||
|
||||
export function resolveSearchScopeLabel(ctx: SearchScopeContext): string {
|
||||
const { kind, context, section, translate } = ctx;
|
||||
|
||||
if (kind === 'sources') {
|
||||
return translate('WORKSPACE.SHELL.RAIL_SOURCES');
|
||||
}
|
||||
|
||||
if (kind === 'global-favorites') {
|
||||
return translate('HOME.PLAYLISTS.GLOBAL_FAVORITES');
|
||||
}
|
||||
|
||||
if (kind === 'global-recent') {
|
||||
return translate('PORTALS.RECENTLY_VIEWED');
|
||||
}
|
||||
|
||||
if (kind === 'downloads') {
|
||||
return translate('WORKSPACE.SHELL.RAIL_DOWNLOADS');
|
||||
}
|
||||
|
||||
if (kind === 'dashboard' || section === 'search') {
|
||||
return translate('WORKSPACE.SHELL.RAIL_SEARCH');
|
||||
}
|
||||
|
||||
if (!context || !section) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (
|
||||
section === 'vod' ||
|
||||
section === 'series' ||
|
||||
section === 'live' ||
|
||||
section === 'itv'
|
||||
) {
|
||||
const categoryLabel = resolveActiveCategoryLabel(ctx);
|
||||
const sectionLabel = translateRailSection(section, translate);
|
||||
|
||||
return categoryLabel
|
||||
? `${sectionLabel} / ${categoryLabel}`
|
||||
: sectionLabel;
|
||||
}
|
||||
|
||||
return translateRailSection(section, translate);
|
||||
}
|
||||
+91
@@ -0,0 +1,91 @@
|
||||
import { DestroyRef, inject, Injectable } from '@angular/core';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { MatDialog, MatDialogRef } from '@angular/material/dialog';
|
||||
import {
|
||||
WorkspaceCommandSelection,
|
||||
WorkspaceResolvedCommandItem,
|
||||
WorkspaceViewCommandService,
|
||||
} from '@iptvnator/portal/shared/util';
|
||||
import { WorkspaceCommandPaletteComponent } from '../../workspace-command-palette/workspace-command-palette.component';
|
||||
import { RecentCommandsService } from '../../recent-commands';
|
||||
import { WorkspacePlayerCommandsContributor } from '../../workspace-player-commands';
|
||||
import {
|
||||
buildCommandPaletteItems,
|
||||
CommandBuilderContext,
|
||||
} from './helpers/workspace-shell-command-builders';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceShellCommandPaletteService {
|
||||
private readonly dialog = inject(MatDialog);
|
||||
private readonly viewCommands = inject(WorkspaceViewCommandService);
|
||||
private readonly recentCommands = inject(RecentCommandsService);
|
||||
private readonly destroyRef = inject(DestroyRef);
|
||||
// Eager construction registers the player-switch commands via WorkspaceViewCommandService.
|
||||
private readonly _playerCommandsBootstrap = inject(
|
||||
WorkspacePlayerCommandsContributor
|
||||
);
|
||||
|
||||
private commandPaletteRef: MatDialogRef<
|
||||
WorkspaceCommandPaletteComponent,
|
||||
WorkspaceCommandSelection | undefined
|
||||
> | null = null;
|
||||
|
||||
buildPaletteCommands(
|
||||
ctx: CommandBuilderContext
|
||||
): WorkspaceResolvedCommandItem[] {
|
||||
return buildCommandPaletteItems(ctx, this.viewCommands.commands());
|
||||
}
|
||||
|
||||
openCommandPalette(ctx: CommandBuilderContext, initialQuery: string): void {
|
||||
if (this.commandPaletteRef) {
|
||||
this.commandPaletteRef.close();
|
||||
return;
|
||||
}
|
||||
|
||||
const commands = this.buildPaletteCommands(ctx);
|
||||
const recentIds = this.recentCommands.entries().map((entry) => entry.id);
|
||||
const dialogRef = this.dialog.open<
|
||||
WorkspaceCommandPaletteComponent,
|
||||
{
|
||||
commands: WorkspaceResolvedCommandItem[];
|
||||
query: string;
|
||||
recentIds: readonly string[];
|
||||
},
|
||||
WorkspaceCommandSelection | undefined
|
||||
>(WorkspaceCommandPaletteComponent, {
|
||||
width: 'min(760px, 92vw)',
|
||||
maxWidth: '92vw',
|
||||
panelClass: 'workspace-command-palette-overlay',
|
||||
autoFocus: false,
|
||||
data: {
|
||||
commands,
|
||||
query: initialQuery,
|
||||
recentIds,
|
||||
},
|
||||
});
|
||||
this.commandPaletteRef = dialogRef;
|
||||
|
||||
dialogRef
|
||||
.afterClosed()
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((selection) => {
|
||||
this.commandPaletteRef = null;
|
||||
if (!selection) {
|
||||
return;
|
||||
}
|
||||
|
||||
const command = commands.find(
|
||||
(item) =>
|
||||
item.id === selection.commandId &&
|
||||
item.visible &&
|
||||
item.enabled
|
||||
);
|
||||
if (!command) {
|
||||
return;
|
||||
}
|
||||
|
||||
command.run({ query: selection.query.trim() });
|
||||
this.recentCommands.record(command.id);
|
||||
});
|
||||
}
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
import { computed, inject, Injectable } from '@angular/core';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { startWith } from 'rxjs';
|
||||
import { XtreamStore } from '@iptvnator/portal/xtream/data-access';
|
||||
import { XtreamImportPhaseTone } from './helpers/workspace-shell-constants';
|
||||
import {
|
||||
buildXtreamImportDetailLabel,
|
||||
buildXtreamImportPhaseLabel,
|
||||
buildXtreamImportPhaseTone,
|
||||
buildXtreamImportProgressLabel,
|
||||
buildXtreamImportSourceLabel,
|
||||
buildXtreamImportTypeLabel,
|
||||
formatLocalizedNumber,
|
||||
} from './helpers/workspace-shell-import-labels';
|
||||
|
||||
@Injectable()
|
||||
export class WorkspaceShellXtreamImportService {
|
||||
private readonly xtreamStore = inject(XtreamStore);
|
||||
private readonly translate = inject(TranslateService);
|
||||
private readonly isElectron = !!window.electron;
|
||||
|
||||
private readonly languageTick = toSignal(
|
||||
this.translate.onLangChange.pipe(startWith(null)),
|
||||
{ initialValue: null }
|
||||
);
|
||||
|
||||
private readonly translateText = (
|
||||
key: string,
|
||||
params?: Record<string, string | number>
|
||||
): string => this.translate.instant(key, params);
|
||||
|
||||
readonly xtreamImportCount = this.xtreamStore.getImportCount;
|
||||
readonly xtreamItemsToImport = this.xtreamStore.itemsToImport;
|
||||
readonly xtreamActiveImportCount =
|
||||
this.xtreamStore.activeImportCurrentCount;
|
||||
readonly xtreamActiveItemsToImport =
|
||||
this.xtreamStore.activeImportTotalCount;
|
||||
readonly xtreamImportPhase = this.xtreamStore.currentImportPhase;
|
||||
readonly isCancellingXtreamImport = this.xtreamStore.isCancellingImport;
|
||||
|
||||
readonly canCancelXtreamImport = computed(
|
||||
() =>
|
||||
this.isElectron &&
|
||||
this.xtreamStore.isImporting() &&
|
||||
Boolean(this.xtreamStore.activeImportSessionId()) &&
|
||||
!this.xtreamStore.isCancellingImport()
|
||||
);
|
||||
|
||||
readonly xtreamImportTitleLabel = computed(() =>
|
||||
this.translateText('WORKSPACE.SHELL.XTREAM_IMPORT_TITLE')
|
||||
);
|
||||
|
||||
readonly xtreamImportTypeLabel = computed(() => {
|
||||
this.languageTick();
|
||||
return buildXtreamImportTypeLabel(
|
||||
this.xtreamStore.activeImportContentType(),
|
||||
this.translateText
|
||||
);
|
||||
});
|
||||
|
||||
readonly xtreamImportProgressLabel = computed(() =>
|
||||
buildXtreamImportProgressLabel(
|
||||
this.xtreamImportTypeLabel(),
|
||||
this.xtreamActiveImportCount(),
|
||||
this.xtreamActiveItemsToImport(),
|
||||
this.translateText,
|
||||
(value) =>
|
||||
formatLocalizedNumber(
|
||||
value,
|
||||
this.translate.currentLang,
|
||||
this.translate.defaultLang
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
readonly xtreamImportPhaseTone = computed<XtreamImportPhaseTone>(() =>
|
||||
buildXtreamImportPhaseTone(this.xtreamStore.currentImportPhase())
|
||||
);
|
||||
|
||||
readonly xtreamImportSourceLabel = computed(() => {
|
||||
this.languageTick();
|
||||
return buildXtreamImportSourceLabel(
|
||||
this.xtreamImportPhaseTone(),
|
||||
this.translateText
|
||||
);
|
||||
});
|
||||
|
||||
readonly xtreamImportPhaseLabel = computed(() => {
|
||||
this.languageTick();
|
||||
return buildXtreamImportPhaseLabel(
|
||||
this.xtreamStore.currentImportPhase(),
|
||||
this.translateText
|
||||
);
|
||||
});
|
||||
|
||||
readonly xtreamImportDetailLabel = computed(() => {
|
||||
this.languageTick();
|
||||
return buildXtreamImportDetailLabel(
|
||||
this.xtreamImportPhaseTone(),
|
||||
this.translateText
|
||||
);
|
||||
});
|
||||
|
||||
readonly isImportRunning = computed(
|
||||
() =>
|
||||
!this.xtreamStore.contentInitBlockReason() &&
|
||||
this.xtreamStore.isImporting()
|
||||
);
|
||||
|
||||
cancelXtreamImport(): void {
|
||||
void this.xtreamStore.cancelImport();
|
||||
}
|
||||
}
|
||||
+4
@@ -25,6 +25,8 @@ import {
|
||||
import { RecentCommandsService } from '../../recent-commands';
|
||||
import { WorkspacePlayerCommandsContributor } from '../../workspace-player-commands';
|
||||
import { WorkspaceShellFacade } from './workspace-shell.facade';
|
||||
import { WorkspaceShellXtreamImportService } from './workspace-shell-xtream-import.service';
|
||||
import { WorkspaceShellCommandPaletteService } from './workspace-shell-command-palette.service';
|
||||
|
||||
class MockXtreamStore {
|
||||
readonly recentItems = signal<unknown[]>([]);
|
||||
@@ -193,6 +195,8 @@ describe('WorkspaceShellFacade', () => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
WorkspaceShellFacade,
|
||||
WorkspaceShellXtreamImportService,
|
||||
WorkspaceShellCommandPaletteService,
|
||||
{
|
||||
provide: Router,
|
||||
useValue: router,
|
||||
|
||||
+111
-860
File diff suppressed because it is too large.
Load diff
@@ -15,6 +15,8 @@ import { WorkspaceShellContextSidebarComponent } from './components/workspace-sh
|
||||
import { WorkspaceShellHeaderComponent } from './components/workspace-shell-header/workspace-shell-header.component';
|
||||
import { WorkspaceShellRailComponent } from './components/workspace-shell-rail/workspace-shell-rail.component';
|
||||
import { WorkspaceShellFacade } from './services/workspace-shell.facade';
|
||||
import { WorkspaceShellXtreamImportService } from './services/workspace-shell-xtream-import.service';
|
||||
import { WorkspaceShellCommandPaletteService } from './services/workspace-shell-command-palette.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-workspace-shell',
|
||||
@@ -32,7 +34,11 @@ import { WorkspaceShellFacade } from './services/workspace-shell.facade';
|
||||
],
|
||||
templateUrl: './workspace-shell.component.html',
|
||||
styleUrl: './workspace-shell.component.scss',
|
||||
providers: [WorkspaceShellFacade],
|
||||
providers: [
|
||||
WorkspaceShellFacade,
|
||||
WorkspaceShellXtreamImportService,
|
||||
WorkspaceShellCommandPaletteService,
|
||||
],
|
||||
})
|
||||
export class WorkspaceShellComponent {
|
||||
readonly facade = inject(WorkspaceShellFacade);
|
||||
|
||||
Reference in new issue
Block a user