Skip to content

Commit e12d480

Browse files
Coordinate multi-monitor break starts
1 parent b9c052f commit e12d480

File tree

7 files changed

+96
-52
lines changed

7 files changed

+96
-52
lines changed

CLAUDE.md

Lines changed: 46 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -4,34 +4,34 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
44

55
## Project Overview
66

7-
BreakTimer is a cross-platform desktop application built with Electron that helps users manage periodic breaks. The app runs as a system tray application and can display break reminders as notifications or fullscreen overlays.
7+
Cross-platform Electron desktop app for managing periodic breaks. Runs as system tray application with notifications and fullscreen overlays.
88

99
## Architecture
1010

1111
### Main Process (`app/main/`)
1212

13-
- **Entry Point**: `index.ts` - Main application entry point
14-
- **Core Logic**: `lib/` directory contains the main business logic:
13+
- **Entry Point**: `index.ts`
14+
- **Core Logic**: `lib/` directory:
1515
- `breaks.ts` - Break scheduling and management
1616
- `ipc.ts` - Inter-process communication handlers
17-
- `store.ts` - Settings persistence using electron-store
17+
- `store.ts` - Settings persistence
1818
- `tray.ts` - System tray integration
1919
- `windows.ts` - Window management
20-
- `notifications.ts` - Native notification system
21-
- `auto-launch.ts` - Auto-startup functionality
20+
- `notifications.ts` - Native notifications
21+
- `auto-launch.ts` - Auto-startup
2222

2323
### Renderer Process (`app/renderer/`)
2424

25-
- **Entry Point**: `index.tsx` - React application entry point
26-
- **Components**: React components for UI (Break, Settings, etc.)
27-
- **Styling**: CSS with Tailwind CSS for component styling
28-
- **Sounds**: Audio files for break notifications
29-
- **Preload**: `preload.js` - Secure context bridge for IPC
30-
- **Fonts**: Inter font bundled locally in `public/fonts/`
25+
- **Entry Point**: `index.tsx`
26+
- **Components**: React components (Break, Settings, etc.)
27+
- **Styling**: CSS with Tailwind CSS
28+
- **Sounds**: Audio files for notifications
29+
- **Preload**: `preload.js` - Secure IPC bridge
30+
- **Fonts**: Inter font bundled in `public/fonts/`
3131

3232
### Types (`app/types/`)
3333

34-
- Shared TypeScript type definitions for IPC, settings, and breaks
34+
- Shared TypeScript definitions for IPC, settings, breaks
3535

3636
## Common Development Commands
3737

@@ -99,7 +99,7 @@ npm run package-linux # Package for Linux
9999

100100
## IPC Communication
101101

102-
The app uses a typed IPC system defined in `app/types/ipc.ts`. Main process handlers are in `app/main/lib/ipc.ts`, providing secure communication between main and renderer processes for:
102+
Typed IPC system in `app/types/ipc.ts` with handlers in `app/main/lib/ipc.ts`:
103103

104104
- Settings management
105105
- Break control
@@ -108,7 +108,7 @@ The app uses a typed IPC system defined in `app/types/ipc.ts`. Main process hand
108108

109109
## Settings Architecture
110110

111-
Settings are managed through electron-store with TypeScript interfaces defined in `app/types/settings.ts`. The store persists user preferences including:
111+
electron-store with TypeScript interfaces in `app/types/settings.ts`:
112112

113113
- Break intervals and duration
114114
- Working hours
@@ -117,44 +117,51 @@ Settings are managed through electron-store with TypeScript interfaces defined i
117117

118118
## Build System
119119

120-
- **Main Process**: Uses Webpack 5 for building the Electron main process
121-
- **Renderer Process**: Uses Vite for fast development and optimized production builds
122-
- Babel for transpiliation with TypeScript support
123-
- React Fast Refresh for hot module replacement (replaced react-hot-loader)
124-
- Production builds are optimized and minified
125-
- TypeScript configured with `skipLibCheck: true` to avoid node_modules type checking
120+
- **Main Process**: Webpack 5
121+
- **Renderer Process**: Vite
122+
- Babel + TypeScript
123+
- React Fast Refresh
124+
- TypeScript `skipLibCheck: true`
126125

127126
## Recent Updates
128127

129128
### UI and Styling
130129

131-
- **Migrated to shadcn/ui** - Modern component library with Radix UI primitives
132-
- **Added Tailwind CSS** - Utility-first CSS framework for consistent styling
133-
- **Bundled Inter font locally** - High-quality typography with WOFF2 format for offline use
134-
- **Enhanced button interactions** - Improved hover/active states for better UX
130+
- **shadcn/ui** - Modern component library with Radix UI primitives
131+
- **Tailwind CSS** - Utility-first CSS framework
132+
- **Inter font** - Bundled locally (WOFF2)
133+
- **Enhanced button interactions** - Improved hover/active states
135134

136135
### Animation System
137136

138-
- **Replaced react-spring with framer-motion** for better performance and modern API
139-
- Break window animations now use `motion.div` components
140-
- **Smooth progress animations** - 50ms updates for notification progress, 100ms for break window
141-
- Fixed infinite loop issues by using functional state updates in useEffect hooks
137+
- **framer-motion** - Replaced react-spring
138+
- `motion.div` components for break windows
139+
- **Smooth progress** - 50ms notification, 100ms break window updates
140+
- Fixed infinite loops with functional state updates
142141

143142
### Window Management
144143

145-
- **Dynamic notification sizing** - Window width adjusts based on enabled buttons (450px-550px)
146-
- **Circular progress border** - Start button shows countdown progress around its border
147-
- Improved timer cleanup to prevent flickering during hot reloads
144+
- **Dynamic notification sizing** - 450px-550px based on enabled buttons
145+
- **Circular progress border** - Around Start button
146+
- **Multi-screen sync** - Break starts synchronized across displays
147+
- Timer cleanup prevents hot reload flickering
148148

149149
### Dependencies
150150

151-
- **React 19** with new JSX transform (`"jsx": "react-jsx"`)
152-
- **TypeScript 5.8** with improved type checking
153-
- **Framer Motion** for smooth animations
154-
- **Vite** for fast development builds
151+
- **React 19** - New JSX transform
152+
- **TypeScript 5.8**
153+
- **Framer Motion**
154+
- **Vite**
155155

156156
### Development Experience
157157

158-
- Fixed tsconfig.json to only check app code, not node_modules
159-
- Updated script names (`npm run format` instead of `prettier-fix`)
160-
- Improved build configuration for better hot reloading
158+
- tsconfig.json excludes node_modules
159+
- Improved hot reloading
160+
161+
## Development Workflow
162+
163+
**IMPORTANT**: Always run after non-trivial changes:
164+
165+
```bash
166+
npm run format && npm run lint && npm run typecheck
167+
```

app/global.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@ declare const ipcRenderer: {
1111
invokeWasStartedFromTray: () => Promise<boolean>;
1212
invokeGetAppInitialized: () => Promise<boolean>;
1313
invokeSetAppInitialized: () => Promise<void>;
14+
invokeBreakStart: () => Promise<void>;
1415
onPlayEndSound: (
1516
cb: (type: string, volume?: number) => void,
1617
) => Promise<void>;
1718
onPlayStartSound: (
1819
cb: (type: string, volume?: number) => void,
1920
) => Promise<void>;
21+
onBreakStart: (cb: (breakEndTime: number) => void) => void;
2022
};
2123

2224
declare const processEnv: {

app/main/lib/ipc.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,14 @@ ipcMain.handle(
4545
},
4646
);
4747

48+
ipcMain.handle(IpcChannel.BreakStart, (): void => {
49+
log.info(IpcChannel.BreakStart);
50+
// Send break end time so all windows sync their progress to the same timeline
51+
const breakLengthMs = getBreakLengthSeconds() * 1000;
52+
const breakEndTime = Date.now() + breakLengthMs;
53+
sendIpc(IpcChannel.BreakStart, breakEndTime);
54+
});
55+
4856
ipcMain.handle(
4957
IpcChannel.SoundStartPlay,
5058
(event: IpcMainInvokeEvent, type: SoundType, volume: number = 1): void => {

app/renderer/components/break.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ export default function Break() {
1414
);
1515
const [ready, setReady] = useState(false);
1616
const [closing, setClosing] = useState(false);
17+
const [sharedBreakEndTime, setSharedBreakEndTime] = useState<number | null>(
18+
null,
19+
);
1720

1821
useEffect(() => {
1922
const init = async () => {
@@ -37,6 +40,14 @@ export default function Break() {
3740
setReady(true);
3841
};
3942

43+
// Listen for break start broadcasts from other windows
44+
const handleBreakStart = (breakEndTime: number) => {
45+
setSharedBreakEndTime(breakEndTime);
46+
setCountingDown(false);
47+
};
48+
49+
ipcRenderer.onBreakStart(handleBreakStart);
50+
4051
// Delay or the window displays incorrectly.
4152
// FIXME: work out why and how to avoid this.
4253
setTimeout(init, 1000);
@@ -46,8 +57,8 @@ export default function Break() {
4657
setCountingDown(false);
4758
}, []);
4859

49-
const handleStartBreakNow = useCallback(() => {
50-
setCountingDown(false);
60+
const handleStartBreakNow = useCallback(async () => {
61+
await ipcRenderer.invokeBreakStart();
5162
}, []);
5263

5364
useEffect(() => {
@@ -168,6 +179,7 @@ export default function Break() {
168179
settings={settings}
169180
textColor={settings.textColor}
170181
isClosing={closing}
182+
sharedBreakEndTime={sharedBreakEndTime}
171183
/>
172184
)}
173185
</motion.div>

app/renderer/components/break/break-progress.tsx

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ interface BreakProgressProps {
1313
settings: Settings;
1414
textColor: string;
1515
isClosing?: boolean;
16+
sharedBreakEndTime?: number | null;
1617
}
1718

1819
export function BreakProgress({
@@ -23,6 +24,7 @@ export function BreakProgress({
2324
settings,
2425
textColor,
2526
isClosing = false,
27+
sharedBreakEndTime = null,
2628
}: BreakProgressProps) {
2729
const [timeRemaining, setTimeRemaining] = useState<TimeRemaining | null>(
2830
null,
@@ -56,8 +58,14 @@ export function BreakProgress({
5658
}
5759

5860
(async () => {
59-
const lengthSeconds = await ipcRenderer.invokeGetBreakLength();
60-
const breakEndTime = moment().add(lengthSeconds, "seconds");
61+
// Use shared end time if available (from synchronized break start), otherwise calculate it
62+
let breakEndTime: moment.Moment;
63+
if (sharedBreakEndTime) {
64+
breakEndTime = moment(sharedBreakEndTime);
65+
} else {
66+
const lengthSeconds = await ipcRenderer.invokeGetBreakLength();
67+
breakEndTime = moment().add(lengthSeconds, "seconds");
68+
}
6169

6270
const startMsRemaining = moment(breakEndTime).diff(
6371
moment(),
@@ -73,14 +81,6 @@ export function BreakProgress({
7381
const breakDurationMs =
7482
new Date().getTime() - breakStartTime.getTime();
7583
ipcRenderer.invokeCompleteBreakTracking(breakDurationMs);
76-
77-
// Play end sound
78-
if (settings.soundType !== SoundType.None) {
79-
ipcRenderer.invokeEndSound(
80-
settings.soundType,
81-
settings.breakSoundVolume,
82-
);
83-
}
8484
}
8585

8686
onEndBreak();
@@ -108,7 +108,13 @@ export function BreakProgress({
108108
clearTimeout(timeoutId);
109109
}
110110
};
111-
}, [onEndBreak, settings, breakStartTime, isPrimaryWindow]);
111+
}, [
112+
onEndBreak,
113+
settings,
114+
breakStartTime,
115+
isPrimaryWindow,
116+
sharedBreakEndTime,
117+
]);
112118

113119
const fadeIn = {
114120
initial: { opacity: 0 },

app/renderer/preload.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ process.once("loaded", () => {
5050
invokeSetAppInitialized: () => {
5151
return ipcRenderer.invoke("APP_INITIALIZED_SET");
5252
},
53+
invokeBreakStart: () => {
54+
return ipcRenderer.invoke("BREAK_START");
55+
},
5356
onPlayStartSound: (cb) => {
5457
ipcRenderer.on("SOUND_START_PLAY", (_event, type, volume = 1) => {
5558
cb(type, volume);
@@ -60,5 +63,10 @@ process.once("loaded", () => {
6063
cb(type, volume);
6164
});
6265
},
66+
onBreakStart: (cb) => {
67+
ipcRenderer.on("BREAK_START", (_event, breakEndTime) => {
68+
cb(breakEndTime);
69+
});
70+
},
6371
});
6472
});

app/types/ipc.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ export enum IpcChannel {
44
AppInitializedGet = "APP_INITIALIZED_GET",
55
BreakLengthGet = "BREAK_LENGTH_GET",
66
BreakPostpone = "BREAK_POSTPONE",
7+
BreakStart = "BREAK_START",
78
BreakWindowResize = "BREAK_WINDOW_RESIZE",
89
BreakTrackingComplete = "BREAK_TRACKING_COMPLETE",
910
Error = "ERROR",

0 commit comments

Comments
 (0)