mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-05-07 06:12:44 +02:00
wait on screen recording permissions
This commit is contained in:
parent
30e1785fe2
commit
d30cb88651
4 changed files with 70 additions and 33 deletions
|
|
@ -11,18 +11,18 @@ module.exports = {
|
||||||
icon: './icons/icon', // .icns extension added automatically
|
icon: './icons/icon', // .icns extension added automatically
|
||||||
appBundleId: 'com.rowboat.app',
|
appBundleId: 'com.rowboat.app',
|
||||||
appCategoryType: 'public.app-category.productivity',
|
appCategoryType: 'public.app-category.productivity',
|
||||||
osxSign: {
|
// osxSign: {
|
||||||
batchCodesignCalls: true,
|
// batchCodesignCalls: true,
|
||||||
optionsForFile: () => ({
|
// optionsForFile: () => ({
|
||||||
entitlements: path.join(__dirname, 'entitlements.plist'),
|
// entitlements: path.join(__dirname, 'entitlements.plist'),
|
||||||
'entitlements-inherit': path.join(__dirname, 'entitlements.plist'),
|
// 'entitlements-inherit': path.join(__dirname, 'entitlements.plist'),
|
||||||
}),
|
// }),
|
||||||
},
|
// },
|
||||||
osxNotarize: {
|
// osxNotarize: {
|
||||||
appleId: process.env.APPLE_ID,
|
// appleId: process.env.APPLE_ID,
|
||||||
appleIdPassword: process.env.APPLE_PASSWORD,
|
// appleIdPassword: process.env.APPLE_PASSWORD,
|
||||||
teamId: process.env.APPLE_TEAM_ID
|
// teamId: process.env.APPLE_TEAM_ID
|
||||||
},
|
// },
|
||||||
// Since we bundle everything with esbuild, we don't need node_modules at all.
|
// Since we bundle everything with esbuild, we don't need node_modules at all.
|
||||||
// These settings prevent Forge's dependency walker (flora-colossus) from trying
|
// These settings prevent Forge's dependency walker (flora-colossus) from trying
|
||||||
// to analyze/copy node_modules, which fails with pnpm's symlinked workspaces.
|
// to analyze/copy node_modules, which fails with pnpm's symlinked workspaces.
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { ipcMain, BrowserWindow, shell, dialog } from 'electron';
|
import { ipcMain, BrowserWindow, shell, dialog, systemPreferences } from 'electron';
|
||||||
import { ipc } from '@x/shared';
|
import { ipc } from '@x/shared';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
|
|
@ -719,6 +719,16 @@ export function setupIpcHandlers() {
|
||||||
|
|
||||||
return { success: false, error: 'Unknown format' };
|
return { success: false, error: 'Unknown format' };
|
||||||
},
|
},
|
||||||
|
'meeting:checkScreenPermission': async () => {
|
||||||
|
if (process.platform !== 'darwin') return { granted: true };
|
||||||
|
const status = systemPreferences.getMediaAccessStatus('screen');
|
||||||
|
console.log('[meeting] Screen recording permission status:', status);
|
||||||
|
return { granted: status === 'granted' };
|
||||||
|
},
|
||||||
|
'meeting:openScreenRecordingSettings': async () => {
|
||||||
|
await shell.openExternal('x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture');
|
||||||
|
return { success: true };
|
||||||
|
},
|
||||||
'meeting:summarize': async (_event, args) => {
|
'meeting:summarize': async (_event, args) => {
|
||||||
const notes = await summarizeMeeting(args.transcript, args.meetingStartTime, args.calendarEventJson);
|
const notes = await summarizeMeeting(args.transcript, args.meetingStartTime, args.calendarEventJson);
|
||||||
return { notes };
|
return { notes };
|
||||||
|
|
|
||||||
|
|
@ -3417,9 +3417,9 @@ function App() {
|
||||||
const [meetingSummarizing, setMeetingSummarizing] = useState(false)
|
const [meetingSummarizing, setMeetingSummarizing] = useState(false)
|
||||||
const [showMeetingPermissions, setShowMeetingPermissions] = useState(false)
|
const [showMeetingPermissions, setShowMeetingPermissions] = useState(false)
|
||||||
|
|
||||||
const startMeetingAfterPermissions = useCallback(async () => {
|
const [checkingPermission, setCheckingPermission] = useState(false)
|
||||||
setShowMeetingPermissions(false)
|
|
||||||
localStorage.setItem('meeting-permissions-acknowledged', '1')
|
const startMeetingNow = useCallback(async () => {
|
||||||
const calEvent = pendingCalendarEventRef.current
|
const calEvent = pendingCalendarEventRef.current
|
||||||
pendingCalendarEventRef.current = undefined
|
pendingCalendarEventRef.current = undefined
|
||||||
const notePath = await meetingTranscription.start(calEvent)
|
const notePath = await meetingTranscription.start(calEvent)
|
||||||
|
|
@ -3429,6 +3429,23 @@ function App() {
|
||||||
}
|
}
|
||||||
}, [meetingTranscription, handleVoiceNoteCreated])
|
}, [meetingTranscription, handleVoiceNoteCreated])
|
||||||
|
|
||||||
|
const handleCheckPermissionAndRetry = useCallback(async () => {
|
||||||
|
setCheckingPermission(true)
|
||||||
|
try {
|
||||||
|
const { granted } = await window.ipc.invoke('meeting:checkScreenPermission', null)
|
||||||
|
if (granted) {
|
||||||
|
setShowMeetingPermissions(false)
|
||||||
|
await startMeetingNow()
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setCheckingPermission(false)
|
||||||
|
}
|
||||||
|
}, [startMeetingNow])
|
||||||
|
|
||||||
|
const handleOpenScreenRecordingSettings = useCallback(async () => {
|
||||||
|
await window.ipc.invoke('meeting:openScreenRecordingSettings', null)
|
||||||
|
}, [])
|
||||||
|
|
||||||
const handleToggleMeeting = useCallback(async () => {
|
const handleToggleMeeting = useCallback(async () => {
|
||||||
if (meetingTranscription.state === 'recording') {
|
if (meetingTranscription.state === 'recording') {
|
||||||
await meetingTranscription.stop()
|
await meetingTranscription.stop()
|
||||||
|
|
@ -3477,20 +3494,18 @@ function App() {
|
||||||
meetingNotePathRef.current = null
|
meetingNotePathRef.current = null
|
||||||
}
|
}
|
||||||
} else if (meetingTranscription.state === 'idle') {
|
} else if (meetingTranscription.state === 'idle') {
|
||||||
// Show permissions modal on first use (macOS only — Windows works out of the box)
|
// On macOS, check screen recording permission before starting
|
||||||
if (isMac && !localStorage.getItem('meeting-permissions-acknowledged')) {
|
if (isMac) {
|
||||||
|
const result = await window.ipc.invoke('meeting:checkScreenPermission', null)
|
||||||
|
console.log('[meeting] Permission check result:', result)
|
||||||
|
if (!result.granted) {
|
||||||
setShowMeetingPermissions(true)
|
setShowMeetingPermissions(true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const calEvent = pendingCalendarEventRef.current
|
|
||||||
pendingCalendarEventRef.current = undefined
|
|
||||||
const notePath = await meetingTranscription.start(calEvent)
|
|
||||||
if (notePath) {
|
|
||||||
meetingNotePathRef.current = notePath
|
|
||||||
await handleVoiceNoteCreated(notePath)
|
|
||||||
}
|
}
|
||||||
|
await startMeetingNow()
|
||||||
}
|
}
|
||||||
}, [meetingTranscription, handleVoiceNoteCreated])
|
}, [meetingTranscription, handleVoiceNoteCreated, startMeetingNow])
|
||||||
handleToggleMeetingRef.current = handleToggleMeeting
|
handleToggleMeetingRef.current = handleToggleMeeting
|
||||||
|
|
||||||
// Listen for calendar block "join meeting & take notes" events
|
// Listen for calendar block "join meeting & take notes" events
|
||||||
|
|
@ -4421,23 +4436,25 @@ function App() {
|
||||||
<Dialog open={showMeetingPermissions} onOpenChange={setShowMeetingPermissions}>
|
<Dialog open={showMeetingPermissions} onOpenChange={setShowMeetingPermissions}>
|
||||||
<DialogContent showCloseButton={false}>
|
<DialogContent showCloseButton={false}>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Meeting transcription setup</DialogTitle>
|
<DialogTitle>Screen recording permission required</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Rowboat needs <strong>Screen Recording</strong> permission to capture meeting audio from other apps (Zoom, Meet, etc.).
|
Rowboat needs <strong>Screen Recording</strong> permission to capture meeting audio from other apps (Zoom, Meet, etc.). This feature won't work without it.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-3 text-sm text-muted-foreground">
|
<div className="space-y-3 text-sm text-muted-foreground">
|
||||||
<p>To enable this:</p>
|
<p>To enable this:</p>
|
||||||
<ol className="list-decimal list-inside space-y-1.5">
|
<ol className="list-decimal list-inside space-y-1.5">
|
||||||
<li>Open <strong>System Settings</strong> → <strong>Privacy & Security</strong></li>
|
<li>Open <strong>System Settings</strong> → <strong>Privacy & Security</strong> → <strong>Screen Recording</strong></li>
|
||||||
<li>Click <strong>Screen Recording</strong></li>
|
|
||||||
<li>Toggle on <strong>Rowboat</strong></li>
|
<li>Toggle on <strong>Rowboat</strong></li>
|
||||||
<li>You may need to restart the app after granting permission</li>
|
<li>You may need to restart the app after granting permission</li>
|
||||||
</ol>
|
</ol>
|
||||||
</div>
|
</div>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" onClick={() => setShowMeetingPermissions(false)}>Cancel</Button>
|
<Button variant="outline" onClick={() => setShowMeetingPermissions(false)}>Cancel</Button>
|
||||||
<Button onClick={() => { void startMeetingAfterPermissions() }}>Continue</Button>
|
<Button variant="outline" onClick={() => { void handleOpenScreenRecordingSettings() }}>Open System Settings</Button>
|
||||||
|
<Button onClick={() => { void handleCheckPermissionAndRetry() }} disabled={checkingPermission}>
|
||||||
|
{checkingPermission ? 'Checking...' : 'Check Again'}
|
||||||
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
|
||||||
|
|
@ -501,6 +501,16 @@ const ipcSchemas = {
|
||||||
mimeType: z.string(),
|
mimeType: z.string(),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
'meeting:checkScreenPermission': {
|
||||||
|
req: z.null(),
|
||||||
|
res: z.object({
|
||||||
|
granted: z.boolean(),
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
'meeting:openScreenRecordingSettings': {
|
||||||
|
req: z.null(),
|
||||||
|
res: z.object({ success: z.boolean() }),
|
||||||
|
},
|
||||||
'meeting:summarize': {
|
'meeting:summarize': {
|
||||||
req: z.object({
|
req: z.object({
|
||||||
transcript: z.string(),
|
transcript: z.string(),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue