diff --git a/libs/ui/components/src/lib/external-playback-dock/external-playback-dock.component.html b/libs/ui/components/src/lib/external-playback-dock/external-playback-dock.component.html
index 0fa048529..9998539e6 100644
--- a/libs/ui/components/src/lib/external-playback-dock/external-playback-dock.component.html
+++ b/libs/ui/components/src/lib/external-playback-dock/external-playback-dock.component.html
@@ -3,7 +3,18 @@
[class.external-playback-dock--compact]="compact()"
[class.external-playback-dock--error]="session().status === 'error'"
>
-
+
}
-
+
diff --git a/libs/ui/components/src/lib/external-playback-dock/external-playback-dock.component.scss b/libs/ui/components/src/lib/external-playback-dock/external-playback-dock.component.scss
index 91991507b..29158fd71 100644
--- a/libs/ui/components/src/lib/external-playback-dock/external-playback-dock.component.scss
+++ b/libs/ui/components/src/lib/external-playback-dock/external-playback-dock.component.scss
@@ -8,7 +8,7 @@
align-items: center;
gap: 14px;
min-height: 72px;
- padding: 8px 14px;
+ padding: 2px 14px;
border: 1px solid color-mix(in srgb, var(--mat-sys-outline-variant) 60%, transparent);
border-radius: 15px 15px 0 0;
background:
@@ -59,10 +59,17 @@
.external-playback-dock__artwork {
width: 48px;
height: 48px;
+ padding: 0;
+ border: 0;
border-radius: 14px;
overflow: hidden;
background: color-mix(in srgb, var(--mat-sys-surface-container-highest) 92%, transparent);
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.18);
+ color: inherit;
+ cursor: default;
+ transition:
+ transform 0.18s ease,
+ box-shadow 0.18s ease;
img,
.external-playback-dock__placeholder {
@@ -74,6 +81,22 @@
object-fit: cover;
display: block;
}
+
+ &--interactive {
+ cursor: pointer;
+
+ &:hover {
+ transform: scale(1.04);
+ box-shadow:
+ inset 0 0 0 1px rgba(255, 255, 255, 0.32),
+ 0 6px 16px rgba(0, 0, 0, 0.28);
+ }
+
+ &:focus-visible {
+ outline: 2px solid var(--mat-sys-primary);
+ outline-offset: 2px;
+ }
+ }
}
.external-playback-dock__placeholder {
@@ -86,30 +109,21 @@
min-width: 0;
}
-.external-playback-dock__topline {
- display: flex;
- align-items: center;
- gap: 8px;
- margin-bottom: 3px;
-}
-
.external-playback-dock__chip {
display: inline-flex;
align-items: center;
- height: 20px;
- padding: 0 8px;
+ height: 18px;
border-radius: 999px;
- background: color-mix(in srgb, var(--mat-sys-primary-container) 82%, transparent);
- color: var(--mat-sys-on-primary-container);
- font-size: 0.68rem;
- font-weight: 700;
- letter-spacing: 0.08em;
-}
-
-.external-playback-dock__caption {
+ background: color-mix(
+ in srgb,
+ var(--mat-sys-surface-container-highest) 80%,
+ transparent
+ );
color: var(--mat-sys-on-surface-variant);
- font-size: 0.72rem;
- text-transform: uppercase;
+ border: 1px solid
+ color-mix(in srgb, var(--mat-sys-outline-variant) 70%, transparent);
+ font-size: 0.66rem;
+ font-weight: 700;
letter-spacing: 0.08em;
}
@@ -120,20 +134,33 @@
font-size: 0.95rem;
font-weight: 700;
color: var(--mat-sys-on-surface);
+ margin-bottom: 2px;
}
.external-playback-dock__status {
display: flex;
align-items: center;
gap: 6px;
- margin-top: 4px;
color: var(--mat-sys-on-surface-variant);
font-size: 0.78rem;
+ min-width: 0;
- mat-icon {
- width: 16px;
- height: 16px;
- font-size: 16px;
+ .external-playback-dock__status-separator {
+ opacity: 0.55;
+ font-weight: 700;
+ line-height: 1;
+ }
+
+ .external-playback-dock__status-icon {
+ width: 14px;
+ height: 14px;
+ font-size: 14px;
+ }
+
+ .external-playback-dock__status-text {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
mat-spinner {
diff --git a/libs/ui/components/src/lib/external-playback-dock/external-playback-dock.component.ts b/libs/ui/components/src/lib/external-playback-dock/external-playback-dock.component.ts
index 7e66f8226..4d4e6e6b5 100644
--- a/libs/ui/components/src/lib/external-playback-dock/external-playback-dock.component.ts
+++ b/libs/ui/components/src/lib/external-playback-dock/external-playback-dock.component.ts
@@ -30,6 +30,7 @@ export class ExternalPlaybackDockComponent {
readonly compact = input(false);
readonly closeClicked = output();
+ readonly artworkClicked = output();
private readonly artworkFailed = signal(false);
readonly playerLabel = computed(() => this.session().player.toUpperCase());
@@ -38,25 +39,32 @@ export class ExternalPlaybackDockComponent {
);
readonly statusLabel = computed(() => {
const session = this.session();
- const player = this.playerLabel();
switch (session.status) {
case 'launching':
- return `Opening in ${player}...`;
+ return 'Launching…';
case 'opened':
- return `Opened in ${player}`;
case 'playing':
- return `Playing in ${player}`;
+ return 'Opened';
case 'error':
- return session.error || `${player} failed to launch`;
+ return session.error || 'Playback failed';
default:
- return `${player} closed`;
+ return 'Closed';
}
});
+ readonly statusIcon = computed(() => {
+ const status = this.session().status;
+ if (status === 'error') return 'error_outline';
+ return 'open_in_new';
+ });
+
readonly showSpinner = computed(
() => this.session().status === 'launching'
);
+ readonly showStatusIcon = computed(
+ () => this.session().status !== 'launching'
+ );
readonly showArtwork = computed(
() => !!this.artworkUrl() && !this.artworkFailed()
);
@@ -72,6 +80,9 @@ export class ExternalPlaybackDockComponent {
return 'live_tv';
}
});
+ readonly artworkInteractive = computed(
+ () => !!this.session().contentInfo?.playlistId
+ );
readonly showCloseButton = computed(
() => this.session().canClose && this.session().status !== 'error'
);
@@ -86,4 +97,9 @@ export class ExternalPlaybackDockComponent {
onArtworkError(): void {
this.artworkFailed.set(true);
}
+
+ onArtworkClick(): void {
+ if (!this.artworkInteractive()) return;
+ this.artworkClicked.emit();
+ }
}
diff --git a/libs/workspace/shell/feature/src/lib/workspace-shell/services/workspace-shell.facade.ts b/libs/workspace/shell/feature/src/lib/workspace-shell/services/workspace-shell.facade.ts
index 35944df88..e526c8191 100644
--- a/libs/workspace/shell/feature/src/lib/workspace-shell/services/workspace-shell.facade.ts
+++ b/libs/workspace/shell/feature/src/lib/workspace-shell/services/workspace-shell.facade.ts
@@ -720,6 +720,23 @@ export class WorkspaceShellFacade {
);
}
+ openActiveExternalSessionTarget(): void {
+ const playlistId = this.externalPlaybackSession()?.contentInfo
+ ?.playlistId;
+ if (!playlistId) return;
+
+ const playlist = this.playlists().find((p) => p._id === playlistId);
+ if (!playlist) return;
+
+ if (playlist.serverUrl) {
+ void this.router.navigate(['/workspace', 'xtreams', playlistId]);
+ } else if (playlist.macAddress) {
+ void this.router.navigate(['/workspace', 'stalker', playlistId]);
+ } else {
+ void this.router.navigate(['/workspace', 'playlists', playlistId]);
+ }
+ }
+
onSearchInput(value: string): void {
this.searchQuery.set(value);
this.scheduleSearchApply(value);
diff --git a/libs/workspace/shell/feature/src/lib/workspace-shell/workspace-shell.component.html b/libs/workspace/shell/feature/src/lib/workspace-shell/workspace-shell.component.html
index b561bfd0f..3492dec1e 100644
--- a/libs/workspace/shell/feature/src/lib/workspace-shell/workspace-shell.component.html
+++ b/libs/workspace/shell/feature/src/lib/workspace-shell/workspace-shell.component.html
@@ -135,6 +135,9 @@
}
diff --git a/libs/workspace/shell/feature/src/lib/workspace-shell/workspace-shell.component.scss b/libs/workspace/shell/feature/src/lib/workspace-shell/workspace-shell.component.scss
index aff8d10ba..d077df539 100644
--- a/libs/workspace/shell/feature/src/lib/workspace-shell/workspace-shell.component.scss
+++ b/libs/workspace/shell/feature/src/lib/workspace-shell/workspace-shell.component.scss
@@ -52,6 +52,8 @@ app-workspace-shell-context-sidebar {
.workspace-now-playing {
grid-column: 2;
grid-row: 3;
+ padding: 0;
+ background: var(--app-content-bg, var(--mat-sys-surface));
}
.workspace-content {