mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-14 05:06:16 -08:00
fix(playback): clarify external launch feedback
This commit is contained in:
1 parent
c189acf147
commit
7ed1ccfd8e
50 files changed
+2254
-202
No files matched your search
+28
-18
@@ -2,6 +2,7 @@
|
||||
class="external-playback-dock"
|
||||
[class.external-playback-dock--compact]="compact()"
|
||||
[class.external-playback-dock--error]="session().status === 'error'"
|
||||
[attr.aria-busy]="session().status === 'launching' ? 'true' : null"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
@@ -10,9 +11,7 @@
|
||||
artworkInteractive()
|
||||
"
|
||||
[disabled]="!artworkInteractive()"
|
||||
[attr.aria-label]="
|
||||
artworkInteractive() ? 'Open in playlist' : null
|
||||
"
|
||||
[attr.aria-label]="artworkInteractive() ? 'Open in playlist' : null"
|
||||
(click)="onArtworkClick()"
|
||||
>
|
||||
@if (showArtwork()) {
|
||||
@@ -31,39 +30,50 @@
|
||||
|
||||
<div class="external-playback-dock__meta">
|
||||
<div class="external-playback-dock__title">{{ displayTitle() }}</div>
|
||||
<div class="external-playback-dock__status">
|
||||
<span class="external-playback-dock__chip">{{ playerLabel() }}</span>
|
||||
<div class="external-playback-dock__status" aria-live="polite">
|
||||
<span class="external-playback-dock__chip">{{
|
||||
playerLabel()
|
||||
}}</span>
|
||||
<span
|
||||
class="external-playback-dock__status-separator"
|
||||
aria-hidden="true"
|
||||
>·</span
|
||||
>
|
||||
@if (showSpinner()) {
|
||||
<mat-spinner diameter="12" strokeWidth="2"></mat-spinner>
|
||||
<mat-spinner
|
||||
aria-hidden="true"
|
||||
diameter="12"
|
||||
strokeWidth="2"
|
||||
></mat-spinner>
|
||||
} @else if (showStatusIcon()) {
|
||||
<mat-icon class="external-playback-dock__status-icon">{{
|
||||
statusIcon()
|
||||
}}</mat-icon>
|
||||
<mat-icon
|
||||
aria-hidden="true"
|
||||
class="external-playback-dock__status-icon"
|
||||
>{{ statusIcon() }}</mat-icon
|
||||
>
|
||||
}
|
||||
<span class="external-playback-dock__status-text">{{
|
||||
statusLabel()
|
||||
}}</span>
|
||||
<span class="external-playback-dock__status-text">
|
||||
@if (session().status === 'error' && session().error) {
|
||||
{{ session().error }}
|
||||
} @else {
|
||||
{{ statusLabelKey() | translate }}
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="external-playback-dock__actions">
|
||||
@if (showCloseButton()) {
|
||||
@if (showActionButton()) {
|
||||
<button
|
||||
type="button"
|
||||
mat-button
|
||||
class="external-playback-dock__button"
|
||||
(click)="closeClicked.emit()"
|
||||
[attr.aria-label]="actionLabelKey() | translate"
|
||||
(click)="onActionClick()"
|
||||
>
|
||||
<mat-icon>stop_circle</mat-icon>
|
||||
<mat-icon aria-hidden="true">{{ actionIcon() }}</mat-icon>
|
||||
@if (!compact()) {
|
||||
<span>{{
|
||||
'WORKSPACE.SHELL.EXTERNAL_PLAYBACK_CLOSE' | translate
|
||||
}}</span>
|
||||
<span>{{ actionLabelKey() | translate }}</span>
|
||||
}
|
||||
</button>
|
||||
}
|
||||
|
||||
+54
-1
@@ -17,6 +17,11 @@ class FakeTranslateLoader implements TranslateLoader {
|
||||
WORKSPACE: {
|
||||
SHELL: {
|
||||
EXTERNAL_PLAYBACK_CLOSE: 'Close player',
|
||||
EXTERNAL_PLAYBACK_DISMISS: 'Dismiss',
|
||||
EXTERNAL_PLAYBACK_OPENING: 'Opening player…',
|
||||
EXTERNAL_PLAYBACK_STARTED: 'Player started',
|
||||
EXTERNAL_PLAYBACK_PLAYING: 'Playing',
|
||||
EXTERNAL_PLAYBACK_FAILED: 'Playback failed',
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -65,8 +70,18 @@ describe('ExternalPlaybackDockComponent', () => {
|
||||
const text = fixture.nativeElement.textContent;
|
||||
expect(text).toContain('Example Video');
|
||||
expect(text).toContain('MPV');
|
||||
expect(text).toContain('Launching');
|
||||
expect(text).toContain('Opening player');
|
||||
expect(text).toContain('Close player');
|
||||
expect(
|
||||
fixture.nativeElement
|
||||
.querySelector('.external-playback-dock')
|
||||
.getAttribute('aria-busy')
|
||||
).toBe('true');
|
||||
expect(
|
||||
fixture.nativeElement
|
||||
.querySelector('.external-playback-dock__status')
|
||||
.getAttribute('aria-live')
|
||||
).toBe('polite');
|
||||
});
|
||||
|
||||
it('emits a single close action when the close button is clicked', () => {
|
||||
@@ -96,6 +111,44 @@ describe('ExternalPlaybackDockComponent', () => {
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['opened', 'Player started'],
|
||||
['playing', 'Playing'],
|
||||
] as const)('renders exact %s status copy', (status, expected) => {
|
||||
fixture.componentRef.setInput('session', { ...session, status });
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(fixture.nativeElement.textContent).toContain(expected);
|
||||
expect(
|
||||
fixture.nativeElement
|
||||
.querySelector('.external-playback-dock')
|
||||
.getAttribute('aria-busy')
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('keeps a failed session visible with a safe dismiss action and no retry', () => {
|
||||
const dismissSpy = jest.fn();
|
||||
component.dismissClicked.subscribe(dismissSpy);
|
||||
fixture.componentRef.setInput('session', {
|
||||
...session,
|
||||
status: 'error',
|
||||
error: '',
|
||||
canClose: false,
|
||||
});
|
||||
fixture.detectChanges();
|
||||
|
||||
const action = fixture.debugElement.query(
|
||||
By.css('.external-playback-dock__button')
|
||||
);
|
||||
expect(fixture.nativeElement.textContent).toContain('Playback failed');
|
||||
expect(action.nativeElement.textContent).toContain('Dismiss');
|
||||
expect(action.nativeElement.textContent).not.toContain('Retry');
|
||||
expect(action.nativeElement.getAttribute('aria-label')).toBe('Dismiss');
|
||||
|
||||
action.nativeElement.click();
|
||||
expect(dismissSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('disables the artwork button when the session has no playlist target', () => {
|
||||
const artwork = fixture.debugElement.query(
|
||||
By.css('.external-playback-dock__artwork')
|
||||
|
||||
+26
-7
@@ -35,6 +35,7 @@ export class ExternalPlaybackDockComponent {
|
||||
readonly compact = input(false);
|
||||
|
||||
readonly closeClicked = output<void>();
|
||||
readonly dismissClicked = output<void>();
|
||||
readonly artworkClicked = output<void>();
|
||||
private readonly artworkFailed = signal(false);
|
||||
|
||||
@@ -55,25 +56,27 @@ export class ExternalPlaybackDockComponent {
|
||||
readonly artworkUrl = computed(
|
||||
() => this.session().thumbnail?.trim() ?? ''
|
||||
);
|
||||
readonly statusLabel = computed(() => {
|
||||
readonly statusLabelKey = computed(() => {
|
||||
const session = this.session();
|
||||
|
||||
switch (session.status) {
|
||||
case 'launching':
|
||||
return 'Launching…';
|
||||
return 'WORKSPACE.SHELL.EXTERNAL_PLAYBACK_OPENING';
|
||||
case 'opened':
|
||||
return 'WORKSPACE.SHELL.EXTERNAL_PLAYBACK_STARTED';
|
||||
case 'playing':
|
||||
return 'Opened';
|
||||
return 'WORKSPACE.SHELL.EXTERNAL_PLAYBACK_PLAYING';
|
||||
case 'error':
|
||||
return session.error || 'Playback failed';
|
||||
return 'WORKSPACE.SHELL.EXTERNAL_PLAYBACK_FAILED';
|
||||
default:
|
||||
return 'Closed';
|
||||
return 'WORKSPACE.SHELL.EXTERNAL_PLAYBACK_CLOSED';
|
||||
}
|
||||
});
|
||||
|
||||
readonly statusIcon = computed(() => {
|
||||
const status = this.session().status;
|
||||
if (status === 'error') return 'error_outline';
|
||||
if (status === 'playing') return 'play_circle';
|
||||
return 'open_in_new';
|
||||
});
|
||||
|
||||
@@ -101,8 +104,16 @@ export class ExternalPlaybackDockComponent {
|
||||
readonly artworkInteractive = computed(
|
||||
() => !!this.session().contentInfo?.playlistId
|
||||
);
|
||||
readonly showCloseButton = computed(
|
||||
() => this.session().canClose && this.session().status !== 'error'
|
||||
readonly showActionButton = computed(
|
||||
() => this.session().status === 'error' || this.session().canClose
|
||||
);
|
||||
readonly actionLabelKey = computed(() =>
|
||||
this.session().status === 'error'
|
||||
? 'WORKSPACE.SHELL.EXTERNAL_PLAYBACK_DISMISS'
|
||||
: 'WORKSPACE.SHELL.EXTERNAL_PLAYBACK_CLOSE'
|
||||
);
|
||||
readonly actionIcon = computed(() =>
|
||||
this.session().status === 'error' ? 'close' : 'stop_circle'
|
||||
);
|
||||
|
||||
constructor() {
|
||||
@@ -120,4 +131,12 @@ export class ExternalPlaybackDockComponent {
|
||||
if (!this.artworkInteractive()) return;
|
||||
this.artworkClicked.emit();
|
||||
}
|
||||
|
||||
onActionClick(): void {
|
||||
if (this.session().status === 'error') {
|
||||
this.dismissClicked.emit();
|
||||
return;
|
||||
}
|
||||
this.closeClicked.emit();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user