Skip to content

Commit b332900

Browse files
committed
feat(navigation): add navigate event to IPCEvents and handle navigation in main process
feat(auth): update logo path in LoginForm to use BASE_URL feat(home): enhance capture handling with loading state and user-friendly error messages feat(settings): add error handling for cloud save settings with user feedback release:patch
1 parent 93d8572 commit b332900

5 files changed

Lines changed: 85 additions & 10 deletions

File tree

apps/desktop/src/preload/api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,7 @@ export interface IPCEvents {
308308
'recording:space-warning': { availableGb: number };
309309
'tray:end-session': undefined;
310310
'tray:toggle-pause': undefined;
311+
navigate: string;
311312
}
312313

313314
// Type helpers for the API

apps/desktop/src/renderer/components/auth/LoginForm.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ export function LoginForm() {
6464
<Card className="w-full max-w-md border-border">
6565
<CardHeader className="space-y-1 text-center">
6666
<div className="mb-4 flex justify-center">
67-
<img src="/logo.svg" alt="PairUX" className="h-12 w-12" />
67+
<img src={`${import.meta.env.BASE_URL}logo.svg`} alt="PairUX" className="h-12 w-12" />
6868
</div>
6969
<CardTitle className="text-2xl font-semibold">Welcome back</CardTitle>
7070
<CardDescription>Sign in to your PairUX account</CardDescription>

apps/desktop/src/renderer/main.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,17 @@ import React from 'react';
22
import ReactDOM from 'react-dom/client';
33
import { RouterProvider } from 'react-router-dom';
44
import { router } from './routes';
5+
import { getElectronAPI, isElectron } from './lib/ipc';
56
import './styles/globals.css';
67

8+
// Listen for navigation events from main process (menu, tray, etc.)
9+
if (isElectron()) {
10+
const api = getElectronAPI();
11+
api.on('navigate', (path) => {
12+
void router.navigate(path);
13+
});
14+
}
15+
716
const rootElement = document.getElementById('root');
817
if (rootElement) {
918
ReactDOM.createRoot(rootElement).render(

apps/desktop/src/renderer/routes/home.tsx

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useState, useEffect } from 'react';
22
import { useNavigate } from 'react-router-dom';
3-
import { Users, Link2 } from 'lucide-react';
3+
import { Users, Link2, Loader2 } from 'lucide-react';
44
import { SourcePicker } from '@/components/capture/SourcePicker';
55
import { CapturePreview } from '@/components/capture/CapturePreview';
66
import { CreateLinkModal } from '@/components/CreateLinkModal';
@@ -17,6 +17,7 @@ export function HomePage() {
1717
const [error, setError] = useState<string | null>(null);
1818
const [displayServer, setDisplayServer] = useState<DisplayServer>('x11');
1919
const [isWayland, setIsWayland] = useState(false);
20+
const [isCapturing, setIsCapturing] = useState(false);
2021
const [showCreateLinkModal, setShowCreateLinkModal] = useState(false);
2122
const [preCreatedSession, setPreCreatedSession] = useState<Session | null>(null);
2223

@@ -31,8 +32,12 @@ export function HomePage() {
3132
}, []);
3233

3334
const handleSourceSelect = async (source: CaptureSource) => {
35+
// Prevent multiple concurrent capture attempts
36+
if (isCapturing) return;
37+
3438
setSelectedSource(source);
3539
setError(null);
40+
setIsCapturing(true);
3641

3742
try {
3843
// Stop existing stream
@@ -77,14 +82,24 @@ export function HomePage() {
7782
} catch (err) {
7883
console.error('[Renderer] Failed to start capture:', err);
7984
const message = err instanceof Error ? err.message : 'Unknown error';
80-
setError(`Failed to capture: ${message}`);
85+
// Provide more user-friendly error messages
86+
if (message.includes('Permission denied') || message.includes('NotAllowedError')) {
87+
setError('Screen capture was canceled or permission denied. Please try again.');
88+
} else {
89+
setError(`Failed to capture: ${message}`);
90+
}
8191
setSelectedSource(null);
92+
} finally {
93+
setIsCapturing(false);
8294
}
8395
};
8496

8597
// Handle Wayland direct capture (bypasses source picker)
8698
const handleWaylandCapture = async () => {
99+
if (isCapturing) return;
100+
87101
setError(null);
102+
setIsCapturing(true);
88103

89104
try {
90105
if (stream) {
@@ -116,7 +131,13 @@ export function HomePage() {
116131
} catch (err) {
117132
console.error('[Renderer] Failed to start Wayland capture:', err);
118133
const message = err instanceof Error ? err.message : 'Unknown error';
119-
setError(`Failed to capture: ${message}`);
134+
if (message.includes('Permission denied') || message.includes('NotAllowedError')) {
135+
setError('Screen capture was canceled or permission denied. Please try again.');
136+
} else {
137+
setError(`Failed to capture: ${message}`);
138+
}
139+
} finally {
140+
setIsCapturing(false);
120141
}
121142
};
122143

@@ -147,22 +168,36 @@ export function HomePage() {
147168
)}
148169

149170
{!stream ? (
150-
<>
171+
<div className="relative">
172+
{/* Loading overlay */}
173+
{isCapturing && (
174+
<div className="absolute inset-0 z-10 flex items-center justify-center rounded-lg bg-background/80 backdrop-blur-sm">
175+
<div className="flex flex-col items-center gap-3">
176+
<Loader2 className="h-8 w-8 animate-spin text-primary" />
177+
<p className="text-sm text-muted-foreground">
178+
{isWayland ? 'Waiting for system screen picker...' : 'Starting capture...'}
179+
</p>
180+
</div>
181+
</div>
182+
)}
183+
151184
<div className="mb-6 flex items-center justify-between">
152185
<h1 className="text-2xl font-semibold">Select a screen or window to share</h1>
153186
<div className="flex items-center gap-2">
154187
<button
155188
onClick={() => {
156189
setShowCreateLinkModal(true);
157190
}}
158-
className="flex items-center gap-2 rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
191+
disabled={isCapturing}
192+
className="flex items-center gap-2 rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:opacity-50"
159193
>
160194
<Link2 className="h-4 w-4" />
161195
Create Link
162196
</button>
163197
<button
164198
onClick={() => void navigate('/join')}
165-
className="flex items-center gap-2 rounded-lg bg-muted px-4 py-2 text-sm font-medium text-foreground transition-colors hover:bg-muted/80"
199+
disabled={isCapturing}
200+
className="flex items-center gap-2 rounded-lg bg-muted px-4 py-2 text-sm font-medium text-foreground transition-colors hover:bg-muted/80 disabled:opacity-50"
166201
>
167202
<Users className="h-4 w-4" />
168203
Join a Session
@@ -180,9 +215,17 @@ export function HomePage() {
180215
onClick={() => {
181216
void handleWaylandCapture();
182217
}}
183-
className="rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90"
218+
disabled={isCapturing}
219+
className="rounded-lg bg-primary px-4 py-2 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:opacity-50"
184220
>
185-
Open System Screen Picker
221+
{isCapturing ? (
222+
<>
223+
<Loader2 className="mr-2 inline h-4 w-4 animate-spin" />
224+
Waiting...
225+
</>
226+
) : (
227+
'Open System Screen Picker'
228+
)}
186229
</button>
187230
</div>
188231
)}
@@ -192,7 +235,7 @@ export function HomePage() {
192235
void handleSourceSelect(source);
193236
}}
194237
/>
195-
</>
238+
</div>
196239
) : (
197240
<CapturePreview
198241
stream={stream}

apps/desktop/src/renderer/routes/settings.tsx

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
Cloud,
1111
Loader2,
1212
Check,
13+
AlertCircle,
1314
} from 'lucide-react';
1415
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
1516
import { useAuthStore } from '@/stores/auth';
@@ -48,6 +49,7 @@ export function SettingsPage() {
4849
} | null>(null);
4950
const [isSaving, setIsSaving] = useState(false);
5051
const [saveSuccess, setSaveSuccess] = useState(false);
52+
const [saveError, setSaveError] = useState<string | null>(null);
5153
const [hasChanges, setHasChanges] = useState(false);
5254

5355
// Load settings from localStorage on mount
@@ -74,13 +76,15 @@ export function SettingsPage() {
7476
localStorage.setItem('pairux-settings', JSON.stringify(newSettings));
7577
setHasChanges(true);
7678
setSaveSuccess(false);
79+
setSaveError(null);
7780
};
7881

7982
// Save settings to cloud
8083
const handleSaveToCloud = async () => {
8184
if (!user) return;
8285

8386
setIsSaving(true);
87+
setSaveError(null);
8488
try {
8589
const response = await fetch(`${APP_URL}/api/settings`, {
8690
method: 'PUT',
@@ -97,9 +101,11 @@ export function SettingsPage() {
97101
}, 3000);
98102
} else {
99103
console.error('Failed to save settings to cloud');
104+
setSaveError('Failed to save settings');
100105
}
101106
} catch (error) {
102107
console.error('Error saving settings:', error);
108+
setSaveError('Could not connect to server');
103109
} finally {
104110
setIsSaving(false);
105111
}
@@ -167,6 +173,22 @@ export function SettingsPage() {
167173
)}
168174
</div>
169175

176+
{/* Error message */}
177+
{saveError && (
178+
<div className="mb-6 flex max-w-3xl items-center gap-2 rounded-lg bg-destructive/10 p-3 text-sm text-destructive">
179+
<AlertCircle className="h-4 w-4 flex-shrink-0" />
180+
<span>{saveError}</span>
181+
<button
182+
onClick={() => {
183+
setSaveError(null);
184+
}}
185+
className="ml-auto text-xs underline"
186+
>
187+
Dismiss
188+
</button>
189+
</div>
190+
)}
191+
170192
<div className="grid max-w-3xl gap-6">
171193
{/* Account Section */}
172194
<Card>

0 commit comments

Comments
 (0)