mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-12 20:16:15 -08:00
feat: add sidebar width management with resize events in channel list and groups view components
This commit is contained in:
1 parent
4e74ac2546
commit
61a582b8b7
5 files changed
+154
No files matched your search
+3
@@ -40,8 +40,11 @@
|
||||
[shouldShowEpg]="shouldShowEpg()"
|
||||
[activeChannelUrl]="activeChannelUrl()"
|
||||
[favoriteIds]="favoriteIds()"
|
||||
[sidebarWidth]="sidebarWidth()"
|
||||
(channelSelected)="onChannelSelected($event)"
|
||||
(favoriteToggled)="onFavoriteToggled($event)"
|
||||
(sidebarWidthRequested)="onSidebarWidthRequested($event)"
|
||||
(sidebarWidthRequestEnded)="onSidebarWidthRequestEnded($event)"
|
||||
/>
|
||||
}
|
||||
@case ('favorites') {
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
input,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
output,
|
||||
signal,
|
||||
} from '@angular/core';
|
||||
import { toSignal } from '@angular/core/rxjs-interop';
|
||||
@@ -113,6 +114,9 @@ export class ChannelListContainerComponent implements OnInit, OnDestroy {
|
||||
/** Active view (all, groups, favorites, recent) */
|
||||
readonly activeView = input<string>('all');
|
||||
readonly recentItems = input<PlaylistRecentlyViewedItem[]>([]);
|
||||
readonly sidebarWidth = input<number | null>(null);
|
||||
readonly sidebarWidthRequested = output<number>();
|
||||
readonly sidebarWidthRequestEnded = output<number>();
|
||||
readonly isWorkspaceLayout = isWorkspaceLayoutRoute(this.route);
|
||||
private readonly routeSearchTerm = queryParamSignal(
|
||||
this.route,
|
||||
@@ -375,4 +379,12 @@ export class ChannelListContainerComponent implements OnInit, OnDestroy {
|
||||
}) as any
|
||||
);
|
||||
}
|
||||
|
||||
onSidebarWidthRequested(width: number): void {
|
||||
this.sidebarWidthRequested.emit(width);
|
||||
}
|
||||
|
||||
onSidebarWidthRequestEnded(width: number): void {
|
||||
this.sidebarWidthRequestEnded.emit(width);
|
||||
}
|
||||
}
|
||||
+3
@@ -29,6 +29,9 @@
|
||||
[maxWidth]="320"
|
||||
[defaultWidth]="208"
|
||||
storageKey="m3u-groups-nav-width"
|
||||
(resizeStart)="onGroupsNavResizeStart()"
|
||||
(widthChange)="onGroupsNavWidthChange($event)"
|
||||
(resizeEnd)="onGroupsNavResizeEnd($event)"
|
||||
>
|
||||
<div class="groups-nav-shell">
|
||||
<header class="groups-nav-header">
|
||||
|
||||
+42
@@ -121,6 +121,7 @@ describe('GroupsViewComponent', () => {
|
||||
groupedChannels: Record<string, Channel[]>;
|
||||
progressTick: number;
|
||||
searchTerm: string;
|
||||
sidebarWidth: number | null;
|
||||
shouldShowEpg: boolean;
|
||||
}> = {}
|
||||
): void {
|
||||
@@ -149,6 +150,10 @@ describe('GroupsViewComponent', () => {
|
||||
'favoriteIds',
|
||||
overrides.favoriteIds ?? new Set<string>()
|
||||
);
|
||||
fixture.componentRef.setInput(
|
||||
'sidebarWidth',
|
||||
overrides.sidebarWidth ?? 460
|
||||
);
|
||||
fixture.detectChanges();
|
||||
}
|
||||
|
||||
@@ -250,6 +255,43 @@ describe('GroupsViewComponent', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('emits total sidebar width requests while resizing the groups rail', () => {
|
||||
const requested = jest.fn();
|
||||
const committed = jest.fn();
|
||||
let contentWidth = 252;
|
||||
|
||||
component.sidebarWidthRequested.subscribe(requested);
|
||||
component.sidebarWidthRequestEnded.subscribe(committed);
|
||||
|
||||
const contentPanel = fixture.nativeElement.querySelector(
|
||||
'.groups-content-panel'
|
||||
) as HTMLElement;
|
||||
|
||||
jest.spyOn(contentPanel, 'getBoundingClientRect').mockImplementation(
|
||||
() =>
|
||||
({
|
||||
bottom: 0,
|
||||
height: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
top: 0,
|
||||
width: contentWidth,
|
||||
x: 0,
|
||||
y: 0,
|
||||
toJSON: () => ({}),
|
||||
}) as DOMRect
|
||||
);
|
||||
|
||||
component.onGroupsNavResizeStart();
|
||||
contentWidth = 120;
|
||||
|
||||
component.onGroupsNavWidthChange(260);
|
||||
component.onGroupsNavResizeEnd(260);
|
||||
|
||||
expect(requested).toHaveBeenCalledWith(512);
|
||||
expect(committed).toHaveBeenCalledWith(512);
|
||||
});
|
||||
|
||||
it('renders the no-results state for searches without matches', () => {
|
||||
setInputs({ searchTerm: 'zzz' });
|
||||
|
||||
|
||||
+94
@@ -3,6 +3,7 @@ import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
ElementRef,
|
||||
OutputEmitterRef,
|
||||
computed,
|
||||
effect,
|
||||
inject,
|
||||
@@ -61,6 +62,9 @@ export class GroupsViewComponent {
|
||||
/** Set of favorite channel URLs */
|
||||
readonly favoriteIds = input<Set<string>>(new Set());
|
||||
|
||||
/** Current outer sidebar width */
|
||||
readonly sidebarWidth = input<number | null>(null);
|
||||
|
||||
/** Emits when a channel is selected */
|
||||
readonly channelSelected = output<Channel>();
|
||||
|
||||
@@ -70,10 +74,17 @@ export class GroupsViewComponent {
|
||||
event: MouseEvent;
|
||||
}>();
|
||||
|
||||
/** Emits while the groups rail requests a larger total sidebar width */
|
||||
readonly sidebarWidthRequested = output<number>();
|
||||
|
||||
/** Emits when the groups rail resize ends */
|
||||
readonly sidebarWidthRequestEnded = output<number>();
|
||||
|
||||
readonly selectedGroupKey = signal<string | null>(null);
|
||||
readonly itemSize = computed(() => (this.shouldShowEpg() ? 68 : 48));
|
||||
|
||||
private previousActiveChannelUrl: string | undefined;
|
||||
private preservedContentWidth = 0;
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
@@ -254,6 +265,19 @@ export class GroupsViewComponent {
|
||||
this.selectedGroupKey.set(groupKey);
|
||||
}
|
||||
|
||||
onGroupsNavResizeStart(): void {
|
||||
this.preservedContentWidth = this.measureContentPanelWidth();
|
||||
}
|
||||
|
||||
onGroupsNavWidthChange(width: number): void {
|
||||
this.emitSidebarWidthRequest(width, this.sidebarWidthRequested);
|
||||
}
|
||||
|
||||
onGroupsNavResizeEnd(width: number): void {
|
||||
this.emitSidebarWidthRequest(width, this.sidebarWidthRequestEnded);
|
||||
this.preservedContentWidth = 0;
|
||||
}
|
||||
|
||||
trackByChannel(_: number, channel: Channel): string {
|
||||
return channel?.id;
|
||||
}
|
||||
@@ -318,4 +342,74 @@ export class GroupsViewComponent {
|
||||
const elapsed = Math.min(total, Math.max(0, now - start));
|
||||
return Math.round((elapsed / total) * 100);
|
||||
}
|
||||
|
||||
private emitSidebarWidthRequest(
|
||||
navWidth: number,
|
||||
emitter: OutputEmitterRef<number>
|
||||
): void {
|
||||
const preservedContentWidth =
|
||||
this.preservedContentWidth || this.measureContentPanelWidth(navWidth);
|
||||
const requestedWidth = Math.round(navWidth + preservedContentWidth);
|
||||
|
||||
if (requestedWidth > 0) {
|
||||
emitter.emit(requestedWidth);
|
||||
}
|
||||
}
|
||||
|
||||
private measureContentPanelWidth(currentNavWidth?: number): number {
|
||||
const contentPanel = this.hostEl.nativeElement.querySelector(
|
||||
'.groups-content-panel'
|
||||
);
|
||||
const measuredContentWidth = this.readWidth(contentPanel);
|
||||
if (measuredContentWidth > 0) {
|
||||
return measuredContentWidth;
|
||||
}
|
||||
|
||||
const hostWidth = this.readWidth(this.hostEl.nativeElement);
|
||||
const totalWidth =
|
||||
hostWidth > 0 ? hostWidth : Math.max(0, this.sidebarWidth() ?? 0);
|
||||
const navPanel = this.hostEl.nativeElement.querySelector(
|
||||
'.groups-nav-panel'
|
||||
);
|
||||
const navWidth = currentNavWidth ?? this.readWidth(navPanel);
|
||||
|
||||
if (totalWidth > 0 && navWidth > 0) {
|
||||
return Math.max(0, totalWidth - navWidth);
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private readWidth(element: Element | null): number {
|
||||
if (!element) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const rectWidth = element.getBoundingClientRect().width;
|
||||
if (rectWidth > 0) {
|
||||
return rectWidth;
|
||||
}
|
||||
|
||||
if (!(element instanceof HTMLElement)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (element.offsetWidth > 0) {
|
||||
return element.offsetWidth;
|
||||
}
|
||||
|
||||
const inlineWidth = Number.parseFloat(element.style.width);
|
||||
if (Number.isFinite(inlineWidth) && inlineWidth > 0) {
|
||||
return inlineWidth;
|
||||
}
|
||||
|
||||
const computedWidth = Number.parseFloat(
|
||||
window.getComputedStyle(element).width
|
||||
);
|
||||
if (Number.isFinite(computedWidth) && computedWidth > 0) {
|
||||
return computedWidth;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user