mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-12 12:06:16 -08:00
fix(settings): fail closed on a rejected worker lock sync, tear down Stalker synchronously and capture the Xtream dialog context before the PIN
- A rejected lock-state sync to the SQLite worker makes the locked session withhold everything until a later sync succeeds. - The Stalker enforcement chunk is preloaded when a Stalker route opens; a relock runs it synchronously, or leaves the route at once while it is not loaded, instead of awaiting the chunk. - Xtream "Manage categories" captures playlist, provider and section before the PIN prompt and re-checks them after it and after the dialog import. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4aa1b5a8e6
commit
067f920833
8 files changed
+267
-62
No files matched your search
+32
@@ -927,6 +927,38 @@ describe('WorkspaceContextPanelComponent', () => {
|
||||
).not.toBeNull();
|
||||
});
|
||||
|
||||
it('opens no Xtream category dialog when the route changed while the PIN was asked', async () => {
|
||||
fixture.componentRef.setInput('context', {
|
||||
provider: 'xtreams',
|
||||
playlistId: 'xtream-1',
|
||||
});
|
||||
fixture.componentRef.setInput('section', 'vod');
|
||||
fixture.detectChanges();
|
||||
const component = fixture.componentInstance;
|
||||
Object.defineProperty(component, 'canManageXtreamCategories', {
|
||||
configurable: true,
|
||||
value: () => true,
|
||||
});
|
||||
const parentalLock = TestBed.inject(ParentalLockService);
|
||||
jest.spyOn(parentalLock, 'requestUnlock').mockImplementation(
|
||||
async () => {
|
||||
fixture.componentRef.setInput('context', {
|
||||
provider: 'xtreams',
|
||||
playlistId: 'xtream-2',
|
||||
});
|
||||
return true;
|
||||
}
|
||||
);
|
||||
component.loadXtreamCategoryDialog = jest.fn(
|
||||
async () => class DialogStub {}
|
||||
);
|
||||
|
||||
await component.openManageCategories();
|
||||
|
||||
expect(component.loadXtreamCategoryDialog).not.toHaveBeenCalled();
|
||||
expect(dialog.open).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
describe('Stalker lock dialog', () => {
|
||||
function enableLock(): void {
|
||||
const parentalLock = TestBed.inject(ParentalLockService);
|
||||
|
||||
+41
-33
@@ -572,51 +572,59 @@ export class WorkspaceContextPanelComponent {
|
||||
(module) => module.StalkerCategoryLockDialogComponent
|
||||
);
|
||||
|
||||
/** The dynamic import; a field so specs can substitute it. */
|
||||
loadXtreamCategoryDialog = (): Promise<Type<unknown>> =>
|
||||
import('@iptvnator/portal/xtream/feature').then(
|
||||
(module) => module.CategoryManagementDialogComponent
|
||||
);
|
||||
|
||||
async openManageCategories(): Promise<void> {
|
||||
if (!this.canManageXtreamCategories()) {
|
||||
return;
|
||||
}
|
||||
// Hidden AND locked categories are listed there by name, so the
|
||||
// dialog itself sits behind the PIN while the lock is on.
|
||||
if (!(await this.parentalLock.requestUnlock())) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Captured before any await (the PIN prompt and the dialog load
|
||||
// lazily) and re-checked after each: the dialog saves visibility and
|
||||
// locks for the playlist it was opened for, which must still be the
|
||||
// one on screen.
|
||||
const context = this.context();
|
||||
const section = this.section();
|
||||
const unchanged = (): boolean =>
|
||||
this.context().playlistId === context.playlistId &&
|
||||
this.context().provider === context.provider &&
|
||||
this.section() === section &&
|
||||
this.canManageXtreamCategories();
|
||||
const contentType =
|
||||
section === 'series'
|
||||
? 'series'
|
||||
: section === 'live'
|
||||
? 'live'
|
||||
: 'vod';
|
||||
|
||||
void import('@iptvnator/portal/xtream/feature').then(
|
||||
({ CategoryManagementDialogComponent }) => {
|
||||
const dialogRef = this.dialog.open(
|
||||
CategoryManagementDialogComponent,
|
||||
{
|
||||
data: {
|
||||
playlistId: context.playlistId,
|
||||
contentType,
|
||||
itemCounts:
|
||||
this.xtreamStore.getCategoryItemCounts(),
|
||||
},
|
||||
width: '500px',
|
||||
maxHeight: '90vh',
|
||||
}
|
||||
);
|
||||
|
||||
dialogRef
|
||||
.afterClosed()
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((result) => {
|
||||
if (result) {
|
||||
this.xtreamStore.reloadCategories();
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
// Hidden AND locked categories are listed there by name, so the
|
||||
// dialog itself sits behind the PIN while the lock is on.
|
||||
if (!(await this.parentalLock.requestUnlock()) || !unchanged()) {
|
||||
return;
|
||||
}
|
||||
const dialogComponent = await this.loadXtreamCategoryDialog();
|
||||
if (!unchanged()) {
|
||||
return;
|
||||
}
|
||||
const dialogRef = this.dialog.open(dialogComponent, {
|
||||
data: {
|
||||
playlistId: context.playlistId,
|
||||
contentType,
|
||||
itemCounts: this.xtreamStore.getCategoryItemCounts(),
|
||||
},
|
||||
width: '500px',
|
||||
maxHeight: '90vh',
|
||||
});
|
||||
dialogRef
|
||||
.afterClosed()
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((result) => {
|
||||
if (result) {
|
||||
this.xtreamStore.reloadCategories();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
hideCategories(): void {
|
||||
|
||||
Reference in new issue
Block a user