mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-14 05:06:16 -08:00
fix(settings): close lock editors on relock and clear withheld details opened from All
The Xtream, Stalker and M3U category editors are gated by the PIN only when they open; an idle relock left them on screen listing locked names with a lock-rewriting Save. Each now closes itself when the session relocks. ParentalLockEnforcementService also judges the selected Xtream/Stalker item by its own category: a detail opened from All, recently added or search has no selected category to vanish with, so it stayed open after a relock. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
45661e311e
commit
bc1b0eef41
9 files changed
+438
-21
No files matched your search
@@ -0,0 +1,169 @@
|
||||
import { signal } from '@angular/core';
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { Router } from '@angular/router';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { StalkerStore } from '@iptvnator/portal/stalker/data-access';
|
||||
import { XtreamStore } from '@iptvnator/portal/xtream/data-access';
|
||||
import { ParentalLockService } from '@iptvnator/services';
|
||||
import { ParentalLockEnforcementService } from './parental-lock-enforcement.service';
|
||||
import { PlaybackKeepAwakeService } from './playback-keep-awake.service';
|
||||
|
||||
interface Applier {
|
||||
applyXtream(): Promise<void>;
|
||||
applyStalker(): Promise<void>;
|
||||
}
|
||||
|
||||
describe('ParentalLockEnforcementService', () => {
|
||||
const router = { url: '/', navigate: jest.fn() };
|
||||
const lockedStalkerIds = new Set<string>();
|
||||
const parentalLock = {
|
||||
version: signal(0),
|
||||
registerBusyProbe: jest.fn(),
|
||||
isStalkerCategoryLocked: jest.fn(
|
||||
(_playlistId: string, _type: string, id: unknown) =>
|
||||
id !== null &&
|
||||
id !== undefined &&
|
||||
lockedStalkerIds.has(String(id))
|
||||
),
|
||||
isM3uGroupLocked: jest.fn(() => false),
|
||||
};
|
||||
const stalkerStore = {
|
||||
currentPlaylist: signal<{ _id: string } | null>({ _id: 'stalker-1' }),
|
||||
selectedContentType: signal<string>('vod'),
|
||||
selectedCategoryId: signal<string | null>('*'),
|
||||
selectedItem: signal<{ category_id?: string } | null>(null),
|
||||
clearSelectedItem: jest.fn(),
|
||||
setSelectedCategory: jest.fn(),
|
||||
};
|
||||
const xtreamStore = {
|
||||
playlistId: signal<string | null>('xtream-1'),
|
||||
selectedCategoryId: signal<number | null>(null),
|
||||
selectedItem: signal<{ category_id?: number } | null>(null),
|
||||
reloadCategories: jest.fn(async () => undefined),
|
||||
reloadCachedContent: jest.fn(async () => undefined),
|
||||
getCategoriesBySelectedType: jest.fn(() => [{ id: 7 }, { id: 8 }]),
|
||||
setSelectedItem: jest.fn(),
|
||||
setSelectedCategory: jest.fn(),
|
||||
};
|
||||
let service: Applier;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
lockedStalkerIds.clear();
|
||||
router.url = '/';
|
||||
stalkerStore.selectedCategoryId.set('*');
|
||||
stalkerStore.selectedItem.set(null);
|
||||
xtreamStore.selectedCategoryId.set(null);
|
||||
xtreamStore.selectedItem.set(null);
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{ provide: ParentalLockService, useValue: parentalLock },
|
||||
{ provide: XtreamStore, useValue: xtreamStore },
|
||||
{ provide: StalkerStore, useValue: stalkerStore },
|
||||
{ provide: Router, useValue: router },
|
||||
{
|
||||
provide: Store,
|
||||
useValue: {
|
||||
selectSignal: () => signal(null),
|
||||
dispatch: jest.fn(),
|
||||
},
|
||||
},
|
||||
{
|
||||
provide: PlaybackKeepAwakeService,
|
||||
useValue: { hasPlayingVideo: () => false },
|
||||
},
|
||||
],
|
||||
});
|
||||
service = TestBed.inject(
|
||||
ParentalLockEnforcementService
|
||||
) as unknown as Applier;
|
||||
});
|
||||
|
||||
describe('Stalker', () => {
|
||||
it('clears a detail opened from All whose own genre is withheld', async () => {
|
||||
router.url = '/workspace/stalker/stalker-1/vod/42';
|
||||
lockedStalkerIds.add('9');
|
||||
stalkerStore.selectedItem.set({ category_id: '9' });
|
||||
|
||||
await service.applyStalker();
|
||||
|
||||
expect(stalkerStore.clearSelectedItem).toHaveBeenCalled();
|
||||
// "All" itself is not locked, so the category stays selected.
|
||||
expect(stalkerStore.setSelectedCategory).not.toHaveBeenCalled();
|
||||
expect(router.navigate).toHaveBeenCalledWith([
|
||||
'/workspace',
|
||||
'stalker',
|
||||
'stalker-1',
|
||||
'vod',
|
||||
]);
|
||||
});
|
||||
|
||||
it('leaves a detail from All alone when its genre is not locked', async () => {
|
||||
router.url = '/workspace/stalker/stalker-1/vod/42';
|
||||
lockedStalkerIds.add('9');
|
||||
stalkerStore.selectedItem.set({ category_id: '3' });
|
||||
|
||||
await service.applyStalker();
|
||||
|
||||
expect(stalkerStore.clearSelectedItem).not.toHaveBeenCalled();
|
||||
expect(router.navigate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('steps off a locked selected category', async () => {
|
||||
router.url = '/workspace/stalker/stalker-1/vod';
|
||||
lockedStalkerIds.add('9');
|
||||
stalkerStore.selectedCategoryId.set('9');
|
||||
|
||||
await service.applyStalker();
|
||||
|
||||
expect(stalkerStore.clearSelectedItem).toHaveBeenCalled();
|
||||
expect(stalkerStore.setSelectedCategory).toHaveBeenCalledWith(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Xtream', () => {
|
||||
it('clears a selected item whose category is no longer readable', async () => {
|
||||
router.url = '/workspace/xtreams/xtream-1/vod/42';
|
||||
xtreamStore.selectedItem.set({ category_id: 99 });
|
||||
|
||||
await service.applyXtream();
|
||||
|
||||
expect(xtreamStore.reloadCategories).toHaveBeenCalled();
|
||||
expect(xtreamStore.setSelectedItem).toHaveBeenCalledWith(null);
|
||||
expect(xtreamStore.setSelectedCategory).not.toHaveBeenCalled();
|
||||
expect(router.navigate).toHaveBeenCalledWith([
|
||||
'/workspace',
|
||||
'xtreams',
|
||||
'xtream-1',
|
||||
'vod',
|
||||
]);
|
||||
});
|
||||
|
||||
it('keeps a selected item whose category survived the lock', async () => {
|
||||
router.url = '/workspace/xtreams/xtream-1/vod/42';
|
||||
xtreamStore.selectedCategoryId.set(7);
|
||||
xtreamStore.selectedItem.set({ category_id: 7 });
|
||||
|
||||
await service.applyXtream();
|
||||
|
||||
expect(xtreamStore.setSelectedItem).not.toHaveBeenCalled();
|
||||
expect(router.navigate).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('steps off a selected category that vanished', async () => {
|
||||
router.url = '/workspace/xtreams/xtream-1/live';
|
||||
xtreamStore.selectedCategoryId.set(99);
|
||||
|
||||
await service.applyXtream();
|
||||
|
||||
expect(xtreamStore.setSelectedItem).toHaveBeenCalledWith(null);
|
||||
expect(xtreamStore.setSelectedCategory).toHaveBeenCalledWith(null);
|
||||
expect(router.navigate).toHaveBeenCalledWith([
|
||||
'/workspace',
|
||||
'xtreams',
|
||||
'xtream-1',
|
||||
'live',
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -72,20 +72,42 @@ export class ParentalLockEnforcementService {
|
||||
await this.xtreamStore.reloadCachedContent();
|
||||
|
||||
const match = XTREAM_ROUTE.exec(this.router.url);
|
||||
const selectedCategoryId = this.xtreamStore.selectedCategoryId();
|
||||
if (selectedCategoryId === null) {
|
||||
return;
|
||||
}
|
||||
const categoryType = toParentalLockXtreamCategoryType(match?.[2]);
|
||||
const categories = this.xtreamStore.getCategoriesBySelectedType();
|
||||
const stillVisible = categories.some(
|
||||
(category) =>
|
||||
Number(
|
||||
(category as { id?: number | string }).id ??
|
||||
(category as { category_id?: string }).category_id
|
||||
) === Number(selectedCategoryId)
|
||||
);
|
||||
if (stillVisible) {
|
||||
const isVisibleCategory = (categoryId: unknown): boolean =>
|
||||
categories.some(
|
||||
(category) =>
|
||||
Number(
|
||||
(category as { id?: number | string }).id ??
|
||||
(category as { category_id?: string }).category_id
|
||||
) === Number(categoryId)
|
||||
);
|
||||
const selectedCategoryId = this.xtreamStore.selectedCategoryId();
|
||||
// The selected ITEM is judged on its own category: opened from
|
||||
// "All", recently added or search it has no selected category to
|
||||
// vanish with, yet its detail must not outlive the lock.
|
||||
const selectedItem = this.xtreamStore.selectedItem?.() as {
|
||||
category_id?: string | number;
|
||||
} | null;
|
||||
const itemWithheld =
|
||||
selectedItem?.category_id !== undefined &&
|
||||
selectedItem?.category_id !== null &&
|
||||
!isVisibleCategory(selectedItem.category_id);
|
||||
if (itemWithheld) {
|
||||
this.xtreamStore.setSelectedItem(null);
|
||||
}
|
||||
if (
|
||||
selectedCategoryId === null ||
|
||||
isVisibleCategory(selectedCategoryId)
|
||||
) {
|
||||
if (itemWithheld && match && match[1] === playlistId) {
|
||||
void this.router.navigate([
|
||||
'/workspace',
|
||||
'xtreams',
|
||||
match[1],
|
||||
match[2],
|
||||
]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.xtreamStore.setSelectedItem(null);
|
||||
@@ -109,20 +131,36 @@ export class ParentalLockEnforcementService {
|
||||
const contentType = toParentalLockStalkerCategoryType(
|
||||
this.stalkerStore.selectedContentType()
|
||||
);
|
||||
if (!contentType) {
|
||||
return;
|
||||
}
|
||||
const selectedCategoryId = this.stalkerStore.selectedCategoryId();
|
||||
if (
|
||||
!contentType ||
|
||||
!selectedCategoryId ||
|
||||
!this.parentalLock.isStalkerCategoryLocked(
|
||||
const categoryWithheld =
|
||||
!!selectedCategoryId &&
|
||||
this.parentalLock.isStalkerCategoryLocked(
|
||||
playlistId,
|
||||
contentType,
|
||||
selectedCategoryId
|
||||
)
|
||||
) {
|
||||
);
|
||||
// An item opened from "All" (`*`) or search has its own genre to be
|
||||
// judged by; the list dropping its row is not enough.
|
||||
const selectedItem = this.stalkerStore.selectedItem?.() as {
|
||||
category_id?: string | number;
|
||||
} | null;
|
||||
const itemWithheld =
|
||||
!!selectedItem &&
|
||||
this.parentalLock.isStalkerCategoryLocked(
|
||||
playlistId,
|
||||
contentType,
|
||||
selectedItem.category_id
|
||||
);
|
||||
if (!categoryWithheld && !itemWithheld) {
|
||||
return;
|
||||
}
|
||||
this.stalkerStore.clearSelectedItem();
|
||||
this.stalkerStore.setSelectedCategory(null);
|
||||
if (categoryWithheld) {
|
||||
this.stalkerStore.setSelectedCategory(null);
|
||||
}
|
||||
const match = STALKER_ROUTE.exec(this.router.url);
|
||||
if (match && match[1] === playlistId) {
|
||||
void this.router.navigate([
|
||||
|
||||
Reference in new issue
Block a user