refactor(external-playback): update styles

Entire-Checkpoint: c6e522b4276c
This commit is contained in:
4gray committed 2026-04-25 23:48:21 +02:00
1 parent ce7ae1a251
commit 4bc733d9c2
6 files changed
+122 -48

No files matched your search

@@ -3,7 +3,18 @@
[class.external-playback-dock--compact]="compact()"
[class.external-playback-dock--error]="session().status === 'error'"
>
<div class="external-playback-dock__artwork">
<button
type="button"
class="external-playback-dock__artwork"
[class.external-playback-dock__artwork--interactive]="
artworkInteractive()
"
[disabled]="!artworkInteractive()"
[attr.aria-label]="
artworkInteractive() ? 'Open in playlist' : null
"
(click)="onArtworkClick()"
>
@if (showArtwork()) {
<img
[src]="artworkUrl()"
@@ -16,29 +27,27 @@
<mat-icon>{{ artworkPlaceholderIcon() }}</mat-icon>
</div>
}
</div>
</button>
<div class="external-playback-dock__meta">
<div class="external-playback-dock__topline">
<span class="external-playback-dock__chip">{{
playerLabel()
}}</span>
@if (!compact()) {
<span class="external-playback-dock__caption">
External player
</span>
}
</div>
<div class="external-playback-dock__title">{{ session().title }}</div>
<div class="external-playback-dock__status">
<span class="external-playback-dock__chip">{{ playerLabel() }}</span>
<span
class="external-playback-dock__status-separator"
aria-hidden="true"
>·</span
>
@if (showSpinner()) {
<mat-spinner diameter="14" strokeWidth="3"></mat-spinner>
} @else {
<mat-icon>{{
session().status === 'error' ? 'error' : 'play_arrow'
<mat-spinner diameter="12" strokeWidth="2"></mat-spinner>
} @else if (showStatusIcon()) {
<mat-icon class="external-playback-dock__status-icon">{{
statusIcon()
}}</mat-icon>
}
<span>{{ statusLabel() }}</span>
<span class="external-playback-dock__status-text">{{
statusLabel()
}}</span>
</div>
</div>
@@ -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 {
@@ -30,6 +30,7 @@ export class ExternalPlaybackDockComponent {
readonly compact = input(false);
readonly closeClicked = output<void>();
readonly artworkClicked = output<void>();
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();
}
}
@@ -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);
@@ -135,6 +135,9 @@
<app-external-playback-dock
[session]="session"
(closeClicked)="facade.closeActiveExternalSession()"
(artworkClicked)="
facade.openActiveExternalSessionTarget()
"
/>
</footer>
}
@@ -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 {