Merge origin/master into claude/parental-control-feature-31dde2

Resolves the settings-store defaults split, the electron-conf key list and
the guidance reorganization (CLAUDE.md now imports AGENTS.md; the parental
lock contract is linked from the agent context map instead).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5.1 committed 2026-09-25 22:12:58 +02:00
commit ae5d3f49de
732 files changed
+42572 -15380

No files matched your search

@@ -164,6 +164,78 @@ describe('WorkspaceCommandPaletteComponent', () => {
});
});
describe('WorkspaceCommandPaletteComponent - Turkish case folding', () => {
const turkishCommands: WorkspaceResolvedCommandItem[] = [
{
id: 'open-downloads',
label: 'İndirilenleri aç',
description: '',
group: 'global',
icon: 'download',
keywords: [],
priority: 50,
visible: true,
enabled: true,
run: () => undefined,
},
{
id: 'open-settings',
label: 'Ayarlar',
description: '',
group: 'global',
icon: 'settings',
keywords: [],
priority: 40,
visible: true,
enabled: true,
run: () => undefined,
},
];
function setup(query: string): WorkspaceCommandPaletteComponent {
TestBed.resetTestingModule();
TestBed.configureTestingModule({
imports: [WorkspaceCommandPaletteComponent],
providers: [
{ provide: MatDialogRef, useValue: { close: jest.fn() } },
{
provide: MAT_DIALOG_DATA,
useValue: { query, commands: turkishCommands },
},
{
provide: TranslateService,
useValue: {
instant: (key: string) => key,
get: (key: string) => of(key),
stream: (key: string) => of(key),
onLangChange: of(null),
onTranslationChange: of(null),
onDefaultLangChange: of(null),
currentLang: 'en',
defaultLang: 'en',
},
},
],
});
const fixture = TestBed.createComponent(
WorkspaceCommandPaletteComponent
);
fixture.detectChanges();
return fixture.componentInstance;
}
it.each(['indir', 'İndir', 'İNDİR'])(
'finds a command whose label carries the dotted capital İ for the query %s (issue #609)',
(query) => {
expect(
setup(query)
.flatCommands()
.map((command) => command.id)
).toEqual(['open-downloads']);
}
);
});
describe('WorkspaceCommandPaletteComponent - recent section', () => {
function setupComponent(options: {
query: string;
@@ -7,6 +7,7 @@ import {
inject,
signal,
viewChild,
ChangeDetectionStrategy,
} from '@angular/core';
import {
MAT_DIALOG_DATA,
@@ -20,6 +21,7 @@ import {
WorkspaceCommandSelection,
WorkspaceResolvedCommandItem,
} from '@iptvnator/portal/shared/util';
import { foldSearchText } from '@iptvnator/shared/interfaces';
interface WorkspaceCommandPaletteData {
commands: WorkspaceResolvedCommandItem[];
@@ -38,6 +40,7 @@ interface WorkspaceCommandGroupSection {
selector: 'app-workspace-command-palette',
imports: [MatDialogModule, MatIconModule, TranslatePipe],
templateUrl: './workspace-command-palette.component.html',
changeDetection: ChangeDetectionStrategy.Eager,
styleUrl: './workspace-command-palette.component.scss',
})
export class WorkspaceCommandPaletteComponent implements AfterViewInit {
@@ -61,7 +64,7 @@ export class WorkspaceCommandPaletteComponent implements AfterViewInit {
);
readonly filteredCommands = computed(() => {
const term = this.query().trim().toLowerCase();
const term = foldSearchText(this.query().trim());
const commands = this.visibleCommands();
if (!term) {
@@ -73,11 +76,9 @@ export class WorkspaceCommandPaletteComponent implements AfterViewInit {
command.label,
command.description,
...(command.keywords ?? []),
]
.join(' ')
.toLowerCase();
].join(' ');
return haystack.includes(term);
return foldSearchText(haystack).includes(term);
});
});
@@ -1,4 +1,9 @@
import { Component, computed, inject } from '@angular/core';
import {
Component,
computed,
inject,
ChangeDetectionStrategy,
} from '@angular/core';
import { TranslatePipe } from '@ngx-translate/core';
import { PortalCollectionContextService } from '@iptvnator/portal/shared/util';
import { WorkspaceContextCategoryViewComponent } from './components/workspace-context-category-view.component';
@@ -49,15 +54,19 @@ import { WorkspaceShellContextDrawerService } from '@iptvnator/workspace/shell/u
/>
</div>
`,
changeDetection: ChangeDetectionStrategy.Eager,
styleUrl: './workspace-context-panel.component.scss',
})
export class WorkspaceCollectionContextPanelComponent {
readonly ctx = inject(PortalCollectionContextService);
// Root-provided; optional keeps standalone unit tests light. Closes the phone
// drawer after a selection, which never navigates on this panel.
private readonly contextDrawer = inject(WorkspaceShellContextDrawerService, {
optional: true,
});
private readonly contextDrawer = inject(
WorkspaceShellContextDrawerService,
{
optional: true,
}
);
readonly selectedCategory = computed(() => {
const categories = this.ctx.categories();
@@ -9,6 +9,7 @@ import {
signal,
viewChild,
ElementRef,
ChangeDetectionStrategy,
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatIconButton } from '@angular/material/button';
@@ -38,6 +39,7 @@ import { WorkspaceContextCategoryViewComponent } from './components/workspace-co
import { WorkspaceContextErrorViewComponent } from './components/workspace-context-error-view.component';
import { hasActiveLiveCategoryRoute } from './workspace-context-panel-route.utils';
import { WorkspaceShellContextDrawerService } from '@iptvnator/workspace/shell/util';
import { foldSearchText } from '@iptvnator/shared/interfaces';
type WorkspaceProvider = 'xtreams' | 'stalker' | 'playlists';
@@ -70,6 +72,7 @@ interface WorkspaceCategoryLike {
WorkspaceContextErrorViewComponent,
],
templateUrl: './workspace-context-panel.component.html',
changeDetection: ChangeDetectionStrategy.Eager,
styleUrl: './workspace-context-panel.component.scss',
})
export class WorkspaceContextPanelComponent {
@@ -336,10 +339,10 @@ export class WorkspaceContextPanelComponent {
readonly filteredXtreamCategories = computed(() => {
const cats = this.xtreamCategories();
const term = this.categorySearchTerm().trim().toLowerCase();
const term = foldSearchText(this.categorySearchTerm().trim());
const filtered = term
? cats.filter((category) =>
this.getCategoryLabel(category).toLowerCase().includes(term)
foldSearchText(this.getCategoryLabel(category)).includes(term)
)
: cats;
@@ -353,10 +356,10 @@ export class WorkspaceContextPanelComponent {
readonly filteredStalkerCategories = computed(() => {
const cats = this.stalkerCategories();
const term = this.categorySearchTerm().trim().toLowerCase();
const term = foldSearchText(this.categorySearchTerm().trim());
const filtered = term
? cats.filter((category) =>
this.getCategoryLabel(category).toLowerCase().includes(term)
foldSearchText(this.getCategoryLabel(category)).includes(term)
)
: cats;
@@ -1,5 +1,5 @@
import { Location } from '@angular/common';
import { Component, inject } from '@angular/core';
import { Component, inject, ChangeDetectionStrategy } from '@angular/core';
import { MatIconModule } from '@angular/material/icon';
import { RouterLink, RouterLinkActive } from '@angular/router';
import { TranslateModule } from '@ngx-translate/core';
@@ -12,6 +12,7 @@ import {
selector: 'app-workspace-settings-context-panel',
imports: [MatIconModule, RouterLink, RouterLinkActive, TranslateModule],
styleUrls: ['./workspace-settings-context-panel.component.scss'],
changeDetection: ChangeDetectionStrategy.Eager,
template: `
<h2 class="panel-title">{{ 'SETTINGS.TITLE' | translate }}</h2>
<div class="settings-panel-body">
@@ -54,9 +55,12 @@ export class WorkspaceSettingsContextPanelComponent {
// links are real navigations now, so the phone drawer's NavigationEnd
// auto-close fires too — the explicit close just makes the drawer react
// immediately instead of waiting for the navigation to settle.
private readonly contextDrawer = inject(WorkspaceShellContextDrawerService, {
optional: true,
});
private readonly contextDrawer = inject(
WorkspaceShellContextDrawerService,
{
optional: true,
}
);
onSectionClicked() {
this.contextDrawer?.close();
@@ -1,4 +1,4 @@
import { Component, inject } from '@angular/core';
import { Component, inject, ChangeDetectionStrategy } from '@angular/core';
import {
MAT_DIALOG_DATA,
MatDialogModule,
@@ -19,6 +19,7 @@ export interface WorkspaceKeyboardShortcutsDialogData {
selector: 'app-workspace-keyboard-shortcuts-dialog',
imports: [MatButtonModule, MatDialogModule, MatIconModule, TranslatePipe],
templateUrl: './workspace-keyboard-shortcuts-dialog.component.html',
changeDetection: ChangeDetectionStrategy.Eager,
styleUrl: './workspace-keyboard-shortcuts-dialog.component.scss',
})
export class WorkspaceKeyboardShortcutsDialogComponent {
@@ -179,6 +179,116 @@ describe('WorkspaceKeyboardShortcutsService', () => {
});
});
describe('zoom shortcuts', () => {
const testWindow = window as unknown as {
electron?: Record<string, unknown>;
};
let adjustZoomLevel: jest.Mock;
function press(
init: KeyboardEventInit,
target: EventTarget = document
): KeyboardEvent {
const event = new KeyboardEvent('keydown', {
bubbles: true,
cancelable: true,
...init,
});
target.dispatchEvent(event);
return event;
}
beforeEach(() => {
adjustZoomLevel = jest.fn().mockReturnValue(0.5);
testWindow.electron = { adjustZoomLevel, platform: 'linux' };
});
afterEach(() => {
delete testWindow.electron;
});
it('zooms in, out and resets through the bridge with Ctrl on Linux/Windows', () => {
const zoomIn = press({ key: '=', ctrlKey: true });
const zoomOut = press({ key: '-', ctrlKey: true });
const reset = press({ key: '0', ctrlKey: true });
expect(adjustZoomLevel.mock.calls).toEqual([
['in'],
['out'],
['reset'],
]);
expect(zoomIn.defaultPrevented).toBe(true);
expect(zoomOut.defaultPrevented).toBe(true);
expect(reset.defaultPrevented).toBe(true);
expect(dialog.open).not.toHaveBeenCalled();
});
it('takes the numpad keys and Ctrl+Shift+=', () => {
press({ key: '+', code: 'NumpadAdd', ctrlKey: true });
press({ key: '+', ctrlKey: true, shiftKey: true });
expect(adjustZoomLevel.mock.calls).toEqual([['in'], ['in']]);
});
it('follows the bridge platform: Cmd on macOS, and swallows the key so the menu role cannot step twice', () => {
testWindow.electron = { adjustZoomLevel, platform: 'darwin' };
const ctrl = press({ key: '=', ctrlKey: true });
const cmd = press({ key: '=', metaKey: true });
expect(adjustZoomLevel.mock.calls).toEqual([['in']]);
expect(ctrl.defaultPrevented).toBe(false);
expect(cmd.defaultPrevented).toBe(true);
});
it('works while typing in an input, like the browser zoom it replaces', () => {
const input = document.createElement('input');
document.body.appendChild(input);
press({ key: '=', ctrlKey: true }, input);
expect(adjustZoomLevel).toHaveBeenCalledWith('in');
input.remove();
});
it('leaves a key another handler already consumed alone', () => {
const event = new KeyboardEvent('keydown', {
key: '=',
ctrlKey: true,
bubbles: true,
cancelable: true,
});
event.preventDefault();
document.dispatchEvent(event);
expect(adjustZoomLevel).not.toHaveBeenCalled();
});
it('ignores unrelated Ctrl combinations and unmodified keys', () => {
press({ key: 'k', ctrlKey: true });
press({ key: '=' });
press({ key: '=', ctrlKey: true, altKey: true });
expect(adjustZoomLevel).not.toHaveBeenCalled();
});
it('leaves zoom to the browser without a bridge', () => {
delete testWindow.electron;
const event = press({ key: '=', ctrlKey: true });
expect(event.defaultPrevented).toBe(false);
});
it('swallows a bridge failure', () => {
adjustZoomLevel.mockImplementation(() => {
throw new Error('frame gone');
});
expect(() => press({ key: '=', ctrlKey: true })).not.toThrow();
});
});
it('does not open duplicate dialogs while one is active', () => {
service.openShortcutsDialog();
service.openShortcutsDialog();
@@ -5,6 +5,7 @@ import {
getKeyboardShortcutGroups,
isKeyboardShortcutHelpTrigger,
isTypingInInput,
resolveZoomShortcutAction,
} from '@iptvnator/portal/shared/util';
import { RuntimeCapabilitiesService } from '@iptvnator/services';
import {
@@ -78,7 +79,10 @@ export class WorkspaceKeyboardShortcutsService {
}
private handleKeydown(event: KeyboardEvent): void {
if (this.handleWindowFullscreenToggle(event)) {
if (
this.handleWindowFullscreenToggle(event) ||
this.handleZoomShortcut(event)
) {
return;
}
@@ -129,6 +133,44 @@ export class WorkspaceKeyboardShortcutsService {
return true;
}
/**
* Cmd/Ctrl and +/−/0 zoom the app (issue #1109). Like F11 this is not
* gated by `isTypingInInput`: browsers zoom from any focus, and the
* combination is not one a text field consumes. The bridge steps the
* frame-bound zoom level itself, so no native menu is needed — Windows
* and Linux run without one. On macOS the renderer sees the key before
* the application menu's `zoomIn`/`zoomOut`/`resetZoom` roles do, and
* `preventDefault()` stops the role from stepping a second time, so one
* press is one step on every platform. A key another handler already
* consumed is left alone; without a bridge (PWA) the browser keeps its
* own zoom.
*/
private handleZoomShortcut(event: KeyboardEvent): boolean {
if (event.defaultPrevented) {
return false;
}
const bridge = window.electron;
if (typeof bridge?.adjustZoomLevel !== 'function') {
return false;
}
const action = resolveZoomShortcutAction(event, {
isMac: bridge.platform === 'darwin',
});
if (!action) {
return false;
}
event.preventDefault();
try {
bridge.adjustZoomLevel(action);
} catch {
// The frame refused the write; the level on screen is unchanged.
}
return true;
}
private getShortcutPlatform(): 'mac' | 'other' {
if (typeof navigator === 'undefined') {
return 'other';
@@ -5,6 +5,7 @@ import {
input,
output,
signal,
ChangeDetectionStrategy,
} from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
@@ -29,6 +30,7 @@ import { WorkspaceLiveCategoriesPopoverService } from './workspace-live-categori
<button data-test-id="stub-pick" (click)="categorySelected.emit()">
pick
</button>`,
changeDetection: ChangeDetectionStrategy.Eager,
standalone: true,
})
class StubWorkspaceContextPanelComponent {
@@ -82,7 +84,10 @@ describe('WorkspaceLiveCategoriesPopoverService', () => {
provide: LIVE_CATEGORIES_POPOVER,
useExisting: WorkspaceLiveCategoriesPopoverService,
},
{ provide: WorkspaceShellContextDrawerService, useValue: drawer },
{
provide: WorkspaceShellContextDrawerService,
useValue: drawer,
},
{
provide: Router,
useValue: { events: routerEvents.asObservable() },
@@ -1,4 +1,10 @@
import { Component, Directive, input, signal } from '@angular/core';
import {
Component,
Directive,
input,
signal,
ChangeDetectionStrategy,
} from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { MatIcon } from '@angular/material/icon';
@@ -24,6 +30,7 @@ class MockResizableDirective {
@Component({
selector: 'app-workspace-context-panel',
template: '',
changeDetection: ChangeDetectionStrategy.Eager,
standalone: true,
})
class MockWorkspaceContextPanelComponent {
@@ -35,6 +42,7 @@ class MockWorkspaceContextPanelComponent {
@Component({
selector: 'app-workspace-collection-context-panel',
template: '',
changeDetection: ChangeDetectionStrategy.Eager,
standalone: true,
})
class MockWorkspaceCollectionContextPanelComponent {}
@@ -42,6 +50,7 @@ class MockWorkspaceCollectionContextPanelComponent {}
@Component({
selector: 'app-workspace-settings-context-panel',
template: '',
changeDetection: ChangeDetectionStrategy.Eager,
standalone: true,
})
class MockWorkspaceSettingsContextPanelComponent {}
@@ -49,6 +58,7 @@ class MockWorkspaceSettingsContextPanelComponent {}
@Component({
selector: 'app-workspace-sources-filters-panel',
template: '',
changeDetection: ChangeDetectionStrategy.Eager,
standalone: true,
})
class MockWorkspaceSourcesFiltersPanelComponent {}
@@ -111,7 +121,9 @@ describe('WorkspaceShellContextSidebarComponent', () => {
})
.compileComponents();
fixture = TestBed.createComponent(WorkspaceShellContextSidebarComponent);
fixture = TestBed.createComponent(
WorkspaceShellContextSidebarComponent
);
liveSidebarService = TestBed.inject(LiveLayoutSidebarStateService);
liveSidebarService.setState('portal', 'expanded');
});
@@ -1,6 +1,11 @@
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { Component, input, output } from '@angular/core';
import {
Component,
input,
output,
ChangeDetectionStrategy,
} from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { MatIconButton } from '@angular/material/button';
@@ -14,6 +19,7 @@ import { WorkspaceShellHeaderComponent } from './workspace-shell-header.componen
@Component({
selector: 'app-playlist-switcher',
template: '',
changeDetection: ChangeDetectionStrategy.Eager,
standalone: true,
})
class MockPlaylistSwitcherComponent {
@@ -1,4 +1,4 @@
import { Component, input } from '@angular/core';
import { Component, input, ChangeDetectionStrategy } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MatIcon } from '@angular/material/icon';
import { MatTooltip } from '@angular/material/tooltip';
@@ -12,6 +12,7 @@ import { WorkspaceShellRailComponent } from './workspace-shell-rail.component';
@Component({
selector: 'app-workspace-shell-rail-links',
template: '',
changeDetection: ChangeDetectionStrategy.Eager,
standalone: true,
})
class MockWorkspaceShellRailLinksComponent {
@@ -87,9 +88,7 @@ describe('WorkspaceShellRailComponent', () => {
fixture.nativeElement.querySelector('.rail-shortcut.is-active')
).not.toBeNull();
expect(
fixture.nativeElement
.querySelector('.brand')
?.getAttribute('href')
fixture.nativeElement.querySelector('.brand')?.getAttribute('href')
).toContain('/workspace/sources');
expect(
fixture.nativeElement
@@ -1,5 +1,12 @@
import { CdkTrapFocus } from '@angular/cdk/a11y';
import { Component, Directive, input, output, signal } from '@angular/core';
import {
Component,
Directive,
input,
output,
signal,
ChangeDetectionStrategy,
} from '@angular/core';
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
import { of, Subject } from 'rxjs';
import { EmbeddedMpvOverlayVisibilityService } from '@iptvnator/ui/playback';
@@ -21,6 +28,7 @@ import { WorkspaceKeyboardShortcutsService } from '../workspace-keyboard-shortcu
@Component({
selector: 'app-workspace-shell-rail',
template: '',
changeDetection: ChangeDetectionStrategy.Eager,
standalone: true,
})
class MockWorkspaceShellRailComponent {
@@ -39,6 +47,7 @@ class MockWorkspaceShellRailComponent {
@Component({
selector: 'app-workspace-shell-header',
template: '',
changeDetection: ChangeDetectionStrategy.Eager,
standalone: true,
})
class MockWorkspaceShellHeaderComponent {
@@ -89,6 +98,7 @@ class MockWorkspaceShellHeaderComponent {
@Component({
selector: 'app-workspace-shell-context-sidebar',
template: '',
changeDetection: ChangeDetectionStrategy.Eager,
standalone: true,
})
class MockWorkspaceShellContextSidebarComponent {
@@ -101,6 +111,7 @@ class MockWorkspaceShellContextSidebarComponent {
@Component({
selector: 'app-external-playback-dock',
template: '',
changeDetection: ChangeDetectionStrategy.Eager,
standalone: true,
})
class MockExternalPlaybackDockComponent {
@@ -112,6 +123,7 @@ class MockExternalPlaybackDockComponent {
@Component({
selector: 'app-playlist-drop-overlay',
template: '',
changeDetection: ChangeDetectionStrategy.Eager,
standalone: true,
})
class MockPlaylistDropOverlayComponent {
@@ -130,6 +142,7 @@ class MockPlaylistDropZoneDirective {
@Component({
selector: 'app-workspace-shell-import-overlay',
template: '',
changeDetection: ChangeDetectionStrategy.Eager,
standalone: true,
})
class MockWorkspaceShellImportOverlayComponent {}
@@ -7,6 +7,7 @@ import {
inject,
untracked,
viewChild,
ChangeDetectionStrategy,
} from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { TranslatePipe } from '@ngx-translate/core';
@@ -48,6 +49,7 @@ import { LIVE_CATEGORIES_POPOVER } from '@iptvnator/portal/shared/util';
],
templateUrl: './workspace-shell.component.html',
styleUrl: './workspace-shell.component.scss',
changeDetection: ChangeDetectionStrategy.Eager,
providers: [
WorkspaceShellFacade,
WorkspaceShellRouteStateService,
@@ -72,12 +74,10 @@ export class WorkspaceShellComponent {
private readonly mpvOverlayVisibility = inject(
EmbeddedMpvOverlayVisibilityService
);
private readonly header = viewChild<WorkspaceShellHeaderShortcutTarget>(
'workspaceHeader'
);
private readonly workspaceContent = viewChild<ElementRef<HTMLElement>>(
'workspaceContent'
);
private readonly header =
viewChild<WorkspaceShellHeaderShortcutTarget>('workspaceHeader');
private readonly workspaceContent =
viewChild<ElementRef<HTMLElement>>('workspaceContent');
constructor() {
// Focus restoration is the closing half of the drawer's focus
@@ -2,7 +2,13 @@ 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 {
Component,
input,
output,
signal,
ChangeDetectionStrategy,
} from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { MatButtonModule } from '@angular/material/button';
@@ -11,7 +17,7 @@ import { MatMenuModule } from '@angular/material/menu';
import { ActivatedRoute, convertToParamMap } from '@angular/router';
import { provideMockStore } from '@ngrx/store/testing';
import { TranslatePipe, TranslateService } from '@ngx-translate/core';
import { of } from 'rxjs';
import { of, Subject } from 'rxjs';
import {
selectActiveTypeFilters,
selectAllPlaylistsMeta,
@@ -23,6 +29,7 @@ import { WorkspaceSourcesComponent } from './workspace-sources.component';
@Component({
selector: 'app-recent-playlists',
template: '',
changeDetection: ChangeDetectionStrategy.Eager,
standalone: true,
})
class MockRecentPlaylistsComponent {
@@ -116,9 +123,9 @@ describe('WorkspaceSourcesComponent', () => {
},
get: (key: string) => of(key),
stream: (key: string) => of(key),
onLangChange: of(null),
onTranslationChange: of(null),
onDefaultLangChange: of(null),
onLangChange: new Subject(),
onTranslationChange: new Subject(),
onDefaultLangChange: new Subject(),
currentLang: 'en',
defaultLang: 'en',
},
@@ -15,10 +15,16 @@ import {
} from '@iptvnator/portal/shared/util';
import { PlaylistActions } from '@iptvnator/m3u-state';
import {
foldSearchText,
sourceHealthType,
PlaylistUpdateState,
} from '@iptvnator/shared/interfaces';
import { Component, computed, inject } from '@angular/core';
import {
Component,
computed,
inject,
ChangeDetectionStrategy,
} from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { MatButtonModule } from '@angular/material/button';
import { MatIconModule } from '@angular/material/icon';
@@ -55,6 +61,7 @@ interface SortOption {
TranslatePipe,
],
templateUrl: './workspace-sources.component.html',
changeDetection: ChangeDetectionStrategy.Eager,
styleUrl: './workspace-sources.component.scss',
})
export class WorkspaceSourcesComponent {
@@ -189,7 +196,7 @@ export class WorkspaceSourcesComponent {
});
readonly visibleSourcesCount = computed(() => {
const query = this.searchQuery().trim().toLowerCase();
const query = foldSearchText(this.searchQuery().trim());
const filters = this.activeTypeFilters();
const allPlaylists = this.playlists();
@@ -211,7 +218,7 @@ export class WorkspaceSourcesComponent {
return isStalkerFilter || isXtreamFilter || isM3uFilter;
})
.filter((item) => (item.title || '').toLowerCase().includes(query))
.filter((item) => foldSearchText(item.title || '').includes(query))
.length;
});
@@ -1,6 +1,9 @@
import { setupZoneTestEnv } from 'jest-preset-angular/setup-env/zone';
// Keep Zone's fakeAsync helpers while preserving the Angular 21 test scheduler.
import 'zone.js';
import 'zone.js/testing';
import { setupZonelessTestEnv } from 'jest-preset-angular/setup-env/zoneless';
setupZoneTestEnv({
setupZonelessTestEnv({
errorOnUnknownElements: true,
errorOnUnknownProperties: true,
});