mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-12 12:06:16 -08:00
fix(downloads): open completed movies in details
This commit is contained in:
1 parent
5ef5d6713f
commit
4f3567e1d0
4 files changed
+173
-40
No files matched your search
@@ -23,11 +23,14 @@
|
||||
type="button"
|
||||
class="download-library__artwork-button"
|
||||
data-test-id="download-library-series-open"
|
||||
[disabled]="isPending(entity.representative)"
|
||||
[class.download-library__artwork-button--disabled]="
|
||||
!canOpenSeries(entity)
|
||||
!canOpen(entity.representative)
|
||||
"
|
||||
[attr.aria-disabled]="
|
||||
!canOpenSeries(entity) ? 'true' : null
|
||||
!canOpen(entity.representative)
|
||||
? 'true'
|
||||
: null
|
||||
"
|
||||
[attr.aria-label]="
|
||||
'DOWNLOADS.OPEN_ARTWORK'
|
||||
@@ -37,8 +40,10 @@
|
||||
'DOWNLOADS.SOURCE_PLAYLIST_MISSING'
|
||||
| translate
|
||||
"
|
||||
[matTooltipDisabled]="canOpenSeries(entity)"
|
||||
(click)="openSeries(entity)"
|
||||
[matTooltipDisabled]="
|
||||
canOpen(entity.representative)
|
||||
"
|
||||
(click)="openDetails(entity.representative)"
|
||||
>
|
||||
@if (artworkUrl(entity); as artwork) {
|
||||
<img
|
||||
@@ -84,11 +89,16 @@
|
||||
type="button"
|
||||
class="download-library__title-button"
|
||||
data-test-id="download-library-series-open"
|
||||
[disabled]="
|
||||
isPending(entity.representative)
|
||||
"
|
||||
[class.download-library__title-button--disabled]="
|
||||
!canOpenSeries(entity)
|
||||
!canOpen(entity.representative)
|
||||
"
|
||||
[attr.aria-disabled]="
|
||||
!canOpenSeries(entity) ? 'true' : null
|
||||
!canOpen(entity.representative)
|
||||
? 'true'
|
||||
: null
|
||||
"
|
||||
[attr.aria-label]="
|
||||
('DOWNLOADS.OPEN_DETAILS' | translate) +
|
||||
@@ -99,8 +109,10 @@
|
||||
'DOWNLOADS.SOURCE_PLAYLIST_MISSING'
|
||||
| translate
|
||||
"
|
||||
[matTooltipDisabled]="canOpenSeries(entity)"
|
||||
(click)="openSeries(entity)"
|
||||
[matTooltipDisabled]="
|
||||
canOpen(entity.representative)
|
||||
"
|
||||
(click)="openDetails(entity.representative)"
|
||||
>
|
||||
{{ entity.title }}
|
||||
</button>
|
||||
@@ -177,12 +189,38 @@
|
||||
type="button"
|
||||
class="download-library__artwork-button"
|
||||
[disabled]="isPending(entity.item)"
|
||||
[attr.aria-label]="
|
||||
('DOWNLOADS.PLAY' | translate) +
|
||||
': ' +
|
||||
entity.item.title
|
||||
[class.download-library__artwork-button--disabled]="
|
||||
entity.kind === 'movie' &&
|
||||
!canOpen(entity.item)
|
||||
"
|
||||
[attr.aria-disabled]="
|
||||
entity.kind === 'movie' &&
|
||||
!canOpen(entity.item)
|
||||
? 'true'
|
||||
: null
|
||||
"
|
||||
[attr.aria-label]="
|
||||
entity.kind === 'movie'
|
||||
? ('DOWNLOADS.OPEN_ARTWORK'
|
||||
| translate
|
||||
: { title: entity.item.title })
|
||||
: ('DOWNLOADS.PLAY' | translate) +
|
||||
': ' +
|
||||
entity.item.title
|
||||
"
|
||||
[matTooltip]="
|
||||
'DOWNLOADS.SOURCE_PLAYLIST_MISSING'
|
||||
| translate
|
||||
"
|
||||
[matTooltipDisabled]="
|
||||
entity.kind !== 'movie' ||
|
||||
canOpen(entity.item)
|
||||
"
|
||||
(click)="
|
||||
entity.kind === 'movie'
|
||||
? openDetails(entity.item)
|
||||
: emitAction('play', entity.item)
|
||||
"
|
||||
(click)="emitAction('play', entity.item)"
|
||||
>
|
||||
@if (artworkUrl(entity); as artwork) {
|
||||
<img
|
||||
@@ -236,12 +274,37 @@
|
||||
type="button"
|
||||
class="download-library__title-button"
|
||||
[disabled]="isPending(entity.item)"
|
||||
[class.download-library__title-button--disabled]="
|
||||
entity.kind === 'movie' &&
|
||||
!canOpen(entity.item)
|
||||
"
|
||||
[attr.aria-disabled]="
|
||||
entity.kind === 'movie' &&
|
||||
!canOpen(entity.item)
|
||||
? 'true'
|
||||
: null
|
||||
"
|
||||
[attr.aria-label]="
|
||||
('DOWNLOADS.PLAY' | translate) +
|
||||
((entity.kind === 'movie'
|
||||
? 'DOWNLOADS.OPEN_DETAILS'
|
||||
: 'DOWNLOADS.PLAY'
|
||||
) | translate) +
|
||||
': ' +
|
||||
entity.item.title
|
||||
"
|
||||
(click)="emitAction('play', entity.item)"
|
||||
[matTooltip]="
|
||||
'DOWNLOADS.SOURCE_PLAYLIST_MISSING'
|
||||
| translate
|
||||
"
|
||||
[matTooltipDisabled]="
|
||||
entity.kind !== 'movie' ||
|
||||
canOpen(entity.item)
|
||||
"
|
||||
(click)="
|
||||
entity.kind === 'movie'
|
||||
? openDetails(entity.item)
|
||||
: emitAction('play', entity.item)
|
||||
"
|
||||
>
|
||||
{{ entity.item.title }}
|
||||
</button>
|
||||
|
||||
@@ -21,8 +21,8 @@
|
||||
|
||||
.download-library__grid {
|
||||
@include grid.content-grid(
|
||||
$min-width: min(100%, 168px),
|
||||
$gap: clamp(12px, 1.5vw, 20px)
|
||||
$min-width: min(100%, var(--cover-grid-min-width, 148px)),
|
||||
$gap: var(--cover-gap, 16px)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -271,10 +271,6 @@ button:disabled {
|
||||
}
|
||||
|
||||
@media (max-width: 620px) {
|
||||
.download-library__grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.download-library__body {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { FocusMonitor } from '@angular/cdk/a11y';
|
||||
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
@@ -242,12 +244,63 @@ describe('DownloadLibraryComponent', () => {
|
||||
).toContain('1 episode');
|
||||
});
|
||||
|
||||
it('emits every concrete movie action with the movie item', async () => {
|
||||
it('opens movie details from artwork and title without playing locally', async () => {
|
||||
const card = byTestId('download-library-movie-9');
|
||||
const opened: DownloadItem[] = [];
|
||||
const actions: unknown[] = [];
|
||||
component.openRequested.subscribe((selected) => opened.push(selected));
|
||||
component.itemAction.subscribe((action) => actions.push(action));
|
||||
|
||||
await click(button(card, 'Play: Moonrise'));
|
||||
await click(button(card, 'Open details: Moonrise artwork'));
|
||||
await click(button(card, 'Open details: Moonrise'));
|
||||
|
||||
expect(opened).toEqual([MOVIE, MOVIE]);
|
||||
expect(actions).toEqual([]);
|
||||
});
|
||||
|
||||
it('keeps orphaned movie navigation focusable and explains why it cannot open', async () => {
|
||||
fixture.componentRef.setInput('availablePlaylistIds', new Set());
|
||||
await fixture.whenStable();
|
||||
const card = byTestId('download-library-movie-9');
|
||||
const artwork = button(card, 'Open details: Moonrise artwork');
|
||||
const title = button(card, 'Open details: Moonrise');
|
||||
const opened: DownloadItem[] = [];
|
||||
component.openRequested.subscribe((item) => opened.push(item));
|
||||
|
||||
expect(artwork.disabled).toBe(false);
|
||||
expect(title.disabled).toBe(false);
|
||||
expect(artwork.getAttribute('aria-disabled')).toBe('true');
|
||||
expect(title.getAttribute('aria-disabled')).toBe('true');
|
||||
|
||||
const loader = TestbedHarnessEnvironment.loader(fixture);
|
||||
const tooltip = await loader.getHarness(
|
||||
MatTooltipHarness.with({
|
||||
selector:
|
||||
'.download-library__artwork-button[aria-label="Open details: Moonrise artwork"]',
|
||||
})
|
||||
);
|
||||
TestBed.inject(FocusMonitor).focusVia(artwork, 'keyboard');
|
||||
await fixture.whenStable();
|
||||
expect(await tooltip.isOpen()).toBe(true);
|
||||
expect(await tooltip.getTooltipText()).toBe(
|
||||
'This download can no longer open because its source was removed.'
|
||||
);
|
||||
await tooltip.hide();
|
||||
|
||||
await click(artwork);
|
||||
await click(title);
|
||||
expect(opened).toEqual([]);
|
||||
});
|
||||
|
||||
it('keeps explicit movie toolbar and menu commands local', async () => {
|
||||
const card = byTestId('download-library-movie-9');
|
||||
const actions: unknown[] = [];
|
||||
const toolbar = card.querySelector(
|
||||
'.download-library__actions'
|
||||
) as HTMLElement;
|
||||
component.itemAction.subscribe((action) => actions.push(action));
|
||||
|
||||
await click(button(toolbar, 'Play: Moonrise'));
|
||||
await click(button(card, 'Show in folder: Moonrise'));
|
||||
await clickMenuAction(
|
||||
card,
|
||||
@@ -271,7 +324,7 @@ describe('DownloadLibraryComponent', () => {
|
||||
it('opens grouped-series details from both real navigation buttons', async () => {
|
||||
const card = byTestId('download-library-series-playlist-a-77');
|
||||
const opened: DownloadItem[] = [];
|
||||
component.seriesOpened.subscribe((selected) => opened.push(selected));
|
||||
component.openRequested.subscribe((selected) => opened.push(selected));
|
||||
|
||||
const artwork = button(card, 'Open details: Northwind artwork');
|
||||
const title = button(card, 'Open details: Northwind');
|
||||
@@ -294,7 +347,7 @@ describe('DownloadLibraryComponent', () => {
|
||||
const title = button(card, 'Open details: Northwind');
|
||||
const episodes = button(card, 'Open downloaded episodes: Northwind');
|
||||
const opened: DownloadItem[] = [];
|
||||
component.seriesOpened.subscribe((item) => opened.push(item));
|
||||
component.openRequested.subscribe((item) => opened.push(item));
|
||||
|
||||
expect(artwork.disabled).toBe(false);
|
||||
expect(title.disabled).toBe(false);
|
||||
@@ -305,7 +358,8 @@ describe('DownloadLibraryComponent', () => {
|
||||
const loader = TestbedHarnessEnvironment.loader(fixture);
|
||||
const tooltip = await loader.getHarness(
|
||||
MatTooltipHarness.with({
|
||||
selector: '.download-library__artwork-button',
|
||||
selector:
|
||||
'[data-test-id="download-library-series-open"].download-library__artwork-button',
|
||||
})
|
||||
);
|
||||
TestBed.inject(FocusMonitor).focusVia(artwork, 'keyboard');
|
||||
@@ -339,23 +393,30 @@ describe('DownloadLibraryComponent', () => {
|
||||
it('keeps an invalid-series episode local without series navigation', async () => {
|
||||
const card = byTestId('download-library-episode-21');
|
||||
const itemActions: unknown[] = [];
|
||||
const seriesOpened: DownloadItem[] = [];
|
||||
const opened: DownloadItem[] = [];
|
||||
component.itemAction.subscribe((action) => itemActions.push(action));
|
||||
component.seriesOpened.subscribe((selected) =>
|
||||
seriesOpened.push(selected)
|
||||
);
|
||||
component.openRequested.subscribe((selected) => opened.push(selected));
|
||||
|
||||
expect(
|
||||
card.querySelector('[data-test-id="download-library-series-open"]')
|
||||
).toBeNull();
|
||||
await click(button(card, 'Play: Legacy episode'));
|
||||
await click(button(card, 'Show in folder: Legacy episode'));
|
||||
const artwork = card.querySelector(
|
||||
'.download-library__artwork-button'
|
||||
) as HTMLButtonElement;
|
||||
const title = card.querySelector(
|
||||
'.download-library__title-button'
|
||||
) as HTMLButtonElement;
|
||||
|
||||
expect(artwork.getAttribute('aria-label')).toBe('Play: Legacy episode');
|
||||
expect(title.getAttribute('aria-label')).toBe('Play: Legacy episode');
|
||||
await click(artwork);
|
||||
await click(title);
|
||||
|
||||
expect(itemActions).toEqual([
|
||||
{ type: 'play', item: FALLBACK_EPISODE },
|
||||
{ type: 'reveal', item: FALLBACK_EPISODE },
|
||||
{ type: 'play', item: FALLBACK_EPISODE },
|
||||
]);
|
||||
expect(seriesOpened).toEqual([]);
|
||||
expect(opened).toEqual([]);
|
||||
});
|
||||
|
||||
it('disables every local command while the concrete item is pending', async () => {
|
||||
@@ -388,4 +449,17 @@ describe('DownloadLibraryComponent', () => {
|
||||
);
|
||||
expect(placeholders).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('uses the shared cover-grid sizing contract', () => {
|
||||
const styles = readFileSync(
|
||||
join(__dirname, 'download-library.component.scss'),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
expect(styles).toContain('var(--cover-grid-min-width, 148px)');
|
||||
expect(styles).toContain('var(--cover-gap, 16px)');
|
||||
expect(styles).not.toContain(
|
||||
'grid-template-columns: repeat(2, minmax(0, 1fr))'
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -39,7 +39,7 @@ export class DownloadLibraryComponent {
|
||||
readonly availablePlaylistIds = input.required<ReadonlySet<string>>();
|
||||
readonly pendingIds = input<ReadonlySet<number>>(new Set());
|
||||
readonly itemAction = output<DownloadItemAction>();
|
||||
readonly seriesOpened = output<DownloadItem>();
|
||||
readonly openRequested = output<DownloadItem>();
|
||||
readonly episodesOpened = output<DownloadSeriesCardViewModel>();
|
||||
readonly failedArtwork = signal<ReadonlySet<string>>(new Set());
|
||||
|
||||
@@ -81,8 +81,8 @@ export class DownloadLibraryComponent {
|
||||
return this.pendingIds().has(item.id);
|
||||
}
|
||||
|
||||
protected canOpenSeries(group: DownloadSeriesCardViewModel): boolean {
|
||||
return this.availablePlaylistIds().has(group.representative.playlistId);
|
||||
protected canOpen(item: DownloadItem): boolean {
|
||||
return this.availablePlaylistIds().has(item.playlistId);
|
||||
}
|
||||
|
||||
protected emitAction(
|
||||
@@ -94,9 +94,9 @@ export class DownloadLibraryComponent {
|
||||
}
|
||||
}
|
||||
|
||||
protected openSeries(group: DownloadSeriesCardViewModel): void {
|
||||
if (this.canOpenSeries(group)) {
|
||||
this.seriesOpened.emit(group.representative);
|
||||
protected openDetails(item: DownloadItem): void {
|
||||
if (!this.isPending(item) && this.canOpen(item)) {
|
||||
this.openRequested.emit(item);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user