pengx17
2025-04-08 10:18:08 +00:00
parent 780c35eabe
commit 93d8e22b07
8 changed files with 149 additions and 59 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 939 B

View File

@@ -473,7 +473,7 @@ function setupMediaListeners() {
// will be called when the app is ready or when the user has enabled the recording feature in settings // will be called when the app is ready or when the user has enabled the recording feature in settings
export function setupRecordingFeature() { export function setupRecordingFeature() {
if (!MeetingsSettingsState.value.enabled || !checkRecordingAvailable()) { if (!MeetingsSettingsState.value.enabled || !checkCanRecordMeeting()) {
return; return;
} }
@@ -767,9 +767,24 @@ export const checkRecordingAvailable = () => {
return (version.major === 14 && version.minor >= 2) || version.major > 14; return (version.major === 14 && version.minor >= 2) || version.major > 14;
}; };
export const checkScreenRecordingPermission = () => { export const checkMeetingPermissions = () => {
if (!isMacOS()) { if (!isMacOS()) {
return false; return undefined;
} }
return systemPreferences.getMediaAccessStatus('screen') === 'granted'; const mediaTypes = ['screen', 'microphone'] as const;
return Object.fromEntries(
mediaTypes.map(mediaType => [
mediaType,
systemPreferences.getMediaAccessStatus(mediaType) === 'granted',
])
) as Record<(typeof mediaTypes)[number], boolean>;
};
export const checkCanRecordMeeting = () => {
const features = checkMeetingPermissions();
return (
checkRecordingAvailable() &&
features &&
Object.values(features).every(feature => feature)
);
}; };

View File

@@ -9,8 +9,8 @@ import { shell } from 'electron';
import { isMacOS } from '../../shared/utils'; import { isMacOS } from '../../shared/utils';
import type { NamespaceHandlers } from '../type'; import type { NamespaceHandlers } from '../type';
import { import {
checkMeetingPermissions,
checkRecordingAvailable, checkRecordingAvailable,
checkScreenRecordingPermission,
disableRecordingFeature, disableRecordingFeature,
getRawAudioBuffers, getRawAudioBuffers,
getRecording, getRecording,
@@ -73,13 +73,17 @@ export const recordingHandlers = {
disableRecordingFeature: async () => { disableRecordingFeature: async () => {
return disableRecordingFeature(); return disableRecordingFeature();
}, },
checkScreenRecordingPermission: async () => { checkMeetingPermissions: async () => {
return checkScreenRecordingPermission(); return checkMeetingPermissions();
}, },
showScreenRecordingPermissionSetting: async () => { showRecordingPermissionSetting: async (_, type: 'screen' | 'microphone') => {
const urlMap = {
screen: 'Privacy_ScreenCapture',
microphone: 'Privacy_Microphone',
};
if (isMacOS()) { if (isMacOS()) {
return shell.openExternal( return shell.openExternal(
'x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture' `x-apple.systempreferences:com.apple.preference.security?${urlMap[type]}`
); );
} }
// this only available on MacOS // this only available on MacOS

View File

@@ -15,8 +15,8 @@ import { beforeAppQuit } from '../cleanup';
import { logger } from '../logger'; import { logger } from '../logger';
import { import {
appGroups$, appGroups$,
checkCanRecordMeeting,
checkRecordingAvailable, checkRecordingAvailable,
checkScreenRecordingPermission,
MeetingsSettingsState, MeetingsSettingsState,
recordingStatus$, recordingStatus$,
startRecording, startRecording,
@@ -89,7 +89,7 @@ class TrayState implements Disposable {
// tray's icon // tray's icon
icon: NativeImage = nativeImage icon: NativeImage = nativeImage
.createFromPath(icons.tray) .createFromPath(icons.tray)
.resize({ width: 16, height: 16 }); .resize({ width: 18, height: 18 });
// tray's tooltip // tray's tooltip
tooltip: string = 'AFFiNE'; tooltip: string = 'AFFiNE';
@@ -142,10 +142,17 @@ class TrayState implements Disposable {
const getConfig = () => { const getConfig = () => {
const items: TrayMenuConfig = []; const items: TrayMenuConfig = [];
if ( if (!MeetingsSettingsState.value.enabled) {
checkScreenRecordingPermission() && items.push({
MeetingsSettingsState.value.enabled label: 'Meetings are disabled',
) { disabled: true,
});
} else if (!checkCanRecordMeeting()) {
items.push({
label: 'Required permissions not granted',
disabled: true,
});
} else {
const appGroups = appGroups$.value; const appGroups = appGroups$.value;
const runningAppGroups = appGroups.filter( const runningAppGroups = appGroups.filter(
appGroup => appGroup.isRunning appGroup => appGroup.isRunning

View File

@@ -75,6 +75,61 @@ const RecordingModeMenu = () => {
); );
}; };
// Add the PermissionSettingRow component
interface PermissionSettingRowProps {
nameKey: string;
descriptionKey: string;
permissionSettingKey: string;
hasPermission: boolean;
onOpenPermissionSetting: () => void | Promise<void>;
}
const PermissionSettingRow = ({
nameKey,
descriptionKey,
permissionSettingKey,
hasPermission,
onOpenPermissionSetting,
}: PermissionSettingRowProps) => {
const t = useI18n();
const handleClick = () => {
const result = onOpenPermissionSetting();
if (result instanceof Promise) {
result.catch(error => {
console.error('Error opening permission setting:', error);
});
}
};
return (
<SettingRow
name={t[nameKey]()}
desc={
<>
{t[descriptionKey]()}
{!hasPermission && (
<span onClick={handleClick} className={styles.permissionSetting}>
{t[permissionSettingKey]()}
</span>
)}
</>
}
>
<IconButton
icon={
hasPermission ? (
<DoneIcon />
) : (
<InformationFillDuotoneIcon className={styles.noPermissionIcon} />
)
}
onClick={handleClick}
/>
</SettingRow>
);
};
export const MeetingsSettings = () => { export const MeetingsSettings = () => {
const t = useI18n(); const t = useI18n();
const meetingSettingsService = useService(MeetingSettingsService); const meetingSettingsService = useService(MeetingSettingsService);
@@ -82,8 +137,10 @@ export const MeetingsSettings = () => {
const [recordingFeatureAvailable, setRecordingFeatureAvailable] = const [recordingFeatureAvailable, setRecordingFeatureAvailable] =
useState(false); useState(false);
const [screenRecordingPermission, setScreenRecordingPermission] = const [permissions, setPermissions] = useState<{
useState(false); screen: boolean;
microphone: boolean;
}>();
const confirmModal = useConfirmModal(); const confirmModal = useConfirmModal();
@@ -97,9 +154,9 @@ export const MeetingsSettings = () => {
setRecordingFeatureAvailable(false); setRecordingFeatureAvailable(false);
}); });
meetingSettingsService meetingSettingsService
.checkScreenRecordingPermission() .checkMeetingPermissions()
.then(permission => { .then(permission => {
setScreenRecordingPermission(permission ?? false); setPermissions(permission);
}) })
.catch(err => console.log(err)); .catch(err => console.log(err));
}, [meetingSettingsService]); }, [meetingSettingsService]);
@@ -121,7 +178,9 @@ export const MeetingsSettings = () => {
'com.affine.settings.meetings.record.permission-modal.description' 'com.affine.settings.meetings.record.permission-modal.description'
](), ](),
onConfirm: async () => { onConfirm: async () => {
await meetingSettingsService.showScreenRecordingPermissionSetting(); await meetingSettingsService.showRecordingPermissionSetting(
'screen'
);
}, },
cancelText: t['com.affine.recording.dismiss'](), cancelText: t['com.affine.recording.dismiss'](),
confirmButtonOptions: { confirmButtonOptions: {
@@ -146,7 +205,12 @@ export const MeetingsSettings = () => {
const handleOpenScreenRecordingPermissionSetting = const handleOpenScreenRecordingPermissionSetting =
useAsyncCallback(async () => { useAsyncCallback(async () => {
await meetingSettingsService.showScreenRecordingPermissionSetting(); await meetingSettingsService.showRecordingPermissionSetting('screen');
}, [meetingSettingsService]);
const handleOpenMicrophoneRecordingPermissionSetting =
useAsyncCallback(async () => {
await meetingSettingsService.showRecordingPermissionSetting('microphone');
}, [meetingSettingsService]); }, [meetingSettingsService]);
const handleOpenSavedRecordings = useAsyncCallback(async () => { const handleOpenSavedRecordings = useAsyncCallback(async () => {
@@ -230,41 +294,24 @@ export const MeetingsSettings = () => {
<SettingWrapper <SettingWrapper
title={t['com.affine.settings.meetings.privacy.header']()} title={t['com.affine.settings.meetings.privacy.header']()}
> >
<SettingRow <PermissionSettingRow
name={t[ nameKey="com.affine.settings.meetings.privacy.screen-system-audio-recording"
'com.affine.settings.meetings.privacy.screen-system-audio-recording' descriptionKey="com.affine.settings.meetings.privacy.screen-system-audio-recording.description"
]()} permissionSettingKey="com.affine.settings.meetings.privacy.screen-system-audio-recording.permission-setting"
desc={ hasPermission={permissions?.screen || false}
<> onOpenPermissionSetting={
{t[ handleOpenScreenRecordingPermissionSetting
'com.affine.settings.meetings.privacy.screen-system-audio-recording.description'
]()}
{!screenRecordingPermission && (
<span
onClick={handleOpenScreenRecordingPermissionSetting}
className={styles.permissionSetting}
>
{t[
'com.affine.settings.meetings.privacy.screen-system-audio-recording.permission-setting'
]()}
</span>
)}
</>
} }
> />
<IconButton <PermissionSettingRow
icon={ nameKey="com.affine.settings.meetings.privacy.microphone"
screenRecordingPermission ? ( descriptionKey="com.affine.settings.meetings.privacy.microphone.description"
<DoneIcon /> permissionSettingKey="com.affine.settings.meetings.privacy.microphone.permission-setting"
) : ( hasPermission={permissions?.microphone || false}
<InformationFillDuotoneIcon onOpenPermissionSetting={
className={styles.noPermissionIcon} handleOpenMicrophoneRecordingPermissionSetting
/> }
) />
}
onClick={handleOpenScreenRecordingPermissionSetting}
/>
</SettingRow>
</SettingWrapper> </SettingWrapper>
</> </>
)} )}

View File

@@ -103,13 +103,15 @@ export class MeetingSettingsService extends Service {
return this.desktopApiService?.handler.recording.checkRecordingAvailable(); return this.desktopApiService?.handler.recording.checkRecordingAvailable();
} }
async checkScreenRecordingPermission() { async checkMeetingPermissions() {
return this.desktopApiService?.handler.recording.checkScreenRecordingPermission(); return this.desktopApiService?.handler.recording.checkMeetingPermissions();
} }
// the following methods are only available on MacOS right? // the following methods are only available on MacOS right?
async showScreenRecordingPermissionSetting() { async showRecordingPermissionSetting(type: 'screen' | 'microphone') {
return this.desktopApiService?.handler.recording.showScreenRecordingPermissionSetting(); return this.desktopApiService?.handler.recording.showRecordingPermissionSetting(
type
);
} }
setRecordingMode = (mode: MeetingSettingsSchema['recordingMode']) => { setRecordingMode = (mode: MeetingSettingsSchema['recordingMode']) => {

View File

@@ -5411,6 +5411,18 @@ export function useAFFiNEI18N(): {
* `Click to allow` * `Click to allow`
*/ */
["com.affine.settings.meetings.privacy.screen-system-audio-recording.permission-setting"](): string; ["com.affine.settings.meetings.privacy.screen-system-audio-recording.permission-setting"](): string;
/**
* `Microphone`
*/
["com.affine.settings.meetings.privacy.microphone"](): string;
/**
* `The Meeting feature requires permission to be used.`
*/
["com.affine.settings.meetings.privacy.microphone.description"](): string;
/**
* `Click to allow`
*/
["com.affine.settings.meetings.privacy.microphone.permission-setting"](): string;
/** /**
* `Do nothing` * `Do nothing`
*/ */

View File

@@ -1350,6 +1350,9 @@
"com.affine.settings.meetings.privacy.screen-system-audio-recording": "Screen & System audio recording", "com.affine.settings.meetings.privacy.screen-system-audio-recording": "Screen & System audio recording",
"com.affine.settings.meetings.privacy.screen-system-audio-recording.description": "The Meeting feature requires permission to be used.", "com.affine.settings.meetings.privacy.screen-system-audio-recording.description": "The Meeting feature requires permission to be used.",
"com.affine.settings.meetings.privacy.screen-system-audio-recording.permission-setting": "Click to allow", "com.affine.settings.meetings.privacy.screen-system-audio-recording.permission-setting": "Click to allow",
"com.affine.settings.meetings.privacy.microphone": "Microphone",
"com.affine.settings.meetings.privacy.microphone.description": "The Meeting feature requires permission to be used.",
"com.affine.settings.meetings.privacy.microphone.permission-setting": "Click to allow",
"com.affine.settings.meetings.record.recording-mode.none": "Do nothing", "com.affine.settings.meetings.record.recording-mode.none": "Do nothing",
"com.affine.settings.meetings.record.recording-mode.auto-start": "Auto start recording", "com.affine.settings.meetings.record.recording-mode.auto-start": "Auto start recording",
"com.affine.settings.meetings.record.recording-mode.prompt": "Show a recording prompt", "com.affine.settings.meetings.record.recording-mode.prompt": "Show a recording prompt",