mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-10 18:36:15 -08:00
Hide the native title bar on win32/linux (titleBarStyle: 'hidden', frame untouched so native resize borders and snapping keep working) and render minimize / maximize-restore / close buttons in the renderer, mirroring the existing macOS traffic-light setup. - New WINDOW:* IPC contract (minimize, toggle-maximize, close, get-state) handled in window.events.ts, resolved from the sender WebContents; close goes through win.close() so window-bounds persistence still runs. - WINDOW:STATE_CHANGED pushed on maximize/unmaximize/fullscreen so the maximize/restore glyph stays correct for OS-triggered changes; controls hide while fullscreen. - WindowControlsComponent mounts once in app-root as a manual popover so it stays in the browser top layer above CDK overlays (dialogs, multi-EPG) - same behavior as macOS traffic lights. - Theme-aware via CSS vars (--app-on-surface, --app-hover-overlay); Windows-red close hover. Drag regions get right padding through a body-level frameless-platform class. - Gated by RuntimeCapabilitiesService.usesCustomWindowControls; PWA and macOS never mount the controls. Includes unit specs for the component and IPC handlers, an Electron E2E suite (window-controls.e2e.ts), and a window-chrome section in docs/architecture/workspace-shell.md. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
127 lines
3.8 KiB
TypeScript
127 lines
3.8 KiB
TypeScript
import {
|
|
AfterViewInit,
|
|
ChangeDetectionStrategy,
|
|
Component,
|
|
DestroyRef,
|
|
ElementRef,
|
|
inject,
|
|
signal,
|
|
} from '@angular/core';
|
|
import { ElectronBridgeWindowState } from '@iptvnator/shared/interfaces';
|
|
|
|
/**
|
|
* Renderer-drawn window-management buttons (minimize / maximize-restore /
|
|
* close) for the frameless title bar on Windows and Linux. macOS keeps the
|
|
* native traffic lights instead.
|
|
*
|
|
* Rendered once in the app root so the buttons stay reachable above
|
|
* full-window content such as the multi-EPG overlay and dialog backdrops —
|
|
* mirroring how the macOS traffic lights float above everything. CDK
|
|
* overlays render as popovers in the browser top layer (above any
|
|
* z-index), so the host is itself a manual popover and re-enters the top
|
|
* layer whenever another popover opens, keeping the controls topmost.
|
|
* Hidden while the window is in fullscreen, matching native title-bar
|
|
* behavior.
|
|
*/
|
|
@Component({
|
|
selector: 'app-window-controls',
|
|
templateUrl: './window-controls.component.html',
|
|
styleUrl: './window-controls.component.scss',
|
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
host: {
|
|
popover: 'manual',
|
|
'[class.is-hidden]': 'isFullScreen()',
|
|
},
|
|
})
|
|
export class WindowControlsComponent implements AfterViewInit {
|
|
readonly isMaximized = signal(false);
|
|
readonly isFullScreen = signal(false);
|
|
|
|
private readonly host =
|
|
inject<ElementRef<HTMLElement>>(ElementRef).nativeElement;
|
|
|
|
constructor() {
|
|
const bridge = window.electron;
|
|
|
|
void bridge
|
|
?.getWindowState?.()
|
|
.then((state) => this.applyState(state))
|
|
.catch(() => undefined);
|
|
|
|
const unsubscribe = bridge?.onWindowStateChange?.((state) =>
|
|
this.applyState(state)
|
|
);
|
|
|
|
document.addEventListener('toggle', this.onDocumentToggle, true);
|
|
|
|
inject(DestroyRef).onDestroy(() => {
|
|
unsubscribe?.();
|
|
document.removeEventListener('toggle', this.onDocumentToggle, true);
|
|
});
|
|
}
|
|
|
|
ngAfterViewInit(): void {
|
|
this.enterTopLayer();
|
|
}
|
|
|
|
onMinimize(): void {
|
|
void window.electron?.minimizeWindow?.();
|
|
}
|
|
|
|
onToggleMaximize(): void {
|
|
void window.electron
|
|
?.toggleMaximizeWindow?.()
|
|
.then((state) => this.applyState(state))
|
|
.catch(() => undefined);
|
|
}
|
|
|
|
onClose(): void {
|
|
void window.electron?.closeWindow?.();
|
|
}
|
|
|
|
private applyState(state: ElectronBridgeWindowState | undefined): void {
|
|
if (!state) {
|
|
return;
|
|
}
|
|
|
|
this.isMaximized.set(state.isMaximized);
|
|
this.isFullScreen.set(state.isFullScreen);
|
|
}
|
|
|
|
/**
|
|
* Re-enter the top layer after any other popover opens: top-layer
|
|
* elements paint in insertion order, so hiding and re-showing puts the
|
|
* controls back above freshly opened CDK overlays (dialogs, multi-EPG).
|
|
*/
|
|
private readonly onDocumentToggle = (event: Event): void => {
|
|
const newState = (event as { newState?: string }).newState;
|
|
if (event.target === this.host || newState !== 'open') {
|
|
return;
|
|
}
|
|
|
|
requestAnimationFrame(() => this.enterTopLayer(true));
|
|
};
|
|
|
|
private enterTopLayer(reassert = false): void {
|
|
if (
|
|
typeof this.host.showPopover !== 'function' ||
|
|
!this.host.isConnected
|
|
) {
|
|
return;
|
|
}
|
|
|
|
try {
|
|
if (this.host.matches(':popover-open')) {
|
|
if (!reassert) {
|
|
return;
|
|
}
|
|
this.host.hidePopover();
|
|
}
|
|
this.host.showPopover();
|
|
} catch {
|
|
// Stays a regular fixed element when the popover API is
|
|
// unavailable — still above non-top-layer content via z-index.
|
|
}
|
|
}
|
|
}
|