mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
refactor(playlist-ui): use runtime capabilities
This commit is contained in:
1 parent
2fc3d4295f
commit
6ad61ce680
10 files changed
+366
-41
No files matched your search
@@ -10,6 +10,7 @@ import {
|
||||
DbOperationEvent,
|
||||
PlaybackPositionService,
|
||||
PlaylistRefreshService,
|
||||
RuntimeCapabilitiesService,
|
||||
} from '@iptvnator/services';
|
||||
import { ChannelActions, PlaylistActions } from '@iptvnator/m3u-state';
|
||||
import { Playlist, PlaylistMeta } from '@iptvnator/shared/interfaces';
|
||||
@@ -75,6 +76,9 @@ describe('PlaylistRefreshActionService', () => {
|
||||
let playbackPositionService: {
|
||||
getAllPlaybackPositions: jest.Mock;
|
||||
};
|
||||
let runtime: {
|
||||
isElectron: boolean;
|
||||
};
|
||||
let routeProvider: ReturnType<
|
||||
typeof signal<'playlists' | 'xtreams' | null>
|
||||
>;
|
||||
@@ -126,6 +130,9 @@ describe('PlaylistRefreshActionService', () => {
|
||||
playbackPositionService = {
|
||||
getAllPlaybackPositions: jest.fn().mockResolvedValue([]),
|
||||
};
|
||||
runtime = {
|
||||
isElectron: true,
|
||||
};
|
||||
routeProvider = signal<'playlists' | 'xtreams' | null>('xtreams');
|
||||
resolvedPlaylistId = signal<string | null>(null);
|
||||
|
||||
@@ -166,6 +173,10 @@ describe('PlaylistRefreshActionService', () => {
|
||||
provide: PlaybackPositionService,
|
||||
useValue: playbackPositionService,
|
||||
},
|
||||
{
|
||||
provide: RuntimeCapabilitiesService,
|
||||
useValue: runtime,
|
||||
},
|
||||
{
|
||||
provide: PlaylistContextFacade,
|
||||
useValue: {
|
||||
@@ -185,7 +196,7 @@ describe('PlaylistRefreshActionService', () => {
|
||||
});
|
||||
|
||||
it('treats file-backed M3U playlists as refreshable in Electron', () => {
|
||||
window.electron = { platform: 'darwin' } as typeof window.electron;
|
||||
runtime.isElectron = true;
|
||||
|
||||
expect(
|
||||
service.canRefresh(
|
||||
@@ -200,7 +211,7 @@ describe('PlaylistRefreshActionService', () => {
|
||||
});
|
||||
|
||||
it('does not expose filesystem refresh outside Electron', () => {
|
||||
window.electron = undefined as unknown as typeof window.electron;
|
||||
runtime.isElectron = false;
|
||||
|
||||
expect(
|
||||
service.canRefresh(
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
isDbAbortError,
|
||||
PlaybackPositionService,
|
||||
PlaylistRefreshService,
|
||||
RuntimeCapabilitiesService,
|
||||
XtreamPendingRestoreService,
|
||||
} from '@iptvnator/services';
|
||||
import { ChannelActions, PlaylistActions } from '@iptvnator/m3u-state';
|
||||
@@ -34,6 +35,7 @@ export class PlaylistRefreshActionService {
|
||||
private readonly databaseService = inject(DatabaseService);
|
||||
private readonly playbackPositionService = inject(PlaybackPositionService);
|
||||
private readonly playlistRefreshService = inject(PlaylistRefreshService);
|
||||
private readonly runtime = inject(RuntimeCapabilitiesService);
|
||||
private readonly playlistContext = inject(PlaylistContextFacade);
|
||||
private readonly pendingRestoreService = inject(
|
||||
XtreamPendingRestoreService
|
||||
@@ -46,7 +48,7 @@ export class PlaylistRefreshActionService {
|
||||
readonly refreshPreparation = this.refreshPreparationState.asReadonly();
|
||||
|
||||
canRefresh(playlist: PlaylistMeta | null): boolean {
|
||||
if (!playlist || !window.electron) {
|
||||
if (!playlist || !this.runtime.isElectron) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { of } from 'rxjs';
|
||||
import {
|
||||
DatabaseService,
|
||||
PlaylistsService,
|
||||
RuntimeCapabilitiesService,
|
||||
} from '@iptvnator/services';
|
||||
import { Playlist } from '@iptvnator/shared/interfaces';
|
||||
import { PlaylistInfoComponent } from './playlist-info.component';
|
||||
|
||||
describe('PlaylistInfoComponent', () => {
|
||||
let component: PlaylistInfoComponent;
|
||||
let fixture: ComponentFixture<PlaylistInfoComponent>;
|
||||
let playlistsService: {
|
||||
getRawPlaylistById: jest.Mock;
|
||||
};
|
||||
let runtime: {
|
||||
isElectron: boolean;
|
||||
supportsDesktopFileSave: boolean;
|
||||
};
|
||||
let snackBar: {
|
||||
open: jest.Mock;
|
||||
};
|
||||
const originalElectron = window.electron;
|
||||
|
||||
const playlist = {
|
||||
id: 'playlist-1',
|
||||
_id: 'playlist-1',
|
||||
title: 'My Playlist',
|
||||
count: 1,
|
||||
importDate: '2026-04-01T00:00:00.000Z',
|
||||
autoRefresh: false,
|
||||
url: 'https://example.com/playlist.m3u',
|
||||
} as Playlist & { id: string };
|
||||
|
||||
beforeEach(async () => {
|
||||
playlistsService = {
|
||||
getRawPlaylistById: jest.fn(() => of('#EXTM3U\n')),
|
||||
};
|
||||
runtime = {
|
||||
isElectron: false,
|
||||
supportsDesktopFileSave: false,
|
||||
};
|
||||
snackBar = {
|
||||
open: jest.fn(),
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [PlaylistInfoComponent],
|
||||
providers: [
|
||||
{
|
||||
provide: MAT_DIALOG_DATA,
|
||||
useValue: playlist,
|
||||
},
|
||||
{
|
||||
provide: PlaylistsService,
|
||||
useValue: playlistsService,
|
||||
},
|
||||
{
|
||||
provide: DatabaseService,
|
||||
useValue: {
|
||||
updateXtreamPlaylistDetails: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: Store,
|
||||
useValue: {
|
||||
dispatch: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: MatSnackBar,
|
||||
useValue: snackBar,
|
||||
},
|
||||
{
|
||||
provide: TranslateService,
|
||||
useValue: {
|
||||
instant: jest.fn((key: string) => key),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: RuntimeCapabilitiesService,
|
||||
useValue: runtime,
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
window.electron = originalElectron;
|
||||
});
|
||||
|
||||
function createComponent(): void {
|
||||
fixture = TestBed.createComponent(PlaylistInfoComponent);
|
||||
component = fixture.componentInstance;
|
||||
}
|
||||
|
||||
it('uses the Electron save dialog when desktop file saving is available', async () => {
|
||||
runtime.isElectron = true;
|
||||
runtime.supportsDesktopFileSave = true;
|
||||
window.electron = {
|
||||
saveFileDialog: jest.fn().mockResolvedValue('/tmp/export.m3u8'),
|
||||
writeFile: jest.fn().mockResolvedValue({ success: true }),
|
||||
} as typeof window.electron;
|
||||
createComponent();
|
||||
|
||||
await component.exportPlaylist();
|
||||
|
||||
expect(window.electron.saveFileDialog).toHaveBeenCalledWith(
|
||||
'My Playlist.m3u8',
|
||||
[{ name: 'Playlist', extensions: ['m3u8', 'm3u'] }]
|
||||
);
|
||||
expect(window.electron.writeFile).toHaveBeenCalledWith(
|
||||
'/tmp/export.m3u8',
|
||||
'#EXTM3U\n'
|
||||
);
|
||||
expect(snackBar.open).toHaveBeenCalledWith(
|
||||
'HOME.PLAYLISTS.INFO_DIALOG.PLAYLIST_EXPORT_SUCCESS',
|
||||
'CLOSE',
|
||||
{ duration: 3000 }
|
||||
);
|
||||
});
|
||||
|
||||
it('uses file-save capability for desktop-only playlist details UI', () => {
|
||||
runtime.isElectron = true;
|
||||
runtime.supportsDesktopFileSave = false;
|
||||
createComponent();
|
||||
|
||||
expect(component.isDesktop).toBe(false);
|
||||
});
|
||||
|
||||
it('falls back to browser download when desktop file saving is unavailable', async () => {
|
||||
const clickSpy = jest
|
||||
.spyOn(HTMLAnchorElement.prototype, 'click')
|
||||
.mockImplementation();
|
||||
createComponent();
|
||||
|
||||
await component.exportPlaylist();
|
||||
|
||||
expect(playlistsService.getRawPlaylistById).toHaveBeenCalledWith(
|
||||
'playlist-1'
|
||||
);
|
||||
expect(clickSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
+45
-20
@@ -19,9 +19,18 @@ import { Store } from '@ngrx/store';
|
||||
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
|
||||
import { PlaylistActions } from '@iptvnator/m3u-state';
|
||||
import { firstValueFrom } from 'rxjs';
|
||||
import { DatabaseService, PlaylistsService } from '@iptvnator/services';
|
||||
import {
|
||||
DatabaseService,
|
||||
PlaylistsService,
|
||||
RuntimeCapabilitiesService,
|
||||
} from '@iptvnator/services';
|
||||
import { Playlist, PlaylistMeta } from '@iptvnator/shared/interfaces';
|
||||
|
||||
type DesktopFileSaveBridge = Pick<
|
||||
typeof window.electron,
|
||||
'saveFileDialog' | 'writeFile'
|
||||
>;
|
||||
|
||||
@Component({
|
||||
selector: 'app-playlist-info',
|
||||
templateUrl: './playlist-info.component.html',
|
||||
@@ -80,9 +89,12 @@ export class PlaylistInfoComponent {
|
||||
private databaseService = inject(DatabaseService);
|
||||
private snackBar = inject(MatSnackBar);
|
||||
private translate = inject(TranslateService);
|
||||
private runtime = inject(RuntimeCapabilitiesService);
|
||||
public playlistData = inject<Playlist & { id: string }>(MAT_DIALOG_DATA);
|
||||
|
||||
readonly isDesktop = !!window.electron;
|
||||
get isDesktop(): boolean {
|
||||
return this.runtime.supportsDesktopFileSave;
|
||||
}
|
||||
|
||||
/** Playlist object */
|
||||
playlist: Playlist & { id: string };
|
||||
@@ -202,9 +214,12 @@ export class PlaylistInfoComponent {
|
||||
this.playlistsService.getRawPlaylistById(this.playlist._id)
|
||||
);
|
||||
|
||||
if (this.isDesktop) {
|
||||
if (this.runtime.supportsDesktopFileSave) {
|
||||
const desktopFileBridge =
|
||||
window.electron as DesktopFileSaveBridge;
|
||||
|
||||
try {
|
||||
const savePath = await window.electron.saveFileDialog(
|
||||
const savePath = await desktopFileBridge.saveFileDialog(
|
||||
`${this.playlist.title || 'exported'}.m3u8`,
|
||||
[
|
||||
{
|
||||
@@ -215,7 +230,10 @@ export class PlaylistInfoComponent {
|
||||
);
|
||||
|
||||
if (savePath) {
|
||||
await window.electron.writeFile(savePath, playlistAsString);
|
||||
await desktopFileBridge.writeFile(
|
||||
savePath,
|
||||
playlistAsString
|
||||
);
|
||||
this.snackBar.open(
|
||||
this.translate.instant(
|
||||
'HOME.PLAYLISTS.INFO_DIALOG.PLAYLIST_EXPORT_SUCCESS'
|
||||
@@ -224,6 +242,8 @@ export class PlaylistInfoComponent {
|
||||
{ duration: 3000 }
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
} catch (error) {
|
||||
console.error('Failed to export playlist:', error);
|
||||
this.snackBar.open(
|
||||
@@ -235,23 +255,28 @@ export class PlaylistInfoComponent {
|
||||
duration: 3000,
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const element = document.createElement('a');
|
||||
element.setAttribute(
|
||||
'href',
|
||||
'data:text/plain;charset=utf-8,' +
|
||||
encodeURIComponent(playlistAsString)
|
||||
);
|
||||
element.setAttribute(
|
||||
'download',
|
||||
this.playlist.title || 'exported.m3u'
|
||||
);
|
||||
element.style.display = 'none';
|
||||
document.body.appendChild(element);
|
||||
element.click();
|
||||
document.body.removeChild(element);
|
||||
}
|
||||
|
||||
this.downloadPlaylistFile(playlistAsString);
|
||||
}
|
||||
|
||||
private downloadPlaylistFile(playlistAsString: string): void {
|
||||
const element = document.createElement('a');
|
||||
element.setAttribute(
|
||||
'href',
|
||||
'data:text/plain;charset=utf-8,' +
|
||||
encodeURIComponent(playlistAsString)
|
||||
);
|
||||
element.setAttribute(
|
||||
'download',
|
||||
this.playlist.title || 'exported.m3u'
|
||||
);
|
||||
element.style.display = 'none';
|
||||
document.body.appendChild(element);
|
||||
element.click();
|
||||
document.body.removeChild(element);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+71
-8
@@ -4,14 +4,24 @@ import { MatListModule } from '@angular/material/list';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { MockModule } from 'ng-mocks';
|
||||
import { PortalStatusService } from '@iptvnator/services';
|
||||
import {
|
||||
PortalStatusService,
|
||||
RuntimeCapabilitiesService,
|
||||
} from '@iptvnator/services';
|
||||
import { PlaylistItemComponent } from './playlist-item.component';
|
||||
|
||||
describe('PlaylistItemComponent', () => {
|
||||
let component: PlaylistItemComponent;
|
||||
let fixture: ComponentFixture<PlaylistItemComponent>;
|
||||
let runtime: {
|
||||
isElectron: boolean;
|
||||
};
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
runtime = {
|
||||
isElectron: true,
|
||||
};
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
PlaylistItemComponent,
|
||||
@@ -31,6 +41,10 @@ describe('PlaylistItemComponent', () => {
|
||||
getStatusIcon: jest.fn(() => 'check_circle'),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: RuntimeCapabilitiesService,
|
||||
useValue: runtime,
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
}));
|
||||
@@ -94,21 +108,70 @@ describe('PlaylistItemComponent', () => {
|
||||
expect(nativeElement.querySelector('.refresh-btn')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('renders the Xtream refresh action only when Electron capabilities are available', () => {
|
||||
fixture.destroy();
|
||||
runtime.isElectron = true;
|
||||
fixture = TestBed.createComponent(PlaylistItemComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.item = {
|
||||
title: 'Xtream Source',
|
||||
_id: 'xtream-source',
|
||||
count: 10,
|
||||
importDate: Date.now().toString(),
|
||||
autoRefresh: false,
|
||||
serverUrl: 'https://example.com',
|
||||
username: 'demo',
|
||||
password: 'secret',
|
||||
};
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(
|
||||
(fixture.nativeElement as HTMLElement).querySelector(
|
||||
'.refresh-btn'
|
||||
)
|
||||
).not.toBeNull();
|
||||
|
||||
fixture.destroy();
|
||||
runtime.isElectron = false;
|
||||
fixture = TestBed.createComponent(PlaylistItemComponent);
|
||||
component = fixture.componentInstance;
|
||||
component.item = {
|
||||
title: 'Xtream Source',
|
||||
_id: 'xtream-source',
|
||||
count: 10,
|
||||
importDate: Date.now().toString(),
|
||||
autoRefresh: false,
|
||||
serverUrl: 'https://example.com',
|
||||
username: 'demo',
|
||||
password: 'secret',
|
||||
};
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(
|
||||
(fixture.nativeElement as HTMLElement).querySelector(
|
||||
'.refresh-btn'
|
||||
)
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('renders cancel and progress UI for long-running playlist actions', () => {
|
||||
fixture.componentRef.setInput('isDeleting', true);
|
||||
fixture.componentRef.setInput('busyMessage', 'Removing cached content...');
|
||||
fixture.componentRef.setInput(
|
||||
'busyMessage',
|
||||
'Removing cached content...'
|
||||
);
|
||||
fixture.componentRef.setInput('busyProgress', 42);
|
||||
fixture.componentRef.setInput('canCancelBusyAction', true);
|
||||
fixture.detectChanges();
|
||||
|
||||
const nativeElement = fixture.nativeElement as HTMLElement;
|
||||
|
||||
expect(nativeElement.querySelector('.busy-state__message')?.textContent).toContain(
|
||||
'Removing cached content...'
|
||||
);
|
||||
expect(nativeElement.querySelector('.busy-state__value')?.textContent).toContain(
|
||||
'42%'
|
||||
);
|
||||
expect(
|
||||
nativeElement.querySelector('.busy-state__message')?.textContent
|
||||
).toContain('Removing cached content...');
|
||||
expect(
|
||||
nativeElement.querySelector('.busy-state__value')?.textContent
|
||||
).toContain('42%');
|
||||
expect(nativeElement.querySelector('.cancel-btn')).not.toBeNull();
|
||||
});
|
||||
});
|
||||
+7
-2
@@ -18,7 +18,11 @@ import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { normalizeDateLocale } from '@iptvnator/pipes';
|
||||
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
|
||||
import { startWith } from 'rxjs';
|
||||
import { PortalStatus, PortalStatusService } from '@iptvnator/services';
|
||||
import {
|
||||
PortalStatus,
|
||||
PortalStatusService,
|
||||
RuntimeCapabilitiesService,
|
||||
} from '@iptvnator/services';
|
||||
import { PlaylistMeta } from '@iptvnator/shared/interfaces';
|
||||
|
||||
@Component({
|
||||
@@ -56,13 +60,14 @@ export class PlaylistItemComponent implements OnInit {
|
||||
|
||||
portalStatus: PortalStatus = 'unavailable';
|
||||
private readonly portalStatusService = inject(PortalStatusService);
|
||||
private readonly runtime = inject(RuntimeCapabilitiesService);
|
||||
private readonly translate = inject(TranslateService);
|
||||
private readonly languageTick = toSignal(
|
||||
this.translate.onLangChange.pipe(startWith(null)),
|
||||
{ initialValue: null }
|
||||
);
|
||||
|
||||
readonly isElectron = !!window.electron;
|
||||
readonly isElectron = this.runtime.isElectron;
|
||||
readonly currentLocale = computed(() => {
|
||||
this.languageTick();
|
||||
return normalizeDateLocale(
|
||||
|
||||
+61
-6
@@ -20,6 +20,8 @@ import {
|
||||
DbOperationEvent,
|
||||
PlaybackPositionService,
|
||||
PlaylistDeleteActionService,
|
||||
PlaylistRefreshService,
|
||||
RuntimeCapabilitiesService,
|
||||
SortBy,
|
||||
SortOrder,
|
||||
SortService,
|
||||
@@ -81,6 +83,11 @@ describe('RecentPlaylistsComponent busy state', () => {
|
||||
let playlistDeleteAction: {
|
||||
deletePlaylist: jest.Mock;
|
||||
};
|
||||
let playlistRefreshService: {
|
||||
cancelRefresh: jest.Mock;
|
||||
refreshPlaylist: jest.Mock;
|
||||
};
|
||||
let runtimeIsElectron: boolean;
|
||||
let router: {
|
||||
navigate: jest.Mock;
|
||||
};
|
||||
@@ -114,6 +121,14 @@ describe('RecentPlaylistsComponent busy state', () => {
|
||||
playlistDeleteAction = {
|
||||
deletePlaylist: jest.fn().mockResolvedValue(true),
|
||||
};
|
||||
playlistRefreshService = {
|
||||
cancelRefresh: jest.fn().mockResolvedValue(undefined),
|
||||
refreshPlaylist: jest.fn().mockResolvedValue({
|
||||
id: 'playlist-1',
|
||||
items: [],
|
||||
}),
|
||||
};
|
||||
runtimeIsElectron = true;
|
||||
router = {
|
||||
navigate: jest.fn(),
|
||||
};
|
||||
@@ -156,6 +171,18 @@ describe('RecentPlaylistsComponent busy state', () => {
|
||||
provide: PlaylistDeleteActionService,
|
||||
useValue: playlistDeleteAction,
|
||||
},
|
||||
{
|
||||
provide: PlaylistRefreshService,
|
||||
useValue: playlistRefreshService,
|
||||
},
|
||||
{
|
||||
provide: RuntimeCapabilitiesService,
|
||||
useValue: {
|
||||
get isElectron() {
|
||||
return runtimeIsElectron;
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: PlaylistContextFacade,
|
||||
useValue: {
|
||||
@@ -452,12 +479,7 @@ describe('RecentPlaylistsComponent busy state', () => {
|
||||
});
|
||||
|
||||
it('uses the legacy IPC refresh flow for non-Xtream playlists', () => {
|
||||
window.electron = undefined as unknown as typeof window.electron;
|
||||
(
|
||||
component as unknown as {
|
||||
isElectron: boolean;
|
||||
}
|
||||
).isElectron = false;
|
||||
runtimeIsElectron = false;
|
||||
const item = createPlaylistMeta({
|
||||
_id: 'playlist-m3u-1',
|
||||
serverUrl: undefined,
|
||||
@@ -474,4 +496,37 @@ describe('RecentPlaylistsComponent busy state', () => {
|
||||
filePath: item.filePath,
|
||||
});
|
||||
});
|
||||
|
||||
it('re-evaluates Electron availability when refreshing local M3U playlists', async () => {
|
||||
runtimeIsElectron = false;
|
||||
const lateComponent = TestBed.createComponent(
|
||||
RecentPlaylistsComponent
|
||||
).componentInstance;
|
||||
runtimeIsElectron = true;
|
||||
const item = createPlaylistMeta({
|
||||
_id: 'playlist-m3u-2',
|
||||
serverUrl: undefined,
|
||||
username: undefined,
|
||||
password: undefined,
|
||||
filePath: '/tmp/test.m3u',
|
||||
});
|
||||
|
||||
lateComponent.refreshPlaylist(item);
|
||||
|
||||
expect(playlistRefreshService.refreshPlaylist).toHaveBeenCalledWith(
|
||||
{
|
||||
operationId: 'playlist-refresh-op',
|
||||
playlistId: item._id,
|
||||
title: item.title,
|
||||
url: item.url,
|
||||
filePath: item.filePath,
|
||||
},
|
||||
{
|
||||
onEvent: expect.any(Function),
|
||||
}
|
||||
);
|
||||
expect(dataService.sendIpcEvent).not.toHaveBeenCalled();
|
||||
|
||||
await Promise.resolve();
|
||||
});
|
||||
});
|
||||
@@ -97,7 +97,9 @@ export class RecentPlaylistsComponent {
|
||||
readonly playlistClicked = output<string>();
|
||||
readonly addPlaylistClicked = output<PlaylistType | undefined>();
|
||||
|
||||
readonly isElectron = this.runtime.isElectron;
|
||||
get isElectron(): boolean {
|
||||
return this.runtime.isElectron;
|
||||
}
|
||||
|
||||
readonly allPlaylistsLoaded = this.store.selectSignal(
|
||||
selectPlaylistsLoadingFlag
|
||||
@@ -292,7 +294,7 @@ export class RecentPlaylistsComponent {
|
||||
if (item.serverUrl) {
|
||||
// For Xtream playlists, delete and re-import
|
||||
this.refreshXtreamPlaylist(item);
|
||||
} else if (window.electron && (item.url || item.filePath)) {
|
||||
} else if (this.runtime.isElectron && (item.url || item.filePath)) {
|
||||
void this.refreshM3uPlaylist(item);
|
||||
} else {
|
||||
// For M3U playlists, use existing refresh logic
|
||||
|
||||
Reference in new issue
Block a user