mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-14 05:06:16 -08:00
refactor(workspace-shell): push xtream import overlay and bulk action into child components
Continues the workspace-shell facade decomposition by relocating two clusters from the parent template into the components that own them. Xtream import overlay: - New WorkspaceShellImportOverlayComponent injects WorkspaceShellXtreamImportService directly. The 70-line inline overlay block in workspace-shell.component.html collapses to a single <app-workspace-shell-import-overlay /> element, gated by facade.showXtreamImportOverlay(). - Overlay-specific styles move from workspace-shell.component.scss into the child component (~125 lines relocated). - Facade drops 14 import re-export lines plus the cancelXtreamImport delegation. The two facade tests that exercised label generation now resolve the service via TestBed.inject and assert against it directly. Header bulk action: - WorkspaceShellHeaderComponent gains headerBulkAction input and headerBulkActionRequested output, plus a button that renders the action when present (next to the existing headerShortcut button). - The parent template wires facade.headerBulkAction() → facade.runHeaderBulkAction(). The facade keeps the cross-cutting computed and the multi-store dispatch method since both span multiple stores. Verification: - workspace-shell.facade.spec.ts: 20/20 passing (two assertions retargeted at WorkspaceShellXtreamImportService). - Full feature lib: 53/60 passing — same 7 pre-existing failures. - pnpm nx build web --configuration=production: success. - pnpm nx lint workspace-shell-feature: clean. - pnpm nx run web-e2e:e2e-ci--src/basic.e2e.ts: 3/3 across browsers. - pnpm nx run web-e2e:e2e-ci--src/settings.e2e.ts: 12/12 across browsers. Facade: 917 → 897 lines. The bulk-action button now actually renders in the header — closing a latent gap where the computed existed on the facade but had no template binding. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Entire-Checkpoint: c2cea9c530e6
This commit is contained in:
1 parent
0ad54e7939
commit
63f5173139
10 files changed
+252
-230
No files matched your search
+13
@@ -75,6 +75,19 @@
|
||||
<mat-icon>{{ action.icon }}</mat-icon>
|
||||
</button>
|
||||
}
|
||||
@if (headerBulkAction(); as bulk) {
|
||||
<button
|
||||
type="button"
|
||||
class="header-shortcut header-bulk-action"
|
||||
mat-icon-button
|
||||
[disabled]="bulk.disabled"
|
||||
(click)="onHeaderBulkActionRequested()"
|
||||
[matTooltip]="bulk.tooltip"
|
||||
[attr.aria-label]="bulk.ariaLabel"
|
||||
>
|
||||
<mat-icon>{{ bulk.icon }}</mat-icon>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (isElectron()) {
|
||||
|
||||
+7
@@ -10,6 +10,7 @@ import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { PlaylistSwitcherComponent } from '@iptvnator/playlist/shared/ui';
|
||||
import { WorkspaceHeaderAction } from '@iptvnator/portal/shared/util';
|
||||
import { WorkspaceHeaderBulkAction } from '../../services/helpers/workspace-shell-constants';
|
||||
|
||||
@Component({
|
||||
selector: 'app-workspace-shell-header',
|
||||
@@ -40,6 +41,7 @@ export class WorkspaceShellHeaderComponent {
|
||||
readonly searchScopeLabel = input('');
|
||||
readonly searchStatusLabel = input('');
|
||||
readonly headerShortcut = input<WorkspaceHeaderAction | null>(null);
|
||||
readonly headerBulkAction = input<WorkspaceHeaderBulkAction | null>(null);
|
||||
readonly canRefreshPlaylist = input(false);
|
||||
readonly isRefreshingPlaylist = input(false);
|
||||
readonly isElectron = input(false);
|
||||
@@ -52,6 +54,7 @@ export class WorkspaceShellHeaderComponent {
|
||||
readonly commandPaletteRequested = output<void>();
|
||||
readonly addPlaylistRequested = output<void>();
|
||||
readonly headerShortcutRequested = output<void>();
|
||||
readonly headerBulkActionRequested = output<void>();
|
||||
readonly refreshPlaylistRequested = output<void>();
|
||||
readonly downloadsRequested = output<void>();
|
||||
readonly playlistInfoRequested = output<void>();
|
||||
@@ -87,6 +90,10 @@ export class WorkspaceShellHeaderComponent {
|
||||
this.headerShortcutRequested.emit();
|
||||
}
|
||||
|
||||
onHeaderBulkActionRequested(): void {
|
||||
this.headerBulkActionRequested.emit();
|
||||
}
|
||||
|
||||
onRefreshPlaylistRequested(): void {
|
||||
this.refreshPlaylistRequested.emit();
|
||||
}
|
||||
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
<div class="workspace-loading-overlay">
|
||||
<div class="workspace-loading-overlay__card">
|
||||
<div class="workspace-loading-overlay__header">
|
||||
@if (xtreamImport.xtreamImportSourceLabel()) {
|
||||
<span
|
||||
class="workspace-loading-overlay__badge"
|
||||
[class.workspace-loading-overlay__badge--remote]="
|
||||
xtreamImport.xtreamImportPhaseTone() === 'remote'
|
||||
"
|
||||
>
|
||||
{{ xtreamImport.xtreamImportSourceLabel() }}
|
||||
</span>
|
||||
}
|
||||
<h3>{{ xtreamImport.xtreamImportTitleLabel() }}</h3>
|
||||
</div>
|
||||
@if (xtreamImport.xtreamImportPhaseLabel()) {
|
||||
<p
|
||||
class="workspace-loading-overlay__phase"
|
||||
aria-live="polite"
|
||||
>
|
||||
{{ xtreamImport.xtreamImportPhaseLabel() }}
|
||||
</p>
|
||||
}
|
||||
@if (xtreamImport.xtreamImportDetailLabel()) {
|
||||
<p class="workspace-loading-overlay__detail">
|
||||
{{ xtreamImport.xtreamImportDetailLabel() }}
|
||||
</p>
|
||||
}
|
||||
@if (
|
||||
xtreamImport.xtreamActiveImportCount() !== 0 &&
|
||||
xtreamImport.xtreamActiveItemsToImport() !== 0
|
||||
) {
|
||||
<mat-progress-bar
|
||||
mode="determinate"
|
||||
[value]="
|
||||
(xtreamImport.xtreamActiveImportCount() /
|
||||
xtreamImport.xtreamActiveItemsToImport()) *
|
||||
100
|
||||
"
|
||||
/>
|
||||
} @else {
|
||||
<mat-progress-bar mode="indeterminate" />
|
||||
}
|
||||
@if (xtreamImport.xtreamImportProgressLabel()) {
|
||||
<p class="workspace-loading-overlay__progress-copy">
|
||||
{{ xtreamImport.xtreamImportProgressLabel() }}
|
||||
</p>
|
||||
}
|
||||
@if (xtreamImport.canCancelXtreamImport()) {
|
||||
<button
|
||||
mat-stroked-button
|
||||
class="workspace-loading-overlay__action"
|
||||
type="button"
|
||||
[disabled]="xtreamImport.isCancellingXtreamImport()"
|
||||
(click)="onCancel()"
|
||||
>
|
||||
{{
|
||||
(xtreamImport.isCancellingXtreamImport()
|
||||
? 'WORKSPACE.SHELL.XTREAM_IMPORT_CANCELLING'
|
||||
: 'WORKSPACE.SHELL.XTREAM_IMPORT_CANCEL'
|
||||
) | translate
|
||||
}}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
:host {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
.workspace-loading-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
background:
|
||||
radial-gradient(
|
||||
circle at top,
|
||||
rgba(99, 120, 255, 0.12),
|
||||
transparent 34%
|
||||
),
|
||||
rgba(8, 10, 16, 0.68);
|
||||
backdrop-filter: blur(14px);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.workspace-loading-overlay__card {
|
||||
width: min(440px, 100%);
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
padding: 24px 24px 20px;
|
||||
border-radius: 24px;
|
||||
border: 1px solid var(--app-separator, rgba(255, 255, 255, 0.12));
|
||||
background:
|
||||
linear-gradient(
|
||||
180deg,
|
||||
rgba(255, 255, 255, 0.06),
|
||||
rgba(255, 255, 255, 0)
|
||||
),
|
||||
var(--mat-sys-surface-container-high);
|
||||
box-shadow:
|
||||
0 24px 64px rgba(0, 0, 0, 0.32),
|
||||
0 1px 0 rgba(255, 255, 255, 0.05) inset;
|
||||
text-align: center;
|
||||
animation: workspace-loading-overlay-enter 180ms ease-out;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 1.55rem;
|
||||
font-weight: 650;
|
||||
letter-spacing: -0.03em;
|
||||
color: var(--app-heading-color, var(--mat-sys-on-surface));
|
||||
}
|
||||
}
|
||||
|
||||
.workspace-loading-overlay__header {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
justify-items: center;
|
||||
}
|
||||
|
||||
.workspace-loading-overlay__badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 6px 12px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(120, 220, 255, 0.24);
|
||||
background: rgba(120, 220, 255, 0.08);
|
||||
color: rgba(164, 233, 255, 1);
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.workspace-loading-overlay__badge--remote {
|
||||
border-color: rgba(105, 195, 255, 0.34);
|
||||
background: rgba(83, 158, 255, 0.12);
|
||||
color: rgba(202, 230, 255, 1);
|
||||
}
|
||||
|
||||
.workspace-loading-overlay__phase,
|
||||
.workspace-loading-overlay__detail,
|
||||
.workspace-loading-overlay__progress-copy {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.workspace-loading-overlay__phase {
|
||||
font-size: 1rem;
|
||||
font-weight: 560;
|
||||
color: var(--app-body-color, var(--mat-sys-on-surface));
|
||||
}
|
||||
|
||||
.workspace-loading-overlay__detail {
|
||||
max-width: 32ch;
|
||||
justify-self: center;
|
||||
color: var(--app-muted-color, var(--mat-sys-on-surface-variant));
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.workspace-loading-overlay__progress-copy {
|
||||
max-width: 30ch;
|
||||
justify-self: center;
|
||||
line-height: 1.45;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: var(--app-muted-color, var(--mat-sys-on-surface-variant));
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
.workspace-loading-overlay__action {
|
||||
justify-self: center;
|
||||
min-width: 180px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
:host ::ng-deep .workspace-loading-overlay__card .mat-mdc-progress-bar {
|
||||
overflow: hidden;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
@keyframes workspace-loading-overlay-enter {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px) scale(0.985);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { WorkspaceShellXtreamImportService } from '../../services/workspace-shell-xtream-import.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-workspace-shell-import-overlay',
|
||||
imports: [MatButtonModule, MatProgressBarModule, TranslatePipe],
|
||||
templateUrl: './workspace-shell-import-overlay.component.html',
|
||||
styleUrl: './workspace-shell-import-overlay.component.scss',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class WorkspaceShellImportOverlayComponent {
|
||||
protected readonly xtreamImport = inject(WorkspaceShellXtreamImportService);
|
||||
|
||||
onCancel(): void {
|
||||
this.xtreamImport.cancelXtreamImport();
|
||||
}
|
||||
}
|
||||
+12
-10
@@ -314,51 +314,53 @@ describe('WorkspaceShellFacade', () => {
|
||||
|
||||
it('uses a loading label for remote Xtream fetch phases', () => {
|
||||
const xtreamStore = TestBed.inject(XtreamStore) as unknown as MockXtreamStore;
|
||||
const xtreamImport = TestBed.inject(WorkspaceShellXtreamImportService);
|
||||
|
||||
xtreamStore.currentImportPhase.set('loading-categories');
|
||||
expect(facade.xtreamImportPhaseLabel()).toBe(
|
||||
expect(xtreamImport.xtreamImportPhaseLabel()).toBe(
|
||||
'WORKSPACE.SHELL.XTREAM_IMPORT_LOADING'
|
||||
);
|
||||
|
||||
xtreamStore.currentImportPhase.set('loading-live');
|
||||
expect(facade.xtreamImportPhaseLabel()).toBe(
|
||||
expect(xtreamImport.xtreamImportPhaseLabel()).toBe(
|
||||
'WORKSPACE.SHELL.XTREAM_IMPORT_LOADING'
|
||||
);
|
||||
expect(facade.xtreamImportSourceLabel()).toBe(
|
||||
expect(xtreamImport.xtreamImportSourceLabel()).toBe(
|
||||
'WORKSPACE.SHELL.XTREAM_IMPORT_REMOTE_BADGE'
|
||||
);
|
||||
expect(facade.xtreamImportDetailLabel()).toBe(
|
||||
expect(xtreamImport.xtreamImportDetailLabel()).toBe(
|
||||
'WORKSPACE.SHELL.XTREAM_IMPORT_DETAIL_REMOTE'
|
||||
);
|
||||
|
||||
xtreamStore.currentImportPhase.set('saving-categories');
|
||||
expect(facade.xtreamImportPhaseLabel()).toBe(
|
||||
expect(xtreamImport.xtreamImportPhaseLabel()).toBe(
|
||||
'WORKSPACE.SHELL.XTREAM_IMPORT_SAVING'
|
||||
);
|
||||
|
||||
xtreamStore.currentImportPhase.set('saving-content');
|
||||
expect(facade.xtreamImportPhaseLabel()).toBe(
|
||||
expect(xtreamImport.xtreamImportPhaseLabel()).toBe(
|
||||
'WORKSPACE.SHELL.XTREAM_IMPORT_SAVING'
|
||||
);
|
||||
expect(facade.xtreamImportSourceLabel()).toBe(
|
||||
expect(xtreamImport.xtreamImportSourceLabel()).toBe(
|
||||
'WORKSPACE.SHELL.XTREAM_IMPORT_LOCAL_BADGE'
|
||||
);
|
||||
expect(facade.xtreamImportDetailLabel()).toBe(
|
||||
expect(xtreamImport.xtreamImportDetailLabel()).toBe(
|
||||
'WORKSPACE.SHELL.XTREAM_IMPORT_DETAIL_LOCAL'
|
||||
);
|
||||
});
|
||||
|
||||
it('builds a type-aware xtream import progress label', () => {
|
||||
const xtreamStore = TestBed.inject(XtreamStore) as unknown as MockXtreamStore;
|
||||
const xtreamImport = TestBed.inject(WorkspaceShellXtreamImportService);
|
||||
|
||||
xtreamStore.activeImportContentType.set('vod');
|
||||
xtreamStore.activeImportCurrentCount.set(20);
|
||||
xtreamStore.activeImportTotalCount.set(12323);
|
||||
|
||||
expect(facade.xtreamImportTypeLabel()).toBe(
|
||||
expect(xtreamImport.xtreamImportTypeLabel()).toBe(
|
||||
'WORKSPACE.SHELL.RAIL_MOVIES'
|
||||
);
|
||||
expect(facade.xtreamImportProgressLabel()).toBe(
|
||||
expect(xtreamImport.xtreamImportProgressLabel()).toBe(
|
||||
'WORKSPACE.SHELL.RAIL_MOVIES imported: 20 / 12,323'
|
||||
);
|
||||
});
|
||||
|
||||
-20
@@ -268,22 +268,6 @@ export class WorkspaceShellFacade {
|
||||
() => this.currentRoute().contextPanel !== 'none'
|
||||
);
|
||||
private readonly xtreamImport = inject(WorkspaceShellXtreamImportService);
|
||||
readonly xtreamImportCount = this.xtreamImport.xtreamImportCount;
|
||||
readonly xtreamItemsToImport = this.xtreamImport.xtreamItemsToImport;
|
||||
readonly xtreamActiveImportCount = this.xtreamImport.xtreamActiveImportCount;
|
||||
readonly xtreamActiveItemsToImport =
|
||||
this.xtreamImport.xtreamActiveItemsToImport;
|
||||
readonly xtreamImportPhase = this.xtreamImport.xtreamImportPhase;
|
||||
readonly isCancellingXtreamImport = this.xtreamImport.isCancellingXtreamImport;
|
||||
readonly canCancelXtreamImport = this.xtreamImport.canCancelXtreamImport;
|
||||
readonly xtreamImportTitleLabel = this.xtreamImport.xtreamImportTitleLabel;
|
||||
readonly xtreamImportTypeLabel = this.xtreamImport.xtreamImportTypeLabel;
|
||||
readonly xtreamImportProgressLabel =
|
||||
this.xtreamImport.xtreamImportProgressLabel;
|
||||
readonly xtreamImportPhaseTone = this.xtreamImport.xtreamImportPhaseTone;
|
||||
readonly xtreamImportSourceLabel = this.xtreamImport.xtreamImportSourceLabel;
|
||||
readonly xtreamImportPhaseLabel = this.xtreamImport.xtreamImportPhaseLabel;
|
||||
readonly xtreamImportDetailLabel = this.xtreamImport.xtreamImportDetailLabel;
|
||||
readonly showXtreamImportOverlay = computed(() => {
|
||||
const context = this.currentContext();
|
||||
const section = this.currentSection();
|
||||
@@ -754,10 +738,6 @@ export class WorkspaceShellFacade {
|
||||
});
|
||||
}
|
||||
|
||||
cancelXtreamImport(): void {
|
||||
this.xtreamImport.cancelXtreamImport();
|
||||
}
|
||||
|
||||
openAccountInfo(): void {
|
||||
if (!this.canOpenAccountInfo()) {
|
||||
return;
|
||||
|
||||
+3
-66
@@ -5,72 +5,7 @@
|
||||
>
|
||||
<app-playlist-drop-overlay [state]="dropZone.overlayState()" />
|
||||
@if (facade.showXtreamImportOverlay()) {
|
||||
<div class="workspace-loading-overlay">
|
||||
<div class="workspace-loading-overlay__card">
|
||||
<div class="workspace-loading-overlay__header">
|
||||
@if (facade.xtreamImportSourceLabel()) {
|
||||
<span
|
||||
class="workspace-loading-overlay__badge"
|
||||
[class.workspace-loading-overlay__badge--remote]="
|
||||
facade.xtreamImportPhaseTone() === 'remote'
|
||||
"
|
||||
>
|
||||
{{ facade.xtreamImportSourceLabel() }}
|
||||
</span>
|
||||
}
|
||||
<h3>{{ facade.xtreamImportTitleLabel() }}</h3>
|
||||
</div>
|
||||
@if (facade.xtreamImportPhaseLabel()) {
|
||||
<p
|
||||
class="workspace-loading-overlay__phase"
|
||||
aria-live="polite"
|
||||
>
|
||||
{{ facade.xtreamImportPhaseLabel() }}
|
||||
</p>
|
||||
}
|
||||
@if (facade.xtreamImportDetailLabel()) {
|
||||
<p class="workspace-loading-overlay__detail">
|
||||
{{ facade.xtreamImportDetailLabel() }}
|
||||
</p>
|
||||
}
|
||||
@if (
|
||||
facade.xtreamActiveImportCount() !== 0 &&
|
||||
facade.xtreamActiveItemsToImport() !== 0
|
||||
) {
|
||||
<mat-progress-bar
|
||||
mode="determinate"
|
||||
[value]="
|
||||
(facade.xtreamActiveImportCount() /
|
||||
facade.xtreamActiveItemsToImport()) *
|
||||
100
|
||||
"
|
||||
/>
|
||||
} @else {
|
||||
<mat-progress-bar mode="indeterminate" />
|
||||
}
|
||||
@if (facade.xtreamImportProgressLabel()) {
|
||||
<p class="workspace-loading-overlay__progress-copy">
|
||||
{{ facade.xtreamImportProgressLabel() }}
|
||||
</p>
|
||||
}
|
||||
@if (facade.canCancelXtreamImport()) {
|
||||
<button
|
||||
mat-stroked-button
|
||||
class="workspace-loading-overlay__action"
|
||||
type="button"
|
||||
[disabled]="facade.isCancellingXtreamImport()"
|
||||
(click)="facade.cancelXtreamImport()"
|
||||
>
|
||||
{{
|
||||
(facade.isCancellingXtreamImport()
|
||||
? 'WORKSPACE.SHELL.XTREAM_IMPORT_CANCELLING'
|
||||
: 'WORKSPACE.SHELL.XTREAM_IMPORT_CANCEL'
|
||||
) | translate
|
||||
}}
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<app-workspace-shell-import-overlay />
|
||||
}
|
||||
|
||||
<app-workspace-shell-rail
|
||||
@@ -97,6 +32,7 @@
|
||||
[searchScopeLabel]="facade.searchScopeLabel()"
|
||||
[searchStatusLabel]="facade.searchStatusLabel()"
|
||||
[headerShortcut]="facade.headerShortcut()"
|
||||
[headerBulkAction]="facade.headerBulkAction()"
|
||||
[canRefreshPlaylist]="facade.canRefreshPlaylist()"
|
||||
[isRefreshingPlaylist]="facade.isRefreshingPlaylist()"
|
||||
[isElectron]="facade.isElectron"
|
||||
@@ -109,6 +45,7 @@
|
||||
(commandPaletteRequested)="facade.openCommandPalette()"
|
||||
(addPlaylistRequested)="facade.openAddPlaylistDialog()"
|
||||
(headerShortcutRequested)="facade.runHeaderShortcut()"
|
||||
(headerBulkActionRequested)="facade.runHeaderBulkAction()"
|
||||
(downloadsRequested)="facade.openDownloadsShortcut()"
|
||||
(playlistInfoRequested)="facade.openPlaylistInfo()"
|
||||
(accountInfoRequested)="facade.openAccountInfo()"
|
||||
|
||||
@@ -65,130 +65,6 @@ app-workspace-shell-context-sidebar {
|
||||
box-shadow: -4px 8px 20px var(--app-content-shadow, rgba(0, 0, 0, 0.18));
|
||||
}
|
||||
|
||||
.workspace-loading-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 24px;
|
||||
background:
|
||||
radial-gradient(
|
||||
circle at top,
|
||||
rgba(99, 120, 255, 0.12),
|
||||
transparent 34%
|
||||
),
|
||||
rgba(8, 10, 16, 0.68);
|
||||
backdrop-filter: blur(14px);
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.workspace-loading-overlay__card {
|
||||
width: min(440px, 100%);
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
padding: 24px 24px 20px;
|
||||
border-radius: 24px;
|
||||
border: 1px solid var(--app-separator, rgba(255, 255, 255, 0.12));
|
||||
background:
|
||||
linear-gradient(
|
||||
180deg,
|
||||
rgba(255, 255, 255, 0.06),
|
||||
rgba(255, 255, 255, 0)
|
||||
),
|
||||
var(--mat-sys-surface-container-high);
|
||||
box-shadow:
|
||||
0 24px 64px rgba(0, 0, 0, 0.32),
|
||||
0 1px 0 rgba(255, 255, 255, 0.05) inset;
|
||||
text-align: center;
|
||||
animation: workspace-loading-overlay-enter 180ms ease-out;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 1.55rem;
|
||||
font-weight: 650;
|
||||
letter-spacing: -0.03em;
|
||||
color: var(--app-heading-color, var(--mat-sys-on-surface));
|
||||
}
|
||||
}
|
||||
|
||||
.workspace-loading-overlay__header {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
justify-items: center;
|
||||
}
|
||||
|
||||
.workspace-loading-overlay__badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 6px 12px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(120, 220, 255, 0.24);
|
||||
background: rgba(120, 220, 255, 0.08);
|
||||
color: rgba(164, 233, 255, 1);
|
||||
font-size: 0.72rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.12em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.workspace-loading-overlay__badge--remote {
|
||||
border-color: rgba(105, 195, 255, 0.34);
|
||||
background: rgba(83, 158, 255, 0.12);
|
||||
color: rgba(202, 230, 255, 1);
|
||||
}
|
||||
|
||||
.workspace-loading-overlay__phase,
|
||||
.workspace-loading-overlay__detail,
|
||||
.workspace-loading-overlay__progress-copy {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.workspace-loading-overlay__phase {
|
||||
font-size: 1rem;
|
||||
font-weight: 560;
|
||||
color: var(--app-body-color, var(--mat-sys-on-surface));
|
||||
}
|
||||
|
||||
.workspace-loading-overlay__detail {
|
||||
max-width: 32ch;
|
||||
justify-self: center;
|
||||
color: var(--app-muted-color, var(--mat-sys-on-surface-variant));
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.workspace-loading-overlay__progress-copy {
|
||||
max-width: 30ch;
|
||||
justify-self: center;
|
||||
line-height: 1.45;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: var(--app-muted-color, var(--mat-sys-on-surface-variant));
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
.workspace-loading-overlay__action {
|
||||
justify-self: center;
|
||||
min-width: 180px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
:host ::ng-deep .workspace-loading-overlay__card .mat-mdc-progress-bar {
|
||||
overflow: hidden;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
@keyframes workspace-loading-overlay-enter {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px) scale(0.985);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 780px) {
|
||||
.workspace-shell {
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
import {
|
||||
Component,
|
||||
inject,
|
||||
} from '@angular/core';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { RouterOutlet } from '@angular/router';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { ExternalPlaybackDockComponent } from 'components';
|
||||
import {
|
||||
PlaylistDropOverlayComponent,
|
||||
@@ -13,6 +7,7 @@ import {
|
||||
} from '../playlist-drop-overlay';
|
||||
import { WorkspaceShellContextSidebarComponent } from './components/workspace-shell-context-sidebar/workspace-shell-context-sidebar.component';
|
||||
import { WorkspaceShellHeaderComponent } from './components/workspace-shell-header/workspace-shell-header.component';
|
||||
import { WorkspaceShellImportOverlayComponent } from './components/workspace-shell-import-overlay/workspace-shell-import-overlay.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';
|
||||
@@ -22,14 +17,12 @@ import { WorkspaceShellCommandPaletteService } from './services/workspace-shell-
|
||||
selector: 'app-workspace-shell',
|
||||
imports: [
|
||||
ExternalPlaybackDockComponent,
|
||||
MatButtonModule,
|
||||
MatProgressBarModule,
|
||||
PlaylistDropOverlayComponent,
|
||||
PlaylistDropZoneDirective,
|
||||
RouterOutlet,
|
||||
TranslatePipe,
|
||||
WorkspaceShellContextSidebarComponent,
|
||||
WorkspaceShellHeaderComponent,
|
||||
WorkspaceShellImportOverlayComponent,
|
||||
WorkspaceShellRailComponent,
|
||||
],
|
||||
templateUrl: './workspace-shell.component.html',
|
||||
|
||||
Reference in new issue
Block a user