mirror of
https://github.com/4gray/iptvnator.git
synced 2026-10-08 17:06:15 -08:00
feat(ui): custom title bar with window controls for Windows and Linux (#1042)
* feat(ui): add custom title bar window controls for Windows and Linux 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> * feat(linux): upgrade Electron to 41 for frameless window decorations on Wayland With the native title bar hidden, Linux windows lost the WM-drawn shadow and rounded corners. Electron draws client-side decorations only on native Wayland, and frameless-window CSD (GTK drop shadow + extended resize boundaries) landed in Electron 41 - before that, frameless windows render as plain rectangles. - electron ^39.8.5 -> ^41.7.2 (Wayland auto-detected since 38.2; X11 sessions remain undecorated, matching other frameless Electron apps; Windows keeps its DWM shadow and rounded corners). - better-sqlite3 pinned to exactly 12.9.0: the last release shipping prebuilt binaries for both Node 20 (ABI 115, Jest) and Electron 41 (ABI 145, runtime). 12.10.0 dropped the Node 20 prebuilds, forcing a from-source build that fails without a C++ toolchain. - pnpm override node-abi 3.85.0 -> 3.92.0 so electron-builder install-app-deps can map Electron 41 to ABI 145. Reviewed Electron 40/41 breaking changes: only the renderer clipboard deprecation, which this app does not use. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(e2e): address review feedback and window-managerless Linux CI - Skip the three window-manager-dependent E2E assertions (maximize toggle, main-process state sync, minimize) on Linux CI: GitHub's ubuntu runners drive Electron under xvfb without a window manager, so maximize/minimize state never materializes there. Windows CI and local Linux/macOS runs keep the coverage. - WINDOW:TOGGLE_MAXIMIZE now returns the requested state instead of re-reading isMaximized() right after the call, which races on Linux window managers where maximize()/unmaximize() complete asynchronously; the WINDOW:STATE_CHANGED push stays authoritative. - Skip attaching window-state push listeners on macOS, where the custom controls never mount and the IPC traffic had no subscriber. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(ui): gate custom window controls on the full bridge surface Include getWindowState and onWindowStateChange in the usesCustomWindowControls capability check — the controls rely on both for initial state and for keeping the maximize/restore glyph in sync with OS-triggered changes, so a partial bridge should not mount them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2be42257ab
commit
8e0abe6feb
20 files changed
+1041
-200
No files matched your search
@@ -0,0 +1,128 @@
|
||||
import {
|
||||
closeElectronApp,
|
||||
expect,
|
||||
launchElectronApp,
|
||||
test,
|
||||
} from './electron-test-fixtures';
|
||||
|
||||
// Custom window controls are only rendered on Windows/Linux; macOS keeps
|
||||
// the native traffic lights.
|
||||
//
|
||||
// Linux CI runs Electron under xvfb, which has no window manager, so
|
||||
// maximize/minimize never take effect there — those tests are skipped and
|
||||
// rely on Windows CI plus local Linux/macOS runs for coverage.
|
||||
const isWindowManagerlessCi =
|
||||
process.platform === 'linux' && !!process.env['CI'];
|
||||
|
||||
test.describe('Custom window controls', () => {
|
||||
test.skip(
|
||||
process.platform === 'darwin',
|
||||
'macOS uses native traffic lights instead of custom controls'
|
||||
);
|
||||
|
||||
test('@electron renders the window control buttons', async ({
|
||||
dataDir,
|
||||
}) => {
|
||||
const app = await launchElectronApp(dataDir);
|
||||
|
||||
try {
|
||||
await expect(
|
||||
app.mainWindow.getByTestId('window-minimize')
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
app.mainWindow.getByTestId('window-maximize')
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
app.mainWindow.getByTestId('window-close')
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
app.mainWindow.getByTestId('window-maximize-glyph')
|
||||
).toBeVisible();
|
||||
} finally {
|
||||
await closeElectronApp(app);
|
||||
}
|
||||
});
|
||||
|
||||
test('@electron maximize button toggles the native window state', async ({
|
||||
dataDir,
|
||||
}) => {
|
||||
test.skip(
|
||||
isWindowManagerlessCi,
|
||||
'xvfb on Linux CI has no window manager'
|
||||
);
|
||||
const app = await launchElectronApp(dataDir);
|
||||
|
||||
try {
|
||||
const isMaximized = () =>
|
||||
app.electronApp.evaluate(({ BrowserWindow }) => {
|
||||
const mainWindow = BrowserWindow.getAllWindows()[0];
|
||||
return mainWindow ? mainWindow.isMaximized() : false;
|
||||
});
|
||||
|
||||
await app.mainWindow.getByTestId('window-maximize').click();
|
||||
await expect.poll(isMaximized, { timeout: 10_000 }).toBe(true);
|
||||
await expect(
|
||||
app.mainWindow.getByTestId('window-restore-glyph')
|
||||
).toBeVisible();
|
||||
|
||||
await app.mainWindow.getByTestId('window-maximize').click();
|
||||
await expect.poll(isMaximized, { timeout: 10_000 }).toBe(false);
|
||||
await expect(
|
||||
app.mainWindow.getByTestId('window-maximize-glyph')
|
||||
).toBeVisible();
|
||||
} finally {
|
||||
await closeElectronApp(app);
|
||||
}
|
||||
});
|
||||
|
||||
test('@electron reflects window state changes triggered from the main process', async ({
|
||||
dataDir,
|
||||
}) => {
|
||||
test.skip(
|
||||
isWindowManagerlessCi,
|
||||
'xvfb on Linux CI has no window manager'
|
||||
);
|
||||
const app = await launchElectronApp(dataDir);
|
||||
|
||||
try {
|
||||
await app.electronApp.evaluate(({ BrowserWindow }) => {
|
||||
BrowserWindow.getAllWindows()[0]?.maximize();
|
||||
});
|
||||
await expect(
|
||||
app.mainWindow.getByTestId('window-restore-glyph')
|
||||
).toBeVisible({ timeout: 10_000 });
|
||||
|
||||
await app.electronApp.evaluate(({ BrowserWindow }) => {
|
||||
BrowserWindow.getAllWindows()[0]?.unmaximize();
|
||||
});
|
||||
await expect(
|
||||
app.mainWindow.getByTestId('window-maximize-glyph')
|
||||
).toBeVisible({ timeout: 10_000 });
|
||||
} finally {
|
||||
await closeElectronApp(app);
|
||||
}
|
||||
});
|
||||
|
||||
test('@electron minimize button minimizes the window', async ({
|
||||
dataDir,
|
||||
}) => {
|
||||
test.skip(
|
||||
isWindowManagerlessCi,
|
||||
'xvfb on Linux CI has no window manager'
|
||||
);
|
||||
const app = await launchElectronApp(dataDir);
|
||||
|
||||
try {
|
||||
const isMinimized = () =>
|
||||
app.electronApp.evaluate(({ BrowserWindow }) => {
|
||||
const mainWindow = BrowserWindow.getAllWindows()[0];
|
||||
return mainWindow ? mainWindow.isMinimized() : false;
|
||||
});
|
||||
|
||||
await app.mainWindow.getByTestId('window-minimize').click();
|
||||
await expect.poll(isMinimized, { timeout: 10_000 }).toBe(true);
|
||||
} finally {
|
||||
await closeElectronApp(app);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -13,6 +13,7 @@ import type {
|
||||
ElectronBridgePlaylistUpsertInput,
|
||||
ElectronBridgeRemoteControlCommand,
|
||||
ElectronBridgeRemoteControlStatus,
|
||||
ElectronBridgeWindowState,
|
||||
ElectronBridgeXtreamContentStream,
|
||||
ExternalPlayerSession,
|
||||
PlaybackPositionData,
|
||||
@@ -37,6 +38,11 @@ const EXTERNAL_PLAYER_SESSION_UPDATE = 'EXTERNAL_PLAYER_SESSION_UPDATE';
|
||||
const EMBEDDED_MPV_SESSION_UPDATE = 'EMBEDDED_MPV_SESSION_UPDATE';
|
||||
const DB_OPERATION_EVENT = 'DB_OPERATION_EVENT';
|
||||
const PLAYLIST_REFRESH_EVENT = 'PLAYLIST:REFRESH_EVENT';
|
||||
const WINDOW_MINIMIZE = 'WINDOW:MINIMIZE';
|
||||
const WINDOW_TOGGLE_MAXIMIZE = 'WINDOW:TOGGLE_MAXIMIZE';
|
||||
const WINDOW_CLOSE = 'WINDOW:CLOSE';
|
||||
const WINDOW_GET_STATE = 'WINDOW:GET_STATE';
|
||||
const WINDOW_STATE_CHANGED = 'WINDOW:STATE_CHANGED';
|
||||
|
||||
const dbSaveContentProgressListeners = new Set<
|
||||
(
|
||||
@@ -282,6 +288,20 @@ const electronApi: ElectronBridgeApi = {
|
||||
},
|
||||
getAppVersion: () => ipcRenderer.invoke('get-app-version'),
|
||||
platform: process.platform,
|
||||
minimizeWindow: () => ipcRenderer.invoke(WINDOW_MINIMIZE),
|
||||
toggleMaximizeWindow: () => ipcRenderer.invoke(WINDOW_TOGGLE_MAXIMIZE),
|
||||
closeWindow: () => ipcRenderer.invoke(WINDOW_CLOSE),
|
||||
getWindowState: () => ipcRenderer.invoke(WINDOW_GET_STATE),
|
||||
onWindowStateChange: (
|
||||
callback: (state: ElectronBridgeWindowState) => void
|
||||
) => {
|
||||
const handler = (
|
||||
_event: Electron.IpcRendererEvent,
|
||||
state: ElectronBridgeWindowState
|
||||
) => callback(state);
|
||||
ipcRenderer.on(WINDOW_STATE_CHANGED, handler);
|
||||
return () => ipcRenderer.off(WINDOW_STATE_CHANGED, handler);
|
||||
},
|
||||
fetchPlaylistByUrl: (url: string, title?: string) =>
|
||||
ipcRenderer.invoke('fetch-playlist-by-url', url, title),
|
||||
updatePlaylistFromFilePath: (filePath: string, title: string) =>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { app, BrowserWindow, Menu, screen, shell } from 'electron';
|
||||
import { WINDOW_STATE_CHANGED } from '@iptvnator/shared/interfaces';
|
||||
import { join, resolve } from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { rendererAppName, rendererAppPort } from './constants';
|
||||
@@ -237,6 +238,50 @@ export default class App {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hide the native title bar on every desktop platform. macOS keeps the
|
||||
* system traffic lights (overlay), while Windows/Linux rely on the
|
||||
* renderer-drawn window controls (`app-window-controls`) wired up via the
|
||||
* WINDOW:* IPC channels. `frame` stays untouched so native resize borders
|
||||
* and snapping keep working.
|
||||
*/
|
||||
private static getPlatformTitleBarOptions(): Electron.BrowserWindowConstructorOptions {
|
||||
if (process.platform === 'darwin') {
|
||||
return {
|
||||
titleBarStyle: 'hidden',
|
||||
titleBarOverlay: true,
|
||||
trafficLightPosition: { x: 16, y: 20 },
|
||||
};
|
||||
}
|
||||
|
||||
return { titleBarStyle: 'hidden' };
|
||||
}
|
||||
|
||||
private static attachWindowStateEvents(win: Electron.BrowserWindow): void {
|
||||
// Only Windows/Linux render custom window controls that subscribe
|
||||
// to these pushes; macOS keeps the native traffic lights, so
|
||||
// sending state updates there would be dead IPC traffic.
|
||||
if (process.platform === 'darwin') {
|
||||
return;
|
||||
}
|
||||
|
||||
const sendWindowState = () => {
|
||||
if (win.isDestroyed()) {
|
||||
return;
|
||||
}
|
||||
|
||||
win.webContents.send(WINDOW_STATE_CHANGED, {
|
||||
isMaximized: win.isMaximized(),
|
||||
isFullScreen: win.isFullScreen(),
|
||||
});
|
||||
};
|
||||
|
||||
win.on('maximize', sendWindowState);
|
||||
win.on('unmaximize', sendWindowState);
|
||||
win.on('enter-full-screen', sendWindowState);
|
||||
win.on('leave-full-screen', sendWindowState);
|
||||
}
|
||||
|
||||
private static initMainWindow() {
|
||||
const workAreaSize = screen.getPrimaryDisplay().workAreaSize;
|
||||
const width = Math.min(1280, workAreaSize.width || 1280);
|
||||
@@ -254,16 +299,11 @@ export default class App {
|
||||
...savedWindowBounds,
|
||||
minHeight: 600,
|
||||
minWidth: 900,
|
||||
...(process.platform === 'darwin'
|
||||
? {
|
||||
titleBarStyle: 'hidden',
|
||||
titleBarOverlay: true,
|
||||
trafficLightPosition: { x: 16, y: 20 },
|
||||
}
|
||||
: {}),
|
||||
...App.getPlatformTitleBarOptions(),
|
||||
});
|
||||
App.mainWindow.setMenu(null);
|
||||
attachWindowTrace(App.mainWindow);
|
||||
App.attachWindowStateEvents(App.mainWindow);
|
||||
if (!savedWindowBounds) {
|
||||
App.mainWindow.center();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
const mockHandlers = new Map<
|
||||
string,
|
||||
(event: unknown, ...args: unknown[]) => unknown
|
||||
>();
|
||||
const mockFromWebContents = jest.fn();
|
||||
|
||||
jest.mock('electron', () => ({
|
||||
ipcMain: {
|
||||
handle: jest.fn(
|
||||
(
|
||||
channel: string,
|
||||
handler: (event: unknown, ...args: unknown[]) => unknown
|
||||
) => {
|
||||
mockHandlers.set(channel, handler);
|
||||
}
|
||||
),
|
||||
},
|
||||
BrowserWindow: {
|
||||
fromWebContents: (...args: unknown[]) => mockFromWebContents(...args),
|
||||
},
|
||||
}));
|
||||
|
||||
function createFakeWindow(
|
||||
initial: { maximized?: boolean; fullScreen?: boolean } = {}
|
||||
) {
|
||||
const state = {
|
||||
maximized: initial.maximized ?? false,
|
||||
fullScreen: initial.fullScreen ?? false,
|
||||
};
|
||||
|
||||
return {
|
||||
isDestroyed: jest.fn(() => false),
|
||||
isMaximized: jest.fn(() => state.maximized),
|
||||
isFullScreen: jest.fn(() => state.fullScreen),
|
||||
minimize: jest.fn(),
|
||||
maximize: jest.fn(() => {
|
||||
state.maximized = true;
|
||||
}),
|
||||
unmaximize: jest.fn(() => {
|
||||
state.maximized = false;
|
||||
}),
|
||||
close: jest.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
const fakeEvent = { sender: {} };
|
||||
|
||||
describe('WindowEvents', () => {
|
||||
beforeAll(async () => {
|
||||
await import('./window.events');
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
mockFromWebContents.mockReset();
|
||||
});
|
||||
|
||||
it('registers handlers for all window control channels', () => {
|
||||
expect([...mockHandlers.keys()].sort()).toEqual([
|
||||
'WINDOW:CLOSE',
|
||||
'WINDOW:GET_STATE',
|
||||
'WINDOW:MINIMIZE',
|
||||
'WINDOW:TOGGLE_MAXIMIZE',
|
||||
]);
|
||||
});
|
||||
|
||||
it('minimizes the sender window', () => {
|
||||
const win = createFakeWindow();
|
||||
mockFromWebContents.mockReturnValue(win);
|
||||
|
||||
mockHandlers.get('WINDOW:MINIMIZE')!(fakeEvent);
|
||||
|
||||
expect(win.minimize).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('maximizes an unmaximized window and returns the new state', () => {
|
||||
const win = createFakeWindow({ maximized: false });
|
||||
mockFromWebContents.mockReturnValue(win);
|
||||
|
||||
const result = mockHandlers.get('WINDOW:TOGGLE_MAXIMIZE')!(fakeEvent);
|
||||
|
||||
expect(win.maximize).toHaveBeenCalledTimes(1);
|
||||
expect(win.unmaximize).not.toHaveBeenCalled();
|
||||
expect(result).toEqual({ isMaximized: true, isFullScreen: false });
|
||||
});
|
||||
|
||||
it('unmaximizes a maximized window and returns the new state', () => {
|
||||
const win = createFakeWindow({ maximized: true });
|
||||
mockFromWebContents.mockReturnValue(win);
|
||||
|
||||
const result = mockHandlers.get('WINDOW:TOGGLE_MAXIMIZE')!(fakeEvent);
|
||||
|
||||
expect(win.unmaximize).toHaveBeenCalledTimes(1);
|
||||
expect(win.maximize).not.toHaveBeenCalled();
|
||||
expect(result).toEqual({ isMaximized: false, isFullScreen: false });
|
||||
});
|
||||
|
||||
it('closes the sender window', () => {
|
||||
const win = createFakeWindow();
|
||||
mockFromWebContents.mockReturnValue(win);
|
||||
|
||||
mockHandlers.get('WINDOW:CLOSE')!(fakeEvent);
|
||||
|
||||
expect(win.close).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('returns the current window state', () => {
|
||||
const win = createFakeWindow({ maximized: true, fullScreen: true });
|
||||
mockFromWebContents.mockReturnValue(win);
|
||||
|
||||
const result = mockHandlers.get('WINDOW:GET_STATE')!(fakeEvent);
|
||||
|
||||
expect(result).toEqual({ isMaximized: true, isFullScreen: true });
|
||||
});
|
||||
|
||||
it('is a safe no-op when the sender has no window', () => {
|
||||
mockFromWebContents.mockReturnValue(null);
|
||||
|
||||
expect(() =>
|
||||
mockHandlers.get('WINDOW:MINIMIZE')!(fakeEvent)
|
||||
).not.toThrow();
|
||||
expect(mockHandlers.get('WINDOW:TOGGLE_MAXIMIZE')!(fakeEvent)).toEqual({
|
||||
isMaximized: false,
|
||||
isFullScreen: false,
|
||||
});
|
||||
expect(mockHandlers.get('WINDOW:GET_STATE')!(fakeEvent)).toEqual({
|
||||
isMaximized: false,
|
||||
isFullScreen: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('treats a destroyed window like a missing window', () => {
|
||||
const win = createFakeWindow();
|
||||
win.isDestroyed.mockReturnValue(true);
|
||||
mockFromWebContents.mockReturnValue(win);
|
||||
|
||||
mockHandlers.get('WINDOW:MINIMIZE')!(fakeEvent);
|
||||
|
||||
expect(win.minimize).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* IPC handlers for the renderer-drawn window controls used with the custom
|
||||
* title bar on Windows/Linux (`titleBarStyle: 'hidden'`). Each handler
|
||||
* resolves the window from the calling WebContents so it works without
|
||||
* coupling to the static main-window reference.
|
||||
*/
|
||||
|
||||
import { BrowserWindow, ipcMain } from 'electron';
|
||||
import {
|
||||
WINDOW_CLOSE,
|
||||
WINDOW_GET_STATE,
|
||||
WINDOW_MINIMIZE,
|
||||
WINDOW_TOGGLE_MAXIMIZE,
|
||||
} from '@iptvnator/shared/interfaces';
|
||||
|
||||
interface WindowState {
|
||||
isMaximized: boolean;
|
||||
isFullScreen: boolean;
|
||||
}
|
||||
|
||||
function getSenderWindow(
|
||||
event: Electron.IpcMainInvokeEvent
|
||||
): Electron.BrowserWindow | null {
|
||||
const win = BrowserWindow.fromWebContents(event.sender);
|
||||
return win && !win.isDestroyed() ? win : null;
|
||||
}
|
||||
|
||||
function getWindowState(win: Electron.BrowserWindow | null): WindowState {
|
||||
return {
|
||||
isMaximized: !!win?.isMaximized(),
|
||||
isFullScreen: !!win?.isFullScreen(),
|
||||
};
|
||||
}
|
||||
|
||||
export default class WindowEvents {
|
||||
static bootstrapWindowEvents(): Electron.IpcMain {
|
||||
return ipcMain;
|
||||
}
|
||||
}
|
||||
|
||||
ipcMain.handle(WINDOW_MINIMIZE, (event) => {
|
||||
getSenderWindow(event)?.minimize();
|
||||
});
|
||||
|
||||
ipcMain.handle(WINDOW_TOGGLE_MAXIMIZE, (event): WindowState => {
|
||||
const win = getSenderWindow(event);
|
||||
|
||||
if (!win) {
|
||||
return getWindowState(win);
|
||||
}
|
||||
|
||||
// maximize()/unmaximize() complete asynchronously on Linux window
|
||||
// managers, so re-reading isMaximized() here would race. Report the
|
||||
// requested state instead; the WINDOW:STATE_CHANGED push stays the
|
||||
// authoritative update once the window manager has acted.
|
||||
const shouldMaximize = !win.isMaximized();
|
||||
|
||||
if (shouldMaximize) {
|
||||
win.maximize();
|
||||
} else {
|
||||
win.unmaximize();
|
||||
}
|
||||
|
||||
return {
|
||||
isMaximized: shouldMaximize,
|
||||
isFullScreen: win.isFullScreen(),
|
||||
};
|
||||
});
|
||||
|
||||
// win.close() (instead of app.quit()) so the window's 'close' handler still
|
||||
// persists the window bounds before shutdown.
|
||||
ipcMain.handle(WINDOW_CLOSE, (event) => {
|
||||
getSenderWindow(event)?.close();
|
||||
});
|
||||
|
||||
ipcMain.handle(WINDOW_GET_STATE, (event): WindowState => {
|
||||
return getWindowState(getSenderWindow(event));
|
||||
});
|
||||
@@ -24,6 +24,7 @@ import SquirrelEvents from './app/events/squirrel.events';
|
||||
import StalkerEvents from './app/events/stalker.events';
|
||||
import { isStartupTraceEnabled, trace } from './app/services/debug-trace';
|
||||
import { databaseWorkerClient } from './app/services/database-worker-client';
|
||||
import WindowEvents from './app/events/window.events';
|
||||
import XtreamEvents from './app/events/xtream.events';
|
||||
|
||||
app.setName('iptvnator');
|
||||
@@ -90,6 +91,7 @@ export default class Main {
|
||||
}
|
||||
|
||||
ElectronEvents.bootstrapElectronEvents();
|
||||
WindowEvents.bootstrapWindowEvents();
|
||||
EmbeddedMpvEvents.bootstrapEmbeddedMpvEvents();
|
||||
PlaylistEvents.bootstrapPlaylistEvents();
|
||||
SharedEvents.bootstrapSharedEvents();
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
<router-outlet />
|
||||
|
||||
<app-epg-progress-panel />
|
||||
|
||||
@if (usesCustomWindowControls) {
|
||||
<app-window-controls />
|
||||
}
|
||||
@@ -4,9 +4,13 @@ import { Router, RouterOutlet } from '@angular/router';
|
||||
import { Actions, ofType } from '@ngrx/effects';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { EpgRuntimeBridgeService, EpgService } from '@iptvnator/epg/data-access';
|
||||
import {
|
||||
EpgRuntimeBridgeService,
|
||||
EpgService,
|
||||
} from '@iptvnator/epg/data-access';
|
||||
import { WORKSPACE_SHELL_ACTIONS } from '@iptvnator/workspace/shell/util';
|
||||
import { EpgProgressPanelComponent } from '@iptvnator/ui/epg/progress-panel';
|
||||
import { WindowControlsComponent } from '@iptvnator/ui/components';
|
||||
import { PlaylistActions, selectAllPlaylistsMeta } from '@iptvnator/m3u-state';
|
||||
import { filter, take } from 'rxjs';
|
||||
import {
|
||||
@@ -30,12 +34,15 @@ const debugAppComponent = createDevLogger('AppComponent');
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
templateUrl: './app.component.html',
|
||||
imports: [EpgProgressPanelComponent, RouterOutlet],
|
||||
imports: [EpgProgressPanelComponent, RouterOutlet, WindowControlsComponent],
|
||||
})
|
||||
export class AppComponent implements OnInit {
|
||||
@HostBinding('class.macos-platform') get isMacOS() {
|
||||
return this.runtime.isMacOS;
|
||||
}
|
||||
get usesCustomWindowControls() {
|
||||
return this.runtime.usesCustomWindowControls;
|
||||
}
|
||||
private actions$ = inject(Actions);
|
||||
private dataService = inject(DataService);
|
||||
private epgBridge = inject(EpgRuntimeBridgeService);
|
||||
@@ -53,6 +60,12 @@ export class AppComponent implements OnInit {
|
||||
private readonly DEFAULT_LANG = Language.ENGLISH;
|
||||
|
||||
constructor() {
|
||||
// Body-level class (like 'dark-theme') so layout adjustments also
|
||||
// reach content rendered outside app-root, e.g. cdk-overlay content.
|
||||
if (this.runtime.usesCustomWindowControls) {
|
||||
document.body.classList.add('frameless-platform');
|
||||
}
|
||||
|
||||
const electronProcess = this.dataService.remote?.process;
|
||||
if (
|
||||
this.dataService.isElectron &&
|
||||
|
||||
@@ -100,6 +100,17 @@ app-root.macos-platform app-navigation .portal-status-container {
|
||||
padding-top: 30px;
|
||||
}
|
||||
|
||||
// Windows/Linux frameless title bar: reserve room in top-aligned drag
|
||||
// regions for the renderer-drawn window controls (3 × 46px buttons).
|
||||
// Body-level class so it also reaches cdk-overlay content (multi-EPG).
|
||||
body.frameless-platform .workspace-header {
|
||||
padding-right: 150px;
|
||||
}
|
||||
|
||||
body.frameless-platform #epg-navigation {
|
||||
padding-right: 150px;
|
||||
}
|
||||
|
||||
// ─── Base ─────────────────────────────────────────────────────────────────────
|
||||
body,
|
||||
html {
|
||||
|
||||
@@ -74,20 +74,20 @@ Provider route integration:
|
||||
The shell is intentionally split into four persistent regions:
|
||||
|
||||
1. Left rail:
|
||||
1. Static workspace links for dashboard, sources, global favorites, and recently viewed.
|
||||
2. Provider-aware context links derived from the active or current playlist.
|
||||
3. Settings remains a persistent footer shortcut in the rail.
|
||||
1. Static workspace links for dashboard, sources, global favorites, and recently viewed.
|
||||
2. Provider-aware context links derived from the active or current playlist.
|
||||
3. Settings remains a persistent footer shortcut in the rail.
|
||||
2. Top header:
|
||||
1. Playlist switcher.
|
||||
2. Route-aware search input and command palette trigger.
|
||||
3. Add source action.
|
||||
4. Optional playlist refresh and route-specific shortcut actions.
|
||||
5. Downloads shortcut in Electron.
|
||||
1. Playlist switcher.
|
||||
2. Route-aware search input and command palette trigger.
|
||||
3. Add source action.
|
||||
4. Optional playlist refresh and route-specific shortcut actions.
|
||||
5. Downloads shortcut in Electron.
|
||||
3. Main body:
|
||||
1. Optional left context panel.
|
||||
2. Main router outlet content.
|
||||
1. Optional left context panel.
|
||||
2. Main router outlet content.
|
||||
4. Optional footer:
|
||||
1. External playback session bar when a docked session is visible.
|
||||
1. External playback session bar when a docked session is visible.
|
||||
|
||||
`WorkspaceShellComponent` binds only to `WorkspaceShellFacade`. The facade is
|
||||
kept as a thin template-facing API and delegates ownership to component-scoped
|
||||
@@ -116,15 +116,15 @@ for the template unless the template contract itself intentionally changes.
|
||||
The shell decides which secondary panel to show from the current route:
|
||||
|
||||
1. `/workspace/sources`
|
||||
1. `WorkspaceSourcesFiltersPanelComponent`
|
||||
1. `WorkspaceSourcesFiltersPanelComponent`
|
||||
2. Xtream category sections (`live`, `vod`, `series`)
|
||||
1. `WorkspaceContextPanelComponent`
|
||||
1. `WorkspaceContextPanelComponent`
|
||||
3. Stalker category sections (`itv`, `radio`, `vod`, `series`)
|
||||
1. `WorkspaceContextPanelComponent`
|
||||
1. `WorkspaceContextPanelComponent`
|
||||
4. `/workspace/settings`
|
||||
1. `WorkspaceSettingsContextPanelComponent`
|
||||
1. `WorkspaceSettingsContextPanelComponent`
|
||||
5. Downloads sections
|
||||
1. `WorkspaceCollectionContextPanelComponent`
|
||||
1. `WorkspaceCollectionContextPanelComponent`
|
||||
|
||||
The context panel is part of the shell contract. New workspace-level routes
|
||||
should explicitly decide whether they need one rather than adding local
|
||||
@@ -197,6 +197,91 @@ Keyboard shortcut help is shell-owned:
|
||||
added. Do not include native browser/editor behavior such as `Tab` or
|
||||
platform text editing shortcuts.
|
||||
|
||||
## Window Chrome And Custom Title Bar
|
||||
|
||||
The Electron window hides the native title bar on all desktop platforms
|
||||
(`titleBarStyle: 'hidden'` in `apps/electron-backend/src/app/app.ts`):
|
||||
|
||||
1. macOS keeps the native traffic lights (`titleBarOverlay: true`,
|
||||
`trafficLightPosition`); the renderer draws no window buttons.
|
||||
2. Windows and Linux use renderer-drawn window controls
|
||||
(`app-window-controls`, `libs/ui/components/src/lib/window-controls/`).
|
||||
`frame` is intentionally left untouched so native resize borders and
|
||||
window snapping keep working.
|
||||
|
||||
The controls are mounted once in `app-root` (not inside the workspace
|
||||
header) as a `position: fixed` top-right overlay so they stay clickable
|
||||
above full-window content such as the multi-EPG cdk overlay and Material
|
||||
dialog backdrops — the same behavior as the macOS traffic lights. Because
|
||||
CDK overlays render as popovers in the browser top layer (above any
|
||||
z-index), the component host is itself a `popover="manual"` element: it
|
||||
enters the top layer on init and re-enters it (hide + show) whenever
|
||||
another popover opens, so the controls always paint last. The
|
||||
`z-index: 10000` remains only as a fallback when the popover API is
|
||||
unavailable. They render only when
|
||||
`RuntimeCapabilitiesService.usesCustomWindowControls` is true (Windows/Linux
|
||||
Electron with the window-control bridge methods available); the PWA and
|
||||
macOS never mount them.
|
||||
|
||||
IPC contract (constants in `libs/shared/interfaces/src/lib/ipc-commands.ts`,
|
||||
handlers in `apps/electron-backend/src/app/events/window.events.ts`):
|
||||
|
||||
1. `WINDOW:MINIMIZE`, `WINDOW:TOGGLE_MAXIMIZE`, `WINDOW:CLOSE`,
|
||||
`WINDOW:GET_STATE` are `ipcMain.handle` channels resolved from the sender
|
||||
WebContents. Close goes through `win.close()` so the existing
|
||||
window-bounds persistence in `app.ts` still runs.
|
||||
2. `WINDOW:STATE_CHANGED` is pushed main → renderer on
|
||||
maximize/unmaximize/enter-full-screen/leave-full-screen so the
|
||||
maximize/restore glyph stays correct for externally triggered changes
|
||||
(double-click on a drag region, OS snap, F11). The controls hide
|
||||
themselves while the window is fullscreen.
|
||||
|
||||
Layout integration:
|
||||
|
||||
1. `document.body` gets a `frameless-platform` class (set in
|
||||
`AppComponent`, same mechanism as `dark-theme`) — body-level so rules
|
||||
also reach cdk-overlay content rendered outside `app-root`.
|
||||
2. `apps/web/src/styles.scss` reserves `padding-right: 150px` in
|
||||
top-aligned drag regions (`.workspace-header`, multi-EPG
|
||||
`#epg-navigation`) for the 3 × 46px button strip.
|
||||
3. Button colors follow the theme via CSS variables (`--app-on-surface`,
|
||||
`--app-hover-overlay`); the close button uses the Windows-style red
|
||||
hover (`#e81123`). No theme IPC is involved.
|
||||
|
||||
Window decorations on Linux (shadows, corners):
|
||||
|
||||
1. Hiding the title bar removes the window manager's decorations, so the
|
||||
shadow/rounded corners must come from client-side decorations (CSD).
|
||||
Electron only draws CSD on native Wayland, and frameless-window CSD
|
||||
(GTK drop shadow + extended resize boundaries, `hasShadow: true` by
|
||||
default) requires **Electron >= 41** — the reason the dependency was
|
||||
bumped from 39. Electron picks Wayland automatically on Wayland
|
||||
sessions since 38.2.
|
||||
2. On X11 sessions frameless windows stay undecorated (square, no
|
||||
shadow) — an upstream platform limitation shared by e.g. VS Code.
|
||||
3. Rounded corners for frameless Linux windows are not yet supported by
|
||||
Electron (tracked upstream as planned work); Windows 11 keeps its DWM
|
||||
rounded corners and shadow because the standard frame is retained.
|
||||
|
||||
Toolchain notes for the Electron 41 upgrade:
|
||||
|
||||
1. `better-sqlite3` is pinned to exactly `12.9.0` — the last release that
|
||||
ships prebuilt binaries for BOTH Node 20 (ABI 115, used by Jest) and
|
||||
Electron 41 (ABI 145, used at runtime). `12.10.0` dropped the Node 20
|
||||
prebuilds, which forces a from-source build that fails on machines
|
||||
without a C++ toolchain.
|
||||
2. The pnpm override `node-abi@3.85.0 -> 3.92.0` is required so
|
||||
`@electron/rebuild` (via `electron-builder install-app-deps`) can map
|
||||
Electron 41 to its ABI.
|
||||
|
||||
Known caveats:
|
||||
|
||||
1. DIY buttons cannot show the Windows 11 Snap Layouts flyout (only native
|
||||
caption buttons or the Window Controls Overlay get that).
|
||||
2. Double-click-to-maximize on drag regions is handled natively by
|
||||
Electron/Chromium; on Linux the exact behavior depends on the window
|
||||
manager.
|
||||
|
||||
## Maintenance Guidance
|
||||
|
||||
Use this document as the source of truth when changing workspace shell behavior.
|
||||
|
||||
@@ -38,6 +38,32 @@ export class RuntimeCapabilitiesService {
|
||||
return this.platform === 'darwin';
|
||||
}
|
||||
|
||||
get isWindows(): boolean {
|
||||
return this.platform === 'win32';
|
||||
}
|
||||
|
||||
get isLinux(): boolean {
|
||||
return this.platform === 'linux';
|
||||
}
|
||||
|
||||
/**
|
||||
* True when the window has no native title bar and the renderer must
|
||||
* draw its own window-management buttons (Windows/Linux). macOS keeps
|
||||
* the native traffic lights instead.
|
||||
*/
|
||||
get usesCustomWindowControls(): boolean {
|
||||
return (
|
||||
(this.isWindows || this.isLinux) &&
|
||||
[
|
||||
'minimizeWindow',
|
||||
'toggleMaximizeWindow',
|
||||
'closeWindow',
|
||||
'getWindowState',
|
||||
'onWindowStateChange',
|
||||
].every((methodName) => this.hasElectronMethod(methodName))
|
||||
);
|
||||
}
|
||||
|
||||
get supportsEpg(): boolean {
|
||||
return (
|
||||
this.supportsEpgImport &&
|
||||
|
||||
@@ -135,6 +135,11 @@ export interface ElectronBridgeDialogFilter {
|
||||
extensions: string[];
|
||||
}
|
||||
|
||||
export interface ElectronBridgeWindowState {
|
||||
isMaximized: boolean;
|
||||
isFullScreen: boolean;
|
||||
}
|
||||
|
||||
export interface ElectronBridgeAiSettings {
|
||||
aiProvider: string;
|
||||
aiModelName: string;
|
||||
@@ -425,6 +430,13 @@ export interface ElectronBridgeApi {
|
||||
) => () => void;
|
||||
getAppVersion: () => Promise<string>;
|
||||
platform: string;
|
||||
minimizeWindow: () => Promise<void>;
|
||||
toggleMaximizeWindow: () => Promise<ElectronBridgeWindowState>;
|
||||
closeWindow: () => Promise<void>;
|
||||
getWindowState: () => Promise<ElectronBridgeWindowState>;
|
||||
onWindowStateChange: (
|
||||
callback: (state: ElectronBridgeWindowState) => void
|
||||
) => () => void;
|
||||
fetchPlaylistByUrl: (url: string, title?: string) => Promise<Playlist>;
|
||||
updatePlaylistFromFilePath: (
|
||||
filePath: string,
|
||||
|
||||
@@ -75,3 +75,10 @@ export const DELETE_ALL_PLAYLISTS = 'DELETE_ALL_PLAYLISTS';
|
||||
|
||||
// Remote Control
|
||||
export const REMOTE_CONTROL_CHANGE_CHANNEL = 'REMOTE_CONTROL_CHANGE_CHANNEL';
|
||||
|
||||
// Window controls (custom title bar on Windows/Linux)
|
||||
export const WINDOW_MINIMIZE = 'WINDOW:MINIMIZE';
|
||||
export const WINDOW_TOGGLE_MAXIMIZE = 'WINDOW:TOGGLE_MAXIMIZE';
|
||||
export const WINDOW_CLOSE = 'WINDOW:CLOSE';
|
||||
export const WINDOW_GET_STATE = 'WINDOW:GET_STATE';
|
||||
export const WINDOW_STATE_CHANGED = 'WINDOW:STATE_CHANGED';
|
||||
@@ -12,3 +12,4 @@ export * from './lib/progress-capsule/progress-capsule.component';
|
||||
export * from './lib/resizable/resizable.directive';
|
||||
export * from './lib/season-container/season-container.component';
|
||||
export * from './lib/watched-badge/watched-badge.component';
|
||||
export * from './lib/window-controls/window-controls.component';
|
||||
@@ -0,0 +1,55 @@
|
||||
<button
|
||||
type="button"
|
||||
class="window-control"
|
||||
data-test-id="window-minimize"
|
||||
aria-label="Minimize"
|
||||
title="Minimize"
|
||||
(click)="onMinimize()"
|
||||
>
|
||||
<svg viewBox="0 0 10 10" class="crisp" aria-hidden="true">
|
||||
<path d="M0 5h10" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="window-control"
|
||||
data-test-id="window-maximize"
|
||||
[attr.aria-label]="isMaximized() ? 'Restore' : 'Maximize'"
|
||||
[attr.title]="isMaximized() ? 'Restore' : 'Maximize'"
|
||||
(click)="onToggleMaximize()"
|
||||
>
|
||||
@if (isMaximized()) {
|
||||
<svg
|
||||
viewBox="0 0 10 10"
|
||||
class="crisp"
|
||||
data-test-id="window-restore-glyph"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M0.5 2.5h7v7h-7z" />
|
||||
<path d="M2.5 2.5v-2h7v7h-2" />
|
||||
</svg>
|
||||
} @else {
|
||||
<svg
|
||||
viewBox="0 0 10 10"
|
||||
class="crisp"
|
||||
data-test-id="window-maximize-glyph"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M0.5 0.5h9v9h-9z" />
|
||||
</svg>
|
||||
}
|
||||
</button>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="window-control close"
|
||||
data-test-id="window-close"
|
||||
aria-label="Close"
|
||||
title="Close"
|
||||
(click)="onClose()"
|
||||
>
|
||||
<svg viewBox="0 0 10 10" aria-hidden="true">
|
||||
<path d="M0.5 0.5l9 9m0 -9l-9 9" />
|
||||
</svg>
|
||||
</button>
|
||||
@@ -0,0 +1,88 @@
|
||||
// Floats above all app content so the window stays controllable while
|
||||
// dialogs or the multi-EPG overlay are open — same behavior as the macOS
|
||||
// traffic lights. The host is a manual popover (see component) because CDK
|
||||
// overlays live in the browser top layer where z-index cannot compete; the
|
||||
// declarations below also neutralize the UA popover styles (inset, margin,
|
||||
// border, background) and keep the strip visible/positioned when the
|
||||
// popover API is unavailable.
|
||||
:host {
|
||||
position: fixed;
|
||||
inset: 0 0 auto auto;
|
||||
z-index: 10000;
|
||||
display: flex;
|
||||
width: fit-content;
|
||||
height: fit-content;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
overflow: visible;
|
||||
color: inherit;
|
||||
app-region: no-drag;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
:host(.is-hidden) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.window-control {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 46px;
|
||||
height: 32px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
color: var(--app-on-surface, #1d1b1e);
|
||||
opacity: 0.85;
|
||||
// Native caption buttons use the arrow cursor, not the hand.
|
||||
cursor: default;
|
||||
|
||||
svg {
|
||||
display: block;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
svg.crisp {
|
||||
shape-rendering: crispEdges;
|
||||
}
|
||||
|
||||
path {
|
||||
fill: none;
|
||||
stroke: currentColor;
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: var(--app-hover-overlay, rgba(0, 0, 0, 0.06));
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--app-on-surface, #1d1b1e) 14%,
|
||||
transparent
|
||||
);
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid var(--mat-sys-primary);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
&.close:hover {
|
||||
background: #e81123;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
&.close:active {
|
||||
background: #c50f1f;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ElectronBridgeWindowState } from '@iptvnator/shared/interfaces';
|
||||
import { WindowControlsComponent } from './window-controls.component';
|
||||
|
||||
describe('WindowControlsComponent', () => {
|
||||
let fixture: ComponentFixture<WindowControlsComponent>;
|
||||
let stateChangeCallback:
|
||||
| ((state: ElectronBridgeWindowState) => void)
|
||||
| undefined;
|
||||
|
||||
const unsubscribe = jest.fn();
|
||||
const electronMock = {
|
||||
getWindowState: jest.fn(),
|
||||
onWindowStateChange: jest.fn(),
|
||||
minimizeWindow: jest.fn(),
|
||||
toggleMaximizeWindow: jest.fn(),
|
||||
closeWindow: jest.fn(),
|
||||
};
|
||||
|
||||
const query = (testId: string): HTMLElement | null =>
|
||||
fixture.nativeElement.querySelector(`[data-test-id="${testId}"]`);
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
stateChangeCallback = undefined;
|
||||
electronMock.getWindowState.mockResolvedValue({
|
||||
isMaximized: false,
|
||||
isFullScreen: false,
|
||||
});
|
||||
electronMock.toggleMaximizeWindow.mockResolvedValue({
|
||||
isMaximized: true,
|
||||
isFullScreen: false,
|
||||
});
|
||||
electronMock.minimizeWindow.mockResolvedValue(undefined);
|
||||
electronMock.closeWindow.mockResolvedValue(undefined);
|
||||
electronMock.onWindowStateChange.mockImplementation(
|
||||
(callback: (state: ElectronBridgeWindowState) => void) => {
|
||||
stateChangeCallback = callback;
|
||||
return unsubscribe;
|
||||
}
|
||||
);
|
||||
(window as { electron?: unknown }).electron = electronMock;
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [WindowControlsComponent],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(WindowControlsComponent);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
delete (window as { electron?: unknown }).electron;
|
||||
});
|
||||
|
||||
it('renders minimize, maximize and close buttons with the maximize glyph', () => {
|
||||
expect(query('window-minimize')).not.toBeNull();
|
||||
expect(query('window-maximize')).not.toBeNull();
|
||||
expect(query('window-close')).not.toBeNull();
|
||||
expect(query('window-maximize-glyph')).not.toBeNull();
|
||||
expect(query('window-restore-glyph')).toBeNull();
|
||||
expect(electronMock.getWindowState).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls the bridge methods when the buttons are clicked', () => {
|
||||
query('window-minimize')?.click();
|
||||
expect(electronMock.minimizeWindow).toHaveBeenCalledTimes(1);
|
||||
|
||||
query('window-maximize')?.click();
|
||||
expect(electronMock.toggleMaximizeWindow).toHaveBeenCalledTimes(1);
|
||||
|
||||
query('window-close')?.click();
|
||||
expect(electronMock.closeWindow).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('swaps to the restore glyph when the window state reports maximized', () => {
|
||||
stateChangeCallback?.({ isMaximized: true, isFullScreen: false });
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(query('window-restore-glyph')).not.toBeNull();
|
||||
expect(query('window-maximize-glyph')).toBeNull();
|
||||
expect(query('window-maximize')?.getAttribute('aria-label')).toBe(
|
||||
'Restore'
|
||||
);
|
||||
});
|
||||
|
||||
it('applies the restore state returned by toggleMaximizeWindow', async () => {
|
||||
query('window-maximize')?.click();
|
||||
await fixture.whenStable();
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(query('window-restore-glyph')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('hides the host while the window is in fullscreen', () => {
|
||||
stateChangeCallback?.({ isMaximized: false, isFullScreen: true });
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(
|
||||
(fixture.nativeElement as HTMLElement).classList.contains(
|
||||
'is-hidden'
|
||||
)
|
||||
).toBe(true);
|
||||
|
||||
stateChangeCallback?.({ isMaximized: false, isFullScreen: false });
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(
|
||||
(fixture.nativeElement as HTMLElement).classList.contains(
|
||||
'is-hidden'
|
||||
)
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('unsubscribes from window state changes on destroy', () => {
|
||||
fixture.destroy();
|
||||
expect(unsubscribe).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,126 @@
|
||||
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.
|
||||
}
|
||||
}
|
||||
}
|
||||
+3
-2
@@ -89,7 +89,7 @@
|
||||
"angularx-qrcode": "21.0.4",
|
||||
"artplayer": "5.3.0",
|
||||
"axios": "1.15.2",
|
||||
"better-sqlite3": "^12.5.0",
|
||||
"better-sqlite3": "12.9.0",
|
||||
"date-fns": "4.1.0",
|
||||
"drizzle-orm": "0.45.2",
|
||||
"electron-conf": "1.3.0",
|
||||
@@ -181,7 +181,7 @@
|
||||
"conventional-changelog-cli": "5.0.0",
|
||||
"cors": "2.8.6",
|
||||
"drizzle-kit": "0.31.5",
|
||||
"electron": "^39.8.5",
|
||||
"electron": "^41.7.2",
|
||||
"electron-builder": "^26.0.12",
|
||||
"electron-playwright-helpers": "1.8.2",
|
||||
"esbuild": "0.27.0",
|
||||
@@ -229,6 +229,7 @@
|
||||
"hono@4.12.0": "4.12.14",
|
||||
"immutable@5.1.4": "5.1.5",
|
||||
"lodash-es@4.17.22": "4.18.1",
|
||||
"node-abi@3.85.0": "3.92.0",
|
||||
"node-forge@1.3.3": "1.4.0",
|
||||
"path-to-regexp@0.1.12": "0.1.13",
|
||||
"picomatch@2.3.1": "2.3.2",
|
||||
|
||||
Generated
+58
-173
@@ -17,6 +17,7 @@ overrides:
|
||||
hono@4.12.0: 4.12.14
|
||||
immutable@5.1.4: 5.1.5
|
||||
lodash-es@4.17.22: 4.18.1
|
||||
node-abi@3.85.0: 3.92.0
|
||||
node-forge@1.3.3: 1.4.0
|
||||
path-to-regexp@0.1.12: 0.1.13
|
||||
picomatch@2.3.1: 2.3.2
|
||||
@@ -104,17 +105,17 @@ importers:
|
||||
specifier: 1.15.2
|
||||
version: 1.15.2
|
||||
better-sqlite3:
|
||||
specifier: ^12.5.0
|
||||
version: 12.5.0
|
||||
specifier: 12.9.0
|
||||
version: 12.9.0
|
||||
date-fns:
|
||||
specifier: 4.1.0
|
||||
version: 4.1.0
|
||||
drizzle-orm:
|
||||
specifier: 0.45.2
|
||||
version: 0.45.2(@types/better-sqlite3@7.6.13)(better-sqlite3@12.5.0)
|
||||
version: 0.45.2(@types/better-sqlite3@7.6.13)(better-sqlite3@12.9.0)
|
||||
electron-conf:
|
||||
specifier: 1.3.0
|
||||
version: 1.3.0(electron@39.8.8)
|
||||
version: 1.3.0(electron@41.7.2)
|
||||
electron-dl:
|
||||
specifier: 4.0.0
|
||||
version: 4.0.0
|
||||
@@ -354,8 +355,8 @@ importers:
|
||||
specifier: 0.31.5
|
||||
version: 0.31.5
|
||||
electron:
|
||||
specifier: ^39.8.5
|
||||
version: 39.8.8
|
||||
specifier: ^41.7.2
|
||||
version: 41.7.2
|
||||
electron-builder:
|
||||
specifier: ^26.0.12
|
||||
version: 26.0.12(electron-builder-squirrel-windows@26.0.12)
|
||||
@@ -421,7 +422,7 @@ importers:
|
||||
version: 22.7.1(@swc-node/register@1.11.1(@swc/core@1.15.8(@swc/helpers@0.5.18))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.15.8(@swc/helpers@0.5.18))
|
||||
nx-electron:
|
||||
specifier: 22.0.0
|
||||
version: 22.0.0(patch_hash=f4bbde1778360c4c462b255bec47a337c0465d59cdcab14ecb601161f3467002)(@nx/devkit@22.7.1(nx@22.7.1(@swc-node/register@1.11.1(@swc/core@1.15.8(@swc/helpers@0.5.18))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.15.8(@swc/helpers@0.5.18))))(@nx/workspace@22.7.1(@swc-node/register@1.11.1(@swc/core@1.15.8(@swc/helpers@0.5.18))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.15.8(@swc/helpers@0.5.18)))(@swc/core@1.15.8(@swc/helpers@0.5.18))(electron-builder-squirrel-windows@26.0.12)(electron@39.8.8)(esbuild@0.27.0)(rxjs@7.8.2)(typescript@5.9.3)
|
||||
version: 22.0.0(patch_hash=f4bbde1778360c4c462b255bec47a337c0465d59cdcab14ecb601161f3467002)(@nx/devkit@22.7.1(nx@22.7.1(@swc-node/register@1.11.1(@swc/core@1.15.8(@swc/helpers@0.5.18))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.15.8(@swc/helpers@0.5.18))))(@nx/workspace@22.7.1(@swc-node/register@1.11.1(@swc/core@1.15.8(@swc/helpers@0.5.18))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.15.8(@swc/helpers@0.5.18)))(@swc/core@1.15.8(@swc/helpers@0.5.18))(electron-builder-squirrel-windows@26.0.12)(electron@41.7.2)(esbuild@0.27.0)(rxjs@7.8.2)(typescript@5.9.3)
|
||||
prettier:
|
||||
specifier: ^3.8.1
|
||||
version: 3.8.1
|
||||
@@ -1550,6 +1551,10 @@ packages:
|
||||
'@drizzle-team/brocli@0.10.2':
|
||||
resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==}
|
||||
|
||||
'@electron-internal/extract-zip@1.0.2':
|
||||
resolution: {integrity: sha512-VJuNETNPEhrmQEZezeTZO5TZMV+dobBRyJ7zHjGJWIhMS7m7W1UeClt69u4hkUxv9ZZVxuli/E9Yvc4gDNHGsg==}
|
||||
engines: {node: '>=22.12.0'}
|
||||
|
||||
'@electron/asar@3.2.18':
|
||||
resolution: {integrity: sha512-2XyvMe3N3Nrs8cV39IKELRHTYUWFKrmqqSY1U+GMlc0jvqjIVnoxhNd2H4JolWQncbJi1DCvb5TNxZuI2fEjWg==}
|
||||
engines: {node: '>=10.12.0'}
|
||||
@@ -1564,9 +1569,9 @@ packages:
|
||||
resolution: {integrity: sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==}
|
||||
hasBin: true
|
||||
|
||||
'@electron/get@2.0.3':
|
||||
resolution: {integrity: sha512-Qkzpg2s9GnVV2I2BjRksUi43U5e6+zaQMcjoJy0C+C5oxaKl+fmckGDQFtRpZpZV0NQekuZZ+tGz7EA9TVnQtQ==}
|
||||
engines: {node: '>=12'}
|
||||
'@electron/get@5.0.0':
|
||||
resolution: {integrity: sha512-pjoBpru1KdEtcExBnuHAP1cAc/5faoedw0hzJkL3o4/IJp7HNF1+fbrdxT3gMYRX2oJfvnA/WXeCTVQpYYxyJA==}
|
||||
engines: {node: '>=22.12.0'}
|
||||
|
||||
'@electron/node-gyp@https://codeload.github.com/electron/node-gyp/tar.gz/06b29aafb7708acef8b3669835c8a7857ebc92d2':
|
||||
resolution: {tarball: https://codeload.github.com/electron/node-gyp/tar.gz/06b29aafb7708acef8b3669835c8a7857ebc92d2}
|
||||
@@ -4647,8 +4652,8 @@ packages:
|
||||
'@types/node@20.19.9':
|
||||
resolution: {integrity: sha512-cuVNgarYWZqxRJDQHEB58GEONhOK79QVR/qYx4S7kcUObQvUwvFnYxJuuHUKm2aieN9X3yZB4LZsuYNU1Qphsw==}
|
||||
|
||||
'@types/node@22.19.3':
|
||||
resolution: {integrity: sha512-1N9SBnWYOJTrNZCdh/yJE+t910Y128BoyY+zBLWhL3r0TYzlTmFdXrPwHL9DyFZmlEXNQQolTZh3KHV31QDhyA==}
|
||||
'@types/node@24.13.1':
|
||||
resolution: {integrity: sha512-RSpUJGmvsJ1ZeBehQZFhIdpsz+bIpES0nIQXko4Ybq+N+kX6XvOq3Jo+iJ82FWLdblFq85AsMikd3m35jgezYg==}
|
||||
|
||||
'@types/normalize-package-data@2.4.4':
|
||||
resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==}
|
||||
@@ -4728,9 +4733,6 @@ packages:
|
||||
'@types/yargs@17.0.35':
|
||||
resolution: {integrity: sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==}
|
||||
|
||||
'@types/yauzl@2.10.3':
|
||||
resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==}
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.51.0':
|
||||
resolution: {integrity: sha512-XtssGWJvypyM2ytBnSnKtHYOGT+4ZwTnBVl36TA4nRO2f4PRNGz5/1OszHzcZCvcBMh+qb7I06uoCmLTRdR9og==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
@@ -5412,8 +5414,8 @@ packages:
|
||||
before-after-hook@4.0.0:
|
||||
resolution: {integrity: sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==}
|
||||
|
||||
better-sqlite3@12.5.0:
|
||||
resolution: {integrity: sha512-WwCZ/5Diz7rsF29o27o0Gcc1Du+l7Zsv7SYtVPG0X3G/uUI1LqdxrQI7c9Hs2FWpqXXERjW9hp6g3/tH7DlVKg==}
|
||||
better-sqlite3@12.9.0:
|
||||
resolution: {integrity: sha512-wqUv4Gm3toFpHDQmaKD4QhZm3g1DjUBI0yzS4UBl6lElUmXFYdTQmmEDpAFa5o8FiFiymURypEnfVHzILKaxqQ==}
|
||||
engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x}
|
||||
|
||||
big.js@5.2.2:
|
||||
@@ -5443,10 +5445,6 @@ packages:
|
||||
boolbase@1.0.0:
|
||||
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
|
||||
|
||||
boolean@3.2.0:
|
||||
resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==}
|
||||
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
|
||||
|
||||
bottleneck@2.19.5:
|
||||
resolution: {integrity: sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==}
|
||||
|
||||
@@ -5493,9 +5491,6 @@ packages:
|
||||
buffer-builder@0.2.0:
|
||||
resolution: {integrity: sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==}
|
||||
|
||||
buffer-crc32@0.2.13:
|
||||
resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==}
|
||||
|
||||
buffer-from@1.1.2:
|
||||
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
||||
|
||||
@@ -6529,9 +6524,9 @@ packages:
|
||||
resolution: {integrity: sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
|
||||
electron@39.8.8:
|
||||
resolution: {integrity: sha512-24MMLdwCg8xwlWzDxC1XZU2MqW0Xx2UPxt9EU15vMDoRSMHPqmi/BgRCEINXZaBLfFSeXEKGpg+QlBRdL5uwaw==}
|
||||
engines: {node: '>= 12.20.55'}
|
||||
electron@41.7.2:
|
||||
resolution: {integrity: sha512-oYbZNimoMlAy6Fp/o5x2vTggptuPsIbnPKCb6jGf1PJStXH7Gkw0MUQrBliHmDqKLW7yeE+SPsvFNILVN0ORMQ==}
|
||||
engines: {node: '>= 22.12.0'}
|
||||
hasBin: true
|
||||
|
||||
emittery@0.13.1:
|
||||
@@ -6588,6 +6583,10 @@ packages:
|
||||
resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
env-paths@3.0.0:
|
||||
resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
||||
environment@1.1.0:
|
||||
resolution: {integrity: sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -6642,9 +6641,6 @@ packages:
|
||||
resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
es6-error@4.1.1:
|
||||
resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==}
|
||||
|
||||
es6-promise@4.2.8:
|
||||
resolution: {integrity: sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==}
|
||||
|
||||
@@ -6931,11 +6927,6 @@ packages:
|
||||
extend@3.0.2:
|
||||
resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==}
|
||||
|
||||
extract-zip@2.0.1:
|
||||
resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==}
|
||||
engines: {node: '>= 10.17.0'}
|
||||
hasBin: true
|
||||
|
||||
extsprintf@1.4.1:
|
||||
resolution: {integrity: sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA==}
|
||||
engines: {'0': node >=0.6.0}
|
||||
@@ -6969,9 +6960,6 @@ packages:
|
||||
fb-watchman@2.0.2:
|
||||
resolution: {integrity: sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==}
|
||||
|
||||
fd-slicer@1.1.0:
|
||||
resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==}
|
||||
|
||||
fdir@6.5.0:
|
||||
resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==}
|
||||
engines: {node: '>=12.0.0'}
|
||||
@@ -7303,10 +7291,6 @@ packages:
|
||||
engines: {node: '>=12'}
|
||||
deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me
|
||||
|
||||
global-agent@3.0.0:
|
||||
resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==}
|
||||
engines: {node: '>=10.0'}
|
||||
|
||||
global-modules@1.0.0:
|
||||
resolution: {integrity: sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -8259,9 +8243,6 @@ packages:
|
||||
json-stable-stringify-without-jsonify@1.0.1:
|
||||
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
|
||||
|
||||
json-stringify-safe@5.0.1:
|
||||
resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==}
|
||||
|
||||
json5@1.0.2:
|
||||
resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==}
|
||||
hasBin: true
|
||||
@@ -8546,10 +8527,6 @@ packages:
|
||||
engines: {node: '>= 18'}
|
||||
hasBin: true
|
||||
|
||||
matcher@3.0.0:
|
||||
resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
material-design-icons-iconfont@6.7.0:
|
||||
resolution: {integrity: sha512-lSj71DgVv20kO0kGbs42icDzbRot61gEDBLQACzkUuznRQBUYmbxzEkGU6dNBb5fRWHMaScYlAXX96HQ4/cJWA==}
|
||||
|
||||
@@ -9028,8 +9005,8 @@ packages:
|
||||
nlcst-to-string@4.0.0:
|
||||
resolution: {integrity: sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA==}
|
||||
|
||||
node-abi@3.85.0:
|
||||
resolution: {integrity: sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==}
|
||||
node-abi@3.92.0:
|
||||
resolution: {integrity: sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
node-abort-controller@3.1.1:
|
||||
@@ -9586,9 +9563,6 @@ packages:
|
||||
resolution: {integrity: sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==}
|
||||
engines: {node: '>=12', npm: '>=6'}
|
||||
|
||||
pend@1.2.0:
|
||||
resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==}
|
||||
|
||||
piccolore@0.1.3:
|
||||
resolution: {integrity: sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw==}
|
||||
|
||||
@@ -10347,10 +10321,6 @@ packages:
|
||||
deprecated: Rimraf versions prior to v4 are no longer supported
|
||||
hasBin: true
|
||||
|
||||
roarr@2.15.4:
|
||||
resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==}
|
||||
engines: {node: '>=8.0'}
|
||||
|
||||
rolldown@1.0.0-rc.4:
|
||||
resolution: {integrity: sha512-V2tPDUrY3WSevrvU2E41ijZlpF+5PbZu4giH+VpNraaadsJGHa4fR6IFwsocVwEXDoAdIv5qgPPxgrvKAOIPtA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@@ -10602,9 +10572,6 @@ packages:
|
||||
engines: {node: '>=20.8.1'}
|
||||
hasBin: true
|
||||
|
||||
semver-compare@1.0.0:
|
||||
resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==}
|
||||
|
||||
semver-diff@5.0.0:
|
||||
resolution: {integrity: sha512-0HbGtOm+S7T6NGQ/pxJSJipJvc4DK3FcRVMRkhsIwJDJ4Jcz5DQC1cPPzB5GhzyHjwttW878HaWQq46CkL3cqg==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -10645,10 +10612,6 @@ packages:
|
||||
resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
serialize-error@7.0.1:
|
||||
resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
serialize-javascript@6.0.2:
|
||||
resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==}
|
||||
|
||||
@@ -10884,9 +10847,6 @@ packages:
|
||||
sprintf-js@1.0.3:
|
||||
resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
|
||||
|
||||
sprintf-js@1.1.3:
|
||||
resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==}
|
||||
|
||||
ssri@13.0.1:
|
||||
resolution: {integrity: sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ==}
|
||||
engines: {node: ^20.17.0 || >=22.9.0}
|
||||
@@ -11368,10 +11328,6 @@ packages:
|
||||
resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
type-fest@0.13.1:
|
||||
resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
type-fest@0.20.2:
|
||||
resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -11452,6 +11408,9 @@ packages:
|
||||
undici-types@6.21.0:
|
||||
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
||||
|
||||
undici-types@7.18.2:
|
||||
resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==}
|
||||
|
||||
undici@7.24.4:
|
||||
resolution: {integrity: sha512-BM/JzwwaRXxrLdElV2Uo6cTLEjhSb3WXboncJamZ15NgUURmvlXvxa6xkwIOILIjPNo9i8ku136ZvWV0Uly8+w==}
|
||||
engines: {node: '>=20.18.1'}
|
||||
@@ -12137,9 +12096,6 @@ packages:
|
||||
resolution: {integrity: sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==}
|
||||
engines: {node: ^20.19.0 || ^22.12.0 || >=23}
|
||||
|
||||
yauzl@2.10.0:
|
||||
resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==}
|
||||
|
||||
yn@3.1.1:
|
||||
resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -13737,6 +13693,8 @@ snapshots:
|
||||
|
||||
'@drizzle-team/brocli@0.10.2': {}
|
||||
|
||||
'@electron-internal/extract-zip@1.0.2': {}
|
||||
|
||||
'@electron/asar@3.2.18':
|
||||
dependencies:
|
||||
commander: 5.1.0
|
||||
@@ -13755,17 +13713,16 @@ snapshots:
|
||||
fs-extra: 9.1.0
|
||||
minimist: 1.2.8
|
||||
|
||||
'@electron/get@2.0.3':
|
||||
'@electron/get@5.0.0':
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
env-paths: 2.2.1
|
||||
fs-extra: 8.1.0
|
||||
got: 11.8.6
|
||||
env-paths: 3.0.0
|
||||
graceful-fs: 4.2.11
|
||||
progress: 2.0.3
|
||||
semver: 6.3.1
|
||||
semver: 7.7.3
|
||||
sumchecker: 3.0.1
|
||||
optionalDependencies:
|
||||
global-agent: 3.0.0
|
||||
undici: 7.24.7
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -13813,7 +13770,7 @@ snapshots:
|
||||
detect-libc: 2.1.2
|
||||
fs-extra: 10.1.0
|
||||
got: 11.8.6
|
||||
node-abi: 3.85.0
|
||||
node-abi: 3.92.0
|
||||
node-api-version: 0.2.1
|
||||
ora: 5.4.1
|
||||
read-binary-file-arch: 1.0.6
|
||||
@@ -16967,9 +16924,9 @@ snapshots:
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
|
||||
'@types/node@22.19.3':
|
||||
'@types/node@24.13.1':
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
undici-types: 7.18.2
|
||||
|
||||
'@types/normalize-package-data@2.4.4': {}
|
||||
|
||||
@@ -17052,11 +17009,6 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/yargs-parser': 21.0.3
|
||||
|
||||
'@types/yauzl@2.10.3':
|
||||
dependencies:
|
||||
'@types/node': 20.19.9
|
||||
optional: true
|
||||
|
||||
'@typescript-eslint/eslint-plugin@8.51.0(@typescript-eslint/parser@8.51.0(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3))(eslint@9.39.2(jiti@1.21.7))(typescript@5.9.3)':
|
||||
dependencies:
|
||||
'@eslint-community/regexpp': 4.12.2
|
||||
@@ -18008,7 +17960,7 @@ snapshots:
|
||||
|
||||
before-after-hook@4.0.0: {}
|
||||
|
||||
better-sqlite3@12.5.0:
|
||||
better-sqlite3@12.9.0:
|
||||
dependencies:
|
||||
bindings: 1.5.0
|
||||
prebuild-install: 7.1.3
|
||||
@@ -18065,9 +18017,6 @@ snapshots:
|
||||
|
||||
boolbase@1.0.0: {}
|
||||
|
||||
boolean@3.2.0:
|
||||
optional: true
|
||||
|
||||
bottleneck@2.19.5: {}
|
||||
|
||||
boxen@8.0.1:
|
||||
@@ -18126,8 +18075,6 @@ snapshots:
|
||||
|
||||
buffer-builder@0.2.0: {}
|
||||
|
||||
buffer-crc32@0.2.13: {}
|
||||
|
||||
buffer-from@1.1.2: {}
|
||||
|
||||
buffer@5.7.1:
|
||||
@@ -19051,10 +18998,10 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
drizzle-orm@0.45.2(@types/better-sqlite3@7.6.13)(better-sqlite3@12.5.0):
|
||||
drizzle-orm@0.45.2(@types/better-sqlite3@7.6.13)(better-sqlite3@12.9.0):
|
||||
optionalDependencies:
|
||||
'@types/better-sqlite3': 7.6.13
|
||||
better-sqlite3: 12.5.0
|
||||
better-sqlite3: 12.9.0
|
||||
|
||||
dset@3.1.4: {}
|
||||
|
||||
@@ -19105,10 +19052,10 @@ snapshots:
|
||||
- electron-builder-squirrel-windows
|
||||
- supports-color
|
||||
|
||||
electron-conf@1.3.0(electron@39.8.8):
|
||||
electron-conf@1.3.0(electron@41.7.2):
|
||||
dependencies:
|
||||
ajv: 8.17.1
|
||||
electron: 39.8.8
|
||||
electron: 41.7.2
|
||||
|
||||
electron-dl@4.0.0:
|
||||
dependencies:
|
||||
@@ -19149,11 +19096,11 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
electron@39.8.8:
|
||||
electron@41.7.2:
|
||||
dependencies:
|
||||
'@electron/get': 2.0.3
|
||||
'@types/node': 22.19.3
|
||||
extract-zip: 2.0.1
|
||||
'@electron-internal/extract-zip': 1.0.2
|
||||
'@electron/get': 5.0.0
|
||||
'@types/node': 24.13.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -19199,6 +19146,8 @@ snapshots:
|
||||
|
||||
env-paths@2.2.1: {}
|
||||
|
||||
env-paths@3.0.0: {}
|
||||
|
||||
environment@1.1.0: {}
|
||||
|
||||
epg-parser@0.1.6:
|
||||
@@ -19306,9 +19255,6 @@ snapshots:
|
||||
is-date-object: 1.1.0
|
||||
is-symbol: 1.1.1
|
||||
|
||||
es6-error@4.1.1:
|
||||
optional: true
|
||||
|
||||
es6-promise@4.2.8: {}
|
||||
|
||||
esast-util-from-estree@2.0.0:
|
||||
@@ -19835,16 +19781,6 @@ snapshots:
|
||||
|
||||
extend@3.0.2: {}
|
||||
|
||||
extract-zip@2.0.1:
|
||||
dependencies:
|
||||
debug: 4.4.3
|
||||
get-stream: 5.2.0
|
||||
yauzl: 2.10.0
|
||||
optionalDependencies:
|
||||
'@types/yauzl': 2.10.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
extsprintf@1.4.1:
|
||||
optional: true
|
||||
|
||||
@@ -19878,10 +19814,6 @@ snapshots:
|
||||
dependencies:
|
||||
bser: 2.1.1
|
||||
|
||||
fd-slicer@1.1.0:
|
||||
dependencies:
|
||||
pend: 1.2.0
|
||||
|
||||
fdir@6.5.0(picomatch@4.0.3):
|
||||
optionalDependencies:
|
||||
picomatch: 4.0.3
|
||||
@@ -20256,16 +20188,6 @@ snapshots:
|
||||
minimatch: 5.1.9
|
||||
once: 1.4.0
|
||||
|
||||
global-agent@3.0.0:
|
||||
dependencies:
|
||||
boolean: 3.2.0
|
||||
es6-error: 4.1.1
|
||||
matcher: 3.0.0
|
||||
roarr: 2.15.4
|
||||
semver: 7.7.3
|
||||
serialize-error: 7.0.1
|
||||
optional: true
|
||||
|
||||
global-modules@1.0.0:
|
||||
dependencies:
|
||||
global-prefix: 1.0.2
|
||||
@@ -21522,9 +21444,6 @@ snapshots:
|
||||
|
||||
json-stable-stringify-without-jsonify@1.0.1: {}
|
||||
|
||||
json-stringify-safe@5.0.1:
|
||||
optional: true
|
||||
|
||||
json5@1.0.2:
|
||||
dependencies:
|
||||
minimist: 1.2.8
|
||||
@@ -21877,11 +21796,6 @@ snapshots:
|
||||
|
||||
marked@15.0.12: {}
|
||||
|
||||
matcher@3.0.0:
|
||||
dependencies:
|
||||
escape-string-regexp: 4.0.0
|
||||
optional: true
|
||||
|
||||
material-design-icons-iconfont@6.7.0: {}
|
||||
|
||||
math-intrinsics@1.1.0: {}
|
||||
@@ -22604,7 +22518,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@types/nlcst': 2.0.3
|
||||
|
||||
node-abi@3.85.0:
|
||||
node-abi@3.92.0:
|
||||
dependencies:
|
||||
semver: 7.7.3
|
||||
|
||||
@@ -22757,12 +22671,12 @@ snapshots:
|
||||
|
||||
nwsapi@2.2.23: {}
|
||||
|
||||
nx-electron@22.0.0(patch_hash=f4bbde1778360c4c462b255bec47a337c0465d59cdcab14ecb601161f3467002)(@nx/devkit@22.7.1(nx@22.7.1(@swc-node/register@1.11.1(@swc/core@1.15.8(@swc/helpers@0.5.18))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.15.8(@swc/helpers@0.5.18))))(@nx/workspace@22.7.1(@swc-node/register@1.11.1(@swc/core@1.15.8(@swc/helpers@0.5.18))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.15.8(@swc/helpers@0.5.18)))(@swc/core@1.15.8(@swc/helpers@0.5.18))(electron-builder-squirrel-windows@26.0.12)(electron@39.8.8)(esbuild@0.27.0)(rxjs@7.8.2)(typescript@5.9.3):
|
||||
nx-electron@22.0.0(patch_hash=f4bbde1778360c4c462b255bec47a337c0465d59cdcab14ecb601161f3467002)(@nx/devkit@22.7.1(nx@22.7.1(@swc-node/register@1.11.1(@swc/core@1.15.8(@swc/helpers@0.5.18))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.15.8(@swc/helpers@0.5.18))))(@nx/workspace@22.7.1(@swc-node/register@1.11.1(@swc/core@1.15.8(@swc/helpers@0.5.18))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.15.8(@swc/helpers@0.5.18)))(@swc/core@1.15.8(@swc/helpers@0.5.18))(electron-builder-squirrel-windows@26.0.12)(electron@41.7.2)(esbuild@0.27.0)(rxjs@7.8.2)(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@nx/devkit': 22.7.1(nx@22.7.1(@swc-node/register@1.11.1(@swc/core@1.15.8(@swc/helpers@0.5.18))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.15.8(@swc/helpers@0.5.18)))
|
||||
'@nx/workspace': 22.7.1(@swc-node/register@1.11.1(@swc/core@1.15.8(@swc/helpers@0.5.18))(@swc/types@0.1.25)(typescript@5.9.3))(@swc/core@1.15.8(@swc/helpers@0.5.18))
|
||||
copy-webpack-plugin: 12.0.2(webpack@5.104.1(@swc/core@1.15.8(@swc/helpers@0.5.18))(esbuild@0.27.0))
|
||||
electron: 39.8.8
|
||||
electron: 41.7.2
|
||||
electron-builder: 26.0.12(electron-builder-squirrel-windows@26.0.12)
|
||||
fork-ts-checker-webpack-plugin: 9.1.0(typescript@5.9.3)(webpack@5.104.1(@swc/core@1.15.8(@swc/helpers@0.5.18))(esbuild@0.27.0))
|
||||
license-webpack-plugin: 4.0.2(webpack@5.104.1(@swc/core@1.15.8(@swc/helpers@0.5.18))(esbuild@0.27.0))
|
||||
@@ -23298,8 +23212,6 @@ snapshots:
|
||||
|
||||
pe-library@0.4.1: {}
|
||||
|
||||
pend@1.2.0: {}
|
||||
|
||||
piccolore@0.1.3: {}
|
||||
|
||||
picocolors@1.1.1: {}
|
||||
@@ -23646,7 +23558,7 @@ snapshots:
|
||||
minimist: 1.2.8
|
||||
mkdirp-classic: 0.5.3
|
||||
napi-build-utils: 2.0.0
|
||||
node-abi: 3.85.0
|
||||
node-abi: 3.92.0
|
||||
pump: 3.0.3
|
||||
rc: 1.2.8
|
||||
simple-get: 4.0.1
|
||||
@@ -24087,16 +23999,6 @@ snapshots:
|
||||
dependencies:
|
||||
glob: 7.2.3
|
||||
|
||||
roarr@2.15.4:
|
||||
dependencies:
|
||||
boolean: 3.2.0
|
||||
detect-node: 2.1.0
|
||||
globalthis: 1.0.4
|
||||
json-stringify-safe: 5.0.1
|
||||
semver-compare: 1.0.0
|
||||
sprintf-js: 1.1.3
|
||||
optional: true
|
||||
|
||||
rolldown@1.0.0-rc.4:
|
||||
dependencies:
|
||||
'@oxc-project/types': 0.113.0
|
||||
@@ -24397,9 +24299,6 @@ snapshots:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
semver-compare@1.0.0:
|
||||
optional: true
|
||||
|
||||
semver-diff@5.0.0:
|
||||
dependencies:
|
||||
semver: 7.7.3
|
||||
@@ -24450,11 +24349,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
serialize-error@7.0.1:
|
||||
dependencies:
|
||||
type-fest: 0.13.1
|
||||
optional: true
|
||||
|
||||
serialize-javascript@6.0.2:
|
||||
dependencies:
|
||||
randombytes: 2.1.0
|
||||
@@ -24791,9 +24685,6 @@ snapshots:
|
||||
|
||||
sprintf-js@1.0.3: {}
|
||||
|
||||
sprintf-js@1.1.3:
|
||||
optional: true
|
||||
|
||||
ssri@13.0.1:
|
||||
dependencies:
|
||||
minipass: 7.1.3
|
||||
@@ -25330,9 +25221,6 @@ snapshots:
|
||||
|
||||
type-detect@4.0.8: {}
|
||||
|
||||
type-fest@0.13.1:
|
||||
optional: true
|
||||
|
||||
type-fest@0.20.2: {}
|
||||
|
||||
type-fest@0.21.3: {}
|
||||
@@ -25420,6 +25308,8 @@ snapshots:
|
||||
|
||||
undici-types@6.21.0: {}
|
||||
|
||||
undici-types@7.18.2: {}
|
||||
|
||||
undici@7.24.4: {}
|
||||
|
||||
undici@7.24.7: {}
|
||||
@@ -26097,11 +25987,6 @@ snapshots:
|
||||
y18n: 5.0.8
|
||||
yargs-parser: 22.0.0
|
||||
|
||||
yauzl@2.10.0:
|
||||
dependencies:
|
||||
buffer-crc32: 0.2.13
|
||||
fd-slicer: 1.1.0
|
||||
|
||||
yn@3.1.1: {}
|
||||
|
||||
yocto-queue@0.1.0: {}
|
||||
|
||||
Reference in new issue
Block a user