feat(stage-pocket): improve permission management (#2184)

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: LemonNeko <17664845+LemonNekoGH@users.noreply.github.com>
This commit is contained in:
Doji
2026-08-08 20:16:10 +08:00
committed by GitHub
co-authored by autofix-ci[bot] LemonNeko
parent 5addb1574f
commit 584e4960e9
26 changed files with 680 additions and 415 deletions
+9 -8
View File
@@ -58,16 +58,14 @@ dialogs:
Please return to the previous step and check your API key, or check the
network connection.
permissions:
title: Notifications
description: You can allow notifications now or continue and set it up later.
title: Permission management
description: You can grant the required permissions now or continue and configure them later.
notificationsTitle: Notifications
notificationsDescription: Allow reminders and important status updates.
notificationsAction: Request notification permission
notificationsNotGrantedHint: If denied, you can enable notifications later from iOS Settings.
openSettings: Open iOS Settings
optionalHint: You can continue even if notification permission is not granted.
stateUnknown: Not requested
stateRequesting: Requesting...
microphoneTitle: Microphone
microphoneDescription: Allow AIRI to use your microphone for voice input and transcription.
requestAction: Request access
optionalHint: You can continue even if these permissions are not granted.
stateGranted: Granted
stateNotGranted: Not granted
bug-report:
@@ -1512,6 +1510,9 @@ pages:
general:
description: Dark theme, languages, etc.
title: General
permissions:
description: View current permission status.
title: Permission Management
description: Customize your stage!
sections:
section:
+9 -8
View File
@@ -51,16 +51,14 @@ dialogs:
no-models-help: >-
Por favor, vuelva al paso anterior y compruebe su clave API, o compruebe la conexión de red.
permissions:
title: Notificaciones
description: Puedes permitir notificaciones ahora o continuar y configurarlas más tarde.
title: Gestión de permisos
description: Puedes conceder los permisos necesarios ahora o continuar y configurarlos más tarde.
notificationsTitle: Notificaciones
notificationsDescription: Permitir recordatorios y actualizaciones importantes de estado.
notificationsAction: 'Solicitar Permiso de Notificación'
notificationsNotGrantedHint: Si se deniega, puede habilitar las notificaciones más tarde desde la configuración de iOS.
openSettings: Abrir ajustes de iOS
optionalHint: Puedes continuar incluso si no se concede permiso de notificación.
stateUnknown: No solicitado
stateRequesting: Solicitando...
microphoneTitle: Micrófono
microphoneDescription: Permite que AIRI use el micrófono para la entrada de voz y la transcripción.
requestAction: Solicitar acceso
optionalHint: Puedes continuar aunque no se concedan estos permisos.
stateGranted: Concedido
stateNotGranted: No concedido
bug-report:
@@ -1452,6 +1450,9 @@ pages:
general:
description: Tema oscuro, idiomas, etc.
title: General
permissions:
description: Consulta el estado actual de los permisos.
title: Gestión de permisos
description: '¡Personaliza tu escenario!'
sections:
section:
+9 -8
View File
@@ -51,16 +51,14 @@ dialogs:
no-models-help: >-
Veuillez revenir à l’étape précédente et vérifier votre clé API, ou bien vérifier la connexion réseau.
permissions:
title: Notifications
description: Vous pouvez autoriser les notifications maintenant ou continuer et les configurer plus tard.
title: Gestion des autorisations
description: Vous pouvez accorder les autorisations nécessaires maintenant ou les configurer plus tard.
notificationsTitle: Notifications
notificationsDescription: Autorisez les rappels et les mises à jour de statut importantes.
notificationsAction: Demander l'autorisation de notification
notificationsNotGrantedHint: En cas de refus, vous pourrez activer les notifications plus tard dans les Réglages iOS.
openSettings: Ouvrir les Réglages iOS
optionalHint: Vous pouvez continuer même si l'autorisation de notification n'est pas accordée.
stateUnknown: Non demandé
stateRequesting: Demande en cours...
microphoneTitle: Microphone
microphoneDescription: Autorisez AIRI à utiliser le microphone pour la saisie vocale et la transcription.
requestAction: Demander laccès
optionalHint: Vous pouvez continuer même si ces autorisations ne sont pas accordées.
stateGranted: Accordée
stateNotGranted: Non accordée
bug-report:
@@ -1452,6 +1450,9 @@ pages:
general:
description: Thème sombre, langues, etc.
title: Général
permissions:
description: Consultez l’état actuel des autorisations.
title: Gestion des autorisations
description: Personnalisez votre scène !
sections:
section:
+9 -8
View File
@@ -51,16 +51,14 @@ dialogs:
no-models-help: >-
前のステップに戻ってAPIキーを確認するか、ネットワーク接続を確認してください。
permissions:
title: 通知
description: 今すぐ通知を許可するか、続行して後で設定できます。
title: 権限管理
description: 今すぐ必要な権限を許可するか、続行して後で設定できます。
notificationsTitle: 通知
notificationsDescription: リマインダーと重要なステータス更新を許可する。
notificationsAction: 通知権限を要求
notificationsNotGrantedHint: 通知を拒否した場合は、iOS の設定から後で通知を有効にできます。
openSettings: iOSの設定を開く
optionalHint: 通知権限を許可しない場合でも続行できます。
stateUnknown: リクエストされていません
stateRequesting: リクエスト中…
microphoneTitle: マイク
microphoneDescription: AIRI が音声入力と文字起こしにマイクを使用することを許可します。
requestAction: アクセスをリクエスト
optionalHint: これらの権限を許可しない場合でも続行できます。
stateGranted: 許可済み
stateNotGranted: 許可されていません
bug-report:
@@ -1452,6 +1450,9 @@ pages:
general:
description: ダークテーマ、言語など。
title: 一般
permissions:
description: 現在の権限状態を確認します。
title: 権限管理
description: ステージをカスタマイズしよう!
sections:
section:
+10 -9
View File
@@ -51,16 +51,14 @@ dialogs:
no-models-help: >-
이전 단계로 돌아가서 API 키를 확인하거나 네트워크 연결을 확인해 주세요.
permissions:
title: 알림
description: 지금 알림을 받도록 허용할 수 있고 나중에 허용할 수 있어요.
title: 권한 관리
description: 지금 필요한 권한을 허용하거나 계속 진행한 뒤 나중에 설정할 수 있어요.
notificationsTitle: 알림
notificationsDescription: Allow reminders and important status updates.
notificationsAction: Request notification permission
notificationsNotGrantedHint: If denied, you can enable notifications later from iOS Settings.
openSettings: iOS 설정 열기
optionalHint: You can continue even if notification permission is not granted.
stateUnknown: 요청 전
stateRequesting: 요청 중...
notificationsDescription: 미리 알림과 중요한 상태 업데이트를 허용합니다.
microphoneTitle: 마이크
microphoneDescription: AIRI가 음성 입력 및 텍스트 변환에 마이크를 사용하도록 허용합니다.
requestAction: 액세스 요청
optionalHint: 이 권한을 허용하지 않아도 계속할 수 있어요.
stateGranted: 허용됨
stateNotGranted: 허용되지 않음
bug-report:
@@ -1452,6 +1450,9 @@ pages:
general:
description: 어두운 테마, 언어 등.
title: 일반
permissions:
description: 현재 권한 상태를 확인합니다.
title: 권한 관리
description: 무대를 커스터마이징하세요!
sections:
section:
+9 -8
View File
@@ -51,16 +51,14 @@ dialogs:
no-models-help: >-
Вернитесь на предыдущий шаг и проверьте API-ключ или подключение к сети.
permissions:
title: Оповещения
description: Вы можете разрешить уведомления сейчас или продолжить и настроить их позже.
title: Управление разрешениями
description: Вы можете предоставить необходимые разрешения сейчас или настроить их позже.
notificationsTitle: Оповещения
notificationsDescription: Разрешить напоминания и важные обновления статуса.
notificationsAction: Запросить разрешение на уведомление
notificationsNotGrantedHint: Если отказано, вы можете включить уведомления позже в настройках iOS.
openSettings: Открыть настройки iOS
optionalHint: Вы можете продолжить, даже если разрешение на уведомление не предоставлено.
stateUnknown: Не запрошены
stateRequesting: Запрашивается...
microphoneTitle: Микрофон
microphoneDescription: Разрешить AIRI использовать микрофон для голосового ввода и расшифровки речи.
requestAction: Запросить доступ
optionalHint: Вы можете продолжить, даже если эти разрешения не предоставлены.
stateGranted: Предоставлено
stateNotGranted: Не предоставлено
bug-report:
@@ -1452,6 +1450,9 @@ pages:
general:
description: Тема, языки и др.
title: Общие
permissions:
description: Просмотр текущего состояния разрешений.
title: Управление разрешениями
description: Настройте вашу сцену!
sections:
section:
+10 -9
View File
@@ -51,18 +51,16 @@ dialogs:
no-models-help: >-
Vui lòng quay lại bước trước và kiểm tra khóa API, hoặc kiểm tra kết nối mạng.
permissions:
title: Thông báo
description: Bạn có thể cho phép thông báo ngay bây giờ hoặc tiếp tục và chỉnh lại sau.
title: Quản lý quyền
description: Bạn có thể cấp các quyền cần thiết ngay bây giờ hoặc tiếp tục và thiết lập sau.
notificationsTitle: Thông báo
notificationsDescription: Cho phép nhắc nhở và các cập nhật trạng thái quan trọng.
notificationsAction: Yêu cầu quyền thông báo
notificationsNotGrantedHint: Nếu từ chối, bạn có thể bật thông báo ở Cài đặt iOS.
openSettings: Mở Cài đặt iOS
optionalHint: Bạn có thể tiếp tục kể cả khi quyền thông báo không được cấp quyền.
stateUnknown: Không được yêu cầu
stateRequesting: Đang yêu cầu...
microphoneTitle: Micrô
microphoneDescription: Cho phép AIRI sử dụng micrô để nhập giọng nói và phiên âm.
requestAction: Yêu cầu quyền truy cập
optionalHint: Bạn vẫn có thể tiếp tục ngay cả khi chưa cấp các quyền này.
stateGranted: Đã cấp quyền
stateNotGranted: Khônh được cấp quyền
stateNotGranted: Không được cấp quyền
bug-report:
title: Báo lỗi (´;ω;`)ヾ(・∀・`)
subtitle: Úi, xin lỗi nếu chúng tôi đã làm gì sai. Nếu không phiền thì bạn có thể kể cho chúng tôi biết chuyện gì đã xảy ra không?
@@ -1452,6 +1450,9 @@ pages:
general:
description: Chế độ tối, ngôn ngữ, v.v.
title: Chung
permissions:
description: Xem trạng thái quyền hiện tại.
title: Quản lý quyền
description: Tùy chỉnh thiết lập bối cảnh của bạn!
sections:
section:
@@ -51,16 +51,14 @@ dialogs:
no-models-help: >-
请返回上一步并检查您的 API Key,或检查网络连接。
permissions:
title: 通知
description: 您现在可以允许通知或者继续并稍后设置。
title: 权限管理
description: 您现在可以授予所需权限,或继续并稍后在系统设置中配置。
notificationsTitle: 通知
notificationsDescription: 允许提醒以及重要的状态更新。
notificationsAction: 需要获取消息通知权限
notificationsNotGrantedHint: 如果拒绝,您可以稍后从 iOS 设置中启用通知
openSettings: 打开系统/偏好设置
optionalHint: 即使没有授予通知权限,您也可以继续。
stateUnknown: 未请求
stateRequesting: 正在获取...
microphoneTitle: 麦克风
microphoneDescription: 允许 AIRI 使用麦克风进行语音输入和转写
requestAction: 请求获取
optionalHint: 即使没有授予这些权限,您也可以继续。
stateGranted: 已授权
stateNotGranted: 未授权
bug-report:
@@ -1452,6 +1450,9 @@ pages:
general:
description: 深色主题、语言等选项
title: 通用
permissions:
description: 查看当前权限状态
title: 权限管理
description: 自定义你的舞台外观!
sections:
section:
+13 -12
View File
@@ -51,18 +51,16 @@ dialogs:
no-models-help: >-
請返回上一步並檢查您的 API Key,或檢查網路連線。
permissions:
title: Notifications
description: You can allow notifications now or continue and set it up later.
notificationsTitle: Notifications
notificationsDescription: Allow reminders and important status updates.
notificationsAction: Request notification permission
notificationsNotGrantedHint: If denied, you can enable notifications later from iOS Settings.
openSettings: Open iOS Settings
optionalHint: You can continue even if notification permission is not granted.
stateUnknown: Not requested
stateRequesting: Requesting...
stateGranted: Granted
stateNotGranted: Not granted
title: 權限管理
description: 您現在可以授予所需權限,或繼續並稍後在系統設定中配置。
notificationsTitle: 通知
notificationsDescription: 允許提醒以及重要的狀態更新。
microphoneTitle: 麥克風
microphoneDescription: 允許 AIRI 使用麥克風進行語音輸入和轉錄。
requestAction: 請求取得
optionalHint: 即使沒有授予這些權限,您也可以繼續。
stateGranted: 已授權
stateNotGranted: 未授權
bug-report:
title: Bug report (´;ω;`)ヾ(・∀・`)
subtitle: Oops, sorry we made something wrong. Would you mind telling us what happened?
@@ -1452,6 +1450,9 @@ pages:
general:
description: 深色主題、語言等選項
title: 通用
permissions:
description: 查看目前權限狀態
title: 權限管理
description: 自訂你的舞台外觀!
sections:
section:
@@ -38,8 +38,9 @@ const providerStore = useProviderConfigStore()
const { configuredTranscriptionProvidersMetadata } = storeToRefs(providersStore)
const { trackProviderClick } = useAnalytics()
const { stopStream, startStream } = useSettingsAudioDevice()
const { audioInputs, selectedAudioInput, stream } = storeToRefs(useSettingsAudioDevice())
const settingsAudioDeviceStore = useSettingsAudioDevice()
const { askPermission, stopStream, startStream } = settingsAudioDeviceStore
const { audioInputOptions, selectedAudioInput, stream } = storeToRefs(settingsAudioDeviceStore)
const { startRecord, stopRecord, onStopRecord } = useAudioRecorder(stream)
const { startAnalyzer, stopAnalyzer, onAnalyzerUpdate, volumeLevel } = useAudioAnalyzer()
const { audioContext } = storeToRefs(useAudioContext())
@@ -525,8 +526,8 @@ watch(activeTranscriptionProvider, async (provider) => {
}, { immediate: true })
onMounted(async () => {
// Audio devices are loaded on demand when user requests them
syncOpenAICompatibleSettings()
await askPermission()
})
onUnmounted(() => {
@@ -556,10 +557,7 @@ onUnmounted(() => {
v-model="selectedAudioInput"
label="Audio Input Device"
description="Select the audio input device for your hearing module."
:options="audioInputs.map(input => ({
label: input.label || input.deviceId,
value: input.deviceId,
}))"
:options="audioInputOptions"
placeholder="Select an audio input device"
layout="vertical"
/>
@@ -113,15 +113,15 @@ const isWebSpeechAPIAvailable = computed(() => {
&& ('webkitSpeechRecognition' in window || 'SpeechRecognition' in window)
})
const settingsAudioDeviceStore = useSettingsAudioDevice()
const { askPermission, stopStream, startStream } = settingsAudioDeviceStore
const { audioInputOptions, selectedAudioInput, stream } = storeToRefs(settingsAudioDeviceStore)
onMounted(async () => {
ensureProviderSettings()
// Audio devices are loaded on demand when user requests them
await askPermission()
})
// Speech-to-Text test state (always uses Web Speech API)
const { stopStream, startStream } = useSettingsAudioDevice()
const { audioInputs, selectedAudioInput, stream } = storeToRefs(useSettingsAudioDevice())
const isTestingSTT = ref(false)
const testTranscriptionText = ref<string>('')
const testTranscriptionError = ref<string>('')
@@ -405,10 +405,7 @@ onUnmounted(() => {
v-model="selectedAudioInput"
label="Audio Input Device"
description="Select the audio input device for testing"
:options="audioInputs.map(input => ({
label: input.label || input.deviceId,
value: input.deviceId,
}))"
:options="audioInputOptions"
placeholder="Select an audio input device"
layout="vertical"
class="flex-1"
@@ -1,5 +1,3 @@
// @vitest-environment jsdom
import { createPinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createApp, nextTick } from 'vue'
@@ -32,6 +30,9 @@ vi.mock('../../../../composables/audio', async () => {
return {
useAudioDevice: () => ({
audioInputs: ref([createAudioInput('store-microphone', 'Store microphone')]),
audioInputOptions: computed(() => [
{ label: 'Store microphone', value: 'store-microphone' },
]),
selectedAudioInput: ref('store-microphone'),
stream: shallowRef<MediaStream>(),
deviceConstraints: computed(() => ({ audio: true })),
@@ -14,7 +14,7 @@ const props = withDefaults(defineProps<{
const deviceStore = useSettingsAudioDevice()
const { askPermission } = deviceStore
const { audioInputs, enabled, permissionGranted, selectedAudioInput } = storeToRefs(deviceStore)
const { audioInputOptions, enabled, permissionGranted, selectedAudioInput } = storeToRefs(deviceStore)
const { volumeLevel } = useAudioAnalyzer()
const autoSend = defineModel<boolean | undefined>('autoSend')
@@ -119,7 +119,7 @@ function toggleHearingEnabled() {
v-model="selectedAudioInput"
label="Input device"
description="Select the microphone you want to use."
:options="audioInputs.map(device => ({ label: device.label || 'Unknown Device', value: device.deviceId }))"
:options="audioInputOptions"
placeholder="Select microphone"
layout="vertical"
/>
@@ -20,7 +20,7 @@ const props = defineProps<{
}>()
const { t } = useI18n()
const { audioInputs, selectedAudioInput, stream, stopStream, startStream } = useAudioDevice()
const { audioInputs, audioInputOptions, selectedAudioInput, stream, stopStream, startStream } = useAudioDevice()
const { volumeLevel, stopAnalyzer, startAnalyzer } = useAudioAnalyzer()
const { startRecord, stopRecord, onStopRecord } = useAudioRecorder(stream)
@@ -178,10 +178,7 @@ onUnmounted(() => {
v-model="selectedAudioInput"
label="Audio Input Device"
description="Select the audio input device for your hearing module."
:options="audioInputs.map(input => ({
label: input.label || input.deviceId,
value: input.deviceId,
}))"
:options="audioInputOptions"
placeholder="Select an audio input device"
layout="vertical"
h-fit w-full
@@ -45,7 +45,21 @@ export function useAudioDevice(requestPermission: boolean = false) {
trackMicrophonePermissionDenied,
trackMicrophonePermissionRequested,
} = useAnalytics()
const { audioInputs, permissionGranted, ensurePermissions } = useDevicesList({ constraints: { audio: true }, requestPermissions: requestPermission })
const {
devices,
audioInputs,
permissionGranted,
ensurePermissions,
} = useDevicesList({
constraints: { audio: true },
requestPermissions: requestPermission,
})
const audioInputOptions = computed(() => audioInputs.value
.filter(device => device.deviceId)
.map(device => ({
label: device.label || device.deviceId,
value: device.deviceId,
})))
const selectedAudioInput = ref<string>(audioInputs.value.find(device => device.deviceId === 'default')?.deviceId || '')
/**
* Keeps the selected microphone aligned with the currently available device list.
@@ -79,37 +93,47 @@ export function useAudioDevice(requestPermission: boolean = false) {
selectAvailableAudioInput()
})
function askPermission() {
trackMicrophonePermissionRequested({ stt_provider_id: UNKNOWN_STT_PROVIDER_ID })
async function askPermission() {
if (!permissionGranted.value)
trackMicrophonePermissionRequested({ stt_provider_id: UNKNOWN_STT_PROVIDER_ID })
return ensurePermissions()
.then(() => nextTick())
.then(() => {
selectAvailableAudioInput()
if (audioInputs.value.length <= 0) {
trackAudioDeviceUnavailable({
stt_provider_id: UNKNOWN_STT_PROVIDER_ID,
error_code: 'device_unavailable',
})
}
})
.catch((error) => {
const errorCode = audioDeviceErrorCode(error)
if (errorCode === 'permission_denied') {
trackMicrophonePermissionDenied({
stt_provider_id: UNKNOWN_STT_PROVIDER_ID,
error_code: errorCode,
})
}
else {
trackAudioDeviceUnavailable({
stt_provider_id: UNKNOWN_STT_PROVIDER_ID,
error_code: errorCode,
})
}
console.error('Error ensuring permissions:', error)
throw error // Re-throw so callers can handle the error
})
try {
const granted = await ensurePermissions()
if (granted) {
// NOTICE:
// VueUse starts its post-permission device refresh without awaiting it, so callers can
// otherwise observe the anonymous pre-permission list after askPermission() resolves.
// Source: `@vueuse/core` 14.2.1 `useDevicesList.ensurePermissions()`.
// Remove this refresh when VueUse exposes or awaits its internal device-list update.
devices.value = await navigator.mediaDevices.enumerateDevices()
}
selectAvailableAudioInput()
if (audioInputs.value.length <= 0) {
trackAudioDeviceUnavailable({
stt_provider_id: UNKNOWN_STT_PROVIDER_ID,
error_code: 'device_unavailable',
})
}
}
catch (error) {
const errorCode = audioDeviceErrorCode(error)
if (errorCode === 'permission_denied') {
trackMicrophonePermissionDenied({
stt_provider_id: UNKNOWN_STT_PROVIDER_ID,
error_code: errorCode,
})
}
else {
trackAudioDeviceUnavailable({
stt_provider_id: UNKNOWN_STT_PROVIDER_ID,
error_code: errorCode,
})
}
console.error('Error ensuring permissions:', error)
throw error
}
}
async function startStream() {
@@ -138,6 +162,7 @@ export function useAudioDevice(requestPermission: boolean = false) {
return {
audioInputs,
audioInputOptions,
selectedAudioInput,
stream,
deviceConstraints,
@@ -9,6 +9,7 @@ let microphonePermissionStatus: PermissionStatus
export const useSettingsAudioDevice = defineStore('settings-audio-devices', () => {
const {
audioInputs,
audioInputOptions,
deviceConstraints,
permissionGranted,
selectedAudioInput: selectedAudioInputNonPersist,
@@ -146,6 +147,7 @@ export const useSettingsAudioDevice = defineStore('settings-audio-devices', () =
return {
audioInputs,
audioInputOptions,
deviceConstraints,
permissionGranted,
selectedAudioInput: selectedAudioInputPersist,