mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-11 02:46:16 -08:00
This commit is contained in:
1 parent
cbc918b096
commit
bd848aaad6
53 files changed
+1980
-87
No files matched your search
+8
@@ -34,6 +34,14 @@ export class WorkspaceShellXtreamImportService {
|
||||
{ initialValue: null }
|
||||
);
|
||||
|
||||
isSourceBusy(playlistId: string): boolean {
|
||||
return (
|
||||
this.refreshPreparation()?.playlistId === playlistId ||
|
||||
(this.xtreamStore.isImporting() &&
|
||||
this.xtreamStore.playlistId() === playlistId)
|
||||
);
|
||||
}
|
||||
|
||||
private get isElectron(): boolean {
|
||||
return this.runtime.isElectron;
|
||||
}
|
||||
|
||||
+13
-1
@@ -2,12 +2,24 @@
|
||||
class="sources-page"
|
||||
[class.sources-page--empty]="visibleSourcesCount() === 0"
|
||||
>
|
||||
@if (visibleSourcesCount() > 0) {
|
||||
@if (visibleSourcesCount() > 0 || canCleanSources()) {
|
||||
<header class="sources-header">
|
||||
<div class="sources-header__meta">
|
||||
<h2 class="sources-title">{{ title() }}</h2>
|
||||
<span class="sources-subtitle">{{ subtitle() }}</span>
|
||||
</div>
|
||||
@if (canCleanSources()) {
|
||||
<button
|
||||
mat-stroked-button
|
||||
type="button"
|
||||
class="cleanup-trigger"
|
||||
[attr.aria-label]="'SOURCE_CLEANUP.TITLE' | translate"
|
||||
(click)="openCleanup()"
|
||||
>
|
||||
<mat-icon>cleaning_services</mat-icon>
|
||||
<span>{{ 'SOURCE_CLEANUP.TITLE' | translate }}</span>
|
||||
</button>
|
||||
}
|
||||
<button
|
||||
type="button"
|
||||
mat-stroked-button
|
||||
|
||||
+10
@@ -96,3 +96,13 @@
|
||||
padding: 12px 16px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.cleanup-trigger {
|
||||
flex-shrink: 0;
|
||||
app-region: no-drag;
|
||||
}
|
||||
@media (max-width: 1100px) {
|
||||
.cleanup-trigger span {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
+65
-3
@@ -1,4 +1,8 @@
|
||||
import { Component, input, output } from '@angular/core';
|
||||
import { PORTAL_EXTERNAL_PLAYBACK } from '@iptvnator/portal/shared/util';
|
||||
import { SourceActivityService } from '@iptvnator/services';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { PlaylistRefreshActionService } from '@iptvnator/playlist/shared/ui';
|
||||
import { Component, input, output, signal } from '@angular/core';
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
@@ -33,6 +37,18 @@ describe('WorkspaceSourcesComponent', () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [WorkspaceSourcesComponent, NoopAnimationsModule],
|
||||
providers: [
|
||||
{
|
||||
provide: PORTAL_EXTERNAL_PLAYBACK,
|
||||
useValue: { activeSession: signal(null) },
|
||||
},
|
||||
{
|
||||
provide: MatDialog,
|
||||
useValue: { getDialogById: jest.fn(), open: jest.fn() },
|
||||
},
|
||||
{
|
||||
provide: PlaylistRefreshActionService,
|
||||
useValue: { isSourceBusy: jest.fn((id) => id === 'busy') },
|
||||
},
|
||||
provideMockStore({
|
||||
selectors: [
|
||||
{
|
||||
@@ -125,14 +141,60 @@ describe('WorkspaceSourcesComponent', () => {
|
||||
fixture = TestBed.createComponent(WorkspaceSourcesComponent);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['launching', true, true],
|
||||
['opened', true, true],
|
||||
['playing', true, true],
|
||||
['closed', false, false],
|
||||
['error', false, false],
|
||||
['error', true, true],
|
||||
])(
|
||||
'protects only a live external session (%s, closable=%s)',
|
||||
(status, canClose, expected) => {
|
||||
const playback = TestBed.inject(
|
||||
PORTAL_EXTERNAL_PLAYBACK
|
||||
) as unknown as { activeSession: { set(value: unknown): void } };
|
||||
playback.activeSession.set({
|
||||
status,
|
||||
canClose,
|
||||
contentInfo: { playlistId: 'external' },
|
||||
});
|
||||
fixture.componentInstance.openCleanup();
|
||||
const context = (TestBed.inject(MatDialog).open as jest.Mock).mock
|
||||
.calls[0][1].data;
|
||||
expect(context.protected('external')).toBe(expected);
|
||||
}
|
||||
);
|
||||
|
||||
it('protects startup auto-refresh sources across the whole library', () => {
|
||||
const release = TestBed.inject(SourceActivityService).begin([
|
||||
'startup',
|
||||
]);
|
||||
fixture.componentInstance.openCleanup();
|
||||
const context = (TestBed.inject(MatDialog).open as jest.Mock).mock
|
||||
.calls[0][1].data;
|
||||
expect(context.protected('startup')).toBe(true);
|
||||
release();
|
||||
expect(context.protected('startup')).toBe(false);
|
||||
});
|
||||
|
||||
it('protects a source refreshing through the persistent header', () => {
|
||||
fixture.componentInstance.openCleanup();
|
||||
const open = TestBed.inject(MatDialog).open as jest.Mock;
|
||||
const context = open.mock.calls[0][1].data;
|
||||
expect(context.protected('busy')).toBe(true);
|
||||
expect(context.protected('other')).toBe(false);
|
||||
});
|
||||
|
||||
it('renders the shared panel header structure without paragraph subtitle margins', async () => {
|
||||
fixture.detectChanges();
|
||||
await fixture.whenStable();
|
||||
|
||||
const header: HTMLElement =
|
||||
fixture.nativeElement.querySelector('.sources-header');
|
||||
const meta: HTMLElement =
|
||||
fixture.nativeElement.querySelector('.sources-header__meta');
|
||||
const meta: HTMLElement = fixture.nativeElement.querySelector(
|
||||
'.sources-header__meta'
|
||||
);
|
||||
const title: HTMLElement =
|
||||
fixture.nativeElement.querySelector('.sources-title');
|
||||
const subtitle: HTMLElement =
|
||||
|
||||
+73
-1
@@ -1,3 +1,23 @@
|
||||
import { WorkspaceShellXtreamImportService } from '../workspace-shell/services/workspace-shell-xtream-import.service';
|
||||
import { Injector, viewChild } from '@angular/core';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import {
|
||||
SourceCleanupDialogComponent,
|
||||
PlaylistRefreshActionService,
|
||||
} from '@iptvnator/playlist/shared/ui';
|
||||
import {
|
||||
RuntimeCapabilitiesService,
|
||||
SourceActivityService,
|
||||
} from '@iptvnator/services';
|
||||
import {
|
||||
PORTAL_EXTERNAL_PLAYBACK,
|
||||
isLiveExternalPlayerSession,
|
||||
} from '@iptvnator/portal/shared/util';
|
||||
import { PlaylistActions } from '@iptvnator/m3u-state';
|
||||
import {
|
||||
sourceHealthType,
|
||||
PlaylistUpdateState,
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
import { Component, computed, inject } from '@angular/core';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
@@ -7,7 +27,10 @@ import { ActivatedRoute } from '@angular/router';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { RecentPlaylistsComponent } from '@iptvnator/playlist/shared/ui';
|
||||
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
|
||||
import { selectActiveTypeFilters, selectAllPlaylistsMeta } from '@iptvnator/m3u-state';
|
||||
import {
|
||||
selectActiveTypeFilters,
|
||||
selectAllPlaylistsMeta,
|
||||
} from '@iptvnator/m3u-state';
|
||||
import { map, startWith } from 'rxjs';
|
||||
import { SortBy, SortOrder, SortService } from '@iptvnator/services';
|
||||
import {
|
||||
@@ -35,6 +58,55 @@ interface SortOption {
|
||||
styleUrl: './workspace-sources.component.scss',
|
||||
})
|
||||
export class WorkspaceSourcesComponent {
|
||||
private readonly injector = inject(Injector);
|
||||
private readonly runtime = inject(RuntimeCapabilitiesService);
|
||||
private readonly playback = inject(PORTAL_EXTERNAL_PLAYBACK, {
|
||||
optional: true,
|
||||
});
|
||||
private readonly imports = inject(WorkspaceShellXtreamImportService, {
|
||||
optional: true,
|
||||
});
|
||||
private readonly sourceList = viewChild(RecentPlaylistsComponent);
|
||||
readonly canCleanSources = computed(
|
||||
() =>
|
||||
this.runtime.supportsSourceHealth &&
|
||||
this.playlists().some((p) => sourceHealthType(p))
|
||||
);
|
||||
openCleanup(): void {
|
||||
const dialogs = this.injector.get(MatDialog);
|
||||
const refresh = this.injector.get(PlaylistRefreshActionService);
|
||||
const activity = this.injector.get(SourceActivityService);
|
||||
if (dialogs.getDialogById('source-cleanup')) return;
|
||||
dialogs.open(SourceCleanupDialogComponent, {
|
||||
id: 'source-cleanup',
|
||||
width: '680px',
|
||||
maxWidth: '95vw',
|
||||
data: {
|
||||
playlists: this.playlists(),
|
||||
current: (id: string) =>
|
||||
this.playlists().find((p) => p._id === id),
|
||||
protected: (id: string) =>
|
||||
(isLiveExternalPlayerSession(
|
||||
this.playback?.activeSession()
|
||||
) &&
|
||||
this.playback?.activeSession()?.contentInfo
|
||||
?.playlistId === id) ||
|
||||
activity.isBusy(id) ||
|
||||
refresh.isSourceBusy(id) ||
|
||||
!!this.imports?.isSourceBusy(id) ||
|
||||
!!this.sourceList()?.isDeletePending(id) ||
|
||||
!!this.sourceList()?.isRefreshPending(id) ||
|
||||
this.playlists().find((p) => p._id === id)?.updateState ===
|
||||
PlaylistUpdateState.IN_PROGRESS,
|
||||
removed: (id: string) =>
|
||||
this.store.dispatch(
|
||||
PlaylistActions.playlistRemovalCommitted({
|
||||
playlistId: id,
|
||||
})
|
||||
),
|
||||
},
|
||||
});
|
||||
}
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
private readonly store = inject(Store);
|
||||
private readonly workspaceActions = inject(WORKSPACE_SHELL_ACTIONS);
|
||||
|
||||
Reference in new issue
Block a user