Transitions UI to react.js

Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
2026-04-28 01:03:44 +00:00
co-authored by Copilot
parent 1897f8a9e7
commit dcae0b604b
12 changed files with 469 additions and 225 deletions
+215
View File
@@ -0,0 +1,215 @@
'use client';
import { useEffect, useMemo, useRef, useState } from 'react';
type PermissionState = 'granted' | 'denied' | 'prompt';
type BackgroundModePlugin = {
checkMicrophonePermission: () => Promise<{ microphone: PermissionState | string }>;
requestMicrophonePermission: () => Promise<{ microphone: PermissionState | string }>;
checkNotificationsPermission: () => Promise<{ notifications: PermissionState | string }>;
requestNotificationsPermission: () => Promise<{ notifications: PermissionState | string }>;
checkBatteryOptimizations: () => Promise<{ enabled: boolean }>;
requestDisableBatteryOptimizations: () => Promise<void>;
disableWebViewOptimizations: () => Promise<void>;
enable: (options: {
title: string;
text: string;
channelName: string;
channelDescription: string;
resume: boolean;
silent: boolean;
disableWebViewOptimization: boolean;
}) => Promise<void>;
addListener: (eventName: string, listener: () => void) => Promise<void>;
};
type CapacitorRuntime = {
Plugins?: {
BackgroundMode?: BackgroundModePlugin;
};
};
type CapacitorWindow = Window & {
Capacitor?: CapacitorRuntime;
};
const STREAM_URL = 'https://kryz.out.airtime.pro/kryz_b';
export default function HomePage() {
const audioRef = useRef<HTMLAudioElement | null>(null);
const [status, setStatus] = useState('Stream is stopped.');
const [isPlaying, setIsPlaying] = useState(false);
const backgroundMode = useMemo(() => {
if (typeof window === 'undefined') {
return undefined;
}
return (window as CapacitorWindow).Capacitor?.Plugins?.BackgroundMode;
}, []);
useEffect(() => {
const audio = audioRef.current;
if (!audio) {
return;
}
const handleEnded = () => {
setIsPlaying(false);
setStatus('Stream ended.');
};
const handleError = () => {
setIsPlaying(false);
setStatus('Stream error.');
};
audio.addEventListener('ended', handleEnded);
audio.addEventListener('error', handleError);
return () => {
audio.removeEventListener('ended', handleEnded);
audio.removeEventListener('error', handleError);
};
}, []);
useEffect(() => {
if (!backgroundMode) {
console.warn('BackgroundMode plugin is not available in this runtime.');
return;
}
const initializeBackgroundMode = async () => {
try {
const notificationPermission = await backgroundMode.checkNotificationsPermission();
if (notificationPermission.notifications !== 'granted') {
await backgroundMode.requestNotificationsPermission();
}
const microphonePermission = await backgroundMode.checkMicrophonePermission();
if (microphonePermission.microphone !== 'granted') {
const requestedMicrophonePermission = await backgroundMode.requestMicrophonePermission();
if (requestedMicrophonePermission.microphone !== 'granted') {
console.warn(
'BackgroundMode microphone permission denied; background playback may stop when screen is off.'
);
}
}
const batteryOptimizations = await backgroundMode.checkBatteryOptimizations();
if (batteryOptimizations.enabled) {
console.warn(
'Battery optimizations are enabled; requesting exemption for reliable background playback.'
);
await backgroundMode.requestDisableBatteryOptimizations();
}
await backgroundMode.disableWebViewOptimizations();
await backgroundMode.enable({
title: 'KRYZ-Go!',
text: 'Background mode is active',
channelName: 'kryz-go background',
channelDescription: 'Keeps kryz-go running while the app is in background',
resume: true,
silent: false,
disableWebViewOptimization: true,
});
await backgroundMode.addListener('appInBackground', () => {
console.log('kryz-go moved to background');
});
await backgroundMode.addListener('appInForeground', () => {
console.log('kryz-go returned to foreground');
});
console.log('BackgroundMode plugin enabled.');
} catch (error) {
console.error('Failed to initialize BackgroundMode plugin:', error);
}
};
void initializeBackgroundMode();
}, [backgroundMode]);
const ensureBackgroundPlaybackPermissions = async () => {
if (!backgroundMode) {
return true;
}
const microphonePermission = await backgroundMode.checkMicrophonePermission();
if (microphonePermission.microphone === 'granted') {
return true;
}
const requestedMicrophonePermission = await backgroundMode.requestMicrophonePermission();
if (requestedMicrophonePermission.microphone === 'granted') {
return true;
}
setStatus('Enable microphone permission in Android settings for screen-off playback.');
console.warn('Background playback blocked: microphone permission not granted.');
return false;
};
const startStream = async () => {
const audio = audioRef.current;
if (!audio) {
return;
}
try {
const hasBackgroundPermissions = await ensureBackgroundPlaybackPermissions();
if (!hasBackgroundPermissions) {
return;
}
audio.src = STREAM_URL;
await audio.play();
setIsPlaying(true);
setStatus('Stream is playing.');
} catch (error) {
setIsPlaying(false);
setStatus('Unable to start stream.');
console.error('Failed to start stream:', error);
}
};
const stopStream = () => {
const audio = audioRef.current;
if (!audio) {
return;
}
audio.pause();
audio.removeAttribute('src');
audio.load();
setIsPlaying(false);
setStatus('Stream is stopped.');
};
return (
<main className="page-shell">
<section className="player-card" aria-label="KRYZ live stream player">
<p className="eyebrow">KRYZ Go!</p>
<h1>Live Radio</h1>
<p className="status" aria-live="polite">
{status}
</p>
<div className="controls">
<button type="button" onClick={startStream} disabled={isPlaying}>
Start Stream
</button>
<button type="button" onClick={stopStream} disabled={!isPlaying}>
Stop Stream
</button>
</div>
<audio ref={audioRef} preload="none" />
</section>
</main>
);
}