Skip to content

Commit d0ef40e

Browse files
authored
Merge pull request #8 from ccofallen/feat/petdex-import-and-pet-size
sync imported pets through Electron IPC
2 parents 6978424 + 487f527 commit d0ef40e

8 files changed

Lines changed: 88 additions & 3 deletions

File tree

electron/main.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,15 @@ function registerIpcHandlers() {
370370
openPetdexWindow();
371371
});
372372

373+
ipcMain.on('pet:state-changed', (event) => {
374+
if (settingsWindow === undefined
375+
|| settingsWindow.isDestroyed()
376+
|| event.sender.id !== settingsWindow.webContents.id) return;
377+
const currentPetWindow = getCurrentPetWindow();
378+
if (currentPetWindow === undefined) return;
379+
currentPetWindow.webContents.send('pet:state-changed');
380+
});
381+
373382
ipcMain.handle('pet:manual-dock', () => {
374383
if (!EDGE_DOCKING_ENABLED) return;
375384
const currentPetWindow = getCurrentPetWindow();

electron/main.test.js

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,3 +227,26 @@ describe('Electron process lifecycle', () => {
227227
expect(settingsWindows).toHaveLength(2);
228228
});
229229
});
230+
231+
describe('renderer state synchronization', () => {
232+
it('forwards state changes from settings to the current pet window only', async () => {
233+
const openSettings = electron.ipcMain.handle.mock.calls
234+
.find(([channel]) => channel === 'pet:open-settings')[1];
235+
await openSettings();
236+
const settings = electron.windows
237+
.filter(({ options }) => options.title === 'Codex Pet Pause 设置')
238+
.at(-1);
239+
const currentPet = electron.windows
240+
.filter(({ options, destroyed }) => options.transparent === true && !destroyed)
241+
.at(-1);
242+
const forwardState = electron.ipcMain.on.mock.calls
243+
.find(([channel]) => channel === 'pet:state-changed')[1];
244+
245+
forwardState({ sender: settings.webContents });
246+
expect(currentPet.webContents.send).toHaveBeenCalledWith('pet:state-changed');
247+
248+
currentPet.webContents.send.mockClear();
249+
forwardState({ sender: currentPet.webContents });
250+
expect(currentPet.webContents.send).not.toHaveBeenCalled();
251+
});
252+
});

electron/preload.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ contextBridge.exposeInMainWorld('petShell', {
88
ipcRenderer.on('pet:petdex-import', listener);
99
return () => ipcRenderer.removeListener('pet:petdex-import', listener);
1010
},
11+
notifyStateChanged: () => ipcRenderer.send('pet:state-changed'),
12+
onStateChanged: (callback) => {
13+
const listener = () => callback();
14+
ipcRenderer.on('pet:state-changed', listener);
15+
return () => ipcRenderer.removeListener('pet:state-changed', listener);
16+
},
1117
dockNow: () => ipcRenderer.invoke('pet:manual-dock'),
1218
dragWindowTo: (x, y) => ipcRenderer.send('pet:drag-window', { x, y }),
1319
showContextMenu: (x, y) => ipcRenderer.send('pet:show-context-menu', { x, y }),

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "codex-pet-pause",
33
"private": true,
4-
"version": "0.2.5",
4+
"version": "0.2.6",
55
"description": "A playful, local-first break reminder PWA with interactive and Codex-compatible pets.",
66
"license": "MIT",
77
"repository": {
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { afterEach, describe, expect, test, vi } from 'vitest';
2+
import { createRendererSynchronization } from './rendererSynchronization';
3+
4+
describe('renderer synchronization', () => {
5+
afterEach(() => {
6+
delete window.petShell;
7+
});
8+
9+
test('uses native Electron IPC when browser channels are unreliable', () => {
10+
const notifyStateChanged = vi.fn();
11+
const unsubscribe = vi.fn();
12+
let receiveNative: (() => void) | undefined;
13+
window.petShell = {
14+
notifyStateChanged,
15+
onStateChanged(callback) {
16+
receiveNative = callback;
17+
return unsubscribe;
18+
},
19+
};
20+
const synchronization = createRendererSynchronization();
21+
const listener = vi.fn();
22+
synchronization.subscribe(listener);
23+
24+
synchronization.notify();
25+
expect(notifyStateChanged).toHaveBeenCalledOnce();
26+
receiveNative?.();
27+
expect(listener).toHaveBeenCalledOnce();
28+
29+
synchronization.close();
30+
expect(unsubscribe).toHaveBeenCalledOnce();
31+
});
32+
});

src/infrastructure/rendererSynchronization.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export function createRendererSynchronization(): RendererSynchronization {
3131
let closed = false;
3232
let lastToken: string | undefined;
3333
let channel: BroadcastChannel | undefined;
34+
let unsubscribeNative = (): void => undefined;
3435

3536
try {
3637
if (typeof globalThis.BroadcastChannel === 'function') {
@@ -57,6 +58,12 @@ export function createRendererSynchronization(): RendererSynchronization {
5758
if (isSyncMessage(event.data)) receive(event.data.token);
5859
};
5960
}
61+
try {
62+
unsubscribeNative = window.petShell?.onStateChanged?.(() => receive())
63+
?? (() => undefined);
64+
} catch {
65+
unsubscribeNative = () => undefined;
66+
}
6067

6168
return {
6269
notify(): void {
@@ -73,6 +80,11 @@ export function createRendererSynchronization(): RendererSynchronization {
7380
} catch {
7481
// Storage events remain available when BroadcastChannel delivery fails.
7582
}
83+
try {
84+
window.petShell?.notifyStateChanged?.();
85+
} catch {
86+
// Browser synchronization remains available when native IPC delivery fails.
87+
}
7688
},
7789

7890
subscribe(listener): () => void {
@@ -86,6 +98,7 @@ export function createRendererSynchronization(): RendererSynchronization {
8698
closed = true;
8799
listeners.clear();
88100
window.removeEventListener('storage', onStorage);
101+
unsubscribeNative();
89102
if (channel !== undefined) {
90103
channel.onmessage = null;
91104
channel.close();

src/types/electron-api.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ declare global {
77
| { type: 'archive'; name: string; bytes: ArrayBuffer }
88
| { type: 'error' }
99
) => void) => () => void;
10+
notifyStateChanged?: () => void;
11+
onStateChanged?: (callback: () => void) => () => void;
1012
dockNow?: () => void;
1113
dragWindowTo?: (x: number, y: number) => void;
1214
showContextMenu?: (x?: number, y?: number) => void;

0 commit comments

Comments
 (0)