feat(stage-tamagotchi,electron-screen-capture,i18n): check for screen capture permissions
This commit is contained in:
@@ -0,0 +1,111 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import type { SourcesOptions } from 'electron'
|
||||||
|
|
||||||
|
import { useElectronScreenCapture } from '@proj-airi/electron-screen-capture/vue'
|
||||||
|
import { Button } from '@proj-airi/ui'
|
||||||
|
import { useWindowFocus } from '@vueuse/core'
|
||||||
|
import { DialogContent, DialogDescription, DialogOverlay, DialogPortal, DialogRoot, DialogTitle } from 'reka-ui'
|
||||||
|
import { onMounted, ref, toRef, watch } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
sourcesOptions: SourcesOptions
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const sourcesOptions = toRef(props, 'sourcesOptions')
|
||||||
|
|
||||||
|
const hasPermissions = ref(false)
|
||||||
|
const showDialog = ref(false)
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const {
|
||||||
|
getSources,
|
||||||
|
setSource,
|
||||||
|
selectWithSource,
|
||||||
|
checkMacOSPermission,
|
||||||
|
requestMacOSPermission,
|
||||||
|
} = useElectronScreenCapture(window.electron.ipcRenderer, sourcesOptions)
|
||||||
|
const focused = useWindowFocus()
|
||||||
|
|
||||||
|
async function checkPermissions() {
|
||||||
|
if (window.platform === 'darwin') {
|
||||||
|
const status = await checkMacOSPermission()
|
||||||
|
hasPermissions.value = status === 'granted'
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
hasPermissions.value = true
|
||||||
|
}
|
||||||
|
if (!hasPermissions.value) {
|
||||||
|
showDialog.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function requestPermission() {
|
||||||
|
if (window.platform === 'darwin') {
|
||||||
|
await requestMacOSPermission()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await checkPermissions()
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(focused, async (isFocused) => {
|
||||||
|
if (isFocused) {
|
||||||
|
await checkPermissions()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<slot
|
||||||
|
v-bind="{
|
||||||
|
getSources,
|
||||||
|
setSource,
|
||||||
|
selectWithSource,
|
||||||
|
hasPermissions,
|
||||||
|
checkPermissions,
|
||||||
|
requestPermission,
|
||||||
|
}"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<DialogRoot :open="showDialog">
|
||||||
|
<DialogPortal>
|
||||||
|
<DialogOverlay class="fixed inset-0 z-9999 bg-black/50 backdrop-blur-sm data-[state=closed]:animate-fadeOut data-[state=open]:animate-fadeIn" />
|
||||||
|
<DialogContent flex="~ col items-start gap-4" class="fixed left-1/2 top-1/2 z-9999 max-h-full max-w-2xl w-[92dvw] transform overflow-y-scroll rounded-2xl bg-white p-6 shadow-xl outline-none backdrop-blur-md scrollbar-none -translate-x-1/2 -translate-y-1/2 data-[state=closed]:animate-contentHide data-[state=open]:animate-contentShow dark:bg-neutral-900">
|
||||||
|
<DialogTitle class="m-0 text-lg font-semibold">
|
||||||
|
{{ t('tamagotchi.settings.screen-capture.permissions-prompt.title') }}
|
||||||
|
</DialogTitle>
|
||||||
|
|
||||||
|
<DialogDescription>
|
||||||
|
{{ t('tamagotchi.settings.screen-capture.permissions-prompt.description') }}
|
||||||
|
|
||||||
|
<ol mt-4 list-decimal pl-5 text-sm>
|
||||||
|
<li mb-1>
|
||||||
|
{{ t('tamagotchi.settings.screen-capture.permissions-prompt.instructions.step-1') }}
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
{{ t('tamagotchi.settings.screen-capture.permissions-prompt.instructions.step-2') }}
|
||||||
|
<br>
|
||||||
|
<span class="text-neutral-500">
|
||||||
|
{{ t('tamagotchi.settings.screen-capture.permissions-prompt.instructions.step-2-note') }}
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</DialogDescription>
|
||||||
|
|
||||||
|
<div flex="~ row gap-2 mt-4 justify-end" w-full>
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
@click="showDialog = false"
|
||||||
|
>
|
||||||
|
{{ t('tamagotchi.settings.screen-capture.permissions-prompt.dismiss') }}
|
||||||
|
</Button>
|
||||||
|
<Button @click="requestMacOSPermission()">
|
||||||
|
{{ t('tamagotchi.settings.screen-capture.permissions-prompt.open-preferences') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</DialogPortal>
|
||||||
|
</DialogRoot>
|
||||||
|
</template>
|
||||||
@@ -1,29 +1,39 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { SerializableDesktopCapturerSource } from '@proj-airi/electron-screen-capture'
|
import type { SerializableDesktopCapturerSource } from '@proj-airi/electron-screen-capture'
|
||||||
|
import type { SourcesOptions } from 'electron'
|
||||||
|
|
||||||
import { useElectronScreenCapture } from '@proj-airi/electron-screen-capture/vue'
|
import { useElectronScreenCapture } from '@proj-airi/electron-screen-capture/vue'
|
||||||
import { Button } from '@proj-airi/ui'
|
import { Button } from '@proj-airi/ui'
|
||||||
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
import { onBeforeUnmount, onMounted, ref } from 'vue'
|
||||||
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
const sources = ref<ScreenCaptureSource[]>([])
|
import WithScreenCapture from '../../components/WithScreenCapture.vue'
|
||||||
const isRefetching = ref(false)
|
|
||||||
const activeStreams = ref<MediaStream[]>([])
|
|
||||||
|
|
||||||
interface ScreenCaptureSource extends SerializableDesktopCapturerSource {
|
interface ScreenCaptureSource extends SerializableDesktopCapturerSource {
|
||||||
appIconURL?: string
|
appIconURL?: string
|
||||||
thumbnailURL?: string
|
thumbnailURL?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const { getSources, selectWithSource } = useElectronScreenCapture(window.electron.ipcRenderer, {
|
const sources = ref<ScreenCaptureSource[]>([])
|
||||||
|
const isRefetching = ref(false)
|
||||||
|
const activeStreams = ref<MediaStream[]>([])
|
||||||
|
|
||||||
|
const sourcesOptions = ref<SourcesOptions>({
|
||||||
types: ['screen', 'window'],
|
types: ['screen', 'window'],
|
||||||
fetchWindowIcons: true,
|
fetchWindowIcons: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const { t } = useI18n()
|
||||||
|
const {
|
||||||
|
getSources,
|
||||||
|
selectWithSource,
|
||||||
|
} = useElectronScreenCapture(window.electron.ipcRenderer, sourcesOptions)
|
||||||
|
|
||||||
function toLocalArrayBuffer(bytes: Uint8Array) {
|
function toLocalArrayBuffer(bytes: Uint8Array) {
|
||||||
if (bytes.buffer instanceof SharedArrayBuffer) {
|
if (typeof SharedArrayBuffer !== 'undefined' && bytes.buffer instanceof SharedArrayBuffer) {
|
||||||
return bytes.slice().buffer
|
return bytes.slice().buffer
|
||||||
}
|
}
|
||||||
return bytes.buffer
|
return bytes.buffer as ArrayBuffer
|
||||||
}
|
}
|
||||||
|
|
||||||
function toObjectUrl(bytes: Uint8Array, mime: string) {
|
function toObjectUrl(bytes: Uint8Array, mime: string) {
|
||||||
@@ -80,6 +90,9 @@ async function refetchSources() {
|
|||||||
thumbnailURL: source.thumbnail && source.thumbnail.length > 0 ? toObjectUrl(source.thumbnail, 'image/jpeg') : undefined,
|
thumbnailURL: source.thumbnail && source.thumbnail.length > 0 ? toObjectUrl(source.thumbnail, 'image/jpeg') : undefined,
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error('Error fetching sources:', err)
|
||||||
|
}
|
||||||
finally {
|
finally {
|
||||||
isRefetching.value = false
|
isRefetching.value = false
|
||||||
}
|
}
|
||||||
@@ -100,133 +113,146 @@ onBeforeUnmount(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<WithScreenCapture :sources-options="sourcesOptions">
|
||||||
flex="~ col gap-4 items-start" w-full
|
<template #default="{ hasPermissions, requestPermission }">
|
||||||
text="neutral-500 dark:neutral-400"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-if="activeStreams.length > 0"
|
|
||||||
bg="primary-300/10"
|
|
||||||
b="2 solid primary-400/70"
|
|
||||||
w-full overflow-hidden rounded-2xl p-3
|
|
||||||
flex="~ col gap-2"
|
|
||||||
>
|
|
||||||
<div flex="~ row items-center gap-2">
|
|
||||||
<div class="i-solar:videocamera-record-line-duotone" />
|
|
||||||
<div>Capturing</div>
|
|
||||||
</div>
|
|
||||||
<div
|
<div
|
||||||
flex="~ row items-center gap-3"
|
v-if="hasPermissions"
|
||||||
w-full overflow-x-auto
|
flex="~ col gap-4 items-start" w-full
|
||||||
|
text="neutral-500 dark:neutral-400"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-for="stream in activeStreams" :key="stream.id"
|
v-if="activeStreams.length > 0"
|
||||||
relative overflow-hidden rounded-lg
|
bg="primary-300/10"
|
||||||
|
b="2 solid primary-400/70"
|
||||||
|
w-full overflow-hidden rounded-2xl p-3
|
||||||
|
flex="~ col gap-2"
|
||||||
>
|
>
|
||||||
<div
|
<div flex="~ row items-center gap-2">
|
||||||
flex="~ col items-center justify-center gap-1"
|
<div class="i-solar:videocamera-record-line-duotone" />
|
||||||
absolute right-0 top-0 z-10 h-full w-full cursor-pointer
|
<div>Capturing</div>
|
||||||
rounded-lg op-0 backdrop-blur-sm hover:op-100
|
|
||||||
transition="all duration-200"
|
|
||||||
text="light"
|
|
||||||
bg="black/30"
|
|
||||||
@click="stopStream(stream)"
|
|
||||||
>
|
|
||||||
<div class="i-solar:stop-line-duotone" />
|
|
||||||
<div text-sm>
|
|
||||||
Stop
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<video
|
|
||||||
autoplay
|
|
||||||
muted
|
|
||||||
playsinline
|
|
||||||
:srcObject="stream"
|
|
||||||
h-140px
|
|
||||||
w-auto
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
flex="~ col gap-3"
|
|
||||||
w-full pb-6
|
|
||||||
>
|
|
||||||
<div flex="~ row items-center justify-between" w-full>
|
|
||||||
<div>{{ sources.length }} source(s)</div>
|
|
||||||
<Button
|
|
||||||
:label="isRefetching ? 'Refetching...' : 'Refetch'"
|
|
||||||
icon="i-solar:refresh-line-duotone"
|
|
||||||
size="sm"
|
|
||||||
:disabled="isRefetching"
|
|
||||||
@click="refetchSources()"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div grid="~ cols-1 sm:cols-2 md:cols-3 lg:cols-4 xl:cols-5 gap-3">
|
|
||||||
<div
|
|
||||||
v-for="source in sources"
|
|
||||||
:key="source.id"
|
|
||||||
flex="~ col justify-between gap-3"
|
|
||||||
w-full cursor-pointer rounded-2xl p-3
|
|
||||||
transition="all duration-200"
|
|
||||||
border="2 solid neutral-200/60 dark:neutral-800/10 hover:primary-400/70"
|
|
||||||
@click="startCapture(source)"
|
|
||||||
>
|
|
||||||
<div flex="~ col items-start w-full">
|
|
||||||
<div flex="~ row items-center gap-1">
|
|
||||||
<div class="h-16px w-16px">
|
|
||||||
<img
|
|
||||||
v-if="source.appIconURL"
|
|
||||||
:src="source.appIconURL"
|
|
||||||
:alt="source.id.startsWith('screen:') ? 'Screen Icon' : 'Window Icon'"
|
|
||||||
class="h-full w-full shrink-0"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-else-if="source.id.startsWith('screen:')"
|
|
||||||
h-full w-full
|
|
||||||
class="i-solar:screencast-2-line-duotone"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
h-full w-full
|
|
||||||
class="i-solar:window-frame-line-duotone"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div text-sm>
|
|
||||||
{{ source.id.startsWith('screen:') ? 'Screen' : 'Window' }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div text-ellipsis break-all>
|
|
||||||
{{ source.name }}
|
|
||||||
</div>
|
|
||||||
<div text-sm font-mono text="neutral-400 dark:neutral-600">
|
|
||||||
{{ source.id }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
h-200px w-full overflow-hidden rounded-2xl bg-black
|
flex="~ row items-center gap-3"
|
||||||
flex="~ items-center justify-center shrink-0"
|
w-full overflow-x-auto
|
||||||
>
|
>
|
||||||
<img
|
|
||||||
v-if="source.thumbnailURL"
|
|
||||||
:src="source.thumbnailURL"
|
|
||||||
alt="Thumbnail"
|
|
||||||
h-full
|
|
||||||
w-full object-contain
|
|
||||||
>
|
|
||||||
<div
|
<div
|
||||||
v-else
|
v-for="stream in activeStreams" :key="stream.id"
|
||||||
class="i-solar:forbidden-circle-line-duotone"
|
relative overflow-hidden rounded-lg
|
||||||
h-10 w-10 bg-light
|
>
|
||||||
|
<div
|
||||||
|
flex="~ col items-center justify-center gap-1"
|
||||||
|
absolute right-0 top-0 z-10 h-full w-full cursor-pointer
|
||||||
|
rounded-lg op-0 backdrop-blur-sm hover:op-100
|
||||||
|
transition="all duration-200"
|
||||||
|
text="light"
|
||||||
|
bg="black/30"
|
||||||
|
@click="stopStream(stream)"
|
||||||
|
>
|
||||||
|
<div class="i-solar:stop-line-duotone" />
|
||||||
|
<div text-sm>
|
||||||
|
Stop
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<video
|
||||||
|
autoplay
|
||||||
|
muted
|
||||||
|
playsinline
|
||||||
|
:srcObject="stream"
|
||||||
|
h-140px
|
||||||
|
w-auto
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
flex="~ col gap-3"
|
||||||
|
w-full pb-6
|
||||||
|
>
|
||||||
|
<div flex="~ row items-center justify-between" w-full>
|
||||||
|
<div>{{ sources.length }} source(s)</div>
|
||||||
|
<Button
|
||||||
|
:label="isRefetching ? 'Refetching...' : 'Refetch'"
|
||||||
|
icon="i-solar:refresh-line-duotone"
|
||||||
|
size="sm"
|
||||||
|
:disabled="isRefetching"
|
||||||
|
@click="refetchSources()"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div grid="~ cols-1 sm:cols-2 md:cols-3 lg:cols-4 xl:cols-5 gap-3">
|
||||||
|
<div
|
||||||
|
v-for="source in sources"
|
||||||
|
:key="source.id"
|
||||||
|
flex="~ col justify-between gap-3"
|
||||||
|
w-full cursor-pointer rounded-2xl p-3
|
||||||
|
transition="all duration-200"
|
||||||
|
border="2 solid neutral-200/60 dark:neutral-800/10 hover:primary-400/70"
|
||||||
|
@click="startCapture(source)"
|
||||||
|
>
|
||||||
|
<div flex="~ col items-start w-full">
|
||||||
|
<div flex="~ row items-center gap-1">
|
||||||
|
<div class="h-16px w-16px">
|
||||||
|
<img
|
||||||
|
v-if="source.appIconURL"
|
||||||
|
:src="source.appIconURL"
|
||||||
|
:alt="source.id.startsWith('screen:') ? 'Screen Icon' : 'Window Icon'"
|
||||||
|
class="h-full w-full shrink-0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-else-if="source.id.startsWith('screen:')"
|
||||||
|
h-full w-full
|
||||||
|
class="i-solar:screencast-2-line-duotone"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
h-full w-full
|
||||||
|
class="i-solar:window-frame-line-duotone"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div text-sm>
|
||||||
|
{{ source.id.startsWith('screen:') ? 'Screen' : 'Window' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div text-ellipsis break-all>
|
||||||
|
{{ source.name }}
|
||||||
|
</div>
|
||||||
|
<div text-sm font-mono text="neutral-400 dark:neutral-600">
|
||||||
|
{{ source.id }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
h-200px w-full overflow-hidden rounded-2xl bg-black
|
||||||
|
flex="~ items-center justify-center shrink-0"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
v-if="source.thumbnailURL"
|
||||||
|
:src="source.thumbnailURL"
|
||||||
|
alt="Thumbnail"
|
||||||
|
h-full
|
||||||
|
w-full object-contain
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-else
|
||||||
|
class="i-solar:forbidden-circle-line-duotone"
|
||||||
|
h-10 w-10 bg-light
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div v-else flex="~ col gap-4 items-center justify-center" h-full p-6>
|
||||||
</div>
|
<div>
|
||||||
|
{{ t('tamagotchi.settings.screen-capture.permissions-prompt.description') }}
|
||||||
|
</div>
|
||||||
|
<Button @click="requestPermission()">
|
||||||
|
{{ t('tamagotchi.settings.screen-capture.permissions-prompt.open-preferences') }}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</WithScreenCapture>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<route lang="yaml">
|
<route lang="yaml">
|
||||||
|
|||||||
@@ -273,6 +273,7 @@ words:
|
|||||||
- sumimakito
|
- sumimakito
|
||||||
- supergroup
|
- supergroup
|
||||||
- superjson
|
- superjson
|
||||||
|
- systempreferences
|
||||||
- tamagotchi
|
- tamagotchi
|
||||||
- tauri
|
- tauri
|
||||||
- taze
|
- taze
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { DesktopCapturerSource, SourcesOptions } from 'electron'
|
import type { DesktopCapturerSource, SourcesOptions, systemPreferences } from 'electron'
|
||||||
|
|
||||||
import { defineInvokeEventa } from '@moeru/eventa'
|
import { defineInvokeEventa } from '@moeru/eventa'
|
||||||
|
|
||||||
@@ -19,5 +19,8 @@ export interface ScreenCaptureSetSourceRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const screenCaptureGetSources = defineInvokeEventa<SerializableDesktopCapturerSource[], SourcesOptions>('eventa:invoke:electron:screen-capture:get-sources')
|
export const screenCaptureGetSources = defineInvokeEventa<SerializableDesktopCapturerSource[], SourcesOptions>('eventa:invoke:electron:screen-capture:get-sources')
|
||||||
export const screenCaptureSetSourceEx = defineInvokeEventa<any, ScreenCaptureSetSourceRequest>('eventa:invoke:electron:screen-capture:set-source')
|
export const screenCaptureSetSourceEx = defineInvokeEventa<string, ScreenCaptureSetSourceRequest>('eventa:invoke:electron:screen-capture:set-source')
|
||||||
export const screenCaptureResetSource = defineInvokeEventa<any, string>('eventa:invoke:electron:screen-capture:reset-source')
|
export const screenCaptureResetSource = defineInvokeEventa<void, string>('eventa:invoke:electron:screen-capture:reset-source')
|
||||||
|
|
||||||
|
export const screenCaptureCheckMacOSPermission = defineInvokeEventa<ReturnType<typeof systemPreferences.getMediaAccessStatus>, never>('eventa:invoke:electron:screen-capture:check-macos-permission')
|
||||||
|
export const screenCaptureRequestMacOSPermission = defineInvokeEventa<void, never>('eventa:invoke:electron:screen-capture:request-macos-permission')
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import { Mutex, withTimeout } from 'async-mutex'
|
|||||||
import { app, desktopCapturer, ipcMain, session as sessionModule } from 'electron'
|
import { app, desktopCapturer, ipcMain, session as sessionModule } from 'electron'
|
||||||
import { nanoid } from 'nanoid'
|
import { nanoid } from 'nanoid'
|
||||||
|
|
||||||
import { screenCaptureGetSources, screenCaptureResetSource, screenCaptureSetSourceEx } from '..'
|
import { screenCaptureCheckMacOSPermission, screenCaptureGetSources, screenCaptureRequestMacOSPermission, screenCaptureResetSource, screenCaptureSetSourceEx } from '..'
|
||||||
import { toSerializableDesktopCapturerSource } from './utils'
|
import { checkMacOSScreenCapturePermission, requestMacOSScreenCapturePermission, toSerializableDesktopCapturerSource } from './utils'
|
||||||
|
|
||||||
export const defaultSourcesOptions: SourcesOptions = { types: ['screen'] }
|
export const defaultSourcesOptions: SourcesOptions = { types: ['screen'] }
|
||||||
|
|
||||||
@@ -144,6 +144,9 @@ export function initScreenCaptureForWindow(window: BrowserWindow, options?: Init
|
|||||||
const { context } = createContext(ipcMain, window, { onlySameWindow: true })
|
const { context } = createContext(ipcMain, window, { onlySameWindow: true })
|
||||||
const session = sessionModule.defaultSession
|
const session = sessionModule.defaultSession
|
||||||
|
|
||||||
|
defineInvokeHandler(context, screenCaptureCheckMacOSPermission, async () => checkMacOSScreenCapturePermission())
|
||||||
|
defineInvokeHandler(context, screenCaptureRequestMacOSPermission, async () => requestMacOSScreenCapturePermission())
|
||||||
|
|
||||||
defineInvokeHandler(context, screenCaptureGetSources, async (sourcesOptions) => {
|
defineInvokeHandler(context, screenCaptureGetSources, async (sourcesOptions) => {
|
||||||
const sources = await desktopCapturer.getSources(sourcesOptions)
|
const sources = await desktopCapturer.getSources(sourcesOptions)
|
||||||
return sources.map(source => toSerializableDesktopCapturerSource(source))
|
return sources.map(source => toSerializableDesktopCapturerSource(source))
|
||||||
|
|||||||
@@ -2,6 +2,10 @@ import type { DesktopCapturerSource } from 'electron'
|
|||||||
|
|
||||||
import type { SerializableDesktopCapturerSource } from '..'
|
import type { SerializableDesktopCapturerSource } from '..'
|
||||||
|
|
||||||
|
import process from 'node:process'
|
||||||
|
|
||||||
|
import { shell, systemPreferences } from 'electron'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serializes a DesktopCapturerSource to a format that can be sent over IPC.
|
* Serializes a DesktopCapturerSource to a format that can be sent over IPC.
|
||||||
*
|
*
|
||||||
@@ -24,3 +28,19 @@ export function toSerializableDesktopCapturerSource(source: DesktopCapturerSourc
|
|||||||
thumbnail: source.thumbnail != null ? new Uint8Array(source.thumbnail.toJPEG(90).buffer) : undefined,
|
thumbnail: source.thumbnail != null ? new Uint8Array(source.thumbnail.toJPEG(90).buffer) : undefined,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function checkMacOSScreenCapturePermission(): ReturnType<typeof systemPreferences.getMediaAccessStatus> {
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
throw new Error('checkMacOSScreenCapturePermission is only available on macOS (darwin)')
|
||||||
|
}
|
||||||
|
|
||||||
|
return systemPreferences.getMediaAccessStatus('screen')
|
||||||
|
}
|
||||||
|
|
||||||
|
export function requestMacOSScreenCapturePermission(): void {
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
throw new Error('requestMacOSScreenCapturePermission is only available on macOS (darwin)')
|
||||||
|
}
|
||||||
|
|
||||||
|
shell.openExternal('x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture')
|
||||||
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import type { ScreenCaptureSetSourceRequest, SerializableDesktopCapturerSource }
|
|||||||
|
|
||||||
import { defineInvoke } from '@moeru/eventa'
|
import { defineInvoke } from '@moeru/eventa'
|
||||||
|
|
||||||
import { screenCaptureGetSources, screenCaptureResetSource, screenCaptureSetSourceEx } from '.'
|
import { screenCaptureCheckMacOSPermission, screenCaptureGetSources, screenCaptureRequestMacOSPermission, screenCaptureResetSource, screenCaptureSetSourceEx } from '.'
|
||||||
|
|
||||||
export interface SourceOptionsWithRequest {
|
export interface SourceOptionsWithRequest {
|
||||||
sourcesOptions?: SourcesOptions
|
sourcesOptions?: SourcesOptions
|
||||||
@@ -17,6 +17,9 @@ export function setupElectronScreenCapture(context: ReturnType<typeof createCont
|
|||||||
const setSource = defineInvoke(context, screenCaptureSetSourceEx)
|
const setSource = defineInvoke(context, screenCaptureSetSourceEx)
|
||||||
const resetSource = defineInvoke(context, screenCaptureResetSource)
|
const resetSource = defineInvoke(context, screenCaptureResetSource)
|
||||||
|
|
||||||
|
const checkMacOSPermission = defineInvoke(context, screenCaptureCheckMacOSPermission)
|
||||||
|
const requestMacOSPermission = defineInvoke(context, screenCaptureRequestMacOSPermission)
|
||||||
|
|
||||||
async function getSources(sourcesOptions: SourcesOptions) {
|
async function getSources(sourcesOptions: SourcesOptions) {
|
||||||
return invokeGetSources(sourcesOptions)
|
return invokeGetSources(sourcesOptions)
|
||||||
}
|
}
|
||||||
@@ -45,5 +48,12 @@ export function setupElectronScreenCapture(context: ReturnType<typeof createCont
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { getSources, setSource, selectWithSource, resetSource }
|
return {
|
||||||
|
getSources,
|
||||||
|
setSource,
|
||||||
|
selectWithSource,
|
||||||
|
resetSource,
|
||||||
|
checkMacOSPermission,
|
||||||
|
requestMacOSPermission,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import type { ScreenCaptureSetSourceRequest, SerializableDesktopCapturerSource }
|
|||||||
|
|
||||||
import { defineInvoke } from '@moeru/eventa'
|
import { defineInvoke } from '@moeru/eventa'
|
||||||
import { createContext } from '@moeru/eventa/adapters/electron/renderer'
|
import { createContext } from '@moeru/eventa/adapters/electron/renderer'
|
||||||
import { toValue } from 'vue'
|
import { toRaw, toValue } from 'vue'
|
||||||
|
|
||||||
import { screenCaptureGetSources, screenCaptureResetSource, screenCaptureSetSourceEx } from '..'
|
import { screenCaptureCheckMacOSPermission, screenCaptureGetSources, screenCaptureRequestMacOSPermission, screenCaptureResetSource, screenCaptureSetSourceEx } from '..'
|
||||||
|
|
||||||
export function useElectronScreenCapture(ipcRenderer: IpcRenderer, sourcesOptions: MaybeRefOrGetter<SourcesOptions>) {
|
export function useElectronScreenCapture(ipcRenderer: IpcRenderer, sourcesOptions: MaybeRefOrGetter<SourcesOptions>) {
|
||||||
const context = createContext(ipcRenderer).context
|
const context = createContext(ipcRenderer).context
|
||||||
@@ -17,8 +17,11 @@ export function useElectronScreenCapture(ipcRenderer: IpcRenderer, sourcesOption
|
|||||||
const setSource = defineInvoke(context, screenCaptureSetSourceEx)
|
const setSource = defineInvoke(context, screenCaptureSetSourceEx)
|
||||||
const resetSource = defineInvoke(context, screenCaptureResetSource)
|
const resetSource = defineInvoke(context, screenCaptureResetSource)
|
||||||
|
|
||||||
|
const checkMacOSPermission = defineInvoke(context, screenCaptureCheckMacOSPermission)
|
||||||
|
const requestMacOSPermission = defineInvoke(context, screenCaptureRequestMacOSPermission)
|
||||||
|
|
||||||
async function getSources() {
|
async function getSources() {
|
||||||
return invokeGetSources(toValue(sourcesOptions))
|
return invokeGetSources(toRaw(toValue(sourcesOptions)))
|
||||||
}
|
}
|
||||||
|
|
||||||
async function selectWithSource<R>(
|
async function selectWithSource<R>(
|
||||||
@@ -32,7 +35,7 @@ export function useElectronScreenCapture(ipcRenderer: IpcRenderer, sourcesOption
|
|||||||
let handle: string | undefined
|
let handle: string | undefined
|
||||||
try {
|
try {
|
||||||
handle = await setSource({
|
handle = await setSource({
|
||||||
options: toValue(sourcesOptions),
|
options: toRaw(toValue(sourcesOptions)),
|
||||||
sourceId,
|
sourceId,
|
||||||
timeout: request?.timeout,
|
timeout: request?.timeout,
|
||||||
})
|
})
|
||||||
@@ -45,5 +48,12 @@ export function useElectronScreenCapture(ipcRenderer: IpcRenderer, sourcesOption
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return { getSources, setSource, resetSource, selectWithSource }
|
return {
|
||||||
|
getSources,
|
||||||
|
setSource,
|
||||||
|
resetSource,
|
||||||
|
selectWithSource,
|
||||||
|
checkMacOSPermission,
|
||||||
|
requestMacOSPermission,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,3 +42,14 @@ devtools:
|
|||||||
description: Stress markdown parsing and rendering with heavy chat payloads
|
description: Stress markdown parsing and rendering with heavy chat payloads
|
||||||
widgets-calling:
|
widgets-calling:
|
||||||
title: Widget Calling
|
title: Widget Calling
|
||||||
|
|
||||||
|
screen-capture:
|
||||||
|
permissions-prompt:
|
||||||
|
title: Screen Capture
|
||||||
|
description: To capture screens or windows, screen and system audio recording permissions are required.
|
||||||
|
open-preferences: Open System Preferences
|
||||||
|
dismiss: Dismiss
|
||||||
|
instructions:
|
||||||
|
step-1: Open "System Preferences", select "Privacy & Security" from the sidebar.
|
||||||
|
step-2: In the "Screen & System Audio Recording" and "System Audio Recording Only" sections, turn on the switches for AIRI.
|
||||||
|
step-2-note: If AIRI is not listed, please add it manually by clicking the "+" button and selecting AIRI from the Applications folder.
|
||||||
|
|||||||
Reference in New Issue
Block a user