kamerazugriff wieder möglich + anleitung zum erlauben des kamerazugriffs

This commit is contained in:
Codex Agent
2025-12-20 17:43:40 +01:00
parent 80985828d8
commit d4c7d3f93a
5 changed files with 113 additions and 377 deletions

View File

@@ -445,17 +445,24 @@ export const messages: Record<LocaleCode, NestedMessages> = {
message: 'Dein Gerät unterstützt keine Kameravorschau im Browser. Du kannst stattdessen Fotos aus deiner Galerie hochladen.',
openGallery: 'Foto aus Galerie wählen',
},
cameraBlocked: {
title: 'Kamera durch Sicherheitsrichtlinie blockiert',
message: 'Die Kamera ist durch die Sicherheitsrichtlinie dieser Seite blockiert. Öffne den Event-Link im Browser oder lade ein Foto aus der Galerie hoch.',
hint: 'Tipp: Wenn du in einer In-App-Ansicht bist, öffne den Link in Safari/Chrome und lade die Seite neu.',
},
cameraDenied: {
title: 'Kamera-Zugriff verweigert',
explanation: 'Du musst den Zugriff auf die Kamera erlauben, um Fotos aufnehmen zu können.',
reopenPrompt: 'Systemdialog erneut öffnen',
chooseFile: 'Foto aus Galerie wählen',
prompt: 'Wir brauchen Zugriff auf deine Kamera. Erlaube die Anfrage oder wähle alternativ ein Foto aus deiner Galerie.',
hint: 'Tipp: Prüfe in den Browser-Einstellungen, ob Kamera-Zugriff erlaubt ist, und lade die Seite neu.',
},
cameraError: {
title: 'Kamera konnte nicht gestartet werden',
explanation: 'Wir konnten keine Verbindung zur Kamera herstellen. Prüfe die Berechtigungen oder starte dein Gerät neu.',
tryAgain: 'Nochmals versuchen',
hint: 'Tipp: Schließe andere Apps mit Kamerazugriff und versuche es erneut.',
},
readyOverlay: {
title: 'Kamera bereit',
@@ -591,6 +598,7 @@ export const messages: Record<LocaleCode, NestedMessages> = {
buttons: {
startCamera: 'Kamera starten',
tryAgain: 'Erneut versuchen',
recheckCamera: 'Zugriff erneut prüfen',
},
},
settings: {
@@ -1100,17 +1108,24 @@ export const messages: Record<LocaleCode, NestedMessages> = {
message: 'Your device does not support live camera preview in this browser. You can upload photos from your gallery instead.',
openGallery: 'Choose photo from gallery',
},
cameraBlocked: {
title: 'Camera blocked by security policy',
message: 'Camera access is blocked by the site security policy. Open the event link in your browser or upload a photo from your gallery.',
hint: 'Tip: If you are in an in-app browser, open the link in Safari/Chrome and reload the page.',
},
cameraDenied: {
title: 'Camera access denied',
explanation: 'Allow camera access to capture photos.',
reopenPrompt: 'Open system dialog again',
chooseFile: 'Choose photo from gallery',
prompt: 'We need access to your camera. Allow the request or pick a photo from your gallery.',
hint: 'Tip: Check your browser settings for camera permissions and reload the page.',
},
cameraError: {
title: 'Camera could not be started',
explanation: 'We could not connect to the camera. Check permissions or restart your device.',
tryAgain: 'Try again',
hint: 'Tip: Close other apps that might be using the camera and try again.',
},
readyOverlay: {
title: 'Camera ready',
@@ -1246,6 +1261,7 @@ export const messages: Record<LocaleCode, NestedMessages> = {
buttons: {
startCamera: 'Start camera',
tryAgain: 'Try again',
recheckCamera: 'Recheck access',
},
},
settings: {

View File

@@ -53,7 +53,7 @@ interface Task {
difficulty?: 'easy' | 'medium' | 'hard';
}
type PermissionState = 'idle' | 'prompt' | 'granted' | 'denied' | 'error' | 'unsupported';
type PermissionState = 'idle' | 'prompt' | 'granted' | 'denied' | 'error' | 'unsupported' | 'blocked';
type CameraMode = 'preview' | 'countdown' | 'review' | 'uploading';
type CameraPreferences = {
@@ -85,6 +85,21 @@ function getErrorName(error: unknown): string | undefined {
return undefined;
}
function isCameraBlockedByPolicy(): boolean {
if (typeof document === 'undefined') {
return false;
}
const policy = (document as { permissionsPolicy?: { allowsFeature?: (feature: string) => boolean } })
.permissionsPolicy;
if (!policy?.allowsFeature) {
return false;
}
return !policy.allowsFeature('camera');
}
const DEFAULT_PREFS: CameraPreferences = {
facingMode: 'environment',
countdownSeconds: 3,
@@ -449,6 +464,12 @@ const [canUpload, setCanUpload] = useState(true);
if (mode === 'uploading') return;
try {
if (isCameraBlockedByPolicy()) {
setPermissionState('blocked');
setPermissionMessage(t('upload.cameraBlocked.message'));
return;
}
setPermissionState('prompt');
setPermissionMessage(null);
@@ -475,6 +496,18 @@ const [canUpload, setCanUpload] = useState(true);
}
}, [attachStreamToVideo, createConstraint, mode, preferences.facingMode, stopStream, supportsCamera, t]);
const handleRecheckCamera = useCallback(() => {
if (isCameraBlockedByPolicy()) {
setPermissionState('blocked');
setPermissionMessage(t('upload.cameraBlocked.message'));
return;
}
setPermissionState('idle');
setPermissionMessage(null);
void startCamera();
}, [startCamera, t]);
useEffect(() => {
if (loadingTask) return;
startCamera();
@@ -1078,6 +1111,7 @@ const renderWithDialog = (content: ReactNode, wrapperClassName = 'space-y-6 pb-[
denied: t('upload.cameraDenied.title'),
error: t('upload.cameraError.title'),
unsupported: t('upload.cameraUnsupported.title'),
blocked: t('upload.cameraBlocked.title'),
};
const fallbackMessages: Record<PermissionState, string> = {
@@ -1087,11 +1121,20 @@ const renderWithDialog = (content: ReactNode, wrapperClassName = 'space-y-6 pb-[
denied: t('upload.cameraDenied.explanation'),
error: t('upload.cameraError.explanation'),
unsupported: t('upload.cameraUnsupported.message'),
blocked: t('upload.cameraBlocked.message'),
};
const title = titles[permissionState];
const description = permissionMessage ?? fallbackMessages[permissionState];
const canRetryCamera = permissionState !== 'unsupported';
const canRetryCamera = permissionState !== 'unsupported' && permissionState !== 'blocked';
const canRecheckCamera = permissionState === 'blocked';
const helpText = permissionState === 'blocked'
? t('upload.cameraBlocked.hint')
: permissionState === 'denied'
? t('upload.cameraDenied.hint')
: permissionState === 'error'
? t('upload.cameraError.hint')
: null;
return (
<div
@@ -1107,6 +1150,9 @@ const renderWithDialog = (content: ReactNode, wrapperClassName = 'space-y-6 pb-[
<p className="text-xs text-slate-600 dark:text-white/70">{description}</p>
</div>
</div>
{helpText ? (
<p className="mt-3 text-xs text-slate-600 dark:text-white/70">{helpText}</p>
) : null}
<div className="mt-4 flex flex-wrap gap-3">
{canRetryCamera && (
<Button
@@ -1117,6 +1163,17 @@ const renderWithDialog = (content: ReactNode, wrapperClassName = 'space-y-6 pb-[
{t('upload.buttons.startCamera')}
</Button>
)}
{canRecheckCamera && (
<Button
onClick={handleRecheckCamera}
size="sm"
style={buttonStyle === 'outline'
? { borderRadius: radius, background: 'transparent', color: linkColor, border: `1px solid ${linkColor}` }
: { borderRadius: radius }}
>
{t('upload.buttons.recheckCamera')}
</Button>
)}
<Button
variant="secondary"
size="sm"
@@ -1194,11 +1251,19 @@ const renderWithDialog = (content: ReactNode, wrapperClassName = 'space-y-6 pb-[
<span>
{permissionState === 'unsupported'
? t('upload.cameraUnsupported.title')
: permissionState === 'blocked'
? t('upload.cameraBlocked.title')
: t('upload.cameraDenied.title')}
</span>
</div>
)}
{permissionState !== 'granted' && (
<div className="absolute inset-x-4 top-16 z-30 sm:top-20">
{renderPermissionNotice()}
</div>
)}
{mode === 'countdown' && (
<div className="absolute inset-0 z-40 flex flex-col items-center justify-center bg-black/60 text-white">
<div className="text-6xl font-bold">{countdownValue}</div>
@@ -1449,7 +1514,6 @@ const renderWithDialog = (content: ReactNode, wrapperClassName = 'space-y-6 pb-[
</>
)}
{permissionState !== 'granted' && renderPermissionNotice()}
{renderPrimer()}
<input