mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-12 03:56:15 -08:00
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:
commit
ae5d3f49de
732 files changed
+42572
-15380
No files matched your search
+72
@@ -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;
|
||||
|
||||
+6
-5
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+13
-4
@@ -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();
|
||||
|
||||
+7
-4
@@ -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;
|
||||
|
||||
|
||||
+8
-4
@@ -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();
|
||||
|
||||
+2
-1
@@ -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 {
|
||||
|
||||
+110
@@ -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();
|
||||
|
||||
+43
-1
@@ -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';
|
||||
|
||||
+6
-1
@@ -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() },
|
||||
|
||||
+14
-2
@@ -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');
|
||||
});
|
||||
|
||||
+7
-1
@@ -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 {
|
||||
|
||||
+3
-4
@@ -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
|
||||
|
||||
+14
-1
@@ -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
|
||||
|
||||
+12
-5
@@ -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',
|
||||
},
|
||||
|
||||
+10
-3
@@ -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,
|
||||
});
|
||||
Reference in new issue
Block a user