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'" > -
+
} - +
-
- {{ - playerLabel() - }} - @if (!compact()) { - - External player - - } -
{{ session().title }}
+ {{ playerLabel() }} + @if (showSpinner()) { - - } @else { - {{ - session().status === 'error' ? 'error' : 'play_arrow' + + } @else if (showStatusIcon()) { + {{ + statusIcon() }} } - {{ statusLabel() }} + {{ + statusLabel() + }}
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 {